From 501302859693dc8c9d37d9497d7c1d47318f7c4c Mon Sep 17 00:00:00 2001 From: Liran Binyamin Date: Thu, 11 Jul 2024 11:58:09 -0400 Subject: [PATCH] Update bubble notification dot drawing The notification dot drawing state depends on changes in the bubble bar, so the bubble bar now directly requests updates to the dot at specific states. Flag: com.android.wm.shell.enable_bubble_bar Fixes: 351904597 Fixes: 350782375 Test: atest BubbleViewTest Test: manual basically just hammering on it trying to break it 1. Dot appears for new bubble in all states of the bar 2. Expand bubble bar - Dot is removed for selected and shows for others that have a dot 3. After removing bubble while expanded - Dot is removed from newly selected bubble 4. Collapse bubble bar - Dot is hidden for all bubbles 5. Dot removed after tapping on a bubble when expanded Change-Id: I4a940d38a8c77bddaf058b7ef882bab06886bcf4 --- .../taskbar/bubbles/BubbleBarController.java | 10 --- .../taskbar/bubbles/BubbleBarView.java | 41 ++++++++-- .../bubbles/BubbleBarViewController.java | 7 +- .../launcher3/taskbar/bubbles/BubbleView.java | 74 +++++++++++++----- .../taskbar/bubbles/BubbleViewTest.kt | 76 +++++++++++++++++++ 5 files changed, 172 insertions(+), 36 deletions(-) create mode 100644 quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/BubbleViewTest.kt diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarController.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarController.java index a3832cdf8b..88ee192558 100644 --- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarController.java +++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarController.java @@ -33,7 +33,6 @@ import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_S import android.annotation.BinderThread; import android.annotation.Nullable; -import android.app.Notification; import android.content.Context; import android.content.pm.ApplicationInfo; import android.content.pm.LauncherApps; @@ -456,15 +455,6 @@ public class BubbleBarController extends IBubblesListener.Stub { /** Tells WMShell to show the currently selected bubble. */ public void showSelectedBubble() { if (getSelectedBubbleKey() != null) { - if (mSelectedBubble instanceof BubbleBarBubble) { - // Because we've visited this bubble, we should suppress the notification. - // This is updated on WMShell side when we show the bubble, but that update isn't - // passed to launcher, instead we apply it directly here. - BubbleInfo info = ((BubbleBarBubble) mSelectedBubble).getInfo(); - info.setFlags( - info.getFlags() | Notification.BubbleMetadata.FLAG_SUPPRESS_NOTIFICATION); - mSelectedBubble.getView().updateDotVisibility(true /* animate */); - } mLastSentBubbleBarTop = mBarView.getRestingTopPositionOnScreen(); mSystemUiProxy.showBubble(getSelectedBubbleKey(), mLastSentBubbleBarTop); } else { diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarView.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarView.java index fd989b106a..4794dfd8db 100644 --- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarView.java +++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarView.java @@ -240,6 +240,10 @@ public class BubbleBarView extends FrameLayout { BubbleView firstBubble = (BubbleView) getChildAt(0); mUpdateSelectedBubbleAfterCollapse.accept(firstBubble.getBubble().getKey()); } + // If the bar was just expanded, remove the dot from the selected bubble. + if (mIsBarExpanded && mSelectedBubbleView != null) { + mSelectedBubbleView.markSeen(); + } updateWidth(); }, /* onUpdate= */ animator -> { @@ -665,7 +669,7 @@ public class BubbleBarView extends FrameLayout { } /** Add a new bubble to the bubble bar. */ - public void addBubble(View bubble) { + public void addBubble(BubbleView bubble) { FrameLayout.LayoutParams lp = new FrameLayout.LayoutParams((int) mIconSize, (int) mIconSize, Gravity.LEFT); if (isExpanded()) { @@ -673,6 +677,7 @@ public class BubbleBarView extends FrameLayout { bubble.setScaleX(0f); bubble.setScaleY(0f); addView(bubble, 0, lp); + bubble.showDotIfNeeded(/* animate= */ false); mBubbleAnimator = new BubbleAnimator(mIconSize, mExpandedBarIconsSpacing, getChildCount(), mBubbleBarLocation.isOnLeft(isLayoutRtl())); @@ -825,6 +830,23 @@ public class BubbleBarView extends FrameLayout { updateBubbleAccessibilityStates(); updateContentDescription(); mDismissedByDragBubbleView = null; + updateNotificationDotsIfCollapsed(); + } + + private void updateNotificationDotsIfCollapsed() { + if (isExpanded()) { + return; + } + for (int i = 0; i < getChildCount(); i++) { + BubbleView bubbleView = (BubbleView) getChildAt(i); + // when we're collapsed, the first bubble should show the dot if it has it. the rest of + // the bubbles should hide their dots. + if (i == 0 && bubbleView.hasUnseenContent()) { + bubbleView.showDotIfNeeded(/* animate= */ true); + } else { + bubbleView.hideDot(); + } + } } private void updateWidth() { @@ -865,7 +887,6 @@ public class BubbleBarView extends FrameLayout { float bubbleBarAnimatedTop = viewBottom - getBubbleBarHeight(); // When translating X & Y the scale is ignored, so need to deduct it from the translations final float ty = bubbleBarAnimatedTop + mBubbleBarPadding - getScaleIconShift(); - final boolean animate = getVisibility() == VISIBLE; final boolean onLeft = bubbleBarLocation.isOnLeft(isLayoutRtl()); // elevation state is opposite to widthState - when expanded all icons are flat float elevationState = (1 - widthState); @@ -897,10 +918,10 @@ public class BubbleBarView extends FrameLayout { bv.setZ(fullElevationForChild * elevationState); // only update the dot scale if we're expanding or collapsing - // TODO b/351904597: update the dot for the first bubble after removal and reorder - // since those might happen when the bar is collapsed and will need their dot back if (mWidthAnimator.isRunning()) { - bv.setDotScale(widthState); + // The dot for the selected bubble scales in the opposite direction of the expansion + // animation. + bv.showDotIfNeeded(bv == mSelectedBubbleView ? 1 - widthState : widthState); } if (mIsBarExpanded) { @@ -1025,6 +1046,7 @@ public class BubbleBarView extends FrameLayout { } updateBubblesLayoutProperties(mBubbleBarLocation); updateContentDescription(); + updateNotificationDotsIfCollapsed(); } } @@ -1049,6 +1071,14 @@ public class BubbleBarView extends FrameLayout { if (mBubbleAnimator == null) { updateArrowForSelected(previouslySelectedBubble != null); } + if (view != null) { + if (isExpanded()) { + view.markSeen(); + } else { + // when collapsed, the selected bubble should show the dot if it has it + view.showDotIfNeeded(/* animate= */ true); + } + } } /** @@ -1316,6 +1346,7 @@ public class BubbleBarView extends FrameLayout { public void dump(PrintWriter pw) { pw.println("BubbleBarView state:"); pw.println(" visibility: " + getVisibility()); + pw.println(" alpha: " + getAlpha()); pw.println(" translation Y: " + getTranslationY()); pw.println(" bubbles in bar (childCount = " + getChildCount() + ")"); for (BubbleView bubbleView: getBubbles()) { diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java index 0f9de16ee0..3c74ca417c 100644 --- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java +++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java @@ -113,7 +113,7 @@ public class BubbleBarViewController { dp -> updateBubbleBarIconSize(dp.taskbarIconSize, /* animate= */ true)); updateBubbleBarIconSize(mActivity.getDeviceProfile().taskbarIconSize, /* animate= */ false); mBubbleBarScale.updateValue(1f); - mBubbleClickListener = v -> onBubbleClicked(v); + mBubbleClickListener = v -> onBubbleClicked((BubbleView) v); mBubbleBarClickListener = v -> onBubbleBarClicked(); mBubbleDragController.setupBubbleBarView(mBarView); mBarView.setOnClickListener(mBubbleBarClickListener); @@ -137,8 +137,9 @@ public class BubbleBarViewController { }); } - private void onBubbleClicked(View v) { - BubbleBarItem bubble = ((BubbleView) v).getBubble(); + private void onBubbleClicked(BubbleView bubbleView) { + bubbleView.markSeen(); + BubbleBarItem bubble = bubbleView.getBubble(); if (bubble == null) { Log.e(TAG, "bubble click listener, bubble was null"); } diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleView.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleView.java index 4c468bbd6d..acb6b4ecfe 100644 --- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleView.java +++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleView.java @@ -16,6 +16,7 @@ package com.android.launcher3.taskbar.bubbles; import android.annotation.Nullable; +import android.app.Notification; import android.content.Context; import android.graphics.Bitmap; import android.graphics.Canvas; @@ -35,6 +36,7 @@ import com.android.launcher3.R; import com.android.launcher3.icons.DotRenderer; import com.android.launcher3.icons.IconNormalizer; import com.android.wm.shell.animation.Interpolators; +import com.android.wm.shell.common.bubbles.BubbleInfo; // TODO: (b/276978250) This is will be similar to WMShell's BadgedImageView, it'd be nice to share. @@ -217,9 +219,9 @@ public class BubbleView extends ConstraintLayout { } void updateDotVisibility(boolean animate) { - final float targetScale = shouldDrawDot() ? 1f : 0f; + final float targetScale = hasUnseenContent() ? 1f : 0f; if (animate) { - animateDotScale(); + animateDotScale(targetScale); } else { mDotScale = targetScale; mAnimatingToDotScale = targetScale; @@ -241,18 +243,27 @@ public class BubbleView extends ConstraintLayout { mAppIcon.setVisibility(show ? VISIBLE : GONE); } - /** Whether the dot indicating unseen content in a bubble should be shown. */ - private boolean shouldDrawDot() { - boolean bubbleHasUnseenContent = mBubble != null + boolean hasUnseenContent() { + return mBubble != null && mBubble instanceof BubbleBarBubble && !((BubbleBarBubble) mBubble).getInfo().isNotificationSuppressed(); - // Always render the dot if it's animating, since it could be animating out. Otherwise, show - // it if the bubble wants to show it, and we aren't suppressing it. - return bubbleHasUnseenContent || mDotIsAnimating; } - /** How big the dot should be, fraction from 0 to 1. */ - void setDotScale(float fraction) { + /** + * Used to determine if we can skip drawing frames. + * + *

Generally we should draw the dot when it is requested to be shown and there is unseen + * content. But when the dot is removed, we still want to draw frames so that it can be scaled + * out. + */ + private boolean shouldDrawDot() { + // if there's no dot there's nothing to draw, unless the dot was removed and we're in the + // middle of removing it + return hasUnseenContent() || mDotIsAnimating; + } + + /** Updates the dot scale to the specified fraction from 0 to 1. */ + private void setDotScale(float fraction) { if (!shouldDrawDot()) { return; } @@ -260,11 +271,41 @@ public class BubbleView extends ConstraintLayout { invalidate(); } - /** - * Animates the dot to the given scale. - */ - private void animateDotScale() { - float toScale = shouldDrawDot() ? 1f : 0f; + void showDotIfNeeded(float fraction) { + if (!hasUnseenContent()) { + return; + } + setDotScale(fraction); + } + + void showDotIfNeeded(boolean animate) { + // only show the dot if we have unseen content + if (!hasUnseenContent()) { + return; + } + if (animate) { + animateDotScale(1f); + } else { + setDotScale(1f); + } + } + + void hideDot() { + animateDotScale(0f); + } + + /** Marks this bubble such that it no longer has unseen content, and hides the dot. */ + void markSeen() { + if (mBubble instanceof BubbleBarBubble bubble) { + BubbleInfo info = bubble.getInfo(); + info.setFlags( + info.getFlags() | Notification.BubbleMetadata.FLAG_SUPPRESS_NOTIFICATION); + hideDot(); + } + } + + /** Animates the dot to the given scale. */ + private void animateDotScale(float toScale) { boolean isDotScaleChanging = Float.compare(mDotScale, toScale) != 0; // Don't restart the animation if we're already animating to the given value or if the dot @@ -277,8 +318,6 @@ public class BubbleView extends ConstraintLayout { final boolean showDot = toScale > 0f; - // Do NOT wait until after animation ends to setShowDot - // to avoid overriding more recent showDot states. clearAnimation(); animate() .setDuration(200) @@ -293,7 +332,6 @@ public class BubbleView extends ConstraintLayout { }).start(); } - @Override public String toString() { String toString = mBubble != null ? mBubble.getKey() : "null"; diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/BubbleViewTest.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/BubbleViewTest.kt new file mode 100644 index 0000000000..8bad3b9569 --- /dev/null +++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/BubbleViewTest.kt @@ -0,0 +1,76 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.launcher3.taskbar.bubbles + +import android.content.Context +import android.graphics.Color +import android.graphics.Path +import android.graphics.drawable.ColorDrawable +import android.view.LayoutInflater +import androidx.core.graphics.drawable.toBitmap +import androidx.test.core.app.ApplicationProvider +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import androidx.test.platform.app.InstrumentationRegistry +import com.android.launcher3.R +import com.android.wm.shell.common.bubbles.BubbleInfo +import com.google.common.truth.Truth.assertThat +import org.junit.Test +import org.junit.runner.RunWith + +@SmallTest +@RunWith(AndroidJUnit4::class) +class BubbleViewTest { + + private val context = ApplicationProvider.getApplicationContext() + private lateinit var bubbleView: BubbleView + private lateinit var overflowView: BubbleView + private lateinit var bubble: BubbleBarBubble + + @Test + fun hasUnseenContent_bubble() { + setupBubbleViews() + assertThat(bubbleView.hasUnseenContent()).isTrue() + + bubbleView.markSeen() + assertThat(bubbleView.hasUnseenContent()).isFalse() + } + + @Test + fun hasUnseenContent_overflow() { + setupBubbleViews() + assertThat(overflowView.hasUnseenContent()).isFalse() + } + + private fun setupBubbleViews() { + InstrumentationRegistry.getInstrumentation().runOnMainSync { + val inflater = LayoutInflater.from(context) + + val bitmap = ColorDrawable(Color.WHITE).toBitmap(width = 20, height = 20) + overflowView = inflater.inflate(R.layout.bubblebar_item_view, null, false) as BubbleView + overflowView.setOverflow(BubbleBarOverflow(overflowView), bitmap) + + val bubbleInfo = + BubbleInfo("key", 0, null, null, 0, context.packageName, null, null, false) + bubbleView = inflater.inflate(R.layout.bubblebar_item_view, null, false) as BubbleView + bubble = + BubbleBarBubble(bubbleInfo, bubbleView, bitmap, bitmap, Color.WHITE, Path(), "") + bubbleView.setBubble(bubble) + } + InstrumentationRegistry.getInstrumentation().waitForIdleSync() + } +}