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

Commit f1f2c33f authored by Lyn Han's avatar Lyn Han
Browse files

Fix dot for smart reply and bubble groups

Android Messages sends two notifications per message
- New notif
- Smart reply update

This change fixes
- dot bugs that occur when two notifications arrive in quick succession for the same bubble
- regressions for bubble groups

BubbleStackView
- clearFlyoutOnHide: enforce flyout onHide to run once for the bubble it was updated for
- refactors for clarity

BubbleView
- shouldShowDot: refactor show-dot logic into function
	updateViews did not account for mSuppressDot
	=> dot flashed into view before being animated away by later dot visibility updates
- updateDotVisibility: add missing call to setDotScale if animate=false

Fixes: 138659213
Test: add bubble group with test app
	expand bubbles => update dots show for non-expanded bubbles
	expand bubbles, click through bubbles => dots go away
	expand bubbles, dismiss single bubble => summary (with one less notif) stays in scrim
Test: add mixed bubble group with test app
	expand bubbles, dismiss all bubbles => scrim notif for non-bubbling group stays

Bug: 138755533
Test: send android messages sms => flyout and dot behave as expected
Test: create bubble with test app => flyout and dot behave as expected
Test: atest SystemUITests
Change-Id: Ieb2e6c306a0b55aec248bd1582246b67eafab290
parent 13e5df59
Loading
Loading
Loading
Loading
+75 −80
Original line number Diff line number Diff line
@@ -168,7 +168,7 @@ public class BubbleStackView extends FrameLayout {
     * Callback to run after the flyout hides. Also called if a new flyout is shown before the
     * previous one animates out.
     */
    private Runnable mAfterFlyoutHides;
    private Runnable mFlyoutOnHide;

    /** Layout change listener that moves the stack to the nearest valid position on rotation. */
    private OnLayoutChangeListener mOrientationChangedListener;
@@ -1366,73 +1366,48 @@ public class BubbleStackView extends FrameLayout {
    @VisibleForTesting
    void animateInFlyoutForBubble(Bubble bubble) {
        final CharSequence updateMessage = bubble.getUpdateMessage(getContext());

        if (!bubble.showFlyoutForBubble()) {
            // In case flyout was suppressed for this update, reset now.
            bubble.setSuppressFlyout(false);
            return;
        }

        if (updateMessage == null
                || isExpanded()
                || mIsExpansionAnimating
                || mIsGestureInProgress
                || mBubbleToExpandAfterFlyoutCollapse != null) {
                || mBubbleToExpandAfterFlyoutCollapse != null
                || bubble.getIconView() == null) {
            // Skip the message if none exists, we're expanded or animating expansion, or we're
            // about to expand a bubble from the previous tapped flyout.
            // about to expand a bubble from the previous tapped flyout, or if bubble view is null.
            return;
        }

        if (bubble.getIconView() != null) {
            // Temporarily suppress the dot while the flyout is visible.
            bubble.getIconView().setSuppressDot(
                    true /* suppressDot */, false /* animate */);

            mFlyout.removeCallbacks(mAnimateInFlyout);
        mFlyoutDragDeltaX = 0f;

            if (mAfterFlyoutHides != null) {
                mAfterFlyoutHides.run();
            }

            mAfterFlyoutHides = () -> {
                final boolean suppressDot = !bubble.showBubbleDot();
                // If we're going to suppress the dot, make it visible first so it'll
                // visibly animate away.
                if (suppressDot) {
                    bubble.getIconView().setSuppressDot(
                            false /* suppressDot */, false /* animate */);
        clearFlyoutOnHide();
        mFlyoutOnHide = () -> {
            resetDot(bubble);
            if (mBubbleToExpandAfterFlyoutCollapse == null) {
                return;
            }
                // Reset dot suppression. If we're not suppressing due to DND, then
                // stop suppressing it with no animation (since the flyout has
                // transformed into the dot). If we are suppressing due to DND, animate
                // it away.
                bubble.getIconView().setSuppressDot(
                        suppressDot /* suppressDot */,
                        suppressDot /* animate */);

                if (mBubbleToExpandAfterFlyoutCollapse != null) {
            mBubbleData.setSelectedBubble(mBubbleToExpandAfterFlyoutCollapse);
            mBubbleData.setExpanded(true);
            mBubbleToExpandAfterFlyoutCollapse = null;
                }
        };

        mFlyout.setVisibility(INVISIBLE);

            // Post in case layout isn't complete and getWidth returns 0.
        // Temporarily suppress the dot while the flyout is visible.
        bubble.getIconView().setSuppressDot(
                true /* suppressDot */, false /* animate */);

        // Start flyout expansion. Post in case layout isn't complete and getWidth returns 0.
        post(() -> {
            // An auto-expanding bubble could have been posted during the time it takes to
            // layout.
            if (isExpanded()) {
                return;
            }

                final Runnable afterShow = () -> {
            final Runnable expandFlyoutAfterDelay = () -> {
                mAnimateInFlyout = () -> {
                    mFlyout.setVisibility(VISIBLE);
                        bubble.getIconView().setSuppressDot(
                                true /* suppressDot */, false /* animate */);
                    mFlyoutDragDeltaX =
                            mStackAnimationController.isStackOnLeftSide()
                                    ? -mFlyout.getWidth()
@@ -1440,37 +1415,57 @@ public class BubbleStackView extends FrameLayout {
                    animateFlyoutCollapsed(false /* collapsed */, 0 /* velX */);
                    mFlyout.postDelayed(mHideFlyout, FLYOUT_HIDE_AFTER);
                };

                mFlyout.postDelayed(mAnimateInFlyout, 200);
            };

            mFlyout.setupFlyoutStartingAsDot(
                    updateMessage, mStackAnimationController.getStackPosition(), getWidth(),
                    mStackAnimationController.isStackOnLeftSide(),
                        bubble.getIconView().getBadgeColor(),
                        afterShow,
                        mAfterFlyoutHides,
                    bubble.getIconView().getBadgeColor() /* dotColor */,
                    expandFlyoutAfterDelay /* onLayoutComplete */,
                    mFlyoutOnHide,
                    bubble.getIconView().getDotCenter());
            mFlyout.bringToFront();
        });
        }

        mFlyout.removeCallbacks(mHideFlyout);
        mFlyout.postDelayed(mHideFlyout, FLYOUT_HIDE_AFTER);
        logBubbleEvent(bubble, StatsLog.BUBBLE_UICHANGED__ACTION__FLYOUT);
    }

    /** Hide the flyout immediately and cancel any pending hide runnables. */
    private void hideFlyoutImmediate() {
        if (mAfterFlyoutHides != null) {
            mAfterFlyoutHides.run();
    private void resetDot(Bubble bubble) {
        final boolean suppressDot = !bubble.showBubbleDot();
        // If we're going to suppress the dot, make it visible first so it'll
        // visibly animate away.

        if (suppressDot) {
            bubble.getIconView().setSuppressDot(
                    false /* suppressDot */, false /* animate */);
        }
        // Reset dot suppression. If we're not suppressing due to DND, then
        // stop suppressing it with no animation (since the flyout has
        // transformed into the dot). If we are suppressing due to DND, animate
        // it away.
        bubble.getIconView().setSuppressDot(
                suppressDot /* suppressDot */,
                suppressDot /* animate */);
    }

    /** Hide the flyout immediately and cancel any pending hide runnables. */
    private void hideFlyoutImmediate() {
        clearFlyoutOnHide();
        mFlyout.removeCallbacks(mAnimateInFlyout);
        mFlyout.removeCallbacks(mHideFlyout);
        mFlyout.hideFlyout();
    }

    private void clearFlyoutOnHide() {
        mFlyout.removeCallbacks(mAnimateInFlyout);
        if (mFlyoutOnHide == null) {
            return;
        }
        mFlyoutOnHide.run();
        mFlyoutOnHide = null;
    }

    @Override
    public void getBoundsOnScreen(Rect outRect) {
        if (!mIsExpanded) {
+28 −26
Original line number Diff line number Diff line
@@ -61,7 +61,7 @@ public class BubbleView extends FrameLayout {
    // mBubbleIconFactory cannot be static because it depends on Context.
    private BubbleIconFactory mBubbleIconFactory;

    private boolean mSuppressDot = false;
    private boolean mSuppressDot;

    private Bubble mBubble;

@@ -140,6 +140,7 @@ public class BubbleView extends FrameLayout {
    public void setAppIcon(Drawable appIcon) {
        mUserBadgedAppIcon = appIcon;
    }

    /**
     * @return the {@link ExpandableNotificationRow} view to display notification content when the
     * bubble is expanded.
@@ -154,7 +155,6 @@ public class BubbleView extends FrameLayout {
        updateDotVisibility(animate, null /* after */);
    }


    /**
     * Sets whether or not to hide the dot even if we'd otherwise show it. This is used while the
     * flyout is visible or animating, to hide the dot until the flyout visually transforms into it.
@@ -166,7 +166,7 @@ public class BubbleView extends FrameLayout {

    /** Sets the position of the 'new' dot, animating it out and back in if requested. */
    void setDotPosition(boolean onLeft, boolean animate) {
        if (animate && onLeft != mBadgedImageView.getDotOnLeft() && !mSuppressDot) {
        if (animate && onLeft != mBadgedImageView.getDotOnLeft() && shouldShowDot()) {
            animateDot(false /* showDot */, () -> {
                mBadgedImageView.setDotOnLeft(onLeft);
                animateDot(true /* showDot */, null);
@@ -190,12 +190,12 @@ public class BubbleView extends FrameLayout {
     * after animation if requested.
     */
    private void updateDotVisibility(boolean animate, Runnable after) {
        boolean showDot = mBubble.showBubbleDot() && !mSuppressDot;

        final boolean showDot = shouldShowDot();
        if (animate) {
            animateDot(showDot, after);
        } else {
            mBadgedImageView.setShowDot(showDot);
            mBadgedImageView.setDotScale(showDot ? 1f : 0f);
        }
    }

@@ -203,10 +203,12 @@ public class BubbleView extends FrameLayout {
     * Animates the badge to show or hide.
     */
    private void animateDot(boolean showDot, Runnable after) {
        if (mBadgedImageView.isShowingDot() != showDot) {
            if (showDot) {
                mBadgedImageView.setShowDot(true);
        if (mBadgedImageView.isShowingDot() == showDot) {
            return;
        }
        // Do NOT wait until after animation ends to setShowDot
        // to avoid overriding more recent showDot states.
        mBadgedImageView.setShowDot(showDot);
        mBadgedImageView.clearAnimation();
        mBadgedImageView.animate().setDuration(200)
                .setInterpolator(Interpolators.FAST_OUT_SLOW_IN)
@@ -215,16 +217,12 @@ public class BubbleView extends FrameLayout {
                    fraction = showDot ? fraction : 1f - fraction;
                    mBadgedImageView.setDotScale(fraction);
                }).withEndAction(() -> {
                        if (!showDot) {
                            mBadgedImageView.setShowDot(false);
                        }

            mBadgedImageView.setDotScale(showDot ? 1f : 0f);
            if (after != null) {
                after.run();
            }
        }).start();
    }
    }

    void updateViews() {
        if (mBubble == null || mBubbleIconFactory == null) {
@@ -273,7 +271,11 @@ public class BubbleView extends FrameLayout {
        iconPath.transform(matrix);
        mBadgedImageView.drawDot(iconPath);

        animateDot(mBubble.showBubbleDot() /* showDot */, null /* after */);
        animateDot(shouldShowDot(), null /* after */);
    }

    boolean shouldShowDot() {
        return mBubble.showBubbleDot() && !mSuppressDot;
    }

    int getBadgeColor() {