Files
Lawnchair/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarController.java
T
Liran Binyamin 1c03ed001d Pass bubble bar offsets instead of position to WMShell
This allows WMShell to calculate the position of the expanded view
correctly when the bubble bar is being expanded from stashed.

Test: See ag/24012908
Change-Id: I7b917e761c827135942d918917c920b06650f496
2023-07-11 17:08:09 -04:00

534 lines
23 KiB
Java

/*
* 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 static java.lang.Math.abs;
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.content.res.TypedArray;
import android.graphics.Bitmap;
import android.graphics.Color;
import android.graphics.Matrix;
import android.graphics.Path;
import android.graphics.drawable.AdaptiveIconDrawable;
import android.graphics.drawable.ColorDrawable;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.InsetDrawable;
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 androidx.appcompat.content.res.AppCompatResources;
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 final SystemUiProxy mSystemUiProxy;
private BubbleBarItem mSelectedBubble;
private BubbleBarOverflow mOverflowBubble;
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.
mSystemUiProxy = SystemUiProxy.INSTANCE.get(context);
if (BUBBLE_BAR_ENABLED) {
mSystemUiProxy.setBubblesListener(this);
}
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() {
mSystemUiProxy.setBubblesListener(null);
}
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);
mBubbleBarViewController.setUpdateSelectedBubbleAfterCollapse(
key -> setSelectedBubble(mBubbles.get(key)));
});
}
/**
* Creates and adds the overflow bubble to the bubble bar if it hasn't been created yet.
*
* <p>This should be called on the {@link #BUBBLE_STATE_EXECUTOR} executor to avoid inflating
* the overflow multiple times.
*/
private void createAndAddOverflowIfNeeded() {
if (mOverflowBubble == null) {
BubbleBarOverflow overflow = createOverflow(mContext);
mMainExecutor.execute(() -> {
// we're on the main executor now, so check that the overflow hasn't been created
// again to avoid races.
if (mOverflowBubble == null) {
mBubbleBarViewController.addBubble(overflow);
mOverflowBubble = overflow;
}
});
}
}
/**
* 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(() -> {
createAndAddOverflowIfNeeded();
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");
}
}
}
/** Tells WMShell to show the currently selected bubble. */
public void showSelectedBubble() {
if (getSelectedBubbleKey() != null) {
if (mSelectedBubble instanceof BubbleBarBubble) {
// TODO (b/269670235): hide the update dot on the view if needed.
}
mSystemUiProxy.showBubble(getSelectedBubbleKey(),
getBubbleBarOffsetX(), getBubbleBarOffsetY());
} else {
Log.w(TAG, "Trying to show the selected bubble but it's null");
}
}
/** Updates the currently selected bubble for launcher views and tells WMShell to show it. */
public void showAndSelectBubble(BubbleBarItem b) {
if (DEBUG) Log.w(TAG, "showingSelectedBubble: " + b.getKey());
setSelectedBubble(b);
showSelectedBubble();
}
/**
* 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 either
* {@link #showSelectedBubble()} or {@link #showAndSelectBubble(BubbleBarItem)}.
*/
private void setSelectedBubble(BubbleBarItem 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 = b.isImportantConversation();
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;
}
private BubbleBarOverflow createOverflow(Context context) {
Bitmap bitmap = createOverflowBitmap(context);
LayoutInflater inflater = LayoutInflater.from(context);
BubbleView bubbleView = (BubbleView) inflater.inflate(
R.layout.bubblebar_item_view, mBarView, false /* attachToRoot */);
BubbleBarOverflow overflow = new BubbleBarOverflow(bubbleView);
bubbleView.setOverflow(overflow, bitmap);
return overflow;
}
private Bitmap createOverflowBitmap(Context context) {
Drawable iconDrawable = AppCompatResources.getDrawable(mContext,
R.drawable.bubble_ic_overflow_button);
final TypedArray ta = mContext.obtainStyledAttributes(
new int[]{
com.android.internal.R.attr.materialColorOnPrimaryFixed,
com.android.internal.R.attr.materialColorPrimaryFixed
});
int overflowIconColor = ta.getColor(0, Color.WHITE);
int overflowBackgroundColor = ta.getColor(1, Color.BLACK);
ta.recycle();
iconDrawable.setTint(overflowIconColor);
int inset = context.getResources().getDimensionPixelSize(R.dimen.bubblebar_overflow_inset);
Drawable foreground = new InsetDrawable(iconDrawable, inset);
Drawable drawable = new AdaptiveIconDrawable(new ColorDrawable(overflowBackgroundColor),
foreground);
return mIconFactory.createBadgedIconBitmap(drawable).icon;
}
private int getBubbleBarOffsetY() {
final int translation = (int) abs(mBubbleStashController.getBubbleBarTranslationY());
return translation + mBarView.getHeight();
}
private int getBubbleBarOffsetX() {
return mBarView.getWidth() + mBarView.getHorizontalMargin();
}
}