Merge "Prevent QSB from being a part of the icon reveal animation." into tm-qpr-dev
diff --git a/OWNERS b/OWNERS
index 962d63a..dd9fbf9 100644
--- a/OWNERS
+++ b/OWNERS
@@ -36,4 +36,4 @@
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/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java
index ab52adb..dde7d94 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.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.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 @@
* (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/TaskbarManager.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarManager.java
index 4110822..8c91833 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarManager.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarManager.java
@@ -130,10 +130,10 @@
? LauncherAppState.getIDP(mContext).getDeviceProfile(mContext)
: null;
int configDiff = mOldConfig.diff(newConfig);
+ int configDiffForRecreate = configDiff;
int configsRequiringRecreate = ActivityInfo.CONFIG_ASSETS_PATHS
| ActivityInfo.CONFIG_LAYOUT_DIRECTION | ActivityInfo.CONFIG_UI_MODE
| ActivityInfo.CONFIG_SCREEN_SIZE;
- boolean requiresRecreate = (configDiff & configsRequiringRecreate) != 0;
if ((configDiff & ActivityInfo.CONFIG_SCREEN_SIZE) != 0
&& mTaskbarActivityContext != null && dp != null
&& !isPhoneMode(dp)) {
@@ -146,12 +146,19 @@
int oldWidth = isOrientationChange ? oldDp.heightPx : oldDp.widthPx;
int oldHeight = isOrientationChange ? oldDp.widthPx : oldDp.heightPx;
if (dp.widthPx == oldWidth && dp.heightPx == oldHeight) {
- configDiff &= ~ActivityInfo.CONFIG_SCREEN_SIZE;
- requiresRecreate = (configDiff & configsRequiringRecreate) != 0;
+ configDiffForRecreate &= ~ActivityInfo.CONFIG_SCREEN_SIZE;
+ }
+ }
+ if ((configDiff & ActivityInfo.CONFIG_UI_MODE) != 0) {
+ // Only recreate for theme changes, not other UI mode changes such as docking.
+ int oldUiNightMode = (mOldConfig.uiMode & Configuration.UI_MODE_NIGHT_MASK);
+ int newUiNightMode = (newConfig.uiMode & Configuration.UI_MODE_NIGHT_MASK);
+ if (oldUiNightMode == newUiNightMode) {
+ configDiffForRecreate &= ~ActivityInfo.CONFIG_UI_MODE;
}
}
- if (requiresRecreate) {
+ if ((configDiffForRecreate & configsRequiringRecreate) != 0) {
recreateTaskbar();
} else {
// Config change might be handled without re-creating the taskbar
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarUIController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarUIController.java
index 3479255..b552e9b 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarUIController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarUIController.java
@@ -30,16 +30,15 @@
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 @@
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 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 951a9b6..93baf5b 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.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 @@
@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/launcher3/uioverrides/flags/FlagsFactory.java b/quickstep/src/com/android/launcher3/uioverrides/flags/FlagsFactory.java
index 29ef2b4..84b873d 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 @@
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<DebugFlag> sDebugFlags = new ArrayList<>();
+
+ private final Set<String> 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 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 @@
}
}
+ /**
+ * 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<DebugFlag> getDebugFlags() {
if (!Utilities.IS_DEBUG_DEVICE) {
return Collections.emptyList();
@@ -121,4 +156,28 @@
}
}
}
+
+ 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/quickstep/src/com/android/launcher3/uioverrides/states/SplitScreenSelectState.java b/quickstep/src/com/android/launcher3/uioverrides/states/SplitScreenSelectState.java
index 8babd34..3ae221b 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/states/SplitScreenSelectState.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/states/SplitScreenSelectState.java
@@ -53,4 +53,9 @@
return SplitAnimationTimings.ABORT_DURATION;
}
}
+
+ @Override
+ public boolean shouldPreserveDataStateOnReapply() {
+ return true;
+ }
}
diff --git a/quickstep/src/com/android/quickstep/BootAwarePreloader.kt b/quickstep/src/com/android/quickstep/BootAwarePreloader.kt
new file mode 100644
index 0000000..35404a9
--- /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 ff81e08..accab38 100644
--- a/quickstep/src/com/android/quickstep/TouchInteractionService.java
+++ b/quickstep/src/com/android/quickstep/TouchInteractionService.java
@@ -410,6 +410,7 @@
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/quickstep/src/com/android/quickstep/util/SplitSelectStateController.java b/quickstep/src/com/android/quickstep/util/SplitSelectStateController.java
index d1100c7..5214f7c 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.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.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 @@
* 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<Task> callback) {
mRecentTasksModel.getTasks(taskGroups -> {
Task lastActiveTask = null;
@@ -170,12 +170,12 @@
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 @@
* 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/src/com/android/quickstep/views/RecentsView.java b/quickstep/src/com/android/quickstep/views/RecentsView.java
index ffb6e2a..843b2fb 100644
--- a/quickstep/src/com/android/quickstep/views/RecentsView.java
+++ b/quickstep/src/com/android/quickstep/views/RecentsView.java
@@ -4578,7 +4578,6 @@
return true;
}
- /** TODO(b/181707736) More gracefully handle exiting split selection state */
@SuppressLint("WrongCall")
protected void resetFromSplitSelectionState() {
if (mSplitSelectSource != null || mSplitHiddenTaskViewIndex != -1) {
diff --git a/quickstep/tests/src/com/android/quickstep/util/SplitSelectStateControllerTest.kt b/quickstep/tests/src/com/android/quickstep/util/SplitSelectStateControllerTest.kt
index 5abdc93..512df8e 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.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 @@
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 @@
@Test
fun activeTasks_noMatchingTasks() {
+ val nonMatchingComponent = ComponentKey(ComponentName("no", "match"), primaryUserHandle)
val groupTask1 =
generateGroupTask(
ComponentName("pomegranate", "juice"),
@@ -100,7 +106,7 @@
val consumer =
withArgCaptor<Consumer<ArrayList<GroupTask>>> {
splitSelectStateController.findLastActiveTaskAndRunCallback(
- ComponentName("no", "match"),
+ nonMatchingComponent,
taskConsumer
)
verify(recentsModel).getTasks(capture())
@@ -114,6 +120,8 @@
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 @@
val consumer =
withArgCaptor<Consumer<ArrayList<GroupTask>>> {
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<GroupTask> = ArrayList()
+ tasks.add(groupTask1)
+ tasks.add(groupTask2)
+
+ // Assertions happen in the callback we get from what we pass into
+ // #findLastActiveTaskAndRunCallback
+ val taskConsumer =
+ Consumer<Task> { assertNull("No tasks should have matched", it /*task*/) }
+
+ // Capture callback from recentsModel#getTasks()
+ val consumer =
+ withArgCaptor<Consumer<ArrayList<GroupTask>>> {
+ 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<GroupTask> = ArrayList()
+ tasks.add(groupTask1)
+ tasks.add(groupTask2)
+
+ // Assertions happen in the callback we get from what we pass into
+ // #findLastActiveTaskAndRunCallback
+ val taskConsumer =
+ Consumer<Task> {
+ 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<Consumer<ArrayList<GroupTask>>> {
+ splitSelectStateController.findLastActiveTaskAndRunCallback(
+ nonPrimaryUserComponent,
taskConsumer
)
verify(recentsModel).getTasks(capture())
@@ -163,6 +264,8 @@
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 @@
val consumer =
withArgCaptor<Consumer<ArrayList<GroupTask>>> {
splitSelectStateController.findLastActiveTaskAndRunCallback(
- ComponentName(matchingPackage, matchingClass),
+ matchingComponent,
taskConsumer
)
verify(recentsModel).getTasks(capture())
@@ -245,6 +348,7 @@
assertFalse(splitSelectStateController.isSplitSelectActive)
}
+ // Generate GroupTask with default userId.
private fun generateGroupTask(
task1ComponentName: ComponentName,
task2ComponentName: ComponentName
@@ -268,4 +372,34 @@
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)
+ )
+ }
}
diff --git a/src/com/android/launcher3/InvariantDeviceProfile.java b/src/com/android/launcher3/InvariantDeviceProfile.java
index 4ac7f07..a498323 100644
--- a/src/com/android/launcher3/InvariantDeviceProfile.java
+++ b/src/com/android/launcher3/InvariantDeviceProfile.java
@@ -59,6 +59,7 @@
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 @@
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 2b98d98..c81ad01 100644
--- a/src/com/android/launcher3/LauncherAppState.java
+++ b/src/com/android/launcher3/LauncherAppState.java
@@ -44,6 +44,7 @@
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 @@
}
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 5680c18..e675add 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 @@
*
* 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 <T> get(item: ContextualItem<T>): T = getInner(item, item.defaultValueFromContext(context))
+ fun <T> get(item: ContextualItem<T>): T =
+ getInner(item, item.defaultValueFromContext(encryptedContext))
/** Wrapper around `getInner` for an `Item` */
fun <T> get(item: ConstantItem<T>): T = getInner(item, item.defaultValue)
@@ -35,7 +76,7 @@
*/
@Suppress("IMPLICIT_CAST_TO_ANY", "UNCHECKED_CAST")
private fun <T> 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 @@
fun put(vararg itemsToValues: Pair<Item, Any>): Unit =
prepareToPutValues(itemsToValues).forEach { it.apply() }
- /**
- * Stores the value provided in `SharedPreferences` according to the item configuration provided
- * It is asynchronous, so the caller can't assume that the value put is immediately available.
- */
- fun <T : Any> put(item: Item, value: T): Unit =
- context
- .getSharedPreferences(item.sharedPrefFile, Context.MODE_PRIVATE)
- .edit()
- .putValue(item, value)
- .apply()
+ /** See referenced `put` method above. */
+ fun <T : Any> 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 @@
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<SharedPreferences.Editor>` 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<out Pair<Item, Any>>
- ): List<SharedPreferences.Editor> =
- itemsToValues
- .groupBy { it.first.sharedPrefFile }
- .map { fileToItemValueList ->
- context
- .getSharedPreferences(fileToItemValueList.key, Context.MODE_PRIVATE)
- .edit()
- .apply {
- fileToItemValueList.value.forEach { itemToValue ->
- putValue(itemToValue.first, itemToValue.second)
- }
- }
+ updates: Array<out Pair<Item, Any>>
+ ): List<SharedPreferences.Editor> {
+ 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<Item, Any> ->
+ putValue(itemToValue.first, itemToValue.second)
+ }
+ }
+ }
+ }
/**
* Handles adding values to `SharedPreferences` regardless of type. This method is especially
@@ -117,10 +158,10 @@
@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 @@
java.lang.Float::class.java -> putFloat(item.sharedPrefKey, value as Float)
Long::class.java,
java.lang.Long::class.java -> putLong(item.sharedPrefKey, value as Long)
- Set::class.java -> putStringSet(item.sharedPrefKey, value as Set<String>)
+ Set::class.java -> putStringSet(item.sharedPrefKey, value as? Set<String>)
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 @@
*/
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 @@
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 @@
*/
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 @@
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<out Item>): List<SharedPreferences.Editor> =
- items
- .groupBy { it.sharedPrefFile }
- .map { (file, items) ->
- context.getSharedPreferences(file, Context.MODE_PRIVATE).edit().also { editor ->
- items.forEach { item -> editor.remove(item.sharedPrefKey) }
- }
+ private fun prepareToRemove(items: Array<out Item>): List<SharedPreferences.Editor> {
+ 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 <T> backedUpItem(sharedPrefKey: String, defaultValue: T): ConstantItem<T> =
- ConstantItem(sharedPrefKey, true, defaultValue)
+ fun <T> backedUpItem(
+ sharedPrefKey: String,
+ defaultValue: T,
+ isBootAware: Boolean = false
+ ): ConstantItem<T> =
+ ConstantItem(sharedPrefKey, isBackedUp = true, defaultValue, isBootAware)
@JvmStatic
fun <T> backedUpItem(
sharedPrefKey: String,
type: Class<out T>,
+ isBootAware: Boolean = false,
defaultValueFromContext: (c: Context) -> T
- ): ContextualItem<T> = ContextualItem(sharedPrefKey, true, defaultValueFromContext, type)
+ ): ContextualItem<T> =
+ ContextualItem(
+ sharedPrefKey,
+ isBackedUp = true,
+ defaultValueFromContext,
+ isBootAware,
+ type
+ )
@VisibleForTesting
@JvmStatic
- fun <T> nonRestorableItem(sharedPrefKey: String, defaultValue: T): ConstantItem<T> =
- ConstantItem(sharedPrefKey, false, defaultValue)
+ fun <T> nonRestorableItem(
+ sharedPrefKey: String,
+ defaultValue: T,
+ isBootAware: Boolean = false
+ ): ConstantItem<T> =
+ ConstantItem(sharedPrefKey, isBackedUp = false, defaultValue, isBootAware)
@Deprecated("Don't use shared preferences directly. Use other LauncherPref methods.")
@JvmStatic
@@ -255,7 +352,7 @@
// 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 @@
// 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<ConstantItem<*>> = 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 @@
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<out T> = defaultValue!!::class.java
-) : Item()
+) : Item() {
+ init {
+ if (isBootAware && isBootAwareStartupDataEnabled) {
+ BOOT_AWARE_ITEMS.add(this)
+ }
+ }
+}
data class ContextualItem<T>(
override val sharedPrefKey: String,
override val isBackedUp: Boolean,
private val defaultSupplier: (c: Context) -> T,
+ override val isBootAware: Boolean,
override val type: Class<out T>
) : Item() {
private var default: T? = null
diff --git a/src/com/android/launcher3/allapps/AllAppsTransitionController.java b/src/com/android/launcher3/allapps/AllAppsTransitionController.java
index 4430a94..54bf6a8 100644
--- a/src/com/android/launcher3/allapps/AllAppsTransitionController.java
+++ b/src/com/android/launcher3/allapps/AllAppsTransitionController.java
@@ -406,6 +406,8 @@
Interpolator allAppsFade = config.getInterpolator(ANIM_ALL_APPS_FADE, LINEAR);
setter.setFloat(getAppsViewProgressAlpha(), MultiPropertyFactory.MULTI_PROPERTY_VALUE,
hasAllAppsContent ? 1 : 0, allAppsFade);
+ setter.setFloat(getAppsViewPullbackAlpha(), MultiPropertyFactory.MULTI_PROPERTY_VALUE,
+ hasAllAppsContent ? 1 : 0, allAppsFade);
boolean shouldProtectHeader =
ALL_APPS == state || mLauncher.getStateManager().getState() == ALL_APPS;
diff --git a/src/com/android/launcher3/config/FeatureFlags.java b/src/com/android/launcher3/config/FeatureFlags.java
index af24334..c98b60f 100644
--- a/src/com/android/launcher3/config/FeatureFlags.java
+++ b/src/com/android/launcher3/config/FeatureFlags.java
@@ -247,10 +247,6 @@
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 @@
"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 @@
"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;
@@ -400,4 +388,20 @@
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/com/android/launcher3/statemanager/BaseState.java b/src/com/android/launcher3/statemanager/BaseState.java
index 2390425..a01d402 100644
--- a/src/com/android/launcher3/statemanager/BaseState.java
+++ b/src/com/android/launcher3/statemanager/BaseState.java
@@ -70,4 +70,12 @@
default boolean showTaskThumbnailSplash() {
return false;
}
+
+ /**
+ * For this state, whether member variables and other forms of data state should be preserved
+ * or wiped when the state is reapplied. (See {@link StateManager#reapplyState()})
+ */
+ default boolean shouldPreserveDataStateOnReapply() {
+ return false;
+ }
}
diff --git a/src/com/android/launcher3/statemanager/StateManager.java b/src/com/android/launcher3/statemanager/StateManager.java
index 34ac8c2..89d89d6 100644
--- a/src/com/android/launcher3/statemanager/StateManager.java
+++ b/src/com/android/launcher3/statemanager/StateManager.java
@@ -184,6 +184,13 @@
public void reapplyState(boolean cancelCurrentAnimation) {
boolean wasInAnimation = mConfig.currentAnimation != null;
if (cancelCurrentAnimation) {
+ // Animation canceling can trigger a cleanup routine, causing problems when we are in a
+ // launcher state that relies on member variable data. So if we are in one of those
+ // states, accelerate the current animation to its end point rather than canceling it
+ // outright.
+ if (mState.shouldPreserveDataStateOnReapply() && mConfig.currentAnimation != null) {
+ mConfig.currentAnimation.end();
+ }
mAtomicAnimationFactory.cancelAllStateElementAnimation();
cancelAnimation();
}
diff --git a/src/com/android/launcher3/util/LockedUserState.kt b/src/com/android/launcher3/util/LockedUserState.kt
index 7b49583..f5e13d2 100644
--- a/src/com/android/launcher3/util/LockedUserState.kt
+++ b/src/com/android/launcher3/util/LockedUserState.kt
@@ -50,7 +50,9 @@
}
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/src_ui_overrides/com/android/launcher3/uioverrides/flags/FlagsFactory.java b/src_ui_overrides/com/android/launcher3/uioverrides/flags/FlagsFactory.java
index 599969b..4463adc 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;
@@ -43,6 +44,14 @@
}
/**
+ * 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
*/
public static void dump(PrintWriter pw) { }
diff --git a/tests/src/com/android/launcher3/LauncherPrefsTest.kt b/tests/src/com/android/launcher3/LauncherPrefsTest.kt
index e8372b1..41ef3de 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 @@
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 @@
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 @@
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 @@
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 @@
)
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 @@
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 fa4c519..545b645 100644
--- a/tests/src/com/android/launcher3/util/LauncherModelHelper.java
+++ b/tests/src/com/android/launcher3/util/LauncherModelHelper.java
@@ -510,7 +510,7 @@
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());