1 /* <lambda>null2 * Copyright (C) 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 package com.android.launcher3 17 18 import android.content.Context 19 import android.content.Context.MODE_PRIVATE 20 import android.content.SharedPreferences 21 import android.content.SharedPreferences.OnSharedPreferenceChangeListener 22 import android.util.Log 23 import androidx.annotation.VisibleForTesting 24 import com.android.launcher3.LauncherFiles.DEVICE_PREFERENCES_KEY 25 import com.android.launcher3.LauncherFiles.SHARED_PREFERENCES_KEY 26 import com.android.launcher3.allapps.WorkProfileManager 27 import com.android.launcher3.model.DeviceGridState 28 import com.android.launcher3.pm.InstallSessionHelper 29 import com.android.launcher3.provider.RestoreDbTask 30 import com.android.launcher3.states.RotationHelper 31 import com.android.launcher3.util.DisplayController 32 import com.android.launcher3.util.MainThreadInitializedObject 33 import com.android.launcher3.util.Themes 34 35 /** 36 * Use same context for shared preferences, so that we use a single cached instance 37 * 38 * TODO(b/262721340): Replace all direct SharedPreference refs with LauncherPrefs / Item methods. 39 * TODO(b/274501660): Fix ReorderWidgets#simpleReorder test before enabling 40 * isBootAwareStartupDataEnabled 41 */ 42 class LauncherPrefs(private val encryptedContext: Context) { 43 private val deviceProtectedStorageContext = 44 encryptedContext.createDeviceProtectedStorageContext() 45 46 private val bootAwarePrefs 47 get() = 48 deviceProtectedStorageContext.getSharedPreferences(BOOT_AWARE_PREFS_KEY, MODE_PRIVATE) 49 50 private val Item.encryptedPrefs 51 get() = encryptedContext.getSharedPreferences(sharedPrefFile, MODE_PRIVATE) 52 53 // This call to `SharedPreferences` needs to be explicit rather than using `get` since doing so 54 // would result in a circular dependency between `isStartupDataMigrated` and `choosePreferences` 55 val isStartupDataMigrated: Boolean 56 get() = 57 bootAwarePrefs.getBoolean( 58 IS_STARTUP_DATA_MIGRATED.sharedPrefKey, 59 IS_STARTUP_DATA_MIGRATED.defaultValue 60 ) 61 62 private fun chooseSharedPreferences(item: Item): SharedPreferences = 63 if (isBootAwareStartupDataEnabled && item.isBootAware && isStartupDataMigrated) 64 bootAwarePrefs 65 else item.encryptedPrefs 66 67 /** Wrapper around `getInner` for a `ContextualItem` */ 68 fun <T> get(item: ContextualItem<T>): T = 69 getInner(item, item.defaultValueFromContext(encryptedContext)) 70 71 /** Wrapper around `getInner` for an `Item` */ 72 fun <T> get(item: ConstantItem<T>): T = getInner(item, item.defaultValue) 73 74 /** 75 * Retrieves the value for an [Item] from [SharedPreferences]. It handles method typing via the 76 * default value type, and will throw an error if the type of the item provided is not a 77 * `String`, `Boolean`, `Float`, `Int`, `Long`, or `Set<String>`. 78 */ 79 @Suppress("IMPLICIT_CAST_TO_ANY", "UNCHECKED_CAST") 80 private fun <T> getInner(item: Item, default: T): T { 81 val sp = chooseSharedPreferences(item) 82 83 return when (item.type) { 84 String::class.java -> sp.getString(item.sharedPrefKey, default as? String) 85 Boolean::class.java, 86 java.lang.Boolean::class.java -> sp.getBoolean(item.sharedPrefKey, default as Boolean) 87 Int::class.java, 88 java.lang.Integer::class.java -> sp.getInt(item.sharedPrefKey, default as Int) 89 Float::class.java, 90 java.lang.Float::class.java -> sp.getFloat(item.sharedPrefKey, default as Float) 91 Long::class.java, 92 java.lang.Long::class.java -> sp.getLong(item.sharedPrefKey, default as Long) 93 Set::class.java -> sp.getStringSet(item.sharedPrefKey, default as? Set<String>) 94 else -> 95 throw IllegalArgumentException( 96 "item type: ${item.type}" + " is not compatible with sharedPref methods" 97 ) 98 } 99 as T 100 } 101 102 /** 103 * Stores each of the values provided in `SharedPreferences` according to the configuration 104 * contained within the associated items provided. Internally, it uses apply, so the caller 105 * cannot assume that the values that have been put are immediately available for use. 106 * 107 * The forEach loop is necessary here since there is 1 `SharedPreference.Editor` returned from 108 * prepareToPutValue(itemsToValues) for every distinct `SharedPreferences` file present in the 109 * provided item configurations. 110 */ 111 fun put(vararg itemsToValues: Pair<Item, Any>): Unit = 112 prepareToPutValues(itemsToValues).forEach { it.apply() } 113 114 /** See referenced `put` method above. */ 115 fun <T : Any> put(item: Item, value: T): Unit = put(item.to(value)) 116 117 /** 118 * Synchronously stores all the values provided according to their associated Item 119 * configuration. 120 */ 121 fun putSync(vararg itemsToValues: Pair<Item, Any>): Unit = 122 prepareToPutValues(itemsToValues).forEach { it.commit() } 123 124 /** 125 * Updates the values stored in `SharedPreferences` for each corresponding Item-value pair. If 126 * the item is boot aware, this method updates both the boot aware and the encrypted files. This 127 * is done because: 1) It allows for easy roll-back if the data is already in encrypted prefs 128 * and we need to turn off the boot aware data feature & 2) It simplifies Backup/Restore, which 129 * already points to encrypted storage. 130 * 131 * Returns a list of editors with all transactions added so that the caller can determine to use 132 * .apply() or .commit() 133 */ 134 private fun prepareToPutValues( 135 updates: Array<out Pair<Item, Any>> 136 ): List<SharedPreferences.Editor> { 137 val updatesPerPrefFile = updates.groupBy { it.first.encryptedPrefs }.toMutableMap() 138 139 if (isBootAwareStartupDataEnabled) { 140 val bootAwareUpdates = updates.filter { it.first.isBootAware } 141 if (bootAwareUpdates.isNotEmpty()) { 142 updatesPerPrefFile[bootAwarePrefs] = bootAwareUpdates 143 } 144 } 145 146 return updatesPerPrefFile.map { prefToItemValueList -> 147 prefToItemValueList.key.edit().apply { 148 prefToItemValueList.value.forEach { itemToValue: Pair<Item, Any> -> 149 putValue(itemToValue.first, itemToValue.second) 150 } 151 } 152 } 153 } 154 155 /** 156 * Handles adding values to `SharedPreferences` regardless of type. This method is especially 157 * helpful for updating `SharedPreferences` values for `List<<Item>Any>` that have multiple 158 * types of Item values. 159 */ 160 @Suppress("UNCHECKED_CAST") 161 private fun SharedPreferences.Editor.putValue( 162 item: Item, 163 value: Any? 164 ): SharedPreferences.Editor = 165 when (item.type) { 166 String::class.java -> putString(item.sharedPrefKey, value as? String) 167 Boolean::class.java, 168 java.lang.Boolean::class.java -> putBoolean(item.sharedPrefKey, value as Boolean) 169 Int::class.java, 170 java.lang.Integer::class.java -> putInt(item.sharedPrefKey, value as Int) 171 Float::class.java, 172 java.lang.Float::class.java -> putFloat(item.sharedPrefKey, value as Float) 173 Long::class.java, 174 java.lang.Long::class.java -> putLong(item.sharedPrefKey, value as Long) 175 Set::class.java -> putStringSet(item.sharedPrefKey, value as? Set<String>) 176 else -> 177 throw IllegalArgumentException( 178 "item type: ${item.type} is not compatible with sharedPref methods" 179 ) 180 } 181 182 /** 183 * After calling this method, the listener will be notified of any future updates to the 184 * `SharedPreferences` files associated with the provided list of items. The listener will need 185 * to filter update notifications so they don't activate for non-relevant updates. 186 */ 187 fun addListener(listener: OnSharedPreferenceChangeListener, vararg items: Item) { 188 items 189 .map { chooseSharedPreferences(it) } 190 .distinct() 191 .forEach { it.registerOnSharedPreferenceChangeListener(listener) } 192 } 193 194 /** 195 * Stops the listener from getting notified of any more updates to any of the 196 * `SharedPreferences` files associated with any of the provided list of [Item]. 197 */ 198 fun removeListener(listener: OnSharedPreferenceChangeListener, vararg items: Item) { 199 // If a listener is not registered to a SharedPreference, unregistering it does nothing 200 items 201 .map { chooseSharedPreferences(it) } 202 .distinct() 203 .forEach { it.unregisterOnSharedPreferenceChangeListener(listener) } 204 } 205 206 /** 207 * Checks if all the provided [Item] have values stored in their corresponding 208 * `SharedPreferences` files. 209 */ 210 fun has(vararg items: Item): Boolean { 211 items 212 .groupBy { chooseSharedPreferences(it) } 213 .forEach { (prefs, itemsSublist) -> 214 if (!itemsSublist.none { !prefs.contains(it.sharedPrefKey) }) return false 215 } 216 return true 217 } 218 219 /** 220 * Asynchronously removes the [Item]'s value from its corresponding `SharedPreferences` file. 221 */ 222 fun remove(vararg items: Item) = prepareToRemove(items).forEach { it.apply() } 223 224 /** Synchronously removes the [Item]'s value from its corresponding `SharedPreferences` file. */ 225 fun removeSync(vararg items: Item) = prepareToRemove(items).forEach { it.commit() } 226 227 /** 228 * Removes the key value pairs stored in `SharedPreferences` for each corresponding Item. If the 229 * item is boot aware, this method removes the data from both the boot aware and encrypted 230 * files. 231 * 232 * @return a list of editors with all transactions added so that the caller can determine to use 233 * .apply() or .commit() 234 */ 235 private fun prepareToRemove(items: Array<out Item>): List<SharedPreferences.Editor> { 236 val itemsPerFile = items.groupBy { it.encryptedPrefs }.toMutableMap() 237 238 if (isBootAwareStartupDataEnabled) { 239 val bootAwareUpdates = items.filter { it.isBootAware } 240 if (bootAwareUpdates.isNotEmpty()) { 241 itemsPerFile[bootAwarePrefs] = bootAwareUpdates 242 } 243 } 244 245 return itemsPerFile.map { (prefs, items) -> 246 prefs.edit().also { editor -> 247 items.forEach { item -> editor.remove(item.sharedPrefKey) } 248 } 249 } 250 } 251 252 fun migrateStartupDataToDeviceProtectedStorage() { 253 if (!isBootAwareStartupDataEnabled) return 254 255 Log.d( 256 TAG, 257 "Migrating data to unencrypted shared preferences to enable preloading " + 258 "while the user is locked the next time the device reboots." 259 ) 260 261 with(bootAwarePrefs.edit()) { 262 BOOT_AWARE_ITEMS.forEach { putValue(it, get(it)) } 263 putBoolean(IS_STARTUP_DATA_MIGRATED.sharedPrefKey, true) 264 apply() 265 } 266 } 267 268 companion object { 269 private const val TAG = "LauncherPrefs" 270 @VisibleForTesting const val BOOT_AWARE_PREFS_KEY = "boot_aware_prefs" 271 272 @JvmField var INSTANCE = MainThreadInitializedObject { LauncherPrefs(it) } 273 274 @JvmStatic fun get(context: Context): LauncherPrefs = INSTANCE.get(context) 275 276 @JvmField val ICON_STATE = nonRestorableItem(LauncherAppState.KEY_ICON_STATE, "", true) 277 @JvmField val THEMED_ICONS = backedUpItem(Themes.KEY_THEMED_ICONS, false, true) 278 @JvmField val PROMISE_ICON_IDS = backedUpItem(InstallSessionHelper.PROMISE_ICON_IDS, "") 279 @JvmField val WORK_EDU_STEP = backedUpItem(WorkProfileManager.KEY_WORK_EDU_STEP, 0) 280 @JvmField val WORKSPACE_SIZE = backedUpItem(DeviceGridState.KEY_WORKSPACE_SIZE, "", true) 281 @JvmField val HOTSEAT_COUNT = backedUpItem(DeviceGridState.KEY_HOTSEAT_COUNT, -1, true) 282 @JvmField 283 val DEVICE_TYPE = 284 backedUpItem(DeviceGridState.KEY_DEVICE_TYPE, InvariantDeviceProfile.TYPE_PHONE, true) 285 @JvmField val DB_FILE = backedUpItem(DeviceGridState.KEY_DB_FILE, "", true) 286 @JvmField 287 val RESTORE_DEVICE = 288 backedUpItem( 289 RestoreDbTask.RESTORED_DEVICE_TYPE, 290 InvariantDeviceProfile.TYPE_PHONE, 291 true 292 ) 293 @JvmField val APP_WIDGET_IDS = backedUpItem(RestoreDbTask.APPWIDGET_IDS, "") 294 @JvmField val OLD_APP_WIDGET_IDS = backedUpItem(RestoreDbTask.APPWIDGET_OLD_IDS, "") 295 @JvmField 296 val GRID_NAME = 297 ConstantItem( 298 "idp_grid_name", 299 isBackedUp = true, 300 defaultValue = null, 301 isBootAware = true, 302 type = String::class.java 303 ) 304 @JvmField 305 val ALLOW_ROTATION = 306 backedUpItem(RotationHelper.ALLOW_ROTATION_PREFERENCE_KEY, Boolean::class.java) { 307 RotationHelper.getAllowRotationDefaultValue(DisplayController.INSTANCE.get(it).info) 308 } 309 @JvmField 310 val IS_STARTUP_DATA_MIGRATED = 311 ConstantItem( 312 "is_startup_data_boot_aware", 313 isBackedUp = false, 314 defaultValue = false, 315 isBootAware = true 316 ) 317 318 @VisibleForTesting 319 @JvmStatic 320 fun <T> backedUpItem( 321 sharedPrefKey: String, 322 defaultValue: T, 323 isBootAware: Boolean = false 324 ): ConstantItem<T> = 325 ConstantItem(sharedPrefKey, isBackedUp = true, defaultValue, isBootAware) 326 327 @JvmStatic 328 fun <T> backedUpItem( 329 sharedPrefKey: String, 330 type: Class<out T>, 331 isBootAware: Boolean = false, 332 defaultValueFromContext: (c: Context) -> T 333 ): ContextualItem<T> = 334 ContextualItem( 335 sharedPrefKey, 336 isBackedUp = true, 337 defaultValueFromContext, 338 isBootAware, 339 type 340 ) 341 342 @VisibleForTesting 343 @JvmStatic 344 fun <T> nonRestorableItem( 345 sharedPrefKey: String, 346 defaultValue: T, 347 isBootAware: Boolean = false 348 ): ConstantItem<T> = 349 ConstantItem(sharedPrefKey, isBackedUp = false, defaultValue, isBootAware) 350 351 @Deprecated("Don't use shared preferences directly. Use other LauncherPref methods.") 352 @JvmStatic 353 fun getPrefs(context: Context): SharedPreferences { 354 // Use application context for shared preferences, so we use single cached instance 355 return context.applicationContext.getSharedPreferences( 356 SHARED_PREFERENCES_KEY, 357 MODE_PRIVATE 358 ) 359 } 360 361 @Deprecated("Don't use shared preferences directly. Use other LauncherPref methods.") 362 @JvmStatic 363 fun getDevicePrefs(context: Context): SharedPreferences { 364 // Use application context for shared preferences, so we use a single cached instance 365 return context.applicationContext.getSharedPreferences( 366 DEVICE_PREFERENCES_KEY, 367 MODE_PRIVATE 368 ) 369 } 370 } 371 } 372 373 // This is hard-coded to false for now until it is time to release this optimization. It is only 374 // a var because the unit tests are setting this to true so they can run. 375 @VisibleForTesting var isBootAwareStartupDataEnabled: Boolean = false 376 377 private val BOOT_AWARE_ITEMS: MutableSet<ConstantItem<*>> = mutableSetOf() 378 379 abstract class Item { 380 abstract val sharedPrefKey: String 381 abstract val isBackedUp: Boolean 382 abstract val type: Class<*> 383 abstract val isBootAware: Boolean 384 val sharedPrefFile: String 385 get() = if (isBackedUp) SHARED_PREFERENCES_KEY else DEVICE_PREFERENCES_KEY 386 tonull387 fun <T> to(value: T): Pair<Item, T> = Pair(this, value) 388 } 389 390 data class ConstantItem<T>( 391 override val sharedPrefKey: String, 392 override val isBackedUp: Boolean, 393 val defaultValue: T, 394 override val isBootAware: Boolean, 395 // The default value can be null. If so, the type needs to be explicitly stated, or else NPE 396 override val type: Class<out T> = defaultValue!!::class.java 397 ) : Item() { 398 init { 399 if (isBootAware && isBootAwareStartupDataEnabled) { 400 BOOT_AWARE_ITEMS.add(this) 401 } 402 } 403 } 404 405 data class ContextualItem<T>( 406 override val sharedPrefKey: String, 407 override val isBackedUp: Boolean, 408 private val defaultSupplier: (c: Context) -> T, 409 override val isBootAware: Boolean, 410 override val type: Class<out T> 411 ) : Item() { 412 private var default: T? = null 413 defaultValueFromContextnull414 fun defaultValueFromContext(context: Context): T { 415 if (default == null) { 416 default = defaultSupplier(context) 417 } 418 return default!! 419 } 420 } 421