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