• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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