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

Commit a2aa555f authored by Liran Binyamin's avatar Liran Binyamin Committed by Android (Google) Code Review
Browse files

Merge "Update bubble notification dot drawing" into main

parents 45bad725 50130285
Loading
Loading
Loading
Loading
+0 −10
Original line number Diff line number Diff line
@@ -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 {
+36 −5
Original line number Diff line number Diff line
@@ -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()) {
+4 −3
Original line number Diff line number Diff line
@@ -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");
        }
+56 −18
Original line number Diff line number Diff line
@@ -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.
     *
     * <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 @@ 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";
+76 −0
Original line number Diff line number Diff line
/*
 * 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()
    }
}