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