diff --git a/Android.bp b/Android.bp
index 79c1b9fc6d..877f7bbbef 100644
--- a/Android.bp
+++ b/Android.bp
@@ -155,6 +155,11 @@ android_library {
"animationlib",
"SystemUI-statsd",
"launcher-testing-shared",
+ "androidx.lifecycle_lifecycle-common-java8",
+ "androidx.lifecycle_lifecycle-extensions",
+ "androidx.lifecycle_lifecycle-runtime-ktx",
+ "kotlinx_coroutines_android",
+ "kotlinx_coroutines",
"com_android_launcher3_flags_lib",
"com_android_wm_shell_flags_lib",
"android.appwidget.flags-aconfig-java",
diff --git a/go/quickstep/res/layout/overview_actions_container.xml b/go/quickstep/res/layout/overview_actions_container.xml
index 077cfaee19..df0912478b 100644
--- a/go/quickstep/res/layout/overview_actions_container.xml
+++ b/go/quickstep/res/layout/overview_actions_container.xml
@@ -126,7 +126,7 @@
style="@style/GoOverviewActionButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
- android:drawableStart="@drawable/ic_save_app_pair"
+ android:drawableStart="@drawable/ic_save_app_pair_up_down"
android:text="@string/action_save_app_pair"
android:theme="@style/ThemeControlHighlightWorkspaceColor"
android:visibility="gone" />
diff --git a/quickstep/res/drawable/ic_save_app_pair.xml b/quickstep/res/drawable/ic_save_app_pair.xml
deleted file mode 100644
index 4a7ee1ac70..0000000000
--- a/quickstep/res/drawable/ic_save_app_pair.xml
+++ /dev/null
@@ -1,28 +0,0 @@
-
-
-
-
-
-
-
diff --git a/quickstep/res/drawable/ic_save_app_pair_left_right.xml b/quickstep/res/drawable/ic_save_app_pair_left_right.xml
new file mode 100644
index 0000000000..b104f44b76
--- /dev/null
+++ b/quickstep/res/drawable/ic_save_app_pair_left_right.xml
@@ -0,0 +1,28 @@
+
+
+
+
+
+
+
diff --git a/quickstep/res/drawable/ic_save_app_pair_up_down.xml b/quickstep/res/drawable/ic_save_app_pair_up_down.xml
new file mode 100644
index 0000000000..86f110ce5e
--- /dev/null
+++ b/quickstep/res/drawable/ic_save_app_pair_up_down.xml
@@ -0,0 +1,28 @@
+
+
+
+
+
+
+
diff --git a/quickstep/res/layout/overview_actions_container.xml b/quickstep/res/layout/overview_actions_container.xml
index 758622b64b..5d489f5f34 100644
--- a/quickstep/res/layout/overview_actions_container.xml
+++ b/quickstep/res/layout/overview_actions_container.xml
@@ -23,6 +23,7 @@
android:id="@+id/action_buttons"
android:layout_width="match_parent"
android:layout_height="@dimen/overview_actions_height"
+ android:layout_gravity="bottom"
android:gravity="center_horizontal"
android:orientation="horizontal">
diff --git a/quickstep/res/layout/task_view_menu_option.xml b/quickstep/res/layout/task_view_menu_option.xml
index 30ab4b102e..ffe240180e 100644
--- a/quickstep/res/layout/task_view_menu_option.xml
+++ b/quickstep/res/layout/task_view_menu_option.xml
@@ -41,6 +41,8 @@
android:layout_marginStart="@dimen/task_menu_option_text_start_margin"
android:textSize="14sp"
android:textColor="?androidprv:attr/materialColorOnSurface"
- android:focusable="false" />
+ android:focusable="false"
+ android:gravity="start"
+ android:ellipsize="end" />
diff --git a/quickstep/src/com/android/launcher3/HomeTransitionController.java b/quickstep/src/com/android/launcher3/HomeTransitionController.java
index 2b50283acd..c4a2e9e17e 100644
--- a/quickstep/src/com/android/launcher3/HomeTransitionController.java
+++ b/quickstep/src/com/android/launcher3/HomeTransitionController.java
@@ -21,7 +21,7 @@ import androidx.annotation.Nullable;
import com.android.launcher3.uioverrides.QuickstepLauncher;
import com.android.quickstep.SystemUiProxy;
-import com.android.wm.shell.transition.IHomeTransitionListener;
+import com.android.wm.shell.shared.IHomeTransitionListener;
/**
* Controls launcher response to home activity visibility changing.
diff --git a/quickstep/src/com/android/launcher3/uioverrides/states/AllAppsState.java b/quickstep/src/com/android/launcher3/uioverrides/states/AllAppsState.java
index b6002e8b76..7875daee4a 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/states/AllAppsState.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/states/AllAppsState.java
@@ -32,6 +32,8 @@ import com.android.launcher3.views.ActivityContext;
import com.android.quickstep.util.BaseDepthController;
import com.android.systemui.shared.system.InteractionJankMonitorWrapper;
+import java.util.concurrent.TimeUnit;
+
/**
* Definition for AllApps state
*/
@@ -39,6 +41,8 @@ public class AllAppsState extends LauncherState {
private static final int STATE_FLAGS =
FLAG_WORKSPACE_INACCESSIBLE | FLAG_CLOSE_POPUPS | FLAG_HOTSEAT_INACCESSIBLE;
+ private static final long BACK_CUJ_TIMEOUT_MS = TimeUnit.SECONDS.toMillis(5);
+
public AllAppsState(int id) {
super(id, LAUNCHER_STATE_ALLAPPS, STATE_FLAGS);
@@ -53,14 +57,33 @@ public class AllAppsState extends LauncherState {
}
@Override
- public void onBackPressed(Launcher launcher) {
+ public void onBackStarted(Launcher launcher) {
+ // Because the back gesture can take longer time depending on when user release the finger,
+ // we pass BACK_CUJ_TIMEOUT_MS as timeout to the jank monitor.
InteractionJankMonitorWrapper.begin(launcher.getAppsView(),
- Cuj.CUJ_LAUNCHER_CLOSE_ALL_APPS_BACK);
- super.onBackPressed(launcher);
+ Cuj.CUJ_LAUNCHER_CLOSE_ALL_APPS_BACK, BACK_CUJ_TIMEOUT_MS);
+ super.onBackStarted(launcher);
}
@Override
- protected void onBackPressCompleted(boolean success) {
+ public void onBackInvoked(Launcher launcher) {
+ // In predictive back swipe, onBackInvoked() will be called after onBackStarted().
+ // Because the 2nd InteractionJankMonitor.begin() will be ignore within timeout, it's safe
+ // to call InteractionJankMonitorWrapper.begin here.
+ InteractionJankMonitorWrapper.begin(launcher.getAppsView(),
+ Cuj.CUJ_LAUNCHER_CLOSE_ALL_APPS_BACK);
+ super.onBackInvoked(launcher);
+ }
+
+ /** Called when predictive back swipe is cancelled. */
+ @Override
+ public void onBackCancelled(Launcher launcher) {
+ super.onBackCancelled(launcher);
+ InteractionJankMonitorWrapper.cancel(Cuj.CUJ_LAUNCHER_CLOSE_ALL_APPS_BACK);
+ }
+
+ @Override
+ protected void onBackAnimationCompleted(boolean success) {
if (success) {
// Animation was successful.
InteractionJankMonitorWrapper.end(Cuj.CUJ_LAUNCHER_CLOSE_ALL_APPS_BACK);
diff --git a/quickstep/src/com/android/launcher3/uioverrides/states/OverviewModalTaskState.java b/quickstep/src/com/android/launcher3/uioverrides/states/OverviewModalTaskState.java
index 856b519d88..c63eaebc89 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/states/OverviewModalTaskState.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/states/OverviewModalTaskState.java
@@ -60,7 +60,7 @@ public class OverviewModalTaskState extends OverviewState {
}
@Override
- public void onBackPressed(Launcher launcher) {
+ public void onBackInvoked(Launcher launcher) {
launcher.getStateManager().goToState(LauncherState.OVERVIEW);
}
diff --git a/quickstep/src/com/android/launcher3/uioverrides/states/OverviewState.java b/quickstep/src/com/android/launcher3/uioverrides/states/OverviewState.java
index 8c2efc2910..d0eef8e3bb 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/states/OverviewState.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/states/OverviewState.java
@@ -199,7 +199,7 @@ public class OverviewState extends LauncherState {
}
@Override
- public void onBackPressed(Launcher launcher) {
+ public void onBackInvoked(Launcher launcher) {
RecentsView recentsView = launcher.getOverviewPanel();
TaskView taskView = recentsView.getRunningTaskView();
if (taskView != null) {
@@ -209,7 +209,7 @@ public class OverviewState extends LauncherState {
recentsView.snapToPage(recentsView.indexOfChild(taskView));
}
} else {
- super.onBackPressed(launcher);
+ super.onBackInvoked(launcher);
}
}
diff --git a/quickstep/src/com/android/quickstep/AllAppsActionManager.kt b/quickstep/src/com/android/quickstep/AllAppsActionManager.kt
index fd2ed3ac18..6fd68d551e 100644
--- a/quickstep/src/com/android/quickstep/AllAppsActionManager.kt
+++ b/quickstep/src/com/android/quickstep/AllAppsActionManager.kt
@@ -81,6 +81,7 @@ class AllAppsActionManager(
}
fun onDestroy() {
+ isActionRegistered = false
context
.getSystemService(AccessibilityManager::class.java)
?.unregisterSystemAction(
diff --git a/quickstep/src/com/android/quickstep/SystemUiProxy.java b/quickstep/src/com/android/quickstep/SystemUiProxy.java
index 72f67fc544..b6272dad87 100644
--- a/quickstep/src/com/android/quickstep/SystemUiProxy.java
+++ b/quickstep/src/com/android/quickstep/SystemUiProxy.java
@@ -91,13 +91,13 @@ import com.android.wm.shell.draganddrop.IDragAndDrop;
import com.android.wm.shell.onehanded.IOneHanded;
import com.android.wm.shell.recents.IRecentTasks;
import com.android.wm.shell.recents.IRecentTasksListener;
+import com.android.wm.shell.shared.IHomeTransitionListener;
+import com.android.wm.shell.shared.IShellTransitions;
import com.android.wm.shell.splitscreen.ISplitScreen;
import com.android.wm.shell.splitscreen.ISplitScreenListener;
import com.android.wm.shell.splitscreen.ISplitSelectListener;
import com.android.wm.shell.startingsurface.IStartingWindow;
import com.android.wm.shell.startingsurface.IStartingWindowListener;
-import com.android.wm.shell.transition.IHomeTransitionListener;
-import com.android.wm.shell.transition.IShellTransitions;
import com.android.wm.shell.util.GroupedRecentTaskInfo;
import java.io.PrintWriter;
diff --git a/quickstep/src/com/android/quickstep/TaskShortcutFactory.java b/quickstep/src/com/android/quickstep/TaskShortcutFactory.java
index 7f1883d30c..147a3e2408 100644
--- a/quickstep/src/com/android/quickstep/TaskShortcutFactory.java
+++ b/quickstep/src/com/android/quickstep/TaskShortcutFactory.java
@@ -136,8 +136,9 @@ public interface TaskShortcutFactory {
class SaveAppPairSystemShortcut extends SystemShortcut {
private final GroupedTaskView mTaskView;
- public SaveAppPairSystemShortcut(BaseDraggingActivity activity, GroupedTaskView taskView) {
- super(R.drawable.ic_save_app_pair, R.string.save_app_pair, activity,
+ public SaveAppPairSystemShortcut(BaseDraggingActivity activity, GroupedTaskView taskView,
+ int iconResId) {
+ super(iconResId, R.string.save_app_pair, activity,
taskView.getItemInfo(), taskView);
mTaskView = taskView;
}
@@ -342,8 +343,12 @@ public interface TaskShortcutFactory {
return null;
}
+ int iconResId = deviceProfile.isLeftRightSplit
+ ? R.drawable.ic_save_app_pair_left_right
+ : R.drawable.ic_save_app_pair_up_down;
+
return Collections.singletonList(
- new SaveAppPairSystemShortcut(activity, (GroupedTaskView) taskView));
+ new SaveAppPairSystemShortcut(activity, (GroupedTaskView) taskView, iconResId));
}
@Override
diff --git a/quickstep/src/com/android/quickstep/TouchInteractionService.java b/quickstep/src/com/android/quickstep/TouchInteractionService.java
index b43c5201ce..66d7144ae0 100644
--- a/quickstep/src/com/android/quickstep/TouchInteractionService.java
+++ b/quickstep/src/com/android/quickstep/TouchInteractionService.java
@@ -135,9 +135,9 @@ import com.android.wm.shell.desktopmode.IDesktopMode;
import com.android.wm.shell.draganddrop.IDragAndDrop;
import com.android.wm.shell.onehanded.IOneHanded;
import com.android.wm.shell.recents.IRecentTasks;
+import com.android.wm.shell.shared.IShellTransitions;
import com.android.wm.shell.splitscreen.ISplitScreen;
import com.android.wm.shell.startingsurface.IStartingWindow;
-import com.android.wm.shell.transition.IShellTransitions;
import java.io.FileDescriptor;
import java.io.PrintWriter;
@@ -442,8 +442,10 @@ public class TouchInteractionService extends Service {
/** Refreshes the current overview target. */
public void refreshOverviewTarget() {
- executeForTouchInteractionService(tis -> tis.onOverviewTargetChange(
- tis.mOverviewComponentObserver.isHomeAndOverviewSame()));
+ executeForTouchInteractionService(tis -> {
+ tis.mAllAppsActionManager.onDestroy();
+ tis.onOverviewTargetChange(tis.mOverviewComponentObserver.isHomeAndOverviewSame());
+ });
}
}
diff --git a/quickstep/src/com/android/quickstep/views/OverviewActionsView.java b/quickstep/src/com/android/quickstep/views/OverviewActionsView.java
index e0091a5ae2..384a8d8656 100644
--- a/quickstep/src/com/android/quickstep/views/OverviewActionsView.java
+++ b/quickstep/src/com/android/quickstep/views/OverviewActionsView.java
@@ -20,7 +20,6 @@ import android.content.Context;
import android.content.res.Configuration;
import android.graphics.Rect;
import android.util.AttributeSet;
-import android.util.Log;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.Button;
@@ -43,8 +42,6 @@ import com.android.quickstep.util.LayoutUtils;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
-import java.util.Arrays;
-import java.util.stream.Collectors;
/**
* View for showing action buttons in Overview
@@ -297,17 +294,6 @@ public class OverviewActionsView extends FrameLayo
int desiredVisibility = mSplitButtonHiddenFlags == 0 ? VISIBLE : GONE;
mSplitButton.setVisibility(desiredVisibility);
findViewById(R.id.action_split_space).setVisibility(desiredVisibility);
-
- String callStack = Arrays.stream(
- Log.getStackTraceString(new Exception("thread stacktrace"))
- .split("\\n"))
- .limit(5)
- .skip(1) // Removes the line "java.lang.Exception: thread stacktrace"
- .collect(Collectors.joining("\n"));
- Log.d("b/321291049", "updateSplitButtonHiddenFlags called with flag: " + flag
- + " enabled: " + enable
- + " visibility: " + desiredVisibility
- + " partial trace: \n" + callStack);
}
/**
@@ -407,7 +393,11 @@ public class OverviewActionsView extends FrameLayo
? R.drawable.ic_split_horizontal
: R.drawable.ic_split_vertical;
mSplitButton.setCompoundDrawablesRelativeWithIntrinsicBounds(splitIconRes, 0, 0, 0);
+
+ int appPairIconRes = dp.isLeftRightSplit
+ ? R.drawable.ic_save_app_pair_left_right
+ : R.drawable.ic_save_app_pair_up_down;
mSaveAppPairButton.setCompoundDrawablesRelativeWithIntrinsicBounds(
- R.drawable.ic_save_app_pair, 0, 0, 0);
+ appPairIconRes, 0, 0, 0);
}
}
diff --git a/quickstep/src/com/android/quickstep/views/TaskView.java b/quickstep/src/com/android/quickstep/views/TaskView.java
index abd4ec4eae..79bd107b92 100644
--- a/quickstep/src/com/android/quickstep/views/TaskView.java
+++ b/quickstep/src/com/android/quickstep/views/TaskView.java
@@ -135,8 +135,6 @@ import java.util.stream.Stream;
public class TaskView extends FrameLayout implements Reusable {
private static final String TAG = TaskView.class.getSimpleName();
- private static final boolean DEBUG = false;
-
public static final int FLAG_UPDATE_ICON = 1;
public static final int FLAG_UPDATE_THUMBNAIL = FLAG_UPDATE_ICON << 1;
public static final int FLAG_UPDATE_CORNER_RADIUS = FLAG_UPDATE_THUMBNAIL << 1;
@@ -184,7 +182,7 @@ public class TaskView extends FrameLayout implements Reusable {
Collections.singletonList(new Rect());
public static final FloatProperty FOCUS_TRANSITION =
- new FloatProperty("focusTransition") {
+ new FloatProperty<>("focusTransition") {
@Override
public void setValue(TaskView taskView, float v) {
taskView.setIconsAndBannersTransitionProgress(v, false /* invert */);
@@ -197,7 +195,7 @@ public class TaskView extends FrameLayout implements Reusable {
};
private static final FloatProperty SPLIT_SELECT_TRANSLATION_X =
- new FloatProperty("splitSelectTranslationX") {
+ new FloatProperty<>("splitSelectTranslationX") {
@Override
public void setValue(TaskView taskView, float v) {
taskView.setSplitSelectTranslationX(v);
@@ -210,7 +208,7 @@ public class TaskView extends FrameLayout implements Reusable {
};
private static final FloatProperty SPLIT_SELECT_TRANSLATION_Y =
- new FloatProperty("splitSelectTranslationY") {
+ new FloatProperty<>("splitSelectTranslationY") {
@Override
public void setValue(TaskView taskView, float v) {
taskView.setSplitSelectTranslationY(v);
@@ -223,7 +221,7 @@ public class TaskView extends FrameLayout implements Reusable {
};
private static final FloatProperty DISMISS_TRANSLATION_X =
- new FloatProperty("dismissTranslationX") {
+ new FloatProperty<>("dismissTranslationX") {
@Override
public void setValue(TaskView taskView, float v) {
taskView.setDismissTranslationX(v);
@@ -236,7 +234,7 @@ public class TaskView extends FrameLayout implements Reusable {
};
private static final FloatProperty DISMISS_TRANSLATION_Y =
- new FloatProperty("dismissTranslationY") {
+ new FloatProperty<>("dismissTranslationY") {
@Override
public void setValue(TaskView taskView, float v) {
taskView.setDismissTranslationY(v);
@@ -249,7 +247,7 @@ public class TaskView extends FrameLayout implements Reusable {
};
private static final FloatProperty TASK_OFFSET_TRANSLATION_X =
- new FloatProperty("taskOffsetTranslationX") {
+ new FloatProperty<>("taskOffsetTranslationX") {
@Override
public void setValue(TaskView taskView, float v) {
taskView.setTaskOffsetTranslationX(v);
@@ -262,7 +260,7 @@ public class TaskView extends FrameLayout implements Reusable {
};
private static final FloatProperty TASK_OFFSET_TRANSLATION_Y =
- new FloatProperty("taskOffsetTranslationY") {
+ new FloatProperty<>("taskOffsetTranslationY") {
@Override
public void setValue(TaskView taskView, float v) {
taskView.setTaskOffsetTranslationY(v);
@@ -275,7 +273,7 @@ public class TaskView extends FrameLayout implements Reusable {
};
private static final FloatProperty TASK_RESISTANCE_TRANSLATION_X =
- new FloatProperty("taskResistanceTranslationX") {
+ new FloatProperty<>("taskResistanceTranslationX") {
@Override
public void setValue(TaskView taskView, float v) {
taskView.setTaskResistanceTranslationX(v);
@@ -288,7 +286,7 @@ public class TaskView extends FrameLayout implements Reusable {
};
private static final FloatProperty TASK_RESISTANCE_TRANSLATION_Y =
- new FloatProperty("taskResistanceTranslationY") {
+ new FloatProperty<>("taskResistanceTranslationY") {
@Override
public void setValue(TaskView taskView, float v) {
taskView.setTaskResistanceTranslationY(v);
@@ -301,7 +299,7 @@ public class TaskView extends FrameLayout implements Reusable {
};
public static final FloatProperty GRID_END_TRANSLATION_X =
- new FloatProperty("gridEndTranslationX") {
+ new FloatProperty<>("gridEndTranslationX") {
@Override
public void setValue(TaskView taskView, float v) {
taskView.setGridEndTranslationX(v);
@@ -314,7 +312,7 @@ public class TaskView extends FrameLayout implements Reusable {
};
public static final FloatProperty SNAPSHOT_SCALE =
- new FloatProperty("snapshotScale") {
+ new FloatProperty<>("snapshotScale") {
@Override
public void setValue(TaskView taskView, float v) {
taskView.setSnapshotScale(v);
@@ -602,10 +600,7 @@ public class TaskView extends FrameLayout implements Reusable {
if (event.getAction() == MotionEvent.ACTION_DOWN) {
computeAndSetIconTouchDelegate(mIconView, mIconCenterCoords, mIconTouchDelegate);
}
- if (mIconTouchDelegate != null && mIconTouchDelegate.onTouchEvent(event)) {
- return true;
- }
- return false;
+ return mIconTouchDelegate != null && mIconTouchDelegate.onTouchEvent(event);
}
protected void computeAndSetIconTouchDelegate(TaskViewIcon view, float[] tempCenterCoords,
@@ -647,9 +642,6 @@ public class TaskView extends FrameLayout implements Reusable {
/**
* Updates this task view to the given {@param task}.
- *
- * TODO(b/142282126) Re-evaluate if we need to pass in isMultiWindowMode after
- * that issue is fixed
*/
public void bind(Task task, RecentsOrientedState orientedState) {
cancelPendingLoadTasks();
@@ -1594,19 +1586,6 @@ public class TaskView extends FrameLayout implements Reusable {
mEndQuickswitchCuj = endQuickswitchCuj;
}
- private int getExpectedViewHeight(View view) {
- int expectedHeight;
- int h = view.getLayoutParams().height;
- if (h > 0) {
- expectedHeight = h;
- } else {
- int m = MeasureSpec.makeMeasureSpec(MeasureSpec.EXACTLY - 1, MeasureSpec.AT_MOST);
- view.measure(m, m);
- expectedHeight = view.getMeasuredHeight();
- }
- return expectedHeight;
- }
-
@Override
public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {
super.onInitializeAccessibilityNodeInfo(info);
diff --git a/src/com/android/launcher3/Launcher.java b/src/com/android/launcher3/Launcher.java
index 269603c6cb..912583cf4d 100644
--- a/src/com/android/launcher3/Launcher.java
+++ b/src/com/android/launcher3/Launcher.java
@@ -660,6 +660,11 @@ public class Launcher extends StatefulActivity
// #5 state handler
return new OnBackAnimationCallback() {
+ @Override
+ public void onBackStarted(BackEvent backEvent) {
+ Launcher.this.onBackStarted();
+ }
+
@Override
public void onBackInvoked() {
onStateBack();
@@ -2063,8 +2068,12 @@ public class Launcher extends StatefulActivity
getOnBackAnimationCallback().onBackInvoked();
}
+ protected void onBackStarted() {
+ mStateManager.getState().onBackStarted(this);
+ }
+
protected void onStateBack() {
- mStateManager.getState().onBackPressed(this);
+ mStateManager.getState().onBackInvoked(this);
}
protected void onScreenOnChanged(boolean isOn) {
diff --git a/src/com/android/launcher3/LauncherState.java b/src/com/android/launcher3/LauncherState.java
index 6e66c1448d..3bdd8635cf 100644
--- a/src/com/android/launcher3/LauncherState.java
+++ b/src/com/android/launcher3/LauncherState.java
@@ -424,20 +424,29 @@ public abstract class LauncherState implements BaseState {
return TestProtocol.stateOrdinalToString(ordinal);
}
- public void onBackPressed(Launcher launcher) {
+ /** Called when predictive back gesture is started. */
+ public void onBackStarted(Launcher launcher) {}
+
+ /**
+ * Called when back action is invoked. This can happen when:
+ * 1. back button is pressed in 3-button navigation.
+ * 2. when back is committed during back swiped (predictive or non-predictive).
+ * 3. when we programmatically perform back action.
+ */
+ public void onBackInvoked(Launcher launcher) {
if (this != NORMAL) {
StateManager lsm = launcher.getStateManager();
LauncherState lastState = lsm.getLastState();
- lsm.goToState(lastState, forEndCallback(this::onBackPressCompleted));
+ lsm.goToState(lastState, forEndCallback(this::onBackAnimationCompleted));
}
}
/**
- * To be called if back press is completed in a launcher state.
+ * To be called if back animation is completed in a launcher state.
*
- * @param success whether back press animation was successful or canceled.
+ * @param success whether back animation was successful or canceled.
*/
- protected void onBackPressCompleted(boolean success) {
+ protected void onBackAnimationCompleted(boolean success) {
// Do nothing. To be overridden by child class.
}
diff --git a/src/com/android/launcher3/config/FeatureFlags.java b/src/com/android/launcher3/config/FeatureFlags.java
index 6d64c22547..1c34c72d21 100644
--- a/src/com/android/launcher3/config/FeatureFlags.java
+++ b/src/com/android/launcher3/config/FeatureFlags.java
@@ -397,11 +397,6 @@ public final class FeatureFlags {
+ "waiting for SystemUI and then merging the SystemUI progress whenever we "
+ "start receiving the events");
- // TODO(Block 24): Clean up flags
- public static final BooleanFlag ENABLE_NEW_MIGRATION_LOGIC = getDebugFlag(270393455,
- "ENABLE_NEW_MIGRATION_LOGIC", ENABLED,
- "Enable the new grid migration logic, keeping pages when src < dest");
-
// TODO(Block 25): Clean up flags
public static final BooleanFlag ENABLE_NEW_GESTURE_NAV_TUTORIAL = getDebugFlag(270396257,
"ENABLE_NEW_GESTURE_NAV_TUTORIAL", ENABLED,
diff --git a/src/com/android/launcher3/model/DeviceGridState.java b/src/com/android/launcher3/model/DeviceGridState.java
index 8c68eb84ec..729b3814a4 100644
--- a/src/com/android/launcher3/model/DeviceGridState.java
+++ b/src/com/android/launcher3/model/DeviceGridState.java
@@ -156,11 +156,11 @@ public class DeviceGridState implements Comparable {
}
public Integer getColumns() {
- return Integer.parseInt(String.valueOf(mGridSizeString.charAt(0)));
+ return Integer.parseInt(String.valueOf(mGridSizeString.split(",")[0]));
}
public Integer getRows() {
- return Integer.parseInt(String.valueOf(mGridSizeString.charAt(2)));
+ return Integer.parseInt(String.valueOf(mGridSizeString.split(",")[1]));
}
@Override
diff --git a/src/com/android/launcher3/model/GridSizeMigrationUtil.java b/src/com/android/launcher3/model/GridSizeMigrationUtil.java
index 15190c7952..299c952d2a 100644
--- a/src/com/android/launcher3/model/GridSizeMigrationUtil.java
+++ b/src/com/android/launcher3/model/GridSizeMigrationUtil.java
@@ -223,19 +223,13 @@ public class GridSizeMigrationUtil {
screens.add(screenId);
}
- boolean preservePages = false;
- if (screens.isEmpty() && FeatureFlags.ENABLE_NEW_MIGRATION_LOGIC.get()) {
- preservePages = destDeviceState.compareTo(srcDeviceState) >= 0
- && destDeviceState.getColumns() - srcDeviceState.getColumns() <= 2;
- }
-
// Then we place the items on the screens
for (int screenId : screens) {
if (DEBUG) {
Log.d(TAG, "Migrating " + screenId);
}
solveGridPlacement(helper, srcReader,
- destReader, screenId, trgX, trgY, workspaceToBeAdded, false);
+ destReader, screenId, trgX, trgY, workspaceToBeAdded);
if (workspaceToBeAdded.isEmpty()) {
break;
}
@@ -245,8 +239,8 @@ public class GridSizeMigrationUtil {
// any of the screens, in this case we add them to new screens until all of them are placed.
int screenId = destReader.mLastScreenId + 1;
while (!workspaceToBeAdded.isEmpty()) {
- solveGridPlacement(helper, srcReader,
- destReader, screenId, trgX, trgY, workspaceToBeAdded, preservePages);
+ solveGridPlacement(helper, srcReader, destReader, screenId, trgX, trgY,
+ workspaceToBeAdded);
screenId++;
}
@@ -348,7 +342,7 @@ public class GridSizeMigrationUtil {
private static void solveGridPlacement(@NonNull final DatabaseHelper helper,
@NonNull final DbReader srcReader, @NonNull final DbReader destReader,
final int screenId, final int trgX, final int trgY,
- @NonNull final List sortedItemsToPlace, final boolean matchingScreenIdOnly) {
+ @NonNull final List sortedItemsToPlace) {
final GridOccupancy occupied = new GridOccupancy(trgX, trgY);
final Point trg = new Point(trgX, trgY);
final Point next = new Point(0, screenId == 0
@@ -366,8 +360,6 @@ public class GridSizeMigrationUtil {
Iterator iterator = sortedItemsToPlace.iterator();
while (iterator.hasNext()) {
final DbEntry entry = iterator.next();
- if (matchingScreenIdOnly && entry.screenId < screenId) continue;
- if (matchingScreenIdOnly && entry.screenId > screenId) break;
if (entry.minSpanX > trgX || entry.minSpanY > trgY) {
iterator.remove();
continue;
@@ -435,7 +427,8 @@ public class GridSizeMigrationUtil {
}
}
- protected static class DbReader {
+ @VisibleForTesting
+ public static class DbReader {
private final SQLiteDatabase mDb;
private final String mTableName;
@@ -446,7 +439,7 @@ public class GridSizeMigrationUtil {
private final Map> mWorkspaceEntriesByScreenId =
new ArrayMap<>();
- DbReader(SQLiteDatabase db, String tableName, Context context,
+ public DbReader(SQLiteDatabase db, String tableName, Context context,
Set validPackages) {
mDb = db;
mTableName = tableName;
diff --git a/tests/Android.bp b/tests/Android.bp
index c1d41802cc..13a1cbb2ae 100644
--- a/tests/Android.bp
+++ b/tests/Android.bp
@@ -86,6 +86,7 @@ android_library {
"mockito-target-extended-minus-junit4",
"launcher_log_protos_lite",
"truth",
+ "kotlinx_coroutines_test",
"platform-test-rules",
"testables",
"com_android_launcher3_flags_lib",
diff --git a/tests/src/com/android/launcher3/celllayout/testgenerator/RandomBoardGenerator.kt b/tests/src/com/android/launcher3/celllayout/testgenerator/RandomBoardGenerator.kt
index c5dbce42c4..ff4698793f 100644
--- a/tests/src/com/android/launcher3/celllayout/testgenerator/RandomBoardGenerator.kt
+++ b/tests/src/com/android/launcher3/celllayout/testgenerator/RandomBoardGenerator.kt
@@ -21,6 +21,13 @@ import java.util.Random
/** Generates a random CellLayoutBoard. */
open class RandomBoardGenerator(generator: Random) : DeterministicRandomGenerator(generator) {
+
+ companion object {
+ // This is the max number of widgets because we encode the widgets as letters A-Z and we
+ // already have some of those letter used by other things so 22 is a safe number
+ val MAX_NUMBER_OF_WIDGETS = 22
+ }
+
/**
* @param remainingEmptySpaces the maximum number of spaces we will fill with icons and widgets
* meaning that if the number is 100 we will try to fill the board with at most 100 spaces
@@ -33,9 +40,9 @@ open class RandomBoardGenerator(generator: Random) : DeterministicRandomGenerato
}
protected fun fillBoard(
- board: CellLayoutBoard,
- area: Rect,
- remainingEmptySpacesArg: Int
+ board: CellLayoutBoard,
+ area: Rect,
+ remainingEmptySpacesArg: Int
): CellLayoutBoard {
var remainingEmptySpaces = remainingEmptySpacesArg
if (area.height() * area.width() <= 0) return board
@@ -45,11 +52,18 @@ open class RandomBoardGenerator(generator: Random) : DeterministicRandomGenerato
val y = area.top + getRandom(0, area.height() - height)
if (remainingEmptySpaces > 0) {
remainingEmptySpaces -= width * height
- } else if (board.widgets.size <= 22 && width * height > 1) {
+ }
+
+ if (board.widgets.size <= MAX_NUMBER_OF_WIDGETS && width * height > 1) {
board.addWidget(x, y, width, height)
} else {
board.addIcon(x, y)
}
+
+ if (remainingEmptySpaces < 0) {
+ // optimization, no need to keep going
+ return board
+ }
fillBoard(board, Rect(area.left, area.top, area.right, y), remainingEmptySpaces)
fillBoard(board, Rect(area.left, y, x, area.bottom), remainingEmptySpaces)
fillBoard(board, Rect(x, y + height, area.right, area.bottom), remainingEmptySpaces)
diff --git a/tests/src/com/android/launcher3/celllayout/testgenerator/ValidGridMigrationTestCaseGenerator.kt b/tests/src/com/android/launcher3/celllayout/testgenerator/ValidGridMigrationTestCaseGenerator.kt
new file mode 100644
index 0000000000..e773a86a7a
--- /dev/null
+++ b/tests/src/com/android/launcher3/celllayout/testgenerator/ValidGridMigrationTestCaseGenerator.kt
@@ -0,0 +1,160 @@
+/*
+ * Copyright (C) 2024 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.celllayout.testgenerator
+
+import android.graphics.Point
+import com.android.launcher3.LauncherSettings
+import com.android.launcher3.celllayout.board.CellLayoutBoard
+import com.android.launcher3.model.data.LauncherAppWidgetInfo
+import com.android.launcher3.model.gridmigration.WorkspaceItem
+import java.util.Random
+import java.util.concurrent.atomic.AtomicInteger
+
+/**
+ * Generate a list of WorkspaceItem's for the given test case.
+ *
+ * @param repeatAfter a number after which we would repeat the same number of icons and widgets to
+ * account for cases where the user have the same item multiple times.
+ */
+fun generateItemsForTest(
+ testCase: GridMigrationUnitTestCase,
+ repeatAfter: Int
+): List {
+ val id = AtomicInteger(0)
+ val widgetId = AtomicInteger(LauncherAppWidgetInfo.CUSTOM_WIDGET_ID - 1)
+ val boards = testCase.boards
+ // Repeat the same appWidgetProvider and intent to have repeating widgets and icons and test
+ // that case too
+ val getIntent = { i: Int -> "Intent ${i % repeatAfter}" }
+ val getProvider = { i: Int -> "com.test/test.Provider${i % repeatAfter}" }
+ val hotseatEntries =
+ (0 until boards[0].width).map {
+ WorkspaceItem(
+ x = it,
+ y = 0,
+ spanX = 1,
+ spanY = 1,
+ id = id.getAndAdd(1),
+ screenId = it,
+ title = "Hotseat ${id.get()}",
+ appWidgetId = -1,
+ appWidgetProvider = "Hotseat icons don't have a provider",
+ intent = getIntent(id.get()),
+ type = LauncherSettings.Favorites.ITEM_TYPE_APPLICATION,
+ container = LauncherSettings.Favorites.CONTAINER_HOTSEAT
+ )
+ }
+ var widgetEntries =
+ boards
+ .flatMapIndexed { i, board -> board.widgets.map { Pair(i, it) } }
+ .map {
+ WorkspaceItem(
+ x = it.second.cellX,
+ y = it.second.cellY,
+ spanX = it.second.spanX,
+ spanY = it.second.spanY,
+ id = id.getAndAdd(1),
+ screenId = it.first,
+ title = "Title Widget ${id.get()}",
+ appWidgetId = widgetId.getAndAdd(-1),
+ appWidgetProvider = getProvider(id.get()),
+ intent = "Widgets don't have intent",
+ type = LauncherSettings.Favorites.ITEM_TYPE_APPWIDGET,
+ container = LauncherSettings.Favorites.CONTAINER_DESKTOP
+ )
+ }
+ widgetEntries = widgetEntries.filter { it.appWidgetProvider.contains("Provider4") }
+ val iconEntries =
+ boards
+ .flatMapIndexed { i, board -> board.icons.map { Pair(i, it) } }
+ .map {
+ WorkspaceItem(
+ x = it.second.coord.x,
+ y = it.second.coord.y,
+ spanX = 1,
+ spanY = 1,
+ id = id.getAndAdd(1),
+ screenId = it.first,
+ title = "Title Icon ${id.get()}",
+ appWidgetId = -1,
+ appWidgetProvider = "Icons don't have providers",
+ intent = getIntent(id.get()),
+ type = LauncherSettings.Favorites.ITEM_TYPE_APPLICATION,
+ container = LauncherSettings.Favorites.CONTAINER_DESKTOP
+ )
+ }
+ return widgetEntries + hotseatEntries // + iconEntries
+}
+
+data class GridMigrationUnitTestCase(
+ val boards: List,
+ val srcSize: Point,
+ val targetSize: Point,
+ val seed: Long
+)
+
+class ValidGridMigrationTestCaseGenerator(private val generator: Random) :
+ DeterministicRandomGenerator(generator) {
+
+ companion object {
+ const val MAX_BOARD_SIZE = 12
+ const val MAX_BOARD_COUNT = 10
+ const val SEED = 10342
+ }
+
+ private fun generateBoards(
+ boardGenerator: RandomBoardGenerator,
+ width: Int,
+ height: Int,
+ boardCount: Int
+ ): List {
+ val boards = mutableListOf()
+ for (i in 0 until boardCount) {
+ boards.add(
+ boardGenerator.generateBoard(
+ width,
+ height,
+ boardGenerator.getRandom(0, width * height)
+ )
+ )
+ }
+ return boards
+ }
+
+ fun generateTestCase(): GridMigrationUnitTestCase {
+ var seed = generator.nextLong()
+ val randomBoardGenerator = RandomBoardGenerator(Random(seed))
+ val width = randomBoardGenerator.getRandom(3, MAX_BOARD_SIZE)
+ val height = randomBoardGenerator.getRandom(3, MAX_BOARD_SIZE)
+ return GridMigrationUnitTestCase(
+ boards =
+ generateBoards(
+ boardGenerator = randomBoardGenerator,
+ width = width,
+ height = height,
+ boardCount = randomBoardGenerator.getRandom(3, MAX_BOARD_COUNT)
+ ),
+ srcSize = Point(width, height),
+ targetSize =
+ Point(
+ randomBoardGenerator.getRandom(3, MAX_BOARD_SIZE),
+ randomBoardGenerator.getRandom(3, MAX_BOARD_SIZE)
+ ),
+ seed = seed
+ )
+ }
+}
diff --git a/tests/src/com/android/launcher3/model/GridSizeMigrationUtilTest.kt b/tests/src/com/android/launcher3/model/GridSizeMigrationUtilTest.kt
index 04735f20b1..761f06d514 100644
--- a/tests/src/com/android/launcher3/model/GridSizeMigrationUtilTest.kt
+++ b/tests/src/com/android/launcher3/model/GridSizeMigrationUtilTest.kt
@@ -28,7 +28,6 @@ import com.android.launcher3.InvariantDeviceProfile
import com.android.launcher3.LauncherPrefs
import com.android.launcher3.LauncherPrefs.Companion.WORKSPACE_SIZE
import com.android.launcher3.LauncherSettings.Favorites.*
-import com.android.launcher3.config.FeatureFlags
import com.android.launcher3.model.GridSizeMigrationUtil.DbReader
import com.android.launcher3.pm.UserCache
import com.android.launcher3.provider.LauncherDbUtils
@@ -98,10 +97,7 @@ class GridSizeMigrationUtilTest {
modelHelper.destroy()
}
- /**
- * Old migration logic, should be modified once [FeatureFlags.ENABLE_NEW_MIGRATION_LOGIC] is not
- * needed anymore
- */
+ /** Old migration logic, should be modified once is not needed anymore */
@Test
@Throws(Exception::class)
fun testMigration() {
@@ -208,10 +204,7 @@ class GridSizeMigrationUtilTest {
assertThat(locMap[testPackage9]).isEqualTo(Point(0, 2))
}
- /**
- * Old migration logic, should be modified once [FeatureFlags.ENABLE_NEW_MIGRATION_LOGIC] is not
- * needed anymore
- */
+ /** Old migration logic, should be modified once is not needed anymore */
@Test
@Throws(Exception::class)
fun testMigrationBackAndForth() {
@@ -605,68 +598,6 @@ class GridSizeMigrationUtilTest {
c.close()
}
- /**
- * Migrating from a smaller grid to a large one should keep the pages if the column difference
- * is less than 2
- */
- @Test
- @Throws(Exception::class)
- fun migrateFromSmallerGridSmallDifference() {
- enableNewMigrationLogic("4,4")
-
- // Setup src grid
- addItem(ITEM_TYPE_APPLICATION, 0, CONTAINER_DESKTOP, 2, 2, testPackage1, 5, TMP_TABLE)
- addItem(ITEM_TYPE_APPLICATION, 0, CONTAINER_DESKTOP, 2, 3, testPackage2, 6, TMP_TABLE)
- addItem(ITEM_TYPE_APPLICATION, 1, CONTAINER_DESKTOP, 3, 1, testPackage3, 7, TMP_TABLE)
- addItem(ITEM_TYPE_APPLICATION, 1, CONTAINER_DESKTOP, 3, 2, testPackage4, 8, TMP_TABLE)
- addItem(ITEM_TYPE_APPLICATION, 2, CONTAINER_DESKTOP, 3, 3, testPackage5, 9, TMP_TABLE)
-
- idp.numDatabaseHotseatIcons = 4
- idp.numColumns = 6
- idp.numRows = 5
-
- val srcReader = DbReader(db, TMP_TABLE, context, validPackages)
- val destReader = DbReader(db, TABLE_NAME, context, validPackages)
- GridSizeMigrationUtil.migrate(
- dbHelper,
- srcReader,
- destReader,
- idp.numDatabaseHotseatIcons,
- Point(idp.numColumns, idp.numRows),
- DeviceGridState(context),
- DeviceGridState(idp)
- )
-
- // Get workspace items
- val c =
- db.query(
- TABLE_NAME,
- arrayOf(INTENT, SCREEN),
- "container=$CONTAINER_DESKTOP",
- null,
- null,
- null,
- null
- )
- ?: throw IllegalStateException()
- val intentIndex = c.getColumnIndex(INTENT)
- val screenIndex = c.getColumnIndex(SCREEN)
-
- // Get in which screen the icon is
- val locMap = HashMap()
- while (c.moveToNext()) {
- locMap[Intent.parseUri(c.getString(intentIndex), 0).getPackage()] =
- c.getInt(screenIndex)
- }
- c.close()
- assertThat(locMap.size).isEqualTo(5)
- assertThat(locMap[testPackage1]).isEqualTo(0)
- assertThat(locMap[testPackage2]).isEqualTo(0)
- assertThat(locMap[testPackage3]).isEqualTo(1)
- assertThat(locMap[testPackage4]).isEqualTo(1)
- assertThat(locMap[testPackage5]).isEqualTo(2)
- }
-
/**
* Migrating from a smaller grid to a large one should reflow the pages if the column difference
* is more than 2
diff --git a/tests/src/com/android/launcher3/model/gridmigration/GridMigrationUtils.kt b/tests/src/com/android/launcher3/model/gridmigration/GridMigrationUtils.kt
new file mode 100644
index 0000000000..cc8e61d726
--- /dev/null
+++ b/tests/src/com/android/launcher3/model/gridmigration/GridMigrationUtils.kt
@@ -0,0 +1,114 @@
+/*
+ * Copyright (C) 2024 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.model.gridmigration
+
+import android.content.ContentValues
+import android.database.sqlite.SQLiteDatabase
+import android.graphics.Point
+import com.android.launcher3.LauncherSettings.Favorites
+import com.android.launcher3.celllayout.board.CellLayoutBoard
+
+class MockSet(override val size: Int) : Set {
+ override fun contains(element: String): Boolean = true
+ override fun containsAll(elements: Collection): Boolean = true
+ override fun isEmpty(): Boolean = false
+ override fun iterator(): Iterator = listOf().iterator()
+}
+
+fun itemListToBoard(itemsArg: List, boardSize: Point): List {
+ val items = itemsArg.filter { it.container != Favorites.CONTAINER_HOTSEAT }
+ val boardList =
+ List(items.maxOf { it.screenId + 1 }) { CellLayoutBoard(boardSize.x, boardSize.y) }
+ items.forEach {
+ when (it.type) {
+ Favorites.ITEM_TYPE_FOLDER,
+ Favorites.ITEM_TYPE_APP_PAIR -> throw Exception("Not implemented")
+ Favorites.ITEM_TYPE_APPWIDGET ->
+ boardList[it.screenId].addWidget(it.x, it.y, it.spanX, it.spanY)
+ Favorites.ITEM_TYPE_APPLICATION -> boardList[it.screenId].addIcon(it.x, it.y)
+ }
+ }
+ return boardList
+}
+
+fun insertIntoDb(tableName: String, entry: WorkspaceItem, db: SQLiteDatabase) {
+ val values = ContentValues()
+ values.put(Favorites.SCREEN, entry.screenId)
+ values.put(Favorites.CELLX, entry.x)
+ values.put(Favorites.CELLY, entry.y)
+ values.put(Favorites.SPANX, entry.spanX)
+ values.put(Favorites.SPANY, entry.spanY)
+ values.put(Favorites.TITLE, entry.title)
+ values.put(Favorites.INTENT, entry.intent)
+ values.put(Favorites.APPWIDGET_PROVIDER, entry.appWidgetProvider)
+ values.put(Favorites.APPWIDGET_ID, entry.appWidgetId)
+ values.put(Favorites.CONTAINER, entry.container)
+ values.put(Favorites.ITEM_TYPE, entry.type)
+ values.put(Favorites._ID, entry.id)
+ db.insert(tableName, null, values)
+}
+
+fun readDb(tableName: String, db: SQLiteDatabase): List {
+ val result = mutableListOf()
+ val cursor = db.query(tableName, null, null, null, null, null, null)
+ val indexCellX: Int = cursor.getColumnIndexOrThrow(Favorites.CELLX)
+ val indexCellY: Int = cursor.getColumnIndexOrThrow(Favorites.CELLY)
+ val indexSpanX: Int = cursor.getColumnIndexOrThrow(Favorites.SPANX)
+ val indexSpanY: Int = cursor.getColumnIndexOrThrow(Favorites.SPANY)
+ val indexId: Int = cursor.getColumnIndexOrThrow(Favorites._ID)
+ val indexScreen: Int = cursor.getColumnIndexOrThrow(Favorites.SCREEN)
+ val indexTitle: Int = cursor.getColumnIndexOrThrow(Favorites.TITLE)
+ val indexAppWidgetId: Int = cursor.getColumnIndexOrThrow(Favorites.APPWIDGET_ID)
+ val indexWidgetProvider: Int = cursor.getColumnIndexOrThrow(Favorites.APPWIDGET_PROVIDER)
+ val indexIntent: Int = cursor.getColumnIndexOrThrow(Favorites.INTENT)
+ val indexItemType: Int = cursor.getColumnIndexOrThrow(Favorites.ITEM_TYPE)
+ val container: Int = cursor.getColumnIndexOrThrow(Favorites.CONTAINER)
+ while (cursor.moveToNext()) {
+ result.add(
+ WorkspaceItem(
+ x = cursor.getInt(indexCellX),
+ y = cursor.getInt(indexCellY),
+ spanX = cursor.getInt(indexSpanX),
+ spanY = cursor.getInt(indexSpanY),
+ id = cursor.getInt(indexId),
+ screenId = cursor.getInt(indexScreen),
+ title = cursor.getString(indexTitle),
+ appWidgetId = cursor.getInt(indexAppWidgetId),
+ appWidgetProvider = cursor.getString(indexWidgetProvider),
+ intent = cursor.getString(indexIntent),
+ type = cursor.getInt(indexItemType),
+ container = cursor.getInt(container)
+ )
+ )
+ }
+ return result
+}
+
+data class WorkspaceItem(
+ val x: Int,
+ val y: Int,
+ val spanX: Int,
+ val spanY: Int,
+ val id: Int,
+ val screenId: Int,
+ val title: String,
+ val appWidgetId: Int,
+ val appWidgetProvider: String,
+ val intent: String,
+ val type: Int,
+ val container: Int,
+)
diff --git a/tests/src/com/android/launcher3/model/gridmigration/ValidGridMigrationUnitTest.kt b/tests/src/com/android/launcher3/model/gridmigration/ValidGridMigrationUnitTest.kt
new file mode 100644
index 0000000000..100297608b
--- /dev/null
+++ b/tests/src/com/android/launcher3/model/gridmigration/ValidGridMigrationUnitTest.kt
@@ -0,0 +1,173 @@
+/*
+ * 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.model.gridmigration
+
+import android.content.Context
+import android.database.sqlite.SQLiteDatabase
+import android.graphics.Point
+import android.os.Process
+import android.util.Log
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import androidx.test.platform.app.InstrumentationRegistry
+import com.android.launcher3.InvariantDeviceProfile
+import com.android.launcher3.LauncherSettings.Favorites
+import com.android.launcher3.celllayout.testgenerator.ValidGridMigrationTestCaseGenerator
+import com.android.launcher3.celllayout.testgenerator.generateItemsForTest
+import com.android.launcher3.model.DatabaseHelper
+import com.android.launcher3.model.DeviceGridState
+import com.android.launcher3.model.GridSizeMigrationUtil
+import com.android.launcher3.pm.UserCache
+import com.android.launcher3.provider.LauncherDbUtils
+import com.android.launcher3.util.rule.TestStabilityRule
+import com.android.launcher3.util.rule.TestStabilityRule.Stability
+import java.util.Random
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class ValidGridMigrationUnitTest {
+
+ companion object {
+ const val SEED = 1044542
+ const val REPEAT_AFTER = 10
+ const val TAG = "ValidGridMigrationUnitTest"
+ }
+
+ private lateinit var context: Context
+
+ @Before
+ fun setUp() {
+ context = InstrumentationRegistry.getInstrumentation().targetContext
+ }
+
+ private fun validate(
+ srcItems: List,
+ dstItems: List,
+ destinationSize: Point
+ ) {
+ // This returns a map with the number of repeated elements
+ // ex { calculatorIcon : 6, weatherWidget : 2 }
+ val itemsToSet = { it: List ->
+ it.filter { it.container != Favorites.CONTAINER_HOTSEAT }
+ .groupingBy {
+ when (it.type) {
+ Favorites.ITEM_TYPE_FOLDER,
+ Favorites.ITEM_TYPE_APP_PAIR -> throw Exception("Not implemented")
+ Favorites.ITEM_TYPE_APPWIDGET -> it.appWidgetProvider
+ Favorites.ITEM_TYPE_APPLICATION -> it.intent
+ else -> it.title
+ }
+ }
+ .eachCount()
+ }
+ for (it in dstItems) {
+ assert((it.x in 0..destinationSize.x) && (it.y in 0..destinationSize.y)) {
+ "Item outside of the board size. Size = $destinationSize Item = $it"
+ }
+ assert(
+ (it.x + it.spanX in 0..destinationSize.x) &&
+ (it.y + it.spanY in 0..destinationSize.y)
+ ) {
+ "Item doesn't fit in the grid. Size = $destinationSize Item = $it"
+ }
+ }
+
+ assert(itemsToSet(srcItems) == itemsToSet(dstItems)) {
+ "The srcItems do not match the dstItems src = $srcItems dst = $dstItems"
+ }
+ }
+
+ private fun addItemsToDb(db: SQLiteDatabase, tableName: String, items: List) {
+ LauncherDbUtils.SQLiteTransaction(db).use { transaction ->
+ items.forEach { insertIntoDb(tableName, it, transaction.db) }
+ transaction.commit()
+ }
+ }
+
+ private fun migrate(
+ srcItems: List,
+ srcSize: Point,
+ targetSize: Point
+ ): List {
+ val userSerial = UserCache.INSTANCE[context].getSerialNumberForUser(Process.myUserHandle())
+ val dbHelper =
+ DatabaseHelper(
+ context,
+ null,
+ { UserCache.INSTANCE.get(context).getSerialNumberForUser(it) },
+ {}
+ )
+ val srcTableName = Favorites.TMP_TABLE
+ val dstTableName = Favorites.TABLE_NAME
+ Favorites.addTableToDb(dbHelper.writableDatabase, userSerial, false, srcTableName)
+ addItemsToDb(dbHelper.writableDatabase, srcTableName, srcItems)
+ LauncherDbUtils.SQLiteTransaction(dbHelper.writableDatabase).use {
+ GridSizeMigrationUtil.migrate(
+ dbHelper,
+ GridSizeMigrationUtil.DbReader(it.db, srcTableName, context, MockSet(1)),
+ GridSizeMigrationUtil.DbReader(it.db, dstTableName, context, MockSet(1)),
+ targetSize.x,
+ targetSize,
+ DeviceGridState(
+ srcSize.x,
+ srcSize.y,
+ srcSize.x,
+ InvariantDeviceProfile.TYPE_PHONE,
+ srcTableName
+ ),
+ DeviceGridState(
+ targetSize.x,
+ targetSize.y,
+ targetSize.x,
+ InvariantDeviceProfile.TYPE_PHONE,
+ dstTableName
+ )
+ )
+ it.commit()
+ }
+ return readDb(dstTableName, dbHelper.readableDatabase)
+ }
+
+ @Test
+ fun runTestCase() {
+ val caseGenerator = ValidGridMigrationTestCaseGenerator(Random(SEED.toLong()))
+ for (i in 0..50) {
+ val testCase = caseGenerator.generateTestCase()
+ Log.d(TAG, "Test case = $testCase")
+ val srcItemList = generateItemsForTest(testCase, REPEAT_AFTER)
+ val dstItemList = migrate(srcItemList, testCase.srcSize, testCase.targetSize)
+ validate(srcItemList, dstItemList, testCase.targetSize)
+ }
+ }
+
+ // This test takes about 4 minutes, there is no need to run it in presubmit.
+ @Stability(flavors = TestStabilityRule.LOCAL or TestStabilityRule.PLATFORM_POSTSUBMIT)
+ @Test
+ fun runExtensiveTestCases() {
+ val caseGenerator = ValidGridMigrationTestCaseGenerator(Random(SEED.toLong()))
+ for (i in 0..1000) {
+ val testCase = caseGenerator.generateTestCase()
+ Log.d(TAG, "Test case = $testCase")
+ val srcItemList = generateItemsForTest(testCase, REPEAT_AFTER)
+ val dstItemList = migrate(srcItemList, testCase.srcSize, testCase.targetSize)
+ validate(srcItemList, dstItemList, testCase.targetSize)
+ }
+ }
+}