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
diff --git a/java/src/com/android/intentresolver/ChooserActionFactory.java b/java/src/com/android/intentresolver/ChooserActionFactory.java
index 4e595c9..06c7e8d 100644
--- a/java/src/com/android/intentresolver/ChooserActionFactory.java
+++ b/java/src/com/android/intentresolver/ChooserActionFactory.java
@@ -89,6 +89,8 @@
     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<ChooserAction> mCustomActions;
@@ -145,7 +147,7 @@
     @VisibleForTesting
     ChooserActionFactory(
             Context context,
-            Runnable copyButtonRunnable,
+            @Nullable Runnable copyButtonRunnable,
             Runnable editButtonRunnable,
             List<ChooserAction> customActions,
             @Nullable ChooserAction modifyShareAction,
@@ -224,49 +226,24 @@
         return mExcludeSharedTextAction;
     }
 
+    @Nullable
     private static Runnable makeCopyButtonRunnable(
             Context context,
             Intent targetIntent,
             String referrerPackageName,
             Consumer<Integer> 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<Uri> 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 @@
         };
     }
 
+    @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 d72c9aa..8d994f0 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.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<ChooserActivityLogger>()
-    private val flags = mock<FeatureFlagRepository>()
     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<Int> {
-        var latestReturn = Integer.MIN_VALUE
+    private val resultConsumer =
+        object : Consumer<Int> {
+            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 @@
 
         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 @@
         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<ChooserRequestParameters> {
+                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<ChooserRequestParameters> {
+                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<ChooserRequestParameters> {
+                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<ChooserRequestParameters>()
         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<ChooserIntegratedDeviceComponents>(),
+            mock(),
             logger,
-            Consumer<Boolean>{},
-            Callable<View?>{null},
-            mock<ChooserActionFactory.ActionActivityStarter>(),
-            resultConsumer)
+            {},
+            { null },
+            mock(),
+            resultConsumer
+        )
     }
-}
\ No newline at end of file
+}