From d1b33b311c0028bc21e8c00c0866791860cddf40 Mon Sep 17 00:00:00 2001 From: Stefan Andonian Date: Fri, 16 Dec 2022 21:22:27 +0000 Subject: [PATCH] Expanded LauncherPrefs APIs to Replace Direct Shared Preference Usage. LauncherPrefs will contain Launcher's shared preference functionality. It controls optimizations and classifications such as restorable vs non-restorable data, bootaware vs non-bootaware data, and configurations such as default values so the calling code doesn't need to and our code base can have a single source of truth for items that are used in multiple places. The old APIs remain in place, but are deprecated and will be removed after all Shared Preference usage has been gated by LauncherPrefs in future CLs. Bug: 261635315 Test: Manually tested themed icon, Workspace configuration, and app install functionality. Change-Id: I29fd516468bc93fda393062e95be26b6d55c816e --- .../logging/SettingsChangeLogger.java | 4 +- .../android/launcher3/LauncherAppState.java | 14 +- src/com/android/launcher3/LauncherPrefs.kt | 261 +++++++++++++++++- .../allapps/BaseAllAppsContainerView.java | 7 +- .../launcher3/allapps/WorkEduCard.java | 4 +- .../launcher3/allapps/WorkProfileManager.java | 9 +- .../graphics/GridCustomizationsProvider.java | 9 +- .../launcher3/model/DeviceGridState.java | 27 +- .../launcher3/pm/InstallSessionHelper.java | 12 +- .../launcher3/provider/RestoreDbTask.java | 41 ++- src/com/android/launcher3/util/Themes.java | 4 +- .../android/launcher3/LauncherPrefsTest.kt | 148 ++++++++++ .../model/GridSizeMigrationUtilTest.kt | 9 +- .../launcher3/util/LauncherModelHelper.java | 3 +- 14 files changed, 462 insertions(+), 90 deletions(-) create mode 100644 tests/src/com/android/launcher3/LauncherPrefsTest.kt diff --git a/quickstep/src/com/android/quickstep/logging/SettingsChangeLogger.java b/quickstep/src/com/android/quickstep/logging/SettingsChangeLogger.java index 5efc45e385..3d5c143413 100644 --- a/quickstep/src/com/android/quickstep/logging/SettingsChangeLogger.java +++ b/quickstep/src/com/android/quickstep/logging/SettingsChangeLogger.java @@ -16,6 +16,7 @@ package com.android.quickstep.logging; +import static com.android.launcher3.LauncherPrefs.THEMED_ICONS; import static com.android.launcher3.LauncherPrefs.getDevicePrefs; import static com.android.launcher3.LauncherPrefs.getPrefs; import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_HOME_SCREEN_SUGGESTIONS_DISABLED; @@ -39,6 +40,7 @@ import android.util.Log; import android.util.Xml; import com.android.launcher3.AutoInstallsLayout; +import com.android.launcher3.LauncherPrefs; import com.android.launcher3.R; import com.android.launcher3.logging.InstanceId; import com.android.launcher3.logging.StatsLogManager; @@ -178,7 +180,7 @@ public class SettingsChangeLogger implements logger::log); SharedPreferences prefs = getPrefs(mContext); - logger.log(prefs.getBoolean(KEY_THEMED_ICONS, false) + logger.log(LauncherPrefs.get(mContext).get(THEMED_ICONS) ? LAUNCHER_THEMED_ICON_ENABLED : LAUNCHER_THEMED_ICON_DISABLED); diff --git a/src/com/android/launcher3/LauncherAppState.java b/src/com/android/launcher3/LauncherAppState.java index 49659364b9..3461601f1d 100644 --- a/src/com/android/launcher3/LauncherAppState.java +++ b/src/com/android/launcher3/LauncherAppState.java @@ -18,7 +18,8 @@ package com.android.launcher3; import static android.app.admin.DevicePolicyManager.ACTION_DEVICE_POLICY_RESOURCE_UPDATED; -import static com.android.launcher3.LauncherPrefs.getDevicePrefs; +import static com.android.launcher3.LauncherPrefs.ICON_STATE; +import static com.android.launcher3.LauncherPrefs.THEMED_ICONS; import static com.android.launcher3.util.Executors.MODEL_EXECUTOR; import static com.android.launcher3.util.SettingsCache.NOTIFICATION_BADGING_URI; @@ -55,7 +56,7 @@ import com.android.launcher3.widget.custom.CustomWidgetManager; public class LauncherAppState implements SafeCloseable { public static final String ACTION_FORCE_ROLOAD = "force-reload-launcher"; - private static final String KEY_ICON_STATE = "pref_icon_shape_path"; + public static final String KEY_ICON_STATE = "pref_icon_shape_path"; // We do not need any synchronization for this variable as its only written on UI thread. public static final MainThreadInitializedObject INSTANCE = @@ -117,10 +118,9 @@ public class LauncherAppState implements SafeCloseable { observer, MODEL_EXECUTOR.getHandler()); mOnTerminateCallback.add(iconChangeTracker::close); MODEL_EXECUTOR.execute(observer::verifyIconChanged); - SharedPreferences prefs = LauncherPrefs.getPrefs(mContext); - prefs.registerOnSharedPreferenceChangeListener(observer); + LauncherPrefs.get(context).addListener(observer, THEMED_ICONS); mOnTerminateCallback.add( - () -> prefs.unregisterOnSharedPreferenceChangeListener(observer)); + () -> LauncherPrefs.get(mContext).removeListener(observer, THEMED_ICONS)); InstallSessionTracker installSessionTracker = InstallSessionHelper.INSTANCE.get(context).registerInstallTracker(mModel); @@ -207,12 +207,12 @@ public class LauncherAppState implements SafeCloseable { public void onSystemIconStateChanged(String iconState) { IconShape.init(mContext); refreshAndReloadLauncher(); - getDevicePrefs(mContext).edit().putString(KEY_ICON_STATE, iconState).apply(); + LauncherPrefs.get(mContext).put(ICON_STATE, iconState); } void verifyIconChanged() { String iconState = mIconProvider.getSystemIconState(); - if (!iconState.equals(getDevicePrefs(mContext).getString(KEY_ICON_STATE, ""))) { + if (!iconState.equals(LauncherPrefs.get(mContext).get(ICON_STATE))) { onSystemIconStateChanged(iconState); } } diff --git a/src/com/android/launcher3/LauncherPrefs.kt b/src/com/android/launcher3/LauncherPrefs.kt index 0d6ed04f2a..1fb2dce254 100644 --- a/src/com/android/launcher3/LauncherPrefs.kt +++ b/src/com/android/launcher3/LauncherPrefs.kt @@ -2,24 +2,255 @@ 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.util.MainThreadInitializedObject +import com.android.launcher3.util.Themes -object LauncherPrefs { +/** + * 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) { - @JvmStatic - fun getPrefs(context: Context): SharedPreferences { - // Use application context for shared preferences, so that we use a single cached instance - return context.applicationContext.getSharedPreferences( - LauncherFiles.SHARED_PREFERENCES_KEY, - Context.MODE_PRIVATE - ) + /** + * 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`. + */ + @Suppress("IMPLICIT_CAST_TO_ANY", "UNCHECKED_CAST") + fun get(item: Item): T { + val sp = context.getSharedPreferences(item.sharedPrefFile, Context.MODE_PRIVATE) + + return when (item.defaultValue::class.java) { + String::class.java -> sp.getString(item.sharedPrefKey, item.defaultValue as String) + Boolean::class.java, + java.lang.Boolean::class.java -> + sp.getBoolean(item.sharedPrefKey, item.defaultValue as Boolean) + Int::class.java, + java.lang.Integer::class.java -> sp.getInt(item.sharedPrefKey, item.defaultValue as Int) + Float::class.java, + java.lang.Float::class.java -> + sp.getFloat(item.sharedPrefKey, item.defaultValue as Float) + Long::class.java, + java.lang.Long::class.java -> sp.getLong(item.sharedPrefKey, item.defaultValue as Long) + Set::class.java -> sp.getStringSet(item.sharedPrefKey, item.defaultValue as Set) + else -> + throw IllegalArgumentException( + "item type: ${item.defaultValue::class.java}" + + " is not compatible with sharedPref methods" + ) + } + as T } - @JvmStatic - fun getDevicePrefs(context: Context): SharedPreferences { - // Use application context for shared preferences, so that we use a single cached instance - return context.applicationContext.getSharedPreferences( - LauncherFiles.DEVICE_PREFERENCES_KEY, - Context.MODE_PRIVATE - ) + /** + * 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, 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 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, 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` 1 for each distinct shared preference file among the + * items given as part of the itemsToValues parameter + */ + private fun prepareToPutValues( + itemsToValues: Array, Any>> + ): List = + 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<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) + else -> + throw IllegalArgumentException( + "item type: " + + "${item.defaultValue!!::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>): List = + 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, "") + + @VisibleForTesting + @JvmStatic + fun backedUpItem(sharedPrefKey: String, defaultValue: T): Item = + Item(sharedPrefKey, LauncherFiles.SHARED_PREFERENCES_KEY, defaultValue) + + @VisibleForTesting + @JvmStatic + fun nonRestorableItem(sharedPrefKey: String, defaultValue: T): Item = + Item(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 + ) + } } } + +data class Item(val sharedPrefKey: String, val sharedPrefFile: String, val defaultValue: T) { + fun to(value: T): Pair, T> = Pair(this, value) +} diff --git a/src/com/android/launcher3/allapps/BaseAllAppsContainerView.java b/src/com/android/launcher3/allapps/BaseAllAppsContainerView.java index 487807751d..1c676915fd 100644 --- a/src/com/android/launcher3/allapps/BaseAllAppsContainerView.java +++ b/src/com/android/launcher3/allapps/BaseAllAppsContainerView.java @@ -57,7 +57,6 @@ import com.android.launcher3.DragSource; import com.android.launcher3.DropTarget.DragObject; import com.android.launcher3.Insettable; import com.android.launcher3.InsettableFrameLayout; -import com.android.launcher3.LauncherPrefs; import com.android.launcher3.R; import com.android.launcher3.Utilities; import com.android.launcher3.allapps.search.DefaultSearchAdapterProvider; @@ -161,10 +160,8 @@ public abstract class BaseAllAppsContainerView allApps, SharedPreferences prefs, + UserManager userManager, BaseAllAppsContainerView allApps, StatsLogManager statsLogManager) { mUserManager = userManager; mAllApps = allApps; - mPreferences = prefs; mMatcher = mAllApps.mPersonalMatcher.negate(); mStatsLogManager = statsLogManager; } @@ -225,7 +224,7 @@ public class WorkProfileManager implements PersonalWorkSlidingTabStrip.OnActiveP } private boolean isEduSeen() { - return mPreferences.getInt(KEY_WORK_EDU_STEP, 0) != 0; + return LauncherPrefs.get(mAllApps.getContext()).get(WORK_EDU_STEP) != 0; } private void onWorkFabClicked(View view) { diff --git a/src/com/android/launcher3/graphics/GridCustomizationsProvider.java b/src/com/android/launcher3/graphics/GridCustomizationsProvider.java index feadafaf50..9426c22509 100644 --- a/src/com/android/launcher3/graphics/GridCustomizationsProvider.java +++ b/src/com/android/launcher3/graphics/GridCustomizationsProvider.java @@ -1,8 +1,7 @@ package com.android.launcher3.graphics; -import static com.android.launcher3.LauncherPrefs.getPrefs; +import static com.android.launcher3.LauncherPrefs.THEMED_ICONS; import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR; -import static com.android.launcher3.util.Themes.KEY_THEMED_ICONS; import static com.android.launcher3.util.Themes.isThemedIconEnabled; import android.annotation.TargetApi; @@ -25,6 +24,7 @@ import android.util.Log; import com.android.launcher3.InvariantDeviceProfile; import com.android.launcher3.InvariantDeviceProfile.GridOption; +import com.android.launcher3.LauncherPrefs; import com.android.launcher3.Utilities; import com.android.launcher3.util.Executors; @@ -142,9 +142,8 @@ public class GridCustomizationsProvider extends ContentProvider { } case ICON_THEMED: case SET_ICON_THEMED: { - getPrefs(getContext()).edit() - .putBoolean(KEY_THEMED_ICONS, values.getAsBoolean(BOOLEAN_VALUE)) - .apply(); + LauncherPrefs.get(getContext()) + .put(THEMED_ICONS, values.getAsBoolean(BOOLEAN_VALUE)); return 1; } default: diff --git a/src/com/android/launcher3/model/DeviceGridState.java b/src/com/android/launcher3/model/DeviceGridState.java index 85d54c0d1c..edc8c1bcd9 100644 --- a/src/com/android/launcher3/model/DeviceGridState.java +++ b/src/com/android/launcher3/model/DeviceGridState.java @@ -17,7 +17,10 @@ package com.android.launcher3.model; import static com.android.launcher3.InvariantDeviceProfile.DeviceType; -import static com.android.launcher3.InvariantDeviceProfile.TYPE_PHONE; +import static com.android.launcher3.LauncherPrefs.DB_FILE; +import static com.android.launcher3.LauncherPrefs.DEVICE_TYPE; +import static com.android.launcher3.LauncherPrefs.HOTSEAT_COUNT; +import static com.android.launcher3.LauncherPrefs.WORKSPACE_SIZE; import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_GRID_SIZE_2; import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_GRID_SIZE_3; import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_GRID_SIZE_4; @@ -25,7 +28,6 @@ import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCH import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_GRID_SIZE_6; import android.content.Context; -import android.content.SharedPreferences; import android.text.TextUtils; import com.android.launcher3.InvariantDeviceProfile; @@ -58,11 +60,11 @@ public class DeviceGridState implements Comparable { } public DeviceGridState(Context context) { - SharedPreferences prefs = LauncherPrefs.getPrefs(context); - mGridSizeString = prefs.getString(KEY_WORKSPACE_SIZE, ""); - mNumHotseat = prefs.getInt(KEY_HOTSEAT_COUNT, -1); - mDeviceType = prefs.getInt(KEY_DEVICE_TYPE, TYPE_PHONE); - mDbFile = prefs.getString(KEY_DB_FILE, ""); + LauncherPrefs lp = LauncherPrefs.get(context); + mGridSizeString = lp.get(WORKSPACE_SIZE); + mNumHotseat = lp.get(HOTSEAT_COUNT); + mDeviceType = lp.get(DEVICE_TYPE); + mDbFile = lp.get(DB_FILE); } /** @@ -90,12 +92,11 @@ public class DeviceGridState implements Comparable { * Stores the device state to shared preferences */ public void writeToPrefs(Context context) { - LauncherPrefs.getPrefs(context).edit() - .putString(KEY_WORKSPACE_SIZE, mGridSizeString) - .putInt(KEY_HOTSEAT_COUNT, mNumHotseat) - .putInt(KEY_DEVICE_TYPE, mDeviceType) - .putString(KEY_DB_FILE, mDbFile) - .apply(); + LauncherPrefs.get(context).put( + WORKSPACE_SIZE.to(mGridSizeString), + HOTSEAT_COUNT.to(mNumHotseat), + DEVICE_TYPE.to(mDeviceType), + DB_FILE.to(mDbFile)); } /** diff --git a/src/com/android/launcher3/pm/InstallSessionHelper.java b/src/com/android/launcher3/pm/InstallSessionHelper.java index 150bca4058..db235668db 100644 --- a/src/com/android/launcher3/pm/InstallSessionHelper.java +++ b/src/com/android/launcher3/pm/InstallSessionHelper.java @@ -16,8 +16,6 @@ package com.android.launcher3.pm; -import static com.android.launcher3.LauncherPrefs.getPrefs; - import android.content.Context; import android.content.pm.ApplicationInfo; import android.content.pm.LauncherApps; @@ -34,6 +32,7 @@ import androidx.annotation.Nullable; import androidx.annotation.RequiresApi; import androidx.annotation.WorkerThread; +import com.android.launcher3.LauncherPrefs; import com.android.launcher3.LauncherSettings; import com.android.launcher3.SessionCommitReceiver; import com.android.launcher3.Utilities; @@ -65,7 +64,7 @@ public class InstallSessionHelper { // Set of session ids of promise icons that have been added to the home screen // as FLAG_PROMISE_NEW_INSTALLS. @NonNull - protected static final String PROMISE_ICON_IDS = "promise_icon_ids"; + public static final String PROMISE_ICON_IDS = "promise_icon_ids"; private static final boolean DEBUG = false; @@ -102,7 +101,7 @@ public class InstallSessionHelper { return mPromiseIconIds; } mPromiseIconIds = IntSet.wrap(IntArray.fromConcatString( - getPrefs(mAppContext).getString(PROMISE_ICON_IDS, ""))); + LauncherPrefs.get(mAppContext).get(LauncherPrefs.PROMISE_ICON_IDS))); IntArray existingIds = new IntArray(); for (SessionInfo info : getActiveSessions().values()) { @@ -146,9 +145,8 @@ public class InstallSessionHelper { } private void updatePromiseIconPrefs() { - getPrefs(mAppContext).edit() - .putString(PROMISE_ICON_IDS, getPromiseIconIds().getArray().toConcatString()) - .apply(); + LauncherPrefs.get(mAppContext).put(LauncherPrefs.PROMISE_ICON_IDS, + getPromiseIconIds().getArray().toConcatString()); } @Nullable diff --git a/src/com/android/launcher3/provider/RestoreDbTask.java b/src/com/android/launcher3/provider/RestoreDbTask.java index 5e97b2dedd..2a452be8dd 100644 --- a/src/com/android/launcher3/provider/RestoreDbTask.java +++ b/src/com/android/launcher3/provider/RestoreDbTask.java @@ -17,13 +17,14 @@ package com.android.launcher3.provider; import static com.android.launcher3.InvariantDeviceProfile.TYPE_MULTI_DISPLAY; -import static com.android.launcher3.InvariantDeviceProfile.TYPE_PHONE; +import static com.android.launcher3.LauncherPrefs.APP_WIDGET_IDS; +import static com.android.launcher3.LauncherPrefs.OLD_APP_WIDGET_IDS; +import static com.android.launcher3.LauncherPrefs.RESTORE_DEVICE; import static com.android.launcher3.provider.LauncherDbUtils.dropTable; import android.app.backup.BackupManager; import android.content.ContentValues; import android.content.Context; -import android.content.SharedPreferences; import android.database.Cursor; import android.database.sqlite.SQLiteDatabase; import android.os.UserHandle; @@ -62,13 +63,13 @@ import java.util.Arrays; public class RestoreDbTask { private static final String TAG = "RestoreDbTask"; - private static final String RESTORED_DEVICE_TYPE = "restored_task_pending"; + public static final String RESTORED_DEVICE_TYPE = "restored_task_pending"; private static final String INFO_COLUMN_NAME = "name"; private static final String INFO_COLUMN_DEFAULT_VALUE = "dflt_value"; - private static final String APPWIDGET_OLD_IDS = "appwidget_old_ids"; - private static final String APPWIDGET_IDS = "appwidget_ids"; + public static final String APPWIDGET_OLD_IDS = "appwidget_old_ids"; + public static final String APPWIDGET_IDS = "appwidget_ids"; /** * Tries to restore the backup DB if needed @@ -87,7 +88,7 @@ public class RestoreDbTask { // Set is pending to false irrespective of the result, so that it doesn't get // executed again. - LauncherPrefs.getPrefs(context).edit().remove(RESTORED_DEVICE_TYPE).commit(); + LauncherPrefs.get(context).removeSync(RESTORE_DEVICE); idp.reinitializeAfterRestore(context); } @@ -240,8 +241,7 @@ public class RestoreDbTask { } // If restored from a single display backup, remove gaps between screenIds - if (LauncherPrefs.getPrefs(context).getInt(RESTORED_DEVICE_TYPE, TYPE_PHONE) - != TYPE_MULTI_DISPLAY) { + if (LauncherPrefs.get(context).get(RESTORE_DEVICE) != TYPE_MULTI_DISPLAY) { removeScreenIdGaps(db); } @@ -339,7 +339,7 @@ public class RestoreDbTask { } public static boolean isPending(Context context) { - return LauncherPrefs.getPrefs(context).contains(RESTORED_DEVICE_TYPE); + return LauncherPrefs.get(context).has(RESTORE_DEVICE); } /** @@ -347,34 +347,31 @@ public class RestoreDbTask { */ public static void setPending(Context context) { FileLog.d(TAG, "Restore data received through full backup "); - LauncherPrefs.getPrefs(context).edit() - .putInt(RESTORED_DEVICE_TYPE, new DeviceGridState(context).getDeviceType()) - .commit(); + LauncherPrefs.get(context) + .putSync(RESTORE_DEVICE.to(new DeviceGridState(context).getDeviceType())); } private void restoreAppWidgetIdsIfExists(Context context) { - SharedPreferences prefs = LauncherPrefs.getPrefs(context); - if (prefs.contains(APPWIDGET_OLD_IDS) && prefs.contains(APPWIDGET_IDS)) { + LauncherPrefs lp = LauncherPrefs.get(context); + if (lp.has(APP_WIDGET_IDS, OLD_APP_WIDGET_IDS)) { LauncherWidgetHolder holder = LauncherWidgetHolder.newInstance(context); AppWidgetsRestoredReceiver.restoreAppWidgetIds(context, - IntArray.fromConcatString(prefs.getString(APPWIDGET_OLD_IDS, "")).toArray(), - IntArray.fromConcatString(prefs.getString(APPWIDGET_IDS, "")).toArray(), + IntArray.fromConcatString(lp.get(OLD_APP_WIDGET_IDS)).toArray(), + IntArray.fromConcatString(lp.get(APP_WIDGET_IDS)).toArray(), holder); holder.destroy(); } else { FileLog.d(TAG, "No app widget ids to restore."); } - prefs.edit().remove(APPWIDGET_OLD_IDS) - .remove(APPWIDGET_IDS).apply(); + lp.remove(APP_WIDGET_IDS, OLD_APP_WIDGET_IDS); } public static void setRestoredAppWidgetIds(Context context, @NonNull int[] oldIds, @NonNull int[] newIds) { - LauncherPrefs.getPrefs(context).edit() - .putString(APPWIDGET_OLD_IDS, IntArray.wrap(oldIds).toConcatString()) - .putString(APPWIDGET_IDS, IntArray.wrap(newIds).toConcatString()) - .commit(); + LauncherPrefs.get(context).putSync( + OLD_APP_WIDGET_IDS.to(IntArray.wrap(oldIds).toConcatString()), + APP_WIDGET_IDS.to(IntArray.wrap(newIds).toConcatString())); } } diff --git a/src/com/android/launcher3/util/Themes.java b/src/com/android/launcher3/util/Themes.java index 585bea93ad..5526839ce7 100644 --- a/src/com/android/launcher3/util/Themes.java +++ b/src/com/android/launcher3/util/Themes.java @@ -19,6 +19,8 @@ package com.android.launcher3.util; import static android.app.WallpaperColors.HINT_SUPPORTS_DARK_TEXT; import static android.app.WallpaperColors.HINT_SUPPORTS_DARK_THEME; +import static com.android.launcher3.LauncherPrefs.THEMED_ICONS; + import android.app.WallpaperColors; import android.app.WallpaperManager; import android.content.Context; @@ -74,7 +76,7 @@ public class Themes { * Returns true if workspace icon theming is enabled */ public static boolean isThemedIconEnabled(Context context) { - return LauncherPrefs.getPrefs(context).getBoolean(KEY_THEMED_ICONS, false); + return LauncherPrefs.get(context).get(THEMED_ICONS); } public static String getDefaultBodyFont(Context context) { diff --git a/tests/src/com/android/launcher3/LauncherPrefsTest.kt b/tests/src/com/android/launcher3/LauncherPrefsTest.kt new file mode 100644 index 0000000000..151abf1040 --- /dev/null +++ b/tests/src/com/android/launcher3/LauncherPrefsTest.kt @@ -0,0 +1,148 @@ +package com.android.launcher3 + +import android.content.SharedPreferences.OnSharedPreferenceChangeListener +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import androidx.test.platform.app.InstrumentationRegistry +import com.google.common.truth.Truth.assertThat +import java.util.concurrent.CountDownLatch +import java.util.concurrent.TimeUnit +import org.junit.Test +import org.junit.runner.RunWith + +private val TEST_BOOLEAN_ITEM = LauncherPrefs.nonRestorableItem("1", false) +private val TEST_STRING_ITEM = LauncherPrefs.nonRestorableItem("2", "( ͡❛ ͜ʖ ͡❛)") +private val TEST_INT_ITEM = LauncherPrefs.nonRestorableItem("3", -1) + +@SmallTest +@RunWith(AndroidJUnit4::class) +class LauncherPrefsTest { + + private val launcherPrefs by lazy { + LauncherPrefs.get(InstrumentationRegistry.getInstrumentation().targetContext).apply { + remove(TEST_BOOLEAN_ITEM, TEST_STRING_ITEM, TEST_INT_ITEM) + } + } + + @Test + fun has_keyMissingFromLauncherPrefs_returnsFalse() { + assertThat(launcherPrefs.has(TEST_BOOLEAN_ITEM)).isFalse() + } + + @Test + fun has_keyPresentInLauncherPrefs_returnsTrue() { + with(launcherPrefs) { + putSync(TEST_BOOLEAN_ITEM.to(TEST_BOOLEAN_ITEM.defaultValue)) + assertThat(has(TEST_BOOLEAN_ITEM)).isTrue() + remove(TEST_BOOLEAN_ITEM) + } + } + + @Test + fun addListener_listeningForStringItemUpdates_isCorrectlyNotifiedOfUpdates() { + val latch = CountDownLatch(1) + val listener = OnSharedPreferenceChangeListener { _, _ -> latch.countDown() } + + with(launcherPrefs) { + putSync(TEST_STRING_ITEM.to(TEST_STRING_ITEM.defaultValue)) + addListener(listener, TEST_STRING_ITEM) + putSync(TEST_STRING_ITEM.to(TEST_STRING_ITEM.defaultValue + "abc")) + + assertThat(latch.await(2, TimeUnit.SECONDS)).isTrue() + remove(TEST_STRING_ITEM) + } + } + + @Test + fun removeListener_previouslyListeningForStringItemUpdates_isNoLongerNotifiedOfUpdates() { + val latch = CountDownLatch(1) + val listener = OnSharedPreferenceChangeListener { _, _ -> latch.countDown() } + + with(launcherPrefs) { + addListener(listener, TEST_STRING_ITEM) + removeListener(listener, TEST_STRING_ITEM) + putSync(TEST_STRING_ITEM.to(TEST_STRING_ITEM.defaultValue + "hello.")) + + // latch will be still be 1 (and await will return false) if the listener was not called + assertThat(latch.await(2, TimeUnit.SECONDS)).isFalse() + remove(TEST_STRING_ITEM) + } + } + + @Test + fun addListenerAndRemoveListener_forMultipleItems_bothWorkProperly() { + var latch = CountDownLatch(3) + val listener = OnSharedPreferenceChangeListener { _, _ -> latch.countDown() } + + with(launcherPrefs) { + addListener(listener, TEST_INT_ITEM, TEST_STRING_ITEM, TEST_BOOLEAN_ITEM) + putSync( + TEST_INT_ITEM.to(TEST_INT_ITEM.defaultValue + 123), + TEST_STRING_ITEM.to(TEST_STRING_ITEM.defaultValue + "abc"), + TEST_BOOLEAN_ITEM.to(!TEST_BOOLEAN_ITEM.defaultValue) + ) + assertThat(latch.await(2, TimeUnit.SECONDS)).isTrue() + + removeListener(listener, TEST_INT_ITEM, TEST_STRING_ITEM, TEST_BOOLEAN_ITEM) + latch = CountDownLatch(1) + putSync( + TEST_INT_ITEM.to(TEST_INT_ITEM.defaultValue), + TEST_STRING_ITEM.to(TEST_STRING_ITEM.defaultValue), + TEST_BOOLEAN_ITEM.to(TEST_BOOLEAN_ITEM.defaultValue) + ) + remove(TEST_INT_ITEM, TEST_STRING_ITEM, TEST_BOOLEAN_ITEM) + + assertThat(latch.await(2, TimeUnit.SECONDS)).isFalse() + } + } + + @Test + fun get_booleanItemNotInLauncherprefs_returnsDefaultValue() { + assertThat(launcherPrefs.get(TEST_BOOLEAN_ITEM)).isEqualTo(TEST_BOOLEAN_ITEM.defaultValue) + } + + @Test + fun get_stringItemNotInLauncherPrefs_returnsDefaultValue() { + assertThat(launcherPrefs.get(TEST_STRING_ITEM)).isEqualTo(TEST_STRING_ITEM.defaultValue) + } + + @Test + fun get_intItemNotInLauncherprefs_returnsDefaultValue() { + assertThat(launcherPrefs.get(TEST_INT_ITEM)).isEqualTo(TEST_INT_ITEM.defaultValue) + } + + @Test + fun put_storesItemInLauncherPrefs_successfully() { + val notDefaultValue = !TEST_BOOLEAN_ITEM.defaultValue + + with(launcherPrefs) { + putSync(TEST_BOOLEAN_ITEM.to(notDefaultValue)) + assertThat(get(TEST_BOOLEAN_ITEM)).isEqualTo(notDefaultValue) + remove(TEST_BOOLEAN_ITEM) + } + } + + @Test + fun put_storesListOfItemsInLauncherPrefs_successfully() { + with(launcherPrefs) { + putSync( + TEST_STRING_ITEM.to(TEST_STRING_ITEM.defaultValue), + TEST_INT_ITEM.to(TEST_INT_ITEM.defaultValue), + TEST_BOOLEAN_ITEM.to(TEST_BOOLEAN_ITEM.defaultValue) + ) + assertThat(has(TEST_BOOLEAN_ITEM, TEST_INT_ITEM, TEST_STRING_ITEM)).isTrue() + remove(TEST_STRING_ITEM, TEST_INT_ITEM, TEST_BOOLEAN_ITEM) + } + } + + @Test + fun remove_deletesItemFromLauncherPrefs_successfully() { + val notDefaultValue = !TEST_BOOLEAN_ITEM.defaultValue + + with(launcherPrefs) { + putSync(TEST_BOOLEAN_ITEM.to(notDefaultValue)) + remove(TEST_BOOLEAN_ITEM) + assertThat(get(TEST_BOOLEAN_ITEM)).isEqualTo(TEST_BOOLEAN_ITEM.defaultValue) + } + } +} diff --git a/tests/src/com/android/launcher3/model/GridSizeMigrationUtilTest.kt b/tests/src/com/android/launcher3/model/GridSizeMigrationUtilTest.kt index dcc8ec732a..a85fa3a4d1 100644 --- a/tests/src/com/android/launcher3/model/GridSizeMigrationUtilTest.kt +++ b/tests/src/com/android/launcher3/model/GridSizeMigrationUtilTest.kt @@ -24,7 +24,8 @@ import android.os.Process import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.launcher3.InvariantDeviceProfile -import com.android.launcher3.LauncherFiles +import com.android.launcher3.LauncherPrefs +import com.android.launcher3.LauncherPrefs.Companion.WORKSPACE_SIZE import com.android.launcher3.LauncherSettings.Favorites.* import com.android.launcher3.config.FeatureFlags import com.android.launcher3.model.GridSizeMigrationUtil.DbReader @@ -754,11 +755,7 @@ class GridSizeMigrationUtilTest { .edit() .putBoolean(FeatureFlags.ENABLE_NEW_MIGRATION_LOGIC.key, true) .commit() - context - .getSharedPreferences(LauncherFiles.SHARED_PREFERENCES_KEY, Context.MODE_PRIVATE) - .edit() - .putString(DeviceGridState.KEY_WORKSPACE_SIZE, srcGridSize) - .commit() + LauncherPrefs.get(context).putSync(WORKSPACE_SIZE.to(srcGridSize)) FeatureFlags.initialize(context) } diff --git a/tests/src/com/android/launcher3/util/LauncherModelHelper.java b/tests/src/com/android/launcher3/util/LauncherModelHelper.java index 93bf31256d..caec301a9f 100644 --- a/tests/src/com/android/launcher3/util/LauncherModelHelper.java +++ b/tests/src/com/android/launcher3/util/LauncherModelHelper.java @@ -55,6 +55,7 @@ import com.android.launcher3.InvariantDeviceProfile; import com.android.launcher3.LauncherAppState; import com.android.launcher3.LauncherModel; import com.android.launcher3.LauncherModel.ModelUpdateTask; +import com.android.launcher3.LauncherPrefs; import com.android.launcher3.LauncherProvider; import com.android.launcher3.LauncherSettings; import com.android.launcher3.model.AllAppsList; @@ -506,7 +507,7 @@ public class LauncherModelHelper { SanboxModelContext() { super(ApplicationProvider.getApplicationContext(), - UserCache.INSTANCE, InstallSessionHelper.INSTANCE, + UserCache.INSTANCE, InstallSessionHelper.INSTANCE, LauncherPrefs.INSTANCE, LauncherAppState.INSTANCE, InvariantDeviceProfile.INSTANCE, DisplayController.INSTANCE, CustomWidgetManager.INSTANCE, SettingsCache.INSTANCE, PluginManagerWrapper.INSTANCE,