Merge "Add CUJ annotations for going to Recents from Home" into main
diff --git a/aconfig/Android.bp b/aconfig/Android.bp
index dc30a35..5413601 100644
--- a/aconfig/Android.bp
+++ b/aconfig/Android.bp
@@ -20,6 +20,7 @@
aconfig_declarations {
name: "com_android_launcher3_flags",
package: "com.android.launcher3",
+ container: "system",
srcs: ["**/*.aconfig"],
}
diff --git a/aconfig/launcher.aconfig b/aconfig/launcher.aconfig
index 82ae4cb..6d899d9 100644
--- a/aconfig/launcher.aconfig
+++ b/aconfig/launcher.aconfig
@@ -1,4 +1,5 @@
package: "com.android.launcher3"
+container: "system"
flag {
name: "enable_expanding_pause_work_button"
diff --git a/aconfig/launcher_search.aconfig b/aconfig/launcher_search.aconfig
index 4e16e7f..2f2690e 100644
--- a/aconfig/launcher_search.aconfig
+++ b/aconfig/launcher_search.aconfig
@@ -1,4 +1,5 @@
package: "com.android.launcher3"
+container: "system"
flag {
name: "enable_private_space"
diff --git a/quickstep/res/layout/icon_app_chip_view.xml b/quickstep/res/layout/icon_app_chip_view.xml
index b7acb70..fb9bf99 100644
--- a/quickstep/res/layout/icon_app_chip_view.xml
+++ b/quickstep/res/layout/icon_app_chip_view.xml
@@ -17,45 +17,41 @@
<com.android.quickstep.views.IconAppChipView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/icon"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
+ android:layout_width="@dimen/task_thumbnail_icon_menu_expanded_width"
+ android:layout_height="@dimen/task_thumbnail_icon_menu_expanded_height"
+ android:clipToOutline="true"
android:focusable="false"
android:importantForAccessibility="no"
android:autoMirrored="true"
- android:background="@drawable/icon_menu_elevation_background"
- android:elevation="@dimen/task_thumbnail_icon_menu_elevation" >
+ android:elevation="@dimen/task_thumbnail_icon_menu_elevation"
+ android:background="?androidprv:attr/materialColorSurfaceBright">
- <ImageView
- android:id="@+id/icon_view_background_corners_start"
- android:layout_width="@dimen/task_thumbnail_icon_menu_corner_width"
- android:layout_height="@dimen/task_thumbnail_icon_menu_min_height"
- android:src="@drawable/icon_menu_background_corners"
- android:importantForAccessibility="no" />
- <ImageView
- android:id="@+id/icon_view_background"
- android:layout_width="@dimen/task_thumbnail_icon_menu_background_min_width"
- android:layout_height="@dimen/task_thumbnail_icon_menu_min_height"
- android:src="@drawable/icon_menu_background"
- android:importantForAccessibility="no" />
- <ImageView
- android:id="@+id/icon_view_background_corners_end"
- android:layout_width="@dimen/task_thumbnail_icon_menu_corner_width"
- android:layout_height="@dimen/task_thumbnail_icon_menu_min_height"
- android:src="@drawable/icon_menu_background_corners"
- android:importantForAccessibility="no" />
+ <!-- ignoring warning because the user of the anchor is a Rect where RTL is not needed -->
+ <!-- This anchor's bounds is in the expected location after rotations and translations are
+ applied to the parent. The same is not true of the parent so an anchor is used. -->
+ <!-- marginTop is applied in java to get the gap between chip and menu -->
+ <View
+ android:id="@+id/icon_view_menu_anchor"
+ android:layout_width="0dp"
+ android:layout_height="0dp"
+ android:layout_gravity="left|top"
+ android:focusable="false"
+ android:importantForAccessibility="no"
+ tools:ignore="RtlHardcoded" />
<com.android.quickstep.views.IconView
android:id="@+id/icon_view"
- android:layout_width="@dimen/task_thumbnail_icon_size"
- android:layout_height="@dimen/task_thumbnail_icon_size"
+ android:layout_width="@dimen/task_thumbnail_icon_menu_app_icon_collapsed_size"
+ android:layout_height="@dimen/task_thumbnail_icon_menu_app_icon_collapsed_size"
android:focusable="false"
android:importantForAccessibility="no" />
<TextView
android:id="@+id/icon_text_collapsed"
- android:layout_width="@dimen/task_thumbnail_icon_menu_text_width"
- android:layout_height="@dimen/task_thumbnail_icon_menu_drawable_size"
+ android:layout_width="@dimen/task_thumbnail_icon_menu_text_collapsed_max_width"
+ android:layout_height="@dimen/task_thumbnail_icon_menu_app_icon_collapsed_size"
android:gravity="start|center_vertical"
android:maxLines="1"
android:ellipsize="end"
@@ -65,8 +61,8 @@
<TextView
android:id="@+id/icon_text_expanded"
- android:layout_width="@dimen/task_thumbnail_icon_menu_text_max_width"
- android:layout_height="@dimen/task_thumbnail_icon_menu_drawable_size"
+ android:layout_width="@dimen/task_thumbnail_icon_menu_text_expanded_max_width"
+ android:layout_height="@dimen/task_thumbnail_icon_menu_app_icon_collapsed_size"
android:gravity="start|center_vertical"
android:maxLines="1"
android:ellipsize="end"
diff --git a/quickstep/res/values/dimens.xml b/quickstep/res/values/dimens.xml
index a985cd8..853ac74 100644
--- a/quickstep/res/values/dimens.xml
+++ b/quickstep/res/values/dimens.xml
@@ -32,8 +32,11 @@
<dimen name="overview_minimum_next_prev_size">50dp</dimen>
<!-- Overview Task Views -->
- <!-- The primary task thumbnail uses up to this much of the total screen height/width -->
+ <!-- The thumbnail uses up to this much of the total screen height/width in Overview -->
<item name="overview_max_scale" format="float" type="dimen">0.7</item>
+ <!-- The thumbnail should not go smaller than this much of the total screen height/width in
+ tablet app to Overview carousel -->
+ <item name="overview_carousel_min_scale" format="float" type="dimen">0.46</item>
<!-- A touch target for icons, sometimes slightly larger than the icons themselves -->
<dimen name="task_thumbnail_icon_size">48dp</dimen>
<!-- The icon size for the focused task, placed in center of touch target -->
@@ -44,48 +47,38 @@
<dimen name="overview_task_margin">16dp</dimen>
<!-- The horizontal space between tasks -->
<dimen name="overview_page_spacing">16dp</dimen>
- <!-- The min width of the thumbnail icon menu for non-split tasks -->
- <dimen name="task_thumbnail_icon_menu_min_width">156dp</dimen>
- <!-- The max width of the thumbnail icon menu -->
- <dimen name="task_thumbnail_icon_menu_max_width">216dp</dimen>
- <!-- The width of the thumbnail icon menu background -->
- <dimen name="task_thumbnail_icon_menu_background_min_width">120dp</dimen>
- <!-- The width of the icon menu text -->
- <dimen name="task_thumbnail_icon_menu_text_width">86dp</dimen>
- <!-- The max width of the icon menu text -->
- <dimen name="task_thumbnail_icon_menu_text_max_width">118dp</dimen>
+ <!-- The collapsed max width of the icon menu text -->
+ <dimen name="task_thumbnail_icon_menu_text_collapsed_max_width">86dp</dimen>
+ <!-- The expanded max width of the icon menu text -->
+ <dimen name="task_thumbnail_icon_menu_text_expanded_max_width">118dp</dimen>
<!-- The size of the icon menu text -->
<dimen name="task_thumbnail_icon_menu_text_size">14sp</dimen>
- <!-- The max width of the thumbnail icon menu background -->
- <dimen name="task_thumbnail_icon_menu_background_max_width">164dp</dimen>
- <!-- The height of the thumbnail icon menu -->
- <dimen name="task_thumbnail_icon_menu_min_height">36dp</dimen>
- <!-- The corner radius of the thumbnail icon menu -->
- <dimen name="task_thumbnail_icon_menu_corner_radius">28dp</dimen>
- <!-- The width of the thumbnail icon menu backgorund's corners when collapsed -->
- <dimen name="task_thumbnail_icon_menu_corner_width">36dp</dimen>
- <!-- The max height of the thumbnail icon menu -->
- <dimen name="task_thumbnail_icon_menu_max_height">52dp</dimen>
- <!-- The size of the icon menu arrow -->
- <dimen name="task_thumbnail_icon_menu_arrow_size">24dp</dimen>
- <!-- The size of the icon menu arrow drawable -->
- <dimen name="task_thumbnail_icon_menu_arrow_drawable_size">16dp</dimen>
- <!-- The margin at the start of the task icon menu -->
- <dimen name="task_thumbnail_icon_menu_start_margin">12dp</dimen>
- <!-- The margin at the top of the task icon menu -->
- <dimen name="task_thumbnail_icon_menu_top_margin">12dp</dimen>
+ <!-- The width of the thumbnail icon menu when collapsed (for non-split tasks) -->
+ <dimen name="task_thumbnail_icon_menu_collapsed_width">156dp</dimen>
+ <!-- The width of the thumbnail icon menu when expanded -->
+ <dimen name="task_thumbnail_icon_menu_expanded_width">216dp</dimen>
+ <!-- The height of the thumbnail icon menu when collapsed -->
+ <dimen name="task_thumbnail_icon_menu_collapsed_height">36dp</dimen>
+ <!-- The height of the thumbnail icon menu when expanded -->
+ <dimen name="task_thumbnail_icon_menu_expanded_height">52dp</dimen>
+ <!-- The margin at the top/start of the task icon menu when expanded -->
+ <dimen name="task_thumbnail_icon_menu_expanded_top_start_margin">4dp</dimen>
+ <!-- The margin at the start of the background when collapsed -->
+ <dimen name="task_thumbnail_icon_menu_background_margin_top_start">8dp</dimen>
+ <!-- The margin between the app name + app icon and app name + arrow icon when collapsed -->
+ <dimen name="task_thumbnail_icon_menu_app_name_margin_horizontal_collapsed">8dp</dimen>
<!-- The gap at the top of the task icon menu when expanded -->
- <dimen name="task_thumbnail_icon_menu_expanded_gap">6dp</dimen>
+ <dimen name="task_thumbnail_icon_menu_expanded_gap">2dp</dimen>
<!-- The margin at the start of the task icon view in the icon menu -->
<dimen name="task_thumbnail_icon_view_start_margin">6dp</dimen>
<!-- The space around the task icon arrow within the icon menu -->
<dimen name="task_thumbnail_icon_menu_arrow_margin">8dp</dimen>
- <!-- The max space around the task icon within the icon menu -->
- <dimen name="task_thumbnail_icon_menu_touch_max_margin">8dp</dimen>
- <!-- The icon size for the icon menu -->
- <dimen name="task_thumbnail_icon_menu_drawable_size">24dp</dimen>
- <!-- The icon size for the icon menu -->
- <dimen name="task_thumbnail_icon_menu_drawable_max_size">32dp</dimen>
+ <!-- The size for the icon menu arrow -->
+ <dimen name="task_thumbnail_icon_menu_arrow_size">24dp</dimen>
+ <!-- The collapsed size for the icon menu icon -->
+ <dimen name="task_thumbnail_icon_menu_app_icon_collapsed_size">24dp</dimen>
+ <!-- The expanded icon size for the icon menu -->
+ <dimen name="task_thumbnail_icon_menu_app_icon_expanded_size">32dp</dimen>
<!-- The size of the icon menu's icon touch target -->
<dimen name="task_thumbnail_icon_menu_drawable_touch_size">44dp</dimen>
<dimen name="task_thumbnail_icon_menu_elevation">4dp</dimen>
diff --git a/quickstep/src/com/android/launcher3/WidgetPickerActivity.java b/quickstep/src/com/android/launcher3/WidgetPickerActivity.java
index 436fe3b..68e7824 100644
--- a/quickstep/src/com/android/launcher3/WidgetPickerActivity.java
+++ b/quickstep/src/com/android/launcher3/WidgetPickerActivity.java
@@ -23,6 +23,7 @@
import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
import static com.android.launcher3.util.Executors.MODEL_EXECUTOR;
+import android.appwidget.AppWidgetManager;
import android.appwidget.AppWidgetProviderInfo;
import android.content.ClipData;
import android.content.ClipDescription;
@@ -50,7 +51,6 @@
/** An Activity that can host Launcher's widget picker. */
public class WidgetPickerActivity extends BaseActivity {
private static final String TAG = "WidgetPickerActivity";
- private static final boolean DEBUG = false;
/**
* Name of the extra that indicates that a widget being dragged.
@@ -65,13 +65,13 @@
private static final String EXTRA_DESIRED_WIDGET_WIDTH = "desired_widget_width";
private static final String EXTRA_DESIRED_WIDGET_HEIGHT = "desired_widget_height";
-
private SimpleDragLayer<WidgetPickerActivity> mDragLayer;
private WidgetsModel mModel;
private final PopupDataProvider mPopupDataProvider = new PopupDataProvider(i -> {});
private int mDesiredWidgetWidth;
private int mDesiredWidgetHeight;
+ private int mWidgetCategoryFilter;
@Override
protected void onCreate(Bundle savedInstanceState) {
@@ -104,6 +104,10 @@
mDesiredWidgetHeight =
getIntent().getIntExtra(EXTRA_DESIRED_WIDGET_HEIGHT, 0);
+ // Defaults to '0' to indicate that there isn't a category filter.
+ mWidgetCategoryFilter =
+ getIntent().getIntExtra(AppWidgetManager.EXTRA_CATEGORY_FILTER, 0);
+
refreshAndBindWidgets();
}
@@ -199,6 +203,14 @@
return rejectWidget(widget, "shortcut");
}
+ if (mWidgetCategoryFilter > 0 && (info.widgetCategory & mWidgetCategoryFilter) == 0) {
+ return rejectWidget(
+ widget,
+ "doesn't match category filter [filter=%d, widget=%d]",
+ mWidgetCategoryFilter,
+ info.widgetCategory);
+ }
+
if (mDesiredWidgetWidth == 0 && mDesiredWidgetHeight == 0) {
// Accept the widget if the desired dimensions are unspecified.
return acceptWidget(widget);
@@ -210,22 +222,18 @@
if (info.maxResizeWidth > 0 && info.maxResizeWidth < mDesiredWidgetWidth) {
return rejectWidget(
widget,
- String.format(
- Locale.ENGLISH,
- "maxResizeWidth[%d] < mDesiredWidgetWidth[%d]",
- info.maxResizeWidth,
- mDesiredWidgetWidth));
+ "maxResizeWidth[%d] < mDesiredWidgetWidth[%d]",
+ info.maxResizeWidth,
+ mDesiredWidgetWidth);
}
final int minWidth = info.minResizeWidth > 0 ? info.minResizeWidth : info.minWidth;
if (minWidth > mDesiredWidgetWidth) {
return rejectWidget(
widget,
- String.format(
- Locale.ENGLISH,
- "minWidth[%d] > mDesiredWidgetWidth[%d]",
- minWidth,
- mDesiredWidgetWidth));
+ "minWidth[%d] > mDesiredWidgetWidth[%d]",
+ minWidth,
+ mDesiredWidgetWidth);
}
}
@@ -235,22 +243,18 @@
if (info.maxResizeHeight > 0 && info.maxResizeHeight < mDesiredWidgetHeight) {
return rejectWidget(
widget,
- String.format(
- Locale.ENGLISH,
- "maxResizeHeight[%d] < mDesiredWidgetHeight[%d]",
- info.maxResizeHeight,
- mDesiredWidgetHeight));
+ "maxResizeHeight[%d] < mDesiredWidgetHeight[%d]",
+ info.maxResizeHeight,
+ mDesiredWidgetHeight);
}
final int minHeight = info.minResizeHeight > 0 ? info.minResizeHeight : info.minHeight;
if (minHeight > mDesiredWidgetHeight) {
return rejectWidget(
widget,
- String.format(
- Locale.ENGLISH,
- "minHeight[%d] > mDesiredWidgetHeight[%d]",
- minHeight,
- mDesiredWidgetHeight));
+ "minHeight[%d] > mDesiredWidgetHeight[%d]",
+ minHeight,
+ mDesiredWidgetHeight);
}
}
@@ -264,8 +268,11 @@
}
private static WidgetAcceptabilityVerdict rejectWidget(
- WidgetItem widget, String rejectionReason) {
- return new WidgetAcceptabilityVerdict(false, widget.label, rejectionReason);
+ WidgetItem widget, String rejectionReason, Object... args) {
+ return new WidgetAcceptabilityVerdict(
+ false,
+ widget.label,
+ String.format(Locale.ENGLISH, rejectionReason, args));
}
private static WidgetAcceptabilityVerdict acceptWidget(WidgetItem widget) {
@@ -276,7 +283,7 @@
boolean isAcceptable, String widgetLabel, String reason) {
void maybeLogVerdict() {
// Only log a verdict if a reason is specified.
- if (DEBUG && !reason.isEmpty()) {
+ if (Log.isLoggable(TAG, Log.DEBUG) && !reason.isEmpty()) {
Log.i(TAG, String.format(
Locale.ENGLISH,
"%s: %s because %s",
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java
index 3f5793f..55deca8 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java
@@ -1063,7 +1063,7 @@
Toast.LENGTH_SHORT).show();
} else {
// Else launch the selected app pair
- launchFromTaskbarPreservingSplitIfVisible(recents, view, fi.contents);
+ launchFromTaskbar(recents, view, fi.contents);
mControllers.uiController.onTaskbarIconLaunched(fi);
mControllers.taskbarStashController.updateAndAnimateTransientTaskbar(true);
}
@@ -1097,8 +1097,7 @@
getSystemService(LauncherApps.class)
.startShortcut(packageName, id, null, null, info.user);
} else {
- launchFromTaskbarPreservingSplitIfVisible(
- recents, view, Collections.singletonList(info));
+ launchFromTaskbar(recents, view, Collections.singletonList(info));
}
} catch (NullPointerException
@@ -1136,8 +1135,7 @@
// If we are selecting a second app for split, launch the split tasks
taskbarUIController.triggerSecondAppForSplit(info, info.intent, view);
} else {
- launchFromTaskbarPreservingSplitIfVisible(
- recents, view, Collections.singletonList(info));
+ launchFromTaskbar(recents, view, Collections.singletonList(info));
}
mControllers.uiController.onTaskbarIconLaunched(info);
mControllers.taskbarStashController.updateAndAnimateTransientTaskbar(true);
@@ -1153,12 +1151,48 @@
}
/**
+ * Runs when the user taps a Taskbar icon in TaskbarActivityContext (Overview or inside an app),
+ * and calls the appropriate method to animate and launch.
+ */
+ private void launchFromTaskbar(@Nullable RecentsView recents, @Nullable View launchingIconView,
+ List<? extends ItemInfo> itemInfos) {
+ if (isInApp()) {
+ launchFromInAppTaskbar(recents, launchingIconView, itemInfos);
+ } else {
+ launchFromOverviewTaskbar(recents, launchingIconView, itemInfos);
+ }
+ }
+
+ /**
+ * Runs when the user taps a Taskbar icon while inside an app.
+ */
+ private void launchFromInAppTaskbar(@Nullable RecentsView recents,
+ @Nullable View launchingIconView, List<? extends ItemInfo> itemInfos) {
+ if (recents == null) {
+ return;
+ }
+
+ boolean tappedAppPair = itemInfos.size() == 2;
+
+ if (tappedAppPair) {
+ // If the icon is an app pair, the logic gets a bit complicated because we play
+ // different animations depending on which app (or app pair) is currently running on
+ // screen, so delegate logic to appPairsController.
+ recents.getSplitSelectController().getAppPairsController()
+ .handleAppPairLaunchInApp((AppPairIcon) launchingIconView, itemInfos);
+ } else {
+ // Tapped a single app, nothing complicated here.
+ startItemInfoActivity(itemInfos.get(0));
+ }
+ }
+
+ /**
* Run when the user taps a Taskbar icon while in Overview. If the tapped app is currently
* visible to the user in Overview, or is part of a visible split pair, we expand the TaskView
* as if the user tapped on it (preserving the split pair). Otherwise, launch it normally
* (potentially breaking a split pair).
*/
- private void launchFromTaskbarPreservingSplitIfVisible(@Nullable RecentsView recents,
+ private void launchFromOverviewTaskbar(@Nullable RecentsView recents,
@Nullable View launchingIconView, List<? extends ItemInfo> itemInfos) {
if (recents == null) {
return;
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarLauncherStateController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarLauncherStateController.java
index 9f6994a..8d83716 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarLauncherStateController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarLauncherStateController.java
@@ -222,8 +222,7 @@
public void onStateTransitionCompletedAfterSwipeToHome(LauncherState finalState) {
// TODO(b/279514548) Cleans up bad state that can occur when user interacts with
// taskbar on top of transparent activity.
- if (!FeatureFlags.enableHomeTransitionListener()
- && (finalState == LauncherState.NORMAL)
+ if ((finalState == LauncherState.NORMAL)
&& mLauncher.hasBeenResumed()) {
updateStateForFlag(FLAG_VISIBLE, true);
applyState();
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarStashController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarStashController.java
index db7d0eb..7a69c55 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarStashController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarStashController.java
@@ -21,6 +21,7 @@
import static com.android.app.animation.Interpolators.FINAL_FRAME;
import static com.android.app.animation.Interpolators.INSTANT;
import static com.android.app.animation.Interpolators.LINEAR;
+import static com.android.internal.jank.InteractionJankMonitor.Configuration;
import static com.android.launcher3.config.FeatureFlags.ENABLE_TASKBAR_NAVBAR_UNIFICATION;
import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_TRANSIENT_TASKBAR_HIDE;
import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_TRANSIENT_TASKBAR_SHOW;
@@ -572,7 +573,8 @@
mAnimator.cancel();
}
mAnimator = new AnimatorSet();
- addJankMonitorListener(mAnimator, /* appearing= */ !mIsStashed);
+ addJankMonitorListener(
+ mAnimator, /* expanding= */ !mIsStashed, /* animationType= */ animationType);
boolean isTransientTaskbar = DisplayController.isTransientTaskbar(mActivity);
final float stashTranslation = mActivity.isPhoneMode() || isTransientTaskbar
? 0
@@ -798,14 +800,20 @@
as.play(a);
}
- private void addJankMonitorListener(AnimatorSet animator, boolean expanding) {
+ private void addJankMonitorListener(
+ AnimatorSet animator, boolean expanding, @StashAnimation int animationType) {
View v = mControllers.taskbarActivityContext.getDragLayer();
int action = expanding ? InteractionJankMonitor.CUJ_TASKBAR_EXPAND :
InteractionJankMonitor.CUJ_TASKBAR_COLLAPSE;
animator.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationStart(@NonNull Animator animation) {
- InteractionJankMonitor.getInstance().begin(v, action);
+ final Configuration.Builder builder =
+ Configuration.Builder.withView(action, v);
+ if (animationType == TRANSITION_HOME_TO_APP) {
+ builder.setTag("HOME_TO_APP");
+ }
+ InteractionJankMonitor.getInstance().begin(builder);
}
@Override
diff --git a/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java b/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java
index 6698600..4752225 100644
--- a/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java
+++ b/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java
@@ -30,6 +30,7 @@
import static com.android.launcher3.BaseActivity.EVENT_STARTED;
import static com.android.launcher3.BaseActivity.INVISIBLE_BY_STATE_HANDLER;
import static com.android.launcher3.BaseActivity.STATE_HANDLER_INVISIBILITY_FLAGS;
+import static com.android.launcher3.Flags.enableGridOnlyOverview;
import static com.android.launcher3.LauncherPrefs.ALL_APPS_OVERVIEW_THRESHOLD;
import static com.android.launcher3.PagedView.INVALID_PAGE;
import static com.android.launcher3.logging.StatsLogManager.LAUNCHER_STATE_BACKGROUND;
@@ -2561,9 +2562,11 @@
}
float scrollOffset = Math.abs(mRecentsView.getScrollOffset(mRecentsView.getCurrentPage()));
+ Rect carouselTaskSize = enableGridOnlyOverview()
+ ? mRecentsView.getLastComputedCarouselTaskSize()
+ : mRecentsView.getLastComputedTaskSize();
int maxScrollOffset = mRecentsView.getPagedOrientationHandler().getPrimaryValue(
- mRecentsView.getLastComputedTaskSize().width(),
- mRecentsView.getLastComputedTaskSize().height());
+ carouselTaskSize.width(), carouselTaskSize.height());
maxScrollOffset += mRecentsView.getPageSpacing();
float maxScaleProgress =
diff --git a/quickstep/src/com/android/quickstep/BaseActivityInterface.java b/quickstep/src/com/android/quickstep/BaseActivityInterface.java
index b89d20c..879312d 100644
--- a/quickstep/src/com/android/quickstep/BaseActivityInterface.java
+++ b/quickstep/src/com/android/quickstep/BaseActivityInterface.java
@@ -261,6 +261,23 @@
}
}
+ /**
+ * Calculates the taskView size for carousel during app to overview animation on tablets.
+ */
+ public final void calculateCarouselTaskSize(Context context, DeviceProfile dp, Rect outRect,
+ PagedOrientationHandler orientedState) {
+ if (dp.isTablet && dp.isGestureMode) {
+ Resources res = context.getResources();
+ float minScale = res.getFloat(R.dimen.overview_carousel_min_scale);
+ Rect gridRect = new Rect();
+ calculateGridSize(dp, context, gridRect);
+ calculateTaskSizeInternal(context, dp, gridRect, minScale, Gravity.CENTER | Gravity.TOP,
+ outRect);
+ } else {
+ calculateTaskSize(context, dp, outRect, orientedState);
+ }
+ }
+
private void calculateFocusTaskSize(Context context, DeviceProfile dp, Rect outRect) {
Resources res = context.getResources();
float maxScale = res.getFloat(R.dimen.overview_max_scale);
@@ -286,13 +303,13 @@
}
private void calculateTaskSizeInternal(Context context, DeviceProfile dp,
- Rect potentialTaskRect, float maxScale, int gravity, Rect outRect) {
+ Rect potentialTaskRect, float targetScale, int gravity, Rect outRect) {
PointF taskDimension = getTaskDimension(context, dp);
float scale = Math.min(
potentialTaskRect.width() / taskDimension.x,
potentialTaskRect.height() / taskDimension.y);
- scale = Math.min(scale, maxScale);
+ scale = Math.min(scale, targetScale);
int outWidth = Math.round(scale * taskDimension.x);
int outHeight = Math.round(scale * taskDimension.y);
diff --git a/quickstep/src/com/android/quickstep/RecentsAnimationDeviceState.java b/quickstep/src/com/android/quickstep/RecentsAnimationDeviceState.java
index d617828..cb0aa8f 100644
--- a/quickstep/src/com/android/quickstep/RecentsAnimationDeviceState.java
+++ b/quickstep/src/com/android/quickstep/RecentsAnimationDeviceState.java
@@ -22,7 +22,6 @@
import static com.android.launcher3.util.DisplayController.CHANGE_ALL;
import static com.android.launcher3.util.DisplayController.CHANGE_NAVIGATION_MODE;
import static com.android.launcher3.util.DisplayController.CHANGE_ROTATION;
-import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR;
import static com.android.launcher3.util.NavigationMode.NO_BUTTON;
import static com.android.launcher3.util.NavigationMode.THREE_BUTTONS;
import static com.android.launcher3.util.SettingsCache.ONE_HANDED_ENABLED;
@@ -54,15 +53,11 @@
import android.os.RemoteException;
import android.os.SystemProperties;
import android.provider.Settings;
-import android.util.Log;
-import android.view.ISystemGestureExclusionListener;
-import android.view.IWindowManager;
import android.view.MotionEvent;
import android.view.ViewConfiguration;
-import android.view.WindowManagerGlobal;
-import androidx.annotation.BinderThread;
import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import com.android.launcher3.config.FeatureFlags;
@@ -73,6 +68,8 @@
import com.android.launcher3.util.SettingsCache;
import com.android.quickstep.TopTaskTracker.CachedTaskInfo;
import com.android.quickstep.util.ActiveGestureLog;
+import com.android.quickstep.util.GestureExclusionManager;
+import com.android.quickstep.util.GestureExclusionManager.ExclusionListener;
import com.android.quickstep.util.NavBarPosition;
import com.android.systemui.shared.system.ActivityManagerWrapper;
import com.android.systemui.shared.system.QuickStepContract;
@@ -86,7 +83,7 @@
/**
* Manages the state of the system during a swipe up gesture.
*/
-public class RecentsAnimationDeviceState implements DisplayInfoChangeListener {
+public class RecentsAnimationDeviceState implements DisplayInfoChangeListener, ExclusionListener {
private static final String TAG = "RecentsAnimationDeviceState";
@@ -99,25 +96,9 @@
private final Context mContext;
private final DisplayController mDisplayController;
- private final IWindowManager mIWindowManager;
+ private final GestureExclusionManager mExclusionManager;
- @VisibleForTesting
- final ISystemGestureExclusionListener mGestureExclusionListener =
- new ISystemGestureExclusionListener.Stub() {
- @BinderThread
- @Override
- public void onSystemGestureExclusionChanged(int displayId,
- Region systemGestureExclusionRegion, Region unrestrictedOrNull) {
- if (displayId != DEFAULT_DISPLAY) {
- return;
- }
- // Assignments are atomic, it should be safe on binder thread. Also we don't
- // think systemGestureExclusionRegion can be null but just in case, don't
- // let mExclusionRegion be null.
- mExclusionRegion = systemGestureExclusionRegion != null
- ? systemGestureExclusionRegion : new Region();
- }
- };
+
private final RotationTouchHelper mRotationTouchHelper;
private final TaskStackChangeListener mPipListener;
// Cache for better performance since it doesn't change at runtime.
@@ -140,20 +121,20 @@
private boolean mPipIsActive;
private int mGestureBlockingTaskId = -1;
- private @NonNull Region mExclusionRegion = new Region();
+ private @NonNull Region mExclusionRegion = GestureExclusionManager.EMPTY_REGION;
private boolean mExclusionListenerRegistered;
public RecentsAnimationDeviceState(Context context) {
- this(context, false, WindowManagerGlobal.getWindowManagerService());
+ this(context, false, GestureExclusionManager.INSTANCE);
}
public RecentsAnimationDeviceState(Context context, boolean isInstanceForTouches) {
- this(context, isInstanceForTouches, WindowManagerGlobal.getWindowManagerService());
+ this(context, isInstanceForTouches, GestureExclusionManager.INSTANCE);
}
@VisibleForTesting
- RecentsAnimationDeviceState(Context context, IWindowManager windowManager) {
- this(context, false, windowManager);
+ RecentsAnimationDeviceState(Context context, GestureExclusionManager exclusionManager) {
+ this(context, false, exclusionManager);
}
/**
@@ -162,10 +143,10 @@
*/
RecentsAnimationDeviceState(
Context context, boolean isInstanceForTouches,
- IWindowManager windowManager) {
+ GestureExclusionManager exclusionManager) {
mContext = context;
mDisplayController = DisplayController.INSTANCE.get(context);
- mIWindowManager = windowManager;
+ mExclusionManager = exclusionManager;
mIsOneHandedModeSupported = SystemProperties.getBoolean(SUPPORT_ONE_HANDED_MODE, false);
mRotationTouchHelper = RotationTouchHelper.INSTANCE.get(context);
if (isInstanceForTouches) {
@@ -276,43 +257,33 @@
}
}
- /**
- * Registers {@link mGestureExclusionListener} for getting exclusion rect changes. Note that we
- * make binder call on {@link UI_HELPER_EXECUTOR} to avoid jank.
- */
- public void registerExclusionListener() {
- UI_HELPER_EXECUTOR.execute(() -> {
- if (mExclusionListenerRegistered) {
- return;
- }
- try {
- mIWindowManager.registerSystemGestureExclusionListener(
- mGestureExclusionListener, DEFAULT_DISPLAY);
- mExclusionListenerRegistered = true;
- } catch (RemoteException e) {
- Log.e(TAG, "Failed to register window manager callbacks", e);
- }
- });
+ @Override
+ public void onGestureExclusionChanged(@Nullable Region exclusionRegion,
+ @Nullable Region unrestrictedOrNull) {
+ mExclusionRegion = exclusionRegion != null
+ ? exclusionRegion : GestureExclusionManager.EMPTY_REGION;
}
/**
- * Unregisters {@link mGestureExclusionListener} if previously registered. We make binder call
- * on same {@link UI_HELPER_EXECUTOR} as in {@link #registerExclusionListener()} so that
- * read/write {@link mExclusionListenerRegistered} field is thread safe.
+ * Registers itself for getting exclusion rect changes.
+ */
+ public void registerExclusionListener() {
+ if (mExclusionListenerRegistered) {
+ return;
+ }
+ mExclusionManager.addListener(this);
+ mExclusionListenerRegistered = true;
+ }
+
+ /**
+ * Unregisters itself as gesture exclusion listener if previously registered.
*/
public void unregisterExclusionListener() {
- UI_HELPER_EXECUTOR.execute(() -> {
- if (!mExclusionListenerRegistered) {
- return;
- }
- try {
- mIWindowManager.unregisterSystemGestureExclusionListener(
- mGestureExclusionListener, DEFAULT_DISPLAY);
- mExclusionListenerRegistered = false;
- } catch (RemoteException e) {
- Log.e(TAG, "Failed to unregister window manager callbacks", e);
- }
- });
+ if (!mExclusionListenerRegistered) {
+ return;
+ }
+ mExclusionManager.removeListener(this);
+ mExclusionListenerRegistered = false;
}
public void onOneHandedModeChanged(int newGesturalHeight) {
@@ -515,10 +486,8 @@
* This is only used for quickswitch, and not swipe up.
*/
public boolean isInExclusionRegion(MotionEvent event) {
- // mExclusionRegion can change on binder thread, use a local instance here.
- Region exclusionRegion = mExclusionRegion;
return mMode == NO_BUTTON
- && exclusionRegion.contains((int) event.getX(), (int) event.getY());
+ && mExclusionRegion.contains((int) event.getX(), (int) event.getY());
}
/**
diff --git a/quickstep/src/com/android/quickstep/orientation/LandscapePagedViewHandler.java b/quickstep/src/com/android/quickstep/orientation/LandscapePagedViewHandler.java
index a7ca515..8648b56 100644
--- a/quickstep/src/com/android/quickstep/orientation/LandscapePagedViewHandler.java
+++ b/quickstep/src/com/android/quickstep/orientation/LandscapePagedViewHandler.java
@@ -274,21 +274,12 @@
@Override
public float getTaskMenuX(float x, View thumbnailView,
DeviceProfile deviceProfile, float taskInsetMargin, View taskViewIcon) {
- if (enableOverviewIconMenu()) {
- return x + (taskInsetMargin / 2f);
- }
return thumbnailView.getMeasuredWidth() + x - taskInsetMargin;
}
@Override
public float getTaskMenuY(float y, View thumbnailView, int stagePosition,
View taskMenuView, float taskInsetMargin, View taskViewIcon) {
- if (enableOverviewIconMenu()) {
- return y - (thumbnailView.getLayoutDirection() == LAYOUT_DIRECTION_RTL
- ? taskMenuView.getMeasuredHeight() * 2 - (taskInsetMargin / 2f)
- : taskMenuView.getMeasuredHeight());
-
- }
BaseDragLayer.LayoutParams lp = (BaseDragLayer.LayoutParams) taskMenuView.getLayoutParams();
int taskMenuWidth = lp.width;
if (stagePosition == STAGE_POSITION_UNDEFINED) {
@@ -304,7 +295,7 @@
@StagePosition int stagePosition) {
if (enableOverviewIconMenu()) {
return thumbnailView.getResources().getDimensionPixelSize(
- R.dimen.task_thumbnail_icon_menu_max_width);
+ R.dimen.task_thumbnail_icon_menu_expanded_width);
}
if (stagePosition == SplitConfigurationOptions.STAGE_POSITION_UNDEFINED) {
return thumbnailView.getMeasuredWidth();
@@ -582,11 +573,6 @@
@Override
public void setTaskIconParams(FrameLayout.LayoutParams iconParams, int taskIconMargin,
int taskIconHeight, int thumbnailTopMargin, boolean isRtl) {
- if (enableOverviewIconMenu()) {
- iconParams.gravity = Gravity.START | Gravity.CENTER_VERTICAL;
- iconParams.topMargin = 0;
- return;
- }
iconParams.gravity = (isRtl ? START : END) | CENTER_VERTICAL;
iconParams.rightMargin = -taskIconHeight - taskIconMargin / 2;
iconParams.leftMargin = 0;
@@ -595,6 +581,14 @@
}
@Override
+ public void setIconAppChipChildrenParams(FrameLayout.LayoutParams iconParams,
+ int chipChildMarginStart) {
+ iconParams.gravity = Gravity.START | Gravity.CENTER_VERTICAL;
+ iconParams.setMarginStart(chipChildMarginStart);
+ iconParams.topMargin = 0;
+ }
+
+ @Override
public void setIconAppChipMenuParams(IconAppChipView iconAppChipView,
FrameLayout.LayoutParams iconMenuParams, int iconMenuMargin, int thumbnailTopMargin) {
boolean isRtl = iconAppChipView.getLayoutDirection() == LAYOUT_DIRECTION_RTL;
@@ -608,7 +602,7 @@
: iconMenuParams.width / 2f);
iconAppChipView.setPivotY(
isRtl ? (iconMenuParams.height / 2f) : iconMenuParams.width / 2f);
- iconAppChipView.setTranslationY(0);
+ iconAppChipView.setSplitTranslationY(0);
iconAppChipView.setRotation(getDegreesRotated());
}
@@ -647,12 +641,14 @@
primaryIconView.setTranslationX(0);
secondaryIconView.setTranslationX(0);
if (enableOverviewIconMenu()) {
+ IconAppChipView primaryAppChipView = (IconAppChipView) primaryIconView;
+ IconAppChipView secondaryAppChipView = (IconAppChipView) secondaryIconView;
if (primaryIconView.getLayoutDirection() == LAYOUT_DIRECTION_RTL) {
- secondaryIconView.setTranslationY(-primarySnapshotHeight);
- primaryIconView.setTranslationY(0);
+ secondaryAppChipView.setSplitTranslationY(-primarySnapshotHeight);
+ primaryAppChipView.setSplitTranslationY(0);
} else {
int secondarySnapshotHeight = groupedTaskViewHeight - primarySnapshotHeight;
- primaryIconView.setTranslationY(secondarySnapshotHeight);
+ primaryAppChipView.setSplitTranslationY(secondarySnapshotHeight);
}
} else if (splitConfig.initiatedFromSeascape) {
// if the split was initiated from seascape,
diff --git a/quickstep/src/com/android/quickstep/orientation/PortraitPagedViewHandler.java b/quickstep/src/com/android/quickstep/orientation/PortraitPagedViewHandler.java
index 5d9a668..60e6a25 100644
--- a/quickstep/src/com/android/quickstep/orientation/PortraitPagedViewHandler.java
+++ b/quickstep/src/com/android/quickstep/orientation/PortraitPagedViewHandler.java
@@ -21,7 +21,6 @@
import static android.view.Gravity.END;
import static android.view.Gravity.START;
import static android.view.Gravity.TOP;
-import static android.view.View.LAYOUT_DIRECTION_RTL;
import static android.view.ViewGroup.LayoutParams.MATCH_PARENT;
import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT;
@@ -182,12 +181,7 @@
@Override
public float getTaskMenuX(float x, View thumbnailView,
DeviceProfile deviceProfile, float taskInsetMargin, View taskViewIcon) {
- if (enableOverviewIconMenu()) {
- if (thumbnailView.getLayoutDirection() == LAYOUT_DIRECTION_RTL) {
- return x + taskInsetMargin - taskViewIcon.getHeight() - (taskInsetMargin / 2f);
- }
- return x + taskInsetMargin;
- } else if (deviceProfile.isLandscape) {
+ if (deviceProfile.isLandscape) {
return x + taskInsetMargin
+ (thumbnailView.getMeasuredWidth() - thumbnailView.getMeasuredHeight()) / 2f;
} else {
@@ -198,9 +192,6 @@
@Override
public float getTaskMenuY(float y, View thumbnailView, int stagePosition,
View taskMenuView, float taskInsetMargin, View taskViewIcon) {
- if (enableOverviewIconMenu()) {
- return y;
- }
return y + taskInsetMargin;
}
@@ -209,7 +200,7 @@
@StagePosition int stagePosition) {
if (enableOverviewIconMenu()) {
return thumbnailView.getResources().getDimensionPixelSize(
- R.dimen.task_thumbnail_icon_menu_max_width);
+ R.dimen.task_thumbnail_icon_menu_expanded_width);
}
int padding = thumbnailView.getResources()
.getDimensionPixelSize(R.dimen.task_menu_edge_padding);
@@ -623,12 +614,6 @@
@Override
public void setTaskIconParams(FrameLayout.LayoutParams iconParams, int taskIconMargin,
int taskIconHeight, int thumbnailTopMargin, boolean isRtl) {
- if (enableOverviewIconMenu()) {
- iconParams.setMarginStart(taskIconMargin);
- iconParams.gravity = Gravity.START | Gravity.CENTER_VERTICAL;
- iconParams.topMargin = 0;
- return;
- }
iconParams.gravity = TOP | CENTER_HORIZONTAL;
// Reset margins, since they may have been set on rotation
iconParams.leftMargin = iconParams.rightMargin = 0;
@@ -636,6 +621,14 @@
}
@Override
+ public void setIconAppChipChildrenParams(FrameLayout.LayoutParams iconParams,
+ int chipChildMarginStart) {
+ iconParams.setMarginStart(chipChildMarginStart);
+ iconParams.gravity = Gravity.START | Gravity.CENTER_VERTICAL;
+ iconParams.topMargin = 0;
+ }
+
+ @Override
public void setIconAppChipMenuParams(IconAppChipView iconAppChipView,
FrameLayout.LayoutParams iconMenuParams, int iconMenuMargin, int thumbnailTopMargin) {
iconMenuParams.gravity = TOP | START;
@@ -646,7 +639,7 @@
iconAppChipView.setPivotX(0);
iconAppChipView.setPivotY(0);
- iconAppChipView.setTranslationY(0);
+ iconAppChipView.setSplitTranslationY(0);
iconAppChipView.setRotation(getDegreesRotated());
}
@@ -662,6 +655,8 @@
: new FrameLayout.LayoutParams(primaryIconParams);
if (enableOverviewIconMenu()) {
+ IconAppChipView primaryAppChipView = (IconAppChipView) primaryIconView;
+ IconAppChipView secondaryAppChipView = (IconAppChipView) secondaryIconView;
primaryIconParams.gravity = TOP | START;
secondaryIconParams.gravity = TOP | START;
secondaryIconParams.topMargin = primaryIconParams.topMargin;
@@ -669,16 +664,16 @@
if (deviceProfile.isLeftRightSplit) {
if (isRtl) {
int secondarySnapshotWidth = groupedTaskViewWidth - primarySnapshotWidth;
- primaryIconView.setTranslationX(-secondarySnapshotWidth);
+ primaryAppChipView.setSplitTranslationX(-secondarySnapshotWidth);
} else {
- secondaryIconView.setTranslationX(primarySnapshotWidth);
+ secondaryAppChipView.setSplitTranslationX(primarySnapshotWidth);
}
} else {
- primaryIconView.setTranslationX(0);
- secondaryIconView.setTranslationX(0);
+ primaryAppChipView.setSplitTranslationX(0);
+ secondaryAppChipView.setSplitTranslationX(0);
int dividerThickness = Math.min(splitConfig.visualDividerBounds.width(),
splitConfig.visualDividerBounds.height());
- secondaryIconView.setTranslationY(
+ secondaryAppChipView.setSplitTranslationY(
primarySnapshotHeight + (deviceProfile.isTablet ? 0 : dividerThickness));
}
} else if (deviceProfile.isLeftRightSplit) {
diff --git a/quickstep/src/com/android/quickstep/orientation/RecentsPagedOrientationHandler.java b/quickstep/src/com/android/quickstep/orientation/RecentsPagedOrientationHandler.java
index 9084297..01c1225 100644
--- a/quickstep/src/com/android/quickstep/orientation/RecentsPagedOrientationHandler.java
+++ b/quickstep/src/com/android/quickstep/orientation/RecentsPagedOrientationHandler.java
@@ -146,9 +146,16 @@
int parentWidth, int parentHeight);
// Overview TaskMenuView methods
+ /** Sets layout params on a task's app icon. Only use this when app chip is disabled. */
void setTaskIconParams(FrameLayout.LayoutParams iconParams,
int taskIconMargin, int taskIconHeight, int thumbnailTopMargin, boolean isRtl);
+ /**
+ * Sets layout params on the children of an app chip. Only use this when app chip is enabled.
+ */
+ void setIconAppChipChildrenParams(
+ FrameLayout.LayoutParams iconParams, int chipChildMarginStart);
+
void setIconAppChipMenuParams(IconAppChipView iconAppChipView,
FrameLayout.LayoutParams iconMenuParams,
int iconMenuMargin, int thumbnailTopMargin);
diff --git a/quickstep/src/com/android/quickstep/orientation/SeascapePagedViewHandler.java b/quickstep/src/com/android/quickstep/orientation/SeascapePagedViewHandler.java
index f3001fc..a964639 100644
--- a/quickstep/src/com/android/quickstep/orientation/SeascapePagedViewHandler.java
+++ b/quickstep/src/com/android/quickstep/orientation/SeascapePagedViewHandler.java
@@ -94,9 +94,6 @@
@Override
public float getTaskMenuX(float x, View thumbnailView,
DeviceProfile deviceProfile, float taskInsetMargin, View taskViewIcon) {
- if (enableOverviewIconMenu()) {
- return x + (taskViewIcon.getHeight() * 2);
- }
return x + taskInsetMargin;
}
@@ -104,9 +101,7 @@
public float getTaskMenuY(float y, View thumbnailView, int stagePosition,
View taskMenuView, float taskInsetMargin, View taskViewIcon) {
if (enableOverviewIconMenu()) {
- return y + taskViewIcon.getWidth() + (
- thumbnailView.getLayoutDirection() == LAYOUT_DIRECTION_RTL ? taskInsetMargin
- / 2f : -taskViewIcon.getHeight());
+ return y;
}
BaseDragLayer.LayoutParams lp = (BaseDragLayer.LayoutParams) taskMenuView.getLayoutParams();
int taskMenuWidth = lp.width;
@@ -222,18 +217,16 @@
@Override
public void setTaskIconParams(FrameLayout.LayoutParams iconParams,
int taskIconMargin, int taskIconHeight, int thumbnailTopMargin, boolean isRtl) {
- iconParams.rightMargin = 0;
- iconParams.bottomMargin = 0;
- if (enableOverviewIconMenu()) {
- iconParams.setMarginStart(taskIconMargin);
- iconParams.gravity = Gravity.START | Gravity.CENTER_VERTICAL;
- iconParams.leftMargin = 0;
- iconParams.topMargin = 0;
- } else {
- iconParams.gravity = (isRtl ? END : START) | CENTER_VERTICAL;
- iconParams.leftMargin = -taskIconHeight - taskIconMargin / 2;
- iconParams.topMargin = thumbnailTopMargin / 2;
- }
+ iconParams.gravity = (isRtl ? END : START) | CENTER_VERTICAL;
+ iconParams.setMargins(-taskIconHeight - taskIconMargin / 2, thumbnailTopMargin / 2, 0, 0);
+ }
+
+ @Override
+ public void setIconAppChipChildrenParams(FrameLayout.LayoutParams iconParams,
+ int chipChildMarginStart) {
+ iconParams.setMargins(0, 0, 0, 0);
+ iconParams.setMarginStart(chipChildMarginStart);
+ iconParams.gravity = Gravity.START | Gravity.CENTER_VERTICAL;
}
@Override
@@ -251,7 +244,7 @@
iconAppChipView.setPivotX(isRtl ? iconMenuParams.width / 2f : iconCenter);
iconAppChipView.setPivotY(
isRtl ? iconMenuParams.width / 2f : iconCenter - iconMenuMargin);
- iconAppChipView.setTranslationY(0);
+ iconAppChipView.setSplitTranslationY(0);
iconAppChipView.setRotation(getDegreesRotated());
}
@@ -288,12 +281,15 @@
primaryIconView.setTranslationX(0);
secondaryIconView.setTranslationX(0);
if (enableOverviewIconMenu()) {
+ IconAppChipView primaryAppChipView = (IconAppChipView) primaryIconView;
+ IconAppChipView secondaryAppChipView = (IconAppChipView) secondaryIconView;
if (isRtl) {
- primaryIconView.setTranslationY(groupedTaskViewHeight - primarySnapshotHeight);
- secondaryIconView.setTranslationY(0);
+ primaryAppChipView.setSplitTranslationY(
+ groupedTaskViewHeight - primarySnapshotHeight);
+ secondaryAppChipView.setSplitTranslationY(0);
} else {
- secondaryIconView.setTranslationY(-primarySnapshotHeight);
- primaryIconView.setTranslationY(0);
+ secondaryAppChipView.setSplitTranslationY(-primarySnapshotHeight);
+ primaryAppChipView.setSplitTranslationY(0);
}
} else if (splitConfig.initiatedFromSeascape) {
// if the split was initiated from seascape,
diff --git a/quickstep/src/com/android/quickstep/util/AnimatorControllerWithResistance.java b/quickstep/src/com/android/quickstep/util/AnimatorControllerWithResistance.java
index bc8b571..16f2065 100644
--- a/quickstep/src/com/android/quickstep/util/AnimatorControllerWithResistance.java
+++ b/quickstep/src/com/android/quickstep/util/AnimatorControllerWithResistance.java
@@ -17,6 +17,7 @@
import static com.android.app.animation.Interpolators.DECELERATE;
import static com.android.app.animation.Interpolators.LINEAR;
+import static com.android.launcher3.Flags.enableGridOnlyOverview;
import static com.android.launcher3.LauncherPrefs.ALL_APPS_OVERVIEW_THRESHOLD;
import static com.android.quickstep.views.RecentsView.RECENTS_SCALE_PROPERTY;
import static com.android.quickstep.views.RecentsView.TASK_SECONDARY_TRANSLATION;
@@ -58,6 +59,7 @@
FROM_APP(0.75f, 0.5f, 1f, false),
FROM_APP_TO_ALL_APPS(1f, 0.6f, 0.8f, false),
FROM_APP_TABLET(1f, 0.7f, 1f, true),
+ FROM_APP_TABLET_GRID_ONLY(1f, 1f, 1f, true),
FROM_APP_TO_ALL_APPS_TABLET(1f, 0.5f, 0.5f, false),
FROM_OVERVIEW(1f, 0.75f, 0.5f, false);
@@ -239,10 +241,10 @@
float stopResist =
params.resistanceParams.stopScalingAtTop ? 1f - startRect.top / endRectF.top : 1f;
final TimeInterpolator scaleInterpolator = t -> {
- if (t < startResist) {
+ if (t <= startResist) {
return t;
}
- if (t > stopResist) {
+ if (t >= stopResist) {
return maxResist;
}
float resistProgress = Utilities.getProgress(t, startResist, stopResist);
@@ -304,7 +306,9 @@
resistanceParams =
recentsOrientedState.getActivityInterface().allowAllAppsFromOverview()
? RecentsResistanceParams.FROM_APP_TO_ALL_APPS_TABLET
- : RecentsResistanceParams.FROM_APP_TABLET;
+ : enableGridOnlyOverview()
+ ? RecentsResistanceParams.FROM_APP_TABLET_GRID_ONLY
+ : RecentsResistanceParams.FROM_APP_TABLET;
} else {
resistanceParams =
recentsOrientedState.getActivityInterface().allowAllAppsFromOverview()
diff --git a/quickstep/src/com/android/quickstep/util/AppPairsController.java b/quickstep/src/com/android/quickstep/util/AppPairsController.java
index 839320e..8f9395f 100644
--- a/quickstep/src/com/android/quickstep/util/AppPairsController.java
+++ b/quickstep/src/com/android/quickstep/util/AppPairsController.java
@@ -17,17 +17,22 @@
package com.android.quickstep.util;
+import static android.app.ActivityTaskManager.INVALID_TASK_ID;
+
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 static com.android.launcher3.util.SplitConfigurationOptions.STAGE_POSITION_BOTTOM_OR_RIGHT;
+import static com.android.launcher3.util.SplitConfigurationOptions.STAGE_POSITION_TOP_OR_LEFT;
import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_BOTTOM_OR_RIGHT;
import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_TOP_OR_LEFT;
import static com.android.wm.shell.common.split.SplitScreenConstants.isPersistentSnapPosition;
-import android.app.ActivityTaskManager;
import android.content.Context;
import android.content.Intent;
+import android.content.pm.LauncherApps;
import android.util.Log;
+import android.util.Pair;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
@@ -39,16 +44,23 @@
import com.android.launcher3.accessibility.LauncherAccessibilityDelegate;
import com.android.launcher3.apppairs.AppPairIcon;
import com.android.launcher3.icons.IconCache;
+import com.android.launcher3.logging.InstanceId;
import com.android.launcher3.logging.StatsLogManager;
import com.android.launcher3.model.data.FolderInfo;
+import com.android.launcher3.model.data.ItemInfo;
import com.android.launcher3.model.data.WorkspaceItemInfo;
+import com.android.launcher3.taskbar.TaskbarActivityContext;
import com.android.launcher3.util.ComponentKey;
+import com.android.launcher3.util.SplitConfigurationOptions.StagePosition;
+import com.android.quickstep.SystemUiProxy;
+import com.android.quickstep.TopTaskTracker;
import com.android.quickstep.views.GroupedTaskView;
import com.android.quickstep.views.TaskView;
import com.android.systemui.shared.recents.model.Task;
import com.android.wm.shell.common.split.SplitScreenConstants.PersistentSnapPosition;
import java.util.Arrays;
+import java.util.List;
/**
* Controller class that handles app pair interactions: saving, modifying, deleting, etc.
@@ -150,7 +162,7 @@
task1Id = foundTask1.key.id;
task1Intent = null;
} else {
- task1Id = ActivityTaskManager.INVALID_TASK_ID;
+ task1Id = INVALID_TASK_ID;
task1Intent = app1.intent;
}
@@ -177,6 +189,170 @@
}
/**
+ * Handles the complicated logic for how to animate an app pair entrance when already inside an
+ * app or app pair.
+ *
+ * If the user tapped on an app pair while already in an app pair, there are 4 general cases:
+ * a) Clicked app pair A|B, but both apps are already running on screen.
+ * b) App A is already on-screen, but App B isn't.
+ * c) App B is on-screen, but App A isn't.
+ * d) Neither is on-screen.
+ *
+ * If the user tapped an app pair while inside a single app, there are 3 cases:
+ * a) The on-screen app is App A of the app pair.
+ * b) The on-screen app is App B of the app pair.
+ * c) It is neither.
+ *
+ * For each case, we call the appropriate animation and split launch type.
+ */
+ public void handleAppPairLaunchInApp(AppPairIcon launchingIconView,
+ List<? extends ItemInfo> itemInfos) {
+ TaskbarActivityContext context = (TaskbarActivityContext) launchingIconView.getContext();
+ List<ComponentKey> componentKeys =
+ itemInfos.stream().map(ItemInfo::getComponentKey).toList();
+
+ // Use TopTaskTracker to find the currently running app (or apps)
+ TopTaskTracker topTaskTracker = getTopTaskTracker(context);
+
+ // getRunningSplitTasksIds() will return a pair of ids if we are currently running a
+ // split pair, or an empty array with zero length if we are running a single app.
+ int[] runningSplitTasks = topTaskTracker.getRunningSplitTaskIds();
+ if (runningSplitTasks != null && runningSplitTasks.length == 2) {
+ // Tapped an app pair while in an app pair
+ int runningTaskId1 = runningSplitTasks[0];
+ int runningTaskId2 = runningSplitTasks[1];
+
+ mSplitSelectStateController.findLastActiveTasksAndRunCallback(
+ componentKeys,
+ false /* findExactPairMatch */,
+ foundTasks -> {
+ // If our clicked app pair has already-running Tasks, we grab the
+ // taskIds here so we can see if those ids are already on-screen now
+ List<Integer> lastActiveTasksOfAppPair =
+ Arrays.stream(foundTasks).map((Task task) -> {
+ if (task != null) {
+ return task.getKey().getId();
+ } else {
+ return INVALID_TASK_ID;
+ }
+ }).toList();
+
+ if (lastActiveTasksOfAppPair.contains(runningTaskId1)
+ && lastActiveTasksOfAppPair.contains(runningTaskId2)) {
+ // App A and App B are already on-screen, so do nothing.
+ } else if (!lastActiveTasksOfAppPair.contains(runningTaskId1)
+ && !lastActiveTasksOfAppPair.contains(runningTaskId2)) {
+ // Neither A nor B are on screen, so just launch a new app pair
+ // normally.
+ launchAppPair(launchingIconView);
+ } else {
+ // Exactly one app (A or B) is on-screen, so we have to launch the other
+ // on the appropriate side.
+ ItemInfo app1 = itemInfos.get(0);
+ ItemInfo app2 = itemInfos.get(1);
+ int task1 = lastActiveTasksOfAppPair.get(0);
+ int task2 = lastActiveTasksOfAppPair.get(1);
+
+ // If task1 is one of the running on-screen tasks, we launch app2.
+ // If not, task2 must be the running task, and we launch app1.
+ ItemInfo appToLaunch =
+ task1 == runningTaskId1 || task1 == runningTaskId2
+ ? app2
+ : app1;
+ // If the on-screen task is on the bottom/right position, we launch to
+ // the top/left. If not, we launch to the bottom/right.
+ @StagePosition int sideToLaunch =
+ task1 == runningTaskId2 || task2 == runningTaskId2
+ ? STAGE_POSITION_TOP_OR_LEFT
+ : STAGE_POSITION_BOTTOM_OR_RIGHT;
+
+ launchToSide(context, launchingIconView.getInfo(), appToLaunch,
+ sideToLaunch);
+ }
+ }
+ );
+ } else {
+ // Tapped an app pair while in a single app
+ int runningTaskId = topTaskTracker
+ .getCachedTopTask(false /* filterOnlyVisibleRecents */).getTaskId();
+
+ mSplitSelectStateController.findLastActiveTasksAndRunCallback(
+ componentKeys,
+ false /* findExactPairMatch */,
+ foundTasks -> {
+ Task foundTask1 = foundTasks[0];
+ Task foundTask2 = foundTasks[1];
+ boolean task1IsOnScreen =
+ foundTask1 != null && foundTask1.getKey().getId() == runningTaskId;
+ boolean task2IsOnScreen =
+ foundTask2 != null && foundTask2.getKey().getId() == runningTaskId;
+
+ if (!task1IsOnScreen && !task2IsOnScreen) {
+ // Neither App A nor App B are on-screen, launch the app pair normally.
+ launchAppPair(launchingIconView);
+ } else {
+ // Either A or B is on-screen, so launch the other on the appropriate
+ // side.
+ ItemInfo app1 = itemInfos.get(0);
+ ItemInfo app2 = itemInfos.get(1);
+ // If task1 is the running on-screen task, we launch app2 on the
+ // bottom/right. If task2 is on-screen, launch app1 on the top/left.
+ ItemInfo appToLaunch = task1IsOnScreen ? app2 : app1;
+ @StagePosition int sideToLaunch = task1IsOnScreen
+ ? STAGE_POSITION_BOTTOM_OR_RIGHT
+ : STAGE_POSITION_TOP_OR_LEFT;
+
+ launchToSide(context, launchingIconView.getInfo(), appToLaunch,
+ sideToLaunch);
+ }
+ }
+ );
+ }
+ }
+
+ /**
+ * Executes a split launch by launching an app to the side of an existing app.
+ * @param context The TaskbarActivityContext that we are launching the app pair from.
+ * @param launchingItemInfo The itemInfo of the icon that was tapped.
+ * @param app The app that will launch to the side of the existing running app (not necessarily
+ * the same as the previous parameter; e.g. we tap an app pair but launch an app).
+ * @param side A @StagePosition, either STAGE_POSITION_TOP_OR_LEFT or
+ * STAGE_POSITION_BOTTOM_OR_RIGHT.
+ */
+ @VisibleForTesting
+ public void launchToSide(
+ TaskbarActivityContext context,
+ ItemInfo launchingItemInfo,
+ ItemInfo app,
+ @StagePosition int side
+ ) {
+ LauncherApps launcherApps = context.getSystemService(LauncherApps.class);
+
+ // Set up to log app pair launch event
+ Pair<com.android.internal.logging.InstanceId, InstanceId> instanceIds =
+ LogUtils.getShellShareableInstanceId();
+ context.getStatsLogManager()
+ .logger()
+ .withItemInfo(launchingItemInfo)
+ .withInstanceId(instanceIds.second)
+ .log(LAUNCHER_APP_PAIR_LAUNCH);
+
+ SystemUiProxy.INSTANCE.get(context)
+ .startIntent(
+ launcherApps.getMainActivityLaunchIntent(
+ app.getIntent().getComponent(),
+ null,
+ app.user
+ ),
+ app.user.getIdentifier(),
+ new Intent(),
+ side,
+ null,
+ instanceIds.first
+ );
+ }
+
+ /**
* App pair members have a "rank" attribute that contains information about the split position
* and ratio. We implement this by splitting the int in half (e.g. 16 bits each), then use one
* half to store splitPosition (left vs right) and the other half to store snapPosition
@@ -209,4 +385,12 @@
public String getDefaultTitle(CharSequence app1, CharSequence app2) {
return mContext.getString(R.string.app_pair_default_title, app1, app2);
}
+
+ /**
+ * Gets the TopTaskTracker, which is a cached record of the top running Task.
+ */
+ @VisibleForTesting
+ public TopTaskTracker getTopTaskTracker(Context context) {
+ return TopTaskTracker.INSTANCE.get(context);
+ }
}
diff --git a/quickstep/src/com/android/quickstep/util/GestureExclusionManager.kt b/quickstep/src/com/android/quickstep/util/GestureExclusionManager.kt
new file mode 100644
index 0000000..24b0e3a
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/util/GestureExclusionManager.kt
@@ -0,0 +1,111 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.quickstep.util
+
+import android.graphics.Region
+import android.os.RemoteException
+import android.util.Log
+import android.view.Display.DEFAULT_DISPLAY
+import android.view.ISystemGestureExclusionListener
+import android.view.IWindowManager
+import android.view.WindowManagerGlobal
+import androidx.annotation.BinderThread
+import androidx.annotation.VisibleForTesting
+import com.android.launcher3.util.Executors
+
+/** Wrapper over system gesture exclusion listener to optimize for multiple RPCs */
+class GestureExclusionManager(private val windowManager: IWindowManager) {
+
+ private val listeners = mutableListOf<ExclusionListener>()
+
+ private var lastExclusionRegion: Region? = null
+ private var lastUnrestrictedOrNull: Region? = null
+
+ @VisibleForTesting
+ val exclusionListener =
+ object : ISystemGestureExclusionListener.Stub() {
+ @BinderThread
+ override fun onSystemGestureExclusionChanged(
+ displayId: Int,
+ exclusionRegion: Region?,
+ unrestrictedOrNull: Region?
+ ) {
+ if (displayId != DEFAULT_DISPLAY) {
+ return
+ }
+ Executors.MAIN_EXECUTOR.execute {
+ lastExclusionRegion = exclusionRegion
+ lastUnrestrictedOrNull = unrestrictedOrNull
+ listeners.forEach {
+ it.onGestureExclusionChanged(exclusionRegion, unrestrictedOrNull)
+ }
+ }
+ }
+ }
+
+ /** Adds a listener for receiving gesture exclusion regions */
+ fun addListener(listener: ExclusionListener) {
+ val wasEmpty = listeners.isEmpty()
+ listeners.add(listener)
+ if (wasEmpty) {
+ Executors.UI_HELPER_EXECUTOR.execute {
+ try {
+ windowManager.registerSystemGestureExclusionListener(
+ exclusionListener,
+ DEFAULT_DISPLAY
+ )
+ } catch (e: RemoteException) {
+ Log.e(TAG, "Failed to register gesture exclusion listener", e)
+ }
+ }
+ } else {
+ // If we had already registered before, dispatch the last known value,
+ // otherwise registering the listener will initiate a dispatch
+ listener.onGestureExclusionChanged(lastExclusionRegion, lastUnrestrictedOrNull)
+ }
+ }
+
+ /** Removes a previously added exclusion listener */
+ fun removeListener(listener: ExclusionListener) {
+ if (listeners.remove(listener) && listeners.isEmpty()) {
+ Executors.UI_HELPER_EXECUTOR.execute {
+ try {
+ windowManager.unregisterSystemGestureExclusionListener(
+ exclusionListener,
+ DEFAULT_DISPLAY
+ )
+ } catch (e: RemoteException) {
+ Log.e(TAG, "Failed to unregister gesture exclusion listener", e)
+ }
+ }
+ }
+ }
+
+ interface ExclusionListener {
+ fun onGestureExclusionChanged(exclusionRegion: Region?, unrestrictedOrNull: Region?)
+ }
+
+ companion object {
+
+ private const val TAG = "GestureExclusionManager"
+
+ @JvmField
+ val INSTANCE = GestureExclusionManager(WindowManagerGlobal.getWindowManagerService()!!)
+
+ @JvmField val EMPTY_REGION = Region()
+ }
+}
diff --git a/quickstep/src/com/android/quickstep/util/RectFSpringAnim.java b/quickstep/src/com/android/quickstep/util/RectFSpringAnim.java
index 251b756..245dde2 100644
--- a/quickstep/src/com/android/quickstep/util/RectFSpringAnim.java
+++ b/quickstep/src/com/android/quickstep/util/RectFSpringAnim.java
@@ -281,7 +281,15 @@
if (mRectScaleAnim.canSkipToEnd()) {
mRectScaleAnim.skipToEnd();
}
+ mCurrentScaleProgress = mRectScaleAnim.getSpring().getFinalPosition();
+
+ // Ensures that we end the animation with the final values.
+ mRectXAnimEnded = false;
+ mRectYAnimEnded = false;
+ mRectScaleAnimEnded = false;
+ onUpdate();
}
+
mRectXAnimEnded = true;
mRectYAnimEnded = true;
mRectScaleAnimEnded = true;
diff --git a/quickstep/src/com/android/quickstep/util/SplitAnimationController.kt b/quickstep/src/com/android/quickstep/util/SplitAnimationController.kt
index ad9f5ea..0ee27be 100644
--- a/quickstep/src/com/android/quickstep/util/SplitAnimationController.kt
+++ b/quickstep/src/com/android/quickstep/util/SplitAnimationController.kt
@@ -391,14 +391,6 @@
"trying to launch an app pair icon, but encountered an unexpected null"
}
- // If launching an app pair from Taskbar inside of an app context, use fade-in animation
- // TODO (b/316485863): Replace with desired app pair launch animation
- if (launchingIconView.context is TaskbarActivityContext) {
- composeFadeInSplitLaunchAnimator(
- initialTaskId, secondTaskId, info, t, finishCallback)
- return
- }
-
composeIconSplitLaunchAnimator(launchingIconView, info, t, finishCallback)
} else {
// Fallback case: simple fade-in animation
@@ -483,6 +475,10 @@
* We want to animate the Root (grandparent) so that it affects both apps and the divider.
* To do this, we find one of the nodes with WINDOWING_MODE_MULTI_WINDOW (one of the
* left-side ones, for simplicity) and traverse the tree until we find the grandparent.
+ *
+ * This function is only called when we are animating the app pair in from scratch. It is NOT
+ * called when we are animating in from an existing visible TaskView tile or an app that is
+ * already on screen.
*/
@VisibleForTesting
fun composeIconSplitLaunchAnimator(
@@ -491,6 +487,14 @@
t: Transaction,
finishCallback: Runnable
) {
+ // If launching an app pair from Taskbar inside of an app context (no access to Launcher),
+ // use the scale-up animation
+ if (launchingIconView.context is TaskbarActivityContext) {
+ composeScaleUpLaunchAnimation(transitionInfo, t, finishCallback)
+ return
+ }
+
+ // Else we are in Launcher and can launch with the full icon stretch-and-split animation.
val launcher = Launcher.getLauncher(launchingIconView.context)
val dp = launcher.deviceProfile
@@ -650,6 +654,91 @@
}
/**
+ * This is a scale-up-and-fade-in animation (34% to 100%) for launching an app in Overview when
+ * there is no visible associated tile to expand from.
+ */
+ @VisibleForTesting
+ fun composeScaleUpLaunchAnimation(
+ transitionInfo: TransitionInfo,
+ t: Transaction,
+ finishCallback: Runnable
+ ) {
+ val launchAnimation = AnimatorSet()
+ val progressUpdater = ValueAnimator.ofFloat(0f, 1f)
+ progressUpdater.setDuration(QuickstepTransitionManager.APP_LAUNCH_DURATION)
+ progressUpdater.interpolator = Interpolators.EMPHASIZED
+
+ var rootCandidate: Change? = null
+
+ for (change in transitionInfo.changes) {
+ val taskInfo: RunningTaskInfo = change.taskInfo ?: continue
+
+ // TODO (b/316490565): Replace this logic when SplitBounds is available to
+ // startAnimation() and we can know the precise taskIds of launching tasks.
+ // Find a change that has WINDOWING_MODE_MULTI_WINDOW.
+ if (
+ taskInfo.windowingMode == WINDOWING_MODE_MULTI_WINDOW &&
+ (change.mode == TRANSIT_OPEN || change.mode == TRANSIT_TO_FRONT)
+ ) {
+ // Found one!
+ rootCandidate = change
+ break
+ }
+ }
+
+ // If we could not find a proper root candidate, something went wrong.
+ check(rootCandidate != null) { "Could not find a split root candidate" }
+
+ // Recurse up the tree until parent is null, then we've found our root.
+ var parentToken: WindowContainerToken? = rootCandidate.parent
+ while (parentToken != null) {
+ rootCandidate = transitionInfo.getChange(parentToken) ?: break
+ parentToken = rootCandidate.parent
+ }
+
+ // Make sure nothing weird happened, like getChange() returning null.
+ check(rootCandidate != null) { "Failed to find a root leash" }
+
+ // Starting position is a 34% size tile centered in the middle of the screen.
+ // Ending position is the full device screen.
+ val screenBounds = rootCandidate.endAbsBounds
+ val startingScale = 0.34f
+ val startX =
+ screenBounds.left +
+ ((screenBounds.right - screenBounds.left) * ((1 - startingScale) / 2f))
+ val startY =
+ screenBounds.top +
+ ((screenBounds.bottom - screenBounds.top) * ((1 - startingScale) / 2f))
+ val endX = screenBounds.left
+ val endY = screenBounds.top
+
+ progressUpdater.addUpdateListener { valueAnimator: ValueAnimator ->
+ val progress = valueAnimator.animatedFraction
+
+ val x = startX + ((endX - startX) * progress)
+ val y = startY + ((endY - startY) * progress)
+ val scale = startingScale + ((1 - startingScale) * progress)
+
+ t.setPosition(rootCandidate.leash, x, y)
+ t.setScale(rootCandidate.leash, scale, scale)
+ t.setAlpha(rootCandidate.leash, progress)
+ t.apply()
+ }
+
+ // When animation ends, run finishCallback
+ progressUpdater.addListener(
+ object : AnimatorListenerAdapter() {
+ override fun onAnimationEnd(animation: Animator) {
+ finishCallback.run()
+ }
+ }
+ )
+
+ launchAnimation.play(progressUpdater)
+ launchAnimation.start()
+ }
+
+ /**
* If we are launching split screen without any special animation from a starting View, we
* simply fade in the starting apps and fade out launcher.
*/
diff --git a/quickstep/src/com/android/quickstep/util/TaskViewSimulator.java b/quickstep/src/com/android/quickstep/util/TaskViewSimulator.java
index 0bb6b23..1152de2 100644
--- a/quickstep/src/com/android/quickstep/util/TaskViewSimulator.java
+++ b/quickstep/src/com/android/quickstep/util/TaskViewSimulator.java
@@ -38,10 +38,12 @@
import android.graphics.RectF;
import android.util.Log;
import android.view.RemoteAnimationTarget;
+import android.view.animation.Interpolator;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
+import com.android.app.animation.Interpolators;
import com.android.launcher3.DeviceProfile;
import com.android.launcher3.Utilities;
import com.android.launcher3.anim.AnimatedFloat;
@@ -76,6 +78,8 @@
private final Rect mTaskRect = new Rect();
private final Rect mFullTaskSize = new Rect();
+ private final Rect mCarouselTaskSize = new Rect();
+ private PointF mPivotOverride = null;
private final PointF mPivot = new PointF();
private DeviceProfile mDp;
@StagePosition
@@ -95,6 +99,11 @@
public final AnimatedFloat taskPrimaryTranslation = new AnimatedFloat();
public final AnimatedFloat taskSecondaryTranslation = new AnimatedFloat();
+ // Carousel properties
+ public final AnimatedFloat carouselScale = new AnimatedFloat();
+ public final AnimatedFloat carouselPrimaryTranslation = new AnimatedFloat();
+ public final AnimatedFloat carouselSecondaryTranslation = new AnimatedFloat();
+
// RecentsView properties
public final AnimatedFloat recentsViewScale = new AnimatedFloat();
public final AnimatedFloat fullScreenProgress = new AnimatedFloat();
@@ -109,9 +118,9 @@
private Boolean mDrawsBelowRecents = null;
private boolean mIsGridTask;
private boolean mIsDesktopTask;
+ private boolean mScaleToCarouselTaskSize = false;
private int mTaskRectTranslationX;
private int mTaskRectTranslationY;
- private int mPivotOffsetX;
public TaskViewSimulator(Context context, BaseActivityInterface sizeStrategy) {
mContext = context;
@@ -124,6 +133,7 @@
mOrientationStateId = mOrientationState.getStateId();
Resources resources = context.getResources();
mIsRecentsRtl = mOrientationState.getOrientationHandler().getRecentsRtlSetting(resources);
+ carouselScale.value = 1f;
}
/**
@@ -149,6 +159,11 @@
mOrientationState.getOrientationHandler());
}
+ if (enableGridOnlyOverview()) {
+ mSizeStrategy.calculateCarouselTaskSize(mContext, mDp, mCarouselTaskSize,
+ mOrientationState.getOrientationHandler());
+ }
+
if (mSplitBounds != null) {
// The task rect changes according to the staged split task sizes, but recents
// fullscreen scale and pivot remains the same since the task fits into the existing
@@ -193,9 +208,18 @@
}
// Copy mFullTaskSize instead of updating it directly so it could be reused next time
// without recalculating
- Rect scaleRect = new Rect(mFullTaskSize);
- scaleRect.offset(mTaskRectTranslationX + mPivotOffsetX, mTaskRectTranslationY);
- return mOrientationState.getFullScreenScaleAndPivot(scaleRect, mDp, mPivot);
+ Rect scaleRect = new Rect();
+ if (mScaleToCarouselTaskSize) {
+ scaleRect.set(mCarouselTaskSize);
+ } else {
+ scaleRect.set(mFullTaskSize);
+ }
+ scaleRect.offset(mTaskRectTranslationX, mTaskRectTranslationY);
+ float scale = mOrientationState.getFullScreenScaleAndPivot(scaleRect, mDp, mPivot);
+ if (mPivotOverride != null) {
+ mPivot.set(mPivotOverride);
+ }
+ return scale;
}
/**
@@ -278,14 +302,64 @@
/**
* Adds animation for all the components corresponding to transition from an app to overview.
*/
- public void addAppToOverviewAnim(PendingAnimation pa, TimeInterpolator interpolator) {
+ public void addAppToOverviewAnim(PendingAnimation pa, Interpolator interpolator) {
pa.addFloat(fullScreenProgress, AnimatedFloat.VALUE, 1, 0, interpolator);
- if (enableGridOnlyOverview() && mDp.isTablet) {
- int translationXToMiddle = mDp.widthPx / 2 - mFullTaskSize.centerX();
- taskPrimaryTranslation.value = translationXToMiddle;
- mPivotOffsetX = translationXToMiddle;
+ float fullScreenScale;
+ if (enableGridOnlyOverview() && mDp.isTablet && mDp.isGestureMode) {
+ // Move pivot to top right edge of the screen, to avoid task scaling down in opposite
+ // direction of app window movement, otherwise the animation will wiggle left and right.
+ // Also translate the app window to top right edge of the screen to simplify
+ // calculations.
+ taskPrimaryTranslation.value = mIsRecentsRtl
+ ? mDp.widthPx - mFullTaskSize.right
+ : -mFullTaskSize.left;
+ taskSecondaryTranslation.value = -mFullTaskSize.top;
+ mPivotOverride = new PointF(mIsRecentsRtl ? mDp.widthPx : 0, 0);
+
+ // Scale down to the carousel and use the carousel Rect to calculate fullScreenScale.
+ mScaleToCarouselTaskSize = true;
+ carouselScale.value = mCarouselTaskSize.width() / (float) mFullTaskSize.width();
+ fullScreenScale = getFullScreenScale();
+
+ float carouselPrimaryTranslationTarget = mIsRecentsRtl
+ ? mCarouselTaskSize.right - mDp.widthPx
+ : mCarouselTaskSize.left;
+ float carouselSecondaryTranslationTarget = mCarouselTaskSize.top;
+
+ // Expected carousel position's center is in the middle, and invariant of
+ // recentsViewScale.
+ float exceptedCarouselCenterX = mCarouselTaskSize.centerX();
+ // Animating carousel translations linearly will result in a curved path, therefore
+ // we'll need to calculate the expected translation at each recentsView scale. Luckily
+ // primary and secondary follow the same translation, and primary is used here due to
+ // it being simpler.
+ Interpolator carouselTranslationInterpolator = t -> {
+ // recentsViewScale is calculated rather than using recentsViewScale.value, so that
+ // this interpolator works independently even if recentsViewScale don't animate.
+ float recentsViewScale =
+ Utilities.mapToRange(t, 0, 1, fullScreenScale, 1, Interpolators.LINEAR);
+ // Without the translation, the app window will animate from fullscreen into top
+ // right corner.
+ float expectedTaskCenterX = mIsRecentsRtl
+ ? mDp.widthPx - mCarouselTaskSize.width() * recentsViewScale / 2f
+ : mCarouselTaskSize.width() * recentsViewScale / 2f;
+ // Calculate the expected translation, then work back the animatedFraction that
+ // results in this value.
+ float carouselPrimaryTranslation =
+ (exceptedCarouselCenterX - expectedTaskCenterX) / recentsViewScale;
+ return carouselPrimaryTranslation / carouselPrimaryTranslationTarget;
+ };
+
+ // Use addAnimatedFloat so this animation can later be canceled and animate to a
+ // different value in RecentsView.onPrepareGestureEndAnimation.
+ pa.addAnimatedFloat(carouselPrimaryTranslation, 0, carouselPrimaryTranslationTarget,
+ carouselTranslationInterpolator);
+ pa.addAnimatedFloat(carouselSecondaryTranslation, 0, carouselSecondaryTranslationTarget,
+ carouselTranslationInterpolator);
+ } else {
+ fullScreenScale = getFullScreenScale();
}
- pa.addFloat(recentsViewScale, AnimatedFloat.VALUE, getFullScreenScale(), 1, interpolator);
+ pa.addFloat(recentsViewScale, AnimatedFloat.VALUE, fullScreenScale, 1, interpolator);
}
/**
@@ -382,7 +456,7 @@
float fullScreenProgress = Utilities.boundToRange(this.fullScreenProgress.value, 0, 1);
mCurrentFullscreenParams.setProgress(fullScreenProgress, recentsViewScale.value,
- /* taskViewScale= */1f);
+ carouselScale.value);
// Apply thumbnail matrix
float taskWidth = mTaskRect.width();
@@ -396,6 +470,13 @@
taskPrimaryTranslation.value);
mOrientationState.getOrientationHandler().setSecondary(mMatrix, MATRIX_POST_TRANSLATE,
taskSecondaryTranslation.value);
+
+ mMatrix.postScale(carouselScale.value, carouselScale.value, mPivot.x, mPivot.y);
+ mOrientationState.getOrientationHandler().setPrimary(mMatrix, MATRIX_POST_TRANSLATE,
+ carouselPrimaryTranslation.value);
+ mOrientationState.getOrientationHandler().setSecondary(mMatrix, MATRIX_POST_TRANSLATE,
+ carouselSecondaryTranslation.value);
+
mOrientationState.getOrientationHandler().setPrimary(
mMatrix, MATRIX_POST_TRANSLATE, recentsViewScroll.value);
@@ -420,15 +501,18 @@
return;
}
Log.d(TAG, "progress: " + fullScreenProgress
+ + " carouselScale: " + carouselScale.value
+ " recentsViewScale: " + recentsViewScale.value
+ " crop: " + mTmpCropRect
+ " radius: " + getCurrentCornerRadius()
+ " taskW: " + taskWidth + " H: " + taskHeight
+ " taskRect: " + mTaskRect
+ " taskPrimaryT: " + taskPrimaryTranslation.value
+ + " taskSecondaryT: " + taskSecondaryTranslation.value
+ + " carouselPrimaryT: " + carouselPrimaryTranslation.value
+ + " carouselSecondaryT: " + carouselSecondaryTranslation.value
+ " recentsPrimaryT: " + recentsViewPrimaryTranslation.value
+ " recentsSecondaryT: " + recentsViewSecondaryTranslation.value
- + " taskSecondaryT: " + taskSecondaryTranslation.value
+ " recentsScroll: " + recentsViewScroll.value
+ " pivot: " + mPivot
);
diff --git a/quickstep/src/com/android/quickstep/views/GroupedTaskView.java b/quickstep/src/com/android/quickstep/views/GroupedTaskView.java
index 66c67e7..ad4b292 100644
--- a/quickstep/src/com/android/quickstep/views/GroupedTaskView.java
+++ b/quickstep/src/com/android/quickstep/views/GroupedTaskView.java
@@ -398,8 +398,11 @@
layoutParams.width,
layoutParams.height
);
- int iconMargins = getResources().getDimensionPixelSize(
- R.dimen.task_thumbnail_icon_menu_start_margin) * 2;
+ int iconViewMarginStart = getResources().getDimensionPixelSize(
+ R.dimen.task_thumbnail_icon_menu_expanded_top_start_margin);
+ int iconViewBackgroundMarginStart = getResources().getDimensionPixelSize(
+ R.dimen.task_thumbnail_icon_menu_background_margin_top_start);
+ int iconMargins = (iconViewMarginStart + iconViewBackgroundMarginStart) * 2;
((IconAppChipView) mIconView).setMaxWidth(groupedTaskViewSizes.first.x - iconMargins);
((IconAppChipView) mIconView2).setMaxWidth(groupedTaskViewSizes.second.x - iconMargins);
}
diff --git a/quickstep/src/com/android/quickstep/views/IconAppChipView.java b/quickstep/src/com/android/quickstep/views/IconAppChipView.java
index 3347665..cb3566e 100644
--- a/quickstep/src/com/android/quickstep/views/IconAppChipView.java
+++ b/quickstep/src/com/android/quickstep/views/IconAppChipView.java
@@ -17,30 +17,35 @@
import static com.android.app.animation.Interpolators.EMPHASIZED;
import static com.android.app.animation.Interpolators.LINEAR;
+import static com.android.launcher3.LauncherAnimUtils.SCALE_PROPERTY;
+import static com.android.launcher3.LauncherAnimUtils.VIEW_TRANSLATE_X;
+import static com.android.launcher3.LauncherAnimUtils.VIEW_TRANSLATE_Y;
import android.animation.Animator;
-import android.animation.AnimatorListenerAdapter;
import android.animation.AnimatorSet;
import android.animation.ObjectAnimator;
+import android.animation.RectEvaluator;
+import android.animation.ValueAnimator;
import android.content.Context;
import android.content.res.Resources;
+import android.graphics.Outline;
+import android.graphics.Rect;
import android.graphics.drawable.AnimatedVectorDrawable;
import android.graphics.drawable.Drawable;
import android.util.AttributeSet;
-import android.view.Gravity;
import android.view.View;
import android.view.ViewAnimationUtils;
+import android.view.ViewOutlineProvider;
import android.widget.FrameLayout;
import android.widget.ImageView;
import android.widget.TextView;
import androidx.annotation.Nullable;
-import com.android.launcher3.DeviceProfile;
import com.android.launcher3.R;
import com.android.launcher3.Utilities;
+import com.android.launcher3.util.MultiPropertyFactory;
import com.android.launcher3.util.MultiValueAlpha;
-import com.android.launcher3.views.ActivityContext;
import com.android.quickstep.orientation.RecentsPagedOrientationHandler;
import com.android.quickstep.util.RecentsOrientedState;
@@ -59,38 +64,93 @@
private final MultiValueAlpha mMultiValueAlpha;
+ private View mMenuAnchorView;
private IconView mIconView;
// Two textview so we can ellipsize the collapsed view and crossfade on expand to the full name.
private TextView mIconTextCollapsedView;
private TextView mIconTextExpandedView;
private ImageView mIconArrowView;
- private ImageView mIconViewBackground;
- // Use separate views for the rounded corners so we can scale the background view without
- // warping the corners.
- private ImageView mIconViewBackgroundCornersStart;
- private ImageView mIconViewBackgroundCornersEnd;
-
- private final int mMinimumMenuSize;
- private final int mMaxMenuWidth;
- private final int mIconMenuMarginTop;
- private final int mIconMenuMarginStart;
+ private final Rect mBackgroundRelativeLtrLocation = new Rect();
+ final RectEvaluator mBackgroundAnimationRectEvaluator =
+ new RectEvaluator(mBackgroundRelativeLtrLocation);
+ private final int mCollapsedMenuDefaultWidth;
+ private final int mExpandedMenuDefaultWidth;
+ private final int mCollapsedMenuDefaultHeight;
+ private final int mExpandedMenuDefaultHeight;
+ private final int mIconMenuMarginTopStart;
+ private final int mMenuToChipGap;
+ private final int mBackgroundMarginTopStart;
+ private final int mAppNameHorizontalMargin;
private final int mIconViewMarginStart;
- private final int mIconViewDrawableSize;
- private final int mIconViewDrawableMaxSize;
- private final int mIconTextMinWidth;
- private final int mIconTextMaxWidth;
- private final int mTextMaxTranslationX;
- private final int mInnerMargin;
- private final float mArrowMaxTranslationX;
- private final int mMinIconBackgroundWidth;
- private final int mMaxIconBackgroundHeight;
- private final int mMinIconBackgroundHeight;
- private final int mMaxIconBackgroundCornerRadius;
- private final float mMinIconBackgroundCornerRadius;
+ private final int mAppIconSize;
+ private final int mArrowSize;
+ private final int mIconViewDrawableExpandedSize;
+ private final int mArrowMarginEnd;
private AnimatorSet mAnimator;
private int mMaxWidth = Integer.MAX_VALUE;
+ private static final int INDEX_SPLIT_TRANSLATION = 0;
+ private static final int INDEX_MENU_TRANSLATION = 1;
+ private static final int INDEX_COUNT_TRANSLATION = 2;
+
+ private final MultiPropertyFactory<View> mViewTranslationX;
+ private final MultiPropertyFactory<View> mViewTranslationY;
+
+ /**
+ * Sets the view split x-axis translation
+ * @param translationX x-axis translation
+ */
+ public void setSplitTranslationX(float translationX) {
+ mViewTranslationX.get(INDEX_SPLIT_TRANSLATION).setValue(translationX);
+ }
+
+ /**
+ * Sets the view split y-axis translation
+ * @param translationY y-axis translation
+ */
+ public void setSplitTranslationY(float translationY) {
+ mViewTranslationY.get(INDEX_SPLIT_TRANSLATION).setValue(translationY);
+ }
+
+ /**
+ * Gets the menu x-axis translation for split task
+ */
+ public MultiPropertyFactory<View>.MultiProperty getMenuTranslationX() {
+ return mViewTranslationX.get(INDEX_MENU_TRANSLATION);
+ }
+
+ /**
+ * Translate the View on the X-axis without overriding the raw translation.
+ * This function is used for the menu split animation. It allows external animations to
+ * translate this view without affecting the value of the original translation. Thus,
+ * it is possible to restore the initial translation value.
+ *
+ * @param translationX Animated translation to be aggregated to the raw translation.
+ */
+ public void setMenuTranslationX(float translationX) {
+ mViewTranslationX.get(INDEX_MENU_TRANSLATION).setValue(translationX);
+ }
+
+ /**
+ * Gets the menu y-axis translation for split task
+ */
+ public MultiPropertyFactory<View>.MultiProperty getMenuTranslationY() {
+ return mViewTranslationY.get(INDEX_MENU_TRANSLATION);
+ }
+
+ /**
+ * Translate the View on the Y-axis without overriding the raw translation.
+ * This function is used for the menu split animation. It allows external animations to
+ * translate this view without affecting the value of the original translation. Thus,
+ * it is possible to restore the initial translation value.
+ *
+ * @param translationY Animated translation to be aggregated to the raw translation.
+ */
+ public void setMenuTranslationY(float translationY) {
+ mViewTranslationY.get(INDEX_MENU_TRANSLATION).setValue(translationY);
+ }
+
public IconAppChipView(Context context) {
this(context, null);
}
@@ -111,51 +171,42 @@
mMultiValueAlpha.setUpdateVisibility(/* updateVisibility= */ true);
// Menu dimensions
- mMaxMenuWidth = res.getDimensionPixelSize(R.dimen.task_thumbnail_icon_menu_max_width);
- mIconMenuMarginTop = res.getDimensionPixelSize(R.dimen.task_thumbnail_icon_menu_top_margin);
- mIconMenuMarginStart = res.getDimensionPixelSize(
- R.dimen.task_thumbnail_icon_menu_start_margin);
+ mCollapsedMenuDefaultWidth =
+ res.getDimensionPixelSize(R.dimen.task_thumbnail_icon_menu_collapsed_width);
+ mExpandedMenuDefaultWidth =
+ res.getDimensionPixelSize(R.dimen.task_thumbnail_icon_menu_expanded_width);
+ mCollapsedMenuDefaultHeight =
+ res.getDimensionPixelSize(R.dimen.task_thumbnail_icon_menu_collapsed_height);
+ mExpandedMenuDefaultHeight =
+ res.getDimensionPixelSize(R.dimen.task_thumbnail_icon_menu_expanded_height);
+ mIconMenuMarginTopStart = res.getDimensionPixelSize(
+ R.dimen.task_thumbnail_icon_menu_expanded_top_start_margin);
+ mMenuToChipGap = res.getDimensionPixelSize(
+ R.dimen.task_thumbnail_icon_menu_expanded_gap);
// Background dimensions
- mMinIconBackgroundWidth = res.getDimensionPixelSize(
- R.dimen.task_thumbnail_icon_menu_background_min_width);
- mMaxIconBackgroundHeight = res.getDimensionPixelSize(
- R.dimen.task_thumbnail_icon_menu_max_height);
- mMinIconBackgroundHeight = res.getDimensionPixelSize(
- R.dimen.task_thumbnail_icon_menu_min_height);
- mMaxIconBackgroundCornerRadius = res.getDimensionPixelSize(
- R.dimen.task_thumbnail_icon_menu_corner_radius);
+ mBackgroundMarginTopStart = res.getDimensionPixelSize(
+ R.dimen.task_thumbnail_icon_menu_background_margin_top_start);
- // TextView dimensions
- mInnerMargin = (int) res.getDimension(R.dimen.task_thumbnail_icon_menu_arrow_margin);
- mIconTextMinWidth = res.getDimensionPixelSize(R.dimen.task_thumbnail_icon_menu_text_width);
- mIconTextMaxWidth = res.getDimensionPixelSize(
- R.dimen.task_thumbnail_icon_menu_text_max_width);
-
- // IconView dimensions
+ // Contents dimensions
+ mAppNameHorizontalMargin = res.getDimensionPixelSize(
+ R.dimen.task_thumbnail_icon_menu_app_name_margin_horizontal_collapsed);
+ mArrowMarginEnd = res.getDimensionPixelSize(R.dimen.task_thumbnail_icon_menu_arrow_margin);
mIconViewMarginStart = res.getDimensionPixelSize(
R.dimen.task_thumbnail_icon_view_start_margin);
- mIconViewDrawableSize = res.getDimensionPixelSize(
- R.dimen.task_thumbnail_icon_menu_drawable_size);
- mIconViewDrawableMaxSize = res.getDimensionPixelSize(
- R.dimen.task_thumbnail_icon_menu_drawable_max_size);
- mTextMaxTranslationX =
- (mIconViewDrawableMaxSize - mIconViewDrawableSize - mIconViewMarginStart)
- + (mInnerMargin / 2);
-
- // ArrowView dimensions
- int iconArrowViewWidth = res.getDimensionPixelSize(
+ mAppIconSize = res.getDimensionPixelSize(
+ R.dimen.task_thumbnail_icon_menu_app_icon_collapsed_size);
+ mArrowSize = res.getDimensionPixelSize(
R.dimen.task_thumbnail_icon_menu_arrow_size);
- mMinIconBackgroundCornerRadius = mMinIconBackgroundHeight / 2f;
- float maxCornerSize = Math.min(mMaxIconBackgroundHeight / 2f,
- mMaxIconBackgroundCornerRadius);
- mArrowMaxTranslationX = (mMaxMenuWidth - maxCornerSize) - (Math.min(mMaxWidth,
- mMinIconBackgroundWidth + (2 * mMinIconBackgroundCornerRadius)
- - mMinIconBackgroundCornerRadius)) - mInnerMargin;
+ mIconViewDrawableExpandedSize = res.getDimensionPixelSize(
+ R.dimen.task_thumbnail_icon_menu_app_icon_expanded_size);
- // Menu dimensions
- mMinimumMenuSize =
- mIconViewMarginStart + mIconViewDrawableSize + mInnerMargin + iconArrowViewWidth;
+ mViewTranslationX = new MultiPropertyFactory<>(this, VIEW_TRANSLATE_X,
+ INDEX_COUNT_TRANSLATION,
+ Float::sum);
+ mViewTranslationY = new MultiPropertyFactory<>(this, VIEW_TRANSLATE_Y,
+ INDEX_COUNT_TRANSLATION,
+ Float::sum);
}
@Override
@@ -165,9 +216,7 @@
mIconTextCollapsedView = findViewById(R.id.icon_text_collapsed);
mIconTextExpandedView = findViewById(R.id.icon_text_expanded);
mIconArrowView = findViewById(R.id.icon_arrow);
- mIconViewBackground = findViewById(R.id.icon_view_background);
- mIconViewBackgroundCornersStart = findViewById(R.id.icon_view_background_corners_start);
- mIconViewBackgroundCornersEnd = findViewById(R.id.icon_view_background_corners_end);
+ mMenuAnchorView = findViewById(R.id.icon_view_menu_anchor);
}
protected IconView getIconView() {
@@ -204,88 +253,85 @@
}
/**
- * Sets the maximum width of this Icon Menu.
+ * Sets the maximum width of this Icon Menu. This is usually used when space is limited for
+ * split screen.
*/
public void setMaxWidth(int maxWidth) {
- // Only the app icon and caret are visible at its minimum width.
- mMaxWidth = Math.max(maxWidth, mMinimumMenuSize);
+ // Width showing only the app icon and arrow. Max width should not be set to less than this.
+ int minimumMaxWidth = mIconViewMarginStart + mAppIconSize + mArrowSize + mArrowMarginEnd;
+ mMaxWidth = Math.max(maxWidth, minimumMaxWidth);
}
@Override
public void setIconOrientation(RecentsOrientedState orientationState, boolean isGridTask) {
RecentsPagedOrientationHandler orientationHandler =
orientationState.getOrientationHandler();
- boolean isRtl = isLayoutRtl();
- DeviceProfile deviceProfile =
- ActivityContext.lookupContext(getContext()).getDeviceProfile();
+ // Layout params for anchor view
+ LayoutParams anchorLayoutParams = (LayoutParams) mMenuAnchorView.getLayoutParams();
+ anchorLayoutParams.topMargin = mExpandedMenuDefaultHeight + mMenuToChipGap;
+ mMenuAnchorView.setLayoutParams(anchorLayoutParams);
- // Layout Params for the Menu View
- int thumbnailTopMargin =
- deviceProfile.overviewTaskThumbnailTopMarginPx + mIconMenuMarginTop;
+ // Layout Params for the Menu View (this)
LayoutParams iconMenuParams = (LayoutParams) getLayoutParams();
- orientationHandler.setIconAppChipMenuParams(this, iconMenuParams, mIconMenuMarginStart,
- thumbnailTopMargin);
- iconMenuParams.width = Math.min(mMaxWidth,
- mMinIconBackgroundWidth + (int) (2 * mMinIconBackgroundCornerRadius));
- iconMenuParams.height = mMinIconBackgroundHeight;
+ iconMenuParams.width = mExpandedMenuDefaultWidth;
+ iconMenuParams.height = mExpandedMenuDefaultHeight;
+ orientationHandler.setIconAppChipMenuParams(this, iconMenuParams, mIconMenuMarginTopStart,
+ mIconMenuMarginTopStart);
setLayoutParams(iconMenuParams);
+ // Layout params for the background
+ Rect collapsedBackgroundBounds = getCollapsedBackgroundLtrBounds();
+ mBackgroundRelativeLtrLocation.set(collapsedBackgroundBounds);
+ setOutlineProvider(new ViewOutlineProvider() {
+ final Rect mRtlAppliedOutlineBounds = new Rect();
+ @Override
+ public void getOutline(View view, Outline outline) {
+ mRtlAppliedOutlineBounds.set(mBackgroundRelativeLtrLocation);
+ if (isLayoutRtl()) {
+ int width = getWidth();
+ mRtlAppliedOutlineBounds.left = width - mBackgroundRelativeLtrLocation.right;
+ mRtlAppliedOutlineBounds.right = width - mBackgroundRelativeLtrLocation.left;
+ }
+ outline.setRoundRect(
+ mRtlAppliedOutlineBounds, mRtlAppliedOutlineBounds.height() / 2f);
+ }
+ });
+
// Layout Params for the Icon View
LayoutParams iconParams = (LayoutParams) mIconView.getLayoutParams();
- orientationHandler.setTaskIconParams(iconParams, mIconViewMarginStart,
- mIconViewDrawableSize, thumbnailTopMargin, isRtl);
- iconParams.width = iconParams.height = mIconViewDrawableSize;
+ int iconMarginStartRelativeToParent = mIconViewMarginStart + mBackgroundMarginTopStart;
+ orientationHandler.setIconAppChipChildrenParams(
+ iconParams, iconMarginStartRelativeToParent);
+
mIconView.setLayoutParams(iconParams);
- mIconView.setDrawableSize(mIconViewDrawableSize, mIconViewDrawableSize);
+ mIconView.setDrawableSize(mAppIconSize, mAppIconSize);
// Layout Params for the collapsed Icon Text View
+ int textMarginStart =
+ iconMarginStartRelativeToParent + mAppIconSize + mAppNameHorizontalMargin;
LayoutParams iconTextCollapsedParams =
(LayoutParams) mIconTextCollapsedView.getLayoutParams();
- orientationHandler.setTaskIconParams(iconTextCollapsedParams, 0, mIconViewDrawableSize,
- thumbnailTopMargin, isRtl);
- iconTextCollapsedParams.setMarginStart(
- mIconViewDrawableSize + mIconViewMarginStart + mInnerMargin);
- iconTextCollapsedParams.topMargin = (mMinIconBackgroundHeight - mIconViewDrawableSize) / 2;
- iconTextCollapsedParams.gravity = Gravity.TOP | Gravity.START;
- iconTextCollapsedParams.width = Math.min(
- Math.max(mMaxWidth - mMinimumMenuSize - (2 * mInnerMargin), 0), mIconTextMinWidth);
+ orientationHandler.setIconAppChipChildrenParams(iconTextCollapsedParams, textMarginStart);
+ int collapsedTextWidth = collapsedBackgroundBounds.width() - mIconViewMarginStart
+ - mAppIconSize - mArrowSize - mAppNameHorizontalMargin - mArrowMarginEnd;
+ iconTextCollapsedParams.width = collapsedTextWidth;
mIconTextCollapsedView.setLayoutParams(iconTextCollapsedParams);
mIconTextCollapsedView.setAlpha(1f);
// Layout Params for the expanded Icon Text View
LayoutParams iconTextExpandedParams =
(LayoutParams) mIconTextExpandedView.getLayoutParams();
- orientationHandler.setTaskIconParams(iconTextExpandedParams, 0, mIconViewDrawableSize,
- thumbnailTopMargin, isRtl);
- iconTextExpandedParams.setMarginStart(
- mIconViewDrawableSize + mIconViewMarginStart + mInnerMargin);
- iconTextExpandedParams.topMargin = (mMinIconBackgroundHeight - mIconViewDrawableSize) / 2;
- iconTextExpandedParams.gravity = Gravity.TOP | Gravity.START;
+ orientationHandler.setIconAppChipChildrenParams(iconTextExpandedParams, textMarginStart);
mIconTextExpandedView.setLayoutParams(iconTextExpandedParams);
mIconTextExpandedView.setAlpha(0f);
- mIconTextExpandedView.setRevealClip(true, 0, mIconViewDrawableSize / 2f, mIconTextMinWidth);
+ mIconTextExpandedView.setRevealClip(true, 0, mAppIconSize / 2f, collapsedTextWidth);
// Layout Params for the Icon Arrow View
LayoutParams iconArrowParams = (LayoutParams) mIconArrowView.getLayoutParams();
- iconArrowParams.gravity = Gravity.CENTER_VERTICAL | Gravity.END;
- iconArrowParams.setMarginEnd(mInnerMargin);
+ int arrowMarginStart = collapsedBackgroundBounds.right - mArrowMarginEnd - mArrowSize;
+ orientationHandler.setIconAppChipChildrenParams(iconArrowParams, arrowMarginStart);
mIconArrowView.setLayoutParams(iconArrowParams);
- // Layout Params for the Icon View Background and its corners
- int cornerlessBackgroundWidth = (int) Math.min(
- mMaxWidth - (2 * mMinIconBackgroundCornerRadius), mMinIconBackgroundWidth);
- LayoutParams backgroundCornerEndParams =
- (LayoutParams) mIconViewBackgroundCornersEnd.getLayoutParams();
- backgroundCornerEndParams.setMarginStart(cornerlessBackgroundWidth);
- mIconViewBackgroundCornersEnd.setLayoutParams(backgroundCornerEndParams);
- LayoutParams backgroundParams = (LayoutParams) mIconViewBackground.getLayoutParams();
- backgroundParams.width = cornerlessBackgroundWidth;
- backgroundParams.height = mMinIconBackgroundHeight;
- backgroundParams.setMarginStart((int) mMinIconBackgroundCornerRadius);
- mIconViewBackground.setLayoutParams(backgroundParams);
- mIconViewBackground.setPivotX(isRtl ? cornerlessBackgroundWidth : 0);
- mIconViewBackground.setPivotY(mMinIconBackgroundCornerRadius);
-
// This method is called twice sometimes (like when rotating split tasks). It is called
// once before onMeasure and onLayout, and again after onMeasure but before onLayout with
// a new width. This happens because we update widths on rotation and on measure of
@@ -324,108 +370,96 @@
protected void revealAnim(boolean isRevealing) {
cancelInProgressAnimations();
+ final Rect collapsedBackgroundBounds = getCollapsedBackgroundLtrBounds();
+ final Rect expandedBackgroundBounds = getExpandedBackgroundLtrBounds();
+ final Rect initialBackground = new Rect(mBackgroundRelativeLtrLocation);
+ mAnimator = new AnimatorSet();
if (isRevealing) {
boolean isRtl = isLayoutRtl();
bringToFront();
((AnimatedVectorDrawable) mIconArrowView.getDrawable()).start();
- mAnimator = new AnimatorSet();
- float backgroundScaleY = mMaxIconBackgroundHeight / (float) mMinIconBackgroundHeight;
- float maxCornerSize = Math.min(mMaxIconBackgroundHeight / 2f,
- mMaxIconBackgroundCornerRadius);
- float backgroundScaleX = (mMaxMenuWidth - (2 * maxCornerSize)) / Math.min(
- mMaxWidth - (2 * mMinIconBackgroundCornerRadius), mMinIconBackgroundWidth);
- float arrowTranslationX = mArrowMaxTranslationX + (mMinIconBackgroundWidth - Math.min(
- mMaxWidth - (2 * mMinIconBackgroundCornerRadius), mMinIconBackgroundWidth));
// Clip expanded text with reveal animation so it doesn't go beyond the edge of the menu
Animator expandedTextRevealAnim = ViewAnimationUtils.createCircularReveal(
- mIconTextExpandedView, 0, mIconTextExpandedView.getHeight() / 2, 0,
- mIconTextMaxWidth + maxCornerSize);
- expandedTextRevealAnim.addListener(new AnimatorListenerAdapter() {
- @Override
- public void onAnimationEnd(Animator animation) {
- // createCircularReveal removes clip on finish, restore it here to clip text.
- mIconTextExpandedView.setRevealClip(true, 0,
- mIconTextExpandedView.getHeight() / 2f,
- mIconTextMaxWidth + maxCornerSize);
- }
- });
+ mIconTextExpandedView, 0, mIconTextExpandedView.getHeight() / 2,
+ mIconTextCollapsedView.getWidth(), mIconTextExpandedView.getWidth());
+ // Animate background clipping
+ ValueAnimator backgroundAnimator = ValueAnimator.ofObject(
+ mBackgroundAnimationRectEvaluator,
+ initialBackground,
+ expandedBackgroundBounds);
+ backgroundAnimator.addUpdateListener(valueAnimator -> invalidateOutline());
+
+ float iconViewScaling = mIconViewDrawableExpandedSize / (float) mAppIconSize;
+ float arrowTranslationX =
+ expandedBackgroundBounds.right - collapsedBackgroundBounds.right;
+ float iconCenterToTextCollapsed = mAppIconSize / 2f + mAppNameHorizontalMargin;
+ float iconCenterToTextExpanded =
+ mIconViewDrawableExpandedSize / 2f + mAppNameHorizontalMargin;
+ float textTranslationX = iconCenterToTextExpanded - iconCenterToTextCollapsed;
+
+ float textTranslationXWithRtl = isRtl ? -textTranslationX : textTranslationX;
+ float arrowTranslationWithRtl = isRtl ? -arrowTranslationX : arrowTranslationX;
+
mAnimator.playTogether(
expandedTextRevealAnim,
- ObjectAnimator.ofFloat(mIconViewBackgroundCornersStart, SCALE_Y,
- backgroundScaleY),
- ObjectAnimator.ofFloat(mIconViewBackgroundCornersStart, SCALE_X,
- backgroundScaleY),
- ObjectAnimator.ofFloat(mIconViewBackgroundCornersEnd, SCALE_Y,
- backgroundScaleY),
- ObjectAnimator.ofFloat(mIconViewBackgroundCornersEnd, SCALE_X,
- backgroundScaleY),
- ObjectAnimator.ofFloat(mIconViewBackgroundCornersEnd, TRANSLATION_X,
- isRtl ? -arrowTranslationX : arrowTranslationX),
- ObjectAnimator.ofFloat(mIconViewBackground, SCALE_X, backgroundScaleX),
- ObjectAnimator.ofFloat(mIconViewBackground, SCALE_Y, backgroundScaleY),
- ObjectAnimator.ofFloat(mIconView, SCALE_X,
- mIconViewDrawableMaxSize / (float) mIconViewDrawableSize),
- ObjectAnimator.ofFloat(mIconView, SCALE_Y,
- mIconViewDrawableMaxSize / (float) mIconViewDrawableSize),
+ backgroundAnimator,
+ ObjectAnimator.ofFloat(mIconView, SCALE_X, iconViewScaling),
+ ObjectAnimator.ofFloat(mIconView, SCALE_Y, iconViewScaling),
ObjectAnimator.ofFloat(mIconTextCollapsedView, TRANSLATION_X,
- isLayoutRtl() ? -mTextMaxTranslationX : mTextMaxTranslationX),
+ textTranslationXWithRtl),
ObjectAnimator.ofFloat(mIconTextExpandedView, TRANSLATION_X,
- isLayoutRtl() ? -mTextMaxTranslationX : mTextMaxTranslationX),
+ textTranslationXWithRtl),
ObjectAnimator.ofFloat(mIconTextCollapsedView, ALPHA, 0),
ObjectAnimator.ofFloat(mIconTextExpandedView, ALPHA, 1),
- ObjectAnimator.ofFloat(mIconArrowView, TRANSLATION_X,
- isRtl ? -arrowTranslationX : arrowTranslationX));
+ ObjectAnimator.ofFloat(mIconArrowView, TRANSLATION_X, arrowTranslationWithRtl));
mAnimator.setDuration(MENU_BACKGROUND_REVEAL_DURATION);
- mAnimator.setInterpolator(EMPHASIZED);
- mAnimator.start();
} else {
((AnimatedVectorDrawable) mIconArrowView.getDrawable()).reverse();
- float maxCornerSize = Math.min(mMaxIconBackgroundHeight / 2f,
- mMaxIconBackgroundCornerRadius);
// Clip expanded text with reveal animation so it doesn't go beyond the edge of the menu
Animator expandedTextClipAnim = ViewAnimationUtils.createCircularReveal(
mIconTextExpandedView, 0, mIconTextExpandedView.getHeight() / 2,
- mIconTextMaxWidth + maxCornerSize, 0);
- expandedTextClipAnim.addListener(new AnimatorListenerAdapter() {
- @Override
- public void onAnimationEnd(Animator animation) {
- // createCircularReveal removes clip on finish, restore it here to clip text.
- mIconTextExpandedView.setRevealClip(true, 0,
- mIconTextExpandedView.getHeight() / 2f, 0);
- }
- });
- mAnimator = new AnimatorSet();
+ mIconTextExpandedView.getWidth(), mIconTextCollapsedView.getWidth());
+
+ // Animate background clipping
+ ValueAnimator backgroundAnimator = ValueAnimator.ofObject(
+ mBackgroundAnimationRectEvaluator,
+ initialBackground,
+ collapsedBackgroundBounds);
+ backgroundAnimator.addUpdateListener(valueAnimator -> invalidateOutline());
+
mAnimator.playTogether(
expandedTextClipAnim,
- ObjectAnimator.ofFloat(mIconViewBackgroundCornersStart, SCALE_X, 1),
- ObjectAnimator.ofFloat(mIconViewBackgroundCornersStart, SCALE_Y, 1),
- ObjectAnimator.ofFloat(mIconViewBackgroundCornersEnd, SCALE_X, 1),
- ObjectAnimator.ofFloat(mIconViewBackgroundCornersEnd, SCALE_Y, 1),
- ObjectAnimator.ofFloat(mIconViewBackgroundCornersEnd, TRANSLATION_X, 0),
- ObjectAnimator.ofFloat(mIconViewBackground, SCALE_X, 1),
- ObjectAnimator.ofFloat(mIconViewBackground, SCALE_Y, 1),
- ObjectAnimator.ofFloat(mIconView, SCALE_X, 1),
- ObjectAnimator.ofFloat(mIconView, SCALE_Y, 1),
+ backgroundAnimator,
+ ObjectAnimator.ofFloat(mIconView, SCALE_PROPERTY, 1),
ObjectAnimator.ofFloat(mIconTextCollapsedView, TRANSLATION_X, 0),
ObjectAnimator.ofFloat(mIconTextExpandedView, TRANSLATION_X, 0),
ObjectAnimator.ofFloat(mIconTextCollapsedView, ALPHA, 1),
ObjectAnimator.ofFloat(mIconTextExpandedView, ALPHA, 0),
ObjectAnimator.ofFloat(mIconArrowView, TRANSLATION_X, 0));
mAnimator.setDuration(MENU_BACKGROUND_HIDE_DURATION);
- mAnimator.setInterpolator(EMPHASIZED);
- mAnimator.start();
}
+
+ mAnimator.setInterpolator(EMPHASIZED);
+ mAnimator.start();
+ }
+
+ private Rect getCollapsedBackgroundLtrBounds() {
+ Rect bounds = new Rect(
+ 0,
+ 0,
+ Math.min(mMaxWidth, mCollapsedMenuDefaultWidth),
+ mCollapsedMenuDefaultHeight);
+ bounds.offset(mBackgroundMarginTopStart, mBackgroundMarginTopStart);
+ return bounds;
+ }
+
+ private Rect getExpandedBackgroundLtrBounds() {
+ return new Rect(0, 0, mExpandedMenuDefaultWidth, mExpandedMenuDefaultHeight);
}
protected void reset() {
- mIconViewBackgroundCornersStart.setScaleX(1);
- mIconViewBackgroundCornersStart.setScaleY(1);
- mIconViewBackgroundCornersEnd.setScaleX(1);
- mIconViewBackgroundCornersEnd.setScaleY(1);
- mIconViewBackgroundCornersEnd.setTranslationX(0);
- mIconViewBackground.setScaleX(1);
- mIconViewBackground.setScaleY(1);
+ mBackgroundRelativeLtrLocation.set(getCollapsedBackgroundLtrBounds());
mIconView.setScaleX(1);
mIconView.setScaleY(1);
mIconTextCollapsedView.setTranslationX(0);
diff --git a/quickstep/src/com/android/quickstep/views/IconView.java b/quickstep/src/com/android/quickstep/views/IconView.java
index 4d33fda..1312ec3 100644
--- a/quickstep/src/com/android/quickstep/views/IconView.java
+++ b/quickstep/src/com/android/quickstep/views/IconView.java
@@ -15,8 +15,6 @@
*/
package com.android.quickstep.views;
-import static com.android.launcher3.Flags.enableOverviewIconMenu;
-
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Rect;
@@ -196,10 +194,8 @@
setLayoutParams(iconParams);
setRotation(orientationHandler.getDegreesRotated());
- int iconDrawableSize = enableOverviewIconMenu()
- ? deviceProfile.overviewTaskIconAppChipMenuDrawableSizePx
- : isGridTask ? deviceProfile.overviewTaskIconDrawableSizeGridPx
- : deviceProfile.overviewTaskIconDrawableSizePx;
+ int iconDrawableSize = isGridTask ? deviceProfile.overviewTaskIconDrawableSizeGridPx
+ : deviceProfile.overviewTaskIconDrawableSizePx;
setDrawableSize(iconDrawableSize, iconDrawableSize);
}
diff --git a/quickstep/src/com/android/quickstep/views/RecentsView.java b/quickstep/src/com/android/quickstep/views/RecentsView.java
index 9884d8d..f6afaf0 100644
--- a/quickstep/src/com/android/quickstep/views/RecentsView.java
+++ b/quickstep/src/com/android/quickstep/views/RecentsView.java
@@ -459,6 +459,7 @@
@Nullable
protected RemoteTargetHandle[] mRemoteTargetHandles;
+ protected final Rect mLastComputedCarouselTaskSize = new Rect();
protected final Rect mLastComputedTaskSize = new Rect();
protected final Rect mLastComputedGridSize = new Rect();
protected final Rect mLastComputedGridTaskSize = new Rect();
@@ -2108,6 +2109,10 @@
mSizeStrategy.calculateDesktopTaskSize(mActivity, mActivity.getDeviceProfile(),
mLastComputedDesktopTaskSize);
}
+ if (enableGridOnlyOverview()) {
+ mSizeStrategy.calculateCarouselTaskSize(mActivity, dp, mLastComputedCarouselTaskSize,
+ getPagedOrientationHandler());
+ }
mTaskGridVerticalDiff = mLastComputedGridTaskSize.top - mLastComputedTaskSize.top;
mTopBottomRowHeightDiff =
@@ -2137,9 +2142,12 @@
}
float accumulatedTranslationX = 0;
- float translateXToMiddle = enableGridOnlyOverview() && mActivity.getDeviceProfile().isTablet
- ? mActivity.getDeviceProfile().widthPx / 2 - mLastComputedGridTaskSize.centerX()
- : 0;
+ float translateXToMiddle = 0;
+ if (enableGridOnlyOverview() && mActivity.getDeviceProfile().isTablet) {
+ translateXToMiddle = mIsRtl
+ ? mLastComputedCarouselTaskSize.right - mLastComputedTaskSize.right
+ : mLastComputedCarouselTaskSize.left - mLastComputedTaskSize.left;
+ }
for (int i = 0; i < taskCount; i++) {
TaskView taskView = requireTaskViewAt(i);
taskView.updateTaskSize();
@@ -2206,6 +2214,10 @@
return mLastComputedDesktopTaskSize;
}
+ public Rect getLastComputedCarouselTaskSize() {
+ return mLastComputedCarouselTaskSize;
+ }
+
/** Gets the task size for modal state. */
public void getModalTaskSize(Rect outRect) {
mSizeStrategy.calculateModalTaskSize(mActivity, mActivity.getDeviceProfile(), outRect,
@@ -2675,6 +2687,9 @@
tvs.taskSecondaryTranslation.value = runningTaskSecondaryGridTranslation;
} else {
animatorSet.play(ObjectAnimator.ofFloat(this, RECENTS_GRID_PROGRESS, 1));
+ animatorSet.play(tvs.carouselScale.animateToValue(1));
+ animatorSet.play(tvs.carouselPrimaryTranslation.animateToValue(0));
+ animatorSet.play(tvs.carouselSecondaryTranslation.animateToValue(0));
animatorSet.play(tvs.taskPrimaryTranslation.animateToValue(
runningTaskPrimaryGridTranslation));
animatorSet.play(tvs.taskSecondaryTranslation.animateToValue(
@@ -4411,12 +4426,13 @@
mTempPointF.set(mLastComputedTaskSize.centerX(), mLastComputedTaskSize.bottom);
}
} else {
- mTempRect.set(mLastComputedTaskSize);
// Only update pivot when it is tablet and not in grid yet, so the pivot is correct
// for non-current tasks when swiping up to overview
if (enableGridOnlyOverview() && mActivity.getDeviceProfile().isTablet
&& !mOverviewGridEnabled) {
- mTempRect.offset(mActivity.getDeviceProfile().widthPx / 2 - mTempRect.centerX(), 0);
+ mTempRect.set(mLastComputedCarouselTaskSize);
+ } else {
+ mTempRect.set(mLastComputedTaskSize);
}
getPagedViewOrientedState().getFullScreenScaleAndPivot(mTempRect,
mActivity.getDeviceProfile(), mTempPointF);
@@ -5117,7 +5133,12 @@
* Returns the scale up required on the view, so that it coves the screen completely
*/
public float getMaxScaleForFullScreen() {
- getTaskSize(mTempRect);
+ if (enableGridOnlyOverview() && mActivity.getDeviceProfile().isTablet
+ && !mOverviewGridEnabled) {
+ mTempRect.set(mLastComputedCarouselTaskSize);
+ } else {
+ mTempRect.set(mLastComputedTaskSize);
+ }
return getPagedViewOrientedState().getFullScreenScaleAndPivot(
mTempRect, mActivity.getDeviceProfile(), mTempPointF);
}
@@ -5545,7 +5566,7 @@
for (int i = 0; i < taskCount; i++) {
TaskView taskView = requireTaskViewAt(i);
float scrollDiff = taskView.getScrollAdjustment(showAsGrid);
- int pageScroll = newPageScrolls[i] + (int) scrollDiff;
+ int pageScroll = newPageScrolls[i] + Math.round(scrollDiff);
if ((mIsRtl && pageScroll < lastTaskScroll)
|| (!mIsRtl && pageScroll > lastTaskScroll)) {
pageScroll = lastTaskScroll;
diff --git a/quickstep/src/com/android/quickstep/views/TaskMenuView.java b/quickstep/src/com/android/quickstep/views/TaskMenuView.java
index cf50835..39f1e46 100644
--- a/quickstep/src/com/android/quickstep/views/TaskMenuView.java
+++ b/quickstep/src/com/android/quickstep/views/TaskMenuView.java
@@ -18,6 +18,7 @@
import static com.android.app.animation.Interpolators.EMPHASIZED;
import static com.android.launcher3.Flags.enableOverviewIconMenu;
+import static com.android.launcher3.util.MultiPropertyFactory.MULTI_PROPERTY_VALUE;
import static com.android.launcher3.util.SplitConfigurationOptions.STAGE_POSITION_BOTTOM_OR_RIGHT;
import static com.android.quickstep.views.TaskThumbnailView.DIM_ALPHA;
@@ -78,8 +79,6 @@
private LinearLayout mOptionLayout;
private float mMenuTranslationYBeforeOpen;
private float mMenuTranslationXBeforeOpen;
- private float mIconViewTranslationYBeforeOpen;
- private float mIconViewTranslationXBeforeOpen;
public TaskMenuView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
@@ -225,8 +224,10 @@
// Get Position
DeviceProfile deviceProfile = mActivity.getDeviceProfile();
mActivity.getDragLayer().getDescendantRectRelativeToSelf(
- enableOverviewIconMenu() ? taskContainer.getIconView().asView()
- : taskContainer.getThumbnailView(), sTempRect);
+ enableOverviewIconMenu()
+ ? getIconView().findViewById(R.id.icon_view_menu_anchor)
+ : taskContainer.getThumbnailView(),
+ sTempRect);
Rect insets = mActivity.getDragLayer().getInsets();
BaseDragLayer.LayoutParams params = (BaseDragLayer.LayoutParams) getLayoutParams();
params.width = orientationHandler.getTaskMenuWidth(taskContainer.getThumbnailView(),
@@ -246,15 +247,9 @@
orientationHandler.setTaskOptionsMenuLayoutOrientation(
deviceProfile, mOptionLayout, dividerSpacing, divider);
- float thumbnailAlignedX = sTempRect.left - insets.left + (enableOverviewIconMenu()
- ? -getResources().getDimensionPixelSize(
- R.dimen.task_thumbnail_icon_menu_touch_max_margin)
- - getResources().getDimension(R.dimen.task_thumbnail_icon_menu_start_margin)
- : 0);
- float thumbnailAlignedY = sTempRect.top - insets.top + (enableOverviewIconMenu()
- ? getResources().getDimensionPixelSize(R.dimen.task_thumbnail_icon_menu_max_height)
- - getResources().getDimensionPixelSize(
- R.dimen.task_thumbnail_icon_menu_top_margin) : 0);
+ float thumbnailAlignedX = sTempRect.left - insets.left;
+ float thumbnailAlignedY = sTempRect.top - insets.top;
+
// Changing pivot to make computations easier
// NOTE: Changing the pivots means the rotated view gets rotated about the new pivots set,
// which would render the X and Y position set here incorrect
@@ -262,31 +257,33 @@
setPivotY(0);
setRotation(orientationHandler.getDegreesRotated());
- // Margin that insets the menuView inside the taskView
- float taskInsetMarginX = enableOverviewIconMenu() ? getResources().getDimension(
- R.dimen.task_thumbnail_icon_menu_start_margin) : getResources().getDimension(
- R.dimen.task_card_margin);
- float taskInsetMarginY = enableOverviewIconMenu() ? getResources().getDimension(
- R.dimen.task_thumbnail_icon_menu_start_margin) : getResources().getDimension(
- R.dimen.task_card_margin);
- setTranslationX(orientationHandler.getTaskMenuX(thumbnailAlignedX,
- mTaskContainer.getThumbnailView(), deviceProfile, taskInsetMarginX,
- mTaskContainer.getIconView().asView()));
- setTranslationY(orientationHandler.getTaskMenuY(
- thumbnailAlignedY, mTaskContainer.getThumbnailView(),
- mTaskContainer.getStagePosition(), this, taskInsetMarginY,
- mTaskContainer.getIconView().asView()));
+ if (enableOverviewIconMenu()) {
+ setTranslationX(thumbnailAlignedX);
+ setTranslationY(thumbnailAlignedY);
+ } else {
+ // Margin that insets the menuView inside the taskView
+ float taskInsetMargin = getResources().getDimension(R.dimen.task_card_margin);
+ setTranslationX(orientationHandler.getTaskMenuX(thumbnailAlignedX,
+ mTaskContainer.getThumbnailView(), deviceProfile, taskInsetMargin,
+ getIconView()));
+ setTranslationY(orientationHandler.getTaskMenuY(
+ thumbnailAlignedY, mTaskContainer.getThumbnailView(),
+ mTaskContainer.getStagePosition(), this, taskInsetMargin,
+ getIconView()));
+ }
}
private void animateOpen() {
mMenuTranslationYBeforeOpen = getTranslationY();
mMenuTranslationXBeforeOpen = getTranslationX();
- mIconViewTranslationYBeforeOpen = mTaskContainer.getIconView().asView().getTranslationY();
- mIconViewTranslationXBeforeOpen = mTaskContainer.getIconView().asView().getTranslationX();
animateOpenOrClosed(false);
mIsOpen = true;
}
+ private View getIconView() {
+ return mTaskContainer.getIconView().asView();
+ }
+
private void animateClose() {
animateOpenOrClosed(true);
}
@@ -318,19 +315,15 @@
float midpoint = (taskBottom + taskbarTop) / 2f;
additionalTranslationY = -Math.max(menuBottom - midpoint, 0);
}
- // Translate the menu to account for the expansion of the app chip menu as well.
- float expandOffsetTranslationY = getResources().getDimensionPixelSize(
- R.dimen.task_thumbnail_icon_menu_expanded_gap);
ObjectAnimator translationYAnim = ObjectAnimator.ofFloat(this, TRANSLATION_Y,
closing ? mMenuTranslationYBeforeOpen
- : mMenuTranslationYBeforeOpen + additionalTranslationY
- + expandOffsetTranslationY);
+ : mMenuTranslationYBeforeOpen + additionalTranslationY);
translationYAnim.setInterpolator(EMPHASIZED);
+ IconAppChipView iconAppChip = (IconAppChipView) mTaskContainer.getIconView().asView();
ObjectAnimator menuTranslationYAnim = ObjectAnimator.ofFloat(
- mTaskContainer.getIconView().asView(), TRANSLATION_Y,
- closing ? mIconViewTranslationYBeforeOpen
- : mIconViewTranslationYBeforeOpen + additionalTranslationY);
+ iconAppChip.getMenuTranslationY(),
+ MULTI_PROPERTY_VALUE, closing ? 0 : additionalTranslationY);
menuTranslationYAnim.setInterpolator(EMPHASIZED);
mOpenCloseAnimator.playTogether(translationYAnim, menuTranslationYAnim);
@@ -348,10 +341,10 @@
: mMenuTranslationXBeforeOpen - additionalTranslationX);
translationXAnim.setInterpolator(EMPHASIZED);
+ IconAppChipView iconAppChip = (IconAppChipView) mTaskContainer.getIconView().asView();
ObjectAnimator menuTranslationXAnim = ObjectAnimator.ofFloat(
- mTaskContainer.getIconView().asView(), TRANSLATION_X,
- closing ? mIconViewTranslationXBeforeOpen
- : mIconViewTranslationXBeforeOpen - additionalTranslationX);
+ iconAppChip.getMenuTranslationX(),
+ MULTI_PROPERTY_VALUE, closing ? 0 : -additionalTranslationX);
menuTranslationXAnim.setInterpolator(EMPHASIZED);
mOpenCloseAnimator.playTogether(translationXAnim, menuTranslationXAnim);
@@ -391,9 +384,11 @@
private void resetOverviewIconMenu() {
if (enableOverviewIconMenu()) {
- ((IconAppChipView) mTaskContainer.getIconView()).reset();
+ IconAppChipView iconAppChipView = (IconAppChipView) mTaskContainer.getIconView();
+ iconAppChipView.reset();
+ iconAppChipView.setMenuTranslationX(0);
+ iconAppChipView.setMenuTranslationY(0);
setTranslationY(mMenuTranslationYBeforeOpen);
- mTaskContainer.getIconView().asView().setTranslationY(mIconViewTranslationYBeforeOpen);
}
}
diff --git a/quickstep/src/com/android/quickstep/views/TaskView.java b/quickstep/src/com/android/quickstep/views/TaskView.java
index 5057c38..55da160 100644
--- a/quickstep/src/com/android/quickstep/views/TaskView.java
+++ b/quickstep/src/com/android/quickstep/views/TaskView.java
@@ -23,6 +23,7 @@
import static com.android.app.animation.Interpolators.FAST_OUT_SLOW_IN;
import static com.android.app.animation.Interpolators.LINEAR;
import static com.android.launcher3.Flags.enableCursorHoverStates;
+import static com.android.launcher3.Flags.enableGridOnlyOverview;
import static com.android.launcher3.Flags.enableOverviewIconMenu;
import static com.android.launcher3.LauncherState.BACKGROUND_APP;
import static com.android.launcher3.Utilities.getDescendantCoordRelativeToAncestor;
@@ -1750,7 +1751,13 @@
expectedHeight = boxHeight + thumbnailPadding;
// Scale to to fit task Rect.
- nonGridScale = taskWidth / (float) boxWidth;
+ if (enableGridOnlyOverview()) {
+ final Rect lastComputedCarouselTaskSize =
+ getRecentsView().getLastComputedCarouselTaskSize();
+ nonGridScale = lastComputedCarouselTaskSize.width() / (float) taskWidth;
+ } else {
+ nonGridScale = taskWidth / (float) boxWidth;
+ }
// Align to top of task Rect.
boxTranslationY = (expectedHeight - thumbnailPadding - taskHeight) / 2.0f;
diff --git a/quickstep/tests/src/com/android/quickstep/RecentsAnimationDeviceStateTest.kt b/quickstep/tests/src/com/android/quickstep/RecentsAnimationDeviceStateTest.kt
index 53bc2a2..2916952 100644
--- a/quickstep/tests/src/com/android/quickstep/RecentsAnimationDeviceStateTest.kt
+++ b/quickstep/tests/src/com/android/quickstep/RecentsAnimationDeviceStateTest.kt
@@ -2,17 +2,15 @@
import android.content.Context
import android.testing.AndroidTestingRunner
-import android.view.Display
-import android.view.IWindowManager
import androidx.test.core.app.ApplicationProvider
import androidx.test.filters.SmallTest
import com.android.launcher3.util.DisplayController.CHANGE_DENSITY
import com.android.launcher3.util.DisplayController.CHANGE_NAVIGATION_MODE
import com.android.launcher3.util.DisplayController.CHANGE_ROTATION
import com.android.launcher3.util.DisplayController.Info
-import com.android.launcher3.util.Executors
import com.android.launcher3.util.NavigationMode
import com.android.launcher3.util.window.WindowManagerProxy
+import com.android.quickstep.util.GestureExclusionManager
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
@@ -28,7 +26,7 @@
@RunWith(AndroidTestingRunner::class)
class RecentsAnimationDeviceStateTest {
- @Mock private lateinit var windowManager: IWindowManager
+ @Mock private lateinit var exclusionManager: GestureExclusionManager
@Mock private lateinit var windowManagerProxy: WindowManagerProxy
@Mock private lateinit var info: Info
@@ -38,60 +36,45 @@
@Before
fun setup() {
MockitoAnnotations.initMocks(this)
- underTest = RecentsAnimationDeviceState(context, windowManager)
+ underTest = RecentsAnimationDeviceState(context, exclusionManager)
}
@Test
fun registerExclusionListener_success() {
underTest.registerExclusionListener()
- awaitTasksCompleted()
- verify(windowManager)
- .registerSystemGestureExclusionListener(
- underTest.mGestureExclusionListener,
- Display.DEFAULT_DISPLAY
- )
+ verify(exclusionManager).addListener(underTest)
}
@Test
fun registerExclusionListener_again_fail() {
underTest.registerExclusionListener()
- awaitTasksCompleted()
- reset(windowManager)
+ reset(exclusionManager)
underTest.registerExclusionListener()
- awaitTasksCompleted()
- verifyZeroInteractions(windowManager)
+ verifyZeroInteractions(exclusionManager)
}
@Test
fun unregisterExclusionListener_success() {
underTest.registerExclusionListener()
- awaitTasksCompleted()
- reset(windowManager)
+ reset(exclusionManager)
underTest.unregisterExclusionListener()
- awaitTasksCompleted()
- verify(windowManager)
- .unregisterSystemGestureExclusionListener(
- underTest.mGestureExclusionListener,
- Display.DEFAULT_DISPLAY
- )
+ verify(exclusionManager).removeListener(underTest)
}
@Test
fun unregisterExclusionListener_again_fail() {
underTest.registerExclusionListener()
underTest.unregisterExclusionListener()
- awaitTasksCompleted()
- reset(windowManager)
+ reset(exclusionManager)
underTest.unregisterExclusionListener()
- awaitTasksCompleted()
- verifyZeroInteractions(windowManager)
+ verifyZeroInteractions(exclusionManager)
}
@Test
@@ -100,45 +83,28 @@
underTest.onDisplayInfoChanged(context, info, CHANGE_ROTATION or CHANGE_NAVIGATION_MODE)
- awaitTasksCompleted()
- verify(windowManager)
- .registerSystemGestureExclusionListener(
- underTest.mGestureExclusionListener,
- Display.DEFAULT_DISPLAY
- )
+ verify(exclusionManager).addListener(underTest)
}
@Test
fun onDisplayInfoChanged_twoButton_unregisterExclusionListener() {
underTest.registerExclusionListener()
- awaitTasksCompleted()
whenever(info.getNavigationMode()).thenReturn(NavigationMode.TWO_BUTTONS)
- reset(windowManager)
+ reset(exclusionManager)
underTest.onDisplayInfoChanged(context, info, CHANGE_ROTATION or CHANGE_NAVIGATION_MODE)
- awaitTasksCompleted()
- verify(windowManager)
- .unregisterSystemGestureExclusionListener(
- underTest.mGestureExclusionListener,
- Display.DEFAULT_DISPLAY
- )
+ verify(exclusionManager).removeListener(underTest)
}
@Test
fun onDisplayInfoChanged_changeDensity_noOp() {
underTest.registerExclusionListener()
- awaitTasksCompleted()
whenever(info.getNavigationMode()).thenReturn(NavigationMode.NO_BUTTON)
- reset(windowManager)
+ reset(exclusionManager)
underTest.onDisplayInfoChanged(context, info, CHANGE_DENSITY)
- awaitTasksCompleted()
- verifyZeroInteractions(windowManager)
- }
-
- private fun awaitTasksCompleted() {
- Executors.UI_HELPER_EXECUTOR.submit<Any> { null }.get()
+ verifyZeroInteractions(exclusionManager)
}
}
diff --git a/quickstep/tests/src/com/android/quickstep/util/AppPairsControllerTest.kt b/quickstep/tests/src/com/android/quickstep/util/AppPairsControllerTest.kt
index 1723844..510faf6 100644
--- a/quickstep/tests/src/com/android/quickstep/util/AppPairsControllerTest.kt
+++ b/quickstep/tests/src/com/android/quickstep/util/AppPairsControllerTest.kt
@@ -18,18 +18,37 @@
import android.content.Context
import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.android.launcher3.apppairs.AppPairIcon
import com.android.launcher3.logging.StatsLogManager
+import com.android.launcher3.model.data.ItemInfo
+import com.android.launcher3.taskbar.TaskbarActivityContext
import com.android.launcher3.util.SplitConfigurationOptions.STAGE_POSITION_BOTTOM_OR_RIGHT
import com.android.launcher3.util.SplitConfigurationOptions.STAGE_POSITION_TOP_OR_LEFT
+import com.android.quickstep.TopTaskTracker
+import com.android.quickstep.TopTaskTracker.CachedTaskInfo
+import com.android.systemui.shared.recents.model.Task
+import com.android.systemui.shared.recents.model.Task.TaskKey
import com.android.wm.shell.common.split.SplitScreenConstants.SNAP_TO_30_70
import com.android.wm.shell.common.split.SplitScreenConstants.SNAP_TO_50_50
import com.android.wm.shell.common.split.SplitScreenConstants.SNAP_TO_70_30
+import java.util.function.Consumer
import org.junit.Assert.assertEquals
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
+import org.mockito.ArgumentCaptor
+import org.mockito.Captor
import org.mockito.Mock
import org.mockito.MockitoAnnotations
+import org.mockito.kotlin.any
+import org.mockito.kotlin.anyOrNull
+import org.mockito.kotlin.doNothing
+import org.mockito.kotlin.eq
+import org.mockito.kotlin.never
+import org.mockito.kotlin.spy
+import org.mockito.kotlin.times
+import org.mockito.kotlin.verify
+import org.mockito.kotlin.whenever
@RunWith(AndroidJUnit4::class)
class AppPairsControllerTest {
@@ -58,11 +77,38 @@
appPairsController.encodeRank(STAGE_POSITION_BOTTOM_OR_RIGHT, SNAP_TO_70_30)
}
+ @Mock lateinit var mockAppPairIcon: AppPairIcon
+ @Mock lateinit var mockTaskbarActivityContext: TaskbarActivityContext
+ @Mock lateinit var mockTopTaskTracker: TopTaskTracker
+ @Mock lateinit var mockCachedTaskInfo: CachedTaskInfo
+ @Mock lateinit var mockItemInfo1: ItemInfo
+ @Mock lateinit var mockItemInfo2: ItemInfo
+ @Mock lateinit var mockTask1: Task
+ @Mock lateinit var mockTask2: Task
+ @Mock lateinit var mockTaskKey1: TaskKey
+ @Mock lateinit var mockTaskKey2: TaskKey
+ @Captor lateinit var callbackCaptor: ArgumentCaptor<Consumer<Array<Task>>>
+
+ private lateinit var spyAppPairsController: AppPairsController
+
@Before
fun setup() {
MockitoAnnotations.initMocks(this)
appPairsController =
AppPairsController(context, splitSelectStateController, statsLogManager)
+
+ // Stub methods on appPairsController so that they return mocks
+ spyAppPairsController = spy(appPairsController)
+ whenever(mockAppPairIcon.context).thenReturn(mockTaskbarActivityContext)
+ whenever(spyAppPairsController.getTopTaskTracker(mockTaskbarActivityContext))
+ .thenReturn(mockTopTaskTracker)
+ whenever(mockTopTaskTracker.getCachedTopTask(any())).thenReturn(mockCachedTaskInfo)
+ whenever(mockTask1.getKey()).thenReturn(mockTaskKey1)
+ whenever(mockTask2.getKey()).thenReturn(mockTaskKey2)
+ doNothing().whenever(spyAppPairsController).launchAppPair(any())
+ doNothing()
+ .whenever(spyAppPairsController)
+ .launchToSide(anyOrNull(), anyOrNull(), anyOrNull(), anyOrNull())
}
@Test
@@ -144,4 +190,220 @@
AppPairsController.convertRankToSnapPosition(right70),
)
}
+
+ @Test
+ fun handleAppPairLaunchInApp_shouldDoNothingWhenAppsAreAlreadyRunning() {
+ // Test launching apps 1 and 2 from app pair
+ whenever(mockTaskKey1.getId()).thenReturn(1)
+ whenever(mockTaskKey2.getId()).thenReturn(2)
+ // ... with apps 1 and 2 already on screen
+ whenever(mockTopTaskTracker.runningSplitTaskIds).thenReturn(arrayListOf(1, 2).toIntArray())
+
+ // Trigger app pair launch, capture and run callback from findLastActiveTasksAndRunCallback
+ spyAppPairsController.handleAppPairLaunchInApp(
+ mockAppPairIcon,
+ listOf(mockItemInfo1, mockItemInfo2)
+ )
+ verify(splitSelectStateController)
+ .findLastActiveTasksAndRunCallback(any(), any(), callbackCaptor.capture())
+ val callback: Consumer<Array<Task>> = callbackCaptor.value
+ callback.accept(arrayOf(mockTask1, mockTask2))
+
+ // Verify that launchAppPair and launchToSide were never called
+ verify(spyAppPairsController, never()).launchAppPair(any())
+ verify(spyAppPairsController, never())
+ .launchToSide(anyOrNull(), anyOrNull(), anyOrNull(), anyOrNull())
+ }
+
+ @Test
+ fun handleAppPairLaunchInApp_shouldLaunchApp2ToRightWhenApp1IsOnLeft() {
+ // Test launching apps 1 and 2 from app pair
+ whenever(mockTaskKey1.getId()).thenReturn(1)
+ whenever(mockTaskKey2.getId()).thenReturn(2)
+ // ... with apps 1 and 3 already on screen
+ whenever(mockTopTaskTracker.runningSplitTaskIds).thenReturn(arrayListOf(1, 3).toIntArray())
+
+ // Trigger app pair launch, capture and run callback from findLastActiveTasksAndRunCallback
+ spyAppPairsController.handleAppPairLaunchInApp(
+ mockAppPairIcon,
+ listOf(mockItemInfo1, mockItemInfo2)
+ )
+ verify(splitSelectStateController)
+ .findLastActiveTasksAndRunCallback(any(), any(), callbackCaptor.capture())
+ val callback: Consumer<Array<Task>> = callbackCaptor.value
+ callback.accept(arrayOf(mockTask1, mockTask2))
+
+ // Verify that launchToSide was called with the correct arguments
+ verify(spyAppPairsController, never()).launchAppPair(any())
+ verify(spyAppPairsController, times(1))
+ .launchToSide(anyOrNull(), anyOrNull(), anyOrNull(), eq(STAGE_POSITION_BOTTOM_OR_RIGHT))
+ }
+
+ @Test
+ fun handleAppPairLaunchInApp_shouldLaunchApp2ToLeftWhenApp1IsOnRight() {
+ // Test launching apps 1 and 2 from app pair
+ whenever(mockTaskKey1.getId()).thenReturn(1)
+ whenever(mockTaskKey2.getId()).thenReturn(2)
+ // ... with apps 3 and 1 already on screen
+ whenever(mockTopTaskTracker.runningSplitTaskIds).thenReturn(arrayListOf(3, 1).toIntArray())
+
+ // Trigger app pair launch, capture and run callback from findLastActiveTasksAndRunCallback
+ spyAppPairsController.handleAppPairLaunchInApp(
+ mockAppPairIcon,
+ listOf(mockItemInfo1, mockItemInfo2)
+ )
+ verify(splitSelectStateController)
+ .findLastActiveTasksAndRunCallback(any(), any(), callbackCaptor.capture())
+ val callback: Consumer<Array<Task>> = callbackCaptor.value
+ callback.accept(arrayOf(mockTask1, mockTask2))
+
+ // Verify that launchToSide was called with the correct arguments
+ verify(spyAppPairsController, never()).launchAppPair(any())
+ verify(spyAppPairsController, times(1))
+ .launchToSide(anyOrNull(), anyOrNull(), anyOrNull(), eq(STAGE_POSITION_TOP_OR_LEFT))
+ }
+
+ @Test
+ fun handleAppPairLaunchInApp_shouldLaunchApp1ToRightWhenApp2IsOnLeft() {
+ // Test launching apps 1 and 2 from app pair
+ whenever(mockTaskKey1.getId()).thenReturn(1)
+ whenever(mockTaskKey2.getId()).thenReturn(2)
+ // ... with apps 2 and 3 already on screen
+ whenever(mockTopTaskTracker.runningSplitTaskIds).thenReturn(arrayListOf(2, 3).toIntArray())
+
+ // Trigger app pair launch, capture and run callback from findLastActiveTasksAndRunCallback
+ spyAppPairsController.handleAppPairLaunchInApp(
+ mockAppPairIcon,
+ listOf(mockItemInfo1, mockItemInfo2)
+ )
+ verify(splitSelectStateController)
+ .findLastActiveTasksAndRunCallback(any(), any(), callbackCaptor.capture())
+ val callback: Consumer<Array<Task>> = callbackCaptor.value
+ callback.accept(arrayOf(mockTask1, mockTask2))
+
+ // Verify that launchToSide was called with the correct arguments
+ verify(spyAppPairsController, never()).launchAppPair(any())
+ verify(spyAppPairsController, times(1))
+ .launchToSide(anyOrNull(), anyOrNull(), anyOrNull(), eq(STAGE_POSITION_BOTTOM_OR_RIGHT))
+ }
+
+ @Test
+ fun handleAppPairLaunchInApp_shouldLaunchApp1ToLeftWhenApp2IsOnRight() {
+ // Test launching apps 1 and 2 from app pair
+ whenever(mockTaskKey1.getId()).thenReturn(1)
+ whenever(mockTaskKey2.getId()).thenReturn(2)
+ // ... with apps 3 and 2 already on screen
+ whenever(mockTopTaskTracker.runningSplitTaskIds).thenReturn(arrayListOf(3, 2).toIntArray())
+
+ // Trigger app pair launch, capture and run callback from findLastActiveTasksAndRunCallback
+ spyAppPairsController.handleAppPairLaunchInApp(
+ mockAppPairIcon,
+ listOf(mockItemInfo1, mockItemInfo2)
+ )
+ verify(splitSelectStateController)
+ .findLastActiveTasksAndRunCallback(any(), any(), callbackCaptor.capture())
+ val callback: Consumer<Array<Task>> = callbackCaptor.value
+ callback.accept(arrayOf(mockTask1, mockTask2))
+
+ // Verify that launchToSide was called with the correct arguments
+ verify(spyAppPairsController, never()).launchAppPair(any())
+ verify(spyAppPairsController, times(1))
+ .launchToSide(anyOrNull(), anyOrNull(), anyOrNull(), eq(STAGE_POSITION_TOP_OR_LEFT))
+ }
+
+ @Test
+ fun handleAppPairLaunchInApp_shouldLaunchAppPairNormallyWhenUnrelatedPairIsOnScreen() {
+ // Test launching apps 1 and 2 from app pair
+ whenever(mockTaskKey1.getId()).thenReturn(1)
+ whenever(mockTaskKey2.getId()).thenReturn(2)
+ // ... with apps 3 and 4 already on screen
+ whenever(mockTopTaskTracker.runningSplitTaskIds).thenReturn(arrayListOf(3, 4).toIntArray())
+
+ // Trigger app pair launch, capture and run callback from findLastActiveTasksAndRunCallback
+ spyAppPairsController.handleAppPairLaunchInApp(
+ mockAppPairIcon,
+ listOf(mockItemInfo1, mockItemInfo2)
+ )
+ verify(splitSelectStateController)
+ .findLastActiveTasksAndRunCallback(any(), any(), callbackCaptor.capture())
+ val callback: Consumer<Array<Task>> = callbackCaptor.value
+ callback.accept(arrayOf(mockTask1, mockTask2))
+
+ // Verify that launchAppPair was called
+ verify(spyAppPairsController, times(1)).launchAppPair(any())
+ verify(spyAppPairsController, never())
+ .launchToSide(anyOrNull(), anyOrNull(), anyOrNull(), anyOrNull())
+ }
+
+ @Test
+ fun handleAppPairLaunchInApp_shouldLaunchApp2ToRightWhenApp1IsFullscreen() {
+ /// Test launching apps 1 and 2 from app pair
+ whenever(mockTaskKey1.getId()).thenReturn(1)
+ whenever(mockTaskKey2.getId()).thenReturn(2)
+ // ... with app 1 already on screen
+ whenever(mockCachedTaskInfo.taskId).thenReturn(1)
+
+ // Trigger app pair launch, capture and run callback from findLastActiveTasksAndRunCallback
+ spyAppPairsController.handleAppPairLaunchInApp(
+ mockAppPairIcon,
+ listOf(mockItemInfo1, mockItemInfo2)
+ )
+ verify(splitSelectStateController)
+ .findLastActiveTasksAndRunCallback(any(), any(), callbackCaptor.capture())
+ val callback: Consumer<Array<Task>> = callbackCaptor.value
+ callback.accept(arrayOf(mockTask1, mockTask2))
+
+ // Verify that launchToSide was called with the correct arguments
+ verify(spyAppPairsController, never()).launchAppPair(any())
+ verify(spyAppPairsController, times(1))
+ .launchToSide(anyOrNull(), anyOrNull(), anyOrNull(), eq(STAGE_POSITION_BOTTOM_OR_RIGHT))
+ }
+
+ @Test
+ fun handleAppPairLaunchInApp_shouldLaunchApp1ToLeftWhenApp2IsFullscreen() {
+ /// Test launching apps 1 and 2 from app pair
+ whenever(mockTaskKey1.getId()).thenReturn(1)
+ whenever(mockTaskKey2.getId()).thenReturn(2)
+ // ... with app 2 already on screen
+ whenever(mockCachedTaskInfo.taskId).thenReturn(2)
+
+ // Trigger app pair launch, capture and run callback from findLastActiveTasksAndRunCallback
+ spyAppPairsController.handleAppPairLaunchInApp(
+ mockAppPairIcon,
+ listOf(mockItemInfo1, mockItemInfo2)
+ )
+ verify(splitSelectStateController)
+ .findLastActiveTasksAndRunCallback(any(), any(), callbackCaptor.capture())
+ val callback: Consumer<Array<Task>> = callbackCaptor.value
+ callback.accept(arrayOf(mockTask1, mockTask2))
+
+ // Verify that launchToSide was called with the correct arguments
+ verify(spyAppPairsController, never()).launchAppPair(any())
+ verify(spyAppPairsController, times(1))
+ .launchToSide(anyOrNull(), anyOrNull(), anyOrNull(), eq(STAGE_POSITION_TOP_OR_LEFT))
+ }
+
+ @Test
+ fun handleAppPairLaunchInApp_shouldLaunchAppPairNormallyWhenUnrelatedSingleAppIsFullscreen() {
+ // Test launching apps 1 and 2 from app pair
+ whenever(mockTaskKey1.getId()).thenReturn(1)
+ whenever(mockTaskKey2.getId()).thenReturn(2)
+ // ... with app 3 already on screen
+ whenever(mockCachedTaskInfo.taskId).thenReturn(3)
+
+ // Trigger app pair launch, capture and run callback from findLastActiveTasksAndRunCallback
+ spyAppPairsController.handleAppPairLaunchInApp(
+ mockAppPairIcon,
+ listOf(mockItemInfo1, mockItemInfo2)
+ )
+ verify(splitSelectStateController)
+ .findLastActiveTasksAndRunCallback(any(), any(), callbackCaptor.capture())
+ val callback: Consumer<Array<Task>> = callbackCaptor.value
+ callback.accept(arrayOf(mockTask1, mockTask2))
+
+ // Verify that launchAppPair was called
+ verify(spyAppPairsController, times(1)).launchAppPair(any())
+ verify(spyAppPairsController, never())
+ .launchToSide(anyOrNull(), anyOrNull(), anyOrNull(), anyOrNull())
+ }
}
diff --git a/quickstep/tests/src/com/android/quickstep/util/GestureExclusionManagerTest.kt b/quickstep/tests/src/com/android/quickstep/util/GestureExclusionManagerTest.kt
new file mode 100644
index 0000000..c190cfe
--- /dev/null
+++ b/quickstep/tests/src/com/android/quickstep/util/GestureExclusionManagerTest.kt
@@ -0,0 +1,135 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.quickstep.util
+
+import android.graphics.Rect
+import android.graphics.Region
+import android.testing.AndroidTestingRunner
+import android.view.Display.DEFAULT_DISPLAY
+import android.view.IWindowManager
+import androidx.test.filters.SmallTest
+import com.android.launcher3.util.Executors
+import com.android.quickstep.util.GestureExclusionManager.ExclusionListener
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mock
+import org.mockito.MockitoAnnotations
+import org.mockito.kotlin.reset
+import org.mockito.kotlin.verify
+import org.mockito.kotlin.verifyZeroInteractions
+
+/** Unit test for [GestureExclusionManager]. */
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+class GestureExclusionManagerTest {
+
+ @Mock private lateinit var windowManager: IWindowManager
+
+ @Mock private lateinit var listener1: ExclusionListener
+ @Mock private lateinit var listener2: ExclusionListener
+
+ private val r1 = Region().apply { union(Rect(0, 0, 100, 200)) }
+ private val r2 = Region().apply { union(Rect(200, 200, 500, 800)) }
+
+ private lateinit var underTest: GestureExclusionManager
+
+ @Before
+ fun setup() {
+ MockitoAnnotations.initMocks(this)
+ underTest = GestureExclusionManager(windowManager)
+ }
+
+ @Test
+ fun addListener_registers() {
+ underTest.addListener(listener1)
+
+ awaitTasksCompleted()
+ verify(windowManager)
+ .registerSystemGestureExclusionListener(underTest.exclusionListener, DEFAULT_DISPLAY)
+ }
+
+ @Test
+ fun addListener_again_skips_register() {
+ underTest.addListener(listener1)
+ awaitTasksCompleted()
+ reset(windowManager)
+
+ underTest.addListener(listener2)
+
+ awaitTasksCompleted()
+ verifyZeroInteractions(windowManager)
+ }
+
+ @Test
+ fun removeListener_unregisters() {
+ underTest.addListener(listener1)
+ awaitTasksCompleted()
+ reset(windowManager)
+
+ underTest.removeListener(listener1)
+
+ awaitTasksCompleted()
+ verify(windowManager)
+ .unregisterSystemGestureExclusionListener(underTest.exclusionListener, DEFAULT_DISPLAY)
+ }
+
+ @Test
+ fun removeListener_again_skips_unregister() {
+ underTest.addListener(listener1)
+ underTest.addListener(listener2)
+ awaitTasksCompleted()
+ reset(windowManager)
+
+ underTest.removeListener(listener1)
+
+ awaitTasksCompleted()
+ verifyZeroInteractions(windowManager)
+ }
+
+ @Test
+ fun onSystemGestureExclusionChanged_dispatches_to_listeners() {
+ underTest.addListener(listener1)
+ underTest.addListener(listener2)
+ awaitTasksCompleted()
+
+ underTest.exclusionListener.onSystemGestureExclusionChanged(DEFAULT_DISPLAY, r1, r2)
+ awaitTasksCompleted()
+ verify(listener1).onGestureExclusionChanged(r1, r2)
+ verify(listener2).onGestureExclusionChanged(r1, r2)
+ }
+
+ @Test
+ fun addLister_dispatches_second_time() {
+ underTest.exclusionListener.onSystemGestureExclusionChanged(DEFAULT_DISPLAY, r1, r2)
+ awaitTasksCompleted()
+ underTest.addListener(listener1)
+ awaitTasksCompleted()
+ verifyZeroInteractions(listener1)
+
+ underTest.addListener(listener2)
+ awaitTasksCompleted()
+
+ verifyZeroInteractions(listener1)
+ verify(listener2).onGestureExclusionChanged(r1, r2)
+ }
+
+ private fun awaitTasksCompleted() {
+ Executors.UI_HELPER_EXECUTOR.submit<Any> { null }.get()
+ Executors.MAIN_EXECUTOR.submit<Any> { null }.get()
+ }
+}
diff --git a/quickstep/tests/src/com/android/quickstep/util/SplitAnimationControllerTest.kt b/quickstep/tests/src/com/android/quickstep/util/SplitAnimationControllerTest.kt
index de152fa..68c9bf9 100644
--- a/quickstep/tests/src/com/android/quickstep/util/SplitAnimationControllerTest.kt
+++ b/quickstep/tests/src/com/android/quickstep/util/SplitAnimationControllerTest.kt
@@ -281,7 +281,7 @@
whenever(mockAppPairIcon.context).thenReturn(mockTaskbarActivityContext)
doNothing()
.whenever(spySplitAnimationController)
- .composeFadeInSplitLaunchAnimator(any(), any(), any(), any(), any())
+ .composeScaleUpLaunchAnimation(any(), any(), any())
spySplitAnimationController.playSplitLaunchAnimation(
null /* launchingTaskView */,
@@ -298,8 +298,7 @@
{} /* finishCallback */
)
- verify(spySplitAnimationController)
- .composeFadeInSplitLaunchAnimator(any(), any(), any(), any(), any())
+ verify(spySplitAnimationController).composeScaleUpLaunchAnimation(any(), any(), any())
}
@Test
diff --git a/res/drawable/icon_menu_background.xml b/res/drawable/icon_menu_background.xml
deleted file mode 100644
index 8a95c3e..0000000
--- a/res/drawable/icon_menu_background.xml
+++ /dev/null
@@ -1,21 +0,0 @@
-<?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.
--->
-<shape xmlns:android="http://schemas.android.com/apk/res/android"
- xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
- android:shape="rectangle">
- <solid android:color="?androidprv:attr/materialColorSurfaceBright" />
-</shape>
\ No newline at end of file
diff --git a/res/drawable/icon_menu_background_corners.xml b/res/drawable/icon_menu_background_corners.xml
deleted file mode 100644
index 16e3fe2..0000000
--- a/res/drawable/icon_menu_background_corners.xml
+++ /dev/null
@@ -1,22 +0,0 @@
-<?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.
--->
-<shape xmlns:android="http://schemas.android.com/apk/res/android"
- xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
- android:shape="rectangle">
- <solid android:color="?androidprv:attr/materialColorSurfaceBright" />
- <corners android:radius="@dimen/task_thumbnail_icon_menu_corner_radius" />
-</shape>
\ No newline at end of file
diff --git a/res/drawable/icon_menu_elevation_background.xml b/res/drawable/icon_menu_elevation_background.xml
deleted file mode 100644
index 16e3fe2..0000000
--- a/res/drawable/icon_menu_elevation_background.xml
+++ /dev/null
@@ -1,22 +0,0 @@
-<?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.
--->
-<shape xmlns:android="http://schemas.android.com/apk/res/android"
- xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
- android:shape="rectangle">
- <solid android:color="?androidprv:attr/materialColorSurfaceBright" />
- <corners android:radius="@dimen/task_thumbnail_icon_menu_corner_radius" />
-</shape>
\ No newline at end of file
diff --git a/res/values/dimens.xml b/res/values/dimens.xml
index 3aa4a77..c187000 100644
--- a/res/values/dimens.xml
+++ b/res/values/dimens.xml
@@ -414,11 +414,6 @@
<dimen name="task_thumbnail_icon_size">0dp</dimen>
<dimen name="task_thumbnail_icon_drawable_size">0dp</dimen>
<dimen name="task_thumbnail_icon_drawable_size_grid">0dp</dimen>
- <dimen name="task_thumbnail_icon_menu_max_width">0dp</dimen>
- <dimen name="task_thumbnail_icon_menu_start_margin">0dp</dimen>
- <dimen name="task_thumbnail_icon_menu_background_max_width">0dp</dimen>
- <dimen name="task_thumbnail_icon_menu_corner_radius">0dp</dimen>
- <dimen name="task_thumbnail_icon_menu_drawable_size">0dp</dimen>
<dimen name="task_thumbnail_icon_menu_drawable_touch_size">0dp</dimen>
<dimen name="task_menu_edge_padding">0dp</dimen>
<dimen name="overview_task_margin">0dp</dimen>
@@ -490,4 +485,7 @@
<dimen name="ps_button_width">36dp</dimen>
<dimen name="ps_lock_button_width">89dp</dimen>
<dimen name="ps_app_divider_padding">16dp</dimen>
+
+ <!-- WindowManagerProxy -->
+ <dimen name="max_width_and_height_of_small_display_cutout">136px</dimen>
</resources>
diff --git a/src/com/android/launcher3/DeviceProfile.java b/src/com/android/launcher3/DeviceProfile.java
index bf4f6c3..563dfe2 100644
--- a/src/com/android/launcher3/DeviceProfile.java
+++ b/src/com/android/launcher3/DeviceProfile.java
@@ -260,7 +260,6 @@
public int overviewTaskIconSizePx;
public int overviewTaskIconDrawableSizePx;
public int overviewTaskIconDrawableSizeGridPx;
- public int overviewTaskIconAppChipMenuDrawableSizePx;
public int overviewTaskThumbnailTopMarginPx;
public final int overviewActionsHeight;
public final int overviewActionsTopMarginPx;
@@ -686,8 +685,6 @@
res.getDimensionPixelSize(R.dimen.task_thumbnail_icon_drawable_size);
overviewTaskIconDrawableSizeGridPx =
res.getDimensionPixelSize(R.dimen.task_thumbnail_icon_drawable_size_grid);
- overviewTaskIconAppChipMenuDrawableSizePx = res.getDimensionPixelSize(
- R.dimen.task_thumbnail_icon_menu_drawable_size);
overviewTaskThumbnailTopMarginPx =
enableOverviewIconMenu() ? 0 : overviewTaskIconSizePx + overviewTaskMarginPx;
// Don't add margin with floating search bar to minimize risk of overlapping.
@@ -2156,8 +2153,6 @@
overviewTaskIconDrawableSizePx));
writer.println(prefix + pxToDpStr("overviewTaskIconDrawableSizeGridPx",
overviewTaskIconDrawableSizeGridPx));
- writer.println(prefix + pxToDpStr("overviewTaskIconAppChipMenuDrawableSizePx",
- overviewTaskIconAppChipMenuDrawableSizePx));
writer.println(prefix + pxToDpStr("overviewTaskThumbnailTopMarginPx",
overviewTaskThumbnailTopMarginPx));
writer.println(prefix + pxToDpStr("overviewActionsTopMarginPx",
diff --git a/src/com/android/launcher3/Workspace.java b/src/com/android/launcher3/Workspace.java
index 984a9ae..ac0d7ce 100644
--- a/src/com/android/launcher3/Workspace.java
+++ b/src/com/android/launcher3/Workspace.java
@@ -1636,9 +1636,15 @@
mDragController.addDragListener(
new AccessibleDragListenerAdapter(this, WorkspaceAccessibilityHelper::new) {
@Override
- protected void enableAccessibleDrag(boolean enable) {
- super.enableAccessibleDrag(enable);
+ protected void enableAccessibleDrag(boolean enable,
+ @Nullable DragObject dragObject) {
+ super.enableAccessibleDrag(enable, dragObject);
setEnableForLayout(mLauncher.getHotseat(), enable);
+ if (enable && dragObject != null
+ && dragObject.dragInfo instanceof LauncherAppWidgetInfo) {
+ mLauncher.getHotseat().setImportantForAccessibility(
+ IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS);
+ }
}
});
}
diff --git a/src/com/android/launcher3/accessibility/AccessibleDragListenerAdapter.java b/src/com/android/launcher3/accessibility/AccessibleDragListenerAdapter.java
index 0d7df2b..79b8187 100644
--- a/src/com/android/launcher3/accessibility/AccessibleDragListenerAdapter.java
+++ b/src/com/android/launcher3/accessibility/AccessibleDragListenerAdapter.java
@@ -20,6 +20,8 @@
import android.view.ViewGroup;
import android.view.ViewGroup.OnHierarchyChangeListener;
+import androidx.annotation.Nullable;
+
import com.android.launcher3.CellLayout;
import com.android.launcher3.DropTarget.DragObject;
import com.android.launcher3.Launcher;
@@ -50,13 +52,13 @@
@Override
public void onDragStart(DragObject dragObject, DragOptions options) {
mViewGroup.setOnHierarchyChangeListener(this);
- enableAccessibleDrag(true);
+ enableAccessibleDrag(true, dragObject);
}
@Override
public void onDragEnd() {
mViewGroup.setOnHierarchyChangeListener(null);
- enableAccessibleDrag(false);
+ enableAccessibleDrag(false, null);
Launcher.getLauncher(mViewGroup.getContext()).getDragController().removeDragListener(this);
}
@@ -75,7 +77,7 @@
}
}
- protected void enableAccessibleDrag(boolean enable) {
+ protected void enableAccessibleDrag(boolean enable, @Nullable DragObject dragObject) {
for (int i = 0; i < mViewGroup.getChildCount(); i++) {
setEnableForLayout((CellLayout) mViewGroup.getChildAt(i), enable);
}
diff --git a/src/com/android/launcher3/anim/AnimatedFloat.java b/src/com/android/launcher3/anim/AnimatedFloat.java
index 2f3fa63..b414ab6 100644
--- a/src/com/android/launcher3/anim/AnimatedFloat.java
+++ b/src/com/android/launcher3/anim/AnimatedFloat.java
@@ -109,6 +109,13 @@
public void cancelAnimation() {
if (mValueAnimator != null) {
mValueAnimator.cancel();
+ // Clears the property values, so further ObjectAnimator#setCurrentFraction from e.g.
+ // AnimatorPlaybackController calls would do nothing. The null check is necessary to
+ // avoid mValueAnimator being set to null in onAnimationEnd.
+ if (mValueAnimator != null) {
+ mValueAnimator.setValues();
+ mValueAnimator = null;
+ }
}
}
diff --git a/src/com/android/launcher3/anim/PendingAnimation.java b/src/com/android/launcher3/anim/PendingAnimation.java
index 586beb2..e58890f 100644
--- a/src/com/android/launcher3/anim/PendingAnimation.java
+++ b/src/com/android/launcher3/anim/PendingAnimation.java
@@ -83,6 +83,20 @@
add(anim);
}
+ /**
+ * Add an {@link AnimatedFloat} to the animation.
+ * <p>
+ * Different from {@link #addFloat}, this method use animator provided by
+ * {@link AnimatedFloat#animateToValue}, which tracks the animator inside the AnimatedFloat,
+ * allowing the animation to be canceled and animate again from AnimatedFloat side.
+ */
+ public void addAnimatedFloat(AnimatedFloat target, float from, float to,
+ TimeInterpolator interpolator) {
+ Animator anim = target.animateToValue(from, to);
+ anim.setInterpolator(interpolator);
+ add(anim);
+ }
+
/** If trace is enabled, add counter to trace animation progress. */
public void logAnimationProgressToTrace(String counterName) {
if (Trace.isEnabled()) {
diff --git a/src/com/android/launcher3/folder/Folder.java b/src/com/android/launcher3/folder/Folder.java
index 2f3f029..f013126 100644
--- a/src/com/android/launcher3/folder/Folder.java
+++ b/src/com/android/launcher3/folder/Folder.java
@@ -320,8 +320,9 @@
mDragController.addDragListener(new AccessibleDragListenerAdapter(
mContent, FolderAccessibilityHelper::new) {
@Override
- protected void enableAccessibleDrag(boolean enable) {
- super.enableAccessibleDrag(enable);
+ protected void enableAccessibleDrag(boolean enable,
+ @Nullable DragObject dragObject) {
+ super.enableAccessibleDrag(enable, dragObject);
mFooter.setImportantForAccessibility(enable
? IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS
: IMPORTANT_FOR_ACCESSIBILITY_AUTO);
diff --git a/src/com/android/launcher3/model/WorkspaceItemProcessor.kt b/src/com/android/launcher3/model/WorkspaceItemProcessor.kt
index 31ae7c2..88b98aa 100644
--- a/src/com/android/launcher3/model/WorkspaceItemProcessor.kt
+++ b/src/com/android/launcher3/model/WorkspaceItemProcessor.kt
@@ -433,7 +433,9 @@
!c.hasRestoreFlag(LauncherAppWidgetInfo.FLAG_RESTORE_STARTED) &&
!isSafeMode &&
(si == null) &&
- (lapi == null)
+ (lapi == null) &&
+ !(Utilities.enableSupportForArchiving() &&
+ pmHelper.isAppArchived(component.packageName))
) {
// Restore never started
c.markDeleted(
diff --git a/src/com/android/launcher3/util/PackageManagerHelper.java b/src/com/android/launcher3/util/PackageManagerHelper.java
index 50d8886..92288e1 100644
--- a/src/com/android/launcher3/util/PackageManagerHelper.java
+++ b/src/com/android/launcher3/util/PackageManagerHelper.java
@@ -107,6 +107,23 @@
}
/**
+ * Returns whether the target app is in archived state
+ */
+ @SuppressWarnings("NewApi")
+ public boolean isAppArchived(@NonNull final String packageName) {
+ final ApplicationInfo info;
+ try {
+ info = mPm.getPackageInfo(packageName,
+ PackageManager.PackageInfoFlags.of(
+ PackageManager.MATCH_ARCHIVED_PACKAGES)).applicationInfo;
+ return info.isArchived;
+ } catch (NameNotFoundException e) {
+ Log.e(TAG, "Failed to get applicationInfo for package: " + packageName, e);
+ return false;
+ }
+ }
+
+ /**
* Returns the application info for the provided package or null
*/
@Nullable
diff --git a/src/com/android/launcher3/util/window/WindowManagerProxy.java b/src/com/android/launcher3/util/window/WindowManagerProxy.java
index 32f1736..6a0090c 100644
--- a/src/com/android/launcher3/util/window/WindowManagerProxy.java
+++ b/src/com/android/launcher3/util/window/WindowManagerProxy.java
@@ -17,6 +17,7 @@
import static android.view.Display.DEFAULT_DISPLAY;
+import static com.android.launcher3.Utilities.dpToPx;
import static com.android.launcher3.Utilities.dpiFromPx;
import static com.android.launcher3.testing.shared.ResourceUtils.INVALID_RESOURCE_HANDLE;
import static com.android.launcher3.testing.shared.ResourceUtils.NAVBAR_HEIGHT;
@@ -49,6 +50,9 @@
import android.view.WindowManager;
import android.view.WindowMetrics;
+import androidx.annotation.NonNull;
+import androidx.annotation.VisibleForTesting;
+
import com.android.launcher3.R;
import com.android.launcher3.Utilities;
import com.android.launcher3.testing.shared.ResourceUtils;
@@ -130,11 +134,11 @@
Resources systemRes = context.getResources();
Configuration config = systemRes.getConfiguration();
- boolean isTablet = config.smallestScreenWidthDp > MIN_TABLET_WIDTH;
+ boolean isLargeScreen = config.smallestScreenWidthDp > MIN_TABLET_WIDTH;
boolean isGesture = isGestureNav(context);
boolean isPortrait = config.screenHeightDp > config.screenWidthDp;
- int bottomNav = isTablet
+ int bottomNav = isLargeScreen
? 0
: (isPortrait
? getDimenByName(systemRes, NAVBAR_HEIGHT)
@@ -165,6 +169,9 @@
insetsBuilder.setInsets(WindowInsets.Type.tappableElement(), newTappableInsets);
}
+ applyDisplayCutoutBottomInsetOverrideOnLargeScreen(
+ context, isLargeScreen, dpToPx(config.screenWidthDp), oldInsets, insetsBuilder);
+
WindowInsets result = insetsBuilder.build();
Insets systemWindowInsets = result.getInsetsIgnoringVisibility(
WindowInsets.Type.systemBars() | WindowInsets.Type.displayCutout());
@@ -173,6 +180,71 @@
return result;
}
+ /**
+ * For large screen, when display cutout is at bottom left/right corner of screen, override
+ * display cutout's bottom inset to 0, because launcher allows drawing content over that area.
+ */
+ private static void applyDisplayCutoutBottomInsetOverrideOnLargeScreen(
+ @NonNull Context context,
+ boolean isLargeScreen,
+ int screenWidthPx,
+ @NonNull WindowInsets windowInsets,
+ @NonNull WindowInsets.Builder insetsBuilder) {
+ if (!isLargeScreen || !Utilities.ATLEAST_S) {
+ return;
+ }
+
+ final DisplayCutout displayCutout = windowInsets.getDisplayCutout();
+ if (displayCutout == null) {
+ return;
+ }
+
+ if (!areBottomDisplayCutoutsSmallAndAtCorners(
+ displayCutout.getBoundingRectBottom(), screenWidthPx, context.getResources())) {
+ return;
+ }
+
+ Insets oldDisplayCutoutInset = windowInsets.getInsets(WindowInsets.Type.displayCutout());
+ Insets newDisplayCutoutInset = Insets.of(
+ oldDisplayCutoutInset.left,
+ oldDisplayCutoutInset.top,
+ oldDisplayCutoutInset.right,
+ 0);
+ insetsBuilder.setInsetsIgnoringVisibility(
+ WindowInsets.Type.displayCutout(), newDisplayCutoutInset);
+ }
+
+ /**
+ * @see doc at {@link #areBottomDisplayCutoutsSmallAndAtCorners(Rect, int, int)}
+ */
+ private static boolean areBottomDisplayCutoutsSmallAndAtCorners(
+ @NonNull Rect cutoutRectBottom, int screenWidthPx, @NonNull Resources res) {
+ return areBottomDisplayCutoutsSmallAndAtCorners(cutoutRectBottom, screenWidthPx,
+ res.getDimensionPixelSize(R.dimen.max_width_and_height_of_small_display_cutout));
+ }
+
+ /**
+ * Return true if bottom display cutouts are at bottom left/right corners, AND has width or
+ * height <= maxWidthAndHeightOfSmallCutoutPx. Note that display cutout rect and screenWidthPx
+ * passed to this method should be in the SAME screen rotation.
+ *
+ * @param cutoutRectBottom bottom display cutout rect, this is based on current screen rotation
+ * @param screenWidthPx screen width in px based on current screen rotation
+ * @param maxWidthAndHeightOfSmallCutoutPx maximum width and height pixels of cutout.
+ */
+ @VisibleForTesting
+ static boolean areBottomDisplayCutoutsSmallAndAtCorners(
+ @NonNull Rect cutoutRectBottom, int screenWidthPx,
+ int maxWidthAndHeightOfSmallCutoutPx) {
+ // Empty cutoutRectBottom means there is no display cutout at the bottom. We should ignore
+ // it by returning false.
+ if (cutoutRectBottom.isEmpty()) {
+ return false;
+ }
+ return (cutoutRectBottom.right <= maxWidthAndHeightOfSmallCutoutPx)
+ || cutoutRectBottom.left >= (screenWidthPx - maxWidthAndHeightOfSmallCutoutPx);
+ }
+
protected int getStatusBarHeight(Context context, boolean isPortrait, int statusBarInset) {
Resources systemRes = context.getResources();
int statusBarHeight = getDimenByName(systemRes,
@@ -249,6 +321,12 @@
DisplayCutout rotatedCutout = rotateCutout(
displayInfo.cutout, displayInfo.size.x, displayInfo.size.y, rotation, i);
Rect insets = getSafeInsets(rotatedCutout);
+ if (areBottomDisplayCutoutsSmallAndAtCorners(
+ rotatedCutout.getBoundingRectBottom(),
+ bounds.width(),
+ context.getResources())) {
+ insets.bottom = 0;
+ }
insets.top = Math.max(insets.top, statusBarHeight);
insets.bottom = Math.max(insets.bottom, navBarHeight);
diff --git a/tests/assets/dumpTests/DeviceProfileDumpTest/phonePortrait.txt b/tests/assets/dumpTests/DeviceProfileDumpTest/phonePortrait.txt
index 18752e9..af8f67f 100644
--- a/tests/assets/dumpTests/DeviceProfileDumpTest/phonePortrait.txt
+++ b/tests/assets/dumpTests/DeviceProfileDumpTest/phonePortrait.txt
@@ -110,7 +110,6 @@
overviewTaskIconSizePx: 0.0px (0.0dp)
overviewTaskIconDrawableSizePx: 0.0px (0.0dp)
overviewTaskIconDrawableSizeGridPx: 0.0px (0.0dp)
- overviewTaskIconAppChipMenuDrawableSizePx: 0.0px (0.0dp)
overviewTaskThumbnailTopMarginPx: 0.0px (0.0dp)
overviewActionsTopMarginPx: 0.0px (0.0dp)
overviewActionsHeight: 0.0px (0.0dp)
diff --git a/tests/assets/dumpTests/DeviceProfileDumpTest/phonePortrait3Button.txt b/tests/assets/dumpTests/DeviceProfileDumpTest/phonePortrait3Button.txt
index c0de8d8..5b83dd7 100644
--- a/tests/assets/dumpTests/DeviceProfileDumpTest/phonePortrait3Button.txt
+++ b/tests/assets/dumpTests/DeviceProfileDumpTest/phonePortrait3Button.txt
@@ -110,7 +110,6 @@
overviewTaskIconSizePx: 0.0px (0.0dp)
overviewTaskIconDrawableSizePx: 0.0px (0.0dp)
overviewTaskIconDrawableSizeGridPx: 0.0px (0.0dp)
- overviewTaskIconAppChipMenuDrawableSizePx: 0.0px (0.0dp)
overviewTaskThumbnailTopMarginPx: 0.0px (0.0dp)
overviewActionsTopMarginPx: 0.0px (0.0dp)
overviewActionsHeight: 0.0px (0.0dp)
diff --git a/tests/assets/dumpTests/DeviceProfileDumpTest/phoneVerticalBar.txt b/tests/assets/dumpTests/DeviceProfileDumpTest/phoneVerticalBar.txt
index 361247b..03a0048 100644
--- a/tests/assets/dumpTests/DeviceProfileDumpTest/phoneVerticalBar.txt
+++ b/tests/assets/dumpTests/DeviceProfileDumpTest/phoneVerticalBar.txt
@@ -110,7 +110,6 @@
overviewTaskIconSizePx: 0.0px (0.0dp)
overviewTaskIconDrawableSizePx: 0.0px (0.0dp)
overviewTaskIconDrawableSizeGridPx: 0.0px (0.0dp)
- overviewTaskIconAppChipMenuDrawableSizePx: 0.0px (0.0dp)
overviewTaskThumbnailTopMarginPx: 0.0px (0.0dp)
overviewActionsTopMarginPx: 0.0px (0.0dp)
overviewActionsHeight: 0.0px (0.0dp)
diff --git a/tests/assets/dumpTests/DeviceProfileDumpTest/phoneVerticalBar3Button.txt b/tests/assets/dumpTests/DeviceProfileDumpTest/phoneVerticalBar3Button.txt
index d93ec58..84258b3 100644
--- a/tests/assets/dumpTests/DeviceProfileDumpTest/phoneVerticalBar3Button.txt
+++ b/tests/assets/dumpTests/DeviceProfileDumpTest/phoneVerticalBar3Button.txt
@@ -110,7 +110,6 @@
overviewTaskIconSizePx: 0.0px (0.0dp)
overviewTaskIconDrawableSizePx: 0.0px (0.0dp)
overviewTaskIconDrawableSizeGridPx: 0.0px (0.0dp)
- overviewTaskIconAppChipMenuDrawableSizePx: 0.0px (0.0dp)
overviewTaskThumbnailTopMarginPx: 0.0px (0.0dp)
overviewActionsTopMarginPx: 0.0px (0.0dp)
overviewActionsHeight: 0.0px (0.0dp)
diff --git a/tests/assets/dumpTests/DeviceProfileDumpTest/tabletLandscape.txt b/tests/assets/dumpTests/DeviceProfileDumpTest/tabletLandscape.txt
index 92caf23..87a9700 100644
--- a/tests/assets/dumpTests/DeviceProfileDumpTest/tabletLandscape.txt
+++ b/tests/assets/dumpTests/DeviceProfileDumpTest/tabletLandscape.txt
@@ -110,7 +110,6 @@
overviewTaskIconSizePx: 0.0px (0.0dp)
overviewTaskIconDrawableSizePx: 0.0px (0.0dp)
overviewTaskIconDrawableSizeGridPx: 0.0px (0.0dp)
- overviewTaskIconAppChipMenuDrawableSizePx: 0.0px (0.0dp)
overviewTaskThumbnailTopMarginPx: 0.0px (0.0dp)
overviewActionsTopMarginPx: 0.0px (0.0dp)
overviewActionsHeight: 0.0px (0.0dp)
diff --git a/tests/assets/dumpTests/DeviceProfileDumpTest/tabletLandscape3Button.txt b/tests/assets/dumpTests/DeviceProfileDumpTest/tabletLandscape3Button.txt
index 3815fa9..169256d 100644
--- a/tests/assets/dumpTests/DeviceProfileDumpTest/tabletLandscape3Button.txt
+++ b/tests/assets/dumpTests/DeviceProfileDumpTest/tabletLandscape3Button.txt
@@ -110,7 +110,6 @@
overviewTaskIconSizePx: 0.0px (0.0dp)
overviewTaskIconDrawableSizePx: 0.0px (0.0dp)
overviewTaskIconDrawableSizeGridPx: 0.0px (0.0dp)
- overviewTaskIconAppChipMenuDrawableSizePx: 0.0px (0.0dp)
overviewTaskThumbnailTopMarginPx: 0.0px (0.0dp)
overviewActionsTopMarginPx: 0.0px (0.0dp)
overviewActionsHeight: 0.0px (0.0dp)
diff --git a/tests/assets/dumpTests/DeviceProfileDumpTest/tabletPortrait.txt b/tests/assets/dumpTests/DeviceProfileDumpTest/tabletPortrait.txt
index 7e0f316..6ed6879 100644
--- a/tests/assets/dumpTests/DeviceProfileDumpTest/tabletPortrait.txt
+++ b/tests/assets/dumpTests/DeviceProfileDumpTest/tabletPortrait.txt
@@ -110,7 +110,6 @@
overviewTaskIconSizePx: 0.0px (0.0dp)
overviewTaskIconDrawableSizePx: 0.0px (0.0dp)
overviewTaskIconDrawableSizeGridPx: 0.0px (0.0dp)
- overviewTaskIconAppChipMenuDrawableSizePx: 0.0px (0.0dp)
overviewTaskThumbnailTopMarginPx: 0.0px (0.0dp)
overviewActionsTopMarginPx: 0.0px (0.0dp)
overviewActionsHeight: 0.0px (0.0dp)
diff --git a/tests/assets/dumpTests/DeviceProfileDumpTest/tabletPortrait3Button.txt b/tests/assets/dumpTests/DeviceProfileDumpTest/tabletPortrait3Button.txt
index 58c3890..a51669b 100644
--- a/tests/assets/dumpTests/DeviceProfileDumpTest/tabletPortrait3Button.txt
+++ b/tests/assets/dumpTests/DeviceProfileDumpTest/tabletPortrait3Button.txt
@@ -110,7 +110,6 @@
overviewTaskIconSizePx: 0.0px (0.0dp)
overviewTaskIconDrawableSizePx: 0.0px (0.0dp)
overviewTaskIconDrawableSizeGridPx: 0.0px (0.0dp)
- overviewTaskIconAppChipMenuDrawableSizePx: 0.0px (0.0dp)
overviewTaskThumbnailTopMarginPx: 0.0px (0.0dp)
overviewActionsTopMarginPx: 0.0px (0.0dp)
overviewActionsHeight: 0.0px (0.0dp)
diff --git a/tests/assets/dumpTests/DeviceProfileDumpTest/twoPanelLandscape.txt b/tests/assets/dumpTests/DeviceProfileDumpTest/twoPanelLandscape.txt
index 1e363a2..61d5ee6 100644
--- a/tests/assets/dumpTests/DeviceProfileDumpTest/twoPanelLandscape.txt
+++ b/tests/assets/dumpTests/DeviceProfileDumpTest/twoPanelLandscape.txt
@@ -110,7 +110,6 @@
overviewTaskIconSizePx: 0.0px (0.0dp)
overviewTaskIconDrawableSizePx: 0.0px (0.0dp)
overviewTaskIconDrawableSizeGridPx: 0.0px (0.0dp)
- overviewTaskIconAppChipMenuDrawableSizePx: 0.0px (0.0dp)
overviewTaskThumbnailTopMarginPx: 0.0px (0.0dp)
overviewActionsTopMarginPx: 0.0px (0.0dp)
overviewActionsHeight: 0.0px (0.0dp)
diff --git a/tests/assets/dumpTests/DeviceProfileDumpTest/twoPanelLandscape3Button.txt b/tests/assets/dumpTests/DeviceProfileDumpTest/twoPanelLandscape3Button.txt
index 617b54b..ac73e2f 100644
--- a/tests/assets/dumpTests/DeviceProfileDumpTest/twoPanelLandscape3Button.txt
+++ b/tests/assets/dumpTests/DeviceProfileDumpTest/twoPanelLandscape3Button.txt
@@ -110,7 +110,6 @@
overviewTaskIconSizePx: 0.0px (0.0dp)
overviewTaskIconDrawableSizePx: 0.0px (0.0dp)
overviewTaskIconDrawableSizeGridPx: 0.0px (0.0dp)
- overviewTaskIconAppChipMenuDrawableSizePx: 0.0px (0.0dp)
overviewTaskThumbnailTopMarginPx: 0.0px (0.0dp)
overviewActionsTopMarginPx: 0.0px (0.0dp)
overviewActionsHeight: 0.0px (0.0dp)
diff --git a/tests/assets/dumpTests/DeviceProfileDumpTest/twoPanelPortrait.txt b/tests/assets/dumpTests/DeviceProfileDumpTest/twoPanelPortrait.txt
index 483b5e7..93ab6f2 100644
--- a/tests/assets/dumpTests/DeviceProfileDumpTest/twoPanelPortrait.txt
+++ b/tests/assets/dumpTests/DeviceProfileDumpTest/twoPanelPortrait.txt
@@ -110,7 +110,6 @@
overviewTaskIconSizePx: 0.0px (0.0dp)
overviewTaskIconDrawableSizePx: 0.0px (0.0dp)
overviewTaskIconDrawableSizeGridPx: 0.0px (0.0dp)
- overviewTaskIconAppChipMenuDrawableSizePx: 0.0px (0.0dp)
overviewTaskThumbnailTopMarginPx: 0.0px (0.0dp)
overviewActionsTopMarginPx: 0.0px (0.0dp)
overviewActionsHeight: 0.0px (0.0dp)
diff --git a/tests/assets/dumpTests/DeviceProfileDumpTest/twoPanelPortrait3Button.txt b/tests/assets/dumpTests/DeviceProfileDumpTest/twoPanelPortrait3Button.txt
index 8d0640c..ec9a10e 100644
--- a/tests/assets/dumpTests/DeviceProfileDumpTest/twoPanelPortrait3Button.txt
+++ b/tests/assets/dumpTests/DeviceProfileDumpTest/twoPanelPortrait3Button.txt
@@ -110,7 +110,6 @@
overviewTaskIconSizePx: 0.0px (0.0dp)
overviewTaskIconDrawableSizePx: 0.0px (0.0dp)
overviewTaskIconDrawableSizeGridPx: 0.0px (0.0dp)
- overviewTaskIconAppChipMenuDrawableSizePx: 0.0px (0.0dp)
overviewTaskThumbnailTopMarginPx: 0.0px (0.0dp)
overviewActionsTopMarginPx: 0.0px (0.0dp)
overviewActionsHeight: 0.0px (0.0dp)
diff --git a/tests/src/com/android/launcher3/util/window/WindowManagerProxyTest.kt b/tests/src/com/android/launcher3/util/window/WindowManagerProxyTest.kt
new file mode 100644
index 0000000..4819388
--- /dev/null
+++ b/tests/src/com/android/launcher3/util/window/WindowManagerProxyTest.kt
@@ -0,0 +1,138 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.launcher3.util.window
+
+import android.graphics.Rect
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.launcher3.util.window.WindowManagerProxy.areBottomDisplayCutoutsSmallAndAtCorners
+import junit.framework.Assert.assertFalse
+import junit.framework.Assert.assertTrue
+import org.junit.Test
+import org.junit.runner.RunWith
+
+/** Unit test for [WindowManagerProxy] */
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class WindowManagerProxyTest {
+
+ private val windowWidthPx = 2000
+
+ private val bottomLeftCutout = Rect(0, 2364, 136, 2500)
+ private val bottomRightCutout = Rect(1864, 2364, 2000, 2500)
+
+ private val bottomLeftCutoutWithOffset = Rect(10, 2364, 136, 2500)
+ private val bottomRightCutoutWithOffset = Rect(1864, 2364, 1990, 2500)
+
+ private val maxWidthAndHeightOfSmallCutoutPx = 136
+
+ @Test
+ fun cutout_at_bottom_right_corner() {
+ assertTrue(
+ areBottomDisplayCutoutsSmallAndAtCorners(
+ bottomRightCutout,
+ windowWidthPx,
+ maxWidthAndHeightOfSmallCutoutPx
+ )
+ )
+ }
+
+ @Test
+ fun cutout_at_bottom_left_corner_with_offset() {
+ assertTrue(
+ areBottomDisplayCutoutsSmallAndAtCorners(
+ bottomLeftCutoutWithOffset,
+ windowWidthPx,
+ maxWidthAndHeightOfSmallCutoutPx
+ )
+ )
+ }
+
+ @Test
+ fun cutout_at_bottom_right_corner_with_offset() {
+ assertTrue(
+ areBottomDisplayCutoutsSmallAndAtCorners(
+ bottomRightCutoutWithOffset,
+ windowWidthPx,
+ maxWidthAndHeightOfSmallCutoutPx
+ )
+ )
+ }
+
+ @Test
+ fun cutout_at_bottom_left_corner() {
+ assertTrue(
+ areBottomDisplayCutoutsSmallAndAtCorners(
+ bottomLeftCutout,
+ windowWidthPx,
+ maxWidthAndHeightOfSmallCutoutPx
+ )
+ )
+ }
+
+ @Test
+ fun cutout_at_bottom_edge_at_bottom_corners() {
+ assertTrue(
+ areBottomDisplayCutoutsSmallAndAtCorners(
+ bottomLeftCutout,
+ windowWidthPx,
+ maxWidthAndHeightOfSmallCutoutPx
+ )
+ )
+ }
+
+ @Test
+ fun cutout_too_big_not_at_bottom_corners() {
+ // Rect in size of 200px
+ val bigBottomLeftCutout = Rect(0, 2300, 200, 2500)
+
+ assertFalse(
+ areBottomDisplayCutoutsSmallAndAtCorners(
+ bigBottomLeftCutout,
+ windowWidthPx,
+ maxWidthAndHeightOfSmallCutoutPx
+ )
+ )
+ }
+
+ @Test
+ fun cutout_too_small_at_bottom_corners() {
+ // Rect in size of 100px
+ val smallBottomLeft = Rect(0, 2400, 100, 2500)
+
+ assertTrue(
+ areBottomDisplayCutoutsSmallAndAtCorners(
+ smallBottomLeft,
+ windowWidthPx,
+ maxWidthAndHeightOfSmallCutoutPx
+ )
+ )
+ }
+
+ @Test
+ fun cutout_empty_not_at_bottom_corners() {
+ val emptyRect = Rect(0, 0, 0, 0)
+
+ assertFalse(
+ areBottomDisplayCutoutsSmallAndAtCorners(
+ emptyRect,
+ windowWidthPx,
+ maxWidthAndHeightOfSmallCutoutPx
+ )
+ )
+ }
+}