diff --git a/libs/WindowManager/Shell/res/drawable/bubble_manage_menu_section.xml b/libs/WindowManager/Shell/res/drawable/bubble_manage_menu_section.xml
new file mode 100644
index 0000000000000..d99d64d8da209
--- /dev/null
+++ b/libs/WindowManager/Shell/res/drawable/bubble_manage_menu_section.xml
@@ -0,0 +1,24 @@
+
+
+
+ -
+
+
+
+
+
+
\ No newline at end of file
diff --git a/libs/WindowManager/Shell/res/drawable/ic_expand_less.xml b/libs/WindowManager/Shell/res/drawable/ic_expand_less.xml
new file mode 100644
index 0000000000000..f4508464883d0
--- /dev/null
+++ b/libs/WindowManager/Shell/res/drawable/ic_expand_less.xml
@@ -0,0 +1,25 @@
+
+
+
+
+
diff --git a/libs/WindowManager/Shell/res/layout/bubble_bar_menu_item.xml b/libs/WindowManager/Shell/res/layout/bubble_bar_menu_item.xml
new file mode 100644
index 0000000000000..ddcd5c60d9c8a
--- /dev/null
+++ b/libs/WindowManager/Shell/res/layout/bubble_bar_menu_item.xml
@@ -0,0 +1,41 @@
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/libs/WindowManager/Shell/res/layout/bubble_bar_menu_view.xml b/libs/WindowManager/Shell/res/layout/bubble_bar_menu_view.xml
new file mode 100644
index 0000000000000..82e5aee41ff2b
--- /dev/null
+++ b/libs/WindowManager/Shell/res/layout/bubble_bar_menu_view.xml
@@ -0,0 +1,77 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/libs/WindowManager/Shell/res/values/colors.xml b/libs/WindowManager/Shell/res/values/colors.xml
index 6f8a7666e5a8a..f2a07857cd4ad 100644
--- a/libs/WindowManager/Shell/res/values/colors.xml
+++ b/libs/WindowManager/Shell/res/values/colors.xml
@@ -32,6 +32,7 @@
@color/GM2_grey_700
#EBffffff
#99000000
+ #DC362E
#D93025
diff --git a/libs/WindowManager/Shell/res/values/dimen.xml b/libs/WindowManager/Shell/res/values/dimen.xml
index aa05179ed113b..214125928892b 100644
--- a/libs/WindowManager/Shell/res/values/dimen.xml
+++ b/libs/WindowManager/Shell/res/values/dimen.xml
@@ -234,6 +234,20 @@
128dp
4dp
+
+ 200dp
+
+ 16dp
+
+ 8dp
+
+ 2dp
+
+ 2dp
+
+ 52dp
+
+ 20dp
24dp
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubble.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubble.java
index 102f2cb4b8d0c..504839fedf066 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubble.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubble.java
@@ -855,7 +855,8 @@ public class Bubble implements BubbleViewProvider {
return mIsAppBubble;
}
- Intent getSettingsIntent(final Context context) {
+ /** Creates open app settings intent */
+ public Intent getSettingsIntent(final Context context) {
final Intent intent = new Intent(Settings.ACTION_APP_NOTIFICATION_BUBBLE_SETTINGS);
intent.putExtra(Settings.EXTRA_APP_PACKAGE, getPackageName());
final int uid = getUid(context);
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java
index 988ad8c6c0f7b..39338e5baddb9 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java
@@ -720,6 +720,7 @@ public class BubbleController implements ConfigurationChangeListener,
// TODO(b/273312602): consider foldables where we do need a stack view when folded
if (mLayerView == null) {
mLayerView = new BubbleBarLayerView(mContext, this);
+ mLayerView.setUnBubbleConversationCallback(mSysuiProxy::onUnbubbleConversation);
}
} else {
if (mStackView == null) {
@@ -1220,6 +1221,13 @@ public class BubbleController implements ConfigurationChangeListener,
}
}
+ /**
+ * Dismiss bubble if it exists and remove it from the stack
+ */
+ public void dismissBubble(Bubble bubble, @Bubbles.DismissReason int reason) {
+ mBubbleData.dismissBubbleWithKey(bubble.getKey(), reason);
+ }
+
/**
* Performs a screenshot that may exclude the bubble layer, if one is present. The screenshot
* can be access via the supplied {@link ScreenshotSync#get()} asynchronously.
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleViewInfoTask.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleViewInfoTask.java
index 8ab9841ff0c22..3a46797d4a3d1 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleViewInfoTask.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleViewInfoTask.java
@@ -192,6 +192,11 @@ public class BubbleViewInfoTask extends AsyncTask mLayerBoundsSupplier;
+ private @Nullable Consumer mUnBubbleConversationCallback;
private BubbleBarHandleView mHandleView = new BubbleBarHandleView(getContext());
private @Nullable TaskView mTaskView;
@@ -98,6 +106,13 @@ public class BubbleBarExpandedView extends FrameLayout implements BubbleTaskView
});
}
+ @Override
+ protected void onDetachedFromWindow() {
+ super.onDetachedFromWindow();
+ // Hide manage menu when view disappears
+ mMenuViewController.hideMenu(false /* animated */);
+ }
+
/** Set the BubbleController on the view, must be called before doing anything else. */
public void initialize(BubbleController controller) {
mController = controller;
@@ -108,6 +123,36 @@ public class BubbleBarExpandedView extends FrameLayout implements BubbleTaskView
addView(mTaskView);
mTaskView.setEnableSurfaceClipping(true);
mTaskView.setCornerRadius(mCornerRadius);
+ mMenuViewController = new BubbleBarMenuViewController(mContext, this);
+ mMenuViewController.setListener(new BubbleBarMenuViewController.Listener() {
+ @Override
+ public void onMenuVisibilityChanged(boolean visible) {
+ if (mTaskView == null || mLayerBoundsSupplier == null) return;
+ // Updates the obscured touchable region for the task surface.
+ mTaskView.setObscuredTouchRect(visible ? mLayerBoundsSupplier.get() : null);
+ }
+
+ @Override
+ public void onUnBubbleConversation(Bubble bubble) {
+ if (mUnBubbleConversationCallback != null) {
+ mUnBubbleConversationCallback.accept(bubble.getKey());
+ }
+ }
+
+ @Override
+ public void onOpenAppSettings(Bubble bubble) {
+ mController.collapseStack();
+ mContext.startActivityAsUser(bubble.getSettingsIntent(mContext), bubble.getUser());
+ }
+
+ @Override
+ public void onDismissBubble(Bubble bubble) {
+ mController.dismissBubble(bubble, Bubbles.DISMISS_USER_REMOVED);
+ }
+ });
+ mHandleView.setOnClickListener(view -> {
+ mMenuViewController.showMenu(true /* animated */);
+ });
}
// TODO (b/275087636): call this when theme/config changes
@@ -183,11 +228,13 @@ public class BubbleBarExpandedView extends FrameLayout implements BubbleTaskView
}
mBubbleTaskViewHelper.cleanUpTaskView();
}
+ mMenuViewController.hideMenu(false /* animated */);
}
- /** Updates the bubble shown in this task view. */
+ /** Updates the bubble shown in the expanded view. */
public void update(Bubble bubble) {
mBubbleTaskViewHelper.update(bubble);
+ mMenuViewController.updateMenu(bubble);
}
/** The task id of the activity shown in the task view, if it exists. */
@@ -195,6 +242,17 @@ public class BubbleBarExpandedView extends FrameLayout implements BubbleTaskView
return mBubbleTaskViewHelper != null ? mBubbleTaskViewHelper.getTaskId() : INVALID_TASK_ID;
}
+ /** Sets layer bounds supplier used for obscured touchable region of task view */
+ void setLayerBoundsSupplier(@Nullable Supplier supplier) {
+ mLayerBoundsSupplier = supplier;
+ }
+
+ /** Sets the function to call to un-bubble the given conversation. */
+ public void setUnBubbleConversationCallback(
+ @Nullable Consumer unBubbleConversationCallback) {
+ mUnBubbleConversationCallback = unBubbleConversationCallback;
+ }
+
/**
* Call when the location or size of the view has changed to update TaskView.
*/
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarHandleView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarHandleView.java
index e121aa45469d1..ce26bc0322b3c 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarHandleView.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarHandleView.java
@@ -20,7 +20,6 @@ import android.animation.AnimatorListenerAdapter;
import android.animation.ObjectAnimator;
import android.annotation.Nullable;
import android.content.Context;
-import android.content.res.Resources;
import android.graphics.Outline;
import android.graphics.Rect;
import android.util.AttributeSet;
@@ -38,35 +37,35 @@ import com.android.wm.shell.R;
public class BubbleBarHandleView extends View {
private static final long COLOR_CHANGE_DURATION = 120;
- private final int mHandleWidth;
- private final int mHandleHeight;
- private final @ColorInt int mHandleLightColor;
- private final @ColorInt int mHandleDarkColor;
+ private int mHandleWidth;
+ private int mHandleHeight;
+ private @ColorInt int mHandleLightColor;
+ private @ColorInt int mHandleDarkColor;
private @Nullable ObjectAnimator mColorChangeAnim;
public BubbleBarHandleView(Context context) {
- this(context, null);
+ this(context, null /* attrs */);
}
public BubbleBarHandleView(Context context, AttributeSet attrs) {
- this(context, attrs, 0);
+ this(context, attrs, 0 /* defStyleAttr */);
}
public BubbleBarHandleView(Context context, AttributeSet attrs, int defStyleAttr) {
- this(context, attrs, defStyleAttr, 0);
+ this(context, attrs, defStyleAttr, 0 /* defStyleRes */);
}
public BubbleBarHandleView(Context context, AttributeSet attrs, int defStyleAttr,
int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
- Resources resources = context.getResources();
- mHandleWidth = resources.getDimensionPixelSize(
+
+ mHandleWidth = getResources().getDimensionPixelSize(
R.dimen.bubble_bar_expanded_view_handle_width);
- mHandleHeight = resources.getDimensionPixelSize(
+ mHandleHeight = getResources().getDimensionPixelSize(
R.dimen.bubble_bar_expanded_view_handle_height);
- mHandleLightColor = ContextCompat.getColor(context,
+ mHandleLightColor = ContextCompat.getColor(getContext(),
R.color.bubble_bar_expanded_view_handle_light);
- mHandleDarkColor = ContextCompat.getColor(context,
+ mHandleDarkColor = ContextCompat.getColor(getContext(),
R.color.bubble_bar_expanded_view_handle_dark);
setClipToOutline(true);
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarLayerView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarLayerView.java
index b1a725b6e5c4b..bc622e7a7a3eb 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarLayerView.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarLayerView.java
@@ -32,6 +32,8 @@ import com.android.wm.shell.bubbles.BubbleController;
import com.android.wm.shell.bubbles.BubblePositioner;
import com.android.wm.shell.bubbles.BubbleViewProvider;
+import java.util.function.Consumer;
+
/**
* Similar to {@link com.android.wm.shell.bubbles.BubbleStackView}, this view is added to window
* manager to display bubbles. However, it is only used when bubbles are being displayed in
@@ -53,6 +55,7 @@ public class BubbleBarLayerView extends FrameLayout
@Nullable
private BubbleViewProvider mExpandedBubble;
private BubbleBarExpandedView mExpandedView;
+ private @Nullable Consumer mUnBubbleConversationCallback;
// TODO(b/273310265) - currently the view is always on the right, need to update for RTL.
/** Whether the expanded view is displaying on the left of the screen or not. */
@@ -146,6 +149,13 @@ public class BubbleBarLayerView extends FrameLayout
final int width = mPositioner.getExpandedViewWidthForBubbleBar();
final int height = mPositioner.getExpandedViewHeightForBubbleBar();
mExpandedView.setVisibility(GONE);
+ mExpandedView.setUnBubbleConversationCallback(mUnBubbleConversationCallback);
+ mExpandedView.setLayerBoundsSupplier(() -> new Rect(0, 0, getWidth(), getHeight()));
+ mExpandedView.setUnBubbleConversationCallback(bubbleKey -> {
+ if (mUnBubbleConversationCallback != null) {
+ mUnBubbleConversationCallback.accept(bubbleKey);
+ }
+ });
addView(mExpandedView, new FrameLayout.LayoutParams(width, height));
}
@@ -165,6 +175,12 @@ public class BubbleBarLayerView extends FrameLayout
showScrim(false);
}
+ /** Sets the function to call to un-bubble the given conversation. */
+ public void setUnBubbleConversationCallback(
+ @Nullable Consumer unBubbleConversationCallback) {
+ mUnBubbleConversationCallback = unBubbleConversationCallback;
+ }
+
/** Updates the expanded view size and position. */
private void updateExpandedView() {
if (mExpandedView == null) return;
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarMenuItemView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarMenuItemView.java
new file mode 100644
index 0000000000000..00b977721bea1
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarMenuItemView.java
@@ -0,0 +1,77 @@
+/*
+ * Copyright (C) 2023 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.wm.shell.bubbles.bar;
+
+import android.annotation.ColorInt;
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.graphics.Color;
+import android.graphics.drawable.Icon;
+import android.util.AttributeSet;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+
+import com.android.wm.shell.R;
+
+/**
+ * Bubble bar expanded view menu item view to display menu action details
+ */
+public class BubbleBarMenuItemView extends LinearLayout {
+ private ImageView mImageView;
+ private TextView mTextView;
+
+ public BubbleBarMenuItemView(Context context) {
+ this(context, null /* attrs */);
+ }
+
+ public BubbleBarMenuItemView(Context context, AttributeSet attrs) {
+ this(context, attrs, 0 /* defStyleAttr */);
+ }
+
+ public BubbleBarMenuItemView(Context context, AttributeSet attrs, int defStyleAttr) {
+ this(context, attrs, defStyleAttr, 0 /* defStyleRes */);
+ }
+
+ public BubbleBarMenuItemView(Context context, AttributeSet attrs, int defStyleAttr,
+ int defStyleRes) {
+ super(context, attrs, defStyleAttr, defStyleRes);
+ }
+
+ @Override
+ protected void onFinishInflate() {
+ super.onFinishInflate();
+ mImageView = findViewById(R.id.bubble_bar_menu_item_icon);
+ mTextView = findViewById(R.id.bubble_bar_menu_item_title);
+ }
+
+ /**
+ * Update menu item with the details and tint color
+ */
+ void update(Icon icon, String title, @ColorInt int tint) {
+ if (tint == Color.TRANSPARENT) {
+ final TypedArray typedArray = getContext().obtainStyledAttributes(
+ new int[]{android.R.attr.textColorPrimary});
+ mTextView.setTextColor(typedArray.getColor(0, Color.BLACK));
+ } else {
+ icon.setTint(tint);
+ mTextView.setTextColor(tint);
+ }
+
+ mImageView.setImageIcon(icon);
+ mTextView.setText(title);
+ }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarMenuView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarMenuView.java
new file mode 100644
index 0000000000000..211fe0d48e435
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarMenuView.java
@@ -0,0 +1,145 @@
+/*
+ * Copyright (C) 2023 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.wm.shell.bubbles.bar;
+
+import android.annotation.ColorInt;
+import android.content.Context;
+import android.graphics.Color;
+import android.graphics.drawable.Icon;
+import android.util.AttributeSet;
+import android.view.LayoutInflater;
+import android.view.ViewGroup;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+
+import com.android.wm.shell.R;
+import com.android.wm.shell.bubbles.Bubble;
+
+import java.util.ArrayList;
+
+/**
+ * Bubble bar expanded view menu
+ */
+public class BubbleBarMenuView extends LinearLayout {
+ private ViewGroup mBubbleSectionView;
+ private ViewGroup mActionsSectionView;
+ private ImageView mBubbleIconView;
+ private TextView mBubbleTitleView;
+
+ public BubbleBarMenuView(Context context) {
+ this(context, null /* attrs */);
+ }
+
+ public BubbleBarMenuView(Context context, AttributeSet attrs) {
+ this(context, attrs, 0 /* defStyleAttr */);
+ }
+
+ public BubbleBarMenuView(Context context, AttributeSet attrs, int defStyleAttr) {
+ this(context, attrs, defStyleAttr, 0 /* defStyleRes */);
+ }
+
+ public BubbleBarMenuView(Context context, AttributeSet attrs, int defStyleAttr,
+ int defStyleRes) {
+ super(context, attrs, defStyleAttr, defStyleRes);
+ }
+
+ @Override
+ protected void onFinishInflate() {
+ super.onFinishInflate();
+ mBubbleSectionView = findViewById(R.id.bubble_bar_manage_menu_bubble_section);
+ mActionsSectionView = findViewById(R.id.bubble_bar_manage_menu_actions_section);
+ mBubbleIconView = findViewById(R.id.bubble_bar_manage_menu_bubble_icon);
+ mBubbleTitleView = findViewById(R.id.bubble_bar_manage_menu_bubble_title);
+ }
+
+ /** Update menu details with bubble info */
+ void updateInfo(Bubble bubble) {
+ if (bubble.getIcon() != null) {
+ mBubbleIconView.setImageIcon(bubble.getIcon());
+ } else {
+ mBubbleIconView.setImageBitmap(bubble.getBubbleIcon());
+ }
+ mBubbleTitleView.setText(bubble.getTitle());
+ }
+
+ /**
+ * Update menu action items views
+ * @param actions used to populate menu item views
+ */
+ void updateActions(ArrayList actions) {
+ mActionsSectionView.removeAllViews();
+ LayoutInflater inflater = LayoutInflater.from(mContext);
+
+ for (MenuAction action : actions) {
+ BubbleBarMenuItemView itemView = (BubbleBarMenuItemView) inflater.inflate(
+ R.layout.bubble_bar_menu_item, mActionsSectionView, false);
+ itemView.update(action.mIcon, action.mTitle, action.mTint);
+ itemView.setOnClickListener(action.mOnClick);
+ mActionsSectionView.addView(itemView);
+ }
+ }
+
+ /** Sets on close menu listener */
+ void setOnCloseListener(Runnable onClose) {
+ mBubbleSectionView.setOnClickListener(view -> {
+ onClose.run();
+ });
+ }
+
+ /**
+ * Overridden to proxy to section views alpha.
+ * @implNote
+ * If animate alpha on the parent (menu container) view, section view shadows get distorted.
+ * To prevent distortion and artifacts alpha changes applied directly on the section views.
+ */
+ @Override
+ public void setAlpha(float alpha) {
+ mBubbleSectionView.setAlpha(alpha);
+ mActionsSectionView.setAlpha(alpha);
+ }
+
+ /**
+ * Overridden to proxy section view alpha value.
+ * @implNote
+ * The assumption is that both section views have the same alpha value
+ */
+ @Override
+ public float getAlpha() {
+ return mBubbleSectionView.getAlpha();
+ }
+
+ /**
+ * Menu action details used to create menu items
+ */
+ static class MenuAction {
+ private Icon mIcon;
+ private @ColorInt int mTint;
+ private String mTitle;
+ private OnClickListener mOnClick;
+
+ MenuAction(Icon icon, String title, OnClickListener onClick) {
+ this(icon, title, Color.TRANSPARENT, onClick);
+ }
+
+ MenuAction(Icon icon, String title, @ColorInt int tint, OnClickListener onClick) {
+ this.mIcon = icon;
+ this.mTitle = title;
+ this.mTint = tint;
+ this.mOnClick = onClick;
+ }
+ }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarMenuViewController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarMenuViewController.java
new file mode 100644
index 0000000000000..8be140c164356
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarMenuViewController.java
@@ -0,0 +1,242 @@
+/*
+ * Copyright (C) 2023 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.wm.shell.bubbles.bar;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.content.Context;
+import android.content.res.Resources;
+import android.graphics.drawable.Icon;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+
+import androidx.core.content.ContextCompat;
+import androidx.dynamicanimation.animation.DynamicAnimation;
+import androidx.dynamicanimation.animation.SpringForce;
+
+import com.android.wm.shell.R;
+import com.android.wm.shell.animation.PhysicsAnimator;
+import com.android.wm.shell.bubbles.Bubble;
+
+import java.util.ArrayList;
+
+/**
+ * Manages bubble bar expanded view menu presentation and animations
+ */
+class BubbleBarMenuViewController {
+ private static final float MENU_INITIAL_SCALE = 0.5f;
+ private final Context mContext;
+ private final ViewGroup mRootView;
+ private @Nullable Listener mListener;
+ private @Nullable Bubble mBubble;
+ private @Nullable BubbleBarMenuView mMenuView;
+ /** A transparent view used to intercept touches to collapse menu when presented */
+ private @Nullable View mScrimView;
+ private @Nullable PhysicsAnimator mMenuAnimator;
+ private PhysicsAnimator.SpringConfig mMenuSpringConfig;
+
+ BubbleBarMenuViewController(Context context, ViewGroup rootView) {
+ mContext = context;
+ mRootView = rootView;
+ mMenuSpringConfig = new PhysicsAnimator.SpringConfig(
+ SpringForce.STIFFNESS_MEDIUM, SpringForce.DAMPING_RATIO_LOW_BOUNCY);
+ }
+
+ /** Sets menu actions listener */
+ void setListener(@Nullable Listener listener) {
+ mListener = listener;
+ }
+
+ /** Update menu with bubble */
+ void updateMenu(@NonNull Bubble bubble) {
+ mBubble = bubble;
+ }
+
+ /**
+ * Show bubble bar expanded view menu
+ * @param animated if should animate transition
+ */
+ void showMenu(boolean animated) {
+ if (mMenuView == null || mScrimView == null) {
+ setupMenu();
+ }
+ cancelAnimations();
+ mMenuView.setVisibility(View.VISIBLE);
+ mScrimView.setVisibility(View.VISIBLE);
+ Runnable endActions = () -> {
+ mMenuView.getChildAt(0).requestAccessibilityFocus();
+ if (mListener != null) {
+ mListener.onMenuVisibilityChanged(true /* isShown */);
+ }
+ };
+ if (animated) {
+ animateTransition(true /* show */, endActions);
+ } else {
+ endActions.run();
+ }
+ }
+
+ /**
+ * Hide bubble bar expanded view menu
+ * @param animated if should animate transition
+ */
+ void hideMenu(boolean animated) {
+ if (mMenuView == null || mScrimView == null) return;
+ cancelAnimations();
+ Runnable endActions = () -> {
+ mMenuView.setVisibility(View.GONE);
+ mScrimView.setVisibility(View.GONE);
+ if (mListener != null) {
+ mListener.onMenuVisibilityChanged(false /* isShown */);
+ }
+ };
+ if (animated) {
+ animateTransition(false /* show */, endActions);
+ } else {
+ endActions.run();
+ }
+ }
+
+ /**
+ * Animate show/hide menu transition
+ * @param show if should show or hide the menu
+ * @param endActions will be called when animation ends
+ */
+ private void animateTransition(boolean show, Runnable endActions) {
+ if (mMenuView == null) return;
+ mMenuAnimator = PhysicsAnimator.getInstance(mMenuView);
+ mMenuAnimator.setDefaultSpringConfig(mMenuSpringConfig);
+ mMenuAnimator
+ .spring(DynamicAnimation.ALPHA, show ? 1f : 0f)
+ .spring(DynamicAnimation.SCALE_Y, show ? 1f : MENU_INITIAL_SCALE)
+ .withEndActions(() -> {
+ mMenuAnimator = null;
+ endActions.run();
+ })
+ .start();
+ }
+
+ /** Cancel running animations */
+ private void cancelAnimations() {
+ if (mMenuAnimator != null) {
+ mMenuAnimator.cancel();
+ mMenuAnimator = null;
+ }
+ }
+
+ /** Sets up and inflate menu views */
+ private void setupMenu() {
+ // Menu view setup
+ mMenuView = (BubbleBarMenuView) LayoutInflater.from(mContext).inflate(
+ R.layout.bubble_bar_menu_view, mRootView, false);
+ mMenuView.setAlpha(0f);
+ mMenuView.setPivotY(0f);
+ mMenuView.setScaleY(MENU_INITIAL_SCALE);
+ mMenuView.setOnCloseListener(() -> hideMenu(true /* animated */));
+ if (mBubble != null) {
+ mMenuView.updateInfo(mBubble);
+ mMenuView.updateActions(createMenuActions(mBubble));
+ }
+ // Scrim view setup
+ mScrimView = new View(mContext);
+ mScrimView.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO);
+ mScrimView.setOnClickListener(view -> hideMenu(true /* animated */));
+ // Attach to root view
+ mRootView.addView(mScrimView);
+ mRootView.addView(mMenuView);
+ }
+
+ /**
+ * Creates menu actions to populate menu view
+ * @param bubble used to create actions depending on bubble type
+ */
+ private ArrayList createMenuActions(Bubble bubble) {
+ ArrayList menuActions = new ArrayList<>();
+ Resources resources = mContext.getResources();
+
+ if (bubble.isConversation()) {
+ // Don't bubble conversation action
+ menuActions.add(new BubbleBarMenuView.MenuAction(
+ Icon.createWithResource(mContext, R.drawable.bubble_ic_stop_bubble),
+ resources.getString(R.string.bubbles_dont_bubble_conversation),
+ view -> {
+ hideMenu(true /* animated */);
+ if (mListener != null) {
+ mListener.onUnBubbleConversation(bubble);
+ }
+ }
+ ));
+ // Open settings action
+ Icon appIcon = bubble.getRawAppBadge() != null ? Icon.createWithBitmap(
+ bubble.getRawAppBadge()) : null;
+ menuActions.add(new BubbleBarMenuView.MenuAction(
+ appIcon,
+ resources.getString(R.string.bubbles_app_settings, bubble.getAppName()),
+ view -> {
+ hideMenu(true /* animated */);
+ if (mListener != null) {
+ mListener.onOpenAppSettings(bubble);
+ }
+ }
+ ));
+ }
+
+ // Dismiss bubble action
+ menuActions.add(new BubbleBarMenuView.MenuAction(
+ Icon.createWithResource(resources, R.drawable.ic_remove_no_shadow),
+ resources.getString(R.string.bubble_dismiss_text),
+ ContextCompat.getColor(mContext, R.color.bubble_bar_expanded_view_menu_close),
+ view -> {
+ hideMenu(true /* animated */);
+ if (mListener != null) {
+ mListener.onDismissBubble(bubble);
+ }
+ }
+ ));
+
+ return menuActions;
+ }
+
+ /**
+ * Bubble bar expanded view menu actions listener
+ */
+ interface Listener {
+ /**
+ * Called when manage menu is shown/hidden
+ * If animated will be called when animation ends
+ */
+ void onMenuVisibilityChanged(boolean visible);
+
+ /**
+ * Un-bubbles conversation and removes the bubble from the stack
+ * This conversation will not be bubbled with new messages
+ * @see com.android.wm.shell.bubbles.BubbleController
+ */
+ void onUnBubbleConversation(Bubble bubble);
+
+ /**
+ * Launches app notification bubble settings for the bubble with intent created in:
+ * {@code Bubble.getSettingsIntent}
+ */
+ void onOpenAppSettings(Bubble bubble);
+
+ /**
+ * Dismiss bubble and remove it from the bubble stack
+ */
+ void onDismissBubble(Bubble bubble);
+ }
+}