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

Commit 58a55222 authored by Lyn Han's avatar Lyn Han
Browse files

Fix dot for smart reply and bubble groups [DO NOT MERGE]

[Cherrypicked from master]

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
(cherry picked from commit f1f2c33f)
parent 39195ba2
Loading
Loading
Loading
Loading
+75 −80
Original line number Diff line number Diff line
@@ -169,7 +169,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;
@@ -1401,73 +1401,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()
@@ -1475,37 +1450,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() {