diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarController.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarController.java new file mode 100644 index 0000000000..228aabd7ae --- /dev/null +++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarController.java @@ -0,0 +1,435 @@ +/* + * 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.launcher3.taskbar.bubbles; + +import static android.content.pm.LauncherApps.ShortcutQuery.FLAG_GET_PERSONS_DATA; +import static android.content.pm.LauncherApps.ShortcutQuery.FLAG_MATCH_CACHED; +import static android.content.pm.LauncherApps.ShortcutQuery.FLAG_MATCH_DYNAMIC; +import static android.content.pm.LauncherApps.ShortcutQuery.FLAG_MATCH_PINNED_BY_ANY_LAUNCHER; +import static android.os.Process.THREAD_PRIORITY_BACKGROUND; + +import static com.android.launcher3.icons.FastBitmapDrawable.WHITE_SCRIM_ALPHA; +import static com.android.launcher3.util.Executors.MAIN_EXECUTOR; +import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_BOUNCER_SHOWING; +import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_IME_SHOWING; +import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_IME_SWITCHER_SHOWING; +import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_NOTIFICATION_PANEL_EXPANDED; +import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_QUICK_SETTINGS_EXPANDED; +import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_STATUS_BAR_KEYGUARD_SHOWING; +import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_STATUS_BAR_KEYGUARD_SHOWING_OCCLUDED; + +import android.annotation.BinderThread; +import android.annotation.Nullable; +import android.content.Context; +import android.content.pm.ApplicationInfo; +import android.content.pm.LauncherApps; +import android.content.pm.PackageManager; +import android.content.pm.ShortcutInfo; +import android.graphics.Bitmap; +import android.graphics.Color; +import android.graphics.Matrix; +import android.graphics.Path; +import android.graphics.drawable.Drawable; +import android.os.Bundle; +import android.os.SystemProperties; +import android.os.UserHandle; +import android.util.ArrayMap; +import android.util.Log; +import android.util.PathParser; +import android.view.LayoutInflater; + +import com.android.internal.graphics.ColorUtils; +import com.android.launcher3.R; +import com.android.launcher3.icons.BitmapInfo; +import com.android.launcher3.icons.BubbleIconFactory; +import com.android.launcher3.shortcuts.ShortcutRequest; +import com.android.launcher3.taskbar.TaskbarControllers; +import com.android.launcher3.util.Executors.SimpleThreadFactory; +import com.android.quickstep.SystemUiProxy; +import com.android.wm.shell.bubbles.IBubblesListener; +import com.android.wm.shell.common.bubbles.BubbleBarUpdate; +import com.android.wm.shell.common.bubbles.BubbleInfo; +import com.android.wm.shell.common.bubbles.RemovedBubble; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.concurrent.Executor; +import java.util.concurrent.Executors; + +/** + * This registers a listener with SysUIProxy to get information about changes to the bubble + * stack state from WMShell (SysUI). The controller is also responsible for loading the necessary + * information to render each of the bubbles & dispatches changes to + * {@link BubbleBarViewController} which will then update {@link BubbleBarView} as needed. + * + * For details around the behavior of the bubble bar, see {@link BubbleBarView}. + */ +public class BubbleBarController extends IBubblesListener.Stub { + + private static final String TAG = BubbleBarController.class.getSimpleName(); + private static final boolean DEBUG = false; + + // Whether bubbles are showing in the bubble bar from launcher + public static final boolean BUBBLE_BAR_ENABLED = + SystemProperties.getBoolean("persist.wm.debug.bubble_bar", false); + + private static final int MASK_HIDE_BUBBLE_BAR = SYSUI_STATE_BOUNCER_SHOWING + | SYSUI_STATE_STATUS_BAR_KEYGUARD_SHOWING + | SYSUI_STATE_STATUS_BAR_KEYGUARD_SHOWING_OCCLUDED + | SYSUI_STATE_IME_SHOWING + | SYSUI_STATE_NOTIFICATION_PANEL_EXPANDED + | SYSUI_STATE_QUICK_SETTINGS_EXPANDED + | SYSUI_STATE_IME_SWITCHER_SHOWING; + + private static final int MASK_HIDE_HANDLE_VIEW = SYSUI_STATE_BOUNCER_SHOWING + | SYSUI_STATE_STATUS_BAR_KEYGUARD_SHOWING + | SYSUI_STATE_STATUS_BAR_KEYGUARD_SHOWING_OCCLUDED; + + private static final int MASK_SYSUI_LOCKED = SYSUI_STATE_BOUNCER_SHOWING + | SYSUI_STATE_STATUS_BAR_KEYGUARD_SHOWING + | SYSUI_STATE_STATUS_BAR_KEYGUARD_SHOWING_OCCLUDED; + + private final Context mContext; + private final BubbleBarView mBarView; + private final ArrayMap mBubbles = new ArrayMap<>(); + + private static final Executor BUBBLE_STATE_EXECUTOR = Executors.newSingleThreadExecutor( + new SimpleThreadFactory("BubbleStateUpdates-", THREAD_PRIORITY_BACKGROUND)); + private final Executor mMainExecutor; + private final LauncherApps mLauncherApps; + private final BubbleIconFactory mIconFactory; + + private BubbleBarBubble mSelectedBubble; + + private BubbleBarViewController mBubbleBarViewController; + private BubbleStashController mBubbleStashController; + private BubbleStashedHandleViewController mBubbleStashedHandleViewController; + + /** + * Similar to {@link BubbleBarUpdate} but rather than {@link BubbleInfo}s it uses + * {@link BubbleBarBubble}s so that it can be used to update the views. + */ + private static class BubbleBarViewUpdate { + boolean expandedChanged; + boolean expanded; + String selectedBubbleKey; + String suppressedBubbleKey; + String unsuppressedBubbleKey; + List removedBubbles; + List bubbleKeysInOrder; + + // These need to be loaded in the background + BubbleBarBubble addedBubble; + BubbleBarBubble updatedBubble; + List currentBubbles; + + BubbleBarViewUpdate(BubbleBarUpdate update) { + expandedChanged = update.expandedChanged; + expanded = update.expanded; + selectedBubbleKey = update.selectedBubbleKey; + suppressedBubbleKey = update.suppressedBubbleKey; + unsuppressedBubbleKey = update.unsupressedBubbleKey; + removedBubbles = update.removedBubbles; + bubbleKeysInOrder = update.bubbleKeysInOrder; + } + } + + public BubbleBarController(Context context, BubbleBarView bubbleView) { + mContext = context; + mBarView = bubbleView; // Need the view for inflating bubble views. + + // TODO: register the listener with SysUiProxu + mMainExecutor = MAIN_EXECUTOR; + mLauncherApps = context.getSystemService(LauncherApps.class); + mIconFactory = new BubbleIconFactory(context, + context.getResources().getDimensionPixelSize(R.dimen.bubblebar_icon_size), + context.getResources().getDimensionPixelSize(R.dimen.bubblebar_badge_size), + context.getResources().getColor(R.color.important_conversation), + context.getResources().getDimensionPixelSize( + com.android.internal.R.dimen.importance_ring_stroke_width)); + } + + public void onDestroy() { + // TODO: unregister the listener with SysUiProxy + } + + public void init(TaskbarControllers controllers, BubbleControllers bubbleControllers) { + mBubbleBarViewController = bubbleControllers.bubbleBarViewController; + mBubbleStashController = bubbleControllers.bubbleStashController; + mBubbleStashedHandleViewController = bubbleControllers.bubbleStashedHandleViewController; + + bubbleControllers.runAfterInit(() -> { + mBubbleBarViewController.setHiddenForBubbles(!BUBBLE_BAR_ENABLED); + mBubbleStashedHandleViewController.setHiddenForBubbles(!BUBBLE_BAR_ENABLED); + }); + } + + /** + * Updates the bubble bar, handle bar, and stash controllers based on sysui state flags. + */ + public void updateStateForSysuiFlags(int flags) { + boolean hideBubbleBar = (flags & MASK_HIDE_BUBBLE_BAR) != 0; + mBubbleBarViewController.setHiddenForSysui(hideBubbleBar); + + boolean hideHandleView = (flags & MASK_HIDE_HANDLE_VIEW) != 0; + mBubbleStashedHandleViewController.setHiddenForSysui(hideHandleView); + + boolean sysuiLocked = (flags & MASK_SYSUI_LOCKED) != 0; + mBubbleStashController.onSysuiLockedStateChange(sysuiLocked); + } + + // + // Bubble data changes + // + + @BinderThread + @Override + public void onBubbleStateChange(Bundle bundle) { + bundle.setClassLoader(BubbleBarUpdate.class.getClassLoader()); + BubbleBarUpdate update = bundle.getParcelable("update", BubbleBarUpdate.class); + BubbleBarViewUpdate viewUpdate = new BubbleBarViewUpdate(update); + if (update.addedBubble != null + || update.updatedBubble != null + || !update.currentBubbleList.isEmpty()) { + // We have bubbles to load + BUBBLE_STATE_EXECUTOR.execute(() -> { + if (update.addedBubble != null) { + viewUpdate.addedBubble = populateBubble(update.addedBubble, mContext, mBarView); + } + if (update.updatedBubble != null) { + viewUpdate.updatedBubble = + populateBubble(update.updatedBubble, mContext, mBarView); + } + if (update.currentBubbleList != null && !update.currentBubbleList.isEmpty()) { + List currentBubbles = new ArrayList<>(); + for (int i = 0; i < update.currentBubbleList.size(); i++) { + BubbleBarBubble b = + populateBubble(update.currentBubbleList.get(i), mContext, mBarView); + currentBubbles.add(b); + } + viewUpdate.currentBubbles = currentBubbles; + } + mMainExecutor.execute(() -> applyViewChanges(viewUpdate)); + }); + } else { + // No bubbles to load, immediately apply the changes. + BUBBLE_STATE_EXECUTOR.execute( + () -> mMainExecutor.execute(() -> applyViewChanges(viewUpdate))); + } + } + + private void applyViewChanges(BubbleBarViewUpdate update) { + final boolean isCollapsed = (update.expandedChanged && !update.expanded) + || (!update.expandedChanged && !mBubbleBarViewController.isExpanded()); + BubbleBarBubble bubbleToSelect = null; + if (!update.removedBubbles.isEmpty()) { + for (int i = 0; i < update.removedBubbles.size(); i++) { + RemovedBubble removedBubble = update.removedBubbles.get(i); + BubbleBarBubble bubble = mBubbles.remove(removedBubble.getKey()); + if (bubble != null) { + mBubbleBarViewController.removeBubble(bubble); + } else { + Log.w(TAG, "trying to remove bubble that doesn't exist: " + + removedBubble.getKey()); + } + } + } + if (update.addedBubble != null) { + mBubbles.put(update.addedBubble.getKey(), update.addedBubble); + mBubbleBarViewController.addBubble(update.addedBubble); + if (isCollapsed) { + // If we're collapsed, the most recently added bubble will be selected. + bubbleToSelect = update.addedBubble; + } + + } + if (update.currentBubbles != null && !update.currentBubbles.isEmpty()) { + // Iterate in reverse because new bubbles are added in front and the list is in order. + for (int i = update.currentBubbles.size() - 1; i >= 0; i--) { + BubbleBarBubble bubble = update.currentBubbles.get(i); + if (bubble != null) { + mBubbles.put(bubble.getKey(), bubble); + mBubbleBarViewController.addBubble(bubble); + if (isCollapsed) { + // If we're collapsed, the most recently added bubble will be selected. + bubbleToSelect = bubble; + } + } else { + Log.w(TAG, "trying to add bubble but null after loading! " + + update.addedBubble.getKey()); + } + } + } + + // Adds and removals have happened, update visibility before any other visual changes. + mBubbleBarViewController.setHiddenForBubbles(mBubbles.isEmpty()); + mBubbleStashedHandleViewController.setHiddenForBubbles(mBubbles.isEmpty()); + + if (update.updatedBubble != null) { + // TODO: (b/269670235) handle updates: + // (1) if content / icons change -- requires reload & add back in place + // (2) if showing update dot changes -- tell the view to hide / show the dot + } + if (update.bubbleKeysInOrder != null && !update.bubbleKeysInOrder.isEmpty()) { + // Create the new list + List newOrder = update.bubbleKeysInOrder.stream() + .map(mBubbles::get).filter(Objects::nonNull).toList(); + if (!newOrder.isEmpty()) { + mBubbleBarViewController.reorderBubbles(newOrder); + } + } + if (update.suppressedBubbleKey != null) { + // TODO: (b/273316505) handle suppression + } + if (update.unsuppressedBubbleKey != null) { + // TODO: (b/273316505) handle suppression + } + if (update.selectedBubbleKey != null) { + if (mSelectedBubble != null + && !update.selectedBubbleKey.equals(mSelectedBubble.getKey())) { + BubbleBarBubble newlySelected = mBubbles.get(update.selectedBubbleKey); + if (newlySelected != null) { + bubbleToSelect = newlySelected; + } else { + Log.w(TAG, "trying to select bubble that doesn't exist:" + + update.selectedBubbleKey); + } + } + } + if (bubbleToSelect != null) { + setSelectedBubble(bubbleToSelect); + } + if (update.expandedChanged) { + if (update.expanded != mBubbleBarViewController.isExpanded()) { + mBubbleBarViewController.setExpandedFromSysui(update.expanded); + } else { + Log.w(TAG, "expansion was changed but is the same"); + } + } + } + + /** + * Sets the bubble that should be selected. This notifies the views, it does not notify + * WMShell that the selection has changed, that should go through + * {@link SystemUiProxy#showBubble}. + */ + public void setSelectedBubble(BubbleBarBubble b) { + if (!Objects.equals(b, mSelectedBubble)) { + if (DEBUG) Log.w(TAG, "selectingBubble: " + b.getKey()); + mSelectedBubble = b; + mBubbleBarViewController.updateSelectedBubble(mSelectedBubble); + } + } + + /** + * Returns the selected bubble or null if no bubble is selected. + */ + @Nullable + public String getSelectedBubbleKey() { + if (mSelectedBubble != null) { + return mSelectedBubble.getKey(); + } + return null; + } + + // + // Loading data for the bubbles + // + + @Nullable + private BubbleBarBubble populateBubble(BubbleInfo b, Context context, BubbleBarView bbv) { + String appName; + Bitmap badgeBitmap; + Bitmap bubbleBitmap; + Path dotPath; + int dotColor; + + boolean isImportantConvo = false; // TODO: (b/269671451) needs to be added to BubbleInfo + + ShortcutRequest.QueryResult result = new ShortcutRequest(context, + new UserHandle(b.getUserId())) + .forPackage(b.getPackageName(), b.getShortcutId()) + .query(FLAG_MATCH_DYNAMIC + | FLAG_MATCH_PINNED_BY_ANY_LAUNCHER + | FLAG_MATCH_CACHED + | FLAG_GET_PERSONS_DATA); + + ShortcutInfo shortcutInfo = result.size() > 0 ? result.get(0) : null; + if (shortcutInfo == null) { + Log.w(TAG, "No shortcutInfo found for bubble: " + b.getKey() + + " with shortcutId: " + b.getShortcutId()); + } + + ApplicationInfo appInfo; + try { + appInfo = mLauncherApps.getApplicationInfo( + b.getPackageName(), + 0, + new UserHandle(b.getUserId())); + } catch (PackageManager.NameNotFoundException e) { + // If we can't find package... don't think we should show the bubble. + Log.w(TAG, "Unable to find packageName: " + b.getPackageName()); + return null; + } + if (appInfo == null) { + Log.w(TAG, "Unable to find appInfo: " + b.getPackageName()); + return null; + } + PackageManager pm = context.getPackageManager(); + appName = String.valueOf(appInfo.loadLabel(pm)); + Drawable appIcon = appInfo.loadUnbadgedIcon(pm); + Drawable badgedIcon = pm.getUserBadgedIcon(appIcon, new UserHandle(b.getUserId())); + + // Badged bubble image + Drawable bubbleDrawable = mIconFactory.getBubbleDrawable(context, shortcutInfo, + b.getIcon()); + if (bubbleDrawable == null) { + // Default to app icon + bubbleDrawable = appIcon; + } + + BitmapInfo badgeBitmapInfo = mIconFactory.getBadgeBitmap(badgedIcon, isImportantConvo); + badgeBitmap = badgeBitmapInfo.icon; + + float[] bubbleBitmapScale = new float[1]; + bubbleBitmap = mIconFactory.getBubbleBitmap(bubbleDrawable, bubbleBitmapScale); + + // Dot color & placement + Path iconPath = PathParser.createPathFromPathData( + context.getResources().getString( + com.android.internal.R.string.config_icon_mask)); + Matrix matrix = new Matrix(); + float scale = bubbleBitmapScale[0]; + float radius = BubbleView.DEFAULT_PATH_SIZE / 2f; + matrix.setScale(scale /* x scale */, scale /* y scale */, radius /* pivot x */, + radius /* pivot y */); + iconPath.transform(matrix); + dotPath = iconPath; + dotColor = ColorUtils.blendARGB(badgeBitmapInfo.color, + Color.WHITE, WHITE_SCRIM_ALPHA); + + + LayoutInflater inflater = LayoutInflater.from(context); + BubbleView bubbleView = (BubbleView) inflater.inflate( + R.layout.bubblebar_item_view, bbv, false /* attachToRoot */); + + BubbleBarBubble bubble = new BubbleBarBubble(b, bubbleView, + badgeBitmap, bubbleBitmap, dotColor, dotPath, appName); + bubbleView.setBubble(bubble); + return bubble; + } +} diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarView.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarView.java index 07daf065ab..07de3b8c15 100644 --- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarView.java +++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarView.java @@ -34,7 +34,7 @@ import java.util.List; /** * The view that holds all the bubble views. Modifying this view should happen through * {@link BubbleBarViewController}. Updates to the bubbles themselves (adds, removes, updates, - * selection) should happen through BubbleBarController which is the source of truth + * selection) should happen through {@link BubbleBarController} which is the source of truth * for state information about the bubbles. *

* The bubble bar has a couple of visual states: diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java index 4145ac67b3..0afc2cb4fe 100644 --- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java +++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java @@ -48,6 +48,7 @@ public class BubbleBarViewController { // Initialized in init. private BubbleStashController mBubbleStashController; + private BubbleBarController mBubbleBarController; private View.OnClickListener mBubbleClickListener; private View.OnClickListener mBubbleBarClickListener; @@ -75,6 +76,7 @@ public class BubbleBarViewController { public void init(TaskbarControllers controllers, BubbleControllers bubbleControllers) { mBubbleStashController = bubbleControllers.bubbleStashController; + mBubbleBarController = bubbleControllers.bubbleBarController; mActivity.addOnDeviceProfileChangeListener(dp -> mBarView.getLayoutParams().height = mActivity.getDeviceProfile().taskbarHeight @@ -92,7 +94,15 @@ public class BubbleBarViewController { if (bubble == null) { Log.e(TAG, "bubble click listener, bubble was null"); } - // TODO: handle the click + final String currentlySelected = mBubbleBarController.getSelectedBubbleKey(); + if (mBarView.isExpanded() && Objects.equals(bubble.getKey(), currentlySelected)) { + // Tapping the currently selected bubble while expanded collapses the view. + setExpanded(false); + mBubbleStashController.stashBubbleBar(); + } else { + mBubbleBarController.setSelectedBubble(bubble); + // TODO: Tell SysUi to show the expanded view for this bubble. + } } // @@ -262,7 +272,12 @@ public class BubbleBarViewController { if (!isExpanded) { // TODO: Tell SysUi to collapse the bubble } else { - // TODO: Tell SysUi to show the bubble + final String selectedKey = mBubbleBarController.getSelectedBubbleKey(); + if (selectedKey != null) { + // TODO: Tell SysUi to show the bubble + } else { + Log.w(TAG, "trying to expand bubbles when there isn't one selected"); + } // TODO: Tell taskbar stash controller to stash without bubbles following } } diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleControllers.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleControllers.java index 8b07062b52..6417f3c585 100644 --- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleControllers.java +++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleControllers.java @@ -23,6 +23,7 @@ import com.android.launcher3.util.RunnableList; */ public class BubbleControllers { + public final BubbleBarController bubbleBarController; public final BubbleBarViewController bubbleBarViewController; public final BubbleStashController bubbleStashController; public final BubbleStashedHandleViewController bubbleStashedHandleViewController; @@ -34,9 +35,12 @@ public class BubbleControllers { * * Call init * * Call onDestroy */ - public BubbleControllers(BubbleBarViewController bubbleBarViewController, + public BubbleControllers( + BubbleBarController bubbleBarController, + BubbleBarViewController bubbleBarViewController, BubbleStashController bubbleStashController, BubbleStashedHandleViewController bubbleStashedHandleViewController) { + this.bubbleBarController = bubbleBarController; this.bubbleBarViewController = bubbleBarViewController; this.bubbleStashController = bubbleStashController; this.bubbleStashedHandleViewController = bubbleStashedHandleViewController; @@ -48,6 +52,7 @@ public class BubbleControllers { * in constructors for now, as some controllers may still be waiting for init(). */ public void init(TaskbarControllers taskbarControllers) { + bubbleBarController.init(taskbarControllers, this); bubbleBarViewController.init(taskbarControllers, this); bubbleStashedHandleViewController.init(taskbarControllers, this); bubbleStashController.init(taskbarControllers, this); @@ -70,5 +75,6 @@ public class BubbleControllers { */ public void onDestroy() { bubbleStashedHandleViewController.onDestroy(); + bubbleBarController.onDestroy(); } }