1 /* <lambda>null2 * Copyright 2023 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.annotation.SuppressLint 20 import android.content.BroadcastReceiver 21 import android.content.Context 22 import android.content.Intent 23 import android.util.Log 24 import androidx.glance.appwidget.action.LambdaActionBroadcasts 25 import java.lang.IllegalStateException 26 import kotlin.coroutines.resumeWithException 27 import kotlinx.coroutines.CancellableContinuation 28 import kotlinx.coroutines.Dispatchers 29 import kotlinx.coroutines.suspendCancellableCoroutine 30 31 /** 32 * This receiver responds to lambda action clicks for unmanaged sessions (created by 33 * [GlanceAppWidget.runComposition]). In managed sessions that compose UI for a bound widget, the 34 * widget's [GlanceAppWidgetReceiver] is used as the receiver for lambda actions. However, when 35 * running a session with [GlanceAppWidget.runComposition], there is no guarantee that the widget is 36 * attached to some GlanceAppWidgetReceiver. Instead, unmanaged sessions register themselves to 37 * receive lambdas while they are running (with [UnmanagedSessionReceiver.registerSession]), and set 38 * their lambda target to [UnmanagedSessionReceiver]. This is also used by 39 * [GlanceRemoteViewsService] to provide list items for unmanaged sessions. 40 */ 41 open class UnmanagedSessionReceiver : BroadcastReceiver() { 42 override fun onReceive(context: Context?, intent: Intent?) { 43 if (intent != null && intent.action == LambdaActionBroadcasts.ActionTriggerLambda) { 44 val actionKey = 45 intent.getStringExtra(LambdaActionBroadcasts.ExtraActionKey) 46 ?: error("Intent is missing ActionKey extra") 47 val id = intent.getIntExtra(LambdaActionBroadcasts.ExtraAppWidgetId, -1) 48 if (id == -1) error("Intent is missing AppWidgetId extra") 49 getSession(id)?.let { session -> 50 goAsync(Dispatchers.Main) { session.runLambda(actionKey) } 51 } 52 ?: Log.e( 53 GlanceAppWidgetTag, 54 "A lambda created by an unmanaged glance session cannot be serviced" + 55 "because that session is no longer running." 56 ) 57 } 58 } 59 60 internal companion object { 61 @SuppressLint("PrimitiveInCollection") 62 private val activeSessions = mutableMapOf<Int, Registration>() 63 64 private class Registration( 65 val session: AppWidgetSession, 66 val coroutine: CancellableContinuation<Nothing> 67 ) 68 69 /** 70 * Registers [session] to handle lambdas created from an unmanaged session running for 71 * [appWidgetId]. 72 * 73 * This call will suspend once the session is registered. On cancellation, this session will 74 * be unregistered. That way, the registration is tied to the surrounding coroutine scope 75 * and does not need to be manually unregistered. 76 * 77 * If called from another coroutine with the same [appWidgetId], this call will resume with 78 * an exception, and the new registration will succeed. (i.e., only one session per 79 * [appWidgetId] can be registered at the same time). By default, [runComposition] uses 80 * random fake IDs, so this could only happen if the user calls [runComposition] with two 81 * identical real IDs. 82 */ 83 suspend fun registerSession(appWidgetId: Int, session: AppWidgetSession): Nothing = 84 suspendCancellableCoroutine { coroutine -> 85 synchronized(UnmanagedSessionReceiver) { 86 activeSessions[appWidgetId] 87 ?.coroutine 88 ?.resumeWithException( 89 IllegalStateException("Another session for $appWidgetId has started") 90 ) 91 activeSessions[appWidgetId] = Registration(session, coroutine) 92 } 93 coroutine.invokeOnCancellation { 94 synchronized(UnmanagedSessionReceiver) { activeSessions.remove(appWidgetId) } 95 } 96 } 97 98 fun getSession(appWidgetId: Int): AppWidgetSession? = 99 synchronized(UnmanagedSessionReceiver) { activeSessions[appWidgetId]?.session } 100 } 101 } 102