Split module deeplink to api and impl.

This commit is contained in:
Benoit Marty 2025-08-22 18:08:26 +02:00 committed by Benoit Marty
parent 1682fd4c2c
commit 4e5bbaf946
25 changed files with 150 additions and 85 deletions

View file

@ -0,0 +1,37 @@
import extension.setupAnvil
/*
* Copyright 2022-2024 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.
*/
plugins {
id("io.element.android-library")
}
android {
namespace = "io.element.android.libraries.deeplink.impl"
}
setupAnvil()
dependencies {
api(projects.libraries.deeplink.api)
implementation(projects.libraries.di)
implementation(libs.dagger)
implementation(libs.androidx.corektx)
implementation(projects.libraries.core)
implementation(projects.libraries.matrix.api)
implementation(projects.libraries.androidutils)
implementation(projects.libraries.architecture)
implementation(projects.libraries.uiStrings)
implementation(projects.services.toolbox.api)
testImplementation(libs.test.junit)
testImplementation(libs.test.truth)
testImplementation(libs.test.robolectric)
testImplementation(projects.libraries.matrix.test)
testImplementation(projects.tests.testutils)
}

View file

@ -0,0 +1,11 @@
/*
* Copyright 2023, 2024 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.
*/
package io.element.android.libraries.deeplink.impl
internal const val SCHEME = "elementx"
internal const val HOST = "open"

View file

@ -0,0 +1,34 @@
/*
* 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.
*/
package io.element.android.libraries.deeplink.impl
import com.squareup.anvil.annotations.ContributesBinding
import io.element.android.libraries.deeplink.api.DeepLinkCreator
import io.element.android.libraries.di.AppScope
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
@ContributesBinding(AppScope::class)
class DefaultDeepLinkCreator @Inject constructor() : DeepLinkCreator {
override fun room(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)
}
}
}
}
}

View file

@ -0,0 +1,45 @@
/*
* 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.
*/
package io.element.android.libraries.deeplink.impl
import android.content.Intent
import android.net.Uri
import com.squareup.anvil.annotations.ContributesBinding
import io.element.android.libraries.deeplink.api.DeeplinkData
import io.element.android.libraries.deeplink.api.DeeplinkParser
import io.element.android.libraries.di.AppScope
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
@ContributesBinding(AppScope::class)
class DefaultDeeplinkParser @Inject constructor() : DeeplinkParser {
override 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)?.let(::SessionId) ?: return null
return when (val screenPathComponent = pathBits.elementAtOrNull(1)) {
null -> DeeplinkData.Root(sessionId)
else -> {
val roomId = screenPathComponent.let(::RoomId)
val threadId = pathBits.elementAtOrNull(2)?.let(::ThreadId)
DeeplinkData.Room(sessionId, roomId, threadId)
}
}
}
}

View file

@ -0,0 +1,49 @@
/*
* 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.
*/
package io.element.android.libraries.deeplink.impl.usecase
import android.app.Activity
import com.squareup.anvil.annotations.ContributesBinding
import io.element.android.libraries.androidutils.system.startSharePlainTextIntent
import io.element.android.libraries.core.meta.BuildMeta
import io.element.android.libraries.deeplink.api.usecase.InviteFriendsUseCase
import io.element.android.libraries.di.SessionScope
import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.permalink.PermalinkBuilder
import io.element.android.libraries.ui.strings.CommonStrings
import io.element.android.services.toolbox.api.strings.StringProvider
import timber.log.Timber
import javax.inject.Inject
import io.element.android.libraries.androidutils.R as AndroidUtilsR
@ContributesBinding(SessionScope::class)
class DefaultInviteFriendsUseCase @Inject constructor(
private val stringProvider: StringProvider,
private val matrixClient: MatrixClient,
private val buildMeta: BuildMeta,
private val permalinkBuilder: PermalinkBuilder,
) : InviteFriendsUseCase {
override fun execute(activity: Activity) {
val permalinkResult = permalinkBuilder.permalinkForUser(matrixClient.sessionId)
permalinkResult.fold(
onSuccess = { permalink ->
val appName = buildMeta.applicationName
activity.startSharePlainTextIntent(
activityResultLauncher = null,
chooserTitle = stringProvider.getString(CommonStrings.action_invite_friends),
text = stringProvider.getString(CommonStrings.invite_friends_text, appName, permalink),
extraTitle = stringProvider.getString(CommonStrings.invite_friends_rich_title, appName),
noActivityFoundMessage = stringProvider.getString(AndroidUtilsR.string.error_no_compatible_app_found)
)
},
onFailure = {
Timber.e(it)
}
)
}
}

View file

@ -0,0 +1,27 @@
/*
* Copyright 2023, 2024 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.
*/
package io.element.android.libraries.deeplink.impl
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 DefaultDeepLinkCreatorTest {
@Test
fun room() {
val sut = DefaultDeepLinkCreator()
assertThat(sut.room(A_SESSION_ID, null, null))
.isEqualTo("elementx://open/@alice:server.org")
assertThat(sut.room(A_SESSION_ID, A_ROOM_ID, null))
.isEqualTo("elementx://open/@alice:server.org/!aRoomId:domain")
assertThat(sut.room(A_SESSION_ID, A_ROOM_ID, A_THREAD_ID))
.isEqualTo("elementx://open/@alice:server.org/!aRoomId:domain/\$aThreadId")
}
}

View file

@ -0,0 +1,70 @@
/*
* Copyright 2023, 2024 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.
*/
package io.element.android.libraries.deeplink.impl
import android.content.Intent
import androidx.core.net.toUri
import com.google.common.truth.Truth.assertThat
import io.element.android.libraries.deeplink.api.DeeplinkData
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.assertThrowsInDebug
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner
@RunWith(RobolectricTestRunner::class)
class DefaultDeeplinkParserTest {
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"
}
@Test
fun `nominal cases`() {
val sut = DefaultDeeplinkParser()
assertThat(sut.getFromIntent(createIntent(A_URI)))
.isEqualTo(DeeplinkData.Root(A_SESSION_ID))
assertThat(sut.getFromIntent(createIntent(A_URI_WITH_ROOM)))
.isEqualTo(DeeplinkData.Room(A_SESSION_ID, A_ROOM_ID, null))
assertThat(sut.getFromIntent(createIntent(A_URI_WITH_ROOM_WITH_THREAD)))
.isEqualTo(DeeplinkData.Room(A_SESSION_ID, A_ROOM_ID, A_THREAD_ID))
}
@Test
fun `error cases`() {
val sut = DefaultDeeplinkParser()
// 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()
assertThrowsInDebug {
// Invalid sessionId
sut.getFromIntent(createIntent("elementx://open/alice:server.org"))
}
assertThrowsInDebug {
// Empty sessionId
sut.getFromIntent(createIntent("elementx://open//"))
}
}
private fun createIntent(uri: String): Intent {
return Intent().apply {
action = Intent.ACTION_VIEW
data = uri.toUri()
}
}
}