Merge pull request #4027 from vector-im/feature/fre/permalink

Add client base url support for permalinks
This commit is contained in:
Benoit Marty 2021-09-29 17:08:12 +02:00 committed by GitHub
commit c0adde56df
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
15 changed files with 305 additions and 262 deletions

1
changelog.d/4027.feature Normal file
View file

@ -0,0 +1 @@
Add client base url config to customize permalinks

View file

@ -32,8 +32,16 @@ data class MatrixConfiguration(
"https://scalar-staging.riot.im/scalar/api" "https://scalar-staging.riot.im/scalar/api"
), ),
/** /**
* Optional proxy to connect to the matrix servers * Optional base url to create client permalinks (eg. https://www.example.com/#/) instead of Matrix ones (matrix.to links).
* You can create one using for instance Proxy(proxyType, InetSocketAddress.createUnresolved(hostname, port) * Do not forget to add the "#" which is required by the permalink parser.
*
* Note: this field is only used for permalinks creation, you will also have to edit the string-array `permalink_supported_hosts` in the config file
* and add it to your manifest to handle these links in the application.
*/
val clientPermalinkBaseUrl: String? = null,
/**
* Optional proxy to connect to the matrix servers.
* You can create one using for instance Proxy(proxyType, InetSocketAddress.createUnresolved(hostname, port).
*/ */
val proxy: Proxy? = null, val proxy: Proxy? = null,
/** /**
@ -47,7 +55,7 @@ data class MatrixConfiguration(
) { ) {
/** /**
* Can be implemented by your Application class * Can be implemented by your Application class.
*/ */
interface Provider { interface Provider {
fun providesMatrixConfiguration(): MatrixConfiguration fun providesMatrixConfiguration(): MatrixConfiguration

View file

@ -0,0 +1,55 @@
/*
* Copyright 2020 The Matrix.org Foundation C.I.C.
*
* 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 org.matrix.android.sdk.api.session.permalinks
import android.net.Uri
/**
* Mapping of an input URI to a matrix.to compliant URI.
*/
object MatrixToConverter {
/**
* Try to convert a URL from an element web instance or from a client permalink to a matrix.to url.
* To be successfully converted, URL path should contain one of the [SUPPORTED_PATHS].
* Examples:
* - 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://www.example.org/#/room/#element-android:matrix.org -> https://matrix.to/#/#element-android:matrix.org
*/
fun convert(uri: Uri): Uri? {
val uriString = uri.toString()
return when {
// URL is already a matrix.to
uriString.startsWith(PermalinkService.MATRIX_TO_URL_BASE) -> uri
// Web or client url
SUPPORTED_PATHS.any { it in uriString } -> {
val path = SUPPORTED_PATHS.first { it in uriString }
Uri.parse(PermalinkService.MATRIX_TO_URL_BASE + uriString.substringAfter(path))
}
// URL is not supported
else -> null
}
}
private val SUPPORTED_PATHS = listOf(
"/#/room/",
"/#/user/",
"/#/group/"
)
}

View file

@ -26,6 +26,7 @@ import java.net.URLDecoder
* This class turns a uri to a [PermalinkData] * This class turns a uri to a [PermalinkData]
* element-based domains (e.g. https://app.element.io/#/user/@chagai95:matrix.org) permalinks * element-based domains (e.g. https://app.element.io/#/user/@chagai95:matrix.org) permalinks
* or matrix.to permalinks (e.g. https://matrix.to/#/@chagai95:matrix.org) * or matrix.to permalinks (e.g. https://matrix.to/#/@chagai95:matrix.org)
* or client permalinks (e.g. <clientPermalinkBaseUrl>user/@chagai95:matrix.org)
*/ */
object PermalinkParser { object PermalinkParser {
@ -42,13 +43,15 @@ object PermalinkParser {
* https://github.com/matrix-org/matrix-doc/blob/master/proposals/1704-matrix.to-permalinks.md * https://github.com/matrix-org/matrix-doc/blob/master/proposals/1704-matrix.to-permalinks.md
*/ */
fun parse(uri: Uri): PermalinkData { fun parse(uri: Uri): PermalinkData {
if (!uri.toString().startsWith(PermalinkService.MATRIX_TO_URL_BASE)) { // the client or element-based domain permalinks (e.g. https://app.element.io/#/user/@chagai95:matrix.org) don't have the
return PermalinkData.FallbackLink(uri) // mxid in the first param (like matrix.to does - https://matrix.to/#/@chagai95:matrix.org) but rather in the second after /user/ so /user/mxid
} // so convert URI to matrix.to to simplify parsing process
val matrixToUri = MatrixToConverter.convert(uri) ?: return PermalinkData.FallbackLink(uri)
// We can't use uri.fragment as it is decoding to early and it will break the parsing // We can't use uri.fragment as it is decoding to early and it will break the parsing
// of parameters that represents url (like signurl) // of parameters that represents url (like signurl)
val fragment = uri.toString().substringAfter("#") // uri.fragment val fragment = matrixToUri.toString().substringAfter("#") // uri.fragment
if (fragment.isNullOrEmpty()) { if (fragment.isEmpty()) {
return PermalinkData.FallbackLink(uri) return PermalinkData.FallbackLink(uri)
} }
val safeFragment = fragment.substringBefore('?') val safeFragment = fragment.substringBefore('?')
@ -61,20 +64,14 @@ object PermalinkParser {
.map { URLDecoder.decode(it, "UTF-8") } .map { URLDecoder.decode(it, "UTF-8") }
.take(2) .take(2)
// the element-based domain permalinks (e.g. https://app.element.io/#/user/@chagai95:matrix.org) don't have the val identifier = params.getOrNull(0)
// mxid in the first param (like matrix.to does - https://matrix.to/#/@chagai95:matrix.org) but rather in the second after /user/ so /user/mxid
var identifier = params.getOrNull(0)
if (identifier.equals("user")) {
identifier = params.getOrNull(1)
}
val extraParameter = params.getOrNull(1) val extraParameter = params.getOrNull(1)
return when { return when {
identifier.isNullOrEmpty() -> PermalinkData.FallbackLink(uri) identifier.isNullOrEmpty() -> PermalinkData.FallbackLink(uri)
MatrixPatterns.isUserId(identifier) -> PermalinkData.UserLink(userId = identifier) MatrixPatterns.isUserId(identifier) -> PermalinkData.UserLink(userId = identifier)
MatrixPatterns.isGroupId(identifier) -> PermalinkData.GroupLink(groupId = identifier) MatrixPatterns.isGroupId(identifier) -> PermalinkData.GroupLink(groupId = identifier)
MatrixPatterns.isRoomId(identifier) -> { MatrixPatterns.isRoomId(identifier) -> {
handleRoomIdCase(fragment, identifier, uri, extraParameter, viaQueryParameters) handleRoomIdCase(fragment, identifier, matrixToUri, extraParameter, viaQueryParameters)
} }
MatrixPatterns.isRoomAlias(identifier) -> { MatrixPatterns.isRoomAlias(identifier) -> {
PermalinkData.RoomLink( PermalinkData.RoomLink(
@ -125,12 +122,13 @@ object PermalinkParser {
} }
} }
private fun safeExtractParams(fragment: String) = fragment.substringAfter("?").split('&').mapNotNull { private fun safeExtractParams(fragment: String) =
val splitNameValue = it.split("=") fragment.substringAfter("?").split('&').mapNotNull {
if (splitNameValue.size == 2) { val splitNameValue = it.split("=")
Pair(splitNameValue[0], URLDecoder.decode(splitNameValue[1], "UTF-8")) if (splitNameValue.size == 2) {
} else null Pair(splitNameValue[0], URLDecoder.decode(splitNameValue[1], "UTF-8"))
} } else null
}
private fun String.getViaParameters(): List<String> { private fun String.getViaParameters(): List<String> {
return UrlQuerySanitizer(this) return UrlQuerySanitizer(this)
@ -138,9 +136,7 @@ object PermalinkParser {
.filter { .filter {
it.mParameter == "via" it.mParameter == "via"
}.map { }.map {
it.mValue.let { URLDecoder.decode(it.mValue, "UTF-8")
URLDecoder.decode(it, "UTF-8")
}
} }
} }
} }

View file

@ -19,7 +19,8 @@ package org.matrix.android.sdk.api.session.permalinks
import org.matrix.android.sdk.api.session.events.model.Event import org.matrix.android.sdk.api.session.events.model.Event
/** /**
* Useful methods to create Matrix permalink (matrix.to links). * Useful methods to create permalink (like matrix.to links or client permalinks).
* See [org.matrix.android.sdk.api.MatrixConfiguration.clientPermalinkBaseUrl] to setup a custom permalink base url.
*/ */
interface PermalinkService { interface PermalinkService {
@ -32,10 +33,11 @@ interface PermalinkService {
* Ex: "https://matrix.to/#/!nbzmcXAqpxBXjAdgoX:matrix.org/$1531497316352799BevdV:matrix.org" * Ex: "https://matrix.to/#/!nbzmcXAqpxBXjAdgoX:matrix.org/$1531497316352799BevdV:matrix.org"
* *
* @param event the event * @param event the event
* @param forceMatrixTo whether we should force using matrix.to base URL
* *
* @return the permalink, or null in case of error * @return the permalink, or null in case of error
*/ */
fun createPermalink(event: Event): String? fun createPermalink(event: Event, forceMatrixTo: Boolean = false): String?
/** /**
* Creates a permalink for an id (can be a user Id, etc.). * Creates a permalink for an id (can be a user Id, etc.).
@ -43,18 +45,21 @@ interface PermalinkService {
* Ex: "https://matrix.to/#/@benoit:matrix.org" * Ex: "https://matrix.to/#/@benoit:matrix.org"
* *
* @param id the id * @param id the id
* @param forceMatrixTo whether we should force using matrix.to base URL
*
* @return the permalink, or null in case of error * @return the permalink, or null in case of error
*/ */
fun createPermalink(id: String): String? fun createPermalink(id: String, forceMatrixTo: Boolean = false): String?
/** /**
* Creates a permalink for a roomId, including the via parameters * Creates a permalink for a roomId, including the via parameters
* *
* @param roomId the room id * @param roomId the room id
* @param forceMatrixTo whether we should force using matrix.to base URL
* *
* @return the permalink, or null in case of error * @return the permalink, or null in case of error
*/ */
fun createRoomPermalink(roomId: String, viaServers: List<String>? = null): String? fun createRoomPermalink(roomId: String, viaServers: List<String>? = null, forceMatrixTo: Boolean = false): String?
/** /**
* Creates a permalink for an event. If you have an event you can use [createPermalink] * Creates a permalink for an event. If you have an event you can use [createPermalink]
@ -62,10 +67,11 @@ interface PermalinkService {
* *
* @param roomId the id of the room * @param roomId the id of the room
* @param eventId the id of the event * @param eventId the id of the event
* @param forceMatrixTo whether we should force using matrix.to base URL
* *
* @return the permalink * @return the permalink
*/ */
fun createPermalink(roomId: String, eventId: String): String fun createPermalink(roomId: String, eventId: String, forceMatrixTo: Boolean = false): String
/** /**
* Extract the linked id from the universal link * Extract the linked id from the universal link

View file

@ -18,33 +18,29 @@ package org.matrix.android.sdk.internal.session.permalinks
import org.matrix.android.sdk.api.session.events.model.Event import org.matrix.android.sdk.api.session.events.model.Event
import org.matrix.android.sdk.api.session.permalinks.PermalinkService import org.matrix.android.sdk.api.session.permalinks.PermalinkService
import org.matrix.android.sdk.api.session.permalinks.PermalinkService.Companion.MATRIX_TO_URL_BASE
import javax.inject.Inject import javax.inject.Inject
internal class DefaultPermalinkService @Inject constructor( internal class DefaultPermalinkService @Inject constructor(
private val permalinkFactory: PermalinkFactory private val permalinkFactory: PermalinkFactory
) : PermalinkService { ) : PermalinkService {
override fun createPermalink(event: Event): String? { override fun createPermalink(event: Event, forceMatrixTo: Boolean): String? {
return permalinkFactory.createPermalink(event) return permalinkFactory.createPermalink(event, forceMatrixTo)
} }
override fun createPermalink(id: String): String? { override fun createPermalink(id: String, forceMatrixTo: Boolean): String? {
return permalinkFactory.createPermalink(id) return permalinkFactory.createPermalink(id, forceMatrixTo)
} }
override fun createRoomPermalink(roomId: String, viaServers: List<String>?): String? { override fun createRoomPermalink(roomId: String, viaServers: List<String>?, forceMatrixTo: Boolean): String? {
return permalinkFactory.createRoomPermalink(roomId, viaServers) return permalinkFactory.createRoomPermalink(roomId, viaServers, forceMatrixTo)
} }
override fun createPermalink(roomId: String, eventId: String): String { override fun createPermalink(roomId: String, eventId: String, forceMatrixTo: Boolean): String {
return permalinkFactory.createPermalink(roomId, eventId) return permalinkFactory.createPermalink(roomId, eventId, forceMatrixTo)
} }
override fun getLinkedId(url: String): String? { override fun getLinkedId(url: String): String? {
return url return permalinkFactory.getLinkedId(url)
.takeIf { it.startsWith(MATRIX_TO_URL_BASE) }
?.substring(MATRIX_TO_URL_BASE.length)
?.substringBeforeLast("?")
} }
} }

View file

@ -16,7 +16,11 @@
package org.matrix.android.sdk.internal.session.permalinks package org.matrix.android.sdk.internal.session.permalinks
import org.matrix.android.sdk.api.MatrixConfiguration
import org.matrix.android.sdk.api.MatrixPatterns
import org.matrix.android.sdk.api.session.events.model.Event import org.matrix.android.sdk.api.session.events.model.Event
import org.matrix.android.sdk.api.session.permalinks.PermalinkData
import org.matrix.android.sdk.api.session.permalinks.PermalinkParser
import org.matrix.android.sdk.api.session.permalinks.PermalinkService.Companion.MATRIX_TO_URL_BASE import org.matrix.android.sdk.api.session.permalinks.PermalinkService.Companion.MATRIX_TO_URL_BASE
import org.matrix.android.sdk.internal.di.UserId import org.matrix.android.sdk.internal.di.UserId
import javax.inject.Inject import javax.inject.Inject
@ -24,28 +28,44 @@ import javax.inject.Inject
internal class PermalinkFactory @Inject constructor( internal class PermalinkFactory @Inject constructor(
@UserId @UserId
private val userId: String, private val userId: String,
private val viaParameterFinder: ViaParameterFinder private val viaParameterFinder: ViaParameterFinder,
private val matrixConfiguration: MatrixConfiguration
) { ) {
fun createPermalink(event: Event): String? { fun createPermalink(event: Event, forceMatrixTo: Boolean): String? {
if (event.roomId.isNullOrEmpty() || event.eventId.isNullOrEmpty()) { if (event.roomId.isNullOrEmpty() || event.eventId.isNullOrEmpty()) {
return null return null
} }
return createPermalink(event.roomId, event.eventId) return createPermalink(event.roomId, event.eventId, forceMatrixTo)
} }
fun createPermalink(id: String): String? { fun createPermalink(id: String, forceMatrixTo: Boolean): String? {
return if (id.isEmpty()) { return when {
null id.isEmpty() -> null
} else MATRIX_TO_URL_BASE + escape(id) !useClientFormat(forceMatrixTo) -> MATRIX_TO_URL_BASE + escape(id)
else -> {
buildString {
append(matrixConfiguration.clientPermalinkBaseUrl)
when {
MatrixPatterns.isRoomId(id) || MatrixPatterns.isRoomAlias(id) -> append(ROOM_PATH)
MatrixPatterns.isUserId(id) -> append(USER_PATH)
MatrixPatterns.isGroupId(id) -> append(GROUP_PATH)
}
append(escape(id))
}
}
}
} }
fun createRoomPermalink(roomId: String, via: List<String>? = null): String? { fun createRoomPermalink(roomId: String, via: List<String>? = null, forceMatrixTo: Boolean): String? {
return if (roomId.isEmpty()) { return if (roomId.isEmpty()) {
null null
} else { } else {
buildString { buildString {
append(MATRIX_TO_URL_BASE) append(baseUrl(forceMatrixTo))
if (useClientFormat(forceMatrixTo)) {
append(ROOM_PATH)
}
append(escape(roomId)) append(escape(roomId))
append( append(
via?.takeIf { it.isNotEmpty() }?.let { viaParameterFinder.asUrlViaParameters(it) } via?.takeIf { it.isNotEmpty() }?.let { viaParameterFinder.asUrlViaParameters(it) }
@ -55,16 +75,34 @@ internal class PermalinkFactory @Inject constructor(
} }
} }
fun createPermalink(roomId: String, eventId: String): String { fun createPermalink(roomId: String, eventId: String, forceMatrixTo: Boolean): String {
return MATRIX_TO_URL_BASE + escape(roomId) + "/" + escape(eventId) + viaParameterFinder.computeViaParams(userId, roomId) return buildString {
append(baseUrl(forceMatrixTo))
if (useClientFormat(forceMatrixTo)) {
append(ROOM_PATH)
}
append(escape(roomId))
append("/")
append(escape(eventId))
append(viaParameterFinder.computeViaParams(userId, roomId))
}
} }
fun getLinkedId(url: String): String? { fun getLinkedId(url: String): String? {
val isSupported = url.startsWith(MATRIX_TO_URL_BASE) val clientBaseUrl = matrixConfiguration.clientPermalinkBaseUrl
return when {
return if (isSupported) { url.startsWith(MATRIX_TO_URL_BASE) -> url.substring(MATRIX_TO_URL_BASE.length)
url.substring(MATRIX_TO_URL_BASE.length) clientBaseUrl != null && url.startsWith(clientBaseUrl) -> {
} else null when (PermalinkParser.parse(url)) {
is PermalinkData.GroupLink -> url.substring(clientBaseUrl.length + GROUP_PATH.length)
is PermalinkData.RoomLink -> url.substring(clientBaseUrl.length + ROOM_PATH.length)
is PermalinkData.UserLink -> url.substring(clientBaseUrl.length + USER_PATH.length)
else -> null
}
}
else -> null
}
?.substringBeforeLast("?")
} }
/** /**
@ -86,4 +124,28 @@ internal class PermalinkFactory @Inject constructor(
private fun unescape(id: String): String { private fun unescape(id: String): String {
return id.replace("%2F", "/") return id.replace("%2F", "/")
} }
/**
* Get the permalink base URL according to the potential one in [MatrixConfiguration.clientPermalinkBaseUrl]
* and the [forceMatrixTo] parameter.
*
* @param forceMatrixTo whether we should force using matrix.to base URL.
*
* @return the permalink base URL.
*/
private fun baseUrl(forceMatrixTo: Boolean): String {
return matrixConfiguration.clientPermalinkBaseUrl
?.takeUnless { forceMatrixTo }
?: MATRIX_TO_URL_BASE
}
private fun useClientFormat(forceMatrixTo: Boolean): Boolean {
return !forceMatrixTo && matrixConfiguration.clientPermalinkBaseUrl != null
}
companion object {
private const val ROOM_PATH = "room/"
private const val USER_PATH = "user/"
private const val GROUP_PATH = "group/"
}
} }

View file

@ -165,8 +165,8 @@ internal class LocalEchoEventFactory @Inject constructor(
newBodyAutoMarkdown: Boolean, newBodyAutoMarkdown: Boolean,
msgType: String, msgType: String,
compatibilityText: String): Event { compatibilityText: String): Event {
val permalink = permalinkFactory.createPermalink(roomId, originalEvent.root.eventId ?: "") val permalink = permalinkFactory.createPermalink(roomId, originalEvent.root.eventId ?: "", false)
val userLink = originalEvent.root.senderId?.let { permalinkFactory.createPermalink(it) } ?: "" val userLink = originalEvent.root.senderId?.let { permalinkFactory.createPermalink(it, false) } ?: ""
val body = bodyForReply(originalEvent.getLastMessageContent(), originalEvent.isReply()) val body = bodyForReply(originalEvent.getLastMessageContent(), originalEvent.isReply())
val replyFormatted = REPLY_PATTERN.format( val replyFormatted = REPLY_PATTERN.format(
@ -350,9 +350,9 @@ internal class LocalEchoEventFactory @Inject constructor(
autoMarkdown: Boolean): Event? { autoMarkdown: Boolean): Event? {
// Fallbacks and event representation // Fallbacks and event representation
// TODO Add error/warning logs when any of this is null // TODO Add error/warning logs when any of this is null
val permalink = permalinkFactory.createPermalink(eventReplied.root) ?: return null val permalink = permalinkFactory.createPermalink(eventReplied.root, false) ?: return null
val userId = eventReplied.root.senderId ?: return null val userId = eventReplied.root.senderId ?: return null
val userLink = permalinkFactory.createPermalink(userId) ?: return null val userLink = permalinkFactory.createPermalink(userId, false) ?: return null
val body = bodyForReply(eventReplied.getLastMessageContent(), eventReplied.isReply()) val body = bodyForReply(eventReplied.getLastMessageContent(), eventReplied.isReply())
val replyFormatted = REPLY_PATTERN.format( val replyFormatted = REPLY_PATTERN.format(

View file

@ -105,8 +105,8 @@
<activity android:name=".features.home.HomeActivity" /> <activity android:name=".features.home.HomeActivity" />
<activity <activity
android:name=".features.login.LoginActivity" android:name=".features.login.LoginActivity"
android:launchMode="singleTask"
android:enabled="@bool/useLoginV1" android:enabled="@bool/useLoginV1"
android:launchMode="singleTask"
android:windowSoftInputMode="adjustResize"> android:windowSoftInputMode="adjustResize">
<!-- Add intent filter to handle redirection URL after SSO login in external browser --> <!-- Add intent filter to handle redirection URL after SSO login in external browser -->
<intent-filter> <intent-filter>
@ -122,8 +122,8 @@
</activity> </activity>
<activity <activity
android:name=".features.login2.LoginActivity2" android:name=".features.login2.LoginActivity2"
android:launchMode="singleTask"
android:enabled="@bool/useLoginV2" android:enabled="@bool/useLoginV2"
android:launchMode="singleTask"
android:windowSoftInputMode="adjustResize"> android:windowSoftInputMode="adjustResize">
<!-- Add intent filter to handle redirection URL after SSO login in external browser --> <!-- Add intent filter to handle redirection URL after SSO login in external browser -->
<intent-filter> <intent-filter>
@ -180,7 +180,12 @@
<activity android:name=".features.createdirect.CreateDirectRoomActivity" /> <activity android:name=".features.createdirect.CreateDirectRoomActivity" />
<activity android:name=".features.invite.InviteUsersToRoomActivity" /> <activity android:name=".features.invite.InviteUsersToRoomActivity" />
<activity android:name=".features.webview.VectorWebViewActivity" /> <activity android:name=".features.webview.VectorWebViewActivity" />
<activity android:name=".features.link.LinkHandlerActivity">
<!-- Activity to intercept links coming from a web instance -->
<activity
android:name=".features.link.LinkHandlerActivity"
android:enabled="true"
android:exported="true">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.VIEW" /> <action android:name="android.intent.action.VIEW" />
@ -196,6 +201,32 @@
</intent-filter> </intent-filter>
</activity> </activity>
<!-- Activity alias for matrix.to or element permalinks -->
<activity-alias
android:name=".features.permalink.PermalinkHandlerActivity"
android:enabled="true"
android:exported="true"
android:launchMode="singleTask"
android:targetActivity=".features.link.LinkHandlerActivity">
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="http" />
<data android:scheme="https" />
<data android:host="matrix.to" />
<data
android:host="user"
android:scheme="element" />
<data
android:host="room"
android:scheme="element" />
</intent-filter>
</activity-alias>
<activity <activity
android:name=".features.share.IncomingShareActivity" android:name=".features.share.IncomingShareActivity"
android:parentActivityName=".features.home.HomeActivity"> android:parentActivityName=".features.home.HomeActivity">
@ -230,27 +261,6 @@
<activity <activity
android:name=".features.signout.soft.SoftLogoutActivity" android:name=".features.signout.soft.SoftLogoutActivity"
android:windowSoftInputMode="adjustResize" /> android:windowSoftInputMode="adjustResize" />
<activity
android:name=".features.permalink.PermalinkHandlerActivity"
android:launchMode="singleTask">
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="http" />
<data android:scheme="https" />
<data android:host="matrix.to" />
<data
android:host="user"
android:scheme="element" />
<data
android:host="room"
android:scheme="element" />
</intent-filter>
</activity>
<activity <activity
android:name=".features.roommemberprofile.RoomMemberProfileActivity" android:name=".features.roommemberprofile.RoomMemberProfileActivity"
@ -271,12 +281,12 @@
android:name=".features.attachments.preview.AttachmentsPreviewActivity" android:name=".features.attachments.preview.AttachmentsPreviewActivity"
android:theme="@style/Theme.Vector.Black.AttachmentsPreview" /> android:theme="@style/Theme.Vector.Black.AttachmentsPreview" />
<activity <activity
android:supportsPictureInPicture="true"
android:configChanges="screenSize|smallestScreenSize|screenLayout|orientation"
android:name=".features.call.VectorCallActivity" android:name=".features.call.VectorCallActivity"
android:configChanges="screenSize|smallestScreenSize|screenLayout|orientation"
android:excludeFromRecents="true"
android:launchMode="singleTask" android:launchMode="singleTask"
android:taskAffinity=".features.call.VectorCallActivity" android:supportsPictureInPicture="true"
android:excludeFromRecents="true" /> android:taskAffinity=".features.call.VectorCallActivity" />
<!-- PIP Support https://developer.android.com/guide/topics/ui/picture-in-picture --> <!-- PIP Support https://developer.android.com/guide/topics/ui/picture-in-picture -->
<activity <activity
android:name=".features.call.conference.VectorJitsiActivity" android:name=".features.call.conference.VectorJitsiActivity"

View file

@ -62,7 +62,6 @@ import im.vector.app.features.matrixto.MatrixToBottomSheet
import im.vector.app.features.media.BigImageViewerActivity import im.vector.app.features.media.BigImageViewerActivity
import im.vector.app.features.media.VectorAttachmentViewerActivity import im.vector.app.features.media.VectorAttachmentViewerActivity
import im.vector.app.features.navigation.Navigator import im.vector.app.features.navigation.Navigator
import im.vector.app.features.permalink.PermalinkHandlerActivity
import im.vector.app.features.pin.PinLocker import im.vector.app.features.pin.PinLocker
import im.vector.app.features.qrcode.QrCodeScannerActivity import im.vector.app.features.qrcode.QrCodeScannerActivity
import im.vector.app.features.rageshake.BugReportActivity import im.vector.app.features.rageshake.BugReportActivity
@ -155,7 +154,6 @@ interface ScreenComponent {
fun inject(activity: CreateDirectRoomActivity) fun inject(activity: CreateDirectRoomActivity)
fun inject(activity: IncomingShareActivity) fun inject(activity: IncomingShareActivity)
fun inject(activity: SoftLogoutActivity) fun inject(activity: SoftLogoutActivity)
fun inject(activity: PermalinkHandlerActivity)
fun inject(activity: QrCodeScannerActivity) fun inject(activity: QrCodeScannerActivity)
fun inject(activity: DebugMenuActivity) fun inject(activity: DebugMenuActivity)
fun inject(activity: SharedSecureStorageActivity) fun inject(activity: SharedSecureStorageActivity)

View file

@ -51,6 +51,9 @@ import im.vector.app.features.navigation.Navigator
import im.vector.app.features.notifications.NotificationDrawerManager import im.vector.app.features.notifications.NotificationDrawerManager
import im.vector.app.features.permalink.NavigationInterceptor import im.vector.app.features.permalink.NavigationInterceptor
import im.vector.app.features.permalink.PermalinkHandler import im.vector.app.features.permalink.PermalinkHandler
import im.vector.app.features.permalink.PermalinkHandler.Companion.MATRIX_TO_CUSTOM_SCHEME_URL_BASE
import im.vector.app.features.permalink.PermalinkHandler.Companion.ROOM_LINK_PREFIX
import im.vector.app.features.permalink.PermalinkHandler.Companion.USER_LINK_PREFIX
import im.vector.app.features.popup.DefaultVectorAlert import im.vector.app.features.popup.DefaultVectorAlert
import im.vector.app.features.popup.PopupAlertManager import im.vector.app.features.popup.PopupAlertManager
import im.vector.app.features.popup.VerificationVectorAlert import im.vector.app.features.popup.VerificationVectorAlert
@ -272,20 +275,19 @@ class HomeActivity :
private fun handleIntent(intent: Intent?) { private fun handleIntent(intent: Intent?) {
intent?.dataString?.let { deepLink -> intent?.dataString?.let { deepLink ->
val resolvedLink = when { val resolvedLink = when {
deepLink.startsWith(PermalinkService.MATRIX_TO_URL_BASE) -> deepLink // Element custom scheme is not handled by the sdk, convert it to matrix.to link for compatibility
deepLink.startsWith(MATRIX_TO_CUSTOM_SCHEME_URL_BASE) -> { deepLink.startsWith(MATRIX_TO_CUSTOM_SCHEME_URL_BASE) -> {
// This is a bit ugly, but for now just convert to matrix.to link for compatibility val let = when {
when {
deepLink.startsWith(USER_LINK_PREFIX) -> deepLink.substring(USER_LINK_PREFIX.length) deepLink.startsWith(USER_LINK_PREFIX) -> deepLink.substring(USER_LINK_PREFIX.length)
deepLink.startsWith(ROOM_LINK_PREFIX) -> deepLink.substring(ROOM_LINK_PREFIX.length) deepLink.startsWith(ROOM_LINK_PREFIX) -> deepLink.substring(ROOM_LINK_PREFIX.length)
else -> null else -> null
}?.let { }?.let { permalinkId ->
activeSessionHolder.getSafeActiveSession()?.permalinkService()?.createPermalink(it) activeSessionHolder.getSafeActiveSession()?.permalinkService()?.createPermalink(permalinkId)
} }
let
} }
else -> return@let else -> deepLink
} }
permalinkHandler.launch( permalinkHandler.launch(
context = this, context = this,
deepLink = resolvedLink, deepLink = resolvedLink,
@ -296,9 +298,11 @@ class HomeActivity :
.observeOn(AndroidSchedulers.mainThread()) .observeOn(AndroidSchedulers.mainThread())
.subscribe { isHandled -> .subscribe { isHandled ->
if (!isHandled) { if (!isHandled) {
val isMatrixToLink = deepLink.startsWith(PermalinkService.MATRIX_TO_URL_BASE)
|| deepLink.startsWith(MATRIX_TO_CUSTOM_SCHEME_URL_BASE)
MaterialAlertDialogBuilder(this) MaterialAlertDialogBuilder(this)
.setTitle(R.string.dialog_title_error) .setTitle(R.string.dialog_title_error)
.setMessage(R.string.permalink_malformed) .setMessage(if (isMatrixToLink) R.string.permalink_malformed else R.string.universal_link_malformed)
.setPositiveButton(R.string.ok, null) .setPositiveButton(R.string.ok, null)
.show() .show()
} }
@ -579,10 +583,6 @@ class HomeActivity :
putExtra(MvRx.KEY_ARG, args) putExtra(MvRx.KEY_ARG, args)
} }
} }
private const val MATRIX_TO_CUSTOM_SCHEME_URL_BASE = "element://"
private const val ROOM_LINK_PREFIX = "${MATRIX_TO_CUSTOM_SCHEME_URL_BASE}room/"
private const val USER_LINK_PREFIX = "${MATRIX_TO_CUSTOM_SCHEME_URL_BASE}user/"
} }
override fun create(initialState: ActiveSpaceViewState) = promoteRestrictedViewModelFactory.create(initialState) override fun create(initialState: ActiveSpaceViewState) = promoteRestrictedViewModelFactory.create(initialState)

View file

@ -27,13 +27,12 @@ import im.vector.app.core.error.ErrorFormatter
import im.vector.app.core.platform.VectorBaseActivity import im.vector.app.core.platform.VectorBaseActivity
import im.vector.app.core.utils.toast import im.vector.app.core.utils.toast
import im.vector.app.databinding.ActivityProgressBinding import im.vector.app.databinding.ActivityProgressBinding
import im.vector.app.features.home.HomeActivity
import im.vector.app.features.login.LoginConfig import im.vector.app.features.login.LoginConfig
import im.vector.app.features.permalink.PermalinkHandler import im.vector.app.features.permalink.PermalinkHandler
import io.reactivex.android.schedulers.AndroidSchedulers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.matrix.android.sdk.api.session.permalinks.PermalinkService import org.matrix.android.sdk.api.session.permalinks.PermalinkService
import timber.log.Timber import timber.log.Timber
import java.util.concurrent.TimeUnit
import javax.inject.Inject import javax.inject.Inject
/** /**
@ -45,30 +44,38 @@ class LinkHandlerActivity : VectorBaseActivity<ActivityProgressBinding>() {
@Inject lateinit var errorFormatter: ErrorFormatter @Inject lateinit var errorFormatter: ErrorFormatter
@Inject lateinit var permalinkHandler: PermalinkHandler @Inject lateinit var permalinkHandler: PermalinkHandler
override fun getBinding() = ActivityProgressBinding.inflate(layoutInflater)
override fun injectWith(injector: ScreenComponent) { override fun injectWith(injector: ScreenComponent) {
injector.inject(this) injector.inject(this)
} }
override fun getBinding() = ActivityProgressBinding.inflate(layoutInflater)
override fun initUiAndData() { override fun initUiAndData() {
handleIntent()
}
override fun onNewIntent(intent: Intent?) {
super.onNewIntent(intent)
handleIntent()
}
private fun handleIntent() {
val uri = intent.data val uri = intent.data
when {
if (uri == null) { uri == null -> {
// Should not happen // Should not happen
Timber.w("Uri is null") Timber.w("Uri is null")
finish() finish()
return }
} uri.getQueryParameter(LoginConfig.CONFIG_HS_PARAMETER) != null -> handleConfigUrl(uri)
uri.toString().startsWith(PermalinkService.MATRIX_TO_URL_BASE) -> handleSupportedHostUrl()
if (uri.getQueryParameter(LoginConfig.CONFIG_HS_PARAMETER) != null) { uri.toString().startsWith(PermalinkHandler.MATRIX_TO_CUSTOM_SCHEME_URL_BASE) -> handleSupportedHostUrl()
handleConfigUrl(uri) resources.getStringArray(R.array.permalink_supported_hosts).contains(uri.host) -> handleSupportedHostUrl()
} else if (SUPPORTED_HOSTS.contains(uri.host)) { else -> {
handleSupportedHostUrl(uri) // Other links are not yet handled, but should not come here (manifest configuration error?)
} else { toast(R.string.universal_link_malformed)
// Other links are not yet handled, but should not come here (manifest configuration error?) finish()
toast(R.string.universal_link_malformed) }
finish()
} }
} }
@ -81,53 +88,28 @@ class LinkHandlerActivity : VectorBaseActivity<ActivityProgressBinding>() {
} }
} }
private fun handleSupportedHostUrl(uri: Uri) { private fun handleSupportedHostUrl() {
// If we are not logged in, open login screen.
// In the future, we might want to relaunch the process after login.
if (!sessionHolder.hasActiveSession()) { if (!sessionHolder.hasActiveSession()) {
startLoginActivity(uri) startLoginActivity()
finish() return
} else {
convertUriToPermalink(uri)?.let { permalink ->
startPermalinkHandler(permalink)
} ?: run {
// Host is correct but we do not recognize path
Timber.w("Unable to handle this uri: $uri")
finish()
}
} }
// We forward intent to HomeActivity (singleTask) to avoid the dueling app problem
// https://stackoverflow.com/questions/25884954/deep-linking-and-multiple-app-instances
intent.setClass(this, HomeActivity::class.java)
intent.flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP
startActivity(intent)
} }
/** /**
* Convert a URL of element web instance to a matrix.to url * Start the login screen with identity server and homeserver pre-filled, if any
* Examples:
* - 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
*/ */
private fun convertUriToPermalink(uri: Uri): String? { private fun startLoginActivity(uri: Uri? = null) {
val uriString = uri.toString()
val path = SUPPORTED_PATHS.find { it in uriString } ?: return null
return PermalinkService.MATRIX_TO_URL_BASE + uriString.substringAfter(path)
}
private fun startPermalinkHandler(permalink: String) {
permalinkHandler.launch(this, permalink, buildTask = true)
.delay(500, TimeUnit.MILLISECONDS)
.observeOn(AndroidSchedulers.mainThread())
.subscribe { isHandled ->
if (!isHandled) {
toast(R.string.universal_link_malformed)
}
finish()
}
.disposeOnDestroy()
}
/**
* Start the login screen with identity server and homeserver pre-filled
*/
private fun startLoginActivity(uri: Uri) {
navigator.openLogin( navigator.openLogin(
context = this, context = this,
loginConfig = LoginConfig.parse(uri), loginConfig = uri?.let { LoginConfig.parse(uri) },
flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_CLEAR_TASK or Intent.FLAG_ACTIVITY_NEW_TASK flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_CLEAR_TASK or Intent.FLAG_ACTIVITY_NEW_TASK
) )
finish() finish()
@ -173,21 +155,4 @@ class LinkHandlerActivity : VectorBaseActivity<ActivityProgressBinding>() {
.setPositiveButton(R.string.ok) { _, _ -> finish() } .setPositiveButton(R.string.ok) { _, _ -> finish() }
.show() .show()
} }
companion object {
private val SUPPORTED_HOSTS = listOf(
// Regular Element Web instance
"app.element.io",
// Other known instances of Element Web
"develop.element.io",
"staging.element.io",
// Previous Web instance, kept for compatibility reason
"riot.im"
)
private val SUPPORTED_PATHS = listOf(
"/#/room/",
"/#/user/",
"/#/group/"
)
}
} }

View file

@ -29,6 +29,7 @@ import io.reactivex.schedulers.Schedulers
import org.matrix.android.sdk.api.extensions.orFalse import org.matrix.android.sdk.api.extensions.orFalse
import org.matrix.android.sdk.api.session.permalinks.PermalinkData import org.matrix.android.sdk.api.session.permalinks.PermalinkData
import org.matrix.android.sdk.api.session.permalinks.PermalinkParser import org.matrix.android.sdk.api.session.permalinks.PermalinkParser
import org.matrix.android.sdk.api.session.permalinks.PermalinkService
import org.matrix.android.sdk.api.session.room.model.Membership import org.matrix.android.sdk.api.session.room.model.Membership
import org.matrix.android.sdk.api.session.room.model.RoomType import org.matrix.android.sdk.api.session.room.model.RoomType
import org.matrix.android.sdk.api.util.Optional import org.matrix.android.sdk.api.util.Optional
@ -55,7 +56,7 @@ class PermalinkHandler @Inject constructor(private val activeSessionHolder: Acti
navigationInterceptor: NavigationInterceptor? = null, navigationInterceptor: NavigationInterceptor? = null,
buildTask: Boolean = false buildTask: Boolean = false
): Single<Boolean> { ): Single<Boolean> {
if (deepLink == null) { if (deepLink == null || !isPermalinkSupported(context, deepLink.toString())) {
return Single.just(false) return Single.just(false)
} }
return Single return Single
@ -122,6 +123,13 @@ class PermalinkHandler @Inject constructor(private val activeSessionHolder: Acti
} }
} }
private fun isPermalinkSupported(context: Context, url: String): Boolean {
return url.startsWith(PermalinkService.MATRIX_TO_URL_BASE)
|| context.resources.getStringArray(R.array.permalink_supported_hosts).any {
url.startsWith(it)
}
}
private fun PermalinkData.RoomLink.getRoomId(): Single<Optional<String>> { private fun PermalinkData.RoomLink.getRoomId(): Single<Optional<String>> {
val session = activeSessionHolder.getSafeActiveSession() val session = activeSessionHolder.getSafeActiveSession()
return if (isRoomAlias && session != null) { return if (isRoomAlias && session != null) {
@ -179,6 +187,12 @@ class PermalinkHandler @Inject constructor(private val activeSessionHolder: Acti
} }
} }
} }
companion object {
const val MATRIX_TO_CUSTOM_SCHEME_URL_BASE = "element://"
const val ROOM_LINK_PREFIX = "${MATRIX_TO_CUSTOM_SCHEME_URL_BASE}room/"
const val USER_LINK_PREFIX = "${MATRIX_TO_CUSTOM_SCHEME_URL_BASE}user/"
}
} }
interface NavigationInterceptor { interface NavigationInterceptor {

View file

@ -1,79 +0,0 @@
/*
* Copyright 2019 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 im.vector.app.features.permalink
import android.content.Intent
import android.os.Bundle
import im.vector.app.R
import im.vector.app.core.di.ActiveSessionHolder
import im.vector.app.core.di.ScreenComponent
import im.vector.app.core.extensions.replaceFragment
import im.vector.app.core.platform.VectorBaseActivity
import im.vector.app.databinding.FragmentProgressBinding
import im.vector.app.features.home.HomeActivity
import im.vector.app.features.home.LoadingFragment
import javax.inject.Inject
class PermalinkHandlerActivity : VectorBaseActivity<FragmentProgressBinding>() {
@Inject lateinit var permalinkHandler: PermalinkHandler
@Inject lateinit var sessionHolder: ActiveSessionHolder
override fun getBinding() = FragmentProgressBinding.inflate(layoutInflater)
override fun injectWith(injector: ScreenComponent) {
injector.inject(this)
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_simple)
if (isFirstCreation()) {
replaceFragment(R.id.simpleFragmentContainer, LoadingFragment::class.java)
}
handleIntent()
}
private fun handleIntent() {
// If we are not logged in, open login screen.
// In the future, we might want to relaunch the process after login.
if (!sessionHolder.hasActiveSession()) {
startLoginActivity()
return
}
// We forward intent to HomeActivity (singleTask) to avoid the dueling app problem
// https://stackoverflow.com/questions/25884954/deep-linking-and-multiple-app-instances
intent.setClass(this, HomeActivity::class.java)
intent.flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP
startActivity(intent)
finish()
}
override fun onNewIntent(intent: Intent?) {
super.onNewIntent(intent)
handleIntent()
}
private fun startLoginActivity() {
navigator.openLogin(
context = this,
flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_CLEAR_TASK or Intent.FLAG_ACTIVITY_NEW_TASK
)
finish()
}
}

View file

@ -26,4 +26,15 @@
<item>gitter.im</item> <item>gitter.im</item>
</string-array> </string-array>
<!-- Permalink config -->
<string-array name="permalink_supported_hosts" translatable="false">
<!-- Regular Element Web instance -->
<item>app.element.io</item>
<!-- Other known instances of Element Web -->
<item>develop.element.io</item>
<item>staging.element.io</item>
<!-- Previous Web instance, kept for compatibility reason -->
<item>riot.im</item>
</string-array>
</resources> </resources>