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 const val TASKBAR_PINNING_KEY = "TASKBAR_PINNING_KEY" 277 @JvmField val ICON_STATE = nonRestorableItem(LauncherAppState.KEY_ICON_STATE, "", true) 278 @JvmField 279 val ALL_APPS_OVERVIEW_THRESHOLD = 280 nonRestorableItem(LauncherAppState.KEY_ALL_APPS_OVERVIEW_THRESHOLD, 180, true) 281 @JvmField val THEMED_ICONS = backedUpItem(Themes.KEY_THEMED_ICONS, false, true) 282 @JvmField val PROMISE_ICON_IDS = backedUpItem(InstallSessionHelper.PROMISE_ICON_IDS, "") 283 @JvmField val WORK_EDU_STEP = backedUpItem(WorkProfileManager.KEY_WORK_EDU_STEP, 0) 284 @JvmField val WORKSPACE_SIZE = backedUpItem(DeviceGridState.KEY_WORKSPACE_SIZE, "", true) 285 @JvmField val HOTSEAT_COUNT = backedUpItem(DeviceGridState.KEY_HOTSEAT_COUNT, -1, true) 286 @JvmField val TASKBAR_PINNING = backedUpItem(TASKBAR_PINNING_KEY, false) 287 288 @JvmField 289 val DEVICE_TYPE = 290 backedUpItem(DeviceGridState.KEY_DEVICE_TYPE, InvariantDeviceProfile.TYPE_PHONE, true) 291 @JvmField val DB_FILE = backedUpItem(DeviceGridState.KEY_DB_FILE, "", true) 292 @JvmField 293 val RESTORE_DEVICE = 294 backedUpItem( 295 RestoreDbTask.RESTORED_DEVICE_TYPE, 296 InvariantDeviceProfile.TYPE_PHONE, 297 true 298 ) 299 @JvmField val APP_WIDGET_IDS = backedUpItem(RestoreDbTask.APPWIDGET_IDS, "") 300 @JvmField val OLD_APP_WIDGET_IDS = backedUpItem(RestoreDbTask.APPWIDGET_OLD_IDS, "") 301 @JvmField 302 val GRID_NAME = 303 ConstantItem( 304 "idp_grid_name", 305 isBackedUp = true, 306 defaultValue = null, 307 isBootAware = true, 308 type = String::class.java 309 ) 310 @JvmField 311 val ALLOW_ROTATION = 312 backedUpItem(RotationHelper.ALLOW_ROTATION_PREFERENCE_KEY, Boolean::class.java) { 313 RotationHelper.getAllowRotationDefaultValue(DisplayController.INSTANCE.get(it).info) 314 } 315 @JvmField 316 val IS_STARTUP_DATA_MIGRATED = 317 ConstantItem( 318 "is_startup_data_boot_aware", 319 isBackedUp = false, 320 defaultValue = false, 321 isBootAware = true 322 ) 323 324 @VisibleForTesting 325 @JvmStatic 326 fun <T> backedUpItem( 327 sharedPrefKey: String, 328 defaultValue: T, 329 isBootAware: Boolean = false 330 ): ConstantItem<T> = 331 ConstantItem(sharedPrefKey, isBackedUp = true, defaultValue, isBootAware) 332 333 @JvmStatic 334 fun <T> backedUpItem( 335 sharedPrefKey: String, 336 type: Class<out T>, 337 isBootAware: Boolean = false, 338 defaultValueFromContext: (c: Context) -> T 339 ): ContextualItem<T> = 340 ContextualItem( 341 sharedPrefKey, 342 isBackedUp = true, 343 defaultValueFromContext, 344 isBootAware, 345 type 346 ) 347 348 @VisibleForTesting 349 @JvmStatic 350 fun <T> nonRestorableItem( 351 sharedPrefKey: String, 352 defaultValue: T, 353 isBootAware: Boolean = false 354 ): ConstantItem<T> = 355 ConstantItem(sharedPrefKey, isBackedUp = false, defaultValue, isBootAware) 356 357 @Deprecated("Don't use shared preferences directly. Use other LauncherPref methods.") 358 @JvmStatic 359 fun getPrefs(context: Context): SharedPreferences { 360 // Use application context for shared preferences, so we use single cached instance 361 return context.applicationContext.getSharedPreferences( 362 SHARED_PREFERENCES_KEY, 363 MODE_PRIVATE 364 ) 365 } 366 367 @Deprecated("Don't use shared preferences directly. Use other LauncherPref methods.") 368 @JvmStatic 369 fun getDevicePrefs(context: Context): SharedPreferences { 370 // Use application context for shared preferences, so we use a single cached instance 371 return context.applicationContext.getSharedPreferences( 372 DEVICE_PREFERENCES_KEY, 373 MODE_PRIVATE 374 ) 375 } 376 } 377 } 378 379 // This is hard-coded to false for now until it is time to release this optimization. It is only 380 // a var because the unit tests are setting this to true so they can run. 381 @VisibleForTesting var isBootAwareStartupDataEnabled: Boolean = false 382 383 private val BOOT_AWARE_ITEMS: MutableSet<ConstantItem<*>> = mutableSetOf() 384 385 abstract class Item { 386 abstract val sharedPrefKey: String 387 abstract val isBackedUp: Boolean 388 abstract val type: Class<*> 389 abstract val isBootAware: Boolean 390 val sharedPrefFile: String 391 get() = if (isBackedUp) SHARED_PREFERENCES_KEY else DEVICE_PREFERENCES_KEY 392 tonull393 fun <T> to(value: T): Pair<Item, T> = Pair(this, value) 394 } 395 396 data class ConstantItem<T>( 397 override val sharedPrefKey: String, 398 override val isBackedUp: Boolean, 399 val defaultValue: T, 400 override val isBootAware: Boolean, 401 // The default value can be null. If so, the type needs to be explicitly stated, or else NPE 402 override val type: Class<out T> = defaultValue!!::class.java 403 ) : Item() { 404 init { 405 if (isBootAware && isBootAwareStartupDataEnabled) { 406 BOOT_AWARE_ITEMS.add(this) 407 } 408 } 409 } 410 411 data class ContextualItem<T>( 412 override val sharedPrefKey: String, 413 override val isBackedUp: Boolean, 414 private val defaultSupplier: (c: Context) -> T, 415 override val isBootAware: Boolean, 416 override val type: Class<out T> 417 ) : Item() { 418 private var default: T? = null 419 defaultValueFromContextnull420 fun defaultValueFromContext(context: Context): T { 421 if (default == null) { 422 default = defaultSupplier(context) 423 } 424 return default!! 425 } 426 } 427