From ed2f309652cfbac2844dc49518c1efa5591d8322 Mon Sep 17 00:00:00 2001 From: Shen Lin Date: Tue, 20 Dec 2022 11:29:51 +0800 Subject: [PATCH 1/7] Fix NPE in FallbackSwipeHandler when getRunningTask() returns null mGestureState.getRunningTask() is probably null in this situation, we need to add a null check here refer to the usage in TaskAnimationManager. Bug: 263041039 Test: manual Change-Id: I7707bf204190ff506f40640ff764f07cb98aab68 --- quickstep/src/com/android/quickstep/FallbackSwipeHandler.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/quickstep/src/com/android/quickstep/FallbackSwipeHandler.java b/quickstep/src/com/android/quickstep/FallbackSwipeHandler.java index 99f7bdd2e7..3b11228755 100644 --- a/quickstep/src/com/android/quickstep/FallbackSwipeHandler.java +++ b/quickstep/src/com/android/quickstep/FallbackSwipeHandler.java @@ -107,7 +107,8 @@ public class FallbackSwipeHandler extends super(context, deviceState, taskAnimationManager, gestureState, touchTimeMs, continuingLastGesture, inputConsumer); - mRunningOverHome = mGestureState.getRunningTask().isHomeTask(); + mRunningOverHome = mGestureState.getRunningTask() != null + && mGestureState.getRunningTask().isHomeTask(); if (mRunningOverHome) { runActionOnRemoteHandles(remoteTargetHandle -> remoteTargetHandle.getTransformParams().setHomeBuilderProxy( From 2003c1c73fa691ae721220c8345ce08b556c184f Mon Sep 17 00:00:00 2001 From: Sunny Goyal Date: Tue, 28 Feb 2023 12:00:27 -0800 Subject: [PATCH 2/7] Restarting launcher if a flag changes > Only restart when the screen is off. > This allows better propogation of flags similar to systemUI > Adding support for integer flags Bug: 266854800 Test: Verified on device using device_config shell command Change-Id: I4ea9c564f2d973f11f9570b5a21365183afefab7 --- .../uioverrides/flags/FlagsFactory.java | 59 +++++++++++++++++++ .../launcher3/config/FeatureFlags.java | 16 +++++ .../uioverrides/flags/FlagsFactory.java | 9 +++ 3 files changed, 84 insertions(+) diff --git a/quickstep/src/com/android/launcher3/uioverrides/flags/FlagsFactory.java b/quickstep/src/com/android/launcher3/uioverrides/flags/FlagsFactory.java index 29ef2b430a..84b873d9db 100644 --- a/quickstep/src/com/android/launcher3/uioverrides/flags/FlagsFactory.java +++ b/quickstep/src/com/android/launcher3/uioverrides/flags/FlagsFactory.java @@ -18,28 +18,53 @@ package com.android.launcher3.uioverrides.flags; import static android.app.ActivityThread.currentApplication; +import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR; + import android.content.Context; import android.content.SharedPreferences; import android.provider.DeviceConfig; +import android.provider.DeviceConfig.Properties; +import android.util.Log; import com.android.launcher3.Utilities; import com.android.launcher3.config.FeatureFlags.BooleanFlag; +import com.android.launcher3.config.FeatureFlags.IntFlag; +import com.android.launcher3.util.ScreenOnTracker; import java.io.PrintWriter; import java.util.ArrayList; import java.util.Collections; +import java.util.HashSet; import java.util.List; +import java.util.Set; /** * Helper class to create various flags for system build */ public class FlagsFactory { + private static final String TAG = "FlagsFactory"; + + private static final FlagsFactory INSTANCE = new FlagsFactory(); + private static final boolean FLAG_AUTO_APPLY_ENABLED = false; + public static final String FLAGS_PREF_NAME = "featureFlags"; public static final String NAMESPACE_LAUNCHER = "launcher"; private static final List sDebugFlags = new ArrayList<>(); + + private final Set mKeySet = new HashSet<>(); + private boolean mRestartRequested = false; + + private FlagsFactory() { + if (!FLAG_AUTO_APPLY_ENABLED) { + return; + } + DeviceConfig.addOnPropertiesChangedListener( + NAMESPACE_LAUNCHER, UI_HELPER_EXECUTOR, this::onPropertiesChanged); + } + /** * Creates a new debug flag */ @@ -63,6 +88,7 @@ public class FlagsFactory { */ public static BooleanFlag getReleaseFlag( int bugId, String key, boolean defaultValueInCode, String description) { + INSTANCE.mKeySet.add(key); boolean defaultValue = DeviceConfig.getBoolean(NAMESPACE_LAUNCHER, key, defaultValueInCode); if (Utilities.IS_DEBUG_DEVICE) { SharedPreferences prefs = currentApplication() @@ -78,6 +104,15 @@ public class FlagsFactory { } } + /** + * Creates a new integer flag. Integer flags are always release flags + */ + public static IntFlag getIntFlag( + int bugId, String key, int defaultValueInCode, String description) { + INSTANCE.mKeySet.add(key); + return new IntFlag(DeviceConfig.getInt(NAMESPACE_LAUNCHER, key, defaultValueInCode)); + } + static List getDebugFlags() { if (!Utilities.IS_DEBUG_DEVICE) { return Collections.emptyList(); @@ -121,4 +156,28 @@ public class FlagsFactory { } } } + + private void onPropertiesChanged(Properties properties) { + if (!Collections.disjoint(properties.getKeyset(), mKeySet)) { + // Schedule a restart + if (mRestartRequested) { + return; + } + Log.e(TAG, "Flag changed, scheduling restart"); + mRestartRequested = true; + ScreenOnTracker sot = ScreenOnTracker.INSTANCE.get(currentApplication()); + if (sot.isScreenOn()) { + sot.addListener(this::onScreenOnChanged); + } else { + onScreenOnChanged(false); + } + } + } + + private void onScreenOnChanged(boolean isOn) { + if (mRestartRequested && !isOn) { + Log.e(TAG, "Restart requested, killing process"); + System.exit(0); + } + } } diff --git a/src/com/android/launcher3/config/FeatureFlags.java b/src/com/android/launcher3/config/FeatureFlags.java index db2b75970e..6adf54db64 100644 --- a/src/com/android/launcher3/config/FeatureFlags.java +++ b/src/com/android/launcher3/config/FeatureFlags.java @@ -400,4 +400,20 @@ public final class FeatureFlags { return mCurrentValue; } } + + /** + * Class representing an integer flag + */ + public static class IntFlag { + + private final int mCurrentValue; + + public IntFlag(int currentValue) { + mCurrentValue = currentValue; + } + + public int get() { + return mCurrentValue; + } + } } diff --git a/src_ui_overrides/com/android/launcher3/uioverrides/flags/FlagsFactory.java b/src_ui_overrides/com/android/launcher3/uioverrides/flags/FlagsFactory.java index 599969bb8d..4463adcf06 100644 --- a/src_ui_overrides/com/android/launcher3/uioverrides/flags/FlagsFactory.java +++ b/src_ui_overrides/com/android/launcher3/uioverrides/flags/FlagsFactory.java @@ -17,6 +17,7 @@ package com.android.launcher3.uioverrides.flags; import com.android.launcher3.config.FeatureFlags.BooleanFlag; +import com.android.launcher3.config.FeatureFlags.IntFlag; import java.io.PrintWriter; @@ -42,6 +43,14 @@ public class FlagsFactory { return new BooleanFlag(defaultValueInCode); } + /** + * Creates a new integer flag. Integer flags are always release flags + */ + public static IntFlag getIntFlag( + int bugId, String key, int defaultValueInCode, String description) { + return new IntFlag(defaultValueInCode); + } + /** * Dumps the current flags state to the print writer */ From 0788fd792ec36d7181ba943b2ef5fb2998ed570c Mon Sep 17 00:00:00 2001 From: Sunny Goyal Date: Wed, 1 Mar 2023 09:50:12 -0800 Subject: [PATCH 3/7] Removing some usused features Bug: 270386012 Test: Presubmit Change-Id: Ica71109d43c42d9d0d5269520f7b7e72921f42a6 --- OWNERS | 2 +- src/com/android/launcher3/config/FeatureFlags.java | 12 ------------ 2 files changed, 1 insertion(+), 13 deletions(-) diff --git a/OWNERS b/OWNERS index 962d63ad02..dd9fbf968f 100644 --- a/OWNERS +++ b/OWNERS @@ -36,4 +36,4 @@ winsonc@google.com xuqiu@google.com per-file FeatureFlags.java, globs = set noparent -per-file FeatureFlags.java = sunnygoyal@google.com, winsonc@google.com, adamcohen@google.com, hyunyoungs@google.com +per-file FeatureFlags.java = sunnygoyal@google.com, winsonc@google.com, adamcohen@google.com, hyunyoungs@google.com, captaincole@google.com diff --git a/src/com/android/launcher3/config/FeatureFlags.java b/src/com/android/launcher3/config/FeatureFlags.java index af243346b4..b1159cdf07 100644 --- a/src/com/android/launcher3/config/FeatureFlags.java +++ b/src/com/android/launcher3/config/FeatureFlags.java @@ -247,10 +247,6 @@ public final class FeatureFlags { 270394392, "ENABLE_SEARCH_RESULT_LAUNCH_TRANSITION", false, "Enable option to launch search results using the new view container transitions"); - public static final BooleanFlag TWO_PREDICTED_ROWS_ALL_APPS_SEARCH = getReleaseFlag(270394225, - "TWO_PREDICTED_ROWS_ALL_APPS_SEARCH", false, - "Use 2 rows of app predictions in All Apps search zero-state"); - public static final BooleanFlag ENABLE_SHOW_KEYBOARD_OPTION_IN_ALL_APPS = getReleaseFlag( 270394468, "ENABLE_SHOW_KEYBOARD_OPTION_IN_ALL_APPS", true, "Enable option to show keyboard when going to all-apps"); @@ -354,10 +350,6 @@ public final class FeatureFlags { "Enable the ability to generate monochromatic icons, if it is not provided by the app" ); - public static final BooleanFlag ENABLE_DREAM_TRANSITION = getDebugFlag(270396364, - "ENABLE_DREAM_TRANSITION", true, - "Enable the launcher transition when the device enters a dream"); - public static final BooleanFlag ENABLE_TASKBAR_EDU_TOOLTIP = getDebugFlag(270396268, "ENABLE_TASKBAR_EDU_TOOLTIP", true, "Enable the tooltip version of the Taskbar education flow."); @@ -384,10 +376,6 @@ public final class FeatureFlags { "ENABLE_KEYBOARD_QUICK_SWITCH", false, "Enables keyboard quick switching"); - public static final BooleanFlag ENABLE_SMARTSPACE_DEFAULT_DATE_REMOVED = getDebugFlag(269761613, - "ENABLE_SMARTSPACE_DEFAULT_DATE_REMOVED", false, - "Enables remove smartspace default date"); - public static class BooleanFlag { private final boolean mCurrentValue; From e35d11269354ea2c1cd4d686c6ad75b7b5b7ef17 Mon Sep 17 00:00:00 2001 From: Jeremy Sim Date: Thu, 23 Feb 2023 14:40:58 -0800 Subject: [PATCH 4/7] Fix bug with Taskbar not differentiating between user profiles This patch fixes a bug where Taskbar would not differentiate between user profiles when selecting an app to launch from Overview. The bug occurred because findLastActiveTaskAndRunCallback(), which checks for already-running tasks when launching an app from the Taskbar, only checks for a ComponentName match and not a userId match. Fixed by making the findLastActiveTaskAndRunCallback() also check for a userId match. Fixes: 270456926 Test: Manual Change-Id: I43ff06083a5dce775fdbd0b0ed951beaae34c0ab --- .../taskbar/TaskbarActivityContext.java | 5 +- .../taskbar/TaskbarUIController.java | 12 +- .../uioverrides/QuickstepLauncher.java | 6 +- .../util/SplitSelectStateController.java | 13 +- .../util/SplitSelectStateControllerTest.kt | 140 +++++++++++++++++- 5 files changed, 160 insertions(+), 16 deletions(-) diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java index ab52adb774..dde7d94298 100644 --- a/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java +++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java @@ -88,6 +88,7 @@ import com.android.launcher3.testing.TestLogging; import com.android.launcher3.testing.shared.TestProtocol; import com.android.launcher3.touch.ItemClickHandler; import com.android.launcher3.touch.ItemClickHandler.ItemClickProxy; +import com.android.launcher3.util.ComponentKey; import com.android.launcher3.util.DisplayController; import com.android.launcher3.util.NavigationMode; import com.android.launcher3.util.PackageManagerHelper; @@ -105,7 +106,6 @@ import com.android.systemui.unfold.updates.RotationChangeProvider; import com.android.systemui.unfold.util.ScopedUnfoldTransitionProgressProvider; import java.io.PrintWriter; -import java.util.function.Consumer; /** * The {@link ActivityContext} with which we inflate Taskbar-related Views. This allows UI elements @@ -877,8 +877,9 @@ public class TaskbarActivityContext extends BaseTaskbarContext { * (potentially breaking a split pair). */ private void launchFromTaskbarPreservingSplitIfVisible(RecentsView recents, ItemInfo info) { + ComponentKey componentToBeLaunched = new ComponentKey(info.getTargetComponent(), info.user); recents.getSplitSelectController().findLastActiveTaskAndRunCallback( - info.getTargetComponent(), + componentToBeLaunched, foundTask -> { if (foundTask != null) { TaskView foundTaskView = diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarUIController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarUIController.java index 34792555b3..b552e9bd18 100644 --- a/quickstep/src/com/android/launcher3/taskbar/TaskbarUIController.java +++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarUIController.java @@ -30,16 +30,15 @@ import androidx.annotation.Nullable; import com.android.launcher3.model.data.ItemInfo; import com.android.launcher3.model.data.ItemInfoWithIcon; +import com.android.launcher3.util.ComponentKey; import com.android.launcher3.util.DisplayController; import com.android.launcher3.util.SplitConfigurationOptions; import com.android.quickstep.util.GroupTask; import com.android.quickstep.views.RecentsView; import com.android.quickstep.views.TaskView; import com.android.quickstep.views.TaskView.TaskIdAttributeContainer; -import com.android.systemui.shared.recents.model.Task; import java.io.PrintWriter; -import java.util.function.Consumer; /** * Base class for providing different taskbar UI @@ -189,8 +188,12 @@ public class TaskbarUIController { if (recentsView == null) { return; } + + ComponentKey componentToBeStaged = new ComponentKey( + splitSelectSource.itemInfo.getTargetComponent(), + splitSelectSource.itemInfo.user); recentsView.getSplitSelectController().findLastActiveTaskAndRunCallback( - splitSelectSource.intent.getComponent(), + componentToBeStaged, foundTask -> { splitSelectSource.alreadyRunningTaskId = foundTask == null ? INVALID_TASK_ID @@ -206,8 +209,9 @@ public class TaskbarUIController { */ public void triggerSecondAppForSplit(ItemInfoWithIcon info, Intent intent, View startingView) { RecentsView recents = getRecentsView(); + ComponentKey secondAppComponent = new ComponentKey(info.getTargetComponent(), info.user); recents.getSplitSelectController().findLastActiveTaskAndRunCallback( - info.getTargetComponent(), + secondAppComponent, foundTask -> { if (foundTask != null) { TaskView foundTaskView = recents.getTaskViewByTaskId(foundTask.key.id); diff --git a/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java b/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java index 951a9b61dc..93baf5b724 100644 --- a/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java +++ b/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java @@ -136,6 +136,7 @@ import com.android.launcher3.uioverrides.touchcontrollers.TaskViewTouchControlle import com.android.launcher3.uioverrides.touchcontrollers.TransposedQuickSwitchTouchController; import com.android.launcher3.uioverrides.touchcontrollers.TwoButtonNavbarTouchController; import com.android.launcher3.util.ActivityOptionsWrapper; +import com.android.launcher3.util.ComponentKey; import com.android.launcher3.util.DisplayController; import com.android.launcher3.util.IntSet; import com.android.launcher3.util.NavigationMode; @@ -574,10 +575,13 @@ public class QuickstepLauncher extends Launcher { @Override public void startSplitSelection(SplitSelectSource splitSelectSource) { RecentsView recentsView = getOverviewPanel(); + ComponentKey componentToBeStaged = new ComponentKey( + splitSelectSource.itemInfo.getTargetComponent(), + splitSelectSource.itemInfo.user); // Check if there is already an instance of this app running, if so, initiate the split // using that. mSplitSelectStateController.findLastActiveTaskAndRunCallback( - splitSelectSource.intent.getComponent(), + componentToBeStaged, foundTask -> { splitSelectSource.alreadyRunningTaskId = foundTask == null ? INVALID_TASK_ID diff --git a/quickstep/src/com/android/quickstep/util/SplitSelectStateController.java b/quickstep/src/com/android/quickstep/util/SplitSelectStateController.java index d1100c709b..5214f7c68c 100644 --- a/quickstep/src/com/android/quickstep/util/SplitSelectStateController.java +++ b/quickstep/src/com/android/quickstep/util/SplitSelectStateController.java @@ -29,7 +29,6 @@ import android.app.ActivityManager; import android.app.ActivityOptions; import android.app.ActivityThread; import android.app.PendingIntent; -import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.content.pm.PackageManager; @@ -58,6 +57,7 @@ import com.android.launcher3.statehandlers.DepthController; import com.android.launcher3.statemanager.StateManager; import com.android.launcher3.testing.TestLogging; import com.android.launcher3.testing.shared.TestProtocol; +import com.android.launcher3.util.ComponentKey; import com.android.launcher3.util.SplitConfigurationOptions; import com.android.launcher3.util.SplitConfigurationOptions.StagePosition; import com.android.quickstep.RecentsModel; @@ -162,7 +162,7 @@ public class SplitSelectStateController { * Used in various task-switching or splitscreen operations when we need to check if there is a * currently running Task of a certain type and use the most recent one. */ - public void findLastActiveTaskAndRunCallback(ComponentName componentName, + public void findLastActiveTaskAndRunCallback(ComponentKey componentKey, Consumer callback) { mRecentTasksModel.getTasks(taskGroups -> { Task lastActiveTask = null; @@ -170,12 +170,12 @@ public class SplitSelectStateController { for (int i = taskGroups.size() - 1; i >= 0; i--) { GroupTask groupTask = taskGroups.get(i); Task task1 = groupTask.task1; - if (isInstanceOfComponent(task1, componentName)) { + if (isInstanceOfComponent(task1, componentKey)) { lastActiveTask = task1; break; } Task task2 = groupTask.task2; - if (isInstanceOfComponent(task2, componentName)) { + if (isInstanceOfComponent(task2, componentKey)) { lastActiveTask = task2; break; } @@ -189,13 +189,14 @@ public class SplitSelectStateController { * Checks if a given Task is the most recently-active Task of type componentName. Used for * selecting already-running Tasks for splitscreen. */ - public boolean isInstanceOfComponent(@Nullable Task task, ComponentName componentName) { + public boolean isInstanceOfComponent(@Nullable Task task, ComponentKey componentKey) { // Exclude the task that is already staged if (task == null || task.key.id == mInitialTaskId) { return false; } - return task.key.baseIntent.getComponent().equals(componentName); + return task.key.baseIntent.getComponent().equals(componentKey.componentName) + && task.key.userId == componentKey.user.getIdentifier(); } /** diff --git a/quickstep/tests/src/com/android/quickstep/util/SplitSelectStateControllerTest.kt b/quickstep/tests/src/com/android/quickstep/util/SplitSelectStateControllerTest.kt index 5abdc93fb7..512df8e3d0 100644 --- a/quickstep/tests/src/com/android/quickstep/util/SplitSelectStateControllerTest.kt +++ b/quickstep/tests/src/com/android/quickstep/util/SplitSelectStateControllerTest.kt @@ -23,12 +23,14 @@ import android.content.Context import android.content.Intent import android.graphics.Rect import android.os.Handler +import android.os.UserHandle import androidx.test.ext.junit.runners.AndroidJUnit4 import com.android.launcher3.LauncherState import com.android.launcher3.logging.StatsLogManager import com.android.launcher3.model.data.ItemInfo import com.android.launcher3.statehandlers.DepthController import com.android.launcher3.statemanager.StateManager +import com.android.launcher3.util.ComponentKey import com.android.launcher3.util.SplitConfigurationOptions import com.android.launcher3.util.withArgCaptor import com.android.quickstep.RecentsModel @@ -60,6 +62,9 @@ class SplitSelectStateControllerTest { lateinit var splitSelectStateController: SplitSelectStateController + private val primaryUserHandle = UserHandle(ActivityManager.RunningTaskInfo().userId) + private val nonPrimaryUserHandle = UserHandle(ActivityManager.RunningTaskInfo().userId + 10) + @Before fun setup() { MockitoAnnotations.initMocks(this) @@ -77,6 +82,7 @@ class SplitSelectStateControllerTest { @Test fun activeTasks_noMatchingTasks() { + val nonMatchingComponent = ComponentKey(ComponentName("no", "match"), primaryUserHandle) val groupTask1 = generateGroupTask( ComponentName("pomegranate", "juice"), @@ -100,7 +106,7 @@ class SplitSelectStateControllerTest { val consumer = withArgCaptor>> { splitSelectStateController.findLastActiveTaskAndRunCallback( - ComponentName("no", "match"), + nonMatchingComponent, taskConsumer ) verify(recentsModel).getTasks(capture()) @@ -114,6 +120,8 @@ class SplitSelectStateControllerTest { fun activeTasks_singleMatchingTask() { val matchingPackage = "hotdog" val matchingClass = "juice" + val matchingComponent = + ComponentKey(ComponentName(matchingPackage, matchingClass), primaryUserHandle) val groupTask1 = generateGroupTask( ComponentName(matchingPackage, matchingClass), @@ -149,7 +157,100 @@ class SplitSelectStateControllerTest { val consumer = withArgCaptor>> { splitSelectStateController.findLastActiveTaskAndRunCallback( - ComponentName(matchingPackage, matchingClass), + matchingComponent, + taskConsumer + ) + verify(recentsModel).getTasks(capture()) + } + + // Send our mocked tasks + consumer.accept(tasks) + } + + @Test + fun activeTasks_skipTaskWithDifferentUser() { + val matchingPackage = "hotdog" + val matchingClass = "juice" + val nonPrimaryUserComponent = + ComponentKey(ComponentName(matchingPackage, matchingClass), nonPrimaryUserHandle) + val groupTask1 = + generateGroupTask( + ComponentName(matchingPackage, matchingClass), + ComponentName("pomegranate", "juice") + ) + val groupTask2 = + generateGroupTask( + ComponentName("pumpkin", "pie"), + ComponentName("personal", "computer") + ) + val tasks: ArrayList = ArrayList() + tasks.add(groupTask1) + tasks.add(groupTask2) + + // Assertions happen in the callback we get from what we pass into + // #findLastActiveTaskAndRunCallback + val taskConsumer = + Consumer { assertNull("No tasks should have matched", it /*task*/) } + + // Capture callback from recentsModel#getTasks() + val consumer = + withArgCaptor>> { + splitSelectStateController.findLastActiveTaskAndRunCallback( + nonPrimaryUserComponent, + taskConsumer + ) + verify(recentsModel).getTasks(capture()) + } + + // Send our mocked tasks + consumer.accept(tasks) + } + + @Test + fun activeTasks_findTaskAsNonPrimaryUser() { + val matchingPackage = "hotdog" + val matchingClass = "juice" + val nonPrimaryUserComponent = + ComponentKey(ComponentName(matchingPackage, matchingClass), nonPrimaryUserHandle) + val groupTask1 = + generateGroupTask( + ComponentName(matchingPackage, matchingClass), + nonPrimaryUserHandle, + ComponentName("pomegranate", "juice"), + nonPrimaryUserHandle + ) + val groupTask2 = + generateGroupTask( + ComponentName("pumpkin", "pie"), + ComponentName("personal", "computer") + ) + val tasks: ArrayList = ArrayList() + tasks.add(groupTask1) + tasks.add(groupTask2) + + // Assertions happen in the callback we get from what we pass into + // #findLastActiveTaskAndRunCallback + val taskConsumer = + Consumer { + assertEquals( + "ComponentName package mismatched", + it.key.baseIntent.component.packageName, + matchingPackage + ) + assertEquals( + "ComponentName class mismatched", + it.key.baseIntent.component.className, + matchingClass + ) + assertEquals("userId mismatched", it.key.userId, nonPrimaryUserHandle.identifier) + assertEquals(it, groupTask1.task1) + } + + // Capture callback from recentsModel#getTasks() + val consumer = + withArgCaptor>> { + splitSelectStateController.findLastActiveTaskAndRunCallback( + nonPrimaryUserComponent, taskConsumer ) verify(recentsModel).getTasks(capture()) @@ -163,6 +264,8 @@ class SplitSelectStateControllerTest { fun activeTasks_multipleMatchMostRecentTask() { val matchingPackage = "hotdog" val matchingClass = "juice" + val matchingComponent = + ComponentKey(ComponentName(matchingPackage, matchingClass), primaryUserHandle) val groupTask1 = generateGroupTask( ComponentName(matchingPackage, matchingClass), @@ -198,7 +301,7 @@ class SplitSelectStateControllerTest { val consumer = withArgCaptor>> { splitSelectStateController.findLastActiveTaskAndRunCallback( - ComponentName(matchingPackage, matchingClass), + matchingComponent, taskConsumer ) verify(recentsModel).getTasks(capture()) @@ -245,6 +348,7 @@ class SplitSelectStateControllerTest { assertFalse(splitSelectStateController.isSplitSelectActive) } + // Generate GroupTask with default userId. private fun generateGroupTask( task1ComponentName: ComponentName, task2ComponentName: ComponentName @@ -268,4 +372,34 @@ class SplitSelectStateControllerTest { SplitConfigurationOptions.SplitBounds(Rect(), Rect(), -1, -1) ) } + + // Generate GroupTask with custom user handles. + private fun generateGroupTask( + task1ComponentName: ComponentName, + userHandle1: UserHandle, + task2ComponentName: ComponentName, + userHandle2: UserHandle + ): GroupTask { + val task1 = Task() + var taskInfo = ActivityManager.RunningTaskInfo() + // Apply custom userHandle1 + taskInfo.userId = userHandle1.identifier + var intent = Intent() + intent.component = task1ComponentName + taskInfo.baseIntent = intent + task1.key = Task.TaskKey(taskInfo) + val task2 = Task() + taskInfo = ActivityManager.RunningTaskInfo() + // Apply custom userHandle2 + taskInfo.userId = userHandle2.identifier + intent = Intent() + intent.component = task2ComponentName + taskInfo.baseIntent = intent + task2.key = Task.TaskKey(taskInfo) + return GroupTask( + task1, + task2, + SplitConfigurationOptions.SplitBounds(Rect(), Rect(), -1, -1) + ) + } } From 5bd9a225a8c4a21afd88dda28ea643c22d232082 Mon Sep 17 00:00:00 2001 From: Stefan Andonian Date: Thu, 23 Feb 2023 00:58:33 +0000 Subject: [PATCH 5/7] 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()); From f01ca85c209c31c1274728dc0648a59a343df6b9 Mon Sep 17 00:00:00 2001 From: Jon Miranda Date: Wed, 1 Mar 2023 17:17:36 -0800 Subject: [PATCH 6/7] Prevent QSB from being a part of the icon reveal animation. Fixes: 270694593 Test: taskbar on foldable and tablet Change-Id: I1782f715454da1240174be1b4f496c910be3764a --- .../launcher3/taskbar/TaskbarStashController.java | 2 +- .../android/launcher3/taskbar/TaskbarViewController.java | 9 +++++++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarStashController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarStashController.java index bbf861bd8b..115db25b52 100644 --- a/quickstep/src/com/android/launcher3/taskbar/TaskbarStashController.java +++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarStashController.java @@ -723,7 +723,7 @@ public class TaskbarStashController implements TaskbarControllers.LoggableTaskba } } play(as, mControllers.taskbarViewController - .createRevealAnimToIsStashed(isStashed, isInApp()), 0, duration, EMPHASIZED); + .createRevealAnimToIsStashed(isStashed), 0, duration, EMPHASIZED); if (skipInterpolator != null) { as.setInterpolator(skipInterpolator); diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarViewController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarViewController.java index c7088382d0..ac92374e1c 100644 --- a/quickstep/src/com/android/launcher3/taskbar/TaskbarViewController.java +++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarViewController.java @@ -17,6 +17,7 @@ package com.android.launcher3.taskbar; import static com.android.launcher3.LauncherAnimUtils.SCALE_PROPERTY; import static com.android.launcher3.LauncherAnimUtils.VIEW_ALPHA; +import static com.android.launcher3.LauncherAnimUtils.VIEW_TRANSLATE_Y; import static com.android.launcher3.Utilities.squaredHypot; import static com.android.launcher3.anim.AnimatedFloat.VALUE; import static com.android.launcher3.anim.AnimatorListeners.forEndCallback; @@ -303,7 +304,7 @@ public class TaskbarViewController implements TaskbarControllers.LoggableTaskbar * @param isStashed When true, the icon crops vertically to the size of the stashed handle. * When false, the reverse happens. */ - public AnimatorSet createRevealAnimToIsStashed(boolean isStashed, boolean isInApp) { + public AnimatorSet createRevealAnimToIsStashed(boolean isStashed) { AnimatorSet as = new AnimatorSet(); Rect stashedBounds = new Rect(); @@ -317,6 +318,10 @@ public class TaskbarViewController implements TaskbarControllers.LoggableTaskbar for (int i = numChildren - 1; i >= 0; i--) { View child = mTaskbarView.getChildAt(i); + if (child == mTaskbarView.getQsb()) { + continue; + } + // Crop the icons to/from the nav handle shape. as.play(createRevealAnimForView(child, isStashed)); @@ -437,7 +442,7 @@ public class TaskbarViewController implements TaskbarControllers.LoggableTaskbar float scale = ((float) taskbarDp.iconSizePx) / launcherDp.hotseatQsbVisualHeight; setter.addFloat(child, SCALE_PROPERTY, scale, 1f, interpolator); - setter.setFloat(child, ICON_TRANSLATE_Y, mTaskbarBottomMargin, interpolator); + setter.setFloat(child, VIEW_TRANSLATE_Y, mTaskbarBottomMargin, interpolator); if (mIsHotseatIconOnTopWhenAligned) { setter.addFloat(child, VIEW_ALPHA, 0f, 1f, From b4f3807df2c18b39e1a299a108a33590049d6e04 Mon Sep 17 00:00:00 2001 From: Pat Manning Date: Wed, 1 Mar 2023 18:06:52 +0000 Subject: [PATCH 7/7] Add modal action for grid only overview. This is an issue for tablets on landscape. Fix: 271314156 Test: manual. Change-Id: I5c3c1a9ff4b193439bdcc72f17b20bc8da4d16f5 --- .../src/com/android/quickstep/TaskOverlayFactory.java | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/quickstep/src/com/android/quickstep/TaskOverlayFactory.java b/quickstep/src/com/android/quickstep/TaskOverlayFactory.java index 72330efc26..6e47ff4716 100644 --- a/quickstep/src/com/android/quickstep/TaskOverlayFactory.java +++ b/quickstep/src/com/android/quickstep/TaskOverlayFactory.java @@ -81,7 +81,8 @@ public class TaskOverlayFactory implements ResourceBasedOverride { boolean isTablet = activity.getDeviceProfile().isTablet; boolean isGridOnlyOverview = isTablet && FeatureFlags.ENABLE_GRID_ONLY_OVERVIEW.get(); - // Add overview actions to the menu when in in-place rotate landscape mode. + // Add overview actions to the menu when in in-place rotate landscape mode, or in + // grid-only overview. if ((!canLauncherRotate && isInLandscape) || isGridOnlyOverview) { // Add screenshot action to task menu. List screenshotShortcuts = TaskShortcutFactory.SCREENSHOT @@ -90,8 +91,9 @@ public class TaskOverlayFactory implements ResourceBasedOverride { shortcuts.addAll(screenshotShortcuts); } - // Add modal action only if display orientation is the same as the device orientation. - if (orientedState.getDisplayRotation() == ROTATION_0) { + // Add modal action only if display orientation is the same as the device orientation, + // or in grid-only overview. + if (orientedState.getDisplayRotation() == ROTATION_0 || isGridOnlyOverview) { List modalShortcuts = TaskShortcutFactory.MODAL .getShortcuts(activity, taskContainer); if (modalShortcuts != null) {