Create a long-press menu for sending screenshots to bubbles.

Test: Manual. Enable the long-press bubble menu with "adb shell settings set secure allow_bubble_menu 1". Then, long-press on a bubble to show a menu and tap the screenshot button to send a screenshot to the selected bubble. This won't work when the bubble is expanded.
Change-Id: I35380a8a4775e568699cf4527c6071bf932eb715
diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleController.java b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleController.java
index dbb1936..1938194 100644
--- a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleController.java
+++ b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleController.java
@@ -44,13 +44,19 @@
 
 import android.annotation.UserIdInt;
 import android.app.ActivityManager.RunningTaskInfo;
+import android.app.Notification;
 import android.app.NotificationManager;
 import android.app.PendingIntent;
+import android.app.RemoteInput;
 import android.content.Context;
+import android.content.Intent;
 import android.content.pm.ActivityInfo;
 import android.content.pm.PackageManager;
+import android.content.pm.ShortcutManager;
 import android.content.res.Configuration;
 import android.graphics.Rect;
+import android.net.Uri;
+import android.os.Handler;
 import android.os.RemoteException;
 import android.os.ServiceManager;
 import android.service.notification.NotificationListenerService.RankingMap;
@@ -69,6 +75,7 @@
 
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.statusbar.IStatusBarService;
+import com.android.internal.util.ScreenshotHelper;
 import com.android.systemui.R;
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
 import com.android.systemui.shared.system.ActivityManagerWrapper;
@@ -86,6 +93,7 @@
 import com.android.systemui.statusbar.phone.StatusBar;
 import com.android.systemui.statusbar.phone.StatusBarWindowController;
 import com.android.systemui.statusbar.policy.ConfigurationController;
+import com.android.systemui.statusbar.policy.RemoteInputUriController;
 import com.android.systemui.statusbar.policy.ZenModeController;
 
 import java.io.FileDescriptor;
@@ -93,8 +101,10 @@
 import java.lang.annotation.Retention;
 import java.lang.annotation.Target;
 import java.util.ArrayList;
+import java.util.HashMap;
 import java.util.HashSet;
 import java.util.List;
+import java.util.function.Consumer;
 
 import javax.inject.Inject;
 import javax.inject.Singleton;
@@ -138,6 +148,8 @@
     @Nullable private BubbleStackView.SurfaceSynchronizer mSurfaceSynchronizer;
     private final NotificationGroupManager mNotificationGroupManager;
     private final Lazy<ShadeController> mShadeController;
+    private final RemoteInputUriController mRemoteInputUriController;
+    private Handler mHandler = new Handler() {};
 
     private BubbleData mBubbleData;
     @Nullable private BubbleStackView mStackView;
@@ -155,6 +167,8 @@
     private final StatusBarWindowController mStatusBarWindowController;
     private final ZenModeController mZenModeController;
     private StatusBarStateListener mStatusBarStateListener;
+    private final ScreenshotHelper mScreenshotHelper;
+
 
     private final NotificationInterruptionStateProvider mNotificationInterruptionStateProvider;
     private IStatusBarService mBarService;
@@ -192,6 +206,16 @@
     }
 
     /**
+     * Listener for handling bubble screenshot events.
+     */
+    public interface BubbleScreenshotListener {
+        /**
+         * Called to trigger taking a screenshot and sending the result to a bubble.
+         */
+        void onBubbleScreenshot(Bubble bubble);
+    }
+
+    /**
      * Listens for the current state of the status bar and updates the visibility state
      * of bubbles as needed.
      */
@@ -226,10 +250,12 @@
             ZenModeController zenModeController,
             NotificationLockscreenUserManager notifUserManager,
             NotificationGroupManager groupManager,
-            NotificationEntryManager entryManager) {
+            NotificationEntryManager entryManager,
+            RemoteInputUriController remoteInputUriController) {
         this(context, statusBarWindowController, statusBarStateController, shadeController,
                 data, null /* synchronizer */, configurationController, interruptionStateProvider,
-                zenModeController, notifUserManager, groupManager, entryManager);
+                zenModeController, notifUserManager, groupManager, entryManager,
+                remoteInputUriController);
     }
 
     public BubbleController(Context context,
@@ -243,11 +269,13 @@
             ZenModeController zenModeController,
             NotificationLockscreenUserManager notifUserManager,
             NotificationGroupManager groupManager,
-            NotificationEntryManager entryManager) {
+            NotificationEntryManager entryManager,
+            RemoteInputUriController remoteInputUriController) {
         mContext = context;
         mNotificationInterruptionStateProvider = interruptionStateProvider;
         mNotifUserManager = notifUserManager;
         mZenModeController = zenModeController;
+        mRemoteInputUriController = remoteInputUriController;
         mZenModeController.addCallback(new ZenModeController.Callback() {
             @Override
             public void onZenChanged(int zen) {
@@ -320,6 +348,8 @@
                 });
 
         mUserCreatedBubbles = new HashSet<>();
+
+        mScreenshotHelper = new ScreenshotHelper(context);
     }
 
     /**
@@ -337,6 +367,9 @@
             if (mExpandListener != null) {
                 mStackView.setExpandListener(mExpandListener);
             }
+            if (mBubbleScreenshotListener != null) {
+                mStackView.setBubbleScreenshotListener(mBubbleScreenshotListener);
+            }
         }
     }
 
@@ -1058,4 +1091,71 @@
             }
         }
     }
+
+    // TODO: Copied from RemoteInputView. Consolidate RemoteInput intent logic.
+    private Intent prepareRemoteInputFromData(String contentType, Uri data,
+            RemoteInput remoteInput, NotificationEntry entry) {
+        HashMap<String, Uri> results = new HashMap<>();
+        results.put(contentType, data);
+        mRemoteInputUriController.grantInlineReplyUriPermission(entry.getSbn(), data);
+        Intent fillInIntent = new Intent().addFlags(Intent.FLAG_RECEIVER_FOREGROUND);
+        RemoteInput.addDataResultToIntent(remoteInput, fillInIntent, results);
+
+        return fillInIntent;
+    }
+
+    // TODO: Copied from RemoteInputView. Consolidate RemoteInput intent logic.
+    private void sendRemoteInput(Intent intent, NotificationEntry entry,
+            PendingIntent pendingIntent) {
+        // Tell ShortcutManager that this package has been "activated".  ShortcutManager
+        // will reset the throttling for this package.
+        // Strictly speaking, the intent receiver may be different from the notification publisher,
+        // but that's an edge case, and also because we can't always know which package will receive
+        // an intent, so we just reset for the publisher.
+        mContext.getSystemService(ShortcutManager.class).onApplicationActive(
+                entry.getSbn().getPackageName(),
+                entry.getSbn().getUser().getIdentifier());
+
+        try {
+            pendingIntent.send(mContext, 0, intent);
+        } catch (PendingIntent.CanceledException e) {
+            Log.i(TAG, "Unable to send remote input result", e);
+        }
+    }
+
+    private void sendScreenshotToBubble(Bubble bubble) {
+        // delay allows the bubble menu to disappear before the screenshot
+        // done here because we already have a Handler to delay with.
+        // TODO: Hide bubble + menu UI from screenshots entirely instead of just delaying.
+        mHandler.postDelayed(new Runnable() {
+            @Override
+            public void run() {
+                mScreenshotHelper.takeScreenshot(
+                        android.view.WindowManager.TAKE_SCREENSHOT_FULLSCREEN,
+                        true /* hasStatus */,
+                        true /* hasNav */,
+                        mHandler,
+                        new Consumer<Uri>() {
+                            @Override
+                            public void accept(Uri uri) {
+                                if (uri != null) {
+                                    NotificationEntry entry = bubble.getEntry();
+                                    Pair<RemoteInput, Notification.Action> pair = entry.getSbn()
+                                            .getNotification().findRemoteInputActionPair(false);
+                                    RemoteInput remoteInput = pair.first;
+                                    Notification.Action action = pair.second;
+                                    Intent dataIntent = prepareRemoteInputFromData("image/png", uri,
+                                            remoteInput, entry);
+                                    sendRemoteInput(dataIntent, entry, action.actionIntent);
+                                    mBubbleData.setSelectedBubble(bubble);
+                                    mBubbleData.setExpanded(true);
+                                }
+                            }
+                        });
+            }
+        }, 200);
+    }
+
+    private final BubbleScreenshotListener mBubbleScreenshotListener =
+            bubble -> sendScreenshotToBubble(bubble);
 }
diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleExperimentConfig.java b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleExperimentConfig.java
index e138d93..8299f22 100644
--- a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleExperimentConfig.java
+++ b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleExperimentConfig.java
@@ -68,6 +68,9 @@
 
     private static final String WHITELISTED_AUTO_BUBBLE_APPS = "whitelisted_auto_bubble_apps";
 
+    private static final String ALLOW_BUBBLE_MENU = "allow_bubble_screenshot_menu";
+    private static final boolean ALLOW_BUBBLE_MENU_DEFAULT = false;
+
     /**
      * When true, if a notification has the information necessary to bubble (i.e. valid
      * contentIntent and an icon or image), then a {@link android.app.Notification.BubbleMetadata}
@@ -123,6 +126,16 @@
     }
 
     /**
+     * When true, show a menu when a bubble is long-pressed, which will allow the user to take
+     * actions on that bubble.
+     */
+    static boolean allowBubbleScreenshotMenu(Context context) {
+        return Settings.Secure.getInt(context.getContentResolver(),
+                ALLOW_BUBBLE_MENU,
+                ALLOW_BUBBLE_MENU_DEFAULT ? 1 : 0) != 0;
+    }
+
+    /**
      * If {@link #allowAnyNotifToBubble(Context)} is true, this method creates and adds
      * {@link android.app.Notification.BubbleMetadata} to the notification entry as long as
      * the notification has necessary info for BubbleMetadata.
diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleMenuView.java b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleMenuView.java
new file mode 100644
index 0000000..e8eb72e
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleMenuView.java
@@ -0,0 +1,81 @@
+/*
+ * Copyright (C) 2019 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.systemui.bubbles;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.view.View;
+import android.widget.FrameLayout;
+import android.widget.ImageView;
+
+import com.android.systemui.R;
+
+/**
+ * Menu which allows users to take actions on bubbles, ex. screenshots.
+ */
+public class BubbleMenuView extends FrameLayout {
+    private FrameLayout mMenu;
+    private boolean mShowing = false;
+
+    public BubbleMenuView(Context context, AttributeSet attrs) {
+        super(context, attrs);
+    }
+
+    public BubbleMenuView(Context context) {
+        super(context);
+    }
+
+    @Override
+    protected void onFinishInflate() {
+        super.onFinishInflate();
+        mMenu = findViewById(R.id.bubble_menu_view);
+        ImageView icon = findViewById(com.android.internal.R.id.icon);
+        icon.setImageDrawable(mContext.getDrawable(com.android.internal.R.drawable.ic_screenshot));
+    }
+
+    /**
+     * Get the bubble menu view.
+     */
+    public View getMenuView() {
+        return mMenu;
+    }
+
+    /**
+     * Checks whether the bubble menu is currently displayed.
+     */
+    public boolean isShowing() {
+        return mShowing;
+    }
+
+    /**
+     * Show the bubble menu at the specified position on the screen.
+     */
+    public void show(float x, float y) {
+        mShowing = true;
+        this.setVisibility(VISIBLE);
+        mMenu.setTranslationX(x);
+        mMenu.setTranslationY(y);
+    }
+
+    /**
+     * Hide the bubble menu.
+     */
+    public void hide() {
+        mShowing = false;
+        this.setVisibility(GONE);
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleStackView.java b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleStackView.java
index 29de2f0..29a4bb1 100644
--- a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleStackView.java
+++ b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleStackView.java
@@ -110,6 +110,7 @@
     /** How long to wait, in milliseconds, before hiding the flyout. */
     @VisibleForTesting
     static final int FLYOUT_HIDE_AFTER = 5000;
+    private BubbleController.BubbleScreenshotListener mBubbleScreenshotListener;
 
     /**
      * Interface to synchronize {@link View} state and the screen.
@@ -163,6 +164,7 @@
     private ExpandedAnimationController mExpandedAnimationController;
 
     private FrameLayout mExpandedViewContainer;
+    @Nullable private BubbleMenuView mBubbleMenuView;
 
     private BubbleFlyoutView mFlyout;
     /** Runnable that fades out the flyout and then sets it to GONE. */
@@ -194,6 +196,7 @@
     private int mPointerHeight;
     private int mStatusBarHeight;
     private int mImeOffset;
+    private int mBubbleMenuOffset = 252;
     private BubbleIconFactory mBubbleIconFactory;
     private Bubble mExpandedBubble;
     private boolean mIsExpanded;
@@ -492,6 +495,9 @@
             mDesaturateAndDarkenPaint.setColorFilter(new ColorMatrixColorFilter(animatedMatrix));
             mDesaturateAndDarkenTargetView.setLayerPaint(mDesaturateAndDarkenPaint);
         });
+
+        mInflater.inflate(R.layout.bubble_menu_view, this);
+        mBubbleMenuView = findViewById(R.id.bubble_menu_container);
     }
 
     private void setUpFlyout() {
@@ -683,6 +689,13 @@
     }
 
     /**
+     * Sets the screenshot listener.
+     */
+    public void setBubbleScreenshotListener(BubbleController.BubbleScreenshotListener listener) {
+        mBubbleScreenshotListener = listener;
+    }
+
+    /**
      * Whether the stack of bubbles is expanded or not.
      */
     public boolean isExpanded() {
@@ -870,6 +883,12 @@
     public View getTargetView(MotionEvent event) {
         float x = event.getRawX();
         float y = event.getRawY();
+        if (mBubbleMenuView.isShowing()) {
+            if (isIntersecting(mBubbleMenuView.getMenuView(), x, y)) {
+                return mBubbleMenuView;
+            }
+            return null;
+        }
         if (mIsExpanded) {
             if (isIntersecting(mBubbleContainer, x, y)) {
                 // Could be tapping or dragging a bubble while expanded
@@ -1074,6 +1093,7 @@
             return;
         }
 
+        hideBubbleMenu();
         mStackAnimationController.cancelStackPositionAnimations();
         mBubbleContainer.setActiveController(mStackAnimationController);
         hideFlyoutImmediate();
@@ -1473,6 +1493,11 @@
 
     @Override
     public void getBoundsOnScreen(Rect outRect) {
+        // If the bubble menu is open, the entire screen should capture touch events.
+        if (mBubbleMenuView.isShowing()) {
+            outRect.set(0, 0, getWidth(), getHeight());
+            return;
+        }
         if (!mIsExpanded) {
             if (mBubbleContainer.getChildCount() > 0) {
                 mBubbleContainer.getChildAt(0).getBoundsOnScreen(outRect);
@@ -1700,4 +1725,43 @@
         }
         return bubbles;
     }
+
+    /**
+     * Show the bubble menu, positioned relative to the stack.
+     */
+    public void showBubbleMenu() {
+        PointF currentPos = mStackAnimationController.getStackPosition();
+        float yPos = currentPos.y;
+        float xPos = currentPos.x;
+        if (mStackAnimationController.isStackOnLeftSide()) {
+            xPos += mBubbleSize;
+        } else {
+            //TODO: Use the width of the menu instead of this fixed offset. Offset used for now
+            // because menu width isn't correct the first time the menu is shown.
+            xPos -= mBubbleMenuOffset;
+        }
+
+        mBubbleMenuView.show(xPos, yPos);
+    }
+
+    /**
+     * Hide the bubble menu.
+     */
+    public void hideBubbleMenu() {
+        mBubbleMenuView.hide();
+    }
+
+    /**
+     * Determines whether the bubble menu is currently showing.
+     */
+    public boolean isShowingBubbleMenu() {
+        return mBubbleMenuView.isShowing();
+    }
+
+    /**
+     * Take a screenshot and send it to the specified bubble.
+     */
+    public void sendScreenshotToBubble(Bubble bubble) {
+        mBubbleScreenshotListener.onBubbleScreenshot(bubble);
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleTouchHandler.java b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleTouchHandler.java
index 44e013a..b1d205c 100644
--- a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleTouchHandler.java
+++ b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleTouchHandler.java
@@ -57,6 +57,7 @@
     private final PointF mViewPositionOnTouchDown = new PointF();
     private final BubbleStackView mStack;
     private final BubbleData mBubbleData;
+    private final Context mContext;
 
     private BubbleController mController = Dependency.get(BubbleController.class);
 
@@ -75,6 +76,7 @@
         mTouchSlopSquared = touchSlop * touchSlop;
         mBubbleData = bubbleData;
         mStack = stackView;
+        mContext = context;
     }
 
     @Override
@@ -91,15 +93,24 @@
         // anything, collapse the stack.
         if (action == MotionEvent.ACTION_OUTSIDE || mTouchedView == null) {
             mBubbleData.setExpanded(false);
+            mStack.hideBubbleMenu();
             resetForNextGesture();
             return false;
         }
 
+        if (mTouchedView instanceof BubbleMenuView) {
+            mStack.hideBubbleMenu();
+            resetForNextGesture();
+            mStack.sendScreenshotToBubble(mBubbleData.getSelectedBubble());
+            return false;
+        }
+
         if (!(mTouchedView instanceof BadgedImageView)
                 && !(mTouchedView instanceof BubbleStackView)
                 && !(mTouchedView instanceof BubbleFlyoutView)) {
             // Not touching anything touchable, but we shouldn't collapse (e.g. touching edge
             // of expanded view).
+            mStack.hideBubbleMenu();
             resetForNextGesture();
             return false;
         }
@@ -132,6 +143,10 @@
 
                 break;
             case MotionEvent.ACTION_MOVE:
+                // block all further touch inputs once the menu is open
+                if (mStack.isShowingBubbleMenu()) {
+                    return true;
+                }
                 trackMovement(event);
                 final float deltaX = rawX - mTouchDown.x;
                 final float deltaY = rawY - mTouchDown.y;
@@ -148,6 +163,13 @@
                     } else {
                         mStack.onBubbleDragged(mTouchedView, viewX, viewY);
                     }
+                } else {
+                    float touchTime = event.getEventTime() - event.getDownTime();
+                    if (touchTime > ViewConfiguration.getLongPressTimeout() && !mStack.isExpanded()
+                            && BubbleExperimentConfig.allowBubbleScreenshotMenu(mContext)) {
+                        mStack.showBubbleMenu();
+                        return true;
+                    }
                 }
 
                 final boolean currentlyInDismissTarget = mStack.isInDismissTarget(event);
@@ -171,6 +193,10 @@
                 break;
 
             case MotionEvent.ACTION_UP:
+                if (mStack.isShowingBubbleMenu()) {
+                    resetForNextGesture();
+                    return true;
+                }
                 trackMovement(event);
                 mVelocityTracker.computeCurrentVelocity(/* maxVelocity */ 1000);
                 final float velX = mVelocityTracker.getXVelocity();
@@ -261,7 +287,6 @@
             mVelocityTracker.recycle();
             mVelocityTracker = null;
         }
-
         mTouchedView = null;
         mMovedEnough = false;
         mInDismissTarget = false;