Merge "[Dev option] Move DesktopModesStatus to wm/shell/shared/desktopmode from wm/shell/shared/" into main
diff --git a/aconfig/launcher.aconfig b/aconfig/launcher.aconfig
index 31a9009..88804b7 100644
--- a/aconfig/launcher.aconfig
+++ b/aconfig/launcher.aconfig
@@ -23,13 +23,6 @@
}
flag {
- name: "enable_grid_only_overview"
- namespace: "launcher"
- description: "Enable a grid-only overview without a focused task."
- bug: "257950105"
-}
-
-flag {
name: "enable_cursor_hover_states"
namespace: "launcher"
description: "Enables cursor hover states for certain elements."
@@ -44,13 +37,6 @@
}
flag {
- name: "enable_overview_icon_menu"
- namespace: "launcher"
- description: "Enable updated overview icon and menu within task."
- bug: "257950105"
-}
-
-flag {
name: "enable_focus_outline"
namespace: "launcher"
description: "Enables focus states outline for launcher."
@@ -238,13 +224,6 @@
}
flag {
- name: "enable_refactor_task_thumbnail"
- namespace: "launcher"
- description: "Enables rewritten version of TaskThumbnailViews in Overview"
- bug: "331753115"
-}
-
-flag {
name: "enable_handle_delayed_gesture_callbacks"
namespace: "launcher"
description: "Enables additional handling for delayed mid-gesture callbacks"
diff --git a/aconfig/launcher_overview.aconfig b/aconfig/launcher_overview.aconfig
new file mode 100644
index 0000000..f9327fe
--- /dev/null
+++ b/aconfig/launcher_overview.aconfig
@@ -0,0 +1,23 @@
+package: "com.android.launcher3"
+container: "system_ext"
+
+flag {
+ name: "enable_grid_only_overview"
+ namespace: "launcher_overview"
+ description: "Enable a grid-only overview without a focused task."
+ bug: "257950105"
+}
+
+flag {
+ name: "enable_overview_icon_menu"
+ namespace: "launcher_overview"
+ description: "Enable updated overview icon and menu within task."
+ bug: "257950105"
+}
+
+flag {
+ name: "enable_refactor_task_thumbnail"
+ namespace: "launcher_overview"
+ description: "Enables rewritten version of TaskThumbnailViews in Overview"
+ bug: "331753115"
+}
diff --git a/go/quickstep/src/com/android/quickstep/TaskOverlayFactoryGo.java b/go/quickstep/src/com/android/quickstep/TaskOverlayFactoryGo.java
index 68558fa..0fb9718 100644
--- a/go/quickstep/src/com/android/quickstep/TaskOverlayFactoryGo.java
+++ b/go/quickstep/src/com/android/quickstep/TaskOverlayFactoryGo.java
@@ -31,6 +31,7 @@
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
+import android.graphics.Bitmap;
import android.graphics.Color;
import android.graphics.Matrix;
import android.graphics.drawable.ColorDrawable;
@@ -47,6 +48,7 @@
import android.widget.TextView;
import androidx.annotation.IntDef;
+import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import com.android.launcher3.BaseActivity;
@@ -58,7 +60,6 @@
import com.android.quickstep.views.GoOverviewActionsView;
import com.android.quickstep.views.TaskContainer;
import com.android.systemui.shared.recents.model.Task;
-import com.android.systemui.shared.recents.model.ThumbnailData;
import java.lang.annotation.Retention;
@@ -131,7 +132,7 @@
* Called when the current task is interactive for the user
*/
@Override
- public void initOverlay(Task task, ThumbnailData thumbnail, Matrix matrix,
+ public void initOverlay(Task task, @Nullable Bitmap thumbnail, Matrix matrix,
boolean rotated) {
if (mDialog != null && mDialog.isShowing()) {
// Redraw the dialog in case the layout changed
diff --git a/quickstep/src/com/android/launcher3/WidgetPickerActivity.java b/quickstep/src/com/android/launcher3/WidgetPickerActivity.java
index 7cdca74..7235b63 100644
--- a/quickstep/src/com/android/launcher3/WidgetPickerActivity.java
+++ b/quickstep/src/com/android/launcher3/WidgetPickerActivity.java
@@ -51,9 +51,11 @@
import com.android.launcher3.widget.picker.WidgetsFullSheet;
import java.util.ArrayList;
+import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
+import java.util.Set;
import java.util.function.Function;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
@@ -86,6 +88,12 @@
private static final String EXTRA_UI_SURFACE = "ui_surface";
private static final Pattern UI_SURFACE_PATTERN =
Pattern.compile("^(widgets|widgets_hub)$");
+
+ /**
+ * User ids that should be filtered out of the widget lists created by this activity.
+ */
+ private static final String EXTRA_USER_ID_FILTER = "filtered_user_ids";
+
private SimpleDragLayer<WidgetPickerActivity> mDragLayer;
private WidgetsModel mModel;
private LauncherAppState mApp;
@@ -101,6 +109,10 @@
@NonNull
private List<AppWidgetProviderInfo> mAddedWidgets = new ArrayList<>();
+ /** A set of user ids that should be filtered out from the selected widgets. */
+ @NonNull
+ Set<Integer> mFilteredUserIds = new HashSet<>();
+
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
@@ -145,6 +157,12 @@
if (addedWidgets != null) {
mAddedWidgets = addedWidgets;
}
+ ArrayList<Integer> filteredUsers = getIntent().getIntegerArrayListExtra(
+ EXTRA_USER_ID_FILTER);
+ mFilteredUserIds.clear();
+ if (filteredUsers != null) {
+ mFilteredUserIds.addAll(filteredUsers);
+ }
}
@NonNull
@@ -289,6 +307,13 @@
return rejectWidget(widget, "shortcut");
}
+ if (mFilteredUserIds.contains(widget.user.getIdentifier())) {
+ return rejectWidget(
+ widget,
+ "widget user: %d is being filtered",
+ widget.user.getIdentifier());
+ }
+
if (mWidgetCategoryFilter > 0 && (info.widgetCategory & mWidgetCategoryFilter) == 0) {
return rejectWidget(
widget,
diff --git a/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchController.java b/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchController.java
index 358d703..46501c4 100644
--- a/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchController.java
@@ -245,11 +245,20 @@
}
void updateThumbnailInBackground(Task task, Consumer<ThumbnailData> callback) {
- mModel.getThumbnailCache().updateThumbnailInBackground(task, callback);
+ mModel.getThumbnailCache().getThumbnailInBackground(task,
+ thumbnailData -> {
+ task.thumbnail = thumbnailData;
+ callback.accept(thumbnailData);
+ });
}
void updateIconInBackground(Task task, Consumer<Task> callback) {
- mModel.getIconCache().updateIconInBackground(task, callback);
+ mModel.getIconCache().getIconInBackground(task, (icon, contentDescription, title) -> {
+ task.icon = icon;
+ task.titleDescription = contentDescription;
+ task.title = title;
+ callback.accept(task);
+ });
}
void onCloseComplete() {
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java
index 21a8268..6c3b4ad 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java
@@ -43,6 +43,8 @@
import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_VOICE_INTERACTION_WINDOW_SHOWING;
import static com.android.wm.shell.Flags.enableTinyTaskbar;
+import static java.lang.invoke.MethodHandles.Lookup.PROTECTED;
+
import android.animation.AnimatorSet;
import android.animation.ValueAnimator;
import android.app.ActivityOptions;
@@ -1515,7 +1517,8 @@
return mIsNavBarKidsMode && isThreeButtonNav();
}
- protected boolean isNavBarForceVisible() {
+ @VisibleForTesting(otherwise = PROTECTED)
+ public boolean isNavBarForceVisible() {
return mIsNavBarForceVisible;
}
@@ -1649,6 +1652,10 @@
return mControllers.uiController.canToggleHomeAllApps();
}
+ boolean isIconAlignedWithHotseat() {
+ return mControllers.uiController.isIconAlignedWithHotseat();
+ }
+
@VisibleForTesting
public TaskbarControllers getControllers() {
return mControllers;
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarHoverToolTipController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarHoverToolTipController.java
index 0443197..dd14109 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarHoverToolTipController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarHoverToolTipController.java
@@ -40,6 +40,7 @@
import com.android.launcher3.BubbleTextView;
import com.android.launcher3.R;
import com.android.launcher3.Utilities;
+import com.android.launcher3.apppairs.AppPairIcon;
import com.android.launcher3.compat.AccessibilityManagerCompat;
import com.android.launcher3.folder.FolderIcon;
import com.android.launcher3.views.ArrowTipView;
@@ -73,6 +74,8 @@
} else if (mHoverView instanceof FolderIcon
&& ((FolderIcon) mHoverView).mInfo.title != null) {
mToolTipText = ((FolderIcon) mHoverView).mInfo.title.toString();
+ } else if (mHoverView instanceof AppPairIcon) {
+ mToolTipText = ((AppPairIcon) mHoverView).getTitleTextView().getText().toString();
} else {
mToolTipText = null;
}
@@ -156,6 +159,10 @@
if (mHoverView == null || mToolTipText == null) {
return;
}
+ // Do not show tooltip if taskbar icons are transitioning to hotseat.
+ if (mActivity.isIconAlignedWithHotseat()) {
+ return;
+ }
if (mHoverView instanceof FolderIcon && !((FolderIcon) mHoverView).getIconVisible()) {
return;
}
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarManager.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarManager.java
index 8a62bf8..b90e5fd 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarManager.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarManager.java
@@ -115,6 +115,7 @@
private WindowManager mWindowManager;
private FrameLayout mTaskbarRootLayout;
private boolean mAddedWindow;
+ private boolean mIsSuspended;
private final TaskbarNavButtonController mNavButtonController;
private final ComponentCallbacks mComponentCallbacks;
@@ -443,6 +444,8 @@
*/
@VisibleForTesting
public synchronized void recreateTaskbar() {
+ if (mIsSuspended) return;
+
Trace.beginSection("recreateTaskbar");
try {
DeviceProfile dp = mUserUnlocked ?
@@ -648,6 +651,21 @@
}
}
+ /**
+ * Removes Taskbar from the window manager and prevents recreation if {@code true}.
+ * <p>
+ * Suspending is for testing purposes only; avoid calling this method in production.
+ */
+ @VisibleForTesting
+ public void setSuspended(boolean isSuspended) {
+ mIsSuspended = isSuspended;
+ if (mIsSuspended) {
+ removeTaskbarRootViewFromWindow();
+ } else {
+ addTaskbarRootViewToWindow();
+ }
+ }
+
private void addTaskbarRootViewToWindow() {
if (enableTaskbarNoRecreate() && !mAddedWindow && mTaskbarActivityContext != null) {
mWindowManager.addView(mTaskbarRootLayout,
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarRecentAppsController.kt b/quickstep/src/com/android/launcher3/taskbar/TaskbarRecentAppsController.kt
index 0a81f78..36828a8 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarRecentAppsController.kt
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarRecentAppsController.kt
@@ -24,11 +24,9 @@
import com.android.quickstep.RecentsModel
import com.android.quickstep.util.DesktopTask
import com.android.quickstep.util.GroupTask
-import com.android.systemui.shared.recents.model.Task
import com.android.window.flags.Flags.enableDesktopWindowingMode
import com.android.window.flags.Flags.enableDesktopWindowingTaskbarRunningApps
import java.io.PrintWriter
-import java.util.function.Consumer
/**
* Provides recent apps functionality, when the Taskbar Recent Apps section is enabled. Behavior:
@@ -185,9 +183,16 @@
for (groupTask in shownTasks) {
for (task in groupTask.tasks) {
- val callback =
- Consumer<Task> { controllers.taskbarViewController.onTaskUpdated(it) }
- val cancellableTask = recentsModel.iconCache.updateIconInBackground(task, callback)
+ val cancellableTask =
+ recentsModel.iconCache.getIconInBackground(task) {
+ icon,
+ contentDescription,
+ title ->
+ task.icon = icon
+ task.titleDescription = contentDescription
+ task.title = title
+ controllers.taskbarViewController.onTaskUpdated(task)
+ }
if (cancellableTask != null) {
iconLoadRequests.add(cancellableTask)
}
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarStashController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarStashController.java
index fa2d907..64fb04b 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarStashController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarStashController.java
@@ -31,6 +31,7 @@
import static com.android.launcher3.util.FlagDebugUtils.formatFlagChange;
import static com.android.quickstep.util.SystemActionConstants.SYSTEM_ACTION_ID_TASKBAR;
import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_BUBBLES_EXPANDED;
+import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_DIALOG_SHOWING;
import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_IME_SHOWING;
import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_IME_SWITCHER_SHOWING;
import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_NOTIFICATION_PANEL_VISIBLE;
@@ -1018,7 +1019,7 @@
long startDelay = 0;
updateStateForFlag(FLAG_STASHED_IN_APP_SYSUI, hasAnyFlag(systemUiStateFlags,
- SYSUI_STATE_NOTIFICATION_PANEL_VISIBLE));
+ SYSUI_STATE_NOTIFICATION_PANEL_VISIBLE | SYSUI_STATE_DIALOG_SHOWING));
boolean stashForBubbles = hasAnyFlag(FLAG_IN_OVERVIEW)
&& hasAnyFlag(systemUiStateFlags, SYSUI_STATE_BUBBLES_EXPANDED)
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarController.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarController.java
index f6b1328..4100e51 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarController.java
@@ -445,6 +445,13 @@
}
}
+ /**
+ * Removes the given bubble from the backing list of bubbles after it was dismissed by the user.
+ */
+ public void onBubbleDismissed(BubbleView bubble) {
+ mBubbles.remove(bubble.getBubble().getKey());
+ }
+
/** Tells WMShell to show the currently selected bubble. */
public void showSelectedBubble() {
if (getSelectedBubbleKey() != null) {
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarView.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarView.java
index 753237a..402b091 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarView.java
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarView.java
@@ -182,6 +182,8 @@
@Nullable
private BubbleView mDraggedBubbleView;
+ @Nullable
+ private BubbleView mDismissedByDragBubbleView;
private float mAlphaDuringDrag = 1f;
private Controller mController;
@@ -767,6 +769,10 @@
public void removeBubble(View bubble) {
if (isExpanded()) {
// TODO b/347062801 - animate the bubble bar if the last bubble is removed
+ final boolean dismissedByDrag = mDraggedBubbleView == bubble;
+ if (dismissedByDrag) {
+ mDismissedByDragBubbleView = mDraggedBubbleView;
+ }
int bubbleCount = getChildCount();
mBubbleAnimator = new BubbleAnimator(mIconSize, mExpandedBarIconsSpacing,
bubbleCount, mBubbleBarLocation.isOnLeft(isLayoutRtl()));
@@ -786,8 +792,11 @@
@Override
public void onAnimationUpdate(float animatedFraction) {
- bubble.setScaleX(1 - animatedFraction);
- bubble.setScaleY(1 - animatedFraction);
+ // don't update the scale if this bubble was dismissed by drag
+ if (!dismissedByDrag) {
+ bubble.setScaleX(1 - animatedFraction);
+ bubble.setScaleY(1 - animatedFraction);
+ }
updateBubblesLayoutProperties(mBubbleBarLocation);
invalidate();
}
@@ -818,6 +827,7 @@
updateWidth();
updateBubbleAccessibilityStates();
updateContentDescription();
+ mDismissedByDragBubbleView = null;
}
private void updateWidth() {
@@ -864,7 +874,7 @@
float elevationState = (1 - widthState);
for (int i = 0; i < bubbleCount; i++) {
BubbleView bv = (BubbleView) getChildAt(i);
- if (bv == mDraggedBubbleView) {
+ if (bv == mDraggedBubbleView || bv == mDismissedByDragBubbleView) {
// Skip the dragged bubble. Its translation is managed by the drag controller.
continue;
}
@@ -1048,6 +1058,8 @@
mDraggedBubbleView = view;
if (view != null) {
view.setZ(mDragElevation);
+ // we started dragging a bubble. reset the bubble that was previously dismissed by drag
+ mDismissedByDragBubbleView = null;
}
setIsDragging(view != null);
}
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java
index dbc78db..40e5b64 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java
@@ -526,6 +526,12 @@
mSystemUiProxy.stopBubbleDrag(location, mBarView.getRestingTopPositionOnScreen());
}
+ /** Notifies {@link BubbleBarView} that the dragged bubble was dismissed. */
+ public void onBubbleDragDismissed(BubbleView bubble) {
+ mBubbleBarController.onBubbleDismissed(bubble);
+ mBarView.removeBubble(bubble);
+ }
+
/**
* Notifies {@link BubbleBarView} that drag and all animations are finished.
*/
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleDragController.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleDragController.java
index efc747c..8316b5b 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleDragController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleDragController.java
@@ -153,6 +153,7 @@
@Override
protected void onDragDismiss() {
mBubblePinController.onDragEnd();
+ mBubbleBarViewController.onBubbleDragDismissed(bubbleView);
mBubbleBarViewController.onBubbleDragEnd();
}
diff --git a/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/QuickSwitchTouchController.java b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/QuickSwitchTouchController.java
index 93e4fbd..31e4e33 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/QuickSwitchTouchController.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/QuickSwitchTouchController.java
@@ -151,7 +151,7 @@
int sysuiFlags = 0;
TaskView tv = mOverviewPanel.getTaskViewAt(0);
if (tv != null) {
- sysuiFlags = tv.getFirstThumbnailViewDeprecated().getSysUiStatusNavFlags();
+ sysuiFlags = tv.getTaskContainers().getFirst().getSysUiStatusNavFlags();
}
mLauncher.getSystemUiController().updateUiState(UI_STATE_FULLSCREEN_TASK, sysuiFlags);
} else {
diff --git a/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java b/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java
index bdbe826..867533c 100644
--- a/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java
+++ b/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java
@@ -926,7 +926,7 @@
TaskView runningTask = mRecentsView.getRunningTaskView();
TaskView centermostTask = mRecentsView.getTaskViewNearestToCenterOfScreen();
int centermostTaskFlags = centermostTask == null ? 0
- : centermostTask.getFirstThumbnailViewDeprecated().getSysUiStatusNavFlags();
+ : centermostTask.getTaskContainers().getFirst().getSysUiStatusNavFlags();
boolean swipeUpThresholdPassed = windowProgress > 1 - UPDATE_SYSUI_FLAGS_THRESHOLD;
boolean quickswitchThresholdPassed = centermostTask != runningTask;
@@ -2445,21 +2445,20 @@
finishRecentsAnimationOnTasksAppeared(null /* onFinishComplete */);
return;
}
- Optional<RemoteAnimationTarget> taskTargetOptional =
- Arrays.stream(appearedTaskTargets)
- .filter(mGestureState.mLastStartedTaskIdPredicate)
- .findFirst();
- if (!taskTargetOptional.isPresent()) {
+ RemoteAnimationTarget[] taskTargets = Arrays.stream(appearedTaskTargets)
+ .filter(mGestureState.mLastStartedTaskIdPredicate)
+ .toArray(RemoteAnimationTarget[]::new);
+ if (taskTargets.length == 0) {
ActiveGestureLog.INSTANCE.addLog("No appeared task matching started task id");
finishRecentsAnimationOnTasksAppeared(null /* onFinishComplete */);
return;
}
- RemoteAnimationTarget taskTarget = taskTargetOptional.get();
+ RemoteAnimationTarget taskTarget = taskTargets[0];
TaskView taskView = mRecentsView == null
? null : mRecentsView.getTaskViewByTaskId(taskTarget.taskId);
- if (taskView == null
- || !taskView.getFirstThumbnailViewDeprecated().shouldShowSplashView()) {
- ActiveGestureLog.INSTANCE.addLog("Invalid task view splash state");
+ if (taskView == null || taskView.getTaskContainers().stream().noneMatch(
+ TaskContainer::getShouldShowSplashView)) {
+ ActiveGestureLog.INSTANCE.addLog("Splash not needed");
finishRecentsAnimationOnTasksAppeared(null /* onFinishComplete */);
return;
}
@@ -2468,13 +2467,13 @@
finishRecentsAnimationOnTasksAppeared(null /* onFinishComplete */);
return;
}
- animateSplashScreenExit(mContainer, appearedTaskTargets, taskTarget.leash);
+ animateSplashScreenExit(mContainer, appearedTaskTargets, taskTargets);
}
private void animateSplashScreenExit(
@NonNull T activity,
@NonNull RemoteAnimationTarget[] appearedTaskTargets,
- @NonNull SurfaceControl leash) {
+ @NonNull RemoteAnimationTarget[] animatingTargets) {
ViewGroup splashView = activity.getDragLayer();
final QuickstepLauncher quickstepLauncher = activity instanceof QuickstepLauncher
? (QuickstepLauncher) activity : null;
@@ -2492,26 +2491,28 @@
}
surfaceApplier.scheduleApply(transaction);
- SplashScreenExitAnimationUtils.startAnimations(splashView, leash,
- mSplashMainWindowShiftLength, new TransactionPool(), new Rect(),
- SPLASH_ANIMATION_DURATION, SPLASH_FADE_OUT_DURATION,
- /* iconStartAlpha= */ 0, /* brandingStartAlpha= */ 0,
- SPLASH_APP_REVEAL_DELAY, SPLASH_APP_REVEAL_DURATION,
- new AnimatorListenerAdapter() {
- @Override
- public void onAnimationEnd(Animator animation) {
- // Hiding launcher which shows the app surface behind, then
- // finishing recents to the app. After transition finish, showing
- // the views on launcher again, so it can be visible when next
- // animation starts.
- splashView.setAlpha(0);
- if (quickstepLauncher != null) {
- quickstepLauncher.getDepthController()
- .pauseBlursOnWindows(false);
+ for (RemoteAnimationTarget target : animatingTargets) {
+ SplashScreenExitAnimationUtils.startAnimations(splashView, target.leash,
+ mSplashMainWindowShiftLength, new TransactionPool(), target.screenSpaceBounds,
+ SPLASH_ANIMATION_DURATION, SPLASH_FADE_OUT_DURATION,
+ /* iconStartAlpha= */ 0, /* brandingStartAlpha= */ 0,
+ SPLASH_APP_REVEAL_DELAY, SPLASH_APP_REVEAL_DURATION,
+ new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ // Hiding launcher which shows the app surface behind, then
+ // finishing recents to the app. After transition finish, showing
+ // the views on launcher again, so it can be visible when next
+ // animation starts.
+ splashView.setAlpha(0);
+ if (quickstepLauncher != null) {
+ quickstepLauncher.getDepthController()
+ .pauseBlursOnWindows(false);
+ }
+ finishRecentsAnimationOnTasksAppeared(() -> splashView.setAlpha(1));
}
- finishRecentsAnimationOnTasksAppeared(() -> splashView.setAlpha(1));
- }
- });
+ });
+ }
}
private void finishRecentsAnimationOnTasksAppeared(Runnable onFinishComplete) {
diff --git a/quickstep/src/com/android/quickstep/RecentsActivity.java b/quickstep/src/com/android/quickstep/RecentsActivity.java
index 13e9844..18461a6 100644
--- a/quickstep/src/com/android/quickstep/RecentsActivity.java
+++ b/quickstep/src/com/android/quickstep/RecentsActivity.java
@@ -132,6 +132,13 @@
* Init drag layer and overview panel views.
*/
protected void setupViews() {
+ SystemUiProxy systemUiProxy = SystemUiProxy.INSTANCE.get(this);
+ // SplitSelectStateController needs to be created before setContentView()
+ mSplitSelectStateController =
+ new SplitSelectStateController(this, mHandler, getStateManager(),
+ null /* depthController */, getStatsLogManager(),
+ systemUiProxy, RecentsModel.INSTANCE.get(this),
+ null /*activityBackCallback*/);
inflateRootView(R.layout.fallback_recents_activity);
setContentView(getRootView());
mDragLayer = findViewById(R.id.drag_layer);
@@ -139,12 +146,6 @@
mFallbackRecentsView = findViewById(R.id.overview_panel);
mActionsView = findViewById(R.id.overview_actions_view);
getRootView().getSysUiScrim().getSysUIProgress().updateValue(0);
- SystemUiProxy systemUiProxy = SystemUiProxy.INSTANCE.get(this);
- mSplitSelectStateController =
- new SplitSelectStateController(this, mHandler, getStateManager(),
- null /* depthController */, getStatsLogManager(),
- systemUiProxy, RecentsModel.INSTANCE.get(this),
- null /*activityBackCallback*/);
mDragLayer.recreateControllers();
if (enableDesktopWindowingMode()) {
mDesktopRecentsTransitionController = new DesktopRecentsTransitionController(
diff --git a/quickstep/src/com/android/quickstep/TaskIconCache.java b/quickstep/src/com/android/quickstep/TaskIconCache.java
index e6febff..b3a9199 100644
--- a/quickstep/src/com/android/quickstep/TaskIconCache.java
+++ b/quickstep/src/com/android/quickstep/TaskIconCache.java
@@ -55,7 +55,6 @@
import com.android.systemui.shared.system.PackageManagerWrapper;
import java.util.concurrent.Executor;
-import java.util.function.Consumer;
/**
* Manages the caching of task icons and related data.
@@ -103,21 +102,21 @@
* @param callback The callback to receive the task after its data has been populated.
* @return A cancelable handle to the request
*/
- public CancellableTask updateIconInBackground(Task task, Consumer<Task> callback) {
+ public CancellableTask getIconInBackground(Task task, GetTaskIconCallback callback) {
Preconditions.assertUIThread();
if (task.icon != null) {
// Nothing to load, the icon is already loaded
- callback.accept(task);
+ callback.onTaskIconReceived(task.icon, task.titleDescription, task.title);
return null;
}
CancellableTask<TaskCacheEntry> request = new CancellableTask<>(
() -> getCacheEntry(task),
MAIN_EXECUTOR,
result -> {
- task.icon = result.icon;
- task.titleDescription = result.contentDescription;
- task.title = result.title;
- callback.accept(task);
+ callback.onTaskIconReceived(
+ result.icon,
+ result.contentDescription,
+ result.title);
dispatchIconUpdate(task.key.id);
}
);
@@ -280,6 +279,12 @@
public String title = "";
}
+ /** Callback used when retrieving app icons from cache. */
+ public interface GetTaskIconCallback {
+ /** Called when task icon is retrieved. */
+ void onTaskIconReceived(Drawable icon, String contentDescription, String title);
+ }
+
void registerTaskVisualsChangeListener(TaskVisualsChangeListener newListener) {
mTaskVisualsChangeListener = newListener;
}
diff --git a/quickstep/src/com/android/quickstep/TaskOverlayFactory.java b/quickstep/src/com/android/quickstep/TaskOverlayFactory.java
index b7f3f65..80902e3 100644
--- a/quickstep/src/com/android/quickstep/TaskOverlayFactory.java
+++ b/quickstep/src/com/android/quickstep/TaskOverlayFactory.java
@@ -16,18 +16,22 @@
package com.android.quickstep;
+import static com.android.launcher3.Flags.enableRefactorTaskThumbnail;
import static com.android.quickstep.views.OverviewActionsView.DISABLED_NO_THUMBNAIL;
import static com.android.quickstep.views.OverviewActionsView.DISABLED_ROTATED;
import android.annotation.SuppressLint;
import android.content.Context;
+import android.graphics.Bitmap;
import android.graphics.Insets;
import android.graphics.Matrix;
import android.graphics.Rect;
+import android.graphics.RectF;
import android.os.Build;
import android.view.View;
import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import com.android.launcher3.BaseActivity;
@@ -38,6 +42,7 @@
import com.android.launcher3.util.ResourceBasedOverride;
import com.android.launcher3.views.ActivityContext;
import com.android.launcher3.views.Snackbar;
+import com.android.quickstep.task.util.TaskOverlayHelper;
import com.android.quickstep.util.RecentsOrientedState;
import com.android.quickstep.views.DesktopTaskView;
import com.android.quickstep.views.GroupedTaskView;
@@ -47,7 +52,6 @@
import com.android.quickstep.views.TaskContainer;
import com.android.quickstep.views.TaskView;
import com.android.systemui.shared.recents.model.Task;
-import com.android.systemui.shared.recents.model.ThumbnailData;
import java.util.ArrayList;
import java.util.List;
@@ -128,12 +132,43 @@
private T mActionsView;
protected ImageActionsApi mImageApi;
+ protected TaskOverlayHelper mHelper;
protected TaskOverlay(TaskContainer taskContainer) {
mApplicationContext = taskContainer.getTaskView().getContext().getApplicationContext();
mTaskContainer = taskContainer;
- mImageApi = new ImageActionsApi(
- mApplicationContext, mTaskContainer::getThumbnail);
+ if (enableRefactorTaskThumbnail()) {
+ mHelper = new TaskOverlayHelper(mTaskContainer.getTask(), this);
+ }
+ mImageApi = new ImageActionsApi(mApplicationContext, this::getThumbnail);
+ }
+
+ /**
+ * Initialize the overlay when a Task is bound to the TaskView.
+ */
+ public void init() {
+ if (enableRefactorTaskThumbnail()) {
+ mHelper.init();
+ }
+ }
+
+ /**
+ * Destroy the overlay when the TaskView is recycled.
+ */
+ public void destroy() {
+ if (enableRefactorTaskThumbnail()) {
+ mHelper.destroy();
+ }
+ }
+
+ protected @Nullable Bitmap getThumbnail() {
+ return enableRefactorTaskThumbnail() ? mHelper.getEnabledState().getThumbnail()
+ : mTaskContainer.getThumbnailViewDeprecated().getThumbnail();
+ }
+
+ protected boolean isRealSnapshot() {
+ return enableRefactorTaskThumbnail() ? mHelper.getEnabledState().isRealSnapshot()
+ : mTaskContainer.getThumbnailViewDeprecated().isRealSnapshot();
}
protected T getActionsView() {
@@ -152,14 +187,13 @@
/**
* Called when the current task is interactive for the user
*/
- public void initOverlay(Task task, ThumbnailData thumbnail, Matrix matrix,
+ public void initOverlay(Task task, @Nullable Bitmap thumbnail, Matrix matrix,
boolean rotated) {
getActionsView().updateDisabledFlags(DISABLED_NO_THUMBNAIL, thumbnail == null);
if (thumbnail != null) {
getActionsView().updateDisabledFlags(DISABLED_ROTATED, rotated);
- boolean isAllowedByPolicy = mTaskContainer.isRealSnapshot();
- getActionsView().setCallbacks(new OverlayUICallbacksImpl(isAllowedByPolicy, task));
+ getActionsView().setCallbacks(new OverlayUICallbacksImpl(isRealSnapshot(), task));
}
}
@@ -183,8 +217,8 @@
*/
@SuppressLint("NewApi")
protected void saveScreenshot(Task task) {
- if (mTaskContainer.isRealSnapshot()) {
- mImageApi.saveScreenshot(mTaskContainer.getThumbnail(),
+ if (isRealSnapshot()) {
+ mImageApi.saveScreenshot(getThumbnail(),
getTaskSnapshotBounds(), getTaskSnapshotInsets(), task.key);
} else {
showBlockedByPolicyMessage();
@@ -259,7 +293,36 @@
*/
@RequiresApi(api = Build.VERSION_CODES.Q)
public Insets getTaskSnapshotInsets() {
- return mTaskContainer.getScaledInsets();
+ Bitmap thumbnail = getThumbnail();
+ if (thumbnail == null) {
+ return Insets.NONE;
+ }
+
+ RectF bitmapRect = new RectF(
+ 0,
+ 0,
+ thumbnail.getWidth(),
+ thumbnail.getHeight());
+ View snapshotView = mTaskContainer.getSnapshotView();
+ RectF viewRect = new RectF(0, 0, snapshotView.getMeasuredWidth(),
+ snapshotView.getMeasuredHeight());
+
+ // The position helper matrix tells us how to transform the bitmap to fit the view, the
+ // inverse tells us where the view would be in the bitmaps coordinates. The insets are
+ // the difference between the bitmap bounds and the projected view bounds.
+ Matrix boundsToBitmapSpace = new Matrix();
+ Matrix thumbnailMatrix = enableRefactorTaskThumbnail()
+ ? mHelper.getEnabledState().getThumbnailMatrix()
+ : mTaskContainer.getThumbnailViewDeprecated().getThumbnailMatrix();
+ thumbnailMatrix.invert(boundsToBitmapSpace);
+ RectF boundsInBitmapSpace = new RectF();
+ boundsToBitmapSpace.mapRect(boundsInBitmapSpace, viewRect);
+
+ RecentsViewContainer container = RecentsViewContainer.containerFromContext(
+ getTaskView().getContext());
+ int bottomInset = container.getDeviceProfile().isTablet
+ ? Math.round(bitmapRect.bottom - boundsInBitmapSpace.bottom) : 0;
+ return Insets.of(0, 0, 0, bottomInset);
}
/**
diff --git a/quickstep/src/com/android/quickstep/TaskThumbnailCache.java b/quickstep/src/com/android/quickstep/TaskThumbnailCache.java
index 38e927f..3c6c3e4 100644
--- a/quickstep/src/com/android/quickstep/TaskThumbnailCache.java
+++ b/quickstep/src/com/android/quickstep/TaskThumbnailCache.java
@@ -131,8 +131,7 @@
Preconditions.assertUIThread();
// Fetch the thumbnail for this task and put it in the cache
if (task.thumbnail == null) {
- updateThumbnailInBackground(task.key, lowResolution,
- t -> task.thumbnail = t);
+ getThumbnailInBackground(task.key, lowResolution, t -> task.thumbnail = t);
}
}
@@ -145,13 +144,13 @@
}
/**
- * Asynchronously fetches the icon and other task data for the given {@param task}.
+ * Asynchronously fetches the thumbnail for the given {@code task}.
*
* @param callback The callback to receive the task after its data has been populated.
* @return A cancelable handle to the request
*/
@Override
- public CancellableTask<ThumbnailData> updateThumbnailInBackground(
+ public CancellableTask<ThumbnailData> getThumbnailInBackground(
Task task, @NonNull Consumer<ThumbnailData> callback) {
Preconditions.assertUIThread();
@@ -164,10 +163,7 @@
return null;
}
- return updateThumbnailInBackground(task.key, !mHighResLoadingState.isEnabled(), t -> {
- task.thumbnail = t;
- callback.accept(t);
- });
+ return getThumbnailInBackground(task.key, !mHighResLoadingState.isEnabled(), callback);
}
/**
@@ -187,7 +183,7 @@
return newSize > oldSize;
}
- private CancellableTask<ThumbnailData> updateThumbnailInBackground(TaskKey key,
+ private CancellableTask<ThumbnailData> getThumbnailInBackground(TaskKey key,
boolean lowResolution, Consumer<ThumbnailData> callback) {
Preconditions.assertUIThread();
diff --git a/quickstep/src/com/android/quickstep/recents/data/TasksRepository.kt b/quickstep/src/com/android/quickstep/recents/data/TasksRepository.kt
index b21a1b4..9f3ef4a 100644
--- a/quickstep/src/com/android/quickstep/recents/data/TasksRepository.kt
+++ b/quickstep/src/com/android/quickstep/recents/data/TasksRepository.kt
@@ -67,14 +67,15 @@
this.visibleTaskIds.value = visibleTaskIdList.toSet()
}
- /** Flow wrapper for [TaskThumbnailDataSource.updateThumbnailInBackground] api */
+ /** Flow wrapper for [TaskThumbnailDataSource.getThumbnailInBackground] api */
private fun getThumbnailDataRequest(task: Task): ThumbnailDataRequest =
flow {
emit(task.key.id to task.thumbnail)
val thumbnailDataResult: ThumbnailData? =
suspendCancellableCoroutine { continuation ->
val cancellableTask =
- taskThumbnailDataSource.updateThumbnailInBackground(task) {
+ taskThumbnailDataSource.getThumbnailInBackground(task) {
+ task.thumbnail = it
continuation.resume(it)
}
continuation.invokeOnCancellation { cancellableTask?.cancel() }
@@ -94,12 +95,7 @@
tasks.filter { it.key.id in visibleIds }
}
val visibleThumbnailDataRequests: Flow<List<ThumbnailDataRequest>> =
- visibleTasks.map {
- it.map { visibleTask ->
- val taskCopy = Task(visibleTask).apply { thumbnail = visibleTask.thumbnail }
- getThumbnailDataRequest(taskCopy)
- }
- }
+ visibleTasks.map { visibleTasksList -> visibleTasksList.map(::getThumbnailDataRequest) }
return visibleThumbnailDataRequests.flatMapLatest {
thumbnailRequestFlows: List<ThumbnailDataRequest> ->
if (thumbnailRequestFlows.isEmpty()) {
diff --git a/quickstep/src/com/android/quickstep/recents/viewmodel/RecentsViewData.kt b/quickstep/src/com/android/quickstep/recents/viewmodel/RecentsViewData.kt
index 28212cf..fdb62df 100644
--- a/quickstep/src/com/android/quickstep/recents/viewmodel/RecentsViewData.kt
+++ b/quickstep/src/com/android/quickstep/recents/viewmodel/RecentsViewData.kt
@@ -24,4 +24,11 @@
// This is typically a View concern but it is used to invalidate rendering in other Views
val scale = MutableStateFlow(1f)
+
+ // Whether the current RecentsView state supports task overlays.
+ // TODO(b/331753115): Derive from RecentsView state flow once migrated to MVVM.
+ val overlayEnabled = MutableStateFlow(false)
+
+ // The settled set of visible taskIds that is updated after RecentsView scroll settles.
+ val settledFullyVisibleTaskIds = MutableStateFlow(emptySet<Int>())
}
diff --git a/quickstep/src/com/android/quickstep/task/thumbnail/TaskOverlayUiState.kt b/quickstep/src/com/android/quickstep/task/thumbnail/TaskOverlayUiState.kt
new file mode 100644
index 0000000..feee11f
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/task/thumbnail/TaskOverlayUiState.kt
@@ -0,0 +1,31 @@
+/*
+ * 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.task.thumbnail
+
+import android.graphics.Bitmap
+import android.graphics.Matrix
+
+/** Ui state for [com.android.quickstep.TaskOverlayFactory.TaskOverlay] */
+sealed class TaskOverlayUiState {
+ data object Disabled : TaskOverlayUiState()
+
+ data class Enabled(
+ val isRealSnapshot: Boolean,
+ val thumbnail: Bitmap?,
+ val thumbnailMatrix: Matrix
+ ) : TaskOverlayUiState()
+}
diff --git a/quickstep/src/com/android/quickstep/task/thumbnail/data/TaskThumbnailDataSource.kt b/quickstep/src/com/android/quickstep/task/thumbnail/data/TaskThumbnailDataSource.kt
index 55598f0..986acbe 100644
--- a/quickstep/src/com/android/quickstep/task/thumbnail/data/TaskThumbnailDataSource.kt
+++ b/quickstep/src/com/android/quickstep/task/thumbnail/data/TaskThumbnailDataSource.kt
@@ -22,7 +22,7 @@
import java.util.function.Consumer
interface TaskThumbnailDataSource {
- fun updateThumbnailInBackground(
+ fun getThumbnailInBackground(
task: Task,
callback: Consumer<ThumbnailData>
): CancellableTask<ThumbnailData>?
diff --git a/quickstep/src/com/android/quickstep/task/util/TaskOverlayHelper.kt b/quickstep/src/com/android/quickstep/task/util/TaskOverlayHelper.kt
new file mode 100644
index 0000000..de39584
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/task/util/TaskOverlayHelper.kt
@@ -0,0 +1,90 @@
+/*
+ * 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.task.util
+
+import android.util.Log
+import com.android.quickstep.TaskOverlayFactory
+import com.android.quickstep.task.thumbnail.TaskOverlayUiState
+import com.android.quickstep.task.thumbnail.TaskOverlayUiState.Disabled
+import com.android.quickstep.task.thumbnail.TaskOverlayUiState.Enabled
+import com.android.quickstep.task.viewmodel.TaskOverlayViewModel
+import com.android.quickstep.views.RecentsView
+import com.android.quickstep.views.RecentsViewContainer
+import com.android.systemui.shared.recents.model.Task
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.MainScope
+import kotlinx.coroutines.launch
+
+/**
+ * Helper for [TaskOverlayFactory.TaskOverlay] to interact with [TaskOverlayViewModel], this helper
+ * should merge with [TaskOverlayFactory.TaskOverlay] when it's migrated to MVVM.
+ */
+class TaskOverlayHelper(val task: Task, val overlay: TaskOverlayFactory.TaskOverlay<*>) {
+ private lateinit var job: Job
+ private var uiState: TaskOverlayUiState = Disabled
+
+ // TODO(b/335649589): Ideally create and obtain this from DI. This ViewModel should be scoped
+ // to [TaskView], and also shared between [TaskView] and [TaskThumbnailView]
+ // This is using a lazy for now because the dependencies cannot be obtained without DI.
+ private val taskOverlayViewModel by lazy {
+ val recentsView =
+ RecentsViewContainer.containerFromContext<RecentsViewContainer>(
+ overlay.taskView.context
+ )
+ .getOverviewPanel<RecentsView<*, *>>()
+ TaskOverlayViewModel(task, recentsView.mRecentsViewData, recentsView.mTasksRepository)
+ }
+
+ // TODO(b/331753115): TaskOverlay should listen for state changes and react.
+ val enabledState: Enabled
+ get() = uiState as Enabled
+
+ fun init() {
+ // TODO(b/335396935): This should be changed to TaskView's scope.
+ job =
+ MainScope().launch {
+ taskOverlayViewModel.overlayState.collect {
+ uiState = it
+ if (it is Enabled) {
+ Log.d(
+ TAG,
+ "initOverlay - taskId: ${task.key.id}, thumbnail: ${it.thumbnail}"
+ )
+ overlay.initOverlay(
+ task,
+ it.thumbnail,
+ it.thumbnailMatrix,
+ /* rotated= */ false
+ )
+ } else {
+ Log.d(TAG, "reset - taskId: ${task.key.id}")
+ overlay.reset()
+ }
+ }
+ }
+ }
+
+ fun destroy() {
+ job.cancel()
+ uiState = Disabled
+ overlay.reset()
+ }
+
+ companion object {
+ private const val TAG = "TaskOverlayHelper"
+ }
+}
diff --git a/quickstep/src/com/android/quickstep/task/viewmodel/TaskOverlayViewModel.kt b/quickstep/src/com/android/quickstep/task/viewmodel/TaskOverlayViewModel.kt
new file mode 100644
index 0000000..4682323
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/task/viewmodel/TaskOverlayViewModel.kt
@@ -0,0 +1,59 @@
+/*
+ * 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.task.viewmodel
+
+import android.graphics.Matrix
+import com.android.quickstep.recents.data.RecentTasksRepository
+import com.android.quickstep.recents.viewmodel.RecentsViewData
+import com.android.quickstep.task.thumbnail.TaskOverlayUiState.Disabled
+import com.android.quickstep.task.thumbnail.TaskOverlayUiState.Enabled
+import com.android.systemui.shared.recents.model.Task
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.distinctUntilChangedBy
+import kotlinx.coroutines.flow.map
+
+/** View model for TaskOverlay */
+class TaskOverlayViewModel(
+ task: Task,
+ recentsViewData: RecentsViewData,
+ tasksRepository: RecentTasksRepository,
+) {
+ val overlayState =
+ combine(
+ recentsViewData.overlayEnabled,
+ recentsViewData.settledFullyVisibleTaskIds.map { it.contains(task.key.id) },
+ tasksRepository
+ .getTaskDataById(task.key.id)
+ .map { it?.thumbnail }
+ .distinctUntilChangedBy { it?.snapshotId }
+ ) { isOverlayEnabled, isFullyVisible, thumbnailData ->
+ if (isOverlayEnabled && isFullyVisible) {
+ Enabled(
+ isRealSnapshot = (thumbnailData?.isRealSnapshot ?: false) && !task.isLocked,
+ thumbnailData?.thumbnail,
+ // TODO(b/343101424): Use PreviewPositionHelper, listen from a common source
+ // with
+ // TaskThumbnailView.
+ Matrix.IDENTITY_MATRIX
+ )
+ } else {
+ Disabled
+ }
+ }
+ .distinctUntilChanged()
+}
diff --git a/quickstep/src/com/android/quickstep/util/SplitWithKeyboardShortcutController.java b/quickstep/src/com/android/quickstep/util/SplitWithKeyboardShortcutController.java
index 5e42b90..27fb31d 100644
--- a/quickstep/src/com/android/quickstep/util/SplitWithKeyboardShortcutController.java
+++ b/quickstep/src/com/android/quickstep/util/SplitWithKeyboardShortcutController.java
@@ -138,12 +138,13 @@
mLauncher, mLauncher.getDragLayer(),
controller.screenshotTask(runningTaskInfo.taskId).getThumbnail(),
null /* icon */, startingTaskRect);
+ Task task = Task.from(new Task.TaskKey(runningTaskInfo), runningTaskInfo,
+ false /* isLocked */);
RecentsModel.INSTANCE.get(mLauncher.getApplicationContext())
.getIconCache()
- .updateIconInBackground(
- Task.from(new Task.TaskKey(runningTaskInfo), runningTaskInfo,
- false /* isLocked */),
- (task) -> floatingTaskView.setIcon(task.icon));
+ .getIconInBackground(
+ task,
+ (icon, contentDescription, title) -> floatingTaskView.setIcon(icon));
floatingTaskView.setAlpha(1);
floatingTaskView.addStagingAnimation(anim, startingTaskRect, mTempRect,
false /* fadeWithThumbnail */, true /* isStagedTask */);
diff --git a/quickstep/src/com/android/quickstep/util/SwipePipToHomeAnimator.java b/quickstep/src/com/android/quickstep/util/SwipePipToHomeAnimator.java
index 88c3a08..48ed67b 100644
--- a/quickstep/src/com/android/quickstep/util/SwipePipToHomeAnimator.java
+++ b/quickstep/src/com/android/quickstep/util/SwipePipToHomeAnimator.java
@@ -453,8 +453,9 @@
}
// adjust the mSourceRectHint / mAppBounds by display cutout if applicable.
if (mSourceRectHint != null && mDisplayCutoutInsets != null) {
- if (mFromRotation == Surface.ROTATION_0 && mDisplayCutoutInsets.top >= 0) {
- // TODO: this is to special case the issues on Pixel Foldable device(s).
+ if (mFromRotation == Surface.ROTATION_0) {
+ // TODO: this is to special case the issues on Foldable device
+ // with display cutout.
mSourceRectHint.offset(mDisplayCutoutInsets.left, mDisplayCutoutInsets.top);
} else if (mFromRotation == Surface.ROTATION_90) {
mSourceRectHint.offset(mDisplayCutoutInsets.left, mDisplayCutoutInsets.top);
diff --git a/quickstep/src/com/android/quickstep/views/RecentsView.java b/quickstep/src/com/android/quickstep/views/RecentsView.java
index 25d102b..22f3c1d 100644
--- a/quickstep/src/com/android/quickstep/views/RecentsView.java
+++ b/quickstep/src/com/android/quickstep/views/RecentsView.java
@@ -135,7 +135,6 @@
import com.android.launcher3.AbstractFloatingView;
import com.android.launcher3.BaseActivity.MultiWindowModeChangedListener;
import com.android.launcher3.DeviceProfile;
-import com.android.launcher3.Flags;
import com.android.launcher3.Insettable;
import com.android.launcher3.InvariantDeviceProfile;
import com.android.launcher3.PagedView;
@@ -229,10 +228,12 @@
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
+import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
+import java.util.Set;
import java.util.function.Consumer;
import java.util.stream.Collectors;
@@ -1015,14 +1016,17 @@
@Nullable
public Task onTaskThumbnailChanged(int taskId, ThumbnailData thumbnailData) {
if (mHandleTaskStackChanges) {
- TaskView taskView = getTaskViewByTaskId(taskId);
- if (taskView != null) {
- for (TaskContainer container : taskView.getTaskContainers()) {
- if (container == null || taskId != container.getTask().key.id) {
- continue;
+ // TODO(b/342560598): Handle onTaskThumbnailChanged for new TTV.
+ if (!enableRefactorTaskThumbnail()) {
+ TaskView taskView = getTaskViewByTaskId(taskId);
+ if (taskView != null) {
+ for (TaskContainer container : taskView.getTaskContainers()) {
+ if (container == null || taskId != container.getTask().key.id) {
+ continue;
+ }
+ container.getThumbnailViewDeprecated().setThumbnail(container.getTask(),
+ thumbnailData);
}
- container.getThumbnailViewDeprecated().setThumbnail(container.getTask(),
- thumbnailData);
}
}
}
@@ -1059,6 +1063,10 @@
@Nullable
public TaskView updateThumbnail(
HashMap<Integer, ThumbnailData> thumbnailData, boolean refreshNow) {
+ if (enableRefactorTaskThumbnail()) {
+ // TODO(b/342560598): Handle updateThumbnail for new TTV.
+ return null;
+ }
TaskView updatedTaskView = null;
for (Map.Entry<Integer, ThumbnailData> entry : thumbnailData.entrySet()) {
Integer id = entry.getKey();
@@ -2390,12 +2398,11 @@
if (containers.isEmpty()) {
continue;
}
- int index = indexOfChild(taskView);
boolean visible;
if (showAsGrid()) {
visible = isTaskViewWithinBounds(taskView, visibleStart, visibleEnd);
} else {
- visible = lower <= index && index <= upper;
+ visible = lower <= i && i <= upper;
}
if (visible) {
// Default update all non-null tasks, then remove running ones
@@ -2426,7 +2433,7 @@
}
taskView.onTaskListVisibilityChanged(true /* visible */, changes);
}
- mHasVisibleTaskData.put(task.key.id, visible);
+ mHasVisibleTaskData.put(task.key.id, true);
}
} else {
for (TaskContainer container : containers) {
@@ -2914,7 +2921,7 @@
int prevRunningTaskViewId = mRunningTaskViewId;
mRunningTaskViewId = runningTaskViewId;
- if (Flags.enableRefactorTaskThumbnail()) {
+ if (enableRefactorTaskThumbnail()) {
TaskView previousRunningTaskView = getTaskViewFromTaskViewId(prevRunningTaskViewId);
if (previousRunningTaskView != null) {
previousRunningTaskView.notifyIsRunningTaskUpdated();
@@ -4860,14 +4867,16 @@
== mSplitSelectStateController.getInitialTaskId();
TaskContainer taskContainer = mSplitHiddenTaskView
.getTaskContainers().get(primaryTaskSelected ? 1 : 0);
- TaskThumbnailViewDeprecated thumbnail = taskContainer.getThumbnailViewDeprecated();
mSplitSelectStateController.getSplitAnimationController()
.addInitialSplitFromPair(taskContainer, builder,
mContainer.getDeviceProfile(),
mSplitHiddenTaskView.getWidth(), mSplitHiddenTaskView.getHeight(),
primaryTaskSelected);
builder.addOnFrameCallback(() ->{
- thumbnail.refreshSplashView();
+ // TODO(b/334826842): Handle splash icon for new TTV.
+ if (!enableRefactorTaskThumbnail()) {
+ taskContainer.getThumbnailViewDeprecated().refreshSplashView();
+ }
mSplitHiddenTaskView.updateSnapshotRadius();
});
} else if (isInitiatingSplitFromTaskView) {
@@ -5259,7 +5268,7 @@
updateGridProperties();
updateScrollSynchronously();
- int targetSysUiFlags = tv.getFirstThumbnailViewDeprecated().getSysUiStatusNavFlags();
+ int targetSysUiFlags = tv.getTaskContainers().getFirst().getSysUiStatusNavFlags();
final boolean[] passedOverviewThreshold = new boolean[] {false};
ValueAnimator progressAnim = ValueAnimator.ofFloat(0, 1);
progressAnim.addUpdateListener(animator -> {
@@ -5346,6 +5355,43 @@
updateCurrentTaskActionsVisibility();
loadVisibleTaskData(TaskView.FLAG_UPDATE_ALL);
updateEnabledOverlays();
+
+ if (enableRefactorTaskThumbnail()) {
+ int screenStart = 0;
+ int screenEnd = 0;
+ int centerPageIndex = 0;
+ if (showAsGrid()) {
+ screenStart = getPagedOrientationHandler().getPrimaryScroll(this);
+ int pageOrientedSize = getPagedOrientationHandler().getMeasuredSize(this);
+ screenEnd = screenStart + pageOrientedSize;
+ } else {
+ centerPageIndex = getPageNearestToCenterOfScreen();
+ }
+
+ Set<Integer> fullyVisibleTaskIds = new HashSet<>();
+
+ // Update the task data for the in/visible children
+ for (int i = 0; i < getTaskViewCount(); i++) {
+ TaskView taskView = requireTaskViewAt(i);
+ List<TaskContainer> containers = taskView.getTaskContainers();
+ if (containers.isEmpty()) {
+ continue;
+ }
+ boolean isFullyVisible;
+ if (showAsGrid()) {
+ isFullyVisible = isTaskViewFullyWithinBounds(taskView, screenStart,
+ screenEnd);
+ } else {
+ isFullyVisible = i == centerPageIndex;
+ }
+ if (isFullyVisible) {
+ List<Integer> taskIds = containers.stream().map(
+ taskContainer -> taskContainer.getTask().key.id).toList();
+ fullyVisibleTaskIds.addAll(taskIds);
+ }
+ }
+ mRecentsViewData.getSettledFullyVisibleTaskIds().setValue(fullyVisibleTaskIds);
+ }
}
@Override
@@ -5896,6 +5942,7 @@
if (mOverlayEnabled != overlayEnabled) {
mOverlayEnabled = overlayEnabled;
updateEnabledOverlays();
+ mRecentsViewData.getOverlayEnabled().setValue(overlayEnabled);
}
}
@@ -5947,6 +5994,12 @@
}
private void switchToScreenshotInternal(Runnable onFinishRunnable) {
+ // TODO(b/342560598): Handle switchToScreenshot for new TTV.
+ if (enableRefactorTaskThumbnail()) {
+ onFinishRunnable.run();
+ return;
+ }
+
TaskView taskView = getRunningTaskView();
if (taskView == null) {
onFinishRunnable.run();
@@ -6034,7 +6087,7 @@
* tasks to be dimmed while other elements in the recents view are left alone.
*/
public void showForegroundScrim(boolean show) {
- // TODO(b/335606129) Add scrim response into new TTV - this is called from overlay
+ // TODO(b/349601769) Add scrim response into new TTV - this is called from overlay
if (!show && mColorTint == 0) {
if (mTintingAnimator != null) {
mTintingAnimator.cancel();
diff --git a/quickstep/src/com/android/quickstep/views/TaskContainer.kt b/quickstep/src/com/android/quickstep/views/TaskContainer.kt
index cfdee6c..2e01e7e 100644
--- a/quickstep/src/com/android/quickstep/views/TaskContainer.kt
+++ b/quickstep/src/com/android/quickstep/views/TaskContainer.kt
@@ -18,9 +18,9 @@
import android.content.Intent
import android.graphics.Bitmap
-import android.graphics.Insets
import android.view.View
-import com.android.launcher3.Flags
+import com.android.launcher3.Flags.enableRefactorTaskThumbnail
+import com.android.launcher3.Flags.privateSpaceRestrictAccessibilityDrag
import com.android.launcher3.LauncherSettings
import com.android.launcher3.model.data.ItemInfoWithIcon
import com.android.launcher3.model.data.WorkspaceItemInfo
@@ -64,13 +64,16 @@
val thumbnail: Bitmap?
get() = thumbnailViewDeprecated.thumbnail
- // TODO(b/349120849): Extract ThumbnailData from TaskContainerData/TaskThumbnailViewModel
- val isRealSnapshot: Boolean
- get() = thumbnailViewDeprecated.isRealSnapshot()
+ // TODO(b/334826842): Support shouldShowSplashView for new TTV.
+ val shouldShowSplashView: Boolean
+ get() =
+ if (enableRefactorTaskThumbnail()) false
+ else thumbnailViewDeprecated.shouldShowSplashView()
- // TODO(b/349120849): Extract ThumbnailData from TaskContainerData/TaskThumbnailViewModel
- val scaledInsets: Insets
- get() = thumbnailViewDeprecated.scaledInsets
+ // TODO(b/350743460) Support sysUiStatusNavFlags for new TTV.
+ val sysUiStatusNavFlags: Int
+ get() =
+ if (enableRefactorTaskThumbnail()) 0 else thumbnailViewDeprecated.sysUiStatusNavFlags
/** Builds proto for logging */
val itemInfo: WorkspaceItemInfo
@@ -83,7 +86,7 @@
intent = Intent().setComponent(componentKey.componentName)
title = task.title
taskView.recentsView?.let { screenId = it.indexOfChild(taskView) }
- if (Flags.privateSpaceRestrictAccessibilityDrag()) {
+ if (privateSpaceRestrictAccessibilityDrag()) {
if (
UserCache.getInstance(taskView.context)
.getUserInfo(componentKey.user)
@@ -98,12 +101,14 @@
fun destroy() {
digitalWellBeingToast?.destroy()
thumbnailView?.let { taskView.removeView(it) }
+ overlay.destroy()
}
fun bind() {
- if (Flags.enableRefactorTaskThumbnail() && thumbnailView != null) {
+ if (enableRefactorTaskThumbnail() && thumbnailView != null) {
thumbnailViewDeprecated.setTaskOverlay(overlay)
bindThumbnailView()
+ overlay.init()
} else {
thumbnailViewDeprecated.bind(task, overlay)
}
@@ -116,4 +121,10 @@
// this should be decided inside TaskThumbnailViewModel.
thumbnailView?.viewModel?.bind(TaskThumbnail(task.key.id, taskView.isRunningTask))
}
+
+ fun setOverlayEnabled(enabled: Boolean) {
+ if (!enableRefactorTaskThumbnail()) {
+ thumbnailViewDeprecated.setOverlayEnabled(enabled)
+ }
+ }
}
diff --git a/quickstep/src/com/android/quickstep/views/TaskThumbnailViewDeprecated.java b/quickstep/src/com/android/quickstep/views/TaskThumbnailViewDeprecated.java
index 2afb6a6..5b7e6c7 100644
--- a/quickstep/src/com/android/quickstep/views/TaskThumbnailViewDeprecated.java
+++ b/quickstep/src/com/android/quickstep/views/TaskThumbnailViewDeprecated.java
@@ -28,16 +28,13 @@
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.ColorFilter;
-import android.graphics.Insets;
import android.graphics.Matrix;
import android.graphics.Paint;
import android.graphics.PorterDuff;
import android.graphics.PorterDuffXfermode;
import android.graphics.Rect;
-import android.graphics.RectF;
import android.graphics.Shader;
import android.graphics.drawable.Drawable;
-import android.os.Build;
import android.util.AttributeSet;
import android.util.FloatProperty;
import android.util.Property;
@@ -45,7 +42,6 @@
import android.widget.ImageView;
import androidx.annotation.Nullable;
-import androidx.annotation.RequiresApi;
import androidx.core.graphics.ColorUtils;
import com.android.launcher3.DeviceProfile;
@@ -266,40 +262,6 @@
return mDimAlpha;
}
- /**
- * Get the scaled insets that are being used to draw the task view. This is a subsection of
- * the full snapshot.
- *
- * @return the insets in snapshot bitmap coordinates.
- */
- @RequiresApi(api = Build.VERSION_CODES.Q)
- public Insets getScaledInsets() {
- if (mThumbnailData == null) {
- return Insets.NONE;
- }
-
- RectF bitmapRect = new RectF(
- 0,
- 0,
- mThumbnailData.getThumbnail().getWidth(),
- mThumbnailData.getThumbnail().getHeight());
- RectF viewRect = new RectF(0, 0, getMeasuredWidth(), getMeasuredHeight());
-
- // The position helper matrix tells us how to transform the bitmap to fit the view, the
- // inverse tells us where the view would be in the bitmaps coordinates. The insets are the
- // difference between the bitmap bounds and the projected view bounds.
- Matrix boundsToBitmapSpace = new Matrix();
- mPreviewPositionHelper.getMatrix().invert(boundsToBitmapSpace);
- RectF boundsInBitmapSpace = new RectF();
- boundsToBitmapSpace.mapRect(boundsInBitmapSpace, viewRect);
-
- DeviceProfile dp = mContainer.getDeviceProfile();
- int bottomInset = dp.isTablet
- ? Math.round(bitmapRect.bottom - boundsInBitmapSpace.bottom) : 0;
- return Insets.of(0, 0, 0, bottomInset);
- }
-
-
@SystemUiControllerFlags
public int getSysUiStatusNavFlags() {
if (mThumbnailData != null) {
@@ -487,7 +449,9 @@
*/
private void refreshOverlay() {
if (mOverlayEnabled) {
- mOverlay.initOverlay(mTask, mThumbnailData, mPreviewPositionHelper.getMatrix(),
+ mOverlay.initOverlay(mTask,
+ mThumbnailData != null ? mThumbnailData.getThumbnail() : null,
+ mPreviewPositionHelper.getMatrix(),
mPreviewPositionHelper.isOrientationChanged());
} else {
mOverlay.reset();
@@ -560,6 +524,10 @@
return mThumbnailData.isRealSnapshot && !mTask.isLocked;
}
+ public Matrix getThumbnailMatrix() {
+ return mPreviewPositionHelper.getMatrix();
+ }
+
@Override
public void onRecycle() {
// Do nothing
diff --git a/quickstep/src/com/android/quickstep/views/TaskView.kt b/quickstep/src/com/android/quickstep/views/TaskView.kt
index b922df4..9977d30 100644
--- a/quickstep/src/com/android/quickstep/views/TaskView.kt
+++ b/quickstep/src/com/android/quickstep/views/TaskView.kt
@@ -156,11 +156,6 @@
get() = taskContainers[0].task
@get:Deprecated("Use [taskContainers] instead.")
- val firstThumbnailViewDeprecated: TaskThumbnailViewDeprecated
- /** Returns the first thumbnailView of the TaskView. */
- get() = taskContainers[0].thumbnailViewDeprecated
-
- @get:Deprecated("Use [taskContainers] instead.")
val firstSnapshotView: View
/** Returns the first snapshotView of the TaskView. */
get() = taskContainers[0].snapshotView
@@ -851,7 +846,8 @@
taskContainers.forEach {
if (visible) {
recentsModel.thumbnailCache
- .updateThumbnailInBackground(it.task) { thumbnailData ->
+ .getThumbnailInBackground(it.task) { thumbnailData ->
+ it.task.thumbnail = thumbnailData
it.thumbnailViewDeprecated.setThumbnail(it.task, thumbnailData)
}
?.also { request -> pendingThumbnailLoadRequests.add(request) }
@@ -867,12 +863,15 @@
taskContainers.forEach {
if (visible) {
recentsModel.iconCache
- .updateIconInBackground(it.task) { task ->
- setIcon(it.iconView, task.icon)
+ .getIconInBackground(it.task) { icon, contentDescription, title ->
+ it.task.icon = icon
+ it.task.titleDescription = contentDescription
+ it.task.title = title
+ setIcon(it.iconView, icon)
if (enableOverviewIconMenu()) {
- setText(it.iconView, task.title)
+ setText(it.iconView, title)
}
- it.digitalWellBeingToast?.initialize(task)
+ it.digitalWellBeingToast?.initialize(it.task)
}
?.also { request -> pendingIconLoadRequests.add(request) }
} else {
@@ -1394,10 +1393,8 @@
}
open fun setOverlayEnabled(overlayEnabled: Boolean) {
- // TODO(b/335606129) Investigate the usage of [TaskOverlay] in the new TaskThumbnailView.
- // and if it's still necessary we should support that in the new TTV class.
if (!enableRefactorTaskThumbnail()) {
- taskContainers.forEach { it.thumbnailViewDeprecated.setOverlayEnabled(overlayEnabled) }
+ taskContainers.forEach { it.setOverlayEnabled(overlayEnabled) }
}
}
@@ -1472,7 +1469,9 @@
protected open fun updateSnapshotRadius() {
updateCurrentFullscreenParams()
taskContainers.forEach {
- it.thumbnailViewDeprecated.setFullscreenParams(getThumbnailFullscreenParams())
+ if (!enableRefactorTaskThumbnail()) {
+ it.thumbnailViewDeprecated.setFullscreenParams(getThumbnailFullscreenParams())
+ }
it.overlay.setFullscreenParams(getThumbnailFullscreenParams())
}
}
diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/allapps/TaskbarAllAppsControllerTest.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/allapps/TaskbarAllAppsControllerTest.kt
index 9ecd935..2f0b446 100644
--- a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/allapps/TaskbarAllAppsControllerTest.kt
+++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/allapps/TaskbarAllAppsControllerTest.kt
@@ -20,7 +20,6 @@
import android.content.ComponentName
import android.content.Intent
import android.os.Process
-import androidx.test.annotation.UiThreadTest
import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation
import com.android.launcher3.BubbleTextView
import com.android.launcher3.appprediction.PredictionRowView
@@ -34,6 +33,7 @@
import com.android.launcher3.util.LauncherMultivalentJUnit
import com.android.launcher3.util.LauncherMultivalentJUnit.EmulatedDevices
import com.android.launcher3.util.PackageUserKey
+import com.android.launcher3.util.TestUtil
import com.google.common.truth.Truth.assertThat
import org.junit.Rule
import org.junit.Test
@@ -55,17 +55,17 @@
@InjectController lateinit var overlayController: TaskbarOverlayController
@Test
- @UiThreadTest
fun testToggle_once_showsAllApps() {
- allAppsController.toggle()
+ getInstrumentation().runOnMainSync { allAppsController.toggle() }
assertThat(allAppsController.isOpen).isTrue()
}
@Test
- @UiThreadTest
fun testToggle_twice_closesAllApps() {
- allAppsController.toggle()
- allAppsController.toggle()
+ getInstrumentation().runOnMainSync {
+ allAppsController.toggle()
+ allAppsController.toggle()
+ }
assertThat(allAppsController.isOpen).isFalse()
}
@@ -77,54 +77,62 @@
}
@Test
- @UiThreadTest
fun testSetApps_beforeOpened_cachesInfo() {
- allAppsController.setApps(TEST_APPS, 0, emptyMap())
- allAppsController.toggle()
+ val overlayContext =
+ TestUtil.getOnUiThread {
+ allAppsController.setApps(TEST_APPS, 0, emptyMap())
+ allAppsController.toggle()
+ overlayController.requestWindow()
+ }
- val overlayContext = overlayController.requestWindow()
assertThat(overlayContext.appsView.appsStore.apps).isEqualTo(TEST_APPS)
}
@Test
- @UiThreadTest
fun testSetApps_afterOpened_updatesStore() {
- allAppsController.toggle()
- allAppsController.setApps(TEST_APPS, 0, emptyMap())
+ val overlayContext =
+ TestUtil.getOnUiThread {
+ allAppsController.toggle()
+ allAppsController.setApps(TEST_APPS, 0, emptyMap())
+ overlayController.requestWindow()
+ }
- val overlayContext = overlayController.requestWindow()
assertThat(overlayContext.appsView.appsStore.apps).isEqualTo(TEST_APPS)
}
@Test
- @UiThreadTest
fun testSetPredictedApps_beforeOpened_cachesInfo() {
- allAppsController.setPredictedApps(TEST_PREDICTED_APPS)
- allAppsController.toggle()
-
val predictedApps =
- overlayController
- .requestWindow()
- .appsView
- .floatingHeaderView
- .findFixedRowByType(PredictionRowView::class.java)
- .predictedApps
+ TestUtil.getOnUiThread {
+ allAppsController.setPredictedApps(TEST_PREDICTED_APPS)
+ allAppsController.toggle()
+
+ overlayController
+ .requestWindow()
+ .appsView
+ .floatingHeaderView
+ .findFixedRowByType(PredictionRowView::class.java)
+ .predictedApps
+ }
+
assertThat(predictedApps).isEqualTo(TEST_PREDICTED_APPS)
}
@Test
- @UiThreadTest
fun testSetPredictedApps_afterOpened_cachesInfo() {
- allAppsController.toggle()
- allAppsController.setPredictedApps(TEST_PREDICTED_APPS)
-
val predictedApps =
- overlayController
- .requestWindow()
- .appsView
- .floatingHeaderView
- .findFixedRowByType(PredictionRowView::class.java)
- .predictedApps
+ TestUtil.getOnUiThread {
+ allAppsController.toggle()
+ allAppsController.setPredictedApps(TEST_PREDICTED_APPS)
+
+ overlayController
+ .requestWindow()
+ .appsView
+ .floatingHeaderView
+ .findFixedRowByType(PredictionRowView::class.java)
+ .predictedApps
+ }
+
assertThat(predictedApps).isEqualTo(TEST_PREDICTED_APPS)
}
@@ -140,36 +148,38 @@
}
// Ensure the recycler view fully inflates before trying to grab an icon.
- getInstrumentation().runOnMainSync {
- val btv =
+ val btv =
+ TestUtil.getOnUiThread {
overlayController
.requestWindow()
.appsView
.activeRecyclerView
.findViewHolderForAdapterPosition(0)
?.itemView as? BubbleTextView
- assertThat(btv?.hasDot()).isTrue()
- }
+ }
+ assertThat(btv?.hasDot()).isTrue()
}
@Test
- @UiThreadTest
fun testUpdateNotificationDots_predictedApp_hasDot() {
- allAppsController.setPredictedApps(TEST_PREDICTED_APPS)
- allAppsController.toggle()
+ getInstrumentation().runOnMainSync {
+ allAppsController.setPredictedApps(TEST_PREDICTED_APPS)
+ allAppsController.toggle()
+ taskbarUnitTestRule.activityContext.popupDataProvider.onNotificationPosted(
+ PackageUserKey.fromItemInfo(TEST_PREDICTED_APPS[0]),
+ NotificationKeyData("key"),
+ )
+ }
- taskbarUnitTestRule.activityContext.popupDataProvider.onNotificationPosted(
- PackageUserKey.fromItemInfo(TEST_PREDICTED_APPS[0]),
- NotificationKeyData("key"),
- )
-
- val predictionRowView =
- overlayController
- .requestWindow()
- .appsView
- .floatingHeaderView
- .findFixedRowByType(PredictionRowView::class.java)
- val btv = predictionRowView.getChildAt(0) as BubbleTextView
+ val btv =
+ TestUtil.getOnUiThread {
+ overlayController
+ .requestWindow()
+ .appsView
+ .floatingHeaderView
+ .findFixedRowByType(PredictionRowView::class.java)
+ .getChildAt(0) as BubbleTextView
+ }
assertThat(btv.hasDot()).isTrue()
}
diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/overlay/TaskbarOverlayControllerTest.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/overlay/TaskbarOverlayControllerTest.kt
index fae5562..f946d4d 100644
--- a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/overlay/TaskbarOverlayControllerTest.kt
+++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/overlay/TaskbarOverlayControllerTest.kt
@@ -18,7 +18,6 @@
import android.app.ActivityManager.RunningTaskInfo
import android.view.MotionEvent
-import androidx.test.annotation.UiThreadTest
import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation
import com.android.launcher3.AbstractFloatingView
import com.android.launcher3.AbstractFloatingView.TYPE_OPTIONS_POPUP
@@ -31,7 +30,7 @@
import com.android.launcher3.taskbar.rules.TaskbarWindowSandboxContext
import com.android.launcher3.util.LauncherMultivalentJUnit
import com.android.launcher3.util.LauncherMultivalentJUnit.EmulatedDevices
-import com.android.launcher3.views.BaseDragLayer
+import com.android.launcher3.util.TestUtil.getOnUiThread
import com.android.systemui.shared.system.TaskStackChangeListeners
import com.google.common.truth.Truth.assertThat
import org.junit.Rule
@@ -54,74 +53,69 @@
get() = taskbarUnitTestRule.activityContext
@Test
- @UiThreadTest
fun testRequestWindow_twice_reusesWindow() {
- val context1 = overlayController.requestWindow()
- val context2 = overlayController.requestWindow()
+ val (context1, context2) =
+ getOnUiThread {
+ Pair(overlayController.requestWindow(), overlayController.requestWindow())
+ }
assertThat(context1).isSameInstanceAs(context2)
}
@Test
- @UiThreadTest
fun testRequestWindow_afterHidingExistingWindow_createsNewWindow() {
- val context1 = overlayController.requestWindow()
- overlayController.hideWindow()
+ val context1 = getOnUiThread { overlayController.requestWindow() }
+ getInstrumentation().runOnMainSync { overlayController.hideWindow() }
- val context2 = overlayController.requestWindow()
+ val context2 = getOnUiThread { overlayController.requestWindow() }
assertThat(context1).isNotSameInstanceAs(context2)
}
@Test
- @UiThreadTest
fun testRequestWindow_afterHidingOverlay_createsNewWindow() {
- val context1 = overlayController.requestWindow()
- TestOverlayView.show(context1)
- overlayController.hideWindow()
+ val context1 = getOnUiThread { overlayController.requestWindow() }
+ getInstrumentation().runOnMainSync {
+ TestOverlayView.show(context1)
+ overlayController.hideWindow()
+ }
- val context2 = overlayController.requestWindow()
+ val context2 = getOnUiThread { overlayController.requestWindow() }
assertThat(context1).isNotSameInstanceAs(context2)
}
@Test
- @UiThreadTest
fun testRequestWindow_addsProxyView() {
- TestOverlayView.show(overlayController.requestWindow())
+ getInstrumentation().runOnMainSync {
+ TestOverlayView.show(overlayController.requestWindow())
+ }
assertThat(hasOpenView(taskbarContext, TYPE_TASKBAR_OVERLAY_PROXY)).isTrue()
}
@Test
- @UiThreadTest
fun testRequestWindow_closeProxyView_closesOverlay() {
- val overlay = TestOverlayView.show(overlayController.requestWindow())
- AbstractFloatingView.closeOpenContainer(taskbarContext, TYPE_TASKBAR_OVERLAY_PROXY)
+ val overlay = getOnUiThread { TestOverlayView.show(overlayController.requestWindow()) }
+ getInstrumentation().runOnMainSync {
+ AbstractFloatingView.closeOpenContainer(taskbarContext, TYPE_TASKBAR_OVERLAY_PROXY)
+ }
assertThat(overlay.isOpen).isFalse()
}
@Test
fun testRequestWindow_attachesDragLayer() {
- lateinit var dragLayer: BaseDragLayer<*>
- getInstrumentation().runOnMainSync {
- dragLayer = overlayController.requestWindow().dragLayer
- }
-
+ val dragLayer = getOnUiThread { overlayController.requestWindow().dragLayer }
// Allow drag layer to attach before checking.
getInstrumentation().runOnMainSync { assertThat(dragLayer.isAttachedToWindow).isTrue() }
}
@Test
- @UiThreadTest
fun testHideWindow_closesOverlay() {
- val overlay = TestOverlayView.show(overlayController.requestWindow())
- overlayController.hideWindow()
+ val overlay = getOnUiThread { TestOverlayView.show(overlayController.requestWindow()) }
+ getInstrumentation().runOnMainSync { overlayController.hideWindow() }
assertThat(overlay.isOpen).isFalse()
}
@Test
fun testHideWindow_detachesDragLayer() {
- lateinit var dragLayer: BaseDragLayer<*>
- getInstrumentation().runOnMainSync {
- dragLayer = overlayController.requestWindow().dragLayer
- }
+ val dragLayer = getOnUiThread { overlayController.requestWindow().dragLayer }
// Wait for drag layer to be attached to window before hiding.
getInstrumentation().runOnMainSync {
@@ -131,26 +125,30 @@
}
@Test
- @UiThreadTest
fun testTwoOverlays_closeOne_windowStaysOpen() {
- val context = overlayController.requestWindow()
- val overlay1 = TestOverlayView.show(context)
- val overlay2 = TestOverlayView.show(context)
+ val (overlay1, overlay2) =
+ getOnUiThread {
+ val context = overlayController.requestWindow()
+ Pair(TestOverlayView.show(context), TestOverlayView.show(context))
+ }
- overlay1.close(false)
+ getInstrumentation().runOnMainSync { overlay1.close(false) }
assertThat(overlay2.isOpen).isTrue()
assertThat(hasOpenView(taskbarContext, TYPE_TASKBAR_OVERLAY_PROXY)).isTrue()
}
@Test
- @UiThreadTest
fun testTwoOverlays_closeAll_closesWindow() {
- val context = overlayController.requestWindow()
- val overlay1 = TestOverlayView.show(context)
- val overlay2 = TestOverlayView.show(context)
+ val (overlay1, overlay2) =
+ getOnUiThread {
+ val context = overlayController.requestWindow()
+ Pair(TestOverlayView.show(context), TestOverlayView.show(context))
+ }
- overlay1.close(false)
- overlay2.close(false)
+ getInstrumentation().runOnMainSync {
+ overlay1.close(false)
+ overlay2.close(false)
+ }
assertThat(hasOpenView(taskbarContext, TYPE_TASKBAR_OVERLAY_PROXY)).isFalse()
}
@@ -165,11 +163,7 @@
@Test
fun testTaskMovedToFront_closesOverlay() {
- lateinit var overlay: TestOverlayView
- getInstrumentation().runOnMainSync {
- overlay = TestOverlayView.show(overlayController.requestWindow())
- }
-
+ val overlay = getOnUiThread { TestOverlayView.show(overlayController.requestWindow()) }
TaskStackChangeListeners.getInstance().listenerImpl.onTaskMovedToFront(RunningTaskInfo())
// Make sure TaskStackChangeListeners' Handler posts the callback before checking state.
getInstrumentation().runOnMainSync { assertThat(overlay.isOpen).isFalse() }
@@ -177,9 +171,8 @@
@Test
fun testTaskStackChanged_allAppsClosed_overlayStaysOpen() {
- lateinit var overlay: TestOverlayView
+ val overlay = getOnUiThread { TestOverlayView.show(overlayController.requestWindow()) }
getInstrumentation().runOnMainSync {
- overlay = TestOverlayView.show(overlayController.requestWindow())
taskbarContext.controllers.sharedState?.allAppsVisible = false
}
@@ -189,9 +182,8 @@
@Test
fun testTaskStackChanged_allAppsOpen_closesOverlay() {
- lateinit var overlay: TestOverlayView
+ val overlay = getOnUiThread { TestOverlayView.show(overlayController.requestWindow()) }
getInstrumentation().runOnMainSync {
- overlay = TestOverlayView.show(overlayController.requestWindow())
taskbarContext.controllers.sharedState?.allAppsVisible = true
}
@@ -200,33 +192,39 @@
}
@Test
- @UiThreadTest
fun testUpdateLauncherDeviceProfile_overlayNotRebindSafe_closesOverlay() {
- val overlayContext = overlayController.requestWindow()
- val overlay = TestOverlayView.show(overlayContext).apply { type = TYPE_OPTIONS_POPUP }
+ val context = getOnUiThread { overlayController.requestWindow() }
+ val overlay = getOnUiThread {
+ TestOverlayView.show(context).apply { type = TYPE_OPTIONS_POPUP }
+ }
- overlayController.updateLauncherDeviceProfile(
- overlayController.launcherDeviceProfile
- .toBuilder(overlayContext)
- .setGestureMode(false)
- .build()
- )
+ getInstrumentation().runOnMainSync {
+ overlayController.updateLauncherDeviceProfile(
+ overlayController.launcherDeviceProfile
+ .toBuilder(context)
+ .setGestureMode(false)
+ .build()
+ )
+ }
assertThat(overlay.isOpen).isFalse()
}
@Test
- @UiThreadTest
fun testUpdateLauncherDeviceProfile_overlayRebindSafe_overlayStaysOpen() {
- val overlayContext = overlayController.requestWindow()
- val overlay = TestOverlayView.show(overlayContext).apply { type = TYPE_TASKBAR_ALL_APPS }
+ val context = getOnUiThread { overlayController.requestWindow() }
+ val overlay = getOnUiThread {
+ TestOverlayView.show(context).apply { type = TYPE_TASKBAR_ALL_APPS }
+ }
- overlayController.updateLauncherDeviceProfile(
- overlayController.launcherDeviceProfile
- .toBuilder(overlayContext)
- .setGestureMode(false)
- .build()
- )
+ getInstrumentation().runOnMainSync {
+ overlayController.updateLauncherDeviceProfile(
+ overlayController.launcherDeviceProfile
+ .toBuilder(context)
+ .setGestureMode(false)
+ .build()
+ )
+ }
assertThat(overlay.isOpen).isTrue()
}
diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarModeRule.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarModeRule.kt
index 6638736..c48947e 100644
--- a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarModeRule.kt
+++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarModeRule.kt
@@ -16,6 +16,7 @@
package com.android.launcher3.taskbar.rules
+import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation
import com.android.launcher3.taskbar.rules.TaskbarModeRule.Mode
import com.android.launcher3.taskbar.rules.TaskbarModeRule.TaskbarMode
import com.android.launcher3.util.DisplayController
@@ -59,23 +60,25 @@
override fun evaluate() {
val mode = taskbarMode.mode
- context.applicationContext.putObject(
- DisplayController.INSTANCE,
- object : DisplayController(context) {
- override fun getInfo(): Info {
- return spy(super.getInfo()) {
- on { isTransientTaskbar } doReturn (mode == Mode.TRANSIENT)
- on { isPinnedTaskbar } doReturn (mode == Mode.PINNED)
- on { navigationMode } doReturn
- when (mode) {
- Mode.TRANSIENT,
- Mode.PINNED -> NavigationMode.NO_BUTTON
- Mode.THREE_BUTTONS -> NavigationMode.THREE_BUTTONS
- }
+ getInstrumentation().runOnMainSync {
+ context.applicationContext.putObject(
+ DisplayController.INSTANCE,
+ object : DisplayController(context) {
+ override fun getInfo(): Info {
+ return spy(super.getInfo()) {
+ on { isTransientTaskbar } doReturn (mode == Mode.TRANSIENT)
+ on { isPinnedTaskbar } doReturn (mode == Mode.PINNED)
+ on { navigationMode } doReturn
+ when (mode) {
+ Mode.TRANSIENT,
+ Mode.PINNED -> NavigationMode.NO_BUTTON
+ Mode.THREE_BUTTONS -> NavigationMode.THREE_BUTTONS
+ }
+ }
}
- }
- },
- )
+ },
+ )
+ }
base.evaluate()
}
diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarUnitTestRule.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarUnitTestRule.kt
index 74c2390..8a64949 100644
--- a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarUnitTestRule.kt
+++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarUnitTestRule.kt
@@ -20,18 +20,25 @@
import android.app.PendingIntent
import android.content.IIntentSender
import android.content.Intent
+import android.provider.Settings
+import android.provider.Settings.Secure.NAV_BAR_KIDS_MODE
+import android.provider.Settings.Secure.USER_SETUP_COMPLETE
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.rule.ServiceTestRule
import com.android.launcher3.LauncherAppState
import com.android.launcher3.taskbar.TaskbarActivityContext
import com.android.launcher3.taskbar.TaskbarManager
import com.android.launcher3.taskbar.TaskbarNavButtonController.TaskbarNavButtonCallbacks
+import com.android.launcher3.taskbar.rules.TaskbarUnitTestRule.InjectController
import com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR
import com.android.launcher3.util.LauncherMultivalentJUnit.Companion.isRunningInRobolectric
+import com.android.launcher3.util.ModelTestExtensions.loadModelSync
+import com.android.launcher3.util.TestUtil
import com.android.quickstep.AllAppsActionManager
import com.android.quickstep.TouchInteractionService
import com.android.quickstep.TouchInteractionService.TISBinder
import org.junit.Assume.assumeTrue
+import org.junit.rules.RuleChain
import org.junit.rules.TestRule
import org.junit.runner.Description
import org.junit.runners.model.Statement
@@ -48,12 +55,11 @@
* that code that is executed on the main thread in production should also happen on that thread
* when tested.
*
- * `@UiThreadTest` is a simple way to run an entire test body on the main thread. But if a test
- * executes code that appends message(s) to the main thread's `MessageQueue`, the annotation will
- * prevent those messages from being processed until after the test body finishes.
+ * `@UiThreadTest` is incompatible with this rule. The annotation causes this rule to run on the
+ * main thread, but it needs to be run on the test thread for it to work properly. Instead, only run
+ * code that requires the main thread using something like [Instrumentation.runOnMainSync] or
+ * [TestUtil.getOnUiThread].
*
- * To test pending messages, instead use something like [Instrumentation.runOnMainSync] to perform
- * only sections of the test body on the main thread synchronously:
* ```
* @Test
* fun example() {
@@ -71,6 +77,10 @@
private val instrumentation = InstrumentationRegistry.getInstrumentation()
private val serviceTestRule = ServiceTestRule()
+ private val userSetupCompleteRule = TaskbarSecureSettingRule(USER_SETUP_COMPLETE)
+ private val kidsModeRule = TaskbarSecureSettingRule(NAV_BAR_KIDS_MODE)
+ private val settingRules = RuleChain.outerRule(userSetupCompleteRule).around(kidsModeRule)
+
private lateinit var taskbarManager: TaskbarManager
val activityContext: TaskbarActivityContext
@@ -80,15 +90,34 @@
}
override fun apply(base: Statement, description: Description): Statement {
+ return settingRules.apply(createStatement(base, description), description)
+ }
+
+ private fun createStatement(base: Statement, description: Description): Statement {
return object : Statement() {
override fun evaluate() {
+ // Only run test when Taskbar is enabled.
instrumentation.runOnMainSync {
assumeTrue(
LauncherAppState.getIDP(context).getDeviceProfile(context).isTaskbarPresent
)
}
+ // Process secure setting annotations.
+ instrumentation.runOnMainSync {
+ userSetupCompleteRule.putInt(
+ if (description.getAnnotation(UserSetupMode::class.java) != null) {
+ 0
+ } else {
+ 1
+ }
+ )
+ kidsModeRule.putInt(
+ if (description.getAnnotation(NavBarKidsMode::class.java) != null) 1 else 0
+ )
+ }
+
// Check for existing Taskbar instance from Launcher process.
val launcherTaskbarManager: TaskbarManager? =
if (!isRunningInRobolectric) {
@@ -105,8 +134,8 @@
null
}
- instrumentation.runOnMainSync {
- taskbarManager =
+ taskbarManager =
+ TestUtil.getOnUiThread {
TaskbarManager(
context,
AllAppsActionManager(context, UI_HELPER_EXECUTOR) {
@@ -114,19 +143,25 @@
},
object : TaskbarNavButtonCallbacks {},
)
- }
+ }
try {
+ LauncherAppState.getInstance(context).model.loadModelSync()
+
// Replace Launcher Taskbar window with test instance.
instrumentation.runOnMainSync {
- launcherTaskbarManager?.destroy()
+ launcherTaskbarManager?.setSuspended(true)
taskbarManager.onUserUnlocked() // Required to complete initialization.
}
injectControllers()
base.evaluate()
} finally {
- instrumentation.runOnMainSync { taskbarManager.destroy() }
+ // Revert Taskbar window.
+ instrumentation.runOnMainSync {
+ taskbarManager.destroy()
+ launcherTaskbarManager?.setSuspended(false)
+ }
}
}
}
@@ -163,4 +198,35 @@
@Retention(AnnotationRetention.RUNTIME)
@Target(AnnotationTarget.FIELD)
annotation class InjectController
+
+ /** Overrides [USER_SETUP_COMPLETE] to be `false` for tests. */
+ @Retention(AnnotationRetention.RUNTIME)
+ @Target(AnnotationTarget.CLASS, AnnotationTarget.FUNCTION)
+ annotation class UserSetupMode
+
+ /** Overrides [NAV_BAR_KIDS_MODE] to be `true` for tests. */
+ @Retention(AnnotationRetention.RUNTIME)
+ @Target(AnnotationTarget.CLASS, AnnotationTarget.FUNCTION)
+ annotation class NavBarKidsMode
+
+ /** Rule for Taskbar integer-based secure settings. */
+ private inner class TaskbarSecureSettingRule(private val settingName: String) : TestRule {
+
+ override fun apply(base: Statement, description: Description): Statement {
+ return object : Statement() {
+ override fun evaluate() {
+ val originalValue =
+ Settings.Secure.getInt(context.contentResolver, settingName, /* def= */ 0)
+ try {
+ base.evaluate()
+ } finally {
+ instrumentation.runOnMainSync { putInt(originalValue) }
+ }
+ }
+ }
+ }
+
+ /** Puts [value] into secure settings under [settingName]. */
+ fun putInt(value: Int) = Settings.Secure.putInt(context.contentResolver, settingName, value)
+ }
}
diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarUnitTestRuleTest.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarUnitTestRuleTest.kt
index 8262e0f..234e499 100644
--- a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarUnitTestRuleTest.kt
+++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarUnitTestRuleTest.kt
@@ -22,6 +22,8 @@
import com.android.launcher3.taskbar.TaskbarManager
import com.android.launcher3.taskbar.TaskbarStashController
import com.android.launcher3.taskbar.rules.TaskbarUnitTestRule.InjectController
+import com.android.launcher3.taskbar.rules.TaskbarUnitTestRule.NavBarKidsMode
+import com.android.launcher3.taskbar.rules.TaskbarUnitTestRule.UserSetupMode
import com.android.launcher3.util.LauncherMultivalentJUnit
import com.android.launcher3.util.LauncherMultivalentJUnit.EmulatedDevices
import com.google.common.truth.Truth.assertThat
@@ -125,9 +127,40 @@
}
}
- /** Executes [runTest] after the [testRule] setup phase completes. */
+ @Test
+ fun testUserSetupMode_default_isComplete() {
+ onSetup { assertThat(activityContext.isUserSetupComplete).isTrue() }
+ }
+
+ @Test
+ fun testUserSetupMode_withAnnotation_isIncomplete() {
+ @UserSetupMode class Mode
+ onSetup(description = Description.createSuiteDescription(Mode::class.java)) {
+ assertThat(activityContext.isUserSetupComplete).isFalse()
+ }
+ }
+
+ @Test
+ fun testNavBarKidsMode_default_navBarNotForcedVisible() {
+ onSetup { assertThat(activityContext.isNavBarForceVisible).isFalse() }
+ }
+
+ @Test
+ fun testNavBarKidsMode_withAnnotation_navBarForcedVisible() {
+ @NavBarKidsMode class Mode
+ onSetup(description = Description.createSuiteDescription(Mode::class.java)) {
+ assertThat(activityContext.isNavBarForceVisible).isTrue()
+ }
+ }
+
+ /**
+ * Executes [runTest] after the [testRule] setup phase completes.
+ *
+ * A [description] can also be provided to mimic annotating a test or test class.
+ */
private fun onSetup(
testRule: TaskbarUnitTestRule = TaskbarUnitTestRule(this, context),
+ description: Description = DESCRIPTION,
runTest: TaskbarUnitTestRule.() -> Unit,
) {
testRule
@@ -135,7 +168,7 @@
object : Statement() {
override fun evaluate() = runTest(testRule)
},
- DESCRIPTION,
+ description,
)
.evaluate()
}
diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/FakeTaskThumbnailDataSource.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/FakeTaskThumbnailDataSource.kt
index b66b735..30fc491 100644
--- a/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/FakeTaskThumbnailDataSource.kt
+++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/data/FakeTaskThumbnailDataSource.kt
@@ -32,7 +32,7 @@
var shouldLoadSynchronously: Boolean = true
/** Retrieves and sets a thumbnail on [task] from [taskIdToBitmap]. */
- override fun updateThumbnailInBackground(
+ override fun getThumbnailInBackground(
task: Task,
callback: Consumer<ThumbnailData>
): CancellableTask<ThumbnailData>? {
diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/task/viewmodel/TaskOverlayViewModelTest.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/task/viewmodel/TaskOverlayViewModelTest.kt
new file mode 100644
index 0000000..40482c4
--- /dev/null
+++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/task/viewmodel/TaskOverlayViewModelTest.kt
@@ -0,0 +1,160 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.quickstep.task.viewmodel
+
+import android.content.ComponentName
+import android.content.Intent
+import android.graphics.Bitmap
+import android.graphics.Color
+import android.graphics.Matrix
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.android.quickstep.recents.data.FakeTasksRepository
+import com.android.quickstep.recents.viewmodel.RecentsViewData
+import com.android.quickstep.task.thumbnail.TaskOverlayUiState.Disabled
+import com.android.quickstep.task.thumbnail.TaskOverlayUiState.Enabled
+import com.android.systemui.shared.recents.model.Task
+import com.android.systemui.shared.recents.model.ThumbnailData
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.flow.first
+import kotlinx.coroutines.test.runTest
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.whenever
+
+/** Test for [TaskOverlayViewModel] */
+@RunWith(AndroidJUnit4::class)
+class TaskOverlayViewModelTest {
+ private val task =
+ Task(Task.TaskKey(TASK_ID, 0, Intent(), ComponentName("", ""), 0, 2000)).apply {
+ colorBackground = Color.BLACK
+ }
+ private val thumbnailData =
+ ThumbnailData(
+ thumbnail =
+ mock<Bitmap>().apply {
+ whenever(width).thenReturn(THUMBNAIL_WIDTH)
+ whenever(height).thenReturn(THUMBNAIL_HEIGHT)
+ }
+ )
+ private val recentsViewData = RecentsViewData()
+ private val tasksRepository = FakeTasksRepository()
+ private val systemUnderTest = TaskOverlayViewModel(task, recentsViewData, tasksRepository)
+
+ @Test
+ fun initialStateIsDisabled() = runTest {
+ assertThat(systemUnderTest.overlayState.first()).isEqualTo(Disabled)
+ }
+
+ @Test
+ fun recentsViewOverlayDisabled_Disabled() = runTest {
+ recentsViewData.overlayEnabled.value = false
+ recentsViewData.settledFullyVisibleTaskIds.value = setOf(TASK_ID)
+
+ assertThat(systemUnderTest.overlayState.first()).isEqualTo(Disabled)
+ }
+
+ @Test
+ fun taskNotFullyVisible_Disabled() = runTest {
+ recentsViewData.overlayEnabled.value = true
+ recentsViewData.settledFullyVisibleTaskIds.value = setOf()
+
+ assertThat(systemUnderTest.overlayState.first()).isEqualTo(Disabled)
+ }
+
+ @Test
+ fun noThumbnail_Enabled() = runTest {
+ recentsViewData.overlayEnabled.value = true
+ recentsViewData.settledFullyVisibleTaskIds.value = setOf(TASK_ID)
+ task.isLocked = false
+
+ assertThat(systemUnderTest.overlayState.first())
+ .isEqualTo(
+ Enabled(
+ isRealSnapshot = false,
+ thumbnail = null,
+ thumbnailMatrix = Matrix.IDENTITY_MATRIX
+ )
+ )
+ }
+
+ @Test
+ fun withThumbnail_RealSnapshot_NotLocked_Enabled() = runTest {
+ recentsViewData.overlayEnabled.value = true
+ recentsViewData.settledFullyVisibleTaskIds.value = setOf(TASK_ID)
+ tasksRepository.seedTasks(listOf(task))
+ tasksRepository.seedThumbnailData(mapOf(TASK_ID to thumbnailData))
+ tasksRepository.setVisibleTasks(listOf(TASK_ID))
+ thumbnailData.isRealSnapshot = true
+ task.isLocked = false
+
+ assertThat(systemUnderTest.overlayState.first())
+ .isEqualTo(
+ Enabled(
+ isRealSnapshot = true,
+ thumbnail = thumbnailData.thumbnail,
+ thumbnailMatrix = Matrix.IDENTITY_MATRIX
+ )
+ )
+ }
+
+ @Test
+ fun withThumbnail_RealSnapshot_Locked_Enabled() = runTest {
+ recentsViewData.overlayEnabled.value = true
+ recentsViewData.settledFullyVisibleTaskIds.value = setOf(TASK_ID)
+ tasksRepository.seedTasks(listOf(task))
+ tasksRepository.seedThumbnailData(mapOf(TASK_ID to thumbnailData))
+ tasksRepository.setVisibleTasks(listOf(TASK_ID))
+ thumbnailData.isRealSnapshot = true
+ task.isLocked = true
+
+ assertThat(systemUnderTest.overlayState.first())
+ .isEqualTo(
+ Enabled(
+ isRealSnapshot = false,
+ thumbnail = thumbnailData.thumbnail,
+ thumbnailMatrix = Matrix.IDENTITY_MATRIX
+ )
+ )
+ }
+
+ @Test
+ fun withThumbnail_FakeSnapshot_Enabled() = runTest {
+ recentsViewData.overlayEnabled.value = true
+ recentsViewData.settledFullyVisibleTaskIds.value = setOf(TASK_ID)
+ tasksRepository.seedTasks(listOf(task))
+ tasksRepository.seedThumbnailData(mapOf(TASK_ID to thumbnailData))
+ tasksRepository.setVisibleTasks(listOf(TASK_ID))
+ thumbnailData.isRealSnapshot = false
+ task.isLocked = false
+
+ assertThat(systemUnderTest.overlayState.first())
+ .isEqualTo(
+ Enabled(
+ isRealSnapshot = false,
+ thumbnail = thumbnailData.thumbnail,
+ thumbnailMatrix = Matrix.IDENTITY_MATRIX
+ )
+ )
+ }
+
+ companion object {
+ const val TASK_ID = 0
+ const val THUMBNAIL_WIDTH = 100
+ const val THUMBNAIL_HEIGHT = 200
+ }
+}
diff --git a/quickstep/tests/src/com/android/launcher3/taskbar/TaskbarHoverToolTipControllerTest.java b/quickstep/tests/src/com/android/launcher3/taskbar/TaskbarHoverToolTipControllerTest.java
index 9ed3906..ef3a833 100644
--- a/quickstep/tests/src/com/android/launcher3/taskbar/TaskbarHoverToolTipControllerTest.java
+++ b/quickstep/tests/src/com/android/launcher3/taskbar/TaskbarHoverToolTipControllerTest.java
@@ -39,6 +39,7 @@
import androidx.test.filters.SmallTest;
import com.android.launcher3.BubbleTextView;
+import com.android.launcher3.apppairs.AppPairIcon;
import com.android.launcher3.folder.Folder;
import com.android.launcher3.folder.FolderIcon;
import com.android.launcher3.model.data.FolderInfo;
@@ -66,6 +67,7 @@
@Mock private MotionEvent mMotionEvent;
@Mock private BubbleTextView mHoverBubbleTextView;
@Mock private FolderIcon mHoverFolderIcon;
+ @Mock private AppPairIcon mAppPairIcon;
@Mock private Display mDisplay;
@Mock private TaskbarDragLayer mTaskbarDragLayer;
private Folder mSpyFolderView;
@@ -85,6 +87,7 @@
when(taskbarActivityContext.getDragLayer()).thenReturn(mTaskbarDragLayer);
when(taskbarActivityContext.getMainLooper()).thenReturn(context.getMainLooper());
when(taskbarActivityContext.getDisplay()).thenReturn(mDisplay);
+ when(taskbarActivityContext.isIconAlignedWithHotseat()).thenReturn(false);
when(mTaskbarDragLayer.getChildCount()).thenReturn(1);
mSpyFolderView = spy(new Folder(new ActivityContextWrapper(context), null));
@@ -213,6 +216,49 @@
assertThat(hoverHandled).isFalse();
}
+ @Test
+ public void onHover_hoverEnterAppPair_revealToolTip() {
+ when(mMotionEvent.getAction()).thenReturn(MotionEvent.ACTION_HOVER_ENTER);
+ when(mMotionEvent.getActionMasked()).thenReturn(MotionEvent.ACTION_HOVER_ENTER);
+
+ boolean hoverHandled =
+ mTaskbarHoverToolTipController.onHover(mAppPairIcon, mMotionEvent);
+ waitForIdleSync();
+
+ assertThat(hoverHandled).isTrue();
+ verify(taskbarActivityContext).setAutohideSuspendFlag(FLAG_AUTOHIDE_SUSPEND_HOVERING_ICONS,
+ true);
+ }
+
+ @Test
+ public void onHover_hoverExitAppPair_closeToolTip() {
+ when(mMotionEvent.getAction()).thenReturn(MotionEvent.ACTION_HOVER_EXIT);
+ when(mMotionEvent.getActionMasked()).thenReturn(MotionEvent.ACTION_HOVER_EXIT);
+
+ boolean hoverHandled =
+ mTaskbarHoverToolTipController.onHover(mAppPairIcon, mMotionEvent);
+ waitForIdleSync();
+
+ assertThat(hoverHandled).isTrue();
+ verify(taskbarActivityContext).setAutohideSuspendFlag(FLAG_AUTOHIDE_SUSPEND_HOVERING_ICONS,
+ false);
+ }
+
+ @Test
+ public void onHover_hoverEnterIconAlignedWithHotseat_noReveal() {
+ when(mMotionEvent.getAction()).thenReturn(MotionEvent.ACTION_HOVER_ENTER);
+ when(mMotionEvent.getActionMasked()).thenReturn(MotionEvent.ACTION_HOVER_ENTER);
+ when(taskbarActivityContext.isIconAlignedWithHotseat()).thenReturn(true);
+
+ boolean hoverHandled =
+ mTaskbarHoverToolTipController.onHover(mHoverBubbleTextView, mMotionEvent);
+ waitForIdleSync();
+
+ assertThat(hoverHandled).isTrue();
+ verify(taskbarActivityContext).setAutohideSuspendFlag(FLAG_AUTOHIDE_SUSPEND_HOVERING_ICONS,
+ true);
+ }
+
private void waitForIdleSync() {
mTestableLooper.processAllMessages();
}
diff --git a/src/com/android/launcher3/Launcher.java b/src/com/android/launcher3/Launcher.java
index d905801..5c052b2 100644
--- a/src/com/android/launcher3/Launcher.java
+++ b/src/com/android/launcher3/Launcher.java
@@ -771,6 +771,7 @@
// initialized properly.
onSaveInstanceState(new Bundle());
mModel.rebindCallbacks();
+ updateDisallowBack();
} finally {
Trace.endSection();
}
diff --git a/src/com/android/launcher3/Utilities.java b/src/com/android/launcher3/Utilities.java
index 19a3002..a296f46 100644
--- a/src/com/android/launcher3/Utilities.java
+++ b/src/com/android/launcher3/Utilities.java
@@ -381,6 +381,28 @@
}
/**
+ * Scales a {@code RectF} in place about a specified pivot point.
+ *
+ * <p>This method modifies the given {@code RectF} directly to scale it proportionally
+ * by the given {@code scale}, while preserving its center at the specified
+ * {@code (pivotX, pivotY)} coordinates.
+ *
+ * @param rectF the {@code RectF} to scale, modified directly.
+ * @param pivotX the x-coordinate of the pivot point about which to scale.
+ * @param pivotY the y-coordinate of the pivot point about which to scale.
+ * @param scale the factor by which to scale the rectangle. Values less than 1 will
+ * shrink the rectangle, while values greater than 1 will enlarge it.
+ */
+ public static void scaleRectFAboutPivot(RectF rectF, float pivotX, float pivotY, float scale) {
+ rectF.offset(-pivotX, -pivotY);
+ rectF.left *= scale;
+ rectF.top *= scale;
+ rectF.right *= scale;
+ rectF.bottom *= scale;
+ rectF.offset(pivotX, pivotY);
+ }
+
+ /**
* Maps t from one range to another range.
* @param t The value to map.
* @param fromMin The lower bound of the range that t is being mapped from.
diff --git a/src/com/android/launcher3/apppairs/AppPairIcon.java b/src/com/android/launcher3/apppairs/AppPairIcon.java
index 32445ec..870c876 100644
--- a/src/com/android/launcher3/apppairs/AppPairIcon.java
+++ b/src/com/android/launcher3/apppairs/AppPairIcon.java
@@ -18,10 +18,12 @@
import static com.android.launcher3.BubbleTextView.DISPLAY_FOLDER;
+import android.animation.ObjectAnimator;
import android.content.Context;
import android.graphics.Paint;
import android.graphics.Rect;
import android.util.AttributeSet;
+import android.util.FloatProperty;
import android.view.LayoutInflater;
import android.view.ViewGroup;
import android.widget.FrameLayout;
@@ -54,6 +56,26 @@
public class AppPairIcon extends FrameLayout implements DraggableView, Reorderable {
private static final String TAG = "AppPairIcon";
+ // The duration of the scaling animation on hover enter/exit.
+ private static final int HOVER_SCALE_DURATION = 150;
+ // The default scale of the icon when not hovered.
+ private static final Float HOVER_SCALE_DEFAULT = 1f;
+ // The max scale of the icon when hovered.
+ private static final Float HOVER_SCALE_MAX = 1.1f;
+ // Animates the scale of the icon background on hover.
+ private static final FloatProperty<AppPairIcon> HOVER_SCALE_PROPERTY =
+ new FloatProperty<>("hoverScale") {
+ @Override
+ public void setValue(AppPairIcon view, float scale) {
+ view.mIconGraphic.setHoverScale(scale);
+ }
+
+ @Override
+ public Float get(AppPairIcon view) {
+ return view.mIconGraphic.getHoverScale();
+ }
+ };
+
// A view that holds the app pair icon graphic.
private AppPairIconGraphic mIconGraphic;
// A view that holds the app pair's title.
@@ -250,4 +272,14 @@
}
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
+
+ @Override
+ public void onHoverChanged(boolean hovered) {
+ super.onHoverChanged(hovered);
+ ObjectAnimator
+ .ofFloat(this, HOVER_SCALE_PROPERTY,
+ hovered ? HOVER_SCALE_MAX : HOVER_SCALE_DEFAULT)
+ .setDuration(HOVER_SCALE_DURATION)
+ .start();
+ }
}
diff --git a/src/com/android/launcher3/apppairs/AppPairIconDrawable.java b/src/com/android/launcher3/apppairs/AppPairIconDrawable.java
index db83d91..114ed2e 100644
--- a/src/com/android/launcher3/apppairs/AppPairIconDrawable.java
+++ b/src/com/android/launcher3/apppairs/AppPairIconDrawable.java
@@ -26,6 +26,7 @@
import androidx.annotation.NonNull;
+import com.android.launcher3.Utilities;
import com.android.launcher3.icons.FastBitmapDrawable;
/**
@@ -128,6 +129,18 @@
height - (mP.getStandardIconPadding() + mP.getOuterPadding())
);
+ // Scale each background from its center edge closest to the center channel.
+ Utilities.scaleRectFAboutPivot(
+ leftSide,
+ leftSide.left + leftSide.width(),
+ leftSide.top + leftSide.centerY(),
+ mP.getHoverScale());
+ Utilities.scaleRectFAboutPivot(
+ rightSide,
+ rightSide.left,
+ rightSide.top + rightSide.centerY(),
+ mP.getHoverScale());
+
drawCustomRoundedRect(canvas, leftSide, new float[]{
mP.getBigRadius(), mP.getBigRadius(),
mP.getSmallRadius(), mP.getSmallRadius(),
@@ -163,6 +176,18 @@
height - (mP.getStandardIconPadding() + mP.getOuterPadding())
);
+ // Scale each background from its center edge closest to the center channel.
+ Utilities.scaleRectFAboutPivot(
+ topSide,
+ topSide.left + topSide.centerX(),
+ topSide.top + topSide.height(),
+ mP.getHoverScale());
+ Utilities.scaleRectFAboutPivot(
+ bottomSide,
+ bottomSide.left + bottomSide.centerX(),
+ bottomSide.top,
+ mP.getHoverScale());
+
drawCustomRoundedRect(canvas, topSide, new float[]{
mP.getBigRadius(), mP.getBigRadius(),
mP.getBigRadius(), mP.getBigRadius(),
diff --git a/src/com/android/launcher3/apppairs/AppPairIconDrawingParams.kt b/src/com/android/launcher3/apppairs/AppPairIconDrawingParams.kt
index 45dc013..5b546d6 100644
--- a/src/com/android/launcher3/apppairs/AppPairIconDrawingParams.kt
+++ b/src/com/android/launcher3/apppairs/AppPairIconDrawingParams.kt
@@ -64,6 +64,8 @@
var isLeftRightSplit: Boolean = true
// The background paint color (based on container).
var bgColor: Int = 0
+ // The scale of the icon background while hovered.
+ var hoverScale: Float = 1f
init {
val activity: ActivityContext = ActivityContext.lookupContext(context)
diff --git a/src/com/android/launcher3/apppairs/AppPairIconGraphic.kt b/src/com/android/launcher3/apppairs/AppPairIconGraphic.kt
index dce97eb..034b686 100644
--- a/src/com/android/launcher3/apppairs/AppPairIconGraphic.kt
+++ b/src/com/android/launcher3/apppairs/AppPairIconGraphic.kt
@@ -139,4 +139,19 @@
super.dispatchDraw(canvas)
drawable.draw(canvas)
}
+
+ /**
+ * Sets the scale of the icon background while hovered.
+ */
+ fun setHoverScale(scale: Float) {
+ drawParams.hoverScale = scale
+ redraw()
+ }
+
+ /**
+ * Gets the scale of the icon background while hovered.
+ */
+ fun getHoverScale(): Float {
+ return drawParams.hoverScale
+ }
}
diff --git a/src/com/android/launcher3/folder/Folder.java b/src/com/android/launcher3/folder/Folder.java
index dcc55e6..d3c1a02 100644
--- a/src/com/android/launcher3/folder/Folder.java
+++ b/src/com/android/launcher3/folder/Folder.java
@@ -65,6 +65,7 @@
import androidx.annotation.IntDef;
import androidx.annotation.Nullable;
+import androidx.annotation.VisibleForTesting;
import androidx.core.content.res.ResourcesCompat;
import com.android.launcher3.AbstractFloatingView;
@@ -1693,6 +1694,11 @@
return windowBottomPx - folderBottomPx;
}
+ @VisibleForTesting
+ public boolean getDeleteFolderOnDropCompleted() {
+ return mDeleteFolderOnDropCompleted;
+ }
+
/**
* Save this listener for the special case of when we update the state and concurrently
* add another listener to {@link #mOnFolderStateChangedListeners} to avoid a
diff --git a/src/com/android/launcher3/widget/WidgetCell.java b/src/com/android/launcher3/widget/WidgetCell.java
index 35372d3..b7ad95e 100644
--- a/src/com/android/launcher3/widget/WidgetCell.java
+++ b/src/com/android/launcher3/widget/WidgetCell.java
@@ -40,7 +40,6 @@
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewPropertyAnimator;
-import android.view.accessibility.AccessibilityNodeInfo;
import android.widget.Button;
import android.widget.FrameLayout;
import android.widget.LinearLayout;
@@ -500,12 +499,6 @@
}
@Override
- public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {
- super.onInitializeAccessibilityNodeInfo(info);
- info.removeAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_CLICK);
- }
-
- @Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
ViewGroup.LayoutParams containerLp = mWidgetImageContainer.getLayoutParams();
int maxWidth = MeasureSpec.getSize(widthMeasureSpec);
diff --git a/tests/src/com/android/launcher3/folder/FolderTest.kt b/tests/src/com/android/launcher3/folder/FolderTest.kt
new file mode 100644
index 0000000..e1daa74
--- /dev/null
+++ b/tests/src/com/android/launcher3/folder/FolderTest.kt
@@ -0,0 +1,86 @@
+/*
+ * 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.folder
+
+import android.content.Context
+import android.graphics.Point
+import android.view.View
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.launcher3.DropTarget.DragObject
+import com.android.launcher3.LauncherAppState
+import com.android.launcher3.celllayout.board.FolderPoint
+import com.android.launcher3.celllayout.board.TestWorkspaceBuilder
+import com.android.launcher3.util.ActivityContextWrapper
+import com.android.launcher3.util.ModelTestExtensions.clearModelDb
+import junit.framework.TestCase.assertEquals
+import org.junit.After
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito
+import org.mockito.Mockito.times
+import org.mockito.Mockito.verify
+
+/** Tests for [Folder] */
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class FolderTest {
+
+ private val context: Context =
+ ActivityContextWrapper(ApplicationProvider.getApplicationContext())
+ private val workspaceBuilder = TestWorkspaceBuilder(context)
+ private val folder: Folder = Mockito.spy(Folder(context, null))
+
+ @After
+ fun tearDown() {
+ LauncherAppState.getInstance(context).model.clearModelDb()
+ }
+
+ @Test
+ fun `Undo a folder with 1 icon when onDropCompleted is called`() {
+ val folderInfo =
+ workspaceBuilder.createFolderInCell(FolderPoint(Point(1, 0), TWO_ICON_FOLDER_TYPE), 0)
+ folder.mInfo = folderInfo
+ folder.mInfo.getContents().removeAt(0)
+ folder.mContent = Mockito.mock(FolderPagedView::class.java)
+ val dragLayout = Mockito.mock(View::class.java)
+ val dragObject = Mockito.mock(DragObject::class.java)
+ assertEquals(folder.deleteFolderOnDropCompleted, false)
+ folder.onDropCompleted(dragLayout, dragObject, true)
+ verify(folder, times(1)).replaceFolderWithFinalItem()
+ assertEquals(folder.deleteFolderOnDropCompleted, false)
+ }
+
+ @Test
+ fun `Do not undo a folder with 2 icons when onDropCompleted is called`() {
+ val folderInfo =
+ workspaceBuilder.createFolderInCell(FolderPoint(Point(1, 0), TWO_ICON_FOLDER_TYPE), 0)
+ folder.mInfo = folderInfo
+ folder.mContent = Mockito.mock(FolderPagedView::class.java)
+ val dragLayout = Mockito.mock(View::class.java)
+ val dragObject = Mockito.mock(DragObject::class.java)
+ assertEquals(folder.deleteFolderOnDropCompleted, false)
+ folder.onDropCompleted(dragLayout, dragObject, true)
+ verify(folder, times(0)).replaceFolderWithFinalItem()
+ assertEquals(folder.deleteFolderOnDropCompleted, false)
+ }
+
+ companion object {
+ const val TWO_ICON_FOLDER_TYPE = 'A'
+ }
+}
diff --git a/tests/src/com/android/launcher3/ui/workspace/TaplThemeIconsTest.java b/tests/src/com/android/launcher3/ui/workspace/TaplThemeIconsTest.java
index d653317..5dee322 100644
--- a/tests/src/com/android/launcher3/ui/workspace/TaplThemeIconsTest.java
+++ b/tests/src/com/android/launcher3/ui/workspace/TaplThemeIconsTest.java
@@ -16,6 +16,8 @@
package com.android.launcher3.ui.workspace;
import static com.android.launcher3.util.TestConstants.AppNames.TEST_APP_NAME;
+import static com.android.launcher3.util.rule.TestStabilityRule.LOCAL;
+import static com.android.launcher3.util.rule.TestStabilityRule.PLATFORM_POSTSUBMIT;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
@@ -38,6 +40,7 @@
import com.android.launcher3.tapl.HomeAppIconMenuItem;
import com.android.launcher3.ui.AbstractLauncherUiTest;
import com.android.launcher3.util.Executors;
+import com.android.launcher3.util.rule.TestStabilityRule;
import org.junit.Test;
@@ -111,6 +114,7 @@
}
@Test
+ @TestStabilityRule.Stability(flavors = LOCAL | PLATFORM_POSTSUBMIT) // b/350557998
public void testShortcutIconWithTheme() throws Exception {
setThemeEnabled(true);
initialize(this);
diff --git a/tests/src/com/android/launcher3/ui/workspace/TaplTwoPanelWorkspaceTest.java b/tests/src/com/android/launcher3/ui/workspace/TaplTwoPanelWorkspaceTest.java
index ae24a57..bc26c00 100644
--- a/tests/src/com/android/launcher3/ui/workspace/TaplTwoPanelWorkspaceTest.java
+++ b/tests/src/com/android/launcher3/ui/workspace/TaplTwoPanelWorkspaceTest.java
@@ -352,8 +352,11 @@
}
private void assertPagesExist(Launcher launcher, int... pageIds) {
+ waitForLauncherCondition("Existing page count does NOT match. "
+ + "Expected: " + pageIds.length
+ + ". Actual: " + launcher.getWorkspace().getPageCount(),
+ l -> pageIds.length == l.getWorkspace().getPageCount());
int pageCount = launcher.getWorkspace().getPageCount();
- assertEquals("Existing page count does NOT match.", pageIds.length, pageCount);
for (int i = 0; i < pageCount; i++) {
CellLayout page = (CellLayout) launcher.getWorkspace().getPageAt(i);
int pageId = launcher.getWorkspace().getCellLayoutId(page);