Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 5549dd2d authored by Mady Mellor's avatar Mady Mellor
Browse files

Auto bubble some notifications (behind debug flag)

When the flag is flipped notifications that fulfill any
of these will bubble:
- use MessageStyle
- have remote input
- have an app overlay intent
- have priority >= HIGH

Test: atest SystemUITests (no new tests)
Bug: 111236845
Change-Id: Iff86d78a24ab798ddb681a1ad59e1131136814d4
parent c3d6f7d3
Loading
Loading
Loading
Loading
+93 −0
Original line number Diff line number Diff line
@@ -22,8 +22,11 @@ import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT;

import static com.android.systemui.bubbles.BubbleMovementHelper.EDGE_OVERLAP;

import android.app.Notification;
import android.app.NotificationManager;
import android.content.Context;
import android.graphics.Point;
import android.service.notification.StatusBarNotification;
import android.view.ViewGroup;
import android.view.WindowManager;
import android.widget.FrameLayout;
@@ -33,6 +36,7 @@ import com.android.systemui.R;
import com.android.systemui.statusbar.notification.NotificationData;
import com.android.systemui.statusbar.phone.StatusBarWindowController;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;

@@ -47,7 +51,13 @@ public class BubbleController {

    private static final String TAG = "BubbleController";

    // Enables some subset of notifs to automatically become bubbles
    public static final boolean DEBUG_ENABLE_AUTO_BUBBLE = false;
    // When a bubble is dismissed, recreate it as a notification
    public static final boolean DEBUG_DEMOTE_TO_NOTIF = false;

    private Context mContext;
    private BubbleDismissListener mDismissListener;

    private Map<String, BubbleView> mBubbles = new HashMap<>();
    private BubbleStackView mStackView;
@@ -56,6 +66,21 @@ public class BubbleController {
    // Bubbles get added to the status bar view
    private StatusBarWindowController mStatusBarWindowController;

    /**
     * Listener to find out about bubble / bubble stack dismissal events.
     */
    public interface BubbleDismissListener {
        /**
         * Called when the entire stack of bubbles is dismissed by the user.
         */
        void onStackDismissed();

        /**
         * Called when a specific bubble is dismissed by the user.
         */
        void onBubbleDismissed(String key);
    }

    public BubbleController(Context context) {
        mContext = context;
        WindowManager wm = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
@@ -64,6 +89,13 @@ public class BubbleController {
        mStatusBarWindowController = Dependency.get(StatusBarWindowController.class);
    }

    /**
     * Set a listener to be notified of bubble dismissal events.
     */
    public void setDismissListener(BubbleDismissListener listener) {
        mDismissListener = listener;
    }

    /**
     * Whether or not there are bubbles present, regardless of them being visible on the
     * screen (e.g. if on AOD).
@@ -99,6 +131,9 @@ public class BubbleController {
        for (String key: mBubbles.keySet()) {
            removeBubble(key);
        }
        if (mDismissListener != null) {
            mDismissListener.onStackDismissed();
        }
    }

    /**
@@ -146,6 +181,38 @@ public class BubbleController {
            mStackView.removeBubble(bv);
            bv.getEntry().setBubbleDismissed(true);
        }
        if (mDismissListener != null) {
            mDismissListener.onBubbleDismissed(key);
        }
    }

    /**
     * Sets the visibility of the bubbles, doesn't un-bubble them, just changes visibility.
     */
    public void updateVisibility(boolean visible) {
        if (mStackView == null) {
            return;
        }
        ArrayList<BubbleView> viewsToRemove = new ArrayList<>();
        for (BubbleView bv : mBubbles.values()) {
            NotificationData.Entry entry = bv.getEntry();
            if (entry != null) {
                if (entry.row.isRemoved() || entry.isBubbleDismissed() || entry.row.isDismissed()) {
                    viewsToRemove.add(bv);
                }
            }
        }
        for (BubbleView view : viewsToRemove) {
            mBubbles.remove(view.getKey());
            mStackView.removeBubble(view);
            if (mBubbles.size() == 0) {
                ((ViewGroup) mStatusBarWindowController.getStatusBarView()).removeView(mStackView);
                mStackView = null;
            }
        }
        if (mStackView != null) {
            mStackView.setVisibility(visible ? VISIBLE : GONE);
        }
    }

    // TODO: factor in PIP location / maybe last place user had it
@@ -166,4 +233,30 @@ public class BubbleController {
        return new Point(EDGE_OVERLAP, size);
    }

    /**
     * Whether the notification should bubble or not.
     */
    public static boolean shouldAutoBubble(NotificationData.Entry entry, int priority,
            boolean canAppOverlay) {
        if (!DEBUG_ENABLE_AUTO_BUBBLE || entry.isBubbleDismissed()) {
            return false;
        }
        StatusBarNotification n = entry.notification;
        boolean hasRemoteInput = false;
        if (n.getNotification().actions != null) {
            for (Notification.Action action : n.getNotification().actions) {
                if (action.getRemoteInputs() != null) {
                    hasRemoteInput = true;
                    break;
                }
            }
        }
        Class<? extends Notification.Style> style = n.getNotification().getNotificationStyle();
        boolean shouldBubble = priority >= NotificationManager.IMPORTANCE_HIGH
                || Notification.MessagingStyle.class.equals(style)
                || Notification.CATEGORY_MESSAGE.equals(n.getNotification().category)
                || hasRemoteInput
                || canAppOverlay;
        return shouldBubble && !entry.isBubbleDismissed();
    }
}
+57 −0
Original line number Diff line number Diff line
@@ -16,6 +16,8 @@

package com.android.systemui.statusbar;

import static com.android.systemui.statusbar.StatusBarState.SHADE;

import android.content.Context;
import android.content.res.Resources;
import android.os.Trace;
@@ -25,6 +27,7 @@ import android.view.ViewGroup;

import com.android.systemui.Dependency;
import com.android.systemui.R;
import com.android.systemui.bubbles.BubbleController;
import com.android.systemui.statusbar.notification.NotificationData;
import com.android.systemui.statusbar.notification.NotificationEntryManager;
import com.android.systemui.statusbar.notification.VisualStabilityManager;
@@ -62,6 +65,7 @@ public class NotificationViewHierarchyManager {
            Dependency.get(StatusBarStateController.class);
    private final NotificationEntryManager mEntryManager =
            Dependency.get(NotificationEntryManager.class);
    private final BubbleController mBubbleController = Dependency.get(BubbleController.class);

    // Lazy
    private ShadeController mShadeController;
@@ -75,6 +79,41 @@ public class NotificationViewHierarchyManager {

    private NotificationPresenter mPresenter;
    private NotificationListContainer mListContainer;
    private StatusBarStateListener mStatusBarStateListener;

    /**
     * Listens for the current state of the status bar and updates the visibility state
     * of bubbles as needed.
     */
    public class StatusBarStateListener implements StatusBarStateController.StateListener {
        private int mState;
        private BubbleController mController;

        public StatusBarStateListener(BubbleController controller) {
            mController = controller;
        }

        /**
         * Returns the current status bar state.
         */
        public int getCurrentState() {
            return mState;
        }

        @Override
        public void onStateChanged(int newState) {
            mState = newState;
            // Order here matters because we need to remove the expandable notification row
            // from it's current parent (NSSL or bubble) before it can be added to the new parent
            if (mState == SHADE) {
                updateNotificationViews();
                mController.updateVisibility(true);
            } else {
                mController.updateVisibility(false);
                updateNotificationViews();
            }
        }
    }

    private ShadeController getShadeController() {
        if (mShadeController == null) {
@@ -87,6 +126,9 @@ public class NotificationViewHierarchyManager {
        Resources res = context.getResources();
        mAlwaysExpandNonGroupedNotification =
                res.getBoolean(R.bool.config_alwaysExpandNonGroupedNotifications);
        mStatusBarStateListener = new StatusBarStateListener(mBubbleController);
        mEntryManager.setStatusBarStateListener(mStatusBarStateListener);
        Dependency.get(StatusBarStateController.class).addListener(mStatusBarStateListener);
    }

    public void setUpWithPresenter(NotificationPresenter presenter,
@@ -102,6 +144,7 @@ public class NotificationViewHierarchyManager {
        ArrayList<NotificationData.Entry> activeNotifications = mEntryManager.getNotificationData()
                .getActiveNotifications();
        ArrayList<ExpandableNotificationRow> toShow = new ArrayList<>(activeNotifications.size());
        ArrayList<NotificationData.Entry> toBubble = new ArrayList<>();
        final int N = activeNotifications.size();
        for (int i = 0; i < N; i++) {
            NotificationData.Entry ent = activeNotifications.get(i);
@@ -110,6 +153,14 @@ public class NotificationViewHierarchyManager {
                // temporarily become children if they were isolated before.
                continue;
            }
            ent.row.setStatusBarState(mStatusBarStateListener.getCurrentState());
            boolean showAsBubble = ent.isBubble() && !ent.isBubbleDismissed()
                    && mStatusBarStateListener.getCurrentState() == SHADE;
            if (showAsBubble) {
                toBubble.add(ent);
                continue;
            }

            int userId = ent.notification.getUserId();

            // Display public version of the notification if we need to redact.
@@ -210,6 +261,12 @@ public class NotificationViewHierarchyManager {

        }

        for (int i = 0; i < toBubble.size(); i++) {
            // TODO: might make sense to leave them in the shade and just reposition them
            NotificationData.Entry ent = toBubble.get(i);
            mBubbleController.addBubble(ent);
        }

        mVisualStabilityManager.onReorderingFinished();
        // clear the map again for the next usage
        mTmpChildOrderMap.clear();
+60 −1
Original line number Diff line number Diff line
@@ -15,13 +15,16 @@
 */
package com.android.systemui.statusbar.notification;

import static com.android.systemui.bubbles.BubbleController.DEBUG_DEMOTE_TO_NOTIF;
import static com.android.systemui.statusbar.NotificationRemoteInputManager.ENABLE_REMOTE_INPUT;
import static com.android.systemui.statusbar.NotificationRemoteInputManager.FORCE_REMOTE_INPUT_HISTORY;
import static com.android.systemui.statusbar.StatusBarState.SHADE;
import static com.android.systemui.statusbar.notification.row.NotificationInflater.FLAG_CONTENT_VIEW_AMBIENT;
import static com.android.systemui.statusbar.notification.row.NotificationInflater.FLAG_CONTENT_VIEW_HEADS_UP;

import android.annotation.Nullable;
import android.app.Notification;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.Context;
@@ -62,6 +65,7 @@ import com.android.systemui.ForegroundServiceController;
import com.android.systemui.InitController;
import com.android.systemui.R;
import com.android.systemui.UiOffloadThread;
import com.android.systemui.bubbles.BubbleController;
import com.android.systemui.statusbar.AlertingNotificationManager;
import com.android.systemui.statusbar.AmbientPulseManager;
import com.android.systemui.statusbar.NotificationLifetimeExtender;
@@ -72,6 +76,7 @@ import com.android.systemui.statusbar.NotificationPresenter;
import com.android.systemui.statusbar.NotificationRemoteInputManager;
import com.android.systemui.statusbar.NotificationUiAdjustment;
import com.android.systemui.statusbar.NotificationUpdateHandler;
import com.android.systemui.statusbar.NotificationViewHierarchyManager;
import com.android.systemui.statusbar.notification.NotificationData.KeyguardEnvironment;
import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow;
import com.android.systemui.statusbar.notification.row.NotificationGutsManager;
@@ -99,7 +104,7 @@ import java.util.List;
 */
public class NotificationEntryManager implements Dumpable, NotificationInflater.InflationCallback,
        ExpandableNotificationRow.ExpansionLogger, NotificationUpdateHandler,
        VisualStabilityManager.Callback {
        VisualStabilityManager.Callback, BubbleController.BubbleDismissListener {
    private static final String TAG = "NotificationEntryMgr";
    protected static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
    protected static final boolean ENABLE_HEADS_UP = true;
@@ -124,6 +129,7 @@ public class NotificationEntryManager implements Dumpable, NotificationInflater.
            Dependency.get(ForegroundServiceController.class);
    private final AmbientPulseManager mAmbientPulseManager =
            Dependency.get(AmbientPulseManager.class);
    private final BubbleController mBubbleController = Dependency.get(BubbleController.class);

    // Lazily retrieved dependencies
    private NotificationRemoteInputManager mRemoteInputManager;
@@ -146,6 +152,7 @@ public class NotificationEntryManager implements Dumpable, NotificationInflater.
    protected final ArrayList<NotificationLifetimeExtender> mNotificationLifetimeExtenders
            = new ArrayList<>();
    private ExpandableNotificationRow.OnAppOpsClickListener mOnAppOpsClickListener;
    private NotificationViewHierarchyManager.StatusBarStateListener mStatusBarStateListener;

    private final class NotificationClicker implements View.OnClickListener {

@@ -175,6 +182,11 @@ public class NotificationEntryManager implements Dumpable, NotificationInflater.
            row.setJustClicked(true);
            DejankUtils.postAfterTraversal(() -> row.setJustClicked(false));

            // If it was a bubble we should close it
            if (row.getEntry().isBubble()) {
                mBubbleController.collapseStack();
            }

            mCallback.onNotificationClicked(sbn, row);
        }

@@ -229,6 +241,7 @@ public class NotificationEntryManager implements Dumpable, NotificationInflater.
        mDreamManager = IDreamManager.Stub.asInterface(
                ServiceManager.checkService(DreamService.DREAM_SERVICE));
        mMessagingUtil = new NotificationMessagingUtil(context);
        mBubbleController.setDismissListener(this /* bubbleEventListener */);
        mNotificationData = new NotificationData();
        Dependency.get(InitController.class).addPostInitTask(this::onPostInit);
    }
@@ -451,6 +464,23 @@ public class NotificationEntryManager implements Dumpable, NotificationInflater.
        mCallback.onPerformRemoveNotification(n);
    }

    @Override
    public void onStackDismissed() {
        updateNotifications();
    }

    @Override
    public void onBubbleDismissed(String key) {
        NotificationData.Entry entry = mNotificationData.get(key);
        if (entry != null) {
            entry.setBubbleDismissed(true);
            if (!DEBUG_DEMOTE_TO_NOTIF) {
                performRemoveNotification(entry.notification);
            }
        }
        updateNotifications();
    }

    /**
     * Cancel this notification and tell the StatusBarManagerService / NotificationManagerService
     * about the failure.
@@ -727,7 +757,12 @@ public class NotificationEntryManager implements Dumpable, NotificationInflater.
        if (DEBUG) {
            Log.d(TAG, "createNotificationViews(notification=" + sbn + " " + ranking);
        }

        NotificationData.Entry entry = new NotificationData.Entry(sbn, ranking);
        if (shouldAutoBubble(entry)) {
            entry.setIsBubble(true);
        }

        Dependency.get(LeakDetector.class).trackInstance(entry);
        entry.createIcons(mContext, sbn);
        // Construct the expanded view.
@@ -948,6 +983,11 @@ public class NotificationEntryManager implements Dumpable, NotificationInflater.
        }
    }

    public void setStatusBarStateListener(
            NotificationViewHierarchyManager.StatusBarStateListener listener) {
        mStatusBarStateListener  = listener;
    }

    /**
     * Whether the notification should peek in from the top and alert the user.
     *
@@ -965,6 +1005,14 @@ public class NotificationEntryManager implements Dumpable, NotificationInflater.
            return false;
        }

        // TODO: need to changes this, e.g. should still heads up in expanded shade, might want
        // message bubble from the bubble to go through heads up path
        boolean inShade = mStatusBarStateListener != null
                && mStatusBarStateListener.getCurrentState() == SHADE;
        if (entry.isBubble() && !entry.isBubbleDismissed() && inShade) {
            return false;
        }

        if (!canAlertCommon(entry)) {
            if (DEBUG) {
                Log.d(TAG, "No heads up: notification shouldn't alert: " + sbn.getKey());
@@ -1152,6 +1200,17 @@ public class NotificationEntryManager implements Dumpable, NotificationInflater.
        }
    }


    /**
     * Whether a bubble is appropriate to auto-bubble or not.
     */
    private boolean shouldAutoBubble(NotificationData.Entry entry) {
        int priority = mNotificationData.getImportance(entry.key);
        NotificationChannel channel = mNotificationData.getChannel(entry.key);
        boolean canAppOverlay = channel != null && channel.canOverlayApps();
        return BubbleController.shouldAutoBubble(entry, priority, canAppOverlay);
    }

    /**
     * Callback for NotificationEntryManager.
     */