1 /* <lambda>null2 * Copyright (C) 2023 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.systemui.communal.data.repository 18 19 import android.app.backup.BackupManager 20 import android.appwidget.AppWidgetProviderInfo 21 import android.content.ComponentName 22 import android.os.UserHandle 23 import android.os.UserManager 24 import com.android.app.tracing.coroutines.launchTraced as launch 25 import com.android.systemui.Flags.communalResponsiveGrid 26 import com.android.systemui.Flags.communalWidgetResizing 27 import com.android.systemui.common.data.repository.PackageChangeRepository 28 import com.android.systemui.common.shared.model.PackageInstallSession 29 import com.android.systemui.communal.data.backup.CommunalBackupUtils 30 import com.android.systemui.communal.data.db.CommunalWidgetDao 31 import com.android.systemui.communal.data.db.CommunalWidgetItem 32 import com.android.systemui.communal.data.db.DefaultWidgetPopulation 33 import com.android.systemui.communal.data.db.DefaultWidgetPopulation.SkipReason.RESTORED_FROM_BACKUP 34 import com.android.systemui.communal.nano.CommunalHubState 35 import com.android.systemui.communal.proto.toCommunalHubState 36 import com.android.systemui.communal.shared.model.CommunalWidgetContentModel 37 import com.android.systemui.communal.shared.model.SpanValue 38 import com.android.systemui.communal.widgets.CommunalAppWidgetHost 39 import com.android.systemui.communal.widgets.CommunalWidgetHost 40 import com.android.systemui.communal.widgets.WidgetConfigurator 41 import com.android.systemui.dagger.SysUISingleton 42 import com.android.systemui.dagger.qualifiers.Background 43 import com.android.systemui.log.LogBuffer 44 import com.android.systemui.log.core.Logger 45 import com.android.systemui.log.dagger.CommunalLog 46 import javax.inject.Inject 47 import kotlin.coroutines.cancellation.CancellationException 48 import kotlinx.coroutines.CoroutineDispatcher 49 import kotlinx.coroutines.CoroutineScope 50 import kotlinx.coroutines.flow.Flow 51 import kotlinx.coroutines.flow.combine 52 import kotlinx.coroutines.flow.flatMapLatest 53 import kotlinx.coroutines.flow.flowOf 54 import kotlinx.coroutines.flow.flowOn 55 import kotlinx.coroutines.flow.map 56 57 /** Encapsulates the state of widgets for communal mode. */ 58 interface CommunalWidgetRepository { 59 /** A flow of the list of Glanceable Hub widgets ordered by rank. */ 60 val communalWidgets: Flow<List<CommunalWidgetContentModel>> 61 62 /** 63 * Add a widget in the app widget service and the database. 64 * 65 * @param rank The rank of the widget determines its position in the grid. 0 is first place, 1 66 * is second, etc. If rank is not specified, widget is added at the end. 67 */ 68 fun addWidget( 69 provider: ComponentName, 70 user: UserHandle, 71 rank: Int?, 72 configurator: WidgetConfigurator? = null, 73 ) {} 74 75 /** 76 * Delete a widget by id from the database and app widget host. 77 * 78 * @param widgetId id of the widget to remove. 79 */ 80 fun deleteWidget(widgetId: Int) {} 81 82 /** 83 * Update the order of widgets in the database. 84 * 85 * @param widgetIdToRankMap mapping of the widget ids to the rank of the widget. 86 */ 87 fun updateWidgetOrder(widgetIdToRankMap: Map<Int, Int>) 88 89 /** 90 * Restores the database by reading a state file from disk and updating the widget ids according 91 * to [oldToNewWidgetIdMap]. 92 */ 93 fun restoreWidgets(oldToNewWidgetIdMap: Map<Int, Int>) 94 95 /** Aborts the restore process and removes files from disk if necessary. */ 96 fun abortRestoreWidgets() 97 98 /** 99 * Update the spanY of a widget in the database. 100 * 101 * @param appWidgetId id of the widget to update. 102 * @param spanY new spanY value for the widget. 103 * @param widgetIdToRankMap mapping of the widget ids to its rank. Allows re-ordering widgets 104 * alongside the resize, in case resizing also requires re-ordering. This ensures the 105 * re-ordering is done in the same database transaction as the resize. 106 */ 107 fun resizeWidget(appWidgetId: Int, spanY: Int, widgetIdToRankMap: Map<Int, Int>) 108 } 109 110 /** 111 * The local implementation of the [CommunalWidgetRepository] that should be injected in a 112 * foreground user process. 113 */ 114 @SysUISingleton 115 class CommunalWidgetRepositoryLocalImpl 116 @Inject 117 constructor( 118 private val appWidgetHost: CommunalAppWidgetHost, 119 @Background private val bgScope: CoroutineScope, 120 @Background private val bgDispatcher: CoroutineDispatcher, 121 private val communalWidgetHost: CommunalWidgetHost, 122 private val communalWidgetDao: CommunalWidgetDao, 123 @CommunalLog logBuffer: LogBuffer, 124 private val backupManager: BackupManager, 125 private val backupUtils: CommunalBackupUtils, 126 packageChangeRepository: PackageChangeRepository, 127 private val userManager: UserManager, 128 private val defaultWidgetPopulation: DefaultWidgetPopulation, 129 ) : CommunalWidgetRepository { 130 companion object { 131 const val TAG = "CommunalWidgetRepositoryLocalImpl" 132 } 133 134 private val logger = Logger(logBuffer, TAG) 135 136 /** Widget metadata from database + matching [AppWidgetProviderInfo] if any. */ 137 private val widgetEntries: Flow<List<CommunalWidgetEntry>> = 138 combine(communalWidgetDao.getWidgets(), communalWidgetHost.appWidgetProviders) { entriesnull139 entries, 140 providers -> 141 entries.mapNotNull { (rank, widget) -> 142 CommunalWidgetEntry( 143 appWidgetId = widget.widgetId, 144 componentName = widget.componentName, 145 rank = rank.rank, 146 providerInfo = providers[widget.widgetId], 147 spanY = if (communalResponsiveGrid()) widget.spanYNew else widget.spanY, 148 ) 149 } 150 } 151 resizeWidgetnull152 override fun resizeWidget(appWidgetId: Int, spanY: Int, widgetIdToRankMap: Map<Int, Int>) { 153 if (!communalWidgetResizing()) return 154 val spanValue = 155 if (communalResponsiveGrid()) { 156 SpanValue.Responsive(spanY) 157 } else { 158 SpanValue.Fixed(spanY) 159 } 160 bgScope.launch { 161 communalWidgetDao.resizeWidget(appWidgetId, spanValue, widgetIdToRankMap) 162 logger.i({ "Updated spanY of widget $int1 to $int2." }) { 163 int1 = appWidgetId 164 int2 = spanY 165 } 166 backupManager.dataChanged() 167 } 168 } 169 170 override val communalWidgets: Flow<List<CommunalWidgetContentModel>> = 171 widgetEntries widgetEntriesnull172 .flatMapLatest { widgetEntries -> 173 // If and only if any widget is missing provider info, combine with the package 174 // installer sessions flow to check whether they are pending installation. This can 175 // happen after widgets are freshly restored from a backup. In most cases, provider 176 // info is available to all widgets, and is unnecessary to involve an API call to 177 // the package installer. 178 if (widgetEntries.any { it.providerInfo == null }) { 179 packageChangeRepository.packageInstallSessionsForPrimaryUser.map { sessions -> 180 widgetEntries.mapNotNull { entry -> mapToContentModel(entry, sessions) } 181 } 182 } else { 183 flowOf(widgetEntries.map(::mapToContentModel)) 184 } 185 } 186 // As this reads from a database and triggers IPCs to AppWidgetManager, 187 // it should be executed in the background. 188 .flowOn(bgDispatcher) 189 addWidgetnull190 override fun addWidget( 191 provider: ComponentName, 192 user: UserHandle, 193 rank: Int?, 194 configurator: WidgetConfigurator?, 195 ) { 196 bgScope.launch { 197 val id = communalWidgetHost.allocateIdAndBindWidget(provider, user) 198 if (id == null) { 199 logger.e("Failed to allocate widget id to ${provider.flattenToString()}") 200 return@launch 201 } 202 val info = communalWidgetHost.getAppWidgetInfo(id) 203 val configured = 204 if ( 205 configurator != null && 206 info != null && 207 CommunalWidgetHost.requiresConfiguration(info) 208 ) { 209 logger.i("Widget ${provider.flattenToString()} requires configuration.") 210 try { 211 configurator.configureWidget(id) 212 } catch (ex: Exception) { 213 // Cleanup the app widget id if an error happens during configuration. 214 logger.e("Error during widget configuration, cleaning up id $id", ex) 215 if (ex is CancellationException) { 216 appWidgetHost.deleteAppWidgetId(id) 217 // Re-throw cancellation to ensure the parent coroutine also gets 218 // cancelled. 219 throw ex 220 } else { 221 false 222 } 223 } 224 } else { 225 logger.i("Skipping configuration for ${provider.flattenToString()}") 226 true 227 } 228 if (configured) { 229 communalWidgetDao.addWidget( 230 widgetId = id, 231 provider = provider, 232 rank = rank, 233 userSerialNumber = userManager.getUserSerialNumber(user.identifier), 234 spanY = SpanValue.Fixed(3), 235 ) 236 backupManager.dataChanged() 237 } else { 238 appWidgetHost.deleteAppWidgetId(id) 239 } 240 logger.i("Added widget ${provider.flattenToString()} at position $rank.") 241 } 242 } 243 deleteWidgetnull244 override fun deleteWidget(widgetId: Int) { 245 bgScope.launch { 246 if (communalWidgetDao.deleteWidgetById(widgetId)) { 247 appWidgetHost.deleteAppWidgetId(widgetId) 248 logger.i("Deleted widget with id $widgetId.") 249 backupManager.dataChanged() 250 } 251 } 252 } 253 updateWidgetOrdernull254 override fun updateWidgetOrder(widgetIdToRankMap: Map<Int, Int>) { 255 bgScope.launch { 256 communalWidgetDao.updateWidgetOrder(widgetIdToRankMap) 257 logger.i({ "Updated the order of widget list with ids: $str1." }) { 258 str1 = widgetIdToRankMap.toString() 259 } 260 backupManager.dataChanged() 261 } 262 } 263 restoreWidgetsnull264 override fun restoreWidgets(oldToNewWidgetIdMap: Map<Int, Int>) { 265 bgScope.launch { 266 // Read restored state file from disk 267 val state: CommunalHubState 268 try { 269 state = backupUtils.readBytesFromDisk().toCommunalHubState() 270 } catch (e: Exception) { 271 logger.e({ "Failed reading restore data from disk: $str1" }) { 272 str1 = e.localizedMessage 273 } 274 abortRestoreWidgets() 275 return@launch 276 } 277 278 // Abort restoring widgets if this code is somehow run on a device that does not have 279 // a main user, e.g. auto. 280 val mainUser = userManager.mainUser 281 if (mainUser == null) { 282 logger.w("Skipped restoring widgets because device does not have a main user") 283 return@launch 284 } 285 286 val widgetsWithHost = appWidgetHost.appWidgetIds.toList() 287 val widgetsToRemove = widgetsWithHost.toMutableList() 288 289 val oldUserSerialNumbers = state.widgets.map { it.userSerialNumber }.distinct() 290 val usersMap = 291 oldUserSerialNumbers.associateWith { oldUserSerialNumber -> 292 if (oldUserSerialNumber == CommunalWidgetItem.USER_SERIAL_NUMBER_UNDEFINED) { 293 // If user serial number from the backup is undefined, the widget was added 294 // to the hub before user serial numbers are stored in the database. In this 295 // case, we restore the widget with the main user. 296 mainUser 297 } else { 298 // If the user serial number is defined, look up whether the user is 299 // restored. This API returns a user handle matching its backed up user 300 // serial number, if the user is restored. Otherwise, null is returned. 301 backupManager.getUserForAncestralSerialNumber(oldUserSerialNumber.toLong()) 302 ?: null 303 } 304 } 305 logger.d({ "Restored users map: $str1" }) { str1 = usersMap.toString() } 306 307 // A set to hold all widgets that belong to non-main users 308 val secondaryUserWidgets = mutableSetOf<CommunalHubState.CommunalWidgetItem>() 309 310 // Produce a new state to be restored, skipping invalid widgets 311 val newWidgets = 312 state.widgets.mapNotNull { restoredWidget -> 313 val newWidgetId = 314 oldToNewWidgetIdMap[restoredWidget.widgetId] ?: restoredWidget.widgetId 315 316 // Skip if widget id is not registered with the host 317 if (!widgetsWithHost.contains(newWidgetId)) { 318 logger.d({ 319 "Skipped restoring widget (old:$int1 new:$int2) " + 320 "because it is not registered with host" 321 }) { 322 int1 = restoredWidget.widgetId 323 int2 = newWidgetId 324 } 325 return@mapNotNull null 326 } 327 328 // Skip if user / profile is not registered 329 val newUser = usersMap[restoredWidget.userSerialNumber] 330 if (newUser == null) { 331 logger.d({ 332 "Skipped restoring widget $int1 because its user $int2 is not " + 333 "registered" 334 }) { 335 int1 = restoredWidget.widgetId 336 int2 = restoredWidget.userSerialNumber 337 } 338 return@mapNotNull null 339 } 340 341 // Place secondary user widgets in a bucket to be manually bound later because 342 // of a platform bug (b/349852237) that backs up work profile widgets as 343 // personal. 344 if (newUser.identifier != mainUser.identifier) { 345 logger.d({ 346 "Skipped restoring widget $int1 for now because its new user $int2 " + 347 "is secondary. This widget will be bound later." 348 }) { 349 int1 = restoredWidget.widgetId 350 int2 = newUser.identifier 351 } 352 secondaryUserWidgets.add(restoredWidget) 353 return@mapNotNull null 354 } 355 356 widgetsToRemove.remove(newWidgetId) 357 358 CommunalHubState.CommunalWidgetItem().apply { 359 widgetId = newWidgetId 360 componentName = restoredWidget.componentName 361 rank = restoredWidget.rank 362 userSerialNumber = userManager.getUserSerialNumber(newUser.identifier) 363 spanY = restoredWidget.spanY 364 } 365 } 366 val newState = CommunalHubState().apply { widgets = newWidgets.toTypedArray() } 367 368 // Skip default widgets population 369 defaultWidgetPopulation.skipDefaultWidgetsPopulation(RESTORED_FROM_BACKUP) 370 371 // Restore database 372 logger.i("Restoring communal database:\n$newState") 373 communalWidgetDao.restoreCommunalHubState(newState) 374 375 // Manually bind each secondary user widget due to platform bug b/349852237 376 secondaryUserWidgets.forEach { widget -> 377 val newUser = usersMap[widget.userSerialNumber]!! 378 logger.i({ "Binding secondary user ($int1) widget $int2: $str1" }) { 379 int1 = newUser.identifier 380 int2 = widget.widgetId 381 str1 = widget.componentName 382 } 383 addWidget( 384 provider = ComponentName.unflattenFromString(widget.componentName)!!, 385 user = newUser, 386 rank = widget.rank, 387 ) 388 } 389 390 // Delete restored state file from disk 391 backupUtils.clear() 392 393 // Remove widgets from host that have not been restored 394 widgetsToRemove.forEach { widgetId -> 395 logger.i({ "Deleting widget $int1 from host since it has not been restored" }) { 396 int1 = widgetId 397 } 398 appWidgetHost.deleteAppWidgetId(widgetId) 399 } 400 401 // Providers may have changed 402 communalWidgetHost.refreshProviders() 403 } 404 } 405 abortRestoreWidgetsnull406 override fun abortRestoreWidgets() { 407 bgScope.launch { 408 logger.i("Restore widgets aborted") 409 backupUtils.clear() 410 } 411 } 412 413 /** 414 * Maps a [CommunalWidgetEntry] to a [CommunalWidgetContentModel] with the assumption that the 415 * [AppWidgetProviderInfo] of the entry is available. 416 */ mapToContentModelnull417 private fun mapToContentModel(entry: CommunalWidgetEntry): CommunalWidgetContentModel { 418 return CommunalWidgetContentModel.Available( 419 appWidgetId = entry.appWidgetId, 420 providerInfo = entry.providerInfo!!, 421 rank = entry.rank, 422 spanY = entry.spanY, 423 ) 424 } 425 426 /** 427 * Maps a [CommunalWidgetEntry] to a [CommunalWidgetContentModel] with a list of install 428 * sessions. If the [AppWidgetProviderInfo] of the entry is absent, and its package is in the 429 * install sessions, the entry is mapped to a pending widget. 430 */ mapToContentModelnull431 private fun mapToContentModel( 432 entry: CommunalWidgetEntry, 433 installSessions: List<PackageInstallSession>, 434 ): CommunalWidgetContentModel? { 435 if (entry.providerInfo != null) { 436 return CommunalWidgetContentModel.Available( 437 appWidgetId = entry.appWidgetId, 438 providerInfo = entry.providerInfo!!, 439 rank = entry.rank, 440 spanY = entry.spanY, 441 ) 442 } 443 444 val componentName = ComponentName.unflattenFromString(entry.componentName) 445 val session = installSessions.firstOrNull { it.packageName == componentName?.packageName } 446 return if (componentName != null && session != null) { 447 CommunalWidgetContentModel.Pending( 448 appWidgetId = entry.appWidgetId, 449 rank = entry.rank, 450 componentName = componentName, 451 icon = session.icon, 452 user = session.user, 453 spanY = entry.spanY, 454 ) 455 } else { 456 null 457 } 458 } 459 460 private data class CommunalWidgetEntry( 461 val appWidgetId: Int, 462 val componentName: String, 463 val rank: Int, 464 val spanY: Int, 465 var providerInfo: AppWidgetProviderInfo? = null, 466 ) 467 } 468