Repository: element-hq/element-x-android (language: kotlin)

Issue:
[Task] Configure Element Call's widget URL using the new SDK API that parses the element.json wellknown
Story: https://github.com/element-hq/element-meta/issues/2441


Discussion hints:
nan

Pull request: Let the SDK retrieve and parse Element well known content
<!-- Please read [CONTRIBUTING.md](https://github.com/element-hq/element-x-android/blob/develop/CONTRIBUTING.md) before submitting your pull request -->
 
## Type of change

- [ ] Feature
- [ ] Bugfix
- [x] Technical
- [ ] Other :

## Content

<!-- Describe shortly what has been changed -->
Configure Element Call's widget URL using the new SDK APIs that perform HTTP request and parses the element.json wellknown

FTR iOS PR is https://github.com/element-hq/element-x-ios/pull/2971

## Motivation and context

<!-- Provide link to the corresponding issue if applicable or explain the context -->
Closes #3124

## Screenshots / GIFs

<!--
We have screenshot tests in the project, so attaching screenshots to a PR is not mandatory, as far as there
is a Composable Preview covering the changes. In this case, the change will appear in the file diff.
Note that all the UI composables should be covered by a Composable Preview.

Providing a video of the change is still very useful for the reviewer and for the history of the project.

You can use a table like this to show screenshots comparison.
Uncomment this markdown table below and edit the last line `|||`:
|copy screenshot of before here|copy screenshot of after here|

|Before|After|
|-|-|
|||
 -->

## Tests

<!-- Explain how you tested your development -->

- Not tested with a server having custom configuration, but this should not break doing a call using matrix.org for instance.

## Tested devices

- [ ] Physical
- [x] Emulator
- OS version(s):

## Checklist

<!-- Depending on the Pull Request content, it can be acceptable if some of the following checkboxes stay unchecked. -->

- [ ] Changes have been tested on an Android device or Android emulator with API 23
- [ ] UI change has been tested on both light and dark themes
- [ ] Accessibility has been taken into account. See https://github.com/element-hq/element-x-android/blob/develop/CONTRIBUTING.md#accessibility
- [ ] Pull request is based on the develop branch
- [ ] Pull request title will be used in the release note, it clearly define what will change for the user
- [ ] Pull request includes screenshots or videos if containing UI changes
- [ ] Pull request includes a [sign off](https://matrix-org.github.io/synapse/latest/development/contributing_guide.html#sign-off)
- [ ] You've made a self review of your PR


Test command: ./gradlew :features:call:impl:testDebugUnitTest --tests "io.element.android.features.call.utils.DefaultCallWidgetProviderTest" :libraries:matrix:impl:testDebugUnitTest --tests "io.element.android.libraries.matrix.impl.call.DefaultElementCallBaseUrlProviderTest"

Fix patch:
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 f36269abc67..df7a0317537 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
@@ -20,6 +20,7 @@ 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
@@ -41,9 +42,10 @@ class DefaultCallWidgetProvider @Inject constructor(
         languageTag: String?,
         theme: String?,
     ): Result<CallWidgetProvider.GetWidgetResult> = runCatching {
-        val room = matrixClientsProvider.getOrRestore(sessionId).getOrThrow().getRoom(roomId) ?: error("Room not found")
+        val matrixClient = matrixClientsProvider.getOrRestore(sessionId).getOrThrow()
+        val room = matrixClient.getRoom(roomId) ?: error("Room not found")
         val baseUrl = appPreferencesStore.getCustomElementCallBaseUrlFlow().firstOrNull()
-            ?: elementCallBaseUrlProvider.provides(sessionId)
+            ?: elementCallBaseUrlProvider.provides(matrixClient)
             ?: ElementCallConfig.DEFAULT_BASE_URL
         val widgetSettings = callWidgetSettingsProvider.provide(baseUrl, encrypted = room.isEncrypted)
         val callUrl = room.generateWidgetWebViewUrl(
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
deleted file mode 100644
index 63eb5208ddc..00000000000
--- a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/utils/ElementCallBaseUrlProvider.kt
+++ /dev/null
@@ -1,61 +0,0 @@
-/*
- * 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.di.SingleIn
-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?
-}
-
-@SingleIn(AppScope::class)
-@ContributesBinding(AppScope::class)
-class DefaultElementCallBaseUrlProvider @Inject constructor(
-    private val retrofitFactory: RetrofitFactory,
-    private val coroutineDispatchers: CoroutineDispatchers,
-) : ElementCallBaseUrlProvider {
-    private val apiCache = mutableMapOf<SessionId, CallWellknownAPI>()
-
-    override suspend fun provides(sessionId: SessionId): String? = withContext(coroutineDispatchers.io) {
-        val domain = sessionId.value.substringAfter(":")
-        val callWellknownAPI = apiCache.getOrPut(sessionId) {
-            retrofitFactory.create("https://$domain")
-                .create(CallWellknownAPI::class.java)
-        }
-        try {
-            callWellknownAPI.getCallWellKnown().widgetUrl
-        } catch (e: HttpException) {
-            // 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
-        }
-    }
-}
diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/MatrixClient.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/MatrixClient.kt
index 4dd00984f12..b3b47d44999 100644
--- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/MatrixClient.kt
+++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/MatrixClient.kt
@@ -121,4 +121,15 @@ interface MatrixClient : Closeable {
      * This flow will emit a new value whenever the send queue is disabled for a room.
      */
     fun sendQueueDisabledFlow(): Flow<RoomId>
+
+    /**
+     * Return the server name part of the current user ID, using the SDK, and if a failure occurs,
+     * compute it manually.
+     */
+    fun userIdServerName(): String
+
+    /**
+     * Execute generic GET requests through the SDKs internal HTTP client.
+     */
+    suspend fun getUrl(url: String): Result<String>
 }
diff --git a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/wellknown/CallWellKnown.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/call/ElementCallBaseUrlProvider.kt
similarity index 58%
rename from features/call/impl/src/main/kotlin/io/element/android/features/call/impl/wellknown/CallWellKnown.kt
rename to libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/call/ElementCallBaseUrlProvider.kt
index b2e87c907b1..94c9cb34896 100644
--- a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/wellknown/CallWellKnown.kt
+++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/call/ElementCallBaseUrlProvider.kt
@@ -5,7 +5,7 @@
  * 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
+ *     https://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,
@@ -14,22 +14,10 @@
  * limitations under the License.
  */
 
-package io.element.android.features.call.impl.wellknown
+package io.element.android.libraries.matrix.api.call
 
-import kotlinx.serialization.SerialName
-import kotlinx.serialization.Serializable
+import io.element.android.libraries.matrix.api.MatrixClient
 
-/**
- * Example:
- * <pre>
- * {
- *     "widget_url": "https://call.server.com"
- * }
- * </pre>
- * .
- */
-@Serializable
-data class CallWellKnown(
-    @SerialName("widget_url")
-    val widgetUrl: String? = null,
-)
+interface ElementCallBaseUrlProvider {
+    suspend fun provides(matrixClient: MatrixClient): String?
+}
diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/core/MatrixPatterns.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/core/MatrixPatterns.kt
index fecac816337..ac77e589866 100644
--- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/core/MatrixPatterns.kt
+++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/core/MatrixPatterns.kt
@@ -16,9 +16,6 @@
 
 package io.element.android.libraries.matrix.api.core
 
-import io.element.android.libraries.androidutils.metadata.isInDebug
-import timber.log.Timber
-
 /**
  * This class contains pattern to match the different Matrix ids
  * Ref: https://matrix.org/docs/spec/appendices#identifier-grammar
@@ -31,7 +28,7 @@ object MatrixPatterns {
     // See https://matrix.org/docs/spec/appendices#historical-user-ids
     // Sadly, we need to relax the regex pattern a bit as there already exist some ids that don't match the spec.
     private const val MATRIX_USER_IDENTIFIER_REGEX = "^@.*?$DOMAIN_REGEX$"
-    val PATTERN_CONTAIN_MATRIX_USER_IDENTIFIER = MATRIX_USER_IDENTIFIER_REGEX.toRegex(RegexOption.IGNORE_CASE)
+    private val PATTERN_CONTAIN_MATRIX_USER_IDENTIFIER = MATRIX_USER_IDENTIFIER_REGEX.toRegex(RegexOption.IGNORE_CASE)
 
     // regex pattern to find room ids in a string.
     private const val MATRIX_ROOM_IDENTIFIER_REGEX = "![A-Z0-9.-]+$DOMAIN_REGEX"
@@ -54,52 +51,6 @@ object MatrixPatterns {
     private const val MATRIX_EVENT_IDENTIFIER_V4_REGEX = "\\$[A-Z0-9\\-_]+"
     private val PATTERN_CONTAIN_MATRIX_EVENT_IDENTIFIER_V4 = MATRIX_EVENT_IDENTIFIER_V4_REGEX.toRegex(RegexOption.IGNORE_CASE)
 
-    // regex pattern to find group ids in a string.
-    private const val MATRIX_GROUP_IDENTIFIER_REGEX = "\\+[A-Z0-9=_\\-./]+$DOMAIN_REGEX"
-    private val PATTERN_CONTAIN_MATRIX_GROUP_IDENTIFIER = MATRIX_GROUP_IDENTIFIER_REGEX.toRegex(RegexOption.IGNORE_CASE)
-
-    // regex pattern to find permalink with message id.
-    // Android does not support in URL so extract it.
-    private const val PERMALINK_BASE_REGEX = "https://matrix\\.to/#/"
-    private const val APP_BASE_REGEX = "https://[A-Z0-9.-]+\\.[A-Z]{2,}/[A-Z]{3,}/#/room/"
-    const val SEP_REGEX = "/"
-
-    private const val LINK_TO_ROOM_ID_REGEXP = PERMALINK_BASE_REGEX + MATRIX_ROOM_IDENTIFIER_REGEX + SEP_REGEX + MATRIX_EVENT_IDENTIFIER_REGEX
-    private val PATTERN_CONTAIN_MATRIX_TO_PERMALINK_ROOM_ID = LINK_TO_ROOM_ID_REGEXP.toRegex(RegexOption.IGNORE_CASE)
-
-    private const val LINK_TO_ROOM_ALIAS_REGEXP = PERMALINK_BASE_REGEX + MATRIX_ROOM_ALIAS_REGEX + SEP_REGEX + MATRIX_EVENT_IDENTIFIER_REGEX
-    private val PATTERN_CONTAIN_MATRIX_TO_PERMALINK_ROOM_ALIAS = LINK_TO_ROOM_ALIAS_REGEXP.toRegex(RegexOption.IGNORE_CASE)
-
-    private const val LINK_TO_APP_ROOM_ID_REGEXP = APP_BASE_REGEX + MATRIX_ROOM_IDENTIFIER_REGEX + SEP_REGEX + MATRIX_EVENT_IDENTIFIER_REGEX
-    private val PATTERN_CONTAIN_APP_LINK_PERMALINK_ROOM_ID = LINK_TO_APP_ROOM_ID_REGEXP.toRegex(RegexOption.IGNORE_CASE)
-
-    private const val LINK_TO_APP_ROOM_ALIAS_REGEXP = APP_BASE_REGEX + MATRIX_ROOM_ALIAS_REGEX + SEP_REGEX + MATRIX_EVENT_IDENTIFIER_REGEX
-    private val PATTERN_CONTAIN_APP_LINK_PERMALINK_ROOM_ALIAS = LINK_TO_APP_ROOM_ALIAS_REGEXP.toRegex(RegexOption.IGNORE_CASE)
-
-    // ascii characters in the range \x20 (space) to \x7E (~)
-    val ORDER_STRING_REGEX = "[ -~]+".toRegex()
-
-    // list of patterns to find some matrix item.
-    val MATRIX_PATTERNS = listOf(
-        PATTERN_CONTAIN_MATRIX_TO_PERMALINK_ROOM_ID,
-        PATTERN_CONTAIN_MATRIX_TO_PERMALINK_ROOM_ALIAS,
-        PATTERN_CONTAIN_APP_LINK_PERMALINK_ROOM_ID,
-        PATTERN_CONTAIN_APP_LINK_PERMALINK_ROOM_ALIAS,
-        PATTERN_CONTAIN_MATRIX_USER_IDENTIFIER,
-        PATTERN_CONTAIN_MATRIX_ALIAS,
-        PATTERN_CONTAIN_MATRIX_ROOM_IDENTIFIER,
-        PATTERN_CONTAIN_MATRIX_EVENT_IDENTIFIER,
-        PATTERN_CONTAIN_MATRIX_GROUP_IDENTIFIER
-    )
-
-    /**
-     * Tells if a string is a valid session Id. This is an alias for [isUserId]
-     *
-     * @param str the string to test
-     * @return true if the string is a valid session id
-     */
-    fun isSessionId(str: String?) = isUserId(str)
-
     /**
      * Tells if a string is a valid user Id.
      *
@@ -158,69 +109,4 @@ object MatrixPatterns {
      * @return true if the string is a valid thread id.
      */
     fun isThreadId(str: String?) = isEventId(str)
-
-    /**
-     * Tells if a string is a valid group id.
-     *
-     * @param str the string to test
-     * @return true if the string is a valid group id.
-     */
-    fun isGroupId(str: String?): Boolean {
-        return str != null && str matches PATTERN_CONTAIN_MATRIX_GROUP_IDENTIFIER
-    }
-
-    /**
-     * Extract server name from a matrix id.
-     *
-     * @param matrixId
-     * @return null if not found or if matrixId is null
-     */
-    fun extractServerNameFromId(matrixId: String?): String? {
-        return matrixId?.substringAfter(":", missingDelimiterValue = "")?.takeIf { it.isNotEmpty() }
-    }
-
-    /**
-     * Extract user name from a matrix id.
-     *
-     * @param matrixId
-     * @return null if the input is not a valid matrixId
-     */
-    fun extractUserNameFromId(matrixId: String): String? {
-        return if (isUserId(matrixId)) {
-            matrixId.removePrefix("@").substringBefore(":", missingDelimiterValue = "")
-        } else {
-            null
-        }
-    }
-
-    /**
-     * Orders which are not strings, or do not consist solely of ascii characters in the range \x20 (space) to \x7E (~),
-     * or consist of more than 50 characters, are forbidden and the field should be ignored if received.
-     */
-    fun isValidOrderString(order: String?): Boolean {
-        return order != null && order.length < 50 && order matches ORDER_STRING_REGEX
-    }
-
-    /*
-    fun candidateAliasFromRoomName(roomName: String, domain: String): String {
-        return roomName.lowercase()
-                .replaceSpaceChars(replacement = "_")
-                .removeInvalidRoomNameChars()
-                .take(MatrixConstants.maxAliasLocalPartLength(domain))
-    }
-     */
-
-    /**
-     * Return the domain form a userId.
-     * Examples:
-     * - "@alice:domain.org".getDomain() will return "domain.org"
-     * - "@bob:domain.org:3455".getDomain() will return "domain.org:3455"
-     */
-    fun String.getServerName(): String {
-        if (isInDebug && !isUserId(this)) {
-            // They are some invalid userId localpart in the wild, but the domain part should be there anyway
-            Timber.w("Not a valid user ID: $this")
-        }
-        return substringAfter(":")
-    }
 }
diff --git a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/wellknown/CallWellknownAPI.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/server/UserServerResolver.kt
similarity index 67%
rename from features/call/impl/src/main/kotlin/io/element/android/features/call/impl/wellknown/CallWellknownAPI.kt
rename to libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/server/UserServerResolver.kt
index e2b5d0e54f6..1de60a98e3a 100644
--- a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/wellknown/CallWellknownAPI.kt
+++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/server/UserServerResolver.kt
@@ -5,7 +5,7 @@
  * 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
+ *     https://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,
@@ -14,11 +14,8 @@
  * limitations under the License.
  */
 
-package io.element.android.features.call.impl.wellknown
+package io.element.android.libraries.matrix.api.server
 
-import retrofit2.http.GET
-
-internal interface CallWellknownAPI {
-    @GET(".well-known/element/call.json")
-    suspend fun getCallWellKnown(): CallWellKnown
+interface UserServerResolver {
+    fun resolve(): String
 }
diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClient.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClient.kt
index ebb871541d9..c8574a7934f 100644
--- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClient.kt
+++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClient.kt
@@ -280,6 +280,23 @@ class RustMatrixClient(
         }
     }
 
+    override fun userIdServerName(): String {
+        return runCatching {
+            client.userIdServerName()
+        }
+            .onFailure {
+                Timber.w(it, "Failed to get userIdServerName")
+            }
+            .getOrNull()
+            ?: sessionId.value.substringAfter(":")
+    }
+
+    override suspend fun getUrl(url: String): Result<String> = withContext(sessionDispatcher) {
+        runCatching {
+            client.getUrl(url)
+        }
+    }
+
     override suspend fun getRoom(roomId: RoomId): MatrixRoom? {
         return roomFactory.create(roomId)
     }
diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/call/DefaultElementCallBaseUrlProvider.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/call/DefaultElementCallBaseUrlProvider.kt
new file mode 100644
index 00000000000..debc0e91749
--- /dev/null
+++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/call/DefaultElementCallBaseUrlProvider.kt
@@ -0,0 +1,52 @@
+/*
+ * 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
+ *
+ *     https://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.libraries.matrix.impl.call
+
+import com.squareup.anvil.annotations.ContributesBinding
+import io.element.android.libraries.di.AppScope
+import io.element.android.libraries.matrix.api.MatrixClient
+import io.element.android.libraries.matrix.api.call.ElementCallBaseUrlProvider
+import timber.log.Timber
+import javax.inject.Inject
+
+@ContributesBinding(AppScope::class)
+class DefaultElementCallBaseUrlProvider @Inject constructor(
+    private val elementWellKnownParser: ElementWellKnownParser,
+) : ElementCallBaseUrlProvider {
+    override suspend fun provides(matrixClient: MatrixClient): String? {
+        val url = buildString {
+            append("https://")
+            append(matrixClient.userIdServerName())
+            append("/.well-known/element/element.json")
+        }
+        return matrixClient.getUrl(url)
+            .onFailure { failure ->
+                Timber.w(failure, "Failed to fetch well-known element.json")
+            }
+            .getOrNull()
+            ?.let { wellKnownStr ->
+                elementWellKnownParser.parse(wellKnownStr)
+                    .onFailure { failure ->
+                        // Can be a HTML 404.
+                        Timber.w(failure, "Failed to parse content")
+                    }
+                    .getOrNull()
+            }
+            ?.call
+            ?.widgetUrl
+    }
+}
diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/call/ElementWellKnownParser.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/call/ElementWellKnownParser.kt
new file mode 100644
index 00000000000..aac31b89ffb
--- /dev/null
+++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/call/ElementWellKnownParser.kt
@@ -0,0 +1,36 @@
+/*
+ * 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
+ *
+ *     https://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.libraries.matrix.impl.call
+
+import com.squareup.anvil.annotations.ContributesBinding
+import io.element.android.libraries.di.AppScope
+import org.matrix.rustcomponents.sdk.ElementWellKnown
+import org.matrix.rustcomponents.sdk.makeElementWellKnown
+import javax.inject.Inject
+
+interface ElementWellKnownParser {
+    fun parse(str: String): Result<ElementWellKnown>
+}
+
+@ContributesBinding(AppScope::class)
+class RustElementWellKnownParser @Inject constructor() : ElementWellKnownParser {
+    override fun parse(str: String): Result<ElementWellKnown> {
+        return runCatching {
+            makeElementWellKnown(str)
+        }
+    }
+}
diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/server/DefaultUserServerResolver.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/server/DefaultUserServerResolver.kt
new file mode 100644
index 00000000000..ac205733248
--- /dev/null
+++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/server/DefaultUserServerResolver.kt
@@ -0,0 +1,32 @@
+/*
+ * 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
+ *
+ *     https://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.libraries.matrix.impl.server
+
+import com.squareup.anvil.annotations.ContributesBinding
+import io.element.android.libraries.di.SessionScope
+import io.element.android.libraries.matrix.api.MatrixClient
+import io.element.android.libraries.matrix.api.server.UserServerResolver
+import javax.inject.Inject
+
+@ContributesBinding(SessionScope::class)
+class DefaultUserServerResolver @Inject constructor(
+    private val matrixClient: MatrixClient,
+) : UserServerResolver {
+    override fun resolve(): String {
+        return matrixClient.userIdServerName()
+    }
+}


Test patch:
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 73c8d3515eb..7eb426c35e1 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,9 +18,9 @@ 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.libraries.matrix.api.MatrixClient
 import io.element.android.libraries.matrix.api.MatrixClientProvider
-import io.element.android.libraries.matrix.api.core.SessionId
+import io.element.android.libraries.matrix.api.call.ElementCallBaseUrlProvider
 import io.element.android.libraries.matrix.api.widget.CallWidgetSettingsProvider
 import io.element.android.libraries.matrix.test.A_ROOM_ID
 import io.element.android.libraries.matrix.test.A_SESSION_ID
@@ -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 providesLambda = lambdaRecorder<SessionId, String?> { _ -> aCustomUrl }
-        val elementCallBaseUrlProvider = FakeElementCallBaseUrlProvider { sessionId ->
-            providesLambda(sessionId)
+        val providesLambda = lambdaRecorder<MatrixClient, String?> { _ -> aCustomUrl }
+        val elementCallBaseUrlProvider = FakeElementCallBaseUrlProvider { matrixClient ->
+            providesLambda(matrixClient)
         }
         val room = FakeMatrixRoom().apply {
             givenGenerateWidgetWebViewUrlResult(Result.success("url"))
@@ -137,7 +137,7 @@ class DefaultCallWidgetProviderTest {
         assertThat(settingsProvider.providedBaseUrls).containsExactly(aCustomUrl)
         providesLambda.assertions()
             .isCalledOnce()
-            .with(value(A_SESSION_ID))
+            .with(value(client))
     }
 
     private fun createProvider(
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
index 619659e1df7..660bb7249f8 100644
--- 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
@@ -16,14 +16,14 @@
 
 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.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: (SessionId) -> String? = { lambdaError() }
+    private val providesLambda: (MatrixClient) -> String? = { lambdaError() }
 ) : ElementCallBaseUrlProvider {
-    override suspend fun provides(sessionId: SessionId): String? {
-        return providesLambda(sessionId)
+    override suspend fun provides(matrixClient: MatrixClient): String? {
+        return providesLambda(matrixClient)
     }
 }
diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/call/DefaultElementCallBaseUrlProviderTest.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/call/DefaultElementCallBaseUrlProviderTest.kt
new file mode 100644
index 00000000000..1d6e8fe4437
--- /dev/null
+++ b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/call/DefaultElementCallBaseUrlProviderTest.kt
@@ -0,0 +1,107 @@
+/*
+ * 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
+ *
+ *     https://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.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
+            )
+        )
+    }
+}
diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/call/FakeElementWellKnownParser.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/call/FakeElementWellKnownParser.kt
new file mode 100644
index 00000000000..d6108b955f9
--- /dev/null
+++ b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/call/FakeElementWellKnownParser.kt
@@ -0,0 +1,27 @@
+/*
+ * 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
+ *
+ *     https://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.libraries.matrix.impl.call
+
+import org.matrix.rustcomponents.sdk.ElementWellKnown
+
+class FakeElementWellKnownParser(
+    private val result: Result<ElementWellKnown>
+) : ElementWellKnownParser {
+    override fun parse(str: String): Result<ElementWellKnown> {
+        return result
+    }
+}
diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/FakeMatrixClient.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/FakeMatrixClient.kt
index d2eb47a5856..1b6223279c4 100644
--- a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/FakeMatrixClient.kt
+++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/FakeMatrixClient.kt
@@ -82,6 +82,8 @@ class FakeMatrixClient(
     private val resolveRoomAliasResult: (RoomAlias) -> Result<ResolvedRoomAlias> = { Result.success(ResolvedRoomAlias(A_ROOM_ID, emptyList())) },
     private val getRoomPreviewFromRoomIdResult: (RoomId, List<String>) -> Result<RoomPreview> = { _, _ -> Result.failure(AN_EXCEPTION) },
     private val clearCacheLambda: () -> Unit = { lambdaError() },
+    private val userIdServerNameLambda: () -> String = { lambdaError() },
+    private val getUrlLambda: (String) -> Result<String> = { lambdaError() },
 ) : MatrixClient {
     var setDisplayNameCalled: Boolean = false
         private set
@@ -313,4 +315,12 @@ class FakeMatrixClient(
 
     var sendQueueDisabledFlow = emptyFlow<RoomId>()
     override fun sendQueueDisabledFlow(): Flow<RoomId> = sendQueueDisabledFlow
+
+    override fun userIdServerName(): String {
+        return userIdServerNameLambda()
+    }
+
+    override suspend fun getUrl(url: String): Result<String> {
+        return getUrlLambda(url)
+    }
 }