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

Commit 157cd3f7 authored by Mady Mellor's avatar Mady Mellor
Browse files

Allow Bubble overflow to be opened without other active bubbles

Currently you can only access the overflow if you have bubbles in
the stack. There are a bunch of explorations that show accessing
the overflow without the stack (e.g. from taskbar, from people area).

- BubbleData holds onto BubbleOverflow now
- When last bubble is removed, if showing in taskbar/already expanded
  & has overflow bubbs, select the overflow.
- No longer null out BubbleStackView when no more bubbles, this
  allows overflow to continue to show
- Added tests for overflow behaviour in BubblesTest

Test: atest SystemUITests
Test: manua 1) - produce a bunch of bubbles
               - expand the stack, dismiss each bubble
               => when you dismiss the last one, overflow selected
               - collapse the stack
               => overflow goes away
            2) - have taskbar cl / prototype
               - have bubbles in the overflow but no active bubbles
               => observe that taskbar shows the overflow (i.e. sysui
                 renders the stack in that spot)
Bug: 171322463
Bug: 167413172
Change-Id: I36b9cedc492ace59105dba58c89f9ef92ff4b9e8
parent 97fcd7a0
Loading
Loading
Loading
Loading
+5 −4
Original line number Diff line number Diff line
@@ -171,7 +171,7 @@ public class BubbleController implements Bubbles {
            ShellTaskOrganizer organizer) {
        BubbleLogger logger = new BubbleLogger(uiEventLogger);
        BubblePositioner positioner = new BubblePositioner(context, windowManager);
        BubbleData data = new BubbleData(context, logger);
        BubbleData data = new BubbleData(context, logger, positioner);
        return new BubbleController(context, data, synchronizer, floatingContentCoordinator,
                new BubbleDataRepository(context, launcherApps),
                statusBarService, windowManager, windowManagerShellWrapper, launcherApps,
@@ -382,7 +382,6 @@ public class BubbleController implements Bubbles {
        if (mStackView == null) {
            mStackView = new BubbleStackView(
                    mContext, this, mBubbleData, mSurfaceSynchronizer, mFloatingContentCoordinator);
            mStackView.addView(mBubbleScrim);
            mStackView.onOrientationChanged();
            if (mExpandListener != null) {
                mStackView.setExpandListener(mExpandListener);
@@ -422,6 +421,8 @@ public class BubbleController implements Bubbles {

        try {
            mAddedToWindowManager = true;
            mBubbleData.getOverflow().initialize(this);
            mStackView.addView(mBubbleScrim);
            mWindowManager.addView(mStackView, mWmLayoutParams);
            // Position info is dependent on us being attached to a window
            mBubblePositioner.update(mOrientation);
@@ -446,7 +447,7 @@ public class BubbleController implements Bubbles {
            if (mStackView != null) {
                mWindowManager.removeView(mStackView);
                mStackView.removeView(mBubbleScrim);
                mStackView = null;
                mBubbleData.getOverflow().cleanUpExpandedState();
            } else {
                Log.w(TAG, "StackView added to WindowManager, but was null when removing!");
            }
@@ -578,7 +579,7 @@ public class BubbleController implements Bubbles {
        if (mStackView == null) {
            return false;
        }
        return mBubbleData.hasBubbles();
        return mBubbleData.hasBubbles() || mBubbleData.isShowingOverflow();
    }

    @Override
+54 −22
Original line number Diff line number Diff line
@@ -69,7 +69,7 @@ public class BubbleData {
        boolean selectionChanged;
        boolean orderChanged;
        boolean expanded;
        @Nullable Bubble selectedBubble;
        @Nullable BubbleViewProvider selectedBubble;
        @Nullable Bubble addedBubble;
        @Nullable Bubble updatedBubble;
        @Nullable Bubble addedOverflowBubble;
@@ -116,13 +116,15 @@ public class BubbleData {
    }

    private final Context mContext;
    private final BubblePositioner mPositioner;
    /** Bubbles that are actively in the stack. */
    private final List<Bubble> mBubbles;
    /** Bubbles that aged out to overflow. */
    private final List<Bubble> mOverflowBubbles;
    /** Bubbles that are being loaded but haven't been added to the stack just yet. */
    private final HashMap<String, Bubble> mPendingBubbles;
    private Bubble mSelectedBubble;
    private BubbleViewProvider mSelectedBubble;
    private final BubbleOverflow mOverflow;
    private boolean mShowingOverflow;
    private boolean mExpanded;
    private final int mMaxBubbles;
@@ -153,9 +155,11 @@ public class BubbleData {
     */
    private HashMap<String, String> mSuppressedGroupKeys = new HashMap<>();

    public BubbleData(Context context, BubbleLogger bubbleLogger) {
    public BubbleData(Context context, BubbleLogger bubbleLogger, BubblePositioner positioner) {
        mContext = context;
        mLogger = bubbleLogger;
        mPositioner = positioner;
        mOverflow = new BubbleOverflow(context, positioner);
        mBubbles = new ArrayList<>();
        mOverflowBubbles = new ArrayList<>();
        mPendingBubbles = new HashMap<>();
@@ -178,6 +182,10 @@ public class BubbleData {
        return !mBubbles.isEmpty();
    }

    public boolean hasOverflowBubbles() {
        return !mOverflowBubbles.isEmpty();
    }

    public boolean isExpanded() {
        return mExpanded;
    }
@@ -195,10 +203,14 @@ public class BubbleData {
    }

    @Nullable
    public Bubble getSelectedBubble() {
    public BubbleViewProvider getSelectedBubble() {
        return mSelectedBubble;
    }

    public BubbleOverflow getOverflow() {
        return mOverflow;
    }

    /** Return a read-only current active bubble lists. */
    public List<Bubble> getActiveBubbles() {
        return Collections.unmodifiableList(mBubbles);
@@ -212,7 +224,7 @@ public class BubbleData {
        dispatchPendingChanges();
    }

    public void setSelectedBubble(Bubble bubble) {
    public void setSelectedBubble(BubbleViewProvider bubble) {
        if (DEBUG_BUBBLE_DATA) {
            Log.d(TAG, "setSelectedBubble: " + bubble);
        }
@@ -224,6 +236,10 @@ public class BubbleData {
        mShowingOverflow = showingOverflow;
    }

    boolean isShowingOverflow() {
        return mShowingOverflow && (isExpanded() || mPositioner.showingInTaskbar());
    }

    /**
     * Constructs a new bubble or returns an existing one. Does not add new bubbles to
     * bubble data, must go through {@link #notificationEntryUpdated(Bubble, boolean, boolean)}
@@ -264,8 +280,8 @@ public class BubbleData {

    /**
     * When this method is called it is expected that all info in the bubble has completed loading.
     * @see Bubble#inflate(BubbleViewInfoTask.Callback, Context,
     * BubbleStackView, BubbleIconFactory, boolean).
     * @see Bubble#inflate(BubbleViewInfoTask.Callback, Context, BubbleController, BubbleStackView,
     * BubbleIconFactory, boolean)
     */
    void notificationEntryUpdated(Bubble bubble, boolean suppressFlyout, boolean showInShade) {
        if (DEBUG_BUBBLE_DATA) {
@@ -484,11 +500,18 @@ public class BubbleData {
        Bubble bubbleToRemove = mBubbles.get(indexToRemove);
        bubbleToRemove.stopInflation();
        if (mBubbles.size() == 1) {
            // Going to become empty, handle specially.
            if (hasOverflowBubbles() && (mPositioner.showingInTaskbar() || isExpanded())) {
                // No more active bubbles but we have stuff in the overflow -- select that view
                // if we're already expanded or always showing.
                setShowingOverflow(true);
                setSelectedBubbleInternal(mOverflow);
            } else {
                setExpandedInternal(false);
            // Don't use setSelectedBubbleInternal because we don't want to trigger an applyUpdate
                // Don't use setSelectedBubbleInternal because we don't want to trigger an
                // applyUpdate
                mSelectedBubble = null;
            }
        }
        if (indexToRemove < mBubbles.size() - 1) {
            // Removing anything but the last bubble means positions will change.
            mStateChange.orderChanged = true;
@@ -505,7 +528,7 @@ public class BubbleData {
        if (Objects.equals(mSelectedBubble, bubbleToRemove)) {
            // Move selection to the new bubble at the same position.
            int newIndex = Math.min(indexToRemove, mBubbles.size() - 1);
            Bubble newSelected = mBubbles.get(newIndex);
            BubbleViewProvider newSelected = mBubbles.get(newIndex);
            setSelectedBubbleInternal(newSelected);
        }
        maybeSendDeleteIntent(reason, bubbleToRemove);
@@ -564,7 +587,7 @@ public class BubbleData {
     *
     * @param bubble the new selected bubble
     */
    private void setSelectedBubbleInternal(@Nullable Bubble bubble) {
    private void setSelectedBubbleInternal(@Nullable BubbleViewProvider bubble) {
        if (DEBUG_BUBBLE_DATA) {
            Log.d(TAG, "setSelectedBubbleInternal: " + bubble);
        }
@@ -572,14 +595,17 @@ public class BubbleData {
            return;
        }
        // Otherwise, if we are showing the overflow menu, return to the previously selected bubble.

        if (bubble != null && !mBubbles.contains(bubble) && !mOverflowBubbles.contains(bubble)) {
        boolean isOverflow = bubble != null && BubbleOverflow.KEY.equals(bubble.getKey());
        if (bubble != null
                && !mBubbles.contains(bubble)
                && !mOverflowBubbles.contains(bubble)
                && !isOverflow) {
            Log.e(TAG, "Cannot select bubble which doesn't exist!"
                    + " (" + bubble + ") bubbles=" + mBubbles);
            return;
        }
        if (mExpanded && bubble != null) {
            bubble.markAsAccessedAt(mTimeSource.currentTimeMillis());
        if (mExpanded && bubble != null && !isOverflow) {
            ((Bubble) bubble).markAsAccessedAt(mTimeSource.currentTimeMillis());
        }
        mSelectedBubble = bubble;
        mStateChange.selectedBubble = bubble;
@@ -629,7 +655,7 @@ public class BubbleData {
            return;
        }
        if (shouldExpand) {
            if (mBubbles.isEmpty()) {
            if (mBubbles.isEmpty() && !mShowingOverflow) {
                Log.e(TAG, "Attempt to expand stack when empty!");
                return;
            }
@@ -637,7 +663,9 @@ public class BubbleData {
                Log.e(TAG, "Attempt to expand stack without selected bubble!");
                return;
            }
            mSelectedBubble.markAsAccessedAt(mTimeSource.currentTimeMillis());
            if (mSelectedBubble instanceof Bubble) {
                ((Bubble) mSelectedBubble).markAsAccessedAt(mTimeSource.currentTimeMillis());
            }
            mStateChange.orderChanged |= repackAll();
        } else if (!mBubbles.isEmpty()) {
            // Apply ordering and grouping rules from expanded -> collapsed, then save
@@ -647,14 +675,18 @@ public class BubbleData {

            if (mShowingOverflow) {
                // Show previously selected bubble instead of overflow menu on next expansion.
                if (!mSelectedBubble.getKey().equals(mOverflow.getKey())) {
                    setSelectedBubbleInternal(mSelectedBubble);
                } else {
                    setSelectedBubbleInternal(mBubbles.get(0));
                }
            }
            if (mBubbles.indexOf(mSelectedBubble) > 0) {
                // Move the selected bubble to the top while collapsed.
                int index = mBubbles.indexOf(mSelectedBubble);
                if (index != 0) {
                    mBubbles.remove(mSelectedBubble);
                    mBubbles.add(0, mSelectedBubble);
                    mBubbles.remove((Bubble) mSelectedBubble);
                    mBubbles.add(0, (Bubble) mSelectedBubble);
                    mStateChange.orderChanged = true;
                }
            }
+6 −5
Original line number Diff line number Diff line
@@ -63,11 +63,17 @@ class BubbleOverflow(
        overflowBtn = null
    }

    /** Call before use and again if cleanUpExpandedState was called.  */
    fun initialize(controller: BubbleController) {
        getExpandedView()?.initialize(controller, controller.stackView)
        getExpandedView()?.setOverflow(true)
    }

    fun cleanUpExpandedState() {
        expandedView?.cleanUpExpandedState()
        expandedView = null
    }

    fun update() {
        updateResources()
        getExpandedView()?.applyThemeAttrs()
@@ -132,11 +138,6 @@ class BubbleOverflow(
        overflowBtn?.updateDotVisibility(true /* animate */)
    }

    fun cleanUpExpandedState() {
        expandedView?.cleanUpExpandedState()
        expandedView = null
    }

    override fun getExpandedView(): BubbleExpandedView? {
        if (expandedView == null) {
            expandedView = inflater.inflate(R.layout.bubble_expanded_view,
+29 −17
Original line number Diff line number Diff line
@@ -227,7 +227,7 @@ public class BubbleStackView extends FrameLayout
     * once it collapses.
     */
    @Nullable
    private Bubble mBubbleToExpandAfterFlyoutCollapse = null;
    private BubbleViewProvider mBubbleToExpandAfterFlyoutCollapse = null;

    /** Layout change listener that moves the stack to the nearest valid position on rotation. */
    private OnLayoutChangeListener mOrientationChangedListener;
@@ -571,6 +571,16 @@ public class BubbleStackView extends FrameLayout
                mBubbleContainer.setActiveController(mStackAnimationController);
                hideFlyoutImmediate();

                if (!mPositioner.showingInTaskbar()) {
                    // Also, save the magnetized stack so we can dispatch touch events to it.
                    mMagnetizedObject = mStackAnimationController.getMagnetizedStack(
                            mMagneticTarget);
                    mMagnetizedObject.setMagnetListener(mStackMagnetListener);
                } else {
                    // In taskbar, the stack isn't draggable so we shouldn't dispatch touch events.
                    mMagnetizedObject = null;
                }

                // Also, save the magnetized stack so we can dispatch touch events to it.
                mMagnetizedObject = mStackAnimationController.getMagnetizedStack(mMagneticTarget);
                mMagnetizedObject.setMagnetListener(mStackMagnetListener);
@@ -592,7 +602,9 @@ public class BubbleStackView extends FrameLayout
        public void onMove(@NonNull View v, @NonNull MotionEvent ev, float viewInitialX,
                float viewInitialY, float dx, float dy) {
            // If we're expanding or collapsing, ignore all touch events.
            if (mIsExpansionAnimating) {
            if (mIsExpansionAnimating
                    // Also ignore events if we shouldn't be draggable.
                    || (mPositioner.showingInTaskbar() && !mIsExpanded)) {
                return;
            }

@@ -602,7 +614,7 @@ public class BubbleStackView extends FrameLayout
            // First, see if the magnetized object consumes the event - if so, we shouldn't move the
            // bubble since it's stuck to the target.
            if (!passEventToMagnetizedObject(ev)) {
                if (mBubbleData.isExpanded()) {
                if (mBubbleData.isExpanded() || mPositioner.showingInTaskbar()) {
                    mExpandedAnimationController.dragBubbleOut(
                            v, viewInitialX + dx, viewInitialY + dy);
                } else {
@@ -743,7 +755,7 @@ public class BubbleStackView extends FrameLayout
        ta.recycle();

        final Runnable onBubbleAnimatedOut = () -> {
            if (getBubbleCount() == 0) {
            if (getBubbleCount() == 0 && !mBubbleData.isShowingOverflow()) {
                mBubbleController.onAllBubblesAnimatedOut();
            }
        };
@@ -816,16 +828,16 @@ public class BubbleStackView extends FrameLayout
        setFocusable(true);
        mBubbleContainer.bringToFront();

        mBubbleOverflow = new BubbleOverflow(getContext(), mPositioner);
        mBubbleOverflow.initialize(mBubbleController);
        mBubbleOverflow = mBubbleData.getOverflow();
        mBubbleContainer.addView(mBubbleOverflow.getIconView(),
                mBubbleContainer.getChildCount() /* index */,
                new FrameLayout.LayoutParams(mPositioner.getBubbleSize(),
                        mPositioner.getBubbleSize()));
        updateOverflow();
        mBubbleOverflow.getIconView().setOnClickListener((View v) -> {
            setSelectedBubble(mBubbleOverflow);
            showManageMenu(false);
            mBubbleData.setShowingOverflow(true);
            mBubbleData.setSelectedBubble(mBubbleOverflow);
            mBubbleData.setExpanded(true);
        });

        setOnApplyWindowInsetsListener((View view, WindowInsets insets) -> {
@@ -996,11 +1008,12 @@ public class BubbleStackView extends FrameLayout
        mManageMenu.findViewById(R.id.bubble_manage_menu_settings_container).setOnClickListener(
                view -> {
                    showManageMenu(false /* show */);
                    final Bubble bubble = mBubbleData.getSelectedBubble();
                    final BubbleViewProvider bubble = mBubbleData.getSelectedBubble();
                    if (bubble != null && mBubbleData.hasBubbleInStackWithKey(bubble.getKey())) {
                        final Intent intent = bubble.getSettingsIntent(mContext);
                        // If it's in the stack it's a proper Bubble.
                        final Intent intent = ((Bubble) bubble).getSettingsIntent(mContext);
                        mBubbleData.setExpanded(false);
                        mContext.startActivityAsUser(intent, bubble.getUser());
                        mContext.startActivityAsUser(intent, ((Bubble) bubble).getUser());
                        logBubbleEvent(bubble,
                                FrameworkStatsLog.BUBBLE_UICHANGED__ACTION__HEADER_GO_TO_SETTINGS);
                    }
@@ -1443,10 +1456,9 @@ public class BubbleStackView extends FrameLayout
    }

    private void updateOverflowVisibility() {
        if (mBubbleOverflow == null) {
            return;
        }
        mBubbleOverflow.setVisible(mIsExpanded ? VISIBLE : GONE);
        mBubbleOverflow.setVisible((mIsExpanded || mBubbleData.isShowingOverflow())
                ? VISIBLE
                : GONE);
    }

    // via BubbleData.Listener
@@ -2128,7 +2140,7 @@ public class BubbleStackView extends FrameLayout
        }
    }

    private void dismissBubbleIfExists(@Nullable Bubble bubble) {
    private void dismissBubbleIfExists(@Nullable BubbleViewProvider bubble) {
        if (bubble != null && mBubbleData.hasBubbleInStackWithKey(bubble.getKey())) {
            mBubbleData.dismissBubbleWithKey(bubble.getKey(), Bubbles.DISMISS_USER_GESTURE);
        }
@@ -2337,7 +2349,7 @@ public class BubbleStackView extends FrameLayout
        }

        if (!mIsExpanded) {
            if (getBubbleCount() > 0) {
            if (getBubbleCount() > 0 || mBubbleData.isShowingOverflow()) {
                mBubbleContainer.getChildAt(0).getBoundsOnScreen(outRect);
                // Increase the touch target size of the bubble
                outRect.top -= mBubbleTouchPadding;
+4 −2
Original line number Diff line number Diff line
@@ -35,6 +35,7 @@ import android.service.notification.StatusBarNotification;
import android.testing.AndroidTestingRunner;
import android.testing.TestableLooper;
import android.util.Pair;
import android.view.WindowManager;

import androidx.test.filters.SmallTest;

@@ -135,8 +136,9 @@ public class BubbleDataTest extends ShellTestCase {
        mBubbleB2 = new Bubble(mEntryB2, mSuppressionListener, mPendingIntentCanceledListener);
        mBubbleB3 = new Bubble(mEntryB3, mSuppressionListener, mPendingIntentCanceledListener);
        mBubbleC1 = new Bubble(mEntryC1, mSuppressionListener, mPendingIntentCanceledListener);

        mBubbleData = new BubbleData(getContext(), mBubbleLogger);
        TestableBubblePositioner positioner = new TestableBubblePositioner(mContext,
                mock(WindowManager.class));
        mBubbleData = new BubbleData(getContext(), mBubbleLogger, positioner);

        // Used by BubbleData to set lastAccessedTime
        when(mTimeSource.currentTimeMillis()).thenReturn(1000L);
Loading