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