Stefan Andonian | 146701c | 2022-11-10 23:07:40 +0000 | [diff] [blame] | 1 | package com.android.launcher3 |
| 2 | |
| 3 | import android.content.Context |
| 4 | import android.content.SharedPreferences |
Stefan Andonian | d1b33b3 | 2022-12-16 21:22:27 +0000 | [diff] [blame] | 5 | import android.content.SharedPreferences.OnSharedPreferenceChangeListener |
| 6 | import androidx.annotation.VisibleForTesting |
| 7 | import com.android.launcher3.allapps.WorkProfileManager |
| 8 | import com.android.launcher3.model.DeviceGridState |
| 9 | import com.android.launcher3.pm.InstallSessionHelper |
| 10 | import com.android.launcher3.provider.RestoreDbTask |
| 11 | import com.android.launcher3.util.MainThreadInitializedObject |
| 12 | import com.android.launcher3.util.Themes |
Stefan Andonian | 146701c | 2022-11-10 23:07:40 +0000 | [diff] [blame] | 13 | |
Stefan Andonian | d1b33b3 | 2022-12-16 21:22:27 +0000 | [diff] [blame] | 14 | /** |
| 15 | * Use same context for shared preferences, so that we use a single cached instance |
| 16 | * TODO(b/262721340): Replace all direct SharedPreference refs with LauncherPrefs / Item methods. |
| 17 | */ |
| 18 | class LauncherPrefs(private val context: Context) { |
Stefan Andonian | 146701c | 2022-11-10 23:07:40 +0000 | [diff] [blame] | 19 | |
Stefan Andonian | d1b33b3 | 2022-12-16 21:22:27 +0000 | [diff] [blame] | 20 | /** |
| 21 | * Retrieves the value for an [Item] from [SharedPreferences]. It handles method typing via the |
| 22 | * default value type, and will throw an error if the type of the item provided is not a |
| 23 | * `String`, `Boolean`, `Float`, `Int`, `Long`, or `Set<String>`. |
| 24 | */ |
| 25 | @Suppress("IMPLICIT_CAST_TO_ANY", "UNCHECKED_CAST") |
| 26 | fun <T : Any> get(item: Item<T>): T { |
| 27 | val sp = context.getSharedPreferences(item.sharedPrefFile, Context.MODE_PRIVATE) |
| 28 | |
| 29 | return when (item.defaultValue::class.java) { |
| 30 | String::class.java -> sp.getString(item.sharedPrefKey, item.defaultValue as String) |
| 31 | Boolean::class.java, |
| 32 | java.lang.Boolean::class.java -> |
| 33 | sp.getBoolean(item.sharedPrefKey, item.defaultValue as Boolean) |
| 34 | Int::class.java, |
| 35 | java.lang.Integer::class.java -> sp.getInt(item.sharedPrefKey, item.defaultValue as Int) |
| 36 | Float::class.java, |
| 37 | java.lang.Float::class.java -> |
| 38 | sp.getFloat(item.sharedPrefKey, item.defaultValue as Float) |
| 39 | Long::class.java, |
| 40 | java.lang.Long::class.java -> sp.getLong(item.sharedPrefKey, item.defaultValue as Long) |
| 41 | Set::class.java -> sp.getStringSet(item.sharedPrefKey, item.defaultValue as Set<String>) |
| 42 | else -> |
| 43 | throw IllegalArgumentException( |
| 44 | "item type: ${item.defaultValue::class.java}" + |
| 45 | " is not compatible with sharedPref methods" |
| 46 | ) |
| 47 | } |
| 48 | as T |
Stefan Andonian | 146701c | 2022-11-10 23:07:40 +0000 | [diff] [blame] | 49 | } |
| 50 | |
Stefan Andonian | d1b33b3 | 2022-12-16 21:22:27 +0000 | [diff] [blame] | 51 | /** |
| 52 | * Stores each of the values provided in `SharedPreferences` according to the configuration |
| 53 | * contained within the associated items provided. Internally, it uses apply, so the caller |
| 54 | * cannot assume that the values that have been put are immediately available for use. |
| 55 | * |
| 56 | * The forEach loop is necessary here since there is 1 `SharedPreference.Editor` returned from |
| 57 | * prepareToPutValue(itemsToValues) for every distinct `SharedPreferences` file present in the |
| 58 | * provided item configurations. |
| 59 | */ |
| 60 | fun put(vararg itemsToValues: Pair<Item<*>, Any>): Unit = |
| 61 | prepareToPutValues(itemsToValues).forEach { it.apply() } |
| 62 | |
| 63 | /** |
| 64 | * Stores the value provided in `SharedPreferences` according to the item configuration provided |
| 65 | * It is asynchronous, so the caller can't assume that the value put is immediately available. |
| 66 | */ |
| 67 | fun <T : Any> put(item: Item<T>, value: T): Unit = |
| 68 | context |
| 69 | .getSharedPreferences(item.sharedPrefFile, Context.MODE_PRIVATE) |
| 70 | .edit() |
| 71 | .putValue(item, value) |
| 72 | .apply() |
| 73 | |
| 74 | /** |
| 75 | * Synchronously stores all the values provided according to their associated Item |
| 76 | * configuration. |
| 77 | */ |
| 78 | fun putSync(vararg itemsToValues: Pair<Item<*>, Any>): Unit = |
| 79 | prepareToPutValues(itemsToValues).forEach { it.commit() } |
| 80 | |
| 81 | /** |
| 82 | * Update each shared preference file with the item - value pairs provided. This method is |
| 83 | * optimized to avoid retrieving the same shared preference file multiple times. |
| 84 | * |
| 85 | * @return `List<SharedPreferences.Editor>` 1 for each distinct shared preference file among the |
| 86 | * items given as part of the itemsToValues parameter |
| 87 | */ |
| 88 | private fun prepareToPutValues( |
| 89 | itemsToValues: Array<out Pair<Item<*>, Any>> |
| 90 | ): List<SharedPreferences.Editor> = |
| 91 | itemsToValues |
| 92 | .groupBy { it.first.sharedPrefFile } |
| 93 | .map { fileToItemValueList -> |
| 94 | context |
| 95 | .getSharedPreferences(fileToItemValueList.key, Context.MODE_PRIVATE) |
| 96 | .edit() |
| 97 | .apply { |
| 98 | fileToItemValueList.value.forEach { itemToValue -> |
| 99 | putValue(itemToValue.first, itemToValue.second) |
| 100 | } |
| 101 | } |
| 102 | } |
| 103 | |
| 104 | /** |
| 105 | * Handles adding values to `SharedPreferences` regardless of type. This method is especially |
| 106 | * helpful for updating `SharedPreferences` values for `List<<Item>Any>` that have multiple |
| 107 | * types of Item values. |
| 108 | */ |
| 109 | @Suppress("UNCHECKED_CAST") |
| 110 | private fun SharedPreferences.Editor.putValue( |
| 111 | item: Item<*>, |
| 112 | value: Any |
| 113 | ): SharedPreferences.Editor = |
| 114 | when (value::class.java) { |
| 115 | String::class.java -> putString(item.sharedPrefKey, value as String) |
| 116 | Boolean::class.java, |
| 117 | java.lang.Boolean::class.java -> putBoolean(item.sharedPrefKey, value as Boolean) |
| 118 | Int::class.java, |
| 119 | java.lang.Integer::class.java -> putInt(item.sharedPrefKey, value as Int) |
| 120 | Float::class.java, |
| 121 | java.lang.Float::class.java -> putFloat(item.sharedPrefKey, value as Float) |
| 122 | Long::class.java, |
| 123 | java.lang.Long::class.java -> putLong(item.sharedPrefKey, value as Long) |
| 124 | Set::class.java -> putStringSet(item.sharedPrefKey, value as Set<String>) |
| 125 | else -> |
| 126 | throw IllegalArgumentException( |
| 127 | "item type: " + |
| 128 | "${item.defaultValue!!::class} is not compatible with sharedPref methods" |
| 129 | ) |
| 130 | } |
| 131 | |
| 132 | /** |
| 133 | * After calling this method, the listener will be notified of any future updates to the |
| 134 | * `SharedPreferences` files associated with the provided list of items. The listener will need |
| 135 | * to filter update notifications so they don't activate for non-relevant updates. |
| 136 | */ |
| 137 | fun addListener(listener: OnSharedPreferenceChangeListener, vararg items: Item<*>) { |
| 138 | items |
| 139 | .map { it.sharedPrefFile } |
| 140 | .distinct() |
| 141 | .forEach { |
| 142 | context |
| 143 | .getSharedPreferences(it, Context.MODE_PRIVATE) |
| 144 | .registerOnSharedPreferenceChangeListener(listener) |
| 145 | } |
Thales Lima | 03ac377 | 2023-01-06 15:16:41 +0000 | [diff] [blame] | 146 | } |
Stefan Andonian | d1b33b3 | 2022-12-16 21:22:27 +0000 | [diff] [blame] | 147 | |
| 148 | /** |
| 149 | * Stops the listener from getting notified of any more updates to any of the |
| 150 | * `SharedPreferences` files associated with any of the provided list of [Item]. |
| 151 | */ |
| 152 | fun removeListener(listener: OnSharedPreferenceChangeListener, vararg items: Item<*>) { |
| 153 | // If a listener is not registered to a SharedPreference, unregistering it does nothing |
| 154 | items |
| 155 | .map { it.sharedPrefFile } |
| 156 | .distinct() |
| 157 | .forEach { |
| 158 | context |
| 159 | .getSharedPreferences(it, Context.MODE_PRIVATE) |
| 160 | .unregisterOnSharedPreferenceChangeListener(listener) |
| 161 | } |
| 162 | } |
| 163 | |
| 164 | /** |
| 165 | * Checks if all the provided [Item] have values stored in their corresponding |
| 166 | * `SharedPreferences` files. |
| 167 | */ |
| 168 | fun has(vararg items: Item<*>): Boolean { |
| 169 | items |
| 170 | .groupBy { it.sharedPrefFile } |
| 171 | .forEach { (file, itemsSublist) -> |
| 172 | val prefs: SharedPreferences = |
| 173 | context.getSharedPreferences(file, Context.MODE_PRIVATE) |
| 174 | if (!itemsSublist.none { !prefs.contains(it.sharedPrefKey) }) return false |
| 175 | } |
| 176 | return true |
| 177 | } |
| 178 | |
| 179 | /** |
| 180 | * Asynchronously removes the [Item]'s value from its corresponding `SharedPreferences` file. |
| 181 | */ |
| 182 | fun remove(vararg items: Item<*>) = prepareToRemove(items).forEach { it.apply() } |
| 183 | |
| 184 | /** Synchronously removes the [Item]'s value from its corresponding `SharedPreferences` file. */ |
| 185 | fun removeSync(vararg items: Item<*>) = prepareToRemove(items).forEach { it.commit() } |
| 186 | |
| 187 | /** |
| 188 | * Creates `SharedPreferences.Editor` transactions for removing all the provided [Item] values |
| 189 | * from their respective `SharedPreferences` files. These returned `Editors` can then be |
| 190 | * committed or applied for synchronous or async behavior. |
| 191 | */ |
| 192 | private fun prepareToRemove(items: Array<out Item<*>>): List<SharedPreferences.Editor> = |
| 193 | items |
| 194 | .groupBy { it.sharedPrefFile } |
| 195 | .map { (file, items) -> |
| 196 | context.getSharedPreferences(file, Context.MODE_PRIVATE).edit().also { editor -> |
| 197 | items.forEach { item -> editor.remove(item.sharedPrefKey) } |
| 198 | } |
| 199 | } |
| 200 | |
| 201 | companion object { |
| 202 | @JvmField var INSTANCE = MainThreadInitializedObject { LauncherPrefs(it) } |
| 203 | |
| 204 | @JvmStatic fun get(context: Context): LauncherPrefs = INSTANCE.get(context) |
| 205 | |
| 206 | @JvmField val ICON_STATE = nonRestorableItem(LauncherAppState.KEY_ICON_STATE, "") |
| 207 | @JvmField val THEMED_ICONS = backedUpItem(Themes.KEY_THEMED_ICONS, false) |
| 208 | @JvmField val PROMISE_ICON_IDS = backedUpItem(InstallSessionHelper.PROMISE_ICON_IDS, "") |
| 209 | @JvmField val WORK_EDU_STEP = backedUpItem(WorkProfileManager.KEY_WORK_EDU_STEP, 0) |
| 210 | @JvmField val WORKSPACE_SIZE = backedUpItem(DeviceGridState.KEY_WORKSPACE_SIZE, "") |
| 211 | @JvmField val HOTSEAT_COUNT = backedUpItem(DeviceGridState.KEY_HOTSEAT_COUNT, -1) |
| 212 | @JvmField |
| 213 | val DEVICE_TYPE = |
| 214 | backedUpItem(DeviceGridState.KEY_DEVICE_TYPE, InvariantDeviceProfile.TYPE_PHONE) |
| 215 | @JvmField val DB_FILE = backedUpItem(DeviceGridState.KEY_DB_FILE, "") |
| 216 | @JvmField |
| 217 | val RESTORE_DEVICE = |
| 218 | backedUpItem(RestoreDbTask.RESTORED_DEVICE_TYPE, InvariantDeviceProfile.TYPE_PHONE) |
| 219 | @JvmField val APP_WIDGET_IDS = backedUpItem(RestoreDbTask.APPWIDGET_IDS, "") |
| 220 | @JvmField val OLD_APP_WIDGET_IDS = backedUpItem(RestoreDbTask.APPWIDGET_OLD_IDS, "") |
| 221 | |
| 222 | @VisibleForTesting |
| 223 | @JvmStatic |
| 224 | fun <T> backedUpItem(sharedPrefKey: String, defaultValue: T): Item<T> = |
| 225 | Item(sharedPrefKey, LauncherFiles.SHARED_PREFERENCES_KEY, defaultValue) |
| 226 | |
| 227 | @VisibleForTesting |
| 228 | @JvmStatic |
| 229 | fun <T> nonRestorableItem(sharedPrefKey: String, defaultValue: T): Item<T> = |
| 230 | Item(sharedPrefKey, LauncherFiles.DEVICE_PREFERENCES_KEY, defaultValue) |
| 231 | |
| 232 | @Deprecated("Don't use shared preferences directly. Use other LauncherPref methods.") |
| 233 | @JvmStatic |
| 234 | fun getPrefs(context: Context): SharedPreferences { |
| 235 | // Use application context for shared preferences, so we use single cached instance |
| 236 | return context.applicationContext.getSharedPreferences( |
| 237 | LauncherFiles.SHARED_PREFERENCES_KEY, |
| 238 | Context.MODE_PRIVATE |
| 239 | ) |
| 240 | } |
| 241 | |
| 242 | @Deprecated("Don't use shared preferences directly. Use other LauncherPref methods.") |
| 243 | @JvmStatic |
| 244 | fun getDevicePrefs(context: Context): SharedPreferences { |
| 245 | // Use application context for shared preferences, so we use a single cached instance |
| 246 | return context.applicationContext.getSharedPreferences( |
| 247 | LauncherFiles.DEVICE_PREFERENCES_KEY, |
| 248 | Context.MODE_PRIVATE |
| 249 | ) |
| 250 | } |
| 251 | } |
| 252 | } |
| 253 | |
| 254 | data class Item<T>(val sharedPrefKey: String, val sharedPrefFile: String, val defaultValue: T) { |
| 255 | fun to(value: T): Pair<Item<T>, T> = Pair(this, value) |
Thales Lima | 03ac377 | 2023-01-06 15:16:41 +0000 | [diff] [blame] | 256 | } |