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
This commit is contained in:
parent
bac26635af
commit
e2febabcf6
32 changed files with 177 additions and 288 deletions
4
.github/workflows/build.yml
vendored
4
.github/workflows/build.yml
vendored
|
|
@ -46,6 +46,10 @@ jobs:
|
||||||
ELEMENT_ANDROID_MAPTILER_LIGHT_MAP_ID: ${{ secrets.MAPTILER_LIGHT_MAP_ID }}
|
ELEMENT_ANDROID_MAPTILER_LIGHT_MAP_ID: ${{ secrets.MAPTILER_LIGHT_MAP_ID }}
|
||||||
ELEMENT_ANDROID_MAPTILER_DARK_MAP_ID: ${{ secrets.MAPTILER_DARK_MAP_ID }}
|
ELEMENT_ANDROID_MAPTILER_DARK_MAP_ID: ${{ secrets.MAPTILER_DARK_MAP_ID }}
|
||||||
ELEMENT_ANDROID_SENTRY_DSN: ${{ secrets.ELEMENT_ANDROID_SENTRY_DSN }}
|
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
|
run: ./gradlew :app:assembleGplayDebug app:assembleFDroidDebug -PallWarningsAsErrors=true $CI_GRADLE_ARG_PROPERTIES
|
||||||
- name: Upload debug APKs
|
- name: Upload debug APKs
|
||||||
if: ${{ matrix.variant == 'debug' }}
|
if: ${{ matrix.variant == 'debug' }}
|
||||||
|
|
|
||||||
4
.github/workflows/build_enterprise.yml
vendored
4
.github/workflows/build_enterprise.yml
vendored
|
|
@ -54,6 +54,10 @@ jobs:
|
||||||
ELEMENT_ANDROID_MAPTILER_LIGHT_MAP_ID: ${{ secrets.MAPTILER_LIGHT_MAP_ID }}
|
ELEMENT_ANDROID_MAPTILER_LIGHT_MAP_ID: ${{ secrets.MAPTILER_LIGHT_MAP_ID }}
|
||||||
ELEMENT_ANDROID_MAPTILER_DARK_MAP_ID: ${{ secrets.MAPTILER_DARK_MAP_ID }}
|
ELEMENT_ANDROID_MAPTILER_DARK_MAP_ID: ${{ secrets.MAPTILER_DARK_MAP_ID }}
|
||||||
ELEMENT_ANDROID_SENTRY_DSN: ${{ secrets.ELEMENT_ANDROID_SENTRY_DSN }}
|
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
|
run: ./gradlew :app:assembleGplayDebug -PallWarningsAsErrors=true $CI_GRADLE_ARG_PROPERTIES
|
||||||
- name: Upload debug Enterprise APKs
|
- name: Upload debug Enterprise APKs
|
||||||
if: ${{ matrix.variant == 'debug' }}
|
if: ${{ matrix.variant == 'debug' }}
|
||||||
|
|
|
||||||
4
.github/workflows/nightly.yml
vendored
4
.github/workflows/nightly.yml
vendored
|
|
@ -30,6 +30,10 @@ jobs:
|
||||||
ELEMENT_ANDROID_MAPTILER_LIGHT_MAP_ID: ${{ secrets.MAPTILER_LIGHT_MAP_ID }}
|
ELEMENT_ANDROID_MAPTILER_LIGHT_MAP_ID: ${{ secrets.MAPTILER_LIGHT_MAP_ID }}
|
||||||
ELEMENT_ANDROID_MAPTILER_DARK_MAP_ID: ${{ secrets.MAPTILER_DARK_MAP_ID }}
|
ELEMENT_ANDROID_MAPTILER_DARK_MAP_ID: ${{ secrets.MAPTILER_DARK_MAP_ID }}
|
||||||
ELEMENT_ANDROID_SENTRY_DSN: ${{ secrets.ELEMENT_ANDROID_SENTRY_DSN }}
|
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_KEYID: ${{ secrets.ELEMENT_ANDROID_NIGHTLY_KEYID }}
|
||||||
ELEMENT_ANDROID_NIGHTLY_KEYPASSWORD: ${{ secrets.ELEMENT_ANDROID_NIGHTLY_KEYPASSWORD }}
|
ELEMENT_ANDROID_NIGHTLY_KEYPASSWORD: ${{ secrets.ELEMENT_ANDROID_NIGHTLY_KEYPASSWORD }}
|
||||||
ELEMENT_ANDROID_NIGHTLY_STOREPASSWORD: ${{ secrets.ELEMENT_ANDROID_NIGHTLY_STOREPASSWORD }}
|
ELEMENT_ANDROID_NIGHTLY_STOREPASSWORD: ${{ secrets.ELEMENT_ANDROID_NIGHTLY_STOREPASSWORD }}
|
||||||
|
|
|
||||||
4
.github/workflows/nightly_enterprise.yml
vendored
4
.github/workflows/nightly_enterprise.yml
vendored
|
|
@ -36,6 +36,10 @@ jobs:
|
||||||
ELEMENT_ANDROID_MAPTILER_LIGHT_MAP_ID: ${{ secrets.MAPTILER_LIGHT_MAP_ID }}
|
ELEMENT_ANDROID_MAPTILER_LIGHT_MAP_ID: ${{ secrets.MAPTILER_LIGHT_MAP_ID }}
|
||||||
ELEMENT_ANDROID_MAPTILER_DARK_MAP_ID: ${{ secrets.MAPTILER_DARK_MAP_ID }}
|
ELEMENT_ANDROID_MAPTILER_DARK_MAP_ID: ${{ secrets.MAPTILER_DARK_MAP_ID }}
|
||||||
ELEMENT_ANDROID_SENTRY_DSN: ${{ secrets.ELEMENT_ANDROID_SENTRY_DSN }}
|
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_KEYID: ${{ secrets.ELEMENT_ANDROID_NIGHTLY_KEYID }}
|
||||||
ELEMENT_ANDROID_NIGHTLY_KEYPASSWORD: ${{ secrets.ELEMENT_ANDROID_NIGHTLY_KEYPASSWORD }}
|
ELEMENT_ANDROID_NIGHTLY_KEYPASSWORD: ${{ secrets.ELEMENT_ANDROID_NIGHTLY_KEYPASSWORD }}
|
||||||
ELEMENT_ANDROID_NIGHTLY_STOREPASSWORD: ${{ secrets.ELEMENT_ANDROID_NIGHTLY_STOREPASSWORD }}
|
ELEMENT_ANDROID_NIGHTLY_STOREPASSWORD: ${{ secrets.ELEMENT_ANDROID_NIGHTLY_STOREPASSWORD }}
|
||||||
|
|
|
||||||
4
.github/workflows/release.yml
vendored
4
.github/workflows/release.yml
vendored
|
|
@ -32,6 +32,10 @@ jobs:
|
||||||
ELEMENT_ANDROID_MAPTILER_LIGHT_MAP_ID: ${{ secrets.MAPTILER_LIGHT_MAP_ID }}
|
ELEMENT_ANDROID_MAPTILER_LIGHT_MAP_ID: ${{ secrets.MAPTILER_LIGHT_MAP_ID }}
|
||||||
ELEMENT_ANDROID_MAPTILER_DARK_MAP_ID: ${{ secrets.MAPTILER_DARK_MAP_ID }}
|
ELEMENT_ANDROID_MAPTILER_DARK_MAP_ID: ${{ secrets.MAPTILER_DARK_MAP_ID }}
|
||||||
ELEMENT_ANDROID_SENTRY_DSN: ${{ secrets.ELEMENT_ANDROID_SENTRY_DSN }}
|
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
|
run: ./gradlew bundleGplayRelease $CI_GRADLE_ARG_PROPERTIES
|
||||||
- name: Upload bundle as artifact
|
- name: Upload bundle as artifact
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@ import com.google.devtools.ksp.processing.SymbolProcessorProvider
|
||||||
|
|
||||||
class ContributesNodeProcessorProvider : SymbolProcessorProvider {
|
class ContributesNodeProcessorProvider : SymbolProcessorProvider {
|
||||||
override fun create(environment: SymbolProcessorEnvironment): SymbolProcessor {
|
override fun create(environment: SymbolProcessorEnvironment): SymbolProcessor {
|
||||||
val enableLogging = environment.options["enableLogging"]?.toBoolean() ?: false
|
val enableLogging = environment.options["enableLogging"]?.toBoolean() == true
|
||||||
return ContributesNodeProcessor(
|
return ContributesNodeProcessor(
|
||||||
logger = environment.logger,
|
logger = environment.logger,
|
||||||
codeGenerator = environment.codeGenerator,
|
codeGenerator = environment.codeGenerator,
|
||||||
|
|
|
||||||
|
|
@ -8,11 +8,6 @@
|
||||||
package io.element.android.appconfig
|
package io.element.android.appconfig
|
||||||
|
|
||||||
object ElementCallConfig {
|
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.
|
* The default duration of a ringing call in seconds before it's automatically dismissed.
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
import extension.readLocalProperty
|
||||||
import extension.setupAnvil
|
import extension.setupAnvil
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
|
@ -23,6 +24,49 @@ android {
|
||||||
testOptions {
|
testOptions {
|
||||||
unitTests.isIncludeAndroidResources = true
|
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()
|
setupAnvil()
|
||||||
|
|
@ -47,6 +91,7 @@ dependencies {
|
||||||
implementation(libs.coil.compose)
|
implementation(libs.coil.compose)
|
||||||
implementation(libs.network.retrofit)
|
implementation(libs.network.retrofit)
|
||||||
implementation(libs.serialization.json)
|
implementation(libs.serialization.json)
|
||||||
|
implementation(libs.element.call.embedded)
|
||||||
api(projects.features.call.api)
|
api(projects.features.call.api)
|
||||||
|
|
||||||
testImplementation(libs.coroutines.test)
|
testImplementation(libs.coroutines.test)
|
||||||
|
|
|
||||||
|
|
@ -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() }
|
||||||
|
}
|
||||||
|
|
@ -8,10 +8,8 @@
|
||||||
package io.element.android.features.call.impl.utils
|
package io.element.android.features.call.impl.utils
|
||||||
|
|
||||||
import com.squareup.anvil.annotations.ContributesBinding
|
import com.squareup.anvil.annotations.ContributesBinding
|
||||||
import io.element.android.appconfig.ElementCallConfig
|
|
||||||
import io.element.android.libraries.di.AppScope
|
import io.element.android.libraries.di.AppScope
|
||||||
import io.element.android.libraries.matrix.api.MatrixClientProvider
|
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.RoomId
|
||||||
import io.element.android.libraries.matrix.api.core.SessionId
|
import io.element.android.libraries.matrix.api.core.SessionId
|
||||||
import io.element.android.libraries.matrix.api.widget.CallWidgetSettingsProvider
|
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 kotlinx.coroutines.flow.firstOrNull
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
private const val EMBEDDED_CALL_WIDGET_BASE_URL = "https://appassets.androidplatform.net/element-call/index.html"
|
||||||
|
|
||||||
@ContributesBinding(AppScope::class)
|
@ContributesBinding(AppScope::class)
|
||||||
class DefaultCallWidgetProvider @Inject constructor(
|
class DefaultCallWidgetProvider @Inject constructor(
|
||||||
private val matrixClientsProvider: MatrixClientProvider,
|
private val matrixClientsProvider: MatrixClientProvider,
|
||||||
private val appPreferencesStore: AppPreferencesStore,
|
private val appPreferencesStore: AppPreferencesStore,
|
||||||
private val callWidgetSettingsProvider: CallWidgetSettingsProvider,
|
private val callWidgetSettingsProvider: CallWidgetSettingsProvider,
|
||||||
private val elementCallBaseUrlProvider: ElementCallBaseUrlProvider,
|
|
||||||
) : CallWidgetProvider {
|
) : CallWidgetProvider {
|
||||||
override suspend fun getWidget(
|
override suspend fun getWidget(
|
||||||
sessionId: SessionId,
|
sessionId: SessionId,
|
||||||
|
|
@ -35,9 +34,10 @@ class DefaultCallWidgetProvider @Inject constructor(
|
||||||
): Result<CallWidgetProvider.GetWidgetResult> = runCatching {
|
): Result<CallWidgetProvider.GetWidgetResult> = runCatching {
|
||||||
val matrixClient = matrixClientsProvider.getOrRestore(sessionId).getOrThrow()
|
val matrixClient = matrixClientsProvider.getOrRestore(sessionId).getOrThrow()
|
||||||
val room = matrixClient.getRoom(roomId) ?: error("Room not found")
|
val room = matrixClient.getRoom(roomId) ?: error("Room not found")
|
||||||
val baseUrl = appPreferencesStore.getCustomElementCallBaseUrlFlow().firstOrNull()
|
|
||||||
?: elementCallBaseUrlProvider.provides(matrixClient)
|
val customBaseUrl = appPreferencesStore.getCustomElementCallBaseUrlFlow().firstOrNull()
|
||||||
?: ElementCallConfig.DEFAULT_BASE_URL
|
val baseUrl = customBaseUrl ?: EMBEDDED_CALL_WIDGET_BASE_URL
|
||||||
|
|
||||||
val isEncrypted = room.info().isEncrypted ?: room.getUpdatedIsEncrypted().getOrThrow()
|
val isEncrypted = room.info().isEncrypted ?: room.getUpdatedIsEncrypted().getOrThrow()
|
||||||
val widgetSettings = callWidgetSettingsProvider.provide(baseUrl, encrypted = isEncrypted)
|
val widgetSettings = callWidgetSettingsProvider.provide(baseUrl, encrypted = isEncrypted)
|
||||||
val callUrl = room.generateWidgetWebViewUrl(
|
val callUrl = room.generateWidgetWebViewUrl(
|
||||||
|
|
@ -46,9 +46,10 @@ class DefaultCallWidgetProvider @Inject constructor(
|
||||||
languageTag = languageTag,
|
languageTag = languageTag,
|
||||||
theme = theme,
|
theme = theme,
|
||||||
).getOrThrow()
|
).getOrThrow()
|
||||||
|
|
||||||
CallWidgetProvider.GetWidgetResult(
|
CallWidgetProvider.GetWidgetResult(
|
||||||
driver = room.getWidgetDriver(widgetSettings).getOrThrow(),
|
driver = room.getWidgetDriver(widgetSettings).getOrThrow(),
|
||||||
url = callUrl
|
url = callUrl,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,8 @@ import android.webkit.WebResourceRequest
|
||||||
import android.webkit.WebResourceResponse
|
import android.webkit.WebResourceResponse
|
||||||
import android.webkit.WebView
|
import android.webkit.WebView
|
||||||
import android.webkit.WebViewClient
|
import android.webkit.WebViewClient
|
||||||
|
import androidx.core.net.toUri
|
||||||
|
import androidx.webkit.WebViewAssetLoader
|
||||||
import androidx.webkit.WebViewCompat
|
import androidx.webkit.WebViewCompat
|
||||||
import androidx.webkit.WebViewFeature
|
import androidx.webkit.WebViewFeature
|
||||||
import io.element.android.features.call.impl.BuildConfig
|
import io.element.android.features.call.impl.BuildConfig
|
||||||
|
|
@ -37,6 +39,10 @@ class WebViewWidgetMessageInterceptor(
|
||||||
override val interceptedMessages = MutableSharedFlow<String>(extraBufferCapacity = 10)
|
override val interceptedMessages = MutableSharedFlow<String>(extraBufferCapacity = 10)
|
||||||
|
|
||||||
init {
|
init {
|
||||||
|
val assetLoader = WebViewAssetLoader.Builder()
|
||||||
|
.addPathHandler("/", WebViewAssetLoader.AssetsPathHandler(webView.context))
|
||||||
|
.build()
|
||||||
|
|
||||||
webView.webViewClient = object : WebViewClient() {
|
webView.webViewClient = object : WebViewClient() {
|
||||||
override fun onPageStarted(view: WebView?, url: String?, favicon: Bitmap?) {
|
override fun onPageStarted(view: WebView?, url: String?, favicon: Bitmap?) {
|
||||||
super.onPageStarted(view, url, favicon)
|
super.onPageStarted(view, url, favicon)
|
||||||
|
|
@ -117,6 +123,14 @@ class WebViewWidgetMessageInterceptor(
|
||||||
|
|
||||||
super.onReceivedSslError(view, handler, error)
|
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
|
// Create a WebMessageListener, which will receive messages from the WebView and reply to them
|
||||||
|
|
|
||||||
|
|
@ -9,9 +9,7 @@ package io.element.android.features.call.utils
|
||||||
|
|
||||||
import com.google.common.truth.Truth.assertThat
|
import com.google.common.truth.Truth.assertThat
|
||||||
import io.element.android.features.call.impl.utils.DefaultCallWidgetProvider
|
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.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.api.widget.CallWidgetSettingsProvider
|
||||||
import io.element.android.libraries.matrix.test.A_ROOM_ID
|
import io.element.android.libraries.matrix.test.A_ROOM_ID
|
||||||
import io.element.android.libraries.matrix.test.A_SESSION_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.matrix.test.widget.FakeMatrixWidgetDriver
|
||||||
import io.element.android.libraries.preferences.api.store.AppPreferencesStore
|
import io.element.android.libraries.preferences.api.store.AppPreferencesStore
|
||||||
import io.element.android.libraries.preferences.test.InMemoryAppPreferencesStore
|
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 kotlinx.coroutines.test.runTest
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
|
|
||||||
|
|
@ -104,42 +100,13 @@ class DefaultCallWidgetProviderTest {
|
||||||
assertThat(settingsProvider.providedBaseUrls).containsExactly("https://custom.element.io")
|
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<MatrixClient, String?> { _ -> 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(
|
private fun createProvider(
|
||||||
matrixClientProvider: MatrixClientProvider = FakeMatrixClientProvider(),
|
matrixClientProvider: MatrixClientProvider = FakeMatrixClientProvider(),
|
||||||
appPreferencesStore: AppPreferencesStore = InMemoryAppPreferencesStore(),
|
appPreferencesStore: AppPreferencesStore = InMemoryAppPreferencesStore(),
|
||||||
callWidgetSettingsProvider: CallWidgetSettingsProvider = FakeCallWidgetSettingsProvider(),
|
callWidgetSettingsProvider: CallWidgetSettingsProvider = FakeCallWidgetSettingsProvider(),
|
||||||
elementCallBaseUrlProvider: ElementCallBaseUrlProvider = FakeElementCallBaseUrlProvider { _ -> null },
|
|
||||||
) = DefaultCallWidgetProvider(
|
) = DefaultCallWidgetProvider(
|
||||||
matrixClientsProvider = matrixClientProvider,
|
matrixClientsProvider = matrixClientProvider,
|
||||||
appPreferencesStore = appPreferencesStore,
|
appPreferencesStore = appPreferencesStore,
|
||||||
callWidgetSettingsProvider = callWidgetSettingsProvider,
|
callWidgetSettingsProvider = callWidgetSettingsProvider,
|
||||||
elementCallBaseUrlProvider = elementCallBaseUrlProvider,
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -18,7 +18,6 @@ import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
import androidx.compose.runtime.snapshots.SnapshotStateMap
|
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.toLogLevel
|
||||||
import io.element.android.features.preferences.impl.developer.tracing.toLogLevelItem
|
import io.element.android.features.preferences.impl.developer.tracing.toLogLevelItem
|
||||||
import io.element.android.features.preferences.impl.tasks.ClearCacheUseCase
|
import io.element.android.features.preferences.impl.tasks.ClearCacheUseCase
|
||||||
|
|
@ -112,8 +111,7 @@ class DeveloperSettingsPresenter @Inject constructor(
|
||||||
triggerClearCache = { handleEvents(DeveloperSettingsEvents.ClearCache) }
|
triggerClearCache = { handleEvents(DeveloperSettingsEvents.ClearCache) }
|
||||||
)
|
)
|
||||||
is DeveloperSettingsEvents.SetCustomElementCallBaseUrl -> coroutineScope.launch {
|
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() }
|
||||||
val urlToSave = event.baseUrl.takeIf { !it.isNullOrEmpty() && it != ElementCallConfig.DEFAULT_BASE_URL }
|
|
||||||
appPreferencesStore.setCustomElementCallBaseUrl(urlToSave)
|
appPreferencesStore.setCustomElementCallBaseUrl(urlToSave)
|
||||||
}
|
}
|
||||||
DeveloperSettingsEvents.ClearCache -> coroutineScope.clearCache(clearCacheAction)
|
DeveloperSettingsEvents.ClearCache -> coroutineScope.clearCache(clearCacheAction)
|
||||||
|
|
@ -133,7 +131,6 @@ class DeveloperSettingsPresenter @Inject constructor(
|
||||||
rageshakeState = rageshakeState,
|
rageshakeState = rageshakeState,
|
||||||
customElementCallBaseUrlState = CustomElementCallBaseUrlState(
|
customElementCallBaseUrlState = CustomElementCallBaseUrlState(
|
||||||
baseUrl = customElementCallBaseUrl,
|
baseUrl = customElementCallBaseUrl,
|
||||||
defaultUrl = ElementCallConfig.DEFAULT_BASE_URL,
|
|
||||||
validator = ::customElementCallUrlValidator,
|
validator = ::customElementCallUrlValidator,
|
||||||
),
|
),
|
||||||
hideImagesAndVideos = hideImagesAndVideos,
|
hideImagesAndVideos = hideImagesAndVideos,
|
||||||
|
|
|
||||||
|
|
@ -27,6 +27,5 @@ data class DeveloperSettingsState(
|
||||||
|
|
||||||
data class CustomElementCallBaseUrlState(
|
data class CustomElementCallBaseUrlState(
|
||||||
val baseUrl: String?,
|
val baseUrl: String?,
|
||||||
val defaultUrl: String,
|
|
||||||
val validator: (String?) -> Boolean,
|
val validator: (String?) -> Boolean,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -47,10 +47,8 @@ fun aDeveloperSettingsState(
|
||||||
|
|
||||||
fun aCustomElementCallBaseUrlState(
|
fun aCustomElementCallBaseUrlState(
|
||||||
baseUrl: String? = null,
|
baseUrl: String? = null,
|
||||||
defaultUrl: String = "https://call.element.io",
|
|
||||||
validator: (String?) -> Boolean = { true },
|
validator: (String?) -> Boolean = { true },
|
||||||
) = CustomElementCallBaseUrlState(
|
) = CustomElementCallBaseUrlState(
|
||||||
baseUrl = baseUrl,
|
baseUrl = baseUrl,
|
||||||
defaultUrl = defaultUrl,
|
|
||||||
validator = validator,
|
validator = validator,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -136,22 +136,20 @@ private fun ElementCallCategory(
|
||||||
) {
|
) {
|
||||||
PreferenceCategory(title = "Element Call", showTopDivider = true) {
|
PreferenceCategory(title = "Element Call", showTopDivider = true) {
|
||||||
val callUrlState = state.customElementCallBaseUrlState
|
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)
|
stringResource(R.string.screen_advanced_settings_element_call_base_url_description)
|
||||||
} else {
|
} else {
|
||||||
callUrlState.baseUrl
|
callUrlState.baseUrl
|
||||||
}
|
}
|
||||||
PreferenceTextField(
|
PreferenceTextField(
|
||||||
headline = stringResource(R.string.screen_advanced_settings_element_call_base_url),
|
headline = stringResource(R.string.screen_advanced_settings_element_call_base_url),
|
||||||
value = callUrlState.baseUrl ?: callUrlState.defaultUrl,
|
value = callUrlState.baseUrl,
|
||||||
|
placeholder = "https://.../room",
|
||||||
supportingText = supportingText,
|
supportingText = supportingText,
|
||||||
validation = callUrlState.validator,
|
validation = callUrlState.validator,
|
||||||
onValidationErrorMessage = stringResource(R.string.screen_advanced_settings_element_call_base_url_validation_error),
|
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),
|
keyboardOptions = KeyboardOptions.Default.copy(autoCorrectEnabled = false, keyboardType = KeyboardType.Uri),
|
||||||
onChange = { state.eventSink(DeveloperSettingsEvents.SetCustomElementCallBaseUrl(it)) }
|
onChange = { state.eventSink(DeveloperSettingsEvents.SetCustomElementCallBaseUrl(it)) }
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,6 @@
|
||||||
package io.element.android.features.preferences.impl.developer
|
package io.element.android.features.preferences.impl.developer
|
||||||
|
|
||||||
import com.google.common.truth.Truth.assertThat
|
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.developer.tracing.LogLevelItem
|
||||||
import io.element.android.features.preferences.impl.tasks.FakeClearCacheUseCase
|
import io.element.android.features.preferences.impl.tasks.FakeClearCacheUseCase
|
||||||
import io.element.android.features.preferences.impl.tasks.FakeComputeCacheSizeUseCase
|
import io.element.android.features.preferences.impl.tasks.FakeComputeCacheSizeUseCase
|
||||||
|
|
@ -130,7 +129,6 @@ class DeveloperSettingsPresenterTest {
|
||||||
}
|
}
|
||||||
awaitItem().also { state ->
|
awaitItem().also { state ->
|
||||||
assertThat(state.customElementCallBaseUrlState.baseUrl).isEqualTo("https://call.element.ahoy")
|
assertThat(state.customElementCallBaseUrlState.baseUrl).isEqualTo("https://call.element.ahoy")
|
||||||
assertThat(state.customElementCallBaseUrlState.defaultUrl).isEqualTo(ElementCallConfig.DEFAULT_BASE_URL)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,10 +8,16 @@
|
||||||
package io.element.android.features.preferences.impl.developer
|
package io.element.android.features.preferences.impl.developer
|
||||||
|
|
||||||
import androidx.activity.ComponentActivity
|
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.AndroidComposeTestRule
|
||||||
import androidx.compose.ui.test.junit4.createAndroidComposeRule
|
import androidx.compose.ui.test.junit4.createAndroidComposeRule
|
||||||
import androidx.compose.ui.test.onNodeWithText
|
import androidx.compose.ui.test.onNodeWithText
|
||||||
import androidx.compose.ui.test.performClick
|
import androidx.compose.ui.test.performClick
|
||||||
|
import androidx.compose.ui.test.performTextInput
|
||||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||||
import io.element.android.features.preferences.impl.R
|
import io.element.android.features.preferences.impl.R
|
||||||
import io.element.android.features.preferences.impl.developer.tracing.LogLevelItem
|
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)
|
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)
|
rule.clickOn(CommonStrings.action_ok)
|
||||||
eventsRecorder.assertSingle(DeveloperSettingsEvents.SetCustomElementCallBaseUrl("https://call.element.io"))
|
eventsRecorder.assertSingle(DeveloperSettingsEvents.SetCustomElementCallBaseUrl("https://call.element.dev"))
|
||||||
}
|
}
|
||||||
|
|
||||||
@Config(qualifiers = "h1024dp")
|
@Config(qualifiers = "h1024dp")
|
||||||
|
|
|
||||||
|
|
@ -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_api = { module = "dev.zacsweers.anvil:compiler-api", version.ref = "anvil" }
|
||||||
anvil_compiler_utils = { module = "dev.zacsweers.anvil:compiler-utils", 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
|
# Auto services
|
||||||
google_autoservice = { module = "com.google.auto.service:auto-service", version.ref = "autoservice" }
|
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" }
|
google_autoservice_annotations = { module = "com.google.auto.service:auto-service-annotations", version.ref = "autoservice" }
|
||||||
|
|
|
||||||
|
|
@ -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?
|
|
||||||
}
|
|
||||||
|
|
@ -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?
|
||||||
|
}
|
||||||
|
|
@ -33,8 +33,6 @@ dependencies {
|
||||||
implementation(projects.libraries.network)
|
implementation(projects.libraries.network)
|
||||||
implementation(projects.libraries.preferences.api)
|
implementation(projects.libraries.preferences.api)
|
||||||
implementation(projects.services.analytics.api)
|
implementation(projects.services.analytics.api)
|
||||||
implementation(projects.services.analyticsproviders.posthog)
|
|
||||||
implementation(projects.services.analyticsproviders.sentry)
|
|
||||||
implementation(projects.services.toolbox.api)
|
implementation(projects.services.toolbox.api)
|
||||||
api(projects.libraries.matrix.api)
|
api(projects.libraries.matrix.api)
|
||||||
implementation(libs.dagger)
|
implementation(libs.dagger)
|
||||||
|
|
|
||||||
|
|
@ -189,12 +189,12 @@ class RustMatrixAuthenticationService @Inject constructor(
|
||||||
return withContext(coroutineDispatchers.io) {
|
return withContext(coroutineDispatchers.io) {
|
||||||
runCatching {
|
runCatching {
|
||||||
val client = currentClient ?: error("You need to call `setHomeserver()` first")
|
val client = currentClient ?: error("You need to call `setHomeserver()` first")
|
||||||
val oAuthAuthenticationData = client.urlForOidc(
|
val oAuthAuthorizationData = client.urlForOidc(
|
||||||
oidcConfiguration = oidcConfigurationProvider.get(),
|
oidcConfiguration = oidcConfigurationProvider.get(),
|
||||||
prompt = prompt.toRustPrompt(),
|
prompt = prompt.toRustPrompt(),
|
||||||
)
|
)
|
||||||
val url = oAuthAuthenticationData.loginUrl()
|
val url = oAuthAuthorizationData.loginUrl()
|
||||||
pendingOAuthAuthorizationData = oAuthAuthenticationData
|
pendingOAuthAuthorizationData = oAuthAuthorizationData
|
||||||
OidcDetails(url)
|
OidcDetails(url)
|
||||||
}.mapFailure { failure ->
|
}.mapFailure { failure ->
|
||||||
failure.mapAuthenticationException()
|
failure.mapAuthenticationException()
|
||||||
|
|
|
||||||
|
|
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -8,15 +8,13 @@
|
||||||
package io.element.android.libraries.matrix.impl.widget
|
package io.element.android.libraries.matrix.impl.widget
|
||||||
|
|
||||||
import com.squareup.anvil.annotations.ContributesBinding
|
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.BuildMeta
|
||||||
import io.element.android.libraries.core.meta.BuildType
|
import io.element.android.libraries.core.meta.BuildType
|
||||||
import io.element.android.libraries.di.AppScope
|
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.CallWidgetSettingsProvider
|
||||||
import io.element.android.libraries.matrix.api.widget.MatrixWidgetSettings
|
import io.element.android.libraries.matrix.api.widget.MatrixWidgetSettings
|
||||||
import io.element.android.services.analytics.api.store.AnalyticsStore
|
import io.element.android.services.analytics.api.AnalyticsService
|
||||||
import io.element.android.services.analyticsproviders.posthog.PosthogEndpointConfigProvider
|
|
||||||
import io.element.android.services.analyticsproviders.sentry.SentryConfig
|
|
||||||
import kotlinx.coroutines.flow.first
|
import kotlinx.coroutines.flow.first
|
||||||
import org.matrix.rustcomponents.sdk.EncryptionSystem
|
import org.matrix.rustcomponents.sdk.EncryptionSystem
|
||||||
import org.matrix.rustcomponents.sdk.VirtualElementCallWidgetOptions
|
import org.matrix.rustcomponents.sdk.VirtualElementCallWidgetOptions
|
||||||
|
|
@ -27,12 +25,11 @@ import org.matrix.rustcomponents.sdk.Intent as CallIntent
|
||||||
@ContributesBinding(AppScope::class)
|
@ContributesBinding(AppScope::class)
|
||||||
class DefaultCallWidgetSettingsProvider @Inject constructor(
|
class DefaultCallWidgetSettingsProvider @Inject constructor(
|
||||||
private val buildMeta: BuildMeta,
|
private val buildMeta: BuildMeta,
|
||||||
private val posthogEndpointConfigProvider: PosthogEndpointConfigProvider,
|
private val callAnalyticsCredentialsProvider: CallAnalyticCredentialsProvider,
|
||||||
private val analyticsStore: AnalyticsStore,
|
private val analyticsService: AnalyticsService,
|
||||||
) : CallWidgetSettingsProvider {
|
) : CallWidgetSettingsProvider {
|
||||||
override suspend fun provide(baseUrl: String, widgetId: String, encrypted: Boolean): MatrixWidgetSettings {
|
override suspend fun provide(baseUrl: String, widgetId: String, encrypted: Boolean): MatrixWidgetSettings {
|
||||||
val analyticsEnabled = analyticsStore.userConsentFlow.first()
|
val isAnalyticsEnabled = analyticsService.getUserConsent().first()
|
||||||
val posthogEndpointConfig = posthogEndpointConfigProvider.provide()
|
|
||||||
val options = VirtualElementCallWidgetOptions(
|
val options = VirtualElementCallWidgetOptions(
|
||||||
elementCallUrl = baseUrl,
|
elementCallUrl = baseUrl,
|
||||||
widgetId = widgetId,
|
widgetId = widgetId,
|
||||||
|
|
@ -44,12 +41,12 @@ class DefaultCallWidgetSettingsProvider @Inject constructor(
|
||||||
encryption = if (encrypted) EncryptionSystem.PerParticipantKeys else EncryptionSystem.Unencrypted,
|
encryption = if (encrypted) EncryptionSystem.PerParticipantKeys else EncryptionSystem.Unencrypted,
|
||||||
intent = CallIntent.START_CALL,
|
intent = CallIntent.START_CALL,
|
||||||
hideScreensharing = false,
|
hideScreensharing = false,
|
||||||
posthogUserId = null,
|
posthogUserId = callAnalyticsCredentialsProvider.posthogUserId.takeIf { isAnalyticsEnabled },
|
||||||
posthogApiHost = posthogEndpointConfig.host.takeIf { analyticsEnabled },
|
posthogApiHost = callAnalyticsCredentialsProvider.posthogApiHost.takeIf { isAnalyticsEnabled },
|
||||||
posthogApiKey = posthogEndpointConfig.apiKey.takeIf { analyticsEnabled },
|
posthogApiKey = callAnalyticsCredentialsProvider.posthogApiKey.takeIf { isAnalyticsEnabled },
|
||||||
rageshakeSubmitUrl = RageshakeConfig.BUG_REPORT_URL,
|
rageshakeSubmitUrl = callAnalyticsCredentialsProvider.rageshakeSubmitUrl,
|
||||||
sentryDsn = SentryConfig.DSN.takeIf { analyticsEnabled },
|
sentryDsn = callAnalyticsCredentialsProvider.sentryDsn.takeIf { isAnalyticsEnabled },
|
||||||
sentryEnvironment = if (buildMeta.buildType == BuildType.RELEASE) SentryConfig.ENV_RELEASE else SentryConfig.ENV_DEBUG,
|
sentryEnvironment = if (buildMeta.buildType == BuildType.RELEASE) "RELEASE" else "DEBUG",
|
||||||
parentUrl = null,
|
parentUrl = null,
|
||||||
hideHeader = true,
|
hideHeader = true,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -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<String> { "example.com" }
|
|
||||||
val getUrlLambda = lambdaRecorder<String, Result<String>> { _ ->
|
|
||||||
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<String> { "example.com" }
|
|
||||||
val getUrlLambda = lambdaRecorder<String, Result<String>> { _ ->
|
|
||||||
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<String> { "example.com" }
|
|
||||||
val getUrlLambda = lambdaRecorder<String, Result<String>> { _ ->
|
|
||||||
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<ElementWellKnown>): FakeElementWellKnownParser {
|
|
||||||
return FakeElementWellKnownParser(result)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun createElementWellKnown(widgetUrl: String): ElementWellKnown {
|
|
||||||
return ElementWellKnown(
|
|
||||||
call = ElementCallWellKnown(
|
|
||||||
widgetUrl = widgetUrl,
|
|
||||||
),
|
|
||||||
registrationHelperUrl = 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<Boolean>
|
|
||||||
val didAskUserConsentFlow: Flow<Boolean>
|
|
||||||
val analyticsIdFlow: Flow<String>
|
|
||||||
suspend fun setUserConsent(newUserConsent: Boolean)
|
|
||||||
suspend fun setDidAskUserConsent(newValue: Boolean = true)
|
|
||||||
suspend fun setAnalyticsId(newAnalyticsId: String)
|
|
||||||
suspend fun reset()
|
|
||||||
}
|
|
||||||
|
|
@ -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.SessionListener
|
||||||
import io.element.android.libraries.sessionstorage.api.observer.SessionObserver
|
import io.element.android.libraries.sessionstorage.api.observer.SessionObserver
|
||||||
import io.element.android.services.analytics.api.AnalyticsService
|
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.log.analyticsTag
|
||||||
|
import io.element.android.services.analytics.impl.store.AnalyticsStore
|
||||||
import io.element.android.services.analyticsproviders.api.AnalyticsProvider
|
import io.element.android.services.analyticsproviders.api.AnalyticsProvider
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,6 @@ import com.squareup.anvil.annotations.ContributesBinding
|
||||||
import io.element.android.libraries.core.bool.orFalse
|
import io.element.android.libraries.core.bool.orFalse
|
||||||
import io.element.android.libraries.di.AppScope
|
import io.element.android.libraries.di.AppScope
|
||||||
import io.element.android.libraries.di.ApplicationContext
|
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.Flow
|
||||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
|
|
@ -29,6 +28,22 @@ import javax.inject.Inject
|
||||||
*/
|
*/
|
||||||
private val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "vector_analytics")
|
private val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "vector_analytics")
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Local storage for:
|
||||||
|
* - user consent (Boolean);
|
||||||
|
* - did ask user consent (Boolean);
|
||||||
|
* - analytics Id (String).
|
||||||
|
*/
|
||||||
|
interface AnalyticsStore {
|
||||||
|
val userConsentFlow: Flow<Boolean>
|
||||||
|
val didAskUserConsentFlow: Flow<Boolean>
|
||||||
|
val analyticsIdFlow: Flow<String>
|
||||||
|
suspend fun setUserConsent(newUserConsent: Boolean)
|
||||||
|
suspend fun setDidAskUserConsent(newValue: Boolean = true)
|
||||||
|
suspend fun setAnalyticsId(newAnalyticsId: String)
|
||||||
|
suspend fun reset()
|
||||||
|
}
|
||||||
|
|
||||||
@ContributesBinding(AppScope::class)
|
@ContributesBinding(AppScope::class)
|
||||||
class DefaultAnalyticsStore @Inject constructor(
|
class DefaultAnalyticsStore @Inject constructor(
|
||||||
@ApplicationContext private val context: Context
|
@ApplicationContext private val context: Context
|
||||||
|
|
@ -18,7 +18,7 @@ import im.vector.app.features.analytics.plan.SuperProperties
|
||||||
import im.vector.app.features.analytics.plan.UserProperties
|
import im.vector.app.features.analytics.plan.UserProperties
|
||||||
import io.element.android.libraries.sessionstorage.api.observer.SessionObserver
|
import io.element.android.libraries.sessionstorage.api.observer.SessionObserver
|
||||||
import io.element.android.libraries.sessionstorage.test.observer.NoOpSessionObserver
|
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.analytics.impl.store.FakeAnalyticsStore
|
||||||
import io.element.android.services.analyticsproviders.api.AnalyticsProvider
|
import io.element.android.services.analyticsproviders.api.AnalyticsProvider
|
||||||
import io.element.android.services.analyticsproviders.test.FakeAnalyticsProvider
|
import io.element.android.services.analyticsproviders.test.FakeAnalyticsProvider
|
||||||
|
|
|
||||||
|
|
@ -7,11 +7,10 @@
|
||||||
|
|
||||||
package io.element.android.services.analytics.impl.store
|
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 io.element.android.tests.testutils.lambda.lambdaError
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
|
||||||
class FakeAnalyticsStore(
|
internal class FakeAnalyticsStore(
|
||||||
defaultUserConsent: Boolean = false,
|
defaultUserConsent: Boolean = false,
|
||||||
defaultDidAskUserConsent: Boolean = false,
|
defaultDidAskUserConsent: Boolean = false,
|
||||||
defaultAnalyticsId: String = "",
|
defaultAnalyticsId: String = "",
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue