Merge "Fix TaskMenuView to calculate bottom menu's max height" into main
diff --git a/quickstep/res/layout/keyboard_quick_switch_view.xml b/quickstep/res/layout/keyboard_quick_switch_view.xml
index 885bdb9..2dea79c 100644
--- a/quickstep/res/layout/keyboard_quick_switch_view.xml
+++ b/quickstep/res/layout/keyboard_quick_switch_view.xml
@@ -18,6 +18,8 @@
xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/keyboard_quick_switch_view"
+ android:contentDescription="@string/quick_switch_content_description"
+ android:accessibilityPaneTitle="@string/quick_switch_pane_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/keyboard_quick_switch_margin_top"
diff --git a/quickstep/res/values/strings.xml b/quickstep/res/values/strings.xml
index 65f4b3c..7578bd5 100644
--- a/quickstep/res/values/strings.xml
+++ b/quickstep/res/values/strings.xml
@@ -325,6 +325,12 @@
<!-- Label for creating an application bubble (from the Taskbar only). -->
<string name="open_app_as_a_bubble">Open app as a bubble</string>
+ <!-- Accessibility pane title for quick switch view, which lists apps opened by the user, ordered by how recently the app was opened. -->
+ <string name="quick_switch_pane_title">Recent apps</string>
+
+ <!-- Content description for the quick switch view, which lists apps opened by the user, ordered by how recently the app was opened. -->
+ <string name="quick_switch_content_description">Recent app list</string>
+
<!-- Label for quick switch tile showing how many more apps are available. The number will be displayed above this text. [CHAR LIMIT=NONE] -->
<string name="quick_switch_overflow">{count, plural,
=1{more app}
@@ -336,6 +342,8 @@
<!-- Accessibility label for quick switch tiles showing split tasks [CHAR LIMIT=NONE] -->
<string name="quick_switch_split_task"><xliff:g id="app_name_1" example="Chrome">%1$s</xliff:g> and <xliff:g id="app_name_2" example="Gmail">%2$s</xliff:g></string>
+ <!-- Accessibility label for quick switch tiles that include information about the tile's position in the parent list [CHAR LIMIT=NONE] -->
+ <string name="quick_switch_task_with_position_in_parent"><xliff:g id="task_description" example="Chrome">%1$s</xliff:g>, item <xliff:g id="index_in_parent" example="1">%2$d</xliff:g> of <xliff:g id="total_tasks" example="5">%3$d</xliff:g></string>
<!-- Accessibility label for an arrow button within quick switch UI that scrolls the quick switch content left
TODO(b/397975686): Make these translatable when verified by UX. -->
diff --git a/quickstep/src/com/android/launcher3/QuickstepTransitionManager.java b/quickstep/src/com/android/launcher3/QuickstepTransitionManager.java
index 7cf0605..aae8a56 100644
--- a/quickstep/src/com/android/launcher3/QuickstepTransitionManager.java
+++ b/quickstep/src/com/android/launcher3/QuickstepTransitionManager.java
@@ -352,7 +352,7 @@
new RemoteAnimationAdapter(runner, duration, statusBarTransitionDelay),
new RemoteTransition(runner.toRemoteTransition(),
mLauncher.getIApplicationThread(), "QuickstepLaunch"));
- IRemoteCallback endCallback = completeRunnableListCallback(onEndCallback);
+ IRemoteCallback endCallback = completeRunnableListCallback(onEndCallback, mLauncher);
options.setOnAnimationAbortListener(endCallback);
options.setOnAnimationFinishedListener(endCallback);
options.setLaunchCookie(StableViewInfo.toLaunchCookie(itemInfo));
diff --git a/quickstep/src/com/android/launcher3/desktop/DesktopRecentsTransitionController.kt b/quickstep/src/com/android/launcher3/desktop/DesktopRecentsTransitionController.kt
index 40cfe92..a01846d 100644
--- a/quickstep/src/com/android/launcher3/desktop/DesktopRecentsTransitionController.kt
+++ b/quickstep/src/com/android/launcher3/desktop/DesktopRecentsTransitionController.kt
@@ -30,6 +30,7 @@
import com.android.launcher3.util.Executors.MAIN_EXECUTOR
import com.android.quickstep.SystemUiProxy
import com.android.quickstep.TaskViewUtils
+import com.android.quickstep.util.DesksUtils.Companion.areMultiDesksFlagsEnabled
import com.android.quickstep.views.DesktopTaskView
import com.android.quickstep.views.TaskContainer
import com.android.quickstep.views.TaskView
@@ -60,7 +61,11 @@
callback,
)
val transition = RemoteTransition(animRunner, appThread, "RecentsToDesktop")
- systemUiProxy.showDesktopApps(desktopTaskView.displayId, transition)
+ if (areMultiDesksFlagsEnabled()) {
+ systemUiProxy.activateDesk(desktopTaskView.deskId, transition)
+ } else {
+ systemUiProxy.showDesktopApps(desktopTaskView.displayId, transition)
+ }
}
/** Launch desktop tasks from recents view */
diff --git a/quickstep/src/com/android/launcher3/statehandlers/DesktopVisibilityController.kt b/quickstep/src/com/android/launcher3/statehandlers/DesktopVisibilityController.kt
index eb24df1..2402a28 100644
--- a/quickstep/src/com/android/launcher3/statehandlers/DesktopVisibilityController.kt
+++ b/quickstep/src/com/android/launcher3/statehandlers/DesktopVisibilityController.kt
@@ -401,6 +401,39 @@
DisplayController.INSTANCE.get(context).notifyConfigChange()
}
+ private fun notifyOnDeskAdded(displayId: Int, deskId: Int) {
+ if (DEBUG) {
+ Log.d(TAG, "notifyOnDeskAdded: displayId=$displayId, deskId=$deskId")
+ }
+
+ for (listener in desktopVisibilityListeners) {
+ listener.onDeskAdded(displayId, deskId)
+ }
+ }
+
+ private fun notifyOnDeskRemoved(displayId: Int, deskId: Int) {
+ if (DEBUG) {
+ Log.d(TAG, "notifyOnDeskRemoved: displayId=$displayId, deskId=$deskId")
+ }
+
+ for (listener in desktopVisibilityListeners) {
+ listener.onDeskRemoved(displayId, deskId)
+ }
+ }
+
+ private fun notifyOnActiveDeskChanged(displayId: Int, newActiveDesk: Int, oldActiveDesk: Int) {
+ if (DEBUG) {
+ Log.d(
+ TAG,
+ "notifyOnActiveDeskChanged: displayId=$displayId, newActiveDesk=$newActiveDesk, oldActiveDesk=$oldActiveDesk",
+ )
+ }
+
+ for (listener in desktopVisibilityListeners) {
+ listener.onActiveDeskChanged(displayId, newActiveDesk, oldActiveDesk)
+ }
+ }
+
/** TODO: b/333533253 - Remove after flag rollout */
private fun setBackgroundStateEnabled(backgroundStateEnabled: Boolean) {
if (DEBUG) {
@@ -511,6 +544,8 @@
"Found a duplicate desk Id: $deskId on display: $displayId"
}
}
+
+ notifyOnDeskAdded(displayId, deskId)
}
private fun onDeskRemoved(displayId: Int, deskId: Int) {
@@ -526,6 +561,8 @@
it.activeDeskId = INACTIVE_DESK_ID
}
}
+
+ notifyOnDeskRemoved(displayId, deskId)
}
private fun onActiveDeskChanged(displayId: Int, newActiveDesk: Int, oldActiveDesk: Int) {
@@ -539,12 +576,16 @@
check(oldActiveDesk == it.activeDeskId) {
"Mismatch between the Shell's oldActiveDesk: $oldActiveDesk, and Launcher's: ${it.activeDeskId}"
}
- check(it.deskIds.contains(newActiveDesk)) {
+ check(newActiveDesk == INACTIVE_DESK_ID || it.deskIds.contains(newActiveDesk)) {
"newActiveDesk: $newActiveDesk was never added to display: $displayId"
}
it.activeDeskId = newActiveDesk
}
+ if (newActiveDesk != oldActiveDesk) {
+ notifyOnActiveDeskChanged(displayId, newActiveDesk, oldActiveDesk)
+ }
+
if (wasInDesktopMode != isInDesktopModeAndNotInOverview(displayId)) {
notifyIsInDesktopModeChanged(displayId, !wasInDesktopMode)
}
@@ -718,6 +759,6 @@
private const val TAG = "DesktopVisController"
private const val DEBUG = false
- public const val INACTIVE_DESK_ID = -1
+ const val INACTIVE_DESK_ID = -1
}
}
diff --git a/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchController.java b/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchController.java
index cc94824..1698050 100644
--- a/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchController.java
@@ -170,7 +170,7 @@
mHasDesktopTask,
mWasDesktopTaskFilteredOut);
}, shouldShowDesktopTasks ? RecentsFilterState.EMPTY_FILTER
- : RecentsFilterState.getEmptyDesktopTaskFilter());
+ : RecentsFilterState.getDesktopTaskFilter());
}
mQuickSwitchViewController.updateLayoutForSurface(wasOpenedFromTaskbar,
@@ -232,7 +232,7 @@
mWasDesktopTaskFilteredOut,
wasOpenedFromTaskbar);
}, shouldShowDesktopTasks ? RecentsFilterState.EMPTY_FILTER
- : RecentsFilterState.getEmptyDesktopTaskFilter());
+ : RecentsFilterState.getDesktopTaskFilter());
}
private boolean shouldExcludeTask(GroupTask task, Set<Integer> taskIdsToExclude) {
diff --git a/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchTaskView.java b/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchTaskView.java
index 5f867cd..15be03a 100644
--- a/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchTaskView.java
+++ b/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchTaskView.java
@@ -66,6 +66,11 @@
@Nullable private ImageView mIcon2;
@Nullable private View mContent;
+ // Describe the task position in the parent container. Used to add information about the task's
+ // position in a task list to the task view's content description.
+ private int mIndexInParent = -1;
+ private int mTotalTasksInParent = -1;
+
public KeyboardQuickSwitchTaskView(@NonNull Context context) {
this(context, null);
}
@@ -155,36 +160,51 @@
applyThumbnail(mThumbnailView1, task1, thumbnailUpdateFunction);
applyThumbnail(mThumbnailView2, task2, thumbnailUpdateFunction);
+ // Update content description, even in cases task icons, and content descriptions need to be
+ // loaded asynchronously to ensure that the task has non empty description (assuming task
+ // position information was set), as KeyboardQuickSwitch view may request accessibility
+ // focus to be moved to the task when the quick switch UI gets shown. The description will
+ // be updated once the task metadata has been loaded - the delay should be very short, and
+ // the content description when task titles are not available still gives some useful
+ // information to the user (the task's position in the list).
+ updateContentDesctiptionForTasks(task1, task2);
+
if (iconUpdateFunction == null) {
applyIcon(mIcon1, task1);
applyIcon(mIcon2, task2);
- setContentDescription(task2 == null
- ? task1.titleDescription
- : getContext().getString(
- R.string.quick_switch_split_task,
- task1.titleDescription,
- task2.titleDescription));
return;
}
+
iconUpdateFunction.updateIconInBackground(task1, t -> {
applyIcon(mIcon1, task1);
if (task2 != null) {
return;
}
- setContentDescription(task1.titleDescription);
+ updateContentDesctiptionForTasks(task1, null);
});
+
if (task2 == null) {
return;
}
iconUpdateFunction.updateIconInBackground(task2, t -> {
applyIcon(mIcon2, task2);
- setContentDescription(getContext().getString(
- R.string.quick_switch_split_task,
- task1.titleDescription,
- task2.titleDescription));
+ updateContentDesctiptionForTasks(task1, task2);
});
}
+ /**
+ * Initializes information about the task's position within the parent container context - used
+ * to add position information to the view's content description.
+ * Should be called before associating the view with tasks.
+ *
+ * @param index The view's 0-based index within the parent task container.
+ * @param totalTasks The total number of tasks in the parent task container.
+ */
+ protected void setPositionInformation(int index, int totalTasks) {
+ mIndexInParent = index;
+ mTotalTasksInParent = totalTasks;
+ }
+
protected void setThumbnailsForSplitTasks(
@NonNull Task task1,
@Nullable Task task2,
@@ -283,6 +303,28 @@
constantState.newDrawable(getResources(), getContext().getTheme()));
}
+ /**
+ * Updates the task view's content description to reflect tasks represented by the view.
+ */
+ private void updateContentDesctiptionForTasks(@NonNull Task task1, @Nullable Task task2) {
+ String tasksDescription = task1.titleDescription == null || task2 == null
+ ? task1.titleDescription
+ : getContext().getString(
+ R.string.quick_switch_split_task,
+ task1.titleDescription,
+ task2.titleDescription);
+ if (mIndexInParent < 0) {
+ setContentDescription(tasksDescription);
+ return;
+ }
+
+ setContentDescription(
+ getContext().getString(R.string.quick_switch_task_with_position_in_parent,
+ tasksDescription != null ? tasksDescription : "",
+ mIndexInParent + 1,
+ mTotalTasksInParent));
+ }
+
protected interface ThumbnailUpdateFunction {
void updateThumbnailInBackground(Task task, Consumer<ThumbnailData> callback);
diff --git a/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchView.java b/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchView.java
index 845e19f..ab147bb 100644
--- a/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchView.java
+++ b/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchView.java
@@ -301,6 +301,7 @@
continue;
}
+ currentTaskView.setPositionInformation(i, tasksToDisplay);
currentTaskView.setThumbnailsForSplitTasks(
task1,
task2,
@@ -548,6 +549,9 @@
ViewOutlineProvider outlineProvider = getOutlineProvider();
+ int defaultFocusedTaskIndex = Math.min(
+ getTaskCount() - 1,
+ currentFocusIndexOverride == -1 ? 1 : currentFocusIndexOverride);
mOpenAnimation.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationStart(Animator animation) {
@@ -601,9 +605,7 @@
});
}
- animateFocusMove(-1, Math.min(
- getTaskCount() - 1,
- currentFocusIndexOverride == -1 ? 1 : currentFocusIndexOverride));
+ animateFocusMove(-1, defaultFocusedTaskIndex);
displayedContent.setVisibility(VISIBLE);
setVisibility(VISIBLE);
requestFocus();
@@ -623,6 +625,11 @@
invalidateOutline();
mOpenAnimation = null;
InteractionJankMonitorWrapper.end(Cuj.CUJ_LAUNCHER_KEYBOARD_QUICK_SWITCH_OPEN);
+
+ View focusedTask = getTaskAt(defaultFocusedTaskIndex);
+ if (focusedTask != null) {
+ focusedTask.requestAccessibilityFocus();
+ }
}
});
diff --git a/quickstep/src/com/android/launcher3/taskbar/LauncherTaskbarUIController.java b/quickstep/src/com/android/launcher3/taskbar/LauncherTaskbarUIController.java
index 2272d11..62f546b 100644
--- a/quickstep/src/com/android/launcher3/taskbar/LauncherTaskbarUIController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/LauncherTaskbarUIController.java
@@ -135,11 +135,11 @@
@Override
protected void onDestroy() {
onLauncherVisibilityChanged(false /* isVisible */, true /* fromInitOrDestroy */);
+ mLauncher.removeOnDeviceProfileChangeListener(mOnDeviceProfileChangeListener);
super.onDestroy();
mTaskbarLauncherStateController.onDestroy();
mLauncher.setTaskbarUIController(null);
- mLauncher.removeOnDeviceProfileChangeListener(mOnDeviceProfileChangeListener);
mHomeState.removeListener(mVisibilityChangeListener);
}
@@ -279,7 +279,10 @@
private void postAdjustHotseatForBubbleBar() {
Hotseat hotseat = mLauncher.getHotseat();
if (hotseat == null || !isBubbleBarVisible()) return;
- hotseat.post(() -> adjustHotseatForBubbleBar(isBubbleBarVisible()));
+ hotseat.post(() -> {
+ if (mControllers == null) return;
+ adjustHotseatForBubbleBar(isBubbleBarVisible());
+ });
}
private boolean isBubbleBarVisible() {
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java
index 39f80c2..6afbebf 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java
@@ -926,7 +926,7 @@
options.setSplashScreenStyle(splashScreenStyle);
options.setPendingIntentBackgroundActivityStartMode(
ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED);
- IRemoteCallback endCallback = completeRunnableListCallback(callbacks);
+ IRemoteCallback endCallback = completeRunnableListCallback(callbacks, this);
options.setOnAnimationAbortListener(endCallback);
options.setOnAnimationFinishedListener(endCallback);
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarManager.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarManager.java
index cc340ce..bee0997 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarManager.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarManager.java
@@ -15,8 +15,8 @@
*/
package com.android.launcher3.taskbar;
-import static android.content.Context.RECEIVER_NOT_EXPORTED;
import static android.content.Context.RECEIVER_EXPORTED;
+import static android.content.Context.RECEIVER_NOT_EXPORTED;
import static android.view.WindowManager.LayoutParams.TYPE_NAVIGATION_BAR;
import static android.view.WindowManager.LayoutParams.TYPE_NAVIGATION_BAR_PANEL;
@@ -25,6 +25,7 @@
import static com.android.launcher3.Flags.enableUnfoldStateAnimation;
import static com.android.launcher3.config.FeatureFlags.ENABLE_TASKBAR_NAVBAR_UNIFICATION;
import static com.android.launcher3.config.FeatureFlags.enableTaskbarNoRecreate;
+import static com.android.launcher3.taskbar.growth.GrowthConstants.BROADCAST_SHOW_NUDGE;
import static com.android.launcher3.util.DisplayController.CHANGE_DENSITY;
import static com.android.launcher3.util.DisplayController.CHANGE_DESKTOP_MODE;
import static com.android.launcher3.util.DisplayController.CHANGE_NAVIGATION_MODE;
@@ -32,7 +33,6 @@
import static com.android.launcher3.util.DisplayController.CHANGE_TASKBAR_PINNING;
import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR;
import static com.android.launcher3.util.FlagDebugUtils.formatFlagChange;
-import static com.android.launcher3.taskbar.growth.GrowthConstants.BROADCAST_SHOW_NUDGE;
import static com.android.quickstep.util.SystemActionConstants.ACTION_SHOW_TASKBAR;
import static com.android.quickstep.util.SystemActionConstants.SYSTEM_ACTION_ID_TASKBAR;
@@ -81,6 +81,7 @@
import com.android.quickstep.RecentsActivity;
import com.android.quickstep.SystemUiProxy;
import com.android.quickstep.fallback.window.RecentsDisplayModel;
+import com.android.quickstep.fallback.window.RecentsWindowFlags;
import com.android.quickstep.fallback.window.RecentsWindowManager;
import com.android.quickstep.util.ContextualSearchInvoker;
import com.android.quickstep.util.GroupTask;
@@ -628,7 +629,7 @@
/** Creates a {@link TaskbarUIController} to use with non default displays. */
private TaskbarUIController createTaskbarUIControllerForNonDefaultDisplay(int displayId) {
debugPrimaryTaskbar("createTaskbarUIControllerForNonDefaultDisplay");
- if (RecentsDisplayModel.enableOverviewInWindow()) {
+ if (RecentsWindowFlags.Companion.getEnableOverviewInWindow()) {
RecentsViewContainer rvc = mRecentsDisplayModel.getRecentsWindowManager(displayId);
if (rvc != null) {
return createTaskbarUIControllerForRecentsViewContainer(rvc);
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarView.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarView.java
index 07b77c9..93662cd 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarView.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarView.java
@@ -200,10 +200,7 @@
// TODO: Disable touch events on QSB otherwise it can crash.
mQsb = LayoutInflater.from(context).inflate(R.layout.search_container_hotseat, this, false);
- mNumStaticViews =
- ENABLE_TASKBAR_RECENTS_LAYOUT_TRANSITION.isTrue() && !mActivityContext.isPhoneMode()
- ? addStaticViews()
- : 0;
+ mNumStaticViews = ENABLE_TASKBAR_RECENTS_LAYOUT_TRANSITION.isTrue() ? addStaticViews() : 0;
}
/**
diff --git a/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java b/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java
index 1a42d21..cd0a4f3 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java
@@ -1274,7 +1274,7 @@
options.setPendingIntentBackgroundActivityStartMode(
ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED);
- IRemoteCallback endCallback = completeRunnableListCallback(callbacks);
+ IRemoteCallback endCallback = completeRunnableListCallback(callbacks, this);
options.setOnAnimationAbortListener(endCallback);
options.setOnAnimationFinishedListener(endCallback);
return new ActivityOptionsWrapper(options, callbacks);
diff --git a/quickstep/src/com/android/quickstep/GestureState.java b/quickstep/src/com/android/quickstep/GestureState.java
index 74aa8e2..c4ba2d5 100644
--- a/quickstep/src/com/android/quickstep/GestureState.java
+++ b/quickstep/src/com/android/quickstep/GestureState.java
@@ -38,10 +38,10 @@
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
-import com.android.launcher3.Flags;
import com.android.launcher3.statemanager.BaseState;
import com.android.launcher3.statemanager.StatefulContainer;
import com.android.quickstep.TopTaskTracker.CachedTaskInfo;
+import com.android.quickstep.fallback.window.RecentsWindowFlags;
import com.android.quickstep.util.ActiveGestureErrorDetector;
import com.android.quickstep.util.ActiveGestureLog;
import com.android.quickstep.util.ActiveGestureProtoLogProxy;
@@ -323,8 +323,7 @@
*/
public boolean useSyntheticRecentsTransition() {
return mRunningTask.isHomeTask()
- && (Flags.enableFallbackOverviewInWindow()
- || Flags.enableLauncherOverviewInWindow());
+ && RecentsWindowFlags.Companion.getEnableOverviewInWindow();
}
/**
diff --git a/quickstep/src/com/android/quickstep/LauncherBackAnimationController.java b/quickstep/src/com/android/quickstep/LauncherBackAnimationController.java
index 783ec2c..2ff42cd 100644
--- a/quickstep/src/com/android/quickstep/LauncherBackAnimationController.java
+++ b/quickstep/src/com/android/quickstep/LauncherBackAnimationController.java
@@ -220,7 +220,7 @@
mHandler.post(() -> {
LauncherBackAnimationController controller = mControllerRef.get();
if (controller != null) {
- controller.startBack(backEvent);
+ controller.initBackMotion(backEvent);
mProgressAnimator.onBackStarted(backEvent, event -> {
float backProgress = event.getProgress();
controller.mBackProgress =
@@ -270,6 +270,7 @@
}
}
controller.mAnimationFinishedCallback = finishedCallback;
+ controller.startBack();
}
@Override
@@ -294,34 +295,32 @@
mBackCallback = null;
}
- private void startBack(BackMotionEvent backEvent) {
+ private void initBackMotion(BackMotionEvent backEvent) {
// in case we're still animating an onBackCancelled event, let's remove the finish-
// callback from the progress animator to prevent calling finishAnimation() before
// restarting a new animation
- // Side note: startBack is never called during the post-commit phase if the back gesture
- // was committed (not cancelled). BackAnimationController prevents that. Therefore we
- // don't have to handle that case.
+ // Side note: initBackMotion is never called during the post-commit phase if the back
+ // gesture was committed (not cancelled). BackAnimationController prevents that. Therefore
+ // we don't have to handle that case.
mProgressAnimator.removeOnBackCancelledFinishCallback();
mBackInProgress = true;
- RemoteAnimationTarget appTarget = backEvent.getDepartingAnimationTarget();
-
- if (appTarget == null || appTarget.leash == null || !appTarget.leash.isValid()) {
+ mInitialTouchPos.set(backEvent.getTouchX(), backEvent.getTouchY());
+ }
+ private void startBack() {
+ if (mBackTarget == null) {
return;
}
mTransaction
- .show(appTarget.leash)
+ .show(mBackTarget.leash)
.setAnimationTransaction();
- mBackTarget = appTarget;
- mInitialTouchPos.set(backEvent.getTouchX(), backEvent.getTouchY());
-
- mStartRect.set(appTarget.windowConfiguration.getMaxBounds());
+ mStartRect.set(mBackTarget.windowConfiguration.getMaxBounds());
// inset bottom in case of taskbar being present
if (!predictiveBackThreeButtonNav() || mLauncher.getDeviceProfile().isTaskbarPresent
|| DisplayController.getNavigationMode(mLauncher) == NavigationMode.NO_BUTTON) {
- mStartRect.inset(0, 0, 0, appTarget.contentInsets.bottom);
+ mStartRect.inset(0, 0, 0, mBackTarget.contentInsets.bottom);
}
mLauncherTargetView = mQuickstepTransitionManager.findLauncherView(
diff --git a/quickstep/src/com/android/quickstep/OverviewCommandHelper.kt b/quickstep/src/com/android/quickstep/OverviewCommandHelper.kt
index 984f390..bf87291 100644
--- a/quickstep/src/com/android/quickstep/OverviewCommandHelper.kt
+++ b/quickstep/src/com/android/quickstep/OverviewCommandHelper.kt
@@ -30,9 +30,7 @@
import androidx.annotation.VisibleForTesting
import com.android.internal.jank.Cuj
import com.android.launcher3.Flags.enableAltTabKqsOnConnectedDisplays
-import com.android.launcher3.Flags.enableFallbackOverviewInWindow
import com.android.launcher3.Flags.enableLargeDesktopWindowingTile
-import com.android.launcher3.Flags.enableLauncherOverviewInWindow
import com.android.launcher3.Flags.enableOverviewCommandHelperTimeout
import com.android.launcher3.PagedView
import com.android.launcher3.logger.LauncherAtom
@@ -53,6 +51,7 @@
import com.android.quickstep.OverviewCommandHelper.CommandType.SHOW
import com.android.quickstep.OverviewCommandHelper.CommandType.TOGGLE
import com.android.quickstep.fallback.window.RecentsDisplayModel
+import com.android.quickstep.fallback.window.RecentsWindowFlags.Companion.enableOverviewInWindow
import com.android.quickstep.util.ActiveGestureLog
import com.android.quickstep.util.ActiveGestureProtoLogProxy
import com.android.quickstep.views.RecentsView
@@ -299,7 +298,7 @@
val focusedDisplayId = focusState.focusedDisplayId
val focusedDisplayUIController: TaskbarUIController? =
- if (RecentsDisplayModel.enableOverviewInWindow()) {
+ if (enableOverviewInWindow) {
Log.d(
TAG,
"Querying RecentsDisplayModel for TaskbarUIController for display: $focusedDisplayId",
@@ -392,9 +391,7 @@
return false
}
- val recentsInWindowFlagSet =
- enableFallbackOverviewInWindow() || enableLauncherOverviewInWindow()
- if (!recentsInWindowFlagSet) {
+ if (!enableOverviewInWindow) {
containerInterface.getCreatedContainer()?.rootView?.let { view ->
InteractionJankMonitorWrapper.begin(view, Cuj.CUJ_LAUNCHER_QUICK_SWITCH)
}
@@ -425,7 +422,7 @@
transitionInfo: TransitionInfo?,
) {
Log.d(TAG, "recents animation started: $command")
- if (recentsInWindowFlagSet) {
+ if (enableOverviewInWindow) {
containerInterface.getCreatedContainer()?.rootView?.let { view ->
InteractionJankMonitorWrapper.begin(view, Cuj.CUJ_LAUNCHER_QUICK_SWITCH)
}
diff --git a/quickstep/src/com/android/quickstep/OverviewComponentObserver.java b/quickstep/src/com/android/quickstep/OverviewComponentObserver.java
index 7eacef3..7d3a1da 100644
--- a/quickstep/src/com/android/quickstep/OverviewComponentObserver.java
+++ b/quickstep/src/com/android/quickstep/OverviewComponentObserver.java
@@ -24,6 +24,7 @@
import static com.android.launcher3.Flags.enableOverviewOnConnectedDisplays;
import static com.android.launcher3.config.FeatureFlags.SEPARATE_RECENTS_ACTIVITY;
import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
+import static com.android.quickstep.fallback.window.RecentsWindowFlags.enableLauncherOverviewInWindow;
import static com.android.systemui.shared.system.PackageManagerWrapper.ACTION_PREFERRED_ACTIVITY_CHANGED;
import android.content.ActivityNotFoundException;
@@ -41,7 +42,6 @@
import androidx.annotation.Nullable;
import androidx.annotation.UiThread;
-import com.android.launcher3.Flags;
import com.android.launcher3.R;
import com.android.launcher3.dagger.ApplicationContext;
import com.android.launcher3.dagger.LauncherAppComponent;
@@ -50,6 +50,7 @@
import com.android.launcher3.util.DaggerSingletonTracker;
import com.android.launcher3.util.SimpleBroadcastReceiver;
import com.android.quickstep.fallback.window.RecentsDisplayModel;
+import com.android.quickstep.fallback.window.RecentsWindowFlags;
import com.android.quickstep.util.ActiveGestureProtoLogProxy;
import com.android.systemui.shared.system.PackageManagerWrapper;
@@ -180,7 +181,7 @@
mDefaultDisplayContainerInterface.onAssistantVisibilityChanged(0.f);
}
- if (SEPARATE_RECENTS_ACTIVITY.get() || Flags.enableLauncherOverviewInWindow()) {
+ if (SEPARATE_RECENTS_ACTIVITY.get() || enableLauncherOverviewInWindow.isTrue()) {
mIsDefaultHome = false;
if (defaultHome == null) {
defaultHome = mMyHomeIntent.getComponent();
@@ -203,7 +204,7 @@
unregisterOtherHomeAppUpdateReceiver();
} else {
// The default home app is a different launcher. Use the fallback Overview instead.
- if (Flags.enableLauncherOverviewInWindow() || Flags.enableFallbackOverviewInWindow()) {
+ if (RecentsWindowFlags.Companion.getEnableOverviewInWindow()) {
mDefaultDisplayContainerInterface =
mRecentsDisplayModel.getFallbackWindowInterface(DEFAULT_DISPLAY);
} else {
diff --git a/quickstep/src/com/android/quickstep/RecentTasksList.java b/quickstep/src/com/android/quickstep/RecentTasksList.java
index ac88e5a..cc5b2da 100644
--- a/quickstep/src/com/android/quickstep/RecentTasksList.java
+++ b/quickstep/src/com/android/quickstep/RecentTasksList.java
@@ -38,8 +38,10 @@
import androidx.annotation.VisibleForTesting;
import com.android.launcher3.statehandlers.DesktopVisibilityController;
+import com.android.launcher3.util.DaggerSingletonTracker;
import com.android.launcher3.util.LooperExecutor;
import com.android.launcher3.util.SplitConfigurationOptions;
+import com.android.launcher3.util.window.WindowManagerProxy;
import com.android.quickstep.util.DesktopTask;
import com.android.quickstep.util.ExternalDisplaysKt;
import com.android.quickstep.util.GroupTask;
@@ -70,7 +72,10 @@
/**
* Manages the recent task list from the system, caching it as necessary.
*/
-public class RecentTasksList {
+// TODO: b/401602554 - Consider letting [DesktopTasksController] notify [RecentTasksController] of
+// desk changes to trigger [IRecentTasksListener.onRecentTasksChanged()], instead of implementing
+// [DesktopVisibilityListener].
+public class RecentTasksList implements WindowManagerProxy.DesktopVisibilityListener {
private static final TaskLoadResult INVALID_RESULT = new TaskLoadResult(-1, false, 0);
@@ -78,6 +83,7 @@
private final KeyguardManager mKeyguardManager;
private final LooperExecutor mMainThreadExecutor;
private final SystemUiProxy mSysUiProxy;
+ private final DesktopVisibilityController mDesktopVisibilityController;
// The list change id, increments as the task list changes in the system
private int mChangeId;
@@ -95,13 +101,16 @@
public RecentTasksList(Context context, LooperExecutor mainThreadExecutor,
KeyguardManager keyguardManager, SystemUiProxy sysUiProxy,
- TopTaskTracker topTaskTracker) {
+ TopTaskTracker topTaskTracker,
+ DesktopVisibilityController desktopVisibilityController,
+ DaggerSingletonTracker tracker) {
mContext = context;
mMainThreadExecutor = mainThreadExecutor;
mKeyguardManager = keyguardManager;
mChangeId = 1;
mSysUiProxy = sysUiProxy;
- sysUiProxy.registerRecentTasksListener(new IRecentTasksListener.Stub() {
+ mDesktopVisibilityController = desktopVisibilityController;
+ final IRecentTasksListener recentTasksListener = new IRecentTasksListener.Stub() {
@Override
public void onRecentTasksChanged() throws RemoteException {
mMainThreadExecutor.execute(RecentTasksList.this::onRecentTasksChanged);
@@ -147,7 +156,19 @@
topTaskTracker.onVisibleTasksChanged(visibleTasks);
});
}
- });
+ };
+
+ mSysUiProxy.registerRecentTasksListener(recentTasksListener);
+ tracker.addCloseable(
+ () -> mSysUiProxy.unregisterRecentTasksListener(recentTasksListener));
+
+ if (DesktopModeStatus.enableMultipleDesktops(mContext)) {
+ mDesktopVisibilityController.registerDesktopVisibilityListener(
+ this);
+ tracker.addCloseable(
+ () -> mDesktopVisibilityController.unregisterDesktopVisibilityListener(this));
+ }
+
// We may receive onRunningTaskAppeared events later for tasks which have already been
// included in the list returned by mSysUiProxy.getRunningTasks(), or may receive
// onRunningTaskVanished for tasks not included in the returned list. These cases will be
@@ -286,6 +307,27 @@
return mRunningTasks;
}
+ @Override
+ public void onDeskAdded(int displayId, int deskId) {
+ onRecentTasksChanged();
+ }
+
+ @Override
+ public void onDeskRemoved(int displayId, int deskId) {
+ onRecentTasksChanged();
+ }
+
+ @Override
+ public void onActiveDeskChanged(int displayId, int newActiveDesk, int oldActiveDesk) {
+ // Should desk activation changes lead to the invalidation of the loaded tasks? The cases
+ // are:
+ // - Switching from one active desk to another.
+ // - Switching from out of a desk session into an active desk.
+ // - Switching from an active desk to a non-desk session.
+ // These changes don't affect the list of desks, nor their contents, so let's ignore them
+ // for now.
+ }
+
private void onRunningTaskAppeared(RunningTaskInfo taskInfo) {
// Make sure this task is not already in the list
for (RunningTaskInfo existingTask : mRunningTasks) {
diff --git a/quickstep/src/com/android/quickstep/RecentsAnimationCallbacks.java b/quickstep/src/com/android/quickstep/RecentsAnimationCallbacks.java
index d7152b5..ecde37b 100644
--- a/quickstep/src/com/android/quickstep/RecentsAnimationCallbacks.java
+++ b/quickstep/src/com/android/quickstep/RecentsAnimationCallbacks.java
@@ -34,9 +34,9 @@
import androidx.annotation.NonNull;
import androidx.annotation.UiThread;
-import com.android.launcher3.Flags;
import com.android.launcher3.Utilities;
import com.android.launcher3.util.Preconditions;
+import com.android.quickstep.fallback.window.RecentsWindowFlags;
import com.android.quickstep.util.ActiveGestureProtoLogProxy;
import com.android.systemui.shared.recents.model.ThumbnailData;
import com.android.systemui.shared.system.RecentsAnimationControllerCompat;
@@ -113,8 +113,8 @@
boolean isOpeningHome = Arrays.stream(appTargets).filter(app -> app.mode == MODE_OPENING
&& app.windowConfiguration.getActivityType() == ACTIVITY_TYPE_HOME)
.count() > 0;
- if (appCount == 0 && (!(Flags.enableFallbackOverviewInWindow()
- || Flags.enableLauncherOverviewInWindow()) || isOpeningHome)) {
+ if (appCount == 0 && (!RecentsWindowFlags.Companion.getEnableOverviewInWindow()
+ || isOpeningHome)) {
ActiveGestureProtoLogProxy.logOnRecentsAnimationStartCancelled();
// Edge case, if there are no closing app targets, then Launcher has nothing to handle
notifyAnimationCanceled();
diff --git a/quickstep/src/com/android/quickstep/RecentsAnimationTargets.java b/quickstep/src/com/android/quickstep/RecentsAnimationTargets.java
index cf7e499..0deb1ca 100644
--- a/quickstep/src/com/android/quickstep/RecentsAnimationTargets.java
+++ b/quickstep/src/com/android/quickstep/RecentsAnimationTargets.java
@@ -59,6 +59,10 @@
if (!DesktopModeStatus.canEnterDesktopMode(context)) {
return false;
}
+ // TODO: b/400866688 - Check if we need to update this such that for an empty desk, we
+ // receive a list of apps that contain only the Launcher and the `DesktopWallpaperActivity`
+ // and both are fullscreen windowing mode. A desk can also have transparent modals and
+ // immersive apps which may not have a "freeform" windowing mode.
for (RemoteAnimationTarget target : apps) {
if (target.windowConfiguration.getWindowingMode() == WINDOWING_MODE_FREEFORM) {
return true;
diff --git a/quickstep/src/com/android/quickstep/RecentsFilterState.java b/quickstep/src/com/android/quickstep/RecentsFilterState.java
index c4b0f25..1808a97 100644
--- a/quickstep/src/com/android/quickstep/RecentsFilterState.java
+++ b/quickstep/src/com/android/quickstep/RecentsFilterState.java
@@ -18,6 +18,7 @@
import androidx.annotation.Nullable;
+import com.android.quickstep.util.DesksUtils;
import com.android.quickstep.util.GroupTask;
import com.android.quickstep.views.TaskViewType;
import com.android.systemui.shared.recents.model.Task;
@@ -117,37 +118,43 @@
* Returns a predicate for filtering out GroupTasks by package name.
*
* @param packageName package name to filter GroupTasks by
- * if null, Predicate filters out desktop tasks with no non-minimized tasks.
+ * if null, Predicate filters out desktop tasks with no non-minimized tasks,
+ * unless the multiple desks feature is enabled, which allows empty desks.
*/
public static Predicate<GroupTask> getFilter(@Nullable String packageName) {
if (packageName == null) {
- return getEmptyDesktopTaskFilter();
+ return getDesktopTaskFilter();
}
return (groupTask) -> (groupTask.containsPackage(packageName)
- && !isDestopTaskWithMinimizedTasksOnly(groupTask));
+ && shouldKeepGroupTask(groupTask));
}
/**
- * Returns a predicate that filters out desk tasks that contain no non-minimized desktop tasks.
+ * Returns a predicate that filters out desk tasks that contain no non-minimized desktop tasks,
+ * unless the multiple desks feature is enabled, which allows empty desks.
*/
- public static Predicate<GroupTask> getEmptyDesktopTaskFilter() {
- return (groupTask -> !isDestopTaskWithMinimizedTasksOnly(groupTask));
+ public static Predicate<GroupTask> getDesktopTaskFilter() {
+ return (groupTask -> shouldKeepGroupTask(groupTask));
}
/**
- * Whether the provided task is a desktop task with no non-minimized tasks - returns true if the
- * desktop task has no tasks at all.
+ * Returns true if the given `groupTask` should be kept, and false if it should be filtered out.
+ * Desks will be filtered out if they are empty unless the multiple desks feature is enabled.
*
* @param groupTask The group task to check.
*/
- static boolean isDestopTaskWithMinimizedTasksOnly(GroupTask groupTask) {
+ private static boolean shouldKeepGroupTask(GroupTask groupTask) {
if (groupTask.taskViewType != TaskViewType.DESKTOP) {
- return false;
+ return true;
}
+
+ if (DesksUtils.areMultiDesksFlagsEnabled()) {
+ return true;
+ }
+
return groupTask.getTasks().stream()
- .filter(task -> !task.isMinimized)
- .toList().isEmpty();
+ .anyMatch(task -> !task.isMinimized);
}
/**
diff --git a/quickstep/src/com/android/quickstep/RecentsModel.java b/quickstep/src/com/android/quickstep/RecentsModel.java
index 1d83d42..e1adf3d 100644
--- a/quickstep/src/com/android/quickstep/RecentsModel.java
+++ b/quickstep/src/com/android/quickstep/RecentsModel.java
@@ -43,6 +43,7 @@
import com.android.launcher3.graphics.ThemeManager;
import com.android.launcher3.graphics.ThemeManager.ThemeChangeListener;
import com.android.launcher3.icons.IconProvider;
+import com.android.launcher3.statehandlers.DesktopVisibilityController;
import com.android.launcher3.util.DaggerSingletonObject;
import com.android.launcher3.util.DaggerSingletonTracker;
import com.android.launcher3.util.DisplayController;
@@ -61,6 +62,8 @@
import com.android.systemui.shared.system.TaskStackChangeListener;
import com.android.systemui.shared.system.TaskStackChangeListeners;
+import dagger.Lazy;
+
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.List;
@@ -72,8 +75,6 @@
import javax.inject.Inject;
-import dagger.Lazy;
-
/**
* Singleton class to load and manage recents model.
*/
@@ -104,12 +105,14 @@
DisplayController displayController,
LockedUserState lockedUserState,
Lazy<ThemeManager> themeManagerLazy,
+ DesktopVisibilityController desktopVisibilityController,
DaggerSingletonTracker tracker
) {
// Lazily inject the ThemeManager and access themeManager once the device is
// unlocked. See b/393248495 for details.
this(context, new IconProvider(context), systemUiProxy, topTaskTracker,
- displayController, lockedUserState,themeManagerLazy, tracker);
+ displayController, lockedUserState, themeManagerLazy, desktopVisibilityController,
+ tracker);
}
@SuppressLint("VisibleForTests")
@@ -120,6 +123,7 @@
DisplayController displayController,
LockedUserState lockedUserState,
Lazy<ThemeManager> themeManagerLazy,
+ DesktopVisibilityController desktopVisibilityController,
DaggerSingletonTracker tracker) {
this(context,
new RecentTasksList(
@@ -127,7 +131,7 @@
MAIN_EXECUTOR,
context.getSystemService(KeyguardManager.class),
systemUiProxy,
- topTaskTracker),
+ topTaskTracker, desktopVisibilityController, tracker),
new TaskIconCache(context, RECENTS_MODEL_EXECUTOR, iconProvider, displayController),
new TaskThumbnailCache(context, RECENTS_MODEL_EXECUTOR),
iconProvider,
@@ -205,7 +209,7 @@
@Override
public int getTasks(@Nullable Consumer<List<GroupTask>> callback) {
return mTaskList.getTasks(false /* loadKeysOnly */, callback,
- RecentsFilterState.getEmptyDesktopTaskFilter());
+ RecentsFilterState.getDesktopTaskFilter());
}
/**
diff --git a/quickstep/src/com/android/quickstep/SystemUiProxy.kt b/quickstep/src/com/android/quickstep/SystemUiProxy.kt
index d6f6540..506f85d 100644
--- a/quickstep/src/com/android/quickstep/SystemUiProxy.kt
+++ b/quickstep/src/com/android/quickstep/SystemUiProxy.kt
@@ -1096,18 +1096,26 @@
// Desktop Mode
//
/** Calls shell to create a new desk (if possible) on the display whose ID is `displayId`. */
- fun createDesktop(displayId: Int) =
+ fun createDesk(displayId: Int) =
executeWithErrorLog({ "Failed call createDesk" }) { desktopMode?.createDesk(displayId) }
/**
* Calls shell to activate the desk whose ID is `deskId` on whatever display it exists on. This
* will bring all tasks on this desk to the front.
*/
- fun activateDesktop(deskId: Int, transition: RemoteTransition?) =
+ fun activateDesk(deskId: Int, transition: RemoteTransition?) =
executeWithErrorLog({ "Failed call activateDesk" }) {
desktopMode?.activateDesk(deskId, transition)
}
+ /** Calls shell to remove the desk whose ID is `deskId`. */
+ fun removeDesk(deskId: Int) =
+ executeWithErrorLog({ "Failed call removeDesk" }) { desktopMode?.removeDesk(deskId) }
+
+ /** Calls shell to remove all the available desks on all displays. */
+ fun removeAllDesks() =
+ executeWithErrorLog({ "Failed call removeAllDesks" }) { desktopMode?.removeAllDesks() }
+
/** Call shell to show all apps active on the desktop */
fun showDesktopApps(displayId: Int, transition: RemoteTransition?) =
executeWithErrorLog({ "Failed call showDesktopApps" }) {
@@ -1159,9 +1167,9 @@
}
/** Call shell to remove the desktop that is on given `displayId` */
- fun removeDesktop(displayId: Int) =
- executeWithErrorLog({ "Failed call removeDesktop" }) {
- desktopMode?.removeDesktop(displayId)
+ fun removeDefaultDeskInDisplay(displayId: Int) =
+ executeWithErrorLog({ "Failed call removeDefaultDeskInDisplay" }) {
+ desktopMode?.removeDefaultDeskInDisplay(displayId)
}
/** Call shell to move a task with given `taskId` to external display. */
diff --git a/quickstep/src/com/android/quickstep/TaskAnimationManager.java b/quickstep/src/com/android/quickstep/TaskAnimationManager.java
index 64a8c25..1c7f23c 100644
--- a/quickstep/src/com/android/quickstep/TaskAnimationManager.java
+++ b/quickstep/src/com/android/quickstep/TaskAnimationManager.java
@@ -42,12 +42,12 @@
import androidx.annotation.UiThread;
import com.android.internal.util.ArrayUtils;
-import com.android.launcher3.Flags;
import com.android.launcher3.Utilities;
import com.android.launcher3.config.FeatureFlags;
import com.android.launcher3.taskbar.TaskbarUIController;
import com.android.launcher3.util.DisplayController;
import com.android.quickstep.fallback.window.RecentsDisplayModel;
+import com.android.quickstep.fallback.window.RecentsWindowFlags;
import com.android.quickstep.fallback.window.RecentsWindowManager;
import com.android.quickstep.util.ActiveGestureProtoLogProxy;
import com.android.quickstep.util.SystemUiFlagUtils;
@@ -63,7 +63,6 @@
public class TaskAnimationManager implements RecentsAnimationCallbacks.RecentsAnimationListener {
public static final boolean SHELL_TRANSITIONS_ROTATION =
SystemProperties.getBoolean("persist.wm.debug.shell_transit_rotate", false);
-
private final Context mCtx;
private RecentsAnimationController mController;
private RecentsAnimationCallbacks mCallbacks;
@@ -313,8 +312,7 @@
}
if(containerInterface.getCreatedContainer() instanceof RecentsWindowManager
- && (Flags.enableFallbackOverviewInWindow()
- || Flags.enableLauncherOverviewInWindow())) {
+ && RecentsWindowFlags.Companion.getEnableOverviewInWindow()) {
mRecentsAnimationStartPending = getSystemUiProxy().startRecentsActivity(intent, options,
mCallbacks, gestureState.useSyntheticRecentsTransition());
RecentsDisplayModel.getINSTANCE().get(mCtx)
diff --git a/quickstep/src/com/android/quickstep/TouchInteractionService.java b/quickstep/src/com/android/quickstep/TouchInteractionService.java
index ba662c4..741ae7d 100644
--- a/quickstep/src/com/android/quickstep/TouchInteractionService.java
+++ b/quickstep/src/com/android/quickstep/TouchInteractionService.java
@@ -94,6 +94,7 @@
import com.android.quickstep.OverviewComponentObserver.OverviewChangeListener;
import com.android.quickstep.fallback.window.RecentsDisplayModel;
import com.android.quickstep.fallback.window.RecentsDisplayModel.RecentsDisplayResource;
+import com.android.quickstep.fallback.window.RecentsWindowFlags;
import com.android.quickstep.fallback.window.RecentsWindowSwipeHandler;
import com.android.quickstep.inputconsumers.BubbleBarInputConsumer;
import com.android.quickstep.inputconsumers.OneHandedModeInputConsumer;
@@ -1081,10 +1082,9 @@
}
public AbsSwipeUpHandler.Factory getSwipeUpHandlerFactory() {
- boolean recentsInWindow =
- Flags.enableFallbackOverviewInWindow() || Flags.enableLauncherOverviewInWindow();
return mOverviewComponentObserver.isHomeAndOverviewSame()
- ? mLauncherSwipeHandlerFactory : (recentsInWindow
+ ? mLauncherSwipeHandlerFactory
+ : (RecentsWindowFlags.Companion.getEnableOverviewInWindow()
? mRecentsWindowSwipeHandlerFactory : mFallbackSwipeHandlerFactory);
}
diff --git a/quickstep/src/com/android/quickstep/fallback/FallbackRecentsView.java b/quickstep/src/com/android/quickstep/fallback/FallbackRecentsView.java
index d8662f2..8ec97ed 100644
--- a/quickstep/src/com/android/quickstep/fallback/FallbackRecentsView.java
+++ b/quickstep/src/com/android/quickstep/fallback/FallbackRecentsView.java
@@ -30,7 +30,6 @@
import androidx.annotation.Nullable;
import com.android.launcher3.AbstractFloatingView;
-import com.android.launcher3.Flags;
import com.android.launcher3.anim.AnimatorPlaybackController;
import com.android.launcher3.anim.PendingAnimation;
import com.android.launcher3.desktop.DesktopRecentsTransitionController;
@@ -46,6 +45,7 @@
import com.android.quickstep.GestureState;
import com.android.quickstep.RemoteTargetGluer.RemoteTargetHandle;
import com.android.quickstep.fallback.window.RecentsDisplayModel;
+import com.android.quickstep.fallback.window.RecentsWindowFlags;
import com.android.quickstep.util.GroupTask;
import com.android.quickstep.util.SingleTask;
import com.android.quickstep.util.SplitSelectStateController;
@@ -80,9 +80,10 @@
@Override
public BaseContainerInterface<RecentsState, ?> getContainerInterface(int displayId) {
- return (Flags.enableFallbackOverviewInWindow() || Flags.enableLauncherOverviewInWindow())
+ return RecentsWindowFlags.Companion.getEnableOverviewInWindow()
? RecentsDisplayModel.getINSTANCE().get(mContext)
- .getFallbackWindowInterface(displayId) : FallbackActivityInterface.INSTANCE;
+ .getFallbackWindowInterface(displayId)
+ : FallbackActivityInterface.INSTANCE;
}
@Override
@@ -290,8 +291,7 @@
}
// disabling this so app icons aren't drawn on top of recent tasks.
- if (isOverlayEnabled && !(Flags.enableFallbackOverviewInWindow()
- || Flags.enableLauncherOverviewInWindow())) {
+ if (isOverlayEnabled && !RecentsWindowFlags.Companion.getEnableOverviewInWindow()) {
runActionOnRemoteHandles(remoteTargetHandle ->
remoteTargetHandle.getTaskViewSimulator().setDrawsBelowRecents(true));
}
diff --git a/quickstep/src/com/android/quickstep/fallback/window/RecentsDisplayModel.kt b/quickstep/src/com/android/quickstep/fallback/window/RecentsDisplayModel.kt
index 58c6c50..12dc177 100644
--- a/quickstep/src/com/android/quickstep/fallback/window/RecentsDisplayModel.kt
+++ b/quickstep/src/com/android/quickstep/fallback/window/RecentsDisplayModel.kt
@@ -19,7 +19,6 @@
import android.content.Context
import android.view.Display
import androidx.core.util.valueIterator
-import com.android.launcher3.Flags
import com.android.launcher3.dagger.ApplicationContext
import com.android.launcher3.dagger.LauncherAppSingleton
import com.android.launcher3.util.DaggerSingletonObject
@@ -29,6 +28,7 @@
import com.android.quickstep.FallbackWindowInterface
import com.android.quickstep.dagger.QuickstepBaseAppComponent
import com.android.quickstep.fallback.window.RecentsDisplayModel.RecentsDisplayResource
+import com.android.quickstep.fallback.window.RecentsWindowFlags.Companion.enableOverviewInWindow
import java.io.PrintWriter
import javax.inject.Inject
@@ -50,14 +50,10 @@
DaggerSingletonObject<RecentsDisplayModel>(
QuickstepBaseAppComponent::getRecentsDisplayModel
)
-
- @JvmStatic
- fun enableOverviewInWindow() =
- Flags.enableFallbackOverviewInWindow() || Flags.enableLauncherOverviewInWindow()
}
init {
- if (enableOverviewInWindow()) {
+ if (enableOverviewInWindow) {
registerDisplayListener()
tracker.addCloseable { destroy() }
}
diff --git a/quickstep/src/com/android/quickstep/fallback/window/RecentsWindowContext.kt b/quickstep/src/com/android/quickstep/fallback/window/RecentsWindowContext.kt
index 333571c..d70d7eb 100644
--- a/quickstep/src/com/android/quickstep/fallback/window/RecentsWindowContext.kt
+++ b/quickstep/src/com/android/quickstep/fallback/window/RecentsWindowContext.kt
@@ -18,7 +18,7 @@
import android.content.Context
import android.graphics.PixelFormat
-import android.view.Display.DEFAULT_DISPLAY
+import android.view.Display
import android.view.ViewGroup
import android.view.WindowManager
import android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS
@@ -53,7 +53,7 @@
fun initDeviceProfile() {
deviceProfile =
- if (displayId == DEFAULT_DISPLAY)
+ if (displayId == Display.DEFAULT_DISPLAY)
InvariantDeviceProfile.INSTANCE[this].getDeviceProfile(this)
else InvariantDeviceProfile.INSTANCE[this].createDeviceProfileForSecondaryDisplay(this)
}
diff --git a/quickstep/src/com/android/quickstep/fallback/window/RecentsWindowFlags.kt b/quickstep/src/com/android/quickstep/fallback/window/RecentsWindowFlags.kt
new file mode 100644
index 0000000..9953154
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/fallback/window/RecentsWindowFlags.kt
@@ -0,0 +1,36 @@
+/*
+ * Copyright (C) 2025 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.fallback.window
+
+import android.window.DesktopModeFlags.DesktopModeFlag
+import com.android.launcher3.Flags
+
+class RecentsWindowFlags {
+ companion object {
+ @JvmField
+ val enableLauncherOverviewInWindow: DesktopModeFlag =
+ DesktopModeFlag(Flags::enableLauncherOverviewInWindow, false)
+
+ @JvmField
+ val enableFallbackOverviewInWindow: DesktopModeFlag =
+ DesktopModeFlag(Flags::enableFallbackOverviewInWindow, false)
+
+ @JvmStatic
+ val enableOverviewInWindow
+ get() = enableLauncherOverviewInWindow.isTrue || enableFallbackOverviewInWindow.isTrue
+ }
+}
diff --git a/quickstep/src/com/android/quickstep/inputconsumers/OtherActivityInputConsumer.java b/quickstep/src/com/android/quickstep/inputconsumers/OtherActivityInputConsumer.java
index 7cae5b8..c8cf58c 100644
--- a/quickstep/src/com/android/quickstep/inputconsumers/OtherActivityInputConsumer.java
+++ b/quickstep/src/com/android/quickstep/inputconsumers/OtherActivityInputConsumer.java
@@ -42,7 +42,6 @@
import androidx.annotation.UiThread;
-import com.android.launcher3.Flags;
import com.android.launcher3.R;
import com.android.launcher3.Utilities;
import com.android.launcher3.testing.TestLogging;
@@ -59,6 +58,7 @@
import com.android.quickstep.RecentsAnimationTargets;
import com.android.quickstep.RotationTouchHelper;
import com.android.quickstep.TaskAnimationManager;
+import com.android.quickstep.fallback.window.RecentsWindowFlags;
import com.android.quickstep.util.CachedEventDispatcher;
import com.android.quickstep.util.MotionPauseDetector;
import com.android.quickstep.util.NavBarPosition;
@@ -438,10 +438,8 @@
notifyGestureStarted(true /*isLikelyToStartNewTask*/);
} else {
// todo differentiate intent based on if we are on home or in app for overview in window
- boolean useHomeIntentForWindow = Flags.enableFallbackOverviewInWindow()
- || Flags.enableLauncherOverviewInWindow();
- Intent intent = new Intent(useHomeIntentForWindow ? mInteractionHandler.getHomeIntent()
- : mInteractionHandler.getLaunchIntent());
+ Intent intent = new Intent(RecentsWindowFlags.Companion.getEnableOverviewInWindow()
+ ? mInteractionHandler.getHomeIntent() : mInteractionHandler.getLaunchIntent());
intent.putExtra(INTENT_EXTRA_LOG_TRACE_ID, mGestureState.getGestureId());
mActiveCallbacks = mTaskAnimationManager.startRecentsAnimation(mGestureState, intent,
mInteractionHandler);
diff --git a/quickstep/src/com/android/quickstep/util/AnimUtils.java b/quickstep/src/com/android/quickstep/util/AnimUtils.java
index fda0c29..3492788 100644
--- a/quickstep/src/com/android/quickstep/util/AnimUtils.java
+++ b/quickstep/src/com/android/quickstep/util/AnimUtils.java
@@ -21,16 +21,22 @@
import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
import android.animation.AnimatorSet;
-import android.annotation.NonNull;
+import android.os.BinderUtils;
import android.os.Bundle;
+import android.os.IBinder;
import android.os.IRemoteCallback;
import android.view.animation.Interpolator;
+import androidx.annotation.NonNull;
+import androidx.lifecycle.DefaultLifecycleObserver;
+import androidx.lifecycle.LifecycleOwner;
+
import com.android.launcher3.logging.StatsLogManager;
import com.android.launcher3.statemanager.BaseState;
import com.android.launcher3.statemanager.StateManager;
import com.android.launcher3.states.StateAnimationConfig;
import com.android.launcher3.util.RunnableList;
+import com.android.launcher3.views.ActivityContext;
import com.android.quickstep.views.RecentsViewContainer;
/**
@@ -95,14 +101,30 @@
}
/**
- * Returns a IRemoteCallback which completes the provided list as a result
+ * Returns a IRemoteCallback which completes the provided list as a result or when the owner
+ * is destroyed
*/
- public static IRemoteCallback completeRunnableListCallback(RunnableList list) {
+ public static IRemoteCallback completeRunnableListCallback(
+ RunnableList list, ActivityContext owner) {
+ DefaultLifecycleObserver destroyObserver = new DefaultLifecycleObserver() {
+ @Override
+ public void onDestroy(@NonNull LifecycleOwner owner) {
+ list.executeAllAndClear();
+ }
+ };
+ MAIN_EXECUTOR.execute(() -> owner.getLifecycle().addObserver(destroyObserver));
+ list.add(() -> owner.getLifecycle().removeObserver(destroyObserver));
+
return new IRemoteCallback.Stub() {
@Override
public void sendResult(Bundle bundle) {
MAIN_EXECUTOR.execute(list::executeAllAndDestroy);
}
+
+ @Override
+ public IBinder asBinder() {
+ return BinderUtils.wrapLifecycle(this, owner.getOwnerCleanupSet());
+ }
};
}
diff --git a/quickstep/src/com/android/quickstep/util/DesksUtils.kt b/quickstep/src/com/android/quickstep/util/DesksUtils.kt
new file mode 100644
index 0000000..ccfdbb9
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/util/DesksUtils.kt
@@ -0,0 +1,43 @@
+/*
+ * Copyright (C) 2025 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.util
+
+import android.content.Context
+import android.window.DesktopExperienceFlags
+import com.android.systemui.shared.recents.model.Task
+
+class DesksUtils {
+ companion object {
+ @JvmStatic
+ fun areMultiDesksFlagsEnabled() =
+ DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue() &&
+ DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_FRONTEND.isTrue()
+
+ /** Returns true if this [task] contains the [DesktopWallpaperActivity]. */
+ @JvmStatic
+ fun isDesktopWallpaperTask(context: Context, task: Task): Boolean {
+ val sysUiPackage =
+ context.getResources().getString(com.android.internal.R.string.config_systemUi)
+ val component = task.key.component
+ if (component != null) {
+ return component.className.contains("DesktopWallpaperActivity") &&
+ component.packageName.contains(sysUiPackage)
+ }
+ return false
+ }
+ }
+}
diff --git a/quickstep/src/com/android/quickstep/views/DesktopTaskView.kt b/quickstep/src/com/android/quickstep/views/DesktopTaskView.kt
index 1d035e9..27657b4 100644
--- a/quickstep/src/com/android/quickstep/views/DesktopTaskView.kt
+++ b/quickstep/src/com/android/quickstep/views/DesktopTaskView.kt
@@ -35,6 +35,7 @@
import com.android.launcher3.Flags.enableOverviewIconMenu
import com.android.launcher3.Flags.enableRefactorTaskThumbnail
import com.android.launcher3.R
+import com.android.launcher3.statehandlers.DesktopVisibilityController
import com.android.launcher3.testing.TestLogging
import com.android.launcher3.testing.shared.TestProtocol
import com.android.launcher3.util.RunnableList
@@ -68,6 +69,8 @@
type = TaskViewType.DESKTOP,
thumbnailFullscreenParams = DesktopFullscreenDrawParams(context),
) {
+ var deskId = DesktopVisibilityController.INACTIVE_DESK_ID
+
private val contentViewFullscreenParams = FullscreenDrawParams(context)
private val taskContentViewPool =
@@ -281,6 +284,7 @@
orientedState: RecentsOrientedState,
taskOverlayFactory: TaskOverlayFactory,
) {
+ deskId = desktopTask.deskId
// TODO(b/370495260): Minimized tasks should not be filtered with desktop exploded view
// support.
// Minimized tasks should not be shown in Overview.
@@ -332,12 +336,18 @@
override fun onRecycle() {
super.onRecycle()
+ deskId = DesktopVisibilityController.INACTIVE_DESK_ID
explodeProgress = 0.0f
viewModel = null
visibility = VISIBLE
taskContainers.forEach { removeAndRecycleThumbnailView(it) }
}
+ override fun setOrientationState(orientationState: RecentsOrientedState) {
+ super.setOrientationState(orientationState)
+ iconView.setIconOrientation(orientationState, isGridTask)
+ }
+
@SuppressLint("RtlHardcoded")
override fun updateTaskSize(lastComputedTaskSize: Rect, lastComputedGridTaskSize: Rect) {
super.updateTaskSize(lastComputedTaskSize, lastComputedGridTaskSize)
diff --git a/quickstep/src/com/android/quickstep/views/RecentsView.java b/quickstep/src/com/android/quickstep/views/RecentsView.java
index c282e77..bb2aa75 100644
--- a/quickstep/src/com/android/quickstep/views/RecentsView.java
+++ b/quickstep/src/com/android/quickstep/views/RecentsView.java
@@ -200,6 +200,7 @@
import com.android.quickstep.TaskViewUtils;
import com.android.quickstep.TopTaskTracker;
import com.android.quickstep.ViewUtils;
+import com.android.quickstep.fallback.window.RecentsWindowFlags;
import com.android.quickstep.orientation.RecentsPagedOrientationHandler;
import com.android.quickstep.recents.data.RecentTasksRepository;
import com.android.quickstep.recents.data.RecentsDeviceProfileRepository;
@@ -211,6 +212,7 @@
import com.android.quickstep.recents.viewmodel.RecentsViewModel;
import com.android.quickstep.util.ActiveGestureProtoLogProxy;
import com.android.quickstep.util.AnimUtils;
+import com.android.quickstep.util.DesksUtils;
import com.android.quickstep.util.DesktopTask;
import com.android.quickstep.util.GroupTask;
import com.android.quickstep.util.LayoutUtils;
@@ -253,11 +255,11 @@
import java.util.HashSet;
import java.util.List;
import java.util.Map;
+import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.function.Consumer;
import java.util.stream.Collectors;
-
/**
* A list of recent tasks.
*
@@ -1924,6 +1926,8 @@
return;
}
+ // TODO: b/400532675 - The use of `currentTaskIds`, `runningTaskIds`, and `focusedTaskIds`
+ // needs to be audited so that they can work with empty desks that have no tasks.
int[] currentTaskIds;
TaskView currentTaskView = getTaskViewAt(mCurrentPage);
if (currentTaskView != null) {
@@ -2826,9 +2830,13 @@
/**
* Called when a gesture from an app is starting.
*/
+ // TODO: b/401582344 - Implement a way to exclude the `DesktopWallpaperActivity` from being
+ // considered in Overview.
public void onGestureAnimationStart(Task[] runningTasks) {
Log.d(TAG, "onGestureAnimationStart - runningTasks: " + Arrays.toString(runningTasks));
mActiveGestureRunningTasks = runningTasks;
+
+
// This needs to be called before the other states are set since it can create the task view
if (mOrientationState.setGestureActive(true)) {
reapplyActiveRotation();
@@ -3062,6 +3070,21 @@
}
/**
+ * Creates a `DesktopTaskView` for the currently active desk on this display, which contains the
+ * gievn `runningTasks`.
+ */
+ private DesktopTaskView createDesktopTaskViewForActiveDesk(Task[] runningTasks) {
+ final int activeDeskId = mUtils.getActiveDeskIdOnThisDisplay();
+ final var desktopTaskView = (DesktopTaskView) getTaskViewFromPool(TaskViewType.DESKTOP);
+
+ // TODO: b/401582344 - Implement a way to exclude the `DesktopWallpaperActivity`.
+ desktopTaskView.bind(
+ new DesktopTask(activeDeskId, Arrays.asList(runningTasks)),
+ mOrientationState, mTaskOverlayFactory);
+ return desktopTaskView;
+ }
+
+ /**
* Creates a task view (if necessary) to represent the task with the {@param runningTaskId}.
*
* All subsequent calls to reload will keep the task as the first item until {@link #reset()}
@@ -3075,20 +3098,14 @@
}
int runningTaskViewId = -1;
- boolean needGroupTaskView = runningTasks.length > 1;
- boolean needDesktopTask = hasDesktopTask(runningTasks);
if (shouldAddStubTaskView(runningTasks)) {
boolean wasEmpty = getChildCount() == 0;
// Add an empty view for now until the task plan is loaded and applied
final TaskView taskView;
+ final boolean needGroupTaskView = runningTasks.length > 1;
+ final boolean needDesktopTask = hasDesktopTask(runningTasks);
if (needDesktopTask) {
- final int activeDeskId =
- DesktopVisibilityController.INSTANCE.get(mContext).getActiveDeskId(
- mContainer.getDisplay().getDisplayId());
- taskView = getTaskViewFromPool(TaskViewType.DESKTOP);
- ((DesktopTaskView) taskView).bind(
- new DesktopTask(activeDeskId, Arrays.asList(runningTasks)),
- mOrientationState, mTaskOverlayFactory);
+ taskView = createDesktopTaskViewForActiveDesk(runningTasks);
} else if (needGroupTaskView) {
taskView = getTaskViewFromPool(TaskViewType.GROUPED);
// When we create a placeholder task view mSplitBoundsConfig will be null, but with
@@ -3114,8 +3131,11 @@
measure(makeMeasureSpec(getMeasuredWidth(), EXACTLY),
makeMeasureSpec(getMeasuredHeight(), EXACTLY));
layout(getLeft(), getTop(), getRight(), getBottom());
- } else if (getTaskViewByTaskId(runningTasks[0].key.id) != null) {
- runningTaskViewId = getTaskViewByTaskId(runningTasks[0].key.id).getTaskViewId();
+ } else {
+ var runningTaskView = getTaskViewByTaskId(runningTasks[0].key.id);
+ if (runningTaskView != null) {
+ runningTaskViewId = runningTaskView.getTaskViewId();
+ }
}
boolean runningTaskTileHidden = mRunningTaskTileHidden;
@@ -3154,6 +3174,10 @@
return true;
}
}
+
+ // A running empty desk will have a single running app for the `DesktopWallpaperActivity`.
+ // TODO: b/401582344 - Implement a way to exclude the `DesktopWallpaperActivity`.
+
return false;
}
@@ -4525,24 +4549,33 @@
return lastVisibleTaskView;
}
- private void removeTaskInternal(@NonNull TaskView dismissedTaskView) {
- UI_HELPER_EXECUTOR
- .getHandler()
- .post(
- () -> {
- if (DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION.isTrue()
- && dismissedTaskView instanceof DesktopTaskView) {
- // TODO: b/362720497 - Use the api with desktop id instead.
- SystemUiProxy.INSTANCE
+ private void removeTaskInternal(@NonNull TaskView dismissedTaskView) {
+ UI_HELPER_EXECUTOR
+ .getHandler()
+ .post(
+ () -> {
+ if (dismissedTaskView instanceof DesktopTaskView desktopTaskView) {
+ removeDesktopTaskView(desktopTaskView);
+ } else {
+ for (int taskId : dismissedTaskView.getTaskIds()) {
+ ActivityManagerWrapper.getInstance().removeTask(taskId);
+ }
+ }
+ });
+ }
+
+ private void removeDesktopTaskView(DesktopTaskView desktopTaskView) {
+ if (DesksUtils.areMultiDesksFlagsEnabled()) {
+ SystemUiProxy.INSTANCE
.get(getContext())
- .removeDesktop(mContainer.getDisplay().getDisplayId());
- } else {
- for (int taskId : dismissedTaskView.getTaskIds()) {
- ActivityManagerWrapper.getInstance().removeTask(taskId);
- }
- }
- });
- }
+ .removeDesk(desktopTaskView.getDeskId());
+ } else if (DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION.isTrue()) {
+ SystemUiProxy.INSTANCE
+ .get(getContext())
+ .removeDefaultDeskInDisplay(
+ mContainer.getDisplay().getDisplayId());
+ }
+ }
protected void onDismissAnimationEnds() {
AccessibilityManagerCompat.sendTestProtocolEventToTest(getContext(),
@@ -4562,6 +4595,12 @@
mPendingAnimation = anim;
mPendingAnimation.addEndListener(isSuccess -> {
if (isSuccess) {
+ // Remove desktops first, since desks can be empty (so they have no recent tasks),
+ // and closing all tasks on a desk doesn't always necessarily mean that the desk
+ // will be removed. So, there are no guarantees that the below call to
+ // `ActivityManagerWrapper::removeAllRecentTasks()` will be enough.
+ SystemUiProxy.INSTANCE.get(getContext()).removeAllDesks();
+
// Remove all the task views now
finishRecentsAnimation(true /* toRecents */, false /* shouldPip */, () -> {
UI_HELPER_EXECUTOR.getHandler().post(
@@ -4691,7 +4730,7 @@
private void createDesk(View view) {
SystemUiProxy.INSTANCE
.get(getContext())
- .createDesktop(mContainer.getDisplay().getDisplayId());
+ .createDesk(mContainer.getDisplay().getDisplayId());
}
@Override
@@ -6026,7 +6065,7 @@
// mSyncTransactionApplier doesn't get transferred over
runActionOnRemoteHandles(remoteTargetHandle -> {
final TransformParams params = remoteTargetHandle.getTransformParams();
- if (Flags.enableFallbackOverviewInWindow() || Flags.enableLauncherOverviewInWindow()) {
+ if (RecentsWindowFlags.Companion.getEnableOverviewInWindow()) {
params.setHomeBuilderProxy((builder, app, transformParams) -> {
mTmpMatrix.setScale(
1f, 1f, app.localBounds.exactCenterX(), app.localBounds.exactCenterY());
@@ -6924,6 +6963,58 @@
// TODO: b/389209338 - update the AddDesktopButton's visibility on this.
}
+ @Override
+ public void onDeskAdded(int displayId, int deskId) {
+ // Ignore desk changes that don't belong to this display.
+ if (displayId != mContainer.getDisplay().getDisplayId()) {
+ return;
+ }
+
+ if (mUtils.getDesktopTaskViewForDeskId(deskId) != null) {
+ Log.e(TAG, "A task view for this desk has already been added.");
+ return;
+ }
+
+ // We assume that a newly added desk is always empty and gets added to the left of the
+ // `AddNewDesktopButton`.
+ DesktopTaskView desktopTaskView =
+ (DesktopTaskView) getTaskViewFromPool(TaskViewType.DESKTOP);
+ desktopTaskView.bind(new DesktopTask(deskId, new ArrayList<>()),
+ mOrientationState, mTaskOverlayFactory);
+
+ Objects.requireNonNull(mAddDesktopButton);
+ final int insertionIndex = 1 + indexOfChild(mAddDesktopButton);
+ addView(desktopTaskView, insertionIndex);
+
+ updateTaskSize();
+ updateChildTaskOrientations();
+
+ // TODO: b/401002178 - Recalculate the new current page such that the addition of the new
+ // desk does not result in a change in the current scroll page.
+ }
+
+ @Override
+ public void onDeskRemoved(int displayId, int deskId) {
+ // Ignore desk changes that don't belong to this display.
+ if (displayId != mContainer.getDisplay().getDisplayId()) {
+ return;
+ }
+
+ // We need to distinguish between desk removals that are triggered from outside of overview
+ // vs. the ones that were initiated from overview by dismissing the corresponding desktop
+ // task view.
+ var taskView = mUtils.getDesktopTaskViewForDeskId(deskId);
+ if (taskView != null) {
+ dismissTaskView(taskView, true, true);
+ }
+ }
+
+ @Override
+ public void onActiveDeskChanged(int displayId, int newActiveDesk, int oldActiveDesk) {
+ // TODO: b/400870600 - We may need to add code here to special case when an empty desk gets
+ // activated, since `RemoteDesktopLaunchTransitionRunner` doesn't always get triggered.
+ }
+
/** Get the color used for foreground scrimming the RecentsView for sharing. */
public static int getForegroundScrimDimColor(Context context) {
return context.getColor(R.color.overview_foreground_scrim_color);
diff --git a/quickstep/src/com/android/quickstep/views/RecentsViewUtils.kt b/quickstep/src/com/android/quickstep/views/RecentsViewUtils.kt
index 1c37986..037bef6 100644
--- a/quickstep/src/com/android/quickstep/views/RecentsViewUtils.kt
+++ b/quickstep/src/com/android/quickstep/views/RecentsViewUtils.kt
@@ -22,7 +22,11 @@
import androidx.core.view.children
import com.android.launcher3.Flags.enableLargeDesktopWindowingTile
import com.android.launcher3.Flags.enableSeparateExternalDisplayTasks
+import com.android.launcher3.statehandlers.DesktopVisibilityController
+import com.android.launcher3.statehandlers.DesktopVisibilityController.Companion.INACTIVE_DESK_ID
import com.android.launcher3.util.IntArray
+import com.android.quickstep.util.DesksUtils
+import com.android.quickstep.util.DesktopTask
import com.android.quickstep.util.GroupTask
import com.android.quickstep.util.isExternalDisplay
import com.android.quickstep.views.RecentsView.RUNNING_TASK_ATTACH_ALPHA
@@ -52,7 +56,12 @@
* @return Sorted list of GroupTasks to be used in the RecentsView.
*/
fun sortDesktopTasksToFront(tasks: List<GroupTask>): List<GroupTask> {
- val (desktopTasks, otherTasks) = tasks.partition { it.taskViewType == TaskViewType.DESKTOP }
+ var (desktopTasks, otherTasks) = tasks.partition { it.taskViewType == TaskViewType.DESKTOP }
+ if (DesksUtils.areMultiDesksFlagsEnabled()) {
+ // Desk IDs of newer desks are larger than those of older desks, hence we can use them
+ // to sort desks from old to new.
+ desktopTasks = desktopTasks.sortedBy { (it as DesktopTask).deskId }
+ }
return otherTasks + desktopTasks
}
@@ -114,6 +123,22 @@
it.isLargeTile && !(recentsView.isSplitSelectionActive && it is DesktopTaskView)
}
+ /**
+ * Returns the [DesktopTaskView] that matches the given [deskId], or null if it doesn't exist.
+ */
+ fun getDesktopTaskViewForDeskId(deskId: Int): DesktopTaskView? {
+ if (deskId == INACTIVE_DESK_ID) {
+ return null
+ }
+ return taskViews.firstOrNull { it is DesktopTaskView && it.deskId == deskId }
+ as? DesktopTaskView
+ }
+
+ /** Returns the active desk ID of the display that contains the [recentsView] instance. */
+ fun getActiveDeskIdOnThisDisplay(): Int =
+ DesktopVisibilityController.INSTANCE.get(recentsView.context)
+ .getActiveDeskId(recentsView.mContainer.display.displayId)
+
/** Returns the expected focus task. */
fun getFirstNonDesktopTaskView(): TaskView? =
if (enableLargeDesktopWindowingTile()) taskViews.firstOrNull { it !is DesktopTaskView }
diff --git a/quickstep/src/com/android/quickstep/views/TaskView.kt b/quickstep/src/com/android/quickstep/views/TaskView.kt
index b7f1d1d..55432b8 100644
--- a/quickstep/src/com/android/quickstep/views/TaskView.kt
+++ b/quickstep/src/com/android/quickstep/views/TaskView.kt
@@ -33,7 +33,6 @@
import android.view.Display
import android.view.MotionEvent
import android.view.View
-import android.view.View.OnClickListener
import android.view.ViewGroup
import android.view.ViewStub
import android.view.accessibility.AccessibilityNodeInfo
@@ -142,6 +141,7 @@
/** Returns whether the task is part of overview grid and not being focused. */
get() = container.deviceProfile.isTablet && !isLargeTile
+ // TODO: b/400532675 - This will not work for empty desks until b/400532675 is fixed.
val isRunningTask: Boolean
get() = this === recentsView?.runningTaskView
diff --git a/quickstep/src_protolog/com/android/launcher3/util/StateManagerProtoLogProxy.java b/quickstep/src_protolog/com/android/launcher3/util/StateManagerProtoLogProxy.java
index c319cb1..cb7254f 100644
--- a/quickstep/src_protolog/com/android/launcher3/util/StateManagerProtoLogProxy.java
+++ b/quickstep/src_protolog/com/android/launcher3/util/StateManagerProtoLogProxy.java
@@ -16,22 +16,25 @@
package com.android.launcher3.util;
-import static com.android.launcher3.Flags.enableStateManagerProtoLog;
import static com.android.quickstep.util.QuickstepProtoLogGroup.LAUNCHER_STATE_MANAGER;
import static com.android.quickstep.util.QuickstepProtoLogGroup.isProtoLogInitialized;
+import android.window.DesktopModeFlags.DesktopModeFlag;
+
import androidx.annotation.NonNull;
import com.android.internal.protolog.ProtoLog;
+import com.android.launcher3.Flags;
/**
* Proxy class used for StateManager ProtoLog support.
*/
public class StateManagerProtoLogProxy {
-
+ private static final DesktopModeFlag ENABLE_STATE_MANAGER_PROTO_LOG =
+ new DesktopModeFlag(Flags::enableStateManagerProtoLog, true);
public static void logGoToState(
@NonNull Object fromState, @NonNull Object toState, @NonNull String trace) {
- if (!enableStateManagerProtoLog() || !isProtoLogInitialized()) return;
+ if (!ENABLE_STATE_MANAGER_PROTO_LOG.isTrue() || !isProtoLogInitialized()) return;
ProtoLog.d(LAUNCHER_STATE_MANAGER,
"StateManager.goToState: fromState: %s, toState: %s, partial trace:\n%s",
fromState,
@@ -41,7 +44,7 @@
public static void logCreateAtomicAnimation(
@NonNull Object fromState, @NonNull Object toState, @NonNull String trace) {
- if (!enableStateManagerProtoLog() || !isProtoLogInitialized()) return;
+ if (!ENABLE_STATE_MANAGER_PROTO_LOG.isTrue() || !isProtoLogInitialized()) return;
ProtoLog.d(LAUNCHER_STATE_MANAGER, "StateManager.createAtomicAnimation: "
+ "fromState: %s, toState: %s, partial trace:\n%s",
fromState,
@@ -50,17 +53,17 @@
}
public static void logOnStateTransitionStart(@NonNull Object state) {
- if (!enableStateManagerProtoLog() || !isProtoLogInitialized()) return;
+ if (!ENABLE_STATE_MANAGER_PROTO_LOG.isTrue() || !isProtoLogInitialized()) return;
ProtoLog.d(LAUNCHER_STATE_MANAGER, "StateManager.onStateTransitionStart: state: %s", state);
}
public static void logOnStateTransitionEnd(@NonNull Object state) {
- if (!enableStateManagerProtoLog() || !isProtoLogInitialized()) return;
+ if (!ENABLE_STATE_MANAGER_PROTO_LOG.isTrue() || !isProtoLogInitialized()) return;
ProtoLog.d(LAUNCHER_STATE_MANAGER, "StateManager.onStateTransitionEnd: state: %s", state);
}
public static void logCancelAnimation(boolean animationOngoing, @NonNull String trace) {
- if (!enableStateManagerProtoLog() || !isProtoLogInitialized()) return;
+ if (!ENABLE_STATE_MANAGER_PROTO_LOG.isTrue() || !isProtoLogInitialized()) return;
ProtoLog.d(LAUNCHER_STATE_MANAGER,
"StateManager.cancelAnimation: animation ongoing: %b, partial trace:\n%s",
animationOngoing,
diff --git a/quickstep/src_protolog/com/android/quickstep/util/RecentsWindowProtoLogProxy.java b/quickstep/src_protolog/com/android/quickstep/util/RecentsWindowProtoLogProxy.java
index 2c9ae33..99888fb 100644
--- a/quickstep/src_protolog/com/android/quickstep/util/RecentsWindowProtoLogProxy.java
+++ b/quickstep/src_protolog/com/android/quickstep/util/RecentsWindowProtoLogProxy.java
@@ -16,14 +16,16 @@
package com.android.quickstep.util;
-import static com.android.launcher3.Flags.enableRecentsWindowProtoLog;
import static com.android.quickstep.util.QuickstepProtoLogGroup.RECENTS_WINDOW;
import static com.android.quickstep.util.QuickstepProtoLogGroup.isProtoLogInitialized;
+import android.window.DesktopModeFlags;
+
import androidx.annotation.NonNull;
import com.android.internal.protolog.ProtoLog;
import com.android.internal.protolog.common.IProtoLogGroup;
+import com.android.launcher3.Flags;
/**
* Proxy class used for Recents Window ProtoLog support.
@@ -35,19 +37,20 @@
* method. Or, if an existing entry needs to be modified, simply update it here.
*/
public class RecentsWindowProtoLogProxy {
-
+ private static final DesktopModeFlags.DesktopModeFlag ENABLE_RECENTS_WINDOW_PROTO_LOG =
+ new DesktopModeFlags.DesktopModeFlag(Flags::enableRecentsWindowProtoLog, true);
public static void logOnStateSetStart(@NonNull String stateName) {
- if (!enableRecentsWindowProtoLog() || !isProtoLogInitialized()) return;
+ if (!ENABLE_RECENTS_WINDOW_PROTO_LOG.isTrue() || !isProtoLogInitialized()) return;
ProtoLog.d(RECENTS_WINDOW, "onStateSetStart: %s", stateName);
}
public static void logOnStateSetEnd(@NonNull String stateName) {
- if (!enableRecentsWindowProtoLog() || !isProtoLogInitialized()) return;
+ if (!ENABLE_RECENTS_WINDOW_PROTO_LOG.isTrue() || !isProtoLogInitialized()) return;
ProtoLog.d(RECENTS_WINDOW, "onStateSetEnd: %s", stateName);
}
public static void logStartRecentsWindow(boolean isShown, boolean windowViewIsNull) {
- if (!enableRecentsWindowProtoLog() || !isProtoLogInitialized()) return;
+ if (!ENABLE_RECENTS_WINDOW_PROTO_LOG.isTrue() || !isProtoLogInitialized()) return;
ProtoLog.d(RECENTS_WINDOW,
"Starting recents window: isShow= %b, windowViewIsNull=%b",
isShown,
@@ -55,7 +58,7 @@
}
public static void logCleanup(boolean isShown) {
- if (!enableRecentsWindowProtoLog() || !isProtoLogInitialized()) return;
+ if (!ENABLE_RECENTS_WINDOW_PROTO_LOG.isTrue() || !isProtoLogInitialized()) return;
ProtoLog.d(RECENTS_WINDOW, "Cleaning up recents window: isShow= %b", isShown);
}
}
diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/RecentTasksListTest.java b/quickstep/tests/multivalentTests/src/com/android/quickstep/RecentTasksListTest.java
index 70bf6b4..9722e9d 100644
--- a/quickstep/tests/multivalentTests/src/com/android/quickstep/RecentTasksListTest.java
+++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/RecentTasksListTest.java
@@ -49,6 +49,8 @@
import androidx.test.filters.SmallTest;
import com.android.internal.R;
+import com.android.launcher3.statehandlers.DesktopVisibilityController;
+import com.android.launcher3.util.DaggerSingletonTracker;
import com.android.launcher3.util.LooperExecutor;
import com.android.quickstep.util.GroupTask;
import com.android.quickstep.views.TaskViewType;
@@ -102,7 +104,9 @@
.thenReturn(true);
mRecentTasksList = new RecentTasksList(mContext, mockMainThreadExecutor,
- mockKeyguardManager, mSystemUiProxy, mTopTaskTracker);
+ mockKeyguardManager, mSystemUiProxy, mTopTaskTracker,
+ mock(DesktopVisibilityController.class),
+ mock(DaggerSingletonTracker.class));
}
@Test
diff --git a/src/android/os/BinderUtils.kt b/src/android/os/BinderUtils.kt
new file mode 100644
index 0000000..0536283
--- /dev/null
+++ b/src/android/os/BinderUtils.kt
@@ -0,0 +1,51 @@
+/*
+ * Copyright (C) 2025 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 android.os
+
+import com.android.launcher3.util.Executors.MAIN_EXECUTOR
+import com.android.launcher3.util.WeakCleanupSet
+import com.android.launcher3.util.WeakCleanupSet.OnOwnerDestroyedCallback
+
+/** Utility methods related to Binder */
+object BinderUtils {
+
+ /** Creates a binder wrapper which is tied to the [lifecycle] */
+ @JvmStatic
+ fun <T : Binder> T.wrapLifecycle(cleanupSet: WeakCleanupSet): Binder =
+ LifecycleBinderWrapper(this, cleanupSet)
+
+ private class LifecycleBinderWrapper<T : Binder>(
+ private var realBinder: T?,
+ cleanupSet: WeakCleanupSet,
+ ) : Binder(realBinder?.interfaceDescriptor), OnOwnerDestroyedCallback {
+
+ init {
+ MAIN_EXECUTOR.execute { cleanupSet.addOnOwnerDestroyedCallback(this) }
+ }
+
+ override fun queryLocalInterface(descriptor: String): IInterface? =
+ realBinder?.queryLocalInterface(descriptor)
+
+ override fun onTransact(code: Int, data: Parcel, reply: Parcel?, flags: Int): Boolean =
+ realBinder?.transact(code, data, reply, flags)
+ ?: throw RemoteException("Original binder cleaned up")
+
+ override fun onOwnerDestroyed() {
+ realBinder = null
+ }
+ }
+}
diff --git a/src/com/android/launcher3/BaseActivity.java b/src/com/android/launcher3/BaseActivity.java
index 3e6b4dd..2426a61 100644
--- a/src/com/android/launcher3/BaseActivity.java
+++ b/src/com/android/launcher3/BaseActivity.java
@@ -55,6 +55,7 @@
import com.android.launcher3.util.RunnableList;
import com.android.launcher3.util.SystemUiController;
import com.android.launcher3.util.ViewCache;
+import com.android.launcher3.util.WeakCleanupSet;
import com.android.launcher3.util.WindowBounds;
import com.android.launcher3.views.ActivityContext;
import com.android.launcher3.views.ScrimView;
@@ -105,6 +106,7 @@
private final SavedStateRegistryController mSavedStateRegistryController =
SavedStateRegistryController.create(this);
private final LifecycleRegistry mLifecycleRegistry = new LifecycleRegistry(this);
+ private final WeakCleanupSet mCleanupSet = new WeakCleanupSet(this);
protected DeviceProfile mDeviceProfile;
protected SystemUiController mSystemUiController;
@@ -504,6 +506,11 @@
return mLifecycleRegistry;
}
+ @Override
+ public WeakCleanupSet getOwnerCleanupSet() {
+ return mCleanupSet;
+ }
+
public static <T extends BaseActivity> T fromContext(Context context) {
if (context instanceof BaseActivity) {
return (T) context;
diff --git a/src/com/android/launcher3/Utilities.java b/src/com/android/launcher3/Utilities.java
index cb3a0bc..9a9bc1d 100644
--- a/src/com/android/launcher3/Utilities.java
+++ b/src/com/android/launcher3/Utilities.java
@@ -18,6 +18,7 @@
import static com.android.launcher3.BuildConfig.WIDGET_ON_FIRST_SCREEN;
import static com.android.launcher3.Flags.enableSmartspaceAsAWidget;
+import static com.android.launcher3.graphics.ShapeDelegate.DEFAULT_PATH_SIZE;
import static com.android.launcher3.icons.BitmapInfo.FLAG_THEMED;
import static com.android.launcher3.util.SplitConfigurationOptions.STAGE_POSITION_BOTTOM_OR_RIGHT;
import static com.android.launcher3.util.SplitConfigurationOptions.STAGE_POSITION_TOP_OR_LEFT;
@@ -39,6 +40,7 @@
import android.graphics.LightingColorFilter;
import android.graphics.Matrix;
import android.graphics.Paint;
+import android.graphics.Path;
import android.graphics.Point;
import android.graphics.PointF;
import android.graphics.Rect;
@@ -632,7 +634,7 @@
Drawable badge = null;
if ((info instanceof ItemInfoWithIcon iiwi) && !iiwi.getMatchingLookupFlag().useLowRes()) {
- badge = iiwi.bitmap.getBadgeDrawable(context, useTheme);
+ badge = iiwi.bitmap.getBadgeDrawable(context, useTheme, getIconShapeOrNull(context));
}
if (info instanceof PendingAddShortcutInfo) {
@@ -659,8 +661,11 @@
// Only fetch badge if the icon is on workspace
if (info.id != ItemInfo.NO_ID && badge == null) {
badge = appState.getIconCache().getShortcutInfoBadge(si).newIcon(
- context, ThemeManager.INSTANCE.get(context).isIconThemeEnabled()
- ? FLAG_THEMED : 0);
+ context,
+ ThemeManager.INSTANCE.get(context).isIconThemeEnabled()
+ ? FLAG_THEMED : 0,
+ getIconShapeOrNull(context)
+ );
}
}
} else if (info.itemType == LauncherSettings.Favorites.ITEM_TYPE_FOLDER) {
@@ -706,10 +711,11 @@
if (badge == null) {
badge = BitmapInfo.LOW_RES_INFO.withFlags(
- UserCache.INSTANCE.get(context)
- .getUserInfo(info.user)
- .applyBitmapInfoFlags(FlagOp.NO_OP))
- .getBadgeDrawable(context, useTheme);
+ UserCache.INSTANCE.get(context)
+ .getUserInfo(info.user)
+ .applyBitmapInfoFlags(FlagOp.NO_OP)
+ )
+ .getBadgeDrawable(context, useTheme, getIconShapeOrNull(context));
if (badge == null) {
badge = new ColorDrawable(Color.TRANSPARENT);
}
@@ -939,4 +945,18 @@
}
return null;
}
+
+ /**
+ * Returns current icon shape to use for badges if flag is on, otherwise null.
+ */
+ @Nullable
+ public static Path getIconShapeOrNull(Context context) {
+ if (Flags.enableLauncherIconShapes()) {
+ return ThemeManager.INSTANCE.get(context)
+ .getIconShape()
+ .getPath(DEFAULT_PATH_SIZE);
+ } else {
+ return null;
+ }
+ }
}
diff --git a/src/com/android/launcher3/graphics/ShapeDelegate.kt b/src/com/android/launcher3/graphics/ShapeDelegate.kt
index 9033eac..7c04292 100644
--- a/src/com/android/launcher3/graphics/ShapeDelegate.kt
+++ b/src/com/android/launcher3/graphics/ShapeDelegate.kt
@@ -203,7 +203,11 @@
start =
poly.transformed(
Matrix().apply {
- setRectToRect(RectF(0f, 0f, 100f, 100f), RectF(startRect), FILL)
+ setRectToRect(
+ RectF(0f, 0f, DEFAULT_PATH_SIZE, DEFAULT_PATH_SIZE),
+ RectF(startRect),
+ FILL,
+ )
}
),
end =
@@ -281,7 +285,10 @@
PathParser.createPathFromPathData(shapeStr).apply {
transform(
Matrix().apply {
- setScale(AREA_CALC_SIZE / 100f, AREA_CALC_SIZE / 100f)
+ setScale(
+ AREA_CALC_SIZE / DEFAULT_PATH_SIZE,
+ AREA_CALC_SIZE / DEFAULT_PATH_SIZE,
+ )
}
)
}
diff --git a/src/com/android/launcher3/model/data/ItemInfoWithIcon.java b/src/com/android/launcher3/model/data/ItemInfoWithIcon.java
index b60b8cc..f5e5e16 100644
--- a/src/com/android/launcher3/model/data/ItemInfoWithIcon.java
+++ b/src/com/android/launcher3/model/data/ItemInfoWithIcon.java
@@ -26,6 +26,7 @@
import androidx.annotation.Nullable;
import com.android.launcher3.Flags;
+import com.android.launcher3.Utilities;
import com.android.launcher3.graphics.ThemeManager;
import com.android.launcher3.icons.BitmapInfo;
import com.android.launcher3.icons.BitmapInfo.DrawableCreationFlags;
@@ -325,10 +326,12 @@
* Returns a FastBitmapDrawable with the icon and context theme applied
*/
public FastBitmapDrawable newIcon(Context context, @DrawableCreationFlags int creationFlags) {
- if (!ThemeManager.INSTANCE.get(context).isIconThemeEnabled()) {
+ ThemeManager themeManager = ThemeManager.INSTANCE.get(context);
+ if (!themeManager.isIconThemeEnabled()) {
creationFlags &= ~FLAG_THEMED;
}
- FastBitmapDrawable drawable = bitmap.newIcon(context, creationFlags);
+ FastBitmapDrawable drawable = bitmap.newIcon(
+ context, creationFlags, Utilities.getIconShapeOrNull(context));
drawable.setIsDisabled(isDisabled());
return drawable;
}
diff --git a/src/com/android/launcher3/pm/UserCache.java b/src/com/android/launcher3/pm/UserCache.java
index 20c0ecc..98a3882 100644
--- a/src/com/android/launcher3/pm/UserCache.java
+++ b/src/com/android/launcher3/pm/UserCache.java
@@ -219,6 +219,6 @@
public static UserBadgeDrawable getBadgeDrawable(Context context, UserHandle userHandle) {
return (UserBadgeDrawable) BitmapInfo.LOW_RES_INFO.withFlags(UserCache.getInstance(context)
.getUserInfo(userHandle).applyBitmapInfoFlags(FlagOp.NO_OP))
- .getBadgeDrawable(context, false /* isThemed */);
+ .getBadgeDrawable(context, false /* isThemed */, null);
}
}
diff --git a/src/com/android/launcher3/util/BaseContext.kt b/src/com/android/launcher3/util/BaseContext.kt
index 819470b..8aa10f3 100644
--- a/src/com/android/launcher3/util/BaseContext.kt
+++ b/src/com/android/launcher3/util/BaseContext.kt
@@ -49,6 +49,7 @@
private val savedStateRegistryController = SavedStateRegistryController.create(this)
private val lifecycleRegistry = LifecycleRegistry(this)
+ private val cleanupSet = WeakCleanupSet(this)
override val savedStateRegistry: SavedStateRegistry
get() = savedStateRegistryController.savedStateRegistry
@@ -110,6 +111,8 @@
override fun getViewCache() = viewCache
+ override fun getOwnerCleanupSet() = cleanupSet
+
private fun updateState() {
if (lifecycleRegistry.currentState.isAtLeast(CREATED)) {
lifecycleRegistry.currentState =
diff --git a/src/com/android/launcher3/util/WeakCleanupSet.kt b/src/com/android/launcher3/util/WeakCleanupSet.kt
new file mode 100644
index 0000000..7bf3289
--- /dev/null
+++ b/src/com/android/launcher3/util/WeakCleanupSet.kt
@@ -0,0 +1,56 @@
+/*
+ * Copyright (C) 2025 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.util
+
+import androidx.lifecycle.DefaultLifecycleObserver
+import androidx.lifecycle.LifecycleOwner
+import com.android.launcher3.util.Executors.MAIN_EXECUTOR
+import java.util.Collections
+import java.util.WeakHashMap
+
+/**
+ * Utility class which maintains a list of cleanup callbacks using weak-references. These callbacks
+ * are called when the [owner] is destroyed, but can also be cleared when the caller is GCed
+ */
+class WeakCleanupSet(owner: LifecycleOwner) {
+
+ private val callbacks = Collections.newSetFromMap<OnOwnerDestroyedCallback>(WeakHashMap())
+ private var destroyed = false
+
+ init {
+ MAIN_EXECUTOR.execute {
+ owner.lifecycle.addObserver(
+ object : DefaultLifecycleObserver {
+
+ override fun onDestroy(owner: LifecycleOwner) {
+ destroyed = true
+ callbacks.forEach { it.onOwnerDestroyed() }
+ }
+ }
+ )
+ }
+ }
+
+ fun addOnOwnerDestroyedCallback(callback: OnOwnerDestroyedCallback) {
+ if (destroyed) callback.onOwnerDestroyed() else callbacks.add(callback)
+ }
+
+ /** Callback when the owner is destroyed */
+ interface OnOwnerDestroyedCallback {
+ fun onOwnerDestroyed()
+ }
+}
diff --git a/src/com/android/launcher3/util/window/WindowManagerProxy.java b/src/com/android/launcher3/util/window/WindowManagerProxy.java
index 11f0bc2..68e0324 100644
--- a/src/com/android/launcher3/util/window/WindowManagerProxy.java
+++ b/src/com/android/launcher3/util/window/WindowManagerProxy.java
@@ -520,6 +520,33 @@
*/
default void onCanCreateDesksChanged(boolean canCreateDesks) {
}
+
+ /**
+ * Called when a new desk is added.
+ *
+ * @param displayId The ID of the display on which the desk was added.
+ * @param deskId The ID of the newly added desk.
+ */
+ default void onDeskAdded(int displayId, int deskId) {}
+
+ /**
+ * Called when an existing desk is removed.
+ *
+ * @param displayId The ID of the display on which the desk was removed.
+ * @param deskId The ID of the desk that was removed.
+ */
+ default void onDeskRemoved(int displayId, int deskId) {}
+
+ /**
+ * Called when the active desk changes.
+ *
+ * @param displayId The ID of the display on which the desk activation change is happening.
+ * @param newActiveDesk The ID of the new active desk or -1 if no desk is active anymore
+ * (i.e. exit desktop mode).
+ * @param oldActiveDesk The ID of the desk that was previously active, or -1 if no desk was
+ * active before.
+ */
+ default void onActiveDeskChanged(int displayId, int newActiveDesk, int oldActiveDesk) {}
}
}
diff --git a/src/com/android/launcher3/views/ActivityContext.java b/src/com/android/launcher3/views/ActivityContext.java
index bcb9295..cbf5341 100644
--- a/src/com/android/launcher3/views/ActivityContext.java
+++ b/src/com/android/launcher3/views/ActivityContext.java
@@ -87,6 +87,7 @@
import com.android.launcher3.util.SplitConfigurationOptions;
import com.android.launcher3.util.SystemUiController;
import com.android.launcher3.util.ViewCache;
+import com.android.launcher3.util.WeakCleanupSet;
import com.android.launcher3.widget.picker.model.WidgetPickerDataProvider;
import java.util.List;
@@ -520,6 +521,9 @@
return new CellPosMapper(dp.isVerticalBarLayout(), dp.numShownHotseatIcons);
}
+ /** Set to manage objects that can be cleaned up along with the context */
+ WeakCleanupSet getOwnerCleanupSet();
+
/** Whether bubbles are enabled. */
default boolean isBubbleBarEnabled() {
return false;
diff --git a/src/com/android/launcher3/widget/picker/WidgetRecommendationsView.java b/src/com/android/launcher3/widget/picker/WidgetRecommendationsView.java
index 4ccf16b..ec91622 100644
--- a/src/com/android/launcher3/widget/picker/WidgetRecommendationsView.java
+++ b/src/com/android/launcher3/widget/picker/WidgetRecommendationsView.java
@@ -16,6 +16,7 @@
package com.android.launcher3.widget.picker;
+import static com.android.launcher3.widget.picker.WidgetRecommendationCategory.DEFAULT_WIDGET_RECOMMENDATION_CATEGORY;
import static com.android.launcher3.widget.util.WidgetsTableUtils.groupWidgetItemsUsingRowPxWithReordering;
import android.content.ComponentName;
@@ -55,6 +56,7 @@
public final class WidgetRecommendationsView extends PagedView<PageIndicatorDots> {
private @Px float mAvailableHeight = Float.MAX_VALUE;
private @Px float mAvailableWidth = 0;
+ private int mLastUiMode = -1;
private static final String INITIALLY_DISPLAYED_WIDGETS_STATE_KEY =
"widgetRecommendationsView:mDisplayedWidgets";
private static final int MAX_CATEGORIES = 3;
@@ -151,41 +153,7 @@
mPageSwitchListeners.add(pageChangeListener);
}
- /**
- * Displays all the provided recommendations in a single table if they fit.
- *
- * @param recommendedWidgets list of widgets to be displayed in recommendation section.
- * @param deviceProfile the current {@link DeviceProfile}
- * @param availableHeight height in px that can be used to display the recommendations;
- * recommendations that don't fit in this height won't be shown
- * @param availableWidth width in px that the recommendations should display in
- * @param cellPadding padding in px that should be applied to each widget in the
- * recommendations
- * @return number of recommendations that could fit in the available space.
- */
- public int setRecommendations(
- List<WidgetItem> recommendedWidgets, DeviceProfile deviceProfile,
- final @Px float availableHeight, final @Px int availableWidth,
- final @Px int cellPadding) {
- this.mAvailableHeight = availableHeight;
- this.mAvailableWidth = availableWidth;
- clear();
-
- Set<ComponentName> displayedWidgets = maybeDisplayInTable(recommendedWidgets,
- deviceProfile,
- availableWidth, cellPadding);
-
- if (mDisplayedWidgets.isEmpty()) {
- // Save the widgets shown for the first time user opened the picker; so that, they can
- // be maintained across orientation changes.
- mDisplayedWidgets = displayedWidgets;
- }
-
- updateTitleAndIndicator(/* requestedPage= */ 0);
- return displayedWidgets.size();
- }
-
- private boolean shouldShowFullPageView(
+ private boolean shouldShowSinglePageView(
Map<WidgetRecommendationCategory, List<WidgetItem>> recommendations) {
if (mShowFullPageViewIfLowDensity) {
boolean hasLessCategories = recommendations.size() < MAX_CATEGORIES;
@@ -213,63 +181,82 @@
* @param cellPadding padding in px that should be applied to each widget in the
* recommendations
* @param requestedPage page number to display initially.
+ * @param forceUpdate whether to re-render even if available space didn't change
* @return number of recommendations that could fit in the available space.
*/
public int setRecommendations(
Map<WidgetRecommendationCategory, List<WidgetItem>> recommendations,
DeviceProfile deviceProfile, final @Px float availableHeight,
- final @Px int availableWidth, final @Px int cellPadding, final int requestedPage) {
- if (shouldShowFullPageView(recommendations)) {
- // Show all widgets in single page with unlimited available height.
- return setRecommendations(
- recommendations.values().stream().flatMap(Collection::stream)
- .collect(Collectors.toList()),
- deviceProfile, /*availableHeight=*/ Float.MAX_VALUE, availableWidth,
- cellPadding);
+ final @Px int availableWidth, final @Px int cellPadding, final int requestedPage,
+ final boolean forceUpdate) {
+ if (forceUpdate || shouldUpdate(availableWidth, availableHeight)) {
+ Context context = getContext();
+ this.mAvailableHeight = availableHeight;
+ this.mAvailableWidth = availableWidth;
+ this.mLastUiMode = context.getResources().getConfiguration().uiMode;
- }
- this.mAvailableHeight = availableHeight;
- this.mAvailableWidth = availableWidth;
- Context context = getContext();
- // For purpose of recommendations section, we don't want paging dots to be halved in two
- // pane display, so, we always provide isTwoPanels = "false".
- mPageIndicator.setPauseScroll(/*pause=*/true, /*isTwoPanels=*/ false);
- clear();
-
- int displayedCategories = 0;
- Set<ComponentName> allDisplayedWidgets = new HashSet<>();
-
- // Render top MAX_CATEGORIES in separate tables. Each table becomes a page.
- for (Map.Entry<WidgetRecommendationCategory, List<WidgetItem>> entry :
- new TreeMap<>(recommendations).entrySet()) {
- // If none of the recommendations for the category could fit in the mAvailableHeight, we
- // don't want to add that category; and we look for the next one.
- Set<ComponentName> displayedWidgetsForCategory = maybeDisplayInTable(entry.getValue(),
- deviceProfile,
- availableWidth, cellPadding);
- if (!displayedWidgetsForCategory.isEmpty()) {
- mCategoryTitles.add(
- context.getResources().getString(entry.getKey().categoryTitleRes));
- displayedCategories++;
- allDisplayedWidgets.addAll(displayedWidgetsForCategory);
+ final Map<WidgetRecommendationCategory, List<WidgetItem>> mappedRecommendations;
+ if (shouldShowSinglePageView(recommendations)) { // map to single category.
+ mappedRecommendations = Map.of(DEFAULT_WIDGET_RECOMMENDATION_CATEGORY,
+ recommendations.values().stream().flatMap(
+ Collection::stream).toList());
+ } else {
+ mappedRecommendations = recommendations;
}
- if (displayedCategories == MAX_CATEGORIES) {
- break;
+ // For purpose of recommendations section, we don't want paging dots to be halved in two
+ // pane display, so, we always provide isTwoPanels = "false".
+ mPageIndicator.setPauseScroll(/*pause=*/true, /*isTwoPanels=*/ false);
+ clear();
+
+ int displayedCategories = 0;
+ Set<ComponentName> allDisplayedWidgets = new HashSet<>();
+
+ // Render top MAX_CATEGORIES in separate tables. Each table becomes a page.
+ for (Map.Entry<WidgetRecommendationCategory, List<WidgetItem>> entry :
+ new TreeMap<>(mappedRecommendations).entrySet()) {
+ // If none of the recommendations for the category could fit in the
+ // mAvailableHeight, we don't want to add that category; and we look for the next
+ // one.
+ Set<ComponentName> displayedWidgetsForCategory = maybeDisplayInTable(
+ entry.getValue(),
+ deviceProfile,
+ availableWidth, cellPadding);
+ if (!displayedWidgetsForCategory.isEmpty()) {
+ mCategoryTitles.add(
+ context.getResources().getString(entry.getKey().categoryTitleRes));
+ displayedCategories++;
+ allDisplayedWidgets.addAll(displayedWidgetsForCategory);
+ }
+
+ if (displayedCategories == MAX_CATEGORIES) {
+ break;
+ }
}
- }
- if (mDisplayedWidgets.isEmpty()) {
- // Save the widgets shown for the first time user opened the picker; so that, they can
- // be maintained across orientation changes.
- mDisplayedWidgets = allDisplayedWidgets;
- }
+ if (mDisplayedWidgets.isEmpty()) {
+ // Save the widgets shown for the first time user opened the picker; so that,
+ // they can
+ // be maintained across orientation changes.
+ mDisplayedWidgets = allDisplayedWidgets;
+ }
- updateTitleAndIndicator(requestedPage);
- // For purpose of recommendations section, we don't want paging dots to be halved in two
- // pane display, so, we always provide isTwoPanels = "false".
- mPageIndicator.setPauseScroll(/*pause=*/false, /*isTwoPanels=*/false);
- return allDisplayedWidgets.size();
+ updateTitleAndIndicator(requestedPage);
+ // For purpose of recommendations section, we don't want paging dots to be halved in two
+ // pane display, so, we always provide isTwoPanels = "false".
+ mPageIndicator.setPauseScroll(/*pause=*/false, /*isTwoPanels=*/false);
+ return allDisplayedWidgets.size();
+ } else {
+ return mDisplayedWidgets.size();
+ }
+ }
+
+ /**
+ * Returns if we should re-render the views.
+ */
+ private boolean shouldUpdate(int availableWidth, float availableHeight) {
+ return this.mAvailableWidth != availableWidth || this.mAvailableHeight != availableHeight
+ || getContext().getResources().getConfiguration().uiMode != this.mLastUiMode;
}
private void clear() {
diff --git a/src/com/android/launcher3/widget/picker/WidgetsFullSheet.java b/src/com/android/launcher3/widget/picker/WidgetsFullSheet.java
index b0abf23..44c0ebd 100644
--- a/src/com/android/launcher3/widget/picker/WidgetsFullSheet.java
+++ b/src/com/android/launcher3/widget/picker/WidgetsFullSheet.java
@@ -628,10 +628,12 @@
if (mIsInSearchMode) {
return;
}
+ boolean forceUpdate = false;
// We avoid applying new recommendations when some are already displayed.
if (mRecommendedWidgetsMap.isEmpty()) {
mRecommendedWidgetsMap =
mActivityContext.getWidgetPickerDataProvider().get().getRecommendations();
+ forceUpdate = true;
}
mRecommendedWidgetsCount = mWidgetRecommendationsView.setRecommendations(
mRecommendedWidgetsMap,
@@ -639,7 +641,8 @@
/* availableHeight= */ getMaxAvailableHeightForRecommendations(),
/* availableWidth= */ mMaxSpanPerRow,
/* cellPadding= */ mWidgetCellHorizontalPadding,
- /* requestedPage= */ mRecommendationsCurrentPage
+ /* requestedPage= */ mRecommendationsCurrentPage,
+ /* forceUpdate= */ forceUpdate
);
mWidgetRecommendationsContainer.setVisibility(
diff --git a/src/com/android/launcher3/widget/picker/WidgetsListTableViewHolderBinder.java b/src/com/android/launcher3/widget/picker/WidgetsListTableViewHolderBinder.java
index 679b0f5..fc99fcc 100644
--- a/src/com/android/launcher3/widget/picker/WidgetsListTableViewHolderBinder.java
+++ b/src/com/android/launcher3/widget/picker/WidgetsListTableViewHolderBinder.java
@@ -112,21 +112,48 @@
// Bind the widget items.
for (int i = 0; i < widgetItemsTable.size(); i++) {
List<WidgetItem> widgetItemsPerRow = widgetItemsTable.get(i);
- for (int j = 0; j < widgetItemsPerRow.size(); j++) {
- WidgetTableRow row = (WidgetTableRow) table.getChildAt(i);
- row.setVisibility(View.VISIBLE);
- WidgetCell widget = (WidgetCell) row.getChildAt(j);
- widget.clear();
- widget.addPreviewReadyListener(row);
- WidgetItem widgetItem = widgetItemsPerRow.get(j);
- widget.setVisibility(View.VISIBLE);
+ WidgetTableRow row = (WidgetTableRow) table.getChildAt(i);
- widget.applyFromCellItem(widgetItem);
- widget.requestLayout();
+ if (areRowItemsUnchanged(row, widgetItemsPerRow)) { // Just show widgets in row as is
+ row.setVisibility(View.VISIBLE);
+ for (int j = 0; j < widgetItemsPerRow.size(); j++) {
+ WidgetCell widget = (WidgetCell) row.getChildAt(j);
+ widget.setVisibility(View.VISIBLE);
+ }
+ } else {
+ for (int j = 0; j < widgetItemsPerRow.size(); j++) {
+ row.setVisibility(View.VISIBLE);
+ WidgetCell widget = (WidgetCell) row.getChildAt(j);
+ widget.clear();
+ WidgetItem widgetItem = widgetItemsPerRow.get(j);
+ widget.addPreviewReadyListener(row);
+ widget.setVisibility(View.VISIBLE);
+
+ widget.applyFromCellItem(widgetItem);
+ widget.requestLayout();
+ }
}
}
}
+ private boolean areRowItemsUnchanged(WidgetTableRow row, List<WidgetItem> widgetItemsPerRow) {
+ // NOTE: on rotation or fold / unfold, we bind different view holders
+ // so, we don't any special handling for that case.
+ if (row.getChildCount() != widgetItemsPerRow.size()) { // Items not equal
+ return false;
+ }
+
+ for (int j = 0; j < widgetItemsPerRow.size(); j++) {
+ WidgetCell widgetCell = (WidgetCell) row.getChildAt(j);
+ WidgetItem widgetItem = widgetItemsPerRow.get(j);
+ if (widgetCell.getWidgetItem() == null
+ || !widgetCell.getWidgetItem().equals(widgetItem)) {
+ return false; // Items at given position in row aren't same.
+ }
+ }
+ return true;
+ }
+
/**
* Adds and hides table rows and columns from {@code table} to ensure there is sufficient room
* to display {@code widgetItemsTable}.
@@ -151,26 +178,31 @@
tableRow.setGravity(Gravity.TOP);
table.addView(tableRow);
}
- // Pass resize delay to let the "move" and "change" animations run before resizing the
- // row.
- tableRow.setupRow(widgetItems.size(),
- /*resizeDelayMs=*/ WIDGET_LIST_ITEM_APPEARANCE_START_DELAY);
- if (tableRow.getChildCount() > widgetItems.size()) {
- for (int j = widgetItems.size(); j < tableRow.getChildCount(); j++) {
- tableRow.getChildAt(j).setVisibility(View.GONE);
- }
- } else {
- for (int j = tableRow.getChildCount(); j < widgetItems.size(); j++) {
- WidgetCell widget = (WidgetCell) mLayoutInflater.inflate(
- R.layout.widget_cell, tableRow, false);
- // set up touch.
- widget.setOnClickListener(mIconClickListener);
- widget.addPreviewReadyListener(tableRow);
- View preview = widget.findViewById(R.id.widget_preview_container);
- preview.setOnClickListener(mIconClickListener);
- preview.setOnLongClickListener(mIconLongClickListener);
- widget.setAnimatePreview(false);
- tableRow.addView(widget);
+
+ // If the row items are unchanged, we don't need to re-setup the row or the items;
+ // we can just show the row as is.
+ if (!areRowItemsUnchanged(tableRow, widgetItems)) {
+ // Pass resize delay to let the "move" and "change" animations run before resizing
+ // the row.
+ tableRow.setupRow(widgetItems.size(),
+ /*resizeDelayMs=*/ WIDGET_LIST_ITEM_APPEARANCE_START_DELAY);
+ if (tableRow.getChildCount() > widgetItems.size()) {
+ for (int j = widgetItems.size(); j < tableRow.getChildCount(); j++) {
+ tableRow.getChildAt(j).setVisibility(View.GONE);
+ }
+ } else {
+ for (int j = tableRow.getChildCount(); j < widgetItems.size(); j++) {
+ WidgetCell widget = (WidgetCell) mLayoutInflater.inflate(
+ R.layout.widget_cell, tableRow, false);
+ // set up touch.
+ widget.setOnClickListener(mIconClickListener);
+ widget.addPreviewReadyListener(tableRow);
+ View preview = widget.findViewById(R.id.widget_preview_container);
+ preview.setOnClickListener(mIconClickListener);
+ preview.setOnLongClickListener(mIconLongClickListener);
+ widget.setAnimatePreview(false);
+ tableRow.addView(widget);
+ }
}
}
}
diff --git a/tests/multivalentTests/src/com/android/launcher3/icons/UserBadgeDrawableTest.kt b/tests/multivalentTests/src/com/android/launcher3/icons/UserBadgeDrawableTest.kt
index d611ae8..91ba628 100644
--- a/tests/multivalentTests/src/com/android/launcher3/icons/UserBadgeDrawableTest.kt
+++ b/tests/multivalentTests/src/com/android/launcher3/icons/UserBadgeDrawableTest.kt
@@ -34,19 +34,20 @@
private val context = InstrumentationRegistry.getInstrumentation().targetContext
private val canvas = mock<Canvas>()
private val systemUnderTest =
- UserBadgeDrawable(context, R.drawable.ic_work_app_badge, R.color.badge_tint_work, false)
+ UserBadgeDrawable(
+ context,
+ R.drawable.ic_work_app_badge,
+ R.color.badge_tint_work,
+ false /* isThemed */,
+ null, /* shape */
+ )
@Test
fun draw_opaque() {
val colorList = mutableListOf<Int>()
- whenever(
- canvas.drawCircle(
- any(),
- any(),
- any(),
- any()
- )
- ).then { colorList.add(it.getArgument<Paint>(3).color) }
+ whenever(canvas.drawCircle(any(), any(), any(), any())).then {
+ colorList.add(it.getArgument<Paint>(3).color)
+ }
systemUnderTest.alpha = 255
systemUnderTest.draw(canvas)
@@ -57,14 +58,9 @@
@Test
fun draw_transparent() {
val colorList = mutableListOf<Int>()
- whenever(
- canvas.drawCircle(
- any(),
- any(),
- any(),
- any()
- )
- ).then { colorList.add(it.getArgument<Paint>(3).color) }
+ whenever(canvas.drawCircle(any(), any(), any(), any())).then {
+ colorList.add(it.getArgument<Paint>(3).color)
+ }
systemUnderTest.alpha = 0
systemUnderTest.draw(canvas)
diff --git a/tests/src/com/android/launcher3/dragging/TaplUninstallRemoveTest.java b/tests/src/com/android/launcher3/dragging/TaplUninstallRemoveTest.java
index 95d5076..f490bd6 100644
--- a/tests/src/com/android/launcher3/dragging/TaplUninstallRemoveTest.java
+++ b/tests/src/com/android/launcher3/dragging/TaplUninstallRemoveTest.java
@@ -36,7 +36,6 @@
import com.android.launcher3.ui.PortraitLandscapeRunner.PortraitLandscape;
import com.android.launcher3.util.TestUtil;
import com.android.launcher3.util.Wait;
-import com.android.launcher3.util.rule.ScreenRecordRule;
import org.junit.Test;
@@ -127,7 +126,6 @@
* Adds three icons to the workspace and removes one of them by dragging to uninstall.
*/
@Test
- @ScreenRecordRule.ScreenRecord // b/399756302
@PlatinumTest(focusArea = "launcher")
public void uninstallWorkspaceIcon() throws IOException {
Point[] gridPositions = TestUtil.getCornersAndCenterPositions(mLauncher);