summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--libs/WindowManager/Shell/res/layout/bubble_bar_expanded_view.xml24
-rw-r--r--libs/WindowManager/Shell/res/values/dimen.xml2
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubble.java41
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java234
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleOverflow.kt5
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleViewInfoTask.java87
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleViewProvider.java26
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarAnimationHelper.java233
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarExpandedView.java267
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarLayerView.java212
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/HandleView.java42
11 files changed, 1102 insertions, 71 deletions
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 000000000000..681a52bea2b2
--- /dev/null
+++ b/libs/WindowManager/Shell/res/layout/bubble_bar_expanded_view.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ 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.
+ -->
+<com.android.wm.shell.bubbles.bar.BubbleBarExpandedView
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_height="wrap_content"
+ android:layout_width="wrap_content"
+ android:orientation="vertical"
+ android:id="@+id/bubble_bar_expanded_view">
+
+</com.android.wm.shell.bubbles.bar.BubbleBarExpandedView>
diff --git a/libs/WindowManager/Shell/res/values/dimen.xml b/libs/WindowManager/Shell/res/values/dimen.xml
index 9049ed574ba5..75e0d834d4f4 100644
--- a/libs/WindowManager/Shell/res/values/dimen.xml
+++ b/libs/WindowManager/Shell/res/values/dimen.xml
@@ -228,6 +228,8 @@
<dimen name="bubble_user_education_stack_padding">16dp</dimen>
<!-- Size of the bubble bar (height), should match transient_taskbar_size in Launcher. -->
<dimen name="bubblebar_size">72dp</dimen>
+ <!-- The size of the drag handle / menu shown along with a bubble bar expanded view. -->
+ <dimen name="bubblebar_expanded_view_menu_size">16dp</dimen>
<!-- Bottom and end margin for compat buttons. -->
<dimen name="compat_button_margin">24dp</dimen>
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 5f2b63089009..8f364b448bf2 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,8 @@ 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.bubbles.bar.BubbleBarLayerView;
import com.android.wm.shell.common.bubbles.BubbleInfo;
import java.io.PrintWriter;
@@ -87,8 +89,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,13 +339,19 @@ 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 +382,9 @@ public class Bubble implements BubbleViewProvider {
mExpandedView.cleanUpExpandedState();
mExpandedView = null;
}
+ if (mBubbleBarExpandedView != null) {
+ mBubbleBarExpandedView.cleanUpExpandedState();
+ }
if (mIntent != null) {
mIntent.unregisterCancelListener(mIntentCancelListener);
}
@@ -410,14 +431,16 @@ public class Bubble implements BubbleViewProvider {
* @param callback the callback to notify one the bubble is ready to be displayed.
* @param context the context for the bubble.
* @param controller the bubble controller.
- * @param stackView the stackView the bubble is eventually added to.
+ * @param stackView the view the bubble is added to, iff showing as floating.
+ * @param layerView the layer the bubble is added to, iff showing in the bubble bar.
* @param iconFactory the icon factory use to create images for the bubble.
* @param badgeIconFactory the icon factory to create app badges for the bubble.
*/
void inflate(BubbleViewInfoTask.Callback callback,
Context context,
BubbleController controller,
- BubbleStackView stackView,
+ @Nullable BubbleStackView stackView,
+ @Nullable BubbleBarLayerView layerView,
BubbleIconFactory iconFactory,
BubbleBadgeIconFactory badgeIconFactory,
boolean skipInflation) {
@@ -428,6 +451,7 @@ public class Bubble implements BubbleViewProvider {
context,
controller,
stackView,
+ layerView,
iconFactory,
badgeIconFactory,
skipInflation,
@@ -445,7 +469,7 @@ public class Bubble implements BubbleViewProvider {
}
boolean isInflated() {
- return mIconView != null && mExpandedView != null;
+ return (mIconView != null && mExpandedView != null) || mBubbleBarExpandedView != null;
}
void stopInflation() {
@@ -459,6 +483,7 @@ public class Bubble implements BubbleViewProvider {
if (!isInflated()) {
mIconView = info.imageView;
mExpandedView = info.expandedView;
+ mBubbleBarExpandedView = info.bubbleBarExpandedView;
}
mShortcutInfo = info.shortcutInfo;
@@ -469,7 +494,7 @@ public class Bubble implements BubbleViewProvider {
mFlyoutMessage = info.flyoutMessage;
mBadgeBitmap = info.badgeBitmap;
- mRawBadgeBitmap = info.mRawBadgeBitmap;
+ mRawBadgeBitmap = info.rawBadgeBitmap;
mBubbleBitmap = info.bubbleBitmap;
mDotColor = info.dotColor;
@@ -478,6 +503,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 +635,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/BubbleController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java
index 3dbb745f0c6c..cf0dbe340f2b 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
@@ -89,6 +89,7 @@ import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.statusbar.IStatusBarService;
import com.android.wm.shell.ShellTaskOrganizer;
import com.android.wm.shell.WindowManagerShellWrapper;
+import com.android.wm.shell.bubbles.bar.BubbleBarLayerView;
import com.android.wm.shell.common.DisplayController;
import com.android.wm.shell.common.ExternalInterfaceBinder;
import com.android.wm.shell.common.FloatingContentCoordinator;
@@ -201,6 +202,7 @@ public class BubbleController implements ConfigurationChangeListener,
private BubbleLogger mLogger;
private BubbleData mBubbleData;
@Nullable private BubbleStackView mStackView;
+ @Nullable private BubbleBarLayerView mLayerView;
private BubbleIconFactory mBubbleIconFactory;
private BubbleBadgeIconFactory mBubbleBadgeIconFactory;
private BubblePositioner mBubblePositioner;
@@ -260,6 +262,9 @@ public class BubbleController implements ConfigurationChangeListener,
/** Used to send bubble events to launcher. */
private Bubbles.BubbleStateListener mBubbleStateListener;
+ /** Used to send updates to the views from {@link #mBubbleDataListener}. */
+ private BubbleViewCallback mBubbleViewCallback;
+
public BubbleController(Context context,
ShellInit shellInit,
ShellCommandHandler shellCommandHandler,
@@ -341,6 +346,9 @@ public class BubbleController implements ConfigurationChangeListener,
}
protected void onInit() {
+ mBubbleViewCallback = isShowingAsBubbleBar()
+ ? mBubbleBarViewCallback
+ : mBubbleStackViewCallback;
mBubbleData.setListener(mBubbleDataListener);
mBubbleData.setSuppressionChangedListener(this::onBubbleMetadataFlagChanged);
mDataRepository.setSuppressionChangedListener(this::onBubbleMetadataFlagChanged);
@@ -546,7 +554,7 @@ public class BubbleController implements ConfigurationChangeListener,
}
private void openBubbleOverflow() {
- ensureStackViewCreated();
+ ensureBubbleViewsAndWindowCreated();
mBubbleData.setShowingOverflow(true);
mBubbleData.setSelectedBubble(mBubbleData.getOverflow());
mBubbleData.setExpanded(true);
@@ -586,7 +594,7 @@ public class BubbleController implements ConfigurationChangeListener,
expandStackAndSelectBubble(mNotifEntryToExpandOnShadeUnlock);
}
- updateStack();
+ updateBubbleViews();
}
@VisibleForTesting
@@ -682,39 +690,59 @@ public class BubbleController implements ConfigurationChangeListener,
return mBubblePositioner;
}
- Bubbles.SysuiProxy getSysuiProxy() {
+ public Bubbles.SysuiProxy getSysuiProxy() {
return mSysuiProxy;
}
/**
- * BubbleStackView is lazily created by this method the first time a Bubble is added. This
- * method initializes the stack view and adds it to window manager.
+ * The view and window for bubbles is lazily created by this method the first time a Bubble
+ * is added. Depending on the device state, this method will:
+ * - initialize a {@link BubbleStackView} and add it to window manager OR
+ * - initialize a {@link com.android.wm.shell.bubbles.bar.BubbleBarLayerView} and adds
+ * it to window manager.
*/
- private void ensureStackViewCreated() {
- if (mStackView == null) {
- mStackView = new BubbleStackView(
- mContext, this, mBubbleData, mSurfaceSynchronizer, mFloatingContentCoordinator,
- mMainExecutor);
- mStackView.onOrientationChanged();
- if (mExpandListener != null) {
- mStackView.setExpandListener(mExpandListener);
+ private void ensureBubbleViewsAndWindowCreated() {
+ mBubblePositioner.setShowingInBubbleBar(isShowingAsBubbleBar());
+ if (isShowingAsBubbleBar()) {
+ // When we're showing in launcher / bubble bar is enabled, we don't have bubble stack
+ // view, instead we just show the expanded bubble view as necessary. We still need a
+ // window to show this in, but we use a separate code path.
+ // TODO(b/273312602): consider foldables where we do need a stack view when folded
+ if (mLayerView == null) {
+ mLayerView = new BubbleBarLayerView(mContext, this);
+ }
+ } else {
+ if (mStackView == null) {
+ mStackView = new BubbleStackView(
+ mContext, this, mBubbleData, mSurfaceSynchronizer,
+ mFloatingContentCoordinator,
+ mMainExecutor);
+ mStackView.onOrientationChanged();
+ if (mExpandListener != null) {
+ mStackView.setExpandListener(mExpandListener);
+ }
+ mStackView.setUnbubbleConversationCallback(mSysuiProxy::onUnbubbleConversation);
}
- mStackView.setUnbubbleConversationCallback(mSysuiProxy::onUnbubbleConversation);
}
-
addToWindowManagerMaybe();
}
- /** Adds the BubbleStackView to the WindowManager if it's not already there. */
+ /** Adds the appropriate view to WindowManager if it's not already there. */
private void addToWindowManagerMaybe() {
- // If the stack is null, or already added, don't add it.
- if (mStackView == null || mAddedToWindowManager) {
+ // If already added, don't add it.
+ if (mAddedToWindowManager) {
+ return;
+ }
+ // If the appropriate view is null, don't add it.
+ if (isShowingAsBubbleBar() && mLayerView == null) {
+ return;
+ } else if (!isShowingAsBubbleBar() && mStackView == null) {
return;
}
mWmLayoutParams = new WindowManager.LayoutParams(
// Fill the screen so we can use translation animations to position the bubble
- // stack. We'll use touchable regions to ignore touches that are not on the bubbles
+ // views. We'll use touchable regions to ignore touches that are not on the bubbles
// themselves.
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT,
@@ -737,17 +765,30 @@ public class BubbleController implements ConfigurationChangeListener,
mAddedToWindowManager = true;
registerBroadcastReceiver();
mBubbleData.getOverflow().initialize(this);
- mWindowManager.addView(mStackView, mWmLayoutParams);
- mStackView.setOnApplyWindowInsetsListener((view, windowInsets) -> {
- if (!windowInsets.equals(mWindowInsets)) {
- mWindowInsets = windowInsets;
- mBubblePositioner.update();
- mStackView.onDisplaySizeChanged();
- }
- return windowInsets;
- });
+ // (TODO: b/273314541) some duplication in the inset listener
+ if (isShowingAsBubbleBar()) {
+ mWindowManager.addView(mLayerView, mWmLayoutParams);
+ mLayerView.setOnApplyWindowInsetsListener((view, windowInsets) -> {
+ if (!windowInsets.equals(mWindowInsets)) {
+ mWindowInsets = windowInsets;
+ mBubblePositioner.update();
+ mLayerView.onDisplaySizeChanged();
+ }
+ return windowInsets;
+ });
+ } else {
+ mWindowManager.addView(mStackView, mWmLayoutParams);
+ mStackView.setOnApplyWindowInsetsListener((view, windowInsets) -> {
+ if (!windowInsets.equals(mWindowInsets)) {
+ mWindowInsets = windowInsets;
+ mBubblePositioner.update();
+ mStackView.onDisplaySizeChanged();
+ }
+ return windowInsets;
+ });
+ }
} catch (IllegalStateException e) {
- // This means the stack has already been added. This shouldn't happen...
+ // This means the view has already been added. This shouldn't happen...
e.printStackTrace();
}
}
@@ -770,7 +811,7 @@ public class BubbleController implements ConfigurationChangeListener,
}
}
- /** Removes the BubbleStackView from the WindowManager if it's there. */
+ /** Removes any bubble views from the WindowManager that exist. */
private void removeFromWindowManagerMaybe() {
if (!mAddedToWindowManager) {
return;
@@ -791,8 +832,10 @@ public class BubbleController implements ConfigurationChangeListener,
if (mStackView != null) {
mWindowManager.removeView(mStackView);
mBubbleData.getOverflow().cleanUpExpandedState();
- } else {
- Log.w(TAG, "StackView added to WindowManager, but was null when removing!");
+ }
+ if (mLayerView != null) {
+ mWindowManager.removeView(mLayerView);
+ mBubbleData.getOverflow().cleanUpExpandedState();
}
} catch (IllegalArgumentException e) {
// This means the stack has already been removed - it shouldn't happen, but ignore if it
@@ -887,12 +930,22 @@ public class BubbleController implements ConfigurationChangeListener,
// Reload each bubble
for (Bubble b : mBubbleData.getBubbles()) {
- b.inflate(null /* callback */, mContext, this, mStackView, mBubbleIconFactory,
+ b.inflate(null /* callback */,
+ mContext,
+ this,
+ mStackView,
+ mLayerView,
+ mBubbleIconFactory,
mBubbleBadgeIconFactory,
false /* skipInflation */);
}
for (Bubble b : mBubbleData.getOverflowBubbles()) {
- b.inflate(null /* callback */, mContext, this, mStackView, mBubbleIconFactory,
+ b.inflate(null /* callback */,
+ mContext,
+ this,
+ mStackView,
+ mLayerView,
+ mBubbleIconFactory,
mBubbleBadgeIconFactory,
false /* skipInflation */);
}
@@ -959,7 +1012,7 @@ public class BubbleController implements ConfigurationChangeListener,
*/
@VisibleForTesting
public boolean hasBubbles() {
- if (mStackView == null) {
+ if (mStackView == null && mLayerView == null) {
return false;
}
return mBubbleData.hasBubbles() || mBubbleData.isShowingOverflow();
@@ -1166,7 +1219,12 @@ public class BubbleController implements ConfigurationChangeListener,
}
bubble.inflate(
(b) -> mBubbleData.overflowBubble(Bubbles.DISMISS_RELOAD_FROM_DISK, bubble),
- mContext, this, mStackView, mBubbleIconFactory, mBubbleBadgeIconFactory,
+ mContext,
+ this,
+ mStackView,
+ mLayerView,
+ mBubbleIconFactory,
+ mBubbleBadgeIconFactory,
true /* skipInflation */);
});
return null;
@@ -1238,10 +1296,11 @@ public class BubbleController implements ConfigurationChangeListener,
@VisibleForTesting
public void inflateAndAdd(Bubble bubble, boolean suppressFlyout, boolean showInShade) {
// Lazy init stack view when a bubble is created
- ensureStackViewCreated();
+ ensureBubbleViewsAndWindowCreated();
bubble.setInflateSynchronously(mInflateSynchronously);
bubble.inflate(b -> mBubbleData.notificationEntryUpdated(b, suppressFlyout, showInShade),
- mContext, this, mStackView, mBubbleIconFactory, mBubbleBadgeIconFactory,
+ mContext, this, mStackView, mLayerView,
+ mBubbleIconFactory, mBubbleBadgeIconFactory,
false /* skipInflation */);
}
@@ -1406,7 +1465,8 @@ public class BubbleController implements ConfigurationChangeListener,
});
}
- private final BubbleViewCallback mBubbleViewCallback = new BubbleViewCallback() {
+ /** When bubbles are floating, this will be used to notify the floating views. */
+ private final BubbleViewCallback mBubbleStackViewCallback = new BubbleViewCallback() {
@Override
public void removeBubble(Bubble removedBubble) {
if (mStackView != null) {
@@ -1458,6 +1518,62 @@ public class BubbleController implements ConfigurationChangeListener,
}
};
+ /** When bubbles are in the bubble bar, this will be used to notify bubble bar views. */
+ private final BubbleViewCallback mBubbleBarViewCallback = new BubbleViewCallback() {
+ @Override
+ public void removeBubble(Bubble removedBubble) {
+ if (mLayerView != null) {
+ // TODO: need to check if there's something that needs to happen here, e.g. if
+ // the currently selected & expanded bubble is removed?
+ }
+ }
+
+ @Override
+ public void addBubble(Bubble addedBubble) {
+ // Nothing to do for adds, these are handled by launcher / in the bubble bar.
+ }
+
+ @Override
+ public void updateBubble(Bubble updatedBubble) {
+ // Nothing to do for updates, these are handled by launcher / in the bubble bar.
+ }
+
+ @Override
+ public void bubbleOrderChanged(List<Bubble> bubbleOrder, boolean updatePointer) {
+ // Nothing to do for order changes, these are handled by launcher / in the bubble bar.
+ }
+
+ @Override
+ public void suppressionChanged(Bubble bubble, boolean isSuppressed) {
+ if (mLayerView != null) {
+ // TODO (b/273316505) handle suppression changes, although might not need to
+ // to do anything on the layerview side for this...
+ }
+ }
+
+ @Override
+ public void expansionChanged(boolean isExpanded) {
+ if (mLayerView != null) {
+ if (!isExpanded) {
+ mLayerView.collapse();
+ } else {
+ BubbleViewProvider selectedBubble = mBubbleData.getSelectedBubble();
+ if (selectedBubble != null) {
+ mLayerView.showExpandedView(selectedBubble);
+ }
+ }
+ }
+ }
+
+ @Override
+ public void selectionChanged(BubbleViewProvider selectedBubble) {
+ // Only need to update the layer view if we're currently expanded for selection changes.
+ if (mLayerView != null && isStackExpanded()) {
+ mLayerView.showExpandedView(selectedBubble);
+ }
+ }
+ };
+
@SuppressWarnings("FieldCanBeLocal")
private final BubbleData.Listener mBubbleDataListener = new BubbleData.Listener() {
@@ -1475,7 +1591,7 @@ public class BubbleController implements ConfigurationChangeListener,
+ " unsuppressed=" + (update.unsuppressedBubble != null));
}
- ensureStackViewCreated();
+ ensureBubbleViewsAndWindowCreated();
// Lazy load overflow bubbles from disk
loadOverflowBubblesFromDisk();
@@ -1570,7 +1686,7 @@ public class BubbleController implements ConfigurationChangeListener,
}
mSysuiProxy.notifyInvalidateNotifications("BubbleData.Listener.applyUpdate");
- updateStack();
+ updateBubbleViews();
// Update the cached state for queries from SysUI
mImpl.mCachedState.update(update);
@@ -1650,28 +1766,42 @@ public class BubbleController implements ConfigurationChangeListener,
/**
* Updates the visibility of the bubbles based on current state.
- * Does not un-bubble, just hides or un-hides.
- * Updates stack description for TalkBack focus.
- * Updates bubbles' icon views clickable states
+ * Does not un-bubble, just hides or un-hides the views themselves.
+ *
+ * Updates view description for TalkBack focus.
+ * Updates bubbles' icon views clickable states (when floating).
*/
- public void updateStack() {
- if (mStackView == null) {
+ public void updateBubbleViews() {
+ if (mStackView == null && mLayerView == null) {
return;
}
if (!mIsStatusBarShade) {
- // Bubbles don't appear over the locked shade.
- mStackView.setVisibility(INVISIBLE);
+ // Bubbles don't appear when the device is locked.
+ if (mStackView != null) {
+ mStackView.setVisibility(INVISIBLE);
+ }
+ if (mLayerView != null) {
+ mLayerView.setVisibility(INVISIBLE);
+ }
} else if (hasBubbles()) {
// If we're unlocked, show the stack if we have bubbles. If we don't have bubbles, the
// stack will be set to INVISIBLE in onAllBubblesAnimatedOut after the bubbles animate
// out.
- mStackView.setVisibility(VISIBLE);
+ if (mStackView != null) {
+ mStackView.setVisibility(VISIBLE);
+ }
+ if (mLayerView != null && isStackExpanded()) {
+ mLayerView.setVisibility(VISIBLE);
+ }
}
- mStackView.updateContentDescription();
-
- mStackView.updateBubblesAcessibillityStates();
+ if (mStackView != null) {
+ mStackView.updateContentDescription();
+ mStackView.updateBubblesAcessibillityStates();
+ } else if (mLayerView != null) {
+ // TODO(b/273313561): handle a11y for BubbleBarLayerView
+ }
}
@VisibleForTesting
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 eb7929b8ca54..6cdb80b2bb93 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/BubbleViewInfoTask.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleViewInfoTask.java
index f437553337ef..1a97c0504b37 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
@@ -22,6 +22,7 @@ import static com.android.wm.shell.bubbles.BubbleDebugConfig.TAG_BUBBLES;
import static com.android.wm.shell.bubbles.BubbleDebugConfig.TAG_WITH_CLASS_NAME;
import android.annotation.NonNull;
+import android.annotation.Nullable;
import android.content.Context;
import android.content.Intent;
import android.content.pm.ApplicationInfo;
@@ -38,12 +39,12 @@ import android.util.Log;
import android.util.PathParser;
import android.view.LayoutInflater;
-import androidx.annotation.Nullable;
-import androidx.annotation.VisibleForTesting;
-
+import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.graphics.ColorUtils;
import com.android.launcher3.icons.BitmapInfo;
import com.android.wm.shell.R;
+import com.android.wm.shell.bubbles.bar.BubbleBarExpandedView;
+import com.android.wm.shell.bubbles.bar.BubbleBarLayerView;
import java.lang.ref.WeakReference;
import java.util.Objects;
@@ -70,6 +71,7 @@ public class BubbleViewInfoTask extends AsyncTask<Void, Void, BubbleViewInfoTask
private WeakReference<Context> mContext;
private WeakReference<BubbleController> mController;
private WeakReference<BubbleStackView> mStackView;
+ private WeakReference<BubbleBarLayerView> mLayerView;
private BubbleIconFactory mIconFactory;
private BubbleBadgeIconFactory mBadgeIconFactory;
private boolean mSkipInflation;
@@ -83,7 +85,8 @@ public class BubbleViewInfoTask extends AsyncTask<Void, Void, BubbleViewInfoTask
BubbleViewInfoTask(Bubble b,
Context context,
BubbleController controller,
- BubbleStackView stackView,
+ @Nullable BubbleStackView stackView,
+ @Nullable BubbleBarLayerView layerView,
BubbleIconFactory factory,
BubbleBadgeIconFactory badgeFactory,
boolean skipInflation,
@@ -93,6 +96,7 @@ public class BubbleViewInfoTask extends AsyncTask<Void, Void, BubbleViewInfoTask
mContext = new WeakReference<>(context);
mController = new WeakReference<>(controller);
mStackView = new WeakReference<>(stackView);
+ mLayerView = new WeakReference<>(layerView);
mIconFactory = factory;
mBadgeIconFactory = badgeFactory;
mSkipInflation = skipInflation;
@@ -102,8 +106,13 @@ public class BubbleViewInfoTask extends AsyncTask<Void, Void, BubbleViewInfoTask
@Override
protected BubbleViewInfo doInBackground(Void... voids) {
- return BubbleViewInfo.populate(mContext.get(), mController.get(), mStackView.get(),
- mIconFactory, mBadgeIconFactory, mBubble, mSkipInflation);
+ if (mController.get().isShowingAsBubbleBar()) {
+ return BubbleViewInfo.populateForBubbleBar(mContext.get(), mController.get(),
+ mLayerView.get(), mBadgeIconFactory, mBubble, mSkipInflation);
+ } else {
+ return BubbleViewInfo.populate(mContext.get(), mController.get(), mStackView.get(),
+ mIconFactory, mBadgeIconFactory, mBubble, mSkipInflation);
+ }
}
@Override
@@ -124,16 +133,70 @@ public class BubbleViewInfoTask extends AsyncTask<Void, Void, BubbleViewInfoTask
*/
@VisibleForTesting
public static class BubbleViewInfo {
- BadgedImageView imageView;
- BubbleExpandedView expandedView;
+ // TODO(b/273312602): for foldables it might make sense to populate all of the views
+
+ // Always populated
ShortcutInfo shortcutInfo;
String appName;
- Bitmap bubbleBitmap;
- Bitmap badgeBitmap;
- Bitmap mRawBadgeBitmap;
+ Bitmap rawBadgeBitmap;
+
+ // Only populated when showing in taskbar
+ BubbleBarExpandedView bubbleBarExpandedView;
+
+ // These are only populated when not showing in taskbar
+ BadgedImageView imageView;
+ BubbleExpandedView expandedView;
int dotColor;
Path dotPath;
Bubble.FlyoutMessage flyoutMessage;
+ Bitmap bubbleBitmap;
+ Bitmap badgeBitmap;
+
+ @Nullable
+ public static BubbleViewInfo populateForBubbleBar(Context c, BubbleController controller,
+ BubbleBarLayerView layerView, BubbleBadgeIconFactory badgeIconFactory, Bubble b,
+ boolean skipInflation) {
+ BubbleViewInfo info = new BubbleViewInfo();
+
+ if (!skipInflation && !b.isInflated()) {
+ LayoutInflater inflater = LayoutInflater.from(c);
+ info.bubbleBarExpandedView = (BubbleBarExpandedView) inflater.inflate(
+ R.layout.bubble_bar_expanded_view, layerView, false /* attachToRoot */);
+ info.bubbleBarExpandedView.initialize(controller);
+ }
+
+ if (b.getShortcutInfo() != null) {
+ info.shortcutInfo = b.getShortcutInfo();
+ }
+
+ // App name & app icon
+ PackageManager pm = BubbleController.getPackageManagerForUser(c,
+ b.getUser().getIdentifier());
+ ApplicationInfo appInfo;
+ Drawable badgedIcon;
+ Drawable appIcon;
+ try {
+ appInfo = pm.getApplicationInfo(
+ b.getPackageName(),
+ PackageManager.MATCH_UNINSTALLED_PACKAGES
+ | PackageManager.MATCH_DISABLED_COMPONENTS
+ | PackageManager.MATCH_DIRECT_BOOT_UNAWARE
+ | PackageManager.MATCH_DIRECT_BOOT_AWARE);
+ if (appInfo != null) {
+ info.appName = String.valueOf(pm.getApplicationLabel(appInfo));
+ }
+ appIcon = pm.getApplicationIcon(b.getPackageName());
+ badgedIcon = pm.getUserBadgedIcon(appIcon, b.getUser());
+ } catch (PackageManager.NameNotFoundException exception) {
+ // If we can't find package... don't think we should show the bubble.
+ Log.w(TAG, "Unable to find package: " + b.getPackageName());
+ return null;
+ }
+
+ info.rawBadgeBitmap = badgeIconFactory.getBadgeBitmap(badgedIcon, false).icon;
+
+ return info;
+ }
@VisibleForTesting
@Nullable
@@ -195,7 +258,7 @@ public class BubbleViewInfoTask extends AsyncTask<Void, Void, BubbleViewInfoTask
b.isImportantConversation());
info.badgeBitmap = badgeBitmapInfo.icon;
// Raw badge bitmap never includes the important conversation ring
- info.mRawBadgeBitmap = b.isImportantConversation()
+ info.rawBadgeBitmap = b.isImportantConversation()
? badgeIconFactory.getBadgeBitmap(badgedIcon, false).icon
: badgeBitmapInfo.icon;
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 3f6d41bb2b68..6bdc3b9f5aec 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/BubbleBarAnimationHelper.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarAnimationHelper.java
new file mode 100644
index 000000000000..23f65f943aa4
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarAnimationHelper.java
@@ -0,0 +1,233 @@
+/*
+ * 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 static android.view.View.VISIBLE;
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.ValueAnimator;
+import android.content.Context;
+import android.graphics.PointF;
+import android.util.Log;
+import android.widget.FrameLayout;
+
+import com.android.wm.shell.animation.Interpolators;
+import com.android.wm.shell.animation.PhysicsAnimator;
+import com.android.wm.shell.bubbles.BubblePositioner;
+import com.android.wm.shell.bubbles.BubbleViewProvider;
+import com.android.wm.shell.bubbles.animation.AnimatableScaleMatrix;
+
+/**
+ * Helper class to animate a {@link BubbleBarExpandedView} on a bubble.
+ */
+public class BubbleBarAnimationHelper {
+
+ private static final String TAG = BubbleBarAnimationHelper.class.getSimpleName();
+
+ private static final float EXPANDED_VIEW_ANIMATE_SCALE_AMOUNT = 0.1f;
+ private static final float EXPANDED_VIEW_ANIMATE_OUT_SCALE_AMOUNT = .75f;
+ private static final int EXPANDED_VIEW_ALPHA_ANIMATION_DURATION = 150;
+
+ /** Spring config for the expanded view scale-in animation. */
+ private final PhysicsAnimator.SpringConfig mScaleInSpringConfig =
+ new PhysicsAnimator.SpringConfig(300f, 0.9f);
+
+ /** Spring config for the expanded view scale-out animation. */
+ private final PhysicsAnimator.SpringConfig mScaleOutSpringConfig =
+ new PhysicsAnimator.SpringConfig(900f, 1f);
+
+ /** Matrix used to scale the expanded view container with a given pivot point. */
+ private final AnimatableScaleMatrix mExpandedViewContainerMatrix = new AnimatableScaleMatrix();
+
+ /** Animator for animating the expanded view's alpha (including the TaskView inside it). */
+ private final ValueAnimator mExpandedViewAlphaAnimator = ValueAnimator.ofFloat(0f, 1f);
+
+ private final Context mContext;
+ private final BubbleBarLayerView mLayerView;
+ private final BubblePositioner mPositioner;
+
+ private BubbleViewProvider mExpandedBubble;
+ private boolean mIsExpanded = false;
+
+ public BubbleBarAnimationHelper(Context context,
+ BubbleBarLayerView bubbleBarLayerView,
+ BubblePositioner positioner) {
+ mContext = context;
+ mLayerView = bubbleBarLayerView;
+ mPositioner = positioner;
+
+ mExpandedViewAlphaAnimator.setDuration(EXPANDED_VIEW_ALPHA_ANIMATION_DURATION);
+ mExpandedViewAlphaAnimator.setInterpolator(Interpolators.PANEL_CLOSE_ACCELERATED);
+ mExpandedViewAlphaAnimator.addListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationStart(Animator animation) {
+ if (mExpandedBubble != null && mExpandedBubble.getBubbleBarExpandedView() != null) {
+ // We need to be Z ordered on top in order for alpha animations to work.
+ mExpandedBubble.getBubbleBarExpandedView().setSurfaceZOrderedOnTop(true);
+ mExpandedBubble.getBubbleBarExpandedView().setAnimating(true);
+ }
+ }
+
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ if (mExpandedBubble != null && mExpandedBubble.getBubbleBarExpandedView() != null) {
+ // The surface needs to be Z ordered on top for alpha values to work on the
+ // TaskView, and if we're temporarily hidden, we are still on the screen
+ // with alpha = 0f until we animate back. Stay Z ordered on top so the alpha
+ // = 0f remains in effect.
+ if (mIsExpanded) {
+ mExpandedBubble.getBubbleBarExpandedView().setSurfaceZOrderedOnTop(false);
+ }
+
+ mExpandedBubble.getBubbleBarExpandedView().setContentVisibility(mIsExpanded);
+ mExpandedBubble.getBubbleBarExpandedView().setAnimating(false);
+ }
+ }
+ });
+ mExpandedViewAlphaAnimator.addUpdateListener(valueAnimator -> {
+ if (mExpandedBubble != null && mExpandedBubble.getBubbleBarExpandedView() != null) {
+ float alpha = (float) valueAnimator.getAnimatedValue();
+ mExpandedBubble.getBubbleBarExpandedView().setTaskViewAlpha(alpha);
+ mExpandedBubble.getBubbleBarExpandedView().setAlpha(alpha);
+ }
+ });
+ }
+
+ /**
+ * Animates the provided bubble's expanded view to the expanded state.
+ */
+ public void animateExpansion(BubbleViewProvider expandedBubble) {
+ mExpandedBubble = expandedBubble;
+ if (mExpandedBubble == null) {
+ return;
+ }
+ BubbleBarExpandedView bev = mExpandedBubble.getBubbleBarExpandedView();
+ if (bev == null) {
+ return;
+ }
+ mIsExpanded = true;
+
+ mExpandedViewContainerMatrix.setScaleX(0f);
+ mExpandedViewContainerMatrix.setScaleY(0f);
+
+ updateExpandedView();
+ bev.setAnimating(true);
+ bev.setContentVisibility(false);
+ bev.setAlpha(0f);
+ bev.setTaskViewAlpha(0f);
+ bev.setVisibility(VISIBLE);
+
+ // Set the pivot point for the scale, so the view animates out from the bubble bar.
+ PointF bubbleBarPosition = mPositioner.getBubbleBarPosition();
+ mExpandedViewContainerMatrix.setScale(
+ 1f - EXPANDED_VIEW_ANIMATE_SCALE_AMOUNT,
+ 1f - EXPANDED_VIEW_ANIMATE_SCALE_AMOUNT,
+ bubbleBarPosition.x,
+ bubbleBarPosition.y);
+
+ bev.setAnimationMatrix(mExpandedViewContainerMatrix);
+
+ mExpandedViewAlphaAnimator.start();
+
+ PhysicsAnimator.getInstance(mExpandedViewContainerMatrix).cancel();
+ PhysicsAnimator.getInstance(mExpandedViewContainerMatrix)
+ .spring(AnimatableScaleMatrix.SCALE_X,
+ AnimatableScaleMatrix.getAnimatableValueForScaleFactor(1f),
+ mScaleInSpringConfig)
+ .spring(AnimatableScaleMatrix.SCALE_Y,
+ AnimatableScaleMatrix.getAnimatableValueForScaleFactor(1f),
+ mScaleInSpringConfig)
+ .addUpdateListener((target, values) -> {
+ mExpandedBubble.getBubbleBarExpandedView().setAnimationMatrix(
+ mExpandedViewContainerMatrix);
+ })
+ .withEndActions(() -> {
+ bev.setAnimationMatrix(null);
+ updateExpandedView();
+ bev.setSurfaceZOrderedOnTop(false);
+ })
+ .start();
+ }
+
+ /**
+ * Collapses the currently expanded bubble.
+ *
+ * @param endRunnable a runnable to run at the end of the animation.
+ */
+ public void animateCollapse(Runnable endRunnable) {
+ mIsExpanded = false;
+ if (mExpandedBubble == null || mExpandedBubble.getBubbleBarExpandedView() == null) {
+ Log.w(TAG, "Trying to animate collapse without a bubble");
+ return;
+ }
+
+ mExpandedViewContainerMatrix.setScaleX(1f);
+ mExpandedViewContainerMatrix.setScaleY(1f);
+
+ PhysicsAnimator.getInstance(mExpandedViewContainerMatrix).cancel();
+ PhysicsAnimator.getInstance(mExpandedViewContainerMatrix)
+ .spring(AnimatableScaleMatrix.SCALE_X,
+ AnimatableScaleMatrix.getAnimatableValueForScaleFactor(
+ EXPANDED_VIEW_ANIMATE_OUT_SCALE_AMOUNT),
+ mScaleOutSpringConfig)
+ .spring(AnimatableScaleMatrix.SCALE_Y,
+ AnimatableScaleMatrix.getAnimatableValueForScaleFactor(
+ EXPANDED_VIEW_ANIMATE_OUT_SCALE_AMOUNT),
+ mScaleOutSpringConfig)
+ .addUpdateListener((target, values) -> {
+ if (mExpandedBubble != null
+ && mExpandedBubble.getBubbleBarExpandedView() != null) {
+ mExpandedBubble.getBubbleBarExpandedView().setAnimationMatrix(
+ mExpandedViewContainerMatrix);
+ }
+ })
+ .withEndActions(() -> {
+ if (mExpandedBubble != null
+ && mExpandedBubble.getBubbleBarExpandedView() != null) {
+ mExpandedBubble.getBubbleBarExpandedView().setAnimationMatrix(null);
+ }
+ if (endRunnable != null) {
+ endRunnable.run();
+ }
+ })
+ .start();
+ mExpandedViewAlphaAnimator.reverse();
+ }
+
+ private void updateExpandedView() {
+ if (mExpandedBubble == null || mExpandedBubble.getBubbleBarExpandedView() == null) {
+ Log.w(TAG, "Trying to update the expanded view without a bubble");
+ return;
+ }
+ BubbleBarExpandedView bbev = mExpandedBubble.getBubbleBarExpandedView();
+
+ final int padding = mPositioner.getBubbleBarExpandedViewPadding();
+ final int width = mPositioner.getExpandedViewWidthForBubbleBar();
+ final int height = mPositioner.getExpandedViewHeightForBubbleBar();
+ FrameLayout.LayoutParams lp = (FrameLayout.LayoutParams) bbev.getLayoutParams();
+ lp.width = width;
+ lp.height = height;
+ bbev.setLayoutParams(lp);
+ if (mLayerView.isOnLeft()) {
+ bbev.setX(mPositioner.getInsets().left + padding);
+ } else {
+ bbev.setX(mPositioner.getAvailableRect().width() - width - padding);
+ }
+ bbev.setY(mPositioner.getInsets().top + padding);
+ bbev.updateLocation();
+ }
+}
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 000000000000..b8f049becb6f
--- /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/BubbleBarLayerView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarLayerView.java
new file mode 100644
index 000000000000..b1a725b6e5c4
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarLayerView.java
@@ -0,0 +1,212 @@
+/*
+ * 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 static com.android.wm.shell.animation.Interpolators.ALPHA_IN;
+import static com.android.wm.shell.animation.Interpolators.ALPHA_OUT;
+
+import android.annotation.Nullable;
+import android.content.Context;
+import android.graphics.Rect;
+import android.graphics.Region;
+import android.graphics.drawable.ColorDrawable;
+import android.view.View;
+import android.view.ViewTreeObserver;
+import android.widget.FrameLayout;
+
+import com.android.wm.shell.bubbles.BubbleController;
+import com.android.wm.shell.bubbles.BubblePositioner;
+import com.android.wm.shell.bubbles.BubbleViewProvider;
+
+/**
+ * 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
+ * launcher in the bubble bar. This view does not show a stack of bubbles that can be moved around
+ * on screen and instead shows & animates the expanded bubble for the bubble bar.
+ */
+public class BubbleBarLayerView extends FrameLayout
+ implements ViewTreeObserver.OnComputeInternalInsetsListener {
+
+ private static final String TAG = BubbleBarLayerView.class.getSimpleName();
+
+ private static final float SCRIM_ALPHA = 0.2f;
+
+ private final BubbleController mBubbleController;
+ private final BubblePositioner mPositioner;
+ private final BubbleBarAnimationHelper mAnimationHelper;
+ private final View mScrimView;
+
+ @Nullable
+ private BubbleViewProvider mExpandedBubble;
+ private BubbleBarExpandedView mExpandedView;
+
+ // 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. */
+ private boolean mOnLeft = false;
+
+ /** Whether a bubble is expanded. */
+ private boolean mIsExpanded = false;
+
+ private final Region mTouchableRegion = new Region();
+ private final Rect mTempRect = new Rect();
+
+ public BubbleBarLayerView(Context context, BubbleController controller) {
+ super(context);
+ mBubbleController = controller;
+ mPositioner = mBubbleController.getPositioner();
+
+ mAnimationHelper = new BubbleBarAnimationHelper(context,
+ this, mPositioner);
+
+ mScrimView = new View(getContext());
+ mScrimView.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO);
+ mScrimView.setBackgroundDrawable(new ColorDrawable(
+ getResources().getColor(android.R.color.system_neutral1_1000)));
+ addView(mScrimView);
+ mScrimView.setAlpha(0f);
+ mScrimView.setBackgroundDrawable(new ColorDrawable(
+ getResources().getColor(android.R.color.system_neutral1_1000)));
+
+ setOnClickListener(view -> {
+ mBubbleController.collapseStack();
+ });
+ }
+
+ @Override
+ protected void onAttachedToWindow() {
+ super.onAttachedToWindow();
+ mPositioner.update();
+ getViewTreeObserver().addOnComputeInternalInsetsListener(this);
+ }
+
+ @Override
+ protected void onDetachedFromWindow() {
+ super.onDetachedFromWindow();
+ getViewTreeObserver().removeOnComputeInternalInsetsListener(this);
+
+ if (mExpandedView != null) {
+ removeView(mExpandedView);
+ mExpandedView = null;
+ }
+ }
+
+ @Override
+ public void onComputeInternalInsets(ViewTreeObserver.InternalInsetsInfo inoutInfo) {
+ inoutInfo.setTouchableInsets(ViewTreeObserver.InternalInsetsInfo.TOUCHABLE_INSETS_REGION);
+ mTouchableRegion.setEmpty();
+ getTouchableRegion(mTouchableRegion);
+ inoutInfo.touchableRegion.set(mTouchableRegion);
+ }
+
+ /** Updates the sizes of any displaying expanded view. */
+ public void onDisplaySizeChanged() {
+ if (mIsExpanded && mExpandedView != null) {
+ updateExpandedView();
+ }
+ }
+
+ /** Whether the stack of bubbles is expanded or not. */
+ public boolean isExpanded() {
+ return mIsExpanded;
+ }
+
+ // (TODO: b/273310265): BubblePositioner should be source of truth when this work is done.
+ /** Whether the expanded view is positioned on the left or right side of the screen. */
+ public boolean isOnLeft() {
+ return mOnLeft;
+ }
+
+ /** Shows the expanded view of the provided bubble. */
+ public void showExpandedView(BubbleViewProvider b) {
+ BubbleBarExpandedView expandedView = b.getBubbleBarExpandedView();
+ if (expandedView == null) {
+ return;
+ }
+ if (mExpandedBubble != null && !b.getKey().equals(mExpandedBubble.getKey())) {
+ removeView(mExpandedView);
+ mExpandedView = null;
+ }
+ if (mExpandedView == null) {
+ mExpandedBubble = b;
+ mExpandedView = expandedView;
+ final int width = mPositioner.getExpandedViewWidthForBubbleBar();
+ final int height = mPositioner.getExpandedViewHeightForBubbleBar();
+ mExpandedView.setVisibility(GONE);
+ addView(mExpandedView, new FrameLayout.LayoutParams(width, height));
+ }
+
+ mIsExpanded = true;
+ mBubbleController.getSysuiProxy().onStackExpandChanged(true);
+ mAnimationHelper.animateExpansion(mExpandedBubble);
+ showScrim(true);
+ }
+
+ /** Collapses any showing expanded view */
+ public void collapse() {
+ mIsExpanded = false;
+ final BubbleBarExpandedView viewToRemove = mExpandedView;
+ mAnimationHelper.animateCollapse(() -> removeView(viewToRemove));
+ mBubbleController.getSysuiProxy().onStackExpandChanged(false);
+ mExpandedView = null;
+ showScrim(false);
+ }
+
+ /** Updates the expanded view size and position. */
+ private void updateExpandedView() {
+ if (mExpandedView == null) return;
+ final int padding = mPositioner.getBubbleBarExpandedViewPadding();
+ final int width = mPositioner.getExpandedViewWidthForBubbleBar();
+ final int height = mPositioner.getExpandedViewHeightForBubbleBar();
+ FrameLayout.LayoutParams lp = (LayoutParams) mExpandedView.getLayoutParams();
+ lp.width = width;
+ lp.height = height;
+ mExpandedView.setLayoutParams(lp);
+ if (mOnLeft) {
+ mExpandedView.setX(mPositioner.getInsets().left + padding);
+ } else {
+ mExpandedView.setX(mPositioner.getAvailableRect().width() - width - padding);
+ }
+ mExpandedView.setY(mPositioner.getInsets().top + padding);
+ mExpandedView.updateLocation();
+ }
+
+ private void showScrim(boolean show) {
+ if (show) {
+ mScrimView.animate()
+ .setInterpolator(ALPHA_IN)
+ .alpha(SCRIM_ALPHA)
+ .start();
+ } else {
+ mScrimView.animate()
+ .alpha(0f)
+ .setInterpolator(ALPHA_OUT)
+ .start();
+ }
+ }
+
+ /**
+ * Fills in the touchable region for expanded view. This is used by window manager to
+ * decide which touch events go to the expanded view.
+ */
+ private void getTouchableRegion(Region outRegion) {
+ mTempRect.setEmpty();
+ if (mIsExpanded) {
+ getBoundsOnScreen(mTempRect);
+ outRegion.op(mTempRect, Region.Op.UNION);
+ }
+ }
+}
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 000000000000..9ee8a9d98aa1
--- /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);
+ }
+}