blob: 2e07e3022a7175273978f6d6198eb827929387de [file] [log] [blame]
package com.android.launcher3
import android.content.Context
import android.content.SharedPreferences
import android.content.SharedPreferences.OnSharedPreferenceChangeListener
import androidx.annotation.VisibleForTesting
import com.android.launcher3.allapps.WorkProfileManager
import com.android.launcher3.model.DeviceGridState
import com.android.launcher3.pm.InstallSessionHelper
import com.android.launcher3.provider.RestoreDbTask
import com.android.launcher3.states.RotationHelper
import com.android.launcher3.util.DisplayController
import com.android.launcher3.util.MainThreadInitializedObject
import com.android.launcher3.util.Themes
/**
* Use same context for shared preferences, so that we use a single cached instance
* TODO(b/262721340): Replace all direct SharedPreference refs with LauncherPrefs / Item methods.
*/
class LauncherPrefs(private val context: Context) {
/** Wrapper around `getInner` for a `ContextualItem` */
fun <T : Any> get(item: ContextualItem<T>): T =
getInner(item, item.defaultValueFromContext(context))
/** Wrapper around `getInner` for an `Item` */
fun <T : Any> get(item: ConstantItem<T>): T = getInner(item, item.defaultValue)
/**
* Retrieves the value for an [Item] from [SharedPreferences]. It handles method typing via the
* default value type, and will throw an error if the type of the item provided is not a
* `String`, `Boolean`, `Float`, `Int`, `Long`, or `Set<String>`.
*/
@Suppress("IMPLICIT_CAST_TO_ANY", "UNCHECKED_CAST")
private fun <T : Any> getInner(item: Item, default: T): T {
val sp = context.getSharedPreferences(item.sharedPrefFile, Context.MODE_PRIVATE)
return when (default::class.java) {
String::class.java -> sp.getString(item.sharedPrefKey, default as String)
Boolean::class.java,
java.lang.Boolean::class.java -> sp.getBoolean(item.sharedPrefKey, default as Boolean)
Int::class.java,
java.lang.Integer::class.java -> sp.getInt(item.sharedPrefKey, default as Int)
Float::class.java,
java.lang.Float::class.java -> sp.getFloat(item.sharedPrefKey, default as Float)
Long::class.java,
java.lang.Long::class.java -> sp.getLong(item.sharedPrefKey, default as Long)
Set::class.java -> sp.getStringSet(item.sharedPrefKey, default as Set<String>)
else ->
throw IllegalArgumentException(
"item type: ${default::class.java}" +
" is not compatible with sharedPref methods"
)
}
as T
}
/**
* Stores each of the values provided in `SharedPreferences` according to the configuration
* contained within the associated items provided. Internally, it uses apply, so the caller
* cannot assume that the values that have been put are immediately available for use.
*
* The forEach loop is necessary here since there is 1 `SharedPreference.Editor` returned from
* prepareToPutValue(itemsToValues) for every distinct `SharedPreferences` file present in the
* provided item configurations.
*/
fun put(vararg itemsToValues: Pair<Item, Any>): Unit =
prepareToPutValues(itemsToValues).forEach { it.apply() }
/**
* Stores the value provided in `SharedPreferences` according to the item configuration provided
* It is asynchronous, so the caller can't assume that the value put is immediately available.
*/
fun <T : Any> put(item: Item, value: T): Unit =
context
.getSharedPreferences(item.sharedPrefFile, Context.MODE_PRIVATE)
.edit()
.putValue(item, value)
.apply()
/**
* Synchronously stores all the values provided according to their associated Item
* configuration.
*/
fun putSync(vararg itemsToValues: Pair<Item, Any>): Unit =
prepareToPutValues(itemsToValues).forEach { it.commit() }
/**
* Update each shared preference file with the item - value pairs provided. This method is
* optimized to avoid retrieving the same shared preference file multiple times.
*
* @return `List<SharedPreferences.Editor>` 1 for each distinct shared preference file among the
* items given as part of the itemsToValues parameter
*/
private fun prepareToPutValues(
itemsToValues: Array<out Pair<Item, Any>>
): List<SharedPreferences.Editor> =
itemsToValues
.groupBy { it.first.sharedPrefFile }
.map { fileToItemValueList ->
context
.getSharedPreferences(fileToItemValueList.key, Context.MODE_PRIVATE)
.edit()
.apply {
fileToItemValueList.value.forEach { itemToValue ->
putValue(itemToValue.first, itemToValue.second)
}
}
}
/**
* Handles adding values to `SharedPreferences` regardless of type. This method is especially
* helpful for updating `SharedPreferences` values for `List<<Item>Any>` that have multiple
* types of Item values.
*/
@Suppress("UNCHECKED_CAST")
private fun SharedPreferences.Editor.putValue(
item: Item,
value: Any
): SharedPreferences.Editor =
when (value::class.java) {
String::class.java -> putString(item.sharedPrefKey, value as String)
Boolean::class.java,
java.lang.Boolean::class.java -> putBoolean(item.sharedPrefKey, value as Boolean)
Int::class.java,
java.lang.Integer::class.java -> putInt(item.sharedPrefKey, value as Int)
Float::class.java,
java.lang.Float::class.java -> putFloat(item.sharedPrefKey, value as Float)
Long::class.java,
java.lang.Long::class.java -> putLong(item.sharedPrefKey, value as Long)
Set::class.java -> putStringSet(item.sharedPrefKey, value as Set<String>)
else ->
throw IllegalArgumentException(
"item type: ${value::class} is not compatible with sharedPref methods"
)
}
/**
* After calling this method, the listener will be notified of any future updates to the
* `SharedPreferences` files associated with the provided list of items. The listener will need
* to filter update notifications so they don't activate for non-relevant updates.
*/
fun addListener(listener: OnSharedPreferenceChangeListener, vararg items: Item) {
items
.map { it.sharedPrefFile }
.distinct()
.forEach {
context
.getSharedPreferences(it, Context.MODE_PRIVATE)
.registerOnSharedPreferenceChangeListener(listener)
}
}
/**
* Stops the listener from getting notified of any more updates to any of the
* `SharedPreferences` files associated with any of the provided list of [Item].
*/
fun removeListener(listener: OnSharedPreferenceChangeListener, vararg items: Item) {
// If a listener is not registered to a SharedPreference, unregistering it does nothing
items
.map { it.sharedPrefFile }
.distinct()
.forEach {
context
.getSharedPreferences(it, Context.MODE_PRIVATE)
.unregisterOnSharedPreferenceChangeListener(listener)
}
}
/**
* Checks if all the provided [Item] have values stored in their corresponding
* `SharedPreferences` files.
*/
fun has(vararg items: Item): Boolean {
items
.groupBy { it.sharedPrefFile }
.forEach { (file, itemsSublist) ->
val prefs: SharedPreferences =
context.getSharedPreferences(file, Context.MODE_PRIVATE)
if (!itemsSublist.none { !prefs.contains(it.sharedPrefKey) }) return false
}
return true
}
/**
* Asynchronously removes the [Item]'s value from its corresponding `SharedPreferences` file.
*/
fun remove(vararg items: Item) = prepareToRemove(items).forEach { it.apply() }
/** Synchronously removes the [Item]'s value from its corresponding `SharedPreferences` file. */
fun removeSync(vararg items: Item) = prepareToRemove(items).forEach { it.commit() }
/**
* Creates `SharedPreferences.Editor` transactions for removing all the provided [Item] values
* from their respective `SharedPreferences` files. These returned `Editors` can then be
* committed or applied for synchronous or async behavior.
*/
private fun prepareToRemove(items: Array<out Item>): List<SharedPreferences.Editor> =
items
.groupBy { it.sharedPrefFile }
.map { (file, items) ->
context.getSharedPreferences(file, Context.MODE_PRIVATE).edit().also { editor ->
items.forEach { item -> editor.remove(item.sharedPrefKey) }
}
}
companion object {
@JvmField var INSTANCE = MainThreadInitializedObject { LauncherPrefs(it) }
@JvmStatic fun get(context: Context): LauncherPrefs = INSTANCE.get(context)
@JvmField val ICON_STATE = nonRestorableItem(LauncherAppState.KEY_ICON_STATE, "")
@JvmField val THEMED_ICONS = backedUpItem(Themes.KEY_THEMED_ICONS, false)
@JvmField val PROMISE_ICON_IDS = backedUpItem(InstallSessionHelper.PROMISE_ICON_IDS, "")
@JvmField val WORK_EDU_STEP = backedUpItem(WorkProfileManager.KEY_WORK_EDU_STEP, 0)
@JvmField val WORKSPACE_SIZE = backedUpItem(DeviceGridState.KEY_WORKSPACE_SIZE, "")
@JvmField val HOTSEAT_COUNT = backedUpItem(DeviceGridState.KEY_HOTSEAT_COUNT, -1)
@JvmField
val DEVICE_TYPE =
backedUpItem(DeviceGridState.KEY_DEVICE_TYPE, InvariantDeviceProfile.TYPE_PHONE)
@JvmField val DB_FILE = backedUpItem(DeviceGridState.KEY_DB_FILE, "")
@JvmField
val RESTORE_DEVICE =
backedUpItem(RestoreDbTask.RESTORED_DEVICE_TYPE, InvariantDeviceProfile.TYPE_PHONE)
@JvmField val APP_WIDGET_IDS = backedUpItem(RestoreDbTask.APPWIDGET_IDS, "")
@JvmField val OLD_APP_WIDGET_IDS = backedUpItem(RestoreDbTask.APPWIDGET_OLD_IDS, "")
@JvmField
val ALLOW_ROTATION =
backedUpItem(RotationHelper.ALLOW_ROTATION_PREFERENCE_KEY) {
RotationHelper.getAllowRotationDefaultValue(DisplayController.INSTANCE.get(it).info)
}
@VisibleForTesting
@JvmStatic
fun <T> backedUpItem(sharedPrefKey: String, defaultValue: T): ConstantItem<T> =
ConstantItem(sharedPrefKey, LauncherFiles.SHARED_PREFERENCES_KEY, defaultValue)
@JvmStatic
fun <T> backedUpItem(
sharedPrefKey: String,
defaultValueFromContext: (c: Context) -> T
): ContextualItem<T> =
ContextualItem(
sharedPrefKey,
LauncherFiles.SHARED_PREFERENCES_KEY,
defaultValueFromContext
)
@VisibleForTesting
@JvmStatic
fun <T> nonRestorableItem(sharedPrefKey: String, defaultValue: T): ConstantItem<T> =
ConstantItem(sharedPrefKey, LauncherFiles.DEVICE_PREFERENCES_KEY, defaultValue)
@Deprecated("Don't use shared preferences directly. Use other LauncherPref methods.")
@JvmStatic
fun getPrefs(context: Context): SharedPreferences {
// Use application context for shared preferences, so we use single cached instance
return context.applicationContext.getSharedPreferences(
LauncherFiles.SHARED_PREFERENCES_KEY,
Context.MODE_PRIVATE
)
}
@Deprecated("Don't use shared preferences directly. Use other LauncherPref methods.")
@JvmStatic
fun getDevicePrefs(context: Context): SharedPreferences {
// Use application context for shared preferences, so we use a single cached instance
return context.applicationContext.getSharedPreferences(
LauncherFiles.DEVICE_PREFERENCES_KEY,
Context.MODE_PRIVATE
)
}
}
}
abstract class Item {
abstract val sharedPrefKey: String
abstract val sharedPrefFile: String
fun <T> to(value: T): Pair<Item, T> = Pair(this, value)
}
data class ConstantItem<T>(
override val sharedPrefKey: String,
override val sharedPrefFile: String,
val defaultValue: T
) : Item()
data class ContextualItem<T>(
override val sharedPrefKey: String,
override val sharedPrefFile: String,
private val defaultSupplier: (c: Context) -> T
) : Item() {
private var default: T? = null
fun defaultValueFromContext(context: Context): T {
if (default == null) {
default = defaultSupplier(context)
}
return default!!
}
}