Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit bfd48620 authored by Orhan Uysal's avatar Orhan Uysal
Browse files

Implement persistent repository.

Implement an initial version of persistent repository that has the
functionality to store the whole desktop state.

This cl also edits the existing DesktopModeTasksRepository to be
initilized using the persistent values. Also to persist adding a new
task, minimizing the task, removing the task.

Test: atest DesktopTasksControllerTest
Test: atest DesktopModeTaskRepositoryTest
Test: atest DesktopModePersistentRepositoryTest
Bug: 358031378
Flag: com.android.window.flags.enable_desktop_windowing_persistence

Change-Id: Icac92f2e13f5ac67996142b0f31121b70d129ddd
parent 7ab5adc9
Loading
Loading
Loading
Loading
+4 −2
Original line number Diff line number Diff line
@@ -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",
    },
+17 −2
Original line number Diff line number Diff line
@@ -78,6 +78,7 @@ import com.android.wm.shell.desktopmode.ToggleResizeDesktopTaskTransitionHandler
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;
@@ -722,8 +723,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
@@ -802,6 +809,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
    //
+94 −3
Original line number Diff line number Diff line
@@ -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
@@ -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.
@@ -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
@@ -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)
@@ -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] */
@@ -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)
        }
    }

    /**
@@ -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")
@@ -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"
    }
+21 −4
Original line number Diff line number Diff line
@@ -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
@@ -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 com.android.wm.shell.shared.desktopmode.DesktopModeFlags
@@ -721,7 +722,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)
@@ -888,6 +889,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
@@ -899,12 +901,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))
@@ -1202,6 +1218,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.CASCADING_WINDOWS.isEnabled(context)
+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