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.appwidget.AppWidgetManager
20 import android.appwidget.AppWidgetProviderInfo
21 import android.content.ComponentName
22 import android.content.Context
23 import android.os.Build
24 import android.os.Bundle
25 import android.util.Log
26 import android.widget.RemoteViews
27 import androidx.annotation.LayoutRes
28 import androidx.annotation.RestrictTo
29 import androidx.annotation.RestrictTo.Scope
30 import androidx.compose.runtime.Composable
31 import androidx.glance.GlanceComposable
32 import androidx.glance.GlanceId
33 import androidx.glance.appwidget.action.ActionCallbackBroadcastReceiver
34 import androidx.glance.appwidget.action.ActionTrampolineActivity
35 import androidx.glance.appwidget.action.InvisibleActionTrampolineActivity
36 import androidx.glance.appwidget.state.getAppWidgetState
37 import androidx.glance.session.GlanceSessionManager
38 import androidx.glance.session.SessionManager
39 import androidx.glance.session.SessionManagerScope
40 import androidx.glance.state.GlanceState
41 import androidx.glance.state.GlanceStateDefinition
42 import androidx.glance.state.PreferencesGlanceStateDefinition
43 import kotlin.coroutines.coroutineContext
44 import kotlinx.coroutines.CancellationException
45 
46 /**
47  * Object handling the composition and the communication with [AppWidgetManager].
48  *
49  * The UI is defined by calling [provideContent] from within [provideGlance]. When the widget is
50  * requested, the composition is run and translated into a [RemoteViews] which is then sent to the
51  * [AppWidgetManager].
52  *
53  * @param errorUiLayout Used by [onCompositionError]. When [onCompositionError] is called, it will,
54  *   unless overridden, update the appwidget to display error UI using this layout resource ID,
55  *   unless [errorUiLayout] is 0, in which case the error will be rethrown. If [onCompositionError]
56  *   is overridden, [errorUiLayout] will not be read..
57  */
58 abstract class GlanceAppWidget(
59     @LayoutRes internal open val errorUiLayout: Int = R.layout.glance_error_layout,
60 ) {
61     @RestrictTo(Scope.LIBRARY_GROUP)
62     open fun getSessionManager(context: Context): SessionManager = GlanceSessionManager
63 
64     /**
65      * Override this function to provide the Glance Composable.
66      *
67      * This is a good place to load any data needed to render the Composable. Use [provideContent]
68      * to provide the Composable once the data is ready.
69      *
70      * [provideGlance] is run in the background as a [androidx.work.CoroutineWorker] in response to
71      * calls to [update] and [updateAll], as well as requests from the Launcher. Before
72      * `provideContent` is called, `provideGlance` is subject to the typical
73      * [androidx.work.WorkManager] time limit (currently ten minutes). After `provideContent` is
74      * called, the composition continues to run and recompose for about 45 seconds. When UI
75      * interactions or update requests are received, additional time is added to process these
76      * requests.
77      *
78      * Note: [update] and [updateAll] do not restart `provideGlance` if it is already running. As a
79      * result, you should load initial data before calling `provideContent`, and then observe your
80      * sources of data within the composition (e.g. [androidx.compose.runtime.collectAsState]). This
81      * ensures that your widget will continue to update while the composition is active. When you
82      * update your data source from elsewhere in the app, make sure to call `update` in case a
83      * Worker for this widget is not currently running.
84      *
85      * @sample androidx.glance.appwidget.samples.provideGlanceSample
86      * @sample androidx.glance.appwidget.samples.provideGlancePeriodicWorkSample
87      */
88     abstract suspend fun provideGlance(
89         context: Context,
90         id: GlanceId,
91     )
92 
93     /**
94      * Override this function to provide a Glance Composable that will be used when running this
95      * widget in preview mode. Use [provideContent] to provide the composable once the data is
96      * ready.
97      *
98      * In order to generate and publish the previews for a provider, use [setWidgetPreviews]. You
99      * can use [composeForPreview] to generate a [RemoteViews] from this Composable without
100      * publishing it.
101      *
102      * The given [widgetCategory] value will be one of
103      * [AppWidgetProviderInfo.WIDGET_CATEGORY_HOME_SCREEN],
104      * [AppWidgetProviderInfo.WIDGET_CATEGORY_KEYGUARD], or
105      * [AppWidgetProviderInfo.WIDGET_CATEGORY_SEARCHBOX], or some combination of all three. This
106      * indicates what kind of widget host this preview can be used for. [widgetCategory] corresponds
107      * to the categories passed to [setWidgetPreviews].
108      *
109      * @sample androidx.glance.appwidget.samples.providePreviewSample
110      * @see AppWidgetProviderInfo.WIDGET_CATEGORY_HOME_SCREEN
111      */
112     open suspend fun providePreview(context: Context, widgetCategory: Int) {}
113 
114     /** Defines the handling of sizes. */
115     open val sizeMode: SizeMode = SizeMode.Single
116 
117     /** Defines handling of sizes for previews. */
118     open val previewSizeMode: PreviewSizeMode = SizeMode.Single
119 
120     /** Data store for widget data specific to the view. */
121     open val stateDefinition: GlanceStateDefinition<*>? = PreferencesGlanceStateDefinition
122 
123     /**
124      * Method called by the framework when an App Widget has been removed from its host.
125      *
126      * When the method returns, the state associated with the [glanceId] will be deleted.
127      */
128     open suspend fun onDelete(context: Context, glanceId: GlanceId) {}
129 
130     /** Run the composition in [provideGlance] and send the result to [AppWidgetManager]. */
131     suspend fun update(context: Context, id: GlanceId) {
132         require(id is AppWidgetId && id.isRealId) { "Invalid Glance ID" }
133         update(context, id.appWidgetId)
134     }
135 
136     /**
137      * Calls [onDelete], then clear local data associated with the [appWidgetId].
138      *
139      * This is meant to be called when the App Widget instance has been deleted from the host.
140      */
141     internal suspend fun deleted(context: Context, appWidgetId: Int) {
142         val glanceId = AppWidgetId(appWidgetId)
143         getSessionManager(context).runWithLock { closeSession(glanceId.toSessionKey()) }
144         try {
145             onDelete(context, glanceId)
146         } catch (cancelled: CancellationException) {
147             // Nothing to do here
148         } catch (t: Throwable) {
149             Log.e(GlanceAppWidgetTag, "Error in user-provided deletion callback", t)
150         } finally {
151             stateDefinition?.let {
152                 GlanceState.deleteStore(context, it, createUniqueRemoteUiName(appWidgetId))
153             }
154             LayoutConfiguration.delete(context, glanceId)
155         }
156     }
157 
158     /** Internal version of [update], to be used by the broadcast receiver directly. */
159     internal suspend fun update(
160         context: Context,
161         appWidgetId: Int,
162         options: Bundle? = null,
163     ) {
164         Tracing.beginGlanceAppWidgetUpdate()
165         val glanceId = AppWidgetId(appWidgetId)
166         getOrCreateAppWidgetSession(context, glanceId, options) { session, wasRunning ->
167             if (wasRunning) session.updateGlance()
168         }
169     }
170 
171     /**
172      * Trigger an action to be run in the [AppWidgetSession] for this widget, starting the session
173      * if necessary.
174      */
175     internal suspend fun triggerAction(
176         context: Context,
177         appWidgetId: Int,
178         actionKey: String,
179         options: Bundle? = null,
180     ) {
181         val glanceId = AppWidgetId(appWidgetId)
182         getOrCreateAppWidgetSession(context, glanceId, options) { session, _ ->
183             session.runLambda(actionKey)
184         }
185     }
186 
187     /** Internal method called when a resize event is detected. */
188     internal suspend fun resize(context: Context, appWidgetId: Int, options: Bundle) {
189         // Note, on Android S, if the mode is `Responsive`, then all the sizes are specified from
190         // the start and we don't need to update the AppWidget when the size changes.
191         if (
192             sizeMode is SizeMode.Single ||
193                 (Build.VERSION.SDK_INT > Build.VERSION_CODES.S && sizeMode is SizeMode.Responsive)
194         ) {
195             return
196         }
197         val glanceId = AppWidgetId(appWidgetId)
198         getOrCreateAppWidgetSession(context, glanceId, options) { session, _ ->
199             session.updateAppWidgetOptions(options)
200         }
201     }
202 
203     /**
204      * A callback invoked when the [AppWidgetSession] encounters an exception. At this point, the
205      * session will be closed down. The default implementation of this method creates a
206      * [RemoteViews] from [errorUiLayout] and sets this as the widget's content.
207      *
208      * This method should be overridden if you want to log the error, create a custom error layout,
209      * or attempt to recover from or ignore the error by updating the widget's view state and then
210      * restarting composition.
211      *
212      * @param context Context.
213      * @param glanceId The [GlanceId] of the widget experiencing the error.
214      * @param appWidgetId The appWidgetId of the widget experiencing the error. This is provided as
215      *   a convenience in addition to [GlanceId].
216      * @param throwable The exception that was caught by [AppWidgetSession]
217      */
218     @Suppress("GenericException")
219     @Throws(Throwable::class)
220     open fun onCompositionError(
221         context: Context,
222         glanceId: GlanceId,
223         appWidgetId: Int,
224         throwable: Throwable
225     ) {
226         if (errorUiLayout == 0) {
227             throw throwable // Maintains consistency with Glance 1.0 behavior.
228         } else {
229             val rv =
230                 RemoteViews(
231                     context.packageName,
232                     errorUiLayout
233                 ) // default impl: inflate the error layout
234             AppWidgetManager.getInstance(context).updateAppWidget(appWidgetId, rv)
235         }
236     }
237 
238     internal suspend fun <T> getOrCreateAppWidgetSession(
239         context: Context,
240         glanceId: AppWidgetId,
241         options: Bundle? = null,
242         block: suspend SessionManagerScope.(AppWidgetSession, Boolean) -> T,
243     ): T =
244         getSessionManager(context).runWithLock {
245             val wasRunning = isSessionRunning(context, glanceId.toSessionKey())
246             if (!wasRunning) {
247                 startSession(context, createAppWidgetSession(context, glanceId, options))
248             }
249             val session = getSession(glanceId.toSessionKey()) as AppWidgetSession
250             return@runWithLock block(session, wasRunning)
251         }
252 
253     /**
254      * Override this function to specify the components that will be used for actions and
255      * RemoteViewsService. All of the components must run in the same process.
256      *
257      * If null, then the default components will be used.
258      */
259     @RestrictTo(Scope.LIBRARY_GROUP)
260     open fun getComponents(context: Context): GlanceComponents? = null
261 
262     @RestrictTo(Scope.LIBRARY_GROUP)
263     protected open fun createAppWidgetSession(
264         context: Context,
265         id: AppWidgetId,
266         options: Bundle? = null
267     ) = AppWidgetSession(this@GlanceAppWidget, id, options)
268 }
269 
270 @RestrictTo(Scope.LIBRARY_GROUP) data class AppWidgetId(val appWidgetId: Int) : GlanceId
271 
272 /** Update all App Widgets managed by the [GlanceAppWidget] class. */
updateAllnull273 suspend fun GlanceAppWidget.updateAll(@Suppress("ContextFirst") context: Context) {
274     val manager = GlanceAppWidgetManager(context)
275     manager.getGlanceIds(javaClass).forEach { update(context, it) }
276 }
277 
278 /**
279  * Update all App Widgets managed by the [GlanceAppWidget] class, if they fulfill some condition.
280  */
updateIfnull281 suspend inline fun <reified State> GlanceAppWidget.updateIf(
282     @Suppress("ContextFirst") context: Context,
283     predicate: (State) -> Boolean
284 ) {
285     val stateDef = stateDefinition
286     requireNotNull(stateDef) { "GlanceAppWidget.updateIf cannot be used if no state is defined." }
287     val manager = GlanceAppWidgetManager(context)
288     manager.getGlanceIds(javaClass).forEach { glanceId ->
289         val state = getAppWidgetState(context, stateDef, glanceId) as State
290         if (predicate(state)) update(context, glanceId)
291     }
292 }
293 
294 /**
295  * Provides [content] to the Glance host, suspending until the Glance session is shut down.
296  *
297  * If this function is called concurrently with itself, the previous call will throw
298  * [CancellationException] and the new content will replace it. This function should only be called
299  * from [GlanceAppWidget.provideGlance].
300  *
301  * TODO: make this a protected member once b/206013293 is fixed.
302  */
303 suspend fun GlanceAppWidget.provideContent(
304     content: @Composable @GlanceComposable () -> Unit
305 ): Nothing {
306     coroutineContext[ContentReceiver]?.provideContent(content)
307         ?: error(
308             "provideContent requires a ContentReceiver and should only be called from " +
309                 "GlanceAppWidget.provideGlance"
310         )
311 }
312 
313 /**
314  * Specifies which components will be used as targets for action trampolines, RunCallback actions,
315  * and RemoteViewsService when creating RemoteViews. These components must all run in the same
316  * process.
317  */
318 @RestrictTo(Scope.LIBRARY_GROUP)
319 class GlanceComponents(
320     val actionTrampolineActivity: ComponentName,
321     val invisibleActionTrampolineActivity: ComponentName,
322     val actionCallbackBroadcastReceiver: ComponentName,
323     val remoteViewsService: ComponentName,
324 ) {
325     companion object {
326         /** The default components used for GlanceAppWidget. */
getDefaultnull327         fun getDefault(context: Context) =
328             GlanceComponents(
329                 actionTrampolineActivity =
330                     ComponentName(context, ActionTrampolineActivity::class.java),
331                 invisibleActionTrampolineActivity =
332                     ComponentName(context, InvisibleActionTrampolineActivity::class.java),
333                 actionCallbackBroadcastReceiver =
334                     ComponentName(context, ActionCallbackBroadcastReceiver::class.java),
335                 remoteViewsService = ComponentName(context, GlanceRemoteViewsService::class.java),
336             )
337     }
338 }
339