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 b3e2f9227a..b903b437d8 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 @@ -54,21 +54,50 @@ class CallIntentDataParser @Inject constructor() { } /** - * Ensure the uri has the following parameters and value: + * Ensure the uri has the following parameters and value in the fragment: * - appPrompt=false * - confineToRoom=true * to ensure that the rendering will bo correct on the embedded Webview. */ private fun Uri.withCustomParameters(): String { val builder = buildUpon() + // Remove the existing query parameters 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() + // Remove the existing fragment parameters, and build the new fragment + val currentFragment = fragment ?: "" + // Reset the current fragment + builder.fragment("") + val queryFragmentPosition = currentFragment.lastIndexOf("?") + val newFragment = if (queryFragmentPosition == -1) { + // No existing query, build it. + "$currentFragment?$APP_PROMPT_PARAMETER=false&$CONFINE_TO_ROOM_PARAMETER=true" + } else { + buildString { + append(currentFragment.substring(0, queryFragmentPosition + 1)) + val queryFragment = currentFragment.substring(queryFragmentPosition + 1) + // Replace the existing parameters + val newQueryFragment = queryFragment + .replace("$APP_PROMPT_PARAMETER=true", "$APP_PROMPT_PARAMETER=false") + .replace("$CONFINE_TO_ROOM_PARAMETER=false", "$CONFINE_TO_ROOM_PARAMETER=true") + append(newQueryFragment) + // Ensure the parameters are there + if (!newQueryFragment.contains("$APP_PROMPT_PARAMETER=false")) { + if (newQueryFragment.isNotEmpty()) { + append("&") + } + append("$APP_PROMPT_PARAMETER=false") + } + if (!newQueryFragment.contains("$CONFINE_TO_ROOM_PARAMETER=true")) { + append("&$CONFINE_TO_ROOM_PARAMETER=true") + } + } + } + // We do not want to encode the Fragment part, so append it manually + return builder.build().toString() + "#" + newFragment } private const val APP_PROMPT_PARAMETER = "appPrompt" 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 aee97ed982..f23a5fb43c 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 @@ -35,150 +35,188 @@ class CallIntentDataParserTests { @Test fun `empty data returns null`() { - val url = "" - assertThat(callIntentDataParser.parse(url)).isNull() + doTest("", null) } @Test fun `invalid data returns null`() { - val url = "!" - assertThat(callIntentDataParser.parse(url)).isNull() + doTest("!", null) } @Test fun `data with no scheme returns null`() { - val url = "test" - assertThat(callIntentDataParser.parse(url)).isNull() + doTest("test", null) } @Test 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() + doTest("http://call.element.io", null) + doTest("http://call.element.io/some-actual-call?with=parameters", null) } @Test fun `Element Call urls will be returned as is`() { - val httpsBaseUrl = "https://call.element.io" - val httpsCallUrl = VALID_CALL_URL_WITH_PARAM - assertThat(callIntentDataParser.parse(httpsBaseUrl)).isEqualTo("$httpsBaseUrl?$EXTRA_PARAMS") - assertThat(callIntentDataParser.parse(httpsCallUrl)).isEqualTo("$httpsCallUrl&$EXTRA_PARAMS") + doTest( + url = "https://call.element.io", + expectedResult = "https://call.element.io#?$EXTRA_PARAMS" + ) } @Test - fun `HTTP and HTTPS urls that don't come from EC return null`() { - val httpBaseUrl = "http://app.element.io" - 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() + fun `Element Call url with url param gets url extracted`() { + doTest( + url = VALID_CALL_URL_WITH_PARAM, + expectedResult = "$VALID_CALL_URL_WITH_PARAM#?$EXTRA_PARAMS" + ) } @Test - 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)).isNull() + fun `HTTP and HTTPS urls that don't come from EC return null`() { + doTest("http://app.element.io", null) + doTest("https://app.element.io", null, testEmbedded = false) + doTest("http://", null) + doTest("https://", null) } @Test - fun `element scheme with call host and url param gets url extracted`() { + fun `Element Call url with no url returns null`() { 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") + val url = "io.element.call:/?no_url=$encodedUrl" + assertThat(callIntentDataParser.parse(url)).isNull() } @Test - fun `element scheme 2 with url param with http returns null`() { - val embeddedUrl = "http://call.element.io/some-actual-call?with=parameters" + fun `element scheme with no call host returns null`() { + val embeddedUrl = VALID_CALL_URL_WITH_PARAM val encodedUrl = URLEncoder.encode(embeddedUrl, "utf-8") - val url = "io.element.call:/?url=$encodedUrl" + val url = "element://no-call?url=$encodedUrl" assertThat(callIntentDataParser.parse(url)).isNull() } @Test - fun `element scheme 2 with url param gets url extracted`() { - 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("$VALID_CALL_URL_WITH_PARAM&$EXTRA_PARAMS") + fun `element scheme with no data returns null`() { + val url = "element://call?url=" + assertThat(callIntentDataParser.parse(url)).isNull() } @Test - fun `element scheme with call host and no url param returns null`() { - 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" + fun `Element Call url with no data returns null`() { + val url = "io.element.call:/?url=" assertThat(callIntentDataParser.parse(url)).isNull() } @Test - fun `element scheme 2 with no url returns null`() { + fun `element invalid scheme returns null`() { val embeddedUrl = VALID_CALL_URL_WITH_PARAM val encodedUrl = URLEncoder.encode(embeddedUrl, "utf-8") - val url = "io.element.call:/?no_url=$encodedUrl" + val url = "bad.scheme:/?url=$encodedUrl" assertThat(callIntentDataParser.parse(url)).isNull() } @Test - fun `element scheme with no call host returns null`() { - 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() + fun `Element Call url with url extra param appPrompt gets url extracted`() { + doTest( + url = "${VALID_CALL_URL_WITH_PARAM}&appPrompt=true", + expectedResult = "$VALID_CALL_URL_WITH_PARAM#?$EXTRA_PARAMS" + ) } @Test - fun `element scheme with no data returns null`() { - val url = "element://call?url=" - assertThat(callIntentDataParser.parse(url)).isNull() + fun `Element Call url with url extra param in fragment appPrompt gets url extracted`() { + doTest( + url = "${VALID_CALL_URL_WITH_PARAM}#?appPrompt=true", + expectedResult = "$VALID_CALL_URL_WITH_PARAM#?appPrompt=false&confineToRoom=true" + ) } @Test - fun `element scheme 2 with no data returns null`() { - val url = "io.element.call:/?url=" - assertThat(callIntentDataParser.parse(url)).isNull() + fun `Element Call url with url extra param in fragment appPrompt and other gets url extracted`() { + doTest( + url = "${VALID_CALL_URL_WITH_PARAM}#?appPrompt=true&otherParam=maybe", + expectedResult = "$VALID_CALL_URL_WITH_PARAM#?appPrompt=false&otherParam=maybe&confineToRoom=true" + ) } @Test - fun `element invalid scheme returns null`() { - 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() + fun `Element Call url with url extra param confineToRoom gets url extracted`() { + doTest( + url = "${VALID_CALL_URL_WITH_PARAM}&confineToRoom=false", + expectedResult = "$VALID_CALL_URL_WITH_PARAM#?$EXTRA_PARAMS" + ) } @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") + fun `Element Call url with url extra param in fragment confineToRoom gets url extracted`() { + doTest( + url = "${VALID_CALL_URL_WITH_PARAM}#?confineToRoom=false", + expectedResult = "$VALID_CALL_URL_WITH_PARAM#?confineToRoom=true&appPrompt=false" + ) } @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") + fun `Element Call url with url extra param in fragment confineToRoom and more gets url extracted`() { + doTest( + url = "${VALID_CALL_URL_WITH_PARAM}#?confineToRoom=false&otherParam=maybe", + expectedResult = "$VALID_CALL_URL_WITH_PARAM#?confineToRoom=true&otherParam=maybe&appPrompt=false" + ) } @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") + fun `Element Call url with url fragment gets url extracted`() { + doTest( + url = "${VALID_CALL_URL_WITH_PARAM}#fragment", + expectedResult = "$VALID_CALL_URL_WITH_PARAM#fragment?$EXTRA_PARAMS" + ) } + @Test + fun `Element Call url with url fragment with params gets url extracted`() { + doTest( + url = "${VALID_CALL_URL_WITH_PARAM}#fragment?otherParam=maybe", + expectedResult = "$VALID_CALL_URL_WITH_PARAM#fragment?otherParam=maybe&$EXTRA_PARAMS" + ) + } + + @Test + fun `Element Call url with url fragment with other params gets url extracted`() { + doTest( + url = "${VALID_CALL_URL_WITH_PARAM}#?otherParam=maybe", + expectedResult = "$VALID_CALL_URL_WITH_PARAM#?otherParam=maybe&$EXTRA_PARAMS" + ) + } + + @Test + fun `Element Call url with empty fragment`() { + doTest( + url = "${VALID_CALL_URL_WITH_PARAM}#", + expectedResult = "$VALID_CALL_URL_WITH_PARAM#?$EXTRA_PARAMS" + ) + } + + @Test + fun `Element Call url with empty fragment query`() { + doTest( + url = "${VALID_CALL_URL_WITH_PARAM}#?", + expectedResult = "$VALID_CALL_URL_WITH_PARAM#?$EXTRA_PARAMS" + ) + } + + private fun doTest(url: String, expectedResult: String?, testEmbedded: Boolean = true) { + // Test direct parsing + assertThat(callIntentDataParser.parse(url)).isEqualTo(expectedResult) + + if (testEmbedded) { + // Test embedded url, scheme 1 + val encodedUrl = URLEncoder.encode(url, "utf-8") + val urlScheme1 = "element://call?url=$encodedUrl" + assertThat(callIntentDataParser.parse(urlScheme1)).isEqualTo(expectedResult) + + // Test embedded url, scheme 2 + val urlScheme2 = "io.element.call:/?url=$encodedUrl" + assertThat(callIntentDataParser.parse(urlScheme2)).isEqualTo(expectedResult) + } + } companion object { const val VALID_CALL_URL_WITH_PARAM = "https://call.element.io/some-actual-call?with=parameters"