diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarController.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarController.java index 7426dc7259..83d4df29c8 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; @@ -463,15 +462,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 24b91395f8..1c33be624a 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 @@ 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); @@ -139,8 +139,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