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:
Jorge Martin Espinosa 2025-03-26 09:35:21 +01:00 committed by GitHub
parent 03f4122b3f
commit ba626fc173
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
32 changed files with 177 additions and 288 deletions

View file

@ -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() }
}

View file

@ -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<CallWidgetProvider.GetWidgetResult> = 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,
)
}
}

View file

@ -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<String>(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