diff --git a/quickstep/src/com/android/launcher3/desktop/DesktopRecentsTransitionController.kt b/quickstep/src/com/android/launcher3/desktop/DesktopRecentsTransitionController.kt index 40cfe924c4..a01846dda9 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.statemanager.StateManager 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 @@ class DesktopRecentsTransitionController( 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 eb24df13e1..2402a2827e 100644 --- a/quickstep/src/com/android/launcher3/statehandlers/DesktopVisibilityController.kt +++ b/quickstep/src/com/android/launcher3/statehandlers/DesktopVisibilityController.kt @@ -401,6 +401,39 @@ constructor( 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 @@ constructor( "Found a duplicate desk Id: $deskId on display: $displayId" } } + + notifyOnDeskAdded(displayId, deskId) } private fun onDeskRemoved(displayId: Int, deskId: Int) { @@ -526,6 +561,8 @@ constructor( it.activeDeskId = INACTIVE_DESK_ID } } + + notifyOnDeskRemoved(displayId, deskId) } private fun onActiveDeskChanged(displayId: Int, newActiveDesk: Int, oldActiveDesk: Int) { @@ -539,12 +576,16 @@ constructor( 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 @@ constructor( 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 8555376fd6..ab20c1cdc6 100644 --- a/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchController.java +++ b/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchController.java @@ -170,7 +170,7 @@ public final class KeyboardQuickSwitchController implements mHasDesktopTask, mWasDesktopTaskFilteredOut); }, shouldShowDesktopTasks ? RecentsFilterState.EMPTY_FILTER - : RecentsFilterState.getEmptyDesktopTaskFilter()); + : RecentsFilterState.getDesktopTaskFilter()); } mQuickSwitchViewController.updateLayoutForSurface(wasOpenedFromTaskbar, @@ -232,7 +232,7 @@ public final class KeyboardQuickSwitchController implements mWasDesktopTaskFilteredOut, wasOpenedFromTaskbar); }, shouldShowDesktopTasks ? RecentsFilterState.EMPTY_FILTER - : RecentsFilterState.getEmptyDesktopTaskFilter()); + : RecentsFilterState.getDesktopTaskFilter()); } private boolean shouldExcludeTask(GroupTask task, Set taskIdsToExclude) { diff --git a/quickstep/src/com/android/quickstep/RecentTasksList.java b/quickstep/src/com/android/quickstep/RecentTasksList.java index ac88e5a5c7..cc5b2dabb7 100644 --- a/quickstep/src/com/android/quickstep/RecentTasksList.java +++ b/quickstep/src/com/android/quickstep/RecentTasksList.java @@ -38,8 +38,10 @@ import androidx.annotation.Nullable; 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 @@ import java.util.stream.Collectors; /** * 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 @@ public class RecentTasksList { 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 class RecentTasksList { 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 @@ public class RecentTasksList { 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 @@ public class RecentTasksList { 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/RecentsAnimationTargets.java b/quickstep/src/com/android/quickstep/RecentsAnimationTargets.java index cf7e4993e6..0deb1ca953 100644 --- a/quickstep/src/com/android/quickstep/RecentsAnimationTargets.java +++ b/quickstep/src/com/android/quickstep/RecentsAnimationTargets.java @@ -59,6 +59,10 @@ public class RecentsAnimationTargets extends RemoteAnimationTargets { 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 c4b0f252c3..1808a9769f 100644 --- a/quickstep/src/com/android/quickstep/RecentsFilterState.java +++ b/quickstep/src/com/android/quickstep/RecentsFilterState.java @@ -18,6 +18,7 @@ package com.android.quickstep; 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 @@ public class RecentsFilterState { * 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 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 getEmptyDesktopTaskFilter() { - return (groupTask -> !isDestopTaskWithMinimizedTasksOnly(groupTask)); + public static Predicate 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 1d83d42781..e1adf3dc8e 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.dagger.LauncherAppSingleton; 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.ActivityManagerWrapper; 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 java.util.function.Predicate; import javax.inject.Inject; -import dagger.Lazy; - /** * Singleton class to load and manage recents model. */ @@ -104,12 +105,14 @@ public class RecentsModel implements RecentTasksDataSource, TaskStackChangeListe DisplayController displayController, LockedUserState lockedUserState, Lazy 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 @@ public class RecentsModel implements RecentTasksDataSource, TaskStackChangeListe DisplayController displayController, LockedUserState lockedUserState, Lazy themeManagerLazy, + DesktopVisibilityController desktopVisibilityController, DaggerSingletonTracker tracker) { this(context, new RecentTasksList( @@ -127,7 +131,7 @@ public class RecentsModel implements RecentTasksDataSource, TaskStackChangeListe 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 @@ public class RecentsModel implements RecentTasksDataSource, TaskStackChangeListe @Override public int getTasks(@Nullable Consumer> 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 1de696660c..56b5a77d9c 100644 --- a/quickstep/src/com/android/quickstep/SystemUiProxy.kt +++ b/quickstep/src/com/android/quickstep/SystemUiProxy.kt @@ -1089,18 +1089,26 @@ class SystemUiProxy @Inject constructor(@ApplicationContext private val context: // 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" }) { @@ -1152,9 +1160,9 @@ class SystemUiProxy @Inject constructor(@ApplicationContext private val context: } /** 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/util/DesksUtils.kt b/quickstep/src/com/android/quickstep/util/DesksUtils.kt new file mode 100644 index 0000000000..ccfdbb9f32 --- /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 1d035e9cd4..27657b4c0f 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.enableDesktopExplodedView 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 @@ class DesktopTaskView @JvmOverloads constructor(context: Context, attrs: Attribu type = TaskViewType.DESKTOP, thumbnailFullscreenParams = DesktopFullscreenDrawParams(context), ) { + var deskId = DesktopVisibilityController.INACTIVE_DESK_ID + private val contentViewFullscreenParams = FullscreenDrawParams(context) private val taskContentViewPool = @@ -281,6 +284,7 @@ class DesktopTaskView @JvmOverloads constructor(context: Context, attrs: Attribu 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 @@ class DesktopTaskView @JvmOverloads constructor(context: Context, attrs: Attribu 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 3b943800e4..bd3a46648e 100644 --- a/quickstep/src/com/android/quickstep/views/RecentsView.java +++ b/quickstep/src/com/android/quickstep/views/RecentsView.java @@ -211,6 +211,7 @@ import com.android.quickstep.recents.viewmodel.RecentsViewData; 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,6 +254,7 @@ import java.util.HashMap; 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; @@ -1921,6 +1923,8 @@ public abstract class RecentsView< 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) { @@ -2823,9 +2827,13 @@ public abstract class RecentsView< /** * 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(); @@ -3054,6 +3062,21 @@ public abstract class RecentsView< return matchingTaskView == null; } + /** + * 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}. * @@ -3068,20 +3091,14 @@ public abstract class RecentsView< } 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 @@ -3107,8 +3124,11 @@ public abstract class RecentsView< 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; @@ -3147,6 +3167,10 @@ public abstract class RecentsView< 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; } @@ -4518,24 +4542,33 @@ public abstract class RecentsView< 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(), @@ -4555,6 +4588,12 @@ public abstract class RecentsView< 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( @@ -4684,7 +4723,7 @@ public abstract class RecentsView< private void createDesk(View view) { SystemUiProxy.INSTANCE .get(getContext()) - .createDesktop(mContainer.getDisplay().getDisplayId()); + .createDesk(mContainer.getDisplay().getDisplayId()); } @Override @@ -6914,6 +6953,58 @@ public abstract class RecentsView< // 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 1c37986584..037bef6848 100644 --- a/quickstep/src/com/android/quickstep/views/RecentsViewUtils.kt +++ b/quickstep/src/com/android/quickstep/views/RecentsViewUtils.kt @@ -22,7 +22,11 @@ import android.view.View 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 @@ class RecentsViewUtils(private val recentsView: RecentsView<*, *>) { * @return Sorted list of GroupTasks to be used in the RecentsView. */ fun sortDesktopTasksToFront(tasks: List): List { - 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 @@ class RecentsViewUtils(private val recentsView: RecentsView<*, *>) { 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 91a4273cf3..0a44f37d07 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.util.Log 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 @@ -141,6 +140,7 @@ constructor( /** 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/tests/multivalentTests/src/com/android/quickstep/RecentTasksListTest.java b/quickstep/tests/multivalentTests/src/com/android/quickstep/RecentTasksListTest.java index ad9bbb9534..637902ba32 100644 --- a/quickstep/tests/multivalentTests/src/com/android/quickstep/RecentTasksListTest.java +++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/RecentTasksListTest.java @@ -48,6 +48,8 @@ import androidx.test.ext.junit.runners.AndroidJUnit4; 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; @@ -101,7 +103,9 @@ public class RecentTasksListTest { .thenReturn(true); mRecentTasksList = new RecentTasksList(mContext, mockMainThreadExecutor, - mockKeyguardManager, mSystemUiProxy, mTopTaskTracker); + mockKeyguardManager, mSystemUiProxy, mTopTaskTracker, + mock(DesktopVisibilityController.class), + mock(DaggerSingletonTracker.class)); } @Test diff --git a/src/com/android/launcher3/util/window/WindowManagerProxy.java b/src/com/android/launcher3/util/window/WindowManagerProxy.java index 11f0bc21b9..68e032426b 100644 --- a/src/com/android/launcher3/util/window/WindowManagerProxy.java +++ b/src/com/android/launcher3/util/window/WindowManagerProxy.java @@ -520,6 +520,33 @@ public class WindowManagerProxy { */ 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) {} } }