diff --git a/changelog.d/1434.misc b/changelog.d/1434.misc new file mode 100644 index 0000000000..8babe4771a --- /dev/null +++ b/changelog.d/1434.misc @@ -0,0 +1 @@ +Element call: add custom parameters to Element Call urls. diff --git a/features/call/src/main/AndroidManifest.xml b/features/call/src/main/AndroidManifest.xml index d106d4e7b8..877b7fb0a8 100644 --- a/features/call/src/main/AndroidManifest.xml +++ b/features/call/src/main/AndroidManifest.xml @@ -39,7 +39,6 @@ - diff --git a/features/call/src/main/kotlin/io/element/android/features/call/CallIntentDataParser.kt b/features/call/src/main/kotlin/io/element/android/features/call/CallIntentDataParser.kt index 30e71d6201..b3e2f9227a 100644 --- a/features/call/src/main/kotlin/io/element/android/features/call/CallIntentDataParser.kt +++ b/features/call/src/main/kotlin/io/element/android/features/call/CallIntentDataParser.kt @@ -21,13 +21,13 @@ import javax.inject.Inject class CallIntentDataParser @Inject constructor() { - private val validHttpSchemes = sequenceOf("http", "https") + private val validHttpSchemes = sequenceOf("https") fun parse(data: String?): String? { val parsedUrl = data?.let { Uri.parse(data) } ?: return null val scheme = parsedUrl.scheme return when { - scheme in validHttpSchemes && parsedUrl.host == "call.element.io" -> data + scheme in validHttpSchemes && parsedUrl.host == "call.element.io" -> parsedUrl scheme == "element" && parsedUrl.host == "call" -> { // We use this custom scheme to load arbitrary URLs for other instances of Element Call, // so we can only verify it's an HTTP/HTTPs URL with a non-empty host @@ -40,14 +40,36 @@ class CallIntentDataParser @Inject constructor() { } // This should never be possible, but we still need to take into account the possibility else -> null - } + }?.withCustomParameters() } - private fun Uri.getUrlParameter(): String? { + private fun Uri.getUrlParameter(): Uri? { return getQueryParameter("url") - ?.takeIf { - val internalUri = Uri.parse(it) - internalUri.scheme in validHttpSchemes && !internalUri.host.isNullOrBlank() + ?.let { urlParameter -> + Uri.parse(urlParameter).takeIf { uri -> + uri.scheme in validHttpSchemes && !uri.host.isNullOrBlank() + } } } } + +/** + * Ensure the uri has the following parameters and value: + * - appPrompt=false + * - confineToRoom=true + * to ensure that the rendering will bo correct on the embedded Webview. + */ +private fun Uri.withCustomParameters(): String { + val builder = buildUpon() + builder.clearQuery() + queryParameterNames.forEach { + if (it == APP_PROMPT_PARAMETER || it == CONFINE_TO_ROOM_PARAMETER) return@forEach + builder.appendQueryParameter(it, getQueryParameter(it)) + } + builder.appendQueryParameter(APP_PROMPT_PARAMETER, "false") + builder.appendQueryParameter(CONFINE_TO_ROOM_PARAMETER, "true") + return builder.build().toString() +} + +private const val APP_PROMPT_PARAMETER = "appPrompt" +private const val CONFINE_TO_ROOM_PARAMETER = "confineToRoom" diff --git a/features/call/src/test/kotlin/io/element/android/features/call/CallIntentDataParserTests.kt b/features/call/src/test/kotlin/io/element/android/features/call/CallIntentDataParserTests.kt index 71290f15b7..aee97ed982 100644 --- a/features/call/src/test/kotlin/io/element/android/features/call/CallIntentDataParserTests.kt +++ b/features/call/src/test/kotlin/io/element/android/features/call/CallIntentDataParserTests.kt @@ -52,15 +52,19 @@ class CallIntentDataParserTests { } @Test - fun `Element Call urls will be returned as is`() { + fun `Element Call http urls returns null`() { val httpBaseUrl = "http://call.element.io" val httpCallUrl = "http://call.element.io/some-actual-call?with=parameters" + assertThat(callIntentDataParser.parse(httpBaseUrl)).isNull() + assertThat(callIntentDataParser.parse(httpCallUrl)).isNull() + } + + @Test + fun `Element Call urls will be returned as is`() { val httpsBaseUrl = "https://call.element.io" - val httpsCallUrl = "https://call.element.io/some-actual-call?with=parameters" - assertThat(callIntentDataParser.parse(httpBaseUrl)).isEqualTo(httpBaseUrl) - assertThat(callIntentDataParser.parse(httpCallUrl)).isEqualTo(httpCallUrl) - assertThat(callIntentDataParser.parse(httpsBaseUrl)).isEqualTo(httpsBaseUrl) - assertThat(callIntentDataParser.parse(httpsCallUrl)).isEqualTo(httpsCallUrl) + val httpsCallUrl = VALID_CALL_URL_WITH_PARAM + assertThat(callIntentDataParser.parse(httpsBaseUrl)).isEqualTo("$httpsBaseUrl?$EXTRA_PARAMS") + assertThat(callIntentDataParser.parse(httpsCallUrl)).isEqualTo("$httpsCallUrl&$EXTRA_PARAMS") } @Test @@ -76,19 +80,35 @@ class CallIntentDataParserTests { } @Test - fun `element scheme with call host and url param gets url extracted`() { + fun `element scheme with call host and url with http will returns null`() { val embeddedUrl = "http://call.element.io/some-actual-call?with=parameters" val encodedUrl = URLEncoder.encode(embeddedUrl, "utf-8") val url = "element://call?url=$encodedUrl" - assertThat(callIntentDataParser.parse(url)).isEqualTo(embeddedUrl) + assertThat(callIntentDataParser.parse(url)).isNull() + } + + @Test + fun `element scheme with call host and url param gets url extracted`() { + val embeddedUrl = VALID_CALL_URL_WITH_PARAM + val encodedUrl = URLEncoder.encode(embeddedUrl, "utf-8") + val url = "element://call?url=$encodedUrl" + assertThat(callIntentDataParser.parse(url)).isEqualTo("$VALID_CALL_URL_WITH_PARAM&$EXTRA_PARAMS") + } + + @Test + fun `element scheme 2 with url param with http returns null`() { + val embeddedUrl = "http://call.element.io/some-actual-call?with=parameters" + val encodedUrl = URLEncoder.encode(embeddedUrl, "utf-8") + val url = "io.element.call:/?url=$encodedUrl" + assertThat(callIntentDataParser.parse(url)).isNull() } @Test fun `element scheme 2 with url param gets url extracted`() { - val embeddedUrl = "http://call.element.io/some-actual-call?with=parameters" + val embeddedUrl = VALID_CALL_URL_WITH_PARAM val encodedUrl = URLEncoder.encode(embeddedUrl, "utf-8") val url = "io.element.call:/?url=$encodedUrl" - assertThat(callIntentDataParser.parse(url)).isEqualTo(embeddedUrl) + assertThat(callIntentDataParser.parse(url)).isEqualTo("$VALID_CALL_URL_WITH_PARAM&$EXTRA_PARAMS") } @Test @@ -101,7 +121,7 @@ class CallIntentDataParserTests { @Test fun `element scheme 2 with no url returns null`() { - val embeddedUrl = "http://call.element.io/some-actual-call?with=parameters" + val embeddedUrl = VALID_CALL_URL_WITH_PARAM val encodedUrl = URLEncoder.encode(embeddedUrl, "utf-8") val url = "io.element.call:/?no_url=$encodedUrl" assertThat(callIntentDataParser.parse(url)).isNull() @@ -109,7 +129,7 @@ class CallIntentDataParserTests { @Test fun `element scheme with no call host returns null`() { - val embeddedUrl = "http://call.element.io/some-actual-call?with=parameters" + val embeddedUrl = VALID_CALL_URL_WITH_PARAM val encodedUrl = URLEncoder.encode(embeddedUrl, "utf-8") val url = "element://no-call?url=$encodedUrl" assertThat(callIntentDataParser.parse(url)).isNull() @@ -129,9 +149,39 @@ class CallIntentDataParserTests { @Test fun `element invalid scheme returns null`() { - val embeddedUrl = "http://call.element.io/some-actual-call?with=parameters" + val embeddedUrl = VALID_CALL_URL_WITH_PARAM val encodedUrl = URLEncoder.encode(embeddedUrl, "utf-8") val url = "bad.scheme:/?url=$encodedUrl" assertThat(callIntentDataParser.parse(url)).isNull() } + + @Test + fun `element scheme 2 with url extra param appPrompt gets url extracted`() { + val embeddedUrl = "${VALID_CALL_URL_WITH_PARAM}&appPrompt=true" + val encodedUrl = URLEncoder.encode(embeddedUrl, "utf-8") + val url = "io.element.call:/?url=$encodedUrl" + assertThat(callIntentDataParser.parse(url)).isEqualTo("$VALID_CALL_URL_WITH_PARAM&$EXTRA_PARAMS") + } + + @Test + fun `element scheme 2 with url extra param confineToRoom gets url extracted`() { + val embeddedUrl = "${VALID_CALL_URL_WITH_PARAM}&confineToRoom=false" + val encodedUrl = URLEncoder.encode(embeddedUrl, "utf-8") + val url = "io.element.call:/?url=$encodedUrl" + assertThat(callIntentDataParser.parse(url)).isEqualTo("$VALID_CALL_URL_WITH_PARAM&$EXTRA_PARAMS") + } + + @Test + fun `element scheme 2 with url fragment gets url extracted`() { + val embeddedUrl = "${VALID_CALL_URL_WITH_PARAM}#fragment" + val encodedUrl = URLEncoder.encode(embeddedUrl, "utf-8") + val url = "io.element.call:/?url=$encodedUrl" + assertThat(callIntentDataParser.parse(url)).isEqualTo("$VALID_CALL_URL_WITH_PARAM&$EXTRA_PARAMS#fragment") + } + + + companion object { + const val VALID_CALL_URL_WITH_PARAM = "https://call.element.io/some-actual-call?with=parameters" + const val EXTRA_PARAMS = "appPrompt=false&confineToRoom=true" + } }