Home | History | Annotate | Download | only in bubbles
      1 /*
      2  * Copyright (C) 2018 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package com.android.systemui.bubbles;
     18 
     19 import static android.app.Notification.FLAG_BUBBLE;
     20 import static android.app.NotificationManager.Policy.SUPPRESSED_EFFECT_BADGE;
     21 import static android.app.NotificationManager.Policy.SUPPRESSED_EFFECT_NOTIFICATION_LIST;
     22 import static android.app.NotificationManager.Policy.SUPPRESSED_EFFECT_PEEK;
     23 import static android.content.pm.ActivityInfo.DOCUMENT_LAUNCH_ALWAYS;
     24 import static android.service.notification.NotificationListenerService.REASON_APP_CANCEL;
     25 import static android.service.notification.NotificationListenerService.REASON_APP_CANCEL_ALL;
     26 import static android.service.notification.NotificationListenerService.REASON_CANCEL;
     27 import static android.service.notification.NotificationListenerService.REASON_CANCEL_ALL;
     28 import static android.view.Display.DEFAULT_DISPLAY;
     29 import static android.view.Display.INVALID_DISPLAY;
     30 import static android.view.View.INVISIBLE;
     31 import static android.view.View.VISIBLE;
     32 import static android.view.ViewGroup.LayoutParams.MATCH_PARENT;
     33 
     34 import static com.android.systemui.statusbar.StatusBarState.SHADE;
     35 import static com.android.systemui.statusbar.notification.NotificationEntryManager.UNDEFINED_DISMISS_REASON;
     36 
     37 import static java.lang.annotation.ElementType.FIELD;
     38 import static java.lang.annotation.ElementType.LOCAL_VARIABLE;
     39 import static java.lang.annotation.ElementType.PARAMETER;
     40 import static java.lang.annotation.RetentionPolicy.SOURCE;
     41 
     42 import android.app.ActivityManager;
     43 import android.app.ActivityManager.RunningTaskInfo;
     44 import android.app.Notification;
     45 import android.app.NotificationManager;
     46 import android.app.PendingIntent;
     47 import android.content.Context;
     48 import android.content.pm.ActivityInfo;
     49 import android.content.pm.ParceledListSlice;
     50 import android.content.res.Configuration;
     51 import android.graphics.Rect;
     52 import android.os.RemoteException;
     53 import android.os.ServiceManager;
     54 import android.provider.Settings;
     55 import android.service.notification.NotificationListenerService.RankingMap;
     56 import android.service.notification.StatusBarNotification;
     57 import android.service.notification.ZenModeConfig;
     58 import android.util.Log;
     59 import android.util.Pair;
     60 import android.view.Display;
     61 import android.view.IPinnedStackController;
     62 import android.view.IPinnedStackListener;
     63 import android.view.ViewGroup;
     64 import android.widget.FrameLayout;
     65 
     66 import androidx.annotation.IntDef;
     67 import androidx.annotation.MainThread;
     68 import androidx.annotation.Nullable;
     69 
     70 import com.android.internal.annotations.VisibleForTesting;
     71 import com.android.internal.statusbar.IStatusBarService;
     72 import com.android.systemui.Dependency;
     73 import com.android.systemui.R;
     74 import com.android.systemui.plugins.statusbar.StatusBarStateController;
     75 import com.android.systemui.shared.system.ActivityManagerWrapper;
     76 import com.android.systemui.shared.system.TaskStackChangeListener;
     77 import com.android.systemui.shared.system.WindowManagerWrapper;
     78 import com.android.systemui.statusbar.NotificationRemoveInterceptor;
     79 import com.android.systemui.statusbar.notification.NotificationEntryListener;
     80 import com.android.systemui.statusbar.notification.NotificationEntryManager;
     81 import com.android.systemui.statusbar.notification.NotificationInterruptionStateProvider;
     82 import com.android.systemui.statusbar.notification.collection.NotificationEntry;
     83 import com.android.systemui.statusbar.notification.row.NotificationContentInflater.InflationFlag;
     84 import com.android.systemui.statusbar.phone.StatusBarWindowController;
     85 import com.android.systemui.statusbar.policy.ConfigurationController;
     86 import com.android.systemui.statusbar.policy.ZenModeController;
     87 
     88 import java.lang.annotation.Retention;
     89 import java.lang.annotation.Target;
     90 import java.util.List;
     91 
     92 import javax.inject.Inject;
     93 import javax.inject.Singleton;
     94 
     95 /**
     96  * Bubbles are a special type of content that can "float" on top of other apps or System UI.
     97  * Bubbles can be expanded to show more content.
     98  *
     99  * The controller manages addition, removal, and visible state of bubbles on screen.
    100  */
    101 @Singleton
    102 public class BubbleController implements ConfigurationController.ConfigurationListener {
    103 
    104     private static final String TAG = "BubbleController";
    105     private static final boolean DEBUG = false;
    106 
    107     @Retention(SOURCE)
    108     @IntDef({DISMISS_USER_GESTURE, DISMISS_AGED, DISMISS_TASK_FINISHED, DISMISS_BLOCKED,
    109             DISMISS_NOTIF_CANCEL, DISMISS_ACCESSIBILITY_ACTION, DISMISS_NO_LONGER_BUBBLE})
    110     @Target({FIELD, LOCAL_VARIABLE, PARAMETER})
    111     @interface DismissReason {}
    112 
    113     static final int DISMISS_USER_GESTURE = 1;
    114     static final int DISMISS_AGED = 2;
    115     static final int DISMISS_TASK_FINISHED = 3;
    116     static final int DISMISS_BLOCKED = 4;
    117     static final int DISMISS_NOTIF_CANCEL = 5;
    118     static final int DISMISS_ACCESSIBILITY_ACTION = 6;
    119     static final int DISMISS_NO_LONGER_BUBBLE = 7;
    120 
    121     public static final int MAX_BUBBLES = 5; // TODO: actually enforce this
    122 
    123     // Enables some subset of notifs to automatically become bubbles
    124     public static final boolean DEBUG_ENABLE_AUTO_BUBBLE = false;
    125 
    126     /** Flag to enable or disable the entire feature */
    127     private static final String ENABLE_BUBBLES = "experiment_enable_bubbles";
    128     /** Auto bubble flags set whether different notif types should be presented as a bubble */
    129     private static final String ENABLE_AUTO_BUBBLE_MESSAGES = "experiment_autobubble_messaging";
    130     private static final String ENABLE_AUTO_BUBBLE_ONGOING = "experiment_autobubble_ongoing";
    131     private static final String ENABLE_AUTO_BUBBLE_ALL = "experiment_autobubble_all";
    132 
    133     /** Use an activityView for an auto-bubbled notifs if it has an appropriate content intent */
    134     private static final String ENABLE_BUBBLE_CONTENT_INTENT = "experiment_bubble_content_intent";
    135 
    136     private static final String BUBBLE_STIFFNESS = "experiment_bubble_stiffness";
    137     private static final String BUBBLE_BOUNCINESS = "experiment_bubble_bounciness";
    138 
    139     private final Context mContext;
    140     private final NotificationEntryManager mNotificationEntryManager;
    141     private final BubbleTaskStackListener mTaskStackListener;
    142     private BubbleStateChangeListener mStateChangeListener;
    143     private BubbleExpandListener mExpandListener;
    144     @Nullable private BubbleStackView.SurfaceSynchronizer mSurfaceSynchronizer;
    145 
    146     private BubbleData mBubbleData;
    147     @Nullable private BubbleStackView mStackView;
    148 
    149     // Bubbles get added to the status bar view
    150     private final StatusBarWindowController mStatusBarWindowController;
    151     private final ZenModeController mZenModeController;
    152     private StatusBarStateListener mStatusBarStateListener;
    153 
    154     private final NotificationInterruptionStateProvider mNotificationInterruptionStateProvider;
    155     private IStatusBarService mBarService;
    156 
    157     // Used for determining view rect for touch interaction
    158     private Rect mTempRect = new Rect();
    159 
    160     /** Last known orientation, used to detect orientation changes in {@link #onConfigChanged}. */
    161     private int mOrientation = Configuration.ORIENTATION_UNDEFINED;
    162 
    163     /**
    164      * Listener to be notified when some states of the bubbles change.
    165      */
    166     public interface BubbleStateChangeListener {
    167         /**
    168          * Called when the stack has bubbles or no longer has bubbles.
    169          */
    170         void onHasBubblesChanged(boolean hasBubbles);
    171     }
    172 
    173     /**
    174      * Listener to find out about stack expansion / collapse events.
    175      */
    176     public interface BubbleExpandListener {
    177         /**
    178          * Called when the expansion state of the bubble stack changes.
    179          *
    180          * @param isExpanding whether it's expanding or collapsing
    181          * @param key the notification key associated with bubble being expanded
    182          */
    183         void onBubbleExpandChanged(boolean isExpanding, String key);
    184     }
    185 
    186     /**
    187      * Listens for the current state of the status bar and updates the visibility state
    188      * of bubbles as needed.
    189      */
    190     private class StatusBarStateListener implements StatusBarStateController.StateListener {
    191         private int mState;
    192         /**
    193          * Returns the current status bar state.
    194          */
    195         public int getCurrentState() {
    196             return mState;
    197         }
    198 
    199         @Override
    200         public void onStateChanged(int newState) {
    201             mState = newState;
    202             boolean shouldCollapse = (mState != SHADE);
    203             if (shouldCollapse) {
    204                 collapseStack();
    205             }
    206             updateStack();
    207         }
    208     }
    209 
    210     @Inject
    211     public BubbleController(Context context, StatusBarWindowController statusBarWindowController,
    212             BubbleData data, ConfigurationController configurationController,
    213             NotificationInterruptionStateProvider interruptionStateProvider,
    214             ZenModeController zenModeController) {
    215         this(context, statusBarWindowController, data, null /* synchronizer */,
    216                 configurationController, interruptionStateProvider, zenModeController);
    217     }
    218 
    219     public BubbleController(Context context, StatusBarWindowController statusBarWindowController,
    220             BubbleData data, @Nullable BubbleStackView.SurfaceSynchronizer synchronizer,
    221             ConfigurationController configurationController,
    222             NotificationInterruptionStateProvider interruptionStateProvider,
    223             ZenModeController zenModeController) {
    224         mContext = context;
    225         mNotificationInterruptionStateProvider = interruptionStateProvider;
    226         mZenModeController = zenModeController;
    227         mZenModeController.addCallback(new ZenModeController.Callback() {
    228             @Override
    229             public void onZenChanged(int zen) {
    230                 updateStackViewForZenConfig();
    231             }
    232 
    233             @Override
    234             public void onConfigChanged(ZenModeConfig config) {
    235                 updateStackViewForZenConfig();
    236             }
    237         });
    238 
    239         configurationController.addCallback(this /* configurationListener */);
    240 
    241         mBubbleData = data;
    242         mBubbleData.setListener(mBubbleDataListener);
    243 
    244         mNotificationEntryManager = Dependency.get(NotificationEntryManager.class);
    245         mNotificationEntryManager.addNotificationEntryListener(mEntryListener);
    246         mNotificationEntryManager.setNotificationRemoveInterceptor(mRemoveInterceptor);
    247 
    248         mStatusBarWindowController = statusBarWindowController;
    249         mStatusBarStateListener = new StatusBarStateListener();
    250         Dependency.get(StatusBarStateController.class).addCallback(mStatusBarStateListener);
    251 
    252         mTaskStackListener = new BubbleTaskStackListener();
    253         ActivityManagerWrapper.getInstance().registerTaskStackListener(mTaskStackListener);
    254 
    255         try {
    256             WindowManagerWrapper.getInstance().addPinnedStackListener(new BubblesImeListener());
    257         } catch (RemoteException e) {
    258             e.printStackTrace();
    259         }
    260         mSurfaceSynchronizer = synchronizer;
    261 
    262         mBarService = IStatusBarService.Stub.asInterface(
    263                 ServiceManager.getService(Context.STATUS_BAR_SERVICE));
    264     }
    265 
    266     /**
    267      * BubbleStackView is lazily created by this method the first time a Bubble is added. This
    268      * method initializes the stack view and adds it to the StatusBar just above the scrim.
    269      */
    270     private void ensureStackViewCreated() {
    271         if (mStackView == null) {
    272             mStackView = new BubbleStackView(mContext, mBubbleData, mSurfaceSynchronizer);
    273             ViewGroup sbv = mStatusBarWindowController.getStatusBarView();
    274             // TODO(b/130237686): When you expand the shade on top of expanded bubble, there is no
    275             //  scrim between bubble and the shade
    276             int bubblePosition = sbv.indexOfChild(sbv.findViewById(R.id.scrim_behind)) + 1;
    277             sbv.addView(mStackView, bubblePosition,
    278                     new FrameLayout.LayoutParams(MATCH_PARENT, MATCH_PARENT));
    279             if (mExpandListener != null) {
    280                 mStackView.setExpandListener(mExpandListener);
    281             }
    282 
    283             updateStackViewForZenConfig();
    284         }
    285     }
    286 
    287     @Override
    288     public void onUiModeChanged() {
    289         if (mStackView != null) {
    290             mStackView.onThemeChanged();
    291         }
    292     }
    293 
    294     @Override
    295     public void onOverlayChanged() {
    296         if (mStackView != null) {
    297             mStackView.onThemeChanged();
    298         }
    299     }
    300 
    301     @Override
    302     public void onConfigChanged(Configuration newConfig) {
    303         if (mStackView != null && newConfig != null && newConfig.orientation != mOrientation) {
    304             mStackView.onOrientationChanged();
    305             mOrientation = newConfig.orientation;
    306         }
    307     }
    308 
    309     /**
    310      * Set a listener to be notified when some states of the bubbles change.
    311      */
    312     public void setBubbleStateChangeListener(BubbleStateChangeListener listener) {
    313         mStateChangeListener = listener;
    314     }
    315 
    316     /**
    317      * Set a listener to be notified of bubble expand events.
    318      */
    319     public void setExpandListener(BubbleExpandListener listener) {
    320         mExpandListener = ((isExpanding, key) -> {
    321             if (listener != null) {
    322                 listener.onBubbleExpandChanged(isExpanding, key);
    323             }
    324             mStatusBarWindowController.setBubbleExpanded(isExpanding);
    325         });
    326         if (mStackView != null) {
    327             mStackView.setExpandListener(mExpandListener);
    328         }
    329     }
    330 
    331     /**
    332      * Whether or not there are bubbles present, regardless of them being visible on the
    333      * screen (e.g. if on AOD).
    334      */
    335     public boolean hasBubbles() {
    336         if (mStackView == null) {
    337             return false;
    338         }
    339         return mBubbleData.hasBubbles();
    340     }
    341 
    342     /**
    343      * Whether the stack of bubbles is expanded or not.
    344      */
    345     public boolean isStackExpanded() {
    346         return mBubbleData.isExpanded();
    347     }
    348 
    349     /**
    350      * Tell the stack of bubbles to expand.
    351      */
    352     public void expandStack() {
    353         mBubbleData.setExpanded(true);
    354     }
    355 
    356     /**
    357      * Tell the stack of bubbles to collapse.
    358      */
    359     public void collapseStack() {
    360         mBubbleData.setExpanded(false /* expanded */);
    361     }
    362 
    363     void selectBubble(Bubble bubble) {
    364         mBubbleData.setSelectedBubble(bubble);
    365     }
    366 
    367     @VisibleForTesting
    368     void selectBubble(String key) {
    369         Bubble bubble = mBubbleData.getBubbleWithKey(key);
    370         selectBubble(bubble);
    371     }
    372 
    373     /**
    374      * Request the stack expand if needed, then select the specified Bubble as current.
    375      *
    376      * @param notificationKey the notification key for the bubble to be selected
    377      */
    378     public void expandStackAndSelectBubble(String notificationKey) {
    379         Bubble bubble = mBubbleData.getBubbleWithKey(notificationKey);
    380         if (bubble != null) {
    381             mBubbleData.setSelectedBubble(bubble);
    382             mBubbleData.setExpanded(true);
    383         }
    384     }
    385 
    386     /**
    387      * Tell the stack of bubbles to be dismissed, this will remove all of the bubbles in the stack.
    388      */
    389     void dismissStack(@DismissReason int reason) {
    390         mBubbleData.dismissAll(reason);
    391     }
    392 
    393     /**
    394      * Directs a back gesture at the bubble stack. When opened, the current expanded bubble
    395      * is forwarded a back key down/up pair.
    396      */
    397     public void performBackPressIfNeeded() {
    398         if (mStackView != null) {
    399             mStackView.performBackPressIfNeeded();
    400         }
    401     }
    402 
    403     /**
    404      * Adds or updates a bubble associated with the provided notification entry.
    405      *
    406      * @param notif the notification associated with this bubble.
    407      */
    408     void updateBubble(NotificationEntry notif) {
    409         // If this is an interruptive notif, mark that it's interrupted
    410         if (notif.importance >= NotificationManager.IMPORTANCE_HIGH) {
    411             notif.setInterruption();
    412         }
    413         mBubbleData.notificationEntryUpdated(notif);
    414     }
    415 
    416     /**
    417      * Removes the bubble associated with the {@param uri}.
    418      * <p>
    419      * Must be called from the main thread.
    420      */
    421     @MainThread
    422     void removeBubble(String key, int reason) {
    423         // TEMP: refactor to change this to pass entry
    424         Bubble bubble = mBubbleData.getBubbleWithKey(key);
    425         if (bubble != null) {
    426             mBubbleData.notificationEntryRemoved(bubble.entry, reason);
    427         }
    428     }
    429 
    430     @SuppressWarnings("FieldCanBeLocal")
    431     private final NotificationRemoveInterceptor mRemoveInterceptor =
    432             new NotificationRemoveInterceptor() {
    433             @Override
    434             public boolean onNotificationRemoveRequested(String key, int reason) {
    435                 if (!mBubbleData.hasBubbleWithKey(key)) {
    436                     return false;
    437                 }
    438                 NotificationEntry entry = mBubbleData.getBubbleWithKey(key).entry;
    439 
    440                 final boolean isClearAll = reason == REASON_CANCEL_ALL;
    441                 final boolean isUserDimiss = reason == REASON_CANCEL;
    442                 final boolean isAppCancel = reason == REASON_APP_CANCEL
    443                         || reason == REASON_APP_CANCEL_ALL;
    444 
    445                 // Need to check for !appCancel here because the notification may have
    446                 // previously been dismissed & entry.isRowDismissed would still be true
    447                 boolean userRemovedNotif = (entry.isRowDismissed() && !isAppCancel)
    448                         || isClearAll || isUserDimiss;
    449 
    450                 // The bubble notification sticks around in the data as long as the bubble is
    451                 // not dismissed and the app hasn't cancelled the notification.
    452                 boolean bubbleExtended = entry.isBubble() && !entry.isBubbleDismissed()
    453                         && userRemovedNotif;
    454                 if (bubbleExtended) {
    455                     entry.setShowInShadeWhenBubble(false);
    456                     if (mStackView != null) {
    457                         mStackView.updateDotVisibility(entry.key);
    458                     }
    459                     mNotificationEntryManager.updateNotifications();
    460                     return true;
    461                 } else if (!userRemovedNotif && !entry.isBubbleDismissed()) {
    462                     // This wasn't a user removal so we should remove the bubble as well
    463                     mBubbleData.notificationEntryRemoved(entry, DISMISS_NOTIF_CANCEL);
    464                     return false;
    465                 }
    466                 return false;
    467             }
    468         };
    469 
    470     @SuppressWarnings("FieldCanBeLocal")
    471     private final NotificationEntryListener mEntryListener = new NotificationEntryListener() {
    472         @Override
    473         public void onPendingEntryAdded(NotificationEntry entry) {
    474             if (!areBubblesEnabled(mContext)) {
    475                 return;
    476             }
    477             if (mNotificationInterruptionStateProvider.shouldBubbleUp(entry)
    478                     && canLaunchInActivityView(mContext, entry)) {
    479                 updateShowInShadeForSuppressNotification(entry);
    480             }
    481         }
    482 
    483         @Override
    484         public void onEntryInflated(NotificationEntry entry, @InflationFlag int inflatedFlags) {
    485             if (!areBubblesEnabled(mContext)) {
    486                 return;
    487             }
    488             if (mNotificationInterruptionStateProvider.shouldBubbleUp(entry)
    489                     && canLaunchInActivityView(mContext, entry)) {
    490                 updateBubble(entry);
    491             }
    492         }
    493 
    494         @Override
    495         public void onPreEntryUpdated(NotificationEntry entry) {
    496             if (!areBubblesEnabled(mContext)) {
    497                 return;
    498             }
    499             boolean shouldBubble = mNotificationInterruptionStateProvider.shouldBubbleUp(entry)
    500                     && canLaunchInActivityView(mContext, entry);
    501             if (!shouldBubble && mBubbleData.hasBubbleWithKey(entry.key)) {
    502                 // It was previously a bubble but no longer a bubble -- lets remove it
    503                 removeBubble(entry.key, DISMISS_NO_LONGER_BUBBLE);
    504             } else if (shouldBubble) {
    505                 updateShowInShadeForSuppressNotification(entry);
    506                 entry.setBubbleDismissed(false); // updates come back as bubbles even if dismissed
    507                 updateBubble(entry);
    508             }
    509         }
    510 
    511         @Override
    512         public void onNotificationRankingUpdated(RankingMap rankingMap) {
    513             // Forward to BubbleData to block any bubbles which should no longer be shown
    514             mBubbleData.notificationRankingUpdated(rankingMap);
    515         }
    516     };
    517 
    518     @SuppressWarnings("FieldCanBeLocal")
    519     private final BubbleData.Listener mBubbleDataListener = new BubbleData.Listener() {
    520 
    521         @Override
    522         public void applyUpdate(BubbleData.Update update) {
    523             if (mStackView == null && update.addedBubble != null) {
    524                 // Lazy init stack view when the first bubble is added.
    525                 ensureStackViewCreated();
    526             }
    527 
    528             // If not yet initialized, ignore all other changes.
    529             if (mStackView == null) {
    530                 return;
    531             }
    532 
    533             if (update.addedBubble != null) {
    534                 mStackView.addBubble(update.addedBubble);
    535             }
    536 
    537             // Collapsing? Do this first before remaining steps.
    538             if (update.expandedChanged && !update.expanded) {
    539                 mStackView.setExpanded(false);
    540             }
    541 
    542             // Do removals, if any.
    543             for (Pair<Bubble, Integer> removed : update.removedBubbles) {
    544                 final Bubble bubble = removed.first;
    545                 @DismissReason final int reason = removed.second;
    546                 mStackView.removeBubble(bubble);
    547 
    548                 if (!mBubbleData.hasBubbleWithKey(bubble.getKey())
    549                         && !bubble.entry.showInShadeWhenBubble()) {
    550                     // The bubble is gone & the notification is gone, time to actually remove it
    551                     mNotificationEntryManager.performRemoveNotification(bubble.entry.notification,
    552                             UNDEFINED_DISMISS_REASON);
    553                 } else {
    554                     // Update the flag for SysUI
    555                     bubble.entry.notification.getNotification().flags &= ~FLAG_BUBBLE;
    556 
    557                     // Make sure NoMan knows it's not a bubble anymore so anyone querying it will
    558                     // get right result back
    559                     try {
    560                         mBarService.onNotificationBubbleChanged(bubble.getKey(),
    561                                 false /* isBubble */);
    562                     } catch (RemoteException e) {
    563                         // Bad things have happened
    564                     }
    565                 }
    566             }
    567 
    568             if (update.updatedBubble != null) {
    569                 mStackView.updateBubble(update.updatedBubble);
    570             }
    571 
    572             if (update.orderChanged) {
    573                 mStackView.updateBubbleOrder(update.bubbles);
    574             }
    575 
    576             if (update.selectionChanged) {
    577                 mStackView.setSelectedBubble(update.selectedBubble);
    578             }
    579 
    580             // Expanding? Apply this last.
    581             if (update.expandedChanged && update.expanded) {
    582                 mStackView.setExpanded(true);
    583             }
    584 
    585             mNotificationEntryManager.updateNotifications();
    586             updateStack();
    587 
    588             if (DEBUG) {
    589                 Log.d(TAG, "[BubbleData]");
    590                 Log.d(TAG, formatBubblesString(mBubbleData.getBubbles(),
    591                         mBubbleData.getSelectedBubble()));
    592 
    593                 if (mStackView != null) {
    594                     Log.d(TAG, "[BubbleStackView]");
    595                     Log.d(TAG, formatBubblesString(mStackView.getBubblesOnScreen(),
    596                             mStackView.getExpandedBubble()));
    597                 }
    598             }
    599         }
    600     };
    601 
    602     /**
    603      * Updates the stack view's suppression flags from the latest config from the zen (do not
    604      * disturb) controller.
    605      */
    606     private void updateStackViewForZenConfig() {
    607         final ZenModeConfig zenModeConfig = mZenModeController.getConfig();
    608 
    609         if (zenModeConfig == null || mStackView == null) {
    610             return;
    611         }
    612 
    613         final int suppressedEffects = zenModeConfig.suppressedVisualEffects;
    614         final boolean hideNotificationDotsSelected =
    615                 (suppressedEffects & SUPPRESSED_EFFECT_BADGE) != 0;
    616         final boolean dontPopNotifsOnScreenSelected =
    617                 (suppressedEffects & SUPPRESSED_EFFECT_PEEK) != 0;
    618         final boolean hideFromPullDownShadeSelected =
    619                 (suppressedEffects & SUPPRESSED_EFFECT_NOTIFICATION_LIST) != 0;
    620 
    621         final boolean dndEnabled = mZenModeController.getZen() != Settings.Global.ZEN_MODE_OFF;
    622 
    623         mStackView.setSuppressNewDot(
    624                 dndEnabled && hideNotificationDotsSelected);
    625         mStackView.setSuppressFlyout(
    626                 dndEnabled && (dontPopNotifsOnScreenSelected
    627                         || hideFromPullDownShadeSelected));
    628     }
    629 
    630     /**
    631      * Lets any listeners know if bubble state has changed.
    632      * Updates the visibility of the bubbles based on current state.
    633      * Does not un-bubble, just hides or un-hides. Notifies any
    634      * {@link BubbleStateChangeListener}s of visibility changes.
    635      * Updates stack description for TalkBack focus.
    636      */
    637     public void updateStack() {
    638         if (mStackView == null) {
    639             return;
    640         }
    641         if (mStatusBarStateListener.getCurrentState() == SHADE && hasBubbles()) {
    642             // Bubbles only appear in unlocked shade
    643             mStackView.setVisibility(hasBubbles() ? VISIBLE : INVISIBLE);
    644         } else if (mStackView != null) {
    645             mStackView.setVisibility(INVISIBLE);
    646         }
    647 
    648         // Let listeners know if bubble state changed.
    649         boolean hadBubbles = mStatusBarWindowController.getBubblesShowing();
    650         boolean hasBubblesShowing = hasBubbles() && mStackView.getVisibility() == VISIBLE;
    651         mStatusBarWindowController.setBubblesShowing(hasBubblesShowing);
    652         if (mStateChangeListener != null && hadBubbles != hasBubblesShowing) {
    653             mStateChangeListener.onHasBubblesChanged(hasBubblesShowing);
    654         }
    655 
    656         mStackView.updateContentDescription();
    657     }
    658 
    659     /**
    660      * Rect indicating the touchable region for the bubble stack / expanded stack.
    661      */
    662     public Rect getTouchableRegion() {
    663         if (mStackView == null || mStackView.getVisibility() != VISIBLE) {
    664             return null;
    665         }
    666         mStackView.getBoundsOnScreen(mTempRect);
    667         return mTempRect;
    668     }
    669 
    670     /**
    671      * The display id of the expanded view, if the stack is expanded and not occluded by the
    672      * status bar, otherwise returns {@link Display#INVALID_DISPLAY}.
    673      */
    674     public int getExpandedDisplayId(Context context) {
    675         if (mStackView == null) {
    676             return INVALID_DISPLAY;
    677         }
    678         boolean defaultDisplay = context.getDisplay() != null
    679                 && context.getDisplay().getDisplayId() == DEFAULT_DISPLAY;
    680         Bubble b = mStackView.getExpandedBubble();
    681         if (defaultDisplay && b != null && isStackExpanded()
    682                 && !mStatusBarWindowController.getPanelExpanded()) {
    683             return b.expandedView.getVirtualDisplayId();
    684         }
    685         return INVALID_DISPLAY;
    686     }
    687 
    688     @VisibleForTesting
    689     BubbleStackView getStackView() {
    690         return mStackView;
    691     }
    692 
    693     /**
    694      * Whether the notification should automatically bubble or not. Gated by secure settings flags.
    695      */
    696     @VisibleForTesting
    697     protected boolean shouldAutoBubbleForFlags(Context context, NotificationEntry entry) {
    698         if (entry.isBubbleDismissed()) {
    699             return false;
    700         }
    701         StatusBarNotification n = entry.notification;
    702 
    703         boolean autoBubbleMessages = shouldAutoBubbleMessages(context) || DEBUG_ENABLE_AUTO_BUBBLE;
    704         boolean autoBubbleOngoing = shouldAutoBubbleOngoing(context) || DEBUG_ENABLE_AUTO_BUBBLE;
    705         boolean autoBubbleAll = shouldAutoBubbleAll(context) || DEBUG_ENABLE_AUTO_BUBBLE;
    706 
    707         boolean hasRemoteInput = false;
    708         if (n.getNotification().actions != null) {
    709             for (Notification.Action action : n.getNotification().actions) {
    710                 if (action.getRemoteInputs() != null) {
    711                     hasRemoteInput = true;
    712                     break;
    713                 }
    714             }
    715         }
    716         boolean isCall = Notification.CATEGORY_CALL.equals(n.getNotification().category)
    717                 && n.isOngoing();
    718         boolean isMusic = n.getNotification().hasMediaSession();
    719         boolean isImportantOngoing = isMusic || isCall;
    720 
    721         Class<? extends Notification.Style> style = n.getNotification().getNotificationStyle();
    722         boolean isMessageType = Notification.CATEGORY_MESSAGE.equals(n.getNotification().category);
    723         boolean isMessageStyle = Notification.MessagingStyle.class.equals(style);
    724         return (((isMessageType && hasRemoteInput) || isMessageStyle) && autoBubbleMessages)
    725                 || (isImportantOngoing && autoBubbleOngoing)
    726                 || autoBubbleAll;
    727     }
    728 
    729     private void updateShowInShadeForSuppressNotification(NotificationEntry entry) {
    730         boolean suppressNotification = entry.getBubbleMetadata() != null
    731                 && entry.getBubbleMetadata().isNotificationSuppressed()
    732                 && isForegroundApp(mContext, entry.notification.getPackageName());
    733         entry.setShowInShadeWhenBubble(!suppressNotification);
    734     }
    735 
    736     static String formatBubblesString(List<Bubble> bubbles, Bubble selected) {
    737         StringBuilder sb = new StringBuilder();
    738         for (Bubble bubble : bubbles) {
    739             if (bubble == null) {
    740                 sb.append("   <null> !!!!!\n");
    741             } else {
    742                 boolean isSelected = (bubble == selected);
    743                 sb.append(String.format("%s Bubble{act=%12d, ongoing=%d, key=%s}\n",
    744                         ((isSelected) ? "->" : "  "),
    745                         bubble.getLastActivity(),
    746                         (bubble.isOngoing() ? 1 : 0),
    747                         bubble.getKey()));
    748             }
    749         }
    750         return sb.toString();
    751     }
    752 
    753     /**
    754      * Return true if the applications with the package name is running in foreground.
    755      *
    756      * @param context application context.
    757      * @param pkgName application package name.
    758      */
    759     public static boolean isForegroundApp(Context context, String pkgName) {
    760         ActivityManager am = context.getSystemService(ActivityManager.class);
    761         List<RunningTaskInfo> tasks = am.getRunningTasks(1 /* maxNum */);
    762         return !tasks.isEmpty() && pkgName.equals(tasks.get(0).topActivity.getPackageName());
    763     }
    764 
    765     /**
    766      * This task stack listener is responsible for responding to tasks moved to the front
    767      * which are on the default (main) display. When this happens, expanded bubbles must be
    768      * collapsed so the user may interact with the app which was just moved to the front.
    769      * <p>
    770      * This listener is registered with SystemUI's ActivityManagerWrapper which dispatches
    771      * these calls via a main thread Handler.
    772      */
    773     @MainThread
    774     private class BubbleTaskStackListener extends TaskStackChangeListener {
    775 
    776         @Override
    777         public void onTaskMovedToFront(RunningTaskInfo taskInfo) {
    778             if (mStackView != null && taskInfo.displayId == Display.DEFAULT_DISPLAY) {
    779                 mBubbleData.setExpanded(false);
    780             }
    781         }
    782 
    783         @Override
    784         public void onActivityLaunchOnSecondaryDisplayRerouted() {
    785             if (mStackView != null) {
    786                 mBubbleData.setExpanded(false);
    787             }
    788         }
    789 
    790         @Override
    791         public void onBackPressedOnTaskRoot(RunningTaskInfo taskInfo) {
    792             if (mStackView != null && taskInfo.displayId == getExpandedDisplayId(mContext)) {
    793                 mBubbleData.setExpanded(false);
    794             }
    795         }
    796     }
    797 
    798     private static boolean shouldAutoBubbleMessages(Context context) {
    799         return Settings.Secure.getInt(context.getContentResolver(),
    800                 ENABLE_AUTO_BUBBLE_MESSAGES, 0) != 0;
    801     }
    802 
    803     private static boolean shouldAutoBubbleOngoing(Context context) {
    804         return Settings.Secure.getInt(context.getContentResolver(),
    805                 ENABLE_AUTO_BUBBLE_ONGOING, 0) != 0;
    806     }
    807 
    808     private static boolean shouldAutoBubbleAll(Context context) {
    809         return Settings.Secure.getInt(context.getContentResolver(),
    810                 ENABLE_AUTO_BUBBLE_ALL, 0) != 0;
    811     }
    812 
    813     static boolean shouldUseContentIntent(Context context) {
    814         return Settings.Secure.getInt(context.getContentResolver(),
    815                 ENABLE_BUBBLE_CONTENT_INTENT, 0) != 0;
    816     }
    817 
    818     private static boolean areBubblesEnabled(Context context) {
    819         return Settings.Secure.getInt(context.getContentResolver(),
    820                 ENABLE_BUBBLES, 1) != 0;
    821     }
    822 
    823     /** Default stiffness to use for bubble physics animations. */
    824     public static int getBubbleStiffness(Context context, int defaultStiffness) {
    825         return Settings.Secure.getInt(
    826                 context.getContentResolver(), BUBBLE_STIFFNESS, defaultStiffness);
    827     }
    828 
    829     /** Default bounciness/damping ratio to use for bubble physics animations. */
    830     public static float getBubbleBounciness(Context context, float defaultBounciness) {
    831         return Settings.Secure.getInt(
    832                 context.getContentResolver(),
    833                 BUBBLE_BOUNCINESS,
    834                 (int) (defaultBounciness * 100)) / 100f;
    835     }
    836 
    837     /**
    838      * Whether an intent is properly configured to display in an {@link android.app.ActivityView}.
    839      *
    840      * Keep checks in sync with NotificationManagerService#canLaunchInActivityView. Typically
    841      * that should filter out any invalid bubbles, but should protect SysUI side just in case.
    842      *
    843      * @param context the context to use.
    844      * @param entry the entry to bubble.
    845      */
    846     static boolean canLaunchInActivityView(Context context, NotificationEntry entry) {
    847         PendingIntent intent = entry.getBubbleMetadata() != null
    848                 ? entry.getBubbleMetadata().getIntent()
    849                 : null;
    850         if (intent == null) {
    851             Log.w(TAG, "Unable to create bubble -- no intent");
    852             return false;
    853         }
    854         ActivityInfo info =
    855                 intent.getIntent().resolveActivityInfo(context.getPackageManager(), 0);
    856         if (info == null) {
    857             Log.w(TAG, "Unable to send as bubble -- couldn't find activity info for intent: "
    858                     + intent);
    859             return false;
    860         }
    861         if (!ActivityInfo.isResizeableMode(info.resizeMode)) {
    862             Log.w(TAG, "Unable to send as bubble -- activity is not resizable for intent: "
    863                     + intent);
    864             return false;
    865         }
    866         if (info.documentLaunchMode != DOCUMENT_LAUNCH_ALWAYS) {
    867             Log.w(TAG, "Unable to send as bubble -- activity is not documentLaunchMode=always "
    868                     + "for intent: " + intent);
    869             return false;
    870         }
    871         if ((info.flags & ActivityInfo.FLAG_ALLOW_EMBEDDED) == 0) {
    872             Log.w(TAG, "Unable to send as bubble -- activity is not embeddable for intent: "
    873                     + intent);
    874             return false;
    875         }
    876         return true;
    877     }
    878 
    879     /** PinnedStackListener that dispatches IME visibility updates to the stack. */
    880     private class BubblesImeListener extends IPinnedStackListener.Stub {
    881 
    882         @Override
    883         public void onListenerRegistered(IPinnedStackController controller) throws RemoteException {
    884         }
    885 
    886         @Override
    887         public void onMovementBoundsChanged(Rect insetBounds, Rect normalBounds,
    888                 Rect animatingBounds, boolean fromImeAdjustment, boolean fromShelfAdjustment,
    889                 int displayRotation) throws RemoteException {}
    890 
    891         @Override
    892         public void onImeVisibilityChanged(boolean imeVisible, int imeHeight) {
    893             if (mStackView != null && mStackView.getBubbleCount() > 0) {
    894                 mStackView.post(() -> mStackView.onImeVisibilityChanged(imeVisible, imeHeight));
    895             }
    896         }
    897 
    898         @Override
    899         public void onShelfVisibilityChanged(boolean shelfVisible, int shelfHeight)
    900                 throws RemoteException {}
    901 
    902         @Override
    903         public void onMinimizedStateChanged(boolean isMinimized) throws RemoteException {}
    904 
    905         @Override
    906         public void onActionsChanged(ParceledListSlice actions) throws RemoteException {}
    907     }
    908 }
    909