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