From a2db29316c783c96a353c25a64485ee1eb141bd8 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 11 Jun 2024 15:54:02 +0200 Subject: [PATCH 1/7] Introduce GetWidgetResult to avoid using Pair with generic String. --- .../features/call/impl/ui/CallScreenPresenter.kt | 6 +++--- .../features/call/impl/utils/CallWidgetProvider.kt | 7 ++++++- .../call/impl/utils/DefaultCallWidgetProvider.kt | 8 +++++--- .../features/call/utils/FakeCallWidgetProvider.kt | 10 +++++++--- 4 files changed, 21 insertions(+), 10 deletions(-) diff --git a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/CallScreenPresenter.kt b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/CallScreenPresenter.kt index a2e8359284..691963f90d 100644 --- a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/CallScreenPresenter.kt +++ b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/CallScreenPresenter.kt @@ -188,13 +188,13 @@ class CallScreenPresenter @AssistedInject constructor( inputs.url } is CallType.RoomCall -> { - val (driver, url) = callWidgetProvider.getWidget( + val result = callWidgetProvider.getWidget( sessionId = inputs.sessionId, roomId = inputs.roomId, clientId = UUID.randomUUID().toString(), ).getOrThrow() - callWidgetDriver.value = driver - url + callWidgetDriver.value = result.driver + result.url } } } diff --git a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/utils/CallWidgetProvider.kt b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/utils/CallWidgetProvider.kt index 670571476c..61843c471a 100644 --- a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/utils/CallWidgetProvider.kt +++ b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/utils/CallWidgetProvider.kt @@ -27,5 +27,10 @@ interface CallWidgetProvider { clientId: String, languageTag: String? = null, theme: String? = null, - ): Result> + ): Result + + data class GetWidgetResult( + val driver: MatrixWidgetDriver, + val url: String, + ) } 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 1daa0a8f3d..f1a71f88f5 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 @@ -24,7 +24,6 @@ import io.element.android.libraries.matrix.api.MatrixClientProvider 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 -import io.element.android.libraries.matrix.api.widget.MatrixWidgetDriver import kotlinx.coroutines.flow.firstOrNull import javax.inject.Inject @@ -40,11 +39,14 @@ class DefaultCallWidgetProvider @Inject constructor( clientId: String, languageTag: String?, theme: String?, - ): Result> = runCatching { + ): Result = runCatching { val room = matrixClientsProvider.getOrRestore(sessionId).getOrThrow().getRoom(roomId) ?: error("Room not found") val baseUrl = appPreferencesStore.getCustomElementCallBaseUrlFlow().firstOrNull() ?: ElementCallConfig.DEFAULT_BASE_URL val widgetSettings = callWidgetSettingsProvider.provide(baseUrl, encrypted = room.isEncrypted) val callUrl = room.generateWidgetWebViewUrl(widgetSettings, clientId, languageTag, theme).getOrThrow() - room.getWidgetDriver(widgetSettings).getOrThrow() to callUrl + CallWidgetProvider.GetWidgetResult( + driver = room.getWidgetDriver(widgetSettings).getOrThrow(), + url = callUrl + ) } } diff --git a/features/call/impl/src/test/kotlin/io/element/android/features/call/utils/FakeCallWidgetProvider.kt b/features/call/impl/src/test/kotlin/io/element/android/features/call/utils/FakeCallWidgetProvider.kt index b957122a3f..c085d70cb1 100644 --- a/features/call/impl/src/test/kotlin/io/element/android/features/call/utils/FakeCallWidgetProvider.kt +++ b/features/call/impl/src/test/kotlin/io/element/android/features/call/utils/FakeCallWidgetProvider.kt @@ -19,7 +19,6 @@ package io.element.android.features.call.utils import io.element.android.features.call.impl.utils.CallWidgetProvider 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.MatrixWidgetDriver import io.element.android.libraries.matrix.test.widget.FakeMatrixWidgetDriver class FakeCallWidgetProvider( @@ -35,8 +34,13 @@ class FakeCallWidgetProvider( clientId: String, languageTag: String?, theme: String? - ): Result> { + ): Result { getWidgetCalled = true - return Result.success(widgetDriver to url) + return Result.success( + CallWidgetProvider.GetWidgetResult( + driver = widgetDriver, + url = url, + ) + ) } } From dd1c0d7a119bd699a9511bbd14391f2f06553412 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 11 Jun 2024 17:05:31 +0200 Subject: [PATCH 2/7] Create CallScreenStateProvider to be able to preview errors. --- .../call/impl/ui/CallScreenStateProvider.kt | 42 +++++++++++++++++++ .../features/call/impl/ui/CallScreenView.kt | 26 +++++------- 2 files changed, 53 insertions(+), 15 deletions(-) create mode 100644 features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/CallScreenStateProvider.kt diff --git a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/CallScreenStateProvider.kt b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/CallScreenStateProvider.kt new file mode 100644 index 0000000000..be6622d8ee --- /dev/null +++ b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/CallScreenStateProvider.kt @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2024 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.call.impl.ui + +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import io.element.android.libraries.architecture.AsyncData + +open class CallScreenStateProvider : PreviewParameterProvider { + override val values: Sequence + get() = sequenceOf( + aCallScreenState(), + aCallScreenState(urlState = AsyncData.Failure(Exception("An error occurred"))), + ) +} + +private fun aCallScreenState( + urlState: AsyncData = AsyncData.Success("https://call.element.io/some-actual-call?with=parameters"), + userAgent: String = "", + isInWidgetMode: Boolean = false, + eventSink: (CallScreenEvents) -> Unit = {}, +): CallScreenState { + return CallScreenState( + urlState = urlState, + userAgent = userAgent, + isInWidgetMode = isInWidgetMode, + eventSink = eventSink, + ) +} diff --git a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/CallScreenView.kt b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/CallScreenView.kt index ebf006d102..88f4359203 100644 --- a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/CallScreenView.kt +++ b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/CallScreenView.kt @@ -32,6 +32,7 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalInspectionMode import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.viewinterop.AndroidView import io.element.android.compound.tokens.generated.CompoundIcons import io.element.android.features.call.impl.R @@ -76,9 +77,9 @@ internal fun CallScreenView( } CallWebView( modifier = Modifier - .padding(padding) - .consumeWindowInsets(padding) - .fillMaxSize(), + .padding(padding) + .consumeWindowInsets(padding) + .fillMaxSize(), url = state.urlState, userAgent = state.userAgent, onPermissionsRequest = { request -> @@ -157,16 +158,11 @@ private fun WebView.setup( @PreviewsDayNight @Composable -internal fun CallScreenViewPreview() { - ElementPreview { - CallScreenView( - state = CallScreenState( - urlState = AsyncData.Success("https://call.element.io/some-actual-call?with=parameters"), - isInWidgetMode = false, - userAgent = "", - eventSink = {}, - ), - requestPermissions = { _, _ -> }, - ) - } +internal fun CallScreenViewPreview( + @PreviewParameter(CallScreenStateProvider::class) state: CallScreenState, +) = ElementPreview { + CallScreenView( + state = state, + requestPermissions = { _, _ -> }, + ) } From 120eedbd4c402ee251a6be55d8a12097e8d1235b Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 11 Jun 2024 17:15:58 +0200 Subject: [PATCH 3/7] Read element call base url from .well-known file if it exists. --- changelog.d/3009.misc | 1 + features/call/impl/build.gradle.kts | 1 + .../features/call/impl/ui/CallScreenView.kt | 20 ++++++- .../impl/utils/DefaultCallWidgetProvider.kt | 5 +- .../impl/utils/ElementCallBaseUrlProvider.kt | 55 +++++++++++++++++++ .../call/impl/wellknown/CallWellKnown.kt | 35 ++++++++++++ .../call/impl/wellknown/CallWellknownAPI.kt | 24 ++++++++ .../utils/DefaultCallWidgetProviderTest.kt | 41 ++++++++++++-- .../utils/FakeElementCallBaseUrlProvider.kt | 29 ++++++++++ 9 files changed, 203 insertions(+), 8 deletions(-) create mode 100644 changelog.d/3009.misc create mode 100644 features/call/impl/src/main/kotlin/io/element/android/features/call/impl/utils/ElementCallBaseUrlProvider.kt create mode 100644 features/call/impl/src/main/kotlin/io/element/android/features/call/impl/wellknown/CallWellKnown.kt create mode 100644 features/call/impl/src/main/kotlin/io/element/android/features/call/impl/wellknown/CallWellknownAPI.kt create mode 100644 features/call/impl/src/test/kotlin/io/element/android/features/call/utils/FakeElementCallBaseUrlProvider.kt diff --git a/changelog.d/3009.misc b/changelog.d/3009.misc new file mode 100644 index 0000000000..9a06b57eee --- /dev/null +++ b/changelog.d/3009.misc @@ -0,0 +1 @@ +Make Element Call widget URL configurable diff --git a/features/call/impl/build.gradle.kts b/features/call/impl/build.gradle.kts index 72ccdc089b..4e2aa65d44 100644 --- a/features/call/impl/build.gradle.kts +++ b/features/call/impl/build.gradle.kts @@ -54,6 +54,7 @@ dependencies { implementation(projects.services.toolbox.api) implementation(libs.androidx.webkit) implementation(libs.coil.compose) + implementation(libs.network.retrofit) implementation(libs.serialization.json) api(projects.features.call.api) ksp(libs.showkase.processor) diff --git a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/CallScreenView.kt b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/CallScreenView.kt index 88f4359203..23d0a4769e 100644 --- a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/CallScreenView.kt +++ b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/CallScreenView.kt @@ -38,12 +38,15 @@ import io.element.android.compound.tokens.generated.CompoundIcons import io.element.android.features.call.impl.R import io.element.android.features.call.impl.utils.WebViewWidgetMessageInterceptor import io.element.android.libraries.architecture.AsyncData +import io.element.android.libraries.designsystem.components.ProgressDialog import io.element.android.libraries.designsystem.components.button.BackButton +import io.element.android.libraries.designsystem.components.dialogs.ErrorDialog import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.PreviewsDayNight import io.element.android.libraries.designsystem.theme.components.Scaffold import io.element.android.libraries.designsystem.theme.components.Text import io.element.android.libraries.designsystem.theme.components.TopAppBar +import io.element.android.libraries.ui.strings.CommonStrings typealias RequestPermissionCallback = (Array) -> Unit @@ -77,9 +80,9 @@ internal fun CallScreenView( } CallWebView( modifier = Modifier - .padding(padding) - .consumeWindowInsets(padding) - .fillMaxSize(), + .padding(padding) + .consumeWindowInsets(padding) + .fillMaxSize(), url = state.urlState, userAgent = state.userAgent, onPermissionsRequest = { request -> @@ -92,6 +95,17 @@ internal fun CallScreenView( state.eventSink(CallScreenEvents.SetupMessageChannels(interceptor)) } ) + when (state.urlState) { + AsyncData.Uninitialized, + is AsyncData.Loading -> + ProgressDialog(text = stringResource(id = CommonStrings.common_please_wait)) + is AsyncData.Failure -> + ErrorDialog( + content = state.urlState.error.message.orEmpty(), + onDismiss = { state.eventSink(CallScreenEvents.Hangup) }, + ) + is AsyncData.Success -> Unit + } } } 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 f1a71f88f5..6f20129dcc 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 @@ -32,6 +32,7 @@ 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, @@ -41,7 +42,9 @@ class DefaultCallWidgetProvider @Inject constructor( theme: String?, ): Result = runCatching { val room = matrixClientsProvider.getOrRestore(sessionId).getOrThrow().getRoom(roomId) ?: error("Room not found") - val baseUrl = appPreferencesStore.getCustomElementCallBaseUrlFlow().firstOrNull() ?: ElementCallConfig.DEFAULT_BASE_URL + val baseUrl = appPreferencesStore.getCustomElementCallBaseUrlFlow().firstOrNull() + ?: elementCallBaseUrlProvider.provides(sessionId) + ?: ElementCallConfig.DEFAULT_BASE_URL val widgetSettings = callWidgetSettingsProvider.provide(baseUrl, encrypted = room.isEncrypted) val callUrl = room.generateWidgetWebViewUrl(widgetSettings, clientId, languageTag, theme).getOrThrow() CallWidgetProvider.GetWidgetResult( diff --git a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/utils/ElementCallBaseUrlProvider.kt b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/utils/ElementCallBaseUrlProvider.kt new file mode 100644 index 0000000000..883b5de73e --- /dev/null +++ b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/utils/ElementCallBaseUrlProvider.kt @@ -0,0 +1,55 @@ +/* + * Copyright (c) 2024 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.call.impl.utils + +import com.squareup.anvil.annotations.ContributesBinding +import io.element.android.features.call.impl.wellknown.CallWellknownAPI +import io.element.android.libraries.core.coroutine.CoroutineDispatchers +import io.element.android.libraries.di.AppScope +import io.element.android.libraries.matrix.api.core.SessionId +import io.element.android.libraries.network.RetrofitFactory +import kotlinx.coroutines.withContext +import retrofit2.HttpException +import timber.log.Timber +import java.net.HttpURLConnection +import javax.inject.Inject + +interface ElementCallBaseUrlProvider { + suspend fun provides(sessionId: SessionId): String? +} + +@ContributesBinding(AppScope::class) +class DefaultElementCallBaseUrlProvider @Inject constructor( + private val retrofitFactory: RetrofitFactory, + private val coroutineDispatchers: CoroutineDispatchers, +) : ElementCallBaseUrlProvider { + override suspend fun provides(sessionId: SessionId): String? = withContext(coroutineDispatchers.io) { + val domain = sessionId.value.substringAfter(":") + val callWellknownAPI = retrofitFactory.create("https://$domain") + .create(CallWellknownAPI::class.java) + try { + callWellknownAPI.getCallWellKnown().widgetUrl + } catch (e: HttpException) { + Timber.w(e, "Failed to fetch wellknown data") + // Ignore Http 404, but re-throws any other exceptions + if (e.code() != HttpURLConnection.HTTP_NOT_FOUND /* 404 */) { + throw e + } + null + } + } +} diff --git a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/wellknown/CallWellKnown.kt b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/wellknown/CallWellKnown.kt new file mode 100644 index 0000000000..b2e87c907b --- /dev/null +++ b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/wellknown/CallWellKnown.kt @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2024 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.call.impl.wellknown + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +/** + * Example: + *
+ * {
+ *     "widget_url": "https://call.server.com"
+ * }
+ * 
+ * . + */ +@Serializable +data class CallWellKnown( + @SerialName("widget_url") + val widgetUrl: String? = null, +) diff --git a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/wellknown/CallWellknownAPI.kt b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/wellknown/CallWellknownAPI.kt new file mode 100644 index 0000000000..e2b5d0e54f --- /dev/null +++ b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/wellknown/CallWellknownAPI.kt @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2024 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.call.impl.wellknown + +import retrofit2.http.GET + +internal interface CallWellknownAPI { + @GET(".well-known/element/call.json") + suspend fun getCallWellKnown(): CallWellKnown +} 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 a41b5f0296..99d7defe87 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 @@ -18,8 +18,10 @@ 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.features.call.impl.utils.ElementCallBaseUrlProvider import io.element.android.features.preferences.api.store.AppPreferencesStore import io.element.android.libraries.matrix.api.MatrixClientProvider +import io.element.android.libraries.matrix.api.core.SessionId 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 @@ -29,6 +31,8 @@ import io.element.android.libraries.matrix.test.room.FakeMatrixRoom import io.element.android.libraries.matrix.test.widget.FakeCallWidgetSettingsProvider import io.element.android.libraries.matrix.test.widget.FakeMatrixWidgetDriver 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 @@ -109,13 +113,42 @@ 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 provideLambda = lambdaRecorder { String -> aCustomUrl } + val elementCallBaseUrlProvider = FakeElementCallBaseUrlProvider { sessionId -> + provideLambda(sessionId) + } + val room = FakeMatrixRoom().apply { + givenGenerateWidgetWebViewUrlResult(Result.success("url")) + givenGetWidgetDriverResult(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) + provideLambda.assertions() + .isCalledOnce() + .with(value(A_SESSION_ID)) + } + private fun createProvider( matrixClientProvider: MatrixClientProvider = FakeMatrixClientProvider(), appPreferencesStore: AppPreferencesStore = InMemoryAppPreferencesStore(), - callWidgetSettingsProvider: CallWidgetSettingsProvider = FakeCallWidgetSettingsProvider() + callWidgetSettingsProvider: CallWidgetSettingsProvider = FakeCallWidgetSettingsProvider(), + elementCallBaseUrlProvider: ElementCallBaseUrlProvider = FakeElementCallBaseUrlProvider { _ -> null }, ) = DefaultCallWidgetProvider( - matrixClientProvider, - appPreferencesStore, - callWidgetSettingsProvider, + 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 new file mode 100644 index 0000000000..619659e1df --- /dev/null +++ b/features/call/impl/src/test/kotlin/io/element/android/features/call/utils/FakeElementCallBaseUrlProvider.kt @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2024 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.call.utils + +import io.element.android.features.call.impl.utils.ElementCallBaseUrlProvider +import io.element.android.libraries.matrix.api.core.SessionId +import io.element.android.tests.testutils.lambda.lambdaError + +class FakeElementCallBaseUrlProvider( + private val providesLambda: (SessionId) -> String? = { lambdaError() } +) : ElementCallBaseUrlProvider { + override suspend fun provides(sessionId: SessionId): String? { + return providesLambda(sessionId) + } +} From f2a7a3b8f97c42107652961309a16d9cddc3aab2 Mon Sep 17 00:00:00 2001 From: ElementBot Date: Tue, 11 Jun 2024 15:31:23 +0000 Subject: [PATCH 4/7] Update screenshots --- ...iew_null_CallScreenView-Day-0_1_null_0,NEXUS_5,1.0,en].png} | 0 ...View_null_CallScreenView-Day-0_1_null_1,NEXUS_5,1.0,en].png | 3 +++ ...w_null_CallScreenView-Night-0_2_null_0,NEXUS_5,1.0,en].png} | 0 ...ew_null_CallScreenView-Night-0_2_null_1,NEXUS_5,1.0,en].png | 3 +++ 4 files changed, 6 insertions(+) rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.call.impl.ui_CallScreenView_null_CallScreenView-Day-0_1_null,NEXUS_5,1.0,en].png => ui_S_t[f.call.impl.ui_CallScreenView_null_CallScreenView-Day-0_1_null_0,NEXUS_5,1.0,en].png} (100%) create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.call.impl.ui_CallScreenView_null_CallScreenView-Day-0_1_null_1,NEXUS_5,1.0,en].png rename tests/uitests/src/test/snapshots/images/{ui_S_t[f.call.impl.ui_CallScreenView_null_CallScreenView-Night-0_2_null,NEXUS_5,1.0,en].png => ui_S_t[f.call.impl.ui_CallScreenView_null_CallScreenView-Night-0_2_null_0,NEXUS_5,1.0,en].png} (100%) create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.call.impl.ui_CallScreenView_null_CallScreenView-Night-0_2_null_1,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.call.impl.ui_CallScreenView_null_CallScreenView-Day-0_1_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.call.impl.ui_CallScreenView_null_CallScreenView-Day-0_1_null_0,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.call.impl.ui_CallScreenView_null_CallScreenView-Day-0_1_null,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.call.impl.ui_CallScreenView_null_CallScreenView-Day-0_1_null_0,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.call.impl.ui_CallScreenView_null_CallScreenView-Day-0_1_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.call.impl.ui_CallScreenView_null_CallScreenView-Day-0_1_null_1,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..abd445c244 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.call.impl.ui_CallScreenView_null_CallScreenView-Day-0_1_null_1,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:68d9ca60586aac84157c60f126b17b70ca9d52087da80f253b60f47de87d7ff6 +size 13750 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.call.impl.ui_CallScreenView_null_CallScreenView-Night-0_2_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.call.impl.ui_CallScreenView_null_CallScreenView-Night-0_2_null_0,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[f.call.impl.ui_CallScreenView_null_CallScreenView-Night-0_2_null,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[f.call.impl.ui_CallScreenView_null_CallScreenView-Night-0_2_null_0,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.call.impl.ui_CallScreenView_null_CallScreenView-Night-0_2_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.call.impl.ui_CallScreenView_null_CallScreenView-Night-0_2_null_1,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..1c4cfe0583 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.call.impl.ui_CallScreenView_null_CallScreenView-Night-0_2_null_1,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b676c158a1f820d50c9ffd50d048d5c236ee8356279663f257a4882f06c5a1a9 +size 12214 From 634118ecb865b467e26cc23810359ea5daf094cc Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 12 Jun 2024 09:13:48 +0200 Subject: [PATCH 5/7] Small quality fixes --- .../features/call/impl/utils/ElementCallBaseUrlProvider.kt | 2 +- .../features/call/utils/DefaultCallWidgetProviderTest.kt | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/utils/ElementCallBaseUrlProvider.kt b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/utils/ElementCallBaseUrlProvider.kt index 883b5de73e..6c24a51ac4 100644 --- a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/utils/ElementCallBaseUrlProvider.kt +++ b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/utils/ElementCallBaseUrlProvider.kt @@ -46,7 +46,7 @@ class DefaultElementCallBaseUrlProvider @Inject constructor( } catch (e: HttpException) { Timber.w(e, "Failed to fetch wellknown data") // Ignore Http 404, but re-throws any other exceptions - if (e.code() != HttpURLConnection.HTTP_NOT_FOUND /* 404 */) { + if (e.code() != HttpURLConnection.HTTP_NOT_FOUND) { throw e } null 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 99d7defe87..b0947bb409 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 @@ -116,9 +116,9 @@ class DefaultCallWidgetProviderTest { @Test fun `getWidget - will use a wellknown base url if it exists`() = runTest { val aCustomUrl = "https://custom.element.io" - val provideLambda = lambdaRecorder { String -> aCustomUrl } + val providesLambda = lambdaRecorder { _ -> aCustomUrl } val elementCallBaseUrlProvider = FakeElementCallBaseUrlProvider { sessionId -> - provideLambda(sessionId) + providesLambda(sessionId) } val room = FakeMatrixRoom().apply { givenGenerateWidgetWebViewUrlResult(Result.success("url")) @@ -135,7 +135,7 @@ class DefaultCallWidgetProviderTest { ) provider.getWidget(A_SESSION_ID, A_ROOM_ID, "clientId", "languageTag", "theme") assertThat(settingsProvider.providedBaseUrls).containsExactly(aCustomUrl) - provideLambda.assertions() + providesLambda.assertions() .isCalledOnce() .with(value(A_SESSION_ID)) } From e4759b03d268e56dd3d53584f1c30a712a976691 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 12 Jun 2024 09:15:30 +0200 Subject: [PATCH 6/7] Do not log error in case of 404. --- .../features/call/impl/utils/ElementCallBaseUrlProvider.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/utils/ElementCallBaseUrlProvider.kt b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/utils/ElementCallBaseUrlProvider.kt index 6c24a51ac4..e3f32edb86 100644 --- a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/utils/ElementCallBaseUrlProvider.kt +++ b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/utils/ElementCallBaseUrlProvider.kt @@ -44,11 +44,11 @@ class DefaultElementCallBaseUrlProvider @Inject constructor( try { callWellknownAPI.getCallWellKnown().widgetUrl } catch (e: HttpException) { - Timber.w(e, "Failed to fetch wellknown data") // Ignore Http 404, but re-throws any other exceptions if (e.code() != HttpURLConnection.HTTP_NOT_FOUND) { throw e } + Timber.w(e, "Failed to fetch wellknown data") null } } From cc55738bd4b659c81635e0b9b80cb80a6808116e Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 13 Jun 2024 13:32:18 +0200 Subject: [PATCH 7/7] Implement a memory cache for CallWellknownAPI --- .../call/impl/utils/ElementCallBaseUrlProvider.kt | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/utils/ElementCallBaseUrlProvider.kt b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/utils/ElementCallBaseUrlProvider.kt index e3f32edb86..63eb5208dd 100644 --- a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/utils/ElementCallBaseUrlProvider.kt +++ b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/utils/ElementCallBaseUrlProvider.kt @@ -20,6 +20,7 @@ import com.squareup.anvil.annotations.ContributesBinding import io.element.android.features.call.impl.wellknown.CallWellknownAPI import io.element.android.libraries.core.coroutine.CoroutineDispatchers import io.element.android.libraries.di.AppScope +import io.element.android.libraries.di.SingleIn import io.element.android.libraries.matrix.api.core.SessionId import io.element.android.libraries.network.RetrofitFactory import kotlinx.coroutines.withContext @@ -32,15 +33,20 @@ interface ElementCallBaseUrlProvider { suspend fun provides(sessionId: SessionId): String? } +@SingleIn(AppScope::class) @ContributesBinding(AppScope::class) class DefaultElementCallBaseUrlProvider @Inject constructor( private val retrofitFactory: RetrofitFactory, private val coroutineDispatchers: CoroutineDispatchers, ) : ElementCallBaseUrlProvider { + private val apiCache = mutableMapOf() + override suspend fun provides(sessionId: SessionId): String? = withContext(coroutineDispatchers.io) { val domain = sessionId.value.substringAfter(":") - val callWellknownAPI = retrofitFactory.create("https://$domain") - .create(CallWellknownAPI::class.java) + val callWellknownAPI = apiCache.getOrPut(sessionId) { + retrofitFactory.create("https://$domain") + .create(CallWellknownAPI::class.java) + } try { callWellknownAPI.getCallWellKnown().widgetUrl } catch (e: HttpException) {