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