1 /*
2 * 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.appwidget.AppWidgetManager
20 import android.appwidget.AppWidgetProvider
21 import android.content.ComponentName
22 import android.content.Context
23 import android.content.Intent
24 import android.os.Build
25 import android.os.Bundle
26 import android.util.Log
27 import androidx.annotation.CallSuper
28 import androidx.glance.ExperimentalGlanceApi
29 import androidx.glance.appwidget.action.LambdaActionBroadcasts
30 import kotlin.coroutines.CoroutineContext
31 import kotlinx.coroutines.CancellationException
32 import kotlinx.coroutines.CoroutineScope
33 import kotlinx.coroutines.Dispatchers
34 import kotlinx.coroutines.async
35 import kotlinx.coroutines.awaitAll
36 import kotlinx.coroutines.launch
37
38 /**
39 * [AppWidgetProvider] using the given [GlanceAppWidget] to generate the remote views when needed.
40 *
41 * This should typically used as:
42 *
43 * class MyGlanceAppWidgetProvider : GlanceAppWidgetProvider() {
44 * override val glanceAppWidget: GlanceAppWidget()
45 * get() = MyGlanceAppWidget()
46 * }
47 *
48 * Note: If you override any of the [AppWidgetProvider] methods, ensure you call their super-class
49 * implementation.
50 *
51 * Important: if you override any of the methods of this class, you must call the super
52 * implementation, and you must not call [AppWidgetProvider.goAsync], as it will be called by the
53 * super implementation. This means your processing time must be short.
54 */
55 @OptIn(ExperimentalGlanceApi::class)
56 abstract class GlanceAppWidgetReceiver : AppWidgetProvider() {
57
58 companion object {
59 private const val TAG = "GlanceAppWidgetReceiver"
60
61 /**
62 * Action for a broadcast intent that will try to update all instances of a Glance App
63 * Widget for debugging.
64 * <pre>
65 * adb shell am broadcast -a androidx.glance.appwidget.action.DEBUG_UPDATE -n APP/COMPONENT
66 * </pre>
67 * where APP/COMPONENT is the manifest component for the GlanceAppWidgetReceiver subclass.
68 * This only works if the Receiver is exported (or the target device has adb running as
69 * root), and has androidx.glance.appwidget.DEBUG_UPDATE in its intent-filter. This should
70 * only be done for debug builds and disabled for release.
71 */
72 const val ACTION_DEBUG_UPDATE = "androidx.glance.appwidget.action.DEBUG_UPDATE"
73 }
74
75 /**
76 * Instance of the [GlanceAppWidget] to use to generate the App Widget and send it to the
77 * [AppWidgetManager]
78 */
79 abstract val glanceAppWidget: GlanceAppWidget
80
81 /**
82 * Override [coroutineContext] to provide custom [CoroutineContext] in which to run update
83 * requests.
84 *
85 * Note: This does not set the [CoroutineContext] for the GlanceAppWidget, which will always run
86 * on the main thread.
87 */
88 @get:ExperimentalGlanceApi
89 @Suppress("OPT_IN_MARKER_ON_WRONG_TARGET")
90 @ExperimentalGlanceApi
91 open val coroutineContext: CoroutineContext = Dispatchers.Default
92
93 @CallSuper
onUpdatenull94 override fun onUpdate(
95 context: Context,
96 appWidgetManager: AppWidgetManager,
97 appWidgetIds: IntArray
98 ) {
99 if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
100 Log.w(
101 TAG,
102 "Using Glance in devices with API<23 is untested and might behave unexpectedly."
103 )
104 }
105 goAsync(coroutineContext) {
106 updateManager(context)
107 appWidgetIds.map { async { glanceAppWidget.update(context, it) } }.awaitAll()
108 }
109 }
110
111 @CallSuper
onAppWidgetOptionsChangednull112 override fun onAppWidgetOptionsChanged(
113 context: Context,
114 appWidgetManager: AppWidgetManager,
115 appWidgetId: Int,
116 newOptions: Bundle
117 ) {
118 goAsync(coroutineContext) {
119 updateManager(context)
120 glanceAppWidget.resize(context, appWidgetId, newOptions)
121 }
122 }
123
124 @CallSuper
onDeletednull125 override fun onDeleted(context: Context, appWidgetIds: IntArray) {
126 goAsync(coroutineContext) {
127 updateManager(context)
128 appWidgetIds.forEach { glanceAppWidget.deleted(context, it) }
129 }
130 }
131
CoroutineScopenull132 private fun CoroutineScope.updateManager(context: Context) {
133 launch {
134 runAndLogExceptions {
135 GlanceAppWidgetManager(context)
136 .updateReceiver(this@GlanceAppWidgetReceiver, glanceAppWidget)
137 }
138 }
139 }
140
onReceivenull141 override fun onReceive(context: Context, intent: Intent) {
142 runAndLogExceptions {
143 when (intent.action) {
144 Intent.ACTION_LOCALE_CHANGED,
145 ACTION_DEBUG_UPDATE -> {
146 val appWidgetManager = AppWidgetManager.getInstance(context)
147 val componentName =
148 ComponentName(
149 context.packageName,
150 checkNotNull(javaClass.canonicalName) { "no canonical name" }
151 )
152 val ids =
153 if (intent.hasExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS)) {
154 intent.getIntArrayExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS)!!
155 } else {
156 appWidgetManager.getAppWidgetIds(componentName)
157 }
158 onUpdate(
159 context,
160 appWidgetManager,
161 ids,
162 )
163 }
164 LambdaActionBroadcasts.ActionTriggerLambda -> {
165 val actionKey =
166 intent.getStringExtra(LambdaActionBroadcasts.ExtraActionKey)
167 ?: error("Intent is missing ActionKey extra")
168 val id = intent.getIntExtra(LambdaActionBroadcasts.ExtraAppWidgetId, -1)
169 if (id == -1) error("Intent is missing AppWidgetId extra")
170 goAsync(coroutineContext) {
171 updateManager(context)
172 glanceAppWidget.triggerAction(context, id, actionKey)
173 }
174 }
175 else -> super.onReceive(context, intent)
176 }
177 }
178 }
179 }
180
runAndLogExceptionsnull181 private inline fun runAndLogExceptions(block: () -> Unit) {
182 try {
183 block()
184 } catch (ex: CancellationException) {
185 // Nothing to do
186 } catch (throwable: Throwable) {
187 logException(throwable)
188 }
189 }
190