Loading packages/SystemUI/res/layout/bubble_menu_view.xml 0 → 100644 +43 −0 Original line number Diff line number Diff line <?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> packages/SystemUI/src/com/android/systemui/bubbles/BubbleController.java +103 −3 Original line number Diff line number Diff line Loading @@ -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; Loading @@ -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; Loading @@ -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; Loading @@ -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; Loading Loading @@ -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; Loading @@ -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; Loading Loading @@ -191,6 +205,16 @@ public class BubbleController implements ConfigurationController.ConfigurationLi void onBubbleExpandChanged(boolean isExpanding, String key); } /** * 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. Loading Loading @@ -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, Loading @@ -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) { Loading Loading @@ -320,6 +348,8 @@ public class BubbleController implements ConfigurationController.ConfigurationLi }); mUserCreatedBubbles = new HashSet<>(); mScreenshotHelper = new ScreenshotHelper(context); } /** Loading @@ -337,6 +367,9 @@ public class BubbleController implements ConfigurationController.ConfigurationLi if (mExpandListener != null) { mStackView.setExpandListener(mExpandListener); } if (mBubbleScreenshotListener != null) { mStackView.setBubbleScreenshotListener(mBubbleScreenshotListener); } } } Loading Loading @@ -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); } packages/SystemUI/src/com/android/systemui/bubbles/BubbleExperimentConfig.java +13 −0 Original line number Diff line number Diff line Loading @@ -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} Loading Loading @@ -122,6 +125,16 @@ public class BubbleExperimentConfig { return false; } /** * 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 Loading packages/SystemUI/src/com/android/systemui/bubbles/BubbleMenuView.java 0 → 100644 +81 −0 Original line number Diff line number Diff line /* * 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); } } packages/SystemUI/src/com/android/systemui/bubbles/BubbleStackView.java +64 −0 Original line number Diff line number Diff line Loading @@ -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. Loading Loading @@ -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. */ Loading Loading @@ -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; Loading Loading @@ -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() { Loading Loading @@ -682,6 +688,13 @@ public class BubbleStackView extends FrameLayout { mExpandListener = listener; } /** * Sets the screenshot listener. */ public void setBubbleScreenshotListener(BubbleController.BubbleScreenshotListener listener) { mBubbleScreenshotListener = listener; } /** * Whether the stack of bubbles is expanded or not. */ Loading Loading @@ -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 Loading Loading @@ -1074,6 +1093,7 @@ public class BubbleStackView extends FrameLayout { return; } hideBubbleMenu(); mStackAnimationController.cancelStackPositionAnimations(); mBubbleContainer.setActiveController(mStackAnimationController); hideFlyoutImmediate(); Loading Loading @@ -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); Loading Loading @@ -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); } } Loading
packages/SystemUI/res/layout/bubble_menu_view.xml 0 → 100644 +43 −0 Original line number Diff line number Diff line <?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>
packages/SystemUI/src/com/android/systemui/bubbles/BubbleController.java +103 −3 Original line number Diff line number Diff line Loading @@ -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; Loading @@ -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; Loading @@ -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; Loading @@ -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; Loading Loading @@ -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; Loading @@ -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; Loading Loading @@ -191,6 +205,16 @@ public class BubbleController implements ConfigurationController.ConfigurationLi void onBubbleExpandChanged(boolean isExpanding, String key); } /** * 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. Loading Loading @@ -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, Loading @@ -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) { Loading Loading @@ -320,6 +348,8 @@ public class BubbleController implements ConfigurationController.ConfigurationLi }); mUserCreatedBubbles = new HashSet<>(); mScreenshotHelper = new ScreenshotHelper(context); } /** Loading @@ -337,6 +367,9 @@ public class BubbleController implements ConfigurationController.ConfigurationLi if (mExpandListener != null) { mStackView.setExpandListener(mExpandListener); } if (mBubbleScreenshotListener != null) { mStackView.setBubbleScreenshotListener(mBubbleScreenshotListener); } } } Loading Loading @@ -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); }
packages/SystemUI/src/com/android/systemui/bubbles/BubbleExperimentConfig.java +13 −0 Original line number Diff line number Diff line Loading @@ -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} Loading Loading @@ -122,6 +125,16 @@ public class BubbleExperimentConfig { return false; } /** * 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 Loading
packages/SystemUI/src/com/android/systemui/bubbles/BubbleMenuView.java 0 → 100644 +81 −0 Original line number Diff line number Diff line /* * 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); } }
packages/SystemUI/src/com/android/systemui/bubbles/BubbleStackView.java +64 −0 Original line number Diff line number Diff line Loading @@ -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. Loading Loading @@ -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. */ Loading Loading @@ -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; Loading Loading @@ -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() { Loading Loading @@ -682,6 +688,13 @@ public class BubbleStackView extends FrameLayout { mExpandListener = listener; } /** * Sets the screenshot listener. */ public void setBubbleScreenshotListener(BubbleController.BubbleScreenshotListener listener) { mBubbleScreenshotListener = listener; } /** * Whether the stack of bubbles is expanded or not. */ Loading Loading @@ -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 Loading Loading @@ -1074,6 +1093,7 @@ public class BubbleStackView extends FrameLayout { return; } hideBubbleMenu(); mStackAnimationController.cancelStackPositionAnimations(); mBubbleContainer.setActiveController(mStackAnimationController); hideFlyoutImmediate(); Loading Loading @@ -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); Loading Loading @@ -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); } }