1 /*
<lambda>null2  * Copyright 2022 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.content.ComponentName
20 import android.content.Context
21 import android.os.Bundle
22 import android.util.Log
23 import android.widget.RemoteViews
24 import androidx.annotation.RestrictTo
25 import androidx.annotation.VisibleForTesting
26 import androidx.compose.runtime.Composable
27 import androidx.compose.runtime.CompositionLocalProvider
28 import androidx.compose.runtime.SideEffect
29 import androidx.compose.runtime.collectAsState
30 import androidx.compose.runtime.getValue
31 import androidx.compose.runtime.mutableStateOf
32 import androidx.compose.runtime.neverEqualPolicy
33 import androidx.compose.runtime.produceState
34 import androidx.compose.runtime.remember
35 import androidx.compose.runtime.setValue
36 import androidx.compose.runtime.snapshots.Snapshot
37 import androidx.compose.ui.unit.DpSize
38 import androidx.glance.EmittableWithChildren
39 import androidx.glance.GlanceComposable
40 import androidx.glance.LocalContext
41 import androidx.glance.LocalGlanceId
42 import androidx.glance.LocalState
43 import androidx.glance.action.LambdaAction
44 import androidx.glance.session.Session
45 import androidx.glance.state.ConfigManager
46 import androidx.glance.state.GlanceState
47 import androidx.glance.state.GlanceStateDefinition
48 import kotlinx.coroutines.CancellationException
49 import kotlinx.coroutines.CompletableJob
50 import kotlinx.coroutines.Job
51 import kotlinx.coroutines.flow.MutableStateFlow
52 
53 /**
54  * A session that composes UI for a single app widget.
55  *
56  * This class represents the lifecycle of composition for an app widget. This is started by
57  * [GlanceAppWidget] in response to APPWIDGET_UPDATE broadcasts. The session is run in
58  * [androidx.glance.session.SessionWorker] on a background thread (WorkManager). While it is active,
59  * the session will continue to recompose in response to composition state changes or external
60  * events (e.g. [AppWidgetSession.updateGlance]). If a session is already running, GlanceAppWidget
61  * will trigger events on the session instead of starting a new one.
62  *
63  * @param initialOptions options to be provided to the composition and determine sizing.
64  * @param initialGlanceState initial value of Glance state
65  * @property widget the GlanceAppWidget that contains the composable for this session.
66  * @property id identifies which widget will be updated when the UI is ready.
67  * @property configManager used by the session to retrieve configuration state.
68  * @property lambdaReceiver the BroadcastReceiver that will receive lambda action broadcasts.
69  * @property sizeMode optional override for the widget's specified SizeMode.
70  * @property shouldPublish if true, we will publish RemoteViews with
71  *   [android.appwidget.AppWidgetManager.updateAppWidget]. The [id] must be valid
72  *   ([AppWidgetId.isRealId]) in that case.
73  */
74 @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
75 open class AppWidgetSession(
76     private val widget: GlanceAppWidget,
77     private val id: AppWidgetId,
78     initialOptions: Bundle? = null,
79     private val configManager: ConfigManager = GlanceState,
80     private val lambdaReceiver: ComponentName? = null,
81     private val sizeMode: SizeMode = widget.sizeMode,
82     private val shouldPublish: Boolean = true,
83     initialGlanceState: Any? = null,
84 ) : Session(id.toSessionKey()) {
85     init {
86         if (id.isFakeId) {
87             require(lambdaReceiver != null) {
88                 "If the AppWidgetSession is not created for a bound widget, you must provide " +
89                     "a lambda action receiver"
90             }
91             require(!shouldPublish) {
92                 "Cannot publish RemoteViews to AppWidgetManager since we are not running for a " +
93                     "bound widget"
94             }
95         }
96     }
97 
98     private companion object {
99         const val TAG = "AppWidgetSession"
100         const val DEBUG = false
101     }
102 
103     private var glanceState by mutableStateOf(initialGlanceState, neverEqualPolicy())
104     private var options by mutableStateOf(initialOptions, neverEqualPolicy())
105     private var lambdas = mapOf<String, List<LambdaAction>>()
106     private val parentJob = Job()
107 
108     internal val lastRemoteViews = MutableStateFlow<RemoteViews?>(null)
109 
110     override fun createRootEmittable() = RemoteViewsRoot(MaxComposeTreeDepth)
111 
112     override fun provideGlance(context: Context): @Composable @GlanceComposable () -> Unit = {
113         CompositionLocalProvider(
114             LocalContext provides context,
115             LocalGlanceId provides id,
116             LocalAppWidgetOptions provides (options ?: Bundle.EMPTY),
117             LocalState provides glanceState,
118         ) {
119             var minSize by remember { mutableStateOf(DpSize.Zero) }
120             val configIsReady by
121                 produceState(false) {
122                     // Only get a Glance state value if we did not receive an initial value.
123                     val newGlanceState =
124                         if (glanceState == null) {
125                             widget.stateDefinition?.let { stateDefinition: GlanceStateDefinition<*>
126                                 ->
127                                 configManager.getValue(context, stateDefinition, key)
128                             }
129                         } else null
130 
131                     Snapshot.withMutableSnapshot {
132                         if (id.isRealId) {
133                             // Only get sizing info from app widget manager if we are composing for
134                             // a real bound widget.
135                             val manager = context.appWidgetManager
136                             minSize =
137                                 appWidgetMinSize(
138                                     context.resources.displayMetrics,
139                                     manager,
140                                     id.appWidgetId
141                                 )
142                             if (options == null) {
143                                 options = manager.getAppWidgetOptions(id.appWidgetId)
144                             }
145                         }
146                         newGlanceState?.let { glanceState = it }
147                         value = true
148                     }
149                 }
150             if (configIsReady) {
151                 remember { widget.runGlance(context, id) }
152                     .collectAsState(null)
153                     .value
154                     ?.let { ForEachSize(sizeMode, minSize, it) } ?: IgnoreResult()
155             } else {
156                 IgnoreResult()
157             }
158             // The following line ensures that when glanceState is updated, it increases the
159             // Recomposer.changeCount and triggers processEmittableTree.
160             SideEffect { glanceState }
161         }
162     }
163 
164     override suspend fun processEmittableTree(
165         context: Context,
166         root: EmittableWithChildren
167     ): Boolean {
168         if (root.shouldIgnoreResult()) return false
169         root as RemoteViewsRoot
170         val layoutConfig = LayoutConfiguration.load(context, id.appWidgetId)
171         val appWidgetManager = context.appWidgetManager
172         try {
173             val receiver =
174                 lambdaReceiver
175                     ?: requireNotNull(appWidgetManager.getAppWidgetInfo(id.appWidgetId)) {
176                             "No app widget info for ${id.appWidgetId}"
177                         }
178                         .provider
179             normalizeCompositionTree(root)
180             lambdas = root.updateLambdaActionKeys()
181             val rv =
182                 translateComposition(
183                     context,
184                     id.appWidgetId,
185                     root,
186                     layoutConfig,
187                     layoutConfig.addLayout(root),
188                     DpSize.Unspecified,
189                     receiver,
190                     widget.getComponents(context) ?: GlanceComponents.getDefault(context),
191                 )
192             if (shouldPublish) {
193                 appWidgetManager.updateAppWidget(id.appWidgetId, rv)
194             }
195             lastRemoteViews.value = rv
196         } catch (ex: CancellationException) {
197             // Nothing to do
198         } catch (throwable: Throwable) {
199             notifyWidgetOfError(context, throwable)
200         } finally {
201             layoutConfig.save()
202             Tracing.endGlanceAppWidgetUpdate()
203         }
204         return true
205     }
206 
207     override suspend fun onCompositionError(context: Context, throwable: Throwable) {
208         notifyWidgetOfError(context, throwable)
209     }
210 
211     override suspend fun processEvent(context: Context, event: Any) {
212         when (event) {
213             is UpdateGlanceState -> {
214                 if (DEBUG) Log.i(TAG, "Received UpdateGlanceState event for session($key)")
215                 val newGlanceState =
216                     widget.stateDefinition?.let { configManager.getValue(context, it, key) }
217                 Snapshot.withMutableSnapshot { glanceState = newGlanceState }
218             }
219             is UpdateAppWidgetOptions -> {
220                 if (DEBUG) {
221                     Log.i(
222                         TAG,
223                         "Received UpdateAppWidgetOptions(${event.newOptions}) event" +
224                             "for session($key)"
225                     )
226                 }
227                 Snapshot.withMutableSnapshot { options = event.newOptions }
228             }
229             is RunLambda -> {
230                 if (DEBUG) Log.i(TAG, "Received RunLambda(${event.key}) action for session($key)")
231                 Snapshot.withMutableSnapshot { lambdas[event.key]?.forEach { it.block() } }
232                     ?: Log.w(TAG, "Triggering Action(${event.key}) for session($key) failed")
233             }
234             is WaitForReady -> {
235                 event.job.apply { if (isActive) complete() }
236             }
237             else -> {
238                 throw IllegalArgumentException(
239                     "Sent unrecognized event type ${event.javaClass} to AppWidgetSession"
240                 )
241             }
242         }
243     }
244 
245     override fun onClosed() {
246         // Normally when we are closed, any pending events are processed before the channel is
247         // shutdown. However, it is possible that the Worker for this session will die before
248         // processing the remaining events. So when this session is closed, we will immediately
249         // resume all waiters without waiting for their events to be processed. If the Worker lives
250         // long enough to process their events, it will have no effect because their Jobs are no
251         // longer active.
252         parentJob.cancel()
253     }
254 
255     suspend fun updateGlance() {
256         sendEvent(UpdateGlanceState)
257     }
258 
259     suspend fun updateAppWidgetOptions(newOptions: Bundle) {
260         sendEvent(UpdateAppWidgetOptions(newOptions))
261     }
262 
263     suspend fun runLambda(key: String) {
264         sendEvent(RunLambda(key))
265     }
266 
267     /**
268      * Returns a Job that can be used to wait until the session is ready (i.e. has finished
269      * processEmittableTree for the first time and is now receiving events). You can wait on the
270      * session to be ready by calling [Job.join] on the returned [Job]. When the session is ready,
271      * join will resume successfully (Job is completed). If the session is closed before it is
272      * ready, we call [Job.cancel] and the call to join resumes with [CancellationException].
273      */
274     suspend fun waitForReady(): Job {
275         val event = WaitForReady(Job(parentJob))
276         sendEvent(event)
277         return event.job
278     }
279 
280     private fun notifyWidgetOfError(context: Context, throwable: Throwable) {
281         logException(throwable)
282         if (shouldPublish) {
283             widget.onCompositionError(
284                 context,
285                 glanceId = id,
286                 appWidgetId = id.appWidgetId,
287                 throwable = throwable
288             )
289         } else {
290             throw throwable // rethrow the error if we can't display it
291         }
292     }
293 
294     // Event types that this session supports.
295     @VisibleForTesting internal object UpdateGlanceState
296 
297     @VisibleForTesting internal class UpdateAppWidgetOptions(val newOptions: Bundle)
298 
299     @VisibleForTesting internal class RunLambda(val key: String)
300 
301     @VisibleForTesting internal class WaitForReady(val job: CompletableJob)
302 }
303