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

Commit 3cd75d77 authored by Lyn Han's avatar Lyn Han
Browse files

Interface to represent overflow as bubble

Bug: 138116789
Test: manual - bubble behavior remains the same
Test: atest SystemUITests
Change-Id: I8c17ab9cddba5e3072274d57382d81657e47f7b8
parent e80f306f
Loading
Loading
Loading
Loading
+38 −4
Original line number Diff line number Diff line
@@ -38,9 +38,11 @@ import android.graphics.drawable.Drawable;
import android.os.Bundle;
import android.os.UserHandle;
import android.provider.Settings;
import android.service.notification.StatusBarNotification;
import android.util.Log;

import com.android.internal.annotations.VisibleForTesting;
import com.android.systemui.shared.system.SysUiStatsLog;
import com.android.systemui.statusbar.notification.collection.NotificationEntry;

import java.io.FileDescriptor;
@@ -50,7 +52,7 @@ import java.util.Objects;
/**
 * Encapsulates the data and UI elements of a bubble.
 */
class Bubble {
class Bubble implements BubbleViewProvider {
    private static final String TAG = "Bubble";

    private NotificationEntry mEntry;
@@ -148,12 +150,12 @@ class Bubble {
    }

    @Nullable
    BadgedImageView getIconView() {
    public BadgedImageView getIconView() {
        return mIconView;
    }

    @Nullable
    BubbleExpandedView getExpandedView() {
    public BubbleExpandedView getExpandedView() {
        return mExpandedView;
    }

@@ -238,7 +240,7 @@ class Bubble {
     * Note that this contents visibility doesn't affect visibility at {@link android.view.View},
     * and setting {@code false} actually means rendering the expanded view in transparent.
     */
    void setContentVisibility(boolean visibility) {
    public void setContentVisibility(boolean visibility) {
        if (mExpandedView != null) {
            mExpandedView.setContentVisibility(visibility);
        }
@@ -481,4 +483,36 @@ class Bubble {
    public int hashCode() {
        return Objects.hash(mKey);
    }

    public void logUIEvent(int bubbleCount, int action, float normalX, float normalY, int index) {
        if (this.getEntry() == null
                || this.getEntry().getSbn() == null) {
            SysUiStatsLog.write(SysUiStatsLog.BUBBLE_UI_CHANGED,
                    null /* package name */,
                    null /* notification channel */,
                    0 /* notification ID */,
                    0 /* bubble position */,
                    bubbleCount,
                    action,
                    normalX,
                    normalY,
                    false /* unread bubble */,
                    false /* on-going bubble */,
                    false /* isAppForeground (unused) */);
        } else {
            StatusBarNotification notification = this.getEntry().getSbn();
            SysUiStatsLog.write(SysUiStatsLog.BUBBLE_UI_CHANGED,
                    notification.getPackageName(),
                    notification.getNotification().getChannelId(),
                    notification.getId(),
                    index,
                    bubbleCount,
                    action,
                    normalX,
                    normalY,
                    this.showInShade(),
                    this.isOngoing(),
                    false /* isAppForeground (unused) */);
        }
    }
}
+107 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2020 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.systemui.bubbles;

import static android.view.View.GONE;
import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT;

import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Color;
import android.graphics.drawable.AdaptiveIconDrawable;
import android.graphics.drawable.ColorDrawable;
import android.graphics.drawable.InsetDrawable;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.FrameLayout;
import android.widget.ImageView;

import com.android.systemui.R;

/**
 * Class for showing aged out bubbles.
 */
public class BubbleOverflow implements BubbleViewProvider {

    private ImageView mOverflowBtn;
    private BubbleExpandedView mOverflowExpandedView;
    private LayoutInflater mInflater;
    private Context mContext;

    public BubbleOverflow(Context context) {
        mContext = context;
        mInflater = LayoutInflater.from(context);
    }

    public void setUpOverflow(ViewGroup parentViewGroup) {
        mOverflowExpandedView = (BubbleExpandedView) mInflater.inflate(
                R.layout.bubble_expanded_view, parentViewGroup /* root */,
                false /* attachToRoot */);
        mOverflowExpandedView.setOverflow(true);

        mOverflowBtn = (ImageView) mInflater.inflate(R.layout.bubble_overflow_button,
                parentViewGroup /* root */,
                false /* attachToRoot */);

        setOverflowBtnTheme();
        mOverflowBtn.setVisibility(GONE);
    }

    ImageView getBtn() {
        return mOverflowBtn;
    }

    void setBtnVisible(int visible) {
        mOverflowBtn.setVisibility(visible);
    }

    // TODO(b/149146374) Propagate theme change to bubbles in overflow.
    void setOverflowBtnTheme() {
        TypedArray ta = mContext.obtainStyledAttributes(
                new int[]{android.R.attr.colorBackgroundFloating});
        int bgColor = ta.getColor(0, Color.WHITE /* default */);
        ta.recycle();

        InsetDrawable fg = new InsetDrawable(mOverflowBtn.getDrawable(), 28);
        ColorDrawable bg = new ColorDrawable(bgColor);
        AdaptiveIconDrawable adaptiveIcon = new AdaptiveIconDrawable(bg, fg);
        mOverflowBtn.setImageDrawable(adaptiveIcon);
    }


    public BubbleExpandedView getExpandedView() {
        return mOverflowExpandedView;
    }

    public void setContentVisibility(boolean visible) {
        mOverflowExpandedView.setContentVisibility(visible);
    }

    public void logUIEvent(int bubbleCount, int action, float normalX, float normalY,
            int index) {
        // TODO(b/149133814) Log overflow UI events.
    }

    public View getIconView() {
        return mOverflowBtn;
    }

    public String getKey() {
        return BubbleOverflowActivity.KEY;
    }
}
+1 −0
Original line number Diff line number Diff line
@@ -47,6 +47,7 @@ import javax.inject.Inject;
 * Must be public to be accessible to androidx...AppComponentFactory
 */
public class BubbleOverflowActivity extends Activity {
    public static final String KEY = "Overflow";
    private static final String TAG = TAG_WITH_CLASS_NAME ? "BubbleOverflowActivity" : TAG_BUBBLES;

    private LinearLayout mEmptyState;
+50 −126
Original line number Diff line number Diff line
@@ -33,8 +33,6 @@ import android.app.Notification;
import android.content.Context;
import android.content.res.Configuration;
import android.content.res.Resources;
import android.content.res.TypedArray;
import android.graphics.Color;
import android.graphics.ColorMatrix;
import android.graphics.ColorMatrixColorFilter;
import android.graphics.Paint;
@@ -42,13 +40,9 @@ import android.graphics.Point;
import android.graphics.PointF;
import android.graphics.Rect;
import android.graphics.RectF;
import android.graphics.drawable.AdaptiveIconDrawable;
import android.graphics.drawable.ColorDrawable;
import android.graphics.drawable.InsetDrawable;
import android.os.Bundle;
import android.os.VibrationEffect;
import android.os.Vibrator;
import android.service.notification.StatusBarNotification;
import android.util.Log;
import android.view.Choreographer;
import android.view.DisplayCutout;
@@ -63,7 +57,6 @@ import android.view.accessibility.AccessibilityNodeInfo;
import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction;
import android.view.animation.AccelerateDecelerateInterpolator;
import android.widget.FrameLayout;
import android.widget.ImageView;

import androidx.annotation.MainThread;
import androidx.annotation.Nullable;
@@ -199,7 +192,7 @@ public class BubbleStackView extends FrameLayout {
    private int mPointerHeight;
    private int mStatusBarHeight;
    private int mImeOffset;
    private Bubble mExpandedBubble;
    private BubbleViewProvider mExpandedBubble;
    private boolean mIsExpanded;

    /** Whether the stack is currently on the left side of the screen, or animating there. */
@@ -322,8 +315,8 @@ public class BubbleStackView extends FrameLayout {
    private Runnable mAfterMagnet;

    private int mOrientation = Configuration.ORIENTATION_UNDEFINED;
    private BubbleExpandedView mOverflowExpandedView;
    private ImageView mOverflowBtn;

    private BubbleOverflow mBubbleOverflow;

    public BubbleStackView(Context context, BubbleData data,
            @Nullable SurfaceSynchronizer synchronizer) {
@@ -333,7 +326,6 @@ public class BubbleStackView extends FrameLayout {
        mInflater = LayoutInflater.from(context);
        mTouchHandler = new BubbleTouchHandler(this, data, context);
        setOnTouchListener(mTouchHandler);
        mInflater = LayoutInflater.from(context);

        Resources res = getResources();
        mMaxBubbles = res.getInteger(R.integer.bubbles_max_rendered);
@@ -407,21 +399,21 @@ public class BubbleStackView extends FrameLayout {
                        .setStiffness(SpringForce.STIFFNESS_LOW)
                        .setDampingRatio(SpringForce.DAMPING_RATIO_LOW_BOUNCY));
        mExpandedViewYAnim.addEndListener((anim, cancelled, value, velocity) -> {
            if (mIsExpanded) {
                if (mExpandedBubble == null) {
                    mOverflowExpandedView.updateView();
                } else {
            if (mIsExpanded && mExpandedBubble != null) {
                mExpandedBubble.getExpandedView().updateView();
            }
            }
        });

        setClipChildren(false);
        setFocusable(true);
        mBubbleContainer.bringToFront();

        mBubbleOverflow = new BubbleOverflow(mContext);
        if (BubbleExperimentConfig.allowBubbleOverflow(mContext)) {
            setUpOverflow();
            mBubbleOverflow.setUpOverflow(this);
            mBubbleContainer.addView(mBubbleOverflow.getBtn(), 0,
                    new FrameLayout.LayoutParams(WRAP_CONTENT, WRAP_CONTENT));

        }

        setOnApplyWindowInsetsListener((View view, WindowInsets insets) -> {
@@ -432,9 +424,7 @@ public class BubbleStackView extends FrameLayout {
                    // Update the insets after we're done translating otherwise position
                    // calculation for them won't be correct.
                    () -> {
                        if (mExpandedBubble == null) {
                            mOverflowExpandedView.updateInsets(insets);
                        } else {
                        if (mExpandedBubble != null) {
                            mExpandedBubble.getExpandedView().updateInsets(insets);
                        }
                    });
@@ -449,9 +439,7 @@ public class BubbleStackView extends FrameLayout {
                    // Reposition & adjust the height for new orientation
                    if (mIsExpanded) {
                        mExpandedViewContainer.setTranslationY(getExpandedViewY());
                        if (mExpandedBubble == null) {
                            mOverflowExpandedView.updateView();
                        } else {
                        if (mExpandedBubble != null) {
                            mExpandedBubble.getExpandedView().updateView();
                        }
                    }
@@ -516,40 +504,8 @@ public class BubbleStackView extends FrameLayout {
        });
    }

    private void setUpOverflow() {
        mOverflowExpandedView = (BubbleExpandedView) mInflater.inflate(
                R.layout.bubble_expanded_view, this /* root */, false /* attachToRoot */);
        mOverflowExpandedView.setOverflow(true);

        mOverflowBtn = (ImageView) mInflater.inflate(R.layout.bubble_overflow_button,
                this /* root */,
                false /* attachToRoot */);

        mBubbleContainer.addView(mOverflowBtn, 0,
                new FrameLayout.LayoutParams(WRAP_CONTENT, WRAP_CONTENT));

        setOverflowBtnTheme();
        mOverflowBtn.setVisibility(GONE);
    }

    // TODO(b/149146374) Propagate theme change to bubbles in overflow.
    private void setOverflowBtnTheme() {
        TypedArray ta = mContext.obtainStyledAttributes(
                new int[]{android.R.attr.colorBackgroundFloating});
        int bgColor = ta.getColor(0, Color.WHITE /* default */);
        ta.recycle();

        InsetDrawable fg = new InsetDrawable(mOverflowBtn.getDrawable(), 28);
        ColorDrawable bg = new ColorDrawable(bgColor);
        AdaptiveIconDrawable adaptiveIcon = new AdaptiveIconDrawable(bg, fg);
        mOverflowBtn.setImageDrawable(adaptiveIcon);
    }

    void showExpandedViewContents(int displayId) {
        if (mOverflowExpandedView != null
                && mOverflowExpandedView.getVirtualDisplayId() == displayId) {
            mOverflowExpandedView.setContentVisibility(true);
        } else if (mExpandedBubble != null
        if (mExpandedBubble != null
                && mExpandedBubble.getExpandedView().getVirtualDisplayId() == displayId) {
            mExpandedBubble.setContentVisibility(true);
        }
@@ -573,7 +529,7 @@ public class BubbleStackView extends FrameLayout {
    public void onThemeChanged() {
        setUpFlyout();
        if (BubbleExperimentConfig.allowBubbleOverflow(mContext)) {
            setOverflowBtnTheme();
            mBubbleOverflow.setOverflowBtnTheme();
        }
    }

@@ -756,15 +712,22 @@ public class BubbleStackView extends FrameLayout {
    /**
     * The {@link BadgedImageView} that is expanded, null if one does not exist.
     */
    BadgedImageView getExpandedBubbleView() {
    View getExpandedBubbleView() {
        return mExpandedBubble != null ? mExpandedBubble.getIconView() : null;
    }

    /**
     * The {@link Bubble} that is expanded, null if one does not exist.
     */
    @Nullable
    Bubble getExpandedBubble() {
        return mExpandedBubble;
        if (mExpandedBubble == null
                || (BubbleExperimentConfig.allowBubbleOverflow(mContext)
                    && mExpandedBubble.getIconView() == mBubbleOverflow.getBtn()
                    && mExpandedBubble.getKey() == BubbleOverflowActivity.KEY)) {
            return null;
        }
        return (Bubble) mExpandedBubble;
    }

    // via BubbleData.Listener
@@ -818,7 +781,7 @@ public class BubbleStackView extends FrameLayout {
            if (DEBUG_BUBBLE_STACK_VIEW) {
                Log.d(TAG, "Show overflow button.");
            }
            mOverflowBtn.setVisibility(VISIBLE);
            mBubbleOverflow.setBtnVisible(VISIBLE);
            if (apply) {
                mExpandedAnimationController.expandFromStack(() -> {
                    updatePointerPosition();
@@ -828,7 +791,7 @@ public class BubbleStackView extends FrameLayout {
            if (DEBUG_BUBBLE_STACK_VIEW) {
                Log.d(TAG, "Collapsed. Hide overflow button.");
            }
            mOverflowBtn.setVisibility(GONE);
            mBubbleOverflow.setBtnVisible(GONE);
        }
    }

@@ -849,7 +812,7 @@ public class BubbleStackView extends FrameLayout {
    }

    void showOverflow() {
        setSelectedBubble(null);
        setSelectedBubble(mBubbleOverflow);
    }

    /**
@@ -858,14 +821,14 @@ public class BubbleStackView extends FrameLayout {
     * position of any bubble.
     */
    // via BubbleData.Listener
    public void setSelectedBubble(@Nullable Bubble bubbleToSelect) {
    public void setSelectedBubble(@Nullable BubbleViewProvider bubbleToSelect) {
        if (DEBUG_BUBBLE_STACK_VIEW) {
            Log.d(TAG, "setSelectedBubble: " + bubbleToSelect);
        }
        if (mExpandedBubble != null && mExpandedBubble.equals(bubbleToSelect)) {
            return;
        }
        final Bubble previouslySelected = mExpandedBubble;
        final BubbleViewProvider previouslySelected = mExpandedBubble;
        mExpandedBubble = bubbleToSelect;

        if (mIsExpanded) {
@@ -874,14 +837,11 @@ public class BubbleStackView extends FrameLayout {
            // expanded view becomes visible on the screen. See b/126856255
            mExpandedViewContainer.setAlpha(0.0f);
            mSurfaceSynchronizer.syncSurfaceAndRun(() -> {
                if (previouslySelected == null) {
                    mOverflowExpandedView.setContentVisibility(false);
                } else {
                previouslySelected.setContentVisibility(false);
                }
                updateExpandedBubble();
                updatePointerPosition();
                requestUpdate();

                logBubbleEvent(previouslySelected,
                        SysUiStatsLog.BUBBLE_UICHANGED__ACTION__COLLAPSED);
                logBubbleEvent(bubbleToSelect, SysUiStatsLog.BUBBLE_UICHANGED__ACTION__EXPANDED);
@@ -941,8 +901,8 @@ public class BubbleStackView extends FrameLayout {
        if (mIsExpanded) {
            if (isIntersecting(mBubbleContainer, x, y)) {
                if (BubbleExperimentConfig.allowBubbleOverflow(mContext)
                        && isIntersecting(mOverflowBtn, x, y)) {
                    return mOverflowBtn;
                        && isIntersecting(mBubbleOverflow.getBtn(), x, y)) {
                    return mBubbleOverflow.getBtn();
                }
                // Could be tapping or dragging a bubble while expanded
                for (int i = 0; i < getBubbleCount(); i++) {
@@ -1030,7 +990,7 @@ public class BubbleStackView extends FrameLayout {

    private void animateCollapse() {
        mIsExpanded = false;
        final Bubble previouslySelected = mExpandedBubble;
        final BubbleViewProvider previouslySelected = mExpandedBubble;
        beforeExpandedViewAnimation();

        if (DEBUG_BUBBLE_STACK_VIEW) {
@@ -1046,11 +1006,7 @@ public class BubbleStackView extends FrameLayout {
                () -> {
                    mBubbleContainer.setActiveController(mStackAnimationController);
                    afterExpandedViewAnimation();
                    if (previouslySelected == null) {
                        mOverflowExpandedView.setContentVisibility(false);
                    } else {
                    previouslySelected.setContentVisibility(false);
                    }
                });

        mExpandedViewXAnim.animateToFinalPosition(getCollapsedX());
@@ -1093,7 +1049,7 @@ public class BubbleStackView extends FrameLayout {
                mExpandedAnimateYDistance);
    }

    private void notifyExpansionChanged(Bubble bubble, boolean expanded) {
    private void notifyExpansionChanged(BubbleViewProvider bubble, boolean expanded) {
        if (mExpandListener != null && bubble != null) {
            mExpandListener.onBubbleExpandChanged(expanded, bubble.getKey());
        }
@@ -1613,11 +1569,8 @@ public class BubbleStackView extends FrameLayout {
            Log.d(TAG, "updateExpandedBubble()");
        }
        mExpandedViewContainer.removeAllViews();
        if (mIsExpanded) {
            BubbleExpandedView bev = mOverflowExpandedView;
            if (mExpandedBubble != null) {
                bev = mExpandedBubble.getExpandedView();
            }
        if (mIsExpanded && mExpandedBubble != null) {
            BubbleExpandedView bev = mExpandedBubble.getExpandedView();
            mExpandedViewContainer.addView(bev);
            bev.populateExpandedView();
            mExpandedViewContainer.setVisibility(VISIBLE);
@@ -1636,9 +1589,7 @@ public class BubbleStackView extends FrameLayout {
            if (!mExpandedViewYAnim.isRunning()) {
                // We're not animating so set the value
                mExpandedViewContainer.setTranslationY(y);
                if (mExpandedBubble == null) {
                    mOverflowExpandedView.updateView();
                } else {
                if (mExpandedBubble != null) {
                    mExpandedBubble.getExpandedView().updateView();
                }
            } else {
@@ -1693,15 +1644,16 @@ public class BubbleStackView extends FrameLayout {
    /**
     * Finds the bubble index within the stack.
     *
     * @param bubble the bubble to look up.
     * @param provider the bubble view provider with the bubble to look up.
     * @return the index of the bubble view within the bubble stack. The range of the position
     * is between 0 and the bubble count minus 1.
     */
    int getBubbleIndex(@Nullable Bubble bubble) {
        if (bubble == null) {
    int getBubbleIndex(@Nullable BubbleViewProvider provider) {
        if (provider == null || provider.getKey() == BubbleOverflowActivity.KEY) {
            return 0;
        }
        return mBubbleContainer.indexOfChild(bubble.getIconView());
        Bubble b = (Bubble) provider;
        return mBubbleContainer.indexOfChild(b.getIconView());
    }

    /**
@@ -1733,36 +1685,12 @@ public class BubbleStackView extends FrameLayout {
     *               the user interaction is not specific to one bubble.
     * @param action the user interaction enum.
     */
    private void logBubbleEvent(@Nullable Bubble bubble, int action) {
        if (bubble == null || bubble.getEntry() == null
                || bubble.getEntry().getSbn() == null) {
            SysUiStatsLog.write(SysUiStatsLog.BUBBLE_UI_CHANGED,
                    null /* package name */,
                    null /* notification channel */,
                    0 /* notification ID */,
                    0 /* bubble position */,
                    getBubbleCount(),
                    action,
                    getNormalizedXPosition(),
                    getNormalizedYPosition(),
                    false /* unread bubble */,
                    false /* on-going bubble */,
                    false /* isAppForeground (unused) */);
        } else {
            StatusBarNotification notification = bubble.getEntry().getSbn();
            SysUiStatsLog.write(SysUiStatsLog.BUBBLE_UI_CHANGED,
                    notification.getPackageName(),
                    notification.getNotification().getChannelId(),
                    notification.getId(),
                    getBubbleIndex(bubble),
                    getBubbleCount(),
                    action,
                    getNormalizedXPosition(),
                    getNormalizedYPosition(),
                    bubble.showInShade(),
                    bubble.isOngoing(),
                    false /* isAppForeground (unused) */);
    private void logBubbleEvent(@Nullable BubbleViewProvider bubble, int action) {
        if (bubble == null) {
            return;
        }
        bubble.logUIEvent(getBubbleCount(), action, getNormalizedXPosition(),
                getNormalizedYPosition(), getBubbleIndex(bubble));
    }

    /**
@@ -1770,15 +1698,11 @@ public class BubbleStackView extends FrameLayout {
     * a back key down/up event pair is forwarded to the bubble Activity.
     */
    boolean performBackPressIfNeeded() {
        if (!isExpanded()) {
        if (!isExpanded() || mExpandedBubble == null) {
            return false;
        }
        if (mExpandedBubble == null) {
            return mOverflowExpandedView.performBackPressIfNeeded();
        } else {
        return mExpandedBubble.getExpandedView().performBackPressIfNeeded();
    }
    }

    /** For debugging only */
    List<Bubble> getBubblesOnScreen() {
+30 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2020 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.systemui.bubbles;

import android.view.View;

/**
 * Interface to represent actual Bubbles and UI elements that act like bubbles, like BubbleOverflow.
 */
interface BubbleViewProvider {
    BubbleExpandedView getExpandedView();
    void setContentVisibility(boolean visible);
    View getIconView();
    void logUIEvent(int bubbleCount, int action, float normalX, float normalY, int index);
    String getKey();
}