From e2febabcf631f325150c25a5aacd8213090b25f1 Mon Sep 17 00:00:00 2001 From: Jorge Martin Espinosa Date: Wed, 26 Mar 2025 09:35:21 +0100 Subject: [PATCH] Use embedded version of Element Call (#4470) * Use embedded version of Element Call: for in-app room calls, the app will use an embedded version of Element Call shipped with the app instead of using an external service. * Remove `ElementCallBaseUrlProvider` so we don't use the Element well known file to get the base URL anymore * Remove `ElementCallConfig.DEFAULT_BASE_URL` since it's not used anymore * Restore the usage of the custom EC base URL in developer settings as the actual base URL, it present * Add a way to customise the embedded EC analytic credentials * Update CI to use the EC analytic credentials as secrets * Improve the custom URL placeholder to include the `/room` suffix --- .github/workflows/build.yml | 4 + .github/workflows/build_enterprise.yml | 4 + .github/workflows/nightly.yml | 4 + .github/workflows/nightly_enterprise.yml | 4 + .github/workflows/release.yml | 4 + .../ContributesNodeProcessorProvider.kt | 2 +- .../android/appconfig/ElementCallConfig.kt | 5 - features/call/impl/build.gradle.kts | 45 +++++++++ .../DefaultCallAnalyticCredentialsProvider.kt | 23 +++++ .../impl/utils/DefaultCallWidgetProvider.kt | 15 +-- .../utils/WebViewWidgetMessageInterceptor.kt | 14 +++ .../utils/DefaultCallWidgetProviderTest.kt | 33 ------- .../utils/FakeElementCallBaseUrlProvider.kt | 20 ---- .../developer/DeveloperSettingsPresenter.kt | 5 +- .../impl/developer/DeveloperSettingsState.kt | 1 - .../DeveloperSettingsStateProvider.kt | 2 - .../impl/developer/DeveloperSettingsView.kt | 10 +- .../DeveloperSettingsPresenterTest.kt | 2 - .../developer/DeveloperSettingsViewTest.kt | 10 +- gradle/libs.versions.toml | 3 + .../api/call/ElementCallBaseUrlProvider.kt | 14 --- .../widget/CallAnalyticCredentialsProvider.kt | 16 +++ libraries/matrix/impl/build.gradle.kts | 2 - .../auth/RustMatrixAuthenticationService.kt | 6 +- .../call/DefaultElementCallBaseUrlProvider.kt | 43 -------- .../DefaultCallWidgetSettingsProvider.kt | 25 +++-- .../DefaultElementCallBaseUrlProviderTest.kt | 99 ------------------- .../analytics/api/store/AnalyticsStore.kt | 26 ----- .../analytics/impl/DefaultAnalyticsService.kt | 2 +- ...ultAnalyticsStore.kt => AnalyticsStore.kt} | 17 +++- .../impl/DefaultAnalyticsServiceTest.kt | 2 +- .../impl/store/FakeAnalyticsStore.kt | 3 +- 32 files changed, 177 insertions(+), 288 deletions(-) create mode 100644 features/call/impl/src/main/kotlin/io/element/android/features/call/impl/utils/DefaultCallAnalyticCredentialsProvider.kt delete mode 100644 features/call/impl/src/test/kotlin/io/element/android/features/call/utils/FakeElementCallBaseUrlProvider.kt delete mode 100644 libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/call/ElementCallBaseUrlProvider.kt create mode 100644 libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/widget/CallAnalyticCredentialsProvider.kt delete mode 100644 libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/call/DefaultElementCallBaseUrlProvider.kt delete mode 100644 libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/call/DefaultElementCallBaseUrlProviderTest.kt delete mode 100644 services/analytics/api/src/main/kotlin/io/element/android/services/analytics/api/store/AnalyticsStore.kt rename services/analytics/impl/src/main/kotlin/io/element/android/services/analytics/impl/store/{DefaultAnalyticsStore.kt => AnalyticsStore.kt} (85%) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index bd6e49dad8..3280551607 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -46,6 +46,10 @@ jobs: ELEMENT_ANDROID_MAPTILER_LIGHT_MAP_ID: ${{ secrets.MAPTILER_LIGHT_MAP_ID }} ELEMENT_ANDROID_MAPTILER_DARK_MAP_ID: ${{ secrets.MAPTILER_DARK_MAP_ID }} ELEMENT_ANDROID_SENTRY_DSN: ${{ secrets.ELEMENT_ANDROID_SENTRY_DSN }} + ELEMENT_CALL_SENTRY_DSN: ${{ secrets.ELEMENT_CALL_SENTRY_DSN }} + ELEMENT_CALL_POSTHOG_API_HOST: ${{ secrets.ELEMENT_CALL_POSTHOG_API_HOST }} + ELEMENT_CALL_POSTHOG_API_KEY: ${{ secrets.ELEMENT_CALL_POSTHOG_API_KEY }} + ELEMENT_CALL_RAGESHAKE_URL: ${{ secrets.ELEMENT_CALL_RAGESHAKE_URL }} run: ./gradlew :app:assembleGplayDebug app:assembleFDroidDebug -PallWarningsAsErrors=true $CI_GRADLE_ARG_PROPERTIES - name: Upload debug APKs if: ${{ matrix.variant == 'debug' }} diff --git a/.github/workflows/build_enterprise.yml b/.github/workflows/build_enterprise.yml index 148eb6a670..73c7b6d893 100644 --- a/.github/workflows/build_enterprise.yml +++ b/.github/workflows/build_enterprise.yml @@ -54,6 +54,10 @@ jobs: ELEMENT_ANDROID_MAPTILER_LIGHT_MAP_ID: ${{ secrets.MAPTILER_LIGHT_MAP_ID }} ELEMENT_ANDROID_MAPTILER_DARK_MAP_ID: ${{ secrets.MAPTILER_DARK_MAP_ID }} ELEMENT_ANDROID_SENTRY_DSN: ${{ secrets.ELEMENT_ANDROID_SENTRY_DSN }} + ELEMENT_CALL_SENTRY_DSN: ${{ secrets.ELEMENT_CALL_SENTRY_DSN }} + ELEMENT_CALL_POSTHOG_API_HOST: ${{ secrets.ELEMENT_CALL_POSTHOG_API_HOST }} + ELEMENT_CALL_POSTHOG_API_KEY: ${{ secrets.ELEMENT_CALL_POSTHOG_API_KEY }} + ELEMENT_CALL_RAGESHAKE_URL: ${{ secrets.ELEMENT_CALL_RAGESHAKE_URL }} run: ./gradlew :app:assembleGplayDebug -PallWarningsAsErrors=true $CI_GRADLE_ARG_PROPERTIES - name: Upload debug Enterprise APKs if: ${{ matrix.variant == 'debug' }} diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml index 465a95bd08..d82129e18d 100644 --- a/.github/workflows/nightly.yml +++ b/.github/workflows/nightly.yml @@ -30,6 +30,10 @@ jobs: ELEMENT_ANDROID_MAPTILER_LIGHT_MAP_ID: ${{ secrets.MAPTILER_LIGHT_MAP_ID }} ELEMENT_ANDROID_MAPTILER_DARK_MAP_ID: ${{ secrets.MAPTILER_DARK_MAP_ID }} ELEMENT_ANDROID_SENTRY_DSN: ${{ secrets.ELEMENT_ANDROID_SENTRY_DSN }} + ELEMENT_CALL_SENTRY_DSN: ${{ secrets.ELEMENT_CALL_SENTRY_DSN }} + ELEMENT_CALL_POSTHOG_API_HOST: ${{ secrets.ELEMENT_CALL_POSTHOG_API_HOST }} + ELEMENT_CALL_POSTHOG_API_KEY: ${{ secrets.ELEMENT_CALL_POSTHOG_API_KEY }} + ELEMENT_CALL_RAGESHAKE_URL: ${{ secrets.ELEMENT_CALL_RAGESHAKE_URL }} ELEMENT_ANDROID_NIGHTLY_KEYID: ${{ secrets.ELEMENT_ANDROID_NIGHTLY_KEYID }} ELEMENT_ANDROID_NIGHTLY_KEYPASSWORD: ${{ secrets.ELEMENT_ANDROID_NIGHTLY_KEYPASSWORD }} ELEMENT_ANDROID_NIGHTLY_STOREPASSWORD: ${{ secrets.ELEMENT_ANDROID_NIGHTLY_STOREPASSWORD }} diff --git a/.github/workflows/nightly_enterprise.yml b/.github/workflows/nightly_enterprise.yml index 797879c8c1..5e02c68240 100644 --- a/.github/workflows/nightly_enterprise.yml +++ b/.github/workflows/nightly_enterprise.yml @@ -36,6 +36,10 @@ jobs: ELEMENT_ANDROID_MAPTILER_LIGHT_MAP_ID: ${{ secrets.MAPTILER_LIGHT_MAP_ID }} ELEMENT_ANDROID_MAPTILER_DARK_MAP_ID: ${{ secrets.MAPTILER_DARK_MAP_ID }} ELEMENT_ANDROID_SENTRY_DSN: ${{ secrets.ELEMENT_ANDROID_SENTRY_DSN }} + ELEMENT_CALL_SENTRY_DSN: ${{ secrets.ELEMENT_CALL_SENTRY_DSN }} + ELEMENT_CALL_POSTHOG_API_HOST: ${{ secrets.ELEMENT_CALL_POSTHOG_API_HOST }} + ELEMENT_CALL_POSTHOG_API_KEY: ${{ secrets.ELEMENT_CALL_POSTHOG_API_KEY }} + ELEMENT_CALL_RAGESHAKE_URL: ${{ secrets.ELEMENT_CALL_RAGESHAKE_URL }} ELEMENT_ANDROID_NIGHTLY_KEYID: ${{ secrets.ELEMENT_ANDROID_NIGHTLY_KEYID }} ELEMENT_ANDROID_NIGHTLY_KEYPASSWORD: ${{ secrets.ELEMENT_ANDROID_NIGHTLY_KEYPASSWORD }} ELEMENT_ANDROID_NIGHTLY_STOREPASSWORD: ${{ secrets.ELEMENT_ANDROID_NIGHTLY_STOREPASSWORD }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index c9656970db..f04b203611 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -32,6 +32,10 @@ jobs: ELEMENT_ANDROID_MAPTILER_LIGHT_MAP_ID: ${{ secrets.MAPTILER_LIGHT_MAP_ID }} ELEMENT_ANDROID_MAPTILER_DARK_MAP_ID: ${{ secrets.MAPTILER_DARK_MAP_ID }} ELEMENT_ANDROID_SENTRY_DSN: ${{ secrets.ELEMENT_ANDROID_SENTRY_DSN }} + ELEMENT_CALL_SENTRY_DSN: ${{ secrets.ELEMENT_CALL_SENTRY_DSN }} + ELEMENT_CALL_POSTHOG_API_HOST: ${{ secrets.ELEMENT_CALL_POSTHOG_API_HOST }} + ELEMENT_CALL_POSTHOG_API_KEY: ${{ secrets.ELEMENT_CALL_POSTHOG_API_KEY }} + ELEMENT_CALL_RAGESHAKE_URL: ${{ secrets.ELEMENT_CALL_RAGESHAKE_URL }} run: ./gradlew bundleGplayRelease $CI_GRADLE_ARG_PROPERTIES - name: Upload bundle as artifact uses: actions/upload-artifact@v4 diff --git a/anvilcodegen/src/main/kotlin/io/element/android/anvilcodegen/ContributesNodeProcessorProvider.kt b/anvilcodegen/src/main/kotlin/io/element/android/anvilcodegen/ContributesNodeProcessorProvider.kt index 80c552428f..ec6ad5958e 100644 --- a/anvilcodegen/src/main/kotlin/io/element/android/anvilcodegen/ContributesNodeProcessorProvider.kt +++ b/anvilcodegen/src/main/kotlin/io/element/android/anvilcodegen/ContributesNodeProcessorProvider.kt @@ -13,7 +13,7 @@ import com.google.devtools.ksp.processing.SymbolProcessorProvider class ContributesNodeProcessorProvider : SymbolProcessorProvider { override fun create(environment: SymbolProcessorEnvironment): SymbolProcessor { - val enableLogging = environment.options["enableLogging"]?.toBoolean() ?: false + val enableLogging = environment.options["enableLogging"]?.toBoolean() == true return ContributesNodeProcessor( logger = environment.logger, codeGenerator = environment.codeGenerator, diff --git a/appconfig/src/main/kotlin/io/element/android/appconfig/ElementCallConfig.kt b/appconfig/src/main/kotlin/io/element/android/appconfig/ElementCallConfig.kt index c9b7557c87..ca9a6391f2 100644 --- a/appconfig/src/main/kotlin/io/element/android/appconfig/ElementCallConfig.kt +++ b/appconfig/src/main/kotlin/io/element/android/appconfig/ElementCallConfig.kt @@ -8,11 +8,6 @@ package io.element.android.appconfig object ElementCallConfig { - /** - * The default base URL for the Element Call service. - */ - const val DEFAULT_BASE_URL = "https://call.element.io" - /** * The default duration of a ringing call in seconds before it's automatically dismissed. */ diff --git a/features/call/impl/build.gradle.kts b/features/call/impl/build.gradle.kts index 7a291a7423..be332ec4fe 100644 --- a/features/call/impl/build.gradle.kts +++ b/features/call/impl/build.gradle.kts @@ -1,3 +1,4 @@ +import extension.readLocalProperty import extension.setupAnvil /* @@ -23,6 +24,49 @@ android { testOptions { unitTests.isIncludeAndroidResources = true } + + defaultConfig { + buildConfigField( + type = "String", + name = "SENTRY_DSN", + value = (System.getenv("ELEMENT_CALL_SENTRY_DSN") + ?: readLocalProperty("features.call.sentry.dsn") + ?: "" + ).let { "\"$it\"" } + ) + buildConfigField( + type = "String", + name = "POSTHOG_USER_ID", + value = (System.getenv("ELEMENT_CALL_POSTHOG_USER_ID") + ?: readLocalProperty("features.call.posthog.userid") + ?: "" + ).let { "\"$it\"" } + ) + buildConfigField( + type = "String", + name = "POSTHOG_API_HOST", + value = (System.getenv("ELEMENT_CALL_POSTHOG_API_HOST") + ?: readLocalProperty("features.call.posthog.api.host") + ?: "" + ).let { "\"$it\"" } + ) + buildConfigField( + type = "String", + name = "POSTHOG_API_KEY", + value = (System.getenv("ELEMENT_CALL_POSTHOG_API_KEY") + ?: readLocalProperty("features.call.posthog.api.key") + ?: "" + ).let { "\"$it\"" } + ) + buildConfigField( + type = "String", + name = "RAGESHAKE_URL", + value = (System.getenv("ELEMENT_CALL_RAGESHAKE_URL") + ?: readLocalProperty("features.call.regeshake.url") + ?: "" + ).let { "\"$it\"" } + ) + } } setupAnvil() @@ -47,6 +91,7 @@ dependencies { implementation(libs.coil.compose) implementation(libs.network.retrofit) implementation(libs.serialization.json) + implementation(libs.element.call.embedded) api(projects.features.call.api) testImplementation(libs.coroutines.test) diff --git a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/utils/DefaultCallAnalyticCredentialsProvider.kt b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/utils/DefaultCallAnalyticCredentialsProvider.kt new file mode 100644 index 0000000000..c978aba7e8 --- /dev/null +++ b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/utils/DefaultCallAnalyticCredentialsProvider.kt @@ -0,0 +1,23 @@ +/* + * 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.call.impl.utils + +import com.squareup.anvil.annotations.ContributesBinding +import io.element.android.features.call.impl.BuildConfig +import io.element.android.libraries.di.AppScope +import io.element.android.libraries.matrix.api.widget.CallAnalyticCredentialsProvider +import javax.inject.Inject + +@ContributesBinding(AppScope::class) +class DefaultCallAnalyticCredentialsProvider @Inject constructor() : CallAnalyticCredentialsProvider { + override val posthogUserId: String? = BuildConfig.POSTHOG_USER_ID.takeIf { it.isNotBlank() } + override val posthogApiHost: String? = BuildConfig.POSTHOG_API_HOST.takeIf { it.isNotBlank() } + override val posthogApiKey: String? = BuildConfig.POSTHOG_API_KEY.takeIf { it.isNotBlank() } + override val rageshakeSubmitUrl: String? = BuildConfig.RAGESHAKE_URL.takeIf { it.isNotBlank() } + override val sentryDsn: String? = BuildConfig.SENTRY_DSN.takeIf { it.isNotBlank() } +} diff --git a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/utils/DefaultCallWidgetProvider.kt b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/utils/DefaultCallWidgetProvider.kt index 8d34cfcfd4..a4c05e8e5c 100644 --- a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/utils/DefaultCallWidgetProvider.kt +++ b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/utils/DefaultCallWidgetProvider.kt @@ -8,10 +8,8 @@ package io.element.android.features.call.impl.utils import com.squareup.anvil.annotations.ContributesBinding -import io.element.android.appconfig.ElementCallConfig import io.element.android.libraries.di.AppScope import io.element.android.libraries.matrix.api.MatrixClientProvider -import io.element.android.libraries.matrix.api.call.ElementCallBaseUrlProvider import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.SessionId import io.element.android.libraries.matrix.api.widget.CallWidgetSettingsProvider @@ -19,12 +17,13 @@ import io.element.android.libraries.preferences.api.store.AppPreferencesStore import kotlinx.coroutines.flow.firstOrNull import javax.inject.Inject +private const val EMBEDDED_CALL_WIDGET_BASE_URL = "https://appassets.androidplatform.net/element-call/index.html" + @ContributesBinding(AppScope::class) class DefaultCallWidgetProvider @Inject constructor( private val matrixClientsProvider: MatrixClientProvider, private val appPreferencesStore: AppPreferencesStore, private val callWidgetSettingsProvider: CallWidgetSettingsProvider, - private val elementCallBaseUrlProvider: ElementCallBaseUrlProvider, ) : CallWidgetProvider { override suspend fun getWidget( sessionId: SessionId, @@ -35,9 +34,10 @@ class DefaultCallWidgetProvider @Inject constructor( ): Result = runCatching { val matrixClient = matrixClientsProvider.getOrRestore(sessionId).getOrThrow() val room = matrixClient.getRoom(roomId) ?: error("Room not found") - val baseUrl = appPreferencesStore.getCustomElementCallBaseUrlFlow().firstOrNull() - ?: elementCallBaseUrlProvider.provides(matrixClient) - ?: ElementCallConfig.DEFAULT_BASE_URL + + val customBaseUrl = appPreferencesStore.getCustomElementCallBaseUrlFlow().firstOrNull() + val baseUrl = customBaseUrl ?: EMBEDDED_CALL_WIDGET_BASE_URL + val isEncrypted = room.info().isEncrypted ?: room.getUpdatedIsEncrypted().getOrThrow() val widgetSettings = callWidgetSettingsProvider.provide(baseUrl, encrypted = isEncrypted) val callUrl = room.generateWidgetWebViewUrl( @@ -46,9 +46,10 @@ class DefaultCallWidgetProvider @Inject constructor( languageTag = languageTag, theme = theme, ).getOrThrow() + CallWidgetProvider.GetWidgetResult( driver = room.getWidgetDriver(widgetSettings).getOrThrow(), - url = callUrl + url = callUrl, ) } } diff --git a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/utils/WebViewWidgetMessageInterceptor.kt b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/utils/WebViewWidgetMessageInterceptor.kt index c1013afa6e..8fc5d8b7d6 100644 --- a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/utils/WebViewWidgetMessageInterceptor.kt +++ b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/utils/WebViewWidgetMessageInterceptor.kt @@ -16,6 +16,8 @@ import android.webkit.WebResourceRequest import android.webkit.WebResourceResponse import android.webkit.WebView import android.webkit.WebViewClient +import androidx.core.net.toUri +import androidx.webkit.WebViewAssetLoader import androidx.webkit.WebViewCompat import androidx.webkit.WebViewFeature import io.element.android.features.call.impl.BuildConfig @@ -37,6 +39,10 @@ class WebViewWidgetMessageInterceptor( override val interceptedMessages = MutableSharedFlow(extraBufferCapacity = 10) init { + val assetLoader = WebViewAssetLoader.Builder() + .addPathHandler("/", WebViewAssetLoader.AssetsPathHandler(webView.context)) + .build() + webView.webViewClient = object : WebViewClient() { override fun onPageStarted(view: WebView?, url: String?, favicon: Bitmap?) { super.onPageStarted(view, url, favicon) @@ -117,6 +123,14 @@ class WebViewWidgetMessageInterceptor( super.onReceivedSslError(view, handler, error) } + + override fun shouldInterceptRequest(view: WebView?, request: WebResourceRequest): WebResourceResponse? { + return assetLoader.shouldInterceptRequest(request.url) + } + + override fun shouldInterceptRequest(view: WebView?, url: String): WebResourceResponse? { + return assetLoader.shouldInterceptRequest(url.toUri()) + } } // Create a WebMessageListener, which will receive messages from the WebView and reply to them diff --git a/features/call/impl/src/test/kotlin/io/element/android/features/call/utils/DefaultCallWidgetProviderTest.kt b/features/call/impl/src/test/kotlin/io/element/android/features/call/utils/DefaultCallWidgetProviderTest.kt index 67f59713ee..66a85d1c36 100644 --- a/features/call/impl/src/test/kotlin/io/element/android/features/call/utils/DefaultCallWidgetProviderTest.kt +++ b/features/call/impl/src/test/kotlin/io/element/android/features/call/utils/DefaultCallWidgetProviderTest.kt @@ -9,9 +9,7 @@ package io.element.android.features.call.utils import com.google.common.truth.Truth.assertThat import io.element.android.features.call.impl.utils.DefaultCallWidgetProvider -import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.MatrixClientProvider -import io.element.android.libraries.matrix.api.call.ElementCallBaseUrlProvider import io.element.android.libraries.matrix.api.widget.CallWidgetSettingsProvider import io.element.android.libraries.matrix.test.A_ROOM_ID import io.element.android.libraries.matrix.test.A_SESSION_ID @@ -22,8 +20,6 @@ import io.element.android.libraries.matrix.test.widget.FakeCallWidgetSettingsPro import io.element.android.libraries.matrix.test.widget.FakeMatrixWidgetDriver import io.element.android.libraries.preferences.api.store.AppPreferencesStore import io.element.android.libraries.preferences.test.InMemoryAppPreferencesStore -import io.element.android.tests.testutils.lambda.lambdaRecorder -import io.element.android.tests.testutils.lambda.value import kotlinx.coroutines.test.runTest import org.junit.Test @@ -104,42 +100,13 @@ class DefaultCallWidgetProviderTest { assertThat(settingsProvider.providedBaseUrls).containsExactly("https://custom.element.io") } - @Test - fun `getWidget - will use a wellknown base url if it exists`() = runTest { - val aCustomUrl = "https://custom.element.io" - val providesLambda = lambdaRecorder { _ -> aCustomUrl } - val elementCallBaseUrlProvider = FakeElementCallBaseUrlProvider { matrixClient -> - providesLambda(matrixClient) - } - val room = FakeMatrixRoom( - generateWidgetWebViewUrlResult = { _, _, _, _ -> Result.success("url") }, - getWidgetDriverResult = { Result.success(FakeMatrixWidgetDriver()) }, - ) - val client = FakeMatrixClient().apply { - givenGetRoomResult(A_ROOM_ID, room) - } - val settingsProvider = FakeCallWidgetSettingsProvider() - val provider = createProvider( - matrixClientProvider = FakeMatrixClientProvider { Result.success(client) }, - callWidgetSettingsProvider = settingsProvider, - elementCallBaseUrlProvider = elementCallBaseUrlProvider, - ) - provider.getWidget(A_SESSION_ID, A_ROOM_ID, "clientId", "languageTag", "theme") - assertThat(settingsProvider.providedBaseUrls).containsExactly(aCustomUrl) - providesLambda.assertions() - .isCalledOnce() - .with(value(client)) - } - private fun createProvider( matrixClientProvider: MatrixClientProvider = FakeMatrixClientProvider(), appPreferencesStore: AppPreferencesStore = InMemoryAppPreferencesStore(), callWidgetSettingsProvider: CallWidgetSettingsProvider = FakeCallWidgetSettingsProvider(), - elementCallBaseUrlProvider: ElementCallBaseUrlProvider = FakeElementCallBaseUrlProvider { _ -> null }, ) = DefaultCallWidgetProvider( matrixClientsProvider = matrixClientProvider, appPreferencesStore = appPreferencesStore, callWidgetSettingsProvider = callWidgetSettingsProvider, - elementCallBaseUrlProvider = elementCallBaseUrlProvider, ) } diff --git a/features/call/impl/src/test/kotlin/io/element/android/features/call/utils/FakeElementCallBaseUrlProvider.kt b/features/call/impl/src/test/kotlin/io/element/android/features/call/utils/FakeElementCallBaseUrlProvider.kt deleted file mode 100644 index 4ac716f22d..0000000000 --- a/features/call/impl/src/test/kotlin/io/element/android/features/call/utils/FakeElementCallBaseUrlProvider.kt +++ /dev/null @@ -1,20 +0,0 @@ -/* - * Copyright 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.call.utils - -import io.element.android.libraries.matrix.api.MatrixClient -import io.element.android.libraries.matrix.api.call.ElementCallBaseUrlProvider -import io.element.android.tests.testutils.lambda.lambdaError - -class FakeElementCallBaseUrlProvider( - private val providesLambda: (MatrixClient) -> String? = { lambdaError() } -) : ElementCallBaseUrlProvider { - override suspend fun provides(matrixClient: MatrixClient): String? { - return providesLambda(matrixClient) - } -} diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsPresenter.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsPresenter.kt index ced2c87a9d..ee57d3b8e7 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsPresenter.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsPresenter.kt @@ -18,7 +18,6 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.snapshots.SnapshotStateMap -import io.element.android.appconfig.ElementCallConfig import io.element.android.features.preferences.impl.developer.tracing.toLogLevel import io.element.android.features.preferences.impl.developer.tracing.toLogLevelItem import io.element.android.features.preferences.impl.tasks.ClearCacheUseCase @@ -112,8 +111,7 @@ class DeveloperSettingsPresenter @Inject constructor( triggerClearCache = { handleEvents(DeveloperSettingsEvents.ClearCache) } ) is DeveloperSettingsEvents.SetCustomElementCallBaseUrl -> coroutineScope.launch { - // If the URL is either empty or the default one, we want to save 'null' to remove the custom URL - val urlToSave = event.baseUrl.takeIf { !it.isNullOrEmpty() && it != ElementCallConfig.DEFAULT_BASE_URL } + val urlToSave = event.baseUrl.takeIf { !it.isNullOrEmpty() } appPreferencesStore.setCustomElementCallBaseUrl(urlToSave) } DeveloperSettingsEvents.ClearCache -> coroutineScope.clearCache(clearCacheAction) @@ -133,7 +131,6 @@ class DeveloperSettingsPresenter @Inject constructor( rageshakeState = rageshakeState, customElementCallBaseUrlState = CustomElementCallBaseUrlState( baseUrl = customElementCallBaseUrl, - defaultUrl = ElementCallConfig.DEFAULT_BASE_URL, validator = ::customElementCallUrlValidator, ), hideImagesAndVideos = hideImagesAndVideos, diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsState.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsState.kt index 34574cd5c5..6bc8743439 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsState.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsState.kt @@ -27,6 +27,5 @@ data class DeveloperSettingsState( data class CustomElementCallBaseUrlState( val baseUrl: String?, - val defaultUrl: String, val validator: (String?) -> Boolean, ) diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsStateProvider.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsStateProvider.kt index 83a029f10f..064a208f49 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsStateProvider.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsStateProvider.kt @@ -47,10 +47,8 @@ fun aDeveloperSettingsState( fun aCustomElementCallBaseUrlState( baseUrl: String? = null, - defaultUrl: String = "https://call.element.io", validator: (String?) -> Boolean = { true }, ) = CustomElementCallBaseUrlState( baseUrl = baseUrl, - defaultUrl = defaultUrl, validator = validator, ) diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsView.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsView.kt index a632cd4ebc..de368b9a65 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsView.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsView.kt @@ -136,22 +136,20 @@ private fun ElementCallCategory( ) { PreferenceCategory(title = "Element Call", showTopDivider = true) { val callUrlState = state.customElementCallBaseUrlState - fun isUsingDefaultUrl(value: String?): Boolean { - return value.isNullOrEmpty() || value == callUrlState.defaultUrl - } - val supportingText = if (isUsingDefaultUrl(callUrlState.baseUrl)) { + val supportingText = if (callUrlState.baseUrl.isNullOrEmpty()) { stringResource(R.string.screen_advanced_settings_element_call_base_url_description) } else { callUrlState.baseUrl } PreferenceTextField( headline = stringResource(R.string.screen_advanced_settings_element_call_base_url), - value = callUrlState.baseUrl ?: callUrlState.defaultUrl, + value = callUrlState.baseUrl, + placeholder = "https://.../room", supportingText = supportingText, validation = callUrlState.validator, onValidationErrorMessage = stringResource(R.string.screen_advanced_settings_element_call_base_url_validation_error), - displayValue = { value -> !isUsingDefaultUrl(value) }, + displayValue = { value -> !value.isNullOrEmpty() }, keyboardOptions = KeyboardOptions.Default.copy(autoCorrectEnabled = false, keyboardType = KeyboardType.Uri), onChange = { state.eventSink(DeveloperSettingsEvents.SetCustomElementCallBaseUrl(it)) } ) diff --git a/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsPresenterTest.kt b/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsPresenterTest.kt index f030e5b282..a940e2a3a2 100644 --- a/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsPresenterTest.kt +++ b/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsPresenterTest.kt @@ -10,7 +10,6 @@ package io.element.android.features.preferences.impl.developer import com.google.common.truth.Truth.assertThat -import io.element.android.appconfig.ElementCallConfig import io.element.android.features.preferences.impl.developer.tracing.LogLevelItem import io.element.android.features.preferences.impl.tasks.FakeClearCacheUseCase import io.element.android.features.preferences.impl.tasks.FakeComputeCacheSizeUseCase @@ -130,7 +129,6 @@ class DeveloperSettingsPresenterTest { } awaitItem().also { state -> assertThat(state.customElementCallBaseUrlState.baseUrl).isEqualTo("https://call.element.ahoy") - assertThat(state.customElementCallBaseUrlState.defaultUrl).isEqualTo(ElementCallConfig.DEFAULT_BASE_URL) } } } diff --git a/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsViewTest.kt b/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsViewTest.kt index 4c8631862d..94b70442d9 100644 --- a/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsViewTest.kt +++ b/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsViewTest.kt @@ -8,10 +8,16 @@ package io.element.android.features.preferences.impl.developer import androidx.activity.ComponentActivity +import androidx.compose.ui.test.filterToOne +import androidx.compose.ui.test.hasAnyAncestor +import androidx.compose.ui.test.isDialog +import androidx.compose.ui.test.isEditable +import androidx.compose.ui.test.isFocusable import androidx.compose.ui.test.junit4.AndroidComposeTestRule import androidx.compose.ui.test.junit4.createAndroidComposeRule import androidx.compose.ui.test.onNodeWithText import androidx.compose.ui.test.performClick +import androidx.compose.ui.test.performTextInput import androidx.test.ext.junit.runners.AndroidJUnit4 import io.element.android.features.preferences.impl.R import io.element.android.features.preferences.impl.developer.tracing.LogLevelItem @@ -56,8 +62,10 @@ class DeveloperSettingsViewTest { ), ) rule.clickOn(R.string.screen_advanced_settings_element_call_base_url) + val textInputNode = rule.onAllNodes(isEditable().and(isFocusable())).filterToOne(hasAnyAncestor(isDialog())) + textInputNode.performTextInput("https://call.element.dev") rule.clickOn(CommonStrings.action_ok) - eventsRecorder.assertSingle(DeveloperSettingsEvents.SetCustomElementCallBaseUrl("https://call.element.io")) + eventsRecorder.assertSingle(DeveloperSettingsEvents.SetCustomElementCallBaseUrl("https://call.element.dev")) } @Config(qualifiers = "h1024dp") diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index aa73c2000d..ec5a52e42a 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -211,6 +211,9 @@ dagger_compiler = { module = "com.google.dagger:dagger-compiler", version.ref = anvil_compiler_api = { module = "dev.zacsweers.anvil:compiler-api", version.ref = "anvil" } anvil_compiler_utils = { module = "dev.zacsweers.anvil:compiler-utils", version.ref = "anvil" } +# Element Call +element_call_embedded = "io.element.android:element-call-embedded:0.9.0-rc.2" + # Auto services google_autoservice = { module = "com.google.auto.service:auto-service", version.ref = "autoservice" } google_autoservice_annotations = { module = "com.google.auto.service:auto-service-annotations", version.ref = "autoservice" } diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/call/ElementCallBaseUrlProvider.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/call/ElementCallBaseUrlProvider.kt deleted file mode 100644 index 26422b65bd..0000000000 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/call/ElementCallBaseUrlProvider.kt +++ /dev/null @@ -1,14 +0,0 @@ -/* - * Copyright 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.libraries.matrix.api.call - -import io.element.android.libraries.matrix.api.MatrixClient - -interface ElementCallBaseUrlProvider { - suspend fun provides(matrixClient: MatrixClient): String? -} diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/widget/CallAnalyticCredentialsProvider.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/widget/CallAnalyticCredentialsProvider.kt new file mode 100644 index 0000000000..ee57c6edbc --- /dev/null +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/widget/CallAnalyticCredentialsProvider.kt @@ -0,0 +1,16 @@ +/* + * 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.libraries.matrix.api.widget + +interface CallAnalyticCredentialsProvider { + val posthogUserId: String? + val posthogApiHost: String? + val posthogApiKey: String? + val rageshakeSubmitUrl: String? + val sentryDsn: String? +} diff --git a/libraries/matrix/impl/build.gradle.kts b/libraries/matrix/impl/build.gradle.kts index e0f2d6394b..790d23ec07 100644 --- a/libraries/matrix/impl/build.gradle.kts +++ b/libraries/matrix/impl/build.gradle.kts @@ -33,8 +33,6 @@ dependencies { implementation(projects.libraries.network) implementation(projects.libraries.preferences.api) implementation(projects.services.analytics.api) - implementation(projects.services.analyticsproviders.posthog) - implementation(projects.services.analyticsproviders.sentry) implementation(projects.services.toolbox.api) api(projects.libraries.matrix.api) implementation(libs.dagger) diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/auth/RustMatrixAuthenticationService.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/auth/RustMatrixAuthenticationService.kt index 94f77b2dd2..2549fc622a 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/auth/RustMatrixAuthenticationService.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/auth/RustMatrixAuthenticationService.kt @@ -189,12 +189,12 @@ class RustMatrixAuthenticationService @Inject constructor( return withContext(coroutineDispatchers.io) { runCatching { val client = currentClient ?: error("You need to call `setHomeserver()` first") - val oAuthAuthenticationData = client.urlForOidc( + val oAuthAuthorizationData = client.urlForOidc( oidcConfiguration = oidcConfigurationProvider.get(), prompt = prompt.toRustPrompt(), ) - val url = oAuthAuthenticationData.loginUrl() - pendingOAuthAuthorizationData = oAuthAuthenticationData + val url = oAuthAuthorizationData.loginUrl() + pendingOAuthAuthorizationData = oAuthAuthorizationData OidcDetails(url) }.mapFailure { failure -> failure.mapAuthenticationException() diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/call/DefaultElementCallBaseUrlProvider.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/call/DefaultElementCallBaseUrlProvider.kt deleted file mode 100644 index 6cf607a373..0000000000 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/call/DefaultElementCallBaseUrlProvider.kt +++ /dev/null @@ -1,43 +0,0 @@ -/* - * Copyright 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.libraries.matrix.impl.call - -import com.squareup.anvil.annotations.ContributesBinding -import io.element.android.libraries.di.AppScope -import io.element.android.libraries.matrix.api.MatrixClient -import io.element.android.libraries.matrix.api.call.ElementCallBaseUrlProvider -import timber.log.Timber -import javax.inject.Inject - -@ContributesBinding(AppScope::class) -class DefaultElementCallBaseUrlProvider @Inject constructor( - private val elementWellKnownParser: ElementWellKnownParser, -) : ElementCallBaseUrlProvider { - override suspend fun provides(matrixClient: MatrixClient): String? { - val url = buildString { - append("https://") - append(matrixClient.userIdServerName()) - append("/.well-known/element/element.json") - } - return matrixClient.getUrl(url) - .onFailure { failure -> - Timber.w(failure, "Failed to fetch well-known element.json") - } - .getOrNull() - ?.let { wellKnownStr -> - elementWellKnownParser.parse(wellKnownStr) - .onFailure { failure -> - // Can be a HTML 404. - Timber.w(failure, "Failed to parse content") - } - .getOrNull() - } - ?.call - ?.widgetUrl - } -} diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/widget/DefaultCallWidgetSettingsProvider.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/widget/DefaultCallWidgetSettingsProvider.kt index 437fa4900c..b761dd7e3b 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/widget/DefaultCallWidgetSettingsProvider.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/widget/DefaultCallWidgetSettingsProvider.kt @@ -8,15 +8,13 @@ package io.element.android.libraries.matrix.impl.widget import com.squareup.anvil.annotations.ContributesBinding -import io.element.android.appconfig.RageshakeConfig import io.element.android.libraries.core.meta.BuildMeta import io.element.android.libraries.core.meta.BuildType import io.element.android.libraries.di.AppScope +import io.element.android.libraries.matrix.api.widget.CallAnalyticCredentialsProvider import io.element.android.libraries.matrix.api.widget.CallWidgetSettingsProvider import io.element.android.libraries.matrix.api.widget.MatrixWidgetSettings -import io.element.android.services.analytics.api.store.AnalyticsStore -import io.element.android.services.analyticsproviders.posthog.PosthogEndpointConfigProvider -import io.element.android.services.analyticsproviders.sentry.SentryConfig +import io.element.android.services.analytics.api.AnalyticsService import kotlinx.coroutines.flow.first import org.matrix.rustcomponents.sdk.EncryptionSystem import org.matrix.rustcomponents.sdk.VirtualElementCallWidgetOptions @@ -27,12 +25,11 @@ import org.matrix.rustcomponents.sdk.Intent as CallIntent @ContributesBinding(AppScope::class) class DefaultCallWidgetSettingsProvider @Inject constructor( private val buildMeta: BuildMeta, - private val posthogEndpointConfigProvider: PosthogEndpointConfigProvider, - private val analyticsStore: AnalyticsStore, + private val callAnalyticsCredentialsProvider: CallAnalyticCredentialsProvider, + private val analyticsService: AnalyticsService, ) : CallWidgetSettingsProvider { override suspend fun provide(baseUrl: String, widgetId: String, encrypted: Boolean): MatrixWidgetSettings { - val analyticsEnabled = analyticsStore.userConsentFlow.first() - val posthogEndpointConfig = posthogEndpointConfigProvider.provide() + val isAnalyticsEnabled = analyticsService.getUserConsent().first() val options = VirtualElementCallWidgetOptions( elementCallUrl = baseUrl, widgetId = widgetId, @@ -44,12 +41,12 @@ class DefaultCallWidgetSettingsProvider @Inject constructor( encryption = if (encrypted) EncryptionSystem.PerParticipantKeys else EncryptionSystem.Unencrypted, intent = CallIntent.START_CALL, hideScreensharing = false, - posthogUserId = null, - posthogApiHost = posthogEndpointConfig.host.takeIf { analyticsEnabled }, - posthogApiKey = posthogEndpointConfig.apiKey.takeIf { analyticsEnabled }, - rageshakeSubmitUrl = RageshakeConfig.BUG_REPORT_URL, - sentryDsn = SentryConfig.DSN.takeIf { analyticsEnabled }, - sentryEnvironment = if (buildMeta.buildType == BuildType.RELEASE) SentryConfig.ENV_RELEASE else SentryConfig.ENV_DEBUG, + posthogUserId = callAnalyticsCredentialsProvider.posthogUserId.takeIf { isAnalyticsEnabled }, + posthogApiHost = callAnalyticsCredentialsProvider.posthogApiHost.takeIf { isAnalyticsEnabled }, + posthogApiKey = callAnalyticsCredentialsProvider.posthogApiKey.takeIf { isAnalyticsEnabled }, + rageshakeSubmitUrl = callAnalyticsCredentialsProvider.rageshakeSubmitUrl, + sentryDsn = callAnalyticsCredentialsProvider.sentryDsn.takeIf { isAnalyticsEnabled }, + sentryEnvironment = if (buildMeta.buildType == BuildType.RELEASE) "RELEASE" else "DEBUG", parentUrl = null, hideHeader = true, ) diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/call/DefaultElementCallBaseUrlProviderTest.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/call/DefaultElementCallBaseUrlProviderTest.kt deleted file mode 100644 index 7618a641b2..0000000000 --- a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/call/DefaultElementCallBaseUrlProviderTest.kt +++ /dev/null @@ -1,99 +0,0 @@ -/* - * Copyright 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.libraries.matrix.impl.call - -import com.google.common.truth.Truth.assertThat -import io.element.android.libraries.matrix.test.AN_EXCEPTION -import io.element.android.libraries.matrix.test.FakeMatrixClient -import io.element.android.tests.testutils.lambda.lambdaRecorder -import io.element.android.tests.testutils.lambda.value -import kotlinx.coroutines.test.runTest -import org.junit.Test -import org.matrix.rustcomponents.sdk.ElementCallWellKnown -import org.matrix.rustcomponents.sdk.ElementWellKnown - -class DefaultElementCallBaseUrlProviderTest { - @Test - fun `provides returns null when getUrl returns an error`() = runTest { - val userIdServerNameLambda = lambdaRecorder { "example.com" } - val getUrlLambda = lambdaRecorder> { _ -> - Result.failure(AN_EXCEPTION) - } - val sut = DefaultElementCallBaseUrlProvider( - FakeElementWellKnownParser( - Result.success(createElementWellKnown("")) - ) - ) - val matrixClient = FakeMatrixClient( - userIdServerNameLambda = userIdServerNameLambda, - getUrlLambda = getUrlLambda, - ) - val result = sut.provides(matrixClient) - assertThat(result).isNull() - userIdServerNameLambda.assertions().isCalledOnce() - getUrlLambda.assertions().isCalledOnce() - .with(value("https://example.com/.well-known/element/element.json")) - } - - @Test - fun `provides returns null when content parsing fails`() = runTest { - val userIdServerNameLambda = lambdaRecorder { "example.com" } - val getUrlLambda = lambdaRecorder> { _ -> - Result.success("""{"call":{"widget_url":"https://example.com/call"}}""") - } - val sut = DefaultElementCallBaseUrlProvider( - createFakeElementWellKnownParser( - Result.failure(AN_EXCEPTION) - ) - ) - val matrixClient = FakeMatrixClient( - userIdServerNameLambda = userIdServerNameLambda, - getUrlLambda = getUrlLambda, - ) - val result = sut.provides(matrixClient) - assertThat(result).isNull() - userIdServerNameLambda.assertions().isCalledOnce() - getUrlLambda.assertions().isCalledOnce() - .with(value("https://example.com/.well-known/element/element.json")) - } - - @Test - fun `provides returns value when getUrl returns correct content`() = runTest { - val userIdServerNameLambda = lambdaRecorder { "example.com" } - val getUrlLambda = lambdaRecorder> { _ -> - Result.success("""{"call":{"widget_url":"https://example.com/call"}}""") - } - val sut = DefaultElementCallBaseUrlProvider( - createFakeElementWellKnownParser( - Result.success(createElementWellKnown("aUrl")) - ) - ) - val matrixClient = FakeMatrixClient( - userIdServerNameLambda = userIdServerNameLambda, - getUrlLambda = getUrlLambda, - ) - val result = sut.provides(matrixClient) - assertThat(result).isEqualTo("aUrl") - userIdServerNameLambda.assertions().isCalledOnce() - getUrlLambda.assertions().isCalledOnce() - .with(value("https://example.com/.well-known/element/element.json")) - } - - private fun createFakeElementWellKnownParser(result: Result): FakeElementWellKnownParser { - return FakeElementWellKnownParser(result) - } - - private fun createElementWellKnown(widgetUrl: String): ElementWellKnown { - return ElementWellKnown( - call = ElementCallWellKnown( - widgetUrl = widgetUrl, - ), - registrationHelperUrl = null, - ) - } -} diff --git a/services/analytics/api/src/main/kotlin/io/element/android/services/analytics/api/store/AnalyticsStore.kt b/services/analytics/api/src/main/kotlin/io/element/android/services/analytics/api/store/AnalyticsStore.kt deleted file mode 100644 index aa26896b47..0000000000 --- a/services/analytics/api/src/main/kotlin/io/element/android/services/analytics/api/store/AnalyticsStore.kt +++ /dev/null @@ -1,26 +0,0 @@ -/* - * 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.services.analytics.api.store - -import kotlinx.coroutines.flow.Flow - -/** - * Local storage for: - * - user consent (Boolean); - * - did ask user consent (Boolean); - * - analytics Id (String). - */ -interface AnalyticsStore { - val userConsentFlow: Flow - val didAskUserConsentFlow: Flow - val analyticsIdFlow: Flow - suspend fun setUserConsent(newUserConsent: Boolean) - suspend fun setDidAskUserConsent(newValue: Boolean = true) - suspend fun setAnalyticsId(newAnalyticsId: String) - suspend fun reset() -} diff --git a/services/analytics/impl/src/main/kotlin/io/element/android/services/analytics/impl/DefaultAnalyticsService.kt b/services/analytics/impl/src/main/kotlin/io/element/android/services/analytics/impl/DefaultAnalyticsService.kt index 875d9a913c..6dafd7f334 100644 --- a/services/analytics/impl/src/main/kotlin/io/element/android/services/analytics/impl/DefaultAnalyticsService.kt +++ b/services/analytics/impl/src/main/kotlin/io/element/android/services/analytics/impl/DefaultAnalyticsService.kt @@ -17,8 +17,8 @@ import io.element.android.libraries.di.SingleIn import io.element.android.libraries.sessionstorage.api.observer.SessionListener import io.element.android.libraries.sessionstorage.api.observer.SessionObserver import io.element.android.services.analytics.api.AnalyticsService -import io.element.android.services.analytics.api.store.AnalyticsStore import io.element.android.services.analytics.impl.log.analyticsTag +import io.element.android.services.analytics.impl.store.AnalyticsStore import io.element.android.services.analyticsproviders.api.AnalyticsProvider import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.Flow diff --git a/services/analytics/impl/src/main/kotlin/io/element/android/services/analytics/impl/store/DefaultAnalyticsStore.kt b/services/analytics/impl/src/main/kotlin/io/element/android/services/analytics/impl/store/AnalyticsStore.kt similarity index 85% rename from services/analytics/impl/src/main/kotlin/io/element/android/services/analytics/impl/store/DefaultAnalyticsStore.kt rename to services/analytics/impl/src/main/kotlin/io/element/android/services/analytics/impl/store/AnalyticsStore.kt index c7f130d9ac..7c7109e482 100644 --- a/services/analytics/impl/src/main/kotlin/io/element/android/services/analytics/impl/store/DefaultAnalyticsStore.kt +++ b/services/analytics/impl/src/main/kotlin/io/element/android/services/analytics/impl/store/AnalyticsStore.kt @@ -18,7 +18,6 @@ import com.squareup.anvil.annotations.ContributesBinding import io.element.android.libraries.core.bool.orFalse import io.element.android.libraries.di.AppScope import io.element.android.libraries.di.ApplicationContext -import io.element.android.services.analytics.api.store.AnalyticsStore import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.map @@ -29,6 +28,22 @@ import javax.inject.Inject */ private val Context.dataStore: DataStore by preferencesDataStore(name = "vector_analytics") +/** + * Local storage for: + * - user consent (Boolean); + * - did ask user consent (Boolean); + * - analytics Id (String). + */ +interface AnalyticsStore { + val userConsentFlow: Flow + val didAskUserConsentFlow: Flow + val analyticsIdFlow: Flow + suspend fun setUserConsent(newUserConsent: Boolean) + suspend fun setDidAskUserConsent(newValue: Boolean = true) + suspend fun setAnalyticsId(newAnalyticsId: String) + suspend fun reset() +} + @ContributesBinding(AppScope::class) class DefaultAnalyticsStore @Inject constructor( @ApplicationContext private val context: Context diff --git a/services/analytics/impl/src/test/kotlin/io/element/android/services/analytics/impl/DefaultAnalyticsServiceTest.kt b/services/analytics/impl/src/test/kotlin/io/element/android/services/analytics/impl/DefaultAnalyticsServiceTest.kt index e5ea06dbd7..1b00b238eb 100644 --- a/services/analytics/impl/src/test/kotlin/io/element/android/services/analytics/impl/DefaultAnalyticsServiceTest.kt +++ b/services/analytics/impl/src/test/kotlin/io/element/android/services/analytics/impl/DefaultAnalyticsServiceTest.kt @@ -18,7 +18,7 @@ import im.vector.app.features.analytics.plan.SuperProperties import im.vector.app.features.analytics.plan.UserProperties import io.element.android.libraries.sessionstorage.api.observer.SessionObserver import io.element.android.libraries.sessionstorage.test.observer.NoOpSessionObserver -import io.element.android.services.analytics.api.store.AnalyticsStore +import io.element.android.services.analytics.impl.store.AnalyticsStore import io.element.android.services.analytics.impl.store.FakeAnalyticsStore import io.element.android.services.analyticsproviders.api.AnalyticsProvider import io.element.android.services.analyticsproviders.test.FakeAnalyticsProvider diff --git a/services/analytics/impl/src/test/kotlin/io/element/android/services/analytics/impl/store/FakeAnalyticsStore.kt b/services/analytics/impl/src/test/kotlin/io/element/android/services/analytics/impl/store/FakeAnalyticsStore.kt index d3d4379c6c..8cfa3649ec 100644 --- a/services/analytics/impl/src/test/kotlin/io/element/android/services/analytics/impl/store/FakeAnalyticsStore.kt +++ b/services/analytics/impl/src/test/kotlin/io/element/android/services/analytics/impl/store/FakeAnalyticsStore.kt @@ -7,11 +7,10 @@ package io.element.android.services.analytics.impl.store -import io.element.android.services.analytics.api.store.AnalyticsStore import io.element.android.tests.testutils.lambda.lambdaError import kotlinx.coroutines.flow.MutableStateFlow -class FakeAnalyticsStore( +internal class FakeAnalyticsStore( defaultUserConsent: Boolean = false, defaultDidAskUserConsent: Boolean = false, defaultAnalyticsId: String = "",