diff options
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); + } +} |