/* * 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.app.Notification; 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.Point; 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.util.Executors.SimpleThreadFactory; import com.android.quickstep.SystemUiProxy; import com.android.systemui.shared.system.QuickStepContract.SystemUiStateFlags; import com.android.wm.shell.Flags; import com.android.wm.shell.bubbles.IBubblesListener; import com.android.wm.shell.common.bubbles.BubbleBarLocation; 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";
private static final boolean DEBUG = false;
/**
* Determines whether bubbles can be shown in the bubble bar. This value updates when the
* taskbar is recreated.
*
* @see #onTaskbarRecreated()
*/
private static boolean sBubbleBarEnabled = Flags.enableBubbleBar()
|| SystemProperties.getBoolean("persist.wm.debug.bubble_bar", false);
/** Whether showing bubbles in the launcher bubble bar is enabled. */
public static boolean isBubbleBarEnabled() {
return sBubbleBarEnabled;
}
/** Re-reads the value of the flag from SystemProperties when taskbar is recreated. */
public static void onTaskbarRecreated() {
sBubbleBarEnabled = Flags.enableBubbleBar()
|| SystemProperties.getBoolean("persist.wm.debug.bubble_bar", false);
}
private static final long 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 long MASK_HIDE_HANDLE_VIEW = SYSUI_STATE_BOUNCER_SHOWING
| SYSUI_STATE_STATUS_BAR_KEYGUARD_SHOWING
| SYSUI_STATE_STATUS_BAR_KEYGUARD_SHOWING_OCCLUDED;
private static final long 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 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);
MAIN_EXECUTOR.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, /* isExpanding= */ false, /* suppressAnimation= */ true);
mOverflowBubble = overflow;
}
});
}
}
/**
* Updates the bubble bar, handle bar, and stash controllers based on sysui state flags.
*/
public void updateStateForSysuiFlags(@SystemUiStateFlags long 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(mContext, update.addedBubble, mBarView,
null /* existingBubble */);
}
if (update.updatedBubble != null) {
BubbleBarBubble existingBubble = mBubbles.get(update.updatedBubble.getKey());
viewUpdate.updatedBubble =
populateBubble(mContext, update.updatedBubble, mBarView,
existingBubble);
}
if (update.currentBubbleList != null && !update.currentBubbleList.isEmpty()) {
List
* Updates the value locally in Launcher and in WMShell.
*/
public void updateBubbleBarLocation(BubbleBarLocation location) {
updateBubbleBarLocationInternal(location);
mSystemUiProxy.setBubbleBarLocation(location);
}
private void updateBubbleBarLocationInternal(BubbleBarLocation location) {
mBubbleBarViewController.setBubbleBarLocation(location);
mBubbleStashController.setBubbleBarLocation(location);
}
@Override
public void animateBubbleBarLocation(BubbleBarLocation bubbleBarLocation) {
MAIN_EXECUTOR.execute(
() -> mBubbleBarViewController.animateBubbleBarLocation(bubbleBarLocation));
}
//
// Loading data for the bubbles
//
@Nullable
private BubbleBarBubble populateBubble(Context context, BubbleInfo b, BubbleBarView bbv,
@Nullable BubbleBarBubble existingBubble) {
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 / 255f);
if (existingBubble == null) {
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;
} else {
// If we already have a bubble (so it already has an inflated view), update it.
existingBubble.setInfo(b);
existingBubble.setBadge(badgeBitmap);
existingBubble.setIcon(bubbleBitmap);
existingBubble.setDotColor(dotColor);
existingBubble.setDotPath(dotPath);
existingBubble.setAppName(appName);
return existingBubble;
}
}
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 void onBubbleBarBoundsChanged() {
int newTop = mBarView.getRestingTopPositionOnScreen();
if (newTop != mLastSentBubbleBarTop) {
mLastSentBubbleBarTop = newTop;
mSystemUiProxy.updateBubbleBarTopOnScreen(newTop);
}
}
/** Interface for checking whether the IME is visible. */
public interface ImeVisibilityChecker {
/** Whether the IME is visible. */
boolean isImeVisible();
}
}