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