diff options
3 files changed, 207 insertions, 0 deletions
diff --git a/java/src/com/android/intentresolver/contentpreview/PayloadToggleInteractor.kt b/java/src/com/android/intentresolver/contentpreview/PayloadToggleInteractor.kt index 87f53e85..ca868226 100644 --- a/java/src/com/android/intentresolver/contentpreview/PayloadToggleInteractor.kt +++ b/java/src/com/android/intentresolver/contentpreview/PayloadToggleInteractor.kt @@ -17,6 +17,7 @@ package com.android.intentresolver.contentpreview import android.net.Uri +import android.service.chooser.ChooserAction import android.util.SparseArray import java.io.Closeable import kotlinx.coroutines.flow.Flow @@ -51,6 +52,8 @@ class PayloadToggleInteractor { val previewUri: Uri?, ) + data class CallbackResult(val customActions: List<ChooserAction>?) + interface CursorReader : Closeable { val count: Int val hasMoreBefore: Boolean diff --git a/java/src/com/android/intentresolver/contentpreview/SelectionChangeCallback.kt b/java/src/com/android/intentresolver/contentpreview/SelectionChangeCallback.kt new file mode 100644 index 00000000..2cc58a97 --- /dev/null +++ b/java/src/com/android/intentresolver/contentpreview/SelectionChangeCallback.kt @@ -0,0 +1,70 @@ +/* + * Copyright 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.contentpreview + +import android.content.ContentInterface +import android.content.Intent +import android.content.Intent.EXTRA_CHOOSER_CUSTOM_ACTIONS +import android.content.Intent.EXTRA_INTENT +import android.net.Uri +import android.os.Bundle +import android.service.chooser.ChooserAction +import com.android.intentresolver.contentpreview.PayloadToggleInteractor.CallbackResult + +// TODO: replace with the new API AdditionalContentContract$MethodNames#ON_SELECTION_CHANGED +private const val MethodName = "onSelectionChanged" + +/** + * Encapsulates payload change callback invocation to the sharing app; handles callback arguments + * and result format mapping. + */ +class SelectionChangeCallback( + private val uri: Uri, + private val chooserIntent: Intent, + private val contentResolver: ContentInterface, +) : (Intent) -> CallbackResult? { + fun onSelectionChanged(targetIntent: Intent): CallbackResult? = + contentResolver + .call( + requireNotNull(uri.authority) { "URI authority can not be null" }, + MethodName, + uri.toString(), + Bundle().apply { + putParcelable( + EXTRA_INTENT, + Intent(chooserIntent).apply { putExtra(EXTRA_INTENT, targetIntent) } + ) + } + ) + ?.let { bundle -> + val actions = + if (bundle.containsKey(EXTRA_CHOOSER_CUSTOM_ACTIONS)) { + bundle + .getParcelableArray( + EXTRA_CHOOSER_CUSTOM_ACTIONS, + ChooserAction::class.java + ) + ?.filterNotNull() + ?: emptyList() + } else { + null + } + CallbackResult(actions) + } + + override fun invoke(targetIntent: Intent) = onSelectionChanged(targetIntent) +} diff --git a/tests/unit/src/com/android/intentresolver/contentpreview/SelectionChangeCallbackTest.kt b/tests/unit/src/com/android/intentresolver/contentpreview/SelectionChangeCallbackTest.kt new file mode 100644 index 00000000..110448bb --- /dev/null +++ b/tests/unit/src/com/android/intentresolver/contentpreview/SelectionChangeCallbackTest.kt @@ -0,0 +1,134 @@ +/* + * Copyright 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.contentpreview + +import android.app.PendingIntent +import android.content.ContentInterface +import android.content.Intent +import android.content.Intent.ACTION_CHOOSER +import android.content.Intent.ACTION_SEND_MULTIPLE +import android.content.Intent.EXTRA_CHOOSER_CUSTOM_ACTIONS +import android.content.Intent.EXTRA_INTENT +import android.content.Intent.EXTRA_STREAM +import android.graphics.drawable.Icon +import android.net.Uri +import android.os.Bundle +import android.service.chooser.ChooserAction +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.platform.app.InstrumentationRegistry +import com.android.intentresolver.any +import com.android.intentresolver.argumentCaptor +import com.android.intentresolver.capture +import com.android.intentresolver.mock +import com.android.intentresolver.whenever +import com.google.common.truth.Truth.assertThat +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mockito.times +import org.mockito.Mockito.verify + +// TODO: replace with the new API AdditionalContentContract$MethodNames#ON_SELECTION_CHANGED +private const val MethodName = "onSelectionChanged" + +@RunWith(AndroidJUnit4::class) +class SelectionChangeCallbackTest { + private val uri = Uri.parse("content://org.pkg/content-provider") + private val chooserIntent = Intent(ACTION_CHOOSER) + private val contentResolver = mock<ContentInterface>() + private val context = InstrumentationRegistry.getInstrumentation().context + + @Test + fun testCallbackProducesChooserIntentArgument() { + val a1 = + ChooserAction.Builder( + Icon.createWithContentUri(createUri(10)), + "Action 1", + PendingIntent.getBroadcast( + context, + 1, + Intent("test"), + PendingIntent.FLAG_IMMUTABLE + ) + ) + .build() + val a2 = + ChooserAction.Builder( + Icon.createWithContentUri(createUri(11)), + "Action 2", + PendingIntent.getBroadcast( + context, + 1, + Intent("test"), + PendingIntent.FLAG_IMMUTABLE + ) + ) + .build() + whenever(contentResolver.call(any<String>(), any(), any(), any())) + .thenReturn( + Bundle().apply { putParcelableArray(EXTRA_CHOOSER_CUSTOM_ACTIONS, arrayOf(a1, a2)) } + ) + + val testSubject = SelectionChangeCallback(uri, chooserIntent, contentResolver) + + val u1 = createUri(1) + val u2 = createUri(2) + val targetIntent = + Intent(ACTION_SEND_MULTIPLE).apply { + val uris = + ArrayList<Uri>().apply { + add(u1) + add(u2) + } + putExtra(EXTRA_STREAM, uris) + type = "image/jpg" + } + val result = testSubject.onSelectionChanged(targetIntent) + assertThat(result).isNotNull() + assertThat(result?.customActions).hasSize(2) + assertThat(result?.customActions?.get(0)?.icon).isEqualTo(a1.icon) + assertThat(result?.customActions?.get(0)?.label).isEqualTo(a1.label) + assertThat(result?.customActions?.get(1)?.icon).isEqualTo(a2.icon) + assertThat(result?.customActions?.get(1)?.label).isEqualTo(a2.label) + + val authorityCaptor = argumentCaptor<String>() + val methodCaptor = argumentCaptor<String>() + val argCaptor = argumentCaptor<String>() + val extraCaptor = argumentCaptor<Bundle>() + verify(contentResolver, times(1)) + .call( + capture(authorityCaptor), + capture(methodCaptor), + capture(argCaptor), + capture(extraCaptor) + ) + assertThat(authorityCaptor.value).isEqualTo(uri.authority) + assertThat(methodCaptor.value).isEqualTo(MethodName) + assertThat(argCaptor.value).isEqualTo(uri.toString()) + val extraBundle = extraCaptor.value + assertThat(extraBundle).isNotNull() + val argChooserIntent = extraBundle.getParcelable(EXTRA_INTENT, Intent::class.java) + assertThat(argChooserIntent).isNotNull() + assertThat(argChooserIntent?.action).isEqualTo(chooserIntent.action) + val argTargetIntent = argChooserIntent?.getParcelableExtra(EXTRA_INTENT, Intent::class.java) + assertThat(argTargetIntent?.action).isEqualTo(targetIntent.action) + assertThat(argTargetIntent?.getParcelableArrayListExtra(EXTRA_STREAM, Uri::class.java)) + .containsExactly(u1, u2) + .inOrder() + } +} + +private fun createUri(id: Int) = Uri.parse("content://org.pkg.images/$id.png") |