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 com.android.systemui.common.data.repository.PackageChangeRepository 24 import com.android.systemui.common.shared.model.PackageInstallSession 25 import com.android.systemui.communal.data.backup.CommunalBackupUtils 26 import com.android.systemui.communal.data.db.CommunalWidgetDao 27 import com.android.systemui.communal.nano.CommunalHubState 28 import com.android.systemui.communal.proto.toCommunalHubState 29 import com.android.systemui.communal.shared.model.CommunalWidgetContentModel 30 import com.android.systemui.communal.widgets.CommunalAppWidgetHost 31 import com.android.systemui.communal.widgets.CommunalWidgetHost 32 import com.android.systemui.communal.widgets.WidgetConfigurator 33 import com.android.systemui.dagger.SysUISingleton 34 import com.android.systemui.dagger.qualifiers.Background 35 import com.android.systemui.log.LogBuffer 36 import com.android.systemui.log.core.Logger 37 import com.android.systemui.log.dagger.CommunalLog 38 import javax.inject.Inject 39 import kotlin.coroutines.cancellation.CancellationException 40 import kotlinx.coroutines.CoroutineDispatcher 41 import kotlinx.coroutines.CoroutineScope 42 import kotlinx.coroutines.ExperimentalCoroutinesApi 43 import kotlinx.coroutines.flow.Flow 44 import kotlinx.coroutines.flow.combine 45 import kotlinx.coroutines.flow.flatMapLatest 46 import kotlinx.coroutines.flow.flowOf 47 import kotlinx.coroutines.flow.flowOn 48 import kotlinx.coroutines.flow.map 49 import kotlinx.coroutines.launch 50 51 /** Encapsulates the state of widgets for communal mode. */ 52 interface CommunalWidgetRepository { 53 /** A flow of information about active communal widgets stored in database. */ 54 val communalWidgets: Flow<List<CommunalWidgetContentModel>> 55 56 /** Add a widget at the specified position in the app widget service and the database. */ 57 fun addWidget( 58 provider: ComponentName, 59 user: UserHandle, 60 priority: Int, 61 configurator: WidgetConfigurator? = null 62 ) {} 63 64 /** 65 * Delete a widget by id from the database and app widget host. 66 * 67 * @param widgetId id of the widget to remove. 68 */ 69 fun deleteWidget(widgetId: Int) {} 70 71 /** 72 * Update the order of widgets in the database. 73 * 74 * @param widgetIdToPriorityMap mapping of the widget ids to the priority of the widget. 75 */ 76 fun updateWidgetOrder(widgetIdToPriorityMap: Map<Int, Int>) {} 77 78 /** 79 * Restores the database by reading a state file from disk and updating the widget ids according 80 * to [oldToNewWidgetIdMap]. 81 */ 82 fun restoreWidgets(oldToNewWidgetIdMap: Map<Int, Int>) 83 84 /** Aborts the restore process and removes files from disk if necessary. */ 85 fun abortRestoreWidgets() 86 } 87 88 @SysUISingleton 89 class CommunalWidgetRepositoryImpl 90 @Inject 91 constructor( 92 private val appWidgetHost: CommunalAppWidgetHost, 93 @Background private val bgScope: CoroutineScope, 94 @Background private val bgDispatcher: CoroutineDispatcher, 95 private val communalWidgetHost: CommunalWidgetHost, 96 private val communalWidgetDao: CommunalWidgetDao, 97 @CommunalLog logBuffer: LogBuffer, 98 private val backupManager: BackupManager, 99 private val backupUtils: CommunalBackupUtils, 100 packageChangeRepository: PackageChangeRepository, 101 ) : CommunalWidgetRepository { 102 companion object { 103 const val TAG = "CommunalWidgetRepository" 104 } 105 106 private val logger = Logger(logBuffer, TAG) 107 108 /** Widget metadata from database + matching [AppWidgetProviderInfo] if any. */ 109 private val widgetEntries: Flow<List<CommunalWidgetEntry>> = 110 combine( 111 communalWidgetDao.getWidgets(), 112 communalWidgetHost.appWidgetProviders, entriesnull113 ) { entries, providers -> 114 entries.mapNotNull { (rank, widget) -> 115 CommunalWidgetEntry( 116 appWidgetId = widget.widgetId, 117 componentName = widget.componentName, 118 priority = rank.rank, 119 providerInfo = providers[widget.widgetId] 120 ) 121 } 122 } 123 124 @OptIn(ExperimentalCoroutinesApi::class) 125 override val communalWidgets: Flow<List<CommunalWidgetContentModel>> = 126 widgetEntries widgetEntriesnull127 .flatMapLatest { widgetEntries -> 128 // If and only if any widget is missing provider info, combine with the package 129 // installer sessions flow to check whether they are pending installation. This can 130 // happen after widgets are freshly restored from a backup. In most cases, provider 131 // info is available to all widgets, and is unnecessary to involve an API call to 132 // the package installer. 133 if (widgetEntries.any { it.providerInfo == null }) { 134 packageChangeRepository.packageInstallSessionsForPrimaryUser.map { sessions -> 135 widgetEntries.mapNotNull { entry -> mapToContentModel(entry, sessions) } 136 } 137 } else { 138 flowOf(widgetEntries.map(::mapToContentModel)) 139 } 140 } 141 // As this reads from a database and triggers IPCs to AppWidgetManager, 142 // it should be executed in the background. 143 .flowOn(bgDispatcher) 144 addWidgetnull145 override fun addWidget( 146 provider: ComponentName, 147 user: UserHandle, 148 priority: Int, 149 configurator: WidgetConfigurator? 150 ) { 151 bgScope.launch { 152 val id = communalWidgetHost.allocateIdAndBindWidget(provider, user) 153 if (id == null) { 154 logger.e("Failed to allocate widget id to ${provider.flattenToString()}") 155 return@launch 156 } 157 val info = communalWidgetHost.getAppWidgetInfo(id) 158 val configured = 159 if ( 160 configurator != null && 161 info != null && 162 CommunalWidgetHost.requiresConfiguration(info) 163 ) { 164 logger.i("Widget ${provider.flattenToString()} requires configuration.") 165 try { 166 configurator.configureWidget(id) 167 } catch (ex: Exception) { 168 // Cleanup the app widget id if an error happens during configuration. 169 logger.e("Error during widget configuration, cleaning up id $id", ex) 170 if (ex is CancellationException) { 171 appWidgetHost.deleteAppWidgetId(id) 172 // Re-throw cancellation to ensure the parent coroutine also gets 173 // cancelled. 174 throw ex 175 } else { 176 false 177 } 178 } 179 } else { 180 logger.i("Skipping configuration for ${provider.flattenToString()}") 181 true 182 } 183 if (configured) { 184 communalWidgetDao.addWidget( 185 widgetId = id, 186 provider = provider, 187 priority = priority, 188 ) 189 backupManager.dataChanged() 190 } else { 191 appWidgetHost.deleteAppWidgetId(id) 192 } 193 logger.i("Added widget ${provider.flattenToString()} at position $priority.") 194 } 195 } 196 deleteWidgetnull197 override fun deleteWidget(widgetId: Int) { 198 bgScope.launch { 199 if (communalWidgetDao.deleteWidgetById(widgetId)) { 200 appWidgetHost.deleteAppWidgetId(widgetId) 201 logger.i("Deleted widget with id $widgetId.") 202 backupManager.dataChanged() 203 } 204 } 205 } 206 updateWidgetOrdernull207 override fun updateWidgetOrder(widgetIdToPriorityMap: Map<Int, Int>) { 208 bgScope.launch { 209 communalWidgetDao.updateWidgetOrder(widgetIdToPriorityMap) 210 logger.i({ "Updated the order of widget list with ids: $str1." }) { 211 str1 = widgetIdToPriorityMap.toString() 212 } 213 backupManager.dataChanged() 214 } 215 } 216 restoreWidgetsnull217 override fun restoreWidgets(oldToNewWidgetIdMap: Map<Int, Int>) { 218 bgScope.launch { 219 // Read restored state file from disk 220 val state: CommunalHubState 221 try { 222 state = backupUtils.readBytesFromDisk().toCommunalHubState() 223 } catch (e: Exception) { 224 logger.e({ "Failed reading restore data from disk: $str1" }) { 225 str1 = e.localizedMessage 226 } 227 abortRestoreWidgets() 228 return@launch 229 } 230 231 val widgetsWithHost = appWidgetHost.appWidgetIds.toList() 232 val widgetsToRemove = widgetsWithHost.toMutableList() 233 234 // Produce a new state to be restored, skipping invalid widgets 235 val newWidgets = 236 state.widgets.mapNotNull { restoredWidget -> 237 val newWidgetId = 238 oldToNewWidgetIdMap[restoredWidget.widgetId] ?: restoredWidget.widgetId 239 240 // Skip if widget id is not registered with the host 241 if (!widgetsWithHost.contains(newWidgetId)) { 242 logger.d({ 243 "Skipped restoring widget (old:$int1 new:$int2) " + 244 "because it is not registered with host" 245 }) { 246 int1 = restoredWidget.widgetId 247 int2 = newWidgetId 248 } 249 return@mapNotNull null 250 } 251 252 widgetsToRemove.remove(newWidgetId) 253 254 CommunalHubState.CommunalWidgetItem().apply { 255 widgetId = newWidgetId 256 componentName = restoredWidget.componentName 257 rank = restoredWidget.rank 258 } 259 } 260 val newState = CommunalHubState().apply { widgets = newWidgets.toTypedArray() } 261 262 // Restore database 263 logger.i("Restoring communal database $newState") 264 communalWidgetDao.restoreCommunalHubState(newState) 265 266 // Delete restored state file from disk 267 backupUtils.clear() 268 269 // Remove widgets from host that have not been restored 270 widgetsToRemove.forEach { widgetId -> 271 logger.i({ "Deleting widget $int1 from host since it has not been restored" }) { 272 int1 = widgetId 273 } 274 appWidgetHost.deleteAppWidgetId(widgetId) 275 } 276 277 // Providers may have changed 278 communalWidgetHost.refreshProviders() 279 } 280 } 281 abortRestoreWidgetsnull282 override fun abortRestoreWidgets() { 283 bgScope.launch { 284 logger.i("Restore widgets aborted") 285 backupUtils.clear() 286 } 287 } 288 289 /** 290 * Maps a [CommunalWidgetEntry] to a [CommunalWidgetContentModel] with the assumption that the 291 * [AppWidgetProviderInfo] of the entry is available. 292 */ mapToContentModelnull293 private fun mapToContentModel(entry: CommunalWidgetEntry): CommunalWidgetContentModel { 294 return CommunalWidgetContentModel.Available( 295 appWidgetId = entry.appWidgetId, 296 providerInfo = entry.providerInfo!!, 297 priority = entry.priority, 298 ) 299 } 300 301 /** 302 * Maps a [CommunalWidgetEntry] to a [CommunalWidgetContentModel] with a list of install 303 * sessions. If the [AppWidgetProviderInfo] of the entry is absent, and its package is in the 304 * install sessions, the entry is mapped to a pending widget. 305 */ mapToContentModelnull306 private fun mapToContentModel( 307 entry: CommunalWidgetEntry, 308 installSessions: List<PackageInstallSession>, 309 ): CommunalWidgetContentModel? { 310 if (entry.providerInfo != null) { 311 return CommunalWidgetContentModel.Available( 312 appWidgetId = entry.appWidgetId, 313 providerInfo = entry.providerInfo!!, 314 priority = entry.priority, 315 ) 316 } 317 318 val session = 319 installSessions.firstOrNull { 320 it.packageName == 321 ComponentName.unflattenFromString(entry.componentName)?.packageName 322 } 323 return if (session != null) { 324 CommunalWidgetContentModel.Pending( 325 appWidgetId = entry.appWidgetId, 326 priority = entry.priority, 327 packageName = session.packageName, 328 icon = session.icon, 329 user = session.user, 330 ) 331 } else { 332 null 333 } 334 } 335 336 private data class CommunalWidgetEntry( 337 val appWidgetId: Int, 338 val componentName: String, 339 val priority: Int, 340 var providerInfo: AppWidgetProviderInfo? = null, 341 ) 342 } 343