• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
<lambda>null2  * Copyright (C) 2024 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.wm.shell.desktopmode.persistence
18 
19 import android.content.Context
20 import android.util.ArraySet
21 import android.util.Log
22 import android.view.Display.DEFAULT_DISPLAY
23 import androidx.datastore.core.CorruptionException
24 import androidx.datastore.core.DataStore
25 import androidx.datastore.core.DataStoreFactory
26 import androidx.datastore.core.Serializer
27 import androidx.datastore.core.handlers.ReplaceFileCorruptionHandler
28 import androidx.datastore.dataStoreFile
29 import com.android.framework.protobuf.InvalidProtocolBufferException
30 import com.android.wm.shell.shared.annotations.ShellBackgroundThread
31 import java.io.IOException
32 import java.io.InputStream
33 import java.io.OutputStream
34 import kotlinx.coroutines.CoroutineScope
35 import kotlinx.coroutines.flow.Flow
36 import kotlinx.coroutines.flow.catch
37 import kotlinx.coroutines.flow.first
38 
39 /**
40  * Persistent repository for storing desktop mode related data.
41  *
42  * The main constructor is public only for testing purposes.
43  */
44 class DesktopPersistentRepository(private val dataStore: DataStore<DesktopPersistentRepositories>) {
45     constructor(
46         context: Context,
47         @ShellBackgroundThread bgCoroutineScope: CoroutineScope,
48     ) : this(
49         DataStoreFactory.create(
50             serializer = DesktopPersistentRepositoriesSerializer,
51             produceFile = { context.dataStoreFile(DESKTOP_REPOSITORIES_DATASTORE_FILE) },
52             scope = bgCoroutineScope,
53             corruptionHandler =
54                 ReplaceFileCorruptionHandler(
55                     produceNewData = { DesktopPersistentRepositories.getDefaultInstance() }
56                 ),
57         )
58     )
59 
60     /** Provides `dataStore.data` flow and handles exceptions thrown during collection */
61     private val dataStoreFlow: Flow<DesktopPersistentRepositories> =
62         dataStore.data.catch { exception ->
63             // dataStore.data throws an IOException when an error is encountered when reading data
64             if (exception is IOException) {
65                 Log.e(
66                     TAG,
67                     "Error in reading desktop mode related data from datastore, data is " +
68                         "stored in a file named $DESKTOP_REPOSITORIES_DATASTORE_FILE",
69                     exception,
70                 )
71             } else {
72                 throw exception
73             }
74         }
75 
76     /**
77      * Reads and returns the [DesktopRepositoryState] proto object from the DataStore for a user. If
78      * the DataStore is empty or there's an error reading, it returns the default value of Proto.
79      */
80     suspend fun getDesktopRepositoryState(userId: Int): DesktopRepositoryState? =
81         try {
82             dataStoreFlow.first().desktopRepoByUserMap[userId]
83         } catch (e: Exception) {
84             Log.e(TAG, "Unable to read from datastore", e)
85             null
86         }
87 
88     suspend fun getUserDesktopRepositoryMap(): Map<Int, DesktopRepositoryState>? =
89         try {
90             dataStoreFlow.first().desktopRepoByUserMap
91         } catch (e: Exception) {
92             Log.e(TAG, "Unable to read from datastore", e)
93             null
94         }
95 
96     /**
97      * Reads the [Desktop] of a desktop filtering by the [userId] and [desktopId]. Executes the
98      * [callback] using the [mainCoroutineScope].
99      */
100     suspend fun readDesktop(userId: Int, desktopId: Int = DEFAULT_DESKTOP_ID): Desktop? =
101         try {
102             val repository = getDesktopRepositoryState(userId)
103             repository?.getDesktopOrThrow(desktopId)
104         } catch (e: Exception) {
105             Log.e(TAG, "Unable to get desktop info from persistent repository", e)
106             null
107         }
108 
109     /** Adds or updates a desktop stored in the datastore */
110     suspend fun addOrUpdateDesktop(
111         userId: Int,
112         desktopId: Int = 0,
113         visibleTasks: ArraySet<Int> = ArraySet(),
114         minimizedTasks: ArraySet<Int> = ArraySet(),
115         freeformTasksInZOrder: ArrayList<Int> = ArrayList(),
116         leftTiledTask: Int? = null,
117         rightTiledTask: Int? = null,
118     ) {
119         // TODO: b/367609270 - Improve the API to support multi-user
120         try {
121             dataStore.updateData { persistentRepositories: DesktopPersistentRepositories ->
122                 val currentRepository =
123                     persistentRepositories.getDesktopRepoByUserOrDefault(
124                         userId,
125                         DesktopRepositoryState.getDefaultInstance(),
126                     )
127                 val desktop =
128                     getDesktop(currentRepository, desktopId)
129                         .toBuilder()
130                         .updateTaskStates(
131                             visibleTasks,
132                             minimizedTasks,
133                             freeformTasksInZOrder,
134                             leftTiledTask,
135                             rightTiledTask,
136                         )
137                         .updateZOrder(freeformTasksInZOrder)
138 
139                 persistentRepositories
140                     .toBuilder()
141                     .putDesktopRepoByUser(
142                         userId,
143                         currentRepository.toBuilder().putDesktop(desktopId, desktop.build()).build(),
144                     )
145                     .build()
146             }
147         } catch (exception: Exception) {
148             Log.e(
149                 TAG,
150                 "Error in updating desktop mode related data, data is " +
151                     "stored in a file named $DESKTOP_REPOSITORIES_DATASTORE_FILE",
152                 exception,
153             )
154         }
155     }
156 
157     /** Removes the desktop from the persistent repository. */
158     suspend fun removeDesktop(userId: Int, desktopId: Int) {
159         try {
160             dataStore.updateData { persistentRepositories: DesktopPersistentRepositories ->
161                 val currentRepository =
162                     persistentRepositories.getDesktopRepoByUserOrDefault(
163                         userId,
164                         DesktopRepositoryState.getDefaultInstance(),
165                     )
166                 persistentRepositories
167                     .toBuilder()
168                     .putDesktopRepoByUser(
169                         userId,
170                         currentRepository.toBuilder().removeDesktop(desktopId).build(),
171                     )
172                     .build()
173             }
174         } catch (throwable: Throwable) {
175             Log.e(
176                 TAG,
177                 "Error in removing desktop related data, data is " +
178                     "stored in a file named $DESKTOP_REPOSITORIES_DATASTORE_FILE",
179                 throwable,
180             )
181         }
182     }
183 
184     suspend fun removeUsers(uids: List<Int>) {
185         try {
186             dataStore.updateData { persistentRepositories: DesktopPersistentRepositories ->
187                 val persistentRepositoriesBuilder = persistentRepositories.toBuilder()
188                 uids.forEach { uid -> persistentRepositoriesBuilder.removeDesktopRepoByUser(uid) }
189                 persistentRepositoriesBuilder.build()
190             }
191         } catch (exception: Exception) {
192             Log.e(
193                 TAG,
194                 "Error in removing user related data, data is stored in a file named $DESKTOP_REPOSITORIES_DATASTORE_FILE",
195                 exception,
196             )
197         }
198     }
199 
200     private fun getDesktop(currentRepository: DesktopRepositoryState, desktopId: Int): Desktop =
201         // If there are no desktops set up, create one on the default display
202         currentRepository.getDesktopOrDefault(
203             desktopId,
204             Desktop.newBuilder().setDesktopId(desktopId).setDisplayId(DEFAULT_DISPLAY).build(),
205         )
206 
207     companion object {
208         private const val TAG = "DesktopPersistenceRepo"
209         private const val DESKTOP_REPOSITORIES_DATASTORE_FILE = "desktop_persistent_repositories.pb"
210 
211         private const val DEFAULT_DESKTOP_ID = 0
212 
213         object DesktopPersistentRepositoriesSerializer : Serializer<DesktopPersistentRepositories> {
214 
215             override val defaultValue: DesktopPersistentRepositories =
216                 DesktopPersistentRepositories.getDefaultInstance()
217 
218             override suspend fun readFrom(input: InputStream): DesktopPersistentRepositories =
219                 try {
220                     DesktopPersistentRepositories.parseFrom(input)
221                 } catch (exception: InvalidProtocolBufferException) {
222                     throw CorruptionException("Cannot read proto.", exception)
223                 }
224 
225             override suspend fun writeTo(t: DesktopPersistentRepositories, output: OutputStream) =
226                 t.writeTo(output)
227         }
228 
229         private fun Desktop.Builder.updateTaskStates(
230             visibleTasks: ArraySet<Int>,
231             minimizedTasks: ArraySet<Int>,
232             freeformTasksInZOrder: ArrayList<Int>,
233             leftTiledTask: Int?,
234             rightTiledTask: Int?,
235         ): Desktop.Builder {
236             clearTasksByTaskId()
237 
238             // Handle the case where tasks are not marked as visible but are meant to be visible
239             // after reboot. E.g. User moves out of desktop when there are multiple tasks are
240             // visible, they will be marked as not visible afterwards. This ensures that they are
241             // still persisted as visible.
242             // TODO - b/350476823: Remove this logic once repository holds expanded tasks
243             if (
244                 freeformTasksInZOrder.size > visibleTasks.size + minimizedTasks.size &&
245                     visibleTasks.isEmpty()
246             ) {
247                 visibleTasks.addAll(freeformTasksInZOrder.filterNot { it in minimizedTasks })
248             }
249             putAllTasksByTaskId(
250                 visibleTasks.associateWith {
251                     createDesktopTask(
252                         it,
253                         state = DesktopTaskState.VISIBLE,
254                         getTilingStateForTask(it, leftTiledTask, rightTiledTask),
255                     )
256                 }
257             )
258             putAllTasksByTaskId(
259                 minimizedTasks.associateWith {
260                     createDesktopTask(it, state = DesktopTaskState.MINIMIZED)
261                 }
262             )
263             return this
264         }
265 
266         private fun getTilingStateForTask(
267             taskId: Int,
268             leftTiledTask: Int?,
269             rightTiledTask: Int?,
270         ): DesktopTaskTilingState =
271             when (taskId) {
272                 leftTiledTask -> DesktopTaskTilingState.LEFT
273                 rightTiledTask -> DesktopTaskTilingState.RIGHT
274                 else -> DesktopTaskTilingState.NONE
275             }
276 
277         private fun Desktop.Builder.updateZOrder(
278             freeformTasksInZOrder: ArrayList<Int>
279         ): Desktop.Builder {
280             clearZOrderedTasks()
281             addAllZOrderedTasks(freeformTasksInZOrder)
282             return this
283         }
284 
285         private fun createDesktopTask(
286             taskId: Int,
287             state: DesktopTaskState = DesktopTaskState.VISIBLE,
288             tiling_state: DesktopTaskTilingState = DesktopTaskTilingState.NONE,
289         ): DesktopTask =
290             DesktopTask.newBuilder()
291                 .setTaskId(taskId)
292                 .setDesktopTaskState(state)
293                 .setDesktopTaskTilingState(tiling_state)
294                 .build()
295     }
296 }
297