mirror of
https://github.com/Novaloop-AG/element-x-android.git
synced 2026-10-31 08:15:02 +00:00
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:
@@ -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
@@ -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.
|
||||||
|
|||||||
+7
-12
@@ -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,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
+3
-55
@@ -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,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+3
-1
@@ -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"
|
||||||
|
|||||||
+9
-9
@@ -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")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+10
-10
@@ -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//"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+14
-10
@@ -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/",
|
||||||
|
|||||||
+33
-2
@@ -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"))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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"
|
||||||
@@ -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"
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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"
|
|
||||||
Reference in New Issue
Block a user