diff options
3 files changed, 233 insertions, 11 deletions
diff --git a/java/src/com/android/intentresolver/ui/viewmodel/ChooserRequestReader.kt b/java/src/com/android/intentresolver/ui/viewmodel/ChooserRequestReader.kt index 1644e409..13de84b2 100644 --- a/java/src/com/android/intentresolver/ui/viewmodel/ChooserRequestReader.kt +++ b/java/src/com/android/intentresolver/ui/viewmodel/ChooserRequestReader.kt @@ -36,7 +36,6 @@ import android.content.Intent.EXTRA_TEXT import android.content.Intent.EXTRA_TITLE import android.content.Intent.FLAG_ACTIVITY_MULTIPLE_TASK import android.content.Intent.FLAG_ACTIVITY_NEW_DOCUMENT -import android.content.IntentFilter import android.content.IntentSender import android.net.Uri import android.os.Bundle @@ -164,7 +163,7 @@ fun readChooserRequest( refinementIntentSender = refinementIntentSender, sharedText = sharedText, sharedTextTitle = sharedTextTitle, - shareTargetFilter = targetIntent.toShareTargetFilter(), + shareTargetFilter = targetIntent.createIntentFilter(), additionalContentUri = additionalContentUri, focusedItemPosition = focusedItemPos, contentTypeHint = contentTypeHint, @@ -180,12 +179,3 @@ fun Validation.readChooserActions(): List<ChooserAction>? = optional(array<ChooserAction>(EXTRA_CHOOSER_CUSTOM_ACTIONS)) ?.filter { hasValidIcon(it) } ?.take(MAX_CHOOSER_ACTIONS) - -private fun Intent.toShareTargetFilter(): IntentFilter? { - return type?.let { - IntentFilter().apply { - action?.also { addAction(it) } - addDataType(it) - } - } -} diff --git a/java/src/com/android/intentresolver/ui/viewmodel/IntentExt.kt b/java/src/com/android/intentresolver/ui/viewmodel/IntentExt.kt new file mode 100644 index 00000000..30f16d20 --- /dev/null +++ b/java/src/com/android/intentresolver/ui/viewmodel/IntentExt.kt @@ -0,0 +1,58 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * 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 com.android.intentresolver.ui.viewmodel + +import android.content.Intent +import android.content.IntentFilter +import android.content.IntentFilter.MalformedMimeTypeException +import android.net.Uri +import android.os.PatternMatcher + +/** Collects Uris from standard locations within the Intent. */ +fun Intent.collectUris(): Set<Uri> = buildSet { + data?.also { add(it) } + @Suppress("DEPRECATION") + when (val stream = extras?.get(Intent.EXTRA_STREAM)) { + is Uri -> add(stream) + is ArrayList<*> -> addAll(stream.mapNotNull { it as? Uri }) + else -> Unit + } + clipData?.apply { (0..<itemCount).mapNotNull { getItemAt(it).uri }.forEach(::add) } +} + +fun IntentFilter.addUri(uri: Uri) { + uri.scheme?.also { addDataScheme(it) } + uri.host?.also { addDataAuthority(it, null) } + uri.path?.also { addDataPath(it, PatternMatcher.PATTERN_LITERAL) } +} + +fun Intent.createIntentFilter(): IntentFilter? { + val uris = collectUris() + if (action == null && uris.isEmpty()) { + // at least one is required to be meaningful + return null + } + return IntentFilter().also { filter -> + type?.also { + try { + filter.addDataType(it) + } catch (_: MalformedMimeTypeException) { // ignore malformed type + } + } + action?.also { filter.addAction(it) } + uris.forEach(filter::addUri) + } +} diff --git a/tests/unit/src/com/android/intentresolver/ui/viewmodel/IntentExtTest.kt b/tests/unit/src/com/android/intentresolver/ui/viewmodel/IntentExtTest.kt new file mode 100644 index 00000000..8fc162ca --- /dev/null +++ b/tests/unit/src/com/android/intentresolver/ui/viewmodel/IntentExtTest.kt @@ -0,0 +1,174 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * 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 com.android.intentresolver.ui.viewmodel + +import android.content.Intent +import android.content.Intent.ACTION_SEND +import android.content.Intent.EXTRA_STREAM +import android.net.Uri +import com.google.common.truth.Truth.assertThat +import org.junit.Test + +class IntentExtTest { + + @Test + fun noActionOrUris() { + val intent = Intent() + + assertThat(intent.createIntentFilter()).isNull() + } + + @Test + fun uriInData() { + val intent = Intent(ACTION_SEND) + intent.setDataAndType( + Uri.Builder().scheme("scheme1").encodedAuthority("auth1").path("path1").build(), + "image/png", + ) + + val filter = intent.createIntentFilter() + + assertThat(filter).isNotNull() + assertThat(filter!!.dataTypes()[0]).isEqualTo("image/png") + assertThat(filter.actionsIterator().next()).isEqualTo(ACTION_SEND) + assertThat(filter.schemesIterator().next()).isEqualTo("scheme1") + assertThat(filter.authoritiesIterator().next().host).isEqualTo("auth1") + assertThat(filter.getDataPath(0).path).isEqualTo("/path1") + } + + @Test + fun noAction() { + val intent = Intent() + intent.setDataAndType( + Uri.Builder().scheme("scheme1").encodedAuthority("auth1").path("path1").build(), + "image/png", + ) + + val filter = intent.createIntentFilter() + + assertThat(filter).isNotNull() + assertThat(filter!!.dataTypes()[0]).isEqualTo("image/png") + assertThat(filter.countActions()).isEqualTo(0) + assertThat(filter.schemesIterator().next()).isEqualTo("scheme1") + assertThat(filter.authoritiesIterator().next().host).isEqualTo("auth1") + assertThat(filter.getDataPath(0).path).isEqualTo("/path1") + } + + @Test + fun singleUriInExtraStream() { + val intent = Intent(ACTION_SEND) + intent.type = "image/png" + intent.putExtra( + EXTRA_STREAM, + Uri.Builder().scheme("scheme1").encodedAuthority("auth1").path("path1").build(), + ) + + val filter = intent.createIntentFilter() + + assertThat(filter).isNotNull() + assertThat(filter!!.dataTypes()[0]).isEqualTo("image/png") + assertThat(filter.actionsIterator().next()).isEqualTo(ACTION_SEND) + assertThat(filter.schemesIterator().next()).isEqualTo("scheme1") + assertThat(filter.authoritiesIterator().next().host).isEqualTo("auth1") + assertThat(filter.getDataPath(0).path).isEqualTo("/path1") + } + + @Test + fun uriInDataAndStream() { + val intent = Intent(ACTION_SEND) + intent.setDataAndType( + Uri.Builder().scheme("scheme1").encodedAuthority("auth1").path("path1").build(), + "image/png", + ) + + intent.putExtra( + EXTRA_STREAM, + Uri.Builder().scheme("scheme2").encodedAuthority("auth2").path("path2").build(), + ) + val filter = intent.createIntentFilter() + + assertThat(filter).isNotNull() + assertThat(filter!!.dataTypes()[0]).isEqualTo("image/png") + assertThat(filter.actionsIterator().next()).isEqualTo(ACTION_SEND) + assertThat(filter.getDataScheme(0)).isEqualTo("scheme1") + assertThat(filter.getDataScheme(1)).isEqualTo("scheme2") + assertThat(filter.getDataAuthority(0).host).isEqualTo("auth1") + assertThat(filter.getDataAuthority(1).host).isEqualTo("auth2") + assertThat(filter.getDataPath(0).path).isEqualTo("/path1") + assertThat(filter.getDataPath(1).path).isEqualTo("/path2") + } + + @Test + fun multipleUris() { + val intent = Intent(ACTION_SEND) + intent.type = "image/png" + val uris = + arrayListOf( + Uri.Builder().scheme("scheme1").encodedAuthority("auth1").path("path1").build(), + Uri.Builder().scheme("scheme2").encodedAuthority("auth2").path("path2").build(), + ) + intent.putExtra(EXTRA_STREAM, uris) + + val filter = intent.createIntentFilter() + + assertThat(filter).isNotNull() + assertThat(filter!!.dataTypes()[0]).isEqualTo("image/png") + assertThat(filter.actionsIterator().next()).isEqualTo(ACTION_SEND) + assertThat(filter.getDataScheme(0)).isEqualTo("scheme1") + assertThat(filter.getDataScheme(1)).isEqualTo("scheme2") + assertThat(filter.getDataAuthority(0).host).isEqualTo("auth1") + assertThat(filter.getDataAuthority(1).host).isEqualTo("auth2") + assertThat(filter.getDataPath(0).path).isEqualTo("/path1") + assertThat(filter.getDataPath(1).path).isEqualTo("/path2") + } + + @Test + fun multipleUrisWithNullValues() { + val intent = Intent(ACTION_SEND) + intent.type = "image/png" + val uris = + arrayListOf( + null, + Uri.Builder().scheme("scheme1").encodedAuthority("auth1").path("path1").build(), + null, + ) + intent.putExtra(EXTRA_STREAM, uris) + + val filter = intent.createIntentFilter() + + assertThat(filter).isNotNull() + assertThat(filter!!.dataTypes()[0]).isEqualTo("image/png") + assertThat(filter.actionsIterator().next()).isEqualTo(ACTION_SEND) + assertThat(filter.getDataScheme(0)).isEqualTo("scheme1") + assertThat(filter.getDataAuthority(0).host).isEqualTo("auth1") + assertThat(filter.getDataPath(0).path).isEqualTo("/path1") + } + + @Test + fun badMimeType() { + val intent = Intent(ACTION_SEND) + intent.type = "badType" + intent.putExtra( + EXTRA_STREAM, + Uri.Builder().scheme("scheme1").encodedAuthority("authority1").path("path1").build(), + ) + + val filter = intent.createIntentFilter() + + assertThat(filter).isNotNull() + assertThat(filter!!.countDataTypes()).isEqualTo(0) + } +} |