Skip to content

Commit e0d3209

Browse files
authored
Merge pull request #1385 from vector-im/feature/bma/callScheme
Element call scheme
2 parents cfb71ad + 237c34e commit e0d3209

File tree

8 files changed

+144
-25
lines changed

8 files changed

+144
-25
lines changed

changelog.d/1377.misc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Element Call: support scheme `io.element.call`

features/call/src/main/AndroidManifest.xml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,14 @@
5353
<data android:scheme="element" />
5454
<data android:host="call" />
5555
</intent-filter>
56+
<!-- Custom scheme to handle urls from other domains in the format: io.element.call:/?url=https%3A%2F%2Felement.io -->
57+
<intent-filter>
58+
<action android:name="android.intent.action.VIEW" />
59+
<category android:name="android.intent.category.DEFAULT" />
60+
<category android:name="android.intent.category.BROWSABLE" />
61+
62+
<data android:scheme="io.element.call" />
63+
</intent-filter>
5664

5765
</activity>
5866
<service android:name=".CallForegroundService" android:enabled="true" android:foregroundServiceType="mediaPlayback" />

features/call/src/main/kotlin/io/element/android/features/call/CallIntentDataParser.kt

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,9 @@
1717
package io.element.android.features.call
1818

1919
import android.net.Uri
20-
import java.net.URLDecoder
20+
import javax.inject.Inject
2121

22-
object CallIntentDataParser {
22+
class CallIntentDataParser @Inject constructor() {
2323

2424
private val validHttpSchemes = sequenceOf("http", "https")
2525

@@ -31,15 +31,23 @@ object CallIntentDataParser {
3131
scheme == "element" && parsedUrl.host == "call" -> {
3232
// We use this custom scheme to load arbitrary URLs for other instances of Element Call,
3333
// so we can only verify it's an HTTP/HTTPs URL with a non-empty host
34-
parsedUrl.getQueryParameter("url")
35-
?.let { URLDecoder.decode(it, "utf-8") }
36-
?.takeIf {
37-
val internalUri = Uri.parse(it)
38-
internalUri.scheme in validHttpSchemes && !internalUri.host.isNullOrBlank()
39-
}
34+
parsedUrl.getUrlParameter()
35+
}
36+
scheme == "io.element.call" && parsedUrl.host == null -> {
37+
// We use this custom scheme to load arbitrary URLs for other instances of Element Call,
38+
// so we can only verify it's an HTTP/HTTPs URL with a non-empty host
39+
parsedUrl.getUrlParameter()
4040
}
4141
// This should never be possible, but we still need to take into account the possibility
4242
else -> null
4343
}
4444
}
45+
46+
private fun Uri.getUrlParameter(): String? {
47+
return getQueryParameter("url")
48+
?.takeIf {
49+
val internalUri = Uri.parse(it)
50+
internalUri.scheme in validHttpSchemes && !internalUri.host.isNullOrBlank()
51+
}
52+
}
4553
}

features/call/src/main/kotlin/io/element/android/features/call/ElementCallActivity.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ import javax.inject.Inject
3939
class ElementCallActivity : ComponentActivity() {
4040

4141
@Inject lateinit var userAgentProvider: UserAgentProvider
42+
@Inject lateinit var callIntentDataParser: CallIntentDataParser
4243

4344
private lateinit var audioManager: AudioManager
4445

@@ -129,7 +130,7 @@ class ElementCallActivity : ComponentActivity() {
129130
finishAndRemoveTask()
130131
}
131132

132-
private fun parseUrl(url: String?): String? = CallIntentDataParser.parse(url)
133+
private fun parseUrl(url: String?): String? = callIntentDataParser.parse(url)
133134

134135
private fun registerPermissionResultLauncher(): ActivityResultLauncher<Array<String>> {
135136
return registerForActivityResult(

features/call/src/test/kotlin/io/element/android/features/call/CallIntentDataParserTests.kt

Lines changed: 48 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -25,28 +25,30 @@ import java.net.URLEncoder
2525
@RunWith(RobolectricTestRunner::class)
2626
class CallIntentDataParserTests {
2727

28+
private val callIntentDataParser = CallIntentDataParser()
29+
2830
@Test
2931
fun `a null data returns null`() {
3032
val url: String? = null
31-
assertThat(CallIntentDataParser.parse(url)).isNull()
33+
assertThat(callIntentDataParser.parse(url)).isNull()
3234
}
3335

3436
@Test
3537
fun `empty data returns null`() {
3638
val url = ""
37-
assertThat(CallIntentDataParser.parse(url)).isNull()
39+
assertThat(callIntentDataParser.parse(url)).isNull()
3840
}
3941

4042
@Test
4143
fun `invalid data returns null`() {
4244
val url = "!"
43-
assertThat(CallIntentDataParser.parse(url)).isNull()
45+
assertThat(callIntentDataParser.parse(url)).isNull()
4446
}
4547

4648
@Test
4749
fun `data with no scheme returns null`() {
4850
val url = "test"
49-
assertThat(CallIntentDataParser.parse(url)).isNull()
51+
assertThat(callIntentDataParser.parse(url)).isNull()
5052
}
5153

5254
@Test
@@ -55,10 +57,10 @@ class CallIntentDataParserTests {
5557
val httpCallUrl = "http://call.element.io/some-actual-call?with=parameters"
5658
val httpsBaseUrl = "https://call.element.io"
5759
val httpsCallUrl = "https://call.element.io/some-actual-call?with=parameters"
58-
assertThat(CallIntentDataParser.parse(httpBaseUrl)).isEqualTo(httpBaseUrl)
59-
assertThat(CallIntentDataParser.parse(httpCallUrl)).isEqualTo(httpCallUrl)
60-
assertThat(CallIntentDataParser.parse(httpsBaseUrl)).isEqualTo(httpsBaseUrl)
61-
assertThat(CallIntentDataParser.parse(httpsCallUrl)).isEqualTo(httpsCallUrl)
60+
assertThat(callIntentDataParser.parse(httpBaseUrl)).isEqualTo(httpBaseUrl)
61+
assertThat(callIntentDataParser.parse(httpCallUrl)).isEqualTo(httpCallUrl)
62+
assertThat(callIntentDataParser.parse(httpsBaseUrl)).isEqualTo(httpsBaseUrl)
63+
assertThat(callIntentDataParser.parse(httpsCallUrl)).isEqualTo(httpsCallUrl)
6264
}
6365

6466
@Test
@@ -67,39 +69,69 @@ class CallIntentDataParserTests {
6769
val httpsBaseUrl = "https://app.element.io"
6870
val httpInvalidUrl = "http://"
6971
val httpsInvalidUrl = "http://"
70-
assertThat(CallIntentDataParser.parse(httpBaseUrl)).isNull()
71-
assertThat(CallIntentDataParser.parse(httpsBaseUrl)).isNull()
72-
assertThat(CallIntentDataParser.parse(httpInvalidUrl)).isNull()
73-
assertThat(CallIntentDataParser.parse(httpsInvalidUrl)).isNull()
72+
assertThat(callIntentDataParser.parse(httpBaseUrl)).isNull()
73+
assertThat(callIntentDataParser.parse(httpsBaseUrl)).isNull()
74+
assertThat(callIntentDataParser.parse(httpInvalidUrl)).isNull()
75+
assertThat(callIntentDataParser.parse(httpsInvalidUrl)).isNull()
7476
}
7577

7678
@Test
7779
fun `element scheme with call host and url param gets url extracted`() {
7880
val embeddedUrl = "http://call.element.io/some-actual-call?with=parameters"
7981
val encodedUrl = URLEncoder.encode(embeddedUrl, "utf-8")
8082
val url = "element://call?url=$encodedUrl"
81-
assertThat(CallIntentDataParser.parse(url)).isEqualTo(embeddedUrl)
83+
assertThat(callIntentDataParser.parse(url)).isEqualTo(embeddedUrl)
84+
}
85+
86+
@Test
87+
fun `element scheme 2 with url param gets url extracted`() {
88+
val embeddedUrl = "http://call.element.io/some-actual-call?with=parameters"
89+
val encodedUrl = URLEncoder.encode(embeddedUrl, "utf-8")
90+
val url = "io.element.call:/?url=$encodedUrl"
91+
assertThat(callIntentDataParser.parse(url)).isEqualTo(embeddedUrl)
8292
}
8393

8494
@Test
8595
fun `element scheme with call host and no url param returns null`() {
8696
val embeddedUrl = "http://call.element.io/some-actual-call?with=parameters"
8797
val encodedUrl = URLEncoder.encode(embeddedUrl, "utf-8")
8898
val url = "element://call?no-url=$encodedUrl"
89-
assertThat(CallIntentDataParser.parse(url)).isNull()
99+
assertThat(callIntentDataParser.parse(url)).isNull()
100+
}
101+
102+
@Test
103+
fun `element scheme 2 with no url returns null`() {
104+
val embeddedUrl = "http://call.element.io/some-actual-call?with=parameters"
105+
val encodedUrl = URLEncoder.encode(embeddedUrl, "utf-8")
106+
val url = "io.element.call:/?no_url=$encodedUrl"
107+
assertThat(callIntentDataParser.parse(url)).isNull()
90108
}
91109

92110
@Test
93111
fun `element scheme with no call host returns null`() {
94112
val embeddedUrl = "http://call.element.io/some-actual-call?with=parameters"
95113
val encodedUrl = URLEncoder.encode(embeddedUrl, "utf-8")
96114
val url = "element://no-call?url=$encodedUrl"
97-
assertThat(CallIntentDataParser.parse(url)).isNull()
115+
assertThat(callIntentDataParser.parse(url)).isNull()
98116
}
99117

100118
@Test
101119
fun `element scheme with no data returns null`() {
102120
val url = "element://call?url="
103-
assertThat(CallIntentDataParser.parse(url)).isNull()
121+
assertThat(callIntentDataParser.parse(url)).isNull()
122+
}
123+
124+
@Test
125+
fun `element scheme 2 with no data returns null`() {
126+
val url = "io.element.call:/?url="
127+
assertThat(callIntentDataParser.parse(url)).isNull()
128+
}
129+
130+
@Test
131+
fun `element invalid scheme returns null`() {
132+
val embeddedUrl = "http://call.element.io/some-actual-call?with=parameters"
133+
val encodedUrl = URLEncoder.encode(embeddedUrl, "utf-8")
134+
val url = "bad.scheme:/?url=$encodedUrl"
135+
assertThat(callIntentDataParser.parse(url)).isNull()
104136
}
105137
}

tools/adb/callLinkCustomScheme.sh

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
#! /bin/bash
2+
#
3+
# Copyright (c) 2023 New Vector Ltd
4+
#
5+
# Licensed under the Apache License, Version 2.0 (the "License");
6+
# you may not use this file except in compliance with the License.
7+
# You may obtain a copy of the License at
8+
#
9+
# http://www.apache.org/licenses/LICENSE-2.0
10+
#
11+
# Unless required by applicable law or agreed to in writing, software
12+
# distributed under the License is distributed on an "AS IS" BASIS,
13+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
# See the License for the specific language governing permissions and
15+
# limitations under the License.
16+
#
17+
18+
# Format is:
19+
# element://call?url=some-encoded-url
20+
# For instance
21+
# element://call?url=https%3A%2F%2Fcall.element.io%2FTestElementCall
22+
23+
adb shell am start -a android.intent.action.VIEW -d element://call?url=https%3A%2F%2Fcall.element.io%2FTestElementCall

tools/adb/callLinkCustomScheme2.sh

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
#! /bin/bash
2+
#
3+
# Copyright (c) 2023 New Vector Ltd
4+
#
5+
# Licensed under the Apache License, Version 2.0 (the "License");
6+
# you may not use this file except in compliance with the License.
7+
# You may obtain a copy of the License at
8+
#
9+
# http://www.apache.org/licenses/LICENSE-2.0
10+
#
11+
# Unless required by applicable law or agreed to in writing, software
12+
# distributed under the License is distributed on an "AS IS" BASIS,
13+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
# See the License for the specific language governing permissions and
15+
# limitations under the License.
16+
#
17+
18+
# Format is:
19+
# io.element.call:/?url=some-encoded-url
20+
# For instance
21+
# io.element.call:/?url=https%3A%2F%2Fcall.element.io%2FTestElementCall
22+
23+
adb shell am start -a android.intent.action.VIEW -d io.element.call:/?url=https%3A%2F%2Fcall.element.io%2FTestElementCall

tools/adb/callLinkHttps.sh

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
#! /bin/bash
2+
#
3+
# Copyright (c) 2023 New Vector Ltd
4+
#
5+
# Licensed under the Apache License, Version 2.0 (the "License");
6+
# you may not use this file except in compliance with the License.
7+
# You may obtain a copy of the License at
8+
#
9+
# http://www.apache.org/licenses/LICENSE-2.0
10+
#
11+
# Unless required by applicable law or agreed to in writing, software
12+
# distributed under the License is distributed on an "AS IS" BASIS,
13+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
# See the License for the specific language governing permissions and
15+
# limitations under the License.
16+
#
17+
18+
# Format is:
19+
# https://call.element.io/*
20+
# For instance
21+
# https://call.element.io/TestElementCall
22+
23+
adb shell am start -a android.intent.action.VIEW -d https://call.element.io/TestElementCall

0 commit comments

Comments
 (0)