From 6e616930cd005b15ff235b64fb06362dbdf31f93 Mon Sep 17 00:00:00 2001 From: 1 Date: Fri, 16 Jun 2023 14:56:17 +0000 Subject: Add an extra to sharesheet's image edit intent To allow the editor to customize the behavior for the sharesheet flow if desired. Test: Validate that EXTRA is received by editor. Bug: 287621918 (cherry picked from https://googleplex-android-review.googlesource.com/q/commit:f609ae4c5d04918b328ba12e4aa99ca8fecfde88) Merged-In: Ia70cc645e85054fd73f77f989cb4267cdc524cd4 Change-Id: Ia70cc645e85054fd73f77f989cb4267cdc524cd4 --- java/src/com/android/intentresolver/ChooserActionFactory.java | 6 ++++++ 1 file changed, 6 insertions(+) (limited to 'java/src') diff --git a/java/src/com/android/intentresolver/ChooserActionFactory.java b/java/src/com/android/intentresolver/ChooserActionFactory.java index 6ec62753..4e595c96 100644 --- a/java/src/com/android/intentresolver/ChooserActionFactory.java +++ b/java/src/com/android/intentresolver/ChooserActionFactory.java @@ -78,6 +78,11 @@ public final class ChooserActionFactory implements ChooserContentPreviewUi.Actio | Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION | Intent.FLAG_GRANT_PREFIX_URI_PERMISSION; + // Boolean extra used to inform the editor that it may want to customize the editing experience + // for the sharesheet editing flow. + private static final String EDIT_SOURCE = "edit_source"; + private static final String EDIT_SOURCE_SHARESHEET = "sharesheet"; + private static final String CHIP_LABEL_METADATA_KEY = "android.service.chooser.chip_label"; private static final String CHIP_ICON_METADATA_KEY = "android.service.chooser.chip_icon"; @@ -284,6 +289,7 @@ public final class ChooserActionFactory implements ChooserContentPreviewUi.Actio resolveIntent.setFlags(originalIntent.getFlags() & URI_PERMISSION_INTENT_FLAGS); resolveIntent.setComponent(editorComponent); resolveIntent.setAction(Intent.ACTION_EDIT); + resolveIntent.putExtra(EDIT_SOURCE, EDIT_SOURCE_SHARESHEET); String originalAction = originalIntent.getAction(); if (Intent.ACTION_SEND.equals(originalAction)) { if (resolveIntent.getData() == null) { -- cgit v1.2.3-59-g8ed1b From a09f62d036c55fb7963d4f545bbcd00efb567a5a Mon Sep 17 00:00:00 2001 From: Andrey Epin Date: Tue, 13 Jun 2023 10:45:20 -0700 Subject: Provide copy action only when a text is sent The copy button will be present to the user only when a text without images is sent i.e. intent action is ACTION_SEND, EXTRA_STREAM does not have any URI and EXTRA_TEXT is not null. Fix: 287061873 Fix: 275382122 Fix: 288425116 Test: manual testing Test: atest ChooserActionFactoryTest (cherry picked from https://googleplex-android-review.googlesource.com/q/commit:a1c9dc6af9fb0ff4b50ed1b8e7f59bdfccbfa682) Merged-In: I26c6eef24e7f909842eb066a73286a7525124d21 Change-Id: I26c6eef24e7f909842eb066a73286a7525124d21 --- .../intentresolver/ChooserActionFactory.java | 75 +++++----- .../intentresolver/ChooserActionFactoryTest.kt | 152 +++++++++++++++------ 2 files changed, 151 insertions(+), 76 deletions(-) (limited to 'java/src') diff --git a/java/src/com/android/intentresolver/ChooserActionFactory.java b/java/src/com/android/intentresolver/ChooserActionFactory.java index 4e595c96..06c7e8d7 100644 --- a/java/src/com/android/intentresolver/ChooserActionFactory.java +++ b/java/src/com/android/intentresolver/ChooserActionFactory.java @@ -89,6 +89,8 @@ public final class ChooserActionFactory implements ChooserContentPreviewUi.Actio private static final String IMAGE_EDITOR_SHARED_ELEMENT = "screenshot_preview_image"; private final Context mContext; + + @Nullable private final Runnable mCopyButtonRunnable; private final Runnable mEditButtonRunnable; private final ImmutableList mCustomActions; @@ -145,7 +147,7 @@ public final class ChooserActionFactory implements ChooserContentPreviewUi.Actio @VisibleForTesting ChooserActionFactory( Context context, - Runnable copyButtonRunnable, + @Nullable Runnable copyButtonRunnable, Runnable editButtonRunnable, List customActions, @Nullable ChooserAction modifyShareAction, @@ -224,49 +226,24 @@ public final class ChooserActionFactory implements ChooserContentPreviewUi.Actio return mExcludeSharedTextAction; } + @Nullable private static Runnable makeCopyButtonRunnable( Context context, Intent targetIntent, String referrerPackageName, Consumer finishCallback, ChooserActivityLogger logger) { + final ClipData clipData; + try { + clipData = extractTextToCopy(targetIntent); + } catch (Throwable t) { + Log.e(TAG, "Failed to extract data to copy", t); + return null; + } + if (clipData == null) { + return null; + } return () -> { - if (targetIntent == null) { - finishCallback.accept(null); - return; - } - - final String action = targetIntent.getAction(); - - ClipData clipData = null; - if (Intent.ACTION_SEND.equals(action)) { - String extraText = targetIntent.getStringExtra(Intent.EXTRA_TEXT); - Uri extraStream = targetIntent.getParcelableExtra(Intent.EXTRA_STREAM); - - if (extraText != null) { - clipData = ClipData.newPlainText(null, extraText); - } else if (extraStream != null) { - clipData = ClipData.newUri(context.getContentResolver(), null, extraStream); - } else { - Log.w(TAG, "No data available to copy to clipboard"); - return; - } - } else if (Intent.ACTION_SEND_MULTIPLE.equals(action)) { - final ArrayList streams = targetIntent.getParcelableArrayListExtra( - Intent.EXTRA_STREAM); - clipData = ClipData.newUri(context.getContentResolver(), null, streams.get(0)); - for (int i = 1; i < streams.size(); i++) { - clipData.addItem( - context.getContentResolver(), - new ClipData.Item(streams.get(i))); - } - } else { - // expected to only be visible with ACTION_SEND or ACTION_SEND_MULTIPLE - // so warn about unexpected action - Log.w(TAG, "Action (" + action + ") not supported for copying to clipboard"); - return; - } - ClipboardManager clipboardManager = (ClipboardManager) context.getSystemService( Context.CLIPBOARD_SERVICE); clipboardManager.setPrimaryClipAsPackage(clipData, referrerPackageName); @@ -276,6 +253,30 @@ public final class ChooserActionFactory implements ChooserContentPreviewUi.Actio }; } + @Nullable + private static ClipData extractTextToCopy(Intent targetIntent) { + if (targetIntent == null) { + return null; + } + + final String action = targetIntent.getAction(); + + ClipData clipData = null; + if (Intent.ACTION_SEND.equals(action)) { + String extraText = targetIntent.getStringExtra(Intent.EXTRA_TEXT); + + if (extraText != null) { + clipData = ClipData.newPlainText(null, extraText); + } else { + Log.w(TAG, "No data available to copy to clipboard"); + } + } else { + // expected to only be visible with ACTION_SEND (when a text is shared) + Log.d(TAG, "Action (" + action + ") not supported for copying to clipboard"); + } + return clipData; + } + private static TargetInfo getEditSharingTarget( Context context, Intent originalIntent, diff --git a/java/tests/src/com/android/intentresolver/ChooserActionFactoryTest.kt b/java/tests/src/com/android/intentresolver/ChooserActionFactoryTest.kt index d72c9aa6..8d994f08 100644 --- a/java/tests/src/com/android/intentresolver/ChooserActionFactoryTest.kt +++ b/java/tests/src/com/android/intentresolver/ChooserActionFactoryTest.kt @@ -25,48 +25,45 @@ import android.content.IntentFilter import android.content.res.Resources import android.graphics.drawable.Icon import android.service.chooser.ChooserAction -import android.view.View import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.platform.app.InstrumentationRegistry -import com.android.intentresolver.flags.FeatureFlagRepository import com.google.common.collect.ImmutableList import com.google.common.truth.Truth.assertThat +import java.util.concurrent.CountDownLatch +import java.util.concurrent.TimeUnit +import java.util.function.Consumer import org.junit.After import org.junit.Assert.assertEquals import org.junit.Before import org.junit.Test import org.junit.runner.RunWith import org.mockito.Mockito -import java.util.concurrent.Callable -import java.util.concurrent.CountDownLatch -import java.util.concurrent.TimeUnit -import java.util.function.Consumer @RunWith(AndroidJUnit4::class) class ChooserActionFactoryTest { private val context = InstrumentationRegistry.getInstrumentation().getContext() private val logger = mock() - private val flags = mock() private val actionLabel = "Action label" private val modifyShareLabel = "Modify share" private val testAction = "com.android.intentresolver.testaction" private val countdown = CountDownLatch(1) - private val testReceiver: BroadcastReceiver = object : BroadcastReceiver() { - override fun onReceive(context: Context, intent: Intent) { - // Just doing at most a single countdown per test. - countdown.countDown() + private val testReceiver: BroadcastReceiver = + object : BroadcastReceiver() { + override fun onReceive(context: Context, intent: Intent) { + // Just doing at most a single countdown per test. + countdown.countDown() + } } - } - private object resultConsumer : Consumer { - var latestReturn = Integer.MIN_VALUE + private val resultConsumer = + object : Consumer { + var latestReturn = Integer.MIN_VALUE - override fun accept(resultCode: Int) { - latestReturn = resultCode + override fun accept(resultCode: Int) { + latestReturn = resultCode + } } - } - @Before fun setup() { context.registerReceiver(testReceiver, IntentFilter(testAction)) @@ -91,7 +88,7 @@ class ChooserActionFactoryTest { Mockito.verify(logger).logCustomActionSelected(eq(0)) assertEquals(Activity.RESULT_OK, resultConsumer.latestReturn) - // Verify the pendingintent has been called + // Verify the pending intent has been called countdown.await(500, TimeUnit.MILLISECONDS) } @@ -109,42 +106,119 @@ class ChooserActionFactoryTest { val action = factory.modifyShareAction ?: error("Modify share action should not be null") action.onClicked.run() - Mockito.verify(logger).logActionSelected( - eq(ChooserActivityLogger.SELECTION_TYPE_MODIFY_SHARE)) + Mockito.verify(logger) + .logActionSelected(eq(ChooserActivityLogger.SELECTION_TYPE_MODIFY_SHARE)) assertEquals(Activity.RESULT_OK, resultConsumer.latestReturn) - // Verify the pendingintent has been called + // Verify the pending intent has been called countdown.await(500, TimeUnit.MILLISECONDS) } + @Test + fun nonSendAction_noCopyRunnable() { + val targetIntent = + Intent(Intent.ACTION_SEND_MULTIPLE).apply { + putExtra(Intent.EXTRA_TEXT, "Text to show") + } + + val chooserRequest = + mock { + whenever(this.targetIntent).thenReturn(targetIntent) + whenever(chooserActions).thenReturn(ImmutableList.of()) + } + val testSubject = + ChooserActionFactory( + context, + chooserRequest, + mock(), + logger, + {}, + { null }, + mock(), + {}, + ) + assertThat(testSubject.copyButtonRunnable).isNull() + } + + @Test + fun sendActionNoText_noCopyRunnable() { + val targetIntent = Intent(Intent.ACTION_SEND) + + val chooserRequest = + mock { + whenever(this.targetIntent).thenReturn(targetIntent) + whenever(chooserActions).thenReturn(ImmutableList.of()) + } + val testSubject = + ChooserActionFactory( + context, + chooserRequest, + mock(), + logger, + {}, + { null }, + mock(), + {}, + ) + assertThat(testSubject.copyButtonRunnable).isNull() + } + + @Test + fun sendActionWithText_nonNullCopyRunnable() { + val targetIntent = Intent(Intent.ACTION_SEND).apply { putExtra(Intent.EXTRA_TEXT, "Text") } + + val chooserRequest = + mock { + whenever(this.targetIntent).thenReturn(targetIntent) + whenever(chooserActions).thenReturn(ImmutableList.of()) + } + val testSubject = + ChooserActionFactory( + context, + chooserRequest, + mock(), + logger, + {}, + { null }, + mock(), + {}, + ) + assertThat(testSubject.copyButtonRunnable).isNotNull() + } + private fun createFactory(includeModifyShare: Boolean = false): ChooserActionFactory { - val testPendingIntent = PendingIntent.getActivity(context, 0, Intent(testAction),0) + val testPendingIntent = PendingIntent.getActivity(context, 0, Intent(testAction), 0) val targetIntent = Intent() - val action = ChooserAction.Builder( - Icon.createWithResource("", Resources.ID_NULL), - actionLabel, - testPendingIntent - ).build() + val action = + ChooserAction.Builder( + Icon.createWithResource("", Resources.ID_NULL), + actionLabel, + testPendingIntent + ) + .build() val chooserRequest = mock() whenever(chooserRequest.targetIntent).thenReturn(targetIntent) whenever(chooserRequest.chooserActions).thenReturn(ImmutableList.of(action)) if (includeModifyShare) { - val modifyShare = ChooserAction.Builder( - Icon.createWithResource("", Resources.ID_NULL), - modifyShareLabel, - testPendingIntent - ).build() + val modifyShare = + ChooserAction.Builder( + Icon.createWithResource("", Resources.ID_NULL), + modifyShareLabel, + testPendingIntent + ) + .build() whenever(chooserRequest.modifyShareAction).thenReturn(modifyShare) } return ChooserActionFactory( context, chooserRequest, - mock(), + mock(), logger, - Consumer{}, - Callable{null}, - mock(), - resultConsumer) + {}, + { null }, + mock(), + resultConsumer + ) } -} \ No newline at end of file +} -- cgit v1.2.3-59-g8ed1b