Merge "Revert^3 "Change Overview Actions to round buttons."" into udc-qpr-dev
diff --git a/quickstep/res/values-da/strings.xml b/quickstep/res/values-da/strings.xml
index d0fed79..1603fac 100644
--- a/quickstep/res/values-da/strings.xml
+++ b/quickstep/res/values-da/strings.xml
@@ -72,7 +72,7 @@
<string name="overview_gesture_feedback_home_detected" msgid="663432226180397138">"Prøv at holde fingeren nede på vinduet i længere tid, inden du løfter den"</string>
<string name="overview_gesture_feedback_wrong_swipe_direction" msgid="1191055451018584958">"Stryg lige opad, og hold derefter fingeren stille"</string>
<string name="overview_gesture_feedback_complete_with_follow_up" msgid="3544611727467765026">"Du har lært, hvordan du bruger bevægelser. Du kan aktivere bevægelser i Indstillinger."</string>
- <string name="overview_gesture_feedback_complete_without_follow_up" msgid="2903050864432331629">"Du har fuldført bevægelsen for Skift app"</string>
+ <string name="overview_gesture_feedback_complete_without_follow_up" msgid="2903050864432331629">"Du har fuldført bevægelsen for at skifte mellem apps"</string>
<string name="overview_gesture_intro_title" msgid="2902054412868489378">"Stryg for at skifte app"</string>
<string name="overview_gesture_intro_subtitle" msgid="4968091015637850859">"Skift mellem apps ved at stryge opad fra bunden af skærmen, holde fingeren stille og løfte den."</string>
<string name="overview_gesture_spoken_intro_subtitle" msgid="3853371838260201751">"Skift mellem apps ved at stryge opad fra bunden af skærmen med 2 fingre, holde dem nede og slippe."</string>
diff --git a/quickstep/res/values-en-rAU/strings.xml b/quickstep/res/values-en-rAU/strings.xml
index dde0177..878c062 100644
--- a/quickstep/res/values-en-rAU/strings.xml
+++ b/quickstep/res/values-en-rAU/strings.xml
@@ -70,7 +70,7 @@
<string name="home_gesture_tutorial_success" msgid="1736295017642244751">"Great work!"</string>
<string name="overview_gesture_feedback_swipe_too_far_from_edge" msgid="6402349235265407385">"Make sure you swipe up from the bottom edge of the screen"</string>
<string name="overview_gesture_feedback_home_detected" msgid="663432226180397138">"Try holding the window for longer before releasing"</string>
- <string name="overview_gesture_feedback_wrong_swipe_direction" msgid="1191055451018584958">"Make sure that you swipe straight up, then pause."</string>
+ <string name="overview_gesture_feedback_wrong_swipe_direction" msgid="1191055451018584958">"Make sure that you swipe straight up, then pause"</string>
<string name="overview_gesture_feedback_complete_with_follow_up" msgid="3544611727467765026">"You\'ve learned how to use gestures. To turn off gestures, go to Settings."</string>
<string name="overview_gesture_feedback_complete_without_follow_up" msgid="2903050864432331629">"You completed the switch apps gesture"</string>
<string name="overview_gesture_intro_title" msgid="2902054412868489378">"Swipe to switch apps"</string>
diff --git a/quickstep/res/values-en-rGB/strings.xml b/quickstep/res/values-en-rGB/strings.xml
index dde0177..878c062 100644
--- a/quickstep/res/values-en-rGB/strings.xml
+++ b/quickstep/res/values-en-rGB/strings.xml
@@ -70,7 +70,7 @@
<string name="home_gesture_tutorial_success" msgid="1736295017642244751">"Great work!"</string>
<string name="overview_gesture_feedback_swipe_too_far_from_edge" msgid="6402349235265407385">"Make sure you swipe up from the bottom edge of the screen"</string>
<string name="overview_gesture_feedback_home_detected" msgid="663432226180397138">"Try holding the window for longer before releasing"</string>
- <string name="overview_gesture_feedback_wrong_swipe_direction" msgid="1191055451018584958">"Make sure that you swipe straight up, then pause."</string>
+ <string name="overview_gesture_feedback_wrong_swipe_direction" msgid="1191055451018584958">"Make sure that you swipe straight up, then pause"</string>
<string name="overview_gesture_feedback_complete_with_follow_up" msgid="3544611727467765026">"You\'ve learned how to use gestures. To turn off gestures, go to Settings."</string>
<string name="overview_gesture_feedback_complete_without_follow_up" msgid="2903050864432331629">"You completed the switch apps gesture"</string>
<string name="overview_gesture_intro_title" msgid="2902054412868489378">"Swipe to switch apps"</string>
diff --git a/quickstep/res/values-en-rIN/strings.xml b/quickstep/res/values-en-rIN/strings.xml
index dde0177..878c062 100644
--- a/quickstep/res/values-en-rIN/strings.xml
+++ b/quickstep/res/values-en-rIN/strings.xml
@@ -70,7 +70,7 @@
<string name="home_gesture_tutorial_success" msgid="1736295017642244751">"Great work!"</string>
<string name="overview_gesture_feedback_swipe_too_far_from_edge" msgid="6402349235265407385">"Make sure you swipe up from the bottom edge of the screen"</string>
<string name="overview_gesture_feedback_home_detected" msgid="663432226180397138">"Try holding the window for longer before releasing"</string>
- <string name="overview_gesture_feedback_wrong_swipe_direction" msgid="1191055451018584958">"Make sure that you swipe straight up, then pause."</string>
+ <string name="overview_gesture_feedback_wrong_swipe_direction" msgid="1191055451018584958">"Make sure that you swipe straight up, then pause"</string>
<string name="overview_gesture_feedback_complete_with_follow_up" msgid="3544611727467765026">"You\'ve learned how to use gestures. To turn off gestures, go to Settings."</string>
<string name="overview_gesture_feedback_complete_without_follow_up" msgid="2903050864432331629">"You completed the switch apps gesture"</string>
<string name="overview_gesture_intro_title" msgid="2902054412868489378">"Swipe to switch apps"</string>
diff --git a/quickstep/res/values-sw720dp/dimens.xml b/quickstep/res/values-sw720dp/dimens.xml
index 9e832bc..1caffb8 100644
--- a/quickstep/res/values-sw720dp/dimens.xml
+++ b/quickstep/res/values-sw720dp/dimens.xml
@@ -43,4 +43,7 @@
<dimen name="taskbar_app_window_threshold">100dp</dimen>
<dimen name="taskbar_home_overview_threshold">180dp</dimen>
<dimen name="taskbar_catch_up_threshold">300dp</dimen>
+
+ <!-- Taskbar swipe up threshold multipliers -->
+ <item name="taskbar_nav_threshold_mult" format="float" type="dimen">3</item>
</resources>
diff --git a/quickstep/res/values/dimens.xml b/quickstep/res/values/dimens.xml
index f04a1f9..b024418 100644
--- a/quickstep/res/values/dimens.xml
+++ b/quickstep/res/values/dimens.xml
@@ -328,6 +328,12 @@
<!-- Taskbar swipe down threshold -->
<dimen name="taskbar_to_nav_threshold">24dp</dimen>
+ <!-- Taskbar swipe up threshold multipliers -->
+ <item name="taskbar_nav_threshold_mult" format="float" type="dimen">4.5</item>
+ <item name="taskbar_app_window_threshold_mult" format="float" type="dimen">10</item>
+ <item name="taskbar_home_overview_threshold_mult" format="float" type="dimen">18</item>
+ <item name="taskbar_catch_up_threshold_mult" format="float" type="dimen">30</item>
+
<!-- Taskbar 3 button spacing -->
<dimen name="taskbar_button_space_inbetween">24dp</dimen>
<dimen name="taskbar_button_space_inbetween_phone">40dp</dimen>
diff --git a/quickstep/src/com/android/launcher3/taskbar/NavbarButtonsViewController.java b/quickstep/src/com/android/launcher3/taskbar/NavbarButtonsViewController.java
index 1705f11..fcd8c80 100644
--- a/quickstep/src/com/android/launcher3/taskbar/NavbarButtonsViewController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/NavbarButtonsViewController.java
@@ -736,6 +736,7 @@
dp, mNavButtonsView, res, isInKidsMode, isInSetup, isThreeButtonNav,
TaskbarManager.isPhoneMode(dp), mDisplayController.getInfo().rotation);
navButtonLayoutter.layoutButtons(dp, isContextualButtonShowing());
+ updateNavButtonColor();
return;
}
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java
index f7f3bfd..b58ad38 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java
@@ -119,6 +119,7 @@
import com.android.systemui.unfold.util.ScopedUnfoldTransitionProgressProvider;
import java.io.PrintWriter;
+import java.util.Collections;
import java.util.Optional;
/**
@@ -997,9 +998,10 @@
if (recents == null) {
return;
}
- recents.getSplitSelectController().findLastActiveTaskAndRunCallback(
- info.getComponentKey(),
- foundTask -> {
+ recents.getSplitSelectController().findLastActiveTasksAndRunCallback(
+ Collections.singletonList(info.getComponentKey()),
+ foundTasks -> {
+ @Nullable Task foundTask = foundTasks.get(0);
if (foundTask != null) {
TaskView foundTaskView =
recents.getTaskViewByTaskId(foundTask.key.id);
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarManager.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarManager.java
index 1809d40..528cb30 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarManager.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarManager.java
@@ -40,6 +40,7 @@
import android.net.Uri;
import android.os.Handler;
import android.os.SystemProperties;
+import android.os.Trace;
import android.provider.Settings;
import android.util.Log;
import android.view.Display;
@@ -180,6 +181,8 @@
@Override
public void onConfigurationChanged(Configuration newConfig) {
+ Trace.instantForTrack(Trace.TRACE_TAG_APP, "TaskbarManager",
+ "onConfigurationChanged: " + newConfig);
debugWhyTaskbarNotDestroyed(
"TaskbarManager#mComponentCallbacks.onConfigurationChanged: " + newConfig);
DeviceProfile dp = mUserUnlocked
@@ -347,38 +350,44 @@
*/
@VisibleForTesting
public void recreateTaskbar() {
- DeviceProfile dp = mUserUnlocked ?
+ Trace.beginSection("recreateTaskbar");
+ try {
+ DeviceProfile dp = mUserUnlocked ?
LauncherAppState.getIDP(mContext).getDeviceProfile(mContext) : null;
- destroyExistingTaskbar();
+ destroyExistingTaskbar();
- boolean isTaskbarEnabled = dp != null && isTaskbarPresent(dp);
- debugWhyTaskbarNotDestroyed("recreateTaskbar: isTaskbarEnabled=" + isTaskbarEnabled
+ boolean isTaskbarEnabled = dp != null && isTaskbarPresent(dp);
+ debugWhyTaskbarNotDestroyed("recreateTaskbar: isTaskbarEnabled=" + isTaskbarEnabled
+ " [dp != null (i.e. mUserUnlocked)]=" + (dp != null)
+ " FLAG_HIDE_NAVBAR_WINDOW=" + FLAG_HIDE_NAVBAR_WINDOW
+ " dp.isTaskbarPresent=" + (dp == null ? "null" : dp.isTaskbarPresent));
- if (!isTaskbarEnabled) {
- SystemUiProxy.INSTANCE.get(mContext)
+ if (!isTaskbarEnabled) {
+ SystemUiProxy.INSTANCE.get(mContext)
.notifyTaskbarStatus(/* visible */ false, /* stashed */ false);
- return;
- }
+ return;
+ }
- if (mTaskbarActivityContext == null) {
- mTaskbarActivityContext = new TaskbarActivityContext(mContext, dp, mNavButtonController,
+ if (mTaskbarActivityContext == null) {
+ mTaskbarActivityContext = new TaskbarActivityContext(mContext, dp,
+ mNavButtonController,
mUnfoldProgressProvider);
- } else {
- mTaskbarActivityContext.updateDeviceProfile(dp);
- }
- mTaskbarActivityContext.init(mSharedState);
+ } else {
+ mTaskbarActivityContext.updateDeviceProfile(dp);
+ }
+ mTaskbarActivityContext.init(mSharedState);
- if (mActivity != null) {
- mTaskbarActivityContext.setUIController(
+ if (mActivity != null) {
+ mTaskbarActivityContext.setUIController(
createTaskbarUIControllerForActivity(mActivity));
- }
+ }
- // We to wait until user unlocks the device to attach listener.
- LauncherPrefs.get(mContext).addListener(mTaskbarPinningPreferenceChangeListener,
+ // We to wait until user unlocks the device to attach listener.
+ LauncherPrefs.get(mContext).addListener(mTaskbarPinningPreferenceChangeListener,
TASKBAR_PINNING);
+ } finally {
+ Trace.endSection();
+ }
}
public void onSystemUiFlagsChanged(int systemUiStateFlags) {
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarThresholdUtils.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarThresholdUtils.java
new file mode 100644
index 0000000..5b6fbef
--- /dev/null
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarThresholdUtils.java
@@ -0,0 +1,87 @@
+/*
+ * 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.taskbar;
+
+import static com.android.launcher3.Utilities.dpToPx;
+import static com.android.launcher3.Utilities.dpiFromPx;
+
+import android.content.res.Resources;
+import android.util.DisplayMetrics;
+
+import androidx.core.content.res.ResourcesCompat;
+
+import com.android.launcher3.DeviceProfile;
+import com.android.launcher3.R;
+import com.android.launcher3.config.FeatureFlags;
+
+/**
+ * Utility class that contains the different taskbar thresholds logic.
+ */
+public class TaskbarThresholdUtils {
+
+ // We divide the screen into this many parts, and use the result to scale the thresholds to
+ // any size device. Note that this value was calculated arbitrarily by using two tablet devices
+ // as data points.
+ private static final float SCREEN_UNITS = 1 / 80f;
+
+ private static int getThreshold(Resources r, DeviceProfile dp, int thresholdDimen,
+ int multiplierDimen) {
+ if (!FeatureFlags.ENABLE_DYNAMIC_TASKBAR_THRESHOLDS.get()) {
+ return r.getDimensionPixelSize(thresholdDimen);
+ }
+
+ float landscapeScreenHeight = dp.isLandscape ? dp.heightPx : dp.widthPx;
+ float screenPart = (landscapeScreenHeight * SCREEN_UNITS);
+ float defaultDp = dpiFromPx(screenPart, DisplayMetrics.DENSITY_DEVICE_STABLE);
+ float thisDp = dpToPx(defaultDp);
+ float multiplier = ResourcesCompat.getFloat(r, multiplierDimen);
+ float value = (thisDp) * multiplier;
+
+ return Math.round(value);
+ }
+
+ /**
+ * Returns the threshold that determines if we should show taskbar.
+ */
+ public static int getFromNavThreshold(Resources r, DeviceProfile dp) {
+ return getThreshold(r, dp, R.dimen.taskbar_from_nav_threshold,
+ R.dimen.taskbar_nav_threshold_mult);
+ }
+
+ /**
+ * Returns the threshold that we start moving the app window.
+ */
+ public static int getAppWindowThreshold(Resources r, DeviceProfile dp) {
+ return getThreshold(r, dp, R.dimen.taskbar_app_window_threshold,
+ R.dimen.taskbar_app_window_threshold_mult);
+ }
+
+ /**
+ * Returns the threshold for whether we land in home or overview.
+ */
+ public static int getHomeOverviewThreshold(Resources r, DeviceProfile dp) {
+ return getThreshold(r, dp, R.dimen.taskbar_home_overview_threshold,
+ R.dimen.taskbar_home_overview_threshold_mult);
+ }
+
+ /**
+ * Returns the threshold that we use to allow swipe to catch up to finger.
+ */
+ public static int getCatchUpThreshold(Resources r, DeviceProfile dp) {
+ return getThreshold(r, dp, R.dimen.taskbar_catch_up_threshold,
+ R.dimen.taskbar_catch_up_threshold_mult);
+ }
+}
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarUIController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarUIController.java
index 7154731..6fad655 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarUIController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarUIController.java
@@ -40,8 +40,11 @@
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.Arrays;
+import java.util.Collections;
import java.util.stream.Stream;
/**
@@ -204,9 +207,10 @@
return;
}
- recentsView.getSplitSelectController().findLastActiveTaskAndRunCallback(
- splitSelectSource.itemInfo.getComponentKey(),
- foundTask -> {
+ recentsView.getSplitSelectController().findLastActiveTasksAndRunCallback(
+ Collections.singletonList(splitSelectSource.itemInfo.getComponentKey()),
+ foundTasks -> {
+ @Nullable Task foundTask = foundTasks.get(0);
splitSelectSource.alreadyRunningTaskId = foundTask == null
? INVALID_TASK_ID
: foundTask.key.id;
@@ -221,9 +225,10 @@
*/
public void triggerSecondAppForSplit(ItemInfoWithIcon info, Intent intent, View startingView) {
RecentsView recents = getRecentsView();
- recents.getSplitSelectController().findLastActiveTaskAndRunCallback(
- info.getComponentKey(),
- foundTask -> {
+ recents.getSplitSelectController().findLastActiveTasksAndRunCallback(
+ Collections.singletonList(info.getComponentKey()),
+ foundTasks -> {
+ @Nullable Task foundTask = foundTasks.get(0);
if (foundTask != null) {
TaskView foundTaskView = recents.getTaskViewByTaskId(foundTask.key.id);
// TODO (b/266482558): This additional null check is needed because there
diff --git a/quickstep/src/com/android/launcher3/taskbar/navbutton/PhoneLandscapeNavLayoutter.kt b/quickstep/src/com/android/launcher3/taskbar/navbutton/PhoneLandscapeNavLayoutter.kt
index 92715a7..aeec2c6 100644
--- a/quickstep/src/com/android/launcher3/taskbar/navbutton/PhoneLandscapeNavLayoutter.kt
+++ b/quickstep/src/com/android/launcher3/taskbar/navbutton/PhoneLandscapeNavLayoutter.kt
@@ -52,7 +52,6 @@
navContainerParams.apply {
width = taskbarDimensions.x
height = ViewGroup.LayoutParams.MATCH_PARENT
- gravity = Gravity.CENTER
topMargin = endStartMargins
bottomMargin = endStartMargins
marginEnd = 0
@@ -65,6 +64,7 @@
navButtonContainer.addView(backButton)
navButtonContainer.layoutParams = navContainerParams
+ navButtonContainer.gravity = Gravity.CENTER_HORIZONTAL
// Add the spaces in between the nav buttons
val spaceInBetween: Int =
diff --git a/quickstep/src/com/android/launcher3/taskbar/navbutton/PhonePortraitNavLayoutter.kt b/quickstep/src/com/android/launcher3/taskbar/navbutton/PhonePortraitNavLayoutter.kt
index 7f7fda7..e97e378 100644
--- a/quickstep/src/com/android/launcher3/taskbar/navbutton/PhonePortraitNavLayoutter.kt
+++ b/quickstep/src/com/android/launcher3/taskbar/navbutton/PhonePortraitNavLayoutter.kt
@@ -46,23 +46,27 @@
DimensionUtils.getTaskbarPhoneDimensions(dp, resources,
TaskbarManager.isPhoneMode(dp))
val endStartMargins = resources.getDimensionPixelSize(R.dimen.taskbar_nav_buttons_size)
- navContainerParams.width = taskbarDimensions.x
- navContainerParams.height = ViewGroup.LayoutParams.MATCH_PARENT
- navContainerParams.gravity = Gravity.CENTER_VERTICAL
// Ensure order of buttons is correct
navButtonContainer.removeAllViews()
navButtonContainer.orientation = LinearLayout.HORIZONTAL
- navContainerParams.topMargin = 0
- navContainerParams.bottomMargin = 0
- navContainerParams.marginEnd = endStartMargins
- navContainerParams.marginStart = endStartMargins
+
+ navContainerParams.apply {
+ width = taskbarDimensions.x
+ height = ViewGroup.LayoutParams.MATCH_PARENT
+ topMargin = 0
+ bottomMargin = 0
+ marginEnd = endStartMargins
+ marginStart = endStartMargins
+ }
+
// Swap recents and back button in case we were landscape prior to this
navButtonContainer.addView(backButton)
navButtonContainer.addView(homeButton)
navButtonContainer.addView(recentsButton)
navButtonContainer.layoutParams = navContainerParams
+ navButtonContainer.gravity = Gravity.CENTER_VERTICAL
// Add the spaces in between the nav buttons
val spaceInBetween =
diff --git a/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java b/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java
index 20383f4..45d2fb0 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java
@@ -117,6 +117,7 @@
import com.android.launcher3.model.BgDataModel.FixedContainerItems;
import com.android.launcher3.model.WellbeingModel;
import com.android.launcher3.model.data.ItemInfo;
+import com.android.launcher3.model.data.WorkspaceItemInfo;
import com.android.launcher3.popup.SystemShortcut;
import com.android.launcher3.proxy.ProxyActivityStarter;
import com.android.launcher3.statehandlers.DepthController;
@@ -173,6 +174,7 @@
import com.android.quickstep.views.OverviewActionsView;
import com.android.quickstep.views.RecentsView;
import com.android.quickstep.views.TaskView;
+import com.android.systemui.shared.recents.model.Task;
import com.android.systemui.shared.system.ActivityManagerWrapper;
import com.android.systemui.unfold.RemoteUnfoldSharedComponent;
import com.android.systemui.unfold.UnfoldSharedComponent;
@@ -618,9 +620,10 @@
RecentsView recentsView = getOverviewPanel();
// Check if there is already an instance of this app running, if so, initiate the split
// using that.
- mSplitSelectStateController.findLastActiveTaskAndRunCallback(
- splitSelectSource.itemInfo.getComponentKey(),
- foundTask -> {
+ mSplitSelectStateController.findLastActiveTasksAndRunCallback(
+ Collections.singletonList(splitSelectSource.itemInfo.getComponentKey()),
+ foundTasks -> {
+ @Nullable Task foundTask = foundTasks.get(0);
boolean taskWasFound = foundTask != null;
splitSelectSource.alreadyRunningTaskId = taskWasFound
? foundTask.key.id
@@ -1326,6 +1329,13 @@
: groupTask.mSplitBounds.leftTaskPercent);
}
+ /**
+ * Launches two apps as an app pair.
+ */
+ public void launchAppPair(WorkspaceItemInfo app1, WorkspaceItemInfo app2) {
+ mSplitSelectStateController.getAppPairsController().launchAppPair(app1, app2);
+ }
+
public boolean canStartHomeSafely() {
OverviewCommandHelper overviewCommandHelper = mTISBindHelper.getOverviewCommandHelper();
return overviewCommandHelper == null || overviewCommandHelper.canStartHomeSafely();
diff --git a/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java b/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java
index ff757b1..796840d 100644
--- a/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java
+++ b/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java
@@ -106,6 +106,7 @@
import com.android.launcher3.statehandlers.DesktopVisibilityController;
import com.android.launcher3.statemanager.BaseState;
import com.android.launcher3.statemanager.StatefulActivity;
+import com.android.launcher3.taskbar.TaskbarThresholdUtils;
import com.android.launcher3.taskbar.TaskbarUIController;
import com.android.launcher3.uioverrides.QuickstepLauncher;
import com.android.launcher3.util.ActivityLifecycleCallbacksAdapter;
@@ -379,12 +380,12 @@
mTaskbarAlreadyOpen = controller != null && !controller.isTaskbarStashed();
mIsTaskbarAllAppsOpen = controller != null && controller.isTaskbarAllAppsOpen();
mTaskbarAppWindowThreshold =
- res.getDimensionPixelSize(R.dimen.taskbar_app_window_threshold);
+ TaskbarThresholdUtils.getAppWindowThreshold(res, mDp);
boolean swipeWillNotShowTaskbar = mTaskbarAlreadyOpen || mGestureState.isTrackpadGesture();
mTaskbarHomeOverviewThreshold = swipeWillNotShowTaskbar
? 0
- : res.getDimensionPixelSize(R.dimen.taskbar_home_overview_threshold);
- mTaskbarCatchUpThreshold = res.getDimensionPixelSize(R.dimen.taskbar_catch_up_threshold);
+ : TaskbarThresholdUtils.getHomeOverviewThreshold(res, mDp);
+ mTaskbarCatchUpThreshold = TaskbarThresholdUtils.getCatchUpThreshold(res, mDp);
}
@Nullable
diff --git a/quickstep/src/com/android/quickstep/TaskShortcutFactory.java b/quickstep/src/com/android/quickstep/TaskShortcutFactory.java
index 56f407c..901690b 100644
--- a/quickstep/src/com/android/quickstep/TaskShortcutFactory.java
+++ b/quickstep/src/com/android/quickstep/TaskShortcutFactory.java
@@ -140,6 +140,7 @@
@Override
public void onClick(View view) {
+ dismissTaskMenuView(mTarget);
((RecentsView) mTarget.getOverviewPanel())
.getSplitSelectController().getAppPairsController().saveAppPair(mTaskView);
}
diff --git a/quickstep/src/com/android/quickstep/inputconsumers/TaskbarUnstashInputConsumer.java b/quickstep/src/com/android/quickstep/inputconsumers/TaskbarUnstashInputConsumer.java
index 172c9e9..4b13cd1 100644
--- a/quickstep/src/com/android/quickstep/inputconsumers/TaskbarUnstashInputConsumer.java
+++ b/quickstep/src/com/android/quickstep/inputconsumers/TaskbarUnstashInputConsumer.java
@@ -37,6 +37,7 @@
import com.android.launcher3.R;
import com.android.launcher3.Utilities;
import com.android.launcher3.taskbar.TaskbarActivityContext;
+import com.android.launcher3.taskbar.TaskbarThresholdUtils;
import com.android.launcher3.taskbar.TaskbarTranslationController.TransitionCallback;
import com.android.launcher3.taskbar.bubbles.BubbleControllers;
import com.android.launcher3.touch.OverScroll;
@@ -94,7 +95,8 @@
Resources res = context.getResources();
mUnstashArea = res.getDimensionPixelSize(R.dimen.taskbar_unstash_input_area);
- mTaskbarNavThreshold = res.getDimensionPixelSize(R.dimen.taskbar_from_nav_threshold);
+ mTaskbarNavThreshold = TaskbarThresholdUtils.getFromNavThreshold(res,
+ taskbarActivityContext.getDeviceProfile());
mTaskbarNavThresholdY = taskbarActivityContext.getDeviceProfile().heightPx
- mTaskbarNavThreshold;
mIsTaskbarAllAppsOpen =
diff --git a/quickstep/src/com/android/quickstep/util/AppPairsController.java b/quickstep/src/com/android/quickstep/util/AppPairsController.java
index cbde257..1a7099d 100644
--- a/quickstep/src/com/android/quickstep/util/AppPairsController.java
+++ b/quickstep/src/com/android/quickstep/util/AppPairsController.java
@@ -17,19 +17,30 @@
package com.android.quickstep.util;
+import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_APP_PAIR_LAUNCH;
import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
import static com.android.launcher3.util.Executors.MODEL_EXECUTOR;
+import android.app.ActivityTaskManager;
import android.content.Context;
+import android.content.Intent;
+
+import androidx.annotation.Nullable;
import com.android.launcher3.Launcher;
import com.android.launcher3.LauncherAppState;
import com.android.launcher3.LauncherSettings;
import com.android.launcher3.accessibility.LauncherAccessibilityDelegate;
import com.android.launcher3.icons.IconCache;
+import com.android.launcher3.logging.StatsLogManager;
import com.android.launcher3.model.data.FolderInfo;
import com.android.launcher3.model.data.WorkspaceItemInfo;
+import com.android.launcher3.util.ComponentKey;
+import com.android.launcher3.util.SplitConfigurationOptions;
import com.android.quickstep.views.TaskView;
+import com.android.systemui.shared.recents.model.Task;
+
+import java.util.Arrays;
/**
* Mini controller class that handles app pair interactions: saving, modifying, deleting, etc.
@@ -52,10 +63,13 @@
private final Context mContext;
private final SplitSelectStateController mSplitSelectStateController;
+ private final StatsLogManager mStatsLogManager;
public AppPairsController(Context context,
- SplitSelectStateController splitSelectStateController) {
+ SplitSelectStateController splitSelectStateController,
+ StatsLogManager statsLogManager) {
mContext = context;
mSplitSelectStateController = splitSelectStateController;
+ mStatsLogManager = statsLogManager;
}
/**
@@ -84,11 +98,51 @@
LauncherAccessibilityDelegate delegate =
Launcher.getLauncher(mContext).getAccessibilityDelegate();
if (delegate != null) {
- MAIN_EXECUTOR.execute(() -> delegate.addToWorkspace(newAppPair, true));
+ delegate.addToWorkspace(newAppPair, true);
+ mStatsLogManager.logger().withItemInfo(newAppPair)
+ .log(StatsLogManager.LauncherEvent.LAUNCHER_APP_PAIR_SAVE);
}
});
});
+ }
+ /**
+ * Launches an app pair by searching the RecentsModel for running instances of each app, and
+ * staging either those running instances or launching the apps as new Intents.
+ */
+ public void launchAppPair(WorkspaceItemInfo app1, WorkspaceItemInfo app2) {
+ ComponentKey app1Key = new ComponentKey(app1.getTargetComponent(), app1.user);
+ ComponentKey app2Key = new ComponentKey(app2.getTargetComponent(), app2.user);
+ mSplitSelectStateController.findLastActiveTasksAndRunCallback(
+ Arrays.asList(app1Key, app2Key),
+ foundTasks -> {
+ @Nullable Task foundTask1 = foundTasks.get(0);
+ Intent task1Intent;
+ int task1Id;
+ if (foundTask1 != null) {
+ task1Id = foundTask1.key.id;
+ task1Intent = null;
+ } else {
+ task1Id = ActivityTaskManager.INVALID_TASK_ID;
+ task1Intent = app1.intent;
+ }
+
+ mSplitSelectStateController.setInitialTaskSelect(task1Intent,
+ SplitConfigurationOptions.STAGE_POSITION_TOP_OR_LEFT,
+ app1,
+ LAUNCHER_APP_PAIR_LAUNCH,
+ task1Id);
+
+ @Nullable Task foundTask2 = foundTasks.get(1);
+ if (foundTask2 != null) {
+ mSplitSelectStateController.setSecondTask(foundTask2);
+ } else {
+ mSplitSelectStateController.setSecondTask(
+ app2.intent, app2.user);
+ }
+
+ mSplitSelectStateController.launchSplitTasks();
+ });
}
/**
diff --git a/quickstep/src/com/android/quickstep/util/SplitAnimationController.kt b/quickstep/src/com/android/quickstep/util/SplitAnimationController.kt
index bcb9cec..c3774eb 100644
--- a/quickstep/src/com/android/quickstep/util/SplitAnimationController.kt
+++ b/quickstep/src/com/android/quickstep/util/SplitAnimationController.kt
@@ -60,8 +60,6 @@
)
}
- var splitInstructionsView: SplitInstructionsView? = null
-
/**
* Returns different elements to animate for the initial split selection animation
* depending on the state of the surface from which the split was initiated
@@ -235,7 +233,8 @@
animatorSet.addListener(object : AnimatorListenerAdapter() {
override fun onAnimationEnd(animation: Animator) {
splitSelectStateController.resetState()
- safeRemoveViewFromDragLayer(launcher, splitInstructionsView)
+ safeRemoveViewFromDragLayer(launcher,
+ splitSelectStateController.splitInstructionsView)
}
})
return animatorSet
@@ -246,8 +245,9 @@
* app for splitscreen
*/
fun getShowSplitInstructionsAnim(launcher: StatefulActivity<*>) : PendingAnimation {
- safeRemoveViewFromDragLayer(launcher, splitInstructionsView)
- splitInstructionsView = SplitInstructionsView.getSplitInstructionsView(launcher)
+ safeRemoveViewFromDragLayer(launcher, splitSelectStateController.splitInstructionsView)
+ val splitInstructionsView = SplitInstructionsView.getSplitInstructionsView(launcher)
+ splitSelectStateController.splitInstructionsView = splitInstructionsView
val timings = AnimUtils.getDeviceOverviewToSplitTimings(launcher.deviceProfile.isTablet)
val anim = PendingAnimation(100 /*duration */)
anim.setViewAlpha(splitInstructionsView, 1f,
@@ -267,7 +267,7 @@
/** Removes the split instructions view from [launcher] drag layer. */
fun removeSplitInstructionsView(launcher: StatefulActivity<*>) {
- safeRemoveViewFromDragLayer(launcher, splitInstructionsView)
+ safeRemoveViewFromDragLayer(launcher, splitSelectStateController.splitInstructionsView)
}
private fun safeRemoveViewFromDragLayer(launcher: StatefulActivity<*>, view: View?) {
diff --git a/quickstep/src/com/android/quickstep/util/SplitSelectStateController.java b/quickstep/src/com/android/quickstep/util/SplitSelectStateController.java
index 7ba6d42..453a1bd 100644
--- a/quickstep/src/com/android/quickstep/util/SplitSelectStateController.java
+++ b/quickstep/src/com/android/quickstep/util/SplitSelectStateController.java
@@ -73,11 +73,13 @@
import com.android.quickstep.TaskViewUtils;
import com.android.quickstep.views.FloatingTaskView;
import com.android.quickstep.views.GroupedTaskView;
+import com.android.quickstep.views.SplitInstructionsView;
import com.android.systemui.shared.recents.model.Task;
import com.android.systemui.shared.system.RemoteAnimationRunnerCompat;
import java.io.PrintWriter;
import java.util.ArrayList;
+import java.util.Collections;
import java.util.List;
import java.util.function.Consumer;
@@ -112,6 +114,7 @@
private GroupedTaskView mLaunchingTaskView;
private FloatingTaskView mFirstFloatingTaskView;
+ private SplitInstructionsView mSplitInstructionsView;
private final List<SplitSelectionListener> mSplitSelectionListeners = new ArrayList<>();
@@ -126,7 +129,7 @@
mDepthController = depthController;
mRecentTasksModel = recentsModel;
mSplitAnimationController = new SplitAnimationController(this);
- mAppPairsController = new AppPairsController(context, this);
+ mAppPairsController = new AppPairsController(context, this, statsLogManager);
mSplitSelectDataHolder = new SplitSelectDataHolder(mContext);
}
@@ -153,37 +156,46 @@
}
/**
- * Pulls the list of active Tasks from RecentsModel, and finds the most recently active Task
- * matching a given ComponentName. Then uses that Task (which could be null) with the given
- * callback.
+ * Maps a List<ComponentKey> to List<@Nullable Task>, searching through active Tasks in
+ * RecentsModel. If found, the Task will be the most recently-interacted-with instance of that
+ * Task. Then runs the given callback on that List.
* <p>
* 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(
- @Nullable ComponentKey componentKey, Consumer<Task> callback) {
+ public void findLastActiveTasksAndRunCallback(
+ @Nullable List<ComponentKey> componentKeys, Consumer<List<Task>> callback) {
mRecentTasksModel.getTasks(taskGroups -> {
- if (componentKey == null) {
- callback.accept(null);
+ if (componentKeys == null || componentKeys.isEmpty()) {
+ callback.accept(Collections.emptyList());
return;
}
- Task lastActiveTask = null;
- // Loop through tasks in reverse, since they are ordered with most-recent tasks last.
- for (int i = taskGroups.size() - 1; i >= 0; i--) {
- GroupTask groupTask = taskGroups.get(i);
- Task task1 = groupTask.task1;
- if (isInstanceOfComponent(task1, componentKey)) {
- lastActiveTask = task1;
- break;
+
+ List<Task> lastActiveTasks = new ArrayList<>();
+ // For each key we are looking for, add to lastActiveTasks with the corresponding Task
+ // (or null if not found).
+ for (ComponentKey key : componentKeys) {
+ Task lastActiveTask = null;
+ // Loop through tasks in reverse, since they are ordered with most-recent tasks last
+ for (int i = taskGroups.size() - 1; i >= 0; i--) {
+ GroupTask groupTask = taskGroups.get(i);
+ Task task1 = groupTask.task1;
+ // Don't add duplicate Tasks
+ if (isInstanceOfComponent(task1, key) && !lastActiveTasks.contains(task1)) {
+ lastActiveTask = task1;
+ break;
+ }
+ Task task2 = groupTask.task2;
+ if (isInstanceOfComponent(task2, key) && !lastActiveTasks.contains(task2)) {
+ lastActiveTask = task2;
+ break;
+ }
}
- Task task2 = groupTask.task2;
- if (isInstanceOfComponent(task2, componentKey)) {
- lastActiveTask = task2;
- break;
- }
+
+ lastActiveTasks.add(lastActiveTask);
}
- callback.accept(lastActiveTask);
+ callback.accept(lastActiveTasks);
});
}
@@ -226,7 +238,7 @@
* To be called when the both split tasks are ready to be launched. Call after launcher side
* animations are complete.
*/
- public void launchSplitTasks(Consumer<Boolean> callback) {
+ public void launchSplitTasks(@Nullable Consumer<Boolean> callback) {
Pair<InstanceId, com.android.launcher3.logging.InstanceId> instanceIds =
LogUtils.getShellShareableInstanceId();
launchTasks(callback, false /* freezeTaskList */, DEFAULT_SPLIT_RATIO,
@@ -239,6 +251,14 @@
}
/**
+ * A version of {@link #launchTasks(Consumer, boolean, float, InstanceId)} with no success
+ * callback.
+ */
+ public void launchSplitTasks() {
+ launchSplitTasks(null);
+ }
+
+ /**
* To be called as soon as user selects the second task (even if animations aren't complete)
* @param task The second task that will be launched.
*/
@@ -271,8 +291,8 @@
* create a split instance, null for cases that bring existing instaces to the
* foreground (quickswitch, launching previous pairs from overview)
*/
- public void launchTasks(Consumer<Boolean> callback, boolean freezeTaskList, float splitRatio,
- @Nullable InstanceId shellInstanceId) {
+ public void launchTasks(@Nullable Consumer<Boolean> callback, boolean freezeTaskList,
+ float splitRatio, @Nullable InstanceId shellInstanceId) {
TestLogging.recordEvent(
TestProtocol.SEQUENCE_MAIN, "launchSplitTasks");
final ActivityOptions options1 = ActivityOptions.makeBasic();
@@ -457,7 +477,7 @@
}
private RemoteTransition getShellRemoteTransition(int firstTaskId, int secondTaskId,
- Consumer<Boolean> callback, String transitionName) {
+ @Nullable Consumer<Boolean> callback, String transitionName) {
final RemoteSplitLaunchTransitionRunner animationRunner =
new RemoteSplitLaunchTransitionRunner(firstTaskId, secondTaskId, callback);
return new RemoteTransition(animationRunner,
@@ -465,7 +485,7 @@
}
private RemoteAnimationAdapter getLegacyRemoteAdapter(int firstTaskId, int secondTaskId,
- Consumer<Boolean> callback) {
+ @Nullable Consumer<Boolean> callback) {
final RemoteSplitLaunchAnimationRunner animationRunner =
new RemoteSplitLaunchAnimationRunner(firstTaskId, secondTaskId, callback);
return new RemoteAnimationAdapter(animationRunner, 300, 150,
@@ -514,7 +534,7 @@
private final Consumer<Boolean> mSuccessCallback;
RemoteSplitLaunchTransitionRunner(int initialTaskId, int secondTaskId,
- Consumer<Boolean> callback) {
+ @Nullable Consumer<Boolean> callback) {
mInitialTaskId = initialTaskId;
mSecondTaskId = secondTaskId;
mSuccessCallback = callback;
@@ -563,7 +583,7 @@
private final Consumer<Boolean> mSuccessCallback;
RemoteSplitLaunchAnimationRunner(int initialTaskId, int secondTaskId,
- Consumer<Boolean> successCallback) {
+ @Nullable Consumer<Boolean> successCallback) {
mInitialTaskId = initialTaskId;
mSecondTaskId = secondTaskId;
mSuccessCallback = successCallback;
@@ -612,6 +632,7 @@
mAnimateCurrentTaskDismissal = false;
mDismissingFromSplitPair = false;
mFirstFloatingTaskView = null;
+ mSplitInstructionsView = null;
}
/**
@@ -642,11 +663,20 @@
mFirstFloatingTaskView = floatingTaskView;
}
+ public void setSplitInstructionsView(SplitInstructionsView splitInstructionsView) {
+ mSplitInstructionsView = splitInstructionsView;
+ }
+
@Nullable
public FloatingTaskView getFirstFloatingTaskView() {
return mFirstFloatingTaskView;
}
+ @Nullable
+ public SplitInstructionsView getSplitInstructionsView() {
+ return mSplitInstructionsView;
+ }
+
public AppPairsController getAppPairsController() {
return mAppPairsController;
}
diff --git a/quickstep/src/com/android/quickstep/views/DigitalWellBeingToast.java b/quickstep/src/com/android/quickstep/views/DigitalWellBeingToast.java
index 7bbe36a..e5a0e10 100644
--- a/quickstep/src/com/android/quickstep/views/DigitalWellBeingToast.java
+++ b/quickstep/src/com/android/quickstep/views/DigitalWellBeingToast.java
@@ -145,23 +145,29 @@
mAppUsageLimitTimeMs = mAppRemainingTimeMs = -1;
mTask = task;
THREAD_POOL_EXECUTOR.execute(() -> {
- final AppUsageLimit usageLimit = mLauncherApps.getAppUsageLimit(
- mTask.getTopComponent().getPackageName(),
- UserHandle.of(mTask.key.userId));
+ AppUsageLimit usageLimit = null;
+ try {
+ usageLimit = mLauncherApps.getAppUsageLimit(
+ mTask.getTopComponent().getPackageName(),
+ UserHandle.of(mTask.key.userId));
+ } catch (Exception e) {
+ Log.e(TAG, "Error initializing digital well being toast", e);
+ }
+ final long appUsageLimitTimeMs =
+ usageLimit != null ? usageLimit.getTotalUsageLimit() : -1;
+ final long appRemainingTimeMs =
+ usageLimit != null ? usageLimit.getUsageRemaining() : -1;
- final long appUsageLimitTimeMs =
- usageLimit != null ? usageLimit.getTotalUsageLimit() : -1;
- final long appRemainingTimeMs =
- usageLimit != null ? usageLimit.getUsageRemaining() : -1;
+ mTaskView.post(() -> {
+ if (appUsageLimitTimeMs < 0 || appRemainingTimeMs < 0) {
+ setNoLimit();
+ } else {
+ setLimit(appUsageLimitTimeMs, appRemainingTimeMs);
+ }
+ });
- mTaskView.post(() -> {
- if (appUsageLimitTimeMs < 0 || appRemainingTimeMs < 0) {
- setNoLimit();
- } else {
- setLimit(appUsageLimitTimeMs, appRemainingTimeMs);
}
- });
- });
+ );
}
public void setSplitConfiguration(SplitBounds splitBounds) {
diff --git a/quickstep/src/com/android/quickstep/views/RecentsView.java b/quickstep/src/com/android/quickstep/views/RecentsView.java
index 4da979e..4b8741d 100644
--- a/quickstep/src/com/android/quickstep/views/RecentsView.java
+++ b/quickstep/src/com/android/quickstep/views/RecentsView.java
@@ -683,8 +683,6 @@
private final Toast mSplitUnsupportedToast = Toast.makeText(getContext(),
R.string.toast_split_app_unsupported, Toast.LENGTH_SHORT);
- private SplitInstructionsView mSplitInstructionsView;
-
@Nullable
private SplitSelectSource mSplitSelectSource;
@@ -3252,19 +3250,21 @@
firstFloatingTaskView.setOnClickListener(this::animateToFullscreen);
// SplitInstructionsView: animate in
- safeRemoveDragLayerView(mSplitInstructionsView);
- mSplitInstructionsView = SplitInstructionsView.getSplitInstructionsView(mActivity);
- mSplitInstructionsView.setAlpha(0);
- anim.setViewAlpha(mSplitInstructionsView, 1, clampToProgress(LINEAR,
+ safeRemoveDragLayerView(mSplitSelectStateController.getSplitInstructionsView());
+ SplitInstructionsView splitInstructionsView =
+ SplitInstructionsView.getSplitInstructionsView(mActivity);
+ splitInstructionsView.setAlpha(0);
+ anim.setViewAlpha(splitInstructionsView, 1, clampToProgress(LINEAR,
timings.getInstructionsContainerFadeInStartOffset(),
timings.getInstructionsContainerFadeInEndOffset()));
- anim.setViewAlpha(mSplitInstructionsView.getTextView(), 1, clampToProgress(LINEAR,
+ anim.setViewAlpha(splitInstructionsView.getTextView(), 1, clampToProgress(LINEAR,
timings.getInstructionsTextFadeInStartOffset(),
timings.getInstructionsTextFadeInEndOffset()));
- anim.addFloat(mSplitInstructionsView, mSplitInstructionsView.UNFOLD, 0.1f, 1,
+ anim.addFloat(splitInstructionsView, splitInstructionsView.UNFOLD, 0.1f, 1,
clampToProgress(EMPHASIZED_DECELERATE,
timings.getInstructionsUnfoldStartOffset(),
timings.getInstructionsUnfoldEndOffset()));
+ mSplitSelectStateController.setSplitInstructionsView(splitInstructionsView);
InteractionJankMonitorWrapper.begin(this,
InteractionJankMonitorWrapper.CUJ_SPLIT_SCREEN_ENTER, "First tile selected");
@@ -4781,9 +4781,9 @@
mSecondFloatingTaskView.addConfirmAnimation(pendingAnimation, secondTaskStartingBounds,
secondTaskEndingBounds, true /* fadeWithThumbnail */, false /* isStagedTask */);
- pendingAnimation.setViewAlpha(mSplitInstructionsView, 0, clampToProgress(LINEAR,
- timings.getInstructionsFadeStartOffset(),
- timings.getInstructionsFadeEndOffset()));
+ pendingAnimation.setViewAlpha(mSplitSelectStateController.getSplitInstructionsView(), 0,
+ clampToProgress(LINEAR, timings.getInstructionsFadeStartOffset(),
+ timings.getInstructionsFadeEndOffset()));
pendingAnimation.addEndListener(aBoolean -> {
mSplitSelectStateController.launchSplitTasks(
@@ -4819,9 +4819,8 @@
FeatureFlags.ENABLE_SPLIT_FROM_WORKSPACE_TO_WORKSPACE.get()) {
safeRemoveDragLayerView(mSplitSelectStateController.getFirstFloatingTaskView());
safeRemoveDragLayerView(mSecondFloatingTaskView);
- safeRemoveDragLayerView(mSplitInstructionsView);
+ safeRemoveDragLayerView(mSplitSelectStateController.getSplitInstructionsView());
mSecondFloatingTaskView = null;
- mSplitInstructionsView = null;
mSplitSelectSource = null;
mSplitSelectStateController.getSplitAnimationController()
.removeSplitInstructionsView(mActivity);
@@ -4921,8 +4920,8 @@
taskViewsFloat.first.set(this, getSplitSelectTranslation());
taskViewsFloat.second.set(this, 0f);
- if (mSplitInstructionsView != null) {
- mSplitInstructionsView.ensureProperRotation();
+ if (mSplitSelectStateController.getSplitInstructionsView() != null) {
+ mSplitSelectStateController.getSplitInstructionsView().ensureProperRotation();
}
}
@@ -6034,7 +6033,7 @@
@Nullable
public SplitInstructionsView getSplitInstructionsView() {
- return mSplitInstructionsView;
+ return mSplitSelectStateController.getSplitInstructionsView();
}
/** Update the current activity locus id to show the enabled state of Overview */
diff --git a/quickstep/tests/src/com/android/quickstep/util/SplitSelectStateControllerTest.kt b/quickstep/tests/src/com/android/quickstep/util/SplitSelectStateControllerTest.kt
index 65542cf..69109c2 100644
--- a/quickstep/tests/src/com/android/quickstep/util/SplitSelectStateControllerTest.kt
+++ b/quickstep/tests/src/com/android/quickstep/util/SplitSelectStateControllerTest.kt
@@ -37,6 +37,7 @@
import com.android.quickstep.RecentsModel
import com.android.quickstep.SystemUiProxy
import com.android.systemui.shared.recents.model.Task
+import java.util.function.Consumer
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertNull
@@ -48,7 +49,6 @@
import org.mockito.Mockito.verify
import org.mockito.Mockito.`when`
import org.mockito.MockitoAnnotations
-import java.util.function.Consumer
@RunWith(AndroidJUnit4::class)
class SplitSelectStateControllerTest {
@@ -67,6 +67,9 @@
private val primaryUserHandle = UserHandle(ActivityManager.RunningTaskInfo().userId)
private val nonPrimaryUserHandle = UserHandle(ActivityManager.RunningTaskInfo().userId + 10)
+ private var taskIdCounter = 0
+ private fun getUniqueId(): Int { return ++taskIdCounter }
+
@Before
fun setup() {
MockitoAnnotations.initMocks(this)
@@ -100,15 +103,15 @@
tasks.add(groupTask2)
// Assertions happen in the callback we get from what we pass into
- // #findLastActiveTaskAndRunCallback
+ // #findLastActiveTasksAndRunCallback
val taskConsumer =
- Consumer<Task> { assertNull("No tasks should have matched", it /*task*/) }
+ Consumer<List<Task>> { assertNull("No tasks should have matched", it[0] /*task*/) }
// Capture callback from recentsModel#getTasks()
val consumer =
withArgCaptor<Consumer<ArrayList<GroupTask>>> {
- splitSelectStateController.findLastActiveTaskAndRunCallback(
- nonMatchingComponent,
+ splitSelectStateController.findLastActiveTasksAndRunCallback(
+ listOf(nonMatchingComponent),
taskConsumer
)
verify(recentsModel).getTasks(capture())
@@ -139,27 +142,27 @@
tasks.add(groupTask2)
// Assertions happen in the callback we get from what we pass into
- // #findLastActiveTaskAndRunCallback
+ // #findLastActiveTasksAndRunCallback
val taskConsumer =
- Consumer<Task> {
+ Consumer<List<Task>> {
assertEquals(
"ComponentName package mismatched",
- it.key.baseIntent.component.packageName,
+ it[0].key.baseIntent.component?.packageName,
matchingPackage
)
assertEquals(
"ComponentName class mismatched",
- it.key.baseIntent.component.className,
+ it[0].key.baseIntent.component?.className,
matchingClass
)
- assertEquals(it, groupTask1.task1)
+ assertEquals(it[0], groupTask1.task1)
}
// Capture callback from recentsModel#getTasks()
val consumer =
withArgCaptor<Consumer<ArrayList<GroupTask>>> {
- splitSelectStateController.findLastActiveTaskAndRunCallback(
- matchingComponent,
+ splitSelectStateController.findLastActiveTasksAndRunCallback(
+ listOf(matchingComponent),
taskConsumer
)
verify(recentsModel).getTasks(capture())
@@ -190,15 +193,15 @@
tasks.add(groupTask2)
// Assertions happen in the callback we get from what we pass into
- // #findLastActiveTaskAndRunCallback
+ // #findLastActiveTasksAndRunCallback
val taskConsumer =
- Consumer<Task> { assertNull("No tasks should have matched", it /*task*/) }
+ Consumer<List<Task>> { assertNull("No tasks should have matched", it[0] /*task*/) }
// Capture callback from recentsModel#getTasks()
val consumer =
withArgCaptor<Consumer<ArrayList<GroupTask>>> {
- splitSelectStateController.findLastActiveTaskAndRunCallback(
- nonPrimaryUserComponent,
+ splitSelectStateController.findLastActiveTasksAndRunCallback(
+ listOf(nonPrimaryUserComponent),
taskConsumer
)
verify(recentsModel).getTasks(capture())
@@ -231,28 +234,28 @@
tasks.add(groupTask2)
// Assertions happen in the callback we get from what we pass into
- // #findLastActiveTaskAndRunCallback
+ // #findLastActiveTasksAndRunCallback
val taskConsumer =
- Consumer<Task> {
+ Consumer<List<Task>> {
assertEquals(
"ComponentName package mismatched",
- it.key.baseIntent.component.packageName,
+ it[0].key.baseIntent.component?.packageName,
matchingPackage
)
assertEquals(
"ComponentName class mismatched",
- it.key.baseIntent.component.className,
+ it[0].key.baseIntent.component?.className,
matchingClass
)
- assertEquals("userId mismatched", it.key.userId, nonPrimaryUserHandle.identifier)
- assertEquals(it, groupTask1.task1)
+ assertEquals("userId mismatched", it[0].key.userId, nonPrimaryUserHandle.identifier)
+ assertEquals(it[0], groupTask1.task1)
}
// Capture callback from recentsModel#getTasks()
val consumer =
withArgCaptor<Consumer<ArrayList<GroupTask>>> {
- splitSelectStateController.findLastActiveTaskAndRunCallback(
- nonPrimaryUserComponent,
+ splitSelectStateController.findLastActiveTasksAndRunCallback(
+ listOf(nonPrimaryUserComponent),
taskConsumer
)
verify(recentsModel).getTasks(capture())
@@ -283,27 +286,200 @@
tasks.add(groupTask1)
// Assertions happen in the callback we get from what we pass into
- // #findLastActiveTaskAndRunCallback
+ // #findLastActiveTasksAndRunCallback
val taskConsumer =
- Consumer<Task> {
+ Consumer<List<Task>> {
assertEquals(
"ComponentName package mismatched",
- it.key.baseIntent.component.packageName,
+ it[0].key.baseIntent.component?.packageName,
matchingPackage
)
assertEquals(
"ComponentName class mismatched",
- it.key.baseIntent.component.className,
+ it[0].key.baseIntent.component?.className,
matchingClass
)
- assertEquals(it, groupTask2.task2)
+ assertEquals(it[0], groupTask1.task1)
}
// Capture callback from recentsModel#getTasks()
val consumer =
withArgCaptor<Consumer<ArrayList<GroupTask>>> {
- splitSelectStateController.findLastActiveTaskAndRunCallback(
- matchingComponent,
+ splitSelectStateController.findLastActiveTasksAndRunCallback(
+ listOf(matchingComponent),
+ taskConsumer
+ )
+ verify(recentsModel).getTasks(capture())
+ }
+
+ // Send our mocked tasks
+ consumer.accept(tasks)
+ }
+
+ @Test
+ fun activeTasks_multipleSearchShouldFindTask() {
+ val nonMatchingComponent = ComponentKey(ComponentName("no", "match"), primaryUserHandle)
+ val matchingPackage = "hotdog"
+ val matchingClass = "juice"
+ val matchingComponent =
+ ComponentKey(ComponentName(matchingPackage, matchingClass), primaryUserHandle)
+
+ val groupTask1 =
+ generateGroupTask(
+ ComponentName("hotdog", "pie"),
+ ComponentName("pumpkin", "pie")
+ )
+ val groupTask2 =
+ generateGroupTask(
+ ComponentName("pomegranate", "juice"),
+ ComponentName(matchingPackage, matchingClass)
+ )
+ val tasks: ArrayList<GroupTask> = ArrayList()
+ tasks.add(groupTask2)
+ tasks.add(groupTask1)
+
+ // Assertions happen in the callback we get from what we pass into
+ // #findLastActiveTasksAndRunCallback
+ val taskConsumer =
+ Consumer<List<Task>> {
+ assertEquals("Expected array length 2", 2, it.size)
+ assertNull("No tasks should have matched", it[0] /*task*/)
+ assertEquals(
+ "ComponentName package mismatched",
+ it[1].key.baseIntent.component?.packageName,
+ matchingPackage
+ )
+ assertEquals(
+ "ComponentName class mismatched",
+ it[1].key.baseIntent.component?.className,
+ matchingClass
+ )
+ assertEquals(it[1], groupTask2.task2)
+ }
+
+ // Capture callback from recentsModel#getTasks()
+ val consumer =
+ withArgCaptor<Consumer<ArrayList<GroupTask>>> {
+ splitSelectStateController.findLastActiveTasksAndRunCallback(
+ listOf(nonMatchingComponent, matchingComponent),
+ taskConsumer
+ )
+ verify(recentsModel).getTasks(capture())
+ }
+
+ // Send our mocked tasks
+ consumer.accept(tasks)
+ }
+
+ @Test
+ fun activeTasks_multipleSearchShouldNotFindSameTaskTwice() {
+ val matchingPackage = "hotdog"
+ val matchingClass = "juice"
+ val matchingComponent =
+ ComponentKey(ComponentName(matchingPackage, matchingClass), primaryUserHandle)
+
+ val groupTask1 =
+ generateGroupTask(
+ ComponentName("hotdog", "pie"),
+ ComponentName("pumpkin", "pie")
+ )
+ val groupTask2 =
+ generateGroupTask(
+ ComponentName("pomegranate", "juice"),
+ ComponentName(matchingPackage, matchingClass)
+ )
+ val tasks: ArrayList<GroupTask> = ArrayList()
+ tasks.add(groupTask2)
+ tasks.add(groupTask1)
+
+ // Assertions happen in the callback we get from what we pass into
+ // #findLastActiveTasksAndRunCallback
+ val taskConsumer =
+ Consumer<List<Task>> {
+ assertEquals("Expected array length 2", 2, it.size)
+ assertEquals(
+ "ComponentName package mismatched",
+ it[0].key.baseIntent.component?.packageName,
+ matchingPackage
+ )
+ assertEquals(
+ "ComponentName class mismatched",
+ it[0].key.baseIntent.component?.className,
+ matchingClass
+ )
+ assertEquals(it[0], groupTask2.task2)
+ assertNull("No tasks should have matched", it[1] /*task*/)
+ }
+
+ // Capture callback from recentsModel#getTasks()
+ val consumer =
+ withArgCaptor<Consumer<ArrayList<GroupTask>>> {
+ splitSelectStateController.findLastActiveTasksAndRunCallback(
+ listOf(matchingComponent, matchingComponent),
+ taskConsumer
+ )
+ verify(recentsModel).getTasks(capture())
+ }
+
+ // Send our mocked tasks
+ consumer.accept(tasks)
+ }
+
+ @Test
+ fun activeTasks_multipleSearchShouldFindDifferentInstancesOfSameTask() {
+ val matchingPackage = "hotdog"
+ val matchingClass = "juice"
+ val matchingComponent =
+ ComponentKey(ComponentName(matchingPackage, matchingClass), primaryUserHandle)
+
+ val groupTask1 =
+ generateGroupTask(
+ ComponentName(matchingPackage, matchingClass),
+ ComponentName("pumpkin", "pie")
+ )
+ val groupTask2 =
+ generateGroupTask(
+ ComponentName("pomegranate", "juice"),
+ ComponentName(matchingPackage, matchingClass)
+ )
+ val tasks: ArrayList<GroupTask> = ArrayList()
+ tasks.add(groupTask2)
+ tasks.add(groupTask1)
+
+ // Assertions happen in the callback we get from what we pass into
+ // #findLastActiveTasksAndRunCallback
+ val taskConsumer =
+ Consumer<List<Task>> {
+ assertEquals("Expected array length 2", 2, it.size)
+ assertEquals(
+ "ComponentName package mismatched",
+ it[0].key.baseIntent.component?.packageName,
+ matchingPackage
+ )
+ assertEquals(
+ "ComponentName class mismatched",
+ it[0].key.baseIntent.component?.className,
+ matchingClass
+ )
+ assertEquals(it[0], groupTask1.task1)
+ assertEquals(
+ "ComponentName package mismatched",
+ it[1].key.baseIntent.component?.packageName,
+ matchingPackage
+ )
+ assertEquals(
+ "ComponentName class mismatched",
+ it[1].key.baseIntent.component?.className,
+ matchingClass
+ )
+ assertEquals(it[1], groupTask2.task2)
+ }
+
+ // Capture callback from recentsModel#getTasks()
+ val consumer =
+ withArgCaptor<Consumer<ArrayList<GroupTask>>> {
+ splitSelectStateController.findLastActiveTasksAndRunCallback(
+ listOf(matchingComponent, matchingComponent),
taskConsumer
)
verify(recentsModel).getTasks(capture())
@@ -366,6 +542,7 @@
): GroupTask {
val task1 = Task()
var taskInfo = ActivityManager.RunningTaskInfo()
+ taskInfo.taskId = getUniqueId()
var intent = Intent()
intent.component = task1ComponentName
taskInfo.baseIntent = intent
@@ -373,6 +550,7 @@
val task2 = Task()
taskInfo = ActivityManager.RunningTaskInfo()
+ taskInfo.taskId = getUniqueId()
intent = Intent()
intent.component = task2ComponentName
taskInfo.baseIntent = intent
@@ -393,6 +571,7 @@
): GroupTask {
val task1 = Task()
var taskInfo = ActivityManager.RunningTaskInfo()
+ taskInfo.taskId = getUniqueId()
// Apply custom userHandle1
taskInfo.userId = userHandle1.identifier
var intent = Intent()
@@ -401,6 +580,7 @@
task1.key = Task.TaskKey(taskInfo)
val task2 = Task()
taskInfo = ActivityManager.RunningTaskInfo()
+ taskInfo.taskId = getUniqueId()
// Apply custom userHandle2
taskInfo.userId = userHandle2.identifier
intent = Intent()
diff --git a/res/layout/app_pair_icon.xml b/res/layout/app_pair_icon.xml
new file mode 100644
index 0000000..2b9a98b
--- /dev/null
+++ b/res/layout/app_pair_icon.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+
+<com.android.launcher3.apppairs.AppPairIcon
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="vertical"
+ android:focusable="true" >
+ <com.android.launcher3.views.DoubleShadowBubbleTextView
+ style="@style/BaseIcon.Workspace"
+ android:id="@+id/app_pair_icon_name"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:focusable="false"
+ android:layout_gravity="top" />
+</com.android.launcher3.apppairs.AppPairIcon>
diff --git a/src/com/android/launcher3/Launcher.java b/src/com/android/launcher3/Launcher.java
index 6df9aab..c737074 100644
--- a/src/com/android/launcher3/Launcher.java
+++ b/src/com/android/launcher3/Launcher.java
@@ -145,6 +145,7 @@
import com.android.launcher3.allapps.BaseSearchConfig;
import com.android.launcher3.allapps.DiscoveryBounce;
import com.android.launcher3.anim.PropertyListBuilder;
+import com.android.launcher3.apppairs.AppPairIcon;
import com.android.launcher3.celllayout.CellPosMapper;
import com.android.launcher3.celllayout.CellPosMapper.CellPos;
import com.android.launcher3.celllayout.CellPosMapper.TwoPanelCellPosMapper;
@@ -725,18 +726,23 @@
@Override
protected void onHandleConfigurationChanged() {
- if (!initDeviceProfile(mDeviceProfile.inv)) {
- return;
+ Trace.beginSection("Launcher#onHandleconfigurationChanged");
+ try {
+ if (!initDeviceProfile(mDeviceProfile.inv)) {
+ return;
+ }
+
+ dispatchDeviceProfileChanged();
+ reapplyUi();
+ mDragLayer.recreateControllers();
+
+ // Calling onSaveInstanceState ensures that static cache used by listWidgets is
+ // initialized properly.
+ onSaveInstanceState(new Bundle());
+ mModel.rebindCallbacks();
+ } finally {
+ Trace.endSection();
}
-
- dispatchDeviceProfileChanged();
- reapplyUi();
- mDragLayer.recreateControllers();
-
- // Calling onSaveInstanceState ensures that static cache used by listWidgets is
- // initialized properly.
- onSaveInstanceState(new Bundle());
- mModel.rebindCallbacks();
}
public void onAssistantVisibilityChanged(float visibility) {
@@ -2446,9 +2452,9 @@
break;
}
case LauncherSettings.Favorites.ITEM_TYPE_APP_PAIR: {
- FolderInfo info = (FolderInfo) item;
- // TODO (jeremysim b/274189428): Create app pair icon
- view = null;
+ view = AppPairIcon.inflateIcon(R.layout.app_pair_icon, this,
+ (ViewGroup) workspace.getChildAt(workspace.getCurrentPage()),
+ (FolderInfo) item);
break;
}
case LauncherSettings.Favorites.ITEM_TYPE_APPWIDGET:
@@ -3390,4 +3396,12 @@
public View.OnLongClickListener getAllAppsItemLongClickListener() {
return ItemLongClickListener.INSTANCE_ALL_APPS;
}
+
+ /**
+ * Handles an app pair launch; overridden in
+ * {@link com.android.launcher3.uioverrides.QuickstepLauncher}
+ */
+ public void launchAppPair(WorkspaceItemInfo app1, WorkspaceItemInfo app2) {
+ // Overridden
+ }
}
diff --git a/src/com/android/launcher3/ShortcutAndWidgetContainer.java b/src/com/android/launcher3/ShortcutAndWidgetContainer.java
index f921d1d..07b71b3 100644
--- a/src/com/android/launcher3/ShortcutAndWidgetContainer.java
+++ b/src/com/android/launcher3/ShortcutAndWidgetContainer.java
@@ -28,6 +28,7 @@
import android.graphics.Point;
import android.graphics.PointF;
import android.graphics.Rect;
+import android.os.Trace;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
@@ -182,6 +183,7 @@
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
+ Trace.beginSection("ShortcutAndWidgetConteiner#onLayout");
int count = getChildCount();
for (int i = 0; i < count; i++) {
final View child = getChildAt(i);
@@ -189,6 +191,7 @@
layoutChild(child);
}
}
+ Trace.endSection();
}
/**
diff --git a/src/com/android/launcher3/apppairs/AppPairIcon.java b/src/com/android/launcher3/apppairs/AppPairIcon.java
new file mode 100644
index 0000000..1dc4ad2
--- /dev/null
+++ b/src/com/android/launcher3/apppairs/AppPairIcon.java
@@ -0,0 +1,102 @@
+/*
+ * 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.apppairs;
+
+import android.content.Context;
+import android.graphics.Rect;
+import android.util.AttributeSet;
+import android.view.LayoutInflater;
+import android.view.ViewGroup;
+import android.widget.FrameLayout;
+
+import androidx.annotation.Nullable;
+
+import com.android.launcher3.BubbleTextView;
+import com.android.launcher3.R;
+import com.android.launcher3.dragndrop.DraggableView;
+import com.android.launcher3.model.data.FolderInfo;
+import com.android.launcher3.model.data.WorkspaceItemInfo;
+import com.android.launcher3.views.ActivityContext;
+
+import java.util.Collections;
+import java.util.Comparator;
+
+/**
+ * A {@link android.widget.FrameLayout} used to represent an app pair icon on the workspace.
+ */
+public class AppPairIcon extends FrameLayout implements DraggableView {
+
+ private ActivityContext mActivity;
+ private BubbleTextView mAppPairName;
+ private FolderInfo mInfo;
+
+ public AppPairIcon(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ public AppPairIcon(Context context) {
+ super(context);
+ }
+
+ /**
+ * Builds an AppPairIcon to be added to the Launcher
+ */
+ public static AppPairIcon inflateIcon(int resId, ActivityContext activity,
+ @Nullable ViewGroup group, FolderInfo appPairInfo) {
+
+ LayoutInflater inflater = (group != null)
+ ? LayoutInflater.from(group.getContext())
+ : activity.getLayoutInflater();
+ AppPairIcon icon = (AppPairIcon) inflater.inflate(resId, group, false);
+
+ // Sort contents, so that left-hand app comes first
+ Collections.sort(appPairInfo.contents, Comparator.comparingInt(a -> a.rank));
+
+ icon.setClipToPadding(false);
+ icon.mAppPairName = icon.findViewById(R.id.app_pair_icon_name);
+
+ // TODO (jeremysim b/274189428): Replace this placeholder icon
+ WorkspaceItemInfo placeholder = new WorkspaceItemInfo();
+ placeholder.newIcon(icon.getContext());
+ icon.mAppPairName.applyFromWorkspaceItem(placeholder);
+
+ icon.mAppPairName.setText(appPairInfo.title);
+
+ icon.setTag(appPairInfo);
+ icon.setOnClickListener(activity.getItemOnClickListener());
+ icon.mInfo = appPairInfo;
+ icon.mActivity = activity;
+
+ icon.setAccessibilityDelegate(activity.getAccessibilityDelegate());
+
+ return icon;
+ }
+
+ @Override
+ public int getViewType() {
+ return DRAGGABLE_ICON;
+ }
+
+ @Override
+ public void getWorkspaceVisualDragBounds(Rect bounds) {
+ mAppPairName.getIconBounds(bounds);
+ }
+
+ public FolderInfo getInfo() {
+ return mInfo;
+ }
+}
diff --git a/src/com/android/launcher3/config/FeatureFlags.java b/src/com/android/launcher3/config/FeatureFlags.java
index 1f68e11..273d505 100644
--- a/src/com/android/launcher3/config/FeatureFlags.java
+++ b/src/com/android/launcher3/config/FeatureFlags.java
@@ -277,6 +277,10 @@
"ENABLE_BACK_SWIPE_HOME_ANIMATION", ENABLED,
"Enables home animation to icon when user swipes back.");
+ public static final BooleanFlag ENABLE_DYNAMIC_TASKBAR_THRESHOLDS = getDebugFlag(294252473,
+ "ENABLE_DYNAMIC_TASKBAR_THRESHOLDS", TEAMFOOD,
+ "Enables taskbar thresholds that scale based on screen size.");
+
// TODO(Block 21): Clean up flags
public static final BooleanFlag ENABLE_APP_ICON_FOR_INLINE_SHORTCUTS = getDebugFlag(270395087,
"ENABLE_APP_ICON_IN_INLINE_SHORTCUTS", DISABLED, "Show app icon for inline shortcut");
@@ -359,7 +363,7 @@
// TODO(Block 28): Clean up flags
public static final BooleanFlag ENABLE_SPLIT_FROM_FULLSCREEN_WITH_KEYBOARD_SHORTCUTS =
- getDebugFlag(270394122, "ENABLE_SPLIT_FROM_FULLSCREEN_SHORTCUT", DISABLED,
+ getDebugFlag(270394122, "ENABLE_SPLIT_FROM_FULLSCREEN_SHORTCUT", TEAMFOOD,
"Enable splitting from fullscreen app with keyboard shortcuts");
public static final BooleanFlag ENABLE_SPLIT_FROM_WORKSPACE_TO_WORKSPACE = getDebugFlag(
diff --git a/src/com/android/launcher3/graphics/LauncherPreviewRenderer.java b/src/com/android/launcher3/graphics/LauncherPreviewRenderer.java
index 7241b17..68106c4 100644
--- a/src/com/android/launcher3/graphics/LauncherPreviewRenderer.java
+++ b/src/com/android/launcher3/graphics/LauncherPreviewRenderer.java
@@ -51,6 +51,7 @@
import android.view.ViewGroup;
import android.view.WindowInsets;
import android.view.WindowManager;
+import android.widget.FrameLayout;
import android.widget.TextClock;
import androidx.annotation.NonNull;
@@ -70,6 +71,7 @@
import com.android.launcher3.Utilities;
import com.android.launcher3.Workspace;
import com.android.launcher3.WorkspaceLayoutManager;
+import com.android.launcher3.apppairs.AppPairIcon;
import com.android.launcher3.celllayout.CellLayoutLayoutParams;
import com.android.launcher3.celllayout.CellPosMapper;
import com.android.launcher3.config.FeatureFlags;
@@ -358,12 +360,13 @@
addInScreenFromBind(icon, info);
}
- private void inflateAndAddFolder(FolderInfo info) {
+ private void inflateAndAddCollectionIcon(FolderInfo info) {
CellLayout screen = info.container == Favorites.CONTAINER_DESKTOP
? mWorkspaceScreens.get(info.screenId)
: mHotseat;
- FolderIcon folderIcon = FolderIcon.inflateIcon(R.layout.folder_icon, this, screen,
- info);
+ FrameLayout folderIcon = info.itemType == Favorites.ITEM_TYPE_FOLDER
+ ? FolderIcon.inflateIcon(R.layout.folder_icon, this, screen, info)
+ : AppPairIcon.inflateIcon(R.layout.app_pair_icon, this, screen, info);
addInScreenFromBind(folderIcon, info);
}
@@ -467,7 +470,8 @@
inflateAndAddIcon((WorkspaceItemInfo) itemInfo);
break;
case Favorites.ITEM_TYPE_FOLDER:
- inflateAndAddFolder((FolderInfo) itemInfo);
+ case Favorites.ITEM_TYPE_APP_PAIR:
+ inflateAndAddCollectionIcon((FolderInfo) itemInfo);
break;
default:
break;
diff --git a/src/com/android/launcher3/logging/StatsLogManager.java b/src/com/android/launcher3/logging/StatsLogManager.java
index 2a7cd9a..8bb06c1 100644
--- a/src/com/android/launcher3/logging/StatsLogManager.java
+++ b/src/com/android/launcher3/logging/StatsLogManager.java
@@ -641,11 +641,17 @@
@UiEvent(doc = "User has swiped upwards from the gesture handle to show transient taskbar.")
LAUNCHER_TRANSIENT_TASKBAR_SHOW(1331),
+ @UiEvent(doc = "User has clicked an app pair and launched directly into split screen.")
+ LAUNCHER_APP_PAIR_LAUNCH(1374),
+
+ @UiEvent(doc = "User saved an app pair.")
+ LAUNCHER_APP_PAIR_SAVE(1456),
+
@UiEvent(doc = "App launched through pending intent")
- LAUNCHER_APP_LAUNCH_PENDING_INTENT(1394),
- ;
+ LAUNCHER_APP_LAUNCH_PENDING_INTENT(1394)
// ADD MORE
+ ;
private final int mId;
diff --git a/src/com/android/launcher3/model/BaseLauncherBinder.java b/src/com/android/launcher3/model/BaseLauncherBinder.java
index 556ac26..dbb29b8 100644
--- a/src/com/android/launcher3/model/BaseLauncherBinder.java
+++ b/src/com/android/launcher3/model/BaseLauncherBinder.java
@@ -21,6 +21,7 @@
import static com.android.launcher3.util.Executors.MODEL_EXECUTOR;
import android.os.Process;
+import android.os.Trace;
import android.util.Log;
import com.android.launcher3.InvariantDeviceProfile;
@@ -83,13 +84,18 @@
* Binds all loaded data to actual views on the main thread.
*/
public void bindWorkspace(boolean incrementBindId, boolean isBindSync) {
- if (FeatureFlags.ENABLE_WORKSPACE_LOADING_OPTIMIZATION.get()) {
- DisjointWorkspaceBinder workspaceBinder =
+ Trace.beginSection("BaseLauncherBinder#bindWorkspace");
+ try {
+ if (FeatureFlags.ENABLE_WORKSPACE_LOADING_OPTIMIZATION.get()) {
+ DisjointWorkspaceBinder workspaceBinder =
initWorkspaceBinder(incrementBindId, mBgDataModel.collectWorkspaceScreens());
- workspaceBinder.bindCurrentWorkspacePages(isBindSync);
- workspaceBinder.bindOtherWorkspacePages();
- } else {
- bindWorkspaceAllAtOnce(incrementBindId, isBindSync);
+ workspaceBinder.bindCurrentWorkspacePages(isBindSync);
+ workspaceBinder.bindOtherWorkspacePages();
+ } else {
+ bindWorkspaceAllAtOnce(incrementBindId, isBindSync);
+ }
+ } finally {
+ Trace.endSection();
}
}
diff --git a/src/com/android/launcher3/model/LoaderTask.java b/src/com/android/launcher3/model/LoaderTask.java
index 787ac38..933468c 100644
--- a/src/com/android/launcher3/model/LoaderTask.java
+++ b/src/com/android/launcher3/model/LoaderTask.java
@@ -690,9 +690,11 @@
break;
case Favorites.ITEM_TYPE_FOLDER:
+ case Favorites.ITEM_TYPE_APP_PAIR:
FolderInfo folderInfo = mBgDataModel.findOrMakeFolder(c.id);
c.applyCommonProperties(folderInfo);
+ folderInfo.itemType = c.itemType;
// Do not trim the folder label, as is was set by the user.
folderInfo.title = c.getString(c.mTitleIndex);
folderInfo.spanX = 1;
diff --git a/src/com/android/launcher3/model/ModelWriter.java b/src/com/android/launcher3/model/ModelWriter.java
index a6b4d59..2358a9f 100644
--- a/src/com/android/launcher3/model/ModelWriter.java
+++ b/src/com/android/launcher3/model/ModelWriter.java
@@ -489,6 +489,7 @@
case Favorites.ITEM_TYPE_APPLICATION:
case Favorites.ITEM_TYPE_DEEP_SHORTCUT:
case Favorites.ITEM_TYPE_FOLDER:
+ case Favorites.ITEM_TYPE_APP_PAIR:
if (!mBgDataModel.workspaceItems.contains(modelItem)) {
mBgDataModel.workspaceItems.add(modelItem);
}
diff --git a/src/com/android/launcher3/model/data/FolderInfo.java b/src/com/android/launcher3/model/data/FolderInfo.java
index e5a0eb1..9bf6d43 100644
--- a/src/com/android/launcher3/model/data/FolderInfo.java
+++ b/src/com/android/launcher3/model/data/FolderInfo.java
@@ -119,8 +119,8 @@
public static FolderInfo createAppPair(WorkspaceItemInfo app1, WorkspaceItemInfo app2) {
FolderInfo newAppPair = new FolderInfo();
newAppPair.itemType = LauncherSettings.Favorites.ITEM_TYPE_APP_PAIR;
- newAppPair.contents.add(app1);
- newAppPair.contents.add(app2);
+ newAppPair.add(app1, /* animate */ false);
+ newAppPair.add(app2, /* animate */ false);
return newAppPair;
}
diff --git a/src/com/android/launcher3/testing/TestInformationHandler.java b/src/com/android/launcher3/testing/TestInformationHandler.java
index b054f51..438a4a0 100644
--- a/src/com/android/launcher3/testing/TestInformationHandler.java
+++ b/src/com/android/launcher3/testing/TestInformationHandler.java
@@ -202,10 +202,11 @@
}
case TestProtocol.REQUEST_WORKSPACE_COLUMNS_ROWS: {
+ InvariantDeviceProfile idp = InvariantDeviceProfile.INSTANCE.get(mContext);
return getLauncherUIProperty(Bundle::putParcelable, launcher -> new Point(
- InvariantDeviceProfile.INSTANCE.get(mContext).numColumns,
- InvariantDeviceProfile.INSTANCE.get(mContext).numRows)
- );
+ idp.getDeviceProfile(mContext).getPanelCount() * idp.numColumns,
+ idp.numRows
+ ));
}
case TestProtocol.REQUEST_WORKSPACE_CURRENT_PAGE_INDEX: {
diff --git a/src/com/android/launcher3/touch/ItemClickHandler.java b/src/com/android/launcher3/touch/ItemClickHandler.java
index 790c226..8c12547 100644
--- a/src/com/android/launcher3/touch/ItemClickHandler.java
+++ b/src/com/android/launcher3/touch/ItemClickHandler.java
@@ -42,6 +42,7 @@
import com.android.launcher3.LauncherSettings;
import com.android.launcher3.R;
import com.android.launcher3.Utilities;
+import com.android.launcher3.apppairs.AppPairIcon;
import com.android.launcher3.folder.Folder;
import com.android.launcher3.folder.FolderIcon;
import com.android.launcher3.logging.InstanceId;
@@ -95,6 +96,8 @@
} else if (tag instanceof FolderInfo) {
if (v instanceof FolderIcon) {
onClickFolderIcon(v);
+ } else if (v instanceof AppPairIcon) {
+ onClickAppPairIcon(v);
}
} else if (tag instanceof AppInfo) {
startAppShortcutOrInfoActivity(v, (AppInfo) tag, launcher);
@@ -123,6 +126,17 @@
}
/**
+ * Event handler for an app pair icon click.
+ *
+ * @param v The view that was clicked. Must be an instance of {@link AppPairIcon}.
+ */
+ private static void onClickAppPairIcon(View v) {
+ Launcher launcher = Launcher.getLauncher(v.getContext());
+ FolderInfo folderInfo = ((AppPairIcon) v).getInfo();
+ launcher.launchAppPair(folderInfo.contents.get(0), folderInfo.contents.get(1));
+ }
+
+ /**
* Event handler for the app widget view which has not fully restored.
*/
private static void onClickPendingWidget(PendingAppWidgetHostView v, Launcher launcher) {
diff --git a/tests/assets/ReorderWidgets/multiple_cell_layouts_no_space_reorder b/tests/assets/ReorderWidgets/multiple_cell_layouts_no_space_reorder
new file mode 100644
index 0000000..c6d65f8
--- /dev/null
+++ b/tests/assets/ReorderWidgets/multiple_cell_layouts_no_space_reorder
@@ -0,0 +1,56 @@
+###################################################################################################
+# This file contains test case composed of the following tags:
+# * # (coments): Lines starting with this character would be ignored.
+# * arguments: is set of words separated by spaces that can later be parsed
+# * board: represent a workspace, the first line is the dimensions of the board width x height (wxh)
+# There are different characters on the board that represent different things:
+# * x: The x character represents spaces that would be ignored, for example it can be used in
+# the first row if we don't know how wide the smartspace is.
+# * i: Represents an icon on the workspace, none in particular just an icon
+# * [a-z]: Represents a widget and it can be any number or character
+# except any other already in use. The whole continuos are of the same character is the
+# area of the widget.
+# * [A-Z]: Represents a folder and number of icons in the folder is represented by the order of
+# letter in the alphabet, A=2, B=3, C=4 ... etc.
+# Test are parsed by CellLayoutTestCaseReader.java and boards are parsed by CellLayoutBoard.java
+###################################################################################################
+# 5x5 Test
+board: 5x5
+xxxxx|aeeee
+--mm-|acccc
+--mm-|acccc
+ggggg|acccc
+ggggg|adddd
+arguments: 7 1
+board: 10x5
+xxxxx|aeeee
+---mm|acccc
+---mm|acccc
+ggggg|acccc
+ggggg|adddd
+# 4x4 Test
+board: 4x4
+xxxx|aeee
+--mm|accc
+--mm|accc
+gggg|accc
+arguments: 5 1
+board: 8x4
+xxxx|aeee
+--mm|accc
+--mm|accc
+gggg|accc
+# 6x5 Test
+board: 6x5
+xxxxxx|aeeeee
+--mm--|accccc
+--mm--|accccc
+gggggg|accccc
+gggggg|addddd
+arguments: 8 1
+board: 12x5
+xxxxxx|aeeeee
+----mm|accccc
+----mm|accccc
+gggggg|accccc
+gggggg|addddd
diff --git a/tests/assets/ReorderWidgets/multiple_cell_layouts_reorder_other_side b/tests/assets/ReorderWidgets/multiple_cell_layouts_reorder_other_side
new file mode 100644
index 0000000..376638e
--- /dev/null
+++ b/tests/assets/ReorderWidgets/multiple_cell_layouts_reorder_other_side
@@ -0,0 +1,56 @@
+###################################################################################################
+# This file contains test case composed of the following tags:
+# * # (coments): Lines starting with this character would be ignored.
+# * arguments: is set of words separated by spaces that can later be parsed
+# * board: represent a workspace, the first line is the dimensions of the board width x height (wxh)
+# There are different characters on the board that represent different things:
+# * x: The x character represents spaces that would be ignored, for example it can be used in
+# the first row if we don't know how wide the smartspace is.
+# * i: Represents an icon on the workspace, none in particular just an icon
+# * [a-z]: Represents a widget and it can be any number or character
+# except any other already in use. The whole continuos are of the same character is the
+# area of the widget.
+# * [A-Z]: Represents a folder and number of icons in the folder is represented by the order of
+# letter in the alphabet, A=2, B=3, C=4 ... etc.
+# Test are parsed by CellLayoutTestCaseReader.java and boards are parsed by CellLayoutBoard.java
+###################################################################################################
+# 5x5 Test
+board: 5x5
+xxxxx|aaaaa
+--mm-|plllh
+--mm-|piiih
+ggggg|piiih
+ggggg|fffff
+arguments: 7 1
+board: 10x5
+xxxxx|aaaaa
+--lll|p-mmh
+---ii|pimmh
+ggggg|piiih
+ggggg|fffff
+# 4x4 Test
+board: 4x4
+xxxx|aaaa
+--mm|pllh
+--mm|piih
+gggg|ffff
+arguments: 5 1
+board: 8x4
+xxxx|aaaa
+--ll|pmmh
+--ii|pmmh
+gggg|ffff
+# 6x5 Test
+board: 6x5
+xxxxxx|aaaaaa
+--mmm-|pllllh
+--mmm-|piiiih
+--mmm-|piiiih
+gggggg|ffffff
+arguments: 8 1
+board: 12x5
+xxxxxx|aaaaaa
+--llll|p-mmmh
+---iii|pimmmh
+---iii|pimmmh
+gggggg|ffffff
\ No newline at end of file
diff --git a/tests/assets/ReorderWidgets/multiple_cell_layouts_simple_reorder b/tests/assets/ReorderWidgets/multiple_cell_layouts_simple_reorder
new file mode 100644
index 0000000..44301d2
--- /dev/null
+++ b/tests/assets/ReorderWidgets/multiple_cell_layouts_simple_reorder
@@ -0,0 +1,56 @@
+###################################################################################################
+# This file contains test case composed of the following tags:
+# * # (coments): Lines starting with this character would be ignored.
+# * arguments: is set of words separated by spaces that can later be parsed
+# * board: represent a workspace, the first line is the dimensions of the board width x height (wxh)
+# There are different characters on the board that represent different things:
+# * x: The x character represents spaces that would be ignored, for example it can be used in
+# the first row if we don't know how wide the smartspace is.
+# * i: Represents an icon on the workspace, none in particular just an icon
+# * [a-z]: Represents a widget and it can be any number or character
+# except any other already in use. The whole continuos are of the same character is the
+# area of the widget.
+# * [A-Z]: Represents a folder and number of icons in the folder is represented by the order of
+# letter in the alphabet, A=2, B=3, C=4 ... etc.
+# Test are parsed by CellLayoutTestCaseReader.java and boards are parsed by CellLayoutBoard.java
+###################################################################################################
+# 5x5 Test
+board: 5x5
+xxxxx|-----
+--mm-|-----
+--mm-|-----
+-----|-----
+-----|-----
+arguments: 8 3
+board: 10x5
+xxxxx|-----
+-----|-----
+-----|-----
+-----|---mm
+-----|---mm
+# 4x4 Test
+board: 4x4
+xxxx|----
+--mm|----
+--mm|----
+----|----
+arguments: 5 3
+board: 8x4
+xxxx|----
+----|----
+----|-mm-
+----|-mm-
+# 6x5 Test
+board: 6x5
+xxxxxx|------
+--m---|------
+------|------
+------|------
+------|------
+arguments: 10 4
+board: 12x5
+xxxxxx|------
+------|------
+------|------
+------|------
+------|----m-
\ No newline at end of file
diff --git a/tests/src/com/android/launcher3/celllayout/CellLayoutBoard.java b/tests/src/com/android/launcher3/celllayout/CellLayoutBoard.java
index 28899d9..ff667e6 100644
--- a/tests/src/com/android/launcher3/celllayout/CellLayoutBoard.java
+++ b/tests/src/com/android/launcher3/celllayout/CellLayoutBoard.java
@@ -141,9 +141,14 @@
return this.mType == CellType.IGNORE;
}
+ boolean contains(int x, int y) {
+ return mBounds.contains(x, y);
+ }
+
@Override
public String toString() {
- return "WidgetRect type = " + mType + " bounds = " + mBounds.toString();
+ return "WidgetRect type = " + mType + " x = " + getCellX() + " | y " + getCellY()
+ + " xs = " + getSpanX() + " ys = " + getSpanY();
}
}
@@ -227,6 +232,17 @@
}
}
+ public boolean pointInsideRect(int x, int y, WidgetRect rect) {
+ Boolean isXInRect = x >= rect.getCellX() && x < rect.getCellX() + rect.getSpanX();
+ Boolean isYInRect = y >= rect.getCellY() && y < rect.getCellY() + rect.getSpanY();
+ return isXInRect && isYInRect;
+ }
+
+ public WidgetRect getWidgetAt(int x, int y) {
+ return mWidgetsRects.stream()
+ .filter(widgetRect -> pointInsideRect(x, y, widgetRect)).findFirst().orElse(null);
+ }
+
public List<WidgetRect> getWidgets() {
return mWidgetsRects;
}
@@ -443,6 +459,17 @@
return null;
}
+ public static WidgetRect getWidgetIn(List<CellLayoutBoard> boards, int x, int y) {
+ for (CellLayoutBoard board : boards) {
+ WidgetRect main = board.getWidgetAt(x, y);
+ if (main != null) {
+ return main;
+ }
+ x -= board.mWidth;
+ }
+ return null;
+ }
+
public static CellLayoutBoard boardFromString(String boardStr) {
String[] lines = boardStr.split("\n");
CellLayoutBoard board = new CellLayoutBoard();
diff --git a/tests/src/com/android/launcher3/celllayout/CellLayoutTestUtils.java b/tests/src/com/android/launcher3/celllayout/CellLayoutTestUtils.java
index 0d2f252..b6c55af 100644
--- a/tests/src/com/android/launcher3/celllayout/CellLayoutTestUtils.java
+++ b/tests/src/com/android/launcher3/celllayout/CellLayoutTestUtils.java
@@ -42,8 +42,8 @@
params.getCellX(), params.getCellY(),
launcher.getWorkspace().getIdForScreen(cellLayout), CONTAINER_DESKTOP);
int screenId = pos.screenId;
- if (screenId >= boards.size() - 1) {
- boards.add(new CellLayoutBoard());
+ if (screenId > boards.size() - 1) {
+ boards.add(new CellLayoutBoard(cellLayout.getCountX(), cellLayout.getCountY()));
}
CellLayoutBoard board = boards.get(screenId);
// is icon
@@ -51,7 +51,7 @@
board.addIcon(pos.cellX, pos.cellY);
} else {
// is widget
- board.addWidget(params.getCellX(), params.getCellY(), params.cellHSpan,
+ board.addWidget(pos.cellX, pos.cellY, params.cellHSpan,
params.cellVSpan);
}
}
diff --git a/tests/src/com/android/launcher3/celllayout/MultipleCellLayoutsSimpleReorder.java b/tests/src/com/android/launcher3/celllayout/MultipleCellLayoutsSimpleReorder.java
deleted file mode 100644
index 706c1a7..0000000
--- a/tests/src/com/android/launcher3/celllayout/MultipleCellLayoutsSimpleReorder.java
+++ /dev/null
@@ -1,90 +0,0 @@
-/*
- * Copyright (C) 2022 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.testcases;
-
-import android.graphics.Point;
-
-import com.android.launcher3.celllayout.ReorderTestCase;
-
-import java.util.Map;
-
-/**
- * The grids represent the workspace to be build by TestWorkspaceBuilder, to see what each character
- * in the board mean refer to {@code CellType}
- */
-public class MultipleCellLayoutsSimpleReorder {
-
- /** 5x5 Test
- **/
- private static final String START_BOARD_STR_5x5 = ""
- + "xxxxx|-----\n"
- + "--mm-|-----\n"
- + "--mm-|-----\n"
- + "-----|-----\n"
- + "-----|-----";
- private static final Point MOVE_TO_5x5 = new Point(8, 3);
- private static final String END_BOARD_STR_5x5 = ""
- + "xxxxx|-----\n"
- + "-----|-----\n"
- + "-----|-----\n"
- + "-----|---mm\n"
- + "-----|---mm";
- private static final ReorderTestCase TEST_CASE_5x5 = new ReorderTestCase(START_BOARD_STR_5x5,
- MOVE_TO_5x5,
- END_BOARD_STR_5x5);
-
- /** 4x4 Test
- **/
- private static final String START_BOARD_STR_4x4 = ""
- + "xxxx|----\n"
- + "--mm|----\n"
- + "--mm|----\n"
- + "----|----";
- private static final Point MOVE_TO_4x4 = new Point(5, 3);
- private static final String END_BOARD_STR_4x4 = ""
- + "xxxx|----\n"
- + "----|----\n"
- + "----|-mm-\n"
- + "----|-mm-";
- private static final ReorderTestCase TEST_CASE_4x4 = new ReorderTestCase(START_BOARD_STR_4x4,
- MOVE_TO_4x4,
- END_BOARD_STR_4x4);
-
-
- /** 6x5 Test
- **/
- private static final String START_BOARD_STR_6x5 = ""
- + "xxxxxx|------\n"
- + "--m---|------\n"
- + "------|------\n"
- + "------|------\n"
- + "------|------";
- private static final Point MOVE_TO_6x5 = new Point(10, 4);
- private static final String END_BOARD_STR_6x5 = ""
- + "xxxxxx|------\n"
- + "------|------\n"
- + "------|------\n"
- + "------|------\n"
- + "------|----m-";
- private static final ReorderTestCase TEST_CASE_6x5 = new ReorderTestCase(START_BOARD_STR_6x5,
- MOVE_TO_6x5,
- END_BOARD_STR_6x5);
-
- public static final Map<Point, ReorderTestCase> TEST_BY_GRID_SIZE =
- Map.of(new Point(5, 5), TEST_CASE_5x5,
- new Point(4, 4), TEST_CASE_4x4,
- new Point(6, 5), TEST_CASE_6x5);
-}
diff --git a/tests/src/com/android/launcher3/celllayout/ReorderWidgets.java b/tests/src/com/android/launcher3/celllayout/ReorderWidgets.java
index 7ec78bb..00d7ce6 100644
--- a/tests/src/com/android/launcher3/celllayout/ReorderWidgets.java
+++ b/tests/src/com/android/launcher3/celllayout/ReorderWidgets.java
@@ -18,20 +18,24 @@
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
+import android.content.ContentResolver;
+import android.content.ContentValues;
import android.graphics.Point;
+import android.net.Uri;
import android.util.Log;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.SmallTest;
import com.android.launcher3.InvariantDeviceProfile;
-import com.android.launcher3.celllayout.testcases.MultipleCellLayoutsSimpleReorder;
+import com.android.launcher3.MultipageCellLayout;
import com.android.launcher3.tapl.Widget;
import com.android.launcher3.tapl.WidgetResizeFrame;
import com.android.launcher3.ui.AbstractLauncherUiTest;
import com.android.launcher3.ui.TaplTestsLauncher3;
import com.android.launcher3.util.rule.ShellCommandRule;
+import org.junit.Assert;
import org.junit.Assume;
import org.junit.Before;
import org.junit.Rule;
@@ -44,7 +48,6 @@
import java.util.Iterator;
import java.util.List;
import java.util.Map;
-import java.util.concurrent.ExecutionException;
@SmallTest
@RunWith(AndroidJUnit4.class)
@@ -55,6 +58,8 @@
private static final String TAG = ReorderWidgets.class.getSimpleName();
+ private static final List<String> FOLDABLE_GRIDS = List.of("normal", "practical", "reasonable");
+
TestWorkspaceBuilder mWorkspaceBuilder;
@Before
@@ -101,8 +106,48 @@
return getFromLauncher(CellLayoutTestUtils::workspaceToBoards);
}
- private void runTestCase(ReorderTestCase testCase)
- throws ExecutionException, InterruptedException {
+ private CellLayoutBoard.WidgetRect getWidgetClosestTo(Point point) {
+ ArrayList<CellLayoutBoard> workspaceBoards = workspaceToBoards();
+ int maxDistance = 9999;
+ CellLayoutBoard.WidgetRect bestRect = null;
+ for (int i = 0; i < workspaceBoards.get(0).getWidgets().size(); i++) {
+ CellLayoutBoard.WidgetRect widget = workspaceBoards.get(0).getWidgets().get(i);
+ if (widget.getCellX() == 0 && widget.getCellY() == 0) {
+ continue;
+ }
+ int distance = Math.abs(point.x - widget.getCellX())
+ + Math.abs(point.y - widget.getCellY());
+ if (distance == 0) {
+ break;
+ }
+ if (distance < maxDistance) {
+ maxDistance = distance;
+ bestRect = widget;
+ }
+ }
+ return bestRect;
+ }
+
+ /**
+ * This function might be odd, its function is to select a widget and leave it in its place.
+ * The idea is to make the test broader and also test after a widgets resized because the
+ * underlying code does different things in that case
+ */
+ private void triggerWidgetResize(ReorderTestCase testCase) {
+ CellLayoutBoard.WidgetRect widgetRect = getWidgetClosestTo(testCase.moveMainTo);
+ if (widgetRect == null) {
+ // Some test doesn't have a widget in the final position, in those cases we will ignore
+ // them
+ return;
+ }
+ Widget widget = mLauncher.getWorkspace().getWidgetAtCell(widgetRect.getCellX(),
+ widgetRect.getCellY());
+ WidgetResizeFrame resizeFrame = widget.dragWidgetToWorkspace(widgetRect.getCellX(),
+ widgetRect.getCellY(), widgetRect.getSpanX(), widgetRect.getSpanY());
+ resizeFrame.dismiss();
+ }
+
+ private void runTestCase(ReorderTestCase testCase) {
CellLayoutBoard.WidgetRect mainWidgetCellPos = CellLayoutBoard.getMainFromList(
testCase.mStart);
@@ -115,6 +160,9 @@
// waitForLauncherCondition to wait for that condition, otherwise the condition would
// always be true and it wouldn't wait for the changes to be applied.
waitForLauncherCondition("Workspace didn't finish loading", l -> !l.isWorkspaceLoading());
+
+ triggerWidgetResize(testCase);
+
Widget widget = mLauncher.getWorkspace().getWidgetAtCell(mainWidgetCellPos.getCellX(),
mainWidgetCellPos.getCellY());
assertNotNull(widget);
@@ -136,41 +184,91 @@
*
* @param testCaseMap map containing all the tests per grid size (Point)
*/
- private void runTestCaseMap(Map<Point, ReorderTestCase> testCaseMap, String testName)
- throws ExecutionException, InterruptedException {
+ private boolean runTestCaseMap(Map<Point, ReorderTestCase> testCaseMap, String testName) {
Point iconGridDimensions = mLauncher.getWorkspace().getIconGridDimensions();
Log.d(TAG, "Running test " + testName + " for grid " + iconGridDimensions);
- Assume.assumeTrue(
- "The test " + testName + " doesn't support " + iconGridDimensions + " grid layout",
- testCaseMap.containsKey(iconGridDimensions));
+ if (!testCaseMap.containsKey(iconGridDimensions)) {
+ Log.d(TAG, "The test " + testName + " doesn't support " + iconGridDimensions
+ + " grid layout");
+ return false;
+ }
runTestCase(testCaseMap.get(iconGridDimensions));
+
+ return true;
+ }
+
+ private void runTestCaseMapForAllGrids(Map<Point, ReorderTestCase> testCaseMap,
+ String testName) {
+ boolean runAtLeastOnce = false;
+ for (String grid : FOLDABLE_GRIDS) {
+ applyGridOption(grid);
+ mLauncher.waitForLauncherInitialized();
+ runAtLeastOnce |= runTestCaseMap(testCaseMap, testName);
+ }
+ Assume.assumeTrue("None of the grids are supported", runAtLeastOnce);
+ }
+
+ private void applyGridOption(Object argValue) {
+ String testProviderAuthority = mTargetContext.getPackageName() + ".grid_control";
+ Uri gridUri = new Uri.Builder()
+ .scheme(ContentResolver.SCHEME_CONTENT)
+ .authority(testProviderAuthority)
+ .appendPath("default_grid")
+ .build();
+ ContentValues values = new ContentValues();
+ values.putObject("name", argValue);
+ Assert.assertEquals(1,
+ mTargetContext.getContentResolver().update(gridUri, values, null, null));
}
@Test
public void simpleReorder() throws Exception {
- runTestCaseMap(getTestMap("ReorderWidgets/simple_reorder_case"), "push_reorder_case");
+ runTestCaseMap(getTestMap("ReorderWidgets/simple_reorder_case"),
+ "push_reorder_case");
}
@Test
public void pushTest() throws Exception {
- runTestCaseMap(getTestMap("ReorderWidgets/push_reorder_case"), "push_reorder_case");
+ runTestCaseMap(getTestMap("ReorderWidgets/push_reorder_case"),
+ "push_reorder_case");
}
@Test
public void fullReorder() throws Exception {
- runTestCaseMap(getTestMap("ReorderWidgets/full_reorder_case"), "full_reorder_case");
+ runTestCaseMap(getTestMap("ReorderWidgets/full_reorder_case"),
+ "full_reorder_case");
}
@Test
public void moveOutReorder() throws Exception {
- runTestCaseMap(getTestMap("ReorderWidgets/move_out_reorder_case"), "move_out_reorder_case");
+ runTestCaseMap(getTestMap("ReorderWidgets/move_out_reorder_case"),
+ "move_out_reorder_case");
}
@Test
- public void multipleCellLayoutsSimpleReorder() throws ExecutionException, InterruptedException {
- Assume.assumeTrue("Test doesn't support foldables", !mLauncher.isTwoPanels());
- runTestCaseMap(MultipleCellLayoutsSimpleReorder.TEST_BY_GRID_SIZE,
- MultipleCellLayoutsSimpleReorder.class.getSimpleName());
+ public void multipleCellLayoutsSimpleReorder() throws Exception {
+ Assume.assumeTrue("Test doesn't support foldables", getFromLauncher(
+ l -> l.getWorkspace().getScreenWithId(0) instanceof MultipageCellLayout));
+ runTestCaseMapForAllGrids(getTestMap("ReorderWidgets/multiple_cell_layouts_simple_reorder"),
+ "multiple_cell_layouts_simple_reorder");
+ }
+
+ @Test
+ public void multipleCellLayoutsNoSpaceReorder() throws Exception {
+ Assume.assumeTrue("Test doesn't support foldables", getFromLauncher(
+ l -> l.getWorkspace().getScreenWithId(0) instanceof MultipageCellLayout));
+ runTestCaseMapForAllGrids(
+ getTestMap("ReorderWidgets/multiple_cell_layouts_no_space_reorder"),
+ "multiple_cell_layouts_no_space_reorder");
+ }
+
+ @Test
+ public void multipleCellLayoutsReorderToOtherSide() throws Exception {
+ Assume.assumeTrue("Test doesn't support foldables", getFromLauncher(
+ l -> l.getWorkspace().getScreenWithId(0) instanceof MultipageCellLayout));
+ runTestCaseMapForAllGrids(
+ getTestMap("ReorderWidgets/multiple_cell_layouts_reorder_other_side"),
+ "multiple_cell_layouts_reorder_other_side");
}
private void addTestCase(Iterator<CellLayoutTestCaseReader.TestSection> sections,
@@ -183,7 +281,7 @@
((CellLayoutTestCaseReader.Board) sections.next());
Point moveTo = new Point(Integer.parseInt(point.arguments[0]),
Integer.parseInt(point.arguments[1]));
- testCaseMap.put(startBoard.gridSize,
+ testCaseMap.put(endBoard.gridSize,
new ReorderTestCase(startBoard.board, moveTo, endBoard.board));
}