• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
<lambda>null2  * Copyright (C) 2023 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 package com.android.launcher3
17 
18 import android.content.Context
19 import android.content.Context.MODE_PRIVATE
20 import android.content.SharedPreferences
21 import android.content.SharedPreferences.OnSharedPreferenceChangeListener
22 import android.util.Log
23 import androidx.annotation.VisibleForTesting
24 import com.android.launcher3.LauncherFiles.DEVICE_PREFERENCES_KEY
25 import com.android.launcher3.LauncherFiles.SHARED_PREFERENCES_KEY
26 import com.android.launcher3.allapps.WorkProfileManager
27 import com.android.launcher3.model.DeviceGridState
28 import com.android.launcher3.pm.InstallSessionHelper
29 import com.android.launcher3.provider.RestoreDbTask
30 import com.android.launcher3.states.RotationHelper
31 import com.android.launcher3.util.DisplayController
32 import com.android.launcher3.util.MainThreadInitializedObject
33 import com.android.launcher3.util.Themes
34 
35 /**
36  * Use same context for shared preferences, so that we use a single cached instance
37  *
38  * TODO(b/262721340): Replace all direct SharedPreference refs with LauncherPrefs / Item methods.
39  * TODO(b/274501660): Fix ReorderWidgets#simpleReorder test before enabling
40  *   isBootAwareStartupDataEnabled
41  */
42 class LauncherPrefs(private val encryptedContext: Context) {
43     private val deviceProtectedStorageContext =
44         encryptedContext.createDeviceProtectedStorageContext()
45 
46     private val bootAwarePrefs
47         get() =
48             deviceProtectedStorageContext.getSharedPreferences(BOOT_AWARE_PREFS_KEY, MODE_PRIVATE)
49 
50     private val Item.encryptedPrefs
51         get() = encryptedContext.getSharedPreferences(sharedPrefFile, MODE_PRIVATE)
52 
53     // This call to `SharedPreferences` needs to be explicit rather than using `get` since doing so
54     // would result in a circular dependency between `isStartupDataMigrated` and `choosePreferences`
55     val isStartupDataMigrated: Boolean
56         get() =
57             bootAwarePrefs.getBoolean(
58                 IS_STARTUP_DATA_MIGRATED.sharedPrefKey,
59                 IS_STARTUP_DATA_MIGRATED.defaultValue
60             )
61 
62     private fun chooseSharedPreferences(item: Item): SharedPreferences =
63         if (isBootAwareStartupDataEnabled && item.isBootAware && isStartupDataMigrated)
64             bootAwarePrefs
65         else item.encryptedPrefs
66 
67     /** Wrapper around `getInner` for a `ContextualItem` */
68     fun <T> get(item: ContextualItem<T>): T =
69         getInner(item, item.defaultValueFromContext(encryptedContext))
70 
71     /** Wrapper around `getInner` for an `Item` */
72     fun <T> get(item: ConstantItem<T>): T = getInner(item, item.defaultValue)
73 
74     /**
75      * Retrieves the value for an [Item] from [SharedPreferences]. It handles method typing via the
76      * default value type, and will throw an error if the type of the item provided is not a
77      * `String`, `Boolean`, `Float`, `Int`, `Long`, or `Set<String>`.
78      */
79     @Suppress("IMPLICIT_CAST_TO_ANY", "UNCHECKED_CAST")
80     private fun <T> getInner(item: Item, default: T): T {
81         val sp = chooseSharedPreferences(item)
82 
83         return when (item.type) {
84             String::class.java -> sp.getString(item.sharedPrefKey, default as? String)
85             Boolean::class.java,
86             java.lang.Boolean::class.java -> sp.getBoolean(item.sharedPrefKey, default as Boolean)
87             Int::class.java,
88             java.lang.Integer::class.java -> sp.getInt(item.sharedPrefKey, default as Int)
89             Float::class.java,
90             java.lang.Float::class.java -> sp.getFloat(item.sharedPrefKey, default as Float)
91             Long::class.java,
92             java.lang.Long::class.java -> sp.getLong(item.sharedPrefKey, default as Long)
93             Set::class.java -> sp.getStringSet(item.sharedPrefKey, default as? Set<String>)
94             else ->
95                 throw IllegalArgumentException(
96                     "item type: ${item.type}" + " is not compatible with sharedPref methods"
97                 )
98         }
99             as T
100     }
101 
102     /**
103      * Stores each of the values provided in `SharedPreferences` according to the configuration
104      * contained within the associated items provided. Internally, it uses apply, so the caller
105      * cannot assume that the values that have been put are immediately available for use.
106      *
107      * The forEach loop is necessary here since there is 1 `SharedPreference.Editor` returned from
108      * prepareToPutValue(itemsToValues) for every distinct `SharedPreferences` file present in the
109      * provided item configurations.
110      */
111     fun put(vararg itemsToValues: Pair<Item, Any>): Unit =
112         prepareToPutValues(itemsToValues).forEach { it.apply() }
113 
114     /** See referenced `put` method above. */
115     fun <T : Any> put(item: Item, value: T): Unit = put(item.to(value))
116 
117     /**
118      * Synchronously stores all the values provided according to their associated Item
119      * configuration.
120      */
121     fun putSync(vararg itemsToValues: Pair<Item, Any>): Unit =
122         prepareToPutValues(itemsToValues).forEach { it.commit() }
123 
124     /**
125      * Updates the values stored in `SharedPreferences` for each corresponding Item-value pair. If
126      * the item is boot aware, this method updates both the boot aware and the encrypted files. This
127      * is done because: 1) It allows for easy roll-back if the data is already in encrypted prefs
128      * and we need to turn off the boot aware data feature & 2) It simplifies Backup/Restore, which
129      * already points to encrypted storage.
130      *
131      * Returns a list of editors with all transactions added so that the caller can determine to use
132      * .apply() or .commit()
133      */
134     private fun prepareToPutValues(
135         updates: Array<out Pair<Item, Any>>
136     ): List<SharedPreferences.Editor> {
137         val updatesPerPrefFile = updates.groupBy { it.first.encryptedPrefs }.toMutableMap()
138 
139         if (isBootAwareStartupDataEnabled) {
140             val bootAwareUpdates = updates.filter { it.first.isBootAware }
141             if (bootAwareUpdates.isNotEmpty()) {
142                 updatesPerPrefFile[bootAwarePrefs] = bootAwareUpdates
143             }
144         }
145 
146         return updatesPerPrefFile.map { prefToItemValueList ->
147             prefToItemValueList.key.edit().apply {
148                 prefToItemValueList.value.forEach { itemToValue: Pair<Item, Any> ->
149                     putValue(itemToValue.first, itemToValue.second)
150                 }
151             }
152         }
153     }
154 
155     /**
156      * Handles adding values to `SharedPreferences` regardless of type. This method is especially
157      * helpful for updating `SharedPreferences` values for `List<<Item>Any>` that have multiple
158      * types of Item values.
159      */
160     @Suppress("UNCHECKED_CAST")
161     private fun SharedPreferences.Editor.putValue(
162         item: Item,
163         value: Any?
164     ): SharedPreferences.Editor =
165         when (item.type) {
166             String::class.java -> putString(item.sharedPrefKey, value as? String)
167             Boolean::class.java,
168             java.lang.Boolean::class.java -> putBoolean(item.sharedPrefKey, value as Boolean)
169             Int::class.java,
170             java.lang.Integer::class.java -> putInt(item.sharedPrefKey, value as Int)
171             Float::class.java,
172             java.lang.Float::class.java -> putFloat(item.sharedPrefKey, value as Float)
173             Long::class.java,
174             java.lang.Long::class.java -> putLong(item.sharedPrefKey, value as Long)
175             Set::class.java -> putStringSet(item.sharedPrefKey, value as? Set<String>)
176             else ->
177                 throw IllegalArgumentException(
178                     "item type: ${item.type} is not compatible with sharedPref methods"
179                 )
180         }
181 
182     /**
183      * After calling this method, the listener will be notified of any future updates to the
184      * `SharedPreferences` files associated with the provided list of items. The listener will need
185      * to filter update notifications so they don't activate for non-relevant updates.
186      */
187     fun addListener(listener: OnSharedPreferenceChangeListener, vararg items: Item) {
188         items
189             .map { chooseSharedPreferences(it) }
190             .distinct()
191             .forEach { it.registerOnSharedPreferenceChangeListener(listener) }
192     }
193 
194     /**
195      * Stops the listener from getting notified of any more updates to any of the
196      * `SharedPreferences` files associated with any of the provided list of [Item].
197      */
198     fun removeListener(listener: OnSharedPreferenceChangeListener, vararg items: Item) {
199         // If a listener is not registered to a SharedPreference, unregistering it does nothing
200         items
201             .map { chooseSharedPreferences(it) }
202             .distinct()
203             .forEach { it.unregisterOnSharedPreferenceChangeListener(listener) }
204     }
205 
206     /**
207      * Checks if all the provided [Item] have values stored in their corresponding
208      * `SharedPreferences` files.
209      */
210     fun has(vararg items: Item): Boolean {
211         items
212             .groupBy { chooseSharedPreferences(it) }
213             .forEach { (prefs, itemsSublist) ->
214                 if (!itemsSublist.none { !prefs.contains(it.sharedPrefKey) }) return false
215             }
216         return true
217     }
218 
219     /**
220      * Asynchronously removes the [Item]'s value from its corresponding `SharedPreferences` file.
221      */
222     fun remove(vararg items: Item) = prepareToRemove(items).forEach { it.apply() }
223 
224     /** Synchronously removes the [Item]'s value from its corresponding `SharedPreferences` file. */
225     fun removeSync(vararg items: Item) = prepareToRemove(items).forEach { it.commit() }
226 
227     /**
228      * Removes the key value pairs stored in `SharedPreferences` for each corresponding Item. If the
229      * item is boot aware, this method removes the data from both the boot aware and encrypted
230      * files.
231      *
232      * @return a list of editors with all transactions added so that the caller can determine to use
233      *   .apply() or .commit()
234      */
235     private fun prepareToRemove(items: Array<out Item>): List<SharedPreferences.Editor> {
236         val itemsPerFile = items.groupBy { it.encryptedPrefs }.toMutableMap()
237 
238         if (isBootAwareStartupDataEnabled) {
239             val bootAwareUpdates = items.filter { it.isBootAware }
240             if (bootAwareUpdates.isNotEmpty()) {
241                 itemsPerFile[bootAwarePrefs] = bootAwareUpdates
242             }
243         }
244 
245         return itemsPerFile.map { (prefs, items) ->
246             prefs.edit().also { editor ->
247                 items.forEach { item -> editor.remove(item.sharedPrefKey) }
248             }
249         }
250     }
251 
252     fun migrateStartupDataToDeviceProtectedStorage() {
253         if (!isBootAwareStartupDataEnabled) return
254 
255         Log.d(
256             TAG,
257             "Migrating data to unencrypted shared preferences to enable preloading " +
258                 "while the user is locked the next time the device reboots."
259         )
260 
261         with(bootAwarePrefs.edit()) {
262             BOOT_AWARE_ITEMS.forEach { putValue(it, get(it)) }
263             putBoolean(IS_STARTUP_DATA_MIGRATED.sharedPrefKey, true)
264             apply()
265         }
266     }
267 
268     companion object {
269         private const val TAG = "LauncherPrefs"
270         @VisibleForTesting const val BOOT_AWARE_PREFS_KEY = "boot_aware_prefs"
271 
272         @JvmField var INSTANCE = MainThreadInitializedObject { LauncherPrefs(it) }
273 
274         @JvmStatic fun get(context: Context): LauncherPrefs = INSTANCE.get(context)
275 
276         const val TASKBAR_PINNING_KEY = "TASKBAR_PINNING_KEY"
277         @JvmField val ICON_STATE = nonRestorableItem(LauncherAppState.KEY_ICON_STATE, "", true)
278         @JvmField
279         val ALL_APPS_OVERVIEW_THRESHOLD =
280             nonRestorableItem(LauncherAppState.KEY_ALL_APPS_OVERVIEW_THRESHOLD, 180, true)
281         @JvmField val THEMED_ICONS = backedUpItem(Themes.KEY_THEMED_ICONS, false, true)
282         @JvmField val PROMISE_ICON_IDS = backedUpItem(InstallSessionHelper.PROMISE_ICON_IDS, "")
283         @JvmField val WORK_EDU_STEP = backedUpItem(WorkProfileManager.KEY_WORK_EDU_STEP, 0)
284         @JvmField val WORKSPACE_SIZE = backedUpItem(DeviceGridState.KEY_WORKSPACE_SIZE, "", true)
285         @JvmField val HOTSEAT_COUNT = backedUpItem(DeviceGridState.KEY_HOTSEAT_COUNT, -1, true)
286         @JvmField val TASKBAR_PINNING = backedUpItem(TASKBAR_PINNING_KEY, false)
287 
288         @JvmField
289         val DEVICE_TYPE =
290             backedUpItem(DeviceGridState.KEY_DEVICE_TYPE, InvariantDeviceProfile.TYPE_PHONE, true)
291         @JvmField val DB_FILE = backedUpItem(DeviceGridState.KEY_DB_FILE, "", true)
292         @JvmField
293         val RESTORE_DEVICE =
294             backedUpItem(
295                 RestoreDbTask.RESTORED_DEVICE_TYPE,
296                 InvariantDeviceProfile.TYPE_PHONE,
297                 true
298             )
299         @JvmField val APP_WIDGET_IDS = backedUpItem(RestoreDbTask.APPWIDGET_IDS, "")
300         @JvmField val OLD_APP_WIDGET_IDS = backedUpItem(RestoreDbTask.APPWIDGET_OLD_IDS, "")
301         @JvmField
302         val GRID_NAME =
303             ConstantItem(
304                 "idp_grid_name",
305                 isBackedUp = true,
306                 defaultValue = null,
307                 isBootAware = true,
308                 type = String::class.java
309             )
310         @JvmField
311         val ALLOW_ROTATION =
312             backedUpItem(RotationHelper.ALLOW_ROTATION_PREFERENCE_KEY, Boolean::class.java) {
313                 RotationHelper.getAllowRotationDefaultValue(DisplayController.INSTANCE.get(it).info)
314             }
315         @JvmField
316         val IS_STARTUP_DATA_MIGRATED =
317             ConstantItem(
318                 "is_startup_data_boot_aware",
319                 isBackedUp = false,
320                 defaultValue = false,
321                 isBootAware = true
322             )
323 
324         @VisibleForTesting
325         @JvmStatic
326         fun <T> backedUpItem(
327             sharedPrefKey: String,
328             defaultValue: T,
329             isBootAware: Boolean = false
330         ): ConstantItem<T> =
331             ConstantItem(sharedPrefKey, isBackedUp = true, defaultValue, isBootAware)
332 
333         @JvmStatic
334         fun <T> backedUpItem(
335             sharedPrefKey: String,
336             type: Class<out T>,
337             isBootAware: Boolean = false,
338             defaultValueFromContext: (c: Context) -> T
339         ): ContextualItem<T> =
340             ContextualItem(
341                 sharedPrefKey,
342                 isBackedUp = true,
343                 defaultValueFromContext,
344                 isBootAware,
345                 type
346             )
347 
348         @VisibleForTesting
349         @JvmStatic
350         fun <T> nonRestorableItem(
351             sharedPrefKey: String,
352             defaultValue: T,
353             isBootAware: Boolean = false
354         ): ConstantItem<T> =
355             ConstantItem(sharedPrefKey, isBackedUp = false, defaultValue, isBootAware)
356 
357         @Deprecated("Don't use shared preferences directly. Use other LauncherPref methods.")
358         @JvmStatic
359         fun getPrefs(context: Context): SharedPreferences {
360             // Use application context for shared preferences, so we use single cached instance
361             return context.applicationContext.getSharedPreferences(
362                 SHARED_PREFERENCES_KEY,
363                 MODE_PRIVATE
364             )
365         }
366 
367         @Deprecated("Don't use shared preferences directly. Use other LauncherPref methods.")
368         @JvmStatic
369         fun getDevicePrefs(context: Context): SharedPreferences {
370             // Use application context for shared preferences, so we use a single cached instance
371             return context.applicationContext.getSharedPreferences(
372                 DEVICE_PREFERENCES_KEY,
373                 MODE_PRIVATE
374             )
375         }
376     }
377 }
378 
379 // This is hard-coded to false for now until it is time to release this optimization. It is only
380 // a var because the unit tests are setting this to true so they can run.
381 @VisibleForTesting var isBootAwareStartupDataEnabled: Boolean = false
382 
383 private val BOOT_AWARE_ITEMS: MutableSet<ConstantItem<*>> = mutableSetOf()
384 
385 abstract class Item {
386     abstract val sharedPrefKey: String
387     abstract val isBackedUp: Boolean
388     abstract val type: Class<*>
389     abstract val isBootAware: Boolean
390     val sharedPrefFile: String
391         get() = if (isBackedUp) SHARED_PREFERENCES_KEY else DEVICE_PREFERENCES_KEY
392 
tonull393     fun <T> to(value: T): Pair<Item, T> = Pair(this, value)
394 }
395 
396 data class ConstantItem<T>(
397     override val sharedPrefKey: String,
398     override val isBackedUp: Boolean,
399     val defaultValue: T,
400     override val isBootAware: Boolean,
401     // The default value can be null. If so, the type needs to be explicitly stated, or else NPE
402     override val type: Class<out T> = defaultValue!!::class.java
403 ) : Item() {
404     init {
405         if (isBootAware && isBootAwareStartupDataEnabled) {
406             BOOT_AWARE_ITEMS.add(this)
407         }
408     }
409 }
410 
411 data class ContextualItem<T>(
412     override val sharedPrefKey: String,
413     override val isBackedUp: Boolean,
414     private val defaultSupplier: (c: Context) -> T,
415     override val isBootAware: Boolean,
416     override val type: Class<out T>
417 ) : Item() {
418     private var default: T? = null
419 
defaultValueFromContextnull420     fun defaultValueFromContext(context: Context): T {
421         if (default == null) {
422             default = defaultSupplier(context)
423         }
424         return default!!
425     }
426 }
427