diff options
7 files changed, 339 insertions, 7 deletions
diff --git a/packages/SystemUI/res/layout/bubble_menu_view.xml b/packages/SystemUI/res/layout/bubble_menu_view.xml new file mode 100644 index 000000000000..24608d3e9611 --- /dev/null +++ b/packages/SystemUI/res/layout/bubble_menu_view.xml @@ -0,0 +1,43 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ 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. + --> +<com.android.systemui.bubbles.BubbleMenuView + xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_height="match_parent" + android:layout_width="match_parent" + android:background="#66000000" + android:visibility="gone" + android:id="@+id/bubble_menu_container"> + + <FrameLayout + android:layout_height="@dimen/individual_bubble_size" + android:layout_width="wrap_content" + android:background="#FFFFFF" + android:id="@+id/bubble_menu_view"> + + <ImageView + android:id="@*android:id/icon" + android:layout_width="@dimen/global_actions_grid_item_icon_width" + android:layout_height="@dimen/global_actions_grid_item_icon_height" + android:layout_marginTop="@dimen/global_actions_grid_item_icon_top_margin" + android:layout_marginBottom="@dimen/global_actions_grid_item_icon_bottom_margin" + android:layout_marginLeft="@dimen/global_actions_grid_item_icon_side_margin" + android:layout_marginRight="@dimen/global_actions_grid_item_icon_side_margin" + android:scaleType="centerInside" + android:tint="@color/global_actions_text" + /> + </FrameLayout> +</com.android.systemui.bubbles.BubbleMenuView> diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleController.java b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleController.java index dbb193669083..19381940543e 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 static java.lang.annotation.RetentionPolicy.SOURCE; 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 androidx.annotation.Nullable; 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.ShadeController; 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.io.PrintWriter; 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 @@ public class BubbleController implements ConfigurationController.ConfigurationLi @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 @@ public class BubbleController implements ConfigurationController.ConfigurationLi 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 @@ public class BubbleController implements ConfigurationController.ConfigurationLi } /** + * 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 @@ public class BubbleController implements ConfigurationController.ConfigurationLi 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 @@ public class BubbleController implements ConfigurationController.ConfigurationLi 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 @@ public class BubbleController implements ConfigurationController.ConfigurationLi }); mUserCreatedBubbles = new HashSet<>(); + + mScreenshotHelper = new ScreenshotHelper(context); } /** @@ -337,6 +367,9 @@ public class BubbleController implements ConfigurationController.ConfigurationLi if (mExpandListener != null) { mStackView.setExpandListener(mExpandListener); } + if (mBubbleScreenshotListener != null) { + mStackView.setBubbleScreenshotListener(mBubbleScreenshotListener); + } } } @@ -1058,4 +1091,71 @@ public class BubbleController implements ConfigurationController.ConfigurationLi } } } + + // 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 e138d9387ca6..8299f2261b8e 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 @@ public class BubbleExperimentConfig { 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 @@ public class BubbleExperimentConfig { } /** + * 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 000000000000..e8eb72e8392f --- /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 29de2f049690..29a4bb1fca84 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 @@ public class BubbleStackView extends FrameLayout { /** 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 @@ public class BubbleStackView extends FrameLayout { 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 @@ public class BubbleStackView extends FrameLayout { 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 @@ public class BubbleStackView extends FrameLayout { 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 @@ public class BubbleStackView extends FrameLayout { } /** + * 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 class BubbleStackView extends FrameLayout { 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 @@ public class BubbleStackView extends FrameLayout { return; } + hideBubbleMenu(); mStackAnimationController.cancelStackPositionAnimations(); mBubbleContainer.setActiveController(mStackAnimationController); hideFlyoutImmediate(); @@ -1473,6 +1493,11 @@ public class BubbleStackView extends FrameLayout { @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 @@ public class BubbleStackView extends FrameLayout { } 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 44e013a34f54..b1d205c79c99 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 @@ class BubbleTouchHandler implements View.OnTouchListener { 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 @@ class BubbleTouchHandler implements View.OnTouchListener { mTouchSlopSquared = touchSlop * touchSlop; mBubbleData = bubbleData; mStack = stackView; + mContext = context; } @Override @@ -91,15 +93,24 @@ class BubbleTouchHandler implements View.OnTouchListener { // 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 @@ class BubbleTouchHandler implements View.OnTouchListener { 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 @@ class BubbleTouchHandler implements View.OnTouchListener { } 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 @@ class BubbleTouchHandler implements View.OnTouchListener { 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 @@ class BubbleTouchHandler implements View.OnTouchListener { mVelocityTracker.recycle(); mVelocityTracker = null; } - mTouchedView = null; mMovedEnough = false; mInDismissTarget = false; diff --git a/packages/SystemUI/tests/src/com/android/systemui/bubbles/BubbleControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/bubbles/BubbleControllerTest.java index 4c707f45efc1..ae43aa2f4118 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/bubbles/BubbleControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/bubbles/BubbleControllerTest.java @@ -82,6 +82,7 @@ import com.android.systemui.statusbar.phone.StatusBarWindowController; import com.android.systemui.statusbar.policy.BatteryController; import com.android.systemui.statusbar.policy.ConfigurationController; import com.android.systemui.statusbar.policy.HeadsUpManager; +import com.android.systemui.statusbar.policy.RemoteInputUriController; import com.android.systemui.statusbar.policy.ZenModeController; import com.android.systemui.util.InjectionInflationController; @@ -153,6 +154,8 @@ public class BubbleControllerTest extends SysuiTestCase { private Resources mResources; @Mock private Lazy<ShadeController> mShadeController; + @Mock + private RemoteInputUriController mRemoteInputUriController; private SuperStatusBarViewFactory mSuperStatusBarViewFactory; private BubbleData mBubbleData; @@ -212,7 +215,8 @@ public class BubbleControllerTest extends SysuiTestCase { mZenModeController, mLockscreenUserManager, mNotificationGroupManager, - mNotificationEntryManager); + mNotificationEntryManager, + mRemoteInputUriController); mBubbleController.setBubbleStateChangeListener(mBubbleStateChangeListener); mBubbleController.setExpandListener(mBubbleExpandListener); @@ -708,11 +712,13 @@ public class BubbleControllerTest extends SysuiTestCase { ZenModeController zenModeController, NotificationLockscreenUserManager lockscreenUserManager, NotificationGroupManager groupManager, - NotificationEntryManager entryManager) { + NotificationEntryManager entryManager, + RemoteInputUriController remoteInputUriController) { super(context, statusBarWindowController, statusBarStateController, shadeController, data, Runnable::run, configurationController, interruptionStateProvider, - zenModeController, lockscreenUserManager, groupManager, entryManager); + zenModeController, lockscreenUserManager, groupManager, entryManager, + remoteInputUriController); } } |