diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index 772609f482..7a90fc62b8 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -206,6 +206,7 @@ dependencies {
allLibrariesImpl()
allServicesImpl()
allFeaturesImpl(rootDir)
+ implementation(projects.libraries.deeplink)
implementation(projects.tests.uitests)
implementation(projects.anvilannotations)
implementation(projects.appnav)
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 828788ed80..342e05532c 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -40,6 +40,14 @@
+
+
{
+ override fun init(node: MainNode) {
+ mainNode = node
+ mainNode.handleIntent(intent)
+ }
+ }
+ )
+ )
}
}
}
@@ -63,6 +79,8 @@ class MainActivity : NodeComponentActivity() {
override fun onNewIntent(intent: Intent?) {
super.onNewIntent(intent)
Timber.w("onNewIntent")
+ intent ?: return
+ mainNode.handleIntent(intent)
}
override fun onSaveInstanceState(outState: Bundle) {
diff --git a/app/src/main/kotlin/io/element/android/x/MainNode.kt b/app/src/main/kotlin/io/element/android/x/MainNode.kt
index 6b7dee92b8..fb551f326d 100644
--- a/app/src/main/kotlin/io/element/android/x/MainNode.kt
+++ b/app/src/main/kotlin/io/element/android/x/MainNode.kt
@@ -16,14 +16,17 @@
package io.element.android.x
+import android.content.Intent
import android.os.Parcelable
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
+import androidx.lifecycle.lifecycleScope
import com.bumble.appyx.core.composable.Children
import com.bumble.appyx.core.modality.BuildContext
import com.bumble.appyx.core.navigation.model.permanent.PermanentNavModel
import com.bumble.appyx.core.node.Node
import com.bumble.appyx.core.node.ParentNode
+import com.bumble.appyx.core.plugin.Plugin
import io.element.android.appnav.LoggedInFlowNode
import io.element.android.appnav.RoomFlowNode
import io.element.android.appnav.RootFlowNode
@@ -35,11 +38,13 @@ import io.element.android.libraries.matrix.api.room.MatrixRoom
import io.element.android.x.di.MainDaggerComponentsOwner
import io.element.android.x.di.RoomComponent
import io.element.android.x.di.SessionComponent
+import kotlinx.coroutines.launch
import kotlinx.parcelize.Parcelize
class MainNode(
buildContext: BuildContext,
private val mainDaggerComponentOwner: MainDaggerComponentsOwner,
+ plugins: List,
) :
ParentNode(
navModel = PermanentNavModel(
@@ -47,6 +52,7 @@ class MainNode(
savedStateMap = buildContext.savedStateMap,
),
buildContext = buildContext,
+ plugins = plugins,
),
DaggerComponentOwner by mainDaggerComponentOwner {
@@ -73,7 +79,13 @@ class MainNode(
}
override fun resolve(navTarget: RootNavTarget, buildContext: BuildContext): Node {
- return createNode(buildContext, plugins = listOf(loggedInFlowNodeCallback, roomFlowNodeCallback))
+ return createNode(
+ context = buildContext,
+ plugins = listOf(
+ loggedInFlowNodeCallback,
+ roomFlowNodeCallback,
+ )
+ )
}
@Composable
@@ -81,6 +93,12 @@ class MainNode(
Children(navModel = navModel)
}
+ fun handleIntent(intent: Intent) {
+ lifecycleScope.launch {
+ waitForChildAttached().handleIntent(intent)
+ }
+ }
+
@Parcelize
object RootNavTarget : Parcelable
}
diff --git a/app/src/main/kotlin/io/element/android/x/intent/IntentProviderImpl.kt b/app/src/main/kotlin/io/element/android/x/intent/IntentProviderImpl.kt
index b3c7aa98e0..e777b08906 100644
--- a/app/src/main/kotlin/io/element/android/x/intent/IntentProviderImpl.kt
+++ b/app/src/main/kotlin/io/element/android/x/intent/IntentProviderImpl.kt
@@ -18,7 +18,9 @@ package io.element.android.x.intent
import android.content.Context
import android.content.Intent
+import androidx.core.net.toUri
import com.squareup.anvil.annotations.ContributesBinding
+import io.element.android.libraries.deeplink.DeepLinkCreator
import io.element.android.libraries.di.AppScope
import io.element.android.libraries.di.ApplicationContext
import io.element.android.libraries.matrix.api.core.RoomId
@@ -28,17 +30,19 @@ import io.element.android.libraries.push.impl.intent.IntentProvider
import io.element.android.x.MainActivity
import javax.inject.Inject
-// TODO EAx change to deep-link.
@ContributesBinding(AppScope::class)
class IntentProviderImpl @Inject constructor(
@ApplicationContext private val context: Context,
+ private val deepLinkCreator: DeepLinkCreator,
) : IntentProvider {
- override fun getMainIntent(): Intent {
- return Intent(context, MainActivity::class.java)
- }
-
- override fun getIntent(sessionId: SessionId, roomId: RoomId?, threadId: ThreadId?): Intent {
- // TODO Handle deeplink or pass parameters
- return Intent(context, MainActivity::class.java)
+ override fun getViewIntent(
+ sessionId: SessionId,
+ roomId: RoomId?,
+ threadId: ThreadId?,
+ ): Intent {
+ return Intent(context, MainActivity::class.java).apply {
+ action = Intent.ACTION_VIEW
+ data = deepLinkCreator.create(sessionId, roomId, threadId).toUri()
+ }
}
}
diff --git a/appnav/build.gradle.kts b/appnav/build.gradle.kts
index 17efdc15fc..b672f582f9 100644
--- a/appnav/build.gradle.kts
+++ b/appnav/build.gradle.kts
@@ -43,6 +43,7 @@ dependencies {
implementation(projects.libraries.core)
implementation(projects.libraries.androidutils)
implementation(projects.libraries.architecture)
+ implementation(projects.libraries.deeplink)
implementation(projects.libraries.matrix.api)
implementation(projects.libraries.push.api)
implementation(projects.libraries.pushproviders.api)
diff --git a/appnav/src/main/kotlin/io/element/android/appnav/LoggedInFlowNode.kt b/appnav/src/main/kotlin/io/element/android/appnav/LoggedInFlowNode.kt
index 6f9319f923..fe2d8aa0dc 100644
--- a/appnav/src/main/kotlin/io/element/android/appnav/LoggedInFlowNode.kt
+++ b/appnav/src/main/kotlin/io/element/android/appnav/LoggedInFlowNode.kt
@@ -33,6 +33,7 @@ import com.bumble.appyx.core.plugin.plugins
import com.bumble.appyx.navmodel.backstack.BackStack
import com.bumble.appyx.navmodel.backstack.operation.push
import com.bumble.appyx.navmodel.backstack.operation.replace
+import com.bumble.appyx.navmodel.backstack.operation.singleTop
import dagger.assisted.Assisted
import dagger.assisted.AssistedInject
import io.element.android.anvilannotations.ContributesNode
@@ -56,10 +57,7 @@ import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.ui.di.MatrixUIBindings
import io.element.android.services.appnavstate.api.AppNavigationStateService
import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.runBlocking
import kotlinx.parcelize.Parcelize
-import kotlin.coroutines.coroutineContext
@ContributesNode(AppScope::class)
class LoggedInFlowNode @AssistedInject constructor(
@@ -217,6 +215,19 @@ class LoggedInFlowNode @AssistedInject constructor(
}
}
+ suspend fun attachRoot(): Node {
+ return attachChild {
+ backstack.singleTop(NavTarget.RoomList)
+ }
+ }
+
+ suspend fun attachRoom(roomId: RoomId): RoomFlowNode {
+ return attachChild {
+ backstack.singleTop(NavTarget.RoomList)
+ backstack.push(NavTarget.Room(roomId))
+ }
+ }
+
@Composable
override fun View(modifier: Modifier) {
Box(modifier = modifier) {
diff --git a/appnav/src/main/kotlin/io/element/android/appnav/RoomFlowNode.kt b/appnav/src/main/kotlin/io/element/android/appnav/RoomFlowNode.kt
index 69c02d500e..3609dbf57e 100644
--- a/appnav/src/main/kotlin/io/element/android/appnav/RoomFlowNode.kt
+++ b/appnav/src/main/kotlin/io/element/android/appnav/RoomFlowNode.kt
@@ -18,7 +18,6 @@ package io.element.android.appnav
import android.os.Parcelable
import androidx.compose.runtime.Composable
-import androidx.compose.runtime.DisposableEffect
import androidx.compose.ui.Modifier
import com.bumble.appyx.core.composable.Children
import com.bumble.appyx.core.lifecycle.subscribe
diff --git a/appnav/src/main/kotlin/io/element/android/appnav/RootFlowNode.kt b/appnav/src/main/kotlin/io/element/android/appnav/RootFlowNode.kt
index 8251584308..519c4e734b 100644
--- a/appnav/src/main/kotlin/io/element/android/appnav/RootFlowNode.kt
+++ b/appnav/src/main/kotlin/io/element/android/appnav/RootFlowNode.kt
@@ -17,6 +17,7 @@
package io.element.android.appnav
import android.app.Activity
+import android.content.Intent
import android.os.Parcelable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
@@ -45,6 +46,8 @@ import io.element.android.features.rageshake.api.bugreport.BugReportEntryPoint
import io.element.android.libraries.architecture.BackstackNode
import io.element.android.libraries.architecture.animation.rememberDefaultTransitionHandler
import io.element.android.libraries.architecture.createNode
+import io.element.android.libraries.deeplink.DeeplinkData
+import io.element.android.libraries.deeplink.DeeplinkParser
import io.element.android.libraries.designsystem.theme.components.CircularProgressIndicator
import io.element.android.libraries.di.AppScope
import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService
@@ -65,6 +68,7 @@ class RootFlowNode @AssistedInject constructor(
private val matrixClientsHolder: MatrixClientsHolder,
private val presenter: RootPresenter,
private val bugReportEntryPoint: BugReportEntryPoint,
+ private val deeplinkParser: DeeplinkParser,
) :
BackstackNode(
backstack = BackStack(
@@ -207,4 +211,30 @@ class RootFlowNode @AssistedInject constructor(
CircularProgressIndicator()
}
}
+
+ suspend fun handleIntent(intent: Intent) {
+ deeplinkParser.getFromIntent(intent)
+ ?.let { navigateTo(it) }
+ }
+
+ private suspend fun navigateTo(deeplinkData: DeeplinkData) {
+ Timber.d("Navigating to $deeplinkData")
+ attachSession(deeplinkData.sessionId)
+ .apply {
+ val roomId = deeplinkData.roomId
+ if (roomId == null) {
+ // In case room is not provided, ensure the app navigate back to the room list
+ attachRoot()
+ } else {
+ attachRoom(roomId)
+ // TODO .attachThread(deeplinkData.threadId)
+ }
+ }
+ }
+
+ private suspend fun attachSession(sessionId: SessionId): LoggedInFlowNode {
+ return attachChild {
+ backstack.newRoot(NavTarget.LoggedInFlow(sessionId))
+ }
+ }
}
diff --git a/libraries/deeplink/build.gradle.kts b/libraries/deeplink/build.gradle.kts
new file mode 100644
index 0000000000..3fe27bfd1c
--- /dev/null
+++ b/libraries/deeplink/build.gradle.kts
@@ -0,0 +1,43 @@
+/*
+ * Copyright (c) 2022 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.
+ */
+
+// TODO: Remove once https://youtrack.jetbrains.com/issue/KTIJ-19369 is fixed
+@Suppress("DSL_SCOPE_VIOLATION")
+plugins {
+ id("io.element.android-library")
+ alias(libs.plugins.anvil)
+}
+
+android {
+ namespace = "io.element.android.libraries.deeplink"
+}
+
+anvil {
+ generateDaggerFactories.set(true)
+}
+
+dependencies {
+ implementation(projects.libraries.di)
+ implementation(libs.dagger)
+ implementation(libs.androidx.corektx)
+ implementation(projects.libraries.matrix.api)
+
+ testImplementation(libs.test.junit)
+ testImplementation(libs.test.truth)
+ testImplementation(libs.test.robolectric)
+ testImplementation(projects.libraries.matrix.test)
+ testImplementation(projects.tests.testutils)
+}
diff --git a/libraries/deeplink/src/main/kotlin/io/element/android/libraries/deeplink/Constants.kt b/libraries/deeplink/src/main/kotlin/io/element/android/libraries/deeplink/Constants.kt
new file mode 100644
index 0000000000..df26ef2fa0
--- /dev/null
+++ b/libraries/deeplink/src/main/kotlin/io/element/android/libraries/deeplink/Constants.kt
@@ -0,0 +1,20 @@
+/*
+ * Copyright (c) 2023 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.libraries.deeplink
+
+internal const val SCHEME = "elementx"
+internal const val HOST = "open"
diff --git a/libraries/deeplink/src/main/kotlin/io/element/android/libraries/deeplink/DeepLinkCreator.kt b/libraries/deeplink/src/main/kotlin/io/element/android/libraries/deeplink/DeepLinkCreator.kt
new file mode 100644
index 0000000000..71aa7ebddd
--- /dev/null
+++ b/libraries/deeplink/src/main/kotlin/io/element/android/libraries/deeplink/DeepLinkCreator.kt
@@ -0,0 +1,39 @@
+/*
+ * Copyright (c) 2023 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.libraries.deeplink
+
+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.core.ThreadId
+import javax.inject.Inject
+
+class DeepLinkCreator @Inject constructor() {
+ fun create(sessionId: SessionId, roomId: RoomId?, threadId: ThreadId?): String {
+ return buildString {
+ append("$SCHEME://$HOST/")
+ append(sessionId.value)
+ if (roomId != null) {
+ append("/")
+ append(roomId.value)
+ if (threadId != null) {
+ append("/")
+ append(threadId.value)
+ }
+ }
+ }
+ }
+}
diff --git a/libraries/deeplink/src/main/kotlin/io/element/android/libraries/deeplink/DeeplinkData.kt b/libraries/deeplink/src/main/kotlin/io/element/android/libraries/deeplink/DeeplinkData.kt
new file mode 100644
index 0000000000..d393a37c16
--- /dev/null
+++ b/libraries/deeplink/src/main/kotlin/io/element/android/libraries/deeplink/DeeplinkData.kt
@@ -0,0 +1,27 @@
+/*
+ * Copyright (c) 2023 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.libraries.deeplink
+
+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.core.ThreadId
+
+data class DeeplinkData(
+ val sessionId: SessionId,
+ val roomId: RoomId? = null,
+ val threadId: ThreadId? = null,
+)
diff --git a/libraries/deeplink/src/main/kotlin/io/element/android/libraries/deeplink/DeeplinkParser.kt b/libraries/deeplink/src/main/kotlin/io/element/android/libraries/deeplink/DeeplinkParser.kt
new file mode 100644
index 0000000000..9f217f497e
--- /dev/null
+++ b/libraries/deeplink/src/main/kotlin/io/element/android/libraries/deeplink/DeeplinkParser.kt
@@ -0,0 +1,47 @@
+/*
+ * Copyright (c) 2023 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.libraries.deeplink
+
+import android.content.Intent
+import android.net.Uri
+import io.element.android.libraries.matrix.api.core.asRoomId
+import io.element.android.libraries.matrix.api.core.asSessionId
+import io.element.android.libraries.matrix.api.core.asThreadId
+import javax.inject.Inject
+
+class DeeplinkParser @Inject constructor() {
+ fun getFromIntent(intent: Intent): DeeplinkData? {
+ return intent
+ .takeIf { it.action == Intent.ACTION_VIEW }
+ ?.data
+ ?.toDeeplinkData()
+ }
+
+ private fun Uri.toDeeplinkData(): DeeplinkData? {
+ if (scheme != SCHEME) return null
+ if (host != HOST) return null
+ val pathBits = path.orEmpty().split("/").drop(1)
+ val sessionId = pathBits.elementAtOrNull(0)?.asSessionId() ?: return null
+ val roomId = pathBits.elementAtOrNull(1)?.asRoomId()
+ val threadId = pathBits.elementAtOrNull(2)?.asThreadId()
+ return DeeplinkData(
+ sessionId = sessionId,
+ roomId = roomId,
+ threadId = threadId,
+ )
+ }
+}
diff --git a/libraries/deeplink/src/test/kotlin/io/element/android/libraries/deeplink/DeepLinkCreatorTest.kt b/libraries/deeplink/src/test/kotlin/io/element/android/libraries/deeplink/DeepLinkCreatorTest.kt
new file mode 100644
index 0000000000..730bdde248
--- /dev/null
+++ b/libraries/deeplink/src/test/kotlin/io/element/android/libraries/deeplink/DeepLinkCreatorTest.kt
@@ -0,0 +1,37 @@
+/*
+ * Copyright (c) 2023 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.libraries.deeplink
+
+import com.google.common.truth.Truth.assertThat
+import io.element.android.libraries.matrix.test.A_ROOM_ID
+import io.element.android.libraries.matrix.test.A_SESSION_ID
+import io.element.android.libraries.matrix.test.A_THREAD_ID
+import org.junit.Test
+
+class DeepLinkCreatorTest {
+
+ @Test
+ fun create() {
+ val sut = DeepLinkCreator()
+ assertThat(sut.create(A_SESSION_ID, null, null))
+ .isEqualTo("elementx://open/@alice:server.org")
+ assertThat(sut.create(A_SESSION_ID, A_ROOM_ID, null))
+ .isEqualTo("elementx://open/@alice:server.org/!aRoomId:domain")
+ assertThat(sut.create(A_SESSION_ID, A_ROOM_ID, A_THREAD_ID))
+ .isEqualTo("elementx://open/@alice:server.org/!aRoomId:domain/\$aThreadId")
+ }
+}
diff --git a/libraries/deeplink/src/test/kotlin/io/element/android/libraries/deeplink/DeeplinkParserTest.kt b/libraries/deeplink/src/test/kotlin/io/element/android/libraries/deeplink/DeeplinkParserTest.kt
new file mode 100644
index 0000000000..259cd6fde1
--- /dev/null
+++ b/libraries/deeplink/src/test/kotlin/io/element/android/libraries/deeplink/DeeplinkParserTest.kt
@@ -0,0 +1,78 @@
+/*
+ * Copyright (c) 2023 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.libraries.deeplink
+
+import android.content.Intent
+import androidx.core.net.toUri
+import com.google.common.truth.Truth.assertThat
+import io.element.android.libraries.matrix.test.A_ROOM_ID
+import io.element.android.libraries.matrix.test.A_SESSION_ID
+import io.element.android.libraries.matrix.test.A_THREAD_ID
+import io.element.android.tests.testutils.assertNullOrThrow
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.robolectric.RobolectricTestRunner
+
+@RunWith(RobolectricTestRunner::class)
+class DeeplinkParserTest {
+ companion object {
+ const val A_URI =
+ "elementx://open/@alice:server.org"
+ const val A_URI_WITH_ROOM =
+ "elementx://open/@alice:server.org/!aRoomId:domain"
+ const val A_URI_WITH_ROOM_WITH_THREAD =
+ "elementx://open/@alice:server.org/!aRoomId:domain/\$aThreadId"
+ }
+
+ private val sut = DeeplinkParser()
+
+ @Test
+ fun `nominal cases`() {
+ assertThat(sut.getFromIntent(createIntent(A_URI)))
+ .isEqualTo(DeeplinkData(A_SESSION_ID, null, null))
+ assertThat(sut.getFromIntent(createIntent(A_URI_WITH_ROOM)))
+ .isEqualTo(DeeplinkData(A_SESSION_ID, A_ROOM_ID, null))
+ assertThat(sut.getFromIntent(createIntent(A_URI_WITH_ROOM_WITH_THREAD)))
+ .isEqualTo(DeeplinkData(A_SESSION_ID, A_ROOM_ID, A_THREAD_ID))
+ }
+
+ @Test
+ fun `error cases`() {
+ val sut = DeeplinkParser()
+ // Bad scheme
+ assertThat(sut.getFromIntent(createIntent("x://open/@alice:server.org"))).isNull()
+ // Bad host
+ assertThat(sut.getFromIntent(createIntent("elementx://close/@alice:server.org"))).isNull()
+ // No session Id
+ assertThat(sut.getFromIntent(createIntent("elementx://open"))).isNull()
+ // Invalid sessionId
+ assertNullOrThrow {
+ sut.getFromIntent(createIntent("elementx://open/alice:server.org"))
+ }
+ // Empty sessionId
+ assertNullOrThrow {
+ sut.getFromIntent(createIntent("elementx://open//"))
+ }
+ }
+
+ private fun createIntent(uri: String): Intent {
+ return Intent().apply {
+ action = Intent.ACTION_VIEW
+ data = uri.toUri()
+ }
+ }
+}
diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/intent/IntentProvider.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/intent/IntentProvider.kt
index 52abb3f6a4..ce2b1d3fce 100644
--- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/intent/IntentProvider.kt
+++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/intent/IntentProvider.kt
@@ -25,9 +25,7 @@ interface IntentProvider {
/**
* Provide an intent to start the application.
*/
- fun getMainIntent(): Intent
-
- fun getIntent(
+ fun getViewIntent(
sessionId: SessionId,
roomId: RoomId?,
threadId: ThreadId?,
diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationActionIds.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationActionIds.kt
index 56054bbef8..bc20d49917 100644
--- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationActionIds.kt
+++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationActionIds.kt
@@ -34,7 +34,6 @@ data class NotificationActionIds @Inject constructor(
val smartReply = "${buildMeta.applicationId}.NotificationActions.SMART_REPLY_ACTION"
val dismissSummary = "${buildMeta.applicationId}.NotificationActions.DISMISS_SUMMARY_ACTION"
val dismissRoom = "${buildMeta.applicationId}.NotificationActions.DISMISS_ROOM_NOTIF_ACTION"
- val tapToView = "${buildMeta.applicationId}.NotificationActions.TAP_TO_VIEW_ACTION"
val diagnostic = "${buildMeta.applicationId}.NotificationActions.DIAGNOSTIC"
val push = "${buildMeta.applicationId}.PUSH"
}
diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationUtils.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationUtils.kt
index add8fd74eb..8aeaa998ca 100755
--- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationUtils.kt
+++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationUtils.kt
@@ -482,15 +482,11 @@ class NotificationUtils @Inject constructor(
}
private fun buildOpenRoomIntent(sessionId: SessionId, roomId: RoomId): PendingIntent? {
- val roomIntent = intentProvider.getIntent(sessionId = sessionId, roomId = roomId, threadId = null)
- roomIntent.action = actionIds.tapToView
- // pending intent get reused by system, this will mess up the extra params, so put unique info to avoid that
- roomIntent.data = createIgnoredUri("openRoom?$sessionId&$roomId")
-
+ val intent = intentProvider.getViewIntent(sessionId = sessionId, roomId = roomId, threadId = null)
return PendingIntent.getActivity(
context,
clock.epochMillis().toInt(),
- roomIntent,
+ intent,
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
)
}
@@ -498,22 +494,17 @@ class NotificationUtils @Inject constructor(
private fun buildOpenThreadIntent(roomInfo: RoomEventGroupInfo, threadId: ThreadId?): PendingIntent? {
val sessionId = roomInfo.sessionId
val roomId = roomInfo.roomId
- val threadIntentTap = intentProvider.getIntent(sessionId = sessionId, roomId = roomId, threadId = threadId)
- threadIntentTap.action = actionIds.tapToView
- // pending intent get reused by system, this will mess up the extra params, so put unique info to avoid that
- threadIntentTap.data = createIgnoredUri("openThread?$sessionId&$roomId&$threadId")
-
+ val intent = intentProvider.getViewIntent(sessionId = sessionId, roomId = roomId, threadId = threadId)
return PendingIntent.getActivity(
context,
clock.epochMillis().toInt(),
- threadIntentTap,
+ intent,
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
)
}
private fun buildOpenHomePendingIntentForSummary(sessionId: SessionId): PendingIntent {
- val intent = intentProvider.getIntent(sessionId = sessionId, roomId = null, threadId = null)
- intent.data = createIgnoredUri("tapSummary?$sessionId")
+ val intent = intentProvider.getViewIntent(sessionId = sessionId, roomId = null, threadId = null)
return PendingIntent.getActivity(
context,
clock.epochMillis().toInt(),
diff --git a/tools/adb/deeplink.sh b/tools/adb/deeplink.sh
new file mode 100755
index 0000000000..5d50ec9409
--- /dev/null
+++ b/tools/adb/deeplink.sh
@@ -0,0 +1,28 @@
+#! /bin/bash
+#
+# Copyright (c) 2023 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.
+#
+
+# Format is:
+# elementx://open/{sessionId} to open a session
+# elementx://open/{sessionId}/{roomId} to open a room
+# elementx://open/{sessionId}/{roomId}/{eventId} to open a thread
+
+# Open a session
+# adb shell am start -a android.intent.action.VIEW -d elementx://open/@benoit10518:matrix.org
+# Open a room
+adb shell am start -a android.intent.action.VIEW -d elementx://open/@benoit10518:matrix.org/!dehdDVSkabQLZFYrgo:matrix.org
+# Open a thread
+# adb shell am start -a android.intent.action.VIEW -d elementx://open/@benoit10518:matrix.org/!dehdDVSkabQLZFYrgo:matrix.org/\\\$threadId