Creates BubbleBarController

Creates BubbleBarController which is responsible for registering
a listener to hear about bubble state changes and propagates those
changes to BubbleBarViewController.

Additionally, it loads the data and creates the individual BubbleViews
that are added to BubbleBarView.

Bug: 253318833
Test: manual, with other CLs, see go/bubble-bar-tests
Flag: WM_BUBBLE_BAR
Change-Id: I45b0d3a764cd14b2fc5d34aec2b81c7fecb541cf
This commit is contained in:
Mady Mellor
2023-04-14 11:36:51 -07:00
parent 051a1e35c8
commit a088308343
4 changed files with 460 additions and 4 deletions

View File

@@ -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<String, BubbleBarBubble> 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<RemovedBubble> removedBubbles;
List<String> bubbleKeysInOrder;
// These need to be loaded in the background
BubbleBarBubble addedBubble;
BubbleBarBubble updatedBubble;
List<BubbleBarBubble> 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<BubbleBarBubble> 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<BubbleBarBubble> 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;
}
}