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

[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

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

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)
+    }
 }


Use full date format for day dividers in timeline

## Type of change

- [ ] Feature
- [ ] Bugfix
- [ ] Technical
- [x] Other : improve UX

## Content

- Adds a new `dateWithFullFormatFormatter` that will use the same format as iOS.
- Changes the `onlyTimeFormatter` so it also adds AM/PM for locales that use them.

## Motivation and context

Fixes #2916.

## Screenshots / GIFs

|Before|After|
|-|-|
|||
|||

## Tests

- Check the new formats are used.

## Tested devices

- [x] Physical
- [ ] Emulator
- OS version(s): 14

## Checklist

- [ ] 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
- [x] Pull request is based on the develop branch
- [x] Pull request includes a new file under ./changelog.d. See https://github.com/element-hq/element-x-android/blob/develop/CONTRIBUTING.md#changelog
- [x] 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)
- [x] You've made a self review of your PR

Use a sane date format for day markers

Use a sane date format for day markers
### Your use case

I would like to never have to see a MM/DD/YYYY format while using any application. Instead, the international YYYY-MM-DD format should be used.

### Have you considered any alternatives?

An expanded format with written out day and month names, as done by Element Web for example and requested in #2637. However, implementing that would need comparatively more effort.

### Additional context

_No response_

### Are you willing to provide a PR?

No

Another possibility is to take the format from the system locale.

Unfortunately Android really sucks in this regard though as it only has a single locale setting rather than `LC_*`. There is some limited possibility to customise here as you could, for instance, use en_CA.

While I'm personally a big fan of ISO8601 dates, I could also see using the system locale being a better default.

diff --git a/changelog.d/2916.misc b/changelog.d/2916.misc
new file mode 100644
index 00000000000..b87146dd580
--- /dev/null
+++ b/changelog.d/2916.misc
@@ -0,0 +1,1 @@
+Use a more natural date format for day dividers in the timeline. Also improve the time format for last messages in the room list.
diff --git a/libraries/dateformatter/impl/src/main/kotlin/io/element/android/libraries/dateformatter/impl/DateFormatters.kt b/libraries/dateformatter/impl/src/main/kotlin/io/element/android/libraries/dateformatter/impl/DateFormatters.kt
index 9493079925c..5d92f137e58 100644
--- a/libraries/dateformatter/impl/src/main/kotlin/io/element/android/libraries/dateformatter/impl/DateFormatters.kt
+++ b/libraries/dateformatter/impl/src/main/kotlin/io/element/android/libraries/dateformatter/impl/DateFormatters.kt
@@ -26,19 +26,18 @@ import kotlinx.datetime.toJavaLocalDate
 import kotlinx.datetime.toJavaLocalDateTime
 import java.time.Period
 import java.time.format.DateTimeFormatter
+import java.time.format.FormatStyle
 import java.util.Locale
 import javax.inject.Inject
 import kotlin.math.absoluteValue

-// TODO rework this date formatting
 class DateFormatters @Inject constructor(
     private val locale: Locale,
     private val clock: Clock,
     private val timeZone: TimeZone,
 ) {
     private val onlyTimeFormatter: DateTimeFormatter by lazy {
-        val pattern = DateFormat.getBestDateTimePattern(locale, "HH:mm") ?: "HH:mm"
-        DateTimeFormatter.ofPattern(pattern, locale)
+        DateTimeFormatter.ofLocalizedTime(FormatStyle.SHORT).withLocale(locale)
     }

     private val dateWithMonthFormatter: DateTimeFormatter by lazy {
@@ -51,6 +50,10 @@ class DateFormatters @Inject constructor(
         DateTimeFormatter.ofPattern(pattern, locale)
     }

+    private val dateWithFullFormatFormatter: DateTimeFormatter by lazy {
+        DateTimeFormatter.ofLocalizedDate(FormatStyle.FULL).withLocale(locale)
+    }
+
     internal fun formatTime(localDateTime: LocalDateTime): String {
         return onlyTimeFormatter.format(localDateTime.toJavaLocalDateTime())
     }
@@ -63,6 +66,10 @@ class DateFormatters @Inject constructor(
         return dateWithYearFormatter.format(localDateTime.toJavaLocalDateTime())
     }

+    internal fun formatDateWithFullFormat(localDateTime: LocalDateTime): String {
+        return dateWithFullFormatFormatter.format(localDateTime.toJavaLocalDateTime())
+    }
+
     internal fun formatDate(
         dateToFormat: LocalDateTime,
         currentDate: LocalDateTime,
diff --git a/libraries/dateformatter/impl/src/main/kotlin/io/element/android/libraries/dateformatter/impl/DefaultDaySeparatorFormatter.kt b/libraries/dateformatter/impl/src/main/kotlin/io/element/android/libraries/dateformatter/impl/DefaultDaySeparatorFormatter.kt
index 90d7f5c205e..27608946457 100644
--- a/libraries/dateformatter/impl/src/main/kotlin/io/element/android/libraries/dateformatter/impl/DefaultDaySeparatorFormatter.kt
+++ b/libraries/dateformatter/impl/src/main/kotlin/io/element/android/libraries/dateformatter/impl/DefaultDaySeparatorFormatter.kt
@@ -28,6 +28,7 @@ class DefaultDaySeparatorFormatter @Inject constructor(
 ) : DaySeparatorFormatter {
     override fun format(timestamp: Long): String {
         val dateToFormat = localDateTimeProvider.providesFromTimestamp(timestamp)
-        return dateFormatters.formatDateWithYear(dateToFormat)
+        // TODO use relative formatting once iOS uses it too
+        return dateFormatters.formatDateWithFullFormat(dateToFormat)
     }
 }

diff --git a/libraries/dateformatter/impl/src/test/kotlin/io/element/android/libraries/dateformatter/impl/DefaultLastMessageTimestampFormatterTest.kt b/libraries/dateformatter/impl/src/test/kotlin/io/element/android/libraries/dateformatter/impl/DefaultLastMessageTimestampFormatterTest.kt
index dd8f75cae30..38e4fbffb35 100644
--- a/libraries/dateformatter/impl/src/test/kotlin/io/element/android/libraries/dateformatter/impl/DefaultLastMessageTimestampFormatterTest.kt
+++ b/libraries/dateformatter/impl/src/test/kotlin/io/element/android/libraries/dateformatter/impl/DefaultLastMessageTimestampFormatterTest.kt
@@ -21,6 +21,7 @@ import io.element.android.libraries.dateformatter.api.LastMessageTimestampFormat
 import io.element.android.libraries.dateformatter.test.FakeClock
 import kotlinx.datetime.Instant
 import kotlinx.datetime.TimeZone
+import kotlinx.datetime.toLocalDateTime
 import org.junit.Test
 import java.util.Locale

@@ -44,7 +45,7 @@ class DefaultLastMessageTimestampFormatterTest {
         val now = "1980-04-06T18:35:24.00Z"
         val dat = "1980-04-06T18:35:24.00Z"
         val formatter = createFormatter(now)
-        assertThat(formatter.format(Instant.parse(dat).toEpochMilliseconds())).isEqualTo("18:35")
+        assertThat(formatter.format(Instant.parse(dat).toEpochMilliseconds())).isEqualTo("6:35 PM")
     }

     @Test
@@ -52,7 +53,7 @@ class DefaultLastMessageTimestampFormatterTest {
         val now = "1980-04-06T18:35:24.00Z"
         val dat = "1980-04-06T18:35:23.00Z"
         val formatter = createFormatter(now)
-        assertThat(formatter.format(Instant.parse(dat).toEpochMilliseconds())).isEqualTo("18:35")
+        assertThat(formatter.format(Instant.parse(dat).toEpochMilliseconds())).isEqualTo("6:35 PM")
     }

     @Test
@@ -60,7 +61,7 @@ class DefaultLastMessageTimestampFormatterTest {
         val now = "1980-04-06T18:35:24.00Z"
         val dat = "1980-04-06T18:34:24.00Z"
         val formatter = createFormatter(now)
-        assertThat(formatter.format(Instant.parse(dat).toEpochMilliseconds())).isEqualTo("18:34")
+        assertThat(formatter.format(Instant.parse(dat).toEpochMilliseconds())).isEqualTo("6:34 PM")
     }

     @Test
@@ -68,7 +69,7 @@ class DefaultLastMessageTimestampFormatterTest {
         val now = "1980-04-06T18:35:24.00Z"
         val dat = "1980-04-06T17:35:24.00Z"
         val formatter = createFormatter(now)
-        assertThat(formatter.format(Instant.parse(dat).toEpochMilliseconds())).isEqualTo("17:35")
+        assertThat(formatter.format(Instant.parse(dat).toEpochMilliseconds())).isEqualTo("5:35 PM")
     }

     @Test
@@ -96,6 +97,15 @@ class DefaultLastMessageTimestampFormatterTest {
         assertThat(formatter.format(Instant.parse(dat).toEpochMilliseconds())).isEqualTo("06.04.1979")
     }

+    @Test
+    fun `test full format`() {
+        val now = "1980-04-06T18:35:24.00Z"
+        val dat = "1979-04-06T18:35:24.00Z"
+        val clock = FakeClock().apply { givenInstant(Instant.parse(now)) }
+        val dateFormatters = DateFormatters(Locale.US, clock, TimeZone.UTC)
+        assertThat(dateFormatters.formatDateWithFullFormat(Instant.parse(dat).toLocalDateTime(TimeZone.UTC))).isEqualTo("Friday, April 6, 1979")
+    }
+
     /**
      * Create DefaultLastMessageFormatter and set current time to the provided date.
      */


Fix unregistering pusher local error

<!-- 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
- [x] Bugfix
- [ ] Technical
- [ ] Other :

## Content

<!-- Describe shortly what has been changed -->
Fixes #2895 by ignoring local error when unregistering a pusher (Firebase and UnifiedPush cases)

Base on the `unitTestPush` branch to be able to update the test, but the other PR will be merged first.

## Motivation and context

<!-- Provide link to the corresponding issue if applicable or explain the context -->
Fixes #2895

## 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|
|-|-|
|||
 -->
NA

## Tests

<!-- Explain how you tested your development -->

- Hard to test without hacking the code, but IRL this seems to happen.

Maybe @Xiretza may want to test the patch.

## 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 includes a new file under ./changelog.d. See https://github.com/element-hq/element-x-android/blob/develop/CONTRIBUTING.md#changelog
- [ ] 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

Selecting ntfy as notification provider does not work
### Steps to reproduce

1. In "Advanced settings", tap "Push notification provider"
2. From the selection of "Firebase" and "ntfy", pick "ntfy"
3. "Push notification provider" setting now shows "Error" as current value, going back and then into "Advanced settings" again makes it switch back to "Firebase"

### Outcome

#### What did you expect?

It is possible to use ntfy as a push notification provider.

#### What happened instead?

Changing the provider to ntfy fails. Other apps like FediLab can register their ntfy topics without issue.

### Your phone model

Samsung S10

### Operating system version

LineageOS for microG 20

### Application version and app store

EXA 0.4.13

### Homeserver

xiretza.xyz, Synapse v1.105.1

### Will you send logs?

Yes

### Are you willing to provide a PR?

No

OK, thanks for the issue and the log. If when you come back to the Advanced setting screen and see again "Firebase", it means that the unregistration of Firebase has failed. And indeed, I see in your log:

> java.lang.IllegalStateException: Unable to unregister pusher, Firebase token is not known.

The application should be lenient in this case I guess, I will create a PR.

diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/DefaultPushService.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/DefaultPushService.kt
index bf2b1ed5422..47e26fb9207 100644
--- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/DefaultPushService.kt
+++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/DefaultPushService.kt
@@ -51,13 +51,16 @@ class DefaultPushService @Inject constructor(
         pushProvider: PushProvider,
         distributor: Distributor,
     ): Result<Unit> {
+        Timber.d("Registering with ${pushProvider.name}/${distributor.name}}")
         val userPushStore = userPushStoreFactory.getOrCreate(matrixClient.sessionId)
         val currentPushProviderName = userPushStore.getPushProviderName()
         val currentPushProvider = pushProviders.find { it.name == currentPushProviderName }
         val currentDistributorValue = currentPushProvider?.getCurrentDistributor(matrixClient)?.value
         if (currentPushProviderName != pushProvider.name || currentDistributorValue != distributor.value) {
             // Unregister previous one if any
-            currentPushProvider?.unregister(matrixClient)
+            currentPushProvider
+                ?.also { Timber.d("Unregistering previous push provider $currentPushProviderName/$currentDistributorValue") }
+                ?.unregister(matrixClient)
                 ?.onFailure {
                     Timber.w(it, "Failed to unregister previous push provider")
                     return Result.failure(it)
diff --git a/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/pushproviders/firebase/FirebasePushProvider.kt b/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/pushproviders/firebase/FirebasePushProvider.kt
index b88d70b1572..0228f8f74ba 100644
--- a/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/pushproviders/firebase/FirebasePushProvider.kt
+++ b/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/pushproviders/firebase/FirebasePushProvider.kt
@@ -64,14 +64,13 @@ class FirebasePushProvider @Inject constructor(
     override suspend fun getCurrentDistributor(matrixClient: MatrixClient) = firebaseDistributor

     override suspend fun unregister(matrixClient: MatrixClient): Result<Unit> {
-        val pushKey = firebaseStore.getFcmToken() ?: return Result.failure<Unit>(
-            IllegalStateException(
-                "Unable to unregister pusher, Firebase token is not known."
-            )
-        ).also {
+        val pushKey = firebaseStore.getFcmToken()
+        return if (pushKey == null) {
             Timber.tag(loggerTag.value).w("Unable to unregister pusher, Firebase token is not known.")
+            Result.success(Unit)
+        } else {
+            pusherSubscriber.unregisterPusher(matrixClient, pushKey, FirebaseConfig.PUSHER_HTTP_URL)
         }
-        return pusherSubscriber.unregisterPusher(matrixClient, pushKey, FirebaseConfig.PUSHER_HTTP_URL)
     }

     override suspend fun getCurrentUserPushConfig(): CurrentUserPushConfig? {
diff --git a/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/pushproviders/unifiedpush/UnregisterUnifiedPushUseCase.kt b/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/pushproviders/unifiedpush/UnregisterUnifiedPushUseCase.kt
index 65d924740de..769f6507d5b 100644
--- a/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/pushproviders/unifiedpush/UnregisterUnifiedPushUseCase.kt
+++ b/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/pushproviders/unifiedpush/UnregisterUnifiedPushUseCase.kt
@@ -23,6 +23,7 @@ import io.element.android.libraries.di.ApplicationContext
 import io.element.android.libraries.matrix.api.MatrixClient
 import io.element.android.libraries.pushproviders.api.PusherSubscriber
 import org.unifiedpush.android.connector.UnifiedPush
+import timber.log.Timber
 import javax.inject.Inject

 interface UnregisterUnifiedPushUseCase {
@@ -39,7 +40,11 @@ class DefaultUnregisterUnifiedPushUseCase @Inject constructor(
         val endpoint = unifiedPushStore.getEndpoint(clientSecret)
         val gateway = unifiedPushStore.getPushGateway(clientSecret)
         if (endpoint == null || gateway == null) {
-            return Result.failure(IllegalStateException("No endpoint or gateway found for client secret"))
+            Timber.w("No endpoint or gateway found for client secret")
+            // Ensure we don't have any remaining data, but ignore this error
+            unifiedPushStore.storeUpEndpoint(clientSecret, null)
+            unifiedPushStore.storePushGateway(clientSecret, null)
+            return Result.success(Unit)
         }
         return pusherSubscriber.unregisterPusher(matrixClient, endpoint, gateway)
             .onSuccess {

diff --git a/libraries/pushproviders/firebase/src/test/kotlin/io/element/android/libraries/pushproviders/firebase/FirebasePushProviderTest.kt b/libraries/pushproviders/firebase/src/test/kotlin/io/element/android/libraries/pushproviders/firebase/FirebasePushProviderTest.kt
index 3c826827673..880be2f053f 100644
--- a/libraries/pushproviders/firebase/src/test/kotlin/io/element/android/libraries/pushproviders/firebase/FirebasePushProviderTest.kt
+++ b/libraries/pushproviders/firebase/src/test/kotlin/io/element/android/libraries/pushproviders/firebase/FirebasePushProviderTest.kt
@@ -134,17 +134,14 @@ class FirebasePushProviderTest {
     }

     @Test
-    fun `unregister ko no token`() = runTest {
+    fun `unregister no token - in this case, the error is ignored`() = runTest {
         val firebasePushProvider = createFirebasePushProvider(
             firebaseStore = InMemoryFirebaseStore(
                 token = null
             ),
-            pusherSubscriber = FakePusherSubscriber(
-                unregisterPusherResult = { _, _, _ -> Result.success(Unit) }
-            )
         )
         val result = firebasePushProvider.unregister(FakeMatrixClient())
-        assertThat(result.isFailure).isTrue()
+        assertThat(result.isSuccess).isTrue()
     }

     @Test
diff --git a/libraries/pushproviders/unifiedpush/src/test/kotlin/io/element/android/libraries/pushproviders/unifiedpush/DefaultUnregisterUnifiedPushUseCaseTest.kt b/libraries/pushproviders/unifiedpush/src/test/kotlin/io/element/android/libraries/pushproviders/unifiedpush/DefaultUnregisterUnifiedPushUseCaseTest.kt
index 1d04a07651f..dfa03707cc0 100644
--- a/libraries/pushproviders/unifiedpush/src/test/kotlin/io/element/android/libraries/pushproviders/unifiedpush/DefaultUnregisterUnifiedPushUseCaseTest.kt
+++ b/libraries/pushproviders/unifiedpush/src/test/kotlin/io/element/android/libraries/pushproviders/unifiedpush/DefaultUnregisterUnifiedPushUseCaseTest.kt
@@ -64,29 +64,49 @@ class DefaultUnregisterUnifiedPushUseCaseTest {
     }

     @Test
-    fun `test un registration error - no endpoint`() = runTest {
+    fun `test un registration error - no endpoint - will not unregister but return success`() = runTest {
         val matrixClient = FakeMatrixClient()
+        val storeUpEndpointResult = lambdaRecorder { _: String, _: String? -> }
+        val storePushGatewayResult = lambdaRecorder { _: String, _: String? -> }
         val useCase = createDefaultUnregisterUnifiedPushUseCase(
             unifiedPushStore = FakeUnifiedPushStore(
                 getEndpointResult = { null },
                 getPushGatewayResult = { "aGateway" },
+                storeUpEndpointResult = storeUpEndpointResult,
+                storePushGatewayResult = storePushGatewayResult,
             ),
         )
         val result = useCase.execute(matrixClient, A_SECRET)
-        assertThat(result.isFailure).isTrue()
+        assertThat(result.isSuccess).isTrue()
+        storeUpEndpointResult.assertions()
+            .isCalledOnce()
+            .with(value(A_SECRET), value(null))
+        storePushGatewayResult.assertions()
+            .isCalledOnce()
+            .with(value(A_SECRET), value(null))
     }

     @Test
-    fun `test un registration error - no gateway`() = runTest {
+    fun `test un registration error - no gateway - will not unregister but return success`() = runTest {
         val matrixClient = FakeMatrixClient()
+        val storeUpEndpointResult = lambdaRecorder { _: String, _: String? -> }
+        val storePushGatewayResult = lambdaRecorder { _: String, _: String? -> }
         val useCase = createDefaultUnregisterUnifiedPushUseCase(
             unifiedPushStore = FakeUnifiedPushStore(
                 getEndpointResult = { "aEndpoint" },
                 getPushGatewayResult = { null },
+                storeUpEndpointResult = storeUpEndpointResult,
+                storePushGatewayResult = storePushGatewayResult,
             ),
         )
         val result = useCase.execute(matrixClient, A_SECRET)
-        assertThat(result.isFailure).isTrue()
+        assertThat(result.isSuccess).isTrue()
+        storeUpEndpointResult.assertions()
+            .isCalledOnce()
+            .with(value(A_SECRET), value(null))
+        storePushGatewayResult.assertions()
+            .isCalledOnce()
+            .with(value(A_SECRET), value(null))
     }

     @Test