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.appwidget.AppWidgetManager
20 import android.content.Context
21 import android.content.Intent
22 import android.net.Uri
23 import android.os.Build
24 import android.util.Log
25 import android.widget.RemoteViews
26 import android.widget.RemoteViewsService
27 import androidx.annotation.RequiresApi
28 import androidx.annotation.RestrictTo
29 import kotlinx.coroutines.runBlocking
30 
31 /**
32  * [RemoteViewsService] to be connected to for a remote adapter that returns RemoteViews for lazy
33  * lists / grids.
34  */
35 open class GlanceRemoteViewsService : RemoteViewsService() {
36     override fun onGetViewFactory(intent: Intent?): RemoteViewsFactory {
37         requireNotNull(intent) { "Intent is null" }
38         val appWidgetId = intent.getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, -1)
39         check(appWidgetId != -1) { "No app widget id was present in the intent" }
40 
41         val viewId = intent.getIntExtra(EXTRA_VIEW_ID, -1)
42         check(viewId != -1) { "No view id was present in the intent" }
43 
44         val sizeInfo = intent.getStringExtra(EXTRA_SIZE_INFO)
45         check(!sizeInfo.isNullOrEmpty()) { "No size info was present in the intent" }
46 
47         return GlanceRemoteViewsFactory(this, appWidgetId, viewId, sizeInfo)
48     }
49 
50     @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
51     internal companion object {
52         internal const val EXTRA_VIEW_ID = "androidx.glance.widget.extra.view_id"
53         internal const val EXTRA_SIZE_INFO = "androidx.glance.widget.extra.size_info"
54         internal const val TAG = "GlanceRemoteViewService"
55 
56         // An in-memory store containing items to be returned via the adapter when requested.
57         private val InMemoryStore = RemoteCollectionItemsInMemoryStore()
58 
59         // Adds items to the store for later use by the list adapter to display the items.
60         internal fun saveItems(
61             appWidgetId: Int,
62             viewId: Int,
63             sizeInfo: String,
64             remoteCollectionItems: RemoteCollectionItems
65         ) {
66             synchronized(InMemoryStore) {
67                 InMemoryStore.save(appWidgetId, viewId, sizeInfo, remoteCollectionItems)
68             }
69         }
70 
71         // Returns items in the store for the requested view in appwidget for the specified size.
72         private fun getItems(
73             appWidgetId: Int,
74             viewId: Int,
75             sizeInfo: String
76         ): RemoteCollectionItems {
77             return synchronized(InMemoryStore) {
78                 InMemoryStore.getItems(appWidgetId, viewId, sizeInfo)
79             }
80         }
81 
82         // Removes items in the store for the requested view in appwidget for the specified size.
83         private fun removeItems(appWidgetId: Int, viewId: Int, sizeInfo: String) {
84             synchronized(InMemoryStore) { InMemoryStore.removeItems(appWidgetId, viewId, sizeInfo) }
85         }
86     }
87 
88     /**
89      * A RemoteViewsFactory that holds items in memory and provides it to the host when requested.
90      *
91      * <p>Starts glance session if needed to reload items in memory e.g. when app process was killed
92      * and user scrolled on an existing list / grid view.
93      */
94     internal class GlanceRemoteViewsFactory(
95         private val context: Context,
96         private val appWidgetId: Int,
97         private val viewId: Int,
98         private val size: String
99     ) : RemoteViewsFactory {
100         override fun onCreate() {
101             // OnDataSetChanged is always called even onCreate, so we don't need to load data here.
102         }
103 
104         override fun onDataSetChanged() = loadData()
105 
106         private fun loadData() {
107             runBlocking {
108                 val glanceId = AppWidgetId(appWidgetId)
109                 try {
110                     startSessionIfNeededAndWaitUntilReady(glanceId)
111                 } catch (e: Throwable) {
112                     Log.e(TAG, "Error when trying to start session for list items", e)
113                 }
114             }
115         }
116 
117         private suspend fun startSessionIfNeededAndWaitUntilReady(glanceId: AppWidgetId) {
118             val job =
119                 getGlanceAppWidget()?.getOrCreateAppWidgetSession(
120                     context = context,
121                     glanceId = glanceId,
122                     options = null
123                 ) { session, wasRunning ->
124                     // If session is already running, data must have already been loaded
125                     // into
126                     // the store during composition.
127                     if (wasRunning) return@getOrCreateAppWidgetSession null
128                     session.waitForReady()
129                 } ?: UnmanagedSessionReceiver.getSession(appWidgetId)?.waitForReady()
130             // The following join() may throw CancellationException if the session is closed before
131             // it is ready. This will have the effect of cancelling the runBlocking scope.
132             job?.join()
133         }
134 
135         private fun getGlanceAppWidget(): GlanceAppWidget? {
136             val appWidgetManager = AppWidgetManager.getInstance(context)
137             val providerInfo = appWidgetManager.getAppWidgetInfo(appWidgetId)
138             return providerInfo?.provider?.className?.let { className ->
139                 val receiverClass = Class.forName(className)
140                 (receiverClass.getDeclaredConstructor().newInstance() as GlanceAppWidgetReceiver)
141                     .glanceAppWidget
142             }
143         }
144 
145         override fun onDestroy() {
146             removeItems(appWidgetId, viewId, size)
147         }
148 
149         private fun items() = getItems(appWidgetId, viewId, size)
150 
151         override fun getCount(): Int {
152             return items().itemCount
153         }
154 
155         override fun getViewAt(position: Int): RemoteViews {
156             return try {
157                 items().getItemView(position)
158             } catch (e: ArrayIndexOutOfBoundsException) {
159                 // RemoteViewsAdapter may sometimes request an index that is out of bounds. Return
160                 // an error view in this case. See b/242730601, b/254682488 for more details.
161                 RemoteViews(context.packageName, R.layout.glance_invalid_list_item)
162             }
163         }
164 
165         override fun getLoadingView() = null
166 
167         override fun getViewTypeCount(): Int = items().viewTypeCount
168 
169         override fun getItemId(position: Int): Long =
170             try {
171                 items().getItemId(position)
172             } catch (e: ArrayIndexOutOfBoundsException) {
173                 -1
174             }
175 
176         override fun hasStableIds(): Boolean = items().hasStableIds()
177     }
178 }
179 
180 /** An in-memory store holding [RemoteCollectionItems] for each sized lazy view in appWidgets. */
181 private class RemoteCollectionItemsInMemoryStore {
182     private val items = mutableMapOf<String, RemoteCollectionItems>()
183 
savenull184     fun save(
185         appWidgetId: Int,
186         viewId: Int,
187         sizeInfo: String,
188         remoteCollectionItems: RemoteCollectionItems
189     ) {
190         items[key(appWidgetId, viewId, sizeInfo)] = remoteCollectionItems
191     }
192 
193     /** Returns the collection items corresponding to the requested view in appwidget and size. */
getItemsnull194     fun getItems(appWidgetId: Int, viewId: Int, sizeInfo: String): RemoteCollectionItems {
195         return items[key(appWidgetId, viewId, sizeInfo)] ?: RemoteCollectionItems.Empty
196     }
197 
198     /** Removes the collection items corresponding to the requested view in appwidget and size. */
removeItemsnull199     fun removeItems(appWidgetId: Int, viewId: Int, sizeInfo: String) {
200         items.remove(key(appWidgetId, viewId, sizeInfo))
201     }
202 
203     // A unique key for RemoteCollectionItems in the store. Including size info allows us to compose
204     // for different sizes and maintain separate collection items for each size.
keynull205     private fun key(appWidgetId: Int, viewId: Int, sizeInfo: String): String {
206         return "$appWidgetId-$viewId-$sizeInfo"
207     }
208 }
209 
210 /**
211  * Sets remote views adapter.
212  *
213  * <p>For SDKs higher than S, passes the items in the adapter. For S and below SDKs, connects to a
214  * GlanceRemoteViewsService using an intent.
215  */
216 @Suppress("DEPRECATION")
setRemoteAdapternull217 internal fun RemoteViews.setRemoteAdapter(
218     translationContext: TranslationContext,
219     viewId: Int,
220     sizeInfo: String,
221     items: RemoteCollectionItems
222 ) {
223     if (Build.VERSION.SDK_INT > Build.VERSION_CODES.S) {
224         CollectionItemsApi31Impl.setRemoteAdapter(this, viewId, items)
225     } else {
226         val context = translationContext.context
227         val appWidgetId = translationContext.appWidgetId
228         val intent =
229             Intent()
230                 .setComponent(translationContext.glanceComponents.remoteViewsService)
231                 .putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId)
232                 .putExtra(GlanceRemoteViewsService.EXTRA_VIEW_ID, viewId)
233                 .putExtra(GlanceRemoteViewsService.EXTRA_SIZE_INFO, sizeInfo)
234                 .apply {
235                     // Set a data Uri to disambiguate Intents for different widget/view ids.
236                     data = Uri.parse(toUri(Intent.URI_INTENT_SCHEME))
237                 }
238         check(context.packageManager.resolveService(intent, 0) != null) {
239             "${intent.component} could not be resolved, check the app manifest."
240         }
241         setRemoteAdapter(viewId, intent)
242         GlanceRemoteViewsService.saveItems(appWidgetId, viewId, sizeInfo, items)
243         AppWidgetManager.getInstance(context).notifyAppWidgetViewDataChanged(appWidgetId, viewId)
244     }
245 }
246 
247 @RequiresApi(Build.VERSION_CODES.S)
248 private object CollectionItemsApi31Impl {
setRemoteAdapternull249     fun setRemoteAdapter(remoteViews: RemoteViews, viewId: Int, items: RemoteCollectionItems) {
250         remoteViews.setRemoteAdapter(viewId, toPlatformCollectionItems(items))
251     }
252 
toPlatformCollectionItemsnull253     fun toPlatformCollectionItems(items: RemoteCollectionItems): RemoteViews.RemoteCollectionItems {
254         return RemoteViews.RemoteCollectionItems.Builder()
255             .setHasStableIds(items.hasStableIds())
256             .setViewTypeCount(items.viewTypeCount)
257             .also { builder ->
258                 repeat(items.itemCount) { index ->
259                     builder.addItem(items.getItemId(index), items.getItemView(index))
260                 }
261             }
262             .build()
263     }
264 }
265