1 /*
<lambda>null2  * Copyright 2021 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.core.widget
18 
19 import android.appwidget.AppWidgetManager
20 import android.content.Context
21 import android.content.Intent
22 import android.content.SharedPreferences
23 import android.content.pm.PackageManager
24 import android.net.Uri
25 import android.os.Build
26 import android.os.Parcel
27 import android.os.Parcelable
28 import android.util.Base64
29 import android.util.Log
30 import android.widget.RemoteViews
31 import android.widget.RemoteViewsService
32 import androidx.annotation.RestrictTo
33 import androidx.core.content.pm.PackageInfoCompat
34 import androidx.core.remoteviews.R
35 import androidx.core.widget.RemoteViewsCompat.RemoteCollectionItems
36 
37 /** [RemoteViewsService] to provide [RemoteViews] set using [RemoteViewsCompat.setRemoteAdapter]. */
38 @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
39 public class RemoteViewsCompatService : RemoteViewsService() {
40     override fun onGetViewFactory(intent: Intent): RemoteViewsFactory {
41         val appWidgetId = intent.getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, -1)
42         check(appWidgetId != -1) { "No app widget id was present in the intent" }
43 
44         val viewId = intent.getIntExtra(EXTRA_VIEW_ID, -1)
45         check(viewId != -1) { "No view id was present in the intent" }
46 
47         return RemoteViewsCompatServiceViewFactory(this, appWidgetId, viewId)
48     }
49 
50     private class RemoteViewsCompatServiceViewFactory(
51         private val mContext: Context,
52         private val mAppWidgetId: Int,
53         private val mViewId: Int
54     ) : RemoteViewsFactory {
55         private var mItems = EMPTY
56 
57         override fun onCreate() = loadData()
58 
59         override fun onDataSetChanged() = loadData()
60 
61         private fun loadData() {
62             mItems = RemoteViewsCompatServiceData.load(mContext, mAppWidgetId, mViewId) ?: EMPTY
63         }
64 
65         override fun onDestroy() {}
66 
67         override fun getCount() = mItems.itemCount
68 
69         override fun getViewAt(position: Int) =
70             try {
71                 mItems.getItemView(position)
72             } catch (e: ArrayIndexOutOfBoundsException) {
73                 // RemoteViewsAdapter may sometimes request an index that is out of bounds. Return
74                 // an
75                 // error view in this case. See b/242730601 for more details.
76                 RemoteViews(mContext.packageName, R.layout.invalid_list_item)
77             }
78 
79         override fun getLoadingView() = null
80 
81         override fun getViewTypeCount() = mItems.viewTypeCount
82 
83         override fun getItemId(position: Int) =
84             try {
85                 mItems.getItemId(position)
86             } catch (e: ArrayIndexOutOfBoundsException) {
87                 -1
88             }
89 
90         override fun hasStableIds() = mItems.hasStableIds()
91 
92         companion object {
93             private val EMPTY =
94                 RemoteCollectionItems(
95                     ids = longArrayOf(),
96                     views = emptyArray(),
97                     hasStableIds = false,
98                     viewTypeCount = 1
99                 )
100         }
101     }
102 
103     /**
104      * Wrapper around a serialized [RemoteCollectionItems] with metadata about the versions of
105      * Android and the app when the items were created.
106      *
107      * Our method of serialization and deserialization is to marshall and unmarshall the items to a
108      * byte array using their [Parcelable] implementation. As Parcelable definitions can change over
109      * time, it is not safe to do this across different versions of a package or Android itself.
110      * However, as app widgets are recreated on reboot or when a package is updated, this is not a
111      * problem for the approach used here.
112      *
113      * This data wrapper stores the current build of Android and the provider app at the time of
114      * serialization and deserialization is only attempted in [load] if both are the same as at the
115      * time of serialization.
116      */
117     private class RemoteViewsCompatServiceData {
118         private val mItemsBytes: ByteArray
119         private val mBuildVersion: String
120         private val mAppVersion: Long
121 
122         private constructor(itemsBytes: ByteArray, buildVersion: String, appVersion: Long) {
123             mItemsBytes = itemsBytes
124             mBuildVersion = buildVersion
125             mAppVersion = appVersion
126         }
127 
128         constructor(parcel: Parcel) {
129             val length = parcel.readInt()
130             mItemsBytes = ByteArray(length)
131             parcel.readByteArray(mItemsBytes)
132             mBuildVersion = parcel.readString()!!
133             mAppVersion = parcel.readLong()
134         }
135 
136         fun writeToParcel(dest: Parcel) {
137             dest.writeInt(mItemsBytes.size)
138             dest.writeByteArray(mItemsBytes)
139             dest.writeString(mBuildVersion)
140             dest.writeLong(mAppVersion)
141         }
142 
143         fun save(context: Context, appWidgetId: Int, viewId: Int) {
144             getPrefs(context)
145                 .edit()
146                 .putString(
147                     getKey(appWidgetId, viewId),
148                     serializeToHexString { parcel, _ -> writeToParcel(parcel) }
149                 )
150                 .apply()
151         }
152 
153         companion object {
154             private const val PREFS_FILENAME = "androidx.core.widget.prefs.RemoteViewsCompat"
155 
156             internal fun getKey(appWidgetId: Int, viewId: Int): String {
157                 return "$appWidgetId:$viewId"
158             }
159 
160             internal fun getPrefs(context: Context): SharedPreferences {
161                 return context.getSharedPreferences(PREFS_FILENAME, MODE_PRIVATE)
162             }
163 
164             fun create(
165                 context: Context,
166                 items: RemoteCollectionItems
167             ): RemoteViewsCompatServiceData {
168                 val versionCode = getVersionCode(context)
169                 check(versionCode != null) { "Couldn't obtain version code for app" }
170                 return RemoteViewsCompatServiceData(
171                     itemsBytes = serializeToBytes(items::writeToParcel),
172                     buildVersion = Build.VERSION.INCREMENTAL,
173                     appVersion = versionCode
174                 )
175             }
176 
177             /**
178              * Returns the stored [RemoteCollectionItems] for the widget/view id, or null if it
179              * couldn't be retrieved for any reason.
180              */
181             internal fun load(
182                 context: Context,
183                 appWidgetId: Int,
184                 viewId: Int
185             ): RemoteCollectionItems? {
186                 val prefs = getPrefs(context)
187                 val hexString = prefs.getString(getKey(appWidgetId, viewId), /* defValue= */ null)
188                 if (hexString == null) {
189                     Log.w(TAG, "No collection items were stored for widget $appWidgetId")
190                     return null
191                 }
192                 val data = deserializeFromHexString(hexString) { RemoteViewsCompatServiceData(it) }
193                 if (Build.VERSION.INCREMENTAL != data.mBuildVersion) {
194                     Log.w(
195                         TAG,
196                         "Android version code has changed, not using stored collection items for " +
197                             "widget $appWidgetId"
198                     )
199                     return null
200                 }
201                 val versionCode = getVersionCode(context)
202                 if (versionCode == null) {
203                     Log.w(
204                         TAG,
205                         "Couldn't get version code, not using stored collection items for widget " +
206                             appWidgetId
207                     )
208                     return null
209                 }
210                 if (versionCode != data.mAppVersion) {
211                     Log.w(
212                         TAG,
213                         "App version code has changed, not using stored collection items for " +
214                             "widget $appWidgetId"
215                     )
216                     return null
217                 }
218                 return try {
219                     deserializeFromBytes(data.mItemsBytes) { RemoteCollectionItems(it) }
220                 } catch (t: Throwable) {
221                     Log.e(
222                         TAG,
223                         "Unable to deserialize stored collection items for widget $appWidgetId",
224                         t
225                     )
226                     null
227                 }
228             }
229 
230             @Suppress("DEPRECATION")
231             internal fun getVersionCode(context: Context): Long? {
232                 val packageManager = context.packageManager
233                 val packageInfo =
234                     try {
235                         packageManager.getPackageInfo(context.packageName, 0)
236                     } catch (e: PackageManager.NameNotFoundException) {
237                         Log.e(
238                             TAG,
239                             "Couldn't retrieve version code for " + context.packageManager,
240                             e
241                         )
242                         return null
243                     }
244                 return PackageInfoCompat.getLongVersionCode(packageInfo)
245             }
246 
247             internal fun serializeToHexString(parcelable: (Parcel, Int) -> Unit): String {
248                 return Base64.encodeToString(serializeToBytes(parcelable), Base64.DEFAULT)
249             }
250 
251             internal fun serializeToBytes(parcelable: (Parcel, Int) -> Unit): ByteArray {
252                 val parcel = Parcel.obtain()
253                 return try {
254                     parcel.setDataPosition(0)
255                     parcelable(parcel, 0)
256                     parcel.marshall()
257                 } finally {
258                     parcel.recycle()
259                 }
260             }
261 
262             internal fun <P> deserializeFromHexString(
263                 hexString: String,
264                 creator: (Parcel) -> P,
265             ): P {
266                 return deserializeFromBytes(Base64.decode(hexString, Base64.DEFAULT), creator)
267             }
268 
269             internal fun <P> deserializeFromBytes(
270                 bytes: ByteArray,
271                 creator: (Parcel) -> P,
272             ): P {
273                 val parcel = Parcel.obtain()
274                 return try {
275                     parcel.unmarshall(bytes, 0, bytes.size)
276                     parcel.setDataPosition(0)
277                     creator(parcel)
278                 } finally {
279                     parcel.recycle()
280                 }
281             }
282         }
283     }
284 
285     internal companion object {
286         private const val TAG = "RemoteViewsCompatServic"
287         private const val EXTRA_VIEW_ID = "androidx.core.widget.extra.view_id"
288 
289         /**
290          * Returns an intent use with [RemoteViews.setRemoteAdapter]. These intents are uniquely
291          * identified by the [appWidgetId] and [viewId].
292          */
293         fun createIntent(context: Context, appWidgetId: Int, viewId: Int): Intent {
294             return Intent(context, RemoteViewsCompatService::class.java)
295                 .putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId)
296                 .putExtra(EXTRA_VIEW_ID, viewId)
297                 .also { intent ->
298                     // Set a data Uri to disambiguate Intents for different widget/view ids.
299                     intent.data = Uri.parse(intent.toUri(Intent.URI_INTENT_SCHEME))
300                 }
301         }
302 
303         /**
304          * Stores [items] to the disk to be used by a [RemoteViewsCompatService] for the same
305          * [appWidgetId] and [viewId].
306          */
307         fun saveItems(
308             context: Context,
309             appWidgetId: Int,
310             viewId: Int,
311             items: RemoteCollectionItems
312         ) {
313             RemoteViewsCompatServiceData.create(context, items).save(context, appWidgetId, viewId)
314         }
315     }
316 }
317