From 0c421bdadd402ed60cbd6b4b8840065400577caa Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 20 Sep 2023 09:59:23 +0200 Subject: [PATCH 1/8] Add script to start ElementCallActivity --- tools/adb/callLinkCustomScheme.sh | 23 +++++++++++++++++++++++ tools/adb/callLinkHttps.sh | 23 +++++++++++++++++++++++ 2 files changed, 46 insertions(+) create mode 100755 tools/adb/callLinkCustomScheme.sh create mode 100755 tools/adb/callLinkHttps.sh diff --git a/tools/adb/callLinkCustomScheme.sh b/tools/adb/callLinkCustomScheme.sh new file mode 100755 index 0000000000..d556d88f70 --- /dev/null +++ b/tools/adb/callLinkCustomScheme.sh @@ -0,0 +1,23 @@ +#! /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: +# element://call?url=some-encoded-url +# For instance +# element://call?url=https%3A%2F%2Fcall.element.io%2FTestElementCall + +adb shell am start -a android.intent.action.VIEW -d element://call?url=https%3A%2F%2Fcall.element.io%2FTestElementCall diff --git a/tools/adb/callLinkHttps.sh b/tools/adb/callLinkHttps.sh new file mode 100755 index 0000000000..00ff341de3 --- /dev/null +++ b/tools/adb/callLinkHttps.sh @@ -0,0 +1,23 @@ +#! /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: +# https://call.element.io/* +# For instance +# https://call.element.io/TestElementCall + +adb shell am start -a android.intent.action.VIEW -d https://call.element.io/TestElementCall From acbdac70be4e96a1fca4fb95b002f57bae485609 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 20 Sep 2023 11:05:15 +0200 Subject: [PATCH 2/8] Add support for `io.element.call:/?url=some-encoded-url` uri --- features/call/src/main/AndroidManifest.xml | 8 +++++++ .../features/call/CallIntentDataParser.kt | 10 ++++++++ tools/adb/callLinkCustomScheme2.sh | 23 +++++++++++++++++++ 3 files changed, 41 insertions(+) create mode 100755 tools/adb/callLinkCustomScheme2.sh diff --git a/features/call/src/main/AndroidManifest.xml b/features/call/src/main/AndroidManifest.xml index 1aed77cd95..d106d4e7b8 100644 --- a/features/call/src/main/AndroidManifest.xml +++ b/features/call/src/main/AndroidManifest.xml @@ -53,6 +53,14 @@ + + + + + + + + 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 a664e562f3..cac211cdc1 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 @@ -38,6 +38,16 @@ object CallIntentDataParser { internalUri.scheme in validHttpSchemes && !internalUri.host.isNullOrBlank() } } + scheme == "io.element.call" && parsedUrl.host == null -> { + // 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 + parsedUrl.getQueryParameter("url") + ?.let { URLDecoder.decode(it, "utf-8") } + ?.takeIf { + val internalUri = Uri.parse(it) + internalUri.scheme in validHttpSchemes && !internalUri.host.isNullOrBlank() + } + } // This should never be possible, but we still need to take into account the possibility else -> null } diff --git a/tools/adb/callLinkCustomScheme2.sh b/tools/adb/callLinkCustomScheme2.sh new file mode 100755 index 0000000000..d6dabd595c --- /dev/null +++ b/tools/adb/callLinkCustomScheme2.sh @@ -0,0 +1,23 @@ +#! /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: +# io.element.call:/?url=some-encoded-url +# For instance +# io.element.call:/?url=https%3A%2F%2Fcall.element.io%2FTestElementCall + +adb shell am start -a android.intent.action.VIEW -d io.element.call:/?url=https%3A%2F%2Fcall.element.io%2FTestElementCall From 73d71b9ddbab0a59dae1a4b2bcd449e00c099001 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 20 Sep 2023 11:07:10 +0200 Subject: [PATCH 3/8] CallIntentDataParser is now a class. --- .../features/call/CallIntentDataParser.kt | 3 +- .../features/call/ElementCallActivity.kt | 3 +- .../call/CallIntentDataParserTests.kt | 34 ++++++++++--------- 3 files changed, 22 insertions(+), 18 deletions(-) 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 cac211cdc1..9237087e2b 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 @@ -18,8 +18,9 @@ package io.element.android.features.call import android.net.Uri import java.net.URLDecoder +import javax.inject.Inject -object CallIntentDataParser { +class CallIntentDataParser @Inject constructor() { private val validHttpSchemes = sequenceOf("http", "https") diff --git a/features/call/src/main/kotlin/io/element/android/features/call/ElementCallActivity.kt b/features/call/src/main/kotlin/io/element/android/features/call/ElementCallActivity.kt index 69ef3963cb..481634a4ca 100644 --- a/features/call/src/main/kotlin/io/element/android/features/call/ElementCallActivity.kt +++ b/features/call/src/main/kotlin/io/element/android/features/call/ElementCallActivity.kt @@ -39,6 +39,7 @@ import javax.inject.Inject class ElementCallActivity : ComponentActivity() { @Inject lateinit var userAgentProvider: UserAgentProvider + @Inject lateinit var callIntentDataParser: CallIntentDataParser private lateinit var audioManager: AudioManager @@ -129,7 +130,7 @@ class ElementCallActivity : ComponentActivity() { finishAndRemoveTask() } - private fun parseUrl(url: String?): String? = CallIntentDataParser.parse(url) + private fun parseUrl(url: String?): String? = callIntentDataParser.parse(url) private fun registerPermissionResultLauncher(): ActivityResultLauncher> { return registerForActivityResult( 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 da41692b40..93ae34a9c9 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 @@ -25,28 +25,30 @@ import java.net.URLEncoder @RunWith(RobolectricTestRunner::class) class CallIntentDataParserTests { + private val callIntentDataParser = CallIntentDataParser() + @Test fun `a null data returns null`() { val url: String? = null - assertThat(CallIntentDataParser.parse(url)).isNull() + assertThat(callIntentDataParser.parse(url)).isNull() } @Test fun `empty data returns null`() { val url = "" - assertThat(CallIntentDataParser.parse(url)).isNull() + assertThat(callIntentDataParser.parse(url)).isNull() } @Test fun `invalid data returns null`() { val url = "!" - assertThat(CallIntentDataParser.parse(url)).isNull() + assertThat(callIntentDataParser.parse(url)).isNull() } @Test fun `data with no scheme returns null`() { val url = "test" - assertThat(CallIntentDataParser.parse(url)).isNull() + assertThat(callIntentDataParser.parse(url)).isNull() } @Test @@ -55,10 +57,10 @@ class CallIntentDataParserTests { val httpCallUrl = "http://call.element.io/some-actual-call?with=parameters" 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) + assertThat(callIntentDataParser.parse(httpBaseUrl)).isEqualTo(httpBaseUrl) + assertThat(callIntentDataParser.parse(httpCallUrl)).isEqualTo(httpCallUrl) + assertThat(callIntentDataParser.parse(httpsBaseUrl)).isEqualTo(httpsBaseUrl) + assertThat(callIntentDataParser.parse(httpsCallUrl)).isEqualTo(httpsCallUrl) } @Test @@ -67,10 +69,10 @@ class CallIntentDataParserTests { val httpsBaseUrl = "https://app.element.io" val httpInvalidUrl = "http://" val httpsInvalidUrl = "http://" - assertThat(CallIntentDataParser.parse(httpBaseUrl)).isNull() - assertThat(CallIntentDataParser.parse(httpsBaseUrl)).isNull() - assertThat(CallIntentDataParser.parse(httpInvalidUrl)).isNull() - assertThat(CallIntentDataParser.parse(httpsInvalidUrl)).isNull() + assertThat(callIntentDataParser.parse(httpBaseUrl)).isNull() + assertThat(callIntentDataParser.parse(httpsBaseUrl)).isNull() + assertThat(callIntentDataParser.parse(httpInvalidUrl)).isNull() + assertThat(callIntentDataParser.parse(httpsInvalidUrl)).isNull() } @Test @@ -78,7 +80,7 @@ class CallIntentDataParserTests { 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)).isEqualTo(embeddedUrl) } @Test @@ -86,7 +88,7 @@ class CallIntentDataParserTests { val embeddedUrl = "http://call.element.io/some-actual-call?with=parameters" val encodedUrl = URLEncoder.encode(embeddedUrl, "utf-8") val url = "element://call?no-url=$encodedUrl" - assertThat(CallIntentDataParser.parse(url)).isNull() + assertThat(callIntentDataParser.parse(url)).isNull() } @Test @@ -94,12 +96,12 @@ class CallIntentDataParserTests { val embeddedUrl = "http://call.element.io/some-actual-call?with=parameters" val encodedUrl = URLEncoder.encode(embeddedUrl, "utf-8") val url = "element://no-call?url=$encodedUrl" - assertThat(CallIntentDataParser.parse(url)).isNull() + assertThat(callIntentDataParser.parse(url)).isNull() } @Test fun `element scheme with no data returns null`() { val url = "element://call?url=" - assertThat(CallIntentDataParser.parse(url)).isNull() + assertThat(callIntentDataParser.parse(url)).isNull() } } From 1244761be29fafda9adb2832b3a682c646b633d8 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 20 Sep 2023 11:09:21 +0200 Subject: [PATCH 4/8] Avoid code duplication --- .../features/call/CallIntentDataParser.kt | 23 +++++++++---------- 1 file changed, 11 insertions(+), 12 deletions(-) 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 9237087e2b..91cce30ed3 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 @@ -32,25 +32,24 @@ class CallIntentDataParser @Inject constructor() { 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 - parsedUrl.getQueryParameter("url") - ?.let { URLDecoder.decode(it, "utf-8") } - ?.takeIf { - val internalUri = Uri.parse(it) - internalUri.scheme in validHttpSchemes && !internalUri.host.isNullOrBlank() - } + parsedUrl.getUrlParameter() } scheme == "io.element.call" && parsedUrl.host == null -> { // 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 - parsedUrl.getQueryParameter("url") - ?.let { URLDecoder.decode(it, "utf-8") } - ?.takeIf { - val internalUri = Uri.parse(it) - internalUri.scheme in validHttpSchemes && !internalUri.host.isNullOrBlank() - } + parsedUrl.getUrlParameter() } // This should never be possible, but we still need to take into account the possibility else -> null } } + + private fun Uri.getUrlParameter(): String? { + return getQueryParameter("url") + ?.let { URLDecoder.decode(it, "utf-8") } + ?.takeIf { + val internalUri = Uri.parse(it) + internalUri.scheme in validHttpSchemes && !internalUri.host.isNullOrBlank() + } + } } From 1d0af23d52e56162a1403eef0e4429ccbbbe5c75 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 20 Sep 2023 11:18:51 +0200 Subject: [PATCH 5/8] Add test for the scheme `io.element.call` --- .../call/CallIntentDataParserTests.kt | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) 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 93ae34a9c9..55fdbceb95 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 @@ -83,6 +83,14 @@ class CallIntentDataParserTests { assertThat(callIntentDataParser.parse(url)).isEqualTo(embeddedUrl) } + @Test + fun `element scheme 2 with url param gets url extracted`() { + 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)).isEqualTo(embeddedUrl) + } + @Test fun `element scheme with call host and no url param returns null`() { val embeddedUrl = "http://call.element.io/some-actual-call?with=parameters" @@ -91,6 +99,14 @@ class CallIntentDataParserTests { assertThat(callIntentDataParser.parse(url)).isNull() } + @Test + fun `element scheme 2 with no url 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:/?no_url=$encodedUrl" + assertThat(callIntentDataParser.parse(url)).isNull() + } + @Test fun `element scheme with no call host returns null`() { val embeddedUrl = "http://call.element.io/some-actual-call?with=parameters" @@ -104,4 +120,10 @@ class CallIntentDataParserTests { val url = "element://call?url=" assertThat(callIntentDataParser.parse(url)).isNull() } + + @Test + fun `element scheme 2 with no data returns null`() { + val url = "io.element.call:/?url=" + assertThat(callIntentDataParser.parse(url)).isNull() + } } From 040da1324dceb24957e3f267a3dc57bcd4e72ffb Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 20 Sep 2023 11:19:47 +0200 Subject: [PATCH 6/8] Add one more test. --- .../android/features/call/CallIntentDataParserTests.kt | 8 ++++++++ 1 file changed, 8 insertions(+) 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 55fdbceb95..71290f15b7 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 @@ -126,4 +126,12 @@ class CallIntentDataParserTests { val url = "io.element.call:/?url=" assertThat(callIntentDataParser.parse(url)).isNull() } + + @Test + fun `element invalid scheme returns null`() { + val embeddedUrl = "http://call.element.io/some-actual-call?with=parameters" + val encodedUrl = URLEncoder.encode(embeddedUrl, "utf-8") + val url = "bad.scheme:/?url=$encodedUrl" + assertThat(callIntentDataParser.parse(url)).isNull() + } } From b73fe69dd6a7eca6c80682eac6821b4756f12605 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 20 Sep 2023 11:20:40 +0200 Subject: [PATCH 7/8] No need to decode the parameter value, `getQueryParameter` already does it. --- .../io/element/android/features/call/CallIntentDataParser.kt | 2 -- 1 file changed, 2 deletions(-) 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 91cce30ed3..30e71d6201 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 @@ -17,7 +17,6 @@ package io.element.android.features.call import android.net.Uri -import java.net.URLDecoder import javax.inject.Inject class CallIntentDataParser @Inject constructor() { @@ -46,7 +45,6 @@ class CallIntentDataParser @Inject constructor() { private fun Uri.getUrlParameter(): String? { return getQueryParameter("url") - ?.let { URLDecoder.decode(it, "utf-8") } ?.takeIf { val internalUri = Uri.parse(it) internalUri.scheme in validHttpSchemes && !internalUri.host.isNullOrBlank() From 237c34ef2bdf82653c4d143869f2db112fd105f3 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 20 Sep 2023 11:25:32 +0200 Subject: [PATCH 8/8] Changelog --- changelog.d/1377.misc | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/1377.misc diff --git a/changelog.d/1377.misc b/changelog.d/1377.misc new file mode 100644 index 0000000000..2fa9e98b91 --- /dev/null +++ b/changelog.d/1377.misc @@ -0,0 +1 @@ +Element Call: support scheme `io.element.call`