From 5bd9a225a8c4a21afd88dda28ea643c22d232082 Mon Sep 17 00:00:00 2001 From: Stefan Andonian Date: Thu, 23 Feb 2023 00:58:33 +0000 Subject: [PATCH] Preload expensive objects before user is unlocked. This change includes migrating data required for starting the launcher from encrypted storage into device protected storage. All of the data being moved has already been approved by the correct authorities. Bug: 251502424 Test: Performed latency testing using logs from user unlock until first workspace screen loaded. Change-Id: I58b0cd1c7bad260c2252f9e172ef85ab885c7fe9 --- .../android/quickstep/BootAwarePreloader.kt | 51 ++++ .../quickstep/TouchInteractionService.java | 1 + .../launcher3/InvariantDeviceProfile.java | 5 +- .../android/launcher3/LauncherAppState.java | 31 +- src/com/android/launcher3/LauncherPrefs.kt | 266 +++++++++++++----- .../android/launcher3/util/LockedUserState.kt | 4 +- .../android/launcher3/LauncherPrefsTest.kt | 168 ++++++++++- .../launcher3/util/LauncherModelHelper.java | 2 +- 8 files changed, 425 insertions(+), 103 deletions(-) create mode 100644 quickstep/src/com/android/quickstep/BootAwarePreloader.kt diff --git a/quickstep/src/com/android/quickstep/BootAwarePreloader.kt b/quickstep/src/com/android/quickstep/BootAwarePreloader.kt new file mode 100644 index 0000000000..35404a9925 --- /dev/null +++ b/quickstep/src/com/android/quickstep/BootAwarePreloader.kt @@ -0,0 +1,51 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.quickstep + +import android.content.Context +import android.util.Log +import com.android.launcher3.LauncherAppState +import com.android.launcher3.LauncherPrefs +import com.android.launcher3.isBootAwareStartupDataEnabled +import com.android.launcher3.util.LockedUserState + +/** + * Loads expensive objects in memory before the user is unlocked. This decreases experienced latency + * when starting the launcher for the first time after a reboot. + */ +object BootAwarePreloader { + private const val TAG = "BootAwarePreloader" + + @JvmStatic + fun start(context: Context) { + val lp = LauncherPrefs.get(context) + when { + LockedUserState.get(context).isUserUnlocked || !isBootAwareStartupDataEnabled -> { + /* No-Op */ + } + lp.isStartupDataMigrated -> { + Log.d(TAG, "preloading start up data") + LauncherAppState.INSTANCE.get(context) + } + else -> { + Log.d(TAG, "queuing start up data migration to boot aware prefs") + LockedUserState.get(context).runOnUserUnlocked { + lp.migrateStartupDataToDeviceProtectedStorage() + } + } + } + } +} diff --git a/quickstep/src/com/android/quickstep/TouchInteractionService.java b/quickstep/src/com/android/quickstep/TouchInteractionService.java index ff81e08e8b..accab38bf3 100644 --- a/quickstep/src/com/android/quickstep/TouchInteractionService.java +++ b/quickstep/src/com/android/quickstep/TouchInteractionService.java @@ -410,6 +410,7 @@ public class TouchInteractionService extends Service mDeviceState = new RecentsAnimationDeviceState(this, true); mTaskbarManager = new TaskbarManager(this); mRotationTouchHelper = mDeviceState.getRotationTouchHelper(); + BootAwarePreloader.start(this); // Call runOnUserUnlocked() before any other callbacks to ensure everything is initialized. mDeviceState.runOnUserUnlocked(this::onUserUnlocked); diff --git a/src/com/android/launcher3/InvariantDeviceProfile.java b/src/com/android/launcher3/InvariantDeviceProfile.java index 4ac7f078bf..a498323140 100644 --- a/src/com/android/launcher3/InvariantDeviceProfile.java +++ b/src/com/android/launcher3/InvariantDeviceProfile.java @@ -59,6 +59,7 @@ import com.android.launcher3.testing.shared.ResourceUtils; import com.android.launcher3.util.DisplayController; import com.android.launcher3.util.DisplayController.Info; import com.android.launcher3.util.IntArray; +import com.android.launcher3.util.LockedUserState; import com.android.launcher3.util.MainThreadInitializedObject; import com.android.launcher3.util.Partner; import com.android.launcher3.util.Themes; @@ -205,7 +206,9 @@ public class InvariantDeviceProfile { if (!newGridName.equals(gridName)) { LauncherPrefs.get(context).put(GRID_NAME, newGridName); } - new DeviceGridState(this).writeToPrefs(context); + LockedUserState.get(context).runOnUserUnlocked(() -> { + new DeviceGridState(this).writeToPrefs(context); + }); DisplayController.INSTANCE.get(context).setPriorityListener( (displayContext, info, flags) -> { diff --git a/src/com/android/launcher3/LauncherAppState.java b/src/com/android/launcher3/LauncherAppState.java index 2b98d983a6..c81ad01f2e 100644 --- a/src/com/android/launcher3/LauncherAppState.java +++ b/src/com/android/launcher3/LauncherAppState.java @@ -44,6 +44,7 @@ import com.android.launcher3.notification.NotificationListener; import com.android.launcher3.pm.InstallSessionHelper; import com.android.launcher3.pm.InstallSessionTracker; import com.android.launcher3.pm.UserCache; +import com.android.launcher3.util.LockedUserState; import com.android.launcher3.util.MainThreadInitializedObject; import com.android.launcher3.util.Preconditions; import com.android.launcher3.util.RunnableList; @@ -106,25 +107,27 @@ public class LauncherAppState implements SafeCloseable { } mOnTerminateCallback.add(() -> mContext.unregisterReceiver(modelChangeReceiver)); - CustomWidgetManager.INSTANCE.get(mContext) - .setWidgetRefreshCallback(mModel::refreshAndBindWidgetsAndShortcuts); - SafeCloseable userChangeListener = UserCache.INSTANCE.get(mContext) .addUserChangeListener(mModel::forceReload); mOnTerminateCallback.add(userChangeListener::close); - IconObserver observer = new IconObserver(); - SafeCloseable iconChangeTracker = mIconProvider.registerIconChangeListener( - observer, MODEL_EXECUTOR.getHandler()); - mOnTerminateCallback.add(iconChangeTracker::close); - MODEL_EXECUTOR.execute(observer::verifyIconChanged); - LauncherPrefs.get(context).addListener(observer, THEMED_ICONS); - mOnTerminateCallback.add( - () -> LauncherPrefs.get(mContext).removeListener(observer, THEMED_ICONS)); + LockedUserState.get(context).runOnUserUnlocked(() -> { + CustomWidgetManager.INSTANCE.get(mContext) + .setWidgetRefreshCallback(mModel::refreshAndBindWidgetsAndShortcuts); - InstallSessionTracker installSessionTracker = - InstallSessionHelper.INSTANCE.get(context).registerInstallTracker(mModel); - mOnTerminateCallback.add(installSessionTracker::unregister); + IconObserver observer = new IconObserver(); + SafeCloseable iconChangeTracker = mIconProvider.registerIconChangeListener( + observer, MODEL_EXECUTOR.getHandler()); + mOnTerminateCallback.add(iconChangeTracker::close); + MODEL_EXECUTOR.execute(observer::verifyIconChanged); + LauncherPrefs.get(context).addListener(observer, THEMED_ICONS); + mOnTerminateCallback.add( + () -> LauncherPrefs.get(mContext).removeListener(observer, THEMED_ICONS)); + + InstallSessionTracker installSessionTracker = + InstallSessionHelper.INSTANCE.get(context).registerInstallTracker(mModel); + mOnTerminateCallback.add(installSessionTracker::unregister); + }); // Register an observer to rebind the notification listener when dots are re-enabled. SettingsCache settingsCache = SettingsCache.INSTANCE.get(mContext); diff --git a/src/com/android/launcher3/LauncherPrefs.kt b/src/com/android/launcher3/LauncherPrefs.kt index 5680c18abb..e675adde47 100644 --- a/src/com/android/launcher3/LauncherPrefs.kt +++ b/src/com/android/launcher3/LauncherPrefs.kt @@ -1,8 +1,25 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package com.android.launcher3 import android.content.Context +import android.content.Context.MODE_PRIVATE import android.content.SharedPreferences import android.content.SharedPreferences.OnSharedPreferenceChangeListener +import android.util.Log import androidx.annotation.VisibleForTesting import com.android.launcher3.LauncherFiles.DEVICE_PREFERENCES_KEY import com.android.launcher3.LauncherFiles.SHARED_PREFERENCES_KEY @@ -20,10 +37,34 @@ import com.android.launcher3.util.Themes * * TODO(b/262721340): Replace all direct SharedPreference refs with LauncherPrefs / Item methods. */ -class LauncherPrefs(private val context: Context) { +class LauncherPrefs(private val encryptedContext: Context) { + private val deviceProtectedStorageContext = + encryptedContext.createDeviceProtectedStorageContext() + + private val bootAwarePrefs + get() = + deviceProtectedStorageContext.getSharedPreferences(BOOT_AWARE_PREFS_KEY, MODE_PRIVATE) + + private val Item.encryptedPrefs + get() = encryptedContext.getSharedPreferences(sharedPrefFile, MODE_PRIVATE) + + // This call to `SharedPreferences` needs to be explicit rather than using `get` since doing so + // would result in a circular dependency between `isStartupDataMigrated` and `choosePreferences` + val isStartupDataMigrated: Boolean + get() = + bootAwarePrefs.getBoolean( + IS_STARTUP_DATA_MIGRATED.sharedPrefKey, + IS_STARTUP_DATA_MIGRATED.defaultValue + ) + + private fun chooseSharedPreferences(item: Item): SharedPreferences = + if (isBootAwareStartupDataEnabled && item.isBootAware && isStartupDataMigrated) + bootAwarePrefs + else item.encryptedPrefs /** Wrapper around `getInner` for a `ContextualItem` */ - fun get(item: ContextualItem): T = getInner(item, item.defaultValueFromContext(context)) + fun get(item: ContextualItem): T = + getInner(item, item.defaultValueFromContext(encryptedContext)) /** Wrapper around `getInner` for an `Item` */ fun get(item: ConstantItem): T = getInner(item, item.defaultValue) @@ -35,7 +76,7 @@ class LauncherPrefs(private val context: Context) { */ @Suppress("IMPLICIT_CAST_TO_ANY", "UNCHECKED_CAST") private fun getInner(item: Item, default: T): T { - val sp = context.getSharedPreferences(item.sharedPrefFile, Context.MODE_PRIVATE) + val sp = chooseSharedPreferences(item) return when (item.type) { String::class.java -> sp.getString(item.sharedPrefKey, default as? String) @@ -68,16 +109,8 @@ class LauncherPrefs(private val context: Context) { fun put(vararg itemsToValues: Pair): 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() + /** See referenced `put` method above. */ + fun put(item: Item, value: T): Unit = put(item.to(value)) /** * Synchronously stores all the values provided according to their associated Item @@ -87,27 +120,35 @@ class LauncherPrefs(private val context: Context) { 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. + * Updates the values stored in `SharedPreferences` for each corresponding Item-value pair. If + * the item is boot aware, this method updates both the boot aware and the encrypted files. This + * is done because: 1) It allows for easy roll-back if the data is already in encrypted prefs + * and we need to turn off the boot aware data feature & 2) It simplifies Backup/Restore, which + * already points to encrypted storage. * - * @return `List` 1 for each distinct shared preference file among the - * items given as part of the itemsToValues parameter + * Returns a list of editors with all transactions added so that the caller can determine to use + * .apply() or .commit() */ private fun prepareToPutValues( - itemsToValues: Array> - ): 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) - } - } + updates: Array> + ): List { + val updatesPerPrefFile = updates.groupBy { it.first.encryptedPrefs }.toMutableMap() + + if (isBootAwareStartupDataEnabled) { + val bootAwareUpdates = updates.filter { it.first.isBootAware } + if (bootAwareUpdates.isNotEmpty()) { + updatesPerPrefFile[bootAwarePrefs] = bootAwareUpdates } + } + + return updatesPerPrefFile.map { prefToItemValueList -> + prefToItemValueList.key.edit().apply { + prefToItemValueList.value.forEach { itemToValue: Pair -> + putValue(itemToValue.first, itemToValue.second) + } + } + } + } /** * Handles adding values to `SharedPreferences` regardless of type. This method is especially @@ -117,10 +158,10 @@ class LauncherPrefs(private val context: Context) { @Suppress("UNCHECKED_CAST") private fun SharedPreferences.Editor.putValue( item: Item, - value: Any + value: Any? ): SharedPreferences.Editor = - when (value::class.java) { - String::class.java -> putString(item.sharedPrefKey, value as String) + when (item.type) { + 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, @@ -129,10 +170,10 @@ class LauncherPrefs(private val context: Context) { 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) + Set::class.java -> putStringSet(item.sharedPrefKey, value as? Set) else -> throw IllegalArgumentException( - "item type: ${value::class} is not compatible with sharedPref methods" + "item type: ${item.type} is not compatible with sharedPref methods" ) } @@ -143,13 +184,9 @@ class LauncherPrefs(private val context: Context) { */ fun addListener(listener: OnSharedPreferenceChangeListener, vararg items: Item) { items - .map { it.sharedPrefFile } + .map { chooseSharedPreferences(it) } .distinct() - .forEach { - context - .getSharedPreferences(it, Context.MODE_PRIVATE) - .registerOnSharedPreferenceChangeListener(listener) - } + .forEach { it.registerOnSharedPreferenceChangeListener(listener) } } /** @@ -159,13 +196,9 @@ class LauncherPrefs(private val context: Context) { fun removeListener(listener: OnSharedPreferenceChangeListener, vararg items: Item) { // If a listener is not registered to a SharedPreference, unregistering it does nothing items - .map { it.sharedPrefFile } + .map { chooseSharedPreferences(it) } .distinct() - .forEach { - context - .getSharedPreferences(it, Context.MODE_PRIVATE) - .unregisterOnSharedPreferenceChangeListener(listener) - } + .forEach { it.unregisterOnSharedPreferenceChangeListener(listener) } } /** @@ -174,10 +207,8 @@ class LauncherPrefs(private val context: Context) { */ fun has(vararg items: Item): Boolean { items - .groupBy { it.sharedPrefFile } - .forEach { (file, itemsSublist) -> - val prefs: SharedPreferences = - context.getSharedPreferences(file, Context.MODE_PRIVATE) + .groupBy { chooseSharedPreferences(it) } + .forEach { (prefs, itemsSublist) -> if (!itemsSublist.none { !prefs.contains(it.sharedPrefKey) }) return false } return true @@ -192,62 +223,128 @@ class LauncherPrefs(private val context: Context) { 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. + * Removes the key value pairs stored in `SharedPreferences` for each corresponding Item. If the + * item is boot aware, this method removes the data from both the boot aware and encrypted + * files. + * + * @return a list of editors with all transactions added so that the caller can determine to use + * .apply() or .commit() */ - 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) } - } + private fun prepareToRemove(items: Array): List { + val itemsPerFile = items.groupBy { it.encryptedPrefs }.toMutableMap() + + if (isBootAwareStartupDataEnabled) { + val bootAwareUpdates = items.filter { it.isBootAware } + if (bootAwareUpdates.isNotEmpty()) { + itemsPerFile[bootAwarePrefs] = bootAwareUpdates } + } + + return itemsPerFile.map { (prefs, items) -> + prefs.edit().also { editor -> + items.forEach { item -> editor.remove(item.sharedPrefKey) } + } + } + } + + fun migrateStartupDataToDeviceProtectedStorage() { + if (!isBootAwareStartupDataEnabled) return + + Log.d( + TAG, + "Migrating data to unencrypted shared preferences to enable preloading " + + "while the user is locked the next time the device reboots." + ) + + with(bootAwarePrefs.edit()) { + BOOT_AWARE_ITEMS.forEach { putValue(it, get(it)) } + putBoolean(IS_STARTUP_DATA_MIGRATED.sharedPrefKey, true) + apply() + } + } companion object { + private const val TAG = "LauncherPrefs" + @VisibleForTesting const val BOOT_AWARE_PREFS_KEY = "boot_aware_prefs" + @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 ICON_STATE = nonRestorableItem(LauncherAppState.KEY_ICON_STATE, "", true) + @JvmField val THEMED_ICONS = backedUpItem(Themes.KEY_THEMED_ICONS, false, true) @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 WORKSPACE_SIZE = backedUpItem(DeviceGridState.KEY_WORKSPACE_SIZE, "", true) + @JvmField val HOTSEAT_COUNT = backedUpItem(DeviceGridState.KEY_HOTSEAT_COUNT, -1, true) @JvmField val DEVICE_TYPE = - backedUpItem(DeviceGridState.KEY_DEVICE_TYPE, InvariantDeviceProfile.TYPE_PHONE) - @JvmField val DB_FILE = backedUpItem(DeviceGridState.KEY_DB_FILE, "") + backedUpItem(DeviceGridState.KEY_DEVICE_TYPE, InvariantDeviceProfile.TYPE_PHONE, true) + @JvmField val DB_FILE = backedUpItem(DeviceGridState.KEY_DB_FILE, "", true) @JvmField val RESTORE_DEVICE = - backedUpItem(RestoreDbTask.RESTORED_DEVICE_TYPE, InvariantDeviceProfile.TYPE_PHONE) + backedUpItem( + RestoreDbTask.RESTORED_DEVICE_TYPE, + InvariantDeviceProfile.TYPE_PHONE, + true + ) @JvmField val APP_WIDGET_IDS = backedUpItem(RestoreDbTask.APPWIDGET_IDS, "") @JvmField val OLD_APP_WIDGET_IDS = backedUpItem(RestoreDbTask.APPWIDGET_OLD_IDS, "") - @JvmField val GRID_NAME = ConstantItem("idp_grid_name", true, null, String::class.java) + @JvmField + val GRID_NAME = + ConstantItem( + "idp_grid_name", + isBackedUp = true, + defaultValue = null, + isBootAware = true, + type = String::class.java + ) @JvmField val ALLOW_ROTATION = backedUpItem(RotationHelper.ALLOW_ROTATION_PREFERENCE_KEY, Boolean::class.java) { RotationHelper.getAllowRotationDefaultValue(DisplayController.INSTANCE.get(it).info) } + @JvmField + val IS_STARTUP_DATA_MIGRATED = + ConstantItem( + "is_startup_data_boot_aware", + isBackedUp = false, + defaultValue = false, + isBootAware = true + ) @VisibleForTesting @JvmStatic - fun backedUpItem(sharedPrefKey: String, defaultValue: T): ConstantItem = - ConstantItem(sharedPrefKey, true, defaultValue) + fun backedUpItem( + sharedPrefKey: String, + defaultValue: T, + isBootAware: Boolean = false + ): ConstantItem = + ConstantItem(sharedPrefKey, isBackedUp = true, defaultValue, isBootAware) @JvmStatic fun backedUpItem( sharedPrefKey: String, type: Class, + isBootAware: Boolean = false, defaultValueFromContext: (c: Context) -> T - ): ContextualItem = ContextualItem(sharedPrefKey, true, defaultValueFromContext, type) + ): ContextualItem = + ContextualItem( + sharedPrefKey, + isBackedUp = true, + defaultValueFromContext, + isBootAware, + type + ) @VisibleForTesting @JvmStatic - fun nonRestorableItem(sharedPrefKey: String, defaultValue: T): ConstantItem = - ConstantItem(sharedPrefKey, false, defaultValue) + fun nonRestorableItem( + sharedPrefKey: String, + defaultValue: T, + isBootAware: Boolean = false + ): ConstantItem = + ConstantItem(sharedPrefKey, isBackedUp = false, defaultValue, isBootAware) @Deprecated("Don't use shared preferences directly. Use other LauncherPref methods.") @JvmStatic @@ -255,7 +352,7 @@ class LauncherPrefs(private val context: Context) { // Use application context for shared preferences, so we use single cached instance return context.applicationContext.getSharedPreferences( SHARED_PREFERENCES_KEY, - Context.MODE_PRIVATE + MODE_PRIVATE ) } @@ -265,16 +362,23 @@ class LauncherPrefs(private val context: Context) { // Use application context for shared preferences, so we use a single cached instance return context.applicationContext.getSharedPreferences( DEVICE_PREFERENCES_KEY, - Context.MODE_PRIVATE + MODE_PRIVATE ) } } } +// This is hard-coded to false for now until it is time to release this optimization. It is only +// a var because the unit tests are setting this to true so they can run. +@VisibleForTesting var isBootAwareStartupDataEnabled: Boolean = false + +private val BOOT_AWARE_ITEMS: MutableSet> = mutableSetOf() + abstract class Item { abstract val sharedPrefKey: String abstract val isBackedUp: Boolean abstract val type: Class<*> + abstract val isBootAware: Boolean val sharedPrefFile: String get() = if (isBackedUp) SHARED_PREFERENCES_KEY else DEVICE_PREFERENCES_KEY @@ -285,14 +389,22 @@ data class ConstantItem( override val sharedPrefKey: String, override val isBackedUp: Boolean, val defaultValue: T, + override val isBootAware: Boolean, // The default value can be null. If so, the type needs to be explicitly stated, or else NPE override val type: Class = defaultValue!!::class.java -) : Item() +) : Item() { + init { + if (isBootAware && isBootAwareStartupDataEnabled) { + BOOT_AWARE_ITEMS.add(this) + } + } +} data class ContextualItem( override val sharedPrefKey: String, override val isBackedUp: Boolean, private val defaultSupplier: (c: Context) -> T, + override val isBootAware: Boolean, override val type: Class ) : Item() { private var default: T? = null diff --git a/src/com/android/launcher3/util/LockedUserState.kt b/src/com/android/launcher3/util/LockedUserState.kt index 7b49583b86..f5e13d2487 100644 --- a/src/com/android/launcher3/util/LockedUserState.kt +++ b/src/com/android/launcher3/util/LockedUserState.kt @@ -50,7 +50,9 @@ class LockedUserState(private val mContext: Context) : SafeCloseable { } companion object { - @VisibleForTesting val INSTANCE = MainThreadInitializedObject { LockedUserState(it) } + @VisibleForTesting + @JvmField + val INSTANCE = MainThreadInitializedObject { LockedUserState(it) } @JvmStatic fun get(context: Context): LockedUserState = INSTANCE.get(context) } diff --git a/tests/src/com/android/launcher3/LauncherPrefsTest.kt b/tests/src/com/android/launcher3/LauncherPrefsTest.kt index e8372b1f24..41ef3de267 100644 --- a/tests/src/com/android/launcher3/LauncherPrefsTest.kt +++ b/tests/src/com/android/launcher3/LauncherPrefsTest.kt @@ -1,9 +1,27 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package com.android.launcher3 +import android.content.Context +import android.content.SharedPreferences 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.android.launcher3.LauncherPrefs.Companion.BOOT_AWARE_PREFS_KEY import com.google.common.truth.Truth.assertThat import java.util.concurrent.CountDownLatch import java.util.concurrent.TimeUnit @@ -13,19 +31,22 @@ 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) -private val TEST_CONTEXTUAL_ITEM = ContextualItem("4", true, { true }, Boolean::class.java) +private val TEST_CONTEXTUAL_ITEM = ContextualItem("4", true, { true }, false, Boolean::class.java) private const val TEST_DEFAULT_VALUE = "default" private const val TEST_PREF_KEY = "test_pref_key" +private const val WAIT_TIME_IN_SECONDS = 3L + @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) - } + private val context by lazy { InstrumentationRegistry.getInstrumentation().targetContext } + private val launcherPrefs by lazy { LauncherPrefs.get(context) } + + init { + isBootAwareStartupDataEnabled = true } @Test @@ -52,7 +73,7 @@ class LauncherPrefsTest { addListener(listener, TEST_STRING_ITEM) putSync(TEST_STRING_ITEM.to(TEST_STRING_ITEM.defaultValue + "abc")) - assertThat(latch.await(2, TimeUnit.SECONDS)).isTrue() + assertThat(latch.await(WAIT_TIME_IN_SECONDS, TimeUnit.SECONDS)).isTrue() remove(TEST_STRING_ITEM) } } @@ -68,7 +89,7 @@ class LauncherPrefsTest { 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() + assertThat(latch.await(WAIT_TIME_IN_SECONDS, TimeUnit.SECONDS)).isFalse() remove(TEST_STRING_ITEM) } } @@ -85,7 +106,7 @@ class LauncherPrefsTest { TEST_STRING_ITEM.to(TEST_STRING_ITEM.defaultValue + "abc"), TEST_BOOLEAN_ITEM.to(!TEST_BOOLEAN_ITEM.defaultValue) ) - assertThat(latch.await(2, TimeUnit.SECONDS)).isTrue() + assertThat(latch.await(WAIT_TIME_IN_SECONDS, TimeUnit.SECONDS)).isTrue() removeListener(listener, TEST_INT_ITEM, TEST_STRING_ITEM, TEST_BOOLEAN_ITEM) latch = CountDownLatch(1) @@ -96,7 +117,7 @@ class LauncherPrefsTest { ) remove(TEST_INT_ITEM, TEST_STRING_ITEM, TEST_BOOLEAN_ITEM) - assertThat(latch.await(2, TimeUnit.SECONDS)).isFalse() + assertThat(latch.await(WAIT_TIME_IN_SECONDS, TimeUnit.SECONDS)).isFalse() } } @@ -166,4 +187,133 @@ class LauncherPrefsTest { val backedUpItem = LauncherPrefs.backedUpItem(TEST_PREF_KEY, TEST_DEFAULT_VALUE) assertThat(backedUpItem.sharedPrefFile).isEqualTo(LauncherFiles.SHARED_PREFERENCES_KEY) } + + @Test + fun put_bootAwareItem_updatesDeviceProtectedStorage() { + val bootAwareItem = + LauncherPrefs.backedUpItem(TEST_PREF_KEY, TEST_DEFAULT_VALUE, isBootAware = true) + + val bootAwarePrefs: SharedPreferences = + context + .createDeviceProtectedStorageContext() + .getSharedPreferences(BOOT_AWARE_PREFS_KEY, Context.MODE_PRIVATE) + bootAwarePrefs.edit().remove(bootAwareItem.sharedPrefKey).commit() + + launcherPrefs.putSync(bootAwareItem.to(bootAwareItem.defaultValue)) + assertThat(bootAwarePrefs.contains(bootAwareItem.sharedPrefKey)).isTrue() + + launcherPrefs.removeSync(bootAwareItem) + } + + @Test + fun put_bootAwareItem_updatesEncryptedStorage() { + val bootAwareItem = + LauncherPrefs.backedUpItem(TEST_PREF_KEY, TEST_DEFAULT_VALUE, isBootAware = true) + + val encryptedPrefs: SharedPreferences = + context.getSharedPreferences(bootAwareItem.sharedPrefFile, Context.MODE_PRIVATE) + encryptedPrefs.edit().remove(bootAwareItem.sharedPrefKey).commit() + + launcherPrefs.putSync(bootAwareItem.to(TEST_STRING_ITEM.defaultValue)) + assertThat(encryptedPrefs.contains(bootAwareItem.sharedPrefKey)).isTrue() + + launcherPrefs.removeSync(bootAwareItem) + } + + @Test + fun remove_bootAwareItem_removesFromDeviceProtectedStorage() { + val bootAwareItem = + LauncherPrefs.backedUpItem(TEST_PREF_KEY, TEST_DEFAULT_VALUE, isBootAware = true) + + val bootAwarePrefs: SharedPreferences = + context + .createDeviceProtectedStorageContext() + .getSharedPreferences(BOOT_AWARE_PREFS_KEY, Context.MODE_PRIVATE) + + bootAwarePrefs + .edit() + .putString(bootAwareItem.sharedPrefKey, bootAwareItem.defaultValue) + .commit() + + launcherPrefs.removeSync(bootAwareItem) + assertThat(bootAwarePrefs.contains(bootAwareItem.sharedPrefKey)).isFalse() + } + + @Test + fun remove_bootAwareItem_removesFromEncryptedStorage() { + val bootAwareItem = + LauncherPrefs.backedUpItem(TEST_PREF_KEY, TEST_DEFAULT_VALUE, isBootAware = true) + + val encryptedPrefs: SharedPreferences = + context.getSharedPreferences(bootAwareItem.sharedPrefFile, Context.MODE_PRIVATE) + + encryptedPrefs + .edit() + .putString(bootAwareItem.sharedPrefKey, bootAwareItem.defaultValue) + .commit() + + launcherPrefs.removeSync(bootAwareItem) + assertThat(encryptedPrefs.contains(bootAwareItem.sharedPrefKey)).isFalse() + } + + @Test + fun migrate_bootAwareItemsToDeviceProtectedStorage_worksAsIntended() { + val bootAwareItem = + LauncherPrefs.backedUpItem(TEST_PREF_KEY, TEST_DEFAULT_VALUE, isBootAware = true) + launcherPrefs.removeSync(bootAwareItem) + + val bootAwarePrefs: SharedPreferences = + context + .createDeviceProtectedStorageContext() + .getSharedPreferences(BOOT_AWARE_PREFS_KEY, Context.MODE_PRIVATE) + + if (bootAwarePrefs.contains(bootAwareItem.sharedPrefKey)) { + bootAwarePrefs.edit().remove(bootAwareItem.sharedPrefKey).commit() + } + + val encryptedPrefs: SharedPreferences = + context.getSharedPreferences(bootAwareItem.sharedPrefFile, Context.MODE_PRIVATE) + + encryptedPrefs + .edit() + .putString(bootAwareItem.sharedPrefKey, bootAwareItem.defaultValue) + .commit() + + launcherPrefs.migrateStartupDataToDeviceProtectedStorage() + assertThat(bootAwarePrefs.contains(bootAwareItem.sharedPrefKey)).isTrue() + + launcherPrefs.removeSync(bootAwareItem) + } + + @Test + fun migrate_onlyEncryptedItemsToDeviceProtectedStorage_doesNotHappen() { + val onlyEncryptedItem = + LauncherPrefs.backedUpItem( + TEST_PREF_KEY + "_", + TEST_DEFAULT_VALUE + "_", + isBootAware = false + ) + + val bootAwarePrefs: SharedPreferences = + context + .createDeviceProtectedStorageContext() + .getSharedPreferences(BOOT_AWARE_PREFS_KEY, Context.MODE_PRIVATE) + + if (bootAwarePrefs.contains(onlyEncryptedItem.sharedPrefKey)) { + bootAwarePrefs.edit().remove(onlyEncryptedItem.sharedPrefKey).commit() + } + + val encryptedPrefs: SharedPreferences = + context.getSharedPreferences(onlyEncryptedItem.sharedPrefFile, Context.MODE_PRIVATE) + + encryptedPrefs + .edit() + .putString(onlyEncryptedItem.sharedPrefKey, onlyEncryptedItem.defaultValue) + .commit() + + launcherPrefs.migrateStartupDataToDeviceProtectedStorage() + assertThat(bootAwarePrefs.contains(onlyEncryptedItem.sharedPrefKey)).isFalse() + + encryptedPrefs.edit().remove(onlyEncryptedItem.sharedPrefKey).commit() + } } diff --git a/tests/src/com/android/launcher3/util/LauncherModelHelper.java b/tests/src/com/android/launcher3/util/LauncherModelHelper.java index fa4c5193c8..545b6456e2 100644 --- a/tests/src/com/android/launcher3/util/LauncherModelHelper.java +++ b/tests/src/com/android/launcher3/util/LauncherModelHelper.java @@ -510,7 +510,7 @@ public class LauncherModelHelper { UserCache.INSTANCE, InstallSessionHelper.INSTANCE, LauncherPrefs.INSTANCE, LauncherAppState.INSTANCE, InvariantDeviceProfile.INSTANCE, DisplayController.INSTANCE, CustomWidgetManager.INSTANCE, - SettingsCache.INSTANCE, PluginManagerWrapper.INSTANCE, + SettingsCache.INSTANCE, PluginManagerWrapper.INSTANCE, LockedUserState.INSTANCE, ItemInstallQueue.INSTANCE, WindowManagerProxy.INSTANCE); mPm = spy(getBaseContext().getPackageManager()); mDbDir = new File(getCacheDir(), UUID.randomUUID().toString());