Merge "Add the Manage menu!" into rvc-dev

This commit is contained in:
Josh Tsuji
2020-04-21 19:43:11 +00:00
committed by Android (Google) Code Review
11 changed files with 389 additions and 107 deletions

View File

@@ -0,0 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ 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
-->
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_pressed="true">
<ripple android:color="#99999999" />
</item>
</selector>

View File

@@ -0,0 +1,99 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ 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
-->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/rounded_bg_full"
android:elevation="@dimen/bubble_manage_menu_elevation"
android:orientation="vertical">
<LinearLayout
android:id="@+id/bubble_manage_menu_dismiss_container"
android:background="@drawable/bubble_manage_menu_row"
android:layout_width="match_parent"
android:layout_height="48dp"
android:gravity="center_vertical"
android:paddingStart="16dp"
android:paddingEnd="16dp"
android:orientation="horizontal">
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/ic_remove_no_shadow"
android:tint="@color/global_actions_text"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:textAppearance="@*android:style/TextAppearance.DeviceDefault"
android:text="@string/bubble_dismiss_text" />
</LinearLayout>
<LinearLayout
android:id="@+id/bubble_manage_menu_dont_bubble_container"
android:background="@drawable/bubble_manage_menu_row"
android:layout_width="match_parent"
android:layout_height="48dp"
android:gravity="center_vertical"
android:paddingStart="16dp"
android:paddingEnd="16dp"
android:orientation="horizontal">
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/ic_stop_bubble"
android:tint="@color/global_actions_text"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:textAppearance="@*android:style/TextAppearance.DeviceDefault"
android:text="@string/bubbles_dont_bubble_conversation" />
</LinearLayout>
<LinearLayout
android:id="@+id/bubble_manage_menu_settings_container"
android:background="@drawable/bubble_manage_menu_row"
android:layout_width="match_parent"
android:layout_height="48dp"
android:gravity="center_vertical"
android:paddingStart="16dp"
android:paddingEnd="16dp"
android:orientation="horizontal">
<ImageView
android:id="@+id/bubble_manage_menu_settings_icon"
android:layout_width="24dp"
android:layout_height="24dp"
android:src="@drawable/ic_remove_no_shadow"/>
<TextView
android:id="@+id/bubble_manage_menu_settings_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:textAppearance="@*android:style/TextAppearance.DeviceDefault" />
</LinearLayout>
</LinearLayout>

View File

@@ -1200,6 +1200,7 @@
snap to the dismiss target. --> snap to the dismiss target. -->
<dimen name="bubble_dismiss_target_padding_x">40dp</dimen> <dimen name="bubble_dismiss_target_padding_x">40dp</dimen>
<dimen name="bubble_dismiss_target_padding_y">20dp</dimen> <dimen name="bubble_dismiss_target_padding_y">20dp</dimen>
<dimen name="bubble_manage_menu_elevation">4dp</dimen>
<dimen name="dismiss_circle_size">52dp</dimen> <dimen name="dismiss_circle_size">52dp</dimen>
<dimen name="dismiss_target_x_size">24dp</dimen> <dimen name="dismiss_target_x_size">24dp</dimen>

View File

@@ -2629,6 +2629,8 @@
<string name="bubbles_user_education_manage">Tap Manage to turn off bubbles from this app</string> <string name="bubbles_user_education_manage">Tap Manage to turn off bubbles from this app</string>
<!-- Button text for dismissing the bubble "manage" button tool tip [CHAR LIMIT=20]--> <!-- Button text for dismissing the bubble "manage" button tool tip [CHAR LIMIT=20]-->
<string name="bubbles_user_education_got_it">Got it</string> <string name="bubbles_user_education_got_it">Got it</string>
<!-- Label for the button that takes the user to the notification settings for the given app. -->
<string name="bubbles_app_settings"><xliff:g id="notification_title" example="Android Messages">%1$s</xliff:g> settings</string>
<!-- Notification content text when the system navigation mode changes as a result of changing the default launcher [CHAR LIMIT=NONE] --> <!-- Notification content text when the system navigation mode changes as a result of changing the default launcher [CHAR LIMIT=NONE] -->
<string name="notification_content_system_nav_changed">System navigation updated. To make changes, go to Settings.</string> <string name="notification_content_system_nav_changed">System navigation updated. To make changes, go to Settings.</string>

View File

@@ -90,6 +90,7 @@ class Bubble implements BubbleViewProvider {
} }
private FlyoutMessage mFlyoutMessage; private FlyoutMessage mFlyoutMessage;
private Drawable mBadgedAppIcon;
private Bitmap mBadgedImage; private Bitmap mBadgedImage;
private int mDotColor; private int mDotColor;
private Path mDotPath; private Path mDotPath;
@@ -133,6 +134,10 @@ class Bubble implements BubbleViewProvider {
return mBadgedImage; return mBadgedImage;
} }
public Drawable getBadgedAppIcon() {
return mBadgedAppIcon;
}
@Override @Override
public int getDotColor() { public int getDotColor() {
return mDotColor; return mDotColor;
@@ -239,6 +244,7 @@ class Bubble implements BubbleViewProvider {
mAppName = info.appName; mAppName = info.appName;
mFlyoutMessage = info.flyoutMessage; mFlyoutMessage = info.flyoutMessage;
mBadgedAppIcon = info.badgedAppIcon;
mBadgedImage = info.badgedBubbleImage; mBadgedImage = info.badgedBubbleImage;
mDotColor = info.dotColor; mDotColor = info.dotColor;
mDotPath = info.dotPath; mDotPath = info.dotPath;

View File

@@ -605,6 +605,9 @@ public class BubbleController implements ConfigurationController.ConfigurationLi
if (mExpandListener != null) { if (mExpandListener != null) {
mStackView.setExpandListener(mExpandListener); mStackView.setExpandListener(mExpandListener);
} }
mStackView.setUnbubbleConversationCallback(notificationEntry ->
onUserChangedBubble(notificationEntry, false /* shouldBubble */));
} }
} }

View File

@@ -43,7 +43,6 @@ import android.graphics.Point;
import android.graphics.Rect; import android.graphics.Rect;
import android.graphics.drawable.ShapeDrawable; import android.graphics.drawable.ShapeDrawable;
import android.os.RemoteException; import android.os.RemoteException;
import android.service.notification.StatusBarNotification;
import android.util.AttributeSet; import android.util.AttributeSet;
import android.util.Log; import android.util.Log;
import android.view.View; import android.view.View;
@@ -55,14 +54,13 @@ import com.android.internal.policy.ScreenDecorationsUtils;
import com.android.systemui.Dependency; import com.android.systemui.Dependency;
import com.android.systemui.R; import com.android.systemui.R;
import com.android.systemui.recents.TriangleShape; import com.android.systemui.recents.TriangleShape;
import com.android.systemui.shared.system.SysUiStatsLog;
import com.android.systemui.statusbar.AlphaOptimizedButton; import com.android.systemui.statusbar.AlphaOptimizedButton;
import com.android.systemui.statusbar.notification.collection.NotificationEntry; import com.android.systemui.statusbar.notification.collection.NotificationEntry;
/** /**
* Container for the expanded bubble view, handles rendering the caret and settings icon. * Container for the expanded bubble view, handles rendering the caret and settings icon.
*/ */
public class BubbleExpandedView extends LinearLayout implements View.OnClickListener { public class BubbleExpandedView extends LinearLayout {
private static final String TAG = TAG_WITH_CLASS_NAME ? "BubbleExpandedView" : TAG_BUBBLES; private static final String TAG = TAG_WITH_CLASS_NAME ? "BubbleExpandedView" : TAG_BUBBLES;
private enum ActivityViewStatus { private enum ActivityViewStatus {
@@ -100,9 +98,6 @@ public class BubbleExpandedView extends LinearLayout implements View.OnClickList
private int mPointerWidth; private int mPointerWidth;
private int mPointerHeight; private int mPointerHeight;
private ShapeDrawable mPointerDrawable; private ShapeDrawable mPointerDrawable;
private Rect mTempRect = new Rect();
private int[] mTempLoc = new int[2];
private int mExpandedViewTouchSlop;
@Nullable private Bubble mBubble; @Nullable private Bubble mBubble;
@@ -224,7 +219,6 @@ public class BubbleExpandedView extends LinearLayout implements View.OnClickList
mMinHeight = res.getDimensionPixelSize(R.dimen.bubble_expanded_default_height); mMinHeight = res.getDimensionPixelSize(R.dimen.bubble_expanded_default_height);
mOverflowHeight = res.getDimensionPixelSize(R.dimen.bubble_overflow_height); mOverflowHeight = res.getDimensionPixelSize(R.dimen.bubble_overflow_height);
mPointerMargin = res.getDimensionPixelSize(R.dimen.bubble_pointer_margin); mPointerMargin = res.getDimensionPixelSize(R.dimen.bubble_pointer_margin);
mExpandedViewTouchSlop = res.getDimensionPixelSize(R.dimen.bubble_expanded_view_slop);
} }
@Override @Override
@@ -239,7 +233,6 @@ public class BubbleExpandedView extends LinearLayout implements View.OnClickList
mPointerWidth = res.getDimensionPixelSize(R.dimen.bubble_pointer_width); mPointerWidth = res.getDimensionPixelSize(R.dimen.bubble_pointer_width);
mPointerHeight = res.getDimensionPixelSize(R.dimen.bubble_pointer_height); mPointerHeight = res.getDimensionPixelSize(R.dimen.bubble_pointer_height);
mPointerDrawable = new ShapeDrawable(TriangleShape.create( mPointerDrawable = new ShapeDrawable(TriangleShape.create(
mPointerWidth, mPointerHeight, true /* pointUp */)); mPointerWidth, mPointerHeight, true /* pointUp */));
mPointerView.setBackground(mPointerDrawable); mPointerView.setBackground(mPointerDrawable);
@@ -248,7 +241,6 @@ public class BubbleExpandedView extends LinearLayout implements View.OnClickList
mSettingsIconHeight = getContext().getResources().getDimensionPixelSize( mSettingsIconHeight = getContext().getResources().getDimensionPixelSize(
R.dimen.bubble_manage_button_height); R.dimen.bubble_manage_button_height);
mSettingsIcon = findViewById(R.id.settings_button); mSettingsIcon = findViewById(R.id.settings_button);
mSettingsIcon.setOnClickListener(this);
mActivityView = new ActivityView(mContext, null /* attrs */, 0 /* defStyle */, mActivityView = new ActivityView(mContext, null /* attrs */, 0 /* defStyle */,
true /* singleTaskInstance */); true /* singleTaskInstance */);
@@ -289,6 +281,19 @@ public class BubbleExpandedView extends LinearLayout implements View.OnClickList
return mBubble != null ? mBubble.getEntry() : null; return mBubble != null ? mBubble.getEntry() : null;
} }
void setManageClickListener(OnClickListener manageClickListener) {
findViewById(R.id.settings_button).setOnClickListener(manageClickListener);
}
/**
* Updates the ActivityView's obscured touchable region. This calls onLocationChanged, which
* results in a call to {@link BubbleStackView#subtractObscuredTouchableRegion}. This is useful
* if a view has been added or removed from on top of the ActivityView, such as the manage menu.
*/
void updateObscuredTouchableRegion() {
mActivityView.onLocationChanged();
}
void applyThemeAttrs() { void applyThemeAttrs() {
final TypedArray ta = mContext.obtainStyledAttributes( final TypedArray ta = mContext.obtainStyledAttributes(
new int[] { new int[] {
@@ -472,51 +477,6 @@ public class BubbleExpandedView extends LinearLayout implements View.OnClickList
- mPointerMargin - bottomInset; - mPointerMargin - bottomInset;
} }
/**
* Whether the provided x, y values (in raw coordinates) are in a touchable area of the
* expanded view.
*
* The touchable areas are the ActivityView (plus some slop around it) and the manage button.
*/
boolean intersectingTouchableContent(int rawX, int rawY) {
mTempRect.setEmpty();
if (mActivityView != null) {
mTempLoc = mActivityView.getLocationOnScreen();
mTempRect.set(mTempLoc[0] - mExpandedViewTouchSlop,
mTempLoc[1] - mExpandedViewTouchSlop,
mTempLoc[0] + mActivityView.getWidth() + mExpandedViewTouchSlop,
mTempLoc[1] + mActivityView.getHeight() + mExpandedViewTouchSlop);
}
if (mTempRect.contains(rawX, rawY)) {
return true;
}
mTempLoc = mSettingsIcon.getLocationOnScreen();
mTempRect.set(mTempLoc[0],
mTempLoc[1],
mTempLoc[0] + mSettingsIcon.getWidth(),
mTempLoc[1] + mSettingsIcon.getHeight());
if (mTempRect.contains(rawX, rawY)) {
return true;
}
return false;
}
@Override
public void onClick(View view) {
if (mBubble == null) {
return;
}
int id = view.getId();
if (id == R.id.settings_button) {
Intent intent = mBubble.getSettingsIntent();
mStackView.collapseStack(() -> {
mContext.startActivityAsUser(intent, mBubble.getEntry().getSbn().getUser());
logBubbleClickEvent(mBubble,
SysUiStatsLog.BUBBLE_UICHANGED__ACTION__HEADER_GO_TO_SETTINGS);
});
}
}
/** /**
* Update appearance of the expanded view being displayed. * Update appearance of the expanded view being displayed.
*/ */
@@ -547,10 +507,8 @@ public class BubbleExpandedView extends LinearLayout implements View.OnClickList
* Position of the manage button displayed in the expanded view. Used for placing user * Position of the manage button displayed in the expanded view. Used for placing user
* education about the manage button. * education about the manage button.
*/ */
public Rect getManageButtonLocationOnScreen() { public void getManageButtonBoundsOnScreen(Rect rect) {
mTempLoc = mSettingsIcon.getLocationOnScreen(); mSettingsIcon.getBoundsOnScreen(rect);
return new Rect(mTempLoc[0], mTempLoc[1], mTempLoc[0] + mSettingsIcon.getWidth(),
mTempLoc[1] + mSettingsIcon.getHeight());
} }
/** /**
@@ -611,26 +569,4 @@ public class BubbleExpandedView extends LinearLayout implements View.OnClickList
} }
return INVALID_DISPLAY; return INVALID_DISPLAY;
} }
/**
* Logs bubble UI click event.
*
* @param bubble the bubble notification entry that user is interacting with.
* @param action the user interaction enum.
*/
private void logBubbleClickEvent(Bubble bubble, int action) {
StatusBarNotification notification = bubble.getEntry().getSbn();
SysUiStatsLog.write(SysUiStatsLog.BUBBLE_UI_CHANGED,
notification.getPackageName(),
notification.getNotification().getChannelId(),
notification.getId(),
mStackView.getBubbleIndex(mStackView.getExpandedBubble()),
mStackView.getBubbleCount(),
action,
mStackView.getNormalizedXPosition(),
mStackView.getNormalizedYPosition(),
bubble.showInShade(),
bubble.isOngoing(),
false /* isAppForeground (unused) */);
}
} }

View File

@@ -33,12 +33,14 @@ import android.animation.ValueAnimator;
import android.annotation.SuppressLint; import android.annotation.SuppressLint;
import android.app.Notification; import android.app.Notification;
import android.content.Context; import android.content.Context;
import android.content.Intent;
import android.content.res.Configuration; import android.content.res.Configuration;
import android.content.res.Resources; import android.content.res.Resources;
import android.content.res.TypedArray; import android.content.res.TypedArray;
import android.graphics.Color; import android.graphics.Color;
import android.graphics.ColorMatrix; import android.graphics.ColorMatrix;
import android.graphics.ColorMatrixColorFilter; import android.graphics.ColorMatrixColorFilter;
import android.graphics.Outline;
import android.graphics.Paint; import android.graphics.Paint;
import android.graphics.Point; import android.graphics.Point;
import android.graphics.PointF; import android.graphics.PointF;
@@ -47,6 +49,7 @@ import android.graphics.RectF;
import android.graphics.Region; import android.graphics.Region;
import android.os.Bundle; import android.os.Bundle;
import android.os.Vibrator; import android.os.Vibrator;
import android.service.notification.StatusBarNotification;
import android.util.Log; import android.util.Log;
import android.view.Choreographer; import android.view.Choreographer;
import android.view.DisplayCutout; import android.view.DisplayCutout;
@@ -55,6 +58,7 @@ import android.view.LayoutInflater;
import android.view.MotionEvent; import android.view.MotionEvent;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import android.view.ViewOutlineProvider;
import android.view.ViewTreeObserver; import android.view.ViewTreeObserver;
import android.view.WindowInsets; import android.view.WindowInsets;
import android.view.WindowManager; import android.view.WindowManager;
@@ -62,6 +66,7 @@ import android.view.accessibility.AccessibilityNodeInfo;
import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction; import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction;
import android.view.animation.AccelerateDecelerateInterpolator; import android.view.animation.AccelerateDecelerateInterpolator;
import android.widget.FrameLayout; import android.widget.FrameLayout;
import android.widget.ImageView;
import android.widget.TextView; import android.widget.TextView;
import androidx.annotation.MainThread; import androidx.annotation.MainThread;
@@ -83,6 +88,7 @@ import com.android.systemui.bubbles.animation.StackAnimationController;
import com.android.systemui.model.SysUiState; import com.android.systemui.model.SysUiState;
import com.android.systemui.shared.system.QuickStepContract; import com.android.systemui.shared.system.QuickStepContract;
import com.android.systemui.shared.system.SysUiStatsLog; import com.android.systemui.shared.system.SysUiStatsLog;
import com.android.systemui.statusbar.notification.collection.NotificationEntry;
import com.android.systemui.statusbar.phone.NotificationShadeWindowController; import com.android.systemui.statusbar.phone.NotificationShadeWindowController;
import com.android.systemui.util.DismissCircleView; import com.android.systemui.util.DismissCircleView;
import com.android.systemui.util.FloatingContentCoordinator; import com.android.systemui.util.FloatingContentCoordinator;
@@ -97,6 +103,7 @@ import java.math.RoundingMode;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.function.Consumer;
/** /**
* Renders bubbles in a stack and handles animating expanded and collapsed states. * Renders bubbles in a stack and handles animating expanded and collapsed states.
@@ -224,7 +231,7 @@ public class BubbleStackView extends FrameLayout {
private int mPointerHeight; private int mPointerHeight;
private int mStatusBarHeight; private int mStatusBarHeight;
private int mImeOffset; private int mImeOffset;
private BubbleViewProvider mExpandedBubble; @Nullable private BubbleViewProvider mExpandedBubble;
private boolean mIsExpanded; private boolean mIsExpanded;
/** Whether the stack is currently on the left side of the screen, or animating there. */ /** Whether the stack is currently on the left side of the screen, or animating there. */
@@ -244,6 +251,10 @@ public class BubbleStackView extends FrameLayout {
} }
private BubbleController.BubbleExpandListener mExpandListener; private BubbleController.BubbleExpandListener mExpandListener;
/** Callback to run when we want to unbubble the given notification's conversation. */
private Consumer<NotificationEntry> mUnbubbleConversationCallback;
private SysUiState mSysUiState; private SysUiState mSysUiState;
private boolean mViewUpdatedRequested = false; private boolean mViewUpdatedRequested = false;
@@ -255,9 +266,7 @@ public class BubbleStackView extends FrameLayout {
private LayoutInflater mInflater; private LayoutInflater mInflater;
// Used for determining view / touch intersection private Rect mTempRect = new Rect();
int[] mTempLoc = new int[2];
RectF mTempRect = new RectF();
private final List<Rect> mSystemGestureExclusionRects = Collections.singletonList(new Rect()); private final List<Rect> mSystemGestureExclusionRects = Collections.singletonList(new Rect());
@@ -471,6 +480,11 @@ public class BubbleStackView extends FrameLayout {
return true; return true;
} }
// If the manage menu is visible, just hide it.
if (mShowingManage) {
showManageMenu(false /* show */);
}
if (mBubbleData.isExpanded()) { if (mBubbleData.isExpanded()) {
maybeShowManageEducation(false /* show */); maybeShowManageEducation(false /* show */);
@@ -627,6 +641,13 @@ public class BubbleStackView extends FrameLayout {
private BubbleManageEducationView mManageEducationView; private BubbleManageEducationView mManageEducationView;
private boolean mAnimatingManageEducationAway; private boolean mAnimatingManageEducationAway;
private ViewGroup mManageMenu;
private ImageView mManageSettingsIcon;
private TextView mManageSettingsText;
private boolean mShowingManage = false;
private PhysicsAnimator.SpringConfig mManageSpringConfig = new PhysicsAnimator.SpringConfig(
SpringForce.STIFFNESS_MEDIUM, SpringForce.DAMPING_RATIO_LOW_BOUNCY);
@SuppressLint("ClickableViewAccessibility") @SuppressLint("ClickableViewAccessibility")
public BubbleStackView(Context context, BubbleData data, public BubbleStackView(Context context, BubbleData data,
@Nullable SurfaceSynchronizer synchronizer, @Nullable SurfaceSynchronizer synchronizer,
@@ -689,6 +710,8 @@ public class BubbleStackView extends FrameLayout {
mExpandedViewContainer.setClipChildren(false); mExpandedViewContainer.setClipChildren(false);
addView(mExpandedViewContainer); addView(mExpandedViewContainer);
setUpManageMenu();
setUpFlyout(); setUpFlyout();
mFlyoutTransitionSpring.setSpring(new SpringForce() mFlyoutTransitionSpring.setSpring(new SpringForce()
.setStiffness(SpringForce.STIFFNESS_LOW) .setStiffness(SpringForce.STIFFNESS_LOW)
@@ -838,7 +861,9 @@ public class BubbleStackView extends FrameLayout {
// ActivityViews, etc.) were touched. Collapse the stack if it's expanded. // ActivityViews, etc.) were touched. Collapse the stack if it's expanded.
setOnTouchListener((view, ev) -> { setOnTouchListener((view, ev) -> {
if (ev.getAction() == MotionEvent.ACTION_DOWN) { if (ev.getAction() == MotionEvent.ACTION_DOWN) {
if (mBubbleData.isExpanded()) { if (mShowingManage) {
showManageMenu(false /* show */);
} else if (mBubbleData.isExpanded()) {
mBubbleData.setExpanded(false); mBubbleData.setExpanded(false);
} }
} }
@@ -847,6 +872,66 @@ public class BubbleStackView extends FrameLayout {
}); });
} }
private void setUpManageMenu() {
if (mManageMenu != null) {
removeView(mManageMenu);
}
mManageMenu = (ViewGroup) LayoutInflater.from(getContext()).inflate(
R.layout.bubble_manage_menu, this, false);
mManageMenu.setVisibility(View.INVISIBLE);
PhysicsAnimator.getInstance(mManageMenu).setDefaultSpringConfig(mManageSpringConfig);
final TypedArray ta = mContext.obtainStyledAttributes(
new int[] {android.R.attr.dialogCornerRadius});
final int menuCornerRadius = ta.getDimensionPixelSize(0, 0);
ta.recycle();
mManageMenu.setOutlineProvider(new ViewOutlineProvider() {
@Override
public void getOutline(View view, Outline outline) {
outline.setRoundRect(0, 0, view.getWidth(), view.getHeight(), menuCornerRadius);
}
});
mManageMenu.setClipToOutline(true);
mManageMenu.findViewById(R.id.bubble_manage_menu_dismiss_container).setOnClickListener(
view -> {
showManageMenu(false /* show */);
dismissBubbleIfExists(mBubbleData.getSelectedBubble());
});
mManageMenu.findViewById(R.id.bubble_manage_menu_dont_bubble_container).setOnClickListener(
view -> {
showManageMenu(false /* show */);
final Bubble bubble = mBubbleData.getSelectedBubble();
if (bubble != null && mBubbleData.hasBubbleWithKey(bubble.getKey())) {
mUnbubbleConversationCallback.accept(bubble.getEntry());
}
});
mManageMenu.findViewById(R.id.bubble_manage_menu_settings_container).setOnClickListener(
view -> {
showManageMenu(false /* show */);
final Bubble bubble = mBubbleData.getSelectedBubble();
if (bubble != null && mBubbleData.hasBubbleWithKey(bubble.getKey())) {
final Intent intent = bubble.getSettingsIntent();
collapseStack(() -> {
mContext.startActivityAsUser(
intent, bubble.getEntry().getSbn().getUser());
logBubbleClickEvent(
bubble,
SysUiStatsLog.BUBBLE_UICHANGED__ACTION__HEADER_GO_TO_SETTINGS);
});
}
});
mManageSettingsIcon = mManageMenu.findViewById(R.id.bubble_manage_menu_settings_icon);
mManageSettingsText = mManageMenu.findViewById(R.id.bubble_manage_menu_settings_name);
addView(mManageMenu);
}
private void setUpUserEducation() { private void setUpUserEducation() {
if (mUserEducationView != null) { if (mUserEducationView != null) {
removeView(mUserEducationView); removeView(mUserEducationView);
@@ -934,6 +1019,7 @@ public class BubbleStackView extends FrameLayout {
setUpFlyout(); setUpFlyout();
setUpOverflow(); setUpOverflow();
setUpUserEducation(); setUpUserEducation();
setUpManageMenu();
} }
/** Respond to the phone being rotated by repositioning the stack and hiding any flyouts. */ /** Respond to the phone being rotated by repositioning the stack and hiding any flyouts. */
@@ -960,6 +1046,9 @@ public class BubbleStackView extends FrameLayout {
Math.max(0f, Math.min(1f, mVerticalPosPercentBeforeRotation)); Math.max(0f, Math.min(1f, mVerticalPosPercentBeforeRotation));
addOnLayoutChangeListener(mOrientationChangedListener); addOnLayoutChangeListener(mOrientationChangedListener);
hideFlyoutImmediate(); hideFlyoutImmediate();
mManageMenu.setVisibility(View.INVISIBLE);
mShowingManage = false;
} }
@Override @Override
@@ -1100,6 +1189,12 @@ public class BubbleStackView extends FrameLayout {
mExpandListener = listener; mExpandListener = listener;
} }
/** Sets the function to call to un-bubble the given conversation. */
public void setUnbubbleConversationCallback(
Consumer<NotificationEntry> unbubbleConversationCallback) {
mUnbubbleConversationCallback = unbubbleConversationCallback;
}
/** /**
* Whether the stack of bubbles is expanded or not. * Whether the stack of bubbles is expanded or not.
*/ */
@@ -1361,15 +1456,14 @@ public class BubbleStackView extends FrameLayout {
mManageEducationView.setAlpha(0); mManageEducationView.setAlpha(0);
mManageEducationView.setVisibility(VISIBLE); mManageEducationView.setVisibility(VISIBLE);
mManageEducationView.post(() -> { mManageEducationView.post(() -> {
final Rect position = mExpandedBubble.getExpandedView().getManageButtonBoundsOnScreen(mTempRect);
mExpandedBubble.getExpandedView().getManageButtonLocationOnScreen();
final int viewHeight = mManageEducationView.getManageViewHeight(); final int viewHeight = mManageEducationView.getManageViewHeight();
final int inset = getResources().getDimensionPixelSize( final int inset = getResources().getDimensionPixelSize(
R.dimen.bubbles_manage_education_top_inset); R.dimen.bubbles_manage_education_top_inset);
mManageEducationView.bringToFront(); mManageEducationView.bringToFront();
mManageEducationView.setManageViewPosition(position.left, mManageEducationView.setManageViewPosition(mTempRect.left,
position.top - viewHeight + inset); mTempRect.top - viewHeight + inset);
mManageEducationView.setPointerPosition(position.centerX() - position.left); mManageEducationView.setPointerPosition(mTempRect.centerX() - mTempRect.left);
mManageEducationView.animate() mManageEducationView.animate()
.setDuration(ANIMATE_STACK_USER_EDUCATION_DURATION) .setDuration(ANIMATE_STACK_USER_EDUCATION_DURATION)
.setInterpolator(FAST_OUT_SLOW_IN).alpha(1); .setInterpolator(FAST_OUT_SLOW_IN).alpha(1);
@@ -1443,6 +1537,9 @@ public class BubbleStackView extends FrameLayout {
} }
private void animateCollapse() { private void animateCollapse() {
// Hide the menu if it's visible.
showManageMenu(false);
mIsExpanded = false; mIsExpanded = false;
final BubbleViewProvider previouslySelected = mExpandedBubble; final BubbleViewProvider previouslySelected = mExpandedBubble;
beforeExpandedViewAnimation(); beforeExpandedViewAnimation();
@@ -1570,9 +1667,9 @@ public class BubbleStackView extends FrameLayout {
*/ */
@Override @Override
public void subtractObscuredTouchableRegion(Region touchableRegion, View view) { public void subtractObscuredTouchableRegion(Region touchableRegion, View view) {
// If the notification shade is expanded, we shouldn't let the ActivityView steal any touch // If the notification shade is expanded, or the manage menu is open, we shouldn't let the
// events from any location. // ActivityView steal any touch events from any location.
if (mNotificationShadeWindowController.getPanelExpanded()) { if (mNotificationShadeWindowController.getPanelExpanded() || mShowingManage) {
touchableRegion.setEmpty(); touchableRegion.setEmpty();
} }
} }
@@ -1658,17 +1755,20 @@ public class BubbleStackView extends FrameLayout {
private void dismissMagnetizedObject() { private void dismissMagnetizedObject() {
if (mIsExpanded) { if (mIsExpanded) {
final View draggedOutBubbleView = (View) mMagnetizedObject.getUnderlyingObject(); final View draggedOutBubbleView = (View) mMagnetizedObject.getUnderlyingObject();
final Bubble draggedOutBubble = mBubbleData.getBubbleWithView(draggedOutBubbleView); dismissBubbleIfExists(mBubbleData.getBubbleWithView(draggedOutBubbleView));
if (mBubbleData.hasBubbleWithKey(draggedOutBubble.getKey())) {
mBubbleData.notificationEntryRemoved(
draggedOutBubble.getEntry(), BubbleController.DISMISS_USER_GESTURE);
}
} else { } else {
mBubbleData.dismissAll(BubbleController.DISMISS_USER_GESTURE); mBubbleData.dismissAll(BubbleController.DISMISS_USER_GESTURE);
} }
} }
private void dismissBubbleIfExists(@Nullable Bubble bubble) {
if (bubble != null && mBubbleData.hasBubbleWithKey(bubble.getKey())) {
mBubbleData.notificationEntryRemoved(
bubble.getEntry(), BubbleController.DISMISS_USER_GESTURE);
}
}
/** Prepares and starts the desaturate/darken animation on the bubble stack. */ /** Prepares and starts the desaturate/darken animation on the bubble stack. */
private void animateDesaturateAndDarken(View targetView, boolean desaturateAndDarken) { private void animateDesaturateAndDarken(View targetView, boolean desaturateAndDarken) {
mDesaturateAndDarkenTargetView = targetView; mDesaturateAndDarkenTargetView = targetView;
@@ -1912,6 +2012,63 @@ public class BubbleStackView extends FrameLayout {
invalidate(); invalidate();
} }
private void showManageMenu(boolean show) {
mShowingManage = show;
// This should not happen, since the manage menu is only visible when there's an expanded
// bubble. If we end up in this state, just hide the menu immediately.
if (mExpandedBubble == null || mExpandedBubble.getExpandedView() == null) {
mManageMenu.setVisibility(View.INVISIBLE);
return;
}
// If available, update the manage menu's settings option with the expanded bubble's app
// name and icon.
if (show && mBubbleData.hasBubbleWithKey(mExpandedBubble.getKey())) {
final Bubble bubble = mBubbleData.getBubbleWithKey(mExpandedBubble.getKey());
mManageSettingsIcon.setImageDrawable(bubble.getBadgedAppIcon());
mManageSettingsText.setText(getResources().getString(
R.string.bubbles_app_settings, bubble.getAppName()));
}
mExpandedBubble.getExpandedView().getManageButtonBoundsOnScreen(mTempRect);
// When the menu is open, it should be at these coordinates. This will make the menu's
// bottom left corner match up with the button's bottom left corner.
final float targetX = mTempRect.left;
final float targetY = mTempRect.bottom - mManageMenu.getHeight();
if (show) {
mManageMenu.setScaleX(0.5f);
mManageMenu.setScaleY(0.5f);
mManageMenu.setTranslationX(targetX - mManageMenu.getWidth() / 4);
mManageMenu.setTranslationY(targetY + mManageMenu.getHeight() / 4);
mManageMenu.setAlpha(0f);
PhysicsAnimator.getInstance(mManageMenu)
.spring(DynamicAnimation.ALPHA, 1f)
.spring(DynamicAnimation.SCALE_X, 1f)
.spring(DynamicAnimation.SCALE_Y, 1f)
.spring(DynamicAnimation.TRANSLATION_X, targetX)
.spring(DynamicAnimation.TRANSLATION_Y, targetY)
.start();
mManageMenu.setVisibility(View.VISIBLE);
} else {
PhysicsAnimator.getInstance(mManageMenu)
.spring(DynamicAnimation.ALPHA, 0f)
.spring(DynamicAnimation.SCALE_X, 0.5f)
.spring(DynamicAnimation.SCALE_Y, 0.5f)
.spring(DynamicAnimation.TRANSLATION_X, targetX - mManageMenu.getWidth() / 4)
.spring(DynamicAnimation.TRANSLATION_Y, targetY + mManageMenu.getHeight() / 4)
.withEndActions(() -> mManageMenu.setVisibility(View.INVISIBLE))
.start();
}
// Update the AV's obscured touchable region for the new menu visibility state.
mExpandedBubble.getExpandedView().updateObscuredTouchableRegion();
}
private void updateExpandedBubble() { private void updateExpandedBubble() {
if (DEBUG_BUBBLE_STACK_VIEW) { if (DEBUG_BUBBLE_STACK_VIEW) {
Log.d(TAG, "updateExpandedBubble()"); Log.d(TAG, "updateExpandedBubble()");
@@ -1921,6 +2078,7 @@ public class BubbleStackView extends FrameLayout {
&& mExpandedBubble.getExpandedView() != null) { && mExpandedBubble.getExpandedView() != null) {
BubbleExpandedView bev = mExpandedBubble.getExpandedView(); BubbleExpandedView bev = mExpandedBubble.getExpandedView();
mExpandedViewContainer.addView(bev); mExpandedViewContainer.addView(bev);
bev.setManageClickListener((view) -> showManageMenu(!mShowingManage));
bev.populateExpandedView(); bev.populateExpandedView();
mExpandedViewContainer.setVisibility(VISIBLE); mExpandedViewContainer.setVisibility(VISIBLE);
mExpandedViewContainer.setAlpha(1.0f); mExpandedViewContainer.setAlpha(1.0f);
@@ -2089,4 +2247,26 @@ public class BubbleStackView extends FrameLayout {
} }
return bubbles; return bubbles;
} }
/**
* Logs bubble UI click event.
*
* @param bubble the bubble notification entry that user is interacting with.
* @param action the user interaction enum.
*/
private void logBubbleClickEvent(Bubble bubble, int action) {
StatusBarNotification notification = bubble.getEntry().getSbn();
SysUiStatsLog.write(SysUiStatsLog.BUBBLE_UI_CHANGED,
notification.getPackageName(),
notification.getNotification().getChannelId(),
notification.getId(),
getBubbleIndex(getExpandedBubble()),
getBubbleCount(),
action,
getNormalizedXPosition(),
getNormalizedYPosition(),
bubble.showInShade(),
bubble.isOngoing(),
false /* isAppForeground (unused) */);
}
} }

View File

@@ -116,6 +116,7 @@ public class BubbleViewInfoTask extends AsyncTask<Void, Void, BubbleViewInfoTask
ShortcutInfo shortcutInfo; ShortcutInfo shortcutInfo;
String appName; String appName;
Bitmap badgedBubbleImage; Bitmap badgedBubbleImage;
Drawable badgedAppIcon;
int dotColor; int dotColor;
Path dotPath; Path dotPath;
Bubble.FlyoutMessage flyoutMessage; Bubble.FlyoutMessage flyoutMessage;
@@ -176,6 +177,7 @@ public class BubbleViewInfoTask extends AsyncTask<Void, Void, BubbleViewInfoTask
} }
BitmapInfo badgeBitmapInfo = iconFactory.getBadgeBitmap(badgedIcon); BitmapInfo badgeBitmapInfo = iconFactory.getBadgeBitmap(badgedIcon);
info.badgedAppIcon = badgedIcon;
info.badgedBubbleImage = iconFactory.getBubbleBitmap(bubbleDrawable, info.badgedBubbleImage = iconFactory.getBubbleBitmap(bubbleDrawable,
badgeBitmapInfo).icon; badgeBitmapInfo).icon;

View File

@@ -20,15 +20,17 @@ import android.graphics.Bitmap;
import android.graphics.Path; import android.graphics.Path;
import android.view.View; import android.view.View;
import androidx.annotation.Nullable;
/** /**
* Interface to represent actual Bubbles and UI elements that act like bubbles, like BubbleOverflow. * Interface to represent actual Bubbles and UI elements that act like bubbles, like BubbleOverflow.
*/ */
interface BubbleViewProvider { interface BubbleViewProvider {
BubbleExpandedView getExpandedView(); @Nullable BubbleExpandedView getExpandedView();
void setContentVisibility(boolean visible); void setContentVisibility(boolean visible);
View getIconView(); @Nullable View getIconView();
void logUIEvent(int bubbleCount, int action, float normalX, float normalY, int index); void logUIEvent(int bubbleCount, int action, float normalX, float normalY, int index);

View File

@@ -61,14 +61,17 @@ internal val animators = WeakHashMap<Any, PhysicsAnimator<*>>()
/** /**
* Default spring configuration to use for animations where stiffness and/or damping ratio * Default spring configuration to use for animations where stiffness and/or damping ratio
* were not provided. * were not provided, and a default spring was not set via [PhysicsAnimator.setDefaultSpringConfig].
*/ */
private val defaultSpring = PhysicsAnimator.SpringConfig( private val globalDefaultSpring = PhysicsAnimator.SpringConfig(
SpringForce.STIFFNESS_MEDIUM, SpringForce.STIFFNESS_MEDIUM,
SpringForce.DAMPING_RATIO_MEDIUM_BOUNCY) SpringForce.DAMPING_RATIO_MEDIUM_BOUNCY)
/** Default fling configuration to use for animations where friction was not provided. */ /**
private val defaultFling = PhysicsAnimator.FlingConfig( * Default fling configuration to use for animations where friction was not provided, and a default
* fling config was not set via [PhysicsAnimator.setDefaultFlingConfig].
*/
private val globalDefaultFling = PhysicsAnimator.FlingConfig(
friction = 1f, min = -Float.MAX_VALUE, max = Float.MAX_VALUE) friction = 1f, min = -Float.MAX_VALUE, max = Float.MAX_VALUE)
/** Whether to log helpful debug information about animations. */ /** Whether to log helpful debug information about animations. */
@@ -111,6 +114,12 @@ class PhysicsAnimator<T> private constructor (val target: T) {
/** End actions to run when all animations have completed. */ /** End actions to run when all animations have completed. */
private val endActions = ArrayList<EndAction>() private val endActions = ArrayList<EndAction>()
/** SpringConfig to use by default for properties whose springs were not provided. */
private var defaultSpring: SpringConfig = globalDefaultSpring
/** FlingConfig to use by default for properties whose fling configs were not provided. */
private var defaultFling: FlingConfig = globalDefaultFling
/** /**
* Internal listeners that respond to DynamicAnimations updating and ending, and dispatch to * Internal listeners that respond to DynamicAnimations updating and ending, and dispatch to
* the listeners provided via [addUpdateListener] and [addEndListener]. This allows us to add * the listeners provided via [addUpdateListener] and [addEndListener]. This allows us to add
@@ -203,6 +212,19 @@ class PhysicsAnimator<T> private constructor (val target: T) {
return spring(property, toPosition, 0f, config) return spring(property, toPosition, 0f, config)
} }
/**
* Springs a property to a given value using the provided configuration options, and a start
* velocity of 0f.
*
* @see spring
*/
fun spring(
property: FloatPropertyCompat<in T>,
toPosition: Float
): PhysicsAnimator<T> {
return spring(property, toPosition, 0f)
}
/** /**
* Flings a property using the given start velocity, using a [FlingAnimation] configured using * Flings a property using the given start velocity, using a [FlingAnimation] configured using
* the provided configuration settings. * the provided configuration settings.
@@ -392,6 +414,14 @@ class PhysicsAnimator<T> private constructor (val target: T) {
return this return this
} }
fun setDefaultSpringConfig(defaultSpring: SpringConfig) {
this.defaultSpring = defaultSpring
}
fun setDefaultFlingConfig(defaultFling: FlingConfig) {
this.defaultFling = defaultFling
}
/** Starts the animations! */ /** Starts the animations! */
fun start() { fun start() {
startAction() startAction()
@@ -752,7 +782,7 @@ class PhysicsAnimator<T> private constructor (val target: T) {
) { ) {
constructor() : constructor() :
this(defaultSpring.stiffness, defaultSpring.dampingRatio) this(globalDefaultSpring.stiffness, globalDefaultSpring.dampingRatio)
constructor(stiffness: Float, dampingRatio: Float) : constructor(stiffness: Float, dampingRatio: Float) :
this(stiffness = stiffness, dampingRatio = dampingRatio, startVelocity = 0f) this(stiffness = stiffness, dampingRatio = dampingRatio, startVelocity = 0f)
@@ -782,10 +812,10 @@ class PhysicsAnimator<T> private constructor (val target: T) {
internal var startVelocity: Float internal var startVelocity: Float
) { ) {
constructor() : this(defaultFling.friction) constructor() : this(globalDefaultFling.friction)
constructor(friction: Float) : constructor(friction: Float) :
this(friction, defaultFling.min, defaultFling.max) this(friction, globalDefaultFling.min, globalDefaultFling.max)
constructor(friction: Float, min: Float, max: Float) : constructor(friction: Float, min: Float, max: Float) :
this(friction, min, max, startVelocity = 0f) this(friction, min, max, startVelocity = 0f)