blob: 1fb2dce25410981eb4798c5b6ad5352a0081bbb4 [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
11import com.android.launcher3.util.MainThreadInitializedObject
12import com.android.launcher3.util.Themes
Stefan Andonian146701c2022-11-10 23:07:40 +000013
Stefan Andoniand1b33b32022-12-16 21:22:27 +000014/**
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 */
18class LauncherPrefs(private val context: Context) {
Stefan Andonian146701c2022-11-10 23:07:40 +000019
Stefan Andoniand1b33b32022-12-16 21:22:27 +000020 /**
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 Andonian146701c2022-11-10 23:07:40 +000049 }
50
Stefan Andoniand1b33b32022-12-16 21:22:27 +000051 /**
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 Lima03ac3772023-01-06 15:16:41 +0000146 }
Stefan Andoniand1b33b32022-12-16 21:22:27 +0000147
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
254data 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 Lima03ac3772023-01-06 15:16:41 +0000256}