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