Merge "Take in test instance as field." into main
diff --git a/OWNERS b/OWNERS
index a66bf54..22efa33 100644
--- a/OWNERS
+++ b/OWNERS
@@ -30,6 +30,7 @@
jeremysim@google.com
atsjenk@google.com
brianji@google.com
+hwwang@google.com
# Overview eng team
alexchau@google.com
@@ -52,4 +53,4 @@
per-file DeviceConfigWrapper.java = sunnygoyal@google.com, winsonc@google.com, adamcohen@google.com, hyunyoungs@google.com
# Predictive Back
-per-file LauncherBackAnimationController.java = shanh@google.com, gallmann@google.com
\ No newline at end of file
+per-file LauncherBackAnimationController.java = shanh@google.com, gallmann@google.com
diff --git a/quickstep/src/com/android/launcher3/model/QuickstepModelDelegate.java b/quickstep/src/com/android/launcher3/model/QuickstepModelDelegate.java
index 8b5ed7c..6af5a30 100644
--- a/quickstep/src/com/android/launcher3/model/QuickstepModelDelegate.java
+++ b/quickstep/src/com/android/launcher3/model/QuickstepModelDelegate.java
@@ -205,6 +205,7 @@
mActive = true;
}
+ @WorkerThread
@Override
public void workspaceLoadComplete() {
super.workspaceLoadComplete();
@@ -323,6 +324,7 @@
}
}
+ @WorkerThread
@Override
public void destroy() {
super.destroy();
diff --git a/quickstep/src/com/android/launcher3/model/WellbeingModel.java b/quickstep/src/com/android/launcher3/model/WellbeingModel.java
index a7c9652..28bc01c 100644
--- a/quickstep/src/com/android/launcher3/model/WellbeingModel.java
+++ b/quickstep/src/com/android/launcher3/model/WellbeingModel.java
@@ -111,6 +111,7 @@
mWorkerHandler.post(this::initializeInBackground);
}
+ @WorkerThread
private void initializeInBackground() {
if (!TextUtils.isEmpty(mWellbeingProviderPkg)) {
mContext.registerReceiver(
@@ -134,8 +135,8 @@
public void close() {
if (!TextUtils.isEmpty(mWellbeingProviderPkg)) {
mWorkerHandler.post(() -> {
- mWellbeingAppChangeReceiver.unregisterReceiverSafely(mContext);
- mAppAddRemoveReceiver.unregisterReceiverSafely(mContext);
+ mWellbeingAppChangeReceiver.unregisterReceiverSafelySync(mContext);
+ mAppAddRemoveReceiver.unregisterReceiverSafelySync(mContext);
mContext.getContentResolver().unregisterContentObserver(mContentObserver);
});
}
diff --git a/quickstep/src/com/android/launcher3/statehandlers/DepthController.java b/quickstep/src/com/android/launcher3/statehandlers/DepthController.java
index 747612d..4c24d95 100644
--- a/quickstep/src/com/android/launcher3/statehandlers/DepthController.java
+++ b/quickstep/src/com/android/launcher3/statehandlers/DepthController.java
@@ -19,6 +19,7 @@
import static com.android.app.animation.Interpolators.LINEAR;
import static com.android.launcher3.states.StateAnimationConfig.ANIM_DEPTH;
import static com.android.launcher3.states.StateAnimationConfig.SKIP_DEPTH_CONTROLLER;
+import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR;
import static com.android.launcher3.util.MultiPropertyFactory.MULTI_PROPERTY_VALUE;
import android.animation.Animator;
@@ -74,8 +75,9 @@
mOnAttachListener = new View.OnAttachStateChangeListener() {
@Override
public void onViewAttachedToWindow(View view) {
- CrossWindowBlurListeners.getInstance().addListener(mLauncher.getMainExecutor(),
- mCrossWindowBlurListener);
+ UI_HELPER_EXECUTOR.execute(() ->
+ CrossWindowBlurListeners.getInstance().addListener(
+ mLauncher.getMainExecutor(), mCrossWindowBlurListener));
mLauncher.getScrimView().addOpaquenessListener(mOpaquenessListener);
// To handle the case where window token is invalid during last setDepth call.
@@ -108,7 +110,9 @@
private void removeSecondaryListeners() {
if (mCrossWindowBlurListener != null) {
- CrossWindowBlurListeners.getInstance().removeListener(mCrossWindowBlurListener);
+ UI_HELPER_EXECUTOR.execute(() ->
+ CrossWindowBlurListeners.getInstance()
+ .removeListener(mCrossWindowBlurListener));
}
if (mOpaquenessListener != null) {
mLauncher.getScrimView().removeOpaquenessListener(mOpaquenessListener);
diff --git a/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchViewController.java b/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchViewController.java
index d6ee92f..73819b3 100644
--- a/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchViewController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchViewController.java
@@ -15,12 +15,7 @@
*/
package com.android.launcher3.taskbar;
-import static android.window.SplashScreen.SPLASH_SCREEN_STYLE_UNDEFINED;
-
-import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR;
-
import android.animation.Animator;
-import android.app.ActivityOptions;
import android.view.KeyEvent;
import android.view.animation.AnimationUtils;
import android.window.RemoteTransition;
@@ -31,13 +26,10 @@
import com.android.launcher3.Utilities;
import com.android.launcher3.anim.AnimatorListeners;
import com.android.launcher3.taskbar.overlay.TaskbarOverlayContext;
-import com.android.quickstep.SystemUiProxy;
-import com.android.quickstep.util.DesktopTask;
import com.android.quickstep.util.GroupTask;
import com.android.quickstep.util.SlideInRemoteTransition;
import com.android.systemui.shared.recents.model.Task;
import com.android.systemui.shared.recents.model.ThumbnailData;
-import com.android.systemui.shared.system.ActivityManagerWrapper;
import com.android.systemui.shared.system.QuickStepContract;
import java.io.PrintWriter;
@@ -158,28 +150,8 @@
AnimationUtils.loadInterpolator(
context, android.R.interpolator.fast_out_extra_slow_in)),
"SlideInTransition");
- if (task instanceof DesktopTask) {
- UI_HELPER_EXECUTOR.execute(() ->
- SystemUiProxy.INSTANCE.get(mKeyboardQuickSwitchView.getContext())
- .showDesktopApps(
- mKeyboardQuickSwitchView.getDisplay().getDisplayId(),
- remoteTransition));
- } else if (mOnDesktop) {
- UI_HELPER_EXECUTOR.execute(() ->
- SystemUiProxy.INSTANCE.get(mKeyboardQuickSwitchView.getContext())
- .showDesktopApp(task.task1.key.id));
- } else if (task.task2 == null) {
- UI_HELPER_EXECUTOR.execute(() -> {
- ActivityOptions activityOptions = mControllers.taskbarActivityContext
- .makeDefaultActivityOptions(SPLASH_SCREEN_STYLE_UNDEFINED).options;
- activityOptions.setRemoteTransition(remoteTransition);
-
- ActivityManagerWrapper.getInstance().startActivityFromRecents(
- task.task1.key, activityOptions);
- });
- } else {
- mControllers.uiController.launchSplitTasks(task, remoteTransition);
- }
+ mControllers.taskbarActivityContext.handleGroupTaskLaunch(
+ task, remoteTransition, mOnDesktop);
return -1;
}
diff --git a/quickstep/src/com/android/launcher3/taskbar/NavbarButtonsViewController.java b/quickstep/src/com/android/launcher3/taskbar/NavbarButtonsViewController.java
index 81581b8..63e1e01 100644
--- a/quickstep/src/com/android/launcher3/taskbar/NavbarButtonsViewController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/NavbarButtonsViewController.java
@@ -47,6 +47,7 @@
import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_SCREEN_PINNING;
import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_SHORTCUT_HELPER_SHOWING;
import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_VOICE_INTERACTION_WINDOW_SHOWING;
+import static com.android.wm.shell.Flags.enableTaskbarOnPhones;
import android.animation.ArgbEvaluator;
import android.animation.ObjectAnimator;
@@ -678,14 +679,19 @@
mLightIconColorOnHome,
mDarkIconColorOnHome);
- // Override the color from framework if nav buttons are over an opaque Taskbar surface.
- final int iconColor = (int) argbEvaluator.evaluate(
- mOnBackgroundNavButtonColorOverrideMultiplier.value
- * Math.max(
- mOnTaskbarBackgroundNavButtonColorOverride.value,
- mSlideInViewVisibleNavButtonColorOverride.value),
- sysUiNavButtonIconColorOnHome,
- mOnBackgroundIconColor);
+ final int iconColor;
+ if (ENABLE_TASKBAR_NAVBAR_UNIFICATION && enableTaskbarOnPhones()
+ && mContext.isPhoneMode()) {
+ iconColor = sysUiNavButtonIconColorOnHome;
+ } else {
+ // Override the color from framework if nav buttons are over an opaque Taskbar surface.
+ iconColor = (int) argbEvaluator.evaluate(
+ mOnBackgroundNavButtonColorOverrideMultiplier.value * Math.max(
+ mOnTaskbarBackgroundNavButtonColorOverride.value,
+ mSlideInViewVisibleNavButtonColorOverride.value),
+ sysUiNavButtonIconColorOnHome,
+ mOnBackgroundIconColor);
+ }
for (ImageView button : mAllButtons) {
button.setImageTintList(ColorStateList.valueOf(iconColor));
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java
index 6b62c86..5020206 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java
@@ -67,6 +67,7 @@
import android.view.WindowInsets;
import android.view.WindowManager;
import android.widget.Toast;
+import android.window.RemoteTransition;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
@@ -131,6 +132,9 @@
import com.android.quickstep.LauncherActivityInterface;
import com.android.quickstep.NavHandle;
import com.android.quickstep.RecentsModel;
+import com.android.quickstep.SystemUiProxy;
+import com.android.quickstep.util.DesktopTask;
+import com.android.quickstep.util.GroupTask;
import com.android.quickstep.views.RecentsView;
import com.android.quickstep.views.TaskView;
import com.android.systemui.shared.recents.model.Task;
@@ -298,7 +302,7 @@
TaskbarEduTooltipController.newInstance(this),
new KeyboardQuickSwitchController(),
new TaskbarPinningController(this, () ->
- DisplayController.INSTANCE.get(this).getInfo().isInDesktopMode()),
+ DisplayController.isInDesktopMode(this)),
bubbleControllersOptional);
mLauncherPrefs = LauncherPrefs.get(this);
@@ -1081,10 +1085,9 @@
RecentsView recents = taskbarUIController.getRecentsView();
boolean shouldCloseAllOpenViews = true;
Object tag = view.getTag();
- if (tag instanceof Task) {
- Task task = (Task) tag;
- ActivityManagerWrapper.getInstance().startActivityFromRecents(task.key,
- ActivityOptions.makeBasic());
+ if (tag instanceof GroupTask groupTask) {
+ handleGroupTaskLaunch(groupTask, /* remoteTransition = */ null,
+ DisplayController.isInDesktopMode(this));
mControllers.taskbarStashController.updateAndAnimateTransientTaskbar(true);
} else if (tag instanceof FolderInfo) {
// Tapping an expandable folder icon on Taskbar
@@ -1185,6 +1188,36 @@
}
/**
+ * Launches the given GroupTask with the following behavior:
+ * - If the GroupTask is a DesktopTask, launch the tasks in that Desktop.
+ * - If {@code onDesktop}, bring the given GroupTask to the front.
+ * - If the GroupTask is a single task, launch it via startActivityFromRecents.
+ * - Otherwise, we assume the GroupTask is a Split pair and launch them together.
+ */
+ public void handleGroupTaskLaunch(GroupTask task, @Nullable RemoteTransition remoteTransition,
+ boolean onDesktop) {
+ if (task instanceof DesktopTask) {
+ UI_HELPER_EXECUTOR.execute(() ->
+ SystemUiProxy.INSTANCE.get(this).showDesktopApps(getDisplay().getDisplayId(),
+ remoteTransition));
+ } else if (onDesktop) {
+ UI_HELPER_EXECUTOR.execute(() ->
+ SystemUiProxy.INSTANCE.get(this).showDesktopApp(task.task1.key.id));
+ } else if (task.task2 == null) {
+ UI_HELPER_EXECUTOR.execute(() -> {
+ ActivityOptions activityOptions =
+ makeDefaultActivityOptions(SPLASH_SCREEN_STYLE_UNDEFINED).options;
+ activityOptions.setRemoteTransition(remoteTransition);
+
+ ActivityManagerWrapper.getInstance().startActivityFromRecents(
+ task.task1.key, activityOptions);
+ });
+ } else {
+ mControllers.uiController.launchSplitTasks(task, remoteTransition);
+ }
+ }
+
+ /**
* Runs when the user taps a Taskbar icon in TaskbarActivityContext (Overview or inside an app),
* and calls the appropriate method to animate and launch.
*/
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarDragController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarDragController.java
index 4f5922c..efe42fb 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarDragController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarDragController.java
@@ -82,6 +82,7 @@
import com.android.launcher3.util.ItemInfoMatcher;
import com.android.launcher3.views.BubbleTextHolder;
import com.android.quickstep.LauncherActivityInterface;
+import com.android.quickstep.util.GroupTask;
import com.android.quickstep.util.LogUtils;
import com.android.quickstep.util.MultiValueUpdateListener;
import com.android.systemui.shared.recents.model.Task;
@@ -181,7 +182,9 @@
private DragView startInternalDrag(
BubbleTextView btv, @Nullable DragPreviewProvider dragPreviewProvider) {
- float iconScale = btv.getIcon().getAnimatedScale();
+ // TODO(b/344038728): null check is only necessary because Recents doesn't use
+ // FastBitmapDrawable
+ float iconScale = btv.getIcon() == null ? 1f : btv.getIcon().getAnimatedScale();
// Clear the pressed state if necessary
btv.clearFocus();
@@ -248,7 +251,7 @@
dragLayerX + dragOffset.x,
dragLayerY + dragOffset.y,
(View target, DropTarget.DragObject d, boolean success) -> {} /* DragSource */,
- (ItemInfo) btv.getTag(),
+ btv.getTag() instanceof ItemInfo itemInfo ? itemInfo : null,
dragRect,
scale * iconScale,
scale,
@@ -288,7 +291,9 @@
initialDragViewScale,
dragViewScaleOnDrop,
scalePx);
- dragView.setItemInfo(dragInfo);
+ if (dragInfo != null) {
+ dragView.setItemInfo(dragInfo);
+ }
mDragObject.dragComplete = false;
mDragObject.xOffset = mMotionDown.x - (dragLayerX + dragRegionLeft);
@@ -301,7 +306,8 @@
mDragObject.dragSource = source;
mDragObject.dragInfo = dragInfo;
- mDragObject.originalDragInfo = mDragObject.dragInfo.makeShallowCopy();
+ mDragObject.originalDragInfo =
+ mDragObject.dragInfo != null ? mDragObject.dragInfo.makeShallowCopy() : null;
if (mOptions.preDragCondition != null) {
dragView.setHasDragOffset(mOptions.preDragCondition.getDragOffset().x != 0
@@ -431,8 +437,8 @@
null, item.user));
}
intent.putExtra(Intent.EXTRA_USER, item.user);
- } else if (tag instanceof Task) {
- Task task = (Task) tag;
+ } else if (tag instanceof GroupTask groupTask && !groupTask.hasMultipleTasks()) {
+ Task task = groupTask.task1;
clipDescription = new ClipDescription(task.titleDescription,
new String[] {
ClipDescription.MIMETYPE_APPLICATION_TASK
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarDragLayer.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarDragLayer.java
index f703463..a9b34d2 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarDragLayer.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarDragLayer.java
@@ -18,7 +18,6 @@
import static android.view.KeyEvent.ACTION_UP;
import static android.view.KeyEvent.KEYCODE_BACK;
-import static com.android.launcher3.Flags.enableScalingRevealHomeAnimation;
import static com.android.launcher3.config.FeatureFlags.ENABLE_TASKBAR_NAVBAR_UNIFICATION;
import android.content.Context;
@@ -42,7 +41,6 @@
import com.android.launcher3.AbstractFloatingView;
import com.android.launcher3.testing.TestLogging;
import com.android.launcher3.testing.shared.TestProtocol;
-import com.android.launcher3.util.DisplayController;
import com.android.launcher3.util.MultiPropertyFactory;
import com.android.launcher3.util.MultiPropertyFactory.MultiProperty;
import com.android.launcher3.views.BaseDragLayer;
@@ -106,10 +104,6 @@
mTaskbarBackgroundAlpha = new MultiPropertyFactory<>(this, BG_ALPHA, INDEX_COUNT,
(a, b) -> a * b, 1f);
mTaskbarBackgroundAlpha.get(INDEX_ALL_OTHER_STATES).setValue(0);
- mTaskbarBackgroundAlpha.get(INDEX_STASH_ANIM).setValue(
- enableScalingRevealHomeAnimation() && DisplayController.isTransientTaskbar(context)
- ? 0
- : 1);
}
public void init(TaskbarDragLayerController.TaskbarDragLayerCallbacks callbacks) {
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarManager.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarManager.java
index 2a58db2..051bdc8 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarManager.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarManager.java
@@ -304,7 +304,7 @@
.register(NAV_BAR_KIDS_MODE, mOnSettingsChangeListener);
Log.d(TASKBAR_NOT_DESTROYED_TAG, "registering component callbacks from constructor.");
mContext.registerComponentCallbacks(mComponentCallbacks);
- mShutdownReceiver.register(mContext, Intent.ACTION_SHUTDOWN);
+ mShutdownReceiver.registerAsync(mContext, Intent.ACTION_SHUTDOWN);
UI_HELPER_EXECUTOR.execute(() -> {
mSharedState.taskbarSystemActionPendingIntent = PendingIntent.getBroadcast(
mContext,
@@ -582,8 +582,7 @@
public void destroy() {
debugWhyTaskbarNotDestroyed("TaskbarManager#destroy()");
removeActivityCallbacksAndListeners();
- UI_HELPER_EXECUTOR.execute(
- () -> mTaskbarBroadcastReceiver.unregisterReceiverSafely(mContext));
+ mTaskbarBroadcastReceiver.unregisterReceiverSafelyAsync(mContext);
destroyExistingTaskbar();
removeTaskbarRootViewFromWindow();
if (mUserUnlocked) {
@@ -595,7 +594,7 @@
.unregister(NAV_BAR_KIDS_MODE, mOnSettingsChangeListener);
Log.d(TASKBAR_NOT_DESTROYED_TAG, "unregistering component callbacks from destroy().");
mContext.unregisterComponentCallbacks(mComponentCallbacks);
- mContext.unregisterReceiver(mShutdownReceiver);
+ mShutdownReceiver.unregisterReceiverSafelyAsync(mContext);
}
public @Nullable TaskbarActivityContext getCurrentActivityContext() {
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarModelCallbacks.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarModelCallbacks.java
index 2b0e169..0b7ae39 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarModelCallbacks.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarModelCallbacks.java
@@ -15,9 +15,6 @@
*/
package com.android.launcher3.taskbar;
-import static com.android.window.flags.Flags.enableDesktopWindowingMode;
-import static com.android.window.flags.Flags.enableDesktopWindowingTaskbarRunningApps;
-
import android.util.SparseArray;
import android.view.View;
@@ -29,7 +26,6 @@
import com.android.launcher3.model.data.AppInfo;
import com.android.launcher3.model.data.ItemInfo;
import com.android.launcher3.model.data.WorkspaceItemInfo;
-import com.android.launcher3.statehandlers.DesktopVisibilityController;
import com.android.launcher3.util.ComponentKey;
import com.android.launcher3.util.IntArray;
import com.android.launcher3.util.IntSet;
@@ -37,8 +33,7 @@
import com.android.launcher3.util.LauncherBindableItemsContainer;
import com.android.launcher3.util.PackageUserKey;
import com.android.launcher3.util.Preconditions;
-import com.android.quickstep.LauncherActivityInterface;
-import com.android.quickstep.RecentsModel;
+import com.android.quickstep.util.GroupTask;
import java.io.PrintWriter;
import java.util.ArrayList;
@@ -54,7 +49,7 @@
* Launcher model Callbacks for rendering taskbar.
*/
public class TaskbarModelCallbacks implements
- BgDataModel.Callbacks, LauncherBindableItemsContainer, RecentsModel.RunningTasksListener {
+ BgDataModel.Callbacks, LauncherBindableItemsContainer {
private final SparseArray<ItemInfo> mHotseatItems = new SparseArray<>();
private List<ItemInfo> mPredictedItems = Collections.emptyList();
@@ -68,8 +63,6 @@
// Used to defer any UI updates during the SUW unstash animation.
private boolean mDeferUpdatesForSUW;
private Runnable mDeferredUpdates;
- private final DesktopVisibilityController.DesktopVisibilityListener mDesktopVisibilityListener =
- visible -> updateRunningApps();
public TaskbarModelCallbacks(
TaskbarActivityContext context, TaskbarView container) {
@@ -79,39 +72,6 @@
public void init(TaskbarControllers controllers) {
mControllers = controllers;
- if (mControllers.taskbarRecentAppsController.getCanShowRunningApps()) {
- RecentsModel.INSTANCE.get(mContext).registerRunningTasksListener(this);
-
- if (shouldShowRunningAppsInDesktopMode()) {
- DesktopVisibilityController desktopVisibilityController =
- LauncherActivityInterface.INSTANCE.getDesktopVisibilityController();
- if (desktopVisibilityController != null) {
- desktopVisibilityController.registerDesktopVisibilityListener(
- mDesktopVisibilityListener);
- }
- }
- }
- }
-
- /**
- * Unregisters listeners in this class.
- */
- public void unregisterListeners() {
- RecentsModel.INSTANCE.get(mContext).unregisterRunningTasksListener();
-
- if (shouldShowRunningAppsInDesktopMode()) {
- DesktopVisibilityController desktopVisibilityController =
- LauncherActivityInterface.INSTANCE.getDesktopVisibilityController();
- if (desktopVisibilityController != null) {
- desktopVisibilityController.unregisterDesktopVisibilityListener(
- mDesktopVisibilityListener);
- }
- }
- }
-
- private boolean shouldShowRunningAppsInDesktopMode() {
- // TODO(b/335401172): unify DesktopMode checks in Launcher
- return enableDesktopWindowingMode() && enableDesktopWindowingTaskbarRunningApps();
}
@Override
@@ -171,7 +131,7 @@
final int itemCount = mContainer.getChildCount();
for (int itemIdx = 0; itemIdx < itemCount; itemIdx++) {
View item = mContainer.getChildAt(itemIdx);
- if (op.evaluate((ItemInfo) item.getTag(), item)) {
+ if (item.getTag() instanceof ItemInfo itemInfo && op.evaluate(itemInfo, item)) {
return;
}
}
@@ -232,26 +192,30 @@
predictionNextIndex++;
}
}
- hotseatItemInfos = mControllers.taskbarRecentAppsController
- .updateHotseatItemInfos(hotseatItemInfos);
- Set<String> runningPackages = mControllers.taskbarRecentAppsController.getRunningApps();
- Set<String> minimizedPackages = mControllers.taskbarRecentAppsController.getMinimizedApps();
+
+ final TaskbarRecentAppsController recentAppsController =
+ mControllers.taskbarRecentAppsController;
+ hotseatItemInfos = recentAppsController.updateHotseatItemInfos(hotseatItemInfos);
+ Set<String> runningPackages = recentAppsController.getRunningAppPackages();
+ Set<String> minimizedPackages = recentAppsController.getMinimizedAppPackages();
if (mDeferUpdatesForSUW) {
ItemInfo[] finalHotseatItemInfos = hotseatItemInfos;
mDeferredUpdates = () ->
- commitHotseatItemUpdates(finalHotseatItemInfos, runningPackages,
+ commitHotseatItemUpdates(finalHotseatItemInfos,
+ recentAppsController.getShownTasks(), runningPackages,
minimizedPackages);
} else {
- commitHotseatItemUpdates(hotseatItemInfos, runningPackages, minimizedPackages);
+ commitHotseatItemUpdates(hotseatItemInfos,
+ recentAppsController.getShownTasks(), runningPackages, minimizedPackages);
}
}
- private void commitHotseatItemUpdates(ItemInfo[] hotseatItemInfos, Set<String> runningPackages,
- Set<String> minimizedPackages) {
- mContainer.updateHotseatItems(hotseatItemInfos);
- mControllers.taskbarViewController.updateIconViewsRunningStates(runningPackages,
- minimizedPackages);
+ private void commitHotseatItemUpdates(ItemInfo[] hotseatItemInfos, List<GroupTask> recentTasks,
+ Set<String> runningPackages, Set<String> minimizedPackages) {
+ mContainer.updateHotseatItems(hotseatItemInfos, recentTasks);
+ mControllers.taskbarViewController.updateIconViewsRunningStates(
+ runningPackages, minimizedPackages);
}
/**
@@ -270,21 +234,11 @@
}
}
- @Override
- public void onRunningTasksChanged() {
- updateRunningApps();
- }
-
/** Called when there's a change in running apps to update the UI. */
public void commitRunningAppsToUI() {
commitItemsToUI();
}
- /** Call TaskbarRecentAppsController to update running apps with mHotseatItems. */
- public void updateRunningApps() {
- mControllers.taskbarRecentAppsController.updateRunningApps();
- }
-
@Override
public void bindDeepShortcutMap(HashMap<ComponentKey, Integer> deepShortcutMapCopy) {
mControllers.taskbarPopupController.setDeepShortcutMap(deepShortcutMapCopy);
@@ -296,7 +250,6 @@
Map<PackageUserKey, Integer> packageUserKeytoUidMap) {
Preconditions.assertUIThread();
mControllers.taskbarAllAppsController.setApps(apps, flags, packageUserKeytoUidMap);
- mControllers.taskbarRecentAppsController.setApps(apps);
}
protected void dumpLogs(String prefix, PrintWriter pw) {
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarPopupController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarPopupController.java
index 2730be1..b697590 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarPopupController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarPopupController.java
@@ -148,8 +148,8 @@
icon.clearFocus();
return null;
}
- ItemInfo item = (ItemInfo) icon.getTag();
- if (!ShortcutUtil.supportsShortcuts(item)) {
+ // TODO(b/344657629) support GroupTask as well, for Taskbar Recent apps
+ if (!(icon.getTag() instanceof ItemInfo item) || !ShortcutUtil.supportsShortcuts(item)) {
return null;
}
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarRecentAppsController.kt b/quickstep/src/com/android/launcher3/taskbar/TaskbarRecentAppsController.kt
index b1fc9cc..fc3b4c7 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarRecentAppsController.kt
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarRecentAppsController.kt
@@ -15,19 +15,20 @@
*/
package com.android.launcher3.taskbar
-import android.app.ActivityManager.RunningTaskInfo
-import android.app.WindowConfiguration
import androidx.annotation.VisibleForTesting
import com.android.launcher3.Flags.enableRecentsInTaskbar
-import com.android.launcher3.model.data.AppInfo
import com.android.launcher3.model.data.ItemInfo
-import com.android.launcher3.model.data.WorkspaceItemInfo
import com.android.launcher3.statehandlers.DesktopVisibilityController
import com.android.launcher3.taskbar.TaskbarControllers.LoggableTaskbarController
+import com.android.launcher3.util.CancellableTask
import com.android.quickstep.RecentsModel
+import com.android.quickstep.util.DesktopTask
+import com.android.quickstep.util.GroupTask
+import com.android.systemui.shared.recents.model.Task
import com.android.window.flags.Flags.enableDesktopWindowingMode
import com.android.window.flags.Flags.enableDesktopWindowingTaskbarRunningApps
import java.io.PrintWriter
+import java.util.function.Consumer
/**
* Provides recent apps functionality, when the Taskbar Recent Apps section is enabled. Behavior:
@@ -42,22 +43,28 @@
) : LoggableTaskbarController {
// TODO(b/335401172): unify DesktopMode checks in Launcher.
- val canShowRunningApps =
+ var canShowRunningApps =
enableDesktopWindowingMode() && enableDesktopWindowingTaskbarRunningApps()
+ @VisibleForTesting
+ set(isEnabledFromTest) {
+ field = isEnabledFromTest
+ }
// TODO(b/343532825): Add a setting to disable Recents even when the flag is on.
- var isEnabled: Boolean = enableRecentsInTaskbar() || canShowRunningApps
+ var canShowRecentApps = enableRecentsInTaskbar()
@VisibleForTesting
- set(isEnabledFromTest){
+ set(isEnabledFromTest) {
field = isEnabledFromTest
}
// Initialized in init.
private lateinit var controllers: TaskbarControllers
- private var apps: Array<AppInfo>? = null
- private var allRunningDesktopAppInfos: List<AppInfo>? = null
- private var allMinimizedDesktopAppInfos: List<AppInfo>? = null
+ private var shownHotseatItems: List<ItemInfo> = emptyList()
+ private var allRecentTasks: List<GroupTask> = emptyList()
+ private var desktopTask: DesktopTask? = null
+ var shownTasks: List<GroupTask> = emptyList()
+ private set
private val desktopVisibilityController: DesktopVisibilityController?
get() = desktopVisibilityControllerProvider()
@@ -65,122 +72,170 @@
private val isInDesktopMode: Boolean
get() = desktopVisibilityController?.areDesktopTasksVisible() ?: false
- val runningApps: Set<String>
+ val runningAppPackages: Set<String>
+ /**
+ * Returns the package names of apps that should be indicated as "running" to the user.
+ * Specifically, we return all the open tasks if we are in Desktop mode, else emptySet().
+ */
get() {
- if (!isEnabled || !isInDesktopMode) {
+ if (!canShowRunningApps || !isInDesktopMode) {
return emptySet()
}
- return allRunningDesktopAppInfos?.mapNotNull { it.targetPackage }?.toSet() ?: emptySet()
+ val tasks = desktopTask?.tasks ?: return emptySet()
+ return tasks.map { task -> task.key.packageName }.toSet()
}
- val minimizedApps: Set<String>
+ val minimizedAppPackages: Set<String>
+ /**
+ * Returns the package names of apps that should be indicated as "minimized" to the user.
+ * Specifically, we return all the running packages where all the tasks in that package are
+ * minimized (not visible).
+ */
get() {
- if (!isInDesktopMode) {
+ if (!canShowRunningApps || !isInDesktopMode) {
return emptySet()
}
- return allMinimizedDesktopAppInfos?.mapNotNull { it.targetPackage }?.toSet()
- ?: emptySet()
+ val desktopTasks = desktopTask?.tasks ?: return emptySet()
+ val packageToTasks = desktopTasks.groupBy { it.key.packageName }
+ return packageToTasks.filterValues { tasks -> tasks.all { !it.isVisible } }.keys
}
+ private val recentTasksChangedListener =
+ RecentsModel.RecentTasksChangedListener { reloadRecentTasksIfNeeded() }
+
+ private val iconLoadRequests: MutableSet<CancellableTask<*>> = HashSet()
+
+ // TODO(b/343291428): add TaskVisualsChangListener as well (for calendar/clock?)
+
+ // Used to keep track of the last requested task list ID, so that we do not request to load the
+ // tasks again if we have already requested it and the task list has not changed
+ private var taskListChangeId = -1
+
fun init(taskbarControllers: TaskbarControllers) {
controllers = taskbarControllers
+ recentsModel.registerRecentTasksChangedListener(recentTasksChangedListener)
+ reloadRecentTasksIfNeeded()
}
fun onDestroy() {
- apps = null
- }
-
- /** Stores the current [AppInfo] instances, no-op except in desktop environment. */
- fun setApps(apps: Array<AppInfo>?) {
- this.apps = apps
+ recentsModel.unregisterRecentTasksChangedListener()
+ iconLoadRequests.forEach { it.cancel() }
+ iconLoadRequests.clear()
}
/** Called to update hotseatItems, in order to de-dupe them from Recent/Running tasks later. */
- // TODO(next CL): add new section of Tasks instead of changing Hotseat items
fun updateHotseatItemInfos(hotseatItems: Array<ItemInfo?>): Array<ItemInfo?> {
- if (!isEnabled || !isInDesktopMode) {
+ // Ignore predicted apps - we show running or recent apps instead.
+ val removePredictions =
+ (isInDesktopMode && canShowRunningApps) || (!isInDesktopMode && canShowRecentApps)
+ if (!removePredictions) {
+ shownHotseatItems = hotseatItems.filterNotNull()
+ onRecentsOrHotseatChanged()
return hotseatItems
}
- val newHotseatItemInfos =
+ shownHotseatItems =
hotseatItems
.filterNotNull()
- // Ignore predicted apps - we show running apps instead
.filter { itemInfo -> !itemInfo.isPredictedItem }
.toMutableList()
- val runningDesktopAppInfos =
- allRunningDesktopAppInfos?.let {
- getRunningDesktopAppInfosExceptHotseatApps(it, newHotseatItemInfos.toList())
+
+ onRecentsOrHotseatChanged()
+
+ return shownHotseatItems.toTypedArray()
+ }
+
+ private fun reloadRecentTasksIfNeeded() {
+ if (!recentsModel.isTaskListValid(taskListChangeId)) {
+ taskListChangeId =
+ recentsModel.getTasks { tasks ->
+ allRecentTasks = tasks
+ desktopTask = allRecentTasks.filterIsInstance<DesktopTask>().firstOrNull()
+ onRecentsOrHotseatChanged()
+ controllers.taskbarViewController.commitRunningAppsToUI()
+ }
+ }
+ }
+
+ private fun onRecentsOrHotseatChanged() {
+ shownTasks =
+ if (isInDesktopMode) {
+ computeShownRunningTasks()
+ } else {
+ computeShownRecentTasks()
}
- if (runningDesktopAppInfos != null) {
- newHotseatItemInfos.addAll(runningDesktopAppInfos)
+
+ for (groupTask in shownTasks) {
+ for (task in groupTask.tasks) {
+ val callback =
+ Consumer<Task> { controllers.taskbarViewController.onTaskUpdated(it) }
+ val cancellableTask = recentsModel.iconCache.updateIconInBackground(task, callback)
+ if (cancellableTask != null) {
+ iconLoadRequests.add(cancellableTask)
+ }
+ }
}
- return newHotseatItemInfos.toTypedArray()
}
- private fun getRunningDesktopAppInfosExceptHotseatApps(
- allRunningDesktopAppInfos: List<AppInfo>,
- hotseatItems: List<ItemInfo>
- ): List<ItemInfo> {
- val hotseatPackages = hotseatItems.map { it.targetPackage }
- return allRunningDesktopAppInfos
- .filter { appInfo -> !hotseatPackages.contains(appInfo.targetPackage) }
- .map { WorkspaceItemInfo(it) }
- }
-
- private fun getDesktopRunningTasks(): List<RunningTaskInfo> =
- recentsModel.runningTasks.filter { taskInfo: RunningTaskInfo ->
- taskInfo.windowingMode == WindowConfiguration.WINDOWING_MODE_FREEFORM
- }
-
- // TODO(b/335398876) fetch app icons from Tasks instead of AppInfos
- private fun getAppInfosFromRunningTasks(tasks: List<RunningTaskInfo>): List<AppInfo> {
- // Early return if apps is empty, since we then have no AppInfo to compare to
- if (apps == null) {
+ private fun computeShownRunningTasks(): List<GroupTask> {
+ if (!canShowRunningApps) {
return emptyList()
}
- val packageNames = tasks.map { it.realActivity?.packageName }.distinct().filterNotNull()
- return packageNames
- .map { packageName -> apps?.find { app -> packageName == app.targetPackage } }
- .filterNotNull()
+ val tasks = desktopTask?.tasks ?: emptyList()
+ // Kind of hacky, we wrap each single task in the Desktop as a GroupTask.
+ var desktopTaskAsList = tasks.map { GroupTask(it) }
+ // TODO(b/315344726 Multi-instance support): dedupe Tasks of the same package too.
+ desktopTaskAsList = dedupeHotseatTasks(desktopTaskAsList, shownHotseatItems)
+ val desktopPackages = desktopTaskAsList.map { it.packageNames }
+ // Remove any missing Tasks.
+ val newShownTasks = shownTasks.filter { it.packageNames in desktopPackages }.toMutableList()
+ val newShownPackages = newShownTasks.map { it.packageNames }
+ // Add any new Tasks, maintaining the order from previous shownTasks.
+ newShownTasks.addAll(desktopTaskAsList.filter { it.packageNames !in newShownPackages })
+ return newShownTasks.toList()
}
- /** Called to update the list of currently running apps, no-op except in desktop environment. */
- fun updateRunningApps() {
- if (!isEnabled || !isInDesktopMode) {
- return controllers.taskbarViewController.commitRunningAppsToUI()
+ private fun computeShownRecentTasks(): List<GroupTask> {
+ if (!canShowRecentApps || allRecentTasks.isEmpty()) {
+ return emptyList()
}
- val runningTasks = getDesktopRunningTasks()
- val runningAppInfo = getAppInfosFromRunningTasks(runningTasks)
- allRunningDesktopAppInfos = runningAppInfo
- updateMinimizedApps(runningTasks, runningAppInfo)
- controllers.taskbarViewController.commitRunningAppsToUI()
+ // Remove the current task.
+ val allRecentTasks = allRecentTasks.subList(0, allRecentTasks.size - 1)
+ // TODO(b/315344726 Multi-instance support): dedupe Tasks of the same package too
+ var shownTasks = dedupeHotseatTasks(allRecentTasks, shownHotseatItems)
+ if (shownTasks.size > MAX_RECENT_TASKS) {
+ // Remove any tasks older than MAX_RECENT_TASKS.
+ shownTasks = shownTasks.subList(shownTasks.size - MAX_RECENT_TASKS, shownTasks.size)
+ }
+ return shownTasks
}
- private fun updateMinimizedApps(
- runningTasks: List<RunningTaskInfo>,
- runningAppInfo: List<AppInfo>,
- ) {
- val allRunningAppTasks =
- runningAppInfo
- .mapNotNull { appInfo -> appInfo.targetPackage?.let { appInfo to it } }
- .associate { (appInfo, targetPackage) ->
- appInfo to
- runningTasks
- .filter { it.realActivity?.packageName == targetPackage }
- .map { it.taskId }
- }
- val minimizedTaskIds = runningTasks.associate { it.taskId to !it.isVisible }
- allMinimizedDesktopAppInfos =
- allRunningAppTasks
- .filterValues { taskIds -> taskIds.all { minimizedTaskIds[it] ?: false } }
- .keys
- .toList()
+ private fun dedupeHotseatTasks(
+ groupTasks: List<GroupTask>,
+ shownHotseatItems: List<ItemInfo>
+ ): List<GroupTask> {
+ val hotseatPackages = shownHotseatItems.map { item -> item.targetPackage }
+ return groupTasks.filter { groupTask ->
+ groupTask.hasMultipleTasks() ||
+ !hotseatPackages.contains(groupTask.task1.key.packageName)
+ }
}
override fun dumpLogs(prefix: String, pw: PrintWriter) {
pw.println("$prefix TaskbarRecentAppsController:")
- pw.println("$prefix\tisEnabled=$isEnabled")
pw.println("$prefix\tcanShowRunningApps=$canShowRunningApps")
- // TODO(next CL): add more logs
+ pw.println("$prefix\tcanShowRecentApps=$canShowRecentApps")
+ pw.println("$prefix\tshownHotseatItems=${shownHotseatItems.map{item->item.targetPackage}}")
+ pw.println("$prefix\tallRecentTasks=${allRecentTasks.map { it.packageNames }}")
+ pw.println("$prefix\tdesktopTask=${desktopTask?.packageNames}")
+ pw.println("$prefix\tshownTasks=${shownTasks.map { it.packageNames }}")
+ pw.println("$prefix\trunningTasks=$runningAppPackages")
+ pw.println("$prefix\tminimizedTasks=$minimizedAppPackages")
+ }
+
+ private val GroupTask.packageNames: List<String>
+ get() = tasks.map { task -> task.key.packageName }
+
+ private companion object {
+ const val MAX_RECENT_TASKS = 2
}
}
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarStashController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarStashController.java
index 7ff887c..fa2d907 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarStashController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarStashController.java
@@ -245,7 +245,7 @@
private Animator mTaskbarBackgroundAlphaAnimator;
private long mTaskbarBackgroundDuration;
- private boolean mIsGoingHome;
+ private boolean mUserIsNotGoingHome = false;
// Evaluate whether the handle should be stashed
private final LongPredicate mIsStashedPredicate = flags -> {
@@ -338,7 +338,16 @@
// For now, assume we're in an app, since LauncherTaskbarUIController won't be able to tell
// us that we're paused until a bit later. This avoids flickering upon recreating taskbar.
updateStateForFlag(FLAG_IN_APP, true);
+
applyState(/* duration = */ 0);
+
+ // Hide the background while stashed so it doesn't show on fast swipes home
+ boolean shouldHideTaskbarBackground = enableScalingRevealHomeAnimation()
+ && DisplayController.isTransientTaskbar(mActivity)
+ && isStashed();
+
+ mTaskbarBackgroundAlphaForStash.setValue(shouldHideTaskbarBackground ? 0 : 1);
+
if (mTaskbarSharedState.getTaskbarWasPinned()
|| !mTaskbarSharedState.taskbarWasStashedAuto) {
tryStartTaskbarTimeout();
@@ -828,17 +837,13 @@
private boolean mTaskbarBgAlphaAnimationStarted = false;
@Override
public void onAnimationUpdate(ValueAnimator valueAnimator) {
- if (mIsGoingHome) {
- mTaskbarBgAlphaAnimationStarted = true;
- }
if (mTaskbarBgAlphaAnimationStarted) {
return;
}
if (valueAnimator.getAnimatedFraction() >= ANIMATED_FRACTION_THRESHOLD) {
- if (!mIsGoingHome) {
+ if (mUserIsNotGoingHome) {
playTaskbarBackgroundAlphaAnimation();
- setUserIsGoingHome(false);
mTaskbarBgAlphaAnimationStarted = true;
}
}
@@ -850,8 +855,8 @@
/**
* Sets whether the user is going home based on the current gesture.
*/
- public void setUserIsGoingHome(boolean isGoingHome) {
- mIsGoingHome = isGoingHome;
+ public void setUserIsNotGoingHome(boolean userIsNotGoingHome) {
+ mUserIsNotGoingHome = userIsNotGoingHome;
}
/**
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarUIController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarUIController.java
index 593285f..ce281c3 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarUIController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarUIController.java
@@ -415,7 +415,7 @@
/**
* Sets whether the user is going home based on the current gesture.
*/
- public void setUserIsGoingHome(boolean isGoingHome) {
- mControllers.taskbarStashController.setUserIsGoingHome(isGoingHome);
+ public void setUserIsNotGoingHome(boolean isNotGoingHome) {
+ mControllers.taskbarStashController.setUserIsNotGoingHome(isNotGoingHome);
}
}
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarView.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarView.java
index 570221c..c42d6c6 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarView.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarView.java
@@ -19,6 +19,7 @@
import static com.android.launcher3.BubbleTextView.DISPLAY_TASKBAR;
import static com.android.launcher3.Flags.enableCursorHoverStates;
+import static com.android.launcher3.Flags.enableRecentsInTaskbar;
import static com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_APP_PAIR;
import static com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_FOLDER;
import static com.android.launcher3.config.FeatureFlags.ENABLE_ALL_APPS_SEARCH_IN_TASKBAR;
@@ -30,6 +31,7 @@
import android.content.res.Resources;
import android.graphics.Canvas;
import android.graphics.Rect;
+import android.graphics.drawable.Drawable;
import android.os.Bundle;
import android.util.AttributeSet;
import android.view.DisplayCutout;
@@ -67,7 +69,11 @@
import com.android.launcher3.views.IconButtonView;
import com.android.quickstep.DeviceConfigWrapper;
import com.android.quickstep.util.AssistStateManager;
+import com.android.quickstep.util.DesktopTask;
+import com.android.quickstep.util.GroupTask;
+import com.android.systemui.shared.recents.model.Task;
+import java.util.List;
import java.util.function.Predicate;
/**
@@ -168,7 +174,7 @@
mAllAppsButton.setForegroundTint(
mActivityContext.getColor(R.color.all_apps_button_color));
- if (enableTaskbarPinning()) {
+ if (enableTaskbarPinning() || enableRecentsInTaskbar()) {
mTaskbarDivider = (IconButtonView) LayoutInflater.from(context).inflate(
R.layout.taskbar_divider,
this, false);
@@ -308,9 +314,10 @@
/**
* Inflates/binds the Hotseat views to show in the Taskbar given their ItemInfos.
*/
- protected void updateHotseatItems(ItemInfo[] hotseatItemInfos) {
+ protected void updateHotseatItems(ItemInfo[] hotseatItemInfos, List<GroupTask> recentTasks) {
int nextViewIndex = 0;
int numViewsAnimated = 0;
+ boolean addedDividerForRecents = false;
if (mAllAppsButton != null) {
removeView(mAllAppsButton);
@@ -321,8 +328,8 @@
}
removeView(mQsb);
- for (int i = 0; i < hotseatItemInfos.length; i++) {
- ItemInfo hotseatItemInfo = hotseatItemInfos[i];
+ // Add Hotseat icons.
+ for (ItemInfo hotseatItemInfo : hotseatItemInfos) {
if (hotseatItemInfo == null) {
continue;
}
@@ -388,11 +395,8 @@
}
// Apply the Hotseat ItemInfos, or hide the view if there is none for a given index.
- if (hotseatView instanceof BubbleTextView
- && hotseatItemInfo instanceof WorkspaceItemInfo) {
- BubbleTextView btv = (BubbleTextView) hotseatView;
- WorkspaceItemInfo workspaceInfo = (WorkspaceItemInfo) hotseatItemInfo;
-
+ if (hotseatView instanceof BubbleTextView btv
+ && hotseatItemInfo instanceof WorkspaceItemInfo workspaceInfo) {
boolean animate = btv.shouldAnimateIconChange((WorkspaceItemInfo) hotseatItemInfo);
btv.applyFromWorkspaceItem(workspaceInfo, animate, numViewsAnimated);
if (animate) {
@@ -405,6 +409,67 @@
}
nextViewIndex++;
}
+
+ if (mTaskbarDivider != null && !recentTasks.isEmpty()) {
+ addView(mTaskbarDivider, nextViewIndex++);
+ addedDividerForRecents = true;
+ }
+
+ // Add Recent/Running icons.
+ for (GroupTask task : recentTasks) {
+ // Replace any Recent views with the appropriate type if it's not already that type.
+ final int expectedLayoutResId;
+ boolean isCollection = false;
+ if (task.hasMultipleTasks()) {
+ if (task instanceof DesktopTask) {
+ // TODO(b/316004172): use Desktop tile layout.
+ expectedLayoutResId = -1;
+ } else {
+ // TODO(b/343289567): use R.layout.app_pair_icon
+ expectedLayoutResId = -1;
+ }
+ isCollection = true;
+ } else {
+ expectedLayoutResId = R.layout.taskbar_app_icon;
+ }
+
+ View recentIcon = null;
+ while (nextViewIndex < getChildCount()) {
+ recentIcon = getChildAt(nextViewIndex);
+
+ // see if the view can be reused
+ if ((recentIcon.getSourceLayoutResId() != expectedLayoutResId)
+ || (isCollection && (recentIcon.getTag() != task))) {
+ removeAndRecycle(recentIcon);
+ recentIcon = null;
+ } else {
+ // View found
+ break;
+ }
+ }
+
+ if (recentIcon == null) {
+ if (isCollection) {
+ // TODO(b/343289567 and b/316004172): support app pairs and desktop mode.
+ continue;
+ }
+
+ recentIcon = inflate(expectedLayoutResId);
+ LayoutParams lp = new LayoutParams(mIconTouchSize, mIconTouchSize);
+ recentIcon.setPadding(mItemPadding, mItemPadding, mItemPadding, mItemPadding);
+ addView(recentIcon, nextViewIndex, lp);
+ }
+
+ if (recentIcon instanceof BubbleTextView btv) {
+ applyGroupTaskToBubbleTextView(btv, task);
+ }
+ setClickAndLongClickListenersForIcon(recentIcon);
+ if (enableCursorHoverStates()) {
+ setHoverListenerForIcon(recentIcon);
+ }
+ nextViewIndex++;
+ }
+
// Remove remaining views
while (nextViewIndex < getChildCount()) {
removeAndRecycle(getChildAt(nextViewIndex));
@@ -413,8 +478,8 @@
if (mAllAppsButton != null) {
addView(mAllAppsButton, mIsRtl ? getChildCount() : 0);
- // if only all apps button present, don't include divider view.
- if (mTaskbarDivider != null && getChildCount() > 1) {
+ // If there are no recent tasks, add divider after All Apps (unless it's the only view).
+ if (!addedDividerForRecents && mTaskbarDivider != null && getChildCount() > 1) {
addView(mTaskbarDivider, mIsRtl ? (getChildCount() - 1) : 1);
}
}
@@ -425,6 +490,20 @@
}
}
+ /** Binds the GroupTask to the BubbleTextView to be ready to present to the user. */
+ public void applyGroupTaskToBubbleTextView(BubbleTextView btv, GroupTask groupTask) {
+ // TODO(b/343289567): support app pairs.
+ Task task1 = groupTask.task1;
+ // TODO(b/344038728): use FastBitmapDrawable instead of Drawable, to get disabled state
+ // while dragging.
+ Drawable taskIcon = groupTask.task1.icon;
+ if (taskIcon != null) {
+ taskIcon = taskIcon.getConstantState().newDrawable().mutate();
+ }
+ btv.applyIconAndLabel(taskIcon, task1.titleDescription);
+ btv.setTag(groupTask);
+ }
+
/**
* Sets OnClickListener and OnLongClickListener for the given view.
*/
@@ -677,7 +756,8 @@
// map over all the shortcuts on the taskbar
for (int i = 0; i < getChildCount(); i++) {
View item = getChildAt(i);
- if (op.evaluate((ItemInfo) item.getTag(), item)) {
+ // TODO(b/344657629): Support GroupTask as well for notification dots/popup
+ if (item.getTag() instanceof ItemInfo itemInfo && op.evaluate(itemInfo, item)) {
return;
}
}
@@ -694,6 +774,7 @@
View item = getChildAt(i);
if (!(item.getTag() instanceof ItemInfo)) {
// Should only happen for All Apps button.
+ // Will also happen for Recent/Running app icons. (Which have GroupTask as tags)
continue;
}
ItemInfo info = (ItemInfo) item.getTag();
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarViewController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarViewController.java
index 55745b5..e59a016 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarViewController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarViewController.java
@@ -70,6 +70,8 @@
import com.android.launcher3.util.MultiTranslateDelegate;
import com.android.launcher3.util.MultiValueAlpha;
import com.android.launcher3.views.IconButtonView;
+import com.android.quickstep.util.GroupTask;
+import com.android.systemui.shared.recents.model.Task;
import java.io.PrintWriter;
import java.util.Set;
@@ -224,7 +226,6 @@
}
LauncherAppState.getInstance(mActivity).getModel().removeCallbacks(mModelCallbacks);
mActivity.removeOnDeviceProfileChangeListener(mDeviceProfileChangeListener);
- mModelCallbacks.unregisterListeners();
}
public boolean areIconsVisible() {
@@ -520,21 +521,31 @@
for (View iconView : getIconViews()) {
if (iconView instanceof BubbleTextView btv) {
btv.updateRunningState(
- getRunningAppState(btv.getTargetPackageName(), runningPackages,
- minimizedPackages));
+ getRunningAppState(btv, runningPackages, minimizedPackages));
}
}
}
private BubbleTextView.RunningAppState getRunningAppState(
- String packageName,
+ BubbleTextView btv,
Set<String> runningPackages,
Set<String> minimizedPackages) {
- if (minimizedPackages.contains(packageName)) {
- return BubbleTextView.RunningAppState.MINIMIZED;
+ Object tag = btv.getTag();
+ if (tag instanceof ItemInfo itemInfo) {
+ if (minimizedPackages.contains(itemInfo.getTargetPackage())) {
+ return BubbleTextView.RunningAppState.MINIMIZED;
+ }
+ if (runningPackages.contains(itemInfo.getTargetPackage())) {
+ return BubbleTextView.RunningAppState.RUNNING;
+ }
}
- if (runningPackages.contains(packageName)) {
- return BubbleTextView.RunningAppState.RUNNING;
+ if (tag instanceof GroupTask groupTask && !groupTask.hasMultipleTasks()) {
+ if (minimizedPackages.contains(groupTask.task1.key.getPackageName())) {
+ return BubbleTextView.RunningAppState.MINIMIZED;
+ }
+ if (runningPackages.contains(groupTask.task1.key.getPackageName())) {
+ return BubbleTextView.RunningAppState.RUNNING;
+ }
}
return BubbleTextView.RunningAppState.NOT_RUNNING;
}
@@ -869,6 +880,27 @@
return mTaskbarView.isEventOverAnyItem(ev);
}
+ /** Called when there's a change in running apps to update the UI. */
+ public void commitRunningAppsToUI() {
+ mModelCallbacks.commitRunningAppsToUI();
+ }
+
+ /**
+ * To be called when the given Task is updated, so that we can tell TaskbarView to also update.
+ * @param task The Task whose e.g. icon changed.
+ */
+ public void onTaskUpdated(Task task) {
+ // Find the icon view(s) that changed.
+ for (View view : mTaskbarView.getIconViews()) {
+ if (view instanceof BubbleTextView btv
+ && view.getTag() instanceof GroupTask groupTask) {
+ if (groupTask.containsTask(task.key.id)) {
+ mTaskbarView.applyGroupTaskToBubbleTextView(btv, groupTask);
+ }
+ }
+ }
+ }
+
@Override
public void dumpLogs(String prefix, PrintWriter pw) {
pw.println(prefix + "TaskbarViewController:");
@@ -888,15 +920,4 @@
mModelCallbacks.dumpLogs(prefix + "\t", pw);
}
-
- /** Called when there's a change in running apps to update the UI. */
- public void commitRunningAppsToUI() {
- mModelCallbacks.commitRunningAppsToUI();
- }
-
- /** Call TaskbarModelCallbacks to update running apps. */
- public void updateRunningApps() {
- mModelCallbacks.updateRunningApps();
- }
-
}
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarController.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarController.java
index 028df34..15e4578 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarController.java
@@ -600,7 +600,7 @@
Bitmap bitmap = createOverflowBitmap(context);
LayoutInflater inflater = LayoutInflater.from(context);
BubbleView bubbleView = (BubbleView) inflater.inflate(
- R.layout.bubblebar_item_view, mBarView, false /* attachToRoot */);
+ R.layout.bubble_bar_overflow_button, mBarView, false /* attachToRoot */);
BubbleBarOverflow overflow = new BubbleBarOverflow(bubbleView);
bubbleView.setOverflow(overflow, bitmap);
return overflow;
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarItem.kt b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarItem.kt
index 43e21f4..39d1ed7 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarItem.kt
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarItem.kt
@@ -34,4 +34,8 @@
) : BubbleBarItem(info.key, view)
/** Represents the overflow bubble in the bubble bar. */
-data class BubbleBarOverflow(override var view: BubbleView) : BubbleBarItem("Overflow", view)
+data class BubbleBarOverflow(override var view: BubbleView) : BubbleBarItem(KEY, view) {
+ companion object {
+ const val KEY = "Overflow"
+ }
+}
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarView.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarView.java
index c7c63e8..0ea5031 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarView.java
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarView.java
@@ -44,6 +44,7 @@
import com.android.launcher3.R;
import com.android.launcher3.anim.SpringAnimationBuilder;
+import com.android.launcher3.taskbar.bubbles.animation.BubbleAnimator;
import com.android.launcher3.util.DisplayController;
import com.android.wm.shell.Flags;
import com.android.wm.shell.common.bubbles.BubbleBarLocation;
@@ -101,8 +102,6 @@
// During fade in animation we shift the bubble bar 1/60th of the screen width
private static final float FADE_IN_ANIM_POSITION_SHIFT = 1 / 60f;
- private static final int SCALE_IN_ANIMATION_DURATION_MS = 250;
-
/**
* Custom property to set alpha value for the bar view while a bubble is being dragged.
* Skips applying alpha to the dragged bubble.
@@ -161,11 +160,12 @@
// collapsed state and 1 to the fully expanded state.
private final ValueAnimator mWidthAnimator = ValueAnimator.ofFloat(0, 1);
- /** An animator used for scaling in a new bubble to the bubble bar while expanded. */
+ /** An animator used for animating individual bubbles in the bubble bar while expanded. */
@Nullable
- private ValueAnimator mNewBubbleScaleInAnimator = null;
+ private BubbleAnimator mBubbleAnimator = null;
@Nullable
private ValueAnimator mScalePaddingAnimator;
+
@Nullable
private Animator mBubbleBarLocationAnimator = null;
@@ -258,6 +258,7 @@
}
if (!Flags.animateBubbleSizeChange()) {
setIconSizeAndPadding(newIconSize, newBubbleBarPadding);
+ return;
}
if (mScalePaddingAnimator != null && mScalePaddingAnimator.isRunning()) {
mScalePaddingAnimator.cancel();
@@ -670,38 +671,37 @@
bubble.setScaleX(0f);
bubble.setScaleY(0f);
addView(bubble, 0, lp);
- createNewBubbleScaleInAnimator(bubble);
- mNewBubbleScaleInAnimator.start();
+
+ mBubbleAnimator = new BubbleAnimator(mIconSize, mExpandedBarIconsSpacing,
+ getChildCount(), mBubbleBarLocation.isOnLeft(isLayoutRtl()));
+ BubbleAnimator.Listener listener = new BubbleAnimator.Listener() {
+
+ @Override
+ public void onAnimationEnd() {
+ updateWidth();
+ mBubbleAnimator = null;
+ }
+
+ @Override
+ public void onAnimationCancel() {
+ bubble.setScaleX(1);
+ bubble.setScaleY(1);
+ }
+
+ @Override
+ public void onAnimationUpdate(float animatedFraction) {
+ bubble.setScaleX(animatedFraction);
+ bubble.setScaleY(animatedFraction);
+ updateBubblesLayoutProperties(mBubbleBarLocation);
+ invalidate();
+ }
+ };
+ mBubbleAnimator.animateNewBubble(indexOfChild(mSelectedBubbleView), listener);
} else {
addView(bubble, 0, lp);
}
}
- private void createNewBubbleScaleInAnimator(View bubble) {
- mNewBubbleScaleInAnimator = ValueAnimator.ofFloat(0, 1);
- mNewBubbleScaleInAnimator.setDuration(SCALE_IN_ANIMATION_DURATION_MS);
- mNewBubbleScaleInAnimator.addUpdateListener(animation -> {
- float animatedFraction = animation.getAnimatedFraction();
- bubble.setScaleX(animatedFraction);
- bubble.setScaleY(animatedFraction);
- updateBubblesLayoutProperties(mBubbleBarLocation);
- invalidate();
- });
- mNewBubbleScaleInAnimator.addListener(new AnimatorListenerAdapter() {
- @Override
- public void onAnimationCancel(Animator animation) {
- bubble.setScaleX(1);
- bubble.setScaleY(1);
- }
-
- @Override
- public void onAnimationEnd(Animator animation) {
- updateWidth();
- mNewBubbleScaleInAnimator = null;
- }
- });
- }
-
// TODO: (b/280605790) animate it
@Override
public void addView(View child, int index, ViewGroup.LayoutParams params) {
@@ -716,6 +716,50 @@
updateContentDescription();
}
+ /** Removes the given bubble from the bubble bar. */
+ public void removeBubble(View bubble) {
+ if (isExpanded()) {
+ // TODO b/347062801 - animate the bubble bar if the last bubble is removed
+ int bubbleCount = getChildCount();
+ mBubbleAnimator = new BubbleAnimator(mIconSize, mExpandedBarIconsSpacing,
+ bubbleCount, mBubbleBarLocation.isOnLeft(isLayoutRtl()));
+ BubbleAnimator.Listener listener = new BubbleAnimator.Listener() {
+
+ @Override
+ public void onAnimationEnd() {
+ removeView(bubble);
+ mBubbleAnimator = null;
+ }
+
+ @Override
+ public void onAnimationCancel() {
+ bubble.setScaleX(0);
+ bubble.setScaleY(0);
+ }
+
+ @Override
+ public void onAnimationUpdate(float animatedFraction) {
+ bubble.setScaleX(1 - animatedFraction);
+ bubble.setScaleY(1 - animatedFraction);
+ updateBubblesLayoutProperties(mBubbleBarLocation);
+ invalidate();
+ }
+ };
+ int bubbleIndex = indexOfChild(bubble);
+ BubbleView lastBubble = (BubbleView) getChildAt(bubbleCount - 1);
+ String lastBubbleKey = lastBubble.getBubble().getKey();
+ boolean removingLastBubble =
+ BubbleBarOverflow.KEY.equals(lastBubbleKey)
+ ? bubbleIndex == bubbleCount - 2
+ : bubbleIndex == bubbleCount - 1;
+ mBubbleAnimator.animateRemovedBubble(
+ indexOfChild(bubble), indexOfChild(mSelectedBubbleView), removingLastBubble,
+ listener);
+ } else {
+ removeView(bubble);
+ }
+ }
+
// TODO: (b/283309949) animate it
@Override
public void removeView(View view) {
@@ -781,9 +825,14 @@
bv.setDragTranslationX(0f);
bv.setOffsetX(0f);
- bv.setScaleX(mIconScale);
- bv.setScaleY(mIconScale);
+ if (mBubbleAnimator == null || !mBubbleAnimator.isRunning()) {
+ // if the bubble animator is running don't set scale here, it will be set by the
+ // animator
+ bv.setScaleX(mIconScale);
+ bv.setScaleY(mIconScale);
+ }
bv.setTranslationY(ty);
+
// the position of the bubble when the bar is fully expanded
final float expandedX = getExpandedBubbleTranslationX(i, bubbleCount, onLeft);
// the position of the bubble when the bar is fully collapsed
@@ -861,9 +910,8 @@
}
final float iconAndSpacing = getScaledIconSize() + mExpandedBarIconsSpacing;
float translationX;
- if (mNewBubbleScaleInAnimator != null && mNewBubbleScaleInAnimator.isRunning()) {
- translationX = getExpandedBubbleTranslationXDuringScaleAnimation(
- bubbleIndex, bubbleCount, onLeft);
+ if (mBubbleAnimator != null && mBubbleAnimator.isRunning()) {
+ return mBubbleAnimator.getExpandedBubbleTranslationX(bubbleIndex) + mBubbleBarPadding;
} else if (onLeft) {
translationX = mBubbleBarPadding + (bubbleCount - bubbleIndex - 1) * iconAndSpacing;
} else {
@@ -872,51 +920,6 @@
return translationX - getScaleIconShift();
}
- /**
- * Returns the translation X for the bubble at index {@code bubbleIndex} when the bubble bar is
- * expanded <b>and</b> a new bubble is animating in.
- *
- * <p>This method assumes that the animation is running so callers are expected to verify that
- * before calling it.
- */
- private float getExpandedBubbleTranslationXDuringScaleAnimation(
- int bubbleIndex, int bubbleCount, boolean onLeft) {
- // when the new bubble scale animation is running, a new bubble is animating in while the
- // bubble bar is expanded, so we have at least 2 bubbles in the bubble bar - the expanded
- // one, and the new one animating in.
-
- if (mNewBubbleScaleInAnimator == null) {
- // callers of this method are expected to verify that the animation is running, but the
- // compiler doesn't know that.
- return 0;
- }
- final float iconAndSpacing = getScaledIconSize() + mExpandedBarIconsSpacing;
- final float newBubbleScale = mNewBubbleScaleInAnimator.getAnimatedFraction();
- // the new bubble is scaling in from the center, so we need to adjust its translation so
- // that the distance to the adjacent bubble scales at the same rate.
- final float pivotAdjustment = -(1 - newBubbleScale) * getScaledIconSize() / 2f;
-
- if (onLeft) {
- if (bubbleIndex == 0) {
- // this is the animating bubble. use scaled spacing between it and the bubble to
- // its left
- return (bubbleCount - 1) * getScaledIconSize()
- + (bubbleCount - 2) * mExpandedBarIconsSpacing
- + newBubbleScale * mExpandedBarIconsSpacing
- + pivotAdjustment;
- }
- // when the bubble bar is on the left, only the translation of the right-most bubble
- // is affected by the scale animation.
- return (bubbleCount - bubbleIndex - 1) * iconAndSpacing;
- } else if (bubbleIndex == 0) {
- // the bubble bar is on the right, and this is the animating bubble. it only needs
- // to be adjusted for the scaling pivot.
- return pivotAdjustment;
- } else {
- return iconAndSpacing * (bubbleIndex - 1 + newBubbleScale);
- }
- }
-
private float getCollapsedBubbleTranslationX(int bubbleIndex, int bubbleCount,
boolean onLeft) {
if (bubbleIndex < 0 || bubbleIndex >= bubbleCount) {
@@ -979,9 +982,11 @@
BubbleView previouslySelectedBubble = mSelectedBubbleView;
mSelectedBubbleView = view;
mBubbleBarBackground.showArrow(view != null);
- // TODO: (b/283309949) remove animation should be implemented first, so than arrow
- // animation is adjusted, skip animation for now
- updateArrowForSelected(previouslySelectedBubble != null);
+
+ // if bubbles are being animated, the arrow position will be set as part of the animation
+ if (mBubbleAnimator == null) {
+ updateArrowForSelected(previouslySelectedBubble != null);
+ }
}
/**
@@ -1036,6 +1041,9 @@
}
private float arrowPositionForSelectedWhenExpanded(BubbleBarLocation bubbleBarLocation) {
+ if (mBubbleAnimator != null && mBubbleAnimator.isRunning()) {
+ return mBubbleAnimator.getArrowPosition() + mBubbleBarPadding;
+ }
final int index = indexOfChild(mSelectedBubbleView);
final float selectedBubbleTranslationX = getExpandedBubbleTranslationX(
index, getChildCount(), bubbleBarLocation.isOnLeft(isLayoutRtl()));
@@ -1101,20 +1109,14 @@
*/
public float expandedWidth() {
final int childCount = getChildCount();
- // spaces amount is less than child count by 1, or 0 if no child views
- final float totalSpace;
- final float totalIconSize;
- if (mNewBubbleScaleInAnimator != null && mNewBubbleScaleInAnimator.isRunning()) {
- // when this animation is running, a new bubble is animating in while the bubble bar is
- // expanded, so we have at least 2 bubbles in the bubble bar.
- final float newBubbleScale = mNewBubbleScaleInAnimator.getAnimatedFraction();
- totalSpace = (childCount - 2 + newBubbleScale) * mExpandedBarIconsSpacing;
- totalIconSize = (childCount - 1 + newBubbleScale) * getScaledIconSize();
- } else {
- totalSpace = Math.max(childCount - 1, 0) * mExpandedBarIconsSpacing;
- totalIconSize = childCount * getScaledIconSize();
+ final float horizontalPadding = 2 * mBubbleBarPadding;
+ if (mBubbleAnimator != null && mBubbleAnimator.isRunning()) {
+ return mBubbleAnimator.getExpandedWidth() + horizontalPadding;
}
- return totalIconSize + totalSpace + 2 * mBubbleBarPadding;
+ // spaces amount is less than child count by 1, or 0 if no child views
+ final float totalSpace = Math.max(childCount - 1, 0) * mExpandedBarIconsSpacing;
+ final float totalIconSize = childCount * getScaledIconSize();
+ return totalIconSize + totalSpace + horizontalPadding;
}
private float collapsedWidth() {
@@ -1165,7 +1167,6 @@
return mIsAnimatingNewBubble;
}
-
private boolean hasOverview() {
// Overview is always the last bubble
View lastChild = getChildAt(getChildCount() - 1);
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java
index 951b99d..da0826b 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java
@@ -388,7 +388,7 @@
*/
public void removeBubble(BubbleBarItem b) {
if (b != null) {
- mBarView.removeView(b.getView());
+ mBarView.removeBubble(b.getView());
} else {
Log.w(TAG, "removeBubble, bubble was null!");
}
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleStashController.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleStashController.java
index 74ddf90..185f85f 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleStashController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleStashController.java
@@ -184,7 +184,8 @@
/** Whether bubbles are showing on the launcher home page. */
public boolean isBubblesShowingOnHome() {
- return mBubblesShowingOnHome;
+ boolean hasBubbles = mBarViewController != null && mBarViewController.hasBubbles();
+ return mBubblesShowingOnHome && hasBubbles;
}
// TODO: when tapping on an app in overview, this is a bit delayed compared to taskbar stashing
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/animation/BubbleAnimator.kt b/quickstep/src/com/android/launcher3/taskbar/bubbles/animation/BubbleAnimator.kt
new file mode 100644
index 0000000..7672743
--- /dev/null
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/animation/BubbleAnimator.kt
@@ -0,0 +1,297 @@
+/*
+ * Copyright (C) 2024 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.animation
+
+import androidx.core.animation.Animator
+import androidx.core.animation.ValueAnimator
+
+/**
+ * Animates individual bubbles within the bubble bar while the bubble bar is expanded.
+ *
+ * This class should only be kept for the duration of the animation and a new instance should be
+ * created for each animation.
+ */
+class BubbleAnimator(
+ private val iconSize: Float,
+ private val expandedBarIconSpacing: Float,
+ private val bubbleCount: Int,
+ private val onLeft: Boolean,
+) {
+
+ companion object {
+ const val ANIMATION_DURATION_MS = 250L
+ }
+
+ private var state: State = State.Idle
+ private lateinit var animator: ValueAnimator
+
+ fun animateNewBubble(selectedBubbleIndex: Int, listener: Listener) {
+ animator = createAnimator(listener)
+ state = State.AddingBubble(selectedBubbleIndex)
+ animator.start()
+ }
+
+ fun animateRemovedBubble(
+ bubbleIndex: Int,
+ selectedBubbleIndex: Int,
+ removingLastBubble: Boolean,
+ listener: Listener
+ ) {
+ animator = createAnimator(listener)
+ state = State.RemovingBubble(bubbleIndex, selectedBubbleIndex, removingLastBubble)
+ animator.start()
+ }
+
+ private fun createAnimator(listener: Listener): ValueAnimator {
+ val animator = ValueAnimator.ofFloat(0f, 1f).setDuration(ANIMATION_DURATION_MS)
+ animator.addUpdateListener { animation ->
+ val animatedFraction = (animation as ValueAnimator).animatedFraction
+ listener.onAnimationUpdate(animatedFraction)
+ }
+ animator.addListener(
+ object : Animator.AnimatorListener {
+
+ override fun onAnimationCancel(animation: Animator) {
+ listener.onAnimationCancel()
+ }
+
+ override fun onAnimationEnd(animation: Animator) {
+ state = State.Idle
+ listener.onAnimationEnd()
+ }
+
+ override fun onAnimationRepeat(animation: Animator) {}
+
+ override fun onAnimationStart(animation: Animator) {}
+ }
+ )
+ return animator
+ }
+
+ /**
+ * The translation X of the bubble at index [bubbleIndex] according to the progress of the
+ * animation.
+ *
+ * Callers should verify that the animation is running before calling this.
+ *
+ * @see isRunning
+ */
+ fun getExpandedBubbleTranslationX(bubbleIndex: Int): Float {
+ return when (val state = state) {
+ State.Idle -> 0f
+ is State.AddingBubble ->
+ getExpandedBubbleTranslationXWhileScalingBubble(
+ bubbleIndex = bubbleIndex,
+ scalingBubbleIndex = 0,
+ bubbleScale = animator.animatedFraction
+ )
+ is State.RemovingBubble ->
+ getExpandedBubbleTranslationXWhileScalingBubble(
+ bubbleIndex = bubbleIndex,
+ scalingBubbleIndex = state.bubbleIndex,
+ bubbleScale = 1 - animator.animatedFraction
+ )
+ }
+ }
+
+ /**
+ * The expanded width of the bubble bar according to the progress of the animation.
+ *
+ * Callers should verify that the animation is running before calling this.
+ *
+ * @see isRunning
+ */
+ fun getExpandedWidth(): Float {
+ val bubbleScale =
+ when (state) {
+ State.Idle -> 0f
+ is State.AddingBubble -> animator.animatedFraction
+ is State.RemovingBubble -> 1 - animator.animatedFraction
+ }
+ // When this animator is running the bubble bar is expanded so it's safe to assume that we
+ // have at least 2 bubbles, but should update the logic to support optional overflow.
+ // If we're removing the last bubble, the entire bar should animate and we shouldn't get
+ // here.
+ val totalSpace = (bubbleCount - 2 + bubbleScale) * expandedBarIconSpacing
+ val totalIconSize = (bubbleCount - 1 + bubbleScale) * iconSize
+ return totalIconSize + totalSpace
+ }
+
+ /**
+ * Returns the arrow position according to the progress of the animation and, if the selected
+ * bubble is being removed, accounting to the newly selected bubble.
+ *
+ * Callers should verify that the animation is running before calling this.
+ *
+ * @see isRunning
+ */
+ fun getArrowPosition(): Float {
+ return when (val state = state) {
+ State.Idle -> 0f
+ is State.AddingBubble -> {
+ val tx =
+ getExpandedBubbleTranslationXWhileScalingBubble(
+ bubbleIndex = state.selectedBubbleIndex,
+ scalingBubbleIndex = 0,
+ bubbleScale = animator.animatedFraction
+ )
+ tx + iconSize / 2f
+ }
+ is State.RemovingBubble -> getArrowPositionWhenRemovingBubble(state)
+ }
+ }
+
+ private fun getArrowPositionWhenRemovingBubble(state: State.RemovingBubble): Float {
+ return if (state.selectedBubbleIndex != state.bubbleIndex) {
+ // if we're not removing the selected bubble, the selected bubble doesn't change so just
+ // return the translation X of the selected bubble and add half icon
+ val tx =
+ getExpandedBubbleTranslationXWhileScalingBubble(
+ bubbleIndex = state.selectedBubbleIndex,
+ scalingBubbleIndex = state.bubbleIndex,
+ bubbleScale = 1 - animator.animatedFraction
+ )
+ tx + iconSize / 2f
+ } else {
+ // we're removing the selected bubble so the arrow needs to point to a different bubble.
+ // if we're removing the last bubble the newly selected bubble will be the second to
+ // last. otherwise, it'll be the next bubble (closer to the overflow)
+ val iconAndSpacing = iconSize + expandedBarIconSpacing
+ if (state.removingLastBubble) {
+ if (onLeft) {
+ // the newly selected bubble is the bubble to the right. at the end of the
+ // animation all the bubbles will have shifted left, so the arrow stays at the
+ // same distance from the left edge of bar
+ (bubbleCount - state.bubbleIndex - 1) * iconAndSpacing + iconSize / 2f
+ } else {
+ // the newly selected bubble is the bubble to the left. at the end of the
+ // animation all the bubbles will have shifted right, and the arrow would
+ // eventually be closer to the left edge of the bar by iconAndSpacing
+ val initialTx = state.bubbleIndex * iconAndSpacing + iconSize / 2f
+ initialTx - animator.animatedFraction * iconAndSpacing
+ }
+ } else {
+ if (onLeft) {
+ // the newly selected bubble is to the left, and bubbles are shifting left, so
+ // move the arrow closer to the left edge of the bar by iconAndSpacing
+ val initialTx =
+ (bubbleCount - state.bubbleIndex - 1) * iconAndSpacing + iconSize / 2f
+ initialTx - animator.animatedFraction * iconAndSpacing
+ } else {
+ // the newly selected bubble is to the right, and bubbles are shifting right, so
+ // the arrow stays at the same distance from the left edge of the bar
+ state.bubbleIndex * iconAndSpacing + iconSize / 2f
+ }
+ }
+ }
+ }
+
+ /**
+ * Returns the translation X for the bubble at index {@code bubbleIndex} when the bubble bar is
+ * expanded and a bubble is animating in or out.
+ *
+ * @param bubbleIndex the index of the bubble for which the translation is requested
+ * @param scalingBubbleIndex the index of the bubble that is animating
+ * @param bubbleScale the current scale of the animating bubble
+ */
+ private fun getExpandedBubbleTranslationXWhileScalingBubble(
+ bubbleIndex: Int,
+ scalingBubbleIndex: Int,
+ bubbleScale: Float
+ ): Float {
+ val iconAndSpacing = iconSize + expandedBarIconSpacing
+ // the bubble is scaling from the center, so we need to adjust its translation so
+ // that the distance to the adjacent bubble scales at the same rate.
+ val pivotAdjustment = -(1 - bubbleScale) * iconSize / 2f
+
+ return if (onLeft) {
+ when {
+ bubbleIndex < scalingBubbleIndex ->
+ // the bar is on the left and the current bubble is to the right of the scaling
+ // bubble so account for its scale
+ (bubbleCount - bubbleIndex - 2 + bubbleScale) * iconAndSpacing
+ bubbleIndex == scalingBubbleIndex -> {
+ // the bar is on the left and this is the scaling bubble
+ val totalIconSize = (bubbleCount - bubbleIndex - 1) * iconSize
+ // don't count the spacing between the scaling bubble and the bubble on the left
+ // because we need to scale that space
+ val totalSpacing = (bubbleCount - bubbleIndex - 2) * expandedBarIconSpacing
+ val scaledSpace = bubbleScale * expandedBarIconSpacing
+ totalIconSize + totalSpacing + scaledSpace + pivotAdjustment
+ }
+ else ->
+ // the bar is on the left and the scaling bubble is on the right. the current
+ // bubble is unaffected by the scaling bubble
+ (bubbleCount - bubbleIndex - 1) * iconAndSpacing
+ }
+ } else {
+ when {
+ bubbleIndex < scalingBubbleIndex ->
+ // the bar is on the right and the scaling bubble is on the right. the current
+ // bubble is unaffected by the scaling bubble
+ iconAndSpacing * bubbleIndex
+ bubbleIndex == scalingBubbleIndex ->
+ // the bar is on the right, and this is the animating bubble. it only needs to
+ // be adjusted for the scaling pivot.
+ iconAndSpacing * bubbleIndex + pivotAdjustment
+ else ->
+ // the bar is on the right and the scaling bubble is on the left so account for
+ // its scale
+ iconAndSpacing * (bubbleIndex - 1 + bubbleScale)
+ }
+ }
+ }
+
+ val isRunning: Boolean
+ get() = state != State.Idle
+
+ /** The state of the animation. */
+ sealed interface State {
+
+ /** The animation is not running. */
+ data object Idle : State
+
+ /** A new bubble is being added to the bubble bar. */
+ data class AddingBubble(val selectedBubbleIndex: Int) : State
+
+ /** A bubble is being removed from the bubble bar. */
+ data class RemovingBubble(
+ /** The index of the bubble being removed. */
+ val bubbleIndex: Int,
+ /** The index of the selected bubble. */
+ val selectedBubbleIndex: Int,
+ /** Whether the bubble being removed is also the last bubble. */
+ val removingLastBubble: Boolean
+ ) : State
+ }
+
+ /** Callbacks for the animation. */
+ interface Listener {
+
+ /**
+ * Notifies the listener of an animation update event, where `animatedFraction` represents
+ * the progress of the animation starting from 0 and ending at 1.
+ */
+ fun onAnimationUpdate(animatedFraction: Float)
+
+ /** Notifies the listener that the animation was canceled. */
+ fun onAnimationCancel()
+
+ /** Notifies that listener that the animation ended. */
+ fun onAnimationEnd()
+ }
+}
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/animation/BubbleBarViewAnimator.kt b/quickstep/src/com/android/launcher3/taskbar/bubbles/animation/BubbleBarViewAnimator.kt
index 2dcd932..feff9fd 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/animation/BubbleBarViewAnimator.kt
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/animation/BubbleBarViewAnimator.kt
@@ -41,7 +41,7 @@
private var animatingBubble: AnimatingBubble? = null
private val bubbleBarBounceDistanceInPx =
- bubbleBarView.resources.getDimensionPixelSize(R.dimen.bubblebar_bounce_distance)
+ bubbleBarView.resources.getDimensionPixelSize(R.dimen.bubblebar_bounce_distance)
private companion object {
/** The time to show the flyout. */
@@ -347,7 +347,7 @@
*/
private fun buildBubbleBarBounceAnimation() = Runnable {
bubbleBarView.onAnimatingBubbleStarted()
- val ty = bubbleBarView.translationY
+ val ty = bubbleStashController.bubbleBarTranslationY
val springBackAnimation = PhysicsAnimator.getInstance(bubbleBarView)
springBackAnimation.setDefaultSpringConfig(springConfig)
diff --git a/quickstep/src/com/android/launcher3/taskbar/customization/TaskbarFeatureEvaluator.kt b/quickstep/src/com/android/launcher3/taskbar/customization/TaskbarFeatureEvaluator.kt
index 668a87d..ac7dd06 100644
--- a/quickstep/src/com/android/launcher3/taskbar/customization/TaskbarFeatureEvaluator.kt
+++ b/quickstep/src/com/android/launcher3/taskbar/customization/TaskbarFeatureEvaluator.kt
@@ -33,7 +33,7 @@
val hasNavButtons = taskbarActivityContext.isThreeButtonNav
val hasRecents: Boolean
- get() = taskbarControllers.taskbarRecentAppsController.isEnabled
+ get() = taskbarControllers.taskbarRecentAppsController.shownTasks.isNotEmpty()
val hasDivider: Boolean
get() = enableTaskbarPinning() || hasRecents
diff --git a/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java b/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java
index 2168f7a..037f2f6 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java
@@ -60,6 +60,8 @@
import static com.android.launcher3.util.DisplayController.CHANGE_ACTIVE_SCREEN;
import static com.android.launcher3.util.DisplayController.CHANGE_NAVIGATION_MODE;
import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR;
+import static com.android.quickstep.util.ActiveGestureErrorDetector.GestureEvent.QUICK_SWITCH_FROM_HOME_FAILED;
+import static com.android.quickstep.util.ActiveGestureErrorDetector.GestureEvent.QUICK_SWITCH_FROM_HOME_FALLBACK;
import static com.android.quickstep.util.AnimUtils.completeRunnableListCallback;
import static com.android.quickstep.util.SplitAnimationTimings.TABLET_HOME_TO_SPLIT;
import static com.android.systemui.shared.system.ActivityManagerWrapper.CLOSE_SYSTEM_WINDOWS_REASON_HOME_KEY;
@@ -172,6 +174,7 @@
import com.android.quickstep.SystemUiProxy;
import com.android.quickstep.TaskUtils;
import com.android.quickstep.TouchInteractionService.TISBinder;
+import com.android.quickstep.util.ActiveGestureLog;
import com.android.quickstep.util.AsyncClockEventDelegate;
import com.android.quickstep.util.GroupTask;
import com.android.quickstep.util.LauncherUnfoldAnimationController;
@@ -198,8 +201,6 @@
import com.android.systemui.unfold.progress.RemoteUnfoldTransitionReceiver;
import com.android.systemui.unfold.updates.RotationChangeProvider;
-import kotlin.Unit;
-
import java.io.FileDescriptor;
import java.io.PrintWriter;
import java.util.ArrayList;
@@ -212,6 +213,8 @@
import java.util.function.Predicate;
import java.util.stream.Stream;
+import kotlin.Unit;
+
public class QuickstepLauncher extends Launcher implements RecentsViewContainer {
private static final boolean TRACE_LAYOUTS =
SystemProperties.getBoolean("persist.debug.trace_layouts", false);
@@ -581,9 +584,19 @@
}
case QUICK_SWITCH_STATE_ORDINAL: {
RecentsView rv = getOverviewPanel();
- TaskView tasktolaunch = rv.getCurrentPageTaskView();
- if (tasktolaunch != null) {
- tasktolaunch.launchTask(success -> {
+ TaskView currentPageTask = rv.getCurrentPageTaskView();
+ TaskView fallbackTask = rv.getTaskViewAt(0);
+ if (currentPageTask != null || fallbackTask != null) {
+ TaskView taskToLaunch = currentPageTask;
+ if (currentPageTask == null) {
+ taskToLaunch = fallbackTask;
+ ActiveGestureLog.INSTANCE.addLog(new ActiveGestureLog.CompoundString(
+ "Quick switch from home fallback case: The TaskView at index ")
+ .append(rv.getCurrentPage())
+ .append(" is missing."),
+ QUICK_SWITCH_FROM_HOME_FALLBACK);
+ }
+ taskToLaunch.launchTask(success -> {
if (!success) {
getStateManager().goToState(OVERVIEW);
} else {
@@ -592,6 +605,11 @@
return Unit.INSTANCE;
});
} else {
+ ActiveGestureLog.INSTANCE.addLog(new ActiveGestureLog.CompoundString(
+ "Quick switch from home failed: TaskViews at indices ")
+ .append(rv.getCurrentPage())
+ .append(" and 0 are missing."),
+ QUICK_SWITCH_FROM_HOME_FAILED);
getStateManager().goToState(NORMAL);
}
break;
diff --git a/quickstep/src/com/android/launcher3/uioverrides/QuickstepWidgetHolder.java b/quickstep/src/com/android/launcher3/uioverrides/QuickstepWidgetHolder.java
index 01d5ff0..56fc4d1 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/QuickstepWidgetHolder.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/QuickstepWidgetHolder.java
@@ -17,7 +17,6 @@
import static com.android.launcher3.BuildConfig.WIDGETS_ENABLED;
import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
-import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR;
import android.appwidget.AppWidgetHost;
import android.appwidget.AppWidgetHostView;
@@ -100,7 +99,7 @@
// for concurrent modification.
new ArrayList<>(h.mProviderChangedListeners).forEach(
ProviderChangedListener::notifyWidgetProvidersChanged))),
- UI_HELPER_EXECUTOR.getLooper());
+ getWidgetHolderExecutor().getLooper());
if (WIDGETS_ENABLED) {
sWidgetHost.startListening();
}
@@ -199,8 +198,10 @@
return;
}
- sWidgetHost.setAppWidgetHidden();
- setListeningFlag(false);
+ getWidgetHolderExecutor().execute(() -> {
+ sWidgetHost.setAppWidgetHidden();
+ setListeningFlag(false);
+ });
}
@Override
diff --git a/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java b/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java
index 1acafab..fb2a982 100644
--- a/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java
+++ b/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java
@@ -1196,7 +1196,7 @@
}
if (mContainerInterface.getTaskbarController() != null) {
// Resets this value as the gesture is now complete.
- mContainerInterface.getTaskbarController().setUserIsGoingHome(false);
+ mContainerInterface.getTaskbarController().setUserIsNotGoingHome(false);
}
ActiveGestureLog.INSTANCE.addLog(
new ActiveGestureLog.CompoundString("onSettledOnEndTarget ")
@@ -1205,17 +1205,28 @@
}
/** @return Whether this was the task we were waiting to appear, and thus handled it. */
- protected boolean handleTaskAppeared(RemoteAnimationTarget[] appearedTaskTarget) {
+ protected boolean handleTaskAppeared(@NonNull RemoteAnimationTarget[] appearedTaskTargets,
+ @NonNull ActiveGestureLog.CompoundString failureReason) {
if (mStateCallback.hasStates(STATE_HANDLER_INVALIDATED)) {
+ failureReason.append("State handler was invalidated");
return false;
}
- boolean hasStartedTaskBefore = Arrays.stream(appearedTaskTarget).anyMatch(
- mGestureState.mLastStartedTaskIdPredicate);
- if (mStateCallback.hasStates(STATE_START_NEW_TASK) && hasStartedTaskBefore) {
- reset();
- return true;
+ boolean stateStartNewTaskSet = mStateCallback.hasStates(STATE_START_NEW_TASK);
+ if (!stateStartNewTaskSet || !hasStartedTaskBefore(appearedTaskTargets)) {
+ if (!stateStartNewTaskSet) {
+ failureReason.append("STATE_START_NEW_TASK was never set");
+ } else {
+ TaskInfo taskInfo = appearedTaskTargets[0].taskInfo;
+ failureReason.append("Unexpected task appeared")
+ .append(" id=")
+ .append(taskInfo.taskId)
+ .append(" pkg=")
+ .append(taskInfo.baseIntent.getComponent().getPackageName());
+ }
+ return false;
}
- return false;
+ reset();
+ return true;
}
private float dpiFromPx(float pixels) {
@@ -1350,7 +1361,7 @@
&& mIsTransientTaskbar
&& mContainerInterface.getTaskbarController() != null) {
mContainerInterface.getTaskbarController()
- .setUserIsGoingHome(endTarget == GestureState.GestureEndTarget.HOME);
+ .setUserIsNotGoingHome(endTarget != GestureState.GestureEndTarget.HOME);
}
float endShift = endTarget == ALL_APPS ? mDragLengthFactor
@@ -1796,6 +1807,8 @@
&& (windowRotation == ROTATION_90 || windowRotation == ROTATION_270)) {
builder.setFromRotation(mRemoteTargetHandles[0].getTaskViewSimulator(), windowRotation,
taskInfo.displayCutoutInsets);
+ } else if (taskInfo.displayCutoutInsets != null) {
+ builder.setDisplayCutoutInsets(taskInfo.displayCutoutInsets);
}
final SwipePipToHomeAnimator swipePipToHomeAnimator = builder.build();
AnimatorPlaybackController activityAnimationToHome =
@@ -2400,14 +2413,18 @@
}
}
+ private boolean hasStartedTaskBefore(@NonNull RemoteAnimationTarget[] appearedTaskTargets) {
+ return Arrays.stream(appearedTaskTargets)
+ .anyMatch(mGestureState.mLastStartedTaskIdPredicate);
+ }
+
@Override
public void onTasksAppeared(@NonNull RemoteAnimationTarget[] appearedTaskTargets) {
if (mRecentsAnimationController == null) {
return;
}
- boolean hasStartedTaskBefore = Arrays.stream(appearedTaskTargets).anyMatch(
- mGestureState.mLastStartedTaskIdPredicate);
- if (!mStateCallback.hasStates(STATE_GESTURE_COMPLETED) && !hasStartedTaskBefore) {
+ if (!mStateCallback.hasStates(STATE_GESTURE_COMPLETED)
+ && !hasStartedTaskBefore(appearedTaskTargets)) {
// This is a special case, if a task is started mid-gesture that wasn't a part of a
// previous quickswitch task launch, then cancel the animation back to the app
RemoteAnimationTarget appearedTaskTarget = appearedTaskTargets[0];
@@ -2421,7 +2438,11 @@
finishRecentsAnimationOnTasksAppeared(null /* onFinishComplete */);
return;
}
- if (!handleTaskAppeared(appearedTaskTargets)) {
+ ActiveGestureLog.CompoundString handleTaskFailureReason =
+ new ActiveGestureLog.CompoundString("handleTaskAppeared check failed: ");
+ if (!handleTaskAppeared(appearedTaskTargets, handleTaskFailureReason)) {
+ ActiveGestureLog.INSTANCE.addLog(handleTaskFailureReason);
+ finishRecentsAnimationOnTasksAppeared(null /* onFinishComplete */);
return;
}
Optional<RemoteAnimationTarget> taskTargetOptional =
diff --git a/quickstep/src/com/android/quickstep/FallbackSwipeHandler.java b/quickstep/src/com/android/quickstep/FallbackSwipeHandler.java
index 625b6c6..9b66154 100644
--- a/quickstep/src/com/android/quickstep/FallbackSwipeHandler.java
+++ b/quickstep/src/com/android/quickstep/FallbackSwipeHandler.java
@@ -64,6 +64,7 @@
import com.android.launcher3.util.DisplayController;
import com.android.quickstep.fallback.FallbackRecentsView;
import com.android.quickstep.fallback.RecentsState;
+import com.android.quickstep.util.ActiveGestureLog;
import com.android.quickstep.util.RectFSpringAnim;
import com.android.quickstep.util.SurfaceTransaction.SurfaceProperties;
import com.android.quickstep.util.TransformParams;
@@ -170,14 +171,16 @@
}
@Override
- protected boolean handleTaskAppeared(RemoteAnimationTarget[] appearedTaskTarget) {
+ protected boolean handleTaskAppeared(@NonNull RemoteAnimationTarget[] appearedTaskTarget,
+ @NonNull ActiveGestureLog.CompoundString failureReason) {
if (mActiveAnimationFactory != null
&& mActiveAnimationFactory.handleHomeTaskAppeared(appearedTaskTarget)) {
mActiveAnimationFactory = null;
+ failureReason.append("(FallbackSwipeHandler) should be handled as home task appeared");
return false;
}
- return super.handleTaskAppeared(appearedTaskTarget);
+ return super.handleTaskAppeared(appearedTaskTarget, failureReason);
}
@Override
diff --git a/quickstep/src/com/android/quickstep/LauncherActivityInterface.java b/quickstep/src/com/android/quickstep/LauncherActivityInterface.java
index c428827..1048ea1 100644
--- a/quickstep/src/com/android/quickstep/LauncherActivityInterface.java
+++ b/quickstep/src/com/android/quickstep/LauncherActivityInterface.java
@@ -35,7 +35,6 @@
import androidx.annotation.UiThread;
import com.android.launcher3.DeviceProfile;
-import com.android.launcher3.Flags;
import com.android.launcher3.Launcher;
import com.android.launcher3.LauncherAnimUtils;
import com.android.launcher3.LauncherInitListener;
@@ -213,10 +212,7 @@
if (launcher.isStarted() && (isInLiveTileMode() || launcher.hasBeenResumed())) {
return launcher;
}
- if (Flags.useActivityOverlay()
- && SystemUiProxy.INSTANCE.get(launcher).getHomeVisibilityState().isHomeVisible()) {
- return launcher;
- }
+
return null;
}
diff --git a/quickstep/src/com/android/quickstep/LauncherSwipeHandlerV2.java b/quickstep/src/com/android/quickstep/LauncherSwipeHandlerV2.java
index 3c66590..e17cdcd 100644
--- a/quickstep/src/com/android/quickstep/LauncherSwipeHandlerV2.java
+++ b/quickstep/src/com/android/quickstep/LauncherSwipeHandlerV2.java
@@ -144,8 +144,6 @@
return new FloatingViewHomeAnimationFactory(floatingIconView) {
@Nullable
private RectF mTargetRect;
- @Nullable
- private RectFSpringAnim mSiblingAnimation;
@Nullable
@Override
@@ -173,14 +171,6 @@
}
@Override
- protected void playScalingRevealAnimation() {
- if (mContainer != null) {
- new ScalingWorkspaceRevealAnim(mContainer, mSiblingAnimation,
- getWindowTargetRect()).start();
- }
- }
-
- @Override
public void setAnimation(RectFSpringAnim anim) {
super.setAnimation(anim);
mSiblingAnimation = anim;
@@ -245,6 +235,8 @@
isTargetTranslucent, fallbackBackgroundColor);
return new FloatingViewHomeAnimationFactory(floatingWidgetView) {
+ @Nullable
+ private RectF mTargetRect;
@Override
@Nullable
@@ -254,8 +246,14 @@
@Override
public RectF getWindowTargetRect() {
- super.getWindowTargetRect();
- return backgroundLocation;
+ if (enableScalingRevealHomeAnimation()) {
+ if (mTargetRect == null) {
+ mTargetRect = new RectF(backgroundLocation);
+ }
+ return mTargetRect;
+ } else {
+ return backgroundLocation;
+ }
}
@Override
@@ -266,10 +264,11 @@
@Override
public void setAnimation(RectFSpringAnim anim) {
super.setAnimation(anim);
-
- anim.addAnimatorListener(floatingWidgetView);
- floatingWidgetView.setOnTargetChangeListener(anim::onTargetPositionChanged);
- floatingWidgetView.setFastFinishRunnable(anim::end);
+ mSiblingAnimation = anim;
+ mSiblingAnimation.addAnimatorListener(floatingWidgetView);
+ floatingWidgetView.setOnTargetChangeListener(
+ mSiblingAnimation::onTargetPositionChanged);
+ floatingWidgetView.setFastFinishRunnable(mSiblingAnimation::end);
}
@Override
@@ -330,14 +329,23 @@
}
private class FloatingViewHomeAnimationFactory extends LauncherHomeAnimationFactory {
-
private final FloatingView mFloatingView;
+ @Nullable
+ protected RectFSpringAnim mSiblingAnimation;
FloatingViewHomeAnimationFactory(FloatingView floatingView) {
mFloatingView = floatingView;
}
@Override
+ protected void playScalingRevealAnimation() {
+ if (mContainer != null) {
+ new ScalingWorkspaceRevealAnim(mContainer, mSiblingAnimation,
+ getWindowTargetRect()).start();
+ }
+ }
+
+ @Override
public void onCancel() {
mFloatingView.fastFinish();
}
diff --git a/quickstep/src/com/android/quickstep/OverviewCommandHelper.java b/quickstep/src/com/android/quickstep/OverviewCommandHelper.java
index 7da92bc..8f533a3 100644
--- a/quickstep/src/com/android/quickstep/OverviewCommandHelper.java
+++ b/quickstep/src/com/android/quickstep/OverviewCommandHelper.java
@@ -265,6 +265,10 @@
case TYPE_HOME:
ActiveGestureLog.INSTANCE.addLog(
"OverviewCommandHelper.executeCommand(TYPE_HOME)");
+ // Although IActivityTaskManager$Stub$Proxy.startActivity is a slow binder call,
+ // we should still call it on main thread because launcher is waiting for
+ // ActivityTaskManager to resume it. Also calling startActivity() on bg thread
+ // could potentially delay resuming launcher. See b/348668521 for more details.
mService.startActivity(mOverviewComponentObserver.getHomeIntent());
return true;
case TYPE_SHOW:
diff --git a/quickstep/src/com/android/quickstep/OverviewComponentObserver.java b/quickstep/src/com/android/quickstep/OverviewComponentObserver.java
index a71e314..9c64576 100644
--- a/quickstep/src/com/android/quickstep/OverviewComponentObserver.java
+++ b/quickstep/src/com/android/quickstep/OverviewComponentObserver.java
@@ -36,6 +36,7 @@
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
+import androidx.annotation.UiThread;
import com.android.launcher3.R;
import com.android.launcher3.util.SimpleBroadcastReceiver;
@@ -101,7 +102,7 @@
mConfigChangesMap.append(fallbackComponent.hashCode(), fallbackInfo.configChanges);
} catch (PackageManager.NameNotFoundException ignored) { /* Impossible */ }
- mUserPreferenceChangeReceiver.register(mContext, ACTION_PREFERRED_ACTIVITY_CHANGED);
+ mUserPreferenceChangeReceiver.registerAsync(mContext, ACTION_PREFERRED_ACTIVITY_CHANGED);
updateOverviewTargets();
}
@@ -114,6 +115,8 @@
mOverviewChangeListener = overviewChangeListener;
}
+ /** Called on {@link TouchInteractionService#onSystemUiFlagsChanged} */
+ @UiThread
public void onSystemUiStateChanged() {
if (mDeviceState.isHomeDisabled() != mIsHomeDisabled) {
updateOverviewTargets();
@@ -128,6 +131,7 @@
* Update overview intent and {@link BaseActivityInterface} based off the current launcher home
* component.
*/
+ @UiThread
private void updateOverviewTargets() {
ComponentName defaultHome = PackageManagerWrapper.getInstance()
.getHomeActivities(new ArrayList<>());
@@ -187,8 +191,9 @@
unregisterOtherHomeAppUpdateReceiver();
mUpdateRegisteredPackage = defaultHome.getPackageName();
- mOtherHomeAppUpdateReceiver.registerPkgActions(mContext, mUpdateRegisteredPackage,
- ACTION_PACKAGE_ADDED, ACTION_PACKAGE_CHANGED, ACTION_PACKAGE_REMOVED);
+ mOtherHomeAppUpdateReceiver.registerPkgActionsAsync(
+ mContext, mUpdateRegisteredPackage, ACTION_PACKAGE_ADDED,
+ ACTION_PACKAGE_CHANGED, ACTION_PACKAGE_REMOVED);
}
}
mOverviewChangeListener.accept(mIsHomeAndOverviewSame);
@@ -198,13 +203,13 @@
* Clean up any registered receivers.
*/
public void onDestroy() {
- mContext.unregisterReceiver(mUserPreferenceChangeReceiver);
+ mUserPreferenceChangeReceiver.unregisterReceiverSafelyAsync(mContext);
unregisterOtherHomeAppUpdateReceiver();
}
private void unregisterOtherHomeAppUpdateReceiver() {
if (mUpdateRegisteredPackage != null) {
- mContext.unregisterReceiver(mOtherHomeAppUpdateReceiver);
+ mOtherHomeAppUpdateReceiver.unregisterReceiverSafelyAsync(mContext);
mUpdateRegisteredPackage = null;
}
}
diff --git a/quickstep/src/com/android/quickstep/RecentTasksList.java b/quickstep/src/com/android/quickstep/RecentTasksList.java
index b08a46f..66091d4 100644
--- a/quickstep/src/com/android/quickstep/RecentTasksList.java
+++ b/quickstep/src/com/android/quickstep/RecentTasksList.java
@@ -70,7 +70,8 @@
private TaskLoadResult mResultsBg = INVALID_RESULT;
private TaskLoadResult mResultsUi = INVALID_RESULT;
- private RecentsModel.RunningTasksListener mRunningTasksListener;
+ private @Nullable RecentsModel.RunningTasksListener mRunningTasksListener;
+ private @Nullable RecentsModel.RecentTasksChangedListener mRecentTasksChangedListener;
// Tasks are stored in order of least recently launched to most recently launched.
private ArrayList<ActivityManager.RunningTaskInfo> mRunningTasks;
@@ -199,6 +200,9 @@
public void onRecentTasksChanged() {
invalidateLoadedTasks();
+ if (mRecentTasksChangedListener != null) {
+ mRecentTasksChangedListener.onRecentTasksChanged();
+ }
}
private synchronized void invalidateLoadedTasks() {
@@ -221,6 +225,21 @@
mRunningTasksListener = null;
}
+ /**
+ * Registers a listener for running tasks
+ */
+ public void registerRecentTasksChangedListener(
+ RecentsModel.RecentTasksChangedListener listener) {
+ mRecentTasksChangedListener = listener;
+ }
+
+ /**
+ * Removes the previously registered running tasks listener
+ */
+ public void unregisterRecentTasksChangedListener() {
+ mRecentTasksChangedListener = null;
+ }
+
private void initRunningTasks(ArrayList<ActivityManager.RunningTaskInfo> runningTasks) {
// Tasks are retrieved in order of most recently launched/used to least recently launched.
mRunningTasks = new ArrayList<>(runningTasks);
@@ -357,6 +376,7 @@
task.setLastSnapshotData(taskInfo);
task.positionInParent = taskInfo.positionInParent;
task.appBounds = taskInfo.configuration.windowConfiguration.getAppBounds();
+ task.isVisible = taskInfo.isVisible;
tasks.add(task);
}
return new DesktopTask(tasks);
diff --git a/quickstep/src/com/android/quickstep/RecentsModel.java b/quickstep/src/com/android/quickstep/RecentsModel.java
index 6eefe4a..b796951 100644
--- a/quickstep/src/com/android/quickstep/RecentsModel.java
+++ b/quickstep/src/com/android/quickstep/RecentsModel.java
@@ -42,6 +42,7 @@
import com.android.launcher3.util.MainThreadInitializedObject;
import com.android.launcher3.util.SafeCloseable;
import com.android.quickstep.recents.data.RecentTasksDataSource;
+import com.android.quickstep.util.DesktopTask;
import com.android.quickstep.util.GroupTask;
import com.android.quickstep.util.TaskVisualsChangeListener;
import com.android.systemui.shared.recents.model.Task;
@@ -301,6 +302,8 @@
/**
* Registers a listener for running tasks
+ * TODO(b/343292503): Should we remove RunningTasksListener entirely if it's not needed?
+ * (Note that Desktop mode gets the running tasks by checking {@link DesktopTask#tasks}
*/
public void registerRunningTasksListener(RunningTasksListener listener) {
mTaskList.registerRunningTasksListener(listener);
@@ -314,6 +317,20 @@
}
/**
+ * Registers a listener for recent tasks
+ */
+ public void registerRecentTasksChangedListener(RecentTasksChangedListener listener) {
+ mTaskList.registerRecentTasksChangedListener(listener);
+ }
+
+ /**
+ * Removes the previously registered running tasks listener
+ */
+ public void unregisterRecentTasksChangedListener() {
+ mTaskList.unregisterRecentTasksChangedListener();
+ }
+
+ /**
* Gets the set of running tasks.
*/
public ArrayList<ActivityManager.RunningTaskInfo> getRunningTasks() {
@@ -379,4 +396,14 @@
*/
void onRunningTasksChanged();
}
+
+ /**
+ * Listener for receiving recent tasks changes
+ */
+ public interface RecentTasksChangedListener {
+ /**
+ * Called when there's a change to recent tasks
+ */
+ void onRecentTasksChanged();
+ }
}
diff --git a/quickstep/src/com/android/quickstep/TaskAnimationManager.java b/quickstep/src/com/android/quickstep/TaskAnimationManager.java
index 28fa81a..08bb6cd 100644
--- a/quickstep/src/com/android/quickstep/TaskAnimationManager.java
+++ b/quickstep/src/com/android/quickstep/TaskAnimationManager.java
@@ -100,6 +100,11 @@
TaskAnimationManager(Context ctx) {
mCtx = ctx;
}
+
+ SystemUiProxy getSystemUiProxy() {
+ return SystemUiProxy.INSTANCE.get(mCtx);
+ }
+
/**
* Preloads the recents animation.
*/
@@ -153,7 +158,7 @@
final BaseContainerInterface containerInterface = gestureState.getContainerInterface();
mLastGestureState = gestureState;
RecentsAnimationCallbacks newCallbacks = new RecentsAnimationCallbacks(
- SystemUiProxy.INSTANCE.get(mCtx), containerInterface.allowMinimizeSplitScreen());
+ getSystemUiProxy(), containerInterface.allowMinimizeSplitScreen());
mCallbacks = newCallbacks;
mCallbacks.addListener(new RecentsAnimationCallbacks.RecentsAnimationListener() {
@Override
@@ -260,7 +265,7 @@
}
RemoteAnimationTarget[] nonAppTargets = ENABLE_SHELL_TRANSITIONS
- ? null : SystemUiProxy.INSTANCE.get(mCtx).onStartingSplitLegacy(
+ ? null : getSystemUiProxy().onStartingSplitLegacy(
appearedTaskTargets);
if (nonAppTargets == null) {
nonAppTargets = new RemoteAnimationTarget[0];
@@ -327,12 +332,13 @@
if (ENABLE_SHELL_TRANSITIONS) {
final ActivityOptions options = ActivityOptions.makeBasic();
+ options.setPendingIntentBackgroundActivityLaunchAllowedByPermission(true);
// Use regular (non-transient) launch for all apps page to control IME.
if (!containerInterface.allowAllAppsFromOverview()) {
options.setTransientLaunch();
}
options.setSourceInfo(ActivityOptions.SourceInfo.TYPE_RECENTS_ANIMATION, eventTime);
- mRecentsAnimationStartPending = SystemUiProxy.INSTANCE.get(mCtx)
+ mRecentsAnimationStartPending = getSystemUiProxy()
.startRecentsActivity(intent, options, mCallbacks);
if (enableHandleDelayedGestureCallbacks()) {
ActiveGestureLog.INSTANCE.addLog(new ActiveGestureLog.CompoundString(
diff --git a/quickstep/src/com/android/quickstep/TaskShortcutFactory.java b/quickstep/src/com/android/quickstep/TaskShortcutFactory.java
index fd141c3..4691ea9 100644
--- a/quickstep/src/com/android/quickstep/TaskShortcutFactory.java
+++ b/quickstep/src/com/android/quickstep/TaskShortcutFactory.java
@@ -228,6 +228,7 @@
// Take the thumbnail of the task without a scrim and apply it back after
float alpha = mThumbnailView.getDimAlpha();
+ // TODO(b/348643341) add ability to get override the scrim for this Bitmap retrieval
mThumbnailView.setDimAlpha(0);
Bitmap thumbnail = RecentsTransition.drawViewIntoHardwareBitmap(
taskBounds.width(), taskBounds.height(), mThumbnailView, 1f,
@@ -492,18 +493,8 @@
TaskContainer taskContainer) {
boolean isTablet = container.getDeviceProfile().isTablet;
boolean isGridOnlyOverview = isTablet && Flags.enableGridOnlyOverview();
- // Extra conditions if it's not grid-only overview
if (!isGridOnlyOverview) {
- RecentsOrientedState orientedState = taskContainer.getTaskView().getOrientedState();
- boolean isFakeLandscape = !orientedState.isRecentsActivityRotationAllowed()
- && orientedState.getTouchRotation() != ROTATION_0;
- if (!isFakeLandscape) {
- return null;
- }
- // Disallow "Select" when swiping up from landscape due to rotated thumbnail.
- if (orientedState.getDisplayRotation() != ROTATION_0) {
- return null;
- }
+ return null;
}
SystemShortcut modalStateSystemShortcut =
diff --git a/quickstep/src/com/android/quickstep/orientation/PortraitPagedViewHandler.java b/quickstep/src/com/android/quickstep/orientation/PortraitPagedViewHandler.java
index 758a737..eeacee1 100644
--- a/quickstep/src/com/android/quickstep/orientation/PortraitPagedViewHandler.java
+++ b/quickstep/src/com/android/quickstep/orientation/PortraitPagedViewHandler.java
@@ -259,7 +259,8 @@
return new Pair<>(translationX, translationY);
}
- bannerParams.gravity = BOTTOM | ((deviceProfile.isLandscape) ? START : CENTER_HORIZONTAL);
+ bannerParams.gravity =
+ BOTTOM | (deviceProfile.isLeftRightSplit ? START : CENTER_HORIZONTAL);
// Set correct width
if (desiredTaskId == splitBounds.leftTopTaskId) {
diff --git a/quickstep/src/com/android/quickstep/task/thumbnail/TaskThumbnailView.kt b/quickstep/src/com/android/quickstep/task/thumbnail/TaskThumbnailView.kt
index 2836c89..dbe2b19 100644
--- a/quickstep/src/com/android/quickstep/task/thumbnail/TaskThumbnailView.kt
+++ b/quickstep/src/com/android/quickstep/task/thumbnail/TaskThumbnailView.kt
@@ -53,25 +53,31 @@
TaskThumbnailViewModel(
recentsView.mRecentsViewData,
(parent as TaskView).taskViewData,
+ (parent as TaskView).getTaskContainerForTaskThumbnailView(this)!!.taskContainerData,
recentsView.mTasksRepository,
)
}
private var uiState: TaskThumbnailUiState = Uninitialized
private var inheritedScale: Float = 1f
+ private var dimProgress: Float = 0f
private val backgroundPaint = Paint(Paint.ANTI_ALIAS_FLAG)
+ private val scrimPaint = Paint().apply { color = Color.BLACK }
private val _measuredBounds = Rect()
private val measuredBounds: Rect
get() {
_measuredBounds.set(0, 0, measuredWidth, measuredHeight)
return _measuredBounds
}
+
private var cornerRadius: Float = TaskCornerRadius.get(context)
private var fullscreenCornerRadius: Float = QuickStepContract.getWindowCornerRadius(context)
constructor(context: Context?) : super(context)
+
constructor(context: Context?, attrs: AttributeSet?) : super(context, attrs)
+
constructor(
context: Context?,
attrs: AttributeSet?,
@@ -87,6 +93,13 @@
invalidate()
}
}
+ MainScope().launch {
+ viewModel.dimProgress.collect { dimProgress ->
+ // TODO(b/348195366) Add fade in/out for scrim
+ this@TaskThumbnailView.dimProgress = dimProgress
+ invalidate()
+ }
+ }
MainScope().launch { viewModel.recentsFullscreenProgress.collect { invalidateOutline() } }
MainScope().launch {
viewModel.inheritedScale.collect { viewModelInheritedScale ->
@@ -111,6 +124,10 @@
is Snapshot -> drawSnapshotState(canvas, uiStateVal)
is BackgroundOnly -> drawBackgroundOnly(canvas, uiStateVal.backgroundColor)
}
+
+ if (dimProgress > 0) {
+ drawScrim(canvas)
+ }
}
private fun drawBackgroundOnly(canvas: Canvas, @ColorInt backgroundColor: Int) {
@@ -135,6 +152,11 @@
canvas.drawBitmap(snapshot.bitmap, snapshot.drawnRect, measuredBounds, null)
}
+ private fun drawScrim(canvas: Canvas) {
+ scrimPaint.alpha = (dimProgress * MAX_SCRIM_ALPHA).toInt()
+ canvas.drawRect(measuredBounds, scrimPaint)
+ }
+
private fun getCurrentCornerRadius() =
Utilities.mapRange(
viewModel.recentsFullscreenProgress.value,
@@ -145,5 +167,6 @@
companion object {
private val CLEAR_PAINT =
Paint().apply { xfermode = PorterDuffXfermode(PorterDuff.Mode.CLEAR) }
+ private const val MAX_SCRIM_ALPHA = (0.4f * 255).toInt()
}
}
diff --git a/quickstep/src/com/android/quickstep/task/thumbnail/TaskThumbnailViewModel.kt b/quickstep/src/com/android/quickstep/task/thumbnail/TaskThumbnailViewModel.kt
index 4511ea7..fe21174 100644
--- a/quickstep/src/com/android/quickstep/task/thumbnail/TaskThumbnailViewModel.kt
+++ b/quickstep/src/com/android/quickstep/task/thumbnail/TaskThumbnailViewModel.kt
@@ -25,6 +25,7 @@
import com.android.quickstep.task.thumbnail.TaskThumbnailUiState.LiveTile
import com.android.quickstep.task.thumbnail.TaskThumbnailUiState.Snapshot
import com.android.quickstep.task.thumbnail.TaskThumbnailUiState.Uninitialized
+import com.android.quickstep.task.viewmodel.TaskContainerData
import com.android.quickstep.task.viewmodel.TaskViewData
import com.android.systemui.shared.recents.model.Task
import kotlinx.coroutines.ExperimentalCoroutinesApi
@@ -40,6 +41,7 @@
class TaskThumbnailViewModel(
recentsViewData: RecentsViewData,
taskViewData: TaskViewData,
+ taskContainerData: TaskContainerData,
private val tasksRepository: RecentTasksRepository,
) {
private val task = MutableStateFlow<Flow<Task?>>(flowOf(null))
@@ -50,6 +52,7 @@
combine(recentsViewData.scale, taskViewData.scale) { recentsScale, taskScale ->
recentsScale * taskScale
}
+ val dimProgress: Flow<Float> = taskContainerData.taskMenuOpenProgress
val uiState: Flow<TaskThumbnailUiState> =
task
.flatMapLatest { taskFlow ->
diff --git a/quickstep/src/com/android/quickstep/task/viewmodel/TaskContainerData.kt b/quickstep/src/com/android/quickstep/task/viewmodel/TaskContainerData.kt
new file mode 100644
index 0000000..769424c
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/task/viewmodel/TaskContainerData.kt
@@ -0,0 +1,23 @@
+/*
+ * Copyright (C) 2024 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.quickstep.task.viewmodel
+
+import kotlinx.coroutines.flow.MutableStateFlow
+
+class TaskContainerData {
+ val taskMenuOpenProgress = MutableStateFlow(0f)
+}
diff --git a/quickstep/src/com/android/quickstep/util/ActiveGestureErrorDetector.java b/quickstep/src/com/android/quickstep/util/ActiveGestureErrorDetector.java
index cfa6b98..3140fff 100644
--- a/quickstep/src/com/android/quickstep/util/ActiveGestureErrorDetector.java
+++ b/quickstep/src/com/android/quickstep/util/ActiveGestureErrorDetector.java
@@ -40,6 +40,7 @@
SCROLLER_ANIMATION_ABORTED, TASK_APPEARED, EXPECTING_TASK_APPEARED,
FLAG_USING_OTHER_ACTIVITY_INPUT_CONSUMER, LAUNCHER_DESTROYED, RECENT_TASKS_MISSING,
INVALID_VELOCITY_ON_SWIPE_UP, RECENTS_ANIMATION_START_PENDING,
+ QUICK_SWITCH_FROM_HOME_FALLBACK, QUICK_SWITCH_FROM_HOME_FAILED,
/**
* These GestureEvents are specifically associated to state flags that get set in
@@ -282,6 +283,22 @@
+ " animation is still pending.",
writer);
break;
+ case QUICK_SWITCH_FROM_HOME_FALLBACK:
+ errorDetected |= printErrorIfTrue(
+ true,
+ prefix,
+ /* errorMessage= */ "Quick switch from home fallback case: the "
+ + "TaskView at the current page index was missing.",
+ writer);
+ break;
+ case QUICK_SWITCH_FROM_HOME_FAILED:
+ errorDetected |= printErrorIfTrue(
+ true,
+ prefix,
+ /* errorMessage= */ "Quick switch from home failed: the TaskViews at "
+ + "the current page index and index 0 were missing.",
+ writer);
+ break;
case EXPECTING_TASK_APPEARED:
case MOTION_DOWN:
case SET_END_TARGET:
diff --git a/quickstep/src/com/android/quickstep/util/AsyncClockEventDelegate.java b/quickstep/src/com/android/quickstep/util/AsyncClockEventDelegate.java
index cda87c0..c26fc0c5 100644
--- a/quickstep/src/com/android/quickstep/util/AsyncClockEventDelegate.java
+++ b/quickstep/src/com/android/quickstep/util/AsyncClockEventDelegate.java
@@ -18,8 +18,6 @@
import static android.content.Intent.ACTION_TIMEZONE_CHANGED;
import static android.content.Intent.ACTION_TIME_CHANGED;
-import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR;
-
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
@@ -64,9 +62,7 @@
private AsyncClockEventDelegate(Context context) {
super(context);
mContext = context;
-
- UI_HELPER_EXECUTOR.execute(() ->
- mReceiver.register(mContext, ACTION_TIME_CHANGED, ACTION_TIMEZONE_CHANGED));
+ mReceiver.registerAsync(mContext, ACTION_TIME_CHANGED, ACTION_TIMEZONE_CHANGED);
}
@Override
@@ -127,6 +123,6 @@
public void close() {
mDestroyed = true;
SettingsCache.INSTANCE.get(mContext).unregister(mFormatUri, this);
- UI_HELPER_EXECUTOR.execute(() -> mReceiver.unregisterReceiverSafely(mContext));
+ mReceiver.unregisterReceiverSafelyAsync(mContext);
}
}
diff --git a/quickstep/src/com/android/quickstep/util/SwipePipToHomeAnimator.java b/quickstep/src/com/android/quickstep/util/SwipePipToHomeAnimator.java
index e44f148..88c3a08 100644
--- a/quickstep/src/com/android/quickstep/util/SwipePipToHomeAnimator.java
+++ b/quickstep/src/com/android/quickstep/util/SwipePipToHomeAnimator.java
@@ -164,22 +164,7 @@
}
if (sourceRectHint.isEmpty()) {
- // Crop a Rect matches the aspect ratio and pivots at the center point.
- // To make the animation path simplified.
- if ((appBounds.width() / (float) appBounds.height()) > aspectRatio) {
- // use the full height.
- mSourceRectHint.set(0, 0,
- (int) (appBounds.height() * aspectRatio), appBounds.height());
- mSourceRectHint.offset(
- (appBounds.width() - mSourceRectHint.width()) / 2, 0);
- } else {
- // use the full width.
- mSourceRectHint.set(0, 0,
- appBounds.width(), (int) (appBounds.width() / aspectRatio));
- mSourceRectHint.offset(
- 0, (appBounds.height() - mSourceRectHint.height()) / 2);
- }
-
+ mSourceRectHint.set(getEnterPipWithOverlaySrcRectHint(appBounds, aspectRatio));
// Create a new overlay layer. We do not call detach on this instance, it's propagated
// to other classes like PipTaskOrganizer / RecentsAnimationController to complete
// the cleanup.
@@ -225,6 +210,26 @@
addOnUpdateListener(this::onAnimationUpdate);
}
+ /**
+ * Crop a Rect matches the aspect ratio and pivots at the center point.
+ */
+ private Rect getEnterPipWithOverlaySrcRectHint(Rect appBounds, float aspectRatio) {
+ final float appBoundsAspectRatio = appBounds.width() / (float) appBounds.height();
+ final int width, height;
+ int left = appBounds.left;
+ int top = appBounds.top;
+ if (appBoundsAspectRatio < aspectRatio) {
+ width = appBounds.width();
+ height = (int) (width / aspectRatio);
+ top = appBounds.top + (appBounds.height() - height) / 2;
+ } else {
+ height = appBounds.height();
+ width = (int) (height * aspectRatio);
+ left = appBounds.left + (appBounds.width() - width) / 2;
+ }
+ return new Rect(left, top, left + width, top + height);
+ }
+
private void onAnimationUpdate(RectF currentRect, float progress) {
if (mHasAnimationEnded) return;
final SurfaceControl.Transaction tx =
@@ -437,13 +442,21 @@
return this;
}
+ public Builder setDisplayCutoutInsets(@NonNull Rect displayCutoutInsets) {
+ mDisplayCutoutInsets = new Rect(displayCutoutInsets);
+ return this;
+ }
+
public SwipePipToHomeAnimator build() {
if (mDestinationBoundsTransformed.isEmpty()) {
mDestinationBoundsTransformed.set(mDestinationBounds);
}
// adjust the mSourceRectHint / mAppBounds by display cutout if applicable.
if (mSourceRectHint != null && mDisplayCutoutInsets != null) {
- if (mFromRotation == Surface.ROTATION_90) {
+ if (mFromRotation == Surface.ROTATION_0 && mDisplayCutoutInsets.top >= 0) {
+ // TODO: this is to special case the issues on Pixel Foldable device(s).
+ mSourceRectHint.offset(mDisplayCutoutInsets.left, mDisplayCutoutInsets.top);
+ } else if (mFromRotation == Surface.ROTATION_90) {
mSourceRectHint.offset(mDisplayCutoutInsets.left, mDisplayCutoutInsets.top);
} else if (mFromRotation == Surface.ROTATION_270) {
mAppBounds.inset(mDisplayCutoutInsets);
@@ -457,15 +470,6 @@
}
}
- private static class RotatedPosition {
- private final float degree;
- private final float positionX;
- private final float positionY;
-
- private RotatedPosition(float degree, float positionX, float positionY) {
- this.degree = degree;
- this.positionX = positionX;
- this.positionY = positionY;
- }
+ private record RotatedPosition(float degree, float positionX, float positionY) {
}
}
diff --git a/quickstep/src/com/android/quickstep/views/DesktopTaskView.kt b/quickstep/src/com/android/quickstep/views/DesktopTaskView.kt
index 936f6a1..4c78e21 100644
--- a/quickstep/src/com/android/quickstep/views/DesktopTaskView.kt
+++ b/quickstep/src/com/android/quickstep/views/DesktopTaskView.kt
@@ -193,23 +193,23 @@
}
val taskContainer =
TaskContainer(
- task,
- // TODO(b/338360089): Support new TTV for DesktopTaskView
- thumbnailView = null,
- thumbnailViewDeprecated,
- iconView,
- TransformingTouchDelegate(iconView.asView()),
- SplitConfigurationOptions.STAGE_POSITION_UNDEFINED,
- digitalWellBeingToast = null,
- showWindowsView = null,
- taskOverlayFactory
- )
- .apply { thumbnailViewDeprecated.bind(task, overlay) }
+ task,
+ // TODO(b/338360089): Support new TTV for DesktopTaskView
+ thumbnailView = null,
+ thumbnailViewDeprecated,
+ iconView,
+ TransformingTouchDelegate(iconView.asView()),
+ SplitConfigurationOptions.STAGE_POSITION_UNDEFINED,
+ digitalWellBeingToast = null,
+ showWindowsView = null,
+ taskOverlayFactory
+ )
if (index >= taskContainers.size) {
taskContainers.add(taskContainer)
} else {
taskContainers[index] = taskContainer
}
+ taskContainer.bind()
}
repeat(taskContainers.size - tasks.size) {
with(taskContainers.removeLast()) {
diff --git a/quickstep/src/com/android/quickstep/views/GroupedTaskView.kt b/quickstep/src/com/android/quickstep/views/GroupedTaskView.kt
index d6a3376..6296b0e 100644
--- a/quickstep/src/com/android/quickstep/views/GroupedTaskView.kt
+++ b/quickstep/src/com/android/quickstep/views/GroupedTaskView.kt
@@ -145,6 +145,8 @@
taskOverlayFactory
)
)
+ taskContainers.forEach { it.bind() }
+
this.splitBoundsConfig =
splitBoundsConfig?.also {
taskContainers[0]
diff --git a/quickstep/src/com/android/quickstep/views/RecentsView.java b/quickstep/src/com/android/quickstep/views/RecentsView.java
index b332652..d806e3d 100644
--- a/quickstep/src/com/android/quickstep/views/RecentsView.java
+++ b/quickstep/src/com/android/quickstep/views/RecentsView.java
@@ -31,6 +31,7 @@
import static com.android.app.animation.Interpolators.LINEAR;
import static com.android.app.animation.Interpolators.OVERSHOOT_0_75;
import static com.android.app.animation.Interpolators.clampToProgress;
+import static com.android.launcher3.AbstractFloatingView.TYPE_REBIND_SAFE;
import static com.android.launcher3.AbstractFloatingView.TYPE_TASK_MENU;
import static com.android.launcher3.AbstractFloatingView.getTopOpenViewWithType;
import static com.android.launcher3.BaseActivity.STATE_HANDLER_INVISIBILITY_FLAGS;
@@ -131,6 +132,7 @@
import androidx.core.graphics.ColorUtils;
import com.android.internal.jank.Cuj;
+import com.android.launcher3.AbstractFloatingView;
import com.android.launcher3.BaseActivity.MultiWindowModeChangedListener;
import com.android.launcher3.DeviceProfile;
import com.android.launcher3.Flags;
@@ -314,7 +316,6 @@
/**
* Can be used to tint the color of the RecentsView to simulate a scrim that can views
* excluded from. Really should be a proper scrim.
- * TODO(b/187528071): Remove this and replace with a real scrim.
*/
private static final FloatProperty<RecentsView> COLOR_TINT =
new FloatProperty<RecentsView>("colorTint") {
@@ -555,7 +556,6 @@
@Nullable
protected GestureState.GestureEndTarget mCurrentGestureEndTarget;
- // TODO(b/187528071): Remove these and replace with a real scrim.
private float mColorTint;
private final int mTintingColor;
@Nullable
@@ -2689,6 +2689,7 @@
}
private void animateRotation(int newRotation) {
+ AbstractFloatingView.closeAllOpenViewsExcept(mContainer, false, TYPE_REBIND_SAFE);
AnimatorSet pa = setRecentsChangedOrientation(true);
pa.addListener(AnimatorListeners.forSuccessCallback(() -> {
setLayoutRotation(newRotation, mOrientationState.getDisplayRotation());
@@ -6029,6 +6030,7 @@
* tasks to be dimmed while other elements in the recents view are left alone.
*/
public void showForegroundScrim(boolean show) {
+ // TODO(b/335606129) Add scrim response into new TTV - this is called from overlay
if (!show && mColorTint == 0) {
if (mTintingAnimator != null) {
mTintingAnimator.cancel();
@@ -6044,7 +6046,6 @@
}
/** Tint the RecentsView and TaskViews in to simulate a scrim. */
- // TODO(b/187528071): Replace this tinting with a scrim on top of RecentsView
private void setColorTint(float tintAmount) {
mColorTint = tintAmount;
diff --git a/quickstep/src/com/android/quickstep/views/TaskMenuView.java b/quickstep/src/com/android/quickstep/views/TaskMenuView.java
index eda58c5..4f446b2 100644
--- a/quickstep/src/com/android/quickstep/views/TaskMenuView.java
+++ b/quickstep/src/com/android/quickstep/views/TaskMenuView.java
@@ -18,6 +18,7 @@
import static com.android.app.animation.Interpolators.EMPHASIZED;
import static com.android.launcher3.Flags.enableOverviewIconMenu;
+import static com.android.launcher3.Flags.enableRefactorTaskThumbnail;
import static com.android.launcher3.util.MultiPropertyFactory.MULTI_PROPERTY_VALUE;
import static com.android.launcher3.util.SplitConfigurationOptions.STAGE_POSITION_BOTTOM_OR_RIGHT;
import static com.android.quickstep.views.TaskThumbnailViewDeprecated.DIM_ALPHA;
@@ -367,6 +368,14 @@
mTaskContainer.getThumbnailViewDeprecated(), DIM_ALPHA,
closing ? 0 : TaskView.MAX_PAGE_SCRIM_ALPHA),
ObjectAnimator.ofFloat(this, ALPHA, closing ? 0 : 1));
+ if (enableRefactorTaskThumbnail()) {
+ mRevealAnimator.addUpdateListener(animation -> {
+ float animatedFraction = animation.getAnimatedFraction();
+ float openProgress = closing ? (1 - animatedFraction) : animatedFraction;
+ mTaskContainer.getTaskContainerData()
+ .getTaskMenuOpenProgress().setValue(openProgress);
+ });
+ }
mOpenCloseAnimator.addListener(new AnimationSuccessListener() {
@Override
public void onAnimationStart(Animator animation) {
diff --git a/quickstep/src/com/android/quickstep/views/TaskView.kt b/quickstep/src/com/android/quickstep/views/TaskView.kt
index 9c1aaa6..7a3b00f 100644
--- a/quickstep/src/com/android/quickstep/views/TaskView.kt
+++ b/quickstep/src/com/android/quickstep/views/TaskView.kt
@@ -88,6 +88,7 @@
import com.android.quickstep.orientation.RecentsPagedOrientationHandler
import com.android.quickstep.task.thumbnail.TaskThumbnail
import com.android.quickstep.task.thumbnail.TaskThumbnailView
+import com.android.quickstep.task.viewmodel.TaskContainerData
import com.android.quickstep.task.viewmodel.TaskViewData
import com.android.quickstep.util.ActiveGestureErrorDetector
import com.android.quickstep.util.ActiveGestureLog
@@ -667,6 +668,7 @@
taskOverlayFactory
)
)
+ taskContainers.forEach { it.bind() }
setOrientationState(orientedState)
}
@@ -693,24 +695,16 @@
}
val iconView = getOrInflateIconView(iconViewId)
return TaskContainer(
- task,
- thumbnailView,
- thumbnailViewDeprecated,
- iconView,
- TransformingTouchDelegate(iconView.asView()),
- stagePosition,
- DigitalWellBeingToast(container, this),
- findViewById(showWindowViewId)!!,
- taskOverlayFactory
- )
- .apply {
- if (enableRefactorTaskThumbnail()) {
- thumbnailViewDeprecated.setTaskOverlay(overlay)
- bindThumbnailView()
- } else {
- thumbnailViewDeprecated.bind(task, overlay)
- }
- }
+ task,
+ thumbnailView,
+ thumbnailViewDeprecated,
+ iconView,
+ TransformingTouchDelegate(iconView.asView()),
+ stagePosition,
+ DigitalWellBeingToast(container, this),
+ findViewById(showWindowViewId)!!,
+ taskOverlayFactory
+ )
}
protected fun getOrInflateIconView(@IdRes iconViewId: Int): TaskViewIcon {
@@ -1379,7 +1373,6 @@
open fun setColorTint(amount: Float, tintColor: Int) {
taskContainers.forEach {
if (!enableRefactorTaskThumbnail()) {
- // TODO(b/334832108) Add scrim to new TTV
it.thumbnailViewDeprecated.dimAlpha = amount
}
it.iconView.setIconColorTint(tintColor, amount)
@@ -1522,6 +1515,9 @@
resetViewTransforms()
}
+ fun getTaskContainerForTaskThumbnailView(taskThumbnailView: TaskThumbnailView): TaskContainer? =
+ taskContainers.firstOrNull { it.thumbnailView == taskThumbnailView }
+
open fun resetViewTransforms() {
// fullscreenTranslation and accumulatedTranslation should not be reset, as
// resetViewTransforms is called during QuickSwitch scrolling.
@@ -1623,6 +1619,7 @@
taskOverlayFactory: TaskOverlayFactory
) {
val overlay: TaskOverlay<*> = taskOverlayFactory.createOverlay(this)
+ val taskContainerData = TaskContainerData()
val snapshotView: View
get() = thumbnailView ?: thumbnailViewDeprecated
@@ -1656,6 +1653,15 @@
thumbnailView?.let { taskView.removeView(it) }
}
+ fun bind() {
+ if (enableRefactorTaskThumbnail() && thumbnailView != null) {
+ thumbnailViewDeprecated.setTaskOverlay(overlay)
+ bindThumbnailView()
+ } else {
+ thumbnailViewDeprecated.bind(task, overlay)
+ }
+ }
+
// TODO(b/335649589): TaskView's VM will already have access to TaskThumbnailView's VM
// so there will be no need to access TaskThumbnailView's VM through the TaskThumbnailView
fun bindThumbnailView() {
diff --git a/quickstep/tests/src/com/android/launcher3/model/WidgetsPredictionsRequesterTest.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/model/WidgetsPredictionsRequesterTest.kt
similarity index 100%
rename from quickstep/tests/src/com/android/launcher3/model/WidgetsPredictionsRequesterTest.kt
rename to quickstep/tests/multivalentTests/src/com/android/launcher3/model/WidgetsPredictionsRequesterTest.kt
diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/animation/BubbleAnimatorTest.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/animation/BubbleAnimatorTest.kt
new file mode 100644
index 0000000..20bd617
--- /dev/null
+++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/animation/BubbleAnimatorTest.kt
@@ -0,0 +1,91 @@
+/*
+ * Copyright (C) 2024 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.animation
+
+import androidx.core.animation.AnimatorTestRule
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import androidx.test.platform.app.InstrumentationRegistry
+import com.google.common.truth.Truth.assertThat
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class BubbleAnimatorTest {
+
+ @get:Rule val animatorTestRule = AnimatorTestRule()
+
+ private lateinit var bubbleAnimator: BubbleAnimator
+
+ @Test
+ fun animateNewBubble_isRunning() {
+ bubbleAnimator =
+ BubbleAnimator(
+ iconSize = 40f,
+ expandedBarIconSpacing = 10f,
+ bubbleCount = 5,
+ onLeft = false
+ )
+ val listener = TestBubbleAnimatorListener()
+ InstrumentationRegistry.getInstrumentation().runOnMainSync {
+ bubbleAnimator.animateNewBubble(selectedBubbleIndex = 2, listener)
+ }
+
+ assertThat(bubbleAnimator.isRunning).isTrue()
+ InstrumentationRegistry.getInstrumentation().runOnMainSync {
+ animatorTestRule.advanceTimeBy(250)
+ }
+ assertThat(bubbleAnimator.isRunning).isFalse()
+ }
+
+ @Test
+ fun animateRemovedBubble_isRunning() {
+ bubbleAnimator =
+ BubbleAnimator(
+ iconSize = 40f,
+ expandedBarIconSpacing = 10f,
+ bubbleCount = 5,
+ onLeft = false
+ )
+ val listener = TestBubbleAnimatorListener()
+ InstrumentationRegistry.getInstrumentation().runOnMainSync {
+ bubbleAnimator.animateRemovedBubble(
+ bubbleIndex = 2,
+ selectedBubbleIndex = 3,
+ removingLastBubble = false,
+ listener
+ )
+ }
+
+ assertThat(bubbleAnimator.isRunning).isTrue()
+ InstrumentationRegistry.getInstrumentation().runOnMainSync {
+ animatorTestRule.advanceTimeBy(250)
+ }
+ assertThat(bubbleAnimator.isRunning).isFalse()
+ }
+
+ private class TestBubbleAnimatorListener : BubbleAnimator.Listener {
+
+ override fun onAnimationUpdate(animatedFraction: Float) {}
+
+ override fun onAnimationCancel() {}
+
+ override fun onAnimationEnd() {}
+ }
+}
diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/animation/BubbleBarViewAnimatorTest.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/animation/BubbleBarViewAnimatorTest.kt
index 2ae4e6b..e9c0dd6 100644
--- a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/animation/BubbleBarViewAnimatorTest.kt
+++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/animation/BubbleBarViewAnimatorTest.kt
@@ -388,7 +388,8 @@
fun animateBubbleBarForCollapsed() {
setUpBubbleBar()
setUpBubbleStashController()
- bubbleBarView.translationY = BAR_TRANSLATION_Y_FOR_HOTSEAT
+ whenever(bubbleStashController.bubbleBarTranslationY)
+ .thenReturn(BAR_TRANSLATION_Y_FOR_HOTSEAT)
val barAnimator = PhysicsAnimator.getInstance(bubbleBarView)
diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/task/thumbnail/TaskThumbnailViewModelTest.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/task/thumbnail/TaskThumbnailViewModelTest.kt
index 3b8754c..a394b65 100644
--- a/quickstep/tests/multivalentTests/src/com/android/quickstep/task/thumbnail/TaskThumbnailViewModelTest.kt
+++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/task/thumbnail/TaskThumbnailViewModelTest.kt
@@ -28,6 +28,7 @@
import com.android.quickstep.task.thumbnail.TaskThumbnailUiState.LiveTile
import com.android.quickstep.task.thumbnail.TaskThumbnailUiState.Snapshot
import com.android.quickstep.task.thumbnail.TaskThumbnailUiState.Uninitialized
+import com.android.quickstep.task.viewmodel.TaskContainerData
import com.android.quickstep.task.viewmodel.TaskViewData
import com.android.systemui.shared.recents.model.Task
import com.android.systemui.shared.recents.model.ThumbnailData
@@ -43,9 +44,10 @@
class TaskThumbnailViewModelTest {
private val recentsViewData = RecentsViewData()
private val taskViewData = TaskViewData()
+ private val taskContainerData = TaskContainerData()
private val tasksRepository = FakeTasksRepository()
private val systemUnderTest =
- TaskThumbnailViewModel(recentsViewData, taskViewData, tasksRepository)
+ TaskThumbnailViewModel(recentsViewData, taskViewData, taskContainerData, tasksRepository)
private val tasks = (0..5).map(::createTaskWithId)
diff --git a/quickstep/tests/src/com/android/quickstep/taskbar/customization/TaskbarSpecsEvaluatorTest.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/taskbar/customization/TaskbarSpecsEvaluatorTest.kt
similarity index 100%
rename from quickstep/tests/src/com/android/quickstep/taskbar/customization/TaskbarSpecsEvaluatorTest.kt
rename to quickstep/tests/multivalentTests/src/com/android/quickstep/taskbar/customization/TaskbarSpecsEvaluatorTest.kt
diff --git a/quickstep/tests/src/com/android/launcher3/taskbar/TaskbarRecentAppsControllerTest.kt b/quickstep/tests/src/com/android/launcher3/taskbar/TaskbarRecentAppsControllerTest.kt
index 104263a..486dc68 100644
--- a/quickstep/tests/src/com/android/launcher3/taskbar/TaskbarRecentAppsControllerTest.kt
+++ b/quickstep/tests/src/com/android/launcher3/taskbar/TaskbarRecentAppsControllerTest.kt
@@ -16,24 +16,35 @@
package com.android.launcher3.taskbar
-import android.app.ActivityManager.RunningTaskInfo
import android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM
import android.content.ComponentName
import android.content.Intent
import android.os.Process
import android.os.UserHandle
import android.testing.AndroidTestingRunner
+import com.android.launcher3.LauncherSettings.Favorites.CONTAINER_HOTSEAT
+import com.android.launcher3.LauncherSettings.Favorites.CONTAINER_HOTSEAT_PREDICTION
import com.android.launcher3.model.data.AppInfo
import com.android.launcher3.model.data.ItemInfo
import com.android.launcher3.statehandlers.DesktopVisibilityController
import com.android.quickstep.RecentsModel
+import com.android.quickstep.RecentsModel.RecentTasksChangedListener
+import com.android.quickstep.TaskIconCache
+import com.android.quickstep.util.DesktopTask
+import com.android.quickstep.util.GroupTask
+import com.android.systemui.shared.recents.model.Task
import com.google.common.truth.Truth.assertThat
+import java.util.function.Consumer
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
+import org.mockito.ArgumentCaptor
import org.mockito.Mock
import org.mockito.junit.MockitoJUnit
+import org.mockito.kotlin.any
+import org.mockito.kotlin.doAnswer
+import org.mockito.kotlin.verify
import org.mockito.kotlin.whenever
@RunWith(AndroidTestingRunner::class)
@@ -41,177 +52,471 @@
@get:Rule val mockitoRule = MockitoJUnit.rule()
+ @Mock private lateinit var mockIconCache: TaskIconCache
@Mock private lateinit var mockRecentsModel: RecentsModel
@Mock private lateinit var mockDesktopVisibilityController: DesktopVisibilityController
private var nextTaskId: Int = 500
+ private var taskListChangeId: Int = 1
private lateinit var recentAppsController: TaskbarRecentAppsController
+ private lateinit var recentTasksChangedListener: RecentTasksChangedListener
private lateinit var userHandle: UserHandle
@Before
fun setUp() {
super.setup()
userHandle = Process.myUserHandle()
+
+ whenever(mockRecentsModel.iconCache).thenReturn(mockIconCache)
recentAppsController =
TaskbarRecentAppsController(mockRecentsModel) { mockDesktopVisibilityController }
recentAppsController.init(taskbarControllers)
- recentAppsController.isEnabled = true
- recentAppsController.setApps(
- ALL_APP_PACKAGES.map { createTestAppInfo(packageName = it) }.toTypedArray()
- )
+ recentAppsController.canShowRunningApps = true
+ recentAppsController.canShowRecentApps = true
+
+ val listenerCaptor = ArgumentCaptor.forClass(RecentTasksChangedListener::class.java)
+ verify(mockRecentsModel).registerRecentTasksChangedListener(listenerCaptor.capture())
+ recentTasksChangedListener = listenerCaptor.value
}
@Test
- fun updateHotseatItemInfos_notInDesktopMode_returnsExistingHotseatItems() {
- setInDesktopMode(false)
- val hotseatItems =
- createHotseatItemsFromPackageNames(listOf(HOTSEAT_PACKAGE_1, HOTSEAT_PACKAGE_2))
-
- assertThat(recentAppsController.updateHotseatItemInfos(hotseatItems.toTypedArray()))
- .isEqualTo(hotseatItems.toTypedArray())
- }
-
- @Test
- fun updateHotseatItemInfos_notInDesktopMode_runningApps_returnsExistingHotseatItems() {
- setInDesktopMode(false)
- val hotseatPackages = listOf(HOTSEAT_PACKAGE_1, HOTSEAT_PACKAGE_2)
- val hotseatItems = createHotseatItemsFromPackageNames(hotseatPackages)
- val runningTasks =
- createDesktopTasksFromPackageNames(listOf(RUNNING_APP_PACKAGE_1, RUNNING_APP_PACKAGE_2))
- whenever(mockRecentsModel.runningTasks).thenReturn(runningTasks)
- recentAppsController.updateRunningApps()
-
+ fun updateHotseatItemInfos_cantShowRunning_inDesktopMode_returnsAllHotseatItems() {
+ recentAppsController.canShowRunningApps = false
+ setInDesktopMode(true)
+ val hotseatPackages = listOf(HOTSEAT_PACKAGE_1, HOTSEAT_PACKAGE_2, PREDICTED_PACKAGE_1)
val newHotseatItems =
- recentAppsController.updateHotseatItemInfos(hotseatItems.toTypedArray())
-
+ prepareHotseatAndRunningAndRecentApps(
+ hotseatPackages = hotseatPackages,
+ runningTaskPackages = emptyList(),
+ recentTaskPackages = emptyList()
+ )
assertThat(newHotseatItems.map { it?.targetPackage })
.containsExactlyElementsIn(hotseatPackages)
}
@Test
- fun updateHotseatItemInfos_noRunningApps_returnsExistingHotseatItems() {
- setInDesktopMode(true)
- val hotseatItems =
- createHotseatItemsFromPackageNames(listOf(HOTSEAT_PACKAGE_1, HOTSEAT_PACKAGE_2))
-
- assertThat(recentAppsController.updateHotseatItemInfos(hotseatItems.toTypedArray()))
- .isEqualTo(hotseatItems.toTypedArray())
- }
-
- @Test
- fun updateHotseatItemInfos_returnsExistingHotseatItemsAndRunningApps() {
- setInDesktopMode(true)
- val hotseatItems =
- createHotseatItemsFromPackageNames(listOf(HOTSEAT_PACKAGE_1, HOTSEAT_PACKAGE_2))
- val runningTasks =
- createDesktopTasksFromPackageNames(listOf(RUNNING_APP_PACKAGE_1, RUNNING_APP_PACKAGE_2))
- whenever(mockRecentsModel.runningTasks).thenReturn(runningTasks)
- recentAppsController.updateRunningApps()
-
- val newHotseatItems =
- recentAppsController.updateHotseatItemInfos(hotseatItems.toTypedArray())
-
- val expectedPackages =
- listOf(
- HOTSEAT_PACKAGE_1,
- HOTSEAT_PACKAGE_2,
- RUNNING_APP_PACKAGE_1,
- RUNNING_APP_PACKAGE_2,
- )
- assertThat(newHotseatItems.map { it?.targetPackage })
- .containsExactlyElementsIn(expectedPackages)
- }
-
- @Test
- fun updateHotseatItemInfos_runningAppIsHotseatItem_returnsDistinctItems() {
- setInDesktopMode(true)
- val hotseatItems =
- createHotseatItemsFromPackageNames(listOf(HOTSEAT_PACKAGE_1, HOTSEAT_PACKAGE_2))
- val runningTasks =
- createDesktopTasksFromPackageNames(
- listOf(HOTSEAT_PACKAGE_1, RUNNING_APP_PACKAGE_1, RUNNING_APP_PACKAGE_2)
- )
- whenever(mockRecentsModel.runningTasks).thenReturn(runningTasks)
- recentAppsController.updateRunningApps()
-
- val newHotseatItems =
- recentAppsController.updateHotseatItemInfos(hotseatItems.toTypedArray())
-
- val expectedPackages =
- listOf(
- HOTSEAT_PACKAGE_1,
- HOTSEAT_PACKAGE_2,
- RUNNING_APP_PACKAGE_1,
- RUNNING_APP_PACKAGE_2,
- )
- assertThat(newHotseatItems.map { it?.targetPackage })
- .containsExactlyElementsIn(expectedPackages)
- }
-
- @Test
- fun getRunningApps_notInDesktopMode_returnsEmptySet() {
+ fun updateHotseatItemInfos_cantShowRecent_notInDesktopMode_returnsAllHotseatItems() {
+ recentAppsController.canShowRecentApps = false
setInDesktopMode(false)
- val runningTasks =
- createDesktopTasksFromPackageNames(listOf(RUNNING_APP_PACKAGE_1, RUNNING_APP_PACKAGE_2))
- whenever(mockRecentsModel.runningTasks).thenReturn(runningTasks)
- recentAppsController.updateRunningApps()
-
- assertThat(recentAppsController.runningApps).isEmpty()
- assertThat(recentAppsController.minimizedApps).isEmpty()
+ val hotseatPackages = listOf(HOTSEAT_PACKAGE_1, HOTSEAT_PACKAGE_2, PREDICTED_PACKAGE_1)
+ val newHotseatItems =
+ prepareHotseatAndRunningAndRecentApps(
+ hotseatPackages = hotseatPackages,
+ runningTaskPackages = emptyList(),
+ recentTaskPackages = emptyList()
+ )
+ assertThat(newHotseatItems.map { it?.targetPackage })
+ .containsExactlyElementsIn(hotseatPackages)
}
@Test
- fun getRunningApps_inDesktopMode_returnsRunningApps() {
+ fun updateHotseatItemInfos_canShowRunning_inDesktopMode_returnsNonPredictedHotseatItems() {
+ recentAppsController.canShowRunningApps = true
setInDesktopMode(true)
- val runningTasks =
- createDesktopTasksFromPackageNames(listOf(RUNNING_APP_PACKAGE_1, RUNNING_APP_PACKAGE_2))
- whenever(mockRecentsModel.runningTasks).thenReturn(runningTasks)
- recentAppsController.updateRunningApps()
+ val newHotseatItems =
+ prepareHotseatAndRunningAndRecentApps(
+ hotseatPackages = listOf(HOTSEAT_PACKAGE_1, HOTSEAT_PACKAGE_2, PREDICTED_PACKAGE_1),
+ runningTaskPackages = emptyList(),
+ recentTaskPackages = emptyList()
+ )
+ val expectedPackages = listOf(HOTSEAT_PACKAGE_1, HOTSEAT_PACKAGE_2)
+ assertThat(newHotseatItems.map { it?.targetPackage })
+ .containsExactlyElementsIn(expectedPackages)
+ }
- assertThat(recentAppsController.runningApps)
- .containsExactly(RUNNING_APP_PACKAGE_1, RUNNING_APP_PACKAGE_2)
- assertThat(recentAppsController.minimizedApps).isEmpty()
+ @Test
+ fun updateHotseatItemInfos_canShowRecent_notInDesktopMode_returnsNonPredictedHotseatItems() {
+ recentAppsController.canShowRecentApps = true
+ setInDesktopMode(false)
+ val newHotseatItems =
+ prepareHotseatAndRunningAndRecentApps(
+ hotseatPackages = listOf(HOTSEAT_PACKAGE_1, HOTSEAT_PACKAGE_2, PREDICTED_PACKAGE_1),
+ runningTaskPackages = emptyList(),
+ recentTaskPackages = emptyList()
+ )
+ val expectedPackages = listOf(HOTSEAT_PACKAGE_1, HOTSEAT_PACKAGE_2)
+ assertThat(newHotseatItems.map { it?.targetPackage })
+ .containsExactlyElementsIn(expectedPackages)
+ }
+
+ @Test
+ fun onRecentTasksChanged_cantShowRunning_inDesktopMode_shownTasks_returnsEmptyList() {
+ recentAppsController.canShowRunningApps = false
+ setInDesktopMode(true)
+ prepareHotseatAndRunningAndRecentApps(
+ hotseatPackages = listOf(HOTSEAT_PACKAGE_1, HOTSEAT_PACKAGE_2, PREDICTED_PACKAGE_1),
+ runningTaskPackages = listOf(RECENT_PACKAGE_1, RECENT_PACKAGE_2),
+ recentTaskPackages = emptyList()
+ )
+ assertThat(recentAppsController.shownTasks).isEmpty()
+ }
+
+ @Test
+ fun onRecentTasksChanged_cantShowRecent_notInDesktopMode_shownTasks_returnsEmptyList() {
+ recentAppsController.canShowRecentApps = false
+ setInDesktopMode(false)
+ prepareHotseatAndRunningAndRecentApps(
+ hotseatPackages = listOf(HOTSEAT_PACKAGE_1, HOTSEAT_PACKAGE_2, PREDICTED_PACKAGE_1),
+ runningTaskPackages = emptyList(),
+ recentTaskPackages = listOf(RECENT_PACKAGE_1, RECENT_PACKAGE_2)
+ )
+ assertThat(recentAppsController.shownTasks).isEmpty()
+ }
+
+ @Test
+ fun onRecentTasksChanged_notInDesktopMode_noRecentTasks_shownTasks_returnsEmptyList() {
+ setInDesktopMode(false)
+ prepareHotseatAndRunningAndRecentApps(
+ hotseatPackages = emptyList(),
+ runningTaskPackages = listOf(RUNNING_APP_PACKAGE_1, RUNNING_APP_PACKAGE_2),
+ recentTaskPackages = emptyList()
+ )
+ assertThat(recentAppsController.shownTasks).isEmpty()
+ assertThat(recentAppsController.minimizedAppPackages).isEmpty()
+ }
+
+ @Test
+ fun onRecentTasksChanged_inDesktopMode_noRunningApps_shownTasks_returnsEmptyList() {
+ setInDesktopMode(true)
+ prepareHotseatAndRunningAndRecentApps(
+ hotseatPackages = emptyList(),
+ runningTaskPackages = emptyList(),
+ recentTaskPackages = listOf(RECENT_PACKAGE_1, RECENT_PACKAGE_2)
+ )
+ assertThat(recentAppsController.shownTasks).isEmpty()
+ }
+
+ @Test
+ fun onRecentTasksChanged_inDesktopMode_shownTasks_returnsRunningTasks() {
+ setInDesktopMode(true)
+ val runningTaskPackages = listOf(RUNNING_APP_PACKAGE_1, RUNNING_APP_PACKAGE_2)
+ prepareHotseatAndRunningAndRecentApps(
+ hotseatPackages = emptyList(),
+ runningTaskPackages = runningTaskPackages,
+ recentTaskPackages = emptyList()
+ )
+ val shownPackages = recentAppsController.shownTasks.flatMap { it.packageNames }
+ assertThat(shownPackages).containsExactlyElementsIn(runningTaskPackages)
+ }
+
+ @Test
+ fun onRecentTasksChanged_inDesktopMode_runningAppIsHotseatItem_shownTasks_returnsDistinctItems() {
+ setInDesktopMode(true)
+ prepareHotseatAndRunningAndRecentApps(
+ hotseatPackages = listOf(HOTSEAT_PACKAGE_1, HOTSEAT_PACKAGE_2),
+ runningTaskPackages =
+ listOf(HOTSEAT_PACKAGE_1, RUNNING_APP_PACKAGE_1, RUNNING_APP_PACKAGE_2),
+ recentTaskPackages = emptyList()
+ )
+ val expectedPackages = listOf(RUNNING_APP_PACKAGE_1, RUNNING_APP_PACKAGE_2)
+ val shownPackages = recentAppsController.shownTasks.flatMap { it.packageNames }
+ assertThat(shownPackages).containsExactlyElementsIn(expectedPackages)
+ }
+
+ @Test
+ fun onRecentTasksChanged_notInDesktopMode_getRunningApps_returnsEmptySet() {
+ setInDesktopMode(false)
+ prepareHotseatAndRunningAndRecentApps(
+ hotseatPackages = emptyList(),
+ runningTaskPackages = listOf(RUNNING_APP_PACKAGE_1, RUNNING_APP_PACKAGE_2),
+ recentTaskPackages = emptyList()
+ )
+ assertThat(recentAppsController.runningAppPackages).isEmpty()
+ assertThat(recentAppsController.minimizedAppPackages).isEmpty()
+ }
+
+ @Test
+ fun onRecentTasksChanged_inDesktopMode_getRunningApps_returnsAllDesktopTasks() {
+ setInDesktopMode(true)
+ val runningTaskPackages = listOf(RUNNING_APP_PACKAGE_1, RUNNING_APP_PACKAGE_2)
+ prepareHotseatAndRunningAndRecentApps(
+ hotseatPackages = emptyList(),
+ runningTaskPackages = runningTaskPackages,
+ recentTaskPackages = emptyList()
+ )
+ assertThat(recentAppsController.runningAppPackages)
+ .containsExactlyElementsIn(runningTaskPackages)
+ assertThat(recentAppsController.minimizedAppPackages).isEmpty()
+ }
+
+ @Test
+ fun onRecentTasksChanged_inDesktopMode_getRunningApps_includesHotseat() {
+ setInDesktopMode(true)
+ val runningTaskPackages =
+ listOf(HOTSEAT_PACKAGE_1, RUNNING_APP_PACKAGE_1, RUNNING_APP_PACKAGE_2)
+ prepareHotseatAndRunningAndRecentApps(
+ hotseatPackages = listOf(HOTSEAT_PACKAGE_1, HOTSEAT_PACKAGE_2),
+ runningTaskPackages = runningTaskPackages,
+ recentTaskPackages = listOf(RECENT_PACKAGE_1, RECENT_PACKAGE_2)
+ )
+ assertThat(recentAppsController.runningAppPackages)
+ .containsExactlyElementsIn(runningTaskPackages)
+ assertThat(recentAppsController.minimizedAppPackages).isEmpty()
}
@Test
fun getMinimizedApps_inDesktopMode_returnsAllAppsRunningAndInvisibleAppsMinimized() {
setInDesktopMode(true)
- val runningTasks =
- ArrayList(
- listOf(
- createDesktopTaskInfo(RUNNING_APP_PACKAGE_1) { isVisible = true },
- createDesktopTaskInfo(RUNNING_APP_PACKAGE_2) { isVisible = true },
- createDesktopTaskInfo(RUNNING_APP_PACKAGE_3) { isVisible = false },
- )
- )
- whenever(mockRecentsModel.runningTasks).thenReturn(runningTasks)
- recentAppsController.updateRunningApps()
+ val runningTaskPackages =
+ listOf(RUNNING_APP_PACKAGE_1, RUNNING_APP_PACKAGE_2, RUNNING_APP_PACKAGE_3)
+ val minimizedTaskIndices = setOf(2) // RUNNING_APP_PACKAGE_3
+ prepareHotseatAndRunningAndRecentApps(
+ hotseatPackages = emptyList(),
+ runningTaskPackages = runningTaskPackages,
+ minimizedTaskIndices = minimizedTaskIndices,
+ recentTaskPackages = emptyList()
+ )
+ assertThat(recentAppsController.runningAppPackages)
+ .containsExactlyElementsIn(runningTaskPackages)
+ assertThat(recentAppsController.minimizedAppPackages).containsExactly(RUNNING_APP_PACKAGE_3)
+ }
- assertThat(recentAppsController.runningApps)
- .containsExactly(RUNNING_APP_PACKAGE_1, RUNNING_APP_PACKAGE_2, RUNNING_APP_PACKAGE_3)
- assertThat(recentAppsController.minimizedApps).containsExactly(RUNNING_APP_PACKAGE_3)
+ @Test
+ fun getMinimizedApps_inDesktopMode_twoTasksSamePackageOneMinimizedReturnsNotMinimized() {
+ setInDesktopMode(true)
+ val runningTaskPackages = listOf(RUNNING_APP_PACKAGE_1, RUNNING_APP_PACKAGE_1)
+ val minimizedTaskIndices = setOf(1) // The second RUNNING_APP_PACKAGE_1 task.
+ prepareHotseatAndRunningAndRecentApps(
+ hotseatPackages = emptyList(),
+ runningTaskPackages = runningTaskPackages,
+ minimizedTaskIndices = minimizedTaskIndices,
+ recentTaskPackages = emptyList()
+ )
+ assertThat(recentAppsController.runningAppPackages)
+ .containsExactlyElementsIn(runningTaskPackages.toSet())
+ assertThat(recentAppsController.minimizedAppPackages).isEmpty()
+ }
+
+ @Test
+ fun onRecentTasksChanged_inDesktopMode_shownTasks_maintainsOrder() {
+ setInDesktopMode(true)
+ val originalOrder = listOf(RUNNING_APP_PACKAGE_1, RUNNING_APP_PACKAGE_2)
+ prepareHotseatAndRunningAndRecentApps(
+ hotseatPackages = emptyList(),
+ runningTaskPackages = originalOrder,
+ recentTaskPackages = emptyList()
+ )
+ prepareHotseatAndRunningAndRecentApps(
+ hotseatPackages = emptyList(),
+ runningTaskPackages = listOf(RUNNING_APP_PACKAGE_2, RUNNING_APP_PACKAGE_1),
+ recentTaskPackages = emptyList()
+ )
+ val shownPackages = recentAppsController.shownTasks.flatMap { it.packageNames }
+ assertThat(shownPackages).isEqualTo(originalOrder)
+ }
+
+ @Test
+ fun onRecentTasksChanged_notInDesktopMode_shownTasks_maintainsRecency() {
+ setInDesktopMode(false)
+ prepareHotseatAndRunningAndRecentApps(
+ hotseatPackages = emptyList(),
+ runningTaskPackages = emptyList(),
+ recentTaskPackages = listOf(RECENT_PACKAGE_1, RECENT_PACKAGE_2, RECENT_PACKAGE_3)
+ )
+ prepareHotseatAndRunningAndRecentApps(
+ hotseatPackages = emptyList(),
+ runningTaskPackages = emptyList(),
+ recentTaskPackages = listOf(RECENT_PACKAGE_2, RECENT_PACKAGE_3, RECENT_PACKAGE_1)
+ )
+ val shownPackages = recentAppsController.shownTasks.flatMap { it.packageNames }
+ // Most recent packages, minus the currently running one (RECENT_PACKAGE_1).
+ assertThat(shownPackages).isEqualTo(listOf(RECENT_PACKAGE_2, RECENT_PACKAGE_3))
+ }
+
+ @Test
+ fun onRecentTasksChanged_inDesktopMode_addTask_shownTasks_maintainsOrder() {
+ setInDesktopMode(true)
+ prepareHotseatAndRunningAndRecentApps(
+ hotseatPackages = emptyList(),
+ runningTaskPackages = listOf(RUNNING_APP_PACKAGE_1, RUNNING_APP_PACKAGE_2),
+ recentTaskPackages = emptyList()
+ )
+ prepareHotseatAndRunningAndRecentApps(
+ hotseatPackages = emptyList(),
+ runningTaskPackages =
+ listOf(RUNNING_APP_PACKAGE_2, RUNNING_APP_PACKAGE_1, RUNNING_APP_PACKAGE_3),
+ recentTaskPackages = emptyList()
+ )
+ val shownPackages = recentAppsController.shownTasks.flatMap { it.packageNames }
+ val expectedOrder =
+ listOf(RUNNING_APP_PACKAGE_1, RUNNING_APP_PACKAGE_2, RUNNING_APP_PACKAGE_3)
+ assertThat(shownPackages).isEqualTo(expectedOrder)
+ }
+
+ @Test
+ fun onRecentTasksChanged_notInDesktopMode_addTask_shownTasks_maintainsRecency() {
+ setInDesktopMode(false)
+ prepareHotseatAndRunningAndRecentApps(
+ hotseatPackages = emptyList(),
+ runningTaskPackages = emptyList(),
+ recentTaskPackages = listOf(RECENT_PACKAGE_3, RECENT_PACKAGE_2)
+ )
+ prepareHotseatAndRunningAndRecentApps(
+ hotseatPackages = emptyList(),
+ runningTaskPackages = emptyList(),
+ recentTaskPackages = listOf(RECENT_PACKAGE_2, RECENT_PACKAGE_3, RECENT_PACKAGE_1)
+ )
+ val shownPackages = recentAppsController.shownTasks.flatMap { it.packageNames }
+ // Most recent packages, minus the currently running one (RECENT_PACKAGE_1).
+ assertThat(shownPackages).isEqualTo(listOf(RECENT_PACKAGE_2, RECENT_PACKAGE_3))
+ }
+
+ @Test
+ fun onRecentTasksChanged_inDesktopMode_removeTask_shownTasks_maintainsOrder() {
+ setInDesktopMode(true)
+ prepareHotseatAndRunningAndRecentApps(
+ hotseatPackages = emptyList(),
+ runningTaskPackages =
+ listOf(RUNNING_APP_PACKAGE_1, RUNNING_APP_PACKAGE_2, RUNNING_APP_PACKAGE_3),
+ recentTaskPackages = emptyList()
+ )
+ prepareHotseatAndRunningAndRecentApps(
+ hotseatPackages = emptyList(),
+ runningTaskPackages = listOf(RUNNING_APP_PACKAGE_2, RUNNING_APP_PACKAGE_1),
+ recentTaskPackages = emptyList()
+ )
+ val shownPackages = recentAppsController.shownTasks.flatMap { it.packageNames }
+ assertThat(shownPackages).isEqualTo(listOf(RUNNING_APP_PACKAGE_1, RUNNING_APP_PACKAGE_2))
+ }
+
+ @Test
+ fun onRecentTasksChanged_notInDesktopMode_removeTask_shownTasks_maintainsRecency() {
+ setInDesktopMode(false)
+ prepareHotseatAndRunningAndRecentApps(
+ hotseatPackages = emptyList(),
+ runningTaskPackages = emptyList(),
+ recentTaskPackages = listOf(RECENT_PACKAGE_1, RECENT_PACKAGE_2, RECENT_PACKAGE_3)
+ )
+ prepareHotseatAndRunningAndRecentApps(
+ hotseatPackages = emptyList(),
+ runningTaskPackages = emptyList(),
+ recentTaskPackages = listOf(RECENT_PACKAGE_2, RECENT_PACKAGE_3)
+ )
+ val shownPackages = recentAppsController.shownTasks.flatMap { it.packageNames }
+ // Most recent packages, minus the currently running one (RECENT_PACKAGE_3).
+ assertThat(shownPackages).isEqualTo(listOf(RECENT_PACKAGE_2))
+ }
+
+ @Test
+ fun onRecentTasksChanged_enterDesktopMode_shownTasks_onlyIncludesRunningTasks() {
+ setInDesktopMode(false)
+ val runningTaskPackages = listOf(RUNNING_APP_PACKAGE_1, RUNNING_APP_PACKAGE_2)
+ val recentTaskPackages = listOf(RECENT_PACKAGE_1, RECENT_PACKAGE_2)
+ prepareHotseatAndRunningAndRecentApps(
+ hotseatPackages = emptyList(),
+ runningTaskPackages = runningTaskPackages,
+ recentTaskPackages = recentTaskPackages
+ )
+ setInDesktopMode(true)
+ recentTasksChangedListener.onRecentTasksChanged()
+ val shownPackages = recentAppsController.shownTasks.flatMap { it.packageNames }
+ assertThat(shownPackages).containsExactlyElementsIn(runningTaskPackages)
+ }
+
+ @Test
+ fun onRecentTasksChanged_exitDesktopMode_shownTasks_onlyIncludesRecentTasks() {
+ setInDesktopMode(true)
+ val runningTaskPackages = listOf(RUNNING_APP_PACKAGE_1, RUNNING_APP_PACKAGE_2)
+ val recentTaskPackages = listOf(RECENT_PACKAGE_1, RECENT_PACKAGE_2, RECENT_PACKAGE_3)
+ prepareHotseatAndRunningAndRecentApps(
+ hotseatPackages = emptyList(),
+ runningTaskPackages = runningTaskPackages,
+ recentTaskPackages = recentTaskPackages
+ )
+ setInDesktopMode(false)
+ recentTasksChangedListener.onRecentTasksChanged()
+ val shownPackages = recentAppsController.shownTasks.flatMap { it.packageNames }
+ // Don't expect RECENT_PACKAGE_3 because it is currently running.
+ val expectedPackages = listOf(RECENT_PACKAGE_1, RECENT_PACKAGE_2)
+ assertThat(shownPackages).containsExactlyElementsIn(expectedPackages)
+ }
+
+ @Test
+ fun onRecentTasksChanged_notInDesktopMode_hasRecentTasks_shownTasks_returnsRecentTasks() {
+ setInDesktopMode(false)
+ prepareHotseatAndRunningAndRecentApps(
+ hotseatPackages = emptyList(),
+ runningTaskPackages = emptyList(),
+ recentTaskPackages = listOf(RECENT_PACKAGE_1, RECENT_PACKAGE_2, RECENT_PACKAGE_3)
+ )
+ val shownPackages = recentAppsController.shownTasks.flatMap { it.packageNames }
+ // RECENT_PACKAGE_3 is the top task (visible to user) so should be excluded.
+ val expectedPackages = listOf(RECENT_PACKAGE_1, RECENT_PACKAGE_2)
+ assertThat(shownPackages).containsExactlyElementsIn(expectedPackages)
+ }
+
+ @Test
+ fun onRecentTasksChanged_notInDesktopMode_hasRecentAndRunningTasks_shownTasks_returnsRecentTaskAndDesktopTile() {
+ setInDesktopMode(false)
+ prepareHotseatAndRunningAndRecentApps(
+ hotseatPackages = emptyList(),
+ runningTaskPackages = listOf(RUNNING_APP_PACKAGE_1, RUNNING_APP_PACKAGE_2),
+ recentTaskPackages = listOf(RECENT_PACKAGE_1, RECENT_PACKAGE_2)
+ )
+ val shownPackages = recentAppsController.shownTasks.map { it.packageNames }
+ // Only 2 recent tasks shown: Desktop Tile + 1 Recent Task
+ val desktopTilePackages = listOf(RUNNING_APP_PACKAGE_1, RUNNING_APP_PACKAGE_2)
+ val recentTaskPackages = listOf(RECENT_PACKAGE_1)
+ val expectedPackages = listOf(desktopTilePackages, recentTaskPackages)
+ assertThat(shownPackages).containsExactlyElementsIn(expectedPackages)
+ }
+
+ @Test
+ fun onRecentTasksChanged_notInDesktopMode_hasRecentAndSplitTasks_shownTasks_returnsRecentTaskAndPair() {
+ setInDesktopMode(false)
+ prepareHotseatAndRunningAndRecentApps(
+ hotseatPackages = emptyList(),
+ runningTaskPackages = emptyList(),
+ recentTaskPackages = listOf(RECENT_SPLIT_PACKAGES_1, RECENT_PACKAGE_1, RECENT_PACKAGE_2)
+ )
+ val shownPackages = recentAppsController.shownTasks.map { it.packageNames }
+ // Only 2 recent tasks shown: Pair + 1 Recent Task
+ val pairPackages = RECENT_SPLIT_PACKAGES_1.split("_")
+ val recentTaskPackages = listOf(RECENT_PACKAGE_1)
+ val expectedPackages = listOf(pairPackages, recentTaskPackages)
+ assertThat(shownPackages).containsExactlyElementsIn(expectedPackages)
+ }
+
+ private fun prepareHotseatAndRunningAndRecentApps(
+ hotseatPackages: List<String>,
+ runningTaskPackages: List<String>,
+ minimizedTaskIndices: Set<Int> = emptySet(),
+ recentTaskPackages: List<String>,
+ ): Array<ItemInfo?> {
+ val hotseatItems = createHotseatItemsFromPackageNames(hotseatPackages)
+ val newHotseatItems =
+ recentAppsController.updateHotseatItemInfos(hotseatItems.toTypedArray())
+ val runningTasks = createDesktopTask(runningTaskPackages, minimizedTaskIndices)
+ val recentTasks = createRecentTasksFromPackageNames(recentTaskPackages)
+ val allTasks =
+ ArrayList<GroupTask>().apply {
+ if (runningTasks != null) {
+ add(runningTasks)
+ }
+ addAll(recentTasks)
+ }
+ doAnswer {
+ val callback: Consumer<ArrayList<GroupTask>> = it.getArgument(0)
+ callback.accept(allTasks)
+ taskListChangeId
+ }
+ .whenever(mockRecentsModel)
+ .getTasks(any<Consumer<List<GroupTask>>>())
+ recentTasksChangedListener.onRecentTasksChanged()
+ return newHotseatItems
}
private fun createHotseatItemsFromPackageNames(packageNames: List<String>): List<ItemInfo> {
- return packageNames.map { createTestAppInfo(packageName = it) }
- }
-
- private fun createDesktopTasksFromPackageNames(
- packageNames: List<String>
- ): ArrayList<RunningTaskInfo> {
- return ArrayList(packageNames.map { createDesktopTaskInfo(packageName = it) })
- }
-
- private fun createDesktopTaskInfo(
- packageName: String,
- init: RunningTaskInfo.() -> Unit = { isVisible = true },
- ): RunningTaskInfo {
- return RunningTaskInfo().apply {
- taskId = nextTaskId++
- configuration.windowConfiguration.windowingMode = WINDOWING_MODE_FREEFORM
- realActivity = ComponentName(packageName, "TestActivity")
- init()
+ return packageNames.map {
+ createTestAppInfo(packageName = it).apply {
+ container =
+ if (it.startsWith("predicted")) {
+ CONTAINER_HOTSEAT_PREDICTION
+ } else {
+ CONTAINER_HOTSEAT
+ }
+ }
}
}
@@ -220,23 +525,67 @@
className: String = "testClassName"
) = AppInfo(ComponentName(packageName, className), className /* title */, userHandle, Intent())
+ private fun createDesktopTask(
+ packageNames: List<String>,
+ minimizedTaskIndices: Set<Int>
+ ): DesktopTask? {
+ if (packageNames.isEmpty()) return null
+
+ return DesktopTask(
+ ArrayList(
+ packageNames.mapIndexed { index, packageName ->
+ createTask(packageName, index !in minimizedTaskIndices)
+ }
+ )
+ )
+ }
+
+ private fun createRecentTasksFromPackageNames(packageNames: List<String>): List<GroupTask> {
+ return packageNames.map {
+ if (it.startsWith("split")) {
+ val splitPackages = it.split("_")
+ GroupTask(
+ createTask(splitPackages[0]),
+ createTask(splitPackages[1]),
+ /* splitBounds = */ null
+ )
+ } else {
+ GroupTask(createTask(it))
+ }
+ }
+ }
+
+ private fun createTask(packageName: String, isVisible: Boolean = true): Task {
+ return Task(
+ Task.TaskKey(
+ nextTaskId++,
+ WINDOWING_MODE_FREEFORM,
+ Intent().apply { `package` = packageName },
+ ComponentName(packageName, "TestActivity"),
+ userHandle.identifier,
+ 0
+ )
+ )
+ .apply { this.isVisible = isVisible }
+ }
+
private fun setInDesktopMode(inDesktopMode: Boolean) {
whenever(mockDesktopVisibilityController.areDesktopTasksVisible()).thenReturn(inDesktopMode)
}
+ private val GroupTask.packageNames: List<String>
+ get() = tasks.map { task -> task.key.packageName }
+
private companion object {
const val HOTSEAT_PACKAGE_1 = "hotseat1"
const val HOTSEAT_PACKAGE_2 = "hotseat2"
+ const val PREDICTED_PACKAGE_1 = "predicted1"
const val RUNNING_APP_PACKAGE_1 = "running1"
const val RUNNING_APP_PACKAGE_2 = "running2"
const val RUNNING_APP_PACKAGE_3 = "running3"
- val ALL_APP_PACKAGES =
- listOf(
- HOTSEAT_PACKAGE_1,
- HOTSEAT_PACKAGE_2,
- RUNNING_APP_PACKAGE_1,
- RUNNING_APP_PACKAGE_2,
- RUNNING_APP_PACKAGE_3,
- )
+ const val RECENT_PACKAGE_1 = "recent1"
+ const val RECENT_PACKAGE_2 = "recent2"
+ const val RECENT_PACKAGE_3 = "recent3"
+ const val RECENT_SPLIT_PACKAGES_1 = "split1_split2"
}
}
diff --git a/quickstep/tests/src/com/android/quickstep/TaplTestsQuickstep.java b/quickstep/tests/src/com/android/quickstep/TaplTestsQuickstep.java
index 7877e8a..1dfab26 100644
--- a/quickstep/tests/src/com/android/quickstep/TaplTestsQuickstep.java
+++ b/quickstep/tests/src/com/android/quickstep/TaplTestsQuickstep.java
@@ -401,6 +401,7 @@
@Test
@NavigationModeSwitch
@PortraitLandscape
+ @TestStabilityRule.Stability(flavors = LOCAL | PLATFORM_POSTSUBMIT) // b/325659406
public void testQuickSwitchFromHome() throws Exception {
startTestActivity(2);
mLauncher.goHome().quickSwitchToPreviousApp();
diff --git a/quickstep/tests/src/com/android/quickstep/TaskAnimationManagerTest.java b/quickstep/tests/src/com/android/quickstep/TaskAnimationManagerTest.java
new file mode 100644
index 0000000..2d79623
--- /dev/null
+++ b/quickstep/tests/src/com/android/quickstep/TaskAnimationManagerTest.java
@@ -0,0 +1,77 @@
+/*
+ * Copyright (C) 2024 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.quickstep;
+
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assume.assumeTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+
+import android.app.ActivityOptions;
+import android.content.Context;
+import android.content.Intent;
+
+import androidx.test.filters.SmallTest;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+@SmallTest
+public class TaskAnimationManagerTest {
+
+ @Mock
+ private Context mContext;
+
+ @Mock
+ private SystemUiProxy mSystemUiProxy;
+
+ private TaskAnimationManager mTaskAnimationManager;
+
+ @Before
+ public void setUp() {
+ MockitoAnnotations.initMocks(this);
+ mTaskAnimationManager = new TaskAnimationManager(mContext) {
+ @Override
+ SystemUiProxy getSystemUiProxy() {
+ return mSystemUiProxy;
+ }
+ };
+ }
+
+ @Test
+ public void startRecentsActivity_allowBackgroundLaunch() {
+ assumeTrue(TaskAnimationManager.ENABLE_SHELL_TRANSITIONS);
+
+ final LauncherActivityInterface activityInterface = mock(LauncherActivityInterface.class);
+ final GestureState gestureState = mock(GestureState.class);
+ final RecentsAnimationCallbacks.RecentsAnimationListener listener =
+ mock(RecentsAnimationCallbacks.RecentsAnimationListener.class);
+ doReturn(activityInterface).when(gestureState).getContainerInterface();
+ mTaskAnimationManager.startRecentsAnimation(gestureState, new Intent(), listener);
+
+ final ArgumentCaptor<ActivityOptions> optionsCaptor =
+ ArgumentCaptor.forClass(ActivityOptions.class);
+ verify(mSystemUiProxy).startRecentsActivity(any(), optionsCaptor.capture(), any());
+ assertTrue(optionsCaptor.getValue()
+ .isPendingIntentBackgroundActivityLaunchAllowedByPermission());
+ }
+}
diff --git a/res/layout/bubble_bar_overflow_button.xml b/res/layout/bubble_bar_overflow_button.xml
new file mode 100644
index 0000000..cb54990
--- /dev/null
+++ b/res/layout/bubble_bar_overflow_button.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2024 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.launcher3.taskbar.bubbles.BubbleView
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/bubble_overflow_button"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"/>
\ No newline at end of file
diff --git a/src/com/android/launcher3/BubbleTextView.java b/src/com/android/launcher3/BubbleTextView.java
index 7d09164..83427a0 100644
--- a/src/com/android/launcher3/BubbleTextView.java
+++ b/src/com/android/launcher3/BubbleTextView.java
@@ -430,10 +430,21 @@
setDownloadStateContentDescription(info, info.getProgressLevel());
}
+ /**
+ * Directly set the icon and label.
+ */
+ @UiThread
+ public void applyIconAndLabel(Drawable icon, CharSequence label) {
+ applyCompoundDrawables(icon);
+ setText(label);
+ setContentDescription(label);
+ }
+
/** Updates whether the app this view represents is currently running. */
@UiThread
public void updateRunningState(RunningAppState runningAppState) {
mRunningAppState = runningAppState;
+ invalidate();
}
protected void setItemInfo(ItemInfoWithIcon itemInfo) {
@@ -1291,13 +1302,4 @@
public boolean canShowLongPressPopup() {
return getTag() instanceof ItemInfo && ShortcutUtil.supportsShortcuts((ItemInfo) getTag());
}
-
- /** Returns the package name of the app this icon represents. */
- public String getTargetPackageName() {
- Object tag = getTag();
- if (tag instanceof ItemInfo itemInfo) {
- return itemInfo.getTargetPackage();
- }
- return null;
- }
}
diff --git a/src/com/android/launcher3/Hotseat.java b/src/com/android/launcher3/Hotseat.java
index 8546454..f775673 100644
--- a/src/com/android/launcher3/Hotseat.java
+++ b/src/com/android/launcher3/Hotseat.java
@@ -97,10 +97,9 @@
if (bubbleBarEnabled) {
float adjustedBorderSpace = dp.getHotseatAdjustedBorderSpaceForBubbleBar(getContext());
if (hasBubbles && Float.compare(adjustedBorderSpace, 0f) != 0) {
- getShortcutsAndWidgets().setTranslationProvider(child -> {
- int index = getShortcutsAndWidgets().indexOfChild(child);
+ getShortcutsAndWidgets().setTranslationProvider(cellX -> {
float borderSpaceDelta = adjustedBorderSpace - dp.hotseatBorderSpace;
- return dp.iconSizePx + index * borderSpaceDelta;
+ return dp.iconSizePx + cellX * borderSpaceDelta;
});
if (mQsb instanceof HorizontalInsettableView) {
HorizontalInsettableView insettableQsb = (HorizontalInsettableView) mQsb;
@@ -147,10 +146,7 @@
// update the translation provider for future layout passes of hotseat icons.
if (isBubbleBarVisible) {
- icons.setTranslationProvider(child -> {
- int index = icons.indexOfChild(child);
- return dp.iconSizePx + index * borderSpaceDelta;
- });
+ icons.setTranslationProvider(cellX -> dp.iconSizePx + cellX * borderSpaceDelta);
} else {
icons.setTranslationProvider(null);
}
diff --git a/src/com/android/launcher3/Launcher.java b/src/com/android/launcher3/Launcher.java
index 4e566ab..d905801 100644
--- a/src/com/android/launcher3/Launcher.java
+++ b/src/com/android/launcher3/Launcher.java
@@ -2797,9 +2797,11 @@
}
private void updateDisallowBack() {
- if (BuildCompat.isAtLeastV() && Flags.enableDesktopWindowingMode()
- && mDeviceProfile.isTablet) {
- // TODO(b/330183377) disable back in launcher when when we productionize
+ if (BuildCompat.isAtLeastV()
+ && Flags.enableDesktopWindowingMode()
+ && !Flags.enableDesktopWindowingWallpaperActivity()
+ && mDeviceProfile.isTablet) {
+ // TODO(b/333533253): Clean up after desktop wallpaper activity flag is rolled out
return;
}
LauncherRootView rv = getRootView();
diff --git a/src/com/android/launcher3/LauncherAppState.java b/src/com/android/launcher3/LauncherAppState.java
index 3b8ff62..239967d 100644
--- a/src/com/android/launcher3/LauncherAppState.java
+++ b/src/com/android/launcher3/LauncherAppState.java
@@ -116,13 +116,13 @@
SimpleBroadcastReceiver modelChangeReceiver =
new SimpleBroadcastReceiver(mModel::onBroadcastIntent);
- modelChangeReceiver.register(mContext, Intent.ACTION_LOCALE_CHANGED,
+ modelChangeReceiver.registerAsync(mContext, Intent.ACTION_LOCALE_CHANGED,
ACTION_DEVICE_POLICY_RESOURCE_UPDATED);
if (BuildConfig.IS_STUDIO_BUILD) {
mContext.registerReceiver(modelChangeReceiver, new IntentFilter(ACTION_FORCE_ROLOAD),
RECEIVER_EXPORTED);
}
- mOnTerminateCallback.add(() -> mContext.unregisterReceiver(modelChangeReceiver));
+ mOnTerminateCallback.add(() -> modelChangeReceiver.unregisterReceiverSafelyAsync(mContext));
SafeCloseable userChangeListener = UserCache.INSTANCE.get(mContext)
.addUserEventListener(mModel::onUserEvent);
diff --git a/src/com/android/launcher3/ShortcutAndWidgetContainer.java b/src/com/android/launcher3/ShortcutAndWidgetContainer.java
index 7484b64..d2c3c78 100644
--- a/src/com/android/launcher3/ShortcutAndWidgetContainer.java
+++ b/src/com/android/launcher3/ShortcutAndWidgetContainer.java
@@ -245,7 +245,7 @@
}
child.layout(childLeft, childTop, childLeft + lp.width, childTop + lp.height);
if (mTranslationProvider != null) {
- final float tx = mTranslationProvider.getTranslationX(child);
+ final float tx = mTranslationProvider.getTranslationX(lp.getCellX());
if (child instanceof Reorderable) {
((Reorderable) child).getTranslateDelegate()
.getTranslationX(INDEX_BUBBLE_ADJUSTMENT_ANIM)
@@ -330,6 +330,6 @@
/** Provides translation values to apply when laying out child views. */
interface TranslationProvider {
- float getTranslationX(View child);
+ float getTranslationX(int cellX);
}
}
diff --git a/src/com/android/launcher3/allapps/AllAppsRecyclerView.java b/src/com/android/launcher3/allapps/AllAppsRecyclerView.java
index ba34f59..2a47222 100644
--- a/src/com/android/launcher3/allapps/AllAppsRecyclerView.java
+++ b/src/com/android/launcher3/allapps/AllAppsRecyclerView.java
@@ -305,9 +305,7 @@
@Override
public int getScrollBarTop() {
- return ActivityContext.lookupContext(getContext()).getAppsView().isSearchSupported()
- ? getResources().getDimensionPixelOffset(R.dimen.all_apps_header_top_padding)
- : 0;
+ return getResources().getDimensionPixelOffset(R.dimen.all_apps_header_top_padding);
}
@Override
diff --git a/src/com/android/launcher3/config/FeatureFlags.java b/src/com/android/launcher3/config/FeatureFlags.java
index 33e6f91..d0596fa 100644
--- a/src/com/android/launcher3/config/FeatureFlags.java
+++ b/src/com/android/launcher3/config/FeatureFlags.java
@@ -19,6 +19,7 @@
import static com.android.launcher3.config.FeatureFlags.BooleanFlag.DISABLED;
import static com.android.launcher3.config.FeatureFlags.BooleanFlag.ENABLED;
import static com.android.wm.shell.Flags.enableTaskbarNavbarUnification;
+import static com.android.wm.shell.Flags.enableTaskbarOnPhones;
import android.content.res.Resources;
@@ -143,7 +144,7 @@
DISABLED, "Sends a notification whenever launcher encounters an uncaught exception.");
public static final boolean ENABLE_TASKBAR_NAVBAR_UNIFICATION =
- enableTaskbarNavbarUnification() && !isPhone();
+ enableTaskbarNavbarUnification() && (!isPhone() || enableTaskbarOnPhones());
private static boolean isPhone() {
final boolean isPhone;
diff --git a/src/com/android/launcher3/model/LoaderTask.java b/src/com/android/launcher3/model/LoaderTask.java
index dc6968c..312c6f4 100644
--- a/src/com/android/launcher3/model/LoaderTask.java
+++ b/src/com/android/launcher3/model/LoaderTask.java
@@ -209,7 +209,7 @@
mApp.getContext().getContentResolver(),
"launcher_broadcast_installed_apps",
/* def= */ 0);
- if (launcherBroadcastInstalledApps == 1) {
+ if (launcherBroadcastInstalledApps == 1 && mIsRestoreFromBackup) {
List<FirstScreenBroadcastModel> broadcastModels =
FirstScreenBroadcastHelper.createModelsForFirstScreenBroadcast(
mPmHelper,
diff --git a/src/com/android/launcher3/pm/UserCache.java b/src/com/android/launcher3/pm/UserCache.java
index ed25186..cf03462 100644
--- a/src/com/android/launcher3/pm/UserCache.java
+++ b/src/com/android/launcher3/pm/UserCache.java
@@ -93,12 +93,12 @@
@Override
public void close() {
- MODEL_EXECUTOR.execute(() -> mUserChangeReceiver.unregisterReceiverSafely(mContext));
+ MODEL_EXECUTOR.execute(() -> mUserChangeReceiver.unregisterReceiverSafelySync(mContext));
}
@WorkerThread
private void initAsync() {
- mUserChangeReceiver.register(mContext,
+ mUserChangeReceiver.registerSync(mContext,
Intent.ACTION_MANAGED_PROFILE_AVAILABLE,
Intent.ACTION_MANAGED_PROFILE_UNAVAILABLE,
Intent.ACTION_MANAGED_PROFILE_REMOVED,
diff --git a/src/com/android/launcher3/util/DisplayController.java b/src/com/android/launcher3/util/DisplayController.java
index 16fabe2..3dcc663 100644
--- a/src/com/android/launcher3/util/DisplayController.java
+++ b/src/com/android/launcher3/util/DisplayController.java
@@ -132,11 +132,11 @@
mWindowContext.registerComponentCallbacks(this);
} else {
mWindowContext = null;
- mReceiver.register(mContext, ACTION_CONFIGURATION_CHANGED);
+ mReceiver.registerAsync(mContext, ACTION_CONFIGURATION_CHANGED);
}
// Initialize navigation mode change listener
- mReceiver.registerPkgActions(mContext, TARGET_OVERLAY_PACKAGE, ACTION_OVERLAY_CHANGED);
+ mReceiver.registerPkgActionsAsync(mContext, TARGET_OVERLAY_PACKAGE, ACTION_OVERLAY_CHANGED);
WindowManagerProxy wmProxy = WindowManagerProxy.INSTANCE.get(context);
Context displayInfoContext = getDisplayInfoContext(display);
@@ -182,6 +182,11 @@
return INSTANCE.get(context).getInfo().isTransientTaskbar();
}
+ /** Returns whether we are currently in Desktop mode. */
+ public static boolean isInDesktopMode(Context context) {
+ return INSTANCE.get(context).getInfo().isInDesktopMode();
+ }
+
/**
* Handles info change for desktop mode.
*/
@@ -218,6 +223,7 @@
} else {
// TODO: unregister broadcast receiver
}
+ mReceiver.unregisterReceiverSafelyAsync(mContext);
}
/**
diff --git a/src/com/android/launcher3/util/LockedUserState.kt b/src/com/android/launcher3/util/LockedUserState.kt
index 94f9e4f..2737249 100644
--- a/src/com/android/launcher3/util/LockedUserState.kt
+++ b/src/com/android/launcher3/util/LockedUserState.kt
@@ -25,6 +25,7 @@
val isUserUnlockedAtLauncherStartup: Boolean
var isUserUnlocked: Boolean
private set
+
private val mUserUnlockedActions: RunnableList = RunnableList()
@VisibleForTesting
@@ -50,22 +51,18 @@
if (isUserUnlocked) {
notifyUserUnlocked()
} else {
- mUserUnlockedReceiver.register(mContext, Intent.ACTION_USER_UNLOCKED)
+ mUserUnlockedReceiver.registerAsync(mContext, Intent.ACTION_USER_UNLOCKED)
}
}
private fun notifyUserUnlocked() {
mUserUnlockedActions.executeAllAndDestroy()
- Executors.THREAD_POOL_EXECUTOR.execute {
- mUserUnlockedReceiver.unregisterReceiverSafely(mContext)
- }
+ mUserUnlockedReceiver.unregisterReceiverSafelyAsync(mContext)
}
/** Stops the receiver from listening for ACTION_USER_UNLOCK broadcasts. */
override fun close() {
- Executors.THREAD_POOL_EXECUTOR.execute {
- mUserUnlockedReceiver.unregisterReceiverSafely(mContext)
- }
+ mUserUnlockedReceiver.unregisterReceiverSafelyAsync(mContext)
}
/**
diff --git a/src/com/android/launcher3/util/ScreenOnTracker.java b/src/com/android/launcher3/util/ScreenOnTracker.java
index e16e477..c1d192c 100644
--- a/src/com/android/launcher3/util/ScreenOnTracker.java
+++ b/src/com/android/launcher3/util/ScreenOnTracker.java
@@ -42,12 +42,12 @@
// Assume that the screen is on to begin with
mContext = context;
mIsScreenOn = true;
- mReceiver.register(context, ACTION_SCREEN_ON, ACTION_SCREEN_OFF, ACTION_USER_PRESENT);
+ mReceiver.registerAsync(context, ACTION_SCREEN_ON, ACTION_SCREEN_OFF, ACTION_USER_PRESENT);
}
@Override
public void close() {
- mReceiver.unregisterReceiverSafely(mContext);
+ mReceiver.unregisterReceiverSafelyAsync(mContext);
}
private void onReceive(Intent intent) {
diff --git a/src/com/android/launcher3/util/SettingsCache.java b/src/com/android/launcher3/util/SettingsCache.java
index ccd154a..cd6701d 100644
--- a/src/com/android/launcher3/util/SettingsCache.java
+++ b/src/com/android/launcher3/util/SettingsCache.java
@@ -18,6 +18,8 @@
import static android.provider.Settings.System.ACCELEROMETER_ROTATION;
+import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR;
+
import android.content.ContentResolver;
import android.content.Context;
import android.database.ContentObserver;
@@ -87,7 +89,7 @@
@Override
public void close() {
- mResolver.unregisterContentObserver(this);
+ UI_HELPER_EXECUTOR.execute(() -> mResolver.unregisterContentObserver(this));
}
@Override
@@ -135,7 +137,8 @@
CopyOnWriteArrayList<OnChangeListener> l = new CopyOnWriteArrayList<>();
l.add(changeListener);
mListenerMap.put(uri, l);
- mResolver.registerContentObserver(uri, false, this);
+ UI_HELPER_EXECUTOR.execute(
+ () -> mResolver.registerContentObserver(uri, false, this));
}
}
diff --git a/src/com/android/launcher3/util/SimpleBroadcastReceiver.java b/src/com/android/launcher3/util/SimpleBroadcastReceiver.java
index 064bcd0..5f39cce 100644
--- a/src/com/android/launcher3/util/SimpleBroadcastReceiver.java
+++ b/src/com/android/launcher3/util/SimpleBroadcastReceiver.java
@@ -15,14 +15,21 @@
*/
package com.android.launcher3.util;
+import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR;
+
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
+import android.os.Looper;
import android.os.PatternMatcher;
import android.text.TextUtils;
import androidx.annotation.Nullable;
+import androidx.annotation.UiThread;
+import androidx.annotation.WorkerThread;
+
+import com.android.launcher3.BuildConfig;
import java.util.function.Consumer;
@@ -39,21 +46,63 @@
mIntentConsumer.accept(intent);
}
- /**
- * Helper method to register multiple actions
- */
- public void register(Context context, String... actions) {
+ /** Helper method to register multiple actions. Caller should be on main thread. */
+ @UiThread
+ public void registerAsync(Context context, String... actions) {
+ assertOnMainThread();
+ UI_HELPER_EXECUTOR.execute(() -> registerSync(context, actions));
+ }
+
+ /** Helper method to register multiple actions. Caller should be on main thread. */
+ @WorkerThread
+ public void registerSync(Context context, String... actions) {
+ assertOnBgThread();
context.registerReceiver(this, getFilter(actions));
}
/**
- * Helper method to register multiple actions associated with a paction
+ * Helper method to register multiple actions associated with a action. Caller should be from
+ * main thread.
*/
- public void registerPkgActions(Context context, @Nullable String pkg, String... actions) {
+ @UiThread
+ public void registerPkgActionsAsync(Context context, @Nullable String pkg, String... actions) {
+ assertOnMainThread();
+ UI_HELPER_EXECUTOR.execute(() -> registerPkgActionsSync(context, pkg, actions));
+ }
+
+ /**
+ * Helper method to register multiple actions associated with a action. Caller should be from
+ * bg thread.
+ */
+ @WorkerThread
+ public void registerPkgActionsSync(Context context, @Nullable String pkg, String... actions) {
+ assertOnBgThread();
context.registerReceiver(this, getPackageFilter(pkg, actions));
}
/**
+ * Unregisters the receiver ignoring any errors on bg thread. Caller should be on main thread.
+ */
+ @UiThread
+ public void unregisterReceiverSafelyAsync(Context context) {
+ assertOnMainThread();
+ UI_HELPER_EXECUTOR.execute(() -> unregisterReceiverSafelySync(context));
+ }
+
+ /**
+ * Unregisters the receiver ignoring any errors on bg thread. Caller should be on bg thread.
+ */
+ @WorkerThread
+ public void unregisterReceiverSafelySync(Context context) {
+ assertOnBgThread();
+ try {
+ context.unregisterReceiver(this);
+ } catch (IllegalArgumentException e) {
+ // It was probably never registered or already unregistered. Ignore.
+ }
+ }
+
+ /**
* Creates an intent filter to listen for actions with a specific package in the data field.
*/
public static IntentFilter getPackageFilter(String pkg, String... actions) {
@@ -73,14 +122,19 @@
return filter;
}
- /**
- * Unregisters the receiver ignoring any errors
- */
- public void unregisterReceiverSafely(Context context) {
- try {
- context.unregisterReceiver(this);
- } catch (IllegalArgumentException e) {
- // It was probably never registered or already unregistered. Ignore.
+ private static void assertOnBgThread() {
+ if (BuildConfig.IS_STUDIO_BUILD && isMainThread()) {
+ throw new IllegalStateException("Should not be called from main thread!");
}
}
+
+ private static void assertOnMainThread() {
+ if (BuildConfig.IS_STUDIO_BUILD && !isMainThread()) {
+ throw new IllegalStateException("Should not be called from bg thread!");
+ }
+ }
+
+ private static boolean isMainThread() {
+ return Thread.currentThread() == Looper.getMainLooper().getThread();
+ }
}
diff --git a/src/com/android/launcher3/util/WallpaperOffsetInterpolator.java b/src/com/android/launcher3/util/WallpaperOffsetInterpolator.java
index b97b889..a2277a0 100644
--- a/src/com/android/launcher3/util/WallpaperOffsetInterpolator.java
+++ b/src/com/android/launcher3/util/WallpaperOffsetInterpolator.java
@@ -198,10 +198,11 @@
public void setWindowToken(IBinder token) {
mWindowToken = token;
if (mWindowToken == null && mRegistered) {
- mWallpaperChangeReceiver.unregisterReceiverSafely(mWorkspace.getContext());
+ mWallpaperChangeReceiver.unregisterReceiverSafelyAsync(mWorkspace.getContext());
mRegistered = false;
} else if (mWindowToken != null && !mRegistered) {
- mWallpaperChangeReceiver.register(mWorkspace.getContext(), ACTION_WALLPAPER_CHANGED);
+ mWallpaperChangeReceiver.registerAsync(
+ mWorkspace.getContext(), ACTION_WALLPAPER_CHANGED);
onWallpaperChanged();
mRegistered = true;
}
diff --git a/src/com/android/launcher3/widget/LauncherWidgetHolder.java b/src/com/android/launcher3/widget/LauncherWidgetHolder.java
index a5e22c5..1fb8c83 100644
--- a/src/com/android/launcher3/widget/LauncherWidgetHolder.java
+++ b/src/com/android/launcher3/widget/LauncherWidgetHolder.java
@@ -20,6 +20,7 @@
import static com.android.launcher3.BuildConfig.WIDGETS_ENABLED;
import static com.android.launcher3.Flags.enableWorkspaceInflation;
import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
+import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR;
import static com.android.launcher3.widget.LauncherAppWidgetProviderInfo.fromProviderInfo;
import android.appwidget.AppWidgetHost;
@@ -36,6 +37,7 @@
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
+import androidx.annotation.WorkerThread;
import com.android.launcher3.BaseActivity;
import com.android.launcher3.BaseDraggingActivity;
@@ -44,6 +46,7 @@
import com.android.launcher3.model.data.ItemInfo;
import com.android.launcher3.testing.TestLogging;
import com.android.launcher3.testing.shared.TestProtocol;
+import com.android.launcher3.util.LooperExecutor;
import com.android.launcher3.util.ResourceBasedOverride;
import com.android.launcher3.util.SafeCloseable;
import com.android.launcher3.widget.LauncherAppWidgetHost.ListenableHostView;
@@ -51,6 +54,7 @@
import java.util.ArrayList;
import java.util.List;
+import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.IntConsumer;
/**
@@ -77,7 +81,7 @@
protected final SparseArray<LauncherAppWidgetHostView> mViews = new SparseArray<>();
protected final List<ProviderChangedListener> mProviderChangedListeners = new ArrayList<>();
- protected int mFlags = FLAG_STATE_IS_NORMAL;
+ protected AtomicInteger mFlags = new AtomicInteger(FLAG_STATE_IS_NORMAL);
// TODO(b/191735836): Replace with ActivityOptions.KEY_SPLASH_SCREEN_STYLE when un-hidden
private static final String KEY_SPLASH_SCREEN_STYLE = "android.activity.splashScreenStyle";
@@ -96,6 +100,10 @@
context, appWidgetRemovedCallback, mProviderChangedListeners);
}
+ protected LooperExecutor getWidgetHolderExecutor() {
+ return UI_HELPER_EXECUTOR;
+ }
+
/**
* Starts listening to the widget updates from the server side
*/
@@ -104,21 +112,23 @@
return;
}
- try {
- mWidgetHost.startListening();
- } catch (Exception e) {
- if (!Utilities.isBinderSizeError(e)) {
- throw new RuntimeException(e);
+ getWidgetHolderExecutor().execute(() -> {
+ try {
+ mWidgetHost.startListening();
+ } catch (Exception e) {
+ if (!Utilities.isBinderSizeError(e)) {
+ throw new RuntimeException(e);
+ }
+ // We're willing to let this slide. The exception is being caused by the list of
+ // RemoteViews which is being passed back. The startListening relationship will
+ // have been established by this point, and we will end up populating the
+ // widgets upon bind anyway. See issue 14255011 for more context.
}
- // We're willing to let this slide. The exception is being caused by the list of
- // RemoteViews which is being passed back. The startListening relationship will
- // have been established by this point, and we will end up populating the
- // widgets upon bind anyway. See issue 14255011 for more context.
- }
- // TODO: Investigate why widgetHost.startListening() always return non-empty updates
- setListeningFlag(true);
+ // TODO: Investigate why widgetHost.startListening() always return non-empty updates
+ setListeningFlag(true);
- updateDeferredView();
+ MAIN_EXECUTOR.execute(() -> updateDeferredView());
+ });
}
/**
@@ -282,16 +292,23 @@
if (!WIDGETS_ENABLED) {
return;
}
- mWidgetHost.stopListening();
- setListeningFlag(false);
+ getWidgetHolderExecutor().execute(() -> {
+ mWidgetHost.stopListening();
+ setListeningFlag(false);
+ });
}
+ /**
+ * Update {@link FLAG_LISTENING} on {@link mFlags} after making binder calls from
+ * {@link sWidgetHost}.
+ */
+ @WorkerThread
protected void setListeningFlag(final boolean isListening) {
if (isListening) {
- mFlags |= FLAG_LISTENING;
+ mFlags.updateAndGet(old -> old | FLAG_LISTENING);
return;
}
- mFlags &= ~FLAG_LISTENING;
+ mFlags.updateAndGet(old -> old & ~FLAG_LISTENING);
}
/**
@@ -373,7 +390,7 @@
* as a result of using the same flow.
*/
protected LauncherAppWidgetHostView recycleExistingView(LauncherAppWidgetHostView view) {
- if ((mFlags & FLAG_LISTENING) == 0) {
+ if ((mFlags.get() & FLAG_LISTENING) == 0) {
if (view instanceof PendingAppWidgetHostView pv && pv.isDeferredWidget()) {
return view;
} else {
@@ -395,7 +412,7 @@
@NonNull
protected LauncherAppWidgetHostView createViewInternal(
int appWidgetId, @NonNull LauncherAppWidgetProviderInfo appWidget) {
- if ((mFlags & FLAG_LISTENING) == 0) {
+ if ((mFlags.get() & FLAG_LISTENING) == 0) {
// Since the launcher hasn't started listening to widget updates, we can't simply call
// host.createView here because the later will make a binder call to retrieve
// RemoteViews from system process.
@@ -460,7 +477,7 @@
* @return True if the host is listening to the updates, false otherwise
*/
public boolean isListening() {
- return (mFlags & FLAG_LISTENING) != 0;
+ return (mFlags.get() & FLAG_LISTENING) != 0;
}
/**
@@ -469,16 +486,17 @@
*/
private void setShouldListenFlag(int flag, boolean on) {
if (on) {
- mFlags |= flag;
+ mFlags.updateAndGet(old -> old | flag);
} else {
- mFlags &= ~flag;
+ mFlags.updateAndGet(old -> old & ~flag);
}
final boolean listening = isListening();
- if (!listening && shouldListen(mFlags)) {
+ int currentFlag = mFlags.get();
+ if (!listening && shouldListen(currentFlag)) {
// Postpone starting listening until all flags are on.
startListening();
- } else if (listening && (mFlags & FLAG_ACTIVITY_STARTED) == 0) {
+ } else if (listening && (currentFlag & FLAG_ACTIVITY_STARTED) == 0) {
// Postpone stopping listening until the activity is stopped.
stopListening();
}
diff --git a/tests/src/com/android/launcher3/allapps/PrivateSpaceSettingsButtonTest.java b/tests/multivalentTests/src/com/android/launcher3/allapps/PrivateSpaceSettingsButtonTest.java
similarity index 96%
rename from tests/src/com/android/launcher3/allapps/PrivateSpaceSettingsButtonTest.java
rename to tests/multivalentTests/src/com/android/launcher3/allapps/PrivateSpaceSettingsButtonTest.java
index 9537e1c..1eb4173 100644
--- a/tests/src/com/android/launcher3/allapps/PrivateSpaceSettingsButtonTest.java
+++ b/tests/multivalentTests/src/com/android/launcher3/allapps/PrivateSpaceSettingsButtonTest.java
@@ -24,7 +24,7 @@
import android.content.Context;
-import androidx.test.runner.AndroidJUnit4;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.android.launcher3.model.data.AppInfo;
import com.android.launcher3.util.ActivityContextWrapper;
diff --git a/tests/src/com/android/launcher3/folder/FolderNameProviderTest.java b/tests/multivalentTests/src/com/android/launcher3/folder/FolderNameProviderTest.java
similarity index 100%
rename from tests/src/com/android/launcher3/folder/FolderNameProviderTest.java
rename to tests/multivalentTests/src/com/android/launcher3/folder/FolderNameProviderTest.java
diff --git a/tests/src/com/android/launcher3/model/DbDowngradeHelperTest.java b/tests/multivalentTests/src/com/android/launcher3/model/DbDowngradeHelperTest.java
similarity index 100%
rename from tests/src/com/android/launcher3/model/DbDowngradeHelperTest.java
rename to tests/multivalentTests/src/com/android/launcher3/model/DbDowngradeHelperTest.java
diff --git a/tests/src/com/android/launcher3/provider/RestoreDbTaskTest.java b/tests/multivalentTests/src/com/android/launcher3/provider/RestoreDbTaskTest.java
similarity index 100%
rename from tests/src/com/android/launcher3/provider/RestoreDbTaskTest.java
rename to tests/multivalentTests/src/com/android/launcher3/provider/RestoreDbTaskTest.java
diff --git a/tests/src/com/android/launcher3/dragging/TaplUninstallRemoveTest.java b/tests/src/com/android/launcher3/dragging/TaplUninstallRemoveTest.java
index 362596c..405dae7 100644
--- a/tests/src/com/android/launcher3/dragging/TaplUninstallRemoveTest.java
+++ b/tests/src/com/android/launcher3/dragging/TaplUninstallRemoveTest.java
@@ -159,13 +159,13 @@
mLauncher.getWorkspace().getWorkspaceIconsPositions();
assertThat(initialPositions.keySet()).containsAtLeastElementsIn(appNames);
- mLauncher.getWorkspace().getWorkspaceAppIcon(DUMMY_APP_NAME).uninstall();
- mLauncher.getWorkspace().verifyWorkspaceAppIconIsGone(
+ final Workspace workspace = mLauncher.getWorkspace().getWorkspaceAppIcon(
+ DUMMY_APP_NAME).uninstall();
+ workspace.verifyWorkspaceAppIconIsGone(
DUMMY_APP_NAME + " was expected to disappear after uninstall.", DUMMY_APP_NAME);
Log.d(UIOBJECT_STALE_ELEMENT, "second getWorkspaceIconsPositions()");
- Map<String, Point> finalPositions =
- mLauncher.getWorkspace().getWorkspaceIconsPositions();
+ Map<String, Point> finalPositions = workspace.getWorkspaceIconsPositions();
assertThat(finalPositions).doesNotContainKey(DUMMY_APP_NAME);
} finally {
TestUtil.uninstallDummyApp();
diff --git a/tests/src/com/android/launcher3/model/LoaderTaskTest.kt b/tests/src/com/android/launcher3/model/LoaderTaskTest.kt
index 28a001f..d16674c 100644
--- a/tests/src/com/android/launcher3/model/LoaderTaskTest.kt
+++ b/tests/src/com/android/launcher3/model/LoaderTaskTest.kt
@@ -1,10 +1,13 @@
package com.android.launcher3.model
import android.appwidget.AppWidgetManager
+import android.content.Intent
import android.os.UserHandle
import android.platform.test.flag.junit.SetFlagsRule
+import android.provider.Settings
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
+import com.android.dx.mockito.inline.extended.ExtendedMockito
import com.android.launcher3.Flags
import com.android.launcher3.InvariantDeviceProfile
import com.android.launcher3.LauncherAppState
@@ -14,6 +17,7 @@
import com.android.launcher3.icons.cache.CachingLogic
import com.android.launcher3.icons.cache.IconCacheUpdateHandler
import com.android.launcher3.pm.UserCache
+import com.android.launcher3.provider.RestoreDbTask
import com.android.launcher3.ui.TestViewHelpers
import com.android.launcher3.util.Executors.MODEL_EXECUTOR
import com.android.launcher3.util.LauncherModelHelper.SandboxModelContext
@@ -21,21 +25,30 @@
import com.android.launcher3.util.UserIconInfo
import com.google.common.truth.Truth
import java.util.concurrent.CountDownLatch
+import junit.framework.Assert.assertEquals
import org.junit.After
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
+import org.mockito.ArgumentCaptor
import org.mockito.ArgumentMatchers.any
import org.mockito.ArgumentMatchers.anyInt
+import org.mockito.ArgumentMatchers.anyList
+import org.mockito.ArgumentMatchers.anyMap
import org.mockito.Mock
import org.mockito.Mockito
import org.mockito.Mockito.times
-import org.mockito.Mockito.verify
import org.mockito.Mockito.`when`
import org.mockito.MockitoAnnotations
+import org.mockito.MockitoSession
import org.mockito.Spy
+import org.mockito.kotlin.anyOrNull
import org.mockito.kotlin.doReturn
+import org.mockito.kotlin.spy
+import org.mockito.kotlin.verify
+import org.mockito.kotlin.whenever
+import org.mockito.quality.Strictness
private const val INSERTION_STATEMENT_FILE = "databases/workspace_items.sql"
@@ -43,6 +56,20 @@
@RunWith(AndroidJUnit4::class)
class LoaderTaskTest {
private var context = SandboxModelContext()
+ private val expectedBroadcastModel =
+ FirstScreenBroadcastModel(
+ installerPackage = "installerPackage",
+ pendingCollectionItems = mutableSetOf("pendingCollectionItem"),
+ pendingWidgetItems = mutableSetOf("pendingWidgetItem"),
+ pendingHotseatItems = mutableSetOf("pendingHotseatItem"),
+ pendingWorkspaceItems = mutableSetOf("pendingWorkspaceItem"),
+ installedHotseatItems = mutableSetOf("installedHotseatItem"),
+ installedWorkspaceItems = mutableSetOf("installedWorkspaceItem"),
+ firstScreenInstalledWidgets = mutableSetOf("installedFirstScreenWidget"),
+ secondaryScreenInstalledWidgets = mutableSetOf("installedSecondaryScreenWidget")
+ )
+ private lateinit var mockitoSession: MockitoSession
+
@Mock private lateinit var app: LauncherAppState
@Mock private lateinit var bgAllAppsList: AllAppsList
@Mock private lateinit var modelDelegate: ModelDelegate
@@ -61,7 +88,11 @@
@Before
fun setup() {
MockitoAnnotations.initMocks(this)
-
+ mockitoSession =
+ ExtendedMockito.mockitoSession()
+ .strictness(Strictness.LENIENT)
+ .mockStatic(FirstScreenBroadcastHelper::class.java)
+ .startMocking()
val idp =
InvariantDeviceProfile().apply {
numRows = 5
@@ -90,6 +121,7 @@
@After
fun tearDown() {
context.onDestroy()
+ mockitoSession.finishMocking()
}
@Test
@@ -166,6 +198,141 @@
verify(bgAllAppsList, Mockito.never())
.setFlags(BgDataModel.Callbacks.FLAG_QUIET_MODE_ENABLED, true)
}
+
+ @Test
+ fun `When launcher_broadcast_installed_apps and is restore then send installed item broadcast`() {
+ // Given
+ val spyContext = spy(context)
+ `when`(app.context).thenReturn(spyContext)
+ whenever(
+ FirstScreenBroadcastHelper.createModelsForFirstScreenBroadcast(
+ anyOrNull(),
+ anyList(),
+ anyMap(),
+ anyList()
+ )
+ )
+ .thenReturn(listOf(expectedBroadcastModel))
+
+ whenever(
+ FirstScreenBroadcastHelper.sendBroadcastsForModels(
+ spyContext,
+ listOf(expectedBroadcastModel)
+ )
+ )
+ .thenCallRealMethod()
+
+ Settings.Secure.putInt(spyContext.contentResolver, "launcher_broadcast_installed_apps", 1)
+ RestoreDbTask.setPending(spyContext)
+
+ // When
+ LoaderTask(app, bgAllAppsList, BgDataModel(), modelDelegate, launcherBinder)
+ .runSyncOnBackgroundThread()
+
+ // Then
+ val argumentCaptor = ArgumentCaptor.forClass(Intent::class.java)
+ verify(spyContext).sendBroadcast(argumentCaptor.capture())
+ val actualBroadcastIntent = argumentCaptor.value
+ assertEquals(expectedBroadcastModel.installerPackage, actualBroadcastIntent.`package`)
+ assertEquals(
+ ArrayList(expectedBroadcastModel.installedWorkspaceItems),
+ actualBroadcastIntent.getStringArrayListExtra("workspaceInstalledItems")
+ )
+ assertEquals(
+ ArrayList(expectedBroadcastModel.installedHotseatItems),
+ actualBroadcastIntent.getStringArrayListExtra("hotseatInstalledItems")
+ )
+ assertEquals(
+ ArrayList(
+ expectedBroadcastModel.firstScreenInstalledWidgets +
+ expectedBroadcastModel.secondaryScreenInstalledWidgets
+ ),
+ actualBroadcastIntent.getStringArrayListExtra("widgetInstalledItems")
+ )
+ assertEquals(
+ ArrayList(expectedBroadcastModel.pendingCollectionItems),
+ actualBroadcastIntent.getStringArrayListExtra("folderItem")
+ )
+ assertEquals(
+ ArrayList(expectedBroadcastModel.pendingWorkspaceItems),
+ actualBroadcastIntent.getStringArrayListExtra("workspaceItem")
+ )
+ assertEquals(
+ ArrayList(expectedBroadcastModel.pendingHotseatItems),
+ actualBroadcastIntent.getStringArrayListExtra("hotseatItem")
+ )
+ assertEquals(
+ ArrayList(expectedBroadcastModel.pendingWidgetItems),
+ actualBroadcastIntent.getStringArrayListExtra("widgetItem")
+ )
+ }
+
+ @Test
+ fun `When not a restore then installed item broadcast not sent`() {
+ // Given
+ val spyContext = spy(context)
+ `when`(app.context).thenReturn(spyContext)
+ whenever(
+ FirstScreenBroadcastHelper.createModelsForFirstScreenBroadcast(
+ anyOrNull(),
+ anyList(),
+ anyMap(),
+ anyList()
+ )
+ )
+ .thenReturn(listOf(expectedBroadcastModel))
+
+ whenever(
+ FirstScreenBroadcastHelper.sendBroadcastsForModels(
+ spyContext,
+ listOf(expectedBroadcastModel)
+ )
+ )
+ .thenCallRealMethod()
+
+ Settings.Secure.putInt(spyContext.contentResolver, "launcher_broadcast_installed_apps", 1)
+
+ // When
+ LoaderTask(app, bgAllAppsList, BgDataModel(), modelDelegate, launcherBinder)
+ .runSyncOnBackgroundThread()
+
+ // Then
+ verify(spyContext, times(0)).sendBroadcast(any(Intent::class.java))
+ }
+
+ @Test
+ fun `When launcher_broadcast_installed_apps false then installed item broadcast not sent`() {
+ // Given
+ val spyContext = spy(context)
+ `when`(app.context).thenReturn(spyContext)
+ whenever(
+ FirstScreenBroadcastHelper.createModelsForFirstScreenBroadcast(
+ anyOrNull(),
+ anyList(),
+ anyMap(),
+ anyList()
+ )
+ )
+ .thenReturn(listOf(expectedBroadcastModel))
+
+ whenever(
+ FirstScreenBroadcastHelper.sendBroadcastsForModels(
+ spyContext,
+ listOf(expectedBroadcastModel)
+ )
+ )
+ .thenCallRealMethod()
+
+ Settings.Secure.putInt(spyContext.contentResolver, "launcher_broadcast_installed_apps", 0)
+ RestoreDbTask.setPending(spyContext)
+
+ // When
+ LoaderTask(app, bgAllAppsList, BgDataModel(), modelDelegate, launcherBinder)
+ .runSyncOnBackgroundThread()
+
+ // Then
+ verify(spyContext, times(0)).sendBroadcast(any(Intent::class.java))
+ }
}
private fun LoaderTask.runSyncOnBackgroundThread() {
diff --git a/tests/src/com/android/launcher3/ui/AbstractLauncherUiTest.java b/tests/src/com/android/launcher3/ui/AbstractLauncherUiTest.java
index a6f4441..6e01f9e 100644
--- a/tests/src/com/android/launcher3/ui/AbstractLauncherUiTest.java
+++ b/tests/src/com/android/launcher3/ui/AbstractLauncherUiTest.java
@@ -239,7 +239,8 @@
final CountDownLatch count = new CountDownLatch(2);
final SimpleBroadcastReceiver broadcastReceiver =
new SimpleBroadcastReceiver(i -> count.countDown());
- broadcastReceiver.registerPkgActions(mTargetContext, pkg,
+ // We OK to make binder calls on main thread in test.
+ broadcastReceiver.registerPkgActionsSync(mTargetContext, pkg,
Intent.ACTION_PACKAGE_RESTARTED, Intent.ACTION_PACKAGE_DATA_CLEARED);
mDevice.executeShellCommand("pm clear " + pkg);