1 /*
<lambda>null2  * Copyright 2021 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 androidx.glance.appwidget
18 
19 import android.app.Application
20 import android.app.PendingIntent
21 import android.appwidget.AppWidgetManager
22 import android.appwidget.AppWidgetProviderInfo
23 import android.appwidget.AppWidgetProviderInfo.WIDGET_CATEGORY_HOME_SCREEN
24 import android.appwidget.AppWidgetProviderInfo.WIDGET_CATEGORY_KEYGUARD
25 import android.appwidget.AppWidgetProviderInfo.WIDGET_CATEGORY_SEARCHBOX
26 import android.content.ComponentName
27 import android.content.Context
28 import android.content.Intent
29 import android.os.Build
30 import android.os.Bundle
31 import android.util.Log
32 import android.widget.RemoteViews
33 import androidx.annotation.DoNotInline
34 import androidx.annotation.RequiresApi
35 import androidx.annotation.VisibleForTesting
36 import androidx.collection.IntSet
37 import androidx.collection.intSetOf
38 import androidx.compose.ui.unit.DpSize
39 import androidx.datastore.core.DataStore
40 import androidx.datastore.preferences.core.Preferences
41 import androidx.datastore.preferences.core.edit
42 import androidx.datastore.preferences.core.stringPreferencesKey
43 import androidx.datastore.preferences.core.stringSetPreferencesKey
44 import androidx.datastore.preferences.preferencesDataStore
45 import androidx.datastore.preferences.preferencesDataStoreFile
46 import androidx.glance.GlanceId
47 import kotlin.reflect.KClass
48 import kotlinx.coroutines.flow.first
49 import kotlinx.coroutines.flow.firstOrNull
50 
51 /**
52  * Manager for Glance App Widgets.
53  *
54  * This is used to query the app widgets currently installed on the system, and some of their
55  * properties.
56  */
57 class GlanceAppWidgetManager(private val context: Context) {
58 
59     private data class State(
60         val receiverToProviderName: Map<ComponentName, String> = emptyMap(),
61         val providerNameToReceivers: Map<String, List<ComponentName>> = emptyMap(),
62     ) {
63         constructor(
64             receiverToProviderName: Map<ComponentName, String>
65         ) : this(receiverToProviderName, receiverToProviderName.reverseMapping())
66     }
67 
68     private val appWidgetManager = AppWidgetManager.getInstance(context)
69     private val dataStore by lazy { getOrCreateDataStore() }
70 
71     private fun getOrCreateDataStore(): DataStore<Preferences> {
72         synchronized(GlanceAppWidgetManager) {
73             return dataStoreSingleton
74                 ?: run {
75                     // Delete old file format that did not include the process name.
76                     context
77                         .preferencesDataStoreFile("GlanceAppWidgetManager")
78                         .takeIf { it.exists() }
79                         ?.delete()
80 
81                     val newValue = context.appManagerDataStore
82                     dataStoreSingleton = newValue
83                     newValue
84                 }
85         }
86     }
87 
88     internal suspend fun <R : GlanceAppWidgetReceiver, P : GlanceAppWidget> updateReceiver(
89         receiver: R,
90         provider: P,
91     ) {
92         val receiverName = receiver.canonicalName()
93         val providerName = provider.canonicalName()
94         dataStore.updateData { pref ->
95             pref
96                 .toMutablePreferences()
97                 .also { builder ->
98                     builder[providersKey] = (pref[providersKey] ?: emptySet()) + receiverName
99                     builder[providerKey(receiverName)] = providerName
100                 }
101                 .toPreferences()
102         }
103     }
104 
105     private fun createState(prefs: Preferences): State {
106         val packageName = context.packageName
107         val receivers = prefs[providersKey] ?: return State()
108         return State(
109             receivers
110                 .mapNotNull { receiverName ->
111                     val comp = ComponentName(packageName, receiverName)
112                     val providerName = prefs[providerKey(receiverName)] ?: return@mapNotNull null
113                     comp to providerName
114                 }
115                 .toMap()
116         )
117     }
118 
119     private suspend fun getState(): State {
120         // Preferences won't contain value for providersKey when either -
121         // 1. App doesn't have any widget placed, but app requested for glanceIds for a widget class
122         // 2. User cleared app data, so, the provider to receivers mapping is lost (even if widgets
123         // are still pinned).
124         // In case of #2, we want to return an appropriate list of glance ids, so, we back-fill the
125         // datastore with all known glance receivers and providers.
126         // #1 isn't something that an app would commonly do, and even if it does, it would get empty
127         // IDs as expected.
128         return createState(
129             prefs =
130                 dataStore.data.first().takeIf { it[providersKey] != null }
131                     ?: addAllReceiversAndProvidersToPreferences()
132         )
133     }
134 
135     /** Returns the [GlanceId] of the App Widgets installed for a particular provider. */
136     suspend fun <T : GlanceAppWidget> getGlanceIds(provider: Class<T>): List<GlanceId> {
137         val state = getState()
138         val providerName = requireNotNull(provider.canonicalName) { "no canonical provider name" }
139         val receivers = state.providerNameToReceivers[providerName] ?: return emptyList()
140         return receivers.flatMap { receiver ->
141             appWidgetManager.getAppWidgetIds(receiver).map { AppWidgetId(it) }
142         }
143     }
144 
145     /**
146      * Retrieve the sizes for a given App Widget, if provided by the host.
147      *
148      * The list of sizes will be extracted from the App Widget options bundle, using the content of
149      * [AppWidgetManager.OPTION_APPWIDGET_SIZES] if provided. If not, and if
150      * [AppWidgetManager.OPTION_APPWIDGET_MIN_HEIGHT] and similar are provided, the landscape and
151      * portrait sizes will be estimated from those and returned. Otherwise, the list will contain
152      * [DpSize.Zero] only.
153      */
154     suspend fun getAppWidgetSizes(glanceId: GlanceId): List<DpSize> {
155         require(glanceId is AppWidgetId) { "This method only accepts App Widget Glance Id" }
156         val bundle = appWidgetManager.getAppWidgetOptions(glanceId.appWidgetId)
157         return bundle.extractAllSizes { DpSize.Zero }
158     }
159 
160     /**
161      * Retrieve the platform AppWidget ID from the provided GlanceId
162      *
163      * Important: Do NOT use appwidget ID as identifier, instead create your own and store them in
164      * the GlanceStateDefinition. This method should only be used for compatibility or IPC
165      * communication reasons in conjunction with [getGlanceIdBy]
166      */
167     fun getAppWidgetId(glanceId: GlanceId): Int {
168         require(glanceId is AppWidgetId) { "This method only accepts App Widget Glance Id" }
169         return glanceId.appWidgetId
170     }
171 
172     /**
173      * Retrieve the GlanceId of the provided AppWidget ID.
174      *
175      * @throws IllegalArgumentException if the provided id is not associated with an existing
176      *   GlanceId
177      */
178     fun getGlanceIdBy(appWidgetId: Int): GlanceId {
179         requireNotNull(appWidgetManager.getAppWidgetInfo(appWidgetId)) { "Invalid AppWidget ID." }
180         return AppWidgetId(appWidgetId)
181     }
182 
183     /** Retrieve the GlanceId from the configuration activity intent or null if not valid */
184     fun getGlanceIdBy(configurationIntent: Intent): GlanceId? {
185         val appWidgetId =
186             configurationIntent.extras?.getInt(
187                 AppWidgetManager.EXTRA_APPWIDGET_ID,
188                 AppWidgetManager.INVALID_APPWIDGET_ID
189             ) ?: AppWidgetManager.INVALID_APPWIDGET_ID
190 
191         if (appWidgetId == AppWidgetManager.INVALID_APPWIDGET_ID) {
192             return null
193         }
194 
195         return AppWidgetId(appWidgetId)
196     }
197 
198     /**
199      * Request to pin the [GlanceAppWidget] of the given receiver on the current launcher (if
200      * supported).
201      *
202      * Note: the request is only supported for SDK 26 and beyond, for lower versions this method
203      * will be no-op and return false.
204      *
205      * @param receiver the target [GlanceAppWidgetReceiver] class
206      * @param preview the instance of the GlanceAppWidget to compose the preview that will be shown
207      *   in the request dialog. When not provided the app widget previewImage (as defined in the
208      *   meta-data) will be used instead, or the app's icon if not available either.
209      * @param previewState the state (as defined by the [GlanceAppWidget.stateDefinition] to use for
210      *   the preview
211      * @param successCallback a [PendingIntent] to be invoked if the app widget pinning is accepted
212      *   by the user
213      * @return true if the request was successfully sent to the system, false otherwise
214      * @see AppWidgetManager.requestPinAppWidget for more information and limitations
215      */
216     suspend fun <T : GlanceAppWidgetReceiver> requestPinGlanceAppWidget(
217         receiver: Class<T>,
218         preview: GlanceAppWidget? = null,
219         previewState: Any? = null,
220         successCallback: PendingIntent? = null,
221     ): Boolean {
222         return requestPinGlanceAppWidget(
223             receiver = receiver,
224             preview = preview,
225             previewSize = null,
226             previewState = previewState,
227             successCallback = successCallback,
228         )
229     }
230 
231     /**
232      * Request to pin the [GlanceAppWidget] of the given receiver on the current launcher (if
233      * supported).
234      *
235      * Note: the request is only supported for SDK 26 and beyond, for lower versions this method
236      * will be no-op and return false.
237      *
238      * @param receiver the target [GlanceAppWidgetReceiver] class
239      * @param preview the instance of the GlanceAppWidget to compose the preview that will be shown
240      *   in the request dialog. When not provided the app widget previewImage (as defined in the
241      *   meta-data) will be used instead, or the app's icon if not available either.
242      * @param previewState the state (as defined by the [GlanceAppWidget.stateDefinition] to use for
243      *   the preview
244      * @param previewSize the size to be used for the preview. If none is provided, the widget's
245      *   minimum size (as determined by its' AppWidgetProviderInfo) will be used.
246      * @param successCallback a [PendingIntent] to be invoked if the app widget pinning is accepted
247      *   by the user
248      * @return true if the request was successfully sent to the system, false otherwise
249      * @see AppWidgetManager.requestPinAppWidget for more information and limitations
250      */
251     suspend fun <T : GlanceAppWidgetReceiver> requestPinGlanceAppWidget(
252         receiver: Class<T>,
253         preview: GlanceAppWidget? = null,
254         previewSize: DpSize? = null,
255         previewState: Any? = null,
256         successCallback: PendingIntent? = null,
257     ): Boolean {
258         if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
259             return false
260         }
261         if (AppWidgetManagerApi26Impl.isRequestPinAppWidgetSupported(appWidgetManager)) {
262             val target = ComponentName(context.packageName, receiver.name)
263             val previewBundle =
264                 Bundle().apply {
265                     if (preview != null) {
266                         val info =
267                             appWidgetManager.installedProviders.first { it.provider == target }
268                         val snapshot =
269                             preview.compose(
270                                 context = context,
271                                 id = AppWidgetId(AppWidgetManager.INVALID_APPWIDGET_ID),
272                                 state = previewState,
273                                 options = Bundle.EMPTY,
274                                 size =
275                                     previewSize
276                                         ?: info.getMinSize(context.resources.displayMetrics),
277                             )
278                         putParcelable(AppWidgetManager.EXTRA_APPWIDGET_PREVIEW, snapshot)
279                     }
280                 }
281             return AppWidgetManagerApi26Impl.requestPinAppWidget(
282                 appWidgetManager,
283                 target,
284                 previewBundle,
285                 successCallback
286             )
287         }
288         return false
289     }
290 
291     /**
292      * Generate and publish the widget previews for [receiver] for the given set of
293      * [widgetCategories]. Previews are generated from the layout provided by
294      * [GlanceAppWidget.providePreview] on the widget connected to the given [receiver].
295      *
296      * Previews should be published during the initial setup phase or launch of your app. To avoid
297      * running this unnecessarily, you can see what previews are currently published for your
298      * provider by checking [AppWidgetProviderInfo.generatedPreviewCategories].
299      *
300      * The preview composition is run for each value in the [widgetCategories] array. By default, a
301      * single preview is generated for all widget categories, i.e. `widgetsCategories =
302      * intSetOf(WIDGET_CATEGORY_HOME_SCREEN or WIDGET_CATEGORY_KEYGUARD or
303      * WIDGET_CATEGORY_SEARCHBOX)`. To generate a separate preview for each widget category, pass
304      * each category as a separate item in the int set, e.g. `intSetOf(WIDGET_CATEGORY_HOME_SCREEN,
305      * WIDGET_CATEGORY_KEYGUARD)`. This is only necessary if you want to generate different layouts
306      * for the different categories.
307      *
308      * Note that this API is only available on [Build.VERSION_CODES.VANILLA_ICE_CREAM] and above, so
309      * you will likely want to set [AppWidgetProviderInfo.previewLayout] and
310      * [AppWidgetProviderInfo.previewImage] as well to have the most coverage across versions.
311      *
312      * See also [AppWidgetProviderInfo.generatedPreviewCategories],
313      * [AppWidgetManager.setWidgetPreview], [AppWidgetManager.getWidgetPreview], and
314      * [AppWidgetManager.removeWidgetPreview].
315      *
316      * @param receiver the [GlanceAppWidgetReceiver] which holds the [GlanceAppWidget] to run. This
317      *   receiver must registered as an app widget provider in the application manifest.
318      * @param widgetCategories the widget categories for which to set previews. Each element of this
319      *   set must be a combination of [WIDGET_CATEGORY_HOME_SCREEN], [WIDGET_CATEGORY_KEYGUARD], or
320      *   [WIDGET_CATEGORY_SEARCHBOX].
321      * @return true if the preview was successfully updated, false if otherwise.
322      *   [AppWidgetManager.setWidgetPreview] may return false when the caller has hit a
323      *   system-defined rate limit on setting previews for a particular provider. In this case, you
324      *   may opt to schedule a task in the future to try again, if necessary.
325      */
326     suspend fun setWidgetPreviews(
327         receiver: KClass<out GlanceAppWidgetReceiver>,
328         widgetCategories: IntSet =
329             intSetOf(
330                 WIDGET_CATEGORY_HOME_SCREEN or WIDGET_CATEGORY_KEYGUARD or WIDGET_CATEGORY_SEARCHBOX
331             ),
332     ): Boolean {
333         if (Build.VERSION.SDK_INT < Build.VERSION_CODES.VANILLA_ICE_CREAM) {
334             Log.w(TAG, "setWidgetPreviews is not supported at the current SDK level")
335             return false
336         }
337         val glanceAppWidget =
338             (receiver.java.constructors.first { it.parameters.isEmpty() }.newInstance()
339                     as GlanceAppWidgetReceiver)
340                 .glanceAppWidget
341         val componentName = ComponentName(context, receiver.java)
342         val providerInfo =
343             if (glanceAppWidget.previewSizeMode == SizeMode.Single) {
344                 appWidgetManager.installedProviders.firstOrNull { it.provider == componentName }
345             } else {
346                 null
347             }
348         return widgetCategories.all { category ->
349             val preview = glanceAppWidget.composeForPreview(context, category, providerInfo)
350             AppWidgetManagerApi35Impl.setWidgetPreview(
351                 appWidgetManager,
352                 componentName,
353                 category,
354                 preview,
355             )
356         }
357     }
358 
359     /** Check which receivers still exist, and clean the data store to only keep those. */
360     internal suspend fun cleanReceivers() {
361         val packageName = context.packageName
362         val receivers =
363             appWidgetManager.installedProviders
364                 .filter { it.provider.packageName == packageName }
365                 .map { it.provider.className }
366                 .toSet()
367         dataStore.updateData { prefs ->
368             val knownReceivers = prefs[providersKey] ?: return@updateData prefs
369             val toRemove = knownReceivers.filter { it !in receivers }
370             if (toRemove.isEmpty()) return@updateData prefs
371             prefs
372                 .toMutablePreferences()
373                 .apply {
374                     this[providersKey] = knownReceivers - toRemove
375                     @Suppress("ListIterator")
376                     toRemove.forEach { receiver -> remove(providerKey(receiver)) }
377                 }
378                 .toPreferences()
379         }
380     }
381 
382     /**
383      * Identifies [GlanceAppWidget] (provider) for each [GlanceAppWidgetReceiver] in the app and
384      * saves the mapping in the preferences datastore. Also stores the set of glance-based receiver
385      * class names.
386      *
387      * [getGlanceIds] looks up the set of associated receivers for the given [GlanceAppWidget]
388      * (provider) from the datastore to be able to get the appwidget ids from [AppWidgetManager].
389      *
390      * Typically, the information is stored / overwritten by [updateReceiver] during widget
391      * lifecycle, however, when app data is cleared by the user, it is lost. So, we reconstruct it
392      * (for all known glance-based receivers).
393      *
394      * Follow b/305232907 to know the recommendation from appWidgets on handling cleared app data
395      * scenarios for widgets.
396      */
397     @Suppress("ListIterator")
398     private suspend fun addAllReceiversAndProvidersToPreferences(): Preferences {
399         val installedGlanceAppWidgetReceivers =
400             appWidgetManager.installedProviders
401                 .filter { it.provider.packageName == context.packageName }
402                 .mapNotNull { it.maybeGlanceAppWidgetReceiver() }
403 
404         return dataStore.updateData { prefs ->
405             prefs
406                 .toMutablePreferences()
407                 .apply {
408                     this[providersKey] =
409                         installedGlanceAppWidgetReceivers.map { it.javaClass.name }.toSet()
410                     installedGlanceAppWidgetReceivers.forEach {
411                         this[providerKey(it.canonicalName())] = it.glanceAppWidget.canonicalName()
412                     }
413                 }
414                 .toPreferences()
415         }
416     }
417 
418     @VisibleForTesting
419     internal suspend fun listKnownReceivers(): Collection<String>? =
420         dataStore.data.firstOrNull()?.let { it[providersKey] }
421 
422     /**
423      * Clears the datastore that holds information about glance app widgets (providers) and
424      * receivers.
425      *
426      * Useful for tests that wish to mimic clearing app data.
427      */
428     @VisibleForTesting
429     internal suspend fun clearDataStore() {
430         dataStore.edit { it.clear() }
431     }
432 
433     private companion object {
434         private val Context.appManagerDataStore by
435             preferencesDataStore(name = "GlanceAppWidgetManager-$processName")
436 
437         private val processName: String
438             get() =
439                 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
440                     Application.getProcessName()
441                 } else {
442                     Class.forName("android.app.ActivityThread")
443                         .getDeclaredMethod("currentProcessName")
444                         .apply { isAccessible = true }
445                         .invoke(null) as String
446                 }
447 
448         private var dataStoreSingleton: DataStore<Preferences>? = null
449         private val providersKey = stringSetPreferencesKey("list::Providers")
450 
451         private fun providerKey(provider: String) = stringPreferencesKey("provider:$provider")
452 
453         private fun GlanceAppWidgetReceiver.canonicalName() =
454             requireNotNull(this.javaClass.canonicalName) { "no receiver name" }
455 
456         private fun GlanceAppWidget.canonicalName() =
457             requireNotNull(this.javaClass.canonicalName) { "no provider name" }
458 
459         private fun AppWidgetProviderInfo.maybeGlanceAppWidgetReceiver(): GlanceAppWidgetReceiver? {
460             val receiver = Class.forName(provider.className).getDeclaredConstructor().newInstance()
461             if (receiver is GlanceAppWidgetReceiver) {
462                 return receiver
463             }
464             return null
465         }
466 
467         private const val TAG = "GlanceAppWidgetManager"
468     }
469 
470     @RequiresApi(Build.VERSION_CODES.O)
471     private object AppWidgetManagerApi26Impl {
472 
473         fun isRequestPinAppWidgetSupported(manager: AppWidgetManager) =
474             manager.isRequestPinAppWidgetSupported
475 
476         fun requestPinAppWidget(
477             manager: AppWidgetManager,
478             target: ComponentName,
479             extras: Bundle?,
480             successCallback: PendingIntent?,
481         ) = manager.requestPinAppWidget(target, extras, successCallback)
482     }
483 
484     @RequiresApi(Build.VERSION_CODES.VANILLA_ICE_CREAM)
485     private object AppWidgetManagerApi35Impl {
486         @DoNotInline
487         fun setWidgetPreview(
488             manager: AppWidgetManager,
489             provider: ComponentName,
490             category: Int,
491             preview: RemoteViews,
492         ): Boolean {
493             return manager.setWidgetPreview(provider, category, preview)
494         }
495     }
496 }
497 
498 /**
499  * Generate and publish the widget previews for a [GlanceAppWidgetReceiver] for the given set of
500  * [widgetCategories]. Previews are generated from the layout provided by
501  * [GlanceAppWidget.providePreview] on the widget connected to the given [GlanceAppWidgetReceiver]
502  * class. This receiver must registered as an app widget provider in the application manifest.
503  *
504  * Previews should be published during the initial setup phase or launch of your app. To avoid
505  * running this unnecessarily, you can see what previews are currently published for your provider
506  * by checking [AppWidgetProviderInfo.generatedPreviewCategories].
507  *
508  * The preview composition is run for each value in the [widgetCategories] array. If your widget has
509  * the same layout across categories, you can combine all of the categories in a single value, e.g.
510  * `WIDGET_CATEGORY_HOME_SCREEN or WIDGET_CATEGORY_KEYGUARD or WIDGET_CATEGORY_SEARCHBOX`, which
511  * will call [composeForPreview] once and set the previews for all of the categories.
512  *
513  * Note that this API is only available on [Build.VERSION_CODES.VANILLA_ICE_CREAM] and above, so you
514  * will likely want to set [AppWidgetProviderInfo.previewLayout] and
515  * [AppWidgetProviderInfo.previewImage] as well to have the most coverage across versions.
516  *
517  * See also [AppWidgetProviderInfo.generatedPreviewCategories], [AppWidgetManager.setWidgetPreview],
518  * [AppWidgetManager.getWidgetPreview], and [AppWidgetManager.removeWidgetPreview].
519  *
520  * @param widgetCategories the widget categories for which to set previews. Each element of this set
521  *   must be a combination of [WIDGET_CATEGORY_HOME_SCREEN], [WIDGET_CATEGORY_KEYGUARD], or
522  *   [WIDGET_CATEGORY_SEARCHBOX].
523  * @return true if the preview was successfully updated, false if otherwise.
524  *   [AppWidgetManager.setWidgetPreview] may return false when the caller has hit a system-defined
525  *   rate limit on setting previews for a particular provider. In this case, you may opt to schedule
526  *   a task in the future to try again, if necessary.
527  */
setWidgetPreviewsnull528 suspend inline fun <reified T : GlanceAppWidgetReceiver> GlanceAppWidgetManager.setWidgetPreviews(
529     widgetCategories: IntSet =
530         intSetOf(
531             WIDGET_CATEGORY_HOME_SCREEN or WIDGET_CATEGORY_KEYGUARD or WIDGET_CATEGORY_SEARCHBOX
532         ),
533 ): Boolean {
534     return setWidgetPreviews(T::class, widgetCategories)
535 }
536 
reverseMappingnull537 private fun Map<ComponentName, String>.reverseMapping(): Map<String, List<ComponentName>> =
538     entries.groupBy({ it.value }, { it.key })
539