Interface to represent overflow as bubble

Bug: 138116789
Test: manual - bubble behavior remains the same
Test: atest SystemUITests
Change-Id: I8c17ab9cddba5e3072274d57382d81657e47f7b8
This commit is contained in:
Lyn Han
2020-02-15 19:10:12 -08:00
parent e80f306fdf
commit 3cd75d77e2
5 changed files with 226 additions and 130 deletions

View File

@@ -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) */);
}
}
}

View File

@@ -0,0 +1,107 @@
/*
* 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;
}
}

View File

@@ -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;

View File

@@ -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,12 +399,8 @@ 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 {
mExpandedBubble.getExpandedView().updateView();
}
if (mIsExpanded && mExpandedBubble != null) {
mExpandedBubble.getExpandedView().updateView();
}
});
@@ -420,8 +408,12 @@ public class BubbleStackView extends FrameLayout {
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);
}
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);
}
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,14 +1698,10 @@ 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();
}
return mExpandedBubble.getExpandedView().performBackPressIfNeeded();
}
/** For debugging only */

View File

@@ -0,0 +1,30 @@
/*
* 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();
}