blob: 2e07e3022a7175273978f6d6198eb827929387de [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
7import com.android.launcher3.allapps.WorkProfileManager
8import com.android.launcher3.model.DeviceGridState
9import com.android.launcher3.pm.InstallSessionHelper
10import com.android.launcher3.provider.RestoreDbTask
Stefan Andonian1d7f7032023-01-23 21:55:04 +000011import com.android.launcher3.states.RotationHelper
12import com.android.launcher3.util.DisplayController
Stefan Andoniand1b33b32022-12-16 21:22:27 +000013import com.android.launcher3.util.MainThreadInitializedObject
14import com.android.launcher3.util.Themes
Stefan Andonian146701c2022-11-10 23:07:40 +000015
Stefan Andoniand1b33b32022-12-16 21:22:27 +000016/**
17 * Use same context for shared preferences, so that we use a single cached instance
18 * TODO(b/262721340): Replace all direct SharedPreference refs with LauncherPrefs / Item methods.
19 */
20class LauncherPrefs(private val context: Context) {
Stefan Andonian146701c2022-11-10 23:07:40 +000021
Stefan Andonian1d7f7032023-01-23 21:55:04 +000022 /** Wrapper around `getInner` for a `ContextualItem` */
23 fun <T : Any> get(item: ContextualItem<T>): T =
24 getInner(item, item.defaultValueFromContext(context))
25
26 /** Wrapper around `getInner` for an `Item` */
27 fun <T : Any> get(item: ConstantItem<T>): T = getInner(item, item.defaultValue)
28
Stefan Andoniand1b33b32022-12-16 21:22:27 +000029 /**
30 * Retrieves the value for an [Item] from [SharedPreferences]. It handles method typing via the
31 * default value type, and will throw an error if the type of the item provided is not a
32 * `String`, `Boolean`, `Float`, `Int`, `Long`, or `Set<String>`.
33 */
34 @Suppress("IMPLICIT_CAST_TO_ANY", "UNCHECKED_CAST")
Stefan Andonian1d7f7032023-01-23 21:55:04 +000035 private fun <T : Any> getInner(item: Item, default: T): T {
Stefan Andoniand1b33b32022-12-16 21:22:27 +000036 val sp = context.getSharedPreferences(item.sharedPrefFile, Context.MODE_PRIVATE)
37
Stefan Andonian1d7f7032023-01-23 21:55:04 +000038 return when (default::class.java) {
39 String::class.java -> sp.getString(item.sharedPrefKey, default as String)
Stefan Andoniand1b33b32022-12-16 21:22:27 +000040 Boolean::class.java,
Stefan Andonian1d7f7032023-01-23 21:55:04 +000041 java.lang.Boolean::class.java -> sp.getBoolean(item.sharedPrefKey, default as Boolean)
Stefan Andoniand1b33b32022-12-16 21:22:27 +000042 Int::class.java,
Stefan Andonian1d7f7032023-01-23 21:55:04 +000043 java.lang.Integer::class.java -> sp.getInt(item.sharedPrefKey, default as Int)
Stefan Andoniand1b33b32022-12-16 21:22:27 +000044 Float::class.java,
Stefan Andonian1d7f7032023-01-23 21:55:04 +000045 java.lang.Float::class.java -> sp.getFloat(item.sharedPrefKey, default as Float)
Stefan Andoniand1b33b32022-12-16 21:22:27 +000046 Long::class.java,
Stefan Andonian1d7f7032023-01-23 21:55:04 +000047 java.lang.Long::class.java -> sp.getLong(item.sharedPrefKey, default as Long)
48 Set::class.java -> sp.getStringSet(item.sharedPrefKey, default as Set<String>)
Stefan Andoniand1b33b32022-12-16 21:22:27 +000049 else ->
50 throw IllegalArgumentException(
Stefan Andonian1d7f7032023-01-23 21:55:04 +000051 "item type: ${default::class.java}" +
Stefan Andoniand1b33b32022-12-16 21:22:27 +000052 " is not compatible with sharedPref methods"
53 )
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 Andonian1d7f7032023-01-23 21:55:04 +0000227 @JvmField
228 val ALLOW_ROTATION =
229 backedUpItem(RotationHelper.ALLOW_ROTATION_PREFERENCE_KEY) {
230 RotationHelper.getAllowRotationDefaultValue(DisplayController.INSTANCE.get(it).info)
231 }
Stefan Andoniand1b33b32022-12-16 21:22:27 +0000232
233 @VisibleForTesting
234 @JvmStatic
Stefan Andonian1d7f7032023-01-23 21:55:04 +0000235 fun <T> backedUpItem(sharedPrefKey: String, defaultValue: T): ConstantItem<T> =
236 ConstantItem(sharedPrefKey, LauncherFiles.SHARED_PREFERENCES_KEY, defaultValue)
237
238 @JvmStatic
239 fun <T> backedUpItem(
240 sharedPrefKey: String,
241 defaultValueFromContext: (c: Context) -> T
242 ): ContextualItem<T> =
243 ContextualItem(
244 sharedPrefKey,
245 LauncherFiles.SHARED_PREFERENCES_KEY,
246 defaultValueFromContext
247 )
Stefan Andoniand1b33b32022-12-16 21:22:27 +0000248
249 @VisibleForTesting
250 @JvmStatic
Stefan Andonian1d7f7032023-01-23 21:55:04 +0000251 fun <T> nonRestorableItem(sharedPrefKey: String, defaultValue: T): ConstantItem<T> =
252 ConstantItem(sharedPrefKey, LauncherFiles.DEVICE_PREFERENCES_KEY, defaultValue)
Stefan Andoniand1b33b32022-12-16 21:22:27 +0000253
254 @Deprecated("Don't use shared preferences directly. Use other LauncherPref methods.")
255 @JvmStatic
256 fun getPrefs(context: Context): SharedPreferences {
257 // Use application context for shared preferences, so we use single cached instance
258 return context.applicationContext.getSharedPreferences(
259 LauncherFiles.SHARED_PREFERENCES_KEY,
260 Context.MODE_PRIVATE
261 )
262 }
263
264 @Deprecated("Don't use shared preferences directly. Use other LauncherPref methods.")
265 @JvmStatic
266 fun getDevicePrefs(context: Context): SharedPreferences {
267 // Use application context for shared preferences, so we use a single cached instance
268 return context.applicationContext.getSharedPreferences(
269 LauncherFiles.DEVICE_PREFERENCES_KEY,
270 Context.MODE_PRIVATE
271 )
272 }
273 }
274}
275
Stefan Andonian1d7f7032023-01-23 21:55:04 +0000276abstract class Item {
277 abstract val sharedPrefKey: String
278 abstract val sharedPrefFile: String
279
280 fun <T> to(value: T): Pair<Item, T> = Pair(this, value)
281}
282
283data class ConstantItem<T>(
284 override val sharedPrefKey: String,
285 override val sharedPrefFile: String,
286 val defaultValue: T
287) : Item()
288
289data class ContextualItem<T>(
290 override val sharedPrefKey: String,
291 override val sharedPrefFile: String,
292 private val defaultSupplier: (c: Context) -> T
293) : Item() {
294 private var default: T? = null
295
296 fun defaultValueFromContext(context: Context): T {
297 if (default == null) {
298 default = defaultSupplier(context)
299 }
300 return default!!
301 }
Thales Lima03ac3772023-01-06 15:16:41 +0000302}