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