Loading libs/WindowManager/Shell/Android.bp +4 −2 Original line number Diff line number Diff line Loading @@ -147,8 +147,10 @@ java_library { java_library { name: "WindowManager-Shell-lite-proto", srcs: ["src/com/android/wm/shell/desktopmode/education/data/proto/**/*.proto"], srcs: [ "src/com/android/wm/shell/desktopmode/education/data/proto/**/*.proto", "src/com/android/wm/shell/desktopmode/persistence/*.proto", ], proto: { type: "lite", }, Loading libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java +17 −2 Original line number Diff line number Diff line Loading @@ -79,6 +79,7 @@ import com.android.wm.shell.desktopmode.WindowDecorCaptionHandleRepository; import com.android.wm.shell.desktopmode.education.AppHandleEducationController; import com.android.wm.shell.desktopmode.education.AppHandleEducationFilter; import com.android.wm.shell.desktopmode.education.data.AppHandleEducationDatastoreRepository; import com.android.wm.shell.desktopmode.persistence.DesktopPersistentRepository; import com.android.wm.shell.draganddrop.DragAndDropController; import com.android.wm.shell.draganddrop.GlobalDragListener; import com.android.wm.shell.freeform.FreeformComponents; Loading Loading @@ -712,8 +713,14 @@ public abstract class WMShellModule { @WMSingleton @Provides @DynamicOverride static DesktopModeTaskRepository provideDesktopModeTaskRepository() { return new DesktopModeTaskRepository(); static DesktopModeTaskRepository provideDesktopModeTaskRepository( Context context, ShellInit shellInit, DesktopPersistentRepository desktopPersistentRepository, @ShellMainThread CoroutineScope mainScope ) { return new DesktopModeTaskRepository(context, shellInit, desktopPersistentRepository, mainScope); } @WMSingleton Loading Loading @@ -798,6 +805,14 @@ public abstract class WMShellModule { shellTaskOrganizer, appHandleEducationDatastoreRepository, applicationScope); } @WMSingleton @Provides static DesktopPersistentRepository provideDesktopPersistentRepository( Context context, @ShellBackgroundThread CoroutineScope bgScope) { return new DesktopPersistentRepository(context, bgScope); } // // Drag and drop // Loading libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepository.kt +94 −3 Original line number Diff line number Diff line Loading @@ -16,6 +16,7 @@ package com.android.wm.shell.desktopmode import android.content.Context import android.graphics.Rect import android.graphics.Region import android.util.ArrayMap Loading @@ -27,13 +28,27 @@ import androidx.core.util.forEach import androidx.core.util.keyIterator import androidx.core.util.valueIterator import com.android.internal.protolog.ProtoLog import com.android.window.flags.Flags import com.android.wm.shell.desktopmode.persistence.DesktopPersistentRepository import com.android.wm.shell.desktopmode.persistence.DesktopTask import com.android.wm.shell.desktopmode.persistence.DesktopTaskState import com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE import com.android.wm.shell.shared.annotations.ShellMainThread import com.android.wm.shell.shared.desktopmode.DesktopModeStatus import com.android.wm.shell.sysui.ShellInit import java.io.PrintWriter import java.util.concurrent.Executor import java.util.function.Consumer import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch /** Tracks task data for Desktop Mode. */ class DesktopModeTaskRepository { class DesktopModeTaskRepository ( private val context: Context, shellInit: ShellInit, private val persistentRepository: DesktopPersistentRepository, @ShellMainThread private val mainCoroutineScope: CoroutineScope, ){ /** * Task data tracked per desktop. Loading @@ -54,7 +69,15 @@ class DesktopModeTaskRepository { // TODO(b/332682201): Remove when the repository state is updated via TransitionObserver val closingTasks: ArraySet<Int> = ArraySet(), val freeformTasksInZOrder: ArrayList<Int> = ArrayList(), ) { fun deepCopy(): DesktopTaskData = DesktopTaskData( activeTasks = ArraySet(activeTasks), visibleTasks = ArraySet(visibleTasks), minimizedTasks = ArraySet(minimizedTasks), closingTasks = ArraySet(closingTasks), freeformTasksInZOrder = ArrayList(freeformTasksInZOrder) ) } /* Current wallpaper activity token to remove wallpaper activity when last task is removed. */ var wallpaperActivityToken: WindowContainerToken? = null Loading @@ -77,6 +100,40 @@ class DesktopModeTaskRepository { this[displayId] ?: DesktopTaskData().also { this[displayId] = it } } init { if (DesktopModeStatus.canEnterDesktopMode(context)) { shellInit.addInitCallback(::initRepoFromPersistentStorage, this) } } private fun initRepoFromPersistentStorage() { if (!Flags.enableDesktopWindowingPersistence()) return // TODO: b/365962554 - Handle the case that user moves to desktop before it's initialized mainCoroutineScope.launch { val desktop = persistentRepository.readDesktop() val maxTasks = DesktopModeStatus.getMaxTaskLimit(context).takeIf { it > 0 } ?: desktop.zOrderedTasksCount desktop.zOrderedTasksList // Reverse it so we initialize the repo from bottom to top. .reversed() .map { taskId -> desktop.tasksByTaskIdMap.getOrDefault( taskId, DesktopTask.getDefaultInstance() ) } .filter { task -> task.desktopTaskState == DesktopTaskState.VISIBLE } .take(maxTasks) .forEach { task -> addOrMoveFreeformTaskToTop(desktop.displayId, task.taskId) addActiveTask(desktop.displayId, task.taskId) updateTaskVisibility(desktop.displayId, task.taskId, visible = false) } } } /** Adds [activeTasksListener] to be notified of updates to active tasks. */ fun addActiveTaskListener(activeTasksListener: ActiveTasksListener) { activeTasksListeners.add(activeTasksListener) Loading Loading @@ -266,12 +323,18 @@ class DesktopModeTaskRepository { desktopTaskDataByDisplayId.getOrCreate(displayId).freeformTasksInZOrder.add(0, taskId) // Unminimize the task if it is minimized. unminimizeTask(displayId, taskId) if (Flags.enableDesktopWindowingPersistence()) { updatePersistentRepository(displayId) } } /** Minimizes the task for [taskId] and [displayId] */ fun minimizeTask(displayId: Int, taskId: Int) { logD("Minimize Task: display=%d, task=%d", displayId, taskId) desktopTaskDataByDisplayId.getOrCreate(displayId).minimizedTasks.add(taskId) if (Flags.enableDesktopWindowingPersistence()) { updatePersistentRepository(displayId) } } /** Unminimizes the task for [taskId] and [displayId] */ Loading Loading @@ -315,7 +378,10 @@ class DesktopModeTaskRepository { // Remove task from unminimized task if it is minimized. unminimizeTask(displayId, taskId) removeActiveTask(taskId) updateTaskVisibility(displayId, taskId, visible = false); updateTaskVisibility(displayId, taskId, visible = false) if (Flags.enableDesktopWindowingPersistence()) { updatePersistentRepository(displayId) } } /** Loading Loading @@ -352,6 +418,27 @@ class DesktopModeTaskRepository { fun saveBoundsBeforeMaximize(taskId: Int, bounds: Rect) = boundsBeforeMaximizeByTaskId.set(taskId, Rect(bounds)) private fun updatePersistentRepository(displayId: Int) { // Create a deep copy of the data desktopTaskDataByDisplayId[displayId]?.deepCopy()?.let { desktopTaskDataByDisplayIdCopy -> mainCoroutineScope.launch { try { persistentRepository.addOrUpdateDesktop( visibleTasks = desktopTaskDataByDisplayIdCopy.visibleTasks, minimizedTasks = desktopTaskDataByDisplayIdCopy.minimizedTasks, freeformTasksInZOrder = desktopTaskDataByDisplayIdCopy.freeformTasksInZOrder ) } catch (exception: Exception) { logE( "An exception occurred while updating the persistent repository \n%s", exception.stackTrace ) } } } } internal fun dump(pw: PrintWriter, prefix: String) { val innerPrefix = "$prefix " pw.println("${prefix}DesktopModeTaskRepository") Loading Loading @@ -390,6 +477,10 @@ class DesktopModeTaskRepository { ProtoLog.w(WM_SHELL_DESKTOP_MODE, "%s: $msg", TAG, *arguments) } private fun logE(msg: String, vararg arguments: Any?) { ProtoLog.e(WM_SHELL_DESKTOP_MODE, "%s: $msg", TAG, *arguments) } companion object { private const val TAG = "DesktopModeTaskRepository" } Loading libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt +21 −4 Original line number Diff line number Diff line Loading @@ -58,6 +58,7 @@ import com.android.internal.jank.Cuj.CUJ_DESKTOP_MODE_SNAP_RESIZE import com.android.internal.jank.InteractionJankMonitor import com.android.internal.policy.ScreenDecorationsUtils import com.android.internal.protolog.ProtoLog import com.android.window.flags.Flags import com.android.wm.shell.RootTaskDisplayAreaOrganizer import com.android.wm.shell.ShellTaskOrganizer import com.android.wm.shell.common.DisplayController Loading @@ -80,8 +81,8 @@ import com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE import com.android.wm.shell.recents.RecentTasksController import com.android.wm.shell.recents.RecentsTransitionHandler import com.android.wm.shell.recents.RecentsTransitionStateListener import com.android.wm.shell.shared.TransitionUtil import com.android.wm.shell.shared.ShellSharedConstants import com.android.wm.shell.shared.TransitionUtil import com.android.wm.shell.shared.annotations.ExternalThread import com.android.wm.shell.shared.annotations.ShellMainThread import android.window.flags.DesktopModeFlags Loading Loading @@ -728,7 +729,7 @@ class DesktopTasksController( // exclude current task since maximize/restore transition has not taken place yet. .filterNot { taskId -> taskId == excludeTaskId } .any { taskId -> val taskInfo = shellTaskOrganizer.getRunningTaskInfo(taskId)!! val taskInfo = shellTaskOrganizer.getRunningTaskInfo(taskId) ?: return false val displayLayout = displayController.getDisplayLayout(taskInfo.displayId) val stableBounds = Rect().apply { displayLayout?.getStableBounds(this) } logD("taskInfo = %s", taskInfo) Loading Loading @@ -896,6 +897,7 @@ class DesktopTasksController( val nonMinimizedTasksOrderedFrontToBack = taskRepository.getActiveNonMinimizedOrderedTasks(displayId) // If we're adding a new Task we might need to minimize an old one // TODO(b/365725441): Handle non running task minimization val taskToMinimize: RunningTaskInfo? = if (newTaskIdInFront != null && desktopTasksLimiter.isPresent) { desktopTasksLimiter Loading @@ -907,12 +909,26 @@ class DesktopTasksController( } else { null } nonMinimizedTasksOrderedFrontToBack // If there is a Task to minimize, let it stay behind the Home Task .filter { taskId -> taskId != taskToMinimize?.taskId } .mapNotNull { taskId -> shellTaskOrganizer.getRunningTaskInfo(taskId) } .reversed() // Start from the back so the front task is brought forward last .forEach { task -> wct.reorder(task.token, /* onTop= */ true) } .forEach { taskId -> val runningTaskInfo = shellTaskOrganizer.getRunningTaskInfo(taskId) if (runningTaskInfo != null) { // Task is already running, reorder it to the front wct.reorder(runningTaskInfo.token, /* onTop= */ true) } else if (Flags.enableDesktopWindowingPersistence()) { // Task is not running, start it wct.startTask( taskId, ActivityOptions.makeBasic().apply { launchWindowingMode = WINDOWING_MODE_FREEFORM }.toBundle(), ) } } taskbarDesktopTaskListener?. onTaskbarCornerRoundingUpdate(doesAnyTaskRequireTaskbarRounding(displayId)) Loading Loading @@ -1211,6 +1227,7 @@ class DesktopTasksController( wct.reorder(task.token, true) return wct } // TODO(b/365723620): Handle non running tasks that were launched after reboot. // If task is already visible, it must have been handled already and added to desktop mode. // Cascade task only if it's not visible yet. if (DesktopModeFlags.ENABLE_CASCADING_WINDOWS.isTrue() Loading libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/persistence/DesktopPersistentRepository.kt 0 → 100644 +201 −0 Original line number Diff line number Diff line /* * Copyright (C) 2024 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.wm.shell.desktopmode.persistence import android.content.Context import android.util.ArraySet import android.util.Log import android.view.Display.DEFAULT_DISPLAY import androidx.datastore.core.CorruptionException import androidx.datastore.core.DataStore import androidx.datastore.core.DataStoreFactory import androidx.datastore.core.Serializer import androidx.datastore.dataStoreFile import com.android.framework.protobuf.InvalidProtocolBufferException import com.android.wm.shell.shared.annotations.ShellBackgroundThread import java.io.IOException import java.io.InputStream import java.io.OutputStream import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.first /** * Persistent repository for storing desktop mode related data. * * The main constructor is public only for testing purposes. */ class DesktopPersistentRepository( private val dataStore: DataStore<DesktopPersistentRepositories>, ) { constructor( context: Context, @ShellBackgroundThread bgCoroutineScope: CoroutineScope, ) : this( DataStoreFactory.create( serializer = DesktopPersistentRepositoriesSerializer, produceFile = { context.dataStoreFile(DESKTOP_REPOSITORIES_DATASTORE_FILE) }, scope = bgCoroutineScope)) /** Provides `dataStore.data` flow and handles exceptions thrown during collection */ private val dataStoreFlow: Flow<DesktopPersistentRepositories> = dataStore.data.catch { exception -> // dataStore.data throws an IOException when an error is encountered when reading data if (exception is IOException) { Log.e( TAG, "Error in reading desktop mode related data from datastore, data is " + "stored in a file named $DESKTOP_REPOSITORIES_DATASTORE_FILE", exception) } else { throw exception } } /** * Reads and returns the [DesktopRepositoryState] proto object from the DataStore for a user. If * the DataStore is empty or there's an error reading, it returns the default value of Proto. */ private suspend fun getDesktopRepositoryState( userId: Int = DEFAULT_USER_ID ): DesktopRepositoryState = try { dataStoreFlow .first() .desktopRepoByUserMap .getOrDefault(userId, DesktopRepositoryState.getDefaultInstance()) } catch (e: Exception) { Log.e(TAG, "Unable to read from datastore", e) DesktopRepositoryState.getDefaultInstance() } /** * Reads the [Desktop] of a desktop filtering by the [userId] and [desktopId]. Executes the * [callback] using the [mainCoroutineScope]. */ suspend fun readDesktop( userId: Int = DEFAULT_USER_ID, desktopId: Int = DEFAULT_DESKTOP_ID, ): Desktop = try { val repository = getDesktopRepositoryState(userId) repository.getDesktopOrThrow(desktopId) } catch (e: Exception) { Log.e(TAG, "Unable to get desktop info from persistent repository", e) Desktop.getDefaultInstance() } /** Adds or updates a desktop stored in the datastore */ suspend fun addOrUpdateDesktop( userId: Int = DEFAULT_USER_ID, desktopId: Int = 0, visibleTasks: ArraySet<Int> = ArraySet(), minimizedTasks: ArraySet<Int> = ArraySet(), freeformTasksInZOrder: ArrayList<Int> = ArrayList(), ) { // TODO: b/367609270 - Improve the API to support multi-user try { dataStore.updateData { desktopPersistentRepositories: DesktopPersistentRepositories -> val currentRepository = desktopPersistentRepositories.getDesktopRepoByUserOrDefault( userId, DesktopRepositoryState.getDefaultInstance()) val desktop = getDesktop(currentRepository, desktopId) .toBuilder() .updateTaskStates(visibleTasks, minimizedTasks) .updateZOrder(freeformTasksInZOrder) desktopPersistentRepositories .toBuilder() .putDesktopRepoByUser( userId, currentRepository .toBuilder() .putDesktop(desktopId, desktop.build()) .build()) .build() } } catch (exception: IOException) { Log.e( TAG, "Error in updating desktop mode related data, data is " + "stored in a file named $DESKTOP_REPOSITORIES_DATASTORE_FILE", exception) } } private fun getDesktop(currentRepository: DesktopRepositoryState, desktopId: Int): Desktop = // If there are no desktops set up, create one on the default display currentRepository.getDesktopOrDefault( desktopId, Desktop.newBuilder().setDesktopId(desktopId).setDisplayId(DEFAULT_DISPLAY).build()) companion object { private const val TAG = "DesktopPersistenceRepo" private const val DESKTOP_REPOSITORIES_DATASTORE_FILE = "desktop_persistent_repositories.pb" private const val DEFAULT_USER_ID = 1000 private const val DEFAULT_DESKTOP_ID = 0 object DesktopPersistentRepositoriesSerializer : Serializer<DesktopPersistentRepositories> { override val defaultValue: DesktopPersistentRepositories = DesktopPersistentRepositories.getDefaultInstance() override suspend fun readFrom(input: InputStream): DesktopPersistentRepositories = try { DesktopPersistentRepositories.parseFrom(input) } catch (exception: InvalidProtocolBufferException) { throw CorruptionException("Cannot read proto.", exception) } override suspend fun writeTo(t: DesktopPersistentRepositories, output: OutputStream) = t.writeTo(output) } private fun Desktop.Builder.updateTaskStates( visibleTasks: ArraySet<Int>, minimizedTasks: ArraySet<Int> ): Desktop.Builder { clearTasksByTaskId() putAllTasksByTaskId( visibleTasks.associateWith { createDesktopTask(it, state = DesktopTaskState.VISIBLE) }) putAllTasksByTaskId( minimizedTasks.associateWith { createDesktopTask(it, state = DesktopTaskState.MINIMIZED) }) return this } private fun Desktop.Builder.updateZOrder( freeformTasksInZOrder: ArrayList<Int> ): Desktop.Builder { clearZOrderedTasks() addAllZOrderedTasks(freeformTasksInZOrder) return this } private fun createDesktopTask( taskId: Int, state: DesktopTaskState = DesktopTaskState.VISIBLE ): DesktopTask = DesktopTask.newBuilder().setTaskId(taskId).setDesktopTaskState(state).build() } } Loading
libs/WindowManager/Shell/Android.bp +4 −2 Original line number Diff line number Diff line Loading @@ -147,8 +147,10 @@ java_library { java_library { name: "WindowManager-Shell-lite-proto", srcs: ["src/com/android/wm/shell/desktopmode/education/data/proto/**/*.proto"], srcs: [ "src/com/android/wm/shell/desktopmode/education/data/proto/**/*.proto", "src/com/android/wm/shell/desktopmode/persistence/*.proto", ], proto: { type: "lite", }, Loading
libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java +17 −2 Original line number Diff line number Diff line Loading @@ -79,6 +79,7 @@ import com.android.wm.shell.desktopmode.WindowDecorCaptionHandleRepository; import com.android.wm.shell.desktopmode.education.AppHandleEducationController; import com.android.wm.shell.desktopmode.education.AppHandleEducationFilter; import com.android.wm.shell.desktopmode.education.data.AppHandleEducationDatastoreRepository; import com.android.wm.shell.desktopmode.persistence.DesktopPersistentRepository; import com.android.wm.shell.draganddrop.DragAndDropController; import com.android.wm.shell.draganddrop.GlobalDragListener; import com.android.wm.shell.freeform.FreeformComponents; Loading Loading @@ -712,8 +713,14 @@ public abstract class WMShellModule { @WMSingleton @Provides @DynamicOverride static DesktopModeTaskRepository provideDesktopModeTaskRepository() { return new DesktopModeTaskRepository(); static DesktopModeTaskRepository provideDesktopModeTaskRepository( Context context, ShellInit shellInit, DesktopPersistentRepository desktopPersistentRepository, @ShellMainThread CoroutineScope mainScope ) { return new DesktopModeTaskRepository(context, shellInit, desktopPersistentRepository, mainScope); } @WMSingleton Loading Loading @@ -798,6 +805,14 @@ public abstract class WMShellModule { shellTaskOrganizer, appHandleEducationDatastoreRepository, applicationScope); } @WMSingleton @Provides static DesktopPersistentRepository provideDesktopPersistentRepository( Context context, @ShellBackgroundThread CoroutineScope bgScope) { return new DesktopPersistentRepository(context, bgScope); } // // Drag and drop // Loading
libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepository.kt +94 −3 Original line number Diff line number Diff line Loading @@ -16,6 +16,7 @@ package com.android.wm.shell.desktopmode import android.content.Context import android.graphics.Rect import android.graphics.Region import android.util.ArrayMap Loading @@ -27,13 +28,27 @@ import androidx.core.util.forEach import androidx.core.util.keyIterator import androidx.core.util.valueIterator import com.android.internal.protolog.ProtoLog import com.android.window.flags.Flags import com.android.wm.shell.desktopmode.persistence.DesktopPersistentRepository import com.android.wm.shell.desktopmode.persistence.DesktopTask import com.android.wm.shell.desktopmode.persistence.DesktopTaskState import com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE import com.android.wm.shell.shared.annotations.ShellMainThread import com.android.wm.shell.shared.desktopmode.DesktopModeStatus import com.android.wm.shell.sysui.ShellInit import java.io.PrintWriter import java.util.concurrent.Executor import java.util.function.Consumer import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch /** Tracks task data for Desktop Mode. */ class DesktopModeTaskRepository { class DesktopModeTaskRepository ( private val context: Context, shellInit: ShellInit, private val persistentRepository: DesktopPersistentRepository, @ShellMainThread private val mainCoroutineScope: CoroutineScope, ){ /** * Task data tracked per desktop. Loading @@ -54,7 +69,15 @@ class DesktopModeTaskRepository { // TODO(b/332682201): Remove when the repository state is updated via TransitionObserver val closingTasks: ArraySet<Int> = ArraySet(), val freeformTasksInZOrder: ArrayList<Int> = ArrayList(), ) { fun deepCopy(): DesktopTaskData = DesktopTaskData( activeTasks = ArraySet(activeTasks), visibleTasks = ArraySet(visibleTasks), minimizedTasks = ArraySet(minimizedTasks), closingTasks = ArraySet(closingTasks), freeformTasksInZOrder = ArrayList(freeformTasksInZOrder) ) } /* Current wallpaper activity token to remove wallpaper activity when last task is removed. */ var wallpaperActivityToken: WindowContainerToken? = null Loading @@ -77,6 +100,40 @@ class DesktopModeTaskRepository { this[displayId] ?: DesktopTaskData().also { this[displayId] = it } } init { if (DesktopModeStatus.canEnterDesktopMode(context)) { shellInit.addInitCallback(::initRepoFromPersistentStorage, this) } } private fun initRepoFromPersistentStorage() { if (!Flags.enableDesktopWindowingPersistence()) return // TODO: b/365962554 - Handle the case that user moves to desktop before it's initialized mainCoroutineScope.launch { val desktop = persistentRepository.readDesktop() val maxTasks = DesktopModeStatus.getMaxTaskLimit(context).takeIf { it > 0 } ?: desktop.zOrderedTasksCount desktop.zOrderedTasksList // Reverse it so we initialize the repo from bottom to top. .reversed() .map { taskId -> desktop.tasksByTaskIdMap.getOrDefault( taskId, DesktopTask.getDefaultInstance() ) } .filter { task -> task.desktopTaskState == DesktopTaskState.VISIBLE } .take(maxTasks) .forEach { task -> addOrMoveFreeformTaskToTop(desktop.displayId, task.taskId) addActiveTask(desktop.displayId, task.taskId) updateTaskVisibility(desktop.displayId, task.taskId, visible = false) } } } /** Adds [activeTasksListener] to be notified of updates to active tasks. */ fun addActiveTaskListener(activeTasksListener: ActiveTasksListener) { activeTasksListeners.add(activeTasksListener) Loading Loading @@ -266,12 +323,18 @@ class DesktopModeTaskRepository { desktopTaskDataByDisplayId.getOrCreate(displayId).freeformTasksInZOrder.add(0, taskId) // Unminimize the task if it is minimized. unminimizeTask(displayId, taskId) if (Flags.enableDesktopWindowingPersistence()) { updatePersistentRepository(displayId) } } /** Minimizes the task for [taskId] and [displayId] */ fun minimizeTask(displayId: Int, taskId: Int) { logD("Minimize Task: display=%d, task=%d", displayId, taskId) desktopTaskDataByDisplayId.getOrCreate(displayId).minimizedTasks.add(taskId) if (Flags.enableDesktopWindowingPersistence()) { updatePersistentRepository(displayId) } } /** Unminimizes the task for [taskId] and [displayId] */ Loading Loading @@ -315,7 +378,10 @@ class DesktopModeTaskRepository { // Remove task from unminimized task if it is minimized. unminimizeTask(displayId, taskId) removeActiveTask(taskId) updateTaskVisibility(displayId, taskId, visible = false); updateTaskVisibility(displayId, taskId, visible = false) if (Flags.enableDesktopWindowingPersistence()) { updatePersistentRepository(displayId) } } /** Loading Loading @@ -352,6 +418,27 @@ class DesktopModeTaskRepository { fun saveBoundsBeforeMaximize(taskId: Int, bounds: Rect) = boundsBeforeMaximizeByTaskId.set(taskId, Rect(bounds)) private fun updatePersistentRepository(displayId: Int) { // Create a deep copy of the data desktopTaskDataByDisplayId[displayId]?.deepCopy()?.let { desktopTaskDataByDisplayIdCopy -> mainCoroutineScope.launch { try { persistentRepository.addOrUpdateDesktop( visibleTasks = desktopTaskDataByDisplayIdCopy.visibleTasks, minimizedTasks = desktopTaskDataByDisplayIdCopy.minimizedTasks, freeformTasksInZOrder = desktopTaskDataByDisplayIdCopy.freeformTasksInZOrder ) } catch (exception: Exception) { logE( "An exception occurred while updating the persistent repository \n%s", exception.stackTrace ) } } } } internal fun dump(pw: PrintWriter, prefix: String) { val innerPrefix = "$prefix " pw.println("${prefix}DesktopModeTaskRepository") Loading Loading @@ -390,6 +477,10 @@ class DesktopModeTaskRepository { ProtoLog.w(WM_SHELL_DESKTOP_MODE, "%s: $msg", TAG, *arguments) } private fun logE(msg: String, vararg arguments: Any?) { ProtoLog.e(WM_SHELL_DESKTOP_MODE, "%s: $msg", TAG, *arguments) } companion object { private const val TAG = "DesktopModeTaskRepository" } Loading
libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt +21 −4 Original line number Diff line number Diff line Loading @@ -58,6 +58,7 @@ import com.android.internal.jank.Cuj.CUJ_DESKTOP_MODE_SNAP_RESIZE import com.android.internal.jank.InteractionJankMonitor import com.android.internal.policy.ScreenDecorationsUtils import com.android.internal.protolog.ProtoLog import com.android.window.flags.Flags import com.android.wm.shell.RootTaskDisplayAreaOrganizer import com.android.wm.shell.ShellTaskOrganizer import com.android.wm.shell.common.DisplayController Loading @@ -80,8 +81,8 @@ import com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE import com.android.wm.shell.recents.RecentTasksController import com.android.wm.shell.recents.RecentsTransitionHandler import com.android.wm.shell.recents.RecentsTransitionStateListener import com.android.wm.shell.shared.TransitionUtil import com.android.wm.shell.shared.ShellSharedConstants import com.android.wm.shell.shared.TransitionUtil import com.android.wm.shell.shared.annotations.ExternalThread import com.android.wm.shell.shared.annotations.ShellMainThread import android.window.flags.DesktopModeFlags Loading Loading @@ -728,7 +729,7 @@ class DesktopTasksController( // exclude current task since maximize/restore transition has not taken place yet. .filterNot { taskId -> taskId == excludeTaskId } .any { taskId -> val taskInfo = shellTaskOrganizer.getRunningTaskInfo(taskId)!! val taskInfo = shellTaskOrganizer.getRunningTaskInfo(taskId) ?: return false val displayLayout = displayController.getDisplayLayout(taskInfo.displayId) val stableBounds = Rect().apply { displayLayout?.getStableBounds(this) } logD("taskInfo = %s", taskInfo) Loading Loading @@ -896,6 +897,7 @@ class DesktopTasksController( val nonMinimizedTasksOrderedFrontToBack = taskRepository.getActiveNonMinimizedOrderedTasks(displayId) // If we're adding a new Task we might need to minimize an old one // TODO(b/365725441): Handle non running task minimization val taskToMinimize: RunningTaskInfo? = if (newTaskIdInFront != null && desktopTasksLimiter.isPresent) { desktopTasksLimiter Loading @@ -907,12 +909,26 @@ class DesktopTasksController( } else { null } nonMinimizedTasksOrderedFrontToBack // If there is a Task to minimize, let it stay behind the Home Task .filter { taskId -> taskId != taskToMinimize?.taskId } .mapNotNull { taskId -> shellTaskOrganizer.getRunningTaskInfo(taskId) } .reversed() // Start from the back so the front task is brought forward last .forEach { task -> wct.reorder(task.token, /* onTop= */ true) } .forEach { taskId -> val runningTaskInfo = shellTaskOrganizer.getRunningTaskInfo(taskId) if (runningTaskInfo != null) { // Task is already running, reorder it to the front wct.reorder(runningTaskInfo.token, /* onTop= */ true) } else if (Flags.enableDesktopWindowingPersistence()) { // Task is not running, start it wct.startTask( taskId, ActivityOptions.makeBasic().apply { launchWindowingMode = WINDOWING_MODE_FREEFORM }.toBundle(), ) } } taskbarDesktopTaskListener?. onTaskbarCornerRoundingUpdate(doesAnyTaskRequireTaskbarRounding(displayId)) Loading Loading @@ -1211,6 +1227,7 @@ class DesktopTasksController( wct.reorder(task.token, true) return wct } // TODO(b/365723620): Handle non running tasks that were launched after reboot. // If task is already visible, it must have been handled already and added to desktop mode. // Cascade task only if it's not visible yet. if (DesktopModeFlags.ENABLE_CASCADING_WINDOWS.isTrue() Loading
libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/persistence/DesktopPersistentRepository.kt 0 → 100644 +201 −0 Original line number Diff line number Diff line /* * Copyright (C) 2024 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.wm.shell.desktopmode.persistence import android.content.Context import android.util.ArraySet import android.util.Log import android.view.Display.DEFAULT_DISPLAY import androidx.datastore.core.CorruptionException import androidx.datastore.core.DataStore import androidx.datastore.core.DataStoreFactory import androidx.datastore.core.Serializer import androidx.datastore.dataStoreFile import com.android.framework.protobuf.InvalidProtocolBufferException import com.android.wm.shell.shared.annotations.ShellBackgroundThread import java.io.IOException import java.io.InputStream import java.io.OutputStream import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.first /** * Persistent repository for storing desktop mode related data. * * The main constructor is public only for testing purposes. */ class DesktopPersistentRepository( private val dataStore: DataStore<DesktopPersistentRepositories>, ) { constructor( context: Context, @ShellBackgroundThread bgCoroutineScope: CoroutineScope, ) : this( DataStoreFactory.create( serializer = DesktopPersistentRepositoriesSerializer, produceFile = { context.dataStoreFile(DESKTOP_REPOSITORIES_DATASTORE_FILE) }, scope = bgCoroutineScope)) /** Provides `dataStore.data` flow and handles exceptions thrown during collection */ private val dataStoreFlow: Flow<DesktopPersistentRepositories> = dataStore.data.catch { exception -> // dataStore.data throws an IOException when an error is encountered when reading data if (exception is IOException) { Log.e( TAG, "Error in reading desktop mode related data from datastore, data is " + "stored in a file named $DESKTOP_REPOSITORIES_DATASTORE_FILE", exception) } else { throw exception } } /** * Reads and returns the [DesktopRepositoryState] proto object from the DataStore for a user. If * the DataStore is empty or there's an error reading, it returns the default value of Proto. */ private suspend fun getDesktopRepositoryState( userId: Int = DEFAULT_USER_ID ): DesktopRepositoryState = try { dataStoreFlow .first() .desktopRepoByUserMap .getOrDefault(userId, DesktopRepositoryState.getDefaultInstance()) } catch (e: Exception) { Log.e(TAG, "Unable to read from datastore", e) DesktopRepositoryState.getDefaultInstance() } /** * Reads the [Desktop] of a desktop filtering by the [userId] and [desktopId]. Executes the * [callback] using the [mainCoroutineScope]. */ suspend fun readDesktop( userId: Int = DEFAULT_USER_ID, desktopId: Int = DEFAULT_DESKTOP_ID, ): Desktop = try { val repository = getDesktopRepositoryState(userId) repository.getDesktopOrThrow(desktopId) } catch (e: Exception) { Log.e(TAG, "Unable to get desktop info from persistent repository", e) Desktop.getDefaultInstance() } /** Adds or updates a desktop stored in the datastore */ suspend fun addOrUpdateDesktop( userId: Int = DEFAULT_USER_ID, desktopId: Int = 0, visibleTasks: ArraySet<Int> = ArraySet(), minimizedTasks: ArraySet<Int> = ArraySet(), freeformTasksInZOrder: ArrayList<Int> = ArrayList(), ) { // TODO: b/367609270 - Improve the API to support multi-user try { dataStore.updateData { desktopPersistentRepositories: DesktopPersistentRepositories -> val currentRepository = desktopPersistentRepositories.getDesktopRepoByUserOrDefault( userId, DesktopRepositoryState.getDefaultInstance()) val desktop = getDesktop(currentRepository, desktopId) .toBuilder() .updateTaskStates(visibleTasks, minimizedTasks) .updateZOrder(freeformTasksInZOrder) desktopPersistentRepositories .toBuilder() .putDesktopRepoByUser( userId, currentRepository .toBuilder() .putDesktop(desktopId, desktop.build()) .build()) .build() } } catch (exception: IOException) { Log.e( TAG, "Error in updating desktop mode related data, data is " + "stored in a file named $DESKTOP_REPOSITORIES_DATASTORE_FILE", exception) } } private fun getDesktop(currentRepository: DesktopRepositoryState, desktopId: Int): Desktop = // If there are no desktops set up, create one on the default display currentRepository.getDesktopOrDefault( desktopId, Desktop.newBuilder().setDesktopId(desktopId).setDisplayId(DEFAULT_DISPLAY).build()) companion object { private const val TAG = "DesktopPersistenceRepo" private const val DESKTOP_REPOSITORIES_DATASTORE_FILE = "desktop_persistent_repositories.pb" private const val DEFAULT_USER_ID = 1000 private const val DEFAULT_DESKTOP_ID = 0 object DesktopPersistentRepositoriesSerializer : Serializer<DesktopPersistentRepositories> { override val defaultValue: DesktopPersistentRepositories = DesktopPersistentRepositories.getDefaultInstance() override suspend fun readFrom(input: InputStream): DesktopPersistentRepositories = try { DesktopPersistentRepositories.parseFrom(input) } catch (exception: InvalidProtocolBufferException) { throw CorruptionException("Cannot read proto.", exception) } override suspend fun writeTo(t: DesktopPersistentRepositories, output: OutputStream) = t.writeTo(output) } private fun Desktop.Builder.updateTaskStates( visibleTasks: ArraySet<Int>, minimizedTasks: ArraySet<Int> ): Desktop.Builder { clearTasksByTaskId() putAllTasksByTaskId( visibleTasks.associateWith { createDesktopTask(it, state = DesktopTaskState.VISIBLE) }) putAllTasksByTaskId( minimizedTasks.associateWith { createDesktopTask(it, state = DesktopTaskState.MINIMIZED) }) return this } private fun Desktop.Builder.updateZOrder( freeformTasksInZOrder: ArrayList<Int> ): Desktop.Builder { clearZOrderedTasks() addAllZOrderedTasks(freeformTasksInZOrder) return this } private fun createDesktopTask( taskId: Int, state: DesktopTaskState = DesktopTaskState.VISIBLE ): DesktopTask = DesktopTask.newBuilder().setTaskId(taskId).setDesktopTaskState(state).build() } }