blob: 5680c18abbe23b07355a81dc3f1898fe9a7cc480 [file] [log] [blame]
Stefan Andonian146701c2022-11-10 23:07:40 +00001package com.android.launcher3
2
3import android.content.Context
4import android.content.SharedPreferences
Stefan Andoniand1b33b32022-12-16 21:22:27 +00005import android.content.SharedPreferences.OnSharedPreferenceChangeListener
6import androidx.annotation.VisibleForTesting
Stefan Andonian4c9612b2023-02-22 00:00:03 +00007import com.android.launcher3.LauncherFiles.DEVICE_PREFERENCES_KEY
8import com.android.launcher3.LauncherFiles.SHARED_PREFERENCES_KEY
Stefan Andoniand1b33b32022-12-16 21:22:27 +00009import com.android.launcher3.allapps.WorkProfileManager
10import com.android.launcher3.model.DeviceGridState
11import com.android.launcher3.pm.InstallSessionHelper
12import com.android.launcher3.provider.RestoreDbTask
Stefan Andonian1d7f7032023-01-23 21:55:04 +000013import com.android.launcher3.states.RotationHelper
14import com.android.launcher3.util.DisplayController
Stefan Andoniand1b33b32022-12-16 21:22:27 +000015import com.android.launcher3.util.MainThreadInitializedObject
16import com.android.launcher3.util.Themes
Stefan Andonian146701c2022-11-10 23:07:40 +000017
Stefan Andoniand1b33b32022-12-16 21:22:27 +000018/**
19 * Use same context for shared preferences, so that we use a single cached instance
Jordan Demeulenaerebe82bc62023-03-01 09:11:48 +000020 *
Stefan Andoniand1b33b32022-12-16 21:22:27 +000021 * TODO(b/262721340): Replace all direct SharedPreference refs with LauncherPrefs / Item methods.
22 */
23class LauncherPrefs(private val context: Context) {
Stefan Andonian146701c2022-11-10 23:07:40 +000024
Stefan Andonian1d7f7032023-01-23 21:55:04 +000025 /** Wrapper around `getInner` for a `ContextualItem` */
Stefan Andonian4c9612b2023-02-22 00:00:03 +000026 fun <T> get(item: ContextualItem<T>): T = getInner(item, item.defaultValueFromContext(context))
Stefan Andonian1d7f7032023-01-23 21:55:04 +000027
28 /** Wrapper around `getInner` for an `Item` */
Stefan Andonian4c9612b2023-02-22 00:00:03 +000029 fun <T> get(item: ConstantItem<T>): T = getInner(item, item.defaultValue)
Stefan Andonian1d7f7032023-01-23 21:55:04 +000030
Stefan Andoniand1b33b32022-12-16 21:22:27 +000031 /**
32 * Retrieves the value for an [Item] from [SharedPreferences]. It handles method typing via the
33 * default value type, and will throw an error if the type of the item provided is not a
34 * `String`, `Boolean`, `Float`, `Int`, `Long`, or `Set<String>`.
35 */
36 @Suppress("IMPLICIT_CAST_TO_ANY", "UNCHECKED_CAST")
Stefan Andonian4c9612b2023-02-22 00:00:03 +000037 private fun <T> getInner(item: Item, default: T): T {
Stefan Andoniand1b33b32022-12-16 21:22:27 +000038 val sp = context.getSharedPreferences(item.sharedPrefFile, Context.MODE_PRIVATE)
39
Stefan Andonian4c9612b2023-02-22 00:00:03 +000040 return when (item.type) {
41 String::class.java -> sp.getString(item.sharedPrefKey, default as? String)
Stefan Andoniand1b33b32022-12-16 21:22:27 +000042 Boolean::class.java,
Stefan Andonian1d7f7032023-01-23 21:55:04 +000043 java.lang.Boolean::class.java -> sp.getBoolean(item.sharedPrefKey, default as Boolean)
Stefan Andoniand1b33b32022-12-16 21:22:27 +000044 Int::class.java,
Stefan Andonian1d7f7032023-01-23 21:55:04 +000045 java.lang.Integer::class.java -> sp.getInt(item.sharedPrefKey, default as Int)
Stefan Andoniand1b33b32022-12-16 21:22:27 +000046 Float::class.java,
Stefan Andonian1d7f7032023-01-23 21:55:04 +000047 java.lang.Float::class.java -> sp.getFloat(item.sharedPrefKey, default as Float)
Stefan Andoniand1b33b32022-12-16 21:22:27 +000048 Long::class.java,
Stefan Andonian1d7f7032023-01-23 21:55:04 +000049 java.lang.Long::class.java -> sp.getLong(item.sharedPrefKey, default as Long)
Stefan Andonian4c9612b2023-02-22 00:00:03 +000050 Set::class.java -> sp.getStringSet(item.sharedPrefKey, default as? Set<String>)
Stefan Andoniand1b33b32022-12-16 21:22:27 +000051 else ->
52 throw IllegalArgumentException(
Stefan Andonian4c9612b2023-02-22 00:00:03 +000053 "item type: ${item.type}" + " is not compatible with sharedPref methods"
Stefan Andoniand1b33b32022-12-16 21:22:27 +000054 )
55 }
56 as T
Stefan Andonian146701c2022-11-10 23:07:40 +000057 }
58
Stefan Andoniand1b33b32022-12-16 21:22:27 +000059 /**
60 * Stores each of the values provided in `SharedPreferences` according to the configuration
61 * contained within the associated items provided. Internally, it uses apply, so the caller
62 * cannot assume that the values that have been put are immediately available for use.
63 *
64 * The forEach loop is necessary here since there is 1 `SharedPreference.Editor` returned from
65 * prepareToPutValue(itemsToValues) for every distinct `SharedPreferences` file present in the
66 * provided item configurations.
67 */
Stefan Andonian1d7f7032023-01-23 21:55:04 +000068 fun put(vararg itemsToValues: Pair<Item, Any>): Unit =
Stefan Andoniand1b33b32022-12-16 21:22:27 +000069 prepareToPutValues(itemsToValues).forEach { it.apply() }
70
71 /**
72 * Stores the value provided in `SharedPreferences` according to the item configuration provided
73 * It is asynchronous, so the caller can't assume that the value put is immediately available.
74 */
Stefan Andonian1d7f7032023-01-23 21:55:04 +000075 fun <T : Any> put(item: Item, value: T): Unit =
Stefan Andoniand1b33b32022-12-16 21:22:27 +000076 context
77 .getSharedPreferences(item.sharedPrefFile, Context.MODE_PRIVATE)
78 .edit()
79 .putValue(item, value)
80 .apply()
81
82 /**
83 * Synchronously stores all the values provided according to their associated Item
84 * configuration.
85 */
Stefan Andonian1d7f7032023-01-23 21:55:04 +000086 fun putSync(vararg itemsToValues: Pair<Item, Any>): Unit =
Stefan Andoniand1b33b32022-12-16 21:22:27 +000087 prepareToPutValues(itemsToValues).forEach { it.commit() }
88
89 /**
90 * Update each shared preference file with the item - value pairs provided. This method is
91 * optimized to avoid retrieving the same shared preference file multiple times.
92 *
93 * @return `List<SharedPreferences.Editor>` 1 for each distinct shared preference file among the
Jordan Demeulenaerebe82bc62023-03-01 09:11:48 +000094 * items given as part of the itemsToValues parameter
Stefan Andoniand1b33b32022-12-16 21:22:27 +000095 */
96 private fun prepareToPutValues(
Stefan Andonian1d7f7032023-01-23 21:55:04 +000097 itemsToValues: Array<out Pair<Item, Any>>
Stefan Andoniand1b33b32022-12-16 21:22:27 +000098 ): List<SharedPreferences.Editor> =
99 itemsToValues
100 .groupBy { it.first.sharedPrefFile }
101 .map { fileToItemValueList ->
102 context
103 .getSharedPreferences(fileToItemValueList.key, Context.MODE_PRIVATE)
104 .edit()
105 .apply {
106 fileToItemValueList.value.forEach { itemToValue ->
107 putValue(itemToValue.first, itemToValue.second)
108 }
109 }
110 }
111
112 /**
113 * Handles adding values to `SharedPreferences` regardless of type. This method is especially
114 * helpful for updating `SharedPreferences` values for `List<<Item>Any>` that have multiple
115 * types of Item values.
116 */
117 @Suppress("UNCHECKED_CAST")
118 private fun SharedPreferences.Editor.putValue(
Stefan Andonian1d7f7032023-01-23 21:55:04 +0000119 item: Item,
Stefan Andoniand1b33b32022-12-16 21:22:27 +0000120 value: Any
121 ): SharedPreferences.Editor =
122 when (value::class.java) {
123 String::class.java -> putString(item.sharedPrefKey, value as String)
124 Boolean::class.java,
125 java.lang.Boolean::class.java -> putBoolean(item.sharedPrefKey, value as Boolean)
126 Int::class.java,
127 java.lang.Integer::class.java -> putInt(item.sharedPrefKey, value as Int)
128 Float::class.java,
129 java.lang.Float::class.java -> putFloat(item.sharedPrefKey, value as Float)
130 Long::class.java,
131 java.lang.Long::class.java -> putLong(item.sharedPrefKey, value as Long)
132 Set::class.java -> putStringSet(item.sharedPrefKey, value as Set<String>)
133 else ->
134 throw IllegalArgumentException(
Stefan Andonian1d7f7032023-01-23 21:55:04 +0000135 "item type: ${value::class} is not compatible with sharedPref methods"
Stefan Andoniand1b33b32022-12-16 21:22:27 +0000136 )
137 }
138
139 /**
140 * After calling this method, the listener will be notified of any future updates to the
141 * `SharedPreferences` files associated with the provided list of items. The listener will need
142 * to filter update notifications so they don't activate for non-relevant updates.
143 */
Stefan Andonian1d7f7032023-01-23 21:55:04 +0000144 fun addListener(listener: OnSharedPreferenceChangeListener, vararg items: Item) {
Stefan Andoniand1b33b32022-12-16 21:22:27 +0000145 items
146 .map { it.sharedPrefFile }
147 .distinct()
148 .forEach {
149 context
150 .getSharedPreferences(it, Context.MODE_PRIVATE)
151 .registerOnSharedPreferenceChangeListener(listener)
152 }
Thales Lima03ac3772023-01-06 15:16:41 +0000153 }
Stefan Andoniand1b33b32022-12-16 21:22:27 +0000154
155 /**
156 * Stops the listener from getting notified of any more updates to any of the
157 * `SharedPreferences` files associated with any of the provided list of [Item].
158 */
Stefan Andonian1d7f7032023-01-23 21:55:04 +0000159 fun removeListener(listener: OnSharedPreferenceChangeListener, vararg items: Item) {
Stefan Andoniand1b33b32022-12-16 21:22:27 +0000160 // If a listener is not registered to a SharedPreference, unregistering it does nothing
161 items
162 .map { it.sharedPrefFile }
163 .distinct()
164 .forEach {
165 context
166 .getSharedPreferences(it, Context.MODE_PRIVATE)
167 .unregisterOnSharedPreferenceChangeListener(listener)
168 }
169 }
170
171 /**
172 * Checks if all the provided [Item] have values stored in their corresponding
173 * `SharedPreferences` files.
174 */
Stefan Andonian1d7f7032023-01-23 21:55:04 +0000175 fun has(vararg items: Item): Boolean {
Stefan Andoniand1b33b32022-12-16 21:22:27 +0000176 items
177 .groupBy { it.sharedPrefFile }
178 .forEach { (file, itemsSublist) ->
179 val prefs: SharedPreferences =
180 context.getSharedPreferences(file, Context.MODE_PRIVATE)
181 if (!itemsSublist.none { !prefs.contains(it.sharedPrefKey) }) return false
182 }
183 return true
184 }
185
186 /**
187 * Asynchronously removes the [Item]'s value from its corresponding `SharedPreferences` file.
188 */
Stefan Andonian1d7f7032023-01-23 21:55:04 +0000189 fun remove(vararg items: Item) = prepareToRemove(items).forEach { it.apply() }
Stefan Andoniand1b33b32022-12-16 21:22:27 +0000190
191 /** Synchronously removes the [Item]'s value from its corresponding `SharedPreferences` file. */
Stefan Andonian1d7f7032023-01-23 21:55:04 +0000192 fun removeSync(vararg items: Item) = prepareToRemove(items).forEach { it.commit() }
Stefan Andoniand1b33b32022-12-16 21:22:27 +0000193
194 /**
195 * Creates `SharedPreferences.Editor` transactions for removing all the provided [Item] values
196 * from their respective `SharedPreferences` files. These returned `Editors` can then be
197 * committed or applied for synchronous or async behavior.
198 */
Stefan Andonian1d7f7032023-01-23 21:55:04 +0000199 private fun prepareToRemove(items: Array<out Item>): List<SharedPreferences.Editor> =
Stefan Andoniand1b33b32022-12-16 21:22:27 +0000200 items
201 .groupBy { it.sharedPrefFile }
202 .map { (file, items) ->
203 context.getSharedPreferences(file, Context.MODE_PRIVATE).edit().also { editor ->
204 items.forEach { item -> editor.remove(item.sharedPrefKey) }
205 }
206 }
207
208 companion object {
209 @JvmField var INSTANCE = MainThreadInitializedObject { LauncherPrefs(it) }
210
211 @JvmStatic fun get(context: Context): LauncherPrefs = INSTANCE.get(context)
212
213 @JvmField val ICON_STATE = nonRestorableItem(LauncherAppState.KEY_ICON_STATE, "")
214 @JvmField val THEMED_ICONS = backedUpItem(Themes.KEY_THEMED_ICONS, false)
215 @JvmField val PROMISE_ICON_IDS = backedUpItem(InstallSessionHelper.PROMISE_ICON_IDS, "")
216 @JvmField val WORK_EDU_STEP = backedUpItem(WorkProfileManager.KEY_WORK_EDU_STEP, 0)
217 @JvmField val WORKSPACE_SIZE = backedUpItem(DeviceGridState.KEY_WORKSPACE_SIZE, "")
218 @JvmField val HOTSEAT_COUNT = backedUpItem(DeviceGridState.KEY_HOTSEAT_COUNT, -1)
219 @JvmField
220 val DEVICE_TYPE =
221 backedUpItem(DeviceGridState.KEY_DEVICE_TYPE, InvariantDeviceProfile.TYPE_PHONE)
222 @JvmField val DB_FILE = backedUpItem(DeviceGridState.KEY_DB_FILE, "")
223 @JvmField
224 val RESTORE_DEVICE =
225 backedUpItem(RestoreDbTask.RESTORED_DEVICE_TYPE, InvariantDeviceProfile.TYPE_PHONE)
226 @JvmField val APP_WIDGET_IDS = backedUpItem(RestoreDbTask.APPWIDGET_IDS, "")
227 @JvmField val OLD_APP_WIDGET_IDS = backedUpItem(RestoreDbTask.APPWIDGET_OLD_IDS, "")
Stefan Andonian4c9612b2023-02-22 00:00:03 +0000228 @JvmField val GRID_NAME = ConstantItem("idp_grid_name", true, null, String::class.java)
Stefan Andonian1d7f7032023-01-23 21:55:04 +0000229 @JvmField
230 val ALLOW_ROTATION =
Stefan Andonian4c9612b2023-02-22 00:00:03 +0000231 backedUpItem(RotationHelper.ALLOW_ROTATION_PREFERENCE_KEY, Boolean::class.java) {
Stefan Andonian1d7f7032023-01-23 21:55:04 +0000232 RotationHelper.getAllowRotationDefaultValue(DisplayController.INSTANCE.get(it).info)
233 }
Stefan Andoniand1b33b32022-12-16 21:22:27 +0000234
235 @VisibleForTesting
236 @JvmStatic
Stefan Andonian1d7f7032023-01-23 21:55:04 +0000237 fun <T> backedUpItem(sharedPrefKey: String, defaultValue: T): ConstantItem<T> =
Stefan Andonian4c9612b2023-02-22 00:00:03 +0000238 ConstantItem(sharedPrefKey, true, defaultValue)
Stefan Andonian1d7f7032023-01-23 21:55:04 +0000239
240 @JvmStatic
241 fun <T> backedUpItem(
242 sharedPrefKey: String,
Stefan Andonian4c9612b2023-02-22 00:00:03 +0000243 type: Class<out T>,
Stefan Andonian1d7f7032023-01-23 21:55:04 +0000244 defaultValueFromContext: (c: Context) -> T
Stefan Andonian4c9612b2023-02-22 00:00:03 +0000245 ): ContextualItem<T> = ContextualItem(sharedPrefKey, true, defaultValueFromContext, type)
Stefan Andoniand1b33b32022-12-16 21:22:27 +0000246
247 @VisibleForTesting
248 @JvmStatic
Stefan Andonian1d7f7032023-01-23 21:55:04 +0000249 fun <T> nonRestorableItem(sharedPrefKey: String, defaultValue: T): ConstantItem<T> =
Stefan Andonian4c9612b2023-02-22 00:00:03 +0000250 ConstantItem(sharedPrefKey, false, defaultValue)
Stefan Andoniand1b33b32022-12-16 21:22:27 +0000251
252 @Deprecated("Don't use shared preferences directly. Use other LauncherPref methods.")
253 @JvmStatic
254 fun getPrefs(context: Context): SharedPreferences {
255 // Use application context for shared preferences, so we use single cached instance
256 return context.applicationContext.getSharedPreferences(
Stefan Andonian4c9612b2023-02-22 00:00:03 +0000257 SHARED_PREFERENCES_KEY,
Stefan Andoniand1b33b32022-12-16 21:22:27 +0000258 Context.MODE_PRIVATE
259 )
260 }
261
262 @Deprecated("Don't use shared preferences directly. Use other LauncherPref methods.")
263 @JvmStatic
264 fun getDevicePrefs(context: Context): SharedPreferences {
265 // Use application context for shared preferences, so we use a single cached instance
266 return context.applicationContext.getSharedPreferences(
Stefan Andonian4c9612b2023-02-22 00:00:03 +0000267 DEVICE_PREFERENCES_KEY,
Stefan Andoniand1b33b32022-12-16 21:22:27 +0000268 Context.MODE_PRIVATE
269 )
270 }
271 }
272}
273
Stefan Andonian1d7f7032023-01-23 21:55:04 +0000274abstract class Item {
275 abstract val sharedPrefKey: String
Stefan Andonian4c9612b2023-02-22 00:00:03 +0000276 abstract val isBackedUp: Boolean
277 abstract val type: Class<*>
Stefan Andonian246eeaa2023-02-21 22:14:47 +0000278 val sharedPrefFile: String
279 get() = if (isBackedUp) SHARED_PREFERENCES_KEY else DEVICE_PREFERENCES_KEY
Stefan Andonian1d7f7032023-01-23 21:55:04 +0000280
281 fun <T> to(value: T): Pair<Item, T> = Pair(this, value)
282}
283
284data class ConstantItem<T>(
285 override val sharedPrefKey: String,
Stefan Andonian4c9612b2023-02-22 00:00:03 +0000286 override val isBackedUp: Boolean,
287 val defaultValue: T,
288 // The default value can be null. If so, the type needs to be explicitly stated, or else NPE
289 override val type: Class<out T> = defaultValue!!::class.java
Stefan Andonian1d7f7032023-01-23 21:55:04 +0000290) : Item()
291
292data class ContextualItem<T>(
293 override val sharedPrefKey: String,
Stefan Andonian4c9612b2023-02-22 00:00:03 +0000294 override val isBackedUp: Boolean,
295 private val defaultSupplier: (c: Context) -> T,
296 override val type: Class<out T>
Stefan Andonian1d7f7032023-01-23 21:55:04 +0000297) : Item() {
298 private var default: T? = null
299
300 fun defaultValueFromContext(context: Context): T {
301 if (default == null) {
302 default = defaultSupplier(context)
303 }
304 return default!!
305 }
Thales Lima03ac3772023-01-06 15:16:41 +0000306}