Merge "Update bubble notification dot drawing" into main
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarController.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarController.java
index 7426dc7..83d4df2 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;
@@ -463,15 +462,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 24b9139..1c33be6 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java
@@ -115,7 +115,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);
@@ -139,8 +139,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()
+ }
+}