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,