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
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarController.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarController.java
index a3832cd..88ee192 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 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 @@
/** 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 fd989b1..4794dfd 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 @@
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 @@
}
/** 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 @@
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 @@
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 @@
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 @@
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 @@
}
updateBubblesLayoutProperties(mBubbleBarLocation);
updateContentDescription();
+ updateNotificationDotsIfCollapsed();
}
}
@@ -1049,6 +1071,14 @@
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 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 0f9de16..3c74ca4 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 @@
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 @@
});
}
- 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 4c468bb..acb6b4e 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.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 @@
}
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 @@
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.
+ *
+ * <p>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 @@
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 @@
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 @@
}).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 0000000..8bad3b9
--- /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<Context>()
+ 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()
+ }
+}