diff --git a/libs/WindowManager/Shell/res/layout/bubble_bar_expanded_view.xml b/libs/WindowManager/Shell/res/layout/bubble_bar_expanded_view.xml
new file mode 100644
index 0000000000000..681a52bea2b2c
--- /dev/null
+++ b/libs/WindowManager/Shell/res/layout/bubble_bar_expanded_view.xml
@@ -0,0 +1,24 @@
+
+
+
+
+
diff --git a/libs/WindowManager/Shell/res/values/dimen.xml b/libs/WindowManager/Shell/res/values/dimen.xml
index 3a8614aa65137..df8292ede3e28 100644
--- a/libs/WindowManager/Shell/res/values/dimen.xml
+++ b/libs/WindowManager/Shell/res/values/dimen.xml
@@ -228,6 +228,8 @@
16dp
72dp
+
+ 16dp
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 5f2b63089009c..bd3aa16e726aa 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
@@ -47,6 +47,7 @@ import android.util.Log;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.logging.InstanceId;
+import com.android.wm.shell.bubbles.bar.BubbleBarExpandedView;
import com.android.wm.shell.common.bubbles.BubbleInfo;
import java.io.PrintWriter;
@@ -87,8 +88,18 @@ public class Bubble implements BubbleViewProvider {
private String mAppName;
private ShortcutInfo mShortcutInfo;
private String mMetadataShortcutId;
+
+ /**
+ * If {@link BubbleController#isShowingAsBubbleBar()} is true, the only view that will be
+ * populated will be {@link #mBubbleBarExpandedView}. If it is false, {@link #mIconView}
+ * and {@link #mExpandedView} will be populated.
+ */
+ @Nullable
private BadgedImageView mIconView;
+ @Nullable
private BubbleExpandedView mExpandedView;
+ @Nullable
+ private BubbleBarExpandedView mBubbleBarExpandedView;
private BubbleViewInfoTask mInflationTask;
private boolean mInflateSynchronously;
@@ -327,12 +338,18 @@ public class Bubble implements BubbleViewProvider {
return mIconView;
}
- @Override
@Nullable
+ @Override
public BubbleExpandedView getExpandedView() {
return mExpandedView;
}
+ @Nullable
+ @Override
+ public BubbleBarExpandedView getBubbleBarExpandedView() {
+ return mBubbleBarExpandedView;
+ }
+
@Nullable
public String getTitle() {
return mTitle;
@@ -364,6 +381,9 @@ public class Bubble implements BubbleViewProvider {
mExpandedView.cleanUpExpandedState();
mExpandedView = null;
}
+ if (mBubbleBarExpandedView != null) {
+ mBubbleBarExpandedView.cleanUpExpandedState();
+ }
if (mIntent != null) {
mIntent.unregisterCancelListener(mIntentCancelListener);
}
@@ -445,7 +465,7 @@ public class Bubble implements BubbleViewProvider {
}
boolean isInflated() {
- return mIconView != null && mExpandedView != null;
+ return (mIconView != null && mExpandedView != null) || mBubbleBarExpandedView != null;
}
void stopInflation() {
@@ -478,6 +498,9 @@ public class Bubble implements BubbleViewProvider {
if (mExpandedView != null) {
mExpandedView.update(this /* bubble */);
}
+ if (mBubbleBarExpandedView != null) {
+ mBubbleBarExpandedView.update(this /* bubble */);
+ }
if (mIconView != null) {
mIconView.setRenderedBubble(this /* bubble */);
}
@@ -607,6 +630,9 @@ public class Bubble implements BubbleViewProvider {
*/
@Override
public int getTaskId() {
+ if (mBubbleBarExpandedView != null) {
+ return mBubbleBarExpandedView.getTaskId();
+ }
return mExpandedView != null ? mExpandedView.getTaskId() : mTaskId;
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleOverflow.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleOverflow.kt
index eb7929b8ca54e..6cdb80b2bb93d 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleOverflow.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleOverflow.kt
@@ -29,6 +29,7 @@ import android.util.TypedValue
import android.view.LayoutInflater
import android.widget.FrameLayout
import com.android.wm.shell.R
+import com.android.wm.shell.bubbles.bar.BubbleBarExpandedView
class BubbleOverflow(
private val context: Context,
@@ -136,6 +137,10 @@ class BubbleOverflow(
return expandedView
}
+ override fun getBubbleBarExpandedView(): BubbleBarExpandedView? {
+ return null
+ }
+
override fun getDotColor(): Int {
return dotColor
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleViewProvider.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleViewProvider.java
index 3f6d41bb2b684..6bdc3b9f5aece 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleViewProvider.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleViewProvider.java
@@ -22,18 +22,40 @@ import android.view.View;
import androidx.annotation.Nullable;
+import com.android.wm.shell.bubbles.bar.BubbleBarExpandedView;
+
/**
* Interface to represent actual Bubbles and UI elements that act like bubbles, like BubbleOverflow.
*/
public interface BubbleViewProvider {
- @Nullable BubbleExpandedView getExpandedView();
+
+ /**
+ * Returns the icon view used for a bubble (the click target when collapsed). This is populated
+ * when bubbles are floating, i.e. when {@link BubbleController#isShowingAsBubbleBar()} is
+ * false.
+ */
+ @Nullable
+ View getIconView();
+
+ /**
+ * Returns the expanded view used for a bubble. This is populated when bubbles are floating,
+ * i.e. when {@link BubbleController#isShowingAsBubbleBar()} is false.
+ */
+ @Nullable
+ BubbleExpandedView getExpandedView();
+
+ /**
+ * Returns the expanded view used for a bubble being show in the bubble bar. This is populated
+ * when {@link BubbleController#isShowingAsBubbleBar()} is true.
+ */
+ @Nullable
+ BubbleBarExpandedView getBubbleBarExpandedView();
/**
* Sets whether the contents of the bubble's TaskView should be visible.
*/
void setTaskViewVisibility(boolean visible);
- @Nullable View getIconView();
String getKey();
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarExpandedView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarExpandedView.java
new file mode 100644
index 0000000000000..b8f049becb6f7
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarExpandedView.java
@@ -0,0 +1,267 @@
+/*
+ * 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.app.ActivityManager;
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.graphics.Color;
+import android.graphics.Outline;
+import android.util.AttributeSet;
+import android.view.View;
+import android.view.ViewOutlineProvider;
+import android.widget.FrameLayout;
+
+import com.android.internal.policy.ScreenDecorationsUtils;
+import com.android.wm.shell.R;
+import com.android.wm.shell.bubbles.Bubble;
+import com.android.wm.shell.bubbles.BubbleController;
+import com.android.wm.shell.bubbles.BubbleTaskViewHelper;
+import com.android.wm.shell.taskview.TaskView;
+
+/**
+ * Expanded view of a bubble when it's part of the bubble bar.
+ *
+ * {@link BubbleController#isShowingAsBubbleBar()}
+ */
+public class BubbleBarExpandedView extends FrameLayout implements BubbleTaskViewHelper.Listener {
+
+ private static final String TAG = BubbleBarExpandedView.class.getSimpleName();
+ private static final int INVALID_TASK_ID = -1;
+
+ private BubbleController mController;
+ private BubbleTaskViewHelper mBubbleTaskViewHelper;
+
+ private HandleView mMenuView;
+ private TaskView mTaskView;
+
+ private int mMenuHeight;
+ private int mBackgroundColor;
+ private float mCornerRadius = 0f;
+
+ /**
+ * Whether we want the {@code TaskView}'s content to be visible (alpha = 1f). If
+ * {@link #mIsAnimating} is true, this may not reflect the {@code TaskView}'s actual alpha
+ * value until the animation ends.
+ */
+ private boolean mIsContentVisible = false;
+ private boolean mIsAnimating;
+
+ public BubbleBarExpandedView(Context context) {
+ this(context, null);
+ }
+
+ public BubbleBarExpandedView(Context context, AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ public BubbleBarExpandedView(Context context, AttributeSet attrs, int defStyleAttr) {
+ this(context, attrs, defStyleAttr, 0);
+ }
+
+ public BubbleBarExpandedView(Context context, AttributeSet attrs, int defStyleAttr,
+ int defStyleRes) {
+ super(context, attrs, defStyleAttr, defStyleRes);
+ }
+
+ @Override
+ protected void onFinishInflate() {
+ super.onFinishInflate();
+ Context context = getContext();
+ setElevation(getResources().getDimensionPixelSize(R.dimen.bubble_elevation));
+ mMenuHeight = context.getResources().getDimensionPixelSize(
+ R.dimen.bubblebar_expanded_view_menu_size);
+ mMenuView = new HandleView(context);
+ addView(mMenuView);
+
+ applyThemeAttrs();
+ setClipToOutline(true);
+ setOutlineProvider(new ViewOutlineProvider() {
+ @Override
+ public void getOutline(View view, Outline outline) {
+ outline.setRoundRect(0, 0, view.getWidth(), view.getHeight(), mCornerRadius);
+ }
+ });
+ }
+
+ /** Set the BubbleController on the view, must be called before doing anything else. */
+ public void initialize(BubbleController controller) {
+ mController = controller;
+ mBubbleTaskViewHelper = new BubbleTaskViewHelper(mContext, mController,
+ /* listener= */ this,
+ /* viewParent= */ this);
+ mTaskView = mBubbleTaskViewHelper.getTaskView();
+ addView(mTaskView);
+ mTaskView.setEnableSurfaceClipping(true);
+ mTaskView.setCornerRadius(mCornerRadius);
+ }
+
+ // TODO (b/275087636): call this when theme/config changes
+ void applyThemeAttrs() {
+ boolean supportsRoundedCorners = ScreenDecorationsUtils.supportsRoundedCornersOnWindows(
+ mContext.getResources());
+ final TypedArray ta = mContext.obtainStyledAttributes(new int[] {
+ android.R.attr.dialogCornerRadius,
+ android.R.attr.colorBackgroundFloating});
+ mCornerRadius = supportsRoundedCorners ? ta.getDimensionPixelSize(0, 0) : 0;
+ mCornerRadius = mCornerRadius / 2f;
+ mBackgroundColor = ta.getColor(1, Color.WHITE);
+
+ ta.recycle();
+
+ mMenuView.setCornerRadius(mCornerRadius);
+ mMenuHeight = getResources().getDimensionPixelSize(
+ R.dimen.bubblebar_expanded_view_menu_size);
+
+ if (mTaskView != null) {
+ mTaskView.setCornerRadius(mCornerRadius);
+ mTaskView.setElevation(150);
+ updateMenuColor();
+ }
+ }
+
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ super.onMeasure(widthMeasureSpec, heightMeasureSpec);
+ int height = MeasureSpec.getSize(heightMeasureSpec);
+
+ // Add corner radius here so that the menu extends behind the rounded corners of TaskView.
+ int menuViewHeight = Math.min((int) (mMenuHeight + mCornerRadius), height);
+ measureChild(mMenuView, widthMeasureSpec, MeasureSpec.makeMeasureSpec(menuViewHeight,
+ MeasureSpec.getMode(heightMeasureSpec)));
+
+ if (mTaskView != null) {
+ int taskViewHeight = height - menuViewHeight;
+ measureChild(mTaskView, widthMeasureSpec, MeasureSpec.makeMeasureSpec(taskViewHeight,
+ MeasureSpec.getMode(heightMeasureSpec)));
+ }
+ }
+
+ @Override
+ protected void onLayout(boolean changed, int l, int t, int r, int b) {
+ // Drag handle above
+ final int dragHandleBottom = t + mMenuView.getMeasuredHeight();
+ mMenuView.layout(l, t, r, dragHandleBottom);
+ if (mTaskView != null) {
+ // Subtract radius so that the menu extends behind the rounded corners of TaskView.
+ mTaskView.layout(l, (int) (dragHandleBottom - mCornerRadius), r,
+ dragHandleBottom + mTaskView.getMeasuredHeight());
+ }
+ }
+
+ @Override
+ public void onTaskCreated() {
+ setContentVisibility(true);
+ updateMenuColor();
+ }
+
+ @Override
+ public void onContentVisibilityChanged(boolean visible) {
+ setContentVisibility(visible);
+ }
+
+ @Override
+ public void onBackPressed() {
+ mController.collapseStack();
+ }
+
+ /** Cleans up task view, should be called when the bubble is no longer active. */
+ public void cleanUpExpandedState() {
+ if (mBubbleTaskViewHelper != null) {
+ if (mTaskView != null) {
+ removeView(mTaskView);
+ }
+ mBubbleTaskViewHelper.cleanUpTaskView();
+ }
+ }
+
+ /** Updates the bubble shown in this task view. */
+ public void update(Bubble bubble) {
+ mBubbleTaskViewHelper.update(bubble);
+ }
+
+ /** The task id of the activity shown in the task view, if it exists. */
+ public int getTaskId() {
+ return mBubbleTaskViewHelper != null ? mBubbleTaskViewHelper.getTaskId() : INVALID_TASK_ID;
+ }
+
+ /**
+ * Call when the location or size of the view has changed to update TaskView.
+ */
+ public void updateLocation() {
+ if (mTaskView == null) return;
+ mTaskView.onLocationChanged();
+ }
+
+ /** Sets the alpha of the task view. */
+ public void setContentVisibility(boolean visible) {
+ mIsContentVisible = visible;
+
+ if (mTaskView == null) return;
+
+ if (!mIsAnimating) {
+ mTaskView.setAlpha(visible ? 1f : 0f);
+ }
+ }
+
+ /** Updates the menu bar to be the status bar color specified by the app. */
+ private void updateMenuColor() {
+ if (mTaskView == null) return;
+ ActivityManager.RunningTaskInfo info = mTaskView.getTaskInfo();
+ final int taskBgColor = info.taskDescription.getStatusBarColor();
+ final int color = Color.valueOf(taskBgColor == -1 ? Color.WHITE : taskBgColor).toArgb();
+ if (color != -1) {
+ mMenuView.setBackgroundColor(color);
+ } else {
+ mMenuView.setBackgroundColor(mBackgroundColor);
+ }
+ }
+
+ /**
+ * Sets the alpha of both this view and the task view.
+ */
+ public void setTaskViewAlpha(float alpha) {
+ if (mTaskView != null) {
+ mTaskView.setAlpha(alpha);
+ }
+ setAlpha(alpha);
+ }
+
+ /**
+ * Sets whether the surface displaying app content should sit on top. This is useful for
+ * ordering surfaces during animations. When content is drawn on top of the app (e.g. bubble
+ * being dragged out, the manage menu) this is set to false, otherwise it should be true.
+ */
+ public void setSurfaceZOrderedOnTop(boolean onTop) {
+ if (mTaskView == null) {
+ return;
+ }
+ mTaskView.setZOrderedOnTop(onTop, true /* allowDynamicChange */);
+ }
+
+ /**
+ * Sets whether the view is animating, in this case we won't change the content visibility
+ * until the animation is done.
+ */
+ public void setAnimating(boolean animating) {
+ mIsAnimating = animating;
+ // If we're done animating, apply the correct visibility.
+ if (!animating) {
+ setContentVisibility(mIsContentVisible);
+ }
+ }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/HandleView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/HandleView.java
new file mode 100644
index 0000000000000..9ee8a9d98aa1c
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/HandleView.java
@@ -0,0 +1,42 @@
+/*
+ * 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.content.Context;
+import android.view.Gravity;
+import android.widget.LinearLayout;
+
+/**
+ * Handle / menu view to show at the top of a bubble bar expanded view.
+ */
+public class HandleView extends LinearLayout {
+
+ // TODO(b/273307221): implement the manage menu in this view.
+ public HandleView(Context context) {
+ super(context);
+ setOrientation(LinearLayout.HORIZONTAL);
+ setGravity(Gravity.CENTER);
+ }
+
+ /**
+ * The menu extends past the top of the TaskView because of the rounded corners. This means
+ * to center content in the menu we must subtract the radius (i.e. the amount of space covered
+ * by TaskView).
+ */
+ public void setCornerRadius(float radius) {
+ setPadding(getPaddingLeft(), getPaddingTop(), getPaddingRight(), (int) radius);
+ }
+}