feat(deeplink): point deeplinks at HealthChat hosts

Claims app.healthchat.ch and to.healthchat.ch as App Links, registers the
healthchat:// scheme, and drops the unreachable mobile.element.io handling.
This commit is contained in:
2026-08-03 10:53:35 +02:00
committed by Sedat Qaja
parent fd29aea9a9
commit e4ca0e6208
15 changed files with 155 additions and 168 deletions
+13 -27
View File
@@ -49,7 +49,7 @@
Using launchMode singleTask to avoid multiple instances of the Activity Using launchMode singleTask to avoid multiple instances of the Activity
when the app is already open. This is important for incoming share (see when the app is already open. This is important for incoming share (see
https://github.com/element-hq/element-x-android/issues/4074) and for opening https://github.com/element-hq/element-x-android/issues/4074) and for opening
the application from a mobile.element.io link. the application from a permalink.
--> -->
<activity <activity
android:name=".MainActivity" android:name=".MainActivity"
@@ -70,7 +70,7 @@
<category android:name="android.intent.category.DEFAULT" /> <category android:name="android.intent.category.DEFAULT" />
<data <data
android:host="open" android:host="open"
android:scheme="elementx" /> android:scheme="healthchat" />
</intent-filter> </intent-filter>
<!-- <!--
OAuth redirection OAuth redirection
@@ -84,7 +84,9 @@
<data android:scheme="@string/login_redirect_scheme" /> <data android:scheme="@string/login_redirect_scheme" />
</intent-filter> </intent-filter>
<!-- <!--
Element web links HealthChat web links
Note: hosts are listed one by one on purpose, a "*.healthchat.ch" wildcard would
also intercept the homeserver and authentication domains.
--> -->
<intent-filter android:autoVerify="true"> <intent-filter android:autoVerify="true">
<action android:name="android.intent.action.VIEW" /> <action android:name="android.intent.action.VIEW" />
@@ -93,28 +95,12 @@
<category android:name="android.intent.category.BROWSABLE" /> <category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="https" /> <data android:scheme="https" />
<!-- Note: we can't use "*.element.io" here because it'll intercept the "mas.element.io" domain too. --> <!-- HealthChat web client permalinks, example: https://app.healthchat.ch/#/room/#room:healthchat.ch -->
<!-- Matching asset file: https://app.element.io/.well-known/assetlinks.json --> <!-- Matching asset file: https://app.healthchat.ch/.well-known/assetlinks.json -->
<data android:host="app.element.io" /> <data android:host="app.healthchat.ch" />
<!-- Matching asset file: https://develop.element.io/.well-known/assetlinks.json --> <!-- HealthChat matrix.to deployment, example: https://to.healthchat.ch/#/#room:healthchat.ch -->
<data android:host="develop.element.io" /> <!-- Matching asset file: https://to.healthchat.ch/.well-known/assetlinks.json -->
<!-- Matching asset file: https://staging.element.io/.well-known/assetlinks.json --> <data android:host="to.healthchat.ch" />
<data android:host="staging.element.io" />
</intent-filter>
<!--
Element mobile links
Example: https://mobile.element.io/element/?account_provider=example.org&login_hint=mxid:@alice:example.org
-->
<intent-filter android:autoVerify="true">
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="https" />
<!-- Matching asset file: https://mobile.element.io/.well-known/assetlinks.json -->
<data android:host="mobile.element.io" />
<data android:path="/element/" />
</intent-filter> </intent-filter>
<!-- <!--
matrix: links matrix: links
@@ -128,7 +114,7 @@
<data android:scheme="matrix" /> <data android:scheme="matrix" />
</intent-filter> </intent-filter>
<!-- <!--
links from matrix.to website links from the HealthChat matrix.to website, example: healthchat://room/%23room%3Ahealthchat.ch
--> -->
<intent-filter> <intent-filter>
<action android:name="android.intent.action.VIEW" /> <action android:name="android.intent.action.VIEW" />
@@ -136,7 +122,7 @@
<category android:name="android.intent.category.DEFAULT" /> <category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" /> <category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="element" /> <data android:scheme="healthchat" />
<data android:host="user" /> <data android:host="user" />
<data android:host="room" /> <data android:host="room" />
</intent-filter> </intent-filter>
@@ -10,5 +10,19 @@ package io.element.android.appconfig
object MatrixConfiguration { object MatrixConfiguration {
const val MATRIX_TO_PERMALINK_BASE_URL: String = "https://matrix.to/#/" const val MATRIX_TO_PERMALINK_BASE_URL: String = "https://matrix.to/#/"
/**
* HealthChat's own matrix.to deployment (https://github.com/Novaloop-AG/healthchat-matrix.to).
* It uses the exact same URL format as matrix.to, so links can be translated by swapping the
* base URL. Used both to recognise incoming links and to build links shared outside of Matrix.
*/
const val HEALTHCHAT_TO_PERMALINK_BASE_URL: String = "https://to.healthchat.ch/#/"
/**
* Base URL of the HealthChat web client, which is what the healthchat:// custom scheme
* handed out by [HEALTHCHAT_TO_PERMALINK_BASE_URL] points to.
*/
const val HEALTHCHAT_WEB_BASE_URL: String = "https://app.healthchat.ch/#/"
val clientPermalinkBaseUrl: String? = null val clientPermalinkBaseUrl: String? = null
} }
+40 -20
View File
@@ -1,4 +1,4 @@
# Element X Android deeplink # HealthChat Android deeplink
<!--- TOC --> <!--- TOC -->
@@ -12,31 +12,45 @@
## Introduction ## Introduction
Element X Android supports deep linking to specific screens in the application. This document explains how to use deep links in Element X Android. HealthChat Android supports deep linking to specific screens in the application. This document explains how to use deep links in HealthChat Android.
### Asset Links ### Asset Links
The asset links file is available at https://element.io/.well-known/assetlinks.json Two hosts are claimed as verified Android App Links, and each one must serve a matching asset links
file listing `ch.healthchat.element` and the SHA-256 of the Play app signing certificate:
| Host | Asset links file | Served from |
|---|---|---|
| `app.healthchat.ch` | https://app.healthchat.ch/.well-known/assetlinks.json | the `element-web` fork (`apps/web/res/.well-known/assetlinks.json`) |
| `to.healthchat.ch` | https://to.healthchat.ch/.well-known/assetlinks.json | the `healthchat-matrix.to` fork (generated by `scripts/build.js`) |
### Supported links ### Supported links
Element Call link:
> https://call.element.io/Example
Link to a user: Link to a user:
> https://app.element.io/#/user/@alice:matrix.org > https://to.healthchat.ch/#/@alice:healthchat.ch
> https://app.healthchat.ch/#/user/@alice:healthchat.ch
Link to a room by id or alias: Link to a room by id or alias:
> https://app.element.io/#/room/!roomid:matrix.org > https://to.healthchat.ch/#/!roomid:healthchat.ch
> https://app.element.io/#/room/#element-x-android:matrix.org > https://app.healthchat.ch/#/room/#healthchat:healthchat.ch
Link to a room with a specific event: Link to a room with a specific event:
> https://app.element.io/#/room/!roomid:matrix.org/$eventid > https://app.healthchat.ch/#/room/!roomid:healthchat.ch/$eventid
Note that it will also work with other domain such as: The HealthChat matrix.to deployment hands out a custom scheme to Android, which is registered too:
> https://mobile.element.io > healthchat://room/%23healthchat%3Ahealthchat.ch
> https://develop.element.io > healthchat://user/%40alice%3Ahealthchat.ch
> https://staging.element.io
Standard Matrix URIs also work:
> matrix:r/healthchat:healthchat.ch
Only the two hosts above are declared in the manifest, so only those open the app from a browser.
`DefaultMatrixToConverter` is host-agnostic though: any URL using the `/#/room/`, `/#/user/` or
`/#/group/` path, as well as plain matrix.to links, are still resolved once they reach the app
(for instance when tapped inside a message).
Finally, notifications use an internal deep link that is not browsable:
> healthchat://open/{sessionId}/{roomId}/{eventId}
## Developer tools ## Developer tools
@@ -44,28 +58,34 @@ Using an Android 12 or higher emulator
Ensure links verification is enabled Ensure links verification is enabled
```bash ```bash
adb shell am compat enable 175408749 io.element.android.x.debug adb shell am compat enable 175408749 ch.healthchat.element.debug
``` ```
Reset link verifications for the given package id Reset link verifications for the given package id
```bash ```bash
adb shell pm set-app-links --package io.element.android.x.debug 0 all adb shell pm set-app-links --package ch.healthchat.element.debug 0 all
``` ```
Force the package id links to be verified Force the package id links to be verified
```bash ```bash
adb shell pm verify-app-links --re-verify io.element.android.x.debug adb shell pm verify-app-links --re-verify ch.healthchat.element.debug
``` ```
Print the link verification of the package id Print the link verification of the package id
```bash ```bash
adb shell pm get-app-links io.element.android.x.debug adb shell pm get-app-links ch.healthchat.element.debug
``` ```
``` ```
io.element.android.x.debug: ch.healthchat.element.debug:
ID: e2ece472-c266-4bf0-829c-be79959a6270 ID: e2ece472-c266-4bf0-829c-be79959a6270
Signatures: [B0:B0:51:DC:56:5C:81:2F:E1:7F:6F:3E:94:5B:4D:79:04:71:23:AB:0D:A6:12:86:76:9E:B2:94:91:97:13:0E] Signatures: [B0:B0:51:DC:56:5C:81:2F:E1:7F:6F:3E:94:5B:4D:79:04:71:23:AB:0D:A6:12:86:76:9E:B2:94:91:97:13:0E]
Domain verification state: Domain verification state:
*.element.io: 1024 app.healthchat.ch: verified
to.healthchat.ch: verified
``` ```
Verification only succeeds if the signature above is listed in the asset links files. Release builds
are signed by Google Play, so use the app signing certificate SHA-256 from the Play Console. For a
debug build, add the fingerprint of `app/signature/debug.keystore` (alias `androiddebugkey`) to both
files, or test with `healthchat://` links instead, which need no verification.
@@ -8,23 +8,18 @@
package io.element.android.features.login.impl package io.element.android.features.login.impl
import androidx.core.net.toUri
import dev.zacsweers.metro.AppScope import dev.zacsweers.metro.AppScope
import dev.zacsweers.metro.ContributesBinding import dev.zacsweers.metro.ContributesBinding
import io.element.android.features.login.api.LoginIntentResolver import io.element.android.features.login.api.LoginIntentResolver
import io.element.android.features.login.api.LoginParams import io.element.android.features.login.api.LoginParams
/**
* HealthChat does not support mobile configuration links: the account provider is fixed to the
* HealthChat homeserver (see DefaultEnterpriseService.defaultHomeserverList), so there is nothing
* for such a link to configure. The matching intent-filter has been removed from the manifest as
* well, which means nothing can deliver such an intent in the first place.
*/
@ContributesBinding(AppScope::class) @ContributesBinding(AppScope::class)
class DefaultLoginIntentResolver : LoginIntentResolver { class DefaultLoginIntentResolver : LoginIntentResolver {
override fun parse(uriString: String): LoginParams? { override fun parse(uriString: String): LoginParams? = null
val uri = uriString.toUri()
if (uri.host != "mobile.element.io") return null
if (uri.path.orEmpty().startsWith("/element").not()) return null
val accountProvider = uri.getQueryParameter("account_provider") ?: return null
val loginHint = uri.getQueryParameter("login_hint")
return LoginParams(
accountProvider = accountProvider,
loginHint = loginHint,
)
}
} }
@@ -9,72 +9,20 @@
package io.element.android.features.login.impl package io.element.android.features.login.impl
import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertThat
import io.element.android.features.login.api.LoginParams
import io.element.android.tests.testutils.robolectric.RobolectricTest import io.element.android.tests.testutils.robolectric.RobolectricTest
import org.junit.Test import org.junit.Test
class DefaultLoginIntentResolverTest : RobolectricTest() { class DefaultLoginIntentResolverTest : RobolectricTest() {
@Test @Test
fun `nominal case`() { fun `mobile configuration links are not supported`() {
val sut = DefaultLoginIntentResolver() val sut = DefaultLoginIntentResolver()
val uriString = "https://mobile.element.io/element/?account_provider=example.org&login_hint=mxid:@alice:example.org" val uriString = "https://mobile.element.io/element/?account_provider=example.org&login_hint=mxid:@alice:example.org"
assertThat(sut.parse(uriString)).isEqualTo(
LoginParams(
accountProvider = "example.org",
loginHint = "mxid:@alice:example.org",
)
)
}
@Test
fun `extra unknown param`() {
val sut = DefaultLoginIntentResolver()
val uriString = "https://mobile.element.io/element/?account_provider=example.org&login_hint=mxid:@alice:example.org&extra=uknown"
assertThat(sut.parse(uriString)).isEqualTo(
LoginParams(
accountProvider = "example.org",
loginHint = "mxid:@alice:example.org",
)
)
}
@Test
fun `no account provider`() {
val sut = DefaultLoginIntentResolver()
val uriString = "https://mobile.element.io/element/?login_hint=mxid:@alice:example.org"
assertThat(sut.parse(uriString)).isNull() assertThat(sut.parse(uriString)).isNull()
} }
@Test @Test
fun `no path`() { fun `an arbitrary uri returns null`() {
val sut = DefaultLoginIntentResolver() val sut = DefaultLoginIntentResolver()
val uriString = "https://mobile.element.io?account_provider=example.org&login_hint=mxid:@alice:example.org" assertThat(sut.parse("https://app.healthchat.ch/#/room/#room:healthchat.ch")).isNull()
assertThat(sut.parse(uriString)).isNull()
}
@Test
fun `wrong path`() {
val sut = DefaultLoginIntentResolver()
val uriString = "https://mobile.element.io/wrong?account_provider=example.org&login_hint=mxid:@alice:example.org"
assertThat(sut.parse(uriString)).isNull()
}
@Test
fun `wrong host`() {
val sut = DefaultLoginIntentResolver()
val uriString = "https://wrong.element.io/element/?account_provider=example.org&login_hint=mxid:@alice:example.org"
assertThat(sut.parse(uriString)).isNull()
}
@Test
fun `no login_hint param`() {
val sut = DefaultLoginIntentResolver()
val uriString = "https://mobile.element.io/element/?account_provider=example.org"
assertThat(sut.parse(uriString)).isEqualTo(
LoginParams(
accountProvider = "example.org",
loginHint = null,
)
)
} }
} }
@@ -8,5 +8,7 @@
package io.element.android.libraries.deeplink.impl package io.element.android.libraries.deeplink.impl
internal const val SCHEME = "elementx" // Internal deep link, only used to build the notification PendingIntents. The "open" host keeps
// it distinct from the healthchat://room|user links handed out by the HealthChat matrix.to site.
internal const val SCHEME = "healthchat"
internal const val HOST = "open" internal const val HOST = "open"
@@ -28,15 +28,15 @@ class DefaultDeepLinkCreatorTest {
val threadId = A_THREAD_ID val threadId = A_THREAD_ID
val eventId = AN_EVENT_ID val eventId = AN_EVENT_ID
assertThat(sut.create(sessionId, null, null, null)) assertThat(sut.create(sessionId, null, null, null))
.isEqualTo("elementx://open/%40alice%3Aserver.org") .isEqualTo("healthchat://open/%40alice%3Aserver.org")
assertThat(sut.create(sessionId, roomId, null, null)) assertThat(sut.create(sessionId, roomId, null, null))
.isEqualTo("elementx://open/%40alice%3Aserver.org/%21aRoomId%3Adomain") .isEqualTo("healthchat://open/%40alice%3Aserver.org/%21aRoomId%3Adomain")
assertThat(sut.create(sessionId, roomId, threadId, null)) assertThat(sut.create(sessionId, roomId, threadId, null))
.isEqualTo("elementx://open/%40alice%3Aserver.org/%21aRoomId%3Adomain/%24aThreadId") .isEqualTo("healthchat://open/%40alice%3Aserver.org/%21aRoomId%3Adomain/%24aThreadId")
assertThat(sut.create(sessionId, roomId, threadId, eventId)) assertThat(sut.create(sessionId, roomId, threadId, eventId))
.isEqualTo("elementx://open/%40alice%3Aserver.org/%21aRoomId%3Adomain/%24aThreadId/%24anEventId") .isEqualTo("healthchat://open/%40alice%3Aserver.org/%21aRoomId%3Adomain/%24aThreadId/%24anEventId")
assertThat(sut.create(sessionId, roomId, null, eventId)) assertThat(sut.create(sessionId, roomId, null, eventId))
.isEqualTo("elementx://open/%40alice%3Aserver.org/%21aRoomId%3Adomain//%24anEventId") .isEqualTo("healthchat://open/%40alice%3Aserver.org/%21aRoomId%3Adomain//%24anEventId")
} }
@Test @Test
@@ -47,12 +47,12 @@ class DefaultDeepLinkCreatorTest {
val threadId = ThreadId("\$a/ThreadId") val threadId = ThreadId("\$a/ThreadId")
val eventId = EventId("\$an/EventId") val eventId = EventId("\$an/EventId")
assertThat(sut.create(sessionId, roomId, null, null)) assertThat(sut.create(sessionId, roomId, null, null))
.isEqualTo("elementx://open/%40a%2F%3Adomain/%21a%2FRoomId%3Adomain") .isEqualTo("healthchat://open/%40a%2F%3Adomain/%21a%2FRoomId%3Adomain")
assertThat(sut.create(sessionId, roomId, threadId, null)) assertThat(sut.create(sessionId, roomId, threadId, null))
.isEqualTo("elementx://open/%40a%2F%3Adomain/%21a%2FRoomId%3Adomain/%24a%2FThreadId") .isEqualTo("healthchat://open/%40a%2F%3Adomain/%21a%2FRoomId%3Adomain/%24a%2FThreadId")
assertThat(sut.create(sessionId, roomId, threadId, eventId)) assertThat(sut.create(sessionId, roomId, threadId, eventId))
.isEqualTo("elementx://open/%40a%2F%3Adomain/%21a%2FRoomId%3Adomain/%24a%2FThreadId/%24an%2FEventId") .isEqualTo("healthchat://open/%40a%2F%3Adomain/%21a%2FRoomId%3Adomain/%24a%2FThreadId/%24an%2FEventId")
assertThat(sut.create(sessionId, roomId, null, eventId)) assertThat(sut.create(sessionId, roomId, null, eventId))
.isEqualTo("elementx://open/%40a%2F%3Adomain/%21a%2FRoomId%3Adomain//%24an%2FEventId") .isEqualTo("healthchat://open/%40a%2F%3Adomain/%21a%2FRoomId%3Adomain//%24an%2FEventId")
} }
} }
@@ -27,17 +27,17 @@ import org.junit.Test
class DefaultDeeplinkParserTest : RobolectricTest() { class DefaultDeeplinkParserTest : RobolectricTest() {
companion object { companion object {
const val A_URI = const val A_URI =
"elementx://open/@alice:server.org" "healthchat://open/@alice:server.org"
const val A_URI_WITH_ROOM = const val A_URI_WITH_ROOM =
"elementx://open/@alice:server.org/!aRoomId:domain" "healthchat://open/@alice:server.org/!aRoomId:domain"
const val A_URI_WITH_ROOM_WITH_THREAD = const val A_URI_WITH_ROOM_WITH_THREAD =
"elementx://open/@alice:server.org/!aRoomId:domain/\$aThreadId" "healthchat://open/@alice:server.org/!aRoomId:domain/\$aThreadId"
const val A_URI_WITH_ROOM_WITH_THREAD_AND_EVENT = const val A_URI_WITH_ROOM_WITH_THREAD_AND_EVENT =
"elementx://open/@alice:server.org/!aRoomId:domain/\$aThreadId/\$anEventId" "healthchat://open/@alice:server.org/!aRoomId:domain/\$aThreadId/\$anEventId"
const val A_URI_WITH_ROOM_WITH_EVENT_AND_NO_THREAD = const val A_URI_WITH_ROOM_WITH_EVENT_AND_NO_THREAD =
"elementx://open/@alice:server.org/!aRoomId:domain//\$anEventId" "healthchat://open/@alice:server.org/!aRoomId:domain//\$anEventId"
const val A_URI_WITH_ROOM_WITH_THREAD_AND_EVENT_AND_INVALID_CHARACTERS = const val A_URI_WITH_ROOM_WITH_THREAD_AND_EVENT_AND_INVALID_CHARACTERS =
"elementx://open/@a%2Flice:server.org/!a%2FRoomId:domain/\$a%2FThreadId/\$an%2FEventId" "healthchat://open/@a%2Flice:server.org/!a%2FRoomId:domain/\$a%2FThreadId/\$an%2FEventId"
} }
@Test @Test
@@ -70,17 +70,17 @@ class DefaultDeeplinkParserTest : RobolectricTest() {
// Bad scheme // Bad scheme
assertThat(sut.getFromIntent(createIntent("x://open/@alice:server.org"))).isNull() assertThat(sut.getFromIntent(createIntent("x://open/@alice:server.org"))).isNull()
// Bad host // Bad host
assertThat(sut.getFromIntent(createIntent("elementx://close/@alice:server.org"))).isNull() assertThat(sut.getFromIntent(createIntent("healthchat://close/@alice:server.org"))).isNull()
// No session Id // No session Id
assertThat(sut.getFromIntent(createIntent("elementx://open"))).isNull() assertThat(sut.getFromIntent(createIntent("healthchat://open"))).isNull()
assertThrowsInDebug { assertThrowsInDebug {
// Invalid sessionId // Invalid sessionId
sut.getFromIntent(createIntent("elementx://open/alice:server.org")) sut.getFromIntent(createIntent("healthchat://open/alice:server.org"))
} }
assertThrowsInDebug { assertThrowsInDebug {
// Empty sessionId // Empty sessionId
sut.getFromIntent(createIntent("elementx://open//")) sut.getFromIntent(createIntent("healthchat://open//"))
} }
} }
@@ -22,25 +22,29 @@ import io.element.android.libraries.matrix.api.permalink.MatrixToConverter
@ContributesBinding(AppScope::class) @ContributesBinding(AppScope::class)
class DefaultMatrixToConverter : MatrixToConverter { class DefaultMatrixToConverter : MatrixToConverter {
/** /**
* Try to convert a URL from an element web instance or from a client permalink to a matrix.to url. * Try to convert a URL from a web client instance or from a client permalink to a matrix.to url.
* To be successfully converted, URL path should contain one of the [SUPPORTED_PATHS]. * To be successfully converted, URL path should contain one of the [SUPPORTED_PATHS].
* Examples: * Examples:
* - https://riot.im/develop/#/room/#element-android:matrix.org -> https://matrix.to/#/#element-android:matrix.org * - https://riot.im/develop/#/room/#element-android:matrix.org -> https://matrix.to/#/#element-android:matrix.org
* - https://app.element.io/#/room/#element-android:matrix.org -> https://matrix.to/#/#element-android:matrix.org * - https://app.healthchat.ch/#/room/#room:healthchat.ch -> https://matrix.to/#/#room:healthchat.ch
* - https://www.example.org/#/room/#element-android:matrix.org -> https://matrix.to/#/#element-android:matrix.org * - https://www.example.org/#/room/#element-android:matrix.org -> https://matrix.to/#/#element-android:matrix.org
* Also convert links coming from the matrix.to website: * Also convert links coming from the HealthChat matrix.to website, which uses the matrix.to URL
* - element://room/#element-android:matrix.org -> https://matrix.to/#/#element-android:matrix.org * format on its own host and hands out a custom scheme to Android:
* - element://user/@alice:matrix.org -> https://matrix.to/#/@alice:matrix.org * - https://to.healthchat.ch/#/#room:healthchat.ch -> https://matrix.to/#/#room:healthchat.ch
* - healthchat://room/#room:healthchat.ch -> https://matrix.to/#/#room:healthchat.ch
* - healthchat://user/@alice:healthchat.ch -> https://matrix.to/#/@alice:healthchat.ch
*/ */
override fun convert(uri: Uri): Uri? { override fun convert(uri: Uri): Uri? {
val uriString = uri.toString()
// Handle links coming from the matrix.to website.
.replacePrefix(MATRIX_TO_CUSTOM_SCHEME_BASE_URL, "https://app.element.io/#/")
val baseUrl = MatrixConfiguration.MATRIX_TO_PERMALINK_BASE_URL val baseUrl = MatrixConfiguration.MATRIX_TO_PERMALINK_BASE_URL
val uriString = uri.toString()
// Handle the custom scheme handed out by the HealthChat matrix.to website.
.replacePrefix(MATRIX_TO_CUSTOM_SCHEME_BASE_URL, MatrixConfiguration.HEALTHCHAT_WEB_BASE_URL)
// Our own matrix.to deployment already uses the matrix.to URL format.
.replacePrefix(MatrixConfiguration.HEALTHCHAT_TO_PERMALINK_BASE_URL, baseUrl)
return when { return when {
// URL is already a matrix.to // URL is already a matrix.to
uriString.startsWith(baseUrl) -> uri uriString.startsWith(baseUrl) -> uriString.toUri()
// Web or client url // Web or client url
SUPPORTED_PATHS.any { it in uriString } -> { SUPPORTED_PATHS.any { it in uriString } -> {
val path = SUPPORTED_PATHS.first { it in uriString } val path = SUPPORTED_PATHS.first { it in uriString }
@@ -52,7 +56,7 @@ class DefaultMatrixToConverter : MatrixToConverter {
} }
companion object { companion object {
private const val MATRIX_TO_CUSTOM_SCHEME_BASE_URL = "element://" private const val MATRIX_TO_CUSTOM_SCHEME_BASE_URL = "healthchat://"
private val SUPPORTED_PATHS = listOf( private val SUPPORTED_PATHS = listOf(
"/#/room/", "/#/room/",
"/#/user/", "/#/user/",
@@ -44,15 +44,46 @@ class DefaultMatrixToConverterTest : RobolectricTest() {
assertThat(DefaultMatrixToConverter().convert(url)).isNull() assertThat(DefaultMatrixToConverter().convert(url)).isNull()
} }
@Test
fun `converting the root url of the HealthChat matrix-to website returns null`() {
val url = Uri.parse("https://to.healthchat.ch/")
assertThat(DefaultMatrixToConverter().convert(url)).isNull()
}
@Test
fun `converting a url from the HealthChat matrix-to website returns a matrix-to url for room case`() {
val url = Uri.parse("https://to.healthchat.ch/#/#room:healthchat.ch")
assertThat(DefaultMatrixToConverter().convert(url)).isEqualTo(Uri.parse("https://matrix.to/#/#room:healthchat.ch"))
}
@Test
fun `converting a url from the HealthChat matrix-to website returns a matrix-to url for user case`() {
val url = Uri.parse("https://to.healthchat.ch/#/@alice:healthchat.ch")
assertThat(DefaultMatrixToConverter().convert(url)).isEqualTo(Uri.parse("https://matrix.to/#/@alice:healthchat.ch"))
}
@Test
fun `converting a url from the HealthChat web client returns a matrix-to url`() {
val url = Uri.parse("https://app.healthchat.ch/#/room/%23room%3Ahealthchat.ch")
assertThat(DefaultMatrixToConverter().convert(url)).isEqualTo(Uri.parse("https://matrix.to/#/%23room%3Ahealthchat.ch"))
}
@Test @Test
fun `converting url coming from the matrix-to website returns a matrix-to url for room case`() { fun `converting url coming from the matrix-to website returns a matrix-to url for room case`() {
val url = Uri.parse("element://room/#element-android:matrix.org") val url = Uri.parse("healthchat://room/#element-android:matrix.org")
assertThat(DefaultMatrixToConverter().convert(url)).isEqualTo(Uri.parse("https://matrix.to/#/#element-android:matrix.org")) assertThat(DefaultMatrixToConverter().convert(url)).isEqualTo(Uri.parse("https://matrix.to/#/#element-android:matrix.org"))
} }
@Test @Test
fun `converting url coming from the matrix-to website returns a matrix-to url for user case`() { fun `converting url coming from the matrix-to website returns a matrix-to url for user case`() {
val url = Uri.parse("element://user/@alice:matrix.org") val url = Uri.parse("healthchat://user/@alice:matrix.org")
assertThat(DefaultMatrixToConverter().convert(url)).isEqualTo(Uri.parse("https://matrix.to/#/@alice:matrix.org")) assertThat(DefaultMatrixToConverter().convert(url)).isEqualTo(Uri.parse("https://matrix.to/#/@alice:matrix.org"))
} }
@Test
fun `converting an event url coming from the matrix-to website keeps the event id and the via params`() {
val url = Uri.parse("healthchat://room/%23room%3Ahealthchat.ch/%24eventId?via=healthchat.ch")
assertThat(DefaultMatrixToConverter().convert(url))
.isEqualTo(Uri.parse("https://matrix.to/#/%23room%3Ahealthchat.ch/%24eventId?via=healthchat.ch"))
}
} }
+6 -6
View File
@@ -7,13 +7,13 @@
# Please see LICENSE files in the repository root for full details. # Please see LICENSE files in the repository root for full details.
# Format is: # Format is:
# elementx://open/{sessionId} to open a session # healthchat://open/{sessionId} to open a session
# elementx://open/{sessionId}/{roomId} to open a room # healthchat://open/{sessionId}/{roomId} to open a room
# elementx://open/{sessionId}/{roomId}/{eventId} to open a thread # healthchat://open/{sessionId}/{roomId}/{eventId} to open a thread
# Open a session # Open a session
# adb shell am start -a android.intent.action.VIEW -d elementx://open/@benoit10518:matrix.org # adb shell am start -a android.intent.action.VIEW -d healthchat://open/@benoit10518:matrix.org
# Open a room # Open a room
adb shell am start -a android.intent.action.VIEW -d elementx://open/@benoit10518:matrix.org/!dehdDVSkabQLZFYrgo:matrix.org adb shell am start -a android.intent.action.VIEW -d healthchat://open/@benoit10518:matrix.org/!dehdDVSkabQLZFYrgo:matrix.org
# Open a thread # Open a thread
# adb shell am start -a android.intent.action.VIEW -d elementx://open/@benoit10518:matrix.org/!dehdDVSkabQLZFYrgo:matrix.org/\\\$threadId # adb shell am start -a android.intent.action.VIEW -d healthchat://open/@benoit10518:matrix.org/!dehdDVSkabQLZFYrgo:matrix.org/\\\$threadId
@@ -6,4 +6,4 @@
# Please see LICENSE files in the repository root for full details. # Please see LICENSE files in the repository root for full details.
adb shell am start -a android.intent.action.VIEW \ adb shell am start -a android.intent.action.VIEW \
-d "element://room/%23element-android%3Amatrix.org" -d "healthchat://room/%23healthchat%3Ahealthchat.ch"
+1 -1
View File
@@ -7,4 +7,4 @@
# Please see LICENSE files in the repository root for full details. # Please see LICENSE files in the repository root for full details.
adb shell am start -a android.intent.action.VIEW -c android.intent.category.BROWSABLE \ adb shell am start -a android.intent.action.VIEW -c android.intent.category.BROWSABLE \
-d "https://app.element.io/#/room/!cuqHozLHNBgupgLMKN:matrix.org/%24LZDOueY3R8OD2ZYf8FLKtu95aF7imLBC3F5TIUj-4cc" -d "https://app.healthchat.ch/#/room/!cuqHozLHNBgupgLMKN:healthchat.ch/%24LZDOueY3R8OD2ZYf8FLKtu95aF7imLBC3F5TIUj-4cc"
+1 -1
View File
@@ -7,4 +7,4 @@
# Please see LICENSE files in the repository root for full details. # Please see LICENSE files in the repository root for full details.
adb shell am start -a android.intent.action.VIEW \ adb shell am start -a android.intent.action.VIEW \
-d "https://matrix.to/#/#matrix:matrix.org" -d "https://to.healthchat.ch/#/#healthchat:healthchat.ch"
-13
View File
@@ -1,13 +0,0 @@
#! /bin/bash
# Copyright (c) 2025 Element Creations Ltd.
# Copyright 2025 New Vector Ltd.
#
# SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
# Please see LICENSE files in the repository root for full details.
# Format is:
# https://mobile.element.io/element/?account_provider=example.org&login_hint=mxid:@alice:example.org
adb shell am start -a android.intent.action.VIEW \
-d "https://mobile.element.io/element/?account_provider=element.io\\&login_hint=mxid:@alice:element.io"