Add action chips to clipboard overlay

Uses TextClassifier to obtain additional action chips (e.g. open
Maps for an address) when text is copied.

Bug: 195554988
Test: manual
Change-Id: I65e7d244a083c5c4ea4cbf7476bd5ca94a0cc443
diff --git a/packages/SystemUI/res/layout/clipboard_overlay.xml b/packages/SystemUI/res/layout/clipboard_overlay.xml
index 7ffb3b2..2a3761e 100644
--- a/packages/SystemUI/res/layout/clipboard_overlay.xml
+++ b/packages/SystemUI/res/layout/clipboard_overlay.xml
@@ -17,6 +17,7 @@
 <com.android.systemui.clipboardoverlay.DraggableConstraintLayout
     xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:app="http://schemas.android.com/apk/res-auto"
+    android:alpha="0"
     android:layout_width="match_parent"
     android:layout_height="match_parent">
     <ImageView
diff --git a/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardListener.java b/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardListener.java
index 41a4963..0e1cd51 100644
--- a/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardListener.java
+++ b/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardListener.java
@@ -60,8 +60,8 @@
             return;
         }
         if (mClipboardOverlayController == null) {
-            mClipboardOverlayController = new ClipboardOverlayController(mContext,
-                    new TimeoutHandler(mContext));
+            mClipboardOverlayController =
+                    new ClipboardOverlayController(mContext, new TimeoutHandler(mContext));
         }
         mClipboardOverlayController.setClipData(mClipboardManager.getPrimaryClip());
         mClipboardOverlayController.setOnSessionCompleteListener(() -> {
diff --git a/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardOverlayController.java b/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardOverlayController.java
index caf0307..b6bcb87 100644
--- a/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardOverlayController.java
+++ b/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardOverlayController.java
@@ -28,6 +28,7 @@
 import android.animation.AnimatorListenerAdapter;
 import android.animation.ValueAnimator;
 import android.annotation.MainThread;
+import android.app.RemoteAction;
 import android.content.BroadcastReceiver;
 import android.content.ClipData;
 import android.content.ComponentName;
@@ -43,6 +44,7 @@
 import android.hardware.display.DisplayManager;
 import android.hardware.input.InputManager;
 import android.net.Uri;
+import android.os.AsyncTask;
 import android.os.Looper;
 import android.text.TextUtils;
 import android.util.Log;
@@ -60,8 +62,13 @@
 import android.view.WindowInsets;
 import android.view.WindowManager;
 import android.view.accessibility.AccessibilityManager;
+import android.view.textclassifier.TextClassification;
+import android.view.textclassifier.TextClassificationManager;
+import android.view.textclassifier.TextClassifier;
+import android.view.textclassifier.TextLinks;
 import android.widget.FrameLayout;
 import android.widget.ImageView;
+import android.widget.LinearLayout;
 import android.widget.TextView;
 
 import com.android.internal.policy.PhoneWindow;
@@ -71,6 +78,7 @@
 import com.android.systemui.screenshot.TimeoutHandler;
 
 import java.io.IOException;
+import java.util.ArrayList;
 
 /**
  * Controls state and UI for the overlay that appears when something is added to the clipboard
@@ -93,6 +101,7 @@
     private final PhoneWindow mWindow;
     private final TimeoutHandler mTimeoutHandler;
     private final AccessibilityManager mAccessibilityManager;
+    private final TextClassifier mTextClassifier;
 
     private final DraggableConstraintLayout mView;
     private final ImageView mImagePreview;
@@ -101,9 +110,13 @@
     private final ScreenshotActionChip mRemoteCopyChip;
     private final View mActionContainerBackground;
     private final View mDismissButton;
+    private final LinearLayout mActionContainer;
+    private final ArrayList<ScreenshotActionChip> mActionChips = new ArrayList<>();
 
     private Runnable mOnSessionCompleteListener;
 
+
+    private InputMonitor mInputMonitor;
     private InputEventReceiver mInputEventReceiver;
 
     private BroadcastReceiver mCloseDialogsReceiver;
@@ -117,6 +130,8 @@
         mContext = displayContext.createWindowContext(TYPE_SCREENSHOT, null);
 
         mAccessibilityManager = AccessibilityManager.getInstance(mContext);
+        mTextClassifier = requireNonNull(context.getSystemService(TextClassificationManager.class))
+                .getTextClassifier();
 
         mWindowManager = mContext.getSystemService(WindowManager.class);
 
@@ -134,8 +149,9 @@
 
         mView = (DraggableConstraintLayout)
                 LayoutInflater.from(mContext).inflate(R.layout.clipboard_overlay, null);
-        mActionContainerBackground = requireNonNull(
-                mView.findViewById(R.id.actions_container_background));
+        mActionContainerBackground =
+                requireNonNull(mView.findViewById(R.id.actions_container_background));
+        mActionContainer = requireNonNull(mView.findViewById(R.id.actions));
         mImagePreview = requireNonNull(mView.findViewById(R.id.image_preview));
         mTextPreview = requireNonNull(mView.findViewById(R.id.text_preview));
         mEditChip = requireNonNull(mView.findViewById(R.id.edit_chip));
@@ -143,7 +159,7 @@
         mDismissButton = requireNonNull(mView.findViewById(R.id.dismiss_button));
 
         mView.setOnDismissCallback(this::hideImmediate);
-        mView.setOnInteractionCallback(() -> mTimeoutHandler.resetTimeout());
+        mView.setOnInteractionCallback(mTimeoutHandler::resetTimeout);
 
         mDismissButton.setOnClickListener(view -> animateOut());
 
@@ -166,7 +182,7 @@
         withWindowAttached(() -> {
             mWindow.setContentView(mView);
             updateInsets(mWindowManager.getCurrentWindowMetrics().getWindowInsets());
-            mView.post(() -> getEnterAnimation().start());
+            mView.post(this::animateIn);
         });
 
         mTimeoutHandler.setOnTimeoutRunnable(this::animateOut);
@@ -199,12 +215,15 @@
 
     void setClipData(ClipData clipData) {
         reset();
-
         if (clipData == null || clipData.getItemCount() == 0) {
-            showTextPreview(
-                    mContext.getResources().getString(R.string.clipboard_overlay_text_copied));
+            showTextPreview(mContext.getResources().getString(
+                    R.string.clipboard_overlay_text_copied));
         } else if (!TextUtils.isEmpty(clipData.getItemAt(0).getText())) {
-            showEditableText(clipData.getItemAt(0).getText());
+            ClipData.Item item = clipData.getItemAt(0);
+            if (item.getTextLinks() != null) {
+                AsyncTask.execute(() -> classifyText(clipData.getItemAt(0)));
+            }
+            showEditableText(item.getText());
         } else if (clipData.getItemAt(0).getUri() != null) {
             // How to handle non-image URIs?
             showEditableImage(clipData.getItemAt(0).getUri());
@@ -212,7 +231,6 @@
             showTextPreview(
                     mContext.getResources().getString(R.string.clipboard_overlay_text_copied));
         }
-
         mTimeoutHandler.resetTimeout();
     }
 
@@ -220,10 +238,40 @@
         mOnSessionCompleteListener = runnable;
     }
 
+    private void classifyText(ClipData.Item item) {
+        ArrayList<RemoteAction> actions = new ArrayList<>();
+        for (TextLinks.TextLink link : item.getTextLinks().getLinks()) {
+            TextClassification classification = mTextClassifier.classifyText(
+                    item.getText(), link.getStart(), link.getEnd(), null);
+            actions.addAll(classification.getActions());
+        }
+        mView.post(() -> {
+            for (ScreenshotActionChip chip : mActionChips) {
+                mActionContainer.removeView(chip);
+            }
+            mActionChips.clear();
+            for (RemoteAction action : actions) {
+                ScreenshotActionChip chip = constructActionChip(action);
+                mActionContainer.addView(chip);
+                mActionChips.add(chip);
+            }
+        });
+    }
+
+    private ScreenshotActionChip constructActionChip(RemoteAction action) {
+        ScreenshotActionChip chip = (ScreenshotActionChip) LayoutInflater.from(mContext).inflate(
+                R.layout.screenshot_action_chip, mActionContainer, false);
+        chip.setText(action.getTitle());
+        chip.setIcon(action.getIcon(), false);
+        chip.setPendingIntent(action.getActionIntent(), this::animateOut);
+        chip.setAlpha(1);
+        return chip;
+    }
+
     private void monitorOutsideTouches() {
         InputManager inputManager = mContext.getSystemService(InputManager.class);
-        InputMonitor monitor = inputManager.monitorGestureInput("clipboard overlay", 0);
-        mInputEventReceiver = new InputEventReceiver(monitor.getInputChannel(),
+        mInputMonitor = inputManager.monitorGestureInput("clipboard overlay", 0);
+        mInputEventReceiver = new InputEventReceiver(mInputMonitor.getInputChannel(),
                 Looper.getMainLooper()) {
             @Override
             public void onInputEvent(InputEvent event) {
@@ -311,6 +359,10 @@
         return nearbyIntent;
     }
 
+    private void animateIn() {
+        getEnterAnimation().start();
+    }
+
     private void animateOut() {
         getExitAnimation().start();
     }
@@ -390,6 +442,10 @@
             mInputEventReceiver.dispose();
             mInputEventReceiver = null;
         }
+        if (mInputMonitor != null) {
+            mInputMonitor.dispose();
+            mInputMonitor = null;
+        }
         if (mOnSessionCompleteListener != null) {
             mOnSessionCompleteListener.run();
         }
@@ -398,6 +454,10 @@
     private void reset() {
         mView.setTranslationX(0);
         mView.setAlpha(0);
+        for (ScreenshotActionChip chip : mActionChips) {
+            mActionContainer.removeView(chip);
+        }
+        mActionChips.clear();
         mTimeoutHandler.cancelTimeout();
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotActionChip.java b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotActionChip.java
index 6c01f0e..dec5afd 100644
--- a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotActionChip.java
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotActionChip.java
@@ -21,7 +21,6 @@
 import android.graphics.drawable.Icon;
 import android.util.AttributeSet;
 import android.util.Log;
-import android.view.View;
 import android.widget.FrameLayout;
 import android.widget.ImageView;
 import android.widget.LinearLayout;
@@ -129,7 +128,6 @@
             iconParams.setMarginStart(paddingHorizontal);
             iconParams.setMarginEnd(paddingHorizontal);
         }
-        mTextView.setVisibility(hasText ? View.VISIBLE : View.GONE);
         mIconView.setLayoutParams(iconParams);
         mTextView.setLayoutParams(textParams);
     }