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