blob: befaa64d506a8e21319948f67a6256285001ab28 [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
20 * TODO(b/262721340): Replace all direct SharedPreference refs with LauncherPrefs / Item methods.
21 */
22class LauncherPrefs(private val context: Context) {
Stefan Andonian146701c2022-11-10 23:07:40 +000023
Stefan Andonian1d7f7032023-01-23 21:55:04 +000024 /** Wrapper around `getInner` for a `ContextualItem` */
Stefan Andonian4c9612b2023-02-22 00:00:03 +000025 fun <T> get(item: ContextualItem<T>): T = getInner(item, item.defaultValueFromContext(context))
Stefan Andonian1d7f7032023-01-23 21:55:04 +000026
27 /** Wrapper around `getInner` for an `Item` */
Stefan Andonian4c9612b2023-02-22 00:00:03 +000028 fun <T> get(item: ConstantItem<T>): T = getInner(item, item.defaultValue)
Stefan Andonian1d7f7032023-01-23 21:55:04 +000029
Stefan Andoniand1b33b32022-12-16 21:22:27 +000030 /**
31 * Retrieves the value for an [Item] from [SharedPreferences]. It handles method typing via the
32 * default value type, and will throw an error if the type of the item provided is not a
33 * `String`, `Boolean`, `Float`, `Int`, `Long`, or `Set<String>`.
34 */
35 @Suppress("IMPLICIT_CAST_TO_ANY", "UNCHECKED_CAST")
Stefan Andonian4c9612b2023-02-22 00:00:03 +000036 private fun <T> getInner(item: Item, default: T): T {
Stefan Andoniand1b33b32022-12-16 21:22:27 +000037 val sp = context.getSharedPreferences(item.sharedPrefFile, Context.MODE_PRIVATE)
38
Stefan Andonian4c9612b2023-02-22 00:00:03 +000039 return when (item.type) {
40 String::class.java -> sp.getString(item.sharedPrefKey, default as? String)
Stefan Andoniand1b33b32022-12-16 21:22:27 +000041 Boolean::class.java,
Stefan Andonian1d7f7032023-01-23 21:55:04 +000042 java.lang.Boolean::class.java -> sp.getBoolean(item.sharedPrefKey, default as Boolean)
Stefan Andoniand1b33b32022-12-16 21:22:27 +000043 Int::class.java,
Stefan Andonian1d7f7032023-01-23 21:55:04 +000044 java.lang.Integer::class.java -> sp.getInt(item.sharedPrefKey, default as Int)
Stefan Andoniand1b33b32022-12-16 21:22:27 +000045 Float::class.java,
Stefan Andonian1d7f7032023-01-23 21:55:04 +000046 java.lang.Float::class.java -> sp.getFloat(item.sharedPrefKey, default as Float)
Stefan Andoniand1b33b32022-12-16 21:22:27 +000047 Long::class.java,
Stefan Andonian1d7f7032023-01-23 21:55:04 +000048 java.lang.Long::class.java -> sp.getLong(item.sharedPrefKey, default as Long)
Stefan Andonian4c9612b2023-02-22 00:00:03 +000049 Set::class.java -> sp.getStringSet(item.sharedPrefKey, default as? Set<String>)
Stefan Andoniand1b33b32022-12-16 21:22:27 +000050 else ->
51 throw IllegalArgumentException(
Stefan Andonian4c9612b2023-02-22 00:00:03 +000052 "item type: ${item.type}" + " is not compatible with sharedPref methods"
Stefan Andoniand1b33b32022-12-16 21:22:27 +000053 )
54 }
55 as T
Stefan Andonian146701c2022-11-10 23:07:40 +000056 }
57
Stefan Andoniand1b33b32022-12-16 21:22:27 +000058 /**
59 * Stores each of the values provided in `SharedPreferences` according to the configuration
60 * contained within the associated items provided. Internally, it uses apply, so the caller
61 * cannot assume that the values that have been put are immediately available for use.
62 *
63 * The forEach loop is necessary here since there is 1 `SharedPreference.Editor` returned from
64 * prepareToPutValue(itemsToValues) for every distinct `SharedPreferences` file present in the
65 * provided item configurations.
66 */
Stefan Andonian1d7f7032023-01-23 21:55:04 +000067 fun put(vararg itemsToValues: Pair<Item, Any>): Unit =
Stefan Andoniand1b33b32022-12-16 21:22:27 +000068 prepareToPutValues(itemsToValues).forEach { it.apply() }
69
70 /**
71 * Stores the value provided in `SharedPreferences` according to the item configuration provided
72 * It is asynchronous, so the caller can't assume that the value put is immediately available.
73 */
Stefan Andonian1d7f7032023-01-23 21:55:04 +000074 fun <T : Any> put(item: Item, value: T): Unit =
Stefan Andoniand1b33b32022-12-16 21:22:27 +000075 context
76 .getSharedPreferences(item.sharedPrefFile, Context.MODE_PRIVATE)
77 .edit()
78 .putValue(item, value)
79 .apply()
80
81 /**
82 * Synchronously stores all the values provided according to their associated Item
83 * configuration.
84 */
Stefan Andonian1d7f7032023-01-23 21:55:04 +000085 fun putSync(vararg itemsToValues: Pair<Item, Any>): Unit =
Stefan Andoniand1b33b32022-12-16 21:22:27 +000086 prepareToPutValues(itemsToValues).forEach { it.commit() }
87
88 /**
89 * Update each shared preference file with the item - value pairs provided. This method is
90 * optimized to avoid retrieving the same shared preference file multiple times.
91 *
92 * @return `List<SharedPreferences.Editor>` 1 for each distinct shared preference file among the
93 * items given as part of the itemsToValues parameter
94 */
95 private fun prepareToPutValues(
Stefan Andonian1d7f7032023-01-23 21:55:04 +000096 itemsToValues: Array<out Pair<Item, Any>>
Stefan Andoniand1b33b32022-12-16 21:22:27 +000097 ): List<SharedPreferences.Editor> =
98 itemsToValues
99 .groupBy { it.first.sharedPrefFile }
100 .map { fileToItemValueList ->
101 context
102 .getSharedPreferences(fileToItemValueList.key, Context.MODE_PRIVATE)
103 .edit()
104 .apply {
105 fileToItemValueList.value.forEach { itemToValue ->
106 putValue(itemToValue.first, itemToValue.second)
107 }
108 }
109 }
110
111 /**
112 * Handles adding values to `SharedPreferences` regardless of type. This method is especially
113 * helpful for updating `SharedPreferences` values for `List<<Item>Any>` that have multiple
114 * types of Item values.
115 */
116 @Suppress("UNCHECKED_CAST")
117 private fun SharedPreferences.Editor.putValue(
Stefan Andonian1d7f7032023-01-23 21:55:04 +0000118 item: Item,
Stefan Andoniand1b33b32022-12-16 21:22:27 +0000119 value: Any
120 ): SharedPreferences.Editor =
121 when (value::class.java) {
122 String::class.java -> putString(item.sharedPrefKey, value as String)
123 Boolean::class.java,
124 java.lang.Boolean::class.java -> putBoolean(item.sharedPrefKey, value as Boolean)
125 Int::class.java,
126 java.lang.Integer::class.java -> putInt(item.sharedPrefKey, value as Int)
127 Float::class.java,
128 java.lang.Float::class.java -> putFloat(item.sharedPrefKey, value as Float)
129 Long::class.java,
130 java.lang.Long::class.java -> putLong(item.sharedPrefKey, value as Long)
131 Set::class.java -> putStringSet(item.sharedPrefKey, value as Set<String>)
132 else ->
133 throw IllegalArgumentException(
Stefan Andonian1d7f7032023-01-23 21:55:04 +0000134 "item type: ${value::class} is not compatible with sharedPref methods"
Stefan Andoniand1b33b32022-12-16 21:22:27 +0000135 )
136 }
137
138 /**
139 * After calling this method, the listener will be notified of any future updates to the
140 * `SharedPreferences` files associated with the provided list of items. The listener will need
141 * to filter update notifications so they don't activate for non-relevant updates.
142 */
Stefan Andonian1d7f7032023-01-23 21:55:04 +0000143 fun addListener(listener: OnSharedPreferenceChangeListener, vararg items: Item) {
Stefan Andoniand1b33b32022-12-16 21:22:27 +0000144 items
145 .map { it.sharedPrefFile }
146 .distinct()
147 .forEach {
148 context
149 .getSharedPreferences(it, Context.MODE_PRIVATE)
150 .registerOnSharedPreferenceChangeListener(listener)
151 }
Thales Lima03ac3772023-01-06 15:16:41 +0000152 }
Stefan Andoniand1b33b32022-12-16 21:22:27 +0000153
154 /**
155 * Stops the listener from getting notified of any more updates to any of the
156 * `SharedPreferences` files associated with any of the provided list of [Item].
157 */
Stefan Andonian1d7f7032023-01-23 21:55:04 +0000158 fun removeListener(listener: OnSharedPreferenceChangeListener, vararg items: Item) {
Stefan Andoniand1b33b32022-12-16 21:22:27 +0000159 // If a listener is not registered to a SharedPreference, unregistering it does nothing
160 items
161 .map { it.sharedPrefFile }
162 .distinct()
163 .forEach {
164 context
165 .getSharedPreferences(it, Context.MODE_PRIVATE)
166 .unregisterOnSharedPreferenceChangeListener(listener)
167 }
168 }
169
170 /**
171 * Checks if all the provided [Item] have values stored in their corresponding
172 * `SharedPreferences` files.
173 */
Stefan Andonian1d7f7032023-01-23 21:55:04 +0000174 fun has(vararg items: Item): Boolean {
Stefan Andoniand1b33b32022-12-16 21:22:27 +0000175 items
176 .groupBy { it.sharedPrefFile }
177 .forEach { (file, itemsSublist) ->
178 val prefs: SharedPreferences =
179 context.getSharedPreferences(file, Context.MODE_PRIVATE)
180 if (!itemsSublist.none { !prefs.contains(it.sharedPrefKey) }) return false
181 }
182 return true
183 }
184
185 /**
186 * Asynchronously removes the [Item]'s value from its corresponding `SharedPreferences` file.
187 */
Stefan Andonian1d7f7032023-01-23 21:55:04 +0000188 fun remove(vararg items: Item) = prepareToRemove(items).forEach { it.apply() }
Stefan Andoniand1b33b32022-12-16 21:22:27 +0000189
190 /** Synchronously removes the [Item]'s value from its corresponding `SharedPreferences` file. */
Stefan Andonian1d7f7032023-01-23 21:55:04 +0000191 fun removeSync(vararg items: Item) = prepareToRemove(items).forEach { it.commit() }
Stefan Andoniand1b33b32022-12-16 21:22:27 +0000192
193 /**
194 * Creates `SharedPreferences.Editor` transactions for removing all the provided [Item] values
195 * from their respective `SharedPreferences` files. These returned `Editors` can then be
196 * committed or applied for synchronous or async behavior.
197 */
Stefan Andonian1d7f7032023-01-23 21:55:04 +0000198 private fun prepareToRemove(items: Array<out Item>): List<SharedPreferences.Editor> =
Stefan Andoniand1b33b32022-12-16 21:22:27 +0000199 items
200 .groupBy { it.sharedPrefFile }
201 .map { (file, items) ->
202 context.getSharedPreferences(file, Context.MODE_PRIVATE).edit().also { editor ->
203 items.forEach { item -> editor.remove(item.sharedPrefKey) }
204 }
205 }
206
207 companion object {
208 @JvmField var INSTANCE = MainThreadInitializedObject { LauncherPrefs(it) }
209
210 @JvmStatic fun get(context: Context): LauncherPrefs = INSTANCE.get(context)
211
212 @JvmField val ICON_STATE = nonRestorableItem(LauncherAppState.KEY_ICON_STATE, "")
213 @JvmField val THEMED_ICONS = backedUpItem(Themes.KEY_THEMED_ICONS, false)
214 @JvmField val PROMISE_ICON_IDS = backedUpItem(InstallSessionHelper.PROMISE_ICON_IDS, "")
215 @JvmField val WORK_EDU_STEP = backedUpItem(WorkProfileManager.KEY_WORK_EDU_STEP, 0)
216 @JvmField val WORKSPACE_SIZE = backedUpItem(DeviceGridState.KEY_WORKSPACE_SIZE, "")
217 @JvmField val HOTSEAT_COUNT = backedUpItem(DeviceGridState.KEY_HOTSEAT_COUNT, -1)
218 @JvmField
219 val DEVICE_TYPE =
220 backedUpItem(DeviceGridState.KEY_DEVICE_TYPE, InvariantDeviceProfile.TYPE_PHONE)
221 @JvmField val DB_FILE = backedUpItem(DeviceGridState.KEY_DB_FILE, "")
222 @JvmField
223 val RESTORE_DEVICE =
224 backedUpItem(RestoreDbTask.RESTORED_DEVICE_TYPE, InvariantDeviceProfile.TYPE_PHONE)
225 @JvmField val APP_WIDGET_IDS = backedUpItem(RestoreDbTask.APPWIDGET_IDS, "")
226 @JvmField val OLD_APP_WIDGET_IDS = backedUpItem(RestoreDbTask.APPWIDGET_OLD_IDS, "")
Stefan Andonian4c9612b2023-02-22 00:00:03 +0000227 @JvmField val GRID_NAME = ConstantItem("idp_grid_name", true, null, String::class.java)
Stefan Andonian1d7f7032023-01-23 21:55:04 +0000228 @JvmField
229 val ALLOW_ROTATION =
Stefan Andonian4c9612b2023-02-22 00:00:03 +0000230 backedUpItem(RotationHelper.ALLOW_ROTATION_PREFERENCE_KEY, Boolean::class.java) {
Stefan Andonian1d7f7032023-01-23 21:55:04 +0000231 RotationHelper.getAllowRotationDefaultValue(DisplayController.INSTANCE.get(it).info)
232 }
Stefan Andoniand1b33b32022-12-16 21:22:27 +0000233
234 @VisibleForTesting
235 @JvmStatic
Stefan Andonian1d7f7032023-01-23 21:55:04 +0000236 fun <T> backedUpItem(sharedPrefKey: String, defaultValue: T): ConstantItem<T> =
Stefan Andonian4c9612b2023-02-22 00:00:03 +0000237 ConstantItem(sharedPrefKey, true, defaultValue)
Stefan Andonian1d7f7032023-01-23 21:55:04 +0000238
239 @JvmStatic
240 fun <T> backedUpItem(
241 sharedPrefKey: String,
Stefan Andonian4c9612b2023-02-22 00:00:03 +0000242 type: Class<out T>,
Stefan Andonian1d7f7032023-01-23 21:55:04 +0000243 defaultValueFromContext: (c: Context) -> T
Stefan Andonian4c9612b2023-02-22 00:00:03 +0000244 ): ContextualItem<T> = ContextualItem(sharedPrefKey, true, defaultValueFromContext, type)
Stefan Andoniand1b33b32022-12-16 21:22:27 +0000245
246 @VisibleForTesting
247 @JvmStatic
Stefan Andonian1d7f7032023-01-23 21:55:04 +0000248 fun <T> nonRestorableItem(sharedPrefKey: String, defaultValue: T): ConstantItem<T> =
Stefan Andonian4c9612b2023-02-22 00:00:03 +0000249 ConstantItem(sharedPrefKey, false, defaultValue)
Stefan Andoniand1b33b32022-12-16 21:22:27 +0000250
251 @Deprecated("Don't use shared preferences directly. Use other LauncherPref methods.")
252 @JvmStatic
253 fun getPrefs(context: Context): SharedPreferences {
254 // Use application context for shared preferences, so we use single cached instance
255 return context.applicationContext.getSharedPreferences(
Stefan Andonian4c9612b2023-02-22 00:00:03 +0000256 SHARED_PREFERENCES_KEY,
Stefan Andoniand1b33b32022-12-16 21:22:27 +0000257 Context.MODE_PRIVATE
258 )
259 }
260
261 @Deprecated("Don't use shared preferences directly. Use other LauncherPref methods.")
262 @JvmStatic
263 fun getDevicePrefs(context: Context): SharedPreferences {
264 // Use application context for shared preferences, so we use a single cached instance
265 return context.applicationContext.getSharedPreferences(
Stefan Andonian4c9612b2023-02-22 00:00:03 +0000266 DEVICE_PREFERENCES_KEY,
Stefan Andoniand1b33b32022-12-16 21:22:27 +0000267 Context.MODE_PRIVATE
268 )
269 }
270 }
271}
272
Stefan Andonian1d7f7032023-01-23 21:55:04 +0000273abstract class Item {
274 abstract val sharedPrefKey: String
Stefan Andonian4c9612b2023-02-22 00:00:03 +0000275 abstract val isBackedUp: Boolean
276 abstract val type: Class<*>
277 val sharedPrefFile: String = if (isBackedUp) SHARED_PREFERENCES_KEY else DEVICE_PREFERENCES_KEY
Stefan Andonian1d7f7032023-01-23 21:55:04 +0000278
279 fun <T> to(value: T): Pair<Item, T> = Pair(this, value)
280}
281
282data class ConstantItem<T>(
283 override val sharedPrefKey: String,
Stefan Andonian4c9612b2023-02-22 00:00:03 +0000284 override val isBackedUp: Boolean,
285 val defaultValue: T,
286 // The default value can be null. If so, the type needs to be explicitly stated, or else NPE
287 override val type: Class<out T> = defaultValue!!::class.java
Stefan Andonian1d7f7032023-01-23 21:55:04 +0000288) : Item()
289
290data class ContextualItem<T>(
291 override val sharedPrefKey: String,
Stefan Andonian4c9612b2023-02-22 00:00:03 +0000292 override val isBackedUp: Boolean,
293 private val defaultSupplier: (c: Context) -> T,
294 override val type: Class<out T>
Stefan Andonian1d7f7032023-01-23 21:55:04 +0000295) : Item() {
296 private var default: T? = null
297
298 fun defaultValueFromContext(context: Context): T {
299 if (default == null) {
300 default = defaultSupplier(context)
301 }
302 return default!!
303 }
Thales Lima03ac3772023-01-06 15:16:41 +0000304}