From 87fd1372a9c3971fef72a236d7bd97f19fd9fb4c Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 27 Mar 2025 11:25:04 +0100 Subject: [PATCH] Element config (#4471) * Add handy extension "VariantDimension.buildConfigFieldStr" * Update configuration for MapTiler. * Update configuration for Sentry. * Build AnalyticsConfig depending on analytics configuration. * Configure analytics policy url. * Add handy extension "VariantDimension.buildConfigFieldBoolean" * Configure legal urls. * Add a way to disable rageshake / reporting bugs. * Update screenshots * Quality * Fix test * Use `ifBlank` extension * Add missing configuration for PostHog * Update configuration for Rageshake. * Add build log. * Disable crash detection if rageshake feature is not available. Disabled twice. * Hide link to analytics policy if the link is missing. * Fix test when run in enterprise context. * Use RageshakeFeatureAvailability where appropriate. * Rename file. * Move some classes to their correct module. * Update screenshots --------- Co-authored-by: ElementBot --- .../element/android/x/ElementXApplication.kt | 6 ++- appconfig/build.gradle.kts | 34 ++++++++++++++ .../android/appconfig/AnalyticsConfig.kt | 2 +- .../android/appconfig/RageshakeConfig.kt | 10 +++- .../AnalyticsPreferencesStateProvider.kt | 13 ++++-- .../preferences/AnalyticsPreferencesView.kt | 14 +++--- .../analytics/impl/AnalyticsOptInPresenter.kt | 2 + .../analytics/impl/AnalyticsOptInState.kt | 1 + .../impl/AnalyticsOptInStateProvider.kt | 6 ++- .../analytics/impl/AnalyticsOptInView.kt | 40 ++++++++-------- .../AnalyticsPreferencesPresenterTest.kt | 3 +- features/location/api/build.gradle.kts | 24 ++++++---- .../features/location/api/StaticMapView.kt | 2 +- .../location/api/internal/MapTilerConfig.kt | 21 --------- .../internal/MapTilerStaticMapUrlBuilder.kt | 14 +++--- .../MapTilerTileServerStyleUriBuilder.kt | 14 +++--- .../api/internal/StaticMapUrlBuilder.kt | 4 +- .../api/internal/TileServerStyleUriBuilder.kt | 7 +-- .../MapTilerStaticMapUrlBuilderTest.kt | 26 ++++++----- .../MapTilerTileServerStyleUriBuilderTest.kt | 5 +- features/location/impl/build.gradle.kts | 1 - .../location/impl/DefaultLocationService.kt | 9 ++-- .../impl/DefaultLocationServiceTest.kt | 25 ++-------- features/onboarding/impl/build.gradle.kts | 1 + .../onboarding/impl/OnBoardingPresenter.kt | 5 ++ .../onboarding/impl/OnBoardingState.kt | 1 + .../impl/OnBoardingStateProvider.kt | 7 ++- .../onboarding/impl/OnBoardingView.kt | 20 ++++---- .../impl/OnBoardingPresenterTest.kt | 16 +++++++ .../onboarding/impl/OnboardingViewTest.kt | 18 +++++++- features/preferences/impl/build.gradle.kts | 21 +++++++++ .../preferences/impl/about/ElementLegal.kt | 7 +-- .../impl/root/PreferencesRootPresenter.kt | 4 ++ .../impl/root/PreferencesRootState.kt | 1 + .../impl/root/PreferencesRootStateProvider.kt | 1 + .../impl/root/PreferencesRootView.kt | 12 +++-- .../impl/root/PreferencesRootPresenterTest.kt | 20 ++++++++ .../api/RageshakeFeatureAvailability.kt | 12 +++++ .../preferences/RageshakePreferencesState.kt | 1 + .../RageshakePreferencesStateProvider.kt | 21 ++++++--- .../preferences/RageshakePreferencesView.kt | 46 ++++++++++--------- .../DefaultRageshakeFeatureAvailability.kt | 22 +++++++++ .../impl/bugreport/BugReportPresenter.kt | 4 +- .../rageshake/impl}/crash/CrashDataStore.kt | 2 +- .../crash/DefaultCrashDetectionPresenter.kt | 16 +++++-- .../impl/crash/PreferencesCrashDataStore.kt | 1 - .../DefaultRageshakeDetectionPresenter.kt | 7 +-- .../DefaultRageshakePreferencesPresenter.kt | 9 +++- .../impl/rageshake/DefaultRageShake.kt | 1 - .../PreferencesRageshakeDataStore.kt | 1 - .../rageshake/impl}/rageshake/RageShake.kt | 2 +- .../impl}/rageshake/RageshakeDataStore.kt | 2 +- .../impl/reporter/DefaultBugReporter.kt | 4 +- .../screenshot/DefaultScreenshotHolder.kt | 1 - .../impl}/screenshot/ScreenshotHolder.kt | 2 +- .../impl/bugreport/BugReportPresenterTest.kt | 12 ++--- .../impl}/crash/FakeCrashDataStore.kt | 3 +- .../crash/ui/CrashDetectionPresenterTest.kt | 20 +++++++- .../RageshakeDetectionPresenterTest.kt | 11 +++-- .../RageshakePreferencesPresenterTest.kt | 18 +++++--- .../impl}/rageshake/FakeRageShake.kt | 4 +- .../impl}/rageshake/FakeRageshakeDataStore.kt | 3 +- .../impl/reporter/DefaultBugReporterTest.kt | 7 +-- .../DefaultBugReporterUrlProviderTest.kt | 6 ++- .../impl}/screenshot/FakeScreenshotHolder.kt | 3 +- features/roomlist/impl/build.gradle.kts | 1 + .../roomlist/impl/RoomListPresenter.kt | 4 ++ .../features/roomlist/impl/RoomListState.kt | 1 + .../roomlist/impl/RoomListStateProvider.kt | 2 + .../features/roomlist/impl/RoomListView.kt | 1 + .../impl/components/RoomListTopBar.kt | 7 ++- .../roomlist/impl/RoomListPresenterTest.kt | 6 +++ plugins/src/main/kotlin/ModulesConfig.kt | 28 +++++++++-- .../src/main/kotlin/config/BuildTimeConfig.kt | 14 ++++++ .../extension/VariantDimensionExtension.kt | 32 +++++++++++++ .../posthog/build.gradle.kts | 18 ++++++++ .../posthog/PostHogFactory.kt | 4 +- .../posthog/PosthogEndpointConfig.kt | 4 +- .../posthog/PosthogEndpointConfigProvider.kt | 38 ++++++++++----- .../sentry/build.gradle.kts | 14 ++++-- .../sentry/SentryAnalyticsProvider.kt | 4 +- ...nces_AnalyticsPreferencesView_Day_1_en.png | 3 ++ ...es_AnalyticsPreferencesView_Night_1_en.png | 3 ++ ...ytics.impl_AnalyticsOptInView_Day_1_en.png | 3 ++ ...ics.impl_AnalyticsOptInView_Night_1_en.png | 3 ++ ...nboarding.impl_OnBoardingView_Day_0_en.png | 4 +- ...nboarding.impl_OnBoardingView_Day_1_en.png | 4 +- ...nboarding.impl_OnBoardingView_Day_2_en.png | 4 +- ...nboarding.impl_OnBoardingView_Day_3_en.png | 4 +- ...nboarding.impl_OnBoardingView_Day_4_en.png | 3 ++ ...oarding.impl_OnBoardingView_Night_0_en.png | 4 +- ...oarding.impl_OnBoardingView_Night_1_en.png | 4 +- ...oarding.impl_OnBoardingView_Night_2_en.png | 4 +- ...oarding.impl_OnBoardingView_Night_3_en.png | 4 +- ...oarding.impl_OnBoardingView_Night_4_en.png | 3 ++ 95 files changed, 613 insertions(+), 273 deletions(-) delete mode 100644 features/location/api/src/main/kotlin/io/element/android/features/location/api/internal/MapTilerConfig.kt create mode 100644 features/rageshake/api/src/main/kotlin/io/element/android/features/rageshake/api/RageshakeFeatureAvailability.kt create mode 100644 features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/DefaultRageshakeFeatureAvailability.kt rename features/rageshake/{api/src/main/kotlin/io/element/android/features/rageshake/api => impl/src/main/kotlin/io/element/android/features/rageshake/impl}/crash/CrashDataStore.kt (88%) rename features/rageshake/{api/src/main/kotlin/io/element/android/features/rageshake/api => impl/src/main/kotlin/io/element/android/features/rageshake/impl}/rageshake/RageShake.kt (91%) rename features/rageshake/{api/src/main/kotlin/io/element/android/features/rageshake/api => impl/src/main/kotlin/io/element/android/features/rageshake/impl}/rageshake/RageshakeDataStore.kt (88%) rename features/rageshake/{api/src/main/kotlin/io/element/android/features/rageshake/api => impl/src/main/kotlin/io/element/android/features/rageshake/impl}/screenshot/ScreenshotHolder.kt (84%) rename features/rageshake/{test/src/main/kotlin/io/element/android/features/rageshake/test => impl/src/test/kotlin/io/element/android/features/rageshake/impl}/crash/FakeCrashDataStore.kt (88%) rename features/rageshake/{test/src/main/kotlin/io/element/android/features/rageshake/test => impl/src/test/kotlin/io/element/android/features/rageshake/impl}/rageshake/FakeRageShake.kt (84%) rename features/rageshake/{test/src/main/kotlin/io/element/android/features/rageshake/test => impl/src/test/kotlin/io/element/android/features/rageshake/impl}/rageshake/FakeRageshakeDataStore.kt (87%) rename features/rageshake/{test/src/main/kotlin/io/element/android/features/rageshake/test => impl/src/test/kotlin/io/element/android/features/rageshake/impl}/screenshot/FakeScreenshotHolder.kt (78%) create mode 100644 plugins/src/main/kotlin/extension/VariantDimensionExtension.kt create mode 100644 tests/uitests/src/test/snapshots/images/features.analytics.api.preferences_AnalyticsPreferencesView_Day_1_en.png create mode 100644 tests/uitests/src/test/snapshots/images/features.analytics.api.preferences_AnalyticsPreferencesView_Night_1_en.png create mode 100644 tests/uitests/src/test/snapshots/images/features.analytics.impl_AnalyticsOptInView_Day_1_en.png create mode 100644 tests/uitests/src/test/snapshots/images/features.analytics.impl_AnalyticsOptInView_Night_1_en.png create mode 100644 tests/uitests/src/test/snapshots/images/features.onboarding.impl_OnBoardingView_Day_4_en.png create mode 100644 tests/uitests/src/test/snapshots/images/features.onboarding.impl_OnBoardingView_Night_4_en.png diff --git a/app/src/main/kotlin/io/element/android/x/ElementXApplication.kt b/app/src/main/kotlin/io/element/android/x/ElementXApplication.kt index a4bfe0c60d..30bef76339 100644 --- a/app/src/main/kotlin/io/element/android/x/ElementXApplication.kt +++ b/app/src/main/kotlin/io/element/android/x/ElementXApplication.kt @@ -9,6 +9,8 @@ package io.element.android.x import android.app.Application import androidx.startup.AppInitializer +import io.element.android.appconfig.RageshakeConfig +import io.element.android.appconfig.isEnabled import io.element.android.features.cachecleaner.api.CacheCleanerInitializer import io.element.android.libraries.di.DaggerComponentOwner import io.element.android.x.di.AppComponent @@ -23,7 +25,9 @@ class ElementXApplication : Application(), DaggerComponentOwner { override fun onCreate() { super.onCreate() AppInitializer.getInstance(this).apply { - initializeComponent(CrashInitializer::class.java) + if (RageshakeConfig.isEnabled) { + initializeComponent(CrashInitializer::class.java) + } initializeComponent(PlatformInitializer::class.java) initializeComponent(CacheCleanerInitializer::class.java) } diff --git a/appconfig/build.gradle.kts b/appconfig/build.gradle.kts index 99c7e1defa..15b8bd51ba 100644 --- a/appconfig/build.gradle.kts +++ b/appconfig/build.gradle.kts @@ -1,3 +1,6 @@ +import config.BuildTimeConfig +import extension.buildConfigFieldStr + /* * Copyright 2022-2024 New Vector Ltd. * @@ -10,6 +13,37 @@ plugins { android { namespace = "io.element.android.appconfig" + + buildFeatures { + buildConfig = true + } + + defaultConfig { + buildConfigFieldStr( + name = "URL_POLICY", + value = if (isEnterpriseBuild) { + BuildTimeConfig.URL_POLICY ?: "" + } else { + "https://element.io/cookie-policy" + }, + ) + buildConfigFieldStr( + name = "BUG_REPORT_URL", + value = if (isEnterpriseBuild) { + BuildTimeConfig.BUG_REPORT_URL ?: "" + } else { + "https://riot.im/bugreports/submit" + }, + ) + buildConfigFieldStr( + name = "BUG_REPORT_APP_NAME", + value = if (isEnterpriseBuild) { + BuildTimeConfig.BUG_REPORT_APP_NAME ?: "" + } else { + "element-x-android" + }, + ) + } } dependencies { diff --git a/appconfig/src/main/kotlin/io/element/android/appconfig/AnalyticsConfig.kt b/appconfig/src/main/kotlin/io/element/android/appconfig/AnalyticsConfig.kt index 4b213db637..346fce4725 100644 --- a/appconfig/src/main/kotlin/io/element/android/appconfig/AnalyticsConfig.kt +++ b/appconfig/src/main/kotlin/io/element/android/appconfig/AnalyticsConfig.kt @@ -8,5 +8,5 @@ package io.element.android.appconfig object AnalyticsConfig { - const val POLICY_LINK = "https://element.io/cookie-policy" + const val POLICY_LINK = BuildConfig.URL_POLICY } diff --git a/appconfig/src/main/kotlin/io/element/android/appconfig/RageshakeConfig.kt b/appconfig/src/main/kotlin/io/element/android/appconfig/RageshakeConfig.kt index 47d94cc497..8c836bc8a2 100644 --- a/appconfig/src/main/kotlin/io/element/android/appconfig/RageshakeConfig.kt +++ b/appconfig/src/main/kotlin/io/element/android/appconfig/RageshakeConfig.kt @@ -11,17 +11,23 @@ object RageshakeConfig { /** * The URL to submit bug reports to. */ - const val BUG_REPORT_URL = "https://riot.im/bugreports/submit" + const val BUG_REPORT_URL = BuildConfig.BUG_REPORT_URL /** * As per https://github.com/matrix-org/rageshake: * Identifier for the application (eg 'riot-web'). * Should correspond to a mapping configured in the configuration file for github issue reporting to work. */ - const val BUG_REPORT_APP_NAME = "element-x-android" + const val BUG_REPORT_APP_NAME = BuildConfig.BUG_REPORT_APP_NAME /** * The maximum size of the upload request. Default value is just below CloudFlare's max request size. */ const val MAX_LOG_UPLOAD_SIZE = 50 * 1024 * 1024L } + +/** + * Whether the rageshake feature is enabled. + */ +val RageshakeConfig.isEnabled: Boolean + get() = BUG_REPORT_URL.isNotEmpty() && BUG_REPORT_APP_NAME.isNotEmpty() diff --git a/features/analytics/api/src/main/kotlin/io/element/android/features/analytics/api/preferences/AnalyticsPreferencesStateProvider.kt b/features/analytics/api/src/main/kotlin/io/element/android/features/analytics/api/preferences/AnalyticsPreferencesStateProvider.kt index 8e62bdd305..647b205722 100644 --- a/features/analytics/api/src/main/kotlin/io/element/android/features/analytics/api/preferences/AnalyticsPreferencesStateProvider.kt +++ b/features/analytics/api/src/main/kotlin/io/element/android/features/analytics/api/preferences/AnalyticsPreferencesStateProvider.kt @@ -13,12 +13,17 @@ open class AnalyticsPreferencesStateProvider : PreviewParameterProvider get() = sequenceOf( aAnalyticsPreferencesState().copy(isEnabled = true), + aAnalyticsPreferencesState().copy(isEnabled = true, policyUrl = ""), ) } -fun aAnalyticsPreferencesState() = AnalyticsPreferencesState( - applicationName = "Element X", - isEnabled = false, - policyUrl = "https://element.io", +fun aAnalyticsPreferencesState( + applicationName: String = "Element X", + isEnabled: Boolean = false, + policyUrl: String = "https://element.io", +) = AnalyticsPreferencesState( + applicationName = applicationName, + isEnabled = isEnabled, + policyUrl = policyUrl, eventSink = {} ) diff --git a/features/analytics/api/src/main/kotlin/io/element/android/features/analytics/api/preferences/AnalyticsPreferencesView.kt b/features/analytics/api/src/main/kotlin/io/element/android/features/analytics/api/preferences/AnalyticsPreferencesView.kt index 2e7ee47736..70461f22d3 100644 --- a/features/analytics/api/src/main/kotlin/io/element/android/features/analytics/api/preferences/AnalyticsPreferencesView.kt +++ b/features/analytics/api/src/main/kotlin/io/element/android/features/analytics/api/preferences/AnalyticsPreferencesView.kt @@ -36,11 +36,6 @@ fun AnalyticsPreferencesView( id = R.string.screen_analytics_settings_help_us_improve, state.applicationName ) - val linkText = buildAnnotatedStringWithStyledPart( - R.string.screen_analytics_settings_read_terms, - R.string.screen_analytics_settings_read_terms_content_link, - tagAndLink = LINK_TAG to state.policyUrl, - ) Column(modifier) { ListItem( headlineContent = { @@ -57,7 +52,14 @@ fun AnalyticsPreferencesView( onEnabledChanged(!state.isEnabled) } ) - ListSupportingText(annotatedString = linkText) + if (state.policyUrl.isNotEmpty()) { + val linkText = buildAnnotatedStringWithStyledPart( + R.string.screen_analytics_settings_read_terms, + R.string.screen_analytics_settings_read_terms_content_link, + tagAndLink = LINK_TAG to state.policyUrl, + ) + ListSupportingText(annotatedString = linkText) + } } } diff --git a/features/analytics/impl/src/main/kotlin/io/element/android/features/analytics/impl/AnalyticsOptInPresenter.kt b/features/analytics/impl/src/main/kotlin/io/element/android/features/analytics/impl/AnalyticsOptInPresenter.kt index d545d00951..39b99a9257 100644 --- a/features/analytics/impl/src/main/kotlin/io/element/android/features/analytics/impl/AnalyticsOptInPresenter.kt +++ b/features/analytics/impl/src/main/kotlin/io/element/android/features/analytics/impl/AnalyticsOptInPresenter.kt @@ -9,6 +9,7 @@ package io.element.android.features.analytics.impl import androidx.compose.runtime.Composable import androidx.compose.runtime.rememberCoroutineScope +import io.element.android.appconfig.AnalyticsConfig import io.element.android.features.analytics.api.AnalyticsOptInEvents import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.core.meta.BuildMeta @@ -36,6 +37,7 @@ class AnalyticsOptInPresenter @Inject constructor( return AnalyticsOptInState( applicationName = buildMeta.applicationName, + hasPolicyLink = AnalyticsConfig.POLICY_LINK.isNotEmpty(), eventSink = ::handleEvents ) } diff --git a/features/analytics/impl/src/main/kotlin/io/element/android/features/analytics/impl/AnalyticsOptInState.kt b/features/analytics/impl/src/main/kotlin/io/element/android/features/analytics/impl/AnalyticsOptInState.kt index d7e99e56c1..a0913bdb4f 100644 --- a/features/analytics/impl/src/main/kotlin/io/element/android/features/analytics/impl/AnalyticsOptInState.kt +++ b/features/analytics/impl/src/main/kotlin/io/element/android/features/analytics/impl/AnalyticsOptInState.kt @@ -11,5 +11,6 @@ import io.element.android.features.analytics.api.AnalyticsOptInEvents data class AnalyticsOptInState( val applicationName: String, + val hasPolicyLink: Boolean, val eventSink: (AnalyticsOptInEvents) -> Unit ) diff --git a/features/analytics/impl/src/main/kotlin/io/element/android/features/analytics/impl/AnalyticsOptInStateProvider.kt b/features/analytics/impl/src/main/kotlin/io/element/android/features/analytics/impl/AnalyticsOptInStateProvider.kt index c159b738a7..e6917c237e 100644 --- a/features/analytics/impl/src/main/kotlin/io/element/android/features/analytics/impl/AnalyticsOptInStateProvider.kt +++ b/features/analytics/impl/src/main/kotlin/io/element/android/features/analytics/impl/AnalyticsOptInStateProvider.kt @@ -14,10 +14,14 @@ open class AnalyticsOptInStateProvider @Inject constructor() : PreviewParameterP override val values: Sequence get() = sequenceOf( aAnalyticsOptInState(), + aAnalyticsOptInState(hasPolicyLink = false), ) } -fun aAnalyticsOptInState() = AnalyticsOptInState( +fun aAnalyticsOptInState( + hasPolicyLink: Boolean = true, +) = AnalyticsOptInState( applicationName = "Element X", + hasPolicyLink = hasPolicyLink, eventSink = {} ) diff --git a/features/analytics/impl/src/main/kotlin/io/element/android/features/analytics/impl/AnalyticsOptInView.kt b/features/analytics/impl/src/main/kotlin/io/element/android/features/analytics/impl/AnalyticsOptInView.kt index 3e8b9d98f9..a6ae754944 100644 --- a/features/analytics/impl/src/main/kotlin/io/element/android/features/analytics/impl/AnalyticsOptInView.kt +++ b/features/analytics/impl/src/main/kotlin/io/element/android/features/analytics/impl/AnalyticsOptInView.kt @@ -95,25 +95,27 @@ private fun AnalyticsOptInHeader( subtitle = stringResource(id = R.string.screen_analytics_prompt_help_us_improve), iconStyle = BigIcon.Style.Default(CompoundIcons.Chart()) ) - val text = buildAnnotatedStringWithStyledPart( - R.string.screen_analytics_prompt_read_terms, - R.string.screen_analytics_prompt_read_terms_content_link, - color = Color.Unspecified, - underline = false, - bold = true, - tagAndLink = LINK_TAG to AnalyticsConfig.POLICY_LINK, - ) - ClickableLinkText( - annotatedString = text, - onClick = { onClickTerms() }, - modifier = Modifier - .padding(8.dp), - style = ElementTheme.typography.fontBodyMdRegular - .copy( - color = ElementTheme.colors.textSecondary, - textAlign = TextAlign.Center, - ) - ) + if (state.hasPolicyLink) { + val text = buildAnnotatedStringWithStyledPart( + R.string.screen_analytics_prompt_read_terms, + R.string.screen_analytics_prompt_read_terms_content_link, + color = Color.Unspecified, + underline = false, + bold = true, + tagAndLink = LINK_TAG to AnalyticsConfig.POLICY_LINK, + ) + ClickableLinkText( + annotatedString = text, + onClick = { onClickTerms() }, + modifier = Modifier + .padding(8.dp), + style = ElementTheme.typography.fontBodyMdRegular + .copy( + color = ElementTheme.colors.textSecondary, + textAlign = TextAlign.Center, + ) + ) + } } } diff --git a/features/analytics/impl/src/test/kotlin/io/element/android/features/analytics/impl/preferences/AnalyticsPreferencesPresenterTest.kt b/features/analytics/impl/src/test/kotlin/io/element/android/features/analytics/impl/preferences/AnalyticsPreferencesPresenterTest.kt index 30b64eb8ab..8ba915f03c 100644 --- a/features/analytics/impl/src/test/kotlin/io/element/android/features/analytics/impl/preferences/AnalyticsPreferencesPresenterTest.kt +++ b/features/analytics/impl/src/test/kotlin/io/element/android/features/analytics/impl/preferences/AnalyticsPreferencesPresenterTest.kt @@ -11,6 +11,7 @@ import app.cash.molecule.RecompositionMode import app.cash.molecule.moleculeFlow import app.cash.turbine.test import com.google.common.truth.Truth.assertThat +import io.element.android.appconfig.AnalyticsConfig import io.element.android.features.analytics.api.AnalyticsOptInEvents import io.element.android.libraries.matrix.test.core.aBuildMeta import io.element.android.services.analytics.test.FakeAnalyticsService @@ -35,7 +36,7 @@ class AnalyticsPreferencesPresenterTest { skipItems(1) val initialState = awaitItem() assertThat(initialState.isEnabled).isTrue() - assertThat(initialState.policyUrl).isNotEmpty() + assertThat(initialState.policyUrl).isEqualTo(AnalyticsConfig.POLICY_LINK) } } diff --git a/features/location/api/build.gradle.kts b/features/location/api/build.gradle.kts index 909a3c978a..d299887165 100644 --- a/features/location/api/build.gradle.kts +++ b/features/location/api/build.gradle.kts @@ -6,6 +6,7 @@ */ import config.BuildTimeConfig +import extension.buildConfigFieldStr import extension.readLocalProperty plugins { @@ -16,10 +17,17 @@ plugins { android { namespace = "io.element.android.features.location.api" + buildFeatures { + buildConfig = true + } + defaultConfig { - resValue( - type = "string", - name = "maptiler_api_key", + buildConfigFieldStr( + name = "MAPTILER_BASE_URL", + value = BuildTimeConfig.SERVICES_MAPTILER_BASE_URL ?: "https://api.maptiler.com/maps" + ) + buildConfigFieldStr( + name = "MAPTILER_API_KEY", value = if (isEnterpriseBuild) { BuildTimeConfig.SERVICES_MAPTILER_APIKEY } else { @@ -28,9 +36,8 @@ android { } ?: "" ) - resValue( - type = "string", - name = "maptiler_light_map_id", + buildConfigFieldStr( + name = "MAPTILER_LIGHT_MAP_ID", value = if (isEnterpriseBuild) { BuildTimeConfig.SERVICES_MAPTILER_LIGHT_MAPID } else { @@ -40,9 +47,8 @@ android { // fall back to maptiler's default light map. ?: "basic-v2" ) - resValue( - type = "string", - name = "maptiler_dark_map_id", + buildConfigFieldStr( + name = "MAPTILER_DARK_MAP_ID", value = if (isEnterpriseBuild) { BuildTimeConfig.SERVICES_MAPTILER_DARK_MAPID } else { diff --git a/features/location/api/src/main/kotlin/io/element/android/features/location/api/StaticMapView.kt b/features/location/api/src/main/kotlin/io/element/android/features/location/api/StaticMapView.kt index e80386a9c5..382a65ae47 100644 --- a/features/location/api/src/main/kotlin/io/element/android/features/location/api/StaticMapView.kt +++ b/features/location/api/src/main/kotlin/io/element/android/features/location/api/StaticMapView.kt @@ -57,7 +57,7 @@ fun StaticMapView( ) { val context = LocalContext.current var retryHash by remember { mutableIntStateOf(0) } - val builder = remember { StaticMapUrlBuilder(context) } + val builder = remember { StaticMapUrlBuilder() } val painter = rememberAsyncImagePainter( model = if (constraints.isZero) { // Avoid building a URL if any of the size constraints is zero (else it will thrown an exception). diff --git a/features/location/api/src/main/kotlin/io/element/android/features/location/api/internal/MapTilerConfig.kt b/features/location/api/src/main/kotlin/io/element/android/features/location/api/internal/MapTilerConfig.kt deleted file mode 100644 index 9580280d74..0000000000 --- a/features/location/api/src/main/kotlin/io/element/android/features/location/api/internal/MapTilerConfig.kt +++ /dev/null @@ -1,21 +0,0 @@ -/* - * Copyright 2023, 2024 New Vector Ltd. - * - * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial - * Please see LICENSE files in the repository root for full details. - */ - -package io.element.android.features.location.api.internal - -import android.content.Context -import io.element.android.features.location.api.R - -internal const val MAPTILER_BASE_URL = "https://api.maptiler.com/maps" - -internal fun Context.mapId(darkMode: Boolean) = when (darkMode) { - true -> getString(R.string.maptiler_dark_map_id) - false -> getString(R.string.maptiler_light_map_id) -} - -internal val Context.apiKey: String - get() = getString(R.string.maptiler_api_key) diff --git a/features/location/api/src/main/kotlin/io/element/android/features/location/api/internal/MapTilerStaticMapUrlBuilder.kt b/features/location/api/src/main/kotlin/io/element/android/features/location/api/internal/MapTilerStaticMapUrlBuilder.kt index 20c0466e13..eb8d0ea1b7 100644 --- a/features/location/api/src/main/kotlin/io/element/android/features/location/api/internal/MapTilerStaticMapUrlBuilder.kt +++ b/features/location/api/src/main/kotlin/io/element/android/features/location/api/internal/MapTilerStaticMapUrlBuilder.kt @@ -7,7 +7,7 @@ package io.element.android.features.location.api.internal -import android.content.Context +import io.element.android.features.location.api.BuildConfig import kotlin.math.roundToInt /** @@ -16,14 +16,16 @@ import kotlin.math.roundToInt * https://docs.maptiler.com/cloud/api/static-maps/ */ internal class MapTilerStaticMapUrlBuilder( + private val baseUrl: String, private val apiKey: String, private val lightMapId: String, private val darkMapId: String, ) : StaticMapUrlBuilder { - constructor(context: Context) : this( - apiKey = context.apiKey, - lightMapId = context.mapId(darkMode = false), - darkMapId = context.mapId(darkMode = true), + constructor() : this( + baseUrl = BuildConfig.MAPTILER_BASE_URL.removeSuffix("/"), + apiKey = BuildConfig.MAPTILER_API_KEY, + lightMapId = BuildConfig.MAPTILER_LIGHT_MAP_ID, + darkMapId = BuildConfig.MAPTILER_DARK_MAP_ID, ) override fun build( @@ -55,7 +57,7 @@ internal class MapTilerStaticMapUrlBuilder( // image smaller than the available space in pixels. // The resulting image will have to be scaled to fit the available space in order // to keep the perceived content size constant at the expense of sharpness. - return "$MAPTILER_BASE_URL/$mapId/static/$lon,$lat,$finalZoom/${finalWidth}x${finalHeight}$scale.webp?key=$apiKey&attribution=bottomleft" + return "$baseUrl/$mapId/static/$lon,$lat,$finalZoom/${finalWidth}x${finalHeight}$scale.webp?key=$apiKey&attribution=bottomleft" } override fun isServiceAvailable() = apiKey.isNotEmpty() diff --git a/features/location/api/src/main/kotlin/io/element/android/features/location/api/internal/MapTilerTileServerStyleUriBuilder.kt b/features/location/api/src/main/kotlin/io/element/android/features/location/api/internal/MapTilerTileServerStyleUriBuilder.kt index 3146d589e4..db75b5e30a 100644 --- a/features/location/api/src/main/kotlin/io/element/android/features/location/api/internal/MapTilerTileServerStyleUriBuilder.kt +++ b/features/location/api/src/main/kotlin/io/element/android/features/location/api/internal/MapTilerTileServerStyleUriBuilder.kt @@ -9,21 +9,23 @@ package io.element.android.features.location.api.internal -import android.content.Context +import io.element.android.features.location.api.BuildConfig internal class MapTilerTileServerStyleUriBuilder( + private val baseUrl: String, private val apiKey: String, private val lightMapId: String, private val darkMapId: String, ) : TileServerStyleUriBuilder { - constructor(context: Context) : this( - apiKey = context.apiKey, - lightMapId = context.mapId(darkMode = false), - darkMapId = context.mapId(darkMode = true), + constructor() : this( + baseUrl = BuildConfig.MAPTILER_BASE_URL.removeSuffix("/"), + apiKey = BuildConfig.MAPTILER_API_KEY, + lightMapId = BuildConfig.MAPTILER_LIGHT_MAP_ID, + darkMapId = BuildConfig.MAPTILER_DARK_MAP_ID, ) override fun build(darkMode: Boolean): String { val mapId = if (darkMode) darkMapId else lightMapId - return "$MAPTILER_BASE_URL/$mapId/style.json?key=$apiKey" + return "$baseUrl/$mapId/style.json?key=$apiKey" } } diff --git a/features/location/api/src/main/kotlin/io/element/android/features/location/api/internal/StaticMapUrlBuilder.kt b/features/location/api/src/main/kotlin/io/element/android/features/location/api/internal/StaticMapUrlBuilder.kt index e52205c2f3..533ecf8d2d 100644 --- a/features/location/api/src/main/kotlin/io/element/android/features/location/api/internal/StaticMapUrlBuilder.kt +++ b/features/location/api/src/main/kotlin/io/element/android/features/location/api/internal/StaticMapUrlBuilder.kt @@ -7,8 +7,6 @@ package io.element.android.features.location.api.internal -import android.content.Context - /** * Builds an URL for a 3rd party service provider static maps API. */ @@ -26,4 +24,4 @@ interface StaticMapUrlBuilder { fun isServiceAvailable(): Boolean } -fun StaticMapUrlBuilder(context: Context): StaticMapUrlBuilder = MapTilerStaticMapUrlBuilder(context = context) +fun StaticMapUrlBuilder(): StaticMapUrlBuilder = MapTilerStaticMapUrlBuilder() diff --git a/features/location/api/src/main/kotlin/io/element/android/features/location/api/internal/TileServerStyleUriBuilder.kt b/features/location/api/src/main/kotlin/io/element/android/features/location/api/internal/TileServerStyleUriBuilder.kt index 4d93a8d8bb..051b2448d4 100644 --- a/features/location/api/src/main/kotlin/io/element/android/features/location/api/internal/TileServerStyleUriBuilder.kt +++ b/features/location/api/src/main/kotlin/io/element/android/features/location/api/internal/TileServerStyleUriBuilder.kt @@ -7,10 +7,8 @@ package io.element.android.features.location.api.internal -import android.content.Context import androidx.compose.runtime.Composable import androidx.compose.runtime.remember -import androidx.compose.ui.platform.LocalContext import io.element.android.compound.theme.ElementTheme /** @@ -24,7 +22,7 @@ interface TileServerStyleUriBuilder { ): String } -fun TileServerStyleUriBuilder(context: Context): TileServerStyleUriBuilder = MapTilerTileServerStyleUriBuilder(context = context) +fun TileServerStyleUriBuilder(): TileServerStyleUriBuilder = MapTilerTileServerStyleUriBuilder() /** * Provides and remembers a style URI for a MapLibre compatible tile server. @@ -33,9 +31,8 @@ fun TileServerStyleUriBuilder(context: Context): TileServerStyleUriBuilder = Map */ @Composable fun rememberTileStyleUrl(): String { - val context = LocalContext.current val darkMode = !ElementTheme.isLightTheme return remember(darkMode) { - TileServerStyleUriBuilder(context).build(darkMode) + TileServerStyleUriBuilder().build(darkMode) } } diff --git a/features/location/api/src/test/kotlin/io/element/android/features/location/api/internal/MapTilerStaticMapUrlBuilderTest.kt b/features/location/api/src/test/kotlin/io/element/android/features/location/api/internal/MapTilerStaticMapUrlBuilderTest.kt index b74cebf1e7..ba44426a70 100644 --- a/features/location/api/src/test/kotlin/io/element/android/features/location/api/internal/MapTilerStaticMapUrlBuilderTest.kt +++ b/features/location/api/src/test/kotlin/io/element/android/features/location/api/internal/MapTilerStaticMapUrlBuilderTest.kt @@ -12,6 +12,7 @@ import org.junit.Test class MapTilerStaticMapUrlBuilderTest { private val builder = MapTilerStaticMapUrlBuilder( + baseUrl = "https://base.url", apiKey = "anApiKey", lightMapId = "aLightMapId", darkMapId = "aDarkMapId", @@ -25,6 +26,7 @@ class MapTilerStaticMapUrlBuilderTest { @Test fun `isServiceAvailable returns false if api key is empty`() { val builderWithoutKey = MapTilerStaticMapUrlBuilder( + baseUrl = "https://base.url", apiKey = "", lightMapId = "aLightMapId", darkMapId = "aDarkMapId", @@ -44,7 +46,7 @@ class MapTilerStaticMapUrlBuilderTest { height = 600, density = 1f, ) - ).isEqualTo("https://api.maptiler.com/maps/aLightMapId/static/-4.56,1.23,7.8/800x600.webp?key=anApiKey&attribution=bottomleft") + ).isEqualTo("https://base.url/aLightMapId/static/-4.56,1.23,7.8/800x600.webp?key=anApiKey&attribution=bottomleft") } @Test @@ -59,7 +61,7 @@ class MapTilerStaticMapUrlBuilderTest { height = 900, density = 1.5f, ) - ).isEqualTo("https://api.maptiler.com/maps/aLightMapId/static/-4.56,1.23,7.8/800x600.webp?key=anApiKey&attribution=bottomleft") + ).isEqualTo("https://base.url/aLightMapId/static/-4.56,1.23,7.8/800x600.webp?key=anApiKey&attribution=bottomleft") } @Test @@ -74,7 +76,7 @@ class MapTilerStaticMapUrlBuilderTest { height = 1200, density = 2f, ) - ).isEqualTo("https://api.maptiler.com/maps/aLightMapId/static/-4.56,1.23,7.8/800x600@2x.webp?key=anApiKey&attribution=bottomleft") + ).isEqualTo("https://base.url/aLightMapId/static/-4.56,1.23,7.8/800x600@2x.webp?key=anApiKey&attribution=bottomleft") } @Test @@ -89,7 +91,7 @@ class MapTilerStaticMapUrlBuilderTest { height = 1800, density = 3f, ) - ).isEqualTo("https://api.maptiler.com/maps/aLightMapId/static/-4.56,1.23,7.8/800x600@2x.webp?key=anApiKey&attribution=bottomleft") + ).isEqualTo("https://base.url/aLightMapId/static/-4.56,1.23,7.8/800x600@2x.webp?key=anApiKey&attribution=bottomleft") } @Test @@ -104,7 +106,7 @@ class MapTilerStaticMapUrlBuilderTest { height = 2048, density = 1f, ) - ).isEqualTo("https://api.maptiler.com/maps/aLightMapId/static/-4.56,1.23,7.8/2048x1024.webp?key=anApiKey&attribution=bottomleft") + ).isEqualTo("https://base.url/aLightMapId/static/-4.56,1.23,7.8/2048x1024.webp?key=anApiKey&attribution=bottomleft") assertThat( builder.build( @@ -116,7 +118,7 @@ class MapTilerStaticMapUrlBuilderTest { height = 4096, density = 1f, ) - ).isEqualTo("https://api.maptiler.com/maps/aLightMapId/static/-4.56,1.23,7.8/1024x2048.webp?key=anApiKey&attribution=bottomleft") + ).isEqualTo("https://base.url/aLightMapId/static/-4.56,1.23,7.8/1024x2048.webp?key=anApiKey&attribution=bottomleft") assertThat( builder.build( @@ -128,7 +130,7 @@ class MapTilerStaticMapUrlBuilderTest { height = 2048, density = 2f, ) - ).isEqualTo("https://api.maptiler.com/maps/aLightMapId/static/-4.56,1.23,7.8/1024x512@2x.webp?key=anApiKey&attribution=bottomleft") + ).isEqualTo("https://base.url/aLightMapId/static/-4.56,1.23,7.8/1024x512@2x.webp?key=anApiKey&attribution=bottomleft") assertThat( builder.build( @@ -140,7 +142,7 @@ class MapTilerStaticMapUrlBuilderTest { height = 4096, density = 2f, ) - ).isEqualTo("https://api.maptiler.com/maps/aLightMapId/static/-4.56,1.23,7.8/512x1024@2x.webp?key=anApiKey&attribution=bottomleft") + ).isEqualTo("https://base.url/aLightMapId/static/-4.56,1.23,7.8/512x1024@2x.webp?key=anApiKey&attribution=bottomleft") assertThat( builder.build( @@ -152,7 +154,7 @@ class MapTilerStaticMapUrlBuilderTest { height = Int.MAX_VALUE, density = 2f, ) - ).isEqualTo("https://api.maptiler.com/maps/aLightMapId/static/-4.56,1.23,7.8/1024x1024@2x.webp?key=anApiKey&attribution=bottomleft") + ).isEqualTo("https://base.url/aLightMapId/static/-4.56,1.23,7.8/1024x1024@2x.webp?key=anApiKey&attribution=bottomleft") } @Test @@ -167,7 +169,7 @@ class MapTilerStaticMapUrlBuilderTest { height = 0, density = 1f, ) - ).isEqualTo("https://api.maptiler.com/maps/aLightMapId/static/-4.56,1.23,7.8/0x0.webp?key=anApiKey&attribution=bottomleft") + ).isEqualTo("https://base.url/aLightMapId/static/-4.56,1.23,7.8/0x0.webp?key=anApiKey&attribution=bottomleft") assertThat( builder.build( @@ -179,7 +181,7 @@ class MapTilerStaticMapUrlBuilderTest { height = 0, density = 2f, ) - ).isEqualTo("https://api.maptiler.com/maps/aLightMapId/static/-4.56,1.23,7.8/0x0@2x.webp?key=anApiKey&attribution=bottomleft") + ).isEqualTo("https://base.url/aLightMapId/static/-4.56,1.23,7.8/0x0@2x.webp?key=anApiKey&attribution=bottomleft") assertThat( builder.build( @@ -191,6 +193,6 @@ class MapTilerStaticMapUrlBuilderTest { height = Int.MIN_VALUE, density = 1f, ) - ).isEqualTo("https://api.maptiler.com/maps/aLightMapId/static/-4.56,1.23,7.8/0x0.webp?key=anApiKey&attribution=bottomleft") + ).isEqualTo("https://base.url/aLightMapId/static/-4.56,1.23,7.8/0x0.webp?key=anApiKey&attribution=bottomleft") } } diff --git a/features/location/api/src/test/kotlin/io/element/android/features/location/api/internal/MapTilerTileServerStyleUriBuilderTest.kt b/features/location/api/src/test/kotlin/io/element/android/features/location/api/internal/MapTilerTileServerStyleUriBuilderTest.kt index f65c044540..dc53765836 100644 --- a/features/location/api/src/test/kotlin/io/element/android/features/location/api/internal/MapTilerTileServerStyleUriBuilderTest.kt +++ b/features/location/api/src/test/kotlin/io/element/android/features/location/api/internal/MapTilerTileServerStyleUriBuilderTest.kt @@ -12,6 +12,7 @@ import org.junit.Test class MapTilerTileServerStyleUriBuilderTest { private val builder = MapTilerTileServerStyleUriBuilder( + baseUrl = "https://base.url", apiKey = "anApiKey", lightMapId = "aLightMapId", darkMapId = "aDarkMapId", @@ -21,13 +22,13 @@ class MapTilerTileServerStyleUriBuilderTest { fun `light map uri`() { assertThat( builder.build(darkMode = false) - ).isEqualTo("https://api.maptiler.com/maps/aLightMapId/style.json?key=anApiKey") + ).isEqualTo("https://base.url/aLightMapId/style.json?key=anApiKey") } @Test fun `dark map uri`() { assertThat( builder.build(darkMode = true) - ).isEqualTo("https://api.maptiler.com/maps/aDarkMapId/style.json?key=anApiKey") + ).isEqualTo("https://base.url/aDarkMapId/style.json?key=anApiKey") } } diff --git a/features/location/impl/build.gradle.kts b/features/location/impl/build.gradle.kts index 8a47e77b1a..4c62bdff1a 100644 --- a/features/location/impl/build.gradle.kts +++ b/features/location/impl/build.gradle.kts @@ -49,7 +49,6 @@ dependencies { testImplementation(projects.libraries.testtags) testImplementation(projects.services.analytics.test) testImplementation(projects.features.messages.test) - testImplementation(projects.services.toolbox.test) testImplementation(projects.tests.testutils) testImplementation(libs.androidx.compose.ui.test.junit) testReleaseImplementation(libs.androidx.compose.ui.test.manifest) diff --git a/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/DefaultLocationService.kt b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/DefaultLocationService.kt index 1a9359b301..e662ef8115 100644 --- a/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/DefaultLocationService.kt +++ b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/DefaultLocationService.kt @@ -8,17 +8,14 @@ package io.element.android.features.location.impl import com.squareup.anvil.annotations.ContributesBinding +import io.element.android.features.location.api.BuildConfig import io.element.android.features.location.api.LocationService -import io.element.android.features.location.api.R import io.element.android.libraries.di.AppScope -import io.element.android.services.toolbox.api.strings.StringProvider import javax.inject.Inject @ContributesBinding(AppScope::class) -class DefaultLocationService @Inject constructor( - private val stringProvider: StringProvider, -) : LocationService { +class DefaultLocationService @Inject constructor() : LocationService { override fun isServiceAvailable(): Boolean { - return stringProvider.getString(R.string.maptiler_api_key).isNotEmpty() + return BuildConfig.MAPTILER_API_KEY.isNotEmpty() } } diff --git a/features/location/impl/src/test/kotlin/io/element/android/features/location/impl/DefaultLocationServiceTest.kt b/features/location/impl/src/test/kotlin/io/element/android/features/location/impl/DefaultLocationServiceTest.kt index 213ce52ac5..fd687dd938 100644 --- a/features/location/impl/src/test/kotlin/io/element/android/features/location/impl/DefaultLocationServiceTest.kt +++ b/features/location/impl/src/test/kotlin/io/element/android/features/location/impl/DefaultLocationServiceTest.kt @@ -8,30 +8,15 @@ package io.element.android.features.location.impl import com.google.common.truth.Truth.assertThat -import io.element.android.features.location.api.R -import io.element.android.services.toolbox.test.strings.FakeStringProvider +import io.element.android.features.location.api.BuildConfig import org.junit.Test class DefaultLocationServiceTest { @Test - fun `if apiKey is empty, isServiceAvailable should return false`() { - val fakeStringProvider = FakeStringProvider( - defaultResult = "" + fun `isServiceAvailable should return value depending on BuildConfig MAPTILER_API_KEY`() { + val locationService = DefaultLocationService() + assertThat(locationService.isServiceAvailable()).isEqualTo( + BuildConfig.MAPTILER_API_KEY.isNotEmpty() ) - val locationService = DefaultLocationService( - stringProvider = fakeStringProvider, - ) - assertThat(locationService.isServiceAvailable()).isFalse() - assertThat(fakeStringProvider.lastResIdParam).isEqualTo(R.string.maptiler_api_key) - } - - @Test - fun `if apiKey is not empty, isServiceAvailable should return true`() { - val locationService = DefaultLocationService( - stringProvider = FakeStringProvider( - defaultResult = "aKey" - ) - ) - assertThat(locationService.isServiceAvailable()).isTrue() } } diff --git a/features/onboarding/impl/build.gradle.kts b/features/onboarding/impl/build.gradle.kts index 13e37e0a54..c59cd5684b 100644 --- a/features/onboarding/impl/build.gradle.kts +++ b/features/onboarding/impl/build.gradle.kts @@ -26,6 +26,7 @@ setupAnvil() dependencies { implementation(projects.appconfig) + implementation(projects.features.rageshake.api) implementation(projects.libraries.core) implementation(projects.libraries.androidutils) implementation(projects.libraries.architecture) diff --git a/features/onboarding/impl/src/main/kotlin/io/element/android/features/onboarding/impl/OnBoardingPresenter.kt b/features/onboarding/impl/src/main/kotlin/io/element/android/features/onboarding/impl/OnBoardingPresenter.kt index 8d33f60393..481ad6edc6 100644 --- a/features/onboarding/impl/src/main/kotlin/io/element/android/features/onboarding/impl/OnBoardingPresenter.kt +++ b/features/onboarding/impl/src/main/kotlin/io/element/android/features/onboarding/impl/OnBoardingPresenter.kt @@ -10,7 +10,9 @@ package io.element.android.features.onboarding.impl import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.produceState +import androidx.compose.runtime.remember import io.element.android.appconfig.OnBoardingConfig +import io.element.android.features.rageshake.api.RageshakeFeatureAvailability import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.core.meta.BuildMeta import io.element.android.libraries.featureflag.api.FeatureFlagService @@ -24,16 +26,19 @@ import javax.inject.Inject class OnBoardingPresenter @Inject constructor( private val buildMeta: BuildMeta, private val featureFlagService: FeatureFlagService, + private val rageshakeFeatureAvailability: RageshakeFeatureAvailability, ) : Presenter { @Composable override fun present(): OnBoardingState { val canLoginWithQrCode by produceState(initialValue = false) { value = featureFlagService.isFeatureEnabled(FeatureFlags.QrCodeLogin) } + val canReportBug = remember { rageshakeFeatureAvailability.isAvailable() } return OnBoardingState( productionApplicationName = buildMeta.productionApplicationName, canLoginWithQrCode = canLoginWithQrCode, canCreateAccount = OnBoardingConfig.CAN_CREATE_ACCOUNT, + canReportBug = canReportBug, ) } } diff --git a/features/onboarding/impl/src/main/kotlin/io/element/android/features/onboarding/impl/OnBoardingState.kt b/features/onboarding/impl/src/main/kotlin/io/element/android/features/onboarding/impl/OnBoardingState.kt index 6ffb80c2bd..3a5afb741c 100644 --- a/features/onboarding/impl/src/main/kotlin/io/element/android/features/onboarding/impl/OnBoardingState.kt +++ b/features/onboarding/impl/src/main/kotlin/io/element/android/features/onboarding/impl/OnBoardingState.kt @@ -11,4 +11,5 @@ data class OnBoardingState( val productionApplicationName: String, val canLoginWithQrCode: Boolean, val canCreateAccount: Boolean, + val canReportBug: Boolean, ) diff --git a/features/onboarding/impl/src/main/kotlin/io/element/android/features/onboarding/impl/OnBoardingStateProvider.kt b/features/onboarding/impl/src/main/kotlin/io/element/android/features/onboarding/impl/OnBoardingStateProvider.kt index 25dc697782..d65b6aa2bf 100644 --- a/features/onboarding/impl/src/main/kotlin/io/element/android/features/onboarding/impl/OnBoardingStateProvider.kt +++ b/features/onboarding/impl/src/main/kotlin/io/element/android/features/onboarding/impl/OnBoardingStateProvider.kt @@ -16,15 +16,18 @@ open class OnBoardingStateProvider : PreviewParameterProvider { anOnBoardingState(canLoginWithQrCode = true), anOnBoardingState(canCreateAccount = true), anOnBoardingState(canLoginWithQrCode = true, canCreateAccount = true), + anOnBoardingState(canLoginWithQrCode = true, canCreateAccount = true, canReportBug = true), ) } fun anOnBoardingState( productionApplicationName: String = "Element", canLoginWithQrCode: Boolean = false, - canCreateAccount: Boolean = false + canCreateAccount: Boolean = false, + canReportBug: Boolean = false, ) = OnBoardingState( productionApplicationName = productionApplicationName, canLoginWithQrCode = canLoginWithQrCode, - canCreateAccount = canCreateAccount + canCreateAccount = canCreateAccount, + canReportBug = canReportBug, ) diff --git a/features/onboarding/impl/src/main/kotlin/io/element/android/features/onboarding/impl/OnBoardingView.kt b/features/onboarding/impl/src/main/kotlin/io/element/android/features/onboarding/impl/OnBoardingView.kt index 95a8e2618d..44ab2d84a5 100644 --- a/features/onboarding/impl/src/main/kotlin/io/element/android/features/onboarding/impl/OnBoardingView.kt +++ b/features/onboarding/impl/src/main/kotlin/io/element/android/features/onboarding/impl/OnBoardingView.kt @@ -144,8 +144,8 @@ private fun OnBoardingButtons( text = stringResource(id = signInButtonStringRes), onClick = onSignIn, modifier = Modifier - .fillMaxWidth() - .testTag(TestTags.onBoardingSignIn) + .fillMaxWidth() + .testTag(TestTags.onBoardingSignIn) ) if (state.canCreateAccount) { TextButton( @@ -155,15 +155,17 @@ private fun OnBoardingButtons( .fillMaxWidth() ) } - // Add a report problem text button. Use a Text since we need a special theme here. - Text( - modifier = Modifier + if (state.canReportBug) { + // Add a report problem text button. Use a Text since we need a special theme here. + Text( + modifier = Modifier .padding(16.dp) .clickable(onClick = onReportProblem), - text = stringResource(id = CommonStrings.common_report_a_problem), - style = ElementTheme.typography.fontBodySmRegular, - color = ElementTheme.colors.textSecondary, - ) + text = stringResource(id = CommonStrings.common_report_a_problem), + style = ElementTheme.typography.fontBodySmRegular, + color = ElementTheme.colors.textSecondary, + ) + } } } diff --git a/features/onboarding/impl/src/test/kotlin/io/element/android/features/onboarding/impl/OnBoardingPresenterTest.kt b/features/onboarding/impl/src/test/kotlin/io/element/android/features/onboarding/impl/OnBoardingPresenterTest.kt index e98fbff3db..b692d291b5 100644 --- a/features/onboarding/impl/src/test/kotlin/io/element/android/features/onboarding/impl/OnBoardingPresenterTest.kt +++ b/features/onboarding/impl/src/test/kotlin/io/element/android/features/onboarding/impl/OnBoardingPresenterTest.kt @@ -16,6 +16,7 @@ import io.element.android.libraries.featureflag.api.FeatureFlags import io.element.android.libraries.featureflag.test.FakeFeatureFlagService import io.element.android.libraries.matrix.test.core.aBuildMeta import io.element.android.tests.testutils.WarmUpRule +import io.element.android.tests.testutils.test import kotlinx.coroutines.test.runTest import org.junit.Rule import org.junit.Test @@ -38,6 +39,7 @@ class OnBoardingPresenterTest { val presenter = OnBoardingPresenter( buildMeta = buildMeta, featureFlagService = featureFlagService, + rageshakeFeatureAvailability = { true }, ) moleculeFlow(RecompositionMode.Immediate) { presenter.present() @@ -46,7 +48,21 @@ class OnBoardingPresenterTest { assertThat(initialState.canLoginWithQrCode).isFalse() assertThat(initialState.productionApplicationName).isEqualTo("B") assertThat(initialState.canCreateAccount).isEqualTo(OnBoardingConfig.CAN_CREATE_ACCOUNT) + assertThat(initialState.canReportBug).isTrue() assertThat(awaitItem().canLoginWithQrCode).isTrue() } } + + @Test + fun `present - rageshake not available`() = runTest { + val presenter = OnBoardingPresenter( + buildMeta = aBuildMeta(), + featureFlagService = FakeFeatureFlagService(), + rageshakeFeatureAvailability = { false }, + ) + presenter.test { + skipItems(1) + assertThat(awaitItem().canReportBug).isFalse() + } + } } diff --git a/features/onboarding/impl/src/test/kotlin/io/element/android/features/onboarding/impl/OnboardingViewTest.kt b/features/onboarding/impl/src/test/kotlin/io/element/android/features/onboarding/impl/OnboardingViewTest.kt index 25cb075697..955bbea1f1 100644 --- a/features/onboarding/impl/src/test/kotlin/io/element/android/features/onboarding/impl/OnboardingViewTest.kt +++ b/features/onboarding/impl/src/test/kotlin/io/element/android/features/onboarding/impl/OnboardingViewTest.kt @@ -10,6 +10,7 @@ package io.element.android.features.onboarding.impl import androidx.activity.ComponentActivity import androidx.compose.ui.test.junit4.AndroidComposeTestRule import androidx.compose.ui.test.junit4.createAndroidComposeRule +import androidx.compose.ui.test.onNodeWithText import androidx.test.ext.junit.runners.AndroidJUnit4 import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.tests.testutils.EnsureNeverCalled @@ -76,13 +77,28 @@ class OnboardingViewTest { fun `clicking on report a problem calls the sign in callback`() { ensureCalledOnce { callback -> rule.setOnboardingView( - state = anOnBoardingState(), + state = anOnBoardingState( + canReportBug = true, + ), onReportProblem = callback, ) + val text = rule.activity.getString(CommonStrings.common_report_a_problem) + rule.onNodeWithText(text).assertExists() rule.clickOn(CommonStrings.common_report_a_problem) } } + @Test + fun `cannot report a problem when the feature is disabled`() { + rule.setOnboardingView( + state = anOnBoardingState( + canReportBug = false, + ), + ) + val text = rule.activity.getString(CommonStrings.common_report_a_problem) + rule.onNodeWithText(text).assertDoesNotExist() + } + private fun AndroidComposeTestRule.setOnboardingView( state: OnBoardingState, onSignInWithQrCode: () -> Unit = EnsureNeverCalled(), diff --git a/features/preferences/impl/build.gradle.kts b/features/preferences/impl/build.gradle.kts index bbe96ecb80..c1f32affa3 100644 --- a/features/preferences/impl/build.gradle.kts +++ b/features/preferences/impl/build.gradle.kts @@ -1,3 +1,5 @@ +import config.BuildTimeConfig +import extension.buildConfigFieldStr import extension.setupAnvil /* @@ -19,6 +21,25 @@ android { isIncludeAndroidResources = true } } + + buildFeatures { + buildConfig = true + } + + defaultConfig { + buildConfigFieldStr( + name = "URL_COPYRIGHT", + value = BuildTimeConfig.URL_COPYRIGHT ?: "https://element.io/copyright", + ) + buildConfigFieldStr( + name = "URL_ACCEPTABLE_USE", + value = BuildTimeConfig.URL_ACCEPTABLE_USE ?: "https://element.io/acceptable-use-policy-terms", + ) + buildConfigFieldStr( + name = "URL_PRIVACY", + value = BuildTimeConfig.URL_PRIVACY ?: "https://element.io/privacy", + ) + } } setupAnvil() diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/about/ElementLegal.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/about/ElementLegal.kt index a94e1637ba..a5de31f05d 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/about/ElementLegal.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/about/ElementLegal.kt @@ -8,11 +8,12 @@ package io.element.android.features.preferences.impl.about import androidx.annotation.StringRes +import io.element.android.features.preferences.impl.BuildConfig import io.element.android.libraries.ui.strings.CommonStrings -private const val COPYRIGHT_URL = "https://element.io/copyright" -private const val USE_POLICY_URL = "https://element.io/acceptable-use-policy-terms" -private const val PRIVACY_URL = "https://element.io/privacy" +private const val COPYRIGHT_URL = BuildConfig.URL_COPYRIGHT +private const val USE_POLICY_URL = BuildConfig.URL_ACCEPTABLE_USE +private const val PRIVACY_URL = BuildConfig.URL_PRIVACY sealed class ElementLegal( @StringRes val titleRes: Int, diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootPresenter.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootPresenter.kt index ae5267de0f..15a55f41ca 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootPresenter.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootPresenter.kt @@ -18,6 +18,7 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import io.element.android.features.logout.api.direct.DirectLogoutState import io.element.android.features.preferences.impl.utils.ShowDeveloperSettingsProvider +import io.element.android.features.rageshake.api.RageshakeFeatureAvailability import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher import io.element.android.libraries.designsystem.utils.snackbar.collectSnackbarMessageAsState @@ -44,6 +45,7 @@ class PreferencesRootPresenter @Inject constructor( private val indicatorService: IndicatorService, private val directLogoutPresenter: Presenter, private val showDeveloperSettingsProvider: ShowDeveloperSettingsProvider, + private val rageshakeFeatureAvailability: RageshakeFeatureAvailability, ) : Presenter { @Composable override fun present(): PreferencesRootState { @@ -79,6 +81,7 @@ class PreferencesRootPresenter @Inject constructor( var canDeactivateAccount by remember { mutableStateOf(false) } + val canReportBug = remember { rageshakeFeatureAvailability.isAvailable() } LaunchedEffect(Unit) { canDeactivateAccount = matrixClient.canDeactivateAccount() } @@ -114,6 +117,7 @@ class PreferencesRootPresenter @Inject constructor( accountManagementUrl = accountManagementUrl.value, devicesManagementUrl = devicesManagementUrl.value, showAnalyticsSettings = hasAnalyticsProviders, + canReportBug = canReportBug, showDeveloperSettings = showDeveloperSettings, canDeactivateAccount = canDeactivateAccount, showNotificationSettings = showNotificationSettings.value, diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootState.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootState.kt index 9a16e6f65b..2e5cb4fa14 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootState.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootState.kt @@ -20,6 +20,7 @@ data class PreferencesRootState( val showSecureBackupBadge: Boolean, val accountManagementUrl: String?, val devicesManagementUrl: String?, + val canReportBug: Boolean, val showAnalyticsSettings: Boolean, val showDeveloperSettings: Boolean, val canDeactivateAccount: Boolean, diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootStateProvider.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootStateProvider.kt index e4ca550777..43307e9988 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootStateProvider.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootStateProvider.kt @@ -25,6 +25,7 @@ fun aPreferencesRootState( accountManagementUrl = "aUrl", devicesManagementUrl = "anOtherUrl", showAnalyticsSettings = true, + canReportBug = true, showDeveloperSettings = true, showNotificationSettings = true, showLockScreenSettings = true, diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootView.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootView.kt index 236d659f33..b63919cd26 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootView.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootView.kt @@ -202,11 +202,13 @@ private fun ColumnScope.GeneralSection( leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.Info())), onClick = onOpenAbout, ) - ListItem( - headlineContent = { Text(stringResource(id = CommonStrings.common_report_a_problem)) }, - leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.ChatProblem())), - onClick = onOpenRageShake - ) + if (state.canReportBug) { + ListItem( + headlineContent = { Text(stringResource(id = CommonStrings.common_report_a_problem)) }, + leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.ChatProblem())), + onClick = onOpenRageShake + ) + } if (state.showAnalyticsSettings) { ListItem( headlineContent = { Text(stringResource(id = CommonStrings.common_analytics)) }, diff --git a/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootPresenterTest.kt b/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootPresenterTest.kt index 72ae8bdb10..8075a43485 100644 --- a/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootPresenterTest.kt +++ b/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootPresenterTest.kt @@ -13,6 +13,7 @@ import app.cash.turbine.ReceiveTurbine import com.google.common.truth.Truth.assertThat import io.element.android.features.logout.api.direct.aDirectLogoutState import io.element.android.features.preferences.impl.utils.ShowDeveloperSettingsProvider +import io.element.android.features.rageshake.api.RageshakeFeatureAvailability import io.element.android.libraries.core.meta.BuildType import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher import io.element.android.libraries.featureflag.test.FakeFeatureFlagService @@ -78,6 +79,7 @@ class PreferencesRootPresenterTest { assertThat(loadedState.showLockScreenSettings).isTrue() assertThat(loadedState.showNotificationSettings).isTrue() assertThat(loadedState.canDeactivateAccount).isTrue() + assertThat(loadedState.canReportBug).isTrue() assertThat(loadedState.directLogoutState).isEqualTo(aDirectLogoutState()) assertThat(loadedState.snackbarMessage).isNull() skipItems(1) @@ -92,6 +94,22 @@ class PreferencesRootPresenterTest { } } + @Test + fun `present - cannot report bug`() = runTest { + val matrixClient = FakeMatrixClient( + canDeactivateAccountResult = { true }, + accountManagementUrlResult = { Result.success("") }, + ) + createPresenter( + matrixClient = matrixClient, + rageshakeFeatureAvailability = { false }, + ).test { + val initialState = awaitItem() + assertThat(initialState.canReportBug).isFalse() + skipItems(1) + } + } + @Test fun `present - can deactivate account is false if the Matrix client say so`() = runTest { createPresenter( @@ -146,6 +164,7 @@ class PreferencesRootPresenterTest { matrixClient: FakeMatrixClient = FakeMatrixClient(), sessionVerificationService: FakeSessionVerificationService = FakeSessionVerificationService(), showDeveloperSettingsProvider: ShowDeveloperSettingsProvider = ShowDeveloperSettingsProvider(aBuildMeta(BuildType.DEBUG)), + rageshakeFeatureAvailability: RageshakeFeatureAvailability = RageshakeFeatureAvailability { true }, ) = PreferencesRootPresenter( matrixClient = matrixClient, sessionVerificationService = sessionVerificationService, @@ -159,5 +178,6 @@ class PreferencesRootPresenterTest { ), directLogoutPresenter = { aDirectLogoutState() }, showDeveloperSettingsProvider = showDeveloperSettingsProvider, + rageshakeFeatureAvailability = rageshakeFeatureAvailability, ) } diff --git a/features/rageshake/api/src/main/kotlin/io/element/android/features/rageshake/api/RageshakeFeatureAvailability.kt b/features/rageshake/api/src/main/kotlin/io/element/android/features/rageshake/api/RageshakeFeatureAvailability.kt new file mode 100644 index 0000000000..34e740d4ab --- /dev/null +++ b/features/rageshake/api/src/main/kotlin/io/element/android/features/rageshake/api/RageshakeFeatureAvailability.kt @@ -0,0 +1,12 @@ +/* + * Copyright 2025 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.features.rageshake.api + +fun interface RageshakeFeatureAvailability { + fun isAvailable(): Boolean +} diff --git a/features/rageshake/api/src/main/kotlin/io/element/android/features/rageshake/api/preferences/RageshakePreferencesState.kt b/features/rageshake/api/src/main/kotlin/io/element/android/features/rageshake/api/preferences/RageshakePreferencesState.kt index 9207d5ff38..40a9d0282b 100644 --- a/features/rageshake/api/src/main/kotlin/io/element/android/features/rageshake/api/preferences/RageshakePreferencesState.kt +++ b/features/rageshake/api/src/main/kotlin/io/element/android/features/rageshake/api/preferences/RageshakePreferencesState.kt @@ -8,6 +8,7 @@ package io.element.android.features.rageshake.api.preferences data class RageshakePreferencesState( + val isFeatureEnabled: Boolean, val isEnabled: Boolean, val isSupported: Boolean, val sensitivity: Float, diff --git a/features/rageshake/api/src/main/kotlin/io/element/android/features/rageshake/api/preferences/RageshakePreferencesStateProvider.kt b/features/rageshake/api/src/main/kotlin/io/element/android/features/rageshake/api/preferences/RageshakePreferencesStateProvider.kt index d18123cd8d..a98c75a02a 100644 --- a/features/rageshake/api/src/main/kotlin/io/element/android/features/rageshake/api/preferences/RageshakePreferencesStateProvider.kt +++ b/features/rageshake/api/src/main/kotlin/io/element/android/features/rageshake/api/preferences/RageshakePreferencesStateProvider.kt @@ -12,14 +12,21 @@ import androidx.compose.ui.tooling.preview.PreviewParameterProvider open class RageshakePreferencesStateProvider : PreviewParameterProvider { override val values: Sequence get() = sequenceOf( - aRageshakePreferencesState().copy(isEnabled = true, isSupported = true, sensitivity = 0.5f), - aRageshakePreferencesState().copy(isEnabled = true, isSupported = false, sensitivity = 0.5f), + aRageshakePreferencesState(isEnabled = true, isSupported = true, sensitivity = 0.5f), + aRageshakePreferencesState(isEnabled = true, isSupported = false, sensitivity = 0.5f), ) } -fun aRageshakePreferencesState() = RageshakePreferencesState( - isEnabled = false, - isSupported = true, - sensitivity = 0.3f, - eventSink = {} +fun aRageshakePreferencesState( + isFeatureEnabled: Boolean = true, + isEnabled: Boolean = false, + isSupported: Boolean = true, + sensitivity: Float = 0.3f, + eventSink: (RageshakePreferencesEvents) -> Unit = {} +) = RageshakePreferencesState( + isFeatureEnabled = isFeatureEnabled, + isEnabled = isEnabled, + isSupported = isSupported, + sensitivity = sensitivity, + eventSink = eventSink, ) diff --git a/features/rageshake/api/src/main/kotlin/io/element/android/features/rageshake/api/preferences/RageshakePreferencesView.kt b/features/rageshake/api/src/main/kotlin/io/element/android/features/rageshake/api/preferences/RageshakePreferencesView.kt index 0841e097ba..86f1c05247 100644 --- a/features/rageshake/api/src/main/kotlin/io/element/android/features/rageshake/api/preferences/RageshakePreferencesView.kt +++ b/features/rageshake/api/src/main/kotlin/io/element/android/features/rageshake/api/preferences/RageshakePreferencesView.kt @@ -36,28 +36,30 @@ fun RageshakePreferencesView( } Column(modifier = modifier) { - PreferenceCategory(title = stringResource(id = R.string.settings_rageshake)) { - if (state.isSupported) { - PreferenceSwitch( - title = stringResource(id = CommonStrings.preference_rageshake), - isChecked = state.isEnabled, - onCheckedChange = ::onEnabledChanged - ) - PreferenceSlide( - title = stringResource(id = R.string.settings_rageshake_detection_threshold), - // summary = stringResource(id = CommonStrings.settings_rageshake_detection_threshold_summary), - value = state.sensitivity, - enabled = state.isEnabled, - // 5 possible values - steps are in ]0, 1[ - steps = 3, - onValueChange = ::onSensitivityChanged - ) - } else { - ListItem( - headlineContent = { - Text("Rageshaking is not supported by your device") - }, - ) + if (state.isFeatureEnabled) { + PreferenceCategory(title = stringResource(id = R.string.settings_rageshake)) { + if (state.isSupported) { + PreferenceSwitch( + title = stringResource(id = CommonStrings.preference_rageshake), + isChecked = state.isEnabled, + onCheckedChange = ::onEnabledChanged + ) + PreferenceSlide( + title = stringResource(id = R.string.settings_rageshake_detection_threshold), + // summary = stringResource(id = CommonStrings.settings_rageshake_detection_threshold_summary), + value = state.sensitivity, + enabled = state.isEnabled, + // 5 possible values - steps are in ]0, 1[ + steps = 3, + onValueChange = ::onSensitivityChanged + ) + } else { + ListItem( + headlineContent = { + Text("Rageshaking is not supported by your device") + }, + ) + } } } } diff --git a/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/DefaultRageshakeFeatureAvailability.kt b/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/DefaultRageshakeFeatureAvailability.kt new file mode 100644 index 0000000000..5f7548e9ec --- /dev/null +++ b/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/DefaultRageshakeFeatureAvailability.kt @@ -0,0 +1,22 @@ +/* + * Copyright 2025 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.features.rageshake.impl + +import com.squareup.anvil.annotations.ContributesBinding +import io.element.android.appconfig.RageshakeConfig +import io.element.android.appconfig.isEnabled +import io.element.android.features.rageshake.api.RageshakeFeatureAvailability +import io.element.android.libraries.di.AppScope +import javax.inject.Inject + +@ContributesBinding(AppScope::class) +class DefaultRageshakeFeatureAvailability @Inject constructor() : RageshakeFeatureAvailability { + override fun isAvailable(): Boolean { + return RageshakeConfig.isEnabled + } +} diff --git a/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/bugreport/BugReportPresenter.kt b/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/bugreport/BugReportPresenter.kt index 88bba32f23..294fc194d5 100644 --- a/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/bugreport/BugReportPresenter.kt +++ b/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/bugreport/BugReportPresenter.kt @@ -16,10 +16,10 @@ import androidx.compose.runtime.mutableFloatStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.saveable.rememberSaveable -import io.element.android.features.rageshake.api.crash.CrashDataStore import io.element.android.features.rageshake.api.reporter.BugReporter import io.element.android.features.rageshake.api.reporter.BugReporterListener -import io.element.android.features.rageshake.api.screenshot.ScreenshotHolder +import io.element.android.features.rageshake.impl.crash.CrashDataStore +import io.element.android.features.rageshake.impl.screenshot.ScreenshotHolder import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.architecture.Presenter import kotlinx.coroutines.CoroutineScope diff --git a/features/rageshake/api/src/main/kotlin/io/element/android/features/rageshake/api/crash/CrashDataStore.kt b/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/crash/CrashDataStore.kt similarity index 88% rename from features/rageshake/api/src/main/kotlin/io/element/android/features/rageshake/api/crash/CrashDataStore.kt rename to features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/crash/CrashDataStore.kt index 5a13f44a0a..3d6df9a424 100644 --- a/features/rageshake/api/src/main/kotlin/io/element/android/features/rageshake/api/crash/CrashDataStore.kt +++ b/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/crash/CrashDataStore.kt @@ -5,7 +5,7 @@ * Please see LICENSE files in the repository root for full details. */ -package io.element.android.features.rageshake.api.crash +package io.element.android.features.rageshake.impl.crash import kotlinx.coroutines.flow.Flow diff --git a/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/crash/DefaultCrashDetectionPresenter.kt b/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/crash/DefaultCrashDetectionPresenter.kt index 749ba4255c..fffb87722a 100644 --- a/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/crash/DefaultCrashDetectionPresenter.kt +++ b/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/crash/DefaultCrashDetectionPresenter.kt @@ -9,15 +9,17 @@ package io.element.android.features.rageshake.impl.crash import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import com.squareup.anvil.annotations.ContributesBinding -import io.element.android.features.rageshake.api.crash.CrashDataStore +import io.element.android.features.rageshake.api.RageshakeFeatureAvailability import io.element.android.features.rageshake.api.crash.CrashDetectionEvents import io.element.android.features.rageshake.api.crash.CrashDetectionPresenter import io.element.android.features.rageshake.api.crash.CrashDetectionState import io.element.android.libraries.core.meta.BuildMeta import io.element.android.libraries.di.AppScope import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.launch import javax.inject.Inject @@ -25,12 +27,18 @@ import javax.inject.Inject class DefaultCrashDetectionPresenter @Inject constructor( private val buildMeta: BuildMeta, private val crashDataStore: CrashDataStore, -) : - CrashDetectionPresenter { + private val rageshakeFeatureAvailability: RageshakeFeatureAvailability, +) : CrashDetectionPresenter { @Composable override fun present(): CrashDetectionState { val localCoroutineScope = rememberCoroutineScope() - val crashDetected = crashDataStore.appHasCrashed().collectAsState(initial = false) + val crashDetected = remember { + if (rageshakeFeatureAvailability.isAvailable()) { + crashDataStore.appHasCrashed() + } else { + flowOf(false) + } + }.collectAsState(false) fun handleEvents(event: CrashDetectionEvents) { when (event) { diff --git a/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/crash/PreferencesCrashDataStore.kt b/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/crash/PreferencesCrashDataStore.kt index 2cdea62b1a..0b86bb4ef0 100644 --- a/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/crash/PreferencesCrashDataStore.kt +++ b/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/crash/PreferencesCrashDataStore.kt @@ -15,7 +15,6 @@ import androidx.datastore.preferences.core.edit import androidx.datastore.preferences.core.stringPreferencesKey import androidx.datastore.preferences.preferencesDataStore import com.squareup.anvil.annotations.ContributesBinding -import io.element.android.features.rageshake.api.crash.CrashDataStore import io.element.android.libraries.core.bool.orFalse import io.element.android.libraries.di.AppScope import io.element.android.libraries.di.ApplicationContext diff --git a/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/detection/DefaultRageshakeDetectionPresenter.kt b/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/detection/DefaultRageshakeDetectionPresenter.kt index 29785d34fe..1a8aed7051 100644 --- a/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/detection/DefaultRageshakeDetectionPresenter.kt +++ b/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/detection/DefaultRageshakeDetectionPresenter.kt @@ -20,9 +20,9 @@ import io.element.android.features.rageshake.api.detection.RageshakeDetectionPre import io.element.android.features.rageshake.api.detection.RageshakeDetectionState import io.element.android.features.rageshake.api.preferences.RageshakePreferencesEvents import io.element.android.features.rageshake.api.preferences.RageshakePreferencesPresenter -import io.element.android.features.rageshake.api.rageshake.RageShake import io.element.android.features.rageshake.api.screenshot.ImageResult -import io.element.android.features.rageshake.api.screenshot.ScreenshotHolder +import io.element.android.features.rageshake.impl.rageshake.RageShake +import io.element.android.features.rageshake.impl.screenshot.ScreenshotHolder import io.element.android.libraries.di.AppScope import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch @@ -75,7 +75,8 @@ class DefaultRageshakeDetectionPresenter @Inject constructor( LaunchedEffect(preferencesState.sensitivity) { rageShake.setSensitivity(preferencesState.sensitivity) } - val shouldStart = preferencesState.isEnabled && + val shouldStart = preferencesState.isFeatureEnabled && + preferencesState.isEnabled && preferencesState.isSupported && isStarted.value && !takeScreenshot.value && diff --git a/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/preferences/DefaultRageshakePreferencesPresenter.kt b/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/preferences/DefaultRageshakePreferencesPresenter.kt index e129bed8d5..ba883b50a9 100644 --- a/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/preferences/DefaultRageshakePreferencesPresenter.kt +++ b/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/preferences/DefaultRageshakePreferencesPresenter.kt @@ -11,14 +11,16 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.MutableState import androidx.compose.runtime.collectAsState import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.saveable.rememberSaveable import com.squareup.anvil.annotations.ContributesBinding +import io.element.android.features.rageshake.api.RageshakeFeatureAvailability import io.element.android.features.rageshake.api.preferences.RageshakePreferencesEvents import io.element.android.features.rageshake.api.preferences.RageshakePreferencesPresenter import io.element.android.features.rageshake.api.preferences.RageshakePreferencesState -import io.element.android.features.rageshake.api.rageshake.RageShake -import io.element.android.features.rageshake.api.rageshake.RageshakeDataStore +import io.element.android.features.rageshake.impl.rageshake.RageShake +import io.element.android.features.rageshake.impl.rageshake.RageshakeDataStore import io.element.android.libraries.di.AppScope import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch @@ -28,6 +30,7 @@ import javax.inject.Inject class DefaultRageshakePreferencesPresenter @Inject constructor( private val rageshake: RageShake, private val rageshakeDataStore: RageshakeDataStore, + private val rageshakeFeatureAvailability: RageshakeFeatureAvailability, ) : RageshakePreferencesPresenter { @Composable override fun present(): RageshakePreferencesState { @@ -35,6 +38,7 @@ class DefaultRageshakePreferencesPresenter @Inject constructor( val isSupported: MutableState = rememberSaveable { mutableStateOf(rageshake.isAvailable()) } + val isFeatureAvailable = remember { rageshakeFeatureAvailability.isAvailable() } val isEnabled = rageshakeDataStore .isEnabled() .collectAsState(initial = false) @@ -51,6 +55,7 @@ class DefaultRageshakePreferencesPresenter @Inject constructor( } return RageshakePreferencesState( + isFeatureEnabled = isFeatureAvailable, isEnabled = isEnabled.value, isSupported = isSupported.value, sensitivity = sensitivity.value, diff --git a/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/rageshake/DefaultRageShake.kt b/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/rageshake/DefaultRageShake.kt index 21c5fffdb7..651b71c079 100644 --- a/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/rageshake/DefaultRageShake.kt +++ b/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/rageshake/DefaultRageShake.kt @@ -13,7 +13,6 @@ import android.hardware.SensorManager import androidx.core.content.getSystemService import com.squareup.anvil.annotations.ContributesBinding import com.squareup.seismic.ShakeDetector -import io.element.android.features.rageshake.api.rageshake.RageShake import io.element.android.libraries.di.AppScope import io.element.android.libraries.di.ApplicationContext import io.element.android.libraries.di.SingleIn diff --git a/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/rageshake/PreferencesRageshakeDataStore.kt b/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/rageshake/PreferencesRageshakeDataStore.kt index f9379de16d..9d7171b8a0 100644 --- a/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/rageshake/PreferencesRageshakeDataStore.kt +++ b/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/rageshake/PreferencesRageshakeDataStore.kt @@ -15,7 +15,6 @@ import androidx.datastore.preferences.core.edit import androidx.datastore.preferences.core.floatPreferencesKey import androidx.datastore.preferences.preferencesDataStore import com.squareup.anvil.annotations.ContributesBinding -import io.element.android.features.rageshake.api.rageshake.RageshakeDataStore import io.element.android.libraries.core.bool.orFalse import io.element.android.libraries.di.AppScope import io.element.android.libraries.di.ApplicationContext diff --git a/features/rageshake/api/src/main/kotlin/io/element/android/features/rageshake/api/rageshake/RageShake.kt b/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/rageshake/RageShake.kt similarity index 91% rename from features/rageshake/api/src/main/kotlin/io/element/android/features/rageshake/api/rageshake/RageShake.kt rename to features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/rageshake/RageShake.kt index 548d11a41d..d75d5e5666 100644 --- a/features/rageshake/api/src/main/kotlin/io/element/android/features/rageshake/api/rageshake/RageShake.kt +++ b/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/rageshake/RageShake.kt @@ -5,7 +5,7 @@ * Please see LICENSE files in the repository root for full details. */ -package io.element.android.features.rageshake.api.rageshake +package io.element.android.features.rageshake.impl.rageshake interface RageShake { /** diff --git a/features/rageshake/api/src/main/kotlin/io/element/android/features/rageshake/api/rageshake/RageshakeDataStore.kt b/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/rageshake/RageshakeDataStore.kt similarity index 88% rename from features/rageshake/api/src/main/kotlin/io/element/android/features/rageshake/api/rageshake/RageshakeDataStore.kt rename to features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/rageshake/RageshakeDataStore.kt index a59c3670d8..f13419bfb2 100644 --- a/features/rageshake/api/src/main/kotlin/io/element/android/features/rageshake/api/rageshake/RageshakeDataStore.kt +++ b/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/rageshake/RageshakeDataStore.kt @@ -5,7 +5,7 @@ * Please see LICENSE files in the repository root for full details. */ -package io.element.android.features.rageshake.api.rageshake +package io.element.android.features.rageshake.impl.rageshake import kotlinx.coroutines.flow.Flow diff --git a/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/reporter/DefaultBugReporter.kt b/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/reporter/DefaultBugReporter.kt index 4ec8500417..36ece91408 100755 --- a/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/reporter/DefaultBugReporter.kt +++ b/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/reporter/DefaultBugReporter.kt @@ -13,10 +13,10 @@ import androidx.core.net.toFile import androidx.core.net.toUri import com.squareup.anvil.annotations.ContributesBinding import io.element.android.appconfig.RageshakeConfig -import io.element.android.features.rageshake.api.crash.CrashDataStore import io.element.android.features.rageshake.api.reporter.BugReporter import io.element.android.features.rageshake.api.reporter.BugReporterListener -import io.element.android.features.rageshake.api.screenshot.ScreenshotHolder +import io.element.android.features.rageshake.impl.crash.CrashDataStore +import io.element.android.features.rageshake.impl.screenshot.ScreenshotHolder import io.element.android.libraries.androidutils.file.compressFile import io.element.android.libraries.androidutils.file.safeDelete import io.element.android.libraries.core.coroutine.CoroutineDispatchers diff --git a/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/screenshot/DefaultScreenshotHolder.kt b/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/screenshot/DefaultScreenshotHolder.kt index 270c5628b3..dd3674ab26 100644 --- a/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/screenshot/DefaultScreenshotHolder.kt +++ b/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/screenshot/DefaultScreenshotHolder.kt @@ -11,7 +11,6 @@ import android.content.Context import android.graphics.Bitmap import androidx.core.net.toUri import com.squareup.anvil.annotations.ContributesBinding -import io.element.android.features.rageshake.api.screenshot.ScreenshotHolder import io.element.android.libraries.androidutils.bitmap.writeBitmap import io.element.android.libraries.androidutils.file.safeDelete import io.element.android.libraries.di.AppScope diff --git a/features/rageshake/api/src/main/kotlin/io/element/android/features/rageshake/api/screenshot/ScreenshotHolder.kt b/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/screenshot/ScreenshotHolder.kt similarity index 84% rename from features/rageshake/api/src/main/kotlin/io/element/android/features/rageshake/api/screenshot/ScreenshotHolder.kt rename to features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/screenshot/ScreenshotHolder.kt index 9a7e64da59..c746a573d3 100644 --- a/features/rageshake/api/src/main/kotlin/io/element/android/features/rageshake/api/screenshot/ScreenshotHolder.kt +++ b/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/screenshot/ScreenshotHolder.kt @@ -5,7 +5,7 @@ * Please see LICENSE files in the repository root for full details. */ -package io.element.android.features.rageshake.api.screenshot +package io.element.android.features.rageshake.impl.screenshot import android.graphics.Bitmap diff --git a/features/rageshake/impl/src/test/kotlin/io/element/android/features/rageshake/impl/bugreport/BugReportPresenterTest.kt b/features/rageshake/impl/src/test/kotlin/io/element/android/features/rageshake/impl/bugreport/BugReportPresenterTest.kt index cae6f6e500..027e2fb38c 100644 --- a/features/rageshake/impl/src/test/kotlin/io/element/android/features/rageshake/impl/bugreport/BugReportPresenterTest.kt +++ b/features/rageshake/impl/src/test/kotlin/io/element/android/features/rageshake/impl/bugreport/BugReportPresenterTest.kt @@ -11,13 +11,13 @@ import app.cash.molecule.RecompositionMode import app.cash.molecule.moleculeFlow import app.cash.turbine.test import com.google.common.truth.Truth.assertThat -import io.element.android.features.rageshake.api.crash.CrashDataStore import io.element.android.features.rageshake.api.reporter.BugReporter -import io.element.android.features.rageshake.api.screenshot.ScreenshotHolder -import io.element.android.features.rageshake.test.crash.A_CRASH_DATA -import io.element.android.features.rageshake.test.crash.FakeCrashDataStore -import io.element.android.features.rageshake.test.screenshot.A_SCREENSHOT_URI -import io.element.android.features.rageshake.test.screenshot.FakeScreenshotHolder +import io.element.android.features.rageshake.impl.crash.A_CRASH_DATA +import io.element.android.features.rageshake.impl.crash.CrashDataStore +import io.element.android.features.rageshake.impl.crash.FakeCrashDataStore +import io.element.android.features.rageshake.impl.screenshot.A_SCREENSHOT_URI +import io.element.android.features.rageshake.impl.screenshot.FakeScreenshotHolder +import io.element.android.features.rageshake.impl.screenshot.ScreenshotHolder import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.matrix.test.A_FAILURE_REASON import io.element.android.tests.testutils.WarmUpRule diff --git a/features/rageshake/test/src/main/kotlin/io/element/android/features/rageshake/test/crash/FakeCrashDataStore.kt b/features/rageshake/impl/src/test/kotlin/io/element/android/features/rageshake/impl/crash/FakeCrashDataStore.kt similarity index 88% rename from features/rageshake/test/src/main/kotlin/io/element/android/features/rageshake/test/crash/FakeCrashDataStore.kt rename to features/rageshake/impl/src/test/kotlin/io/element/android/features/rageshake/impl/crash/FakeCrashDataStore.kt index 145e582dec..1a89a52bf7 100644 --- a/features/rageshake/test/src/main/kotlin/io/element/android/features/rageshake/test/crash/FakeCrashDataStore.kt +++ b/features/rageshake/impl/src/test/kotlin/io/element/android/features/rageshake/impl/crash/FakeCrashDataStore.kt @@ -5,9 +5,8 @@ * Please see LICENSE files in the repository root for full details. */ -package io.element.android.features.rageshake.test.crash +package io.element.android.features.rageshake.impl.crash -import io.element.android.features.rageshake.api.crash.CrashDataStore import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow diff --git a/features/rageshake/impl/src/test/kotlin/io/element/android/features/rageshake/impl/crash/ui/CrashDetectionPresenterTest.kt b/features/rageshake/impl/src/test/kotlin/io/element/android/features/rageshake/impl/crash/ui/CrashDetectionPresenterTest.kt index a5fa6671d7..92dfbb02a2 100644 --- a/features/rageshake/impl/src/test/kotlin/io/element/android/features/rageshake/impl/crash/ui/CrashDetectionPresenterTest.kt +++ b/features/rageshake/impl/src/test/kotlin/io/element/android/features/rageshake/impl/crash/ui/CrashDetectionPresenterTest.kt @@ -12,9 +12,9 @@ import app.cash.molecule.moleculeFlow import app.cash.turbine.test import com.google.common.truth.Truth.assertThat import io.element.android.features.rageshake.api.crash.CrashDetectionEvents +import io.element.android.features.rageshake.impl.crash.A_CRASH_DATA import io.element.android.features.rageshake.impl.crash.DefaultCrashDetectionPresenter -import io.element.android.features.rageshake.test.crash.A_CRASH_DATA -import io.element.android.features.rageshake.test.crash.FakeCrashDataStore +import io.element.android.features.rageshake.impl.crash.FakeCrashDataStore import io.element.android.libraries.core.meta.BuildMeta import io.element.android.libraries.matrix.test.core.aBuildMeta import io.element.android.tests.testutils.WarmUpRule @@ -51,6 +51,20 @@ class CrashDetectionPresenterTest { } } + @Test + fun `present - initial state crash is ignored if the feature is not available`() = runTest { + val presenter = createPresenter( + FakeCrashDataStore(appHasCrashed = true), + isFeatureAvailable = false, + ) + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + assertThat(initialState.crashDetected).isFalse() + } + } + @Test fun `present - reset app has crashed`() = runTest { val presenter = createPresenter( @@ -86,8 +100,10 @@ class CrashDetectionPresenterTest { private fun createPresenter( crashDataStore: FakeCrashDataStore = FakeCrashDataStore(), buildMeta: BuildMeta = aBuildMeta(), + isFeatureAvailable: Boolean = true, ) = DefaultCrashDetectionPresenter( buildMeta = buildMeta, crashDataStore = crashDataStore, + rageshakeFeatureAvailability = { isFeatureAvailable }, ) } diff --git a/features/rageshake/impl/src/test/kotlin/io/element/android/features/rageshake/impl/detection/RageshakeDetectionPresenterTest.kt b/features/rageshake/impl/src/test/kotlin/io/element/android/features/rageshake/impl/detection/RageshakeDetectionPresenterTest.kt index 5aa9679b8c..6f433a78d7 100644 --- a/features/rageshake/impl/src/test/kotlin/io/element/android/features/rageshake/impl/detection/RageshakeDetectionPresenterTest.kt +++ b/features/rageshake/impl/src/test/kotlin/io/element/android/features/rageshake/impl/detection/RageshakeDetectionPresenterTest.kt @@ -15,9 +15,9 @@ import com.google.common.truth.Truth.assertThat import io.element.android.features.rageshake.api.detection.RageshakeDetectionEvents import io.element.android.features.rageshake.api.screenshot.ImageResult import io.element.android.features.rageshake.impl.preferences.DefaultRageshakePreferencesPresenter -import io.element.android.features.rageshake.test.rageshake.FakeRageShake -import io.element.android.features.rageshake.test.rageshake.FakeRageshakeDataStore -import io.element.android.features.rageshake.test.screenshot.FakeScreenshotHolder +import io.element.android.features.rageshake.impl.rageshake.FakeRageShake +import io.element.android.features.rageshake.impl.rageshake.FakeRageshakeDataStore +import io.element.android.features.rageshake.impl.screenshot.FakeScreenshotHolder import io.element.android.libraries.matrix.test.AN_EXCEPTION import io.element.android.tests.testutils.WarmUpRule import io.mockk.mockk @@ -52,6 +52,7 @@ class RageshakeDetectionPresenterTest { preferencesPresenter = DefaultRageshakePreferencesPresenter( rageshake = rageshake, rageshakeDataStore = rageshakeDataStore, + rageshakeFeatureAvailability = { true }, ) ) moleculeFlow(RecompositionMode.Immediate) { @@ -76,6 +77,7 @@ class RageshakeDetectionPresenterTest { preferencesPresenter = DefaultRageshakePreferencesPresenter( rageshake = rageshake, rageshakeDataStore = rageshakeDataStore, + rageshakeFeatureAvailability = { true }, ) ) moleculeFlow(RecompositionMode.Immediate) { @@ -101,6 +103,7 @@ class RageshakeDetectionPresenterTest { preferencesPresenter = DefaultRageshakePreferencesPresenter( rageshake = rageshake, rageshakeDataStore = rageshakeDataStore, + rageshakeFeatureAvailability = { true }, ) ) moleculeFlow(RecompositionMode.Immediate) { @@ -135,6 +138,7 @@ class RageshakeDetectionPresenterTest { preferencesPresenter = DefaultRageshakePreferencesPresenter( rageshake = rageshake, rageshakeDataStore = rageshakeDataStore, + rageshakeFeatureAvailability = { true }, ) ) moleculeFlow(RecompositionMode.Immediate) { @@ -169,6 +173,7 @@ class RageshakeDetectionPresenterTest { preferencesPresenter = DefaultRageshakePreferencesPresenter( rageshake = rageshake, rageshakeDataStore = rageshakeDataStore, + rageshakeFeatureAvailability = { true }, ) ) moleculeFlow(RecompositionMode.Immediate) { diff --git a/features/rageshake/impl/src/test/kotlin/io/element/android/features/rageshake/impl/preferences/RageshakePreferencesPresenterTest.kt b/features/rageshake/impl/src/test/kotlin/io/element/android/features/rageshake/impl/preferences/RageshakePreferencesPresenterTest.kt index 796545c859..ba68345440 100644 --- a/features/rageshake/impl/src/test/kotlin/io/element/android/features/rageshake/impl/preferences/RageshakePreferencesPresenterTest.kt +++ b/features/rageshake/impl/src/test/kotlin/io/element/android/features/rageshake/impl/preferences/RageshakePreferencesPresenterTest.kt @@ -12,9 +12,9 @@ import app.cash.molecule.moleculeFlow import app.cash.turbine.test import com.google.common.truth.Truth.assertThat import io.element.android.features.rageshake.api.preferences.RageshakePreferencesEvents -import io.element.android.features.rageshake.test.rageshake.A_SENSITIVITY -import io.element.android.features.rageshake.test.rageshake.FakeRageShake -import io.element.android.features.rageshake.test.rageshake.FakeRageshakeDataStore +import io.element.android.features.rageshake.impl.rageshake.A_SENSITIVITY +import io.element.android.features.rageshake.impl.rageshake.FakeRageShake +import io.element.android.features.rageshake.impl.rageshake.FakeRageshakeDataStore import io.element.android.tests.testutils.WarmUpRule import kotlinx.coroutines.test.runTest import org.junit.Rule @@ -28,7 +28,8 @@ class RageshakePreferencesPresenterTest { fun `present - initial state available`() = runTest { val presenter = DefaultRageshakePreferencesPresenter( FakeRageShake(isAvailableValue = true), - FakeRageshakeDataStore(isEnabled = true) + FakeRageshakeDataStore(isEnabled = true), + rageshakeFeatureAvailability = { true }, ) moleculeFlow(RecompositionMode.Immediate) { presenter.present() @@ -44,7 +45,8 @@ class RageshakePreferencesPresenterTest { fun `present - initial state not available`() = runTest { val presenter = DefaultRageshakePreferencesPresenter( FakeRageShake(isAvailableValue = false), - FakeRageshakeDataStore(isEnabled = true) + FakeRageshakeDataStore(isEnabled = true), + rageshakeFeatureAvailability = { true }, ) moleculeFlow(RecompositionMode.Immediate) { presenter.present() @@ -60,7 +62,8 @@ class RageshakePreferencesPresenterTest { fun `present - enable and disable`() = runTest { val presenter = DefaultRageshakePreferencesPresenter( FakeRageShake(isAvailableValue = true), - FakeRageshakeDataStore(isEnabled = true) + FakeRageshakeDataStore(isEnabled = true), + rageshakeFeatureAvailability = { true }, ) moleculeFlow(RecompositionMode.Immediate) { presenter.present() @@ -79,7 +82,8 @@ class RageshakePreferencesPresenterTest { fun `present - set sensitivity`() = runTest { val presenter = DefaultRageshakePreferencesPresenter( FakeRageShake(isAvailableValue = true), - FakeRageshakeDataStore(isEnabled = true) + FakeRageshakeDataStore(isEnabled = true), + rageshakeFeatureAvailability = { true }, ) moleculeFlow(RecompositionMode.Immediate) { presenter.present() diff --git a/features/rageshake/test/src/main/kotlin/io/element/android/features/rageshake/test/rageshake/FakeRageShake.kt b/features/rageshake/impl/src/test/kotlin/io/element/android/features/rageshake/impl/rageshake/FakeRageShake.kt similarity index 84% rename from features/rageshake/test/src/main/kotlin/io/element/android/features/rageshake/test/rageshake/FakeRageShake.kt rename to features/rageshake/impl/src/test/kotlin/io/element/android/features/rageshake/impl/rageshake/FakeRageShake.kt index 1e3ac96770..b35bde295a 100644 --- a/features/rageshake/test/src/main/kotlin/io/element/android/features/rageshake/test/rageshake/FakeRageShake.kt +++ b/features/rageshake/impl/src/test/kotlin/io/element/android/features/rageshake/impl/rageshake/FakeRageShake.kt @@ -5,9 +5,7 @@ * Please see LICENSE files in the repository root for full details. */ -package io.element.android.features.rageshake.test.rageshake - -import io.element.android.features.rageshake.api.rageshake.RageShake +package io.element.android.features.rageshake.impl.rageshake class FakeRageShake( private var isAvailableValue: Boolean = true diff --git a/features/rageshake/test/src/main/kotlin/io/element/android/features/rageshake/test/rageshake/FakeRageshakeDataStore.kt b/features/rageshake/impl/src/test/kotlin/io/element/android/features/rageshake/impl/rageshake/FakeRageshakeDataStore.kt similarity index 87% rename from features/rageshake/test/src/main/kotlin/io/element/android/features/rageshake/test/rageshake/FakeRageshakeDataStore.kt rename to features/rageshake/impl/src/test/kotlin/io/element/android/features/rageshake/impl/rageshake/FakeRageshakeDataStore.kt index b8fcc22d23..ec67ed13c0 100644 --- a/features/rageshake/test/src/main/kotlin/io/element/android/features/rageshake/test/rageshake/FakeRageshakeDataStore.kt +++ b/features/rageshake/impl/src/test/kotlin/io/element/android/features/rageshake/impl/rageshake/FakeRageshakeDataStore.kt @@ -5,9 +5,8 @@ * Please see LICENSE files in the repository root for full details. */ -package io.element.android.features.rageshake.test.rageshake +package io.element.android.features.rageshake.impl.rageshake -import io.element.android.features.rageshake.api.rageshake.RageshakeDataStore import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow diff --git a/features/rageshake/impl/src/test/kotlin/io/element/android/features/rageshake/impl/reporter/DefaultBugReporterTest.kt b/features/rageshake/impl/src/test/kotlin/io/element/android/features/rageshake/impl/reporter/DefaultBugReporterTest.kt index b38c52e6ef..e73c7863b4 100755 --- a/features/rageshake/impl/src/test/kotlin/io/element/android/features/rageshake/impl/reporter/DefaultBugReporterTest.kt +++ b/features/rageshake/impl/src/test/kotlin/io/element/android/features/rageshake/impl/reporter/DefaultBugReporterTest.kt @@ -8,9 +8,10 @@ package io.element.android.features.rageshake.impl.reporter import com.google.common.truth.Truth.assertThat +import io.element.android.appconfig.RageshakeConfig import io.element.android.features.rageshake.api.reporter.BugReporterListener -import io.element.android.features.rageshake.test.crash.FakeCrashDataStore -import io.element.android.features.rageshake.test.screenshot.FakeScreenshotHolder +import io.element.android.features.rageshake.impl.crash.FakeCrashDataStore +import io.element.android.features.rageshake.impl.screenshot.FakeScreenshotHolder import io.element.android.libraries.matrix.test.FakeMatrixClient import io.element.android.libraries.matrix.test.FakeMatrixClientProvider import io.element.android.libraries.matrix.test.FakeSdkMetadata @@ -138,7 +139,7 @@ class DefaultBugReporterTest { val foundValues = collectValuesFromFormData(request) - assertThat(foundValues["app"]).isEqualTo("element-x-android") + assertThat(foundValues["app"]).isEqualTo(RageshakeConfig.BUG_REPORT_APP_NAME) assertThat(foundValues["can_contact"]).isEqualTo("true") assertThat(foundValues["device_id"]).isEqualTo("ABCDEFGH") assertThat(foundValues["sdk_sha"]).isEqualTo("123456789") diff --git a/features/rageshake/impl/src/test/kotlin/io/element/android/features/rageshake/impl/reporter/DefaultBugReporterUrlProviderTest.kt b/features/rageshake/impl/src/test/kotlin/io/element/android/features/rageshake/impl/reporter/DefaultBugReporterUrlProviderTest.kt index 415440b164..71563892dc 100644 --- a/features/rageshake/impl/src/test/kotlin/io/element/android/features/rageshake/impl/reporter/DefaultBugReporterUrlProviderTest.kt +++ b/features/rageshake/impl/src/test/kotlin/io/element/android/features/rageshake/impl/reporter/DefaultBugReporterUrlProviderTest.kt @@ -16,7 +16,9 @@ class DefaultBugReporterUrlProviderTest { @Test fun `test DefaultBugReporterUrlProvider`() { val sut = DefaultBugReporterUrlProvider() - val result = sut.provide() - assertThat(result).isEqualTo(RageshakeConfig.BUG_REPORT_URL.toHttpUrl()) + if (RageshakeConfig.BUG_REPORT_URL.isNotEmpty()) { + val result = sut.provide() + assertThat(result).isEqualTo(RageshakeConfig.BUG_REPORT_URL.toHttpUrl()) + } } } diff --git a/features/rageshake/test/src/main/kotlin/io/element/android/features/rageshake/test/screenshot/FakeScreenshotHolder.kt b/features/rageshake/impl/src/test/kotlin/io/element/android/features/rageshake/impl/screenshot/FakeScreenshotHolder.kt similarity index 78% rename from features/rageshake/test/src/main/kotlin/io/element/android/features/rageshake/test/screenshot/FakeScreenshotHolder.kt rename to features/rageshake/impl/src/test/kotlin/io/element/android/features/rageshake/impl/screenshot/FakeScreenshotHolder.kt index 3f7d83f1c2..8e37da1910 100644 --- a/features/rageshake/test/src/main/kotlin/io/element/android/features/rageshake/test/screenshot/FakeScreenshotHolder.kt +++ b/features/rageshake/impl/src/test/kotlin/io/element/android/features/rageshake/impl/screenshot/FakeScreenshotHolder.kt @@ -5,10 +5,9 @@ * Please see LICENSE files in the repository root for full details. */ -package io.element.android.features.rageshake.test.screenshot +package io.element.android.features.rageshake.impl.screenshot import android.graphics.Bitmap -import io.element.android.features.rageshake.api.screenshot.ScreenshotHolder const val A_SCREENSHOT_URI = "file://content/uri" diff --git a/features/roomlist/impl/build.gradle.kts b/features/roomlist/impl/build.gradle.kts index a39971b415..87f683d847 100644 --- a/features/roomlist/impl/build.gradle.kts +++ b/features/roomlist/impl/build.gradle.kts @@ -48,6 +48,7 @@ dependencies { implementation(projects.features.networkmonitor.api) implementation(projects.features.logout.api) implementation(projects.features.leaveroom.api) + implementation(projects.features.rageshake.api) implementation(projects.services.analytics.api) implementation(libs.androidx.datastore.preferences) api(projects.features.roomlist.api) diff --git a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListPresenter.kt b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListPresenter.kt index f3e7daf4d7..a9be28e6ae 100644 --- a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListPresenter.kt +++ b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListPresenter.kt @@ -30,6 +30,7 @@ import io.element.android.features.invite.api.response.InviteData import io.element.android.features.leaveroom.api.LeaveRoomEvent import io.element.android.features.leaveroom.api.LeaveRoomState import io.element.android.features.logout.api.direct.DirectLogoutState +import io.element.android.features.rageshake.api.RageshakeFeatureAvailability import io.element.android.features.roomlist.impl.datasource.RoomListDataSource import io.element.android.features.roomlist.impl.filters.RoomListFiltersState import io.element.android.features.roomlist.impl.model.RoomListRoomSummary @@ -91,6 +92,7 @@ class RoomListPresenter @Inject constructor( private val notificationCleaner: NotificationCleaner, private val logoutPresenter: Presenter, private val appPreferencesStore: AppPreferencesStore, + private val rageshakeFeatureAvailability: RageshakeFeatureAvailability, ) : Presenter { private val encryptionService: EncryptionService = client.encryptionService() @@ -103,6 +105,7 @@ class RoomListPresenter @Inject constructor( val filtersState = filtersPresenter.present() val searchState = searchPresenter.present() val acceptDeclineInviteState = acceptDeclineInvitePresenter.present() + val canReportBug = remember { rageshakeFeatureAvailability.isAvailable() } LaunchedEffect(Unit) { roomListDataSource.launchIn(this) @@ -163,6 +166,7 @@ class RoomListPresenter @Inject constructor( contextMenu = contextMenu.value, leaveRoomState = leaveRoomState, filtersState = filtersState, + canReportBug = canReportBug, searchState = searchState, contentState = contentState, acceptDeclineInviteState = acceptDeclineInviteState, diff --git a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListState.kt b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListState.kt index 07994ef8cb..4ee15b7fab 100644 --- a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListState.kt +++ b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListState.kt @@ -29,6 +29,7 @@ data class RoomListState( val contextMenu: ContextMenu, val leaveRoomState: LeaveRoomState, val filtersState: RoomListFiltersState, + val canReportBug: Boolean, val searchState: RoomListSearchState, val contentState: RoomListContentState, val acceptDeclineInviteState: AcceptDeclineInviteState, diff --git a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListStateProvider.kt b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListStateProvider.kt index 4c21d797f1..6b075db9df 100644 --- a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListStateProvider.kt +++ b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListStateProvider.kt @@ -57,6 +57,7 @@ internal fun aRoomListState( leaveRoomState: LeaveRoomState = aLeaveRoomState(), searchState: RoomListSearchState = aRoomListSearchState(), filtersState: RoomListFiltersState = aRoomListFiltersState(), + canReportBug: Boolean = true, contentState: RoomListContentState = aRoomsContentState(), acceptDeclineInviteState: AcceptDeclineInviteState = anAcceptDeclineInviteState(), directLogoutState: DirectLogoutState = aDirectLogoutState(), @@ -69,6 +70,7 @@ internal fun aRoomListState( contextMenu = contextMenu, leaveRoomState = leaveRoomState, filtersState = filtersState, + canReportBug = canReportBug, searchState = searchState, contentState = contentState, acceptDeclineInviteState = acceptDeclineInviteState, diff --git a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListView.kt b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListView.kt index 8a5ade09d2..64ec1f8406 100644 --- a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListView.kt +++ b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListView.kt @@ -127,6 +127,7 @@ private fun RoomListScaffold( displayMenuItems = state.displayActions, displayFilters = state.displayFilters, filtersState = state.filtersState, + canReportBug = state.canReportBug, ) }, content = { padding -> diff --git a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/components/RoomListTopBar.kt b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/components/RoomListTopBar.kt index a0e9078530..1f4c86f579 100644 --- a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/components/RoomListTopBar.kt +++ b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/components/RoomListTopBar.kt @@ -85,6 +85,7 @@ fun RoomListTopBar( displayMenuItems: Boolean, displayFilters: Boolean, filtersState: RoomListFiltersState, + canReportBug: Boolean, modifier: Modifier = Modifier, ) { DefaultRoomListTopBar( @@ -98,6 +99,7 @@ fun RoomListTopBar( displayMenuItems = displayMenuItems, displayFilters = displayFilters, filtersState = filtersState, + canReportBug = canReportBug, modifier = modifier, ) } @@ -115,6 +117,7 @@ private fun DefaultRoomListTopBar( displayMenuItems: Boolean, displayFilters: Boolean, filtersState: RoomListFiltersState, + canReportBug: Boolean, modifier: Modifier = Modifier, ) { // We need this to manually clip the top app bar in preview mode @@ -239,7 +242,7 @@ private fun DefaultRoomListTopBar( } ) } - if (RoomListConfig.SHOW_REPORT_PROBLEM_MENU_ITEM) { + if (RoomListConfig.SHOW_REPORT_PROBLEM_MENU_ITEM && canReportBug) { DropdownMenuItem( onClick = { showMenu = false @@ -319,6 +322,7 @@ internal fun DefaultRoomListTopBarPreview() = ElementPreview { displayMenuItems = true, displayFilters = true, filtersState = aRoomListFiltersState(), + canReportBug = true, onMenuActionClick = {}, ) } @@ -337,6 +341,7 @@ internal fun DefaultRoomListTopBarWithIndicatorPreview() = ElementPreview { displayMenuItems = true, displayFilters = true, filtersState = aRoomListFiltersState(), + canReportBug = true, onMenuActionClick = {}, ) } diff --git a/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/RoomListPresenterTest.kt b/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/RoomListPresenterTest.kt index a48d63cad8..5043b78b10 100644 --- a/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/RoomListPresenterTest.kt +++ b/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/RoomListPresenterTest.kt @@ -19,6 +19,7 @@ import io.element.android.features.leaveroom.api.LeaveRoomEvent import io.element.android.features.leaveroom.api.LeaveRoomState import io.element.android.features.leaveroom.api.aLeaveRoomState import io.element.android.features.logout.api.direct.aDirectLogoutState +import io.element.android.features.rageshake.api.RageshakeFeatureAvailability import io.element.android.features.roomlist.impl.datasource.RoomListDataSource import io.element.android.features.roomlist.impl.datasource.aRoomListRoomSummaryFactory import io.element.android.features.roomlist.impl.filters.RoomListFiltersState @@ -105,12 +106,14 @@ class RoomListPresenterTest { matrixClient.givenGetProfileResult(matrixClient.sessionId, Result.success(MatrixUser(matrixClient.sessionId, A_USER_NAME, AN_AVATAR_URL))) val presenter = createRoomListPresenter( client = matrixClient, + rageshakeFeatureAvailability = { false }, ) moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { val initialState = awaitItem() assertThat(initialState.matrixUser).isEqualTo(MatrixUser(A_USER_ID)) + assertThat(initialState.canReportBug).isFalse() val withUserState = awaitItem() assertThat(withUserState.matrixUser.userId).isEqualTo(A_USER_ID) assertThat(withUserState.matrixUser.displayName).isEqualTo(A_USER_NAME) @@ -135,6 +138,7 @@ class RoomListPresenterTest { }.test { val initialState = awaitItem() assertThat(initialState.showAvatarIndicator).isTrue() + assertThat(initialState.canReportBug).isTrue() sessionVerificationService.emitNeedsSessionVerification(false) encryptionService.emitBackupState(BackupState.ENABLED) val finalState = awaitItem() @@ -675,6 +679,7 @@ class RoomListPresenterTest { acceptDeclineInvitePresenter: Presenter = Presenter { anAcceptDeclineInviteState() }, notificationCleaner: NotificationCleaner = FakeNotificationCleaner(), appPreferencesStore: AppPreferencesStore = InMemoryAppPreferencesStore(), + rageshakeFeatureAvailability: RageshakeFeatureAvailability = RageshakeFeatureAvailability { true }, ) = RoomListPresenter( client = client, syncService = syncService, @@ -705,6 +710,7 @@ class RoomListPresenterTest { notificationCleaner = notificationCleaner, logoutPresenter = { aDirectLogoutState() }, appPreferencesStore = appPreferencesStore, + rageshakeFeatureAvailability = rageshakeFeatureAvailability, ) } diff --git a/plugins/src/main/kotlin/ModulesConfig.kt b/plugins/src/main/kotlin/ModulesConfig.kt index 323fbcf9ab..7575728c96 100644 --- a/plugins/src/main/kotlin/ModulesConfig.kt +++ b/plugins/src/main/kotlin/ModulesConfig.kt @@ -6,6 +6,7 @@ */ import config.AnalyticsConfig +import config.BuildTimeConfig import config.PushProvidersConfig object ModulesConfig { @@ -14,8 +15,27 @@ object ModulesConfig { includeUnifiedPush = true, ) - val analyticsConfig: AnalyticsConfig = AnalyticsConfig.Enabled( - withPosthog = true, - withSentry = true, - ) + val analyticsConfig: AnalyticsConfig = if (isEnterpriseBuild) { + // Is Posthog configuration available? + val withPosthog = BuildTimeConfig.SERVICES_POSTHOG_APIKEY.isNullOrEmpty().not() && + BuildTimeConfig.SERVICES_POSTHOG_HOST.isNullOrEmpty().not() + // Is Sentry configuration available? + val withSentry = BuildTimeConfig.SERVICES_SENTRY_DSN.isNullOrEmpty().not() + if (withPosthog || withSentry) { + println("Analytics enabled with Posthog: $withPosthog, Sentry: $withSentry") + AnalyticsConfig.Enabled( + withPosthog = withPosthog, + withSentry = withSentry, + ) + } else { + println("Analytics disabled") + AnalyticsConfig.Disabled + } + } else { + println("Analytics enabled with Posthog and Sentry") + AnalyticsConfig.Enabled( + withPosthog = true, + withSentry = true, + ) + } } diff --git a/plugins/src/main/kotlin/config/BuildTimeConfig.kt b/plugins/src/main/kotlin/config/BuildTimeConfig.kt index bac0c94d0b..e1fd78c4bb 100644 --- a/plugins/src/main/kotlin/config/BuildTimeConfig.kt +++ b/plugins/src/main/kotlin/config/BuildTimeConfig.kt @@ -14,7 +14,21 @@ object BuildTimeConfig { const val GOOGLE_APP_ID_DEBUG = "1:912726360885:android:def0a4e454042e9b00427c" const val GOOGLE_APP_ID_NIGHTLY = "1:912726360885:android:e17435e0beb0303000427c" + val METADATA_HOST: String? = null + val URL_WEBSITE: String? = null + val URL_LOGO: String? = null + val URL_COPYRIGHT: String? = null + val URL_ACCEPTABLE_USE: String? = null + val URL_PRIVACY: String? = null + val URL_POLICY: String? = null + val SUPPORT_EMAIL_ADDRESS: String? = null + val SERVICES_MAPTILER_BASE_URL: String? = null val SERVICES_MAPTILER_APIKEY: String? = null val SERVICES_MAPTILER_LIGHT_MAPID: String? = null val SERVICES_MAPTILER_DARK_MAPID: String? = null + val SERVICES_POSTHOG_HOST: String? = null + val SERVICES_POSTHOG_APIKEY: String? = null + val SERVICES_SENTRY_DSN: String? = null + val BUG_REPORT_URL: String? = null + val BUG_REPORT_APP_NAME: String? = null } diff --git a/plugins/src/main/kotlin/extension/VariantDimensionExtension.kt b/plugins/src/main/kotlin/extension/VariantDimensionExtension.kt new file mode 100644 index 0000000000..a7589f02a9 --- /dev/null +++ b/plugins/src/main/kotlin/extension/VariantDimensionExtension.kt @@ -0,0 +1,32 @@ +/* + * Copyright 2025 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +package extension + +import com.android.build.api.dsl.VariantDimension + +fun VariantDimension.buildConfigFieldStr( + name: String, + value: String, +) { + buildConfigField( + type = "String", + name = name, + value = "\"$value\"" + ) +} + +fun VariantDimension.buildConfigFieldBoolean( + name: String, + value: Boolean, +) { + buildConfigField( + type = "boolean", + name = name, + value = value.toString() + ) +} diff --git a/services/analyticsproviders/posthog/build.gradle.kts b/services/analyticsproviders/posthog/build.gradle.kts index fb59fb842e..01e6dbc836 100644 --- a/services/analyticsproviders/posthog/build.gradle.kts +++ b/services/analyticsproviders/posthog/build.gradle.kts @@ -1,3 +1,5 @@ +import config.BuildTimeConfig +import extension.buildConfigFieldStr import extension.setupAnvil /* @@ -12,6 +14,21 @@ plugins { android { namespace = "io.element.android.services.analyticsproviders.posthog" + + buildFeatures { + buildConfig = true + } + + defaultConfig { + buildConfigFieldStr( + name = "POSTHOG_HOST", + value = BuildTimeConfig.SERVICES_POSTHOG_HOST.takeIf { isEnterpriseBuild } ?: "" + ) + buildConfigFieldStr( + name = "POSTHOG_APIKEY", + value = BuildTimeConfig.SERVICES_POSTHOG_APIKEY.takeIf { isEnterpriseBuild } ?: "" + ) + } } setupAnvil() @@ -21,6 +38,7 @@ dependencies { implementation(libs.posthog) { exclude("com.android.support", "support-annotations") } + implementation(projects.features.enterprise.api) implementation(projects.libraries.core) implementation(projects.libraries.di) implementation(projects.services.analyticsproviders.api) diff --git a/services/analyticsproviders/posthog/src/main/kotlin/io/element/android/services/analyticsproviders/posthog/PostHogFactory.kt b/services/analyticsproviders/posthog/src/main/kotlin/io/element/android/services/analyticsproviders/posthog/PostHogFactory.kt index 2e87236741..af450cfc84 100644 --- a/services/analyticsproviders/posthog/src/main/kotlin/io/element/android/services/analyticsproviders/posthog/PostHogFactory.kt +++ b/services/analyticsproviders/posthog/src/main/kotlin/io/element/android/services/analyticsproviders/posthog/PostHogFactory.kt @@ -11,7 +11,6 @@ import android.content.Context import com.posthog.PostHogInterface import com.posthog.android.PostHogAndroid import com.posthog.android.PostHogAndroidConfig -import io.element.android.libraries.core.extensions.isElement import io.element.android.libraries.core.meta.BuildMeta import io.element.android.libraries.di.ApplicationContext import javax.inject.Inject @@ -22,8 +21,7 @@ class PostHogFactory @Inject constructor( private val posthogEndpointConfigProvider: PosthogEndpointConfigProvider, ) { fun createPosthog(): PostHogInterface? { - if (!buildMeta.isElement()) return null - val endpoint = posthogEndpointConfigProvider.provide() + val endpoint = posthogEndpointConfigProvider.provide() ?: return null return PostHogAndroid.with( context, PostHogAndroidConfig( diff --git a/services/analyticsproviders/posthog/src/main/kotlin/io/element/android/services/analyticsproviders/posthog/PosthogEndpointConfig.kt b/services/analyticsproviders/posthog/src/main/kotlin/io/element/android/services/analyticsproviders/posthog/PosthogEndpointConfig.kt index c699950f7e..6d14ab6b14 100644 --- a/services/analyticsproviders/posthog/src/main/kotlin/io/element/android/services/analyticsproviders/posthog/PosthogEndpointConfig.kt +++ b/services/analyticsproviders/posthog/src/main/kotlin/io/element/android/services/analyticsproviders/posthog/PosthogEndpointConfig.kt @@ -10,4 +10,6 @@ package io.element.android.services.analyticsproviders.posthog data class PosthogEndpointConfig( val host: String, val apiKey: String, -) +) { + val isValid = host.isNotBlank() && apiKey.isNotBlank() +} diff --git a/services/analyticsproviders/posthog/src/main/kotlin/io/element/android/services/analyticsproviders/posthog/PosthogEndpointConfigProvider.kt b/services/analyticsproviders/posthog/src/main/kotlin/io/element/android/services/analyticsproviders/posthog/PosthogEndpointConfigProvider.kt index 2a4b80a542..da35d661e5 100644 --- a/services/analyticsproviders/posthog/src/main/kotlin/io/element/android/services/analyticsproviders/posthog/PosthogEndpointConfigProvider.kt +++ b/services/analyticsproviders/posthog/src/main/kotlin/io/element/android/services/analyticsproviders/posthog/PosthogEndpointConfigProvider.kt @@ -7,24 +7,40 @@ package io.element.android.services.analyticsproviders.posthog +import io.element.android.features.enterprise.api.EnterpriseService +import io.element.android.libraries.core.extensions.isElement import io.element.android.libraries.core.meta.BuildMeta import io.element.android.libraries.core.meta.BuildType import javax.inject.Inject class PosthogEndpointConfigProvider @Inject constructor( private val buildMeta: BuildMeta, + private val enterpriseService: EnterpriseService, ) { - fun provide(): PosthogEndpointConfig { - return when (buildMeta.buildType) { - BuildType.RELEASE -> PosthogEndpointConfig( - host = "https://posthog.element.io", - apiKey = "phc_Jzsm6DTm6V2705zeU5dcNvQDlonOR68XvX2sh1sEOHO", - ) - BuildType.NIGHTLY, - BuildType.DEBUG -> PosthogEndpointConfig( - host = "https://posthog.element.dev", - apiKey = "phc_VtA1L35nw3aeAtHIx1ayrGdzGkss7k1xINeXcoIQzXN", - ) + fun provide(): PosthogEndpointConfig? { + return if (enterpriseService.isEnterpriseBuild) { + PosthogEndpointConfig( + host = BuildConfig.POSTHOG_HOST, + apiKey = BuildConfig.POSTHOG_APIKEY, + ).takeIf { + // Note that if the config is invalid, this module will not be included in the build. + // So the configuration should be always valid. + it.isValid + } + } else if (buildMeta.isElement()) { + when (buildMeta.buildType) { + BuildType.RELEASE -> PosthogEndpointConfig( + host = "https://posthog.element.io", + apiKey = "phc_Jzsm6DTm6V2705zeU5dcNvQDlonOR68XvX2sh1sEOHO", + ) + BuildType.NIGHTLY, + BuildType.DEBUG -> PosthogEndpointConfig( + host = "https://posthog.element.dev", + apiKey = "phc_VtA1L35nw3aeAtHIx1ayrGdzGkss7k1xINeXcoIQzXN", + ) + } + } else { + null } } } diff --git a/services/analyticsproviders/sentry/build.gradle.kts b/services/analyticsproviders/sentry/build.gradle.kts index e144cb67ea..1b1c351712 100644 --- a/services/analyticsproviders/sentry/build.gradle.kts +++ b/services/analyticsproviders/sentry/build.gradle.kts @@ -1,3 +1,5 @@ +import config.BuildTimeConfig +import extension.buildConfigFieldStr import extension.readLocalProperty import extension.setupAnvil @@ -19,13 +21,15 @@ android { } defaultConfig { - buildConfigField( - type = "String", + buildConfigFieldStr( name = "SENTRY_DSN", - value = (System.getenv("ELEMENT_ANDROID_SENTRY_DSN") - ?: readLocalProperty("services.analyticsproviders.sentry.dsn") + value = if (isEnterpriseBuild) { + BuildTimeConfig.SERVICES_SENTRY_DSN + } else { + System.getenv("ELEMENT_ANDROID_SENTRY_DSN") + ?: readLocalProperty("services.analyticsproviders.sentry.dsn") + } ?: "" - ).let { "\"$it\"" } ) } } diff --git a/services/analyticsproviders/sentry/src/main/kotlin/io/element/android/services/analyticsproviders/sentry/SentryAnalyticsProvider.kt b/services/analyticsproviders/sentry/src/main/kotlin/io/element/android/services/analyticsproviders/sentry/SentryAnalyticsProvider.kt index ede40e3051..654d54f56b 100644 --- a/services/analyticsproviders/sentry/src/main/kotlin/io/element/android/services/analyticsproviders/sentry/SentryAnalyticsProvider.kt +++ b/services/analyticsproviders/sentry/src/main/kotlin/io/element/android/services/analyticsproviders/sentry/SentryAnalyticsProvider.kt @@ -41,9 +41,7 @@ class SentryAnalyticsProvider @Inject constructor( Timber.tag(analyticsTag.value).d("Initializing Sentry") if (Sentry.isEnabled()) return - val dsn = if (SentryConfig.DSN.isNotBlank()) { - SentryConfig.DSN - } else { + val dsn = SentryConfig.DSN.ifBlank { Timber.w("No Sentry DSN provided, Sentry will not be initialized") return } diff --git a/tests/uitests/src/test/snapshots/images/features.analytics.api.preferences_AnalyticsPreferencesView_Day_1_en.png b/tests/uitests/src/test/snapshots/images/features.analytics.api.preferences_AnalyticsPreferencesView_Day_1_en.png new file mode 100644 index 0000000000..76a832569b --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.analytics.api.preferences_AnalyticsPreferencesView_Day_1_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a6241486ded79ef2c079dcc45a0fe97bf01db66c05712ec7f74db035d76a91b5 +size 18115 diff --git a/tests/uitests/src/test/snapshots/images/features.analytics.api.preferences_AnalyticsPreferencesView_Night_1_en.png b/tests/uitests/src/test/snapshots/images/features.analytics.api.preferences_AnalyticsPreferencesView_Night_1_en.png new file mode 100644 index 0000000000..af54ab708a --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.analytics.api.preferences_AnalyticsPreferencesView_Night_1_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2ca4b3953ec0c460052305d9cfa5c0b1db2a97e8d9a27b483e8b5d15d873d32c +size 17500 diff --git a/tests/uitests/src/test/snapshots/images/features.analytics.impl_AnalyticsOptInView_Day_1_en.png b/tests/uitests/src/test/snapshots/images/features.analytics.impl_AnalyticsOptInView_Day_1_en.png new file mode 100644 index 0000000000..4ca3de0a14 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.analytics.impl_AnalyticsOptInView_Day_1_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:232fcbe898588450577fca29090873626c2df7cc4afba91a87d3956d29f0afbc +size 81150 diff --git a/tests/uitests/src/test/snapshots/images/features.analytics.impl_AnalyticsOptInView_Night_1_en.png b/tests/uitests/src/test/snapshots/images/features.analytics.impl_AnalyticsOptInView_Night_1_en.png new file mode 100644 index 0000000000..c3a8af5c2d --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.analytics.impl_AnalyticsOptInView_Night_1_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:26f4a151850c4c21ed57d0a1d36ea72aece585ed5ec6d54303edbeaa91d06df9 +size 73959 diff --git a/tests/uitests/src/test/snapshots/images/features.onboarding.impl_OnBoardingView_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.onboarding.impl_OnBoardingView_Day_0_en.png index 51d29f8292..679efc4aaf 100644 --- a/tests/uitests/src/test/snapshots/images/features.onboarding.impl_OnBoardingView_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.onboarding.impl_OnBoardingView_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9ce6f663ca845a54b4e1aca55f1405e00d29084a95b6a61721e5136efe30f7fc -size 311755 +oid sha256:0ab623f806fc90bef41beccc5d5f444a882e6634cdadecbbc341e10369bbcfdb +size 315380 diff --git a/tests/uitests/src/test/snapshots/images/features.onboarding.impl_OnBoardingView_Day_1_en.png b/tests/uitests/src/test/snapshots/images/features.onboarding.impl_OnBoardingView_Day_1_en.png index 52b4aa94c8..4275f3eac1 100644 --- a/tests/uitests/src/test/snapshots/images/features.onboarding.impl_OnBoardingView_Day_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.onboarding.impl_OnBoardingView_Day_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c7b714d78970f7bfc32c0bb8a226b2896681134a6eb329c871c0270511e8e81f -size 306543 +oid sha256:b73088b5af32e47d18d6961fe5622411f5342388d8a0d78c8f3fb5e27213146a +size 315116 diff --git a/tests/uitests/src/test/snapshots/images/features.onboarding.impl_OnBoardingView_Day_2_en.png b/tests/uitests/src/test/snapshots/images/features.onboarding.impl_OnBoardingView_Day_2_en.png index a303b4946f..69d4181bbe 100644 --- a/tests/uitests/src/test/snapshots/images/features.onboarding.impl_OnBoardingView_Day_2_en.png +++ b/tests/uitests/src/test/snapshots/images/features.onboarding.impl_OnBoardingView_Day_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:563c2a20cbf651c88e85dfba98c02fa60197b298ee621b5b4b1bdc4af51a456e -size 310172 +oid sha256:24735f133066c55c6d88b7f9930ad983cca431611c7e6833ca5a4d657eeb14a5 +size 313116 diff --git a/tests/uitests/src/test/snapshots/images/features.onboarding.impl_OnBoardingView_Day_3_en.png b/tests/uitests/src/test/snapshots/images/features.onboarding.impl_OnBoardingView_Day_3_en.png index 5adfb27ab3..03edadc7be 100644 --- a/tests/uitests/src/test/snapshots/images/features.onboarding.impl_OnBoardingView_Day_3_en.png +++ b/tests/uitests/src/test/snapshots/images/features.onboarding.impl_OnBoardingView_Day_3_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:171e41e96dbb6495c667a57047cf66e5059a19b8dba7d56e799113cd5a8690e4 -size 304775 +oid sha256:1d02a7ff9ff40f73d8c72b9378e10ba1f6580524cbdefcf290066b8047fdc33c +size 307633 diff --git a/tests/uitests/src/test/snapshots/images/features.onboarding.impl_OnBoardingView_Day_4_en.png b/tests/uitests/src/test/snapshots/images/features.onboarding.impl_OnBoardingView_Day_4_en.png new file mode 100644 index 0000000000..5adfb27ab3 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.onboarding.impl_OnBoardingView_Day_4_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:171e41e96dbb6495c667a57047cf66e5059a19b8dba7d56e799113cd5a8690e4 +size 304775 diff --git a/tests/uitests/src/test/snapshots/images/features.onboarding.impl_OnBoardingView_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.onboarding.impl_OnBoardingView_Night_0_en.png index 3e65f22a15..f087a418d1 100644 --- a/tests/uitests/src/test/snapshots/images/features.onboarding.impl_OnBoardingView_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.onboarding.impl_OnBoardingView_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e41a284f2eedb0c7158079f059e05f8f505436ba91a82194faaff459bb30e4b4 -size 392983 +oid sha256:f5ac9f2f168b895e8262181f789b0fe2ad2c97b1296a50c1453879438bfe436a +size 395942 diff --git a/tests/uitests/src/test/snapshots/images/features.onboarding.impl_OnBoardingView_Night_1_en.png b/tests/uitests/src/test/snapshots/images/features.onboarding.impl_OnBoardingView_Night_1_en.png index 0f4c0c9b0d..8ff0422506 100644 --- a/tests/uitests/src/test/snapshots/images/features.onboarding.impl_OnBoardingView_Night_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.onboarding.impl_OnBoardingView_Night_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:12691cbb9890570cb2081921224b13529e256feff1171d24e77cae11afc676e8 -size 380258 +oid sha256:f7c1c7d34935986d01113069c25849fc860133de582fe94ee63f2e28b181e87f +size 397378 diff --git a/tests/uitests/src/test/snapshots/images/features.onboarding.impl_OnBoardingView_Night_2_en.png b/tests/uitests/src/test/snapshots/images/features.onboarding.impl_OnBoardingView_Night_2_en.png index a167708560..1597b13f0a 100644 --- a/tests/uitests/src/test/snapshots/images/features.onboarding.impl_OnBoardingView_Night_2_en.png +++ b/tests/uitests/src/test/snapshots/images/features.onboarding.impl_OnBoardingView_Night_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4f2c6932790cd4bbdff4dd07a728290aeb2eca47811c0b3d220e17aed5e8e34e -size 383588 +oid sha256:a11f8383fc481255fb3fa6f1a5475d23c13c0460c33b04bc190cf3b5931893a3 +size 395108 diff --git a/tests/uitests/src/test/snapshots/images/features.onboarding.impl_OnBoardingView_Night_3_en.png b/tests/uitests/src/test/snapshots/images/features.onboarding.impl_OnBoardingView_Night_3_en.png index 89393180f4..fb78588841 100644 --- a/tests/uitests/src/test/snapshots/images/features.onboarding.impl_OnBoardingView_Night_3_en.png +++ b/tests/uitests/src/test/snapshots/images/features.onboarding.impl_OnBoardingView_Night_3_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ee7b3e5bb14da106ecedbe4827cded220d7222b68837c3c63ae3dfacd66ec2f2 -size 365133 +oid sha256:36424ccefbbbd35201800dbcbb743a2d22aa6fe7a606054fc3acf8b98aead272 +size 381588 diff --git a/tests/uitests/src/test/snapshots/images/features.onboarding.impl_OnBoardingView_Night_4_en.png b/tests/uitests/src/test/snapshots/images/features.onboarding.impl_OnBoardingView_Night_4_en.png new file mode 100644 index 0000000000..89393180f4 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.onboarding.impl_OnBoardingView_Night_4_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ee7b3e5bb14da106ecedbe4827cded220d7222b68837c3c63ae3dfacd66ec2f2 +size 365133