Merge "Move ContextualSearchHapticManager to Dagger" into main
diff --git a/aconfig/launcher.aconfig b/aconfig/launcher.aconfig
index 9fa2f50..d4cea8d 100644
--- a/aconfig/launcher.aconfig
+++ b/aconfig/launcher.aconfig
@@ -559,6 +559,13 @@
}
flag {
+ name: "enable_launcher_visual_refresh"
+ namespace: "launcher"
+ description: "Adds refresh for font family, app longpress menu icons, and pagination dots"
+ bug: "395145453"
+}
+
+flag {
name: "restore_archived_shortcuts"
namespace: "launcher"
description: "Makes sure pre-archived pinned shortcuts also get restored"
@@ -601,3 +608,17 @@
purpose: PURPOSE_BUGFIX
}
}
+
+flag {
+ name: "enable_strict_mode"
+ namespace: "launcher"
+ description: "Enable Strict Mode for the Launcher app"
+ bug: "394651876"
+}
+
+flag {
+ name: "extendible_theme_manager"
+ namespace: "launcher"
+ description: "Enables custom theme manager in Launcher"
+ bug: "381897614"
+}
diff --git a/aconfig/launcher_accessibility.aconfig b/aconfig/launcher_accessibility.aconfig
index afee8fe..13e1127 100644
--- a/aconfig/launcher_accessibility.aconfig
+++ b/aconfig/launcher_accessibility.aconfig
@@ -1,5 +1,5 @@
package: "com.android.launcher3"
-container: "system"
+container: "system_ext"
flag {
name: "remove_exclude_from_screen_magnification_flag_usage"
@@ -9,4 +9,4 @@
metadata {
purpose: PURPOSE_BUGFIX
}
-}
\ No newline at end of file
+}
diff --git a/quickstep/res/drawable/bg_overview_add_desktop_button.xml b/quickstep/res/drawable/bg_overview_add_desktop_button.xml
new file mode 100644
index 0000000..12581bf
--- /dev/null
+++ b/quickstep/res/drawable/bg_overview_add_desktop_button.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2025 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.
+-->
+<ripple android:color="?android:attr/colorControlHighlight"
+ xmlns:android="http://schemas.android.com/apk/res/android">
+ <item>
+ <shape
+ android:shape="rectangle"
+ android:tint="?colorButtonNormal">
+ <corners android:radius="@dimen/add_desktop_button_size" />
+ <solid android:color="@color/materialColorSurfaceBright"/>
+ </shape>
+ </item>
+</ripple>
\ No newline at end of file
diff --git a/quickstep/res/drawable/bg_overview_clear_all_button.xml b/quickstep/res/drawable/bg_overview_clear_all_button.xml
index 7f58cf8..2f28689 100644
--- a/quickstep/res/drawable/bg_overview_clear_all_button.xml
+++ b/quickstep/res/drawable/bg_overview_clear_all_button.xml
@@ -15,8 +15,7 @@
limitations under the License.
-->
<ripple android:color="?android:attr/colorControlHighlight"
- xmlns:android="http://schemas.android.com/apk/res/android"
- xmlns:androidprv="http://schemas.android.com/apk/prv/res/android">
+ xmlns:android="http://schemas.android.com/apk/res/android">
<item>
<shape android:shape="rectangle"
android:tint="?colorButtonNormal">
diff --git a/quickstep/res/layout/overview_add_desktop_button.xml b/quickstep/res/layout/overview_add_desktop_button.xml
index e36cf72..a1c64f3 100644
--- a/quickstep/res/layout/overview_add_desktop_button.xml
+++ b/quickstep/res/layout/overview_add_desktop_button.xml
@@ -16,9 +16,11 @@
-->
<com.android.quickstep.views.AddDesktopButton
xmlns:android="http://schemas.android.com/apk/res/android"
- xmlns:launcher="http://schemas.android.com/apgk/res-auto"
+ xmlns:launcher="http://schemas.android.com/apk/res-auto"
android:id="@+id/add_desktop_button"
android:layout_width="@dimen/add_desktop_button_size"
android:layout_height="@dimen/add_desktop_button_size"
android:src="@drawable/ic_desktop_add"
- android:padding="10dp" />
\ No newline at end of file
+ android:background="@drawable/bg_overview_add_desktop_button"
+ launcher:focusBorderColor="@color/materialColorOutline"
+ android:padding="10dp" />
diff --git a/quickstep/res/layout/overview_clear_all_button.xml b/quickstep/res/layout/overview_clear_all_button.xml
index 18a6240..034c3c2 100644
--- a/quickstep/res/layout/overview_clear_all_button.xml
+++ b/quickstep/res/layout/overview_clear_all_button.xml
@@ -16,7 +16,6 @@
-->
<com.android.quickstep.views.ClearAllButton
xmlns:android="http://schemas.android.com/apk/res/android"
- xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
xmlns:launcher="http://schemas.android.com/apk/res-auto"
style="@style/OverviewClearAllButton"
android:id="@+id/clear_all"
@@ -25,4 +24,4 @@
android:text="@string/recents_clear_all"
android:textColor="@color/materialColorOnSurface"
launcher:focusBorderColor="@color/materialColorOutline"
- android:textSize="14sp" />
\ No newline at end of file
+ android:textSize="14sp" />
diff --git a/quickstep/res/values/attrs.xml b/quickstep/res/values/attrs.xml
index 7fd6b5c..28c0d5c 100644
--- a/quickstep/res/values/attrs.xml
+++ b/quickstep/res/values/attrs.xml
@@ -36,6 +36,11 @@
<attr name="focusBorderColor" />
</declare-styleable>
+ <declare-styleable name="AddDesktopButton">
+ <!-- focus border color for overview add desktop button views -->
+ <attr name="focusBorderColor" />
+ </declare-styleable>
+
<!--
Gesture nav edu specific attributes. These attributes are used to customize Gesture nav edu
view lottie animation colors in XML files.
diff --git a/quickstep/res/values/dimens.xml b/quickstep/res/values/dimens.xml
index b253343..05f0695 100644
--- a/quickstep/res/values/dimens.xml
+++ b/quickstep/res/values/dimens.xml
@@ -88,6 +88,9 @@
<dimen name="task_thumbnail_header_icon_size">18dp</dimen>
<dimen name="task_thumbnail_header_round_corner_radius">16dp</dimen>
+ <!-- How much a task being dragged for dismissal can undershoot the origin when dragged back to its start position. -->
+ <dimen name="task_dismiss_max_undershoot">25dp</dimen>
+
<dimen name="task_icon_cache_default_icon_size">72dp</dimen>
<item name="overview_modal_max_scale" format="float" type="dimen">1.1</item>
@@ -108,6 +111,7 @@
<!-- Recents add desktop button -->
<dimen name="add_desktop_button_size">56dp</dimen>
+ <dimen name="add_desktop_button_outline_padding">2dp</dimen>
<!-- The speed in dp/s at which the user needs to be scrolling in recents such that we start
loading full resolution screenshots. -->
diff --git a/quickstep/src/com/android/launcher3/statehandlers/DesktopVisibilityController.kt b/quickstep/src/com/android/launcher3/statehandlers/DesktopVisibilityController.kt
index 3c4bc91..03f5d96 100644
--- a/quickstep/src/com/android/launcher3/statehandlers/DesktopVisibilityController.kt
+++ b/quickstep/src/com/android/launcher3/statehandlers/DesktopVisibilityController.kt
@@ -18,6 +18,7 @@
import android.content.Context
import android.os.Debug
import android.util.Log
+import android.util.SparseArray
import android.window.DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY
import com.android.launcher3.LauncherState
import com.android.launcher3.dagger.ApplicationContext
@@ -52,6 +53,29 @@
systemUiProxy: SystemUiProxy,
lifecycleTracker: DaggerSingletonTracker,
) {
+ /**
+ * Tracks the desks configurations on each display.
+ *
+ * (Used only when multiple desks are enabled).
+ *
+ * @property displayId The ID of the display this object represents.
+ * @property canCreateDesks true if it's possible to create new desks on the display represented
+ * by this object.
+ * @property activeDeskId The ID of the active desk on the associated display (if any). It has a
+ * value of `-1` if there are no active desks. Note that there can only be at most one active
+ * desk on each display.
+ * @property deskIds a set containing the IDs of the desks on the associated display.
+ */
+ private data class DisplayDeskConfig(
+ val displayId: Int,
+ var canCreateDesks: Boolean,
+ var activeDeskId: Int = -1,
+ val deskIds: MutableSet<Int>,
+ )
+
+ /** Maps each display by its ID to its desks configuration. */
+ private val displaysDesksConfigsMap = SparseArray<DisplayDeskConfig>()
+
private val desktopVisibilityListeners: MutableSet<DesktopVisibilityListener> = HashSet()
private val taskbarDesktopModeListeners: MutableSet<TaskbarDesktopModeListener> = HashSet()
@@ -313,6 +337,81 @@
}
}
+ private fun onListenerConnected(displayDeskStates: Array<DisplayDeskState>) {
+ if (!DesktopModeStatus.enableMultipleDesktops(context)) {
+ return
+ }
+
+ displaysDesksConfigsMap.clear()
+
+ displayDeskStates.forEach { displayDeskState ->
+ displaysDesksConfigsMap[displayDeskState.displayId] =
+ DisplayDeskConfig(
+ displayId = displayDeskState.displayId,
+ canCreateDesks = displayDeskState.canCreateDesk,
+ activeDeskId = displayDeskState.activeDeskId,
+ deskIds = displayDeskState.deskIds.toMutableSet(),
+ )
+ }
+ }
+
+ private fun getDisplayDeskConfig(displayId: Int): DisplayDeskConfig {
+ return checkNotNull(displaysDesksConfigsMap[displayId]) {
+ "Expected non-null desk config for display: $displayId"
+ }
+ }
+
+ private fun onCanCreateDesksChanged(displayId: Int, canCreateDesks: Boolean) {
+ if (!DesktopModeStatus.enableMultipleDesktops(context)) {
+ return
+ }
+
+ getDisplayDeskConfig(displayId).canCreateDesks = canCreateDesks
+ }
+
+ private fun onDeskAdded(displayId: Int, deskId: Int) {
+ if (!DesktopModeStatus.enableMultipleDesktops(context)) {
+ return
+ }
+
+ getDisplayDeskConfig(displayId).also {
+ check(it.deskIds.add(deskId)) {
+ "Found a duplicate desk Id: $deskId on display: $displayId"
+ }
+ }
+ }
+
+ private fun onDeskRemoved(displayId: Int, deskId: Int) {
+ if (!DesktopModeStatus.enableMultipleDesktops(context)) {
+ return
+ }
+
+ getDisplayDeskConfig(displayId).also {
+ check(it.deskIds.remove(deskId)) {
+ "Removing non-existing desk Id: $deskId on display: $displayId"
+ }
+ if (it.activeDeskId == deskId) {
+ it.activeDeskId = -1
+ }
+ }
+ }
+
+ private fun onActiveDeskChanged(displayId: Int, newActiveDesk: Int, oldActiveDesk: Int) {
+ if (!DesktopModeStatus.enableMultipleDesktops(context)) {
+ return
+ }
+
+ getDisplayDeskConfig(displayId).also {
+ check(oldActiveDesk == it.activeDeskId) {
+ "Mismatch between the Shell's oldActiveDesk: $oldActiveDesk, and Launcher's: ${it.activeDeskId}"
+ }
+ check(it.deskIds.contains(newActiveDesk)) {
+ "newActiveDesk: $newActiveDesk was never added to display: $displayId"
+ }
+ it.activeDeskId = newActiveDesk
+ }
+ }
+
/** TODO: b/333533253 - Remove after flag rollout */
private fun markLauncherPaused() {
if (ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY.isTrue) {
@@ -366,10 +465,11 @@
) : Stub() {
private val controller = WeakReference(controller)
- // TODO: b/392986431 - Implement the new desks APIs.
- override fun onListenerConnected(
- displayDeskStates: Array<DisplayDeskState>,
- ) {}
+ override fun onListenerConnected(displayDeskStates: Array<DisplayDeskState>) {
+ Executors.MAIN_EXECUTOR.execute {
+ controller.get()?.onListenerConnected(displayDeskStates)
+ }
+ }
override fun onTasksVisibilityChanged(displayId: Int, visibleTasksCount: Int) {
if (displayId != this.displayId) return
@@ -405,14 +505,25 @@
override fun onExitDesktopModeTransitionStarted(transitionDuration: Int) {}
- // TODO: b/392986431 - Implement all the below new desks APIs.
- override fun onCanCreateDesksChanged(displayId: Int, canCreateDesks: Boolean) {}
+ override fun onCanCreateDesksChanged(displayId: Int, canCreateDesks: Boolean) {
+ Executors.MAIN_EXECUTOR.execute {
+ controller.get()?.onCanCreateDesksChanged(displayId, canCreateDesks)
+ }
+ }
- override fun onDeskAdded(displayId: Int, deskId: Int) {}
+ override fun onDeskAdded(displayId: Int, deskId: Int) {
+ Executors.MAIN_EXECUTOR.execute { controller.get()?.onDeskAdded(displayId, deskId) }
+ }
- override fun onDeskRemoved(displayId: Int, deskId: Int) {}
+ override fun onDeskRemoved(displayId: Int, deskId: Int) {
+ Executors.MAIN_EXECUTOR.execute { controller.get()?.onDeskRemoved(displayId, deskId) }
+ }
- override fun onActiveDeskChanged(displayId: Int, newActiveDesk: Int, oldActiveDesk: Int) {}
+ override fun onActiveDeskChanged(displayId: Int, newActiveDesk: Int, oldActiveDesk: Int) {
+ Executors.MAIN_EXECUTOR.execute {
+ controller.get()?.onActiveDeskChanged(displayId, newActiveDesk, oldActiveDesk)
+ }
+ }
}
/** A listener for Taskbar in Desktop Mode. */
diff --git a/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchController.java b/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchController.java
index 23065b5..5afc5ed 100644
--- a/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchController.java
@@ -15,6 +15,8 @@
*/
package com.android.launcher3.taskbar;
+import static com.android.launcher3.Flags.enableAltTabKqsOnConnectedDisplays;
+
import android.content.ComponentName;
import android.content.pm.ActivityInfo;
import android.view.MotionEvent;
@@ -272,11 +274,26 @@
}
private void processLoadedTasksOnDesktop(List<GroupTask> tasks, Set<Integer> taskIdsToExclude) {
- // Find the single desktop task that contains a grouping of desktop tasks
- DesktopTask desktopTask = findDesktopTask(tasks);
+ // Find all desktop tasks.
+ List<DesktopTask> desktopTasks = tasks.stream()
+ .filter(t -> t instanceof DesktopTask)
+ .map(t -> (DesktopTask) t)
+ .toList();
- if (desktopTask != null) {
- mTasks = desktopTask.getTasks().stream()
+ // Apps on the connected displays seem to be in different Desktop tasks even with the
+ // multiple desktops flag disabled. So, until multiple desktops is implemented the following
+ // should help with team-fooding Alt+tab on connected displays. Post multiple desktop,
+ // further changes maybe required to support launching selected desktops.
+ if (enableAltTabKqsOnConnectedDisplays()) {
+ mTasks = desktopTasks.stream()
+ .flatMap(t -> t.getTasks().stream())
+ .map(SingleTask::new)
+ .filter(task -> !shouldExcludeTask(task, taskIdsToExclude))
+ .collect(Collectors.toList());
+
+ mNumHiddenTasks = Math.max(0, tasks.size() - desktopTasks.size());
+ } else if (!desktopTasks.isEmpty()) {
+ mTasks = desktopTasks.get(0).getTasks().stream()
.map(SingleTask::new)
.filter(task -> !shouldExcludeTask(task, taskIdsToExclude))
.collect(Collectors.toList());
@@ -289,14 +306,6 @@
}
}
- @Nullable
- private DesktopTask findDesktopTask(List<GroupTask> tasks) {
- return (DesktopTask) tasks.stream()
- .filter(t -> t instanceof DesktopTask)
- .findFirst()
- .orElse(null);
- }
-
void closeQuickSwitchView() {
closeQuickSwitchView(true);
}
diff --git a/quickstep/src/com/android/launcher3/taskbar/NavbarButtonsViewController.java b/quickstep/src/com/android/launcher3/taskbar/NavbarButtonsViewController.java
index 2e48910..ee5b8d1 100644
--- a/quickstep/src/com/android/launcher3/taskbar/NavbarButtonsViewController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/NavbarButtonsViewController.java
@@ -74,6 +74,7 @@
import android.graphics.drawable.RotateDrawable;
import android.inputmethodservice.InputMethodService;
import android.os.Handler;
+import android.os.SystemProperties;
import android.util.Property;
import android.view.Gravity;
import android.view.HapticFeedbackConstants;
@@ -191,6 +192,7 @@
private final int mDarkIconColorOnWorkspace;
/** Color to use for navbar buttons, if they are on on a Taskbar surface background. */
private final int mOnBackgroundIconColor;
+ private final boolean mIsExpressiveThemeEnabled;
private @Nullable Animator mNavBarLocationAnimator;
private @Nullable BubbleBarLocation mBubbleBarTargetLocation;
@@ -272,6 +274,9 @@
if (mContext.isPhoneMode()) {
mTaskbarTransitions = new TaskbarTransitions(mContext, mNavButtonsView);
}
+ String SUWTheme = SystemProperties.get("setupwizard.theme", "");
+ mIsExpressiveThemeEnabled = SUWTheme.equals("glif_expressive")
+ || SUWTheme.equals("glif_expressive_light");
}
/**
@@ -336,10 +341,10 @@
// Start at 1 because relevant flags are unset at init.
mOnBackgroundNavButtonColorOverrideMultiplier.value = 1;
- // Force nav buttons (specifically back button) to be visible during setup wizard.
- boolean isInSetup = !mContext.isUserSetupComplete();
+ // Potentially force the back button to be visible during setup wizard.
+ boolean shouldShowInSetup = !mContext.isUserSetupComplete() && !mIsExpressiveThemeEnabled;
boolean isInKidsMode = mContext.isNavBarKidsModeActive();
- boolean alwaysShowButtons = isThreeButtonNav || isInSetup;
+ boolean alwaysShowButtons = isThreeButtonNav || shouldShowInSetup;
// Make sure to remove nav bar buttons translation when any of the following occur:
// - Notification shade is expanded
@@ -930,6 +935,10 @@
}
private void handleSetupUi() {
+ // Setup wizard handles the UI when the expressive theme is enabled.
+ if (mIsExpressiveThemeEnabled) {
+ return;
+ }
// Since setup wizard only has back button enabled, it looks strange to be
// end-aligned, so start-align instead.
FrameLayout.LayoutParams navButtonsLayoutParams = (FrameLayout.LayoutParams)
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java
index 9d1fc15..a6d3cde 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java
@@ -24,6 +24,8 @@
import static android.view.WindowManager.LayoutParams.TYPE_NAVIGATION_BAR_PANEL;
import static android.window.SplashScreen.SPLASH_SCREEN_STYLE_UNDEFINED;
+import static androidx.annotation.VisibleForTesting.PACKAGE_PRIVATE;
+
import static com.android.launcher3.AbstractFloatingView.TYPE_ALL;
import static com.android.launcher3.AbstractFloatingView.TYPE_ON_BOARD_POPUP;
import static com.android.launcher3.AbstractFloatingView.TYPE_REBIND_SAFE;
@@ -2008,7 +2010,8 @@
return mControllers.uiController.isIconAlignedWithHotseat();
}
- @VisibleForTesting
+ // TODO(b/395061396): Remove `otherwise` when overview in widow is enabled.
+ @VisibleForTesting(otherwise = PACKAGE_PRIVATE)
public TaskbarControllers getControllers() {
return mControllers;
}
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarDesktopModeController.kt b/quickstep/src/com/android/launcher3/taskbar/TaskbarDesktopModeController.kt
index 8806bc6..cb399e8 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarDesktopModeController.kt
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarDesktopModeController.kt
@@ -50,8 +50,9 @@
fun shouldShowDesktopTasksInTaskbar(): Boolean {
return desktopVisibilityController.areDesktopTasksVisible() ||
- DisplayController.showLockedTaskbarOnHome(context) &&
- taskbarControllers.taskbarStashController.isOnHome
+ DisplayController.showDesktopTaskbarForFreeformDisplay(context) ||
+ (DisplayController.showLockedTaskbarOnHome(context) &&
+ taskbarControllers.taskbarStashController.isOnHome)
}
fun getTaskbarCornerRoundness(doesAnyTaskRequireTaskbarRounding: Boolean): Float {
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarDragController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarDragController.java
index 3a83db2..f36c481 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarDragController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarDragController.java
@@ -34,6 +34,7 @@
import android.content.ClipDescription;
import android.content.Intent;
import android.content.pm.LauncherApps;
+import android.content.pm.ShortcutInfo;
import android.content.res.Resources;
import android.graphics.Canvas;
import android.graphics.Point;
@@ -84,6 +85,7 @@
import com.android.quickstep.util.MultiValueUpdateListener;
import com.android.quickstep.util.SingleTask;
import com.android.systemui.shared.recents.model.Task;
+import com.android.wm.shell.shared.bubbles.BubbleAnythingFlagHelper;
import com.android.wm.shell.shared.draganddrop.DragAndDropConstants;
import java.io.PrintWriter;
@@ -416,6 +418,10 @@
item.user));
intent.putExtra(Intent.EXTRA_PACKAGE_NAME, item.getIntent().getPackage());
intent.putExtra(Intent.EXTRA_SHORTCUT_ID, deepShortcutId);
+ ShortcutInfo shortcutInfo = ((WorkspaceItemInfo) item).getDeepShortcutInfo();
+ if (BubbleAnythingFlagHelper.enableCreateAnyBubble() && shortcutInfo != null) {
+ intent.putExtra(DragAndDropConstants.EXTRA_SHORTCUT_INFO, shortcutInfo);
+ }
} else if (item.itemType == ITEM_TYPE_SEARCH_ACTION) {
// TODO(b/289261756): Buggy behavior when split opposite to an existing search pane.
intent.putExtra(
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarLauncherStateController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarLauncherStateController.java
index 9a9575d..cada5a3 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarLauncherStateController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarLauncherStateController.java
@@ -58,7 +58,6 @@
import com.android.launcher3.Utilities;
import com.android.launcher3.anim.AnimatedFloat;
import com.android.launcher3.anim.AnimatorListeners;
-import com.android.launcher3.config.FeatureFlags;
import com.android.launcher3.statemanager.StateManager;
import com.android.launcher3.taskbar.bubbles.stashing.BubbleStashController.BubbleLauncherState;
import com.android.launcher3.uioverrides.QuickstepLauncher;
@@ -714,6 +713,10 @@
private static boolean shouldShowTaskbar(Context context, boolean isInLauncher,
boolean isInOverview) {
+ if (DisplayController.showDesktopTaskbarForFreeformDisplay(context)) {
+ return true;
+ }
+
if (DisplayController.showLockedTaskbarOnHome(context) && isInLauncher) {
return true;
}
@@ -769,9 +772,14 @@
* This refers to the intended state - a transition to this state might be in progress.
*/
public boolean isTaskbarAlignedWithHotseat() {
+ if (DisplayController.showDesktopTaskbarForFreeformDisplay(mLauncher)) {
+ return false;
+ }
+
if (DisplayController.showLockedTaskbarOnHome(mLauncher) && isInLauncher()) {
return false;
}
+
return mLauncherState.isTaskbarAlignedWithHotseat(mLauncher);
}
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarManager.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarManager.java
index ea2dec1..f704254 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarManager.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarManager.java
@@ -32,6 +32,7 @@
import static com.android.launcher3.util.FlagDebugUtils.formatFlagChange;
import static com.android.quickstep.util.SystemActionConstants.ACTION_SHOW_TASKBAR;
import static com.android.quickstep.util.SystemActionConstants.SYSTEM_ACTION_ID_TASKBAR;
+import static com.android.window.flags.Flags.enableTaskbarConnectedDisplays;
import android.annotation.SuppressLint;
import android.app.PendingIntent;
@@ -71,6 +72,7 @@
import com.android.quickstep.AllAppsActionManager;
import com.android.quickstep.RecentsActivity;
import com.android.quickstep.SystemUiProxy;
+import com.android.quickstep.fallback.window.RecentsDisplayModel;
import com.android.quickstep.fallback.window.RecentsWindowManager;
import com.android.quickstep.util.ContextualSearchInvoker;
import com.android.quickstep.views.RecentsViewContainer;
@@ -115,7 +117,10 @@
private static final Uri NAV_BAR_KIDS_MODE = Settings.Secure.getUriFor(
Settings.Secure.NAV_BAR_KIDS_MODE);
- private final Context mParentContext;
+ private final Context mBaseContext;
+ // TODO: Remove this during the connected displays lifecycle refactor.
+ private final Context mPrimaryWindowContext;
+ private WindowManager mPrimaryWindowManager;
private final TaskbarNavButtonController mDefaultNavButtonController;
private final ComponentCallbacks mDefaultComponentCallbacks;
@@ -182,6 +187,7 @@
new SimpleBroadcastReceiver(UI_HELPER_EXECUTOR, this::showTaskbarFromBroadcast);
private final AllAppsActionManager mAllAppsActionManager;
+ private final RecentsDisplayModel mRecentsDisplayModel;
private final Runnable mActivityOnDestroyCallback = new Runnable() {
@Override
@@ -242,31 +248,34 @@
public TaskbarManager(
Context context,
AllAppsActionManager allAppsActionManager,
- TaskbarNavButtonCallbacks navCallbacks) {
- mParentContext = context;
- createWindowContext(context.getDisplayId());
+ TaskbarNavButtonCallbacks navCallbacks,
+ RecentsDisplayModel recentsDisplayModel) {
+ mBaseContext = context;
mAllAppsActionManager = allAppsActionManager;
+ mRecentsDisplayModel = recentsDisplayModel;
+ mPrimaryWindowContext = createWindowContext(getDefaultDisplayId());
if (enableTaskbarNoRecreate()) {
+ mPrimaryWindowManager = mPrimaryWindowContext.getSystemService(WindowManager.class);
createTaskbarRootLayout(getDefaultDisplayId());
}
mDefaultNavButtonController = createDefaultNavButtonController(context, navCallbacks);
mDefaultComponentCallbacks = createDefaultComponentCallbacks();
- SettingsCache.INSTANCE.get(getPrimaryWindowContext())
+ SettingsCache.INSTANCE.get(mPrimaryWindowContext)
.register(USER_SETUP_COMPLETE_URI, mOnSettingsChangeListener);
- SettingsCache.INSTANCE.get(getPrimaryWindowContext())
+ SettingsCache.INSTANCE.get(mPrimaryWindowContext)
.register(NAV_BAR_KIDS_MODE, mOnSettingsChangeListener);
Log.d(TASKBAR_NOT_DESTROYED_TAG, "registering component callbacks from constructor.");
- getPrimaryWindowContext().registerComponentCallbacks(mDefaultComponentCallbacks);
- mShutdownReceiver.register(getPrimaryWindowContext(), Intent.ACTION_SHUTDOWN);
+ mPrimaryWindowContext.registerComponentCallbacks(mDefaultComponentCallbacks);
+ mShutdownReceiver.register(mPrimaryWindowContext, Intent.ACTION_SHUTDOWN);
UI_HELPER_EXECUTOR.execute(() -> {
mSharedState.taskbarSystemActionPendingIntent = PendingIntent.getBroadcast(
- getPrimaryWindowContext(),
+ mPrimaryWindowContext,
SYSTEM_ACTION_ID_TASKBAR,
new Intent(ACTION_SHOW_TASKBAR).setPackage(
- getPrimaryWindowContext().getPackageName()),
+ mPrimaryWindowContext.getPackageName()),
PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE);
mTaskbarBroadcastReceiver.register(
- getPrimaryWindowContext(), RECEIVER_NOT_EXPORTED, ACTION_SHOW_TASKBAR);
+ mPrimaryWindowContext, RECEIVER_NOT_EXPORTED, ACTION_SHOW_TASKBAR);
});
debugWhyTaskbarNotDestroyed("TaskbarManager created");
@@ -279,15 +288,15 @@
return new TaskbarNavButtonController(
context,
navCallbacks,
- SystemUiProxy.INSTANCE.get(getPrimaryWindowContext()),
+ SystemUiProxy.INSTANCE.get(mPrimaryWindowContext),
new Handler(),
- new ContextualSearchInvoker(getPrimaryWindowContext()));
+ new ContextualSearchInvoker(mPrimaryWindowContext));
}
private ComponentCallbacks createDefaultComponentCallbacks() {
return new ComponentCallbacks() {
private Configuration mOldConfig =
- getPrimaryWindowContext().getResources().getConfiguration();
+ mPrimaryWindowContext.getResources().getConfiguration();
@Override
public void onConfigurationChanged(Configuration newConfig) {
@@ -297,8 +306,8 @@
"TaskbarManager#mComponentCallbacks.onConfigurationChanged: " + newConfig);
// TODO: adapt this logic to be specific to different displays.
DeviceProfile dp = mUserUnlocked
- ? LauncherAppState.getIDP(getPrimaryWindowContext()).getDeviceProfile(
- getPrimaryWindowContext())
+ ? LauncherAppState.getIDP(mPrimaryWindowContext).getDeviceProfile(
+ mPrimaryWindowContext)
: null;
int configDiff = mOldConfig.diff(newConfig) & ~SKIP_RECREATE_CONFIG_CHANGES;
@@ -350,7 +359,6 @@
int displayId = mTaskbars.keyAt(i);
destroyTaskbarForDisplay(displayId);
removeTaskbarRootViewFromWindow(displayId);
- removeWindowContextFromMap(displayId);
}
}
@@ -417,7 +425,7 @@
*/
public void onUserUnlocked() {
mUserUnlocked = true;
- DisplayController.INSTANCE.get(getPrimaryWindowContext()).addChangeListener(
+ DisplayController.INSTANCE.get(mPrimaryWindowContext).addChangeListener(
mRecreationListener);
recreateTaskbar();
addTaskbarRootViewToWindow(getDefaultDisplayId());
@@ -481,12 +489,23 @@
return ql.getUnfoldTransitionProgressProvider();
}
} else {
- return SystemUiProxy.INSTANCE.get(
- getPrimaryWindowContext()).getUnfoldTransitionProvider();
+ return SystemUiProxy.INSTANCE.get(mPrimaryWindowContext).getUnfoldTransitionProvider();
}
return null;
}
+ /** Creates a {@link TaskbarUIController} to use with non default displays. */
+ private TaskbarUIController createTaskbarUIControllerForNonDefaultDisplay(int displayId) {
+ if (RecentsDisplayModel.enableOverviewInWindow()) {
+ RecentsViewContainer rvc = mRecentsDisplayModel.getRecentsWindowManager(displayId);
+ if (rvc != null) {
+ return createTaskbarUIControllerForRecentsViewContainer(rvc);
+ }
+ }
+
+ return new TaskbarUIController();
+ }
+
/**
* Creates a {@link TaskbarUIController} to use while the given StatefulActivity is active.
*/
@@ -543,7 +562,7 @@
+ " FLAG_HIDE_NAVBAR_WINDOW=" + ENABLE_TASKBAR_NAVBAR_UNIFICATION
+ " dp.isTaskbarPresent=" + (dp == null ? "null" : dp.isTaskbarPresent));
if (!isTaskbarEnabled || !isLargeScreenTaskbar) {
- SystemUiProxy.INSTANCE.get(getPrimaryWindowContext())
+ SystemUiProxy.INSTANCE.get(mPrimaryWindowContext)
.notifyTaskbarStatus(/* visible */ false, /* stashed */ false);
if (!isTaskbarEnabled) {
return;
@@ -561,7 +580,11 @@
mSharedState.allAppsVisible = mSharedState.allAppsVisible && isLargeScreenTaskbar;
taskbar.init(mSharedState);
- if (mRecentsViewContainer != null) {
+ // Non default displays should not use LauncherTaskbarUIController as they shouldn't
+ // have access to the Launcher activity.
+ if (enableTaskbarConnectedDisplays() && !isDefaultDisplay(displayId)) {
+ taskbar.setUIController(createTaskbarUIControllerForNonDefaultDisplay(displayId));
+ } else if (mRecentsViewContainer != null) {
taskbar.setUIController(
createTaskbarUIControllerForRecentsViewContainer(mRecentsViewContainer));
}
@@ -719,6 +742,14 @@
* primary device or a previously mirroring display is switched to extended mode.
*/
public void onDisplayAddSystemDecorations(int displayId) {
+ if (isDefaultDisplay(displayId) || !enableTaskbarConnectedDisplays()) {
+ return;
+ }
+
+ Context newWindowContext = createWindowContext(displayId);
+ if (newWindowContext != null) {
+ addWindowContextToMap(displayId, newWindowContext);
+ }
}
/**
@@ -726,6 +757,14 @@
* removed from the primary device.
*/
public void onDisplayRemoved(int displayId) {
+ if (isDefaultDisplay(displayId) || !enableTaskbarConnectedDisplays()) {
+ return;
+ }
+
+ Context windowContext = getWindowContext(displayId);
+ if (windowContext != null) {
+ removeWindowContextFromMap(displayId);
+ }
}
/**
@@ -755,19 +794,19 @@
mRecentsViewContainer = null;
debugWhyTaskbarNotDestroyed("TaskbarManager#destroy()");
removeActivityCallbacksAndListeners();
- mTaskbarBroadcastReceiver.unregisterReceiverSafely(getPrimaryWindowContext());
+ mTaskbarBroadcastReceiver.unregisterReceiverSafely(mPrimaryWindowContext);
if (mUserUnlocked) {
- DisplayController.INSTANCE.get(getPrimaryWindowContext()).removeChangeListener(
+ DisplayController.INSTANCE.get(mPrimaryWindowContext).removeChangeListener(
mRecreationListener);
}
- SettingsCache.INSTANCE.get(getPrimaryWindowContext())
+ SettingsCache.INSTANCE.get(mPrimaryWindowContext)
.unregister(USER_SETUP_COMPLETE_URI, mOnSettingsChangeListener);
- SettingsCache.INSTANCE.get(getPrimaryWindowContext())
+ SettingsCache.INSTANCE.get(mPrimaryWindowContext)
.unregister(NAV_BAR_KIDS_MODE, mOnSettingsChangeListener);
Log.d(TASKBAR_NOT_DESTROYED_TAG, "unregistering component callbacks from destroy().");
- getPrimaryWindowContext().unregisterComponentCallbacks(mDefaultComponentCallbacks);
- mShutdownReceiver.unregisterReceiverSafely(getPrimaryWindowContext());
+ mPrimaryWindowContext.unregisterComponentCallbacks(mDefaultComponentCallbacks);
+ mShutdownReceiver.unregisterReceiverSafely(mPrimaryWindowContext);
destroyAllTaskbars();
}
@@ -829,6 +868,23 @@
}
/**
+ * Returns the {@link TaskbarUIController} associated with the given display ID.
+ * TODO(b/395061396): Remove this method when overview in widow is enabled.
+ *
+ * @param displayId The ID of the display to retrieve the taskbar for.
+ * @return The {@link TaskbarUIController} for the specified display, or
+ * {@code null} if no taskbar is associated with that display.
+ */
+ @Nullable
+ public TaskbarUIController getUIControllerForDisplay(int displayId) {
+ if (!mTaskbars.contains(displayId)) {
+ return null;
+ }
+
+ return getTaskbarForDisplay(displayId).getControllers().uiController;
+ }
+
+ /**
* Retrieves whether RootLayout was added to window for specific display, or false if no
* such mapping has been made.
*
@@ -855,16 +911,16 @@
* Creates a {@link TaskbarActivityContext} for the given display and adds it to the map.
*/
private TaskbarActivityContext createTaskbarActivityContext(DeviceProfile dp, int displayId) {
- Display display = mParentContext.getSystemService(DisplayManager.class).getDisplay(
+ Display display = mBaseContext.getSystemService(DisplayManager.class).getDisplay(
displayId);
Context navigationBarPanelContext = ENABLE_TASKBAR_NAVBAR_UNIFICATION
- ? mParentContext.createWindowContext(display, TYPE_NAVIGATION_BAR_PANEL, null)
+ ? mBaseContext.createWindowContext(display, TYPE_NAVIGATION_BAR_PANEL, null)
: null;
TaskbarActivityContext newTaskbar = new TaskbarActivityContext(getWindowContext(displayId),
navigationBarPanelContext, dp, mDefaultNavButtonController,
mUnfoldProgressProvider, isDefaultDisplay(displayId),
- SystemUiProxy.INSTANCE.get(getPrimaryWindowContext()));
+ SystemUiProxy.INSTANCE.get(mPrimaryWindowContext));
addTaskbarToMap(displayId, newTaskbar);
return newTaskbar;
@@ -965,22 +1021,26 @@
}
/**
- * Creates {@link Context} for the taskbar on the specified display and›› adds it to map.
+ * Creates {@link Context} for the taskbar on the specified display.
* @param displayId The ID of the display for which to create the window context.
*/
- private void createWindowContext(int displayId) {
- DisplayManager displayManager = mParentContext.getSystemService(DisplayManager.class);
+ private @Nullable Context createWindowContext(int displayId) {
+ DisplayManager displayManager = mBaseContext.getSystemService(DisplayManager.class);
if (displayManager == null) {
- return;
+ return null;
}
Display display = displayManager.getDisplay(displayId);
- if (display != null) {
- int windowType = (ENABLE_TASKBAR_NAVBAR_UNIFICATION && isDefaultDisplay(displayId))
- ? TYPE_NAVIGATION_BAR : TYPE_NAVIGATION_BAR_PANEL;
- Context newContext = mParentContext.createWindowContext(display, windowType, null);
- addWindowContextToMap(displayId, newContext);
+ if (display == null) {
+ return null;
}
+
+ int windowType = TYPE_NAVIGATION_BAR_PANEL;
+ if (ENABLE_TASKBAR_NAVBAR_UNIFICATION && isDefaultDisplay(displayId)) {
+ windowType = TYPE_NAVIGATION_BAR;
+ }
+
+ return mBaseContext.createWindowContext(display, windowType, null);
}
/**
@@ -990,12 +1050,13 @@
* @return The Window Context {@link Context} for a given display or {@code null}.
*/
private Context getWindowContext(int displayId) {
- return mWindowContexts.get(displayId);
+ return (isDefaultDisplay(displayId) || !enableTaskbarConnectedDisplays())
+ ? mPrimaryWindowContext : mWindowContexts.get(displayId);
}
@VisibleForTesting
public Context getPrimaryWindowContext() {
- return getWindowContext(getDefaultDisplayId());
+ return mPrimaryWindowContext;
}
/**
@@ -1004,8 +1065,17 @@
* @param displayId The ID of the display for which to retrieve the window manager.
* @return The window manager {@link WindowManager} for a given display or {@code null}.
*/
- private WindowManager getWindowManager(int displayId) {
- return getWindowContext(displayId).getSystemService(WindowManager.class);
+ private @Nullable WindowManager getWindowManager(int displayId) {
+ if (isDefaultDisplay(displayId) || !enableTaskbarConnectedDisplays()) {
+ return mPrimaryWindowManager;
+ }
+
+ Context externalDisplayContext = getWindowContext(displayId);
+ if (externalDisplayContext == null) {
+ return null;
+ }
+
+ return externalDisplayContext.getSystemService(WindowManager.class);
}
/**
@@ -1032,7 +1102,7 @@
}
private int getDefaultDisplayId() {
- return mParentContext.getDisplayId();
+ return mBaseContext.getDisplayId();
}
/** Temp logs for b/254119092. */
@@ -1076,7 +1146,7 @@
log.add("\t\tWindowContext.getResources().getConfiguration()="
+ windowContext.getResources().getConfiguration());
if (mUserUnlocked) {
- log.add("\t\tLauncherAppState.getIDP().getDeviceProfile(getPrimaryWindowContext())"
+ log.add("\t\tLauncherAppState.getIDP().getDeviceProfile(mPrimaryWindowContext)"
+ ".isTaskbarPresent=" + contextTaskbarPresent);
} else {
log.add("\t\tCouldn't get DeviceProfile because !mUserUnlocked");
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarStashController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarStashController.java
index 1ca3dfb..2ded1bf 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarStashController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarStashController.java
@@ -301,7 +301,8 @@
// TODO(b/390665752): Feature to "lock" pinned taskbar to home screen will be superseded by
// pinning, in other launcher states, at which point this variable can be removed.
mInAppStateAffectsDesktopTasksVisibilityInTaskbar =
- DisplayController.showLockedTaskbarOnHome(mActivity);
+ !DisplayController.showDesktopTaskbarForFreeformDisplay(mActivity)
+ && DisplayController.showLockedTaskbarOnHome(mActivity);
mTaskbarBackgroundDuration = activity.getResources().getInteger(
R.integer.taskbar_background_duration);
diff --git a/quickstep/src/com/android/launcher3/taskbar/navbutton/SetupNavLayoutter.kt b/quickstep/src/com/android/launcher3/taskbar/navbutton/SetupNavLayoutter.kt
index 22a3630..e032430 100644
--- a/quickstep/src/com/android/launcher3/taskbar/navbutton/SetupNavLayoutter.kt
+++ b/quickstep/src/com/android/launcher3/taskbar/navbutton/SetupNavLayoutter.kt
@@ -17,6 +17,7 @@
package com.android.launcher3.taskbar.navbutton
import android.content.res.Resources
+import android.os.SystemProperties
import android.view.Gravity
import android.view.ViewGroup
import android.view.ViewGroup.LayoutParams.WRAP_CONTENT
@@ -39,7 +40,7 @@
startContextualContainer: ViewGroup,
imeSwitcher: ImageView?,
a11yButton: ImageView?,
- space: Space?
+ space: Space?,
) :
AbstractNavButtonLayoutter(
resources,
@@ -48,11 +49,15 @@
startContextualContainer,
imeSwitcher,
a11yButton,
- space
+ space,
) {
private val mNavButtonsView = navButtonsView
override fun layoutButtons(context: TaskbarActivityContext, isA11yButtonPersistent: Boolean) {
+ val SUWTheme = SystemProperties.get("setupwizard.theme", "")
+ if (SUWTheme == "glif_expressive" || SUWTheme == "glif_expressive_light") {
+ return
+ }
// Since setup wizard only has back button enabled, it looks strange to be
// end-aligned, so start-align instead.
val navButtonsLayoutParams = navButtonContainer.layoutParams as FrameLayout.LayoutParams
@@ -80,7 +85,7 @@
adjustForSetupInPhoneMode(
navButtonsLayoutParams,
navButtonsViewLayoutParams,
- deviceProfile
+ deviceProfile,
)
}
mNavButtonsView.layoutParams = navButtonsViewLayoutParams
@@ -97,7 +102,7 @@
WRAP_CONTENT,
contextualMargin,
contextualMargin,
- Gravity.START
+ Gravity.START,
)
if (imeSwitcher != null) {
diff --git a/quickstep/src/com/android/launcher3/uioverrides/PredictedAppIcon.java b/quickstep/src/com/android/launcher3/uioverrides/PredictedAppIcon.java
index 36a4865..b25f999 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/PredictedAppIcon.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/PredictedAppIcon.java
@@ -44,6 +44,7 @@
import androidx.core.graphics.ColorUtils;
import com.android.launcher3.DeviceProfile;
+import com.android.launcher3.Flags;
import com.android.launcher3.Launcher;
import com.android.launcher3.R;
import com.android.launcher3.anim.AnimatedFloat;
@@ -68,8 +69,7 @@
private static final float RING_SCALE_START_VALUE = 0.75f;
private static final int RING_SHADOW_COLOR = 0x99000000;
- private static final float RING_EFFECT_RATIO = 0.095f;
-
+ private static final float RING_EFFECT_RATIO = Flags.enableLauncherIconShapes() ? 0.1f : 0.095f;
private static final long ICON_CHANGE_ANIM_DURATION = 360;
private static final long ICON_CHANGE_ANIM_STAGGER = 50;
@@ -150,12 +150,12 @@
int count = canvas.save();
boolean isSlotMachineAnimRunning = mSlotMachineIcon != null;
if (!mIsPinned) {
- drawEffect(canvas);
+ drawRingEffect(canvas);
if (isSlotMachineAnimRunning) {
// Clip to to outside of the ring during the slot machine animation.
canvas.clipPath(mRingPath);
}
- canvas.scale(1 - 2 * RING_EFFECT_RATIO, 1 - 2 * RING_EFFECT_RATIO,
+ canvas.scale(1 - 2f * RING_EFFECT_RATIO, 1 - 2f * RING_EFFECT_RATIO,
getWidth() * .5f, getHeight() * .5f);
if (isSlotMachineAnimRunning) {
canvas.translate(0, mSlotMachineIconTranslationY);
@@ -388,7 +388,7 @@
mRingScaleAnim.start();
}
- private void drawEffect(Canvas canvas) {
+ private void drawRingEffect(Canvas canvas) {
// Don't draw ring effect if item is about to be dragged or if the icon is not visible.
if (mDrawForDrag || !mIsIconVisible || mForceHideRing) {
return;
@@ -396,12 +396,28 @@
mIconRingPaint.setColor(RING_SHADOW_COLOR);
mIconRingPaint.setMaskFilter(mShadowFilter);
int count = canvas.save();
- if (Float.compare(1, mRingScale) != 0) {
+ if (Flags.enableLauncherIconShapes()) {
+ // Scale canvas properly to for ring to be inner stroke and not exceed bounds.
+ // Since STROKE draws half on either side of Path, scale canvas down by 1x stroke ratio.
+ canvas.scale(
+ mRingScale * (1f - RING_EFFECT_RATIO),
+ mRingScale * (1f - RING_EFFECT_RATIO),
+ canvas.getWidth() / 2f,
+ canvas.getHeight() / 2f);
+ } else if (Float.compare(1, mRingScale) != 0) {
canvas.scale(mRingScale, mRingScale, canvas.getWidth() / 2f, canvas.getHeight() / 2f);
}
+ // Draw ring shadow around canvas.
canvas.drawPath(mRingPath, mIconRingPaint);
mIconRingPaint.setColor(mPlateColor.currentColor);
+ if (Flags.enableLauncherIconShapes()) {
+ mIconRingPaint.setStrokeWidth(canvas.getWidth() * RING_EFFECT_RATIO);
+ // Using FILL_AND_STROKE as there is still some gap to fill,
+ // between inner curve of ring / outer curve of icon.
+ mIconRingPaint.setStyle(Paint.Style.FILL_AND_STROKE);
+ }
mIconRingPaint.setMaskFilter(null);
+ // Draw ring around canvas.
canvas.drawPath(mRingPath, mIconRingPaint);
canvas.restoreToCount(count);
}
diff --git a/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/TaskViewDismissTouchController.kt b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/TaskViewDismissTouchController.kt
index 99b962b..77a05c1 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/TaskViewDismissTouchController.kt
+++ b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/TaskViewDismissTouchController.kt
@@ -20,6 +20,7 @@
import androidx.dynamicanimation.animation.SpringAnimation
import com.android.app.animation.Interpolators.DECELERATE
import com.android.launcher3.AbstractFloatingView
+import com.android.launcher3.R
import com.android.launcher3.Utilities.EDGE_NAV_BAR
import com.android.launcher3.Utilities.boundToRange
import com.android.launcher3.Utilities.isRtl
@@ -144,7 +145,7 @@
0f,
dismissLength.toFloat(),
0f,
- DISMISS_MAX_UNDERSHOOT,
+ container.resources.getDimension(R.dimen.task_dismiss_max_undershoot),
DECELERATE,
)
taskBeingDragged.secondaryDismissTranslationProperty.setValue(
@@ -207,6 +208,5 @@
companion object {
private const val DISMISS_THRESHOLD_FRACTION = 0.5f
- private const val DISMISS_MAX_UNDERSHOOT = 25f
}
}
diff --git a/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java b/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java
index f4400fa..f46f9ae 100644
--- a/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java
+++ b/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java
@@ -1709,7 +1709,7 @@
if (mRecentsView != null) {
mRecentsView.onPrepareGestureEndAnimation(null, mGestureState.getEndTarget(),
- getRemoteTaskViewSimulators());
+ mRemoteTargetHandles);
}
} else {
AnimatorSet animatorSet = new AnimatorSet();
@@ -1753,7 +1753,7 @@
mRecentsView.onPrepareGestureEndAnimation(
mGestureState.isHandlingAtomicEvent() ? null : animatorSet,
mGestureState.getEndTarget(),
- getRemoteTaskViewSimulators());
+ mRemoteTargetHandles);
}
animatorSet.setDuration(duration).setInterpolator(interpolator);
animatorSet.start();
diff --git a/quickstep/src/com/android/quickstep/FocusState.kt b/quickstep/src/com/android/quickstep/FocusState.kt
index ba3991f..7c6aa5b 100644
--- a/quickstep/src/com/android/quickstep/FocusState.kt
+++ b/quickstep/src/com/android/quickstep/FocusState.kt
@@ -27,7 +27,10 @@
class FocusState {
var focusedDisplayId = DEFAULT_DISPLAY
- private set
+ private set(value) {
+ field = value
+ listeners.forEach { it.onFocusedDisplayChanged(value) }
+ }
private var listeners = mutableSetOf<FocusChangeListener>()
@@ -40,9 +43,7 @@
transitions?.setFocusTransitionListener(
object : Stub() {
override fun onFocusedDisplayChanged(displayId: Int) {
- Executors.MAIN_EXECUTOR.execute {
- listeners.forEach { it.onFocusedDisplayChanged(displayId) }
- }
+ Executors.MAIN_EXECUTOR.execute { focusedDisplayId = displayId }
}
}
)
diff --git a/quickstep/src/com/android/quickstep/OverviewCommandHelper.kt b/quickstep/src/com/android/quickstep/OverviewCommandHelper.kt
index 94d115b..42aa86e 100644
--- a/quickstep/src/com/android/quickstep/OverviewCommandHelper.kt
+++ b/quickstep/src/com/android/quickstep/OverviewCommandHelper.kt
@@ -28,6 +28,7 @@
import androidx.annotation.UiThread
import androidx.annotation.VisibleForTesting
import com.android.internal.jank.Cuj
+import com.android.launcher3.Flags.enableAltTabKqsOnConnectedDisplays
import com.android.launcher3.Flags.enableFallbackOverviewInWindow
import com.android.launcher3.Flags.enableLargeDesktopWindowingTile
import com.android.launcher3.Flags.enableLauncherOverviewInWindow
@@ -38,6 +39,8 @@
import com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_OVERVIEW_SHOW_OVERVIEW_FROM_3_BUTTON
import com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_OVERVIEW_SHOW_OVERVIEW_FROM_KEYBOARD_QUICK_SWITCH
import com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_OVERVIEW_SHOW_OVERVIEW_FROM_KEYBOARD_SHORTCUT
+import com.android.launcher3.taskbar.TaskbarManager
+import com.android.launcher3.taskbar.TaskbarUIController
import com.android.launcher3.util.Executors
import com.android.launcher3.util.RunnableList
import com.android.launcher3.util.coroutines.DispatcherProvider
@@ -48,6 +51,7 @@
import com.android.quickstep.OverviewCommandHelper.CommandType.KEYBOARD_INPUT
import com.android.quickstep.OverviewCommandHelper.CommandType.SHOW
import com.android.quickstep.OverviewCommandHelper.CommandType.TOGGLE
+import com.android.quickstep.fallback.window.RecentsDisplayModel
import com.android.quickstep.util.ActiveGestureLog
import com.android.quickstep.util.ActiveGestureProtoLogProxy
import com.android.quickstep.views.RecentsView
@@ -72,6 +76,9 @@
private val overviewComponentObserver: OverviewComponentObserver,
private val taskAnimationManager: TaskAnimationManager,
private val dispatcherProvider: DispatcherProvider = ProductionDispatchers,
+ private val recentsDisplayModel: RecentsDisplayModel,
+ private val focusState: FocusState,
+ private val taskbarManager: TaskbarManager,
) {
private val coroutineScope = CoroutineScope(SupervisorJob() + dispatcherProvider.background)
@@ -291,15 +298,51 @@
deviceProfile != null &&
(deviceProfile.isTablet || deviceProfile.isTwoPanels)
+ val focusedDisplayId = focusState.focusedDisplayId
+ val focusedDisplayUIController: TaskbarUIController? =
+ if (RecentsDisplayModel.enableOverviewInWindow()) {
+ Log.d(
+ TAG,
+ "Querying RecentsDisplayModel for TaskbarUIController for display: $focusedDisplayId",
+ )
+ recentsDisplayModel.getRecentsWindowManager(focusedDisplayId)?.taskbarUIController
+ } else {
+ Log.d(
+ TAG,
+ "Querying TaskbarManager for TaskbarUIController for display: $focusedDisplayId",
+ )
+ // TODO(b/395061396): Remove this path when overview in widow is enabled.
+ taskbarManager.getUIControllerForDisplay(focusedDisplayId)
+ }
+ Log.d(
+ TAG,
+ "TaskbarUIController for display $focusedDisplayId was" +
+ "${if (focusedDisplayUIController == null) " not" else ""} found",
+ )
+
when (command.type) {
HIDE -> {
if (!allowQuickSwitch) return true
- keyboardTaskFocusIndex = uiController!!.launchFocusedTask()
+ keyboardTaskFocusIndex =
+ if (
+ enableAltTabKqsOnConnectedDisplays() && focusedDisplayUIController != null
+ ) {
+ focusedDisplayUIController.launchFocusedTask()
+ } else {
+ uiController!!.launchFocusedTask()
+ }
+
if (keyboardTaskFocusIndex == -1) return true
}
KEYBOARD_INPUT ->
if (allowQuickSwitch) {
- uiController!!.openQuickSwitchView()
+ if (
+ enableAltTabKqsOnConnectedDisplays() && focusedDisplayUIController != null
+ ) {
+ focusedDisplayUIController.openQuickSwitchView()
+ } else {
+ uiController!!.openQuickSwitchView()
+ }
return true
} else {
keyboardTaskFocusIndex = 0
diff --git a/quickstep/src/com/android/quickstep/SystemUiProxy.kt b/quickstep/src/com/android/quickstep/SystemUiProxy.kt
index d5382ad..a1ac39e 100644
--- a/quickstep/src/com/android/quickstep/SystemUiProxy.kt
+++ b/quickstep/src/com/android/quickstep/SystemUiProxy.kt
@@ -149,7 +149,7 @@
private var backToLauncherRunner: IRemoteAnimationRunner? = null
private var dragAndDrop: IDragAndDrop? = null
val homeVisibilityState = HomeVisibilityState()
- private val focusState = FocusState()
+ val focusState = FocusState()
// Used to dedupe calls to SystemUI
private var lastShelfHeight = 0
diff --git a/quickstep/src/com/android/quickstep/TaskIconCache.kt b/quickstep/src/com/android/quickstep/TaskIconCache.kt
index 7692e78..6a7f1af 100644
--- a/quickstep/src/com/android/quickstep/TaskIconCache.kt
+++ b/quickstep/src/com/android/quickstep/TaskIconCache.kt
@@ -40,6 +40,7 @@
import com.android.launcher3.util.FlagOp
import com.android.launcher3.util.Preconditions
import com.android.quickstep.task.thumbnail.data.TaskIconDataSource
+import com.android.quickstep.util.IconLabelUtil.getBadgedContentDescription
import com.android.quickstep.util.TaskKeyLruCache
import com.android.quickstep.util.TaskVisualsChangeListener
import com.android.systemui.shared.recents.model.Task
@@ -207,6 +208,7 @@
TaskCacheEntry(
entryIcon,
getBadgedContentDescription(
+ context,
activityInfo,
task.key.userId,
task.taskDescription,
@@ -216,7 +218,12 @@
else ->
TaskCacheEntry(
entryIcon,
- getBadgedContentDescription(activityInfo, task.key.userId, task.taskDescription),
+ getBadgedContentDescription(
+ context,
+ activityInfo,
+ task.key.userId,
+ task.taskDescription,
+ ),
)
}.also { iconCache.put(task.key, it) }
}
@@ -225,28 +232,6 @@
desc.inMemoryIcon
?: ActivityManager.TaskDescription.loadTaskDescriptionIcon(desc.iconFilename, userId)
- private fun getBadgedContentDescription(
- info: ActivityInfo,
- userId: Int,
- taskDescription: ActivityManager.TaskDescription?,
- ): String {
- val packageManager = context.packageManager
- var taskLabel = taskDescription?.let { Utilities.trim(it.label) }
- if (taskLabel.isNullOrEmpty()) {
- taskLabel = Utilities.trim(info.loadLabel(packageManager))
- }
-
- val applicationLabel = Utilities.trim(info.applicationInfo.loadLabel(packageManager))
- val badgedApplicationLabel =
- if (userId != UserHandle.myUserId())
- packageManager
- .getUserBadgedLabel(applicationLabel, UserHandle.of(userId))
- .toString()
- else applicationLabel
- return if (applicationLabel == taskLabel) badgedApplicationLabel
- else "$badgedApplicationLabel $taskLabel"
- }
-
@WorkerThread
private fun getDefaultIcon(userId: Int): Drawable {
synchronized(defaultIcons) {
diff --git a/quickstep/src/com/android/quickstep/TouchInteractionService.java b/quickstep/src/com/android/quickstep/TouchInteractionService.java
index 2df4a45..ba4c65a 100644
--- a/quickstep/src/com/android/quickstep/TouchInteractionService.java
+++ b/quickstep/src/com/android/quickstep/TouchInteractionService.java
@@ -88,6 +88,7 @@
import com.android.launcher3.util.TraceHelper;
import com.android.quickstep.OverviewCommandHelper.CommandType;
import com.android.quickstep.OverviewComponentObserver.OverviewChangeListener;
+import com.android.quickstep.fallback.window.RecentsDisplayModel;
import com.android.quickstep.fallback.window.RecentsWindowSwipeHandler;
import com.android.quickstep.inputconsumers.BubbleBarInputConsumer;
import com.android.quickstep.inputconsumers.OneHandedModeInputConsumer;
@@ -584,7 +585,8 @@
initInputMonitor("onTrackpadConnected()");
});
- mTaskbarManager = new TaskbarManager(this, mAllAppsActionManager, mNavCallbacks);
+ mTaskbarManager = new TaskbarManager(this, mAllAppsActionManager, mNavCallbacks,
+ RecentsDisplayModel.getINSTANCE().get(this));
mDesktopAppLaunchTransitionManager =
new DesktopAppLaunchTransitionManager(this, SystemUiProxy.INSTANCE.get(this));
mDesktopAppLaunchTransitionManager.registerTransitions();
@@ -641,7 +643,9 @@
mTaskAnimationManager = new TaskAnimationManager(this, mDeviceState);
mOverviewComponentObserver = OverviewComponentObserver.INSTANCE.get(this);
mOverviewCommandHelper = new OverviewCommandHelper(this,
- mOverviewComponentObserver, mTaskAnimationManager);
+ mOverviewComponentObserver, mTaskAnimationManager,
+ RecentsDisplayModel.getINSTANCE().get(this),
+ SystemUiProxy.INSTANCE.get(this).getFocusState(), mTaskbarManager);
mResetGestureInputConsumer = new ResetGestureInputConsumer(
mTaskAnimationManager, mTaskbarManager::getCurrentActivityContext);
mInputConsumer.registerInputConsumer();
diff --git a/quickstep/src/com/android/quickstep/fallback/FallbackRecentsView.java b/quickstep/src/com/android/quickstep/fallback/FallbackRecentsView.java
index 8d010e2..f426bf5 100644
--- a/quickstep/src/com/android/quickstep/fallback/FallbackRecentsView.java
+++ b/quickstep/src/com/android/quickstep/fallback/FallbackRecentsView.java
@@ -44,11 +44,11 @@
import com.android.quickstep.BaseContainerInterface;
import com.android.quickstep.FallbackActivityInterface;
import com.android.quickstep.GestureState;
+import com.android.quickstep.RemoteTargetGluer.RemoteTargetHandle;
import com.android.quickstep.fallback.window.RecentsDisplayModel;
import com.android.quickstep.util.GroupTask;
import com.android.quickstep.util.SingleTask;
import com.android.quickstep.util.SplitSelectStateController;
-import com.android.quickstep.util.TaskViewSimulator;
import com.android.quickstep.views.OverviewActionsView;
import com.android.quickstep.views.RecentsView;
import com.android.quickstep.views.RecentsViewContainer;
@@ -129,8 +129,8 @@
@Override
public void onPrepareGestureEndAnimation(
@Nullable AnimatorSet animatorSet, GestureState.GestureEndTarget endTarget,
- TaskViewSimulator[] taskViewSimulators) {
- super.onPrepareGestureEndAnimation(animatorSet, endTarget, taskViewSimulators);
+ RemoteTargetHandle[] remoteTargetHandles) {
+ super.onPrepareGestureEndAnimation(animatorSet, endTarget, remoteTargetHandles);
if (mHomeTask != null && endTarget == RECENTS && animatorSet != null) {
TaskView tv = getTaskViewByTaskId(mHomeTask.key.id);
if (tv != null) {
diff --git a/quickstep/src/com/android/quickstep/fallback/window/RecentsDisplayModel.kt b/quickstep/src/com/android/quickstep/fallback/window/RecentsDisplayModel.kt
index 31a1be8..95a3ec2 100644
--- a/quickstep/src/com/android/quickstep/fallback/window/RecentsDisplayModel.kt
+++ b/quickstep/src/com/android/quickstep/fallback/window/RecentsDisplayModel.kt
@@ -50,10 +50,14 @@
DaggerSingletonObject<RecentsDisplayModel>(
QuickstepBaseAppComponent::getRecentsDisplayModel
)
+
+ @JvmStatic
+ fun enableOverviewInWindow() =
+ Flags.enableFallbackOverviewInWindow() || Flags.enableLauncherOverviewInWindow()
}
init {
- if (Flags.enableFallbackOverviewInWindow() || Flags.enableLauncherOverviewInWindow()) {
+ if (enableOverviewInWindow()) {
displayManager.registerDisplayListener(displayListener, Executors.MAIN_EXECUTOR.handler)
// In the scenario where displays were added before this display listener was
// registered, we should store the RecentsDisplayResources for those displays
diff --git a/quickstep/src/com/android/quickstep/recents/data/TasksRepository.kt b/quickstep/src/com/android/quickstep/recents/data/TasksRepository.kt
index 002a4e8..b1a5920 100644
--- a/quickstep/src/com/android/quickstep/recents/data/TasksRepository.kt
+++ b/quickstep/src/com/android/quickstep/recents/data/TasksRepository.kt
@@ -66,14 +66,10 @@
)
tasks.value = MapForStateFlow(recentTasks)
- // Request data for completed tasks to prevent stale data.
- // This will prevent thumbnail and icon from being replaced and
- // null due to race condition.
- taskRequests.values.forEach { (taskKey, job) ->
- if (job.isCompleted) {
- requestTaskData(taskKey.id)
- }
- }
+ // Request data for tasks to prevent stale data.
+ // This will prevent thumbnail and icon from being replaced and null due to
+ // race condition. The new request will hit the cache and return immediately.
+ taskRequests.keys.forEach(::requestTaskData)
}
}
return tasks.map { it.values.toList() }
diff --git a/quickstep/src/com/android/quickstep/recents/di/RecentsDependencies.kt b/quickstep/src/com/android/quickstep/recents/di/RecentsDependencies.kt
index 02f48e6..d2f10b6 100644
--- a/quickstep/src/com/android/quickstep/recents/di/RecentsDependencies.kt
+++ b/quickstep/src/com/android/quickstep/recents/di/RecentsDependencies.kt
@@ -28,12 +28,11 @@
import com.android.quickstep.recents.data.TasksRepository
import com.android.quickstep.recents.domain.usecase.GetSysUiStatusNavFlagsUseCase
import com.android.quickstep.recents.domain.usecase.GetTaskUseCase
+import com.android.quickstep.recents.domain.usecase.IsThumbnailValidUseCase
+import com.android.quickstep.recents.domain.usecase.OrganizeDesktopTasksUseCase
import com.android.quickstep.recents.usecase.GetThumbnailPositionUseCase
import com.android.quickstep.recents.usecase.GetThumbnailUseCase
import com.android.quickstep.recents.viewmodel.RecentsViewData
-import com.android.quickstep.task.thumbnail.SplashAlphaUseCase
-import com.android.quickstep.task.thumbnail.TaskThumbnailViewData
-import com.android.quickstep.task.viewmodel.TaskContainerData
import com.android.quickstep.task.viewmodel.TaskOverlayViewModel
import com.android.quickstep.task.viewmodel.TaskThumbnailViewModel
import com.android.quickstep.task.viewmodel.TaskThumbnailViewModelImpl
@@ -174,14 +173,8 @@
val instance: Any =
when (modelClass) {
RecentsViewData::class.java -> RecentsViewData()
- TaskContainerData::class.java -> TaskContainerData()
- TaskThumbnailViewData::class.java -> TaskThumbnailViewData()
TaskThumbnailViewModel::class.java ->
- TaskThumbnailViewModelImpl(
- dispatcherProvider = inject(),
- getThumbnailPositionUseCase = inject(),
- splashAlphaUseCase = inject(scopeId),
- )
+ TaskThumbnailViewModelImpl(getThumbnailPositionUseCase = inject())
TaskOverlayViewModel::class.java -> {
val task = extras["Task"] as Task
TaskOverlayViewModel(
@@ -192,6 +185,8 @@
dispatcherProvider = inject(),
)
}
+ IsThumbnailValidUseCase::class.java ->
+ IsThumbnailValidUseCase(rotationStateRepository = inject())
GetTaskUseCase::class.java -> GetTaskUseCase(repository = inject())
GetThumbnailUseCase::class.java -> GetThumbnailUseCase(taskRepository = inject())
GetSysUiStatusNavFlagsUseCase::class.java -> GetSysUiStatusNavFlagsUseCase()
@@ -201,14 +196,7 @@
rotationStateRepository = inject(),
tasksRepository = inject(),
)
- SplashAlphaUseCase::class.java ->
- SplashAlphaUseCase(
- recentsViewData = inject(),
- taskContainerData = inject(scopeId),
- taskThumbnailViewData = inject(scopeId),
- tasksRepository = inject(),
- rotationStateRepository = inject(),
- )
+ OrganizeDesktopTasksUseCase::class.java -> OrganizeDesktopTasksUseCase()
else -> {
log("Factory for ${modelClass.simpleName} not defined!", Log.ERROR)
error("Factory for ${modelClass.simpleName} not defined!")
diff --git a/quickstep/src/com/android/quickstep/task/viewmodel/TaskContainerData.kt b/quickstep/src/com/android/quickstep/recents/domain/model/DesktopTaskBoundsData.kt
similarity index 70%
rename from quickstep/src/com/android/quickstep/task/viewmodel/TaskContainerData.kt
rename to quickstep/src/com/android/quickstep/recents/domain/model/DesktopTaskBoundsData.kt
index 279ce39..a7f102c 100644
--- a/quickstep/src/com/android/quickstep/task/viewmodel/TaskContainerData.kt
+++ b/quickstep/src/com/android/quickstep/recents/domain/model/DesktopTaskBoundsData.kt
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2024 The Android Open Source Project
+ * Copyright (C) 2025 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.
@@ -14,10 +14,8 @@
* limitations under the License.
*/
-package com.android.quickstep.task.viewmodel
+package com.android.quickstep.recents.domain.model
-import kotlinx.coroutines.flow.MutableStateFlow
+import android.graphics.Rect
-class TaskContainerData {
- val thumbnailSplashProgress = MutableStateFlow(0f)
-}
+data class DesktopTaskBoundsData(val taskId: Int, val bounds: Rect)
diff --git a/quickstep/src/com/android/quickstep/recents/domain/usecase/IsThumbnailValidUseCase.kt b/quickstep/src/com/android/quickstep/recents/domain/usecase/IsThumbnailValidUseCase.kt
new file mode 100644
index 0000000..02f8329
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/recents/domain/usecase/IsThumbnailValidUseCase.kt
@@ -0,0 +1,74 @@
+/*
+ * Copyright (C) 2025 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.recents.domain.usecase
+
+import android.graphics.Bitmap
+import android.view.Surface
+import com.android.quickstep.recents.data.RecentsRotationStateRepository
+import com.android.systemui.shared.recents.model.ThumbnailData
+import com.android.systemui.shared.recents.utilities.PreviewPositionHelper
+import com.android.systemui.shared.recents.utilities.Utilities
+
+/**
+ * Use case responsible for validating the aspect ratio and rotation of a thumbnail against the
+ * expected values based on the view's dimensions and the current rotation state.
+ *
+ * This class checks if the thumbnail's aspect ratio significantly differs from the aspect ratio of
+ * the view it is intended to be displayed in, and if the thumbnail's rotation is consistent with
+ * the device's current rotation state.
+ *
+ * @property rotationStateRepository Repository providing the current rotation state of the device.
+ */
+class IsThumbnailValidUseCase(private val rotationStateRepository: RecentsRotationStateRepository) {
+ operator fun invoke(thumbnailData: ThumbnailData?, viewWidth: Int, viewHeight: Int): Boolean {
+ val thumbnail = thumbnailData?.thumbnail ?: return false
+ return !isInaccurateThumbnail(thumbnail, viewWidth, viewHeight, thumbnailData.rotation)
+ }
+
+ private fun isInaccurateThumbnail(
+ thumbnail: Bitmap,
+ viewWidth: Int,
+ viewHeight: Int,
+ rotation: Int,
+ ): Boolean =
+ isAspectRatioDifferentFromViewAspectRatio(
+ thumbnail = thumbnail,
+ width = viewWidth.toFloat(),
+ height = viewHeight.toFloat(),
+ ) || isRotationDifferentFromTask(rotation)
+
+ private fun isAspectRatioDifferentFromViewAspectRatio(
+ thumbnail: Bitmap,
+ width: Float,
+ height: Float,
+ ): Boolean {
+ return Utilities.isRelativePercentDifferenceGreaterThan(
+ /* first = */ width / height,
+ /* second = */ thumbnail.width / thumbnail.height.toFloat(),
+ /* bound = */ PreviewPositionHelper.MAX_PCT_BEFORE_ASPECT_RATIOS_CONSIDERED_DIFFERENT,
+ )
+ }
+
+ private fun isRotationDifferentFromTask(thumbnailRotation: Int): Boolean {
+ val rotationState = rotationStateRepository.getRecentsRotationState()
+ return if (rotationState.orientationHandlerRotation == Surface.ROTATION_0) {
+ (rotationState.activityRotation - thumbnailRotation) % 2 != 0
+ } else {
+ rotationState.orientationHandlerRotation != thumbnailRotation
+ }
+ }
+}
diff --git a/quickstep/src/com/android/quickstep/recents/domain/usecase/OrganizeDesktopTasksUseCase.kt b/quickstep/src/com/android/quickstep/recents/domain/usecase/OrganizeDesktopTasksUseCase.kt
new file mode 100644
index 0000000..4ea39d8
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/recents/domain/usecase/OrganizeDesktopTasksUseCase.kt
@@ -0,0 +1,311 @@
+/*
+ * Copyright (C) 2025 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.recents.domain.usecase
+
+import android.graphics.Rect
+import android.graphics.RectF
+import androidx.core.graphics.toRect
+import com.android.quickstep.recents.domain.model.DesktopTaskBoundsData
+
+/** This usecase is responsible for organizing desktop windows in a non-overlapping way. */
+class OrganizeDesktopTasksUseCase {
+ /**
+ * Run to layout [taskBounds] within the screen [desktopBounds]. Layout is done in 2 stages:
+ * 1. Optimal height is determined. In this stage height is bisected to find maximum height
+ * which still allows all the windows to fit.
+ * 2. Row widths are balanced. In this stage the available width is reduced until some windows
+ * are no longer fitting or until the difference between the narrowest and the widest rows
+ * starts growing. Overall this achieves the goals of maximum size for previews (or maximum
+ * row height which is equivalent assuming fixed height), balanced rows and minimal wasted
+ * space.
+ */
+ fun run(
+ desktopBounds: Rect,
+ taskBounds: List<DesktopTaskBoundsData>,
+ ): List<DesktopTaskBoundsData> {
+ if (desktopBounds.isEmpty || taskBounds.isEmpty()) {
+ return emptyList()
+ }
+
+ // Filter out [taskBounds] with empty rects before calculating layout.
+ val validTaskBounds = taskBounds.filterNot { it.bounds.isEmpty }
+
+ if (validTaskBounds.isEmpty()) {
+ return emptyList()
+ }
+
+ val availableLayoutBounds = desktopBounds.getLayoutEffectiveBounds()
+ val resultRects = findOptimalHeightAndBalancedWidth(availableLayoutBounds, validTaskBounds)
+
+ centerTaskWindows(
+ availableLayoutBounds,
+ resultRects.maxOf { it.bottom }.toInt(),
+ resultRects,
+ )
+
+ val result = mutableListOf<DesktopTaskBoundsData>()
+ for (i in validTaskBounds.indices) {
+ result.add(DesktopTaskBoundsData(validTaskBounds[i].taskId, resultRects[i].toRect()))
+ }
+ return result
+ }
+
+ /** Calculates the effective bounds for layout by applying insets to the raw desktop bounds. */
+ private fun Rect.getLayoutEffectiveBounds() =
+ Rect(this).apply { inset(OVERVIEW_INSET_TOP_BOTTOM, OVERVIEW_INSET_LEFT_RIGHT) }
+
+ /**
+ * Determines the optimal height for task windows and balances the row widths to minimize wasted
+ * space. Returns the bounds for each task window after layout.
+ */
+ private fun findOptimalHeightAndBalancedWidth(
+ availableLayoutBounds: Rect,
+ validTaskBounds: List<DesktopTaskBoundsData>,
+ ): List<RectF> {
+ // Right bound of the narrowest row.
+ var minRight: Int
+ // Right bound of the widest row.
+ var maxRight: Int
+
+ // Keep track of the difference between the narrowest and the widest row.
+ // Initially this is set to the worst it can ever be assuming the windows fit.
+ var widthDiff = availableLayoutBounds.width()
+
+ // Initially allow the windows to occupy all available width. Shrink this available space
+ // horizontally to find the breakdown into rows that achieves the minimal [widthDiff].
+ var rightBound = availableLayoutBounds.right
+
+ // Determine the optimal height bisecting between [lowHeight] and [highHeight]. Once this
+ // optimal height is known, [heightFixed] is set to `true` and the rows are balanced by
+ // repeatedly squeezing the widest row to cause windows to overflow to the subsequent rows.
+ var lowHeight = VERTICAL_SPACE_BETWEEN_TASKS
+ var highHeight = maxOf(lowHeight, availableLayoutBounds.height() + 1)
+ var optimalHeight = 0.5f * (lowHeight + highHeight)
+ var heightFixed = false
+
+ // Repeatedly try to fit the windows [resultRects] within [rightBound]. If a maximum
+ // [optimalHeight] is found such that all window [resultRects] fit, this fitting continues
+ // while shrinking the [rightBound] in order to balance the rows. If the windows fit the
+ // [rightBound] would have been decremented at least once so it needs to be incremented once
+ // before getting out of this loop and one additional pass made to actually fit the
+ // [resultRects]. If the [resultRects] cannot fit (e.g. there are too many windows) the
+ // bisection will still finish and we might increment the [rightBound] one pixel extra
+ // which is acceptable since there is an unused margin on the right.
+ var makeLastAdjustment = false
+ var resultRects: List<RectF>
+
+ while (true) {
+ val fitWindowResult =
+ fitWindowRectsInBounds(
+ Rect(availableLayoutBounds).apply { right = rightBound },
+ validTaskBounds,
+ minOf(MAXIMUM_TASK_HEIGHT, optimalHeight.toInt()),
+ )
+ val allWindowsFit = fitWindowResult.allWindowsFit
+ resultRects = fitWindowResult.calculatedBounds
+ minRight = fitWindowResult.minRight
+ maxRight = fitWindowResult.maxRight
+
+ if (heightFixed) {
+ if (!allWindowsFit) {
+ // Revert the previous change to [rightBound] and do one last pass.
+ rightBound++
+ makeLastAdjustment = true
+ break
+ }
+ // Break if all the windows are zero-width at the current scale.
+ if (maxRight <= availableLayoutBounds.left) {
+ break
+ }
+ } else {
+ // Find the optimal row height bisecting between [lowHeight] and [highHeight].
+ if (allWindowsFit) {
+ lowHeight = optimalHeight.toInt()
+ } else {
+ highHeight = optimalHeight.toInt()
+ }
+ optimalHeight = 0.5f * (lowHeight + highHeight)
+ // When height can no longer be improved, start balancing the rows.
+ if (optimalHeight.toInt() == lowHeight) {
+ heightFixed = true
+ }
+ }
+
+ if (allWindowsFit && heightFixed) {
+ if (maxRight - minRight <= widthDiff) {
+ // Row alignment is getting better. Try to shrink the [rightBound] in order to
+ // squeeze the widest row.
+ rightBound = maxRight - 1
+ widthDiff = maxRight - minRight
+ } else {
+ // Row alignment is getting worse.
+ // Revert the previous change to [rightBound] and do one last pass.
+ rightBound++
+ makeLastAdjustment = true
+ break
+ }
+ }
+ }
+
+ // Once the windows no longer fit, the change to [rightBound] was reverted. Perform one last
+ // pass to position the [resultRects].
+ if (makeLastAdjustment) {
+ val fitWindowResult =
+ fitWindowRectsInBounds(
+ Rect(availableLayoutBounds).apply { right = rightBound },
+ validTaskBounds,
+ minOf(MAXIMUM_TASK_HEIGHT, optimalHeight.toInt()),
+ )
+ resultRects = fitWindowResult.calculatedBounds
+ }
+
+ return resultRects
+ }
+
+ /**
+ * Data structure to hold the returned result of [fitWindowRectsInBounds] function.
+ * [allWindowsFit] specifies whether all windows can be fit into the provided layout bounds.
+ * [calculatedBounds] specifies the output bounds for all provided task windows. [minRight]
+ * specifies the right bound of the narrowest row. [maxRight] specifies the right bound of the
+ * widest rows.
+ */
+ data class FitWindowResult(
+ val allWindowsFit: Boolean,
+ val calculatedBounds: List<RectF>,
+ val minRight: Int,
+ val maxRight: Int,
+ )
+
+ /**
+ * Attempts to fit all [taskBounds] inside [layoutBounds]. The method ensures that the returned
+ * output bounds list has appropriate size and populates it with the values placing task windows
+ * next to each other left-to-right in rows of equal [optimalWindowHeight].
+ */
+ private fun fitWindowRectsInBounds(
+ layoutBounds: Rect,
+ taskBounds: List<DesktopTaskBoundsData>,
+ optimalWindowHeight: Int,
+ ): FitWindowResult {
+ val numTasks = taskBounds.size
+ val outRects = mutableListOf<RectF>()
+
+ // Start in the top-left corner of [layoutBounds].
+ var left = layoutBounds.left
+ var top = layoutBounds.top
+
+ // Right bound of the narrowest row.
+ var minRight = layoutBounds.right
+ // Right bound of the widest row.
+ var maxRight = layoutBounds.left
+
+ var allWindowsFit = true
+ for (i in 0 until numTasks) {
+ val taskBounds = taskBounds[i].bounds
+
+ // Use the height to calculate the width
+ val scale = optimalWindowHeight / taskBounds.height().toFloat()
+ val width = (taskBounds.width() * scale).toInt()
+ val optimalRowHeight = optimalWindowHeight + VERTICAL_SPACE_BETWEEN_TASKS
+
+ if ((left + width + HORIZONTAL_SPACE_BETWEEN_TASKS) > layoutBounds.right) {
+ // Move to the next row if possible.
+ minRight = minOf(minRight, left)
+ maxRight = maxOf(maxRight, left)
+ top += optimalRowHeight
+
+ // Check if the new row reaches the bottom or if the first item in the new
+ // row does not fit within the available width.
+ if (
+ (top + optimalRowHeight) > layoutBounds.bottom ||
+ layoutBounds.left + width + HORIZONTAL_SPACE_BETWEEN_TASKS >
+ layoutBounds.right
+ ) {
+ allWindowsFit = false
+ break
+ }
+ left = layoutBounds.left
+ }
+
+ // Position the current rect.
+ outRects.add(
+ RectF(
+ left.toFloat(),
+ top.toFloat(),
+ (left + width).toFloat(),
+ (top + optimalWindowHeight).toFloat(),
+ )
+ )
+
+ // Increment horizontal position.
+ left += (width + HORIZONTAL_SPACE_BETWEEN_TASKS)
+ }
+
+ // Update the narrowest and widest row width for the last row.
+ minRight = minOf(minRight, left)
+ maxRight = maxOf(maxRight, left)
+
+ return FitWindowResult(allWindowsFit, outRects, minRight, maxRight)
+ }
+
+ /** Centers task windows in the center of Overview. */
+ private fun centerTaskWindows(layoutBounds: Rect, maxBottom: Int, outWindowRects: List<RectF>) {
+ if (outWindowRects.isEmpty()) {
+ return
+ }
+
+ val currentRowUnionRange = RectF(outWindowRects[0])
+ var currentRowY = outWindowRects[0].top
+ var currentRowFirstItemIndex = 0
+ val offsetY = (layoutBounds.bottom - maxBottom) / 2f
+
+ // Batch process to center overview desktop task windows within the same row.
+ fun batchCenterDesktopTaskWindows(endIndex: Int) {
+ // Calculate the shift amount required to center the desktop task items.
+ val rangeCenterX = (currentRowUnionRange.left + currentRowUnionRange.right) / 2f
+ val currentDiffX = (layoutBounds.centerX() - rangeCenterX).coerceAtLeast(0f)
+ for (j in currentRowFirstItemIndex until endIndex) {
+ outWindowRects[j].offset(currentDiffX, offsetY)
+ }
+ }
+
+ outWindowRects.forEachIndexed { index, rect ->
+ if (rect.top != currentRowY) {
+ // As a new row begins processing, batch-shift the previous row's rects
+ // and reset its parameters.
+ batchCenterDesktopTaskWindows(index)
+ currentRowUnionRange.set(rect)
+ currentRowY = rect.top
+ currentRowFirstItemIndex = index
+ }
+
+ // Extend the range by adding the [rect]'s width and extra in-between items
+ // spacing.
+ currentRowUnionRange.right = rect.right
+ }
+
+ // Post-processing rects in the last row.
+ batchCenterDesktopTaskWindows(outWindowRects.size)
+ }
+
+ private companion object {
+ const val VERTICAL_SPACE_BETWEEN_TASKS = 24
+ const val HORIZONTAL_SPACE_BETWEEN_TASKS = 24
+ const val OVERVIEW_INSET_TOP_BOTTOM = 16
+ const val OVERVIEW_INSET_LEFT_RIGHT = 16
+ const val MAXIMUM_TASK_HEIGHT = 800
+ }
+}
diff --git a/quickstep/src/com/android/quickstep/recents/ui/viewmodel/DesktopTaskViewModel.kt b/quickstep/src/com/android/quickstep/recents/ui/viewmodel/DesktopTaskViewModel.kt
new file mode 100644
index 0000000..4de0b90
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/recents/ui/viewmodel/DesktopTaskViewModel.kt
@@ -0,0 +1,45 @@
+/*
+ * Copyright (C) 2025 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.recents.ui.viewmodel
+
+import android.graphics.Rect
+import android.util.Size
+import com.android.quickstep.recents.domain.model.DesktopTaskBoundsData
+import com.android.quickstep.recents.domain.usecase.OrganizeDesktopTasksUseCase
+
+/** ViewModel used for [com.android.quickstep.views.DesktopTaskView]. */
+class DesktopTaskViewModel(private val organizeDesktopTasksUseCase: OrganizeDesktopTasksUseCase) {
+ /** Positions for desktop tasks as calculated by [organizeDesktopTasksUseCase] */
+ var organizedDesktopTaskPositions = emptyList<DesktopTaskBoundsData>()
+ private set
+
+ /**
+ * Computes new task positions using [organizeDesktopTasksUseCase]. The result is stored in
+ * [organizedDesktopTaskPositions]. This is used for the exploded desktop view where the usecase
+ * will scale and translate tasks so that they don't overlap.
+ *
+ * @param desktopSize the size available for organizing the tasks.
+ * @param defaultPositions the tasks and their bounds as they appear on a desktop.
+ */
+ fun organizeDesktopTasks(desktopSize: Size, defaultPositions: List<DesktopTaskBoundsData>) {
+ organizedDesktopTaskPositions =
+ organizeDesktopTasksUseCase.run(
+ desktopBounds = Rect(0, 0, desktopSize.width, desktopSize.height),
+ taskBounds = defaultPositions,
+ )
+ }
+}
diff --git a/quickstep/src/com/android/quickstep/recents/ui/viewmodel/TaskViewModel.kt b/quickstep/src/com/android/quickstep/recents/ui/viewmodel/TaskViewModel.kt
index 961446f..0b299ee 100644
--- a/quickstep/src/com/android/quickstep/recents/ui/viewmodel/TaskViewModel.kt
+++ b/quickstep/src/com/android/quickstep/recents/ui/viewmodel/TaskViewModel.kt
@@ -24,8 +24,10 @@
import com.android.quickstep.recents.domain.model.TaskModel
import com.android.quickstep.recents.domain.usecase.GetSysUiStatusNavFlagsUseCase
import com.android.quickstep.recents.domain.usecase.GetTaskUseCase
+import com.android.quickstep.recents.domain.usecase.IsThumbnailValidUseCase
import com.android.quickstep.recents.viewmodel.RecentsViewData
import com.android.quickstep.views.TaskViewType
+import com.android.systemui.shared.recents.model.ThumbnailData
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
@@ -45,6 +47,7 @@
recentsViewData: RecentsViewData,
private val getTaskUseCase: GetTaskUseCase,
private val getSysUiStatusNavFlagsUseCase: GetSysUiStatusNavFlagsUseCase,
+ private val isThumbnailValidUseCase: IsThumbnailValidUseCase,
dispatcherProvider: DispatcherProvider,
) {
private var taskIds = MutableStateFlow(emptySet<Int>())
@@ -74,10 +77,12 @@
.flowOn(dispatcherProvider.background)
fun bind(vararg taskId: TaskId) {
- Log.d(TAG, "bind: $taskId")
- taskIds.value = taskId.toSet()
+ taskIds.value = taskId.toSet().also { Log.d(TAG, "bind: $it") }
}
+ fun isThumbnailValid(thumbnail: ThumbnailData?, width: Int, height: Int): Boolean =
+ isThumbnailValidUseCase(thumbnail, width, height)
+
private fun mapToTaskTile(tasks: List<TaskData>, isLiveTile: Boolean): TaskTileUiState {
val firstThumbnailData = (tasks.firstOrNull() as? TaskData.Data)?.thumbnailData
return TaskTileUiState(
diff --git a/quickstep/src/com/android/quickstep/recents/viewmodel/RecentsViewData.kt b/quickstep/src/com/android/quickstep/recents/viewmodel/RecentsViewData.kt
index a1f8454..2465a46 100644
--- a/quickstep/src/com/android/quickstep/recents/viewmodel/RecentsViewData.kt
+++ b/quickstep/src/com/android/quickstep/recents/viewmodel/RecentsViewData.kt
@@ -27,8 +27,6 @@
// The settled set of visible taskIds that is updated after RecentsView scroll settles.
val settledFullyVisibleTaskIds = MutableStateFlow(emptySet<Int>())
- val thumbnailSplashProgress = MutableStateFlow(0f)
-
// A list of taskIds that are associated with a RecentsAnimationController. */
val runningTaskIds = MutableStateFlow(emptySet<Int>())
diff --git a/quickstep/src/com/android/quickstep/recents/viewmodel/RecentsViewModel.kt b/quickstep/src/com/android/quickstep/recents/viewmodel/RecentsViewModel.kt
index 73332fc..5ff8aaa 100644
--- a/quickstep/src/com/android/quickstep/recents/viewmodel/RecentsViewModel.kt
+++ b/quickstep/src/com/android/quickstep/recents/viewmodel/RecentsViewModel.kt
@@ -42,10 +42,6 @@
recentsViewData.overlayEnabled.value = isOverlayEnabled
}
- fun updateThumbnailSplashProgress(taskThumbnailSplashAlpha: Float) {
- recentsViewData.thumbnailSplashProgress.value = taskThumbnailSplashAlpha
- }
-
suspend fun waitForThumbnailsToUpdate(updatedThumbnails: Map<Int, ThumbnailData>?) {
if (updatedThumbnails.isNullOrEmpty()) return
combine(
diff --git a/quickstep/src/com/android/quickstep/recents/viewmodel/TaskContainerViewModel.kt b/quickstep/src/com/android/quickstep/recents/viewmodel/TaskContainerViewModel.kt
deleted file mode 100644
index 723df55..0000000
--- a/quickstep/src/com/android/quickstep/recents/viewmodel/TaskContainerViewModel.kt
+++ /dev/null
@@ -1,26 +0,0 @@
-/*
- * 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.recents.viewmodel
-
-import com.android.quickstep.task.thumbnail.SplashAlphaUseCase
-import kotlinx.coroutines.flow.firstOrNull
-import kotlinx.coroutines.runBlocking
-
-class TaskContainerViewModel(private val splashAlphaUseCase: SplashAlphaUseCase) {
- fun shouldShowThumbnailSplash(taskId: Int): Boolean =
- (runBlocking { splashAlphaUseCase.execute(taskId).firstOrNull() } ?: 0f) > 0f
-}
diff --git a/quickstep/src/com/android/quickstep/task/thumbnail/SplashAlphaUseCase.kt b/quickstep/src/com/android/quickstep/task/thumbnail/SplashAlphaUseCase.kt
deleted file mode 100644
index 7673c71..0000000
--- a/quickstep/src/com/android/quickstep/task/thumbnail/SplashAlphaUseCase.kt
+++ /dev/null
@@ -1,90 +0,0 @@
-/*
- * 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.view.Surface
-import com.android.quickstep.recents.data.RecentTasksRepository
-import com.android.quickstep.recents.data.RecentsRotationStateRepository
-import com.android.quickstep.recents.viewmodel.RecentsViewData
-import com.android.quickstep.task.viewmodel.TaskContainerData
-import com.android.systemui.shared.recents.utilities.PreviewPositionHelper
-import com.android.systemui.shared.recents.utilities.Utilities
-import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.combine
-import kotlinx.coroutines.flow.distinctUntilChanged
-
-class SplashAlphaUseCase(
- private val recentsViewData: RecentsViewData,
- private val taskContainerData: TaskContainerData,
- private val taskThumbnailViewData: TaskThumbnailViewData,
- private val tasksRepository: RecentTasksRepository,
- private val rotationStateRepository: RecentsRotationStateRepository,
-) {
- fun execute(taskId: Int): Flow<Float> =
- combine(
- taskThumbnailViewData.width,
- taskThumbnailViewData.height,
- tasksRepository.getThumbnailById(taskId),
- taskContainerData.thumbnailSplashProgress,
- recentsViewData.thumbnailSplashProgress
- ) { width, height, thumbnailData, taskSplashProgress, globalSplashProgress ->
- val thumbnail = thumbnailData?.thumbnail
- when {
- thumbnail == null -> 0f
- taskSplashProgress > 0f -> taskSplashProgress
- globalSplashProgress > 0f &&
- isInaccurateThumbnail(thumbnail, thumbnailData.rotation, width, height) ->
- globalSplashProgress
- else -> 0f
- }
- }
- .distinctUntilChanged()
-
- private fun isInaccurateThumbnail(
- thumbnail: Bitmap,
- thumbnailRotation: Int,
- width: Int,
- height: Int
- ): Boolean {
- return isThumbnailAspectRatioDifferentFromThumbnailData(thumbnail, width, height) ||
- isThumbnailRotationDifferentFromTask(thumbnailRotation)
- }
-
- private fun isThumbnailAspectRatioDifferentFromThumbnailData(
- thumbnail: Bitmap,
- viewWidth: Int,
- viewHeight: Int
- ): Boolean {
- val viewAspect: Float = viewWidth / viewHeight.toFloat()
- val thumbnailAspect: Float = thumbnail.width / thumbnail.height.toFloat()
- return Utilities.isRelativePercentDifferenceGreaterThan(
- viewAspect,
- thumbnailAspect,
- PreviewPositionHelper.MAX_PCT_BEFORE_ASPECT_RATIOS_CONSIDERED_DIFFERENT
- )
- }
-
- private fun isThumbnailRotationDifferentFromTask(thumbnailRotation: Int): Boolean {
- val rotationState = rotationStateRepository.getRecentsRotationState()
- return if (rotationState.orientationHandlerRotation == Surface.ROTATION_0) {
- (rotationState.activityRotation - thumbnailRotation) % 2 != 0
- } else {
- rotationState.orientationHandlerRotation != thumbnailRotation
- }
- }
-}
diff --git a/quickstep/src/com/android/quickstep/task/thumbnail/TaskThumbnailView.kt b/quickstep/src/com/android/quickstep/task/thumbnail/TaskThumbnailView.kt
index 28152ec..63e93ba 100644
--- a/quickstep/src/com/android/quickstep/task/thumbnail/TaskThumbnailView.kt
+++ b/quickstep/src/com/android/quickstep/task/thumbnail/TaskThumbnailView.kt
@@ -50,10 +50,6 @@
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancel
-import kotlinx.coroutines.flow.dropWhile
-import kotlinx.coroutines.flow.flowOn
-import kotlinx.coroutines.flow.launchIn
-import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
class TaskThumbnailView : FrameLayout, ViewPool.Reusable {
@@ -62,8 +58,6 @@
// This is initialised here and set in onAttachedToWindow because onLayout can be called before
// onAttachedToWindow so this property needs to be initialised as it is used below.
- private var viewData: TaskThumbnailViewData = RecentsDependencies.get(this)
-
private lateinit var viewModel: TaskThumbnailViewModel
private lateinit var viewAttachedScope: CoroutineScope
@@ -110,18 +104,7 @@
CoroutineScope(
SupervisorJob() + Dispatchers.Main.immediate + CoroutineName("TaskThumbnailView")
)
- viewData = RecentsDependencies.get(this)
- updateViewDataValues()
viewModel = RecentsDependencies.get(this)
- viewModel.splashAlpha
- .dropWhile { it == 0f }
- .flowOn(dispatcherProvider.background)
- .onEach { splashAlpha ->
- splashBackground.alpha = splashAlpha
- splashIcon.alpha = splashAlpha
- }
- .launchIn(viewAttachedScope)
-
clipToOutline = true
outlineProvider =
object : ViewOutlineProvider() {
@@ -144,16 +127,9 @@
resetViews()
}
- override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
- super.onLayout(changed, left, top, right, bottom)
- if (changed) {
- updateViewDataValues()
- }
- }
-
fun setState(state: TaskThumbnailUiState, taskId: Int? = null) {
- logDebug("taskId: $taskId - uiState changed from: $uiState to: $state")
if (uiState == state) return
+ logDebug("taskId: $taskId - uiState changed from: $uiState to: $state")
uiState = state
resetViews()
when (state) {
@@ -164,6 +140,12 @@
}
}
+ /**
+ * Updates the alpha of the dim layer on top of this view. If dimAlpha is 0, no dimming is
+ * applied; if dimAlpha is 1, the thumbnail will be the extracted background color.
+ *
+ * @param tintAmount The amount of alpha that will be applied to the dim layer.
+ */
fun updateTintAmount(tintAmount: Float) {
dimAlpha[ScrimViewAlpha.TintAmount.ordinal].value = tintAmount
}
@@ -172,9 +154,9 @@
dimAlpha[ScrimViewAlpha.MenuProgress.ordinal].value = progress * MAX_SCRIM_ALPHA
}
- private fun updateViewDataValues() {
- viewData.width.value = width
- viewData.height.value = height
+ fun updateSplashAlpha(value: Float) {
+ splashBackground.alpha = value
+ splashIcon.alpha = value
}
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
diff --git a/quickstep/src/com/android/quickstep/task/thumbnail/TaskThumbnailViewData.kt b/quickstep/src/com/android/quickstep/task/thumbnail/TaskThumbnailViewData.kt
deleted file mode 100644
index 3502029..0000000
--- a/quickstep/src/com/android/quickstep/task/thumbnail/TaskThumbnailViewData.kt
+++ /dev/null
@@ -1,24 +0,0 @@
-/*
- * 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 kotlinx.coroutines.flow.MutableStateFlow
-
-class TaskThumbnailViewData {
- val width = MutableStateFlow(0)
- val height = MutableStateFlow(0)
-}
diff --git a/quickstep/src/com/android/quickstep/task/viewmodel/TaskThumbnailViewModel.kt b/quickstep/src/com/android/quickstep/task/viewmodel/TaskThumbnailViewModel.kt
index c89bf01..e641737 100644
--- a/quickstep/src/com/android/quickstep/task/viewmodel/TaskThumbnailViewModel.kt
+++ b/quickstep/src/com/android/quickstep/task/viewmodel/TaskThumbnailViewModel.kt
@@ -17,13 +17,9 @@
package com.android.quickstep.task.viewmodel
import android.graphics.Matrix
-import kotlinx.coroutines.flow.Flow
/** ViewModel for representing TaskThumbnails */
interface TaskThumbnailViewModel {
- /** Provides the alpha of the splash icon */
- val splashAlpha: Flow<Float>
-
/** Attaches this ViewModel to a specific task id for it to provide data from. */
fun bind(taskId: Int)
diff --git a/quickstep/src/com/android/quickstep/task/viewmodel/TaskThumbnailViewModelImpl.kt b/quickstep/src/com/android/quickstep/task/viewmodel/TaskThumbnailViewModelImpl.kt
index 635d08b..94c40d1 100644
--- a/quickstep/src/com/android/quickstep/task/viewmodel/TaskThumbnailViewModelImpl.kt
+++ b/quickstep/src/com/android/quickstep/task/viewmodel/TaskThumbnailViewModelImpl.kt
@@ -19,32 +19,17 @@
import android.app.ActivityTaskManager.INVALID_TASK_ID
import android.graphics.Matrix
import android.util.Log
-import com.android.launcher3.util.coroutines.DispatcherProvider
import com.android.quickstep.recents.usecase.GetThumbnailPositionUseCase
import com.android.quickstep.recents.usecase.ThumbnailPositionState
-import com.android.quickstep.task.thumbnail.SplashAlphaUseCase
-import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.flow.MutableStateFlow
-import kotlinx.coroutines.flow.flatMapLatest
-import kotlinx.coroutines.flow.flowOf
-import kotlinx.coroutines.flow.flowOn
-@OptIn(ExperimentalCoroutinesApi::class)
class TaskThumbnailViewModelImpl(
- dispatcherProvider: DispatcherProvider,
- private val getThumbnailPositionUseCase: GetThumbnailPositionUseCase,
- private val splashAlphaUseCase: SplashAlphaUseCase,
+ private val getThumbnailPositionUseCase: GetThumbnailPositionUseCase
) : TaskThumbnailViewModel {
- private val splashProgress = MutableStateFlow(flowOf(0f))
private var taskId: Int = INVALID_TASK_ID
- override val splashAlpha =
- splashProgress.flatMapLatest { it }.flowOn(dispatcherProvider.background)
-
override fun bind(taskId: Int) {
Log.d(TAG, "bind taskId: $taskId")
this.taskId = taskId
- splashProgress.value = splashAlphaUseCase.execute(taskId)
}
override fun getThumbnailPositionState(width: Int, height: Int, isRtl: Boolean): Matrix =
diff --git a/quickstep/src/com/android/quickstep/util/IconLabelUtil.kt b/quickstep/src/com/android/quickstep/util/IconLabelUtil.kt
new file mode 100644
index 0000000..a876bca
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/util/IconLabelUtil.kt
@@ -0,0 +1,50 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.quickstep.util
+
+import android.app.ActivityManager
+import android.content.Context
+import android.content.pm.ActivityInfo
+import android.os.UserHandle
+import com.android.launcher3.Utilities
+
+object IconLabelUtil {
+ @JvmStatic
+ @JvmOverloads
+ fun getBadgedContentDescription(
+ context: Context,
+ info: ActivityInfo,
+ userId: Int,
+ taskDescription: ActivityManager.TaskDescription? = null,
+ ): String {
+ val packageManager = context.packageManager
+ var taskLabel = taskDescription?.let { Utilities.trim(it.label) }
+ if (taskLabel.isNullOrEmpty()) {
+ taskLabel = Utilities.trim(info.loadLabel(packageManager))
+ }
+
+ val applicationLabel = Utilities.trim(info.applicationInfo.loadLabel(packageManager))
+ val badgedApplicationLabel =
+ if (userId != UserHandle.myUserId())
+ packageManager
+ .getUserBadgedLabel(applicationLabel, UserHandle.of(userId))
+ .toString()
+ else applicationLabel
+ return if (applicationLabel == taskLabel) badgedApplicationLabel
+ else "$badgedApplicationLabel $taskLabel"
+ }
+}
diff --git a/quickstep/src/com/android/quickstep/util/SplitAnimationController.kt b/quickstep/src/com/android/quickstep/util/SplitAnimationController.kt
index 0182969..3c0a63a 100644
--- a/quickstep/src/com/android/quickstep/util/SplitAnimationController.kt
+++ b/quickstep/src/com/android/quickstep/util/SplitAnimationController.kt
@@ -52,9 +52,9 @@
import com.android.launcher3.QuickstepTransitionManager
import com.android.launcher3.R
import com.android.launcher3.Utilities
+import com.android.launcher3.anim.AnimatedFloat
import com.android.launcher3.anim.PendingAnimation
import com.android.launcher3.apppairs.AppPairIcon
-import com.android.launcher3.config.FeatureFlags
import com.android.launcher3.logging.StatsLogManager.EventEnum
import com.android.launcher3.model.data.WorkspaceItemInfo
import com.android.launcher3.statehandlers.DepthController
@@ -65,6 +65,7 @@
import com.android.launcher3.util.SplitConfigurationOptions.SplitSelectSource
import com.android.launcher3.views.BaseDragLayer
import com.android.quickstep.TaskViewUtils
+import com.android.quickstep.util.SplitScreenUtils.Companion.extractTopParentAndChildren
import com.android.quickstep.views.FloatingAppPairView
import com.android.quickstep.views.FloatingTaskView
import com.android.quickstep.views.GroupedTaskView
@@ -94,7 +95,7 @@
val fadeWithThumbnail: Boolean,
val isStagedTask: Boolean,
val iconView: View?,
- val contentDescription: CharSequence?
+ val contentDescription: CharSequence?,
)
}
@@ -104,7 +105,7 @@
*/
fun getFirstAnimInitViews(
taskViewSupplier: Supplier<TaskView>,
- splitSelectSourceSupplier: Supplier<SplitSelectSource?>
+ splitSelectSourceSupplier: Supplier<SplitSelectSource?>,
): SplitAnimInitProps {
val splitSelectSource = splitSelectSourceSupplier.get()
if (!splitSelectStateController.isAnimateCurrentTaskDismissal) {
@@ -116,7 +117,7 @@
fadeWithThumbnail = false,
isStagedTask = true,
iconView = null,
- splitSelectSource.itemInfo.contentDescription
+ splitSelectSource.itemInfo.contentDescription,
)
} else if (splitSelectStateController.isDismissingFromSplitPair) {
// Initiating split from overview, but on a split pair
@@ -131,7 +132,7 @@
fadeWithThumbnail = true,
isStagedTask = true,
iconView = container.iconView.asView(),
- container.task.titleDescription
+ container.task.titleDescription,
)
}
}
@@ -151,7 +152,7 @@
fadeWithThumbnail = true,
isStagedTask = true,
iconView = it.iconView.asView(),
- it.task.titleDescription
+ it.task.titleDescription,
)
}
}
@@ -189,29 +190,25 @@
deviceProfile: DeviceProfile,
taskViewWidth: Int,
taskViewHeight: Int,
- isPrimaryTaskSplitting: Boolean
+ isPrimaryTaskSplitting: Boolean,
) {
val snapshot = taskContainer.snapshotView
val iconView: View = taskContainer.iconView.asView()
- if (!enableRefactorTaskThumbnail()) {
+ if (enableRefactorTaskThumbnail()) {
+ builder.add(
+ AnimatedFloat { v -> taskContainer.taskView.splitSplashAlpha = v }
+ .animateToValue(1f)
+ )
+ } else {
val thumbnailViewDeprecated = taskContainer.thumbnailViewDeprecated
builder.add(
ObjectAnimator.ofFloat(
thumbnailViewDeprecated,
TaskThumbnailViewDeprecated.SPLASH_ALPHA,
- 1f
+ 1f,
)
)
thumbnailViewDeprecated.setShowSplashForSplitSelection(true)
- } else {
- builder.add(
- ValueAnimator.ofFloat(0f, 1f).apply {
- addUpdateListener {
- taskContainer.taskContainerData.thumbnailSplashProgress.value =
- it.animatedFraction
- }
- }
- )
}
// With the new `IconAppChipView`, we always want to keep the chip pinned to the
// top left of the task / thumbnail.
@@ -220,7 +217,7 @@
ObjectAnimator.ofFloat(
(iconView as IconAppChipView).splitTranslationX,
MULTI_PROPERTY_VALUE,
- 0f
+ 0f,
)
)
builder.add(
@@ -306,7 +303,7 @@
fun addScrimBehindAnim(
pendingAnimation: PendingAnimation,
container: RecentsViewContainer,
- context: Context
+ context: Context,
): View {
val scrim = View(context)
val recentsView = container.getOverviewPanel<RecentsView<*, *>>()
@@ -334,8 +331,8 @@
Interpolators.clampToProgress(
timings.backingScrimFadeInterpolator,
timings.backingScrimFadeInStartOffset,
- timings.backingScrimFadeInEndOffset
- )
+ timings.backingScrimFadeInEndOffset,
+ ),
)
return scrim
@@ -358,7 +355,7 @@
fun createPlaceholderDismissAnim(
container: RecentsViewContainer,
splitDismissEvent: EventEnum,
- duration: Long?
+ duration: Long?,
): AnimatorSet {
val animatorSet = AnimatorSet()
duration?.let { animatorSet.duration = it }
@@ -375,7 +372,7 @@
Rect(0, 0, floatingTask.width, floatingTask.height),
false,
null,
- onScreenRectF
+ onScreenRectF,
)
// Get the part of the floatingTask that intersects with the DragLayer (i.e. the
// on-screen portion)
@@ -383,7 +380,7 @@
dragLayer.left.toFloat(),
dragLayer.top.toFloat(),
dragLayer.right.toFloat(),
- dragLayer.bottom.toFloat()
+ dragLayer.bottom.toFloat(),
)
animatorSet.play(
ObjectAnimator.ofFloat(
@@ -393,8 +390,8 @@
floatingTask,
onScreenRectF,
floatingTask.stagePosition,
- container.deviceProfile
- )
+ container.deviceProfile,
+ ),
)
)
animatorSet.addListener(
@@ -403,7 +400,7 @@
splitSelectStateController.resetState()
safeRemoveViewFromDragLayer(
container,
- splitSelectStateController.splitInstructionsView
+ splitSelectStateController.splitInstructionsView,
)
}
}
@@ -429,8 +426,8 @@
Interpolators.clampToProgress(
Interpolators.LINEAR,
timings.instructionsContainerFadeInStartOffset,
- timings.instructionsContainerFadeInEndOffset
- )
+ timings.instructionsContainerFadeInEndOffset,
+ ),
)
anim.addFloat(
splitInstructionsView,
@@ -440,8 +437,8 @@
Interpolators.clampToProgress(
Interpolators.EMPHASIZED_DECELERATE,
timings.instructionsUnfoldStartOffset,
- timings.instructionsUnfoldEndOffset
- )
+ timings.instructionsUnfoldEndOffset,
+ ),
)
return anim
}
@@ -459,7 +456,7 @@
fun playAnimPlaceholderToFullscreen(
container: RecentsViewContainer,
view: View,
- resetCallback: Optional<Runnable>
+ resetCallback: Optional<Runnable>,
) {
val stagedTaskView = view as FloatingTaskView
@@ -481,7 +478,7 @@
RectF(firstTaskStartingBounds),
firstTaskEndingBounds,
false /* fadeWithThumbnail */,
- true /* isStagedTask */
+ true, /* isStagedTask */
)
pendingAnimation.addEndListener {
@@ -511,7 +508,7 @@
info: TransitionInfo?,
t: Transaction?,
finishCallback: Runnable,
- cornerRadius: Float
+ cornerRadius: Float,
) {
if (info == null && t == null) {
// (Legacy animation) Tapping a split tile in Overview
@@ -530,7 +527,7 @@
nonApps,
stateManager,
depthController,
- finishCallback
+ finishCallback,
)
return
@@ -548,7 +545,7 @@
depthController,
info,
t,
- finishCallback
+ finishCallback,
)
} else if (launchingIconView != null) {
// Tapping an app pair icon
@@ -563,7 +560,7 @@
info,
t,
finishCallback,
- cornerRadius
+ cornerRadius,
)
} else {
composeFullscreenIconSplitLaunchAnimator(
@@ -571,7 +568,7 @@
info,
t,
finishCallback,
- appPairLaunchingAppIndex
+ appPairLaunchingAppIndex,
)
}
} else {
@@ -587,7 +584,7 @@
info,
t,
finishCallback,
- cornerRadius
+ cornerRadius,
)
}
}
@@ -603,7 +600,7 @@
depthController: DepthController?,
info: TransitionInfo,
t: Transaction,
- finishCallback: Runnable
+ finishCallback: Runnable,
) {
TaskViewUtils.composeRecentsSplitLaunchAnimator(
launchingTaskView,
@@ -611,7 +608,7 @@
depthController,
info,
t,
- finishCallback
+ finishCallback,
)
}
@@ -629,7 +626,7 @@
nonApps: Array<RemoteAnimationTarget>,
stateManager: StateManager<*, *>,
depthController: DepthController?,
- finishCallback: Runnable
+ finishCallback: Runnable,
) {
TaskViewUtils.composeRecentsSplitLaunchAnimatorLegacy(
launchingTaskView,
@@ -640,7 +637,7 @@
nonApps,
stateManager,
depthController,
- finishCallback
+ finishCallback,
)
}
@@ -651,7 +648,7 @@
*/
fun hasChangesForBothAppPairs(
launchingIconView: AppPairIcon,
- transitionInfo: TransitionInfo
+ transitionInfo: TransitionInfo,
): Int {
val intent1 = launchingIconView.info.getFirstApp().intent.component?.packageName
val intent2 = launchingIconView.info.getSecondApp().intent.component?.packageName
@@ -712,7 +709,7 @@
transitionInfo: TransitionInfo,
t: Transaction,
finishCallback: Runnable,
- windowRadius: Float
+ windowRadius: Float,
) {
// If launching an app pair from Taskbar inside of an app context (no access to Launcher),
// use the scale-up animation
@@ -721,7 +718,7 @@
transitionInfo,
t,
finishCallback,
- WINDOWING_MODE_MULTI_WINDOW
+ WINDOWING_MODE_MULTI_WINDOW,
)
return
}
@@ -753,8 +750,7 @@
(!isLeftRightSplit && change.endAbsBounds.top <= 0)
}
val dividerPos =
- if (isLeftRightSplit) leftTopApp.endAbsBounds.right
- else leftTopApp.endAbsBounds.bottom
+ if (isLeftRightSplit) leftTopApp.endAbsBounds.right else leftTopApp.endAbsBounds.bottom
// Create a new floating view in Launcher, positioned above the launching icon
val drawableArea = launchingIconView.iconDrawableArea
@@ -769,7 +765,7 @@
drawableArea,
appIcon1,
appIcon2,
- dividerPos
+ dividerPos,
)
floatingView.bringToFront()
@@ -780,7 +776,7 @@
finishCallback,
launcher,
floatingView,
- mainRootCandidate
+ mainRootCandidate,
)
iconLaunchValueAnimator.addListener(
object : AnimatorListenerAdapter() {
@@ -806,7 +802,7 @@
transitionInfo: TransitionInfo,
t: Transaction,
finishCallback: Runnable,
- launchFullscreenIndex: Int
+ launchFullscreenIndex: Int,
) {
// If launching an app pair from Taskbar inside of an app context (no access to Launcher),
// use the scale-up animation
@@ -815,7 +811,7 @@
transitionInfo,
t,
finishCallback,
- WINDOWING_MODE_FULLSCREEN
+ WINDOWING_MODE_FULLSCREEN,
)
return
}
@@ -867,7 +863,7 @@
drawableArea,
appIcon,
null /*appIcon2*/,
- 0 /*dividerPos*/
+ 0, /*dividerPos*/
)
floatingView.bringToFront()
launchAnimation.play(
@@ -882,7 +878,7 @@
finishCallback: Runnable,
launcher: QuickstepLauncher,
floatingView: FloatingAppPairView,
- rootCandidate: Change
+ rootCandidate: Change,
): ValueAnimator {
val progressUpdater = ValueAnimator.ofFloat(0f, 1f)
val timings = AnimUtils.getDeviceAppPairLaunchTimings(dp.isTablet)
@@ -896,7 +892,7 @@
Interpolators.LINEAR,
valueAnimator.animatedFraction,
timings.appRevealStartOffset,
- timings.appRevealEndOffset
+ timings.appRevealEndOffset,
)
// Set the alpha of the shell layer (2 apps + divider)
@@ -913,8 +909,8 @@
Interpolators.clampToProgress(
timings.getStagedRectXInterpolator(),
timings.stagedRectSlideStartOffset,
- timings.stagedRectSlideEndOffset
- )
+ timings.stagedRectSlideEndOffset,
+ ),
)
var mDy =
FloatProp(
@@ -923,8 +919,8 @@
Interpolators.clampToProgress(
Interpolators.EMPHASIZED,
timings.stagedRectSlideStartOffset,
- timings.stagedRectSlideEndOffset
- )
+ timings.stagedRectSlideEndOffset,
+ ),
)
var mScaleX =
FloatProp(
@@ -933,8 +929,8 @@
Interpolators.clampToProgress(
Interpolators.EMPHASIZED,
timings.stagedRectSlideStartOffset,
- timings.stagedRectSlideEndOffset
- )
+ timings.stagedRectSlideEndOffset,
+ ),
)
var mScaleY =
FloatProp(
@@ -943,8 +939,8 @@
Interpolators.clampToProgress(
Interpolators.EMPHASIZED,
timings.stagedRectSlideStartOffset,
- timings.stagedRectSlideEndOffset
- )
+ timings.stagedRectSlideEndOffset,
+ ),
)
override fun onUpdate(percent: Float, initOnly: Boolean) {
@@ -979,42 +975,16 @@
transitionInfo: TransitionInfo,
t: Transaction,
finishCallback: Runnable,
- windowingMode: Int
+ windowingMode: Int,
) {
val launchAnimation = AnimatorSet()
val progressUpdater = ValueAnimator.ofFloat(0f, 1f)
progressUpdater.setDuration(QuickstepTransitionManager.APP_LAUNCH_DURATION)
progressUpdater.interpolator = Interpolators.EMPHASIZED
- var rootCandidate: Change? = null
-
- for (change in transitionInfo.changes) {
- val taskInfo: RunningTaskInfo = change.taskInfo ?: continue
-
- // TODO (b/316490565): Replace this logic when SplitBounds is available to
- // startAnimation() and we can know the precise taskIds of launching tasks.
- if (
- taskInfo.windowingMode == windowingMode &&
- (change.mode == TRANSIT_OPEN || change.mode == TRANSIT_TO_FRONT)
- ) {
- // Found one!
- rootCandidate = change
- break
- }
- }
-
- // If we could not find a proper root candidate, something went wrong.
- check(rootCandidate != null) { "Could not find a split root candidate" }
-
- // Recurse up the tree until parent is null, then we've found our root.
- var parentToken: WindowContainerToken? = rootCandidate.parent
- while (parentToken != null) {
- rootCandidate = transitionInfo.getChange(parentToken) ?: break
- parentToken = rootCandidate.parent
- }
-
- // Make sure nothing weird happened, like getChange() returning null.
- check(rootCandidate != null) { "Failed to find a root leash" }
+ val splitTree: Pair<Change, List<Change>>? = extractTopParentAndChildren(transitionInfo)
+ check(splitTree != null) { "Could not find a split root candidate" }
+ val rootCandidate = splitTree.first
// Starting position is a 34% size tile centered in the middle of the screen.
// Ending position is the full device screen.
@@ -1066,7 +1036,7 @@
transitionInfo: TransitionInfo,
t: Transaction,
finishCallback: Runnable,
- cornerRadius: Float
+ cornerRadius: Float,
) {
var splitRoot1: Change? = null
var splitRoot2: Change? = null
@@ -1131,7 +1101,7 @@
Interpolators.LINEAR,
valueAnimator.animatedFraction,
0.8f,
- 1f
+ 1f,
)
for (leash in openingTargets) {
animTransaction.setAlpha(leash, progress)
diff --git a/quickstep/src/com/android/quickstep/util/SplitScreenUtils.kt b/quickstep/src/com/android/quickstep/util/SplitScreenUtils.kt
index 4005c5a..7787e30 100644
--- a/quickstep/src/com/android/quickstep/util/SplitScreenUtils.kt
+++ b/quickstep/src/com/android/quickstep/util/SplitScreenUtils.kt
@@ -46,8 +46,8 @@
* Given a TransitionInfo, generates the tree structure for those changes and extracts out
* the top most root and it's two immediate children. Changes can be provided in any order.
*
- * @return a [Pair] where first -> top most split root, second -> [List] of 2,
- * leftTop/bottomRight stage roots
+ * @return null if no root is found, otherwise a [Pair] where first -> top most split root,
+ * second -> [List] of 2, leftTop/bottomRight stage roots
*/
fun extractTopParentAndChildren(
transitionInfo: TransitionInfo
diff --git a/quickstep/src/com/android/quickstep/util/SwipePipToHomeAnimator.java b/quickstep/src/com/android/quickstep/util/SwipePipToHomeAnimator.java
index 828322b..7d5b471 100644
--- a/quickstep/src/com/android/quickstep/util/SwipePipToHomeAnimator.java
+++ b/quickstep/src/com/android/quickstep/util/SwipePipToHomeAnimator.java
@@ -169,9 +169,6 @@
if (sourceRectHint.isEmpty()) {
mSourceRectHint.set(getEnterPipWithOverlaySrcRectHint(appBounds, aspectRatio));
- // Create a new overlay layer. We do not call detach on this instance, it's propagated
- // to other classes like PipTaskOrganizer / RecentsAnimationController to complete
- // the cleanup.
mPipContentOverlay = new PipContentOverlay.PipAppIconOverlay(view.getContext(),
mAppBounds, mDestinationBounds,
new IconProvider(context).getIcon(mActivityInfo), appIconSizePx);
diff --git a/quickstep/src/com/android/quickstep/util/SystemWindowManagerProxy.java b/quickstep/src/com/android/quickstep/util/SystemWindowManagerProxy.java
index 4d56c63..10ae7a3 100644
--- a/quickstep/src/com/android/quickstep/util/SystemWindowManagerProxy.java
+++ b/quickstep/src/com/android/quickstep/util/SystemWindowManagerProxy.java
@@ -33,6 +33,7 @@
import com.android.launcher3.util.window.CachedDisplayInfo;
import com.android.launcher3.util.window.WindowManagerProxy;
import com.android.quickstep.SystemUiProxy;
+import com.android.window.flags.Flags;
import com.android.wm.shell.shared.desktopmode.DesktopModeStatus;
import java.util.List;
@@ -90,6 +91,25 @@
}
@Override
+ public boolean showDesktopTaskbarForFreeformDisplay(Context displayInfoContext) {
+ if (!DesktopModeStatus.canEnterDesktopMode(displayInfoContext)) {
+ return false;
+ }
+
+ if (!DesktopModeStatus.enterDesktopByDefaultOnFreeformDisplay(displayInfoContext)) {
+ return false;
+ }
+
+ if (!Flags.enableDesktopTaskbarOnFreeformDisplays()) {
+ return false;
+ }
+
+ final boolean isFreeformDisplay = displayInfoContext.getResources().getConfiguration()
+ .windowConfiguration.getWindowingMode() == WINDOWING_MODE_FREEFORM;
+ return isFreeformDisplay;
+ }
+
+ @Override
public boolean isHomeVisible(Context context) {
return SystemUiProxy.INSTANCE.get(context).getHomeVisibilityState().isHomeVisible();
}
diff --git a/quickstep/src/com/android/quickstep/util/TaskGridNavHelper.java b/quickstep/src/com/android/quickstep/util/TaskGridNavHelper.java
index 498078b..da26622 100644
--- a/quickstep/src/com/android/quickstep/util/TaskGridNavHelper.java
+++ b/quickstep/src/com/android/quickstep/util/TaskGridNavHelper.java
@@ -29,6 +29,7 @@
*/
public class TaskGridNavHelper {
public static final int CLEAR_ALL_PLACEHOLDER_ID = -1;
+ public static final int ADD_DESK_PLACEHOLDER_ID = -2;
public static final int DIRECTION_UP = 0;
public static final int DIRECTION_DOWN = 1;
@@ -41,44 +42,42 @@
public @interface TASK_NAV_DIRECTION {}
private final IntArray mOriginalTopRowIds;
- private IntArray mTopRowIds;
- private IntArray mBottomRowIds;
+ private final IntArray mTopRowIds = new IntArray();
+ private final IntArray mBottomRowIds = new IntArray();
public TaskGridNavHelper(IntArray topIds, IntArray bottomIds,
- List<Integer> largeTileIds) {
+ List<Integer> largeTileIds, boolean hasAddDesktopButton) {
mOriginalTopRowIds = topIds.clone();
- generateTaskViewIdGrid(topIds, bottomIds, largeTileIds);
+ generateTaskViewIdGrid(topIds, bottomIds, largeTileIds, hasAddDesktopButton);
}
private void generateTaskViewIdGrid(IntArray topRowIdArray, IntArray bottomRowIdArray,
- List<Integer> largeTileIds) {
-
- int maxSize = Math.max(topRowIdArray.size(), bottomRowIdArray.size())
- + largeTileIds.size();
- int minSize = Math.min(topRowIdArray.size(), bottomRowIdArray.size())
- + largeTileIds.size();
-
- // Add Large tile task views first at the beginning
- for (int i = 0; i < largeTileIds.size(); i++) {
- topRowIdArray.add(i, largeTileIds.get(i));
- bottomRowIdArray.add(i, largeTileIds.get(i));
+ List<Integer> largeTileIds, boolean hasAddDesktopButton) {
+ // Add AddDesktopButton and lage tiles to both rows.
+ if (hasAddDesktopButton) {
+ mTopRowIds.add(ADD_DESK_PLACEHOLDER_ID);
+ mBottomRowIds.add(ADD_DESK_PLACEHOLDER_ID);
}
+ for (Integer tileId : largeTileIds) {
+ mTopRowIds.add(tileId);
+ mBottomRowIds.add(tileId);
+ }
+
+ // Add row ids to their respective rows.
+ mTopRowIds.addAll(topRowIdArray);
+ mBottomRowIds.addAll(bottomRowIdArray);
// Fill in the shorter array with the ids from the longer one.
- for (int i = minSize; i < maxSize; i++) {
- if (i >= topRowIdArray.size()) {
- topRowIdArray.add(bottomRowIdArray.get(i));
- } else {
- bottomRowIdArray.add(topRowIdArray.get(i));
- }
+ while (mTopRowIds.size() > mBottomRowIds.size()) {
+ mBottomRowIds.add(mTopRowIds.get(mBottomRowIds.size()));
+ }
+ while (mBottomRowIds.size() > mTopRowIds.size()) {
+ mTopRowIds.add(mBottomRowIds.get(mTopRowIds.size()));
}
- // Add the clear all button to the end of both arrays
- topRowIdArray.add(CLEAR_ALL_PLACEHOLDER_ID);
- bottomRowIdArray.add(CLEAR_ALL_PLACEHOLDER_ID);
-
- mTopRowIds = topRowIdArray;
- mBottomRowIds = bottomRowIdArray;
+ // Add the clear all button to the end of both arrays.
+ mTopRowIds.add(CLEAR_ALL_PLACEHOLDER_ID);
+ mBottomRowIds.add(CLEAR_ALL_PLACEHOLDER_ID);
}
/**
@@ -104,23 +103,28 @@
}
case DIRECTION_RIGHT: {
int boundedIndex =
- cycle ? (nextIndex < 0 ? maxSize - 1 : nextIndex) : Math.max(
- nextIndex, 0);
+ cycle ? (nextIndex < 0 ? maxSize - 1 : nextIndex)
+ : Math.max(nextIndex, 0);
boolean inOriginalTop = mOriginalTopRowIds.contains(currentPageTaskViewId);
return inOriginalTop ? mTopRowIds.get(boundedIndex)
: mBottomRowIds.get(boundedIndex);
}
case DIRECTION_TAB: {
int boundedIndex =
- cycle ? nextIndex < 0 ? maxSize - 1 : nextIndex % maxSize : Math.min(
- nextIndex, maxSize - 1);
+ cycle ? (nextIndex < 0 ? maxSize - 1 : nextIndex % maxSize)
+ : Math.min(nextIndex, maxSize - 1);
if (delta >= 0) {
return inTop && mTopRowIds.get(index) != mBottomRowIds.get(index)
? mBottomRowIds.get(index)
: mTopRowIds.get(boundedIndex);
} else {
if (mTopRowIds.contains(currentPageTaskViewId)) {
- return mBottomRowIds.get(boundedIndex);
+ if (boundedIndex < 0) {
+ // If no cycling, always return the first task.
+ return mTopRowIds.get(0);
+ } else {
+ return mBottomRowIds.get(boundedIndex);
+ }
} else {
// Go up to top if there is task above
return mTopRowIds.get(index) != mBottomRowIds.get(index)
diff --git a/quickstep/src/com/android/quickstep/util/TaskViewSimulator.java b/quickstep/src/com/android/quickstep/util/TaskViewSimulator.java
index a1e55fb..09e9c8b 100644
--- a/quickstep/src/com/android/quickstep/util/TaskViewSimulator.java
+++ b/quickstep/src/com/android/quickstep/util/TaskViewSimulator.java
@@ -120,6 +120,9 @@
private int mTaskRectTranslationY;
private int mDesktopTaskIndex = 0;
+ @Nullable
+ private Matrix mTaskRectTransform = null;
+
public TaskViewSimulator(Context context, BaseContainerInterface sizeStrategy,
boolean isDesktop, int desktopTaskIndex) {
mContext = context;
@@ -364,6 +367,15 @@
}
/**
+ * Sets a matrix used to transform the position of tasks. If set, this matrix is applied to
+ * the task rect after the task has been scaled and positioned inside the fulltask, but
+ * before scaling and translation of the whole recents view is performed.
+ */
+ public void setTaskRectTransform(@Nullable Matrix taskRectTransform) {
+ mTaskRectTransform = taskRectTransform;
+ }
+
+ /**
* Applies the rotation on the matrix to so that it maps from launcher coordinate space to
* window coordinate space.
*/
@@ -424,8 +436,11 @@
mMatrix.set(mPositionHelper.getMatrix());
- // Apply TaskView matrix: taskRect, translate
+ // Apply TaskView matrix: taskRect, optional transform, translate
mMatrix.postTranslate(mTaskRect.left, mTaskRect.top);
+ if (mTaskRectTransform != null) {
+ mMatrix.postConcat(mTaskRectTransform);
+ }
mOrientationState.getOrientationHandler().setPrimary(mMatrix, MATRIX_POST_TRANSLATE,
taskPrimaryTranslation.value);
mOrientationState.getOrientationHandler().setSecondary(mMatrix, MATRIX_POST_TRANSLATE,
diff --git a/quickstep/src/com/android/quickstep/util/TransformParams.java b/quickstep/src/com/android/quickstep/util/TransformParams.java
index bb88818..1c1fbd8 100644
--- a/quickstep/src/com/android/quickstep/util/TransformParams.java
+++ b/quickstep/src/com/android/quickstep/util/TransformParams.java
@@ -22,10 +22,14 @@
import android.view.SurfaceControl;
import android.window.TransitionInfo;
+import androidx.annotation.VisibleForTesting;
+
import com.android.quickstep.RemoteAnimationTargets;
import com.android.quickstep.util.SurfaceTransaction.SurfaceProperties;
import com.android.window.flags.Flags;
+import java.util.function.Supplier;
+
public class TransformParams {
public static FloatProperty<TransformParams> PROGRESS =
@@ -60,15 +64,23 @@
private float mCornerRadius;
private RemoteAnimationTargets mTargetSet;
private TransitionInfo mTransitionInfo;
+ private boolean mCornerRadiusIsOverridden;
private SurfaceTransactionApplier mSyncTransactionApplier;
+ private Supplier<SurfaceTransaction> mSurfaceTransactionSupplier;
private BuilderProxy mHomeBuilderProxy = BuilderProxy.ALWAYS_VISIBLE;
private BuilderProxy mBaseBuilderProxy = BuilderProxy.ALWAYS_VISIBLE;
public TransformParams() {
+ this(SurfaceTransaction::new);
+ }
+
+ @VisibleForTesting
+ public TransformParams(Supplier<SurfaceTransaction> surfaceTransactionSupplier) {
mProgress = 0;
mTargetAlpha = 1;
mCornerRadius = -1;
+ mSurfaceTransactionSupplier = surfaceTransactionSupplier;
}
/**
@@ -115,6 +127,7 @@
*/
public TransformParams setTransitionInfo(TransitionInfo transitionInfo) {
mTransitionInfo = transitionInfo;
+ mCornerRadiusIsOverridden = false;
return this;
}
@@ -148,7 +161,7 @@
/** Builds the SurfaceTransaction from the given BuilderProxy params. */
public SurfaceTransaction createSurfaceParams(BuilderProxy proxy) {
RemoteAnimationTargets targets = mTargetSet;
- SurfaceTransaction transaction = new SurfaceTransaction();
+ SurfaceTransaction transaction = mSurfaceTransactionSupplier.get();
if (targets == null) {
return transaction;
}
@@ -166,8 +179,13 @@
targetProxy.onBuildTargetParams(builder, app, this);
// Override the corner radius for {@code app} with the leash used by Shell, so that it
// doesn't interfere with the window clip and corner radius applied here.
- overrideChangeLeashCornerRadiusToZero(app, transaction.getTransaction());
+ // Only override the corner radius once - so that we don't accidentally override at the
+ // end of transition after WM Shell has reset the corner radius of the task.
+ if (!mCornerRadiusIsOverridden) {
+ overrideFreeformChangeLeashCornerRadiusToZero(app, transaction.getTransaction());
+ }
}
+ mCornerRadiusIsOverridden = true;
// always put wallpaper layer to bottom.
final int wallpaperLength = targets.wallpapers != null ? targets.wallpapers.length : 0;
@@ -178,11 +196,15 @@
return transaction;
}
- private void overrideChangeLeashCornerRadiusToZero(
+ private void overrideFreeformChangeLeashCornerRadiusToZero(
RemoteAnimationTarget app, SurfaceControl.Transaction transaction) {
if (!Flags.enableDesktopRecentsTransitionsCornersBugfix()) {
return;
}
+ if (app.taskInfo == null || !app.taskInfo.isFreeform()) {
+ return;
+ }
+
SurfaceControl changeLeash = getChangeLeashForApp(app);
if (changeLeash != null) {
transaction.setCornerRadius(changeLeash, 0);
diff --git a/quickstep/src/com/android/quickstep/views/AddDesktopButton.kt b/quickstep/src/com/android/quickstep/views/AddDesktopButton.kt
index e353160..9f3c017 100644
--- a/quickstep/src/com/android/quickstep/views/AddDesktopButton.kt
+++ b/quickstep/src/com/android/quickstep/views/AddDesktopButton.kt
@@ -17,13 +17,15 @@
package com.android.quickstep.views
import android.content.Context
-import android.graphics.drawable.ShapeDrawable
-import android.graphics.drawable.shapes.RoundRectShape
+import android.graphics.Canvas
+import android.graphics.Rect
import android.util.AttributeSet
import android.widget.ImageButton
import com.android.launcher3.LauncherAnimUtils.VIEW_TRANSLATE_X
import com.android.launcher3.R
import com.android.launcher3.util.MultiPropertyFactory
+import com.android.quickstep.util.BorderAnimator
+import com.android.quickstep.util.BorderAnimator.Companion.createSimpleBorderAnimator
/**
* Button for supporting multiple desktop sessions. The button will be next to the first TaskView
@@ -55,20 +57,49 @@
multiTranslationX[TranslationX.OFFSET.ordinal].value = value
}
- override fun onFinishInflate() {
- super.onFinishInflate()
+ private val focusBorderAnimator: BorderAnimator =
+ createSimpleBorderAnimator(
+ context.resources.getDimensionPixelSize(R.dimen.add_desktop_button_size),
+ context.resources.getDimensionPixelSize(R.dimen.keyboard_quick_switch_border_width),
+ this::getBorderBounds,
+ this,
+ context
+ .obtainStyledAttributes(attrs, R.styleable.AddDesktopButton)
+ .getColor(
+ R.styleable.AddDesktopButton_focusBorderColor,
+ BorderAnimator.DEFAULT_BORDER_COLOR,
+ ),
+ )
- background =
- ShapeDrawable().apply {
- shape =
- RoundRectShape(
- FloatArray(8) { R.dimen.add_desktop_button_size.toFloat() },
- null,
- null,
- )
- setTint(
- resources.getColor(android.R.color.system_surface_bright_light, context.theme)
- )
+ var borderEnabled = false
+ set(value) {
+ if (field == value) {
+ return
}
+ field = value
+ focusBorderAnimator.setBorderVisibility(visible = field && isFocused, animated = true)
+ }
+
+ public override fun onFocusChanged(
+ gainFocus: Boolean,
+ direction: Int,
+ previouslyFocusedRect: Rect?,
+ ) {
+ super.onFocusChanged(gainFocus, direction, previouslyFocusedRect)
+ if (borderEnabled) {
+ focusBorderAnimator.setBorderVisibility(gainFocus, /* animated= */ true)
+ }
+ }
+
+ private fun getBorderBounds(bounds: Rect) {
+ bounds.set(0, 0, width, height)
+ val outlinePadding =
+ context.resources.getDimensionPixelSize(R.dimen.add_desktop_button_outline_padding)
+ bounds.inset(-outlinePadding, -outlinePadding)
+ }
+
+ override fun draw(canvas: Canvas) {
+ focusBorderAnimator.drawBorder(canvas)
+ super.draw(canvas)
}
}
diff --git a/quickstep/src/com/android/quickstep/views/DesktopTaskView.kt b/quickstep/src/com/android/quickstep/views/DesktopTaskView.kt
index 471313a..02be373 100644
--- a/quickstep/src/com/android/quickstep/views/DesktopTaskView.kt
+++ b/quickstep/src/com/android/quickstep/views/DesktopTaskView.kt
@@ -15,33 +15,45 @@
*/
package com.android.quickstep.views
+import android.animation.Animator
import android.annotation.SuppressLint
import android.content.Context
-import android.graphics.Point
+import android.graphics.Matrix
import android.graphics.PointF
import android.graphics.Rect
+import android.graphics.RectF
import android.util.AttributeSet
import android.util.Log
+import android.util.Size
import android.view.Gravity
import android.view.View
import android.view.ViewStub
import androidx.core.content.res.ResourcesCompat
import androidx.core.view.updateLayoutParams
+import com.android.launcher3.Flags.enableDesktopExplodedView
import com.android.launcher3.Flags.enableOverviewIconMenu
import com.android.launcher3.Flags.enableRefactorTaskThumbnail
import com.android.launcher3.R
+import com.android.launcher3.anim.AnimatedFloat
import com.android.launcher3.testing.TestLogging
import com.android.launcher3.testing.shared.TestProtocol
import com.android.launcher3.util.RunnableList
import com.android.launcher3.util.SplitConfigurationOptions
import com.android.launcher3.util.TransformingTouchDelegate
import com.android.launcher3.util.ViewPool
+import com.android.launcher3.util.rects.lerpRect
import com.android.launcher3.util.rects.set
import com.android.quickstep.BaseContainerInterface
import com.android.quickstep.DesktopFullscreenDrawParams
import com.android.quickstep.FullscreenDrawParams
+import com.android.quickstep.RemoteTargetGluer.RemoteTargetHandle
import com.android.quickstep.TaskOverlayFactory
import com.android.quickstep.ViewUtils
+import com.android.quickstep.recents.di.RecentsDependencies
+import com.android.quickstep.recents.di.get
+import com.android.quickstep.recents.domain.model.DesktopTaskBoundsData
+import com.android.quickstep.recents.ui.viewmodel.DesktopTaskViewModel
+import com.android.quickstep.recents.ui.viewmodel.TaskData
import com.android.quickstep.task.thumbnail.TaskThumbnailView
import com.android.quickstep.util.RecentsOrientedState
import com.android.systemui.shared.recents.model.Task
@@ -79,11 +91,40 @@
} else null
private val tempPointF = PointF()
- private val tempRect = Rect()
+ private val lastComputedTaskSize = Rect()
private lateinit var iconView: TaskViewIcon
private lateinit var contentView: DesktopTaskContentView
private lateinit var backgroundView: View
+ private var viewModel: DesktopTaskViewModel? = null
+
+ /**
+ * Holds the default (user placed) positions of task windows. This can be moved into the
+ * viewModel once RefactorTaskThumbnail has been launched.
+ */
+ private var defaultTaskPositions: List<DesktopTaskBoundsData> = emptyList()
+
+ /**
+ * When enableDesktopExplodedView is enabled, this controls the gradual transition from the
+ * default positions to the organized non-overlapping positions.
+ */
+ var explodeProgress = 0.0f
+ set(value) {
+ field = value
+ positionTaskWindows()
+ }
+
+ var remoteTargetHandles: Array<RemoteTargetHandle>? = null
+ set(value) {
+ field = value
+ positionTaskWindows()
+ }
+
+ private fun getRemoteTargetHandle(taskId: Int): RemoteTargetHandle? =
+ remoteTargetHandles?.firstOrNull {
+ it.transformParams.targetSet.firstAppTargetTaskId == taskId
+ }
+
override fun onFinishInflate() {
super.onFinishInflate()
iconView =
@@ -121,6 +162,113 @@
?.inflate()
}
+ fun startWindowExplodeAnimation(): Animator =
+ AnimatedFloat { progress -> explodeProgress = progress }.animateToValue(0.0f, 1.0f)
+
+ private fun positionTaskWindows() {
+ if (taskContainers.isEmpty()) {
+ return
+ }
+
+ val thumbnailTopMarginPx = container.deviceProfile.overviewTaskThumbnailTopMarginPx
+
+ val containerWidth = layoutParams.width
+ val containerHeight = layoutParams.height - thumbnailTopMarginPx
+
+ BaseContainerInterface.getTaskDimension(mContext, container.deviceProfile, tempPointF)
+
+ val windowWidth = tempPointF.x.toInt()
+ val windowHeight = tempPointF.y.toInt()
+ val scaleWidth = containerWidth / windowWidth.toFloat()
+ val scaleHeight = containerHeight / windowHeight.toFloat()
+
+ taskContainers.forEach {
+ val taskId = it.task.key.id
+ val defaultPosition = defaultTaskPositions.firstOrNull { it.taskId == taskId } ?: return
+ val position =
+ if (enableDesktopExplodedView()) {
+ viewModel!!
+ .organizedDesktopTaskPositions
+ .firstOrNull { it.taskId == taskId }
+ ?.let { organizedPosition ->
+ TEMP_RECT.apply {
+ lerpRect(
+ defaultPosition.bounds,
+ organizedPosition.bounds,
+ explodeProgress,
+ )
+ }
+ } ?: defaultPosition.bounds
+ } else {
+ defaultPosition.bounds
+ }
+
+ if (enableDesktopExplodedView()) {
+ getRemoteTargetHandle(taskId)?.let { remoteTargetHandle ->
+ val fromRect =
+ TEMP_RECTF1.apply {
+ set(defaultPosition.bounds)
+ scale(scaleWidth)
+ offset(
+ lastComputedTaskSize.left.toFloat(),
+ lastComputedTaskSize.top.toFloat(),
+ )
+ }
+ val toRect =
+ TEMP_RECTF2.apply {
+ set(position)
+ scale(scaleWidth)
+ offset(
+ lastComputedTaskSize.left.toFloat(),
+ lastComputedTaskSize.top.toFloat(),
+ )
+ }
+ val transform = Matrix()
+ transform.setRectToRect(fromRect, toRect, Matrix.ScaleToFit.FILL)
+ remoteTargetHandle.taskViewSimulator.setTaskRectTransform(transform)
+ remoteTargetHandle.taskViewSimulator.apply(remoteTargetHandle.transformParams)
+ }
+ }
+
+ val taskLeft = position.left * scaleWidth
+ val taskTop = position.top * scaleHeight
+ val taskWidth = position.width() * scaleWidth
+ val taskHeight = position.height() * scaleHeight
+ // TODO(b/394660950): Revisit the choice to update the layout when explodeProgress == 1.
+ // To run the explode animation in reverse, it may be simpler to use translation/scale
+ // for all cases where the progress is non-zero.
+ if (explodeProgress == 0.0f || explodeProgress == 1.0f) {
+ // Reset scaling and translation that may have been applied during animation.
+ it.snapshotView.apply {
+ scaleX = 1.0f
+ scaleY = 1.0f
+ translationX = 0.0f
+ translationY = 0.0f
+ }
+
+ // Position the task to the same position as it would be on the desktop
+ it.snapshotView.updateLayoutParams<LayoutParams> {
+ gravity = Gravity.LEFT or Gravity.TOP
+ width = taskWidth.toInt()
+ height = taskHeight.toInt()
+ leftMargin = taskLeft.toInt()
+ topMargin = taskTop.toInt()
+ }
+ } else {
+ // During the animation, apply translation and scale such that the view is
+ // transformed to where we want, without triggering layout.
+ it.snapshotView.apply {
+ pivotX = 0.0f
+ pivotY = 0.0f
+ translationX = taskLeft - left
+ translationY = taskTop - top
+ scaleX = taskWidth / width.toFloat()
+ scaleY = taskHeight / height.toFloat()
+ }
+ }
+ }
+ }
+
/** Updates this desktop task to the gives task list defined in `tasks` */
fun bind(
tasks: List<Task>,
@@ -133,6 +281,7 @@
tasks.forEach { sb.append(" key=${it.key}\n") }
Log.d(TAG, sb.toString())
}
+
cancelPendingLoadTasks()
val backgroundViewIndex = contentView.indexOfChild(backgroundView)
taskContainers =
@@ -160,8 +309,19 @@
onBind(orientedState)
}
+ override fun onBind(orientedState: RecentsOrientedState) {
+ super.onBind(orientedState)
+
+ if (enableRefactorTaskThumbnail()) {
+ viewModel =
+ DesktopTaskViewModel(organizeDesktopTasksUseCase = RecentsDependencies.get())
+ }
+ }
+
override fun onRecycle() {
super.onRecycle()
+ explodeProgress = 0.0f
+ viewModel = null
visibility = VISIBLE
taskContainers.forEach {
contentView.removeView(it.snapshotView)
@@ -176,61 +336,21 @@
@SuppressLint("RtlHardcoded")
override fun updateTaskSize(lastComputedTaskSize: Rect, lastComputedGridTaskSize: Rect) {
super.updateTaskSize(lastComputedTaskSize, lastComputedGridTaskSize)
- if (taskContainers.isEmpty()) {
- return
- }
-
- val thumbnailTopMarginPx = container.deviceProfile.overviewTaskThumbnailTopMarginPx
-
- val containerWidth = layoutParams.width
- val containerHeight = layoutParams.height - thumbnailTopMarginPx
+ this.lastComputedTaskSize.set(lastComputedTaskSize)
BaseContainerInterface.getTaskDimension(mContext, container.deviceProfile, tempPointF)
+ val desktopSize = Size(tempPointF.x.toInt(), tempPointF.y.toInt())
+ DEFAULT_BOUNDS.set(0, 0, desktopSize.width / 4, desktopSize.height / 4)
- val windowWidth = tempPointF.x.toInt()
- val windowHeight = tempPointF.y.toInt()
- val scaleWidth = containerWidth / windowWidth.toFloat()
- val scaleHeight = containerHeight / windowHeight.toFloat()
-
- if (DEBUG) {
- Log.d(
- TAG,
- "onMeasure: container=[$containerWidth,$containerHeight]" +
- "window=[$windowWidth,$windowHeight] scale=[$scaleWidth,$scaleHeight]",
- )
- }
-
- // Desktop tile is a shrunk down version of launcher and freeform task thumbnails.
- taskContainers.forEach {
- // Default to quarter of the desktop if we did not get app bounds.
- val taskSize =
- it.task.appBounds
- ?: tempRect.apply {
- left = 0
- top = 0
- right = windowWidth / 4
- bottom = windowHeight / 4
- }
- val positionInParent = it.task.positionInParent ?: ORIGIN
-
- // Position the task to the same position as it would be on the desktop
- it.snapshotView.updateLayoutParams<LayoutParams> {
- gravity = Gravity.LEFT or Gravity.TOP
- width = (taskSize.width() * scaleWidth).toInt()
- height = (taskSize.height() * scaleHeight).toInt()
- leftMargin = (positionInParent.x * scaleWidth).toInt()
- topMargin = (positionInParent.y * scaleHeight).toInt()
+ defaultTaskPositions =
+ taskContainers.map {
+ DesktopTaskBoundsData(it.task.key.id, it.task.appBounds ?: DEFAULT_BOUNDS)
}
- if (DEBUG) {
- with(it.snapshotView.layoutParams as LayoutParams) {
- Log.d(
- TAG,
- "onMeasure: task=${it.task.key} size=[$width,$height]" +
- " margin=[$leftMargin,$topMargin]",
- )
- }
- }
+
+ if (enableDesktopExplodedView()) {
+ viewModel?.organizeDesktopTasks(desktopSize, defaultTaskPositions)
}
+ positionTaskWindows()
}
override fun onTaskListVisibilityChanged(visible: Boolean, changes: Int) {
@@ -245,6 +365,10 @@
taskContainer.snapshotView.contentDescription = taskContainer.task.titleDescription
}
+ override fun setIconState(container: TaskContainer, state: TaskData?) {
+ container.snapshotView.contentDescription = (state as? TaskData.Data)?.titleDescription
+ }
+
// Ignoring [onIconUnloaded] as all tasks shares the same Desktop icon
override fun onIconUnloaded(taskContainer: TaskContainer) {}
@@ -319,6 +443,10 @@
// As DesktopTaskView is inflated in background, use initialSize=0 to avoid initPool.
private const val VIEW_POOL_INITIAL_SIZE = 0
- private val ORIGIN = Point(0, 0)
+ private val DEFAULT_BOUNDS = Rect()
+ // Temporaries used for various purposes to avoid allocations.
+ private val TEMP_RECT = Rect()
+ private val TEMP_RECTF1 = RectF()
+ private val TEMP_RECTF2 = RectF()
}
}
diff --git a/quickstep/src/com/android/quickstep/views/RecentsView.java b/quickstep/src/com/android/quickstep/views/RecentsView.java
index 28ab496..51980f0 100644
--- a/quickstep/src/com/android/quickstep/views/RecentsView.java
+++ b/quickstep/src/com/android/quickstep/views/RecentsView.java
@@ -36,6 +36,7 @@
import static com.android.launcher3.AbstractFloatingView.getTopOpenViewWithType;
import static com.android.launcher3.BaseActivity.STATE_HANDLER_INVISIBILITY_FLAGS;
import static com.android.launcher3.Flags.enableAdditionalHomeAnimations;
+import static com.android.launcher3.Flags.enableDesktopExplodedView;
import static com.android.launcher3.Flags.enableDesktopTaskAlphaAnimation;
import static com.android.launcher3.Flags.enableGridOnlyOverview;
import static com.android.launcher3.Flags.enableLargeDesktopWindowingTile;
@@ -1632,6 +1633,9 @@
taskView.setBorderEnabled(enabled);
}
mClearAllButton.setBorderEnabled(enabled);
+ if (mAddDesktopButton != null) {
+ mAddDesktopButton.setBorderEnabled(enabled);
+ }
}
/**
@@ -2898,7 +2902,7 @@
*/
public void onPrepareGestureEndAnimation(
@Nullable AnimatorSet animatorSet, GestureState.GestureEndTarget endTarget,
- TaskViewSimulator[] taskViewSimulators) {
+ RemoteTargetHandle[] remoteTargetHandles) {
Log.d(TAG, "onPrepareGestureEndAnimation - endTarget: " + endTarget);
mCurrentGestureEndTarget = endTarget;
boolean isOverviewEndTarget = endTarget == GestureState.GestureEndTarget.RECENTS;
@@ -2906,6 +2910,19 @@
updateGridProperties();
}
+ if (enableDesktopExplodedView()) {
+ for (TaskView taskView : getTaskViews()) {
+ if (taskView instanceof DesktopTaskView desktopTaskView) {
+ if (animatorSet == null) {
+ desktopTaskView.setExplodeProgress(1.0f);
+ } else {
+ animatorSet.play(desktopTaskView.startWindowExplodeAnimation());
+ }
+ desktopTaskView.setRemoteTargetHandles(remoteTargetHandles);
+ }
+ }
+ }
+
BaseState<?> endState = mSizeStrategy.stateFromGestureEndTarget(endTarget);
if (endState.displayOverviewTasksAsGrid(mContainer.getDeviceProfile())) {
TaskView runningTaskView = getRunningTaskView();
@@ -2918,7 +2935,8 @@
- runningTaskView.getNonGridTranslationX();
runningTaskSecondaryGridTranslation = runningTaskView.getGridTranslationY();
}
- for (TaskViewSimulator tvs : taskViewSimulators) {
+ for (RemoteTargetHandle remoteTargetHandle : remoteTargetHandles) {
+ TaskViewSimulator tvs = remoteTargetHandle.getTaskViewSimulator();
if (animatorSet == null) {
setGridProgress(1);
tvs.taskPrimaryTranslation.value = runningTaskPrimaryGridTranslation;
@@ -2966,6 +2984,12 @@
startIconFadeInOnGestureComplete();
animateActionsViewIn();
+ for (TaskView taskView : getTaskViews()) {
+ if (taskView instanceof DesktopTaskView desktopTaskView) {
+ desktopTaskView.setRemoteTargetHandles(mRemoteTargetHandles);
+ }
+ }
+
mCurrentGestureEndTarget = null;
}
@@ -3217,6 +3241,7 @@
int topRowWidth = 0;
int bottomRowWidth = 0;
+ int largeTileRowWidth = 0;
float topAccumulatedTranslationX = 0;
float bottomAccumulatedTranslationX = 0;
@@ -3227,9 +3252,12 @@
int focusedTaskViewShift = 0;
int largeTaskWidthAndSpacing = 0;
int snappedTaskRowWidth = 0;
+ int expectedCurrentTaskRowWidth = 0;
int snappedPage = isKeyboardTaskFocusPending() ? mKeyboardTaskFocusIndex : getNextPage();
TaskView snappedTaskView = getTaskViewAt(snappedPage);
TaskView homeTaskView = getHomeTaskView();
+ TaskView expectedCurrentTaskView = mUtils.getExpectedCurrentTask(getFocusedTaskView(),
+ getRunningTaskView());
TaskView nextFocusedTaskView = null;
// Don't clear the top row, if the user has dismissed a task, to maintain the task order.
@@ -3268,6 +3296,7 @@
if (!(taskView instanceof DesktopTaskView && isSplitSelectionActive())) {
topRowWidth += taskWidthAndSpacing;
bottomRowWidth += taskWidthAndSpacing;
+ largeTileRowWidth += taskWidthAndSpacing;
}
gridTranslation += focusedTaskViewShift;
gridTranslation += mIsRtl ? taskWidthAndSpacing : -taskWidthAndSpacing;
@@ -3279,8 +3308,10 @@
largeTaskWidthAndSpacing = taskWidthAndSpacing;
if (taskView == snappedTaskView) {
- // If focused task is snapped, the row width is just task width and spacing.
- snappedTaskRowWidth = taskWidthAndSpacing;
+ snappedTaskRowWidth = largeTileRowWidth;
+ }
+ if (taskView == expectedCurrentTaskView) {
+ expectedCurrentTaskRowWidth = largeTileRowWidth;
}
} else {
if (encounteredLastLargeTaskView) {
@@ -3349,8 +3380,12 @@
lastBottomTaskViews.add(taskView);
lastTopTaskViews.clear();
}
+ int taskViewRowWidth = isTopRow ? topRowWidth : bottomRowWidth;
if (taskView == snappedTaskView) {
- snappedTaskRowWidth = isTopRow ? topRowWidth : bottomRowWidth;
+ snappedTaskRowWidth = taskViewRowWidth;
+ }
+ if (taskView == expectedCurrentTaskView) {
+ expectedCurrentTaskRowWidth = taskViewRowWidth;
}
}
gridTranslations.put(taskView, gridTranslation);
@@ -3391,17 +3426,16 @@
float clearAllShortTotalWidthTranslation = 0;
int longRowWidth = Math.max(topRowWidth, bottomRowWidth);
- // If Recents contains only large task sizes, it should only consider 1 large size
- // for ClearAllButton translation. The space at the left side of the large task will be
- // empty and it should be move ClearAllButton further away as well.
- // TODO(b/359573248): Validate the translation for ClearAllButton for grid only.
- if (enableLargeDesktopWindowingTile() && largeTasksCount == getTaskViewCount()) {
- longRowWidth = largeTaskWidthAndSpacing;
- }
-
// If first task is not in the expected position (mLastComputedTaskSize) and being too close
// to ClearAllButton, then apply extra translation to ClearAllButton.
- int firstTaskStart = mLastComputedGridSize.left + longRowWidth;
+ int rowWidthAfterExpectedCurrentTask = longRowWidth - expectedCurrentTaskRowWidth;
+ int expectedCurrentTaskWidthAndSpacing =
+ (expectedCurrentTaskView != null
+ ? expectedCurrentTaskView.getLayoutParams().width
+ : 0
+ ) + mPageSpacing;
+ int firstTaskStart = mLastComputedGridSize.left + rowWidthAfterExpectedCurrentTask
+ + expectedCurrentTaskWidthAndSpacing;
int expectedFirstTaskStart = mLastComputedTaskSize.right;
if (firstTaskStart < expectedFirstTaskStart) {
mClearAllShortTotalWidthTranslation = expectedFirstTaskStart - firstTaskStart;
@@ -3507,11 +3541,6 @@
}
private void setTaskThumbnailSplashAlpha(float taskThumbnailSplashAlpha) {
- if (enableRefactorTaskThumbnail()) {
- mRecentsViewModel.updateThumbnailSplashProgress(taskThumbnailSplashAlpha);
- return;
- }
-
mTaskThumbnailSplashAlpha = taskThumbnailSplashAlpha;
for (TaskView taskView : getTaskViews()) {
taskView.setTaskThumbnailSplashAlpha(taskThumbnailSplashAlpha);
@@ -4540,24 +4569,32 @@
// Init task grid nav helper with top/bottom id arrays.
TaskGridNavHelper taskGridNavHelper = new TaskGridNavHelper(getTopRowIdArray(),
- getBottomRowIdArray(), mUtils.getLargeTaskViewIds());
+ getBottomRowIdArray(), mUtils.getLargeTaskViewIds(), mAddDesktopButton != null);
// Get current page's task view ID.
TaskView currentPageTaskView = getCurrentPageTaskView();
int currentPageTaskViewId;
+ final int clearAllButtonIndex = indexOfChild(mClearAllButton);
+ final int addDesktopButtonIndex = indexOfChild(mAddDesktopButton);
if (currentPageTaskView != null) {
currentPageTaskViewId = currentPageTaskView.getTaskViewId();
- } else if (mCurrentPage == indexOfChild(mClearAllButton)) {
+ } else if (mCurrentPage == clearAllButtonIndex) {
currentPageTaskViewId = TaskGridNavHelper.CLEAR_ALL_PLACEHOLDER_ID;
+ } else if (mCurrentPage == addDesktopButtonIndex) {
+ currentPageTaskViewId = TaskGridNavHelper.ADD_DESK_PLACEHOLDER_ID;
} else {
return INVALID_PAGE;
}
- int nextGridPage =
+ final int nextGridPage =
taskGridNavHelper.getNextGridPage(currentPageTaskViewId, delta, direction, cycle);
- return nextGridPage == TaskGridNavHelper.CLEAR_ALL_PLACEHOLDER_ID
- ? indexOfChild(mClearAllButton)
- : indexOfChild(getTaskViewFromTaskViewId(nextGridPage));
+ if (nextGridPage == TaskGridNavHelper.CLEAR_ALL_PLACEHOLDER_ID) {
+ return clearAllButtonIndex;
+ }
+ if (nextGridPage == TaskGridNavHelper.ADD_DESK_PLACEHOLDER_ID) {
+ return addDesktopButtonIndex;
+ }
+ return indexOfChild(getTaskViewFromTaskViewId(nextGridPage));
}
private void runDismissAnimation(PendingAnimation pendingAnim) {
@@ -6095,8 +6132,10 @@
}
private int getFirstViewIndex() {
- final TaskView firstView;
- if (mShowAsGridLastOnLayout) {
+ final View firstView;
+ if (mAddDesktopButton != null) {
+ firstView = mAddDesktopButton;
+ } else if (mShowAsGridLastOnLayout) {
// For grid Overview, it always start if a large tile (focused task or desktop task) if
// they exist, otherwise it start with the first task.
TaskView firstLargeTaskView = mUtils.getFirstLargeTaskView();
@@ -6340,7 +6379,7 @@
}
/**
- * @return true if the task in on the top of the grid
+ * @return true if the task in on the bottom of the grid
*/
public boolean isOnGridBottomRow(TaskView taskView) {
return showAsGrid()
@@ -6903,7 +6942,8 @@
* Creates the spring animations which run as a task settles back into its place in overview.
*
* <p>When a task dismiss is cancelled, the task will return to its original position via a
- * spring animation.
+ * spring animation. As it passes the threshold of its settling state, its neighbors will
+ * spring in response to the perceived impact of the settling task.
*/
public SpringAnimation createTaskDismissSettlingSpringAnimation(TaskView draggedTaskView,
float velocity, boolean isDismissing, SingleAxisSwipeDetector detector,
diff --git a/quickstep/src/com/android/quickstep/views/RecentsViewUtils.kt b/quickstep/src/com/android/quickstep/views/RecentsViewUtils.kt
index f610335..d37a3f9 100644
--- a/quickstep/src/com/android/quickstep/views/RecentsViewUtils.kt
+++ b/quickstep/src/com/android/quickstep/views/RecentsViewUtils.kt
@@ -20,6 +20,7 @@
import android.view.View
import androidx.core.view.children
import androidx.dynamicanimation.animation.FloatPropertyCompat
+import androidx.dynamicanimation.animation.FloatValueHolder
import androidx.dynamicanimation.animation.SpringAnimation
import androidx.dynamicanimation.animation.SpringForce
import com.android.launcher3.Flags.enableLargeDesktopWindowingTile
@@ -29,6 +30,7 @@
import com.android.launcher3.util.DynamicResource
import com.android.launcher3.util.IntArray
import com.android.quickstep.util.GroupTask
+import com.android.quickstep.util.TaskGridNavHelper
import com.android.quickstep.util.isExternalDisplay
import com.android.quickstep.views.RecentsView.RUNNING_TASK_ATTACH_ALPHA
import com.android.systemui.shared.recents.model.ThumbnailData
@@ -305,7 +307,8 @@
* Creates the spring animations which run when a dragged task view in overview is released.
*
* <p>When a task dismiss is cancelled, the task will return to its original position via a
- * spring animation.
+ * spring animation. As it passes the threshold of its settling state, its neighbors will spring
+ * in response to the perceived impact of the settling task.
*/
fun createTaskDismissSettlingSpringAnimation(
draggedTaskView: TaskView?,
@@ -320,37 +323,181 @@
FloatPropertyCompat.createFloatPropertyCompat(
draggedTaskView.secondaryDismissTranslationProperty
)
- val rp = DynamicResource.provider(recentsView.mContainer)
- return SpringAnimation(draggedTaskView, taskDismissFloatProperty)
- .setSpring(
- SpringForce()
- .setDampingRatio(rp.getFloat(R.dimen.dismiss_task_trans_y_damping_ratio))
- .setStiffness(rp.getFloat(R.dimen.dismiss_task_trans_y_stiffness))
- )
- .setStartVelocity(if (detector.isFling(velocity)) velocity else 0f)
- .addUpdateListener { animation, value, _ ->
- if (isDismissing && abs(value) >= abs(dismissLength)) {
- // TODO(b/393553524): Remove 0 alpha, instead animate task fully off screen.
- draggedTaskView.alpha = 0f
- animation.cancel()
- } else if (draggedTaskView.isRunningTask && recentsView.enableDrawingLiveTile) {
- recentsView.runActionOnRemoteHandles { remoteTargetHandle ->
- remoteTargetHandle.taskViewSimulator.taskSecondaryTranslation.value =
- taskDismissFloatProperty.getValue(draggedTaskView)
+ // Animate dragged task towards dismissal or rest state.
+ val draggedTaskViewSpringAnimation =
+ SpringAnimation(draggedTaskView, taskDismissFloatProperty)
+ .setSpring(createExpressiveDismissSpringForce())
+ .setStartVelocity(if (detector.isFling(velocity)) velocity else 0f)
+ .addUpdateListener { animation, value, _ ->
+ if (isDismissing && abs(value) >= abs(dismissLength)) {
+ // TODO(b/393553524): Remove 0 alpha, instead animate task fully off screen.
+ draggedTaskView.alpha = 0f
+ animation.cancel()
+ } else if (draggedTaskView.isRunningTask && recentsView.enableDrawingLiveTile) {
+ recentsView.runActionOnRemoteHandles { remoteTargetHandle ->
+ remoteTargetHandle.taskViewSimulator.taskSecondaryTranslation.value =
+ taskDismissFloatProperty.getValue(draggedTaskView)
+ }
+ recentsView.redrawLiveTile()
}
- recentsView.redrawLiveTile()
}
+ .addEndListener { _, _, _, _ ->
+ if (isDismissing) {
+ recentsView.dismissTask(
+ draggedTaskView,
+ /* animateTaskView = */ false,
+ /* removeTask = */ true,
+ )
+ } else {
+ recentsView.onDismissAnimationEnds()
+ }
+ onEndRunnable()
+ }
+ if (!isDismissing) {
+ addNeighboringSpringAnimationsForDismissCancel(
+ draggedTaskView,
+ draggedTaskViewSpringAnimation,
+ recentsView.pageCount,
+ )
+ }
+ return draggedTaskViewSpringAnimation
+ }
+
+ private fun addNeighboringSpringAnimationsForDismissCancel(
+ draggedTaskView: TaskView,
+ draggedTaskViewSpringAnimation: SpringAnimation,
+ taskCount: Int,
+ ) {
+ // Empty spring animation exists for conditional start, and to drive neighboring springs.
+ val neighborsToSettle =
+ SpringAnimation(FloatValueHolder()).setSpring(createExpressiveDismissSpringForce())
+ var lastPosition = 0f
+ var startSettling = false
+ draggedTaskViewSpringAnimation.addUpdateListener { _, value, velocity ->
+ // Start the settling animation the first time the dragged task passes the origin (from
+ // negative displacement to positive displacement). We do not check for an exact value
+ // to compare to, as the update listener does not necessarily hit every value (e.g. a
+ // value of zero). Do not check again once it has started settling, as a spring can
+ // bounce past the origin multiple times depending on the stifness and damping ratio.
+ if (startSettling) return@addUpdateListener
+ if (lastPosition < 0 && value >= 0) {
+ startSettling = true
}
- .addEndListener { _, _, _, _ ->
- if (isDismissing) {
- recentsView.dismissTask(
- draggedTaskView,
- /* animateTaskView = */ false,
- /* removeTask = */ true,
+ lastPosition = value
+ if (startSettling) {
+ neighborsToSettle.setStartVelocity(velocity).animateToFinalPosition(0f)
+ }
+ }
+
+ // Add tasks before dragged index, fanning out from the dragged task.
+ // The order they are added matters, as each spring drives the next.
+ var previousNeighbor = neighborsToSettle
+ getTasksAdjacentToDraggedTask(draggedTaskView, towardsStart = true).forEach {
+ previousNeighbor = createNeighboringTaskViewSpringAnimation(it, previousNeighbor)
+ }
+ // Add tasks after dragged index, fanning out from the dragged task.
+ // The order they are added matters, as each spring drives the next.
+ previousNeighbor = neighborsToSettle
+ getTasksAdjacentToDraggedTask(draggedTaskView, towardsStart = false).forEach {
+ previousNeighbor = createNeighboringTaskViewSpringAnimation(it, previousNeighbor)
+ }
+ }
+
+ /** Gets adjacent tasks either before or after the dragged task in visual order. */
+ private fun getTasksAdjacentToDraggedTask(
+ draggedTaskView: TaskView,
+ towardsStart: Boolean,
+ ): Sequence<TaskView> {
+ if (recentsView.showAsGrid()) {
+ return gridTaskViewInTabOrderSequence(draggedTaskView, towardsStart)
+ } else {
+ val taskViewList = taskViews.toList()
+ val draggedTaskViewIndex = taskViewList.indexOf(draggedTaskView)
+
+ return if (towardsStart) {
+ taskViewList.take(draggedTaskViewIndex).reversed().asSequence()
+ } else {
+ taskViewList.takeLast(taskViewList.size - draggedTaskViewIndex - 1).asSequence()
+ }
+ }
+ }
+
+ /**
+ * Returns a sequence of TaskViews in the grid, ordered according to tab navigation, starting
+ * from the dragged TaskView, in the direction of the provided delta.
+ *
+ * <p>A positive delta moves forward in the tab order towards the end of the grid, while a
+ * negative value moves backward towards the beginning.
+ */
+ private fun gridTaskViewInTabOrderSequence(
+ draggedTaskView: TaskView,
+ towardsStart: Boolean,
+ ): Sequence<TaskView> = sequence {
+ val taskGridNavHelper =
+ TaskGridNavHelper(
+ recentsView.topRowIdArray,
+ recentsView.bottomRowIdArray,
+ getLargeTaskViewIds(),
+ /* hasAddDesktopButton= */ false,
+ )
+ var nextTaskView: TaskView? = draggedTaskView
+ var previousTaskView: TaskView? = null
+ while (nextTaskView != previousTaskView && nextTaskView != null) {
+ previousTaskView = nextTaskView
+ nextTaskView =
+ recentsView.getTaskViewFromTaskViewId(
+ taskGridNavHelper.getNextGridPage(
+ nextTaskView.taskViewId,
+ if (towardsStart) -1 else 1,
+ TaskGridNavHelper.DIRECTION_TAB,
+ /* cycle = */ false,
)
- }
- onEndRunnable()
+ )
+ if (nextTaskView != null && nextTaskView != previousTaskView) {
+ yield(nextTaskView)
}
+ }
+ }
+
+ /** Creates a neighboring task view spring, driven by the spring of its neighbor. */
+ private fun createNeighboringTaskViewSpringAnimation(
+ taskView: TaskView,
+ previousNeighborSpringAnimation: SpringAnimation,
+ ): SpringAnimation {
+ val neighboringTaskViewSpringAnimation =
+ SpringAnimation(
+ taskView,
+ FloatPropertyCompat.createFloatPropertyCompat(
+ taskView.secondaryDismissTranslationProperty
+ ),
+ )
+ .setSpring(createExpressiveDismissSpringForce())
+ // Update live tile on spring animation.
+ if (taskView.isRunningTask && recentsView.enableDrawingLiveTile) {
+ neighboringTaskViewSpringAnimation.addUpdateListener { _, _, _ ->
+ recentsView.runActionOnRemoteHandles { remoteTargetHandle ->
+ remoteTargetHandle.taskViewSimulator.taskSecondaryTranslation.value =
+ taskView.secondaryDismissTranslationProperty.get(taskView)
+ }
+ recentsView.redrawLiveTile()
+ }
+ }
+ // Drive current neighbor's spring with the previous neighbor's.
+ previousNeighborSpringAnimation.addUpdateListener { _, value, _ ->
+ neighboringTaskViewSpringAnimation.animateToFinalPosition(value)
+ }
+ return neighboringTaskViewSpringAnimation
+ }
+
+ private fun createExpressiveDismissSpringForce(): SpringForce {
+ val resourceProvider = DynamicResource.provider(recentsView.mContainer)
+ return SpringForce()
+ .setDampingRatio(
+ resourceProvider.getFloat(R.dimen.expressive_dismiss_task_trans_y_damping_ratio)
+ )
+ .setStiffness(
+ resourceProvider.getFloat(R.dimen.expressive_dismiss_task_trans_y_stiffness)
+ )
}
companion object {
diff --git a/quickstep/src/com/android/quickstep/views/TaskContainer.kt b/quickstep/src/com/android/quickstep/views/TaskContainer.kt
index b6f6bed..bbe1af4 100644
--- a/quickstep/src/com/android/quickstep/views/TaskContainer.kt
+++ b/quickstep/src/com/android/quickstep/views/TaskContainer.kt
@@ -25,16 +25,14 @@
import com.android.quickstep.TaskOverlayFactory
import com.android.quickstep.ViewUtils.addAccessibleChildToList
import com.android.quickstep.recents.di.RecentsDependencies
-import com.android.quickstep.recents.di.get
import com.android.quickstep.recents.di.getScope
import com.android.quickstep.recents.di.inject
import com.android.quickstep.recents.ui.mapper.TaskUiStateMapper
import com.android.quickstep.recents.ui.viewmodel.TaskData
-import com.android.quickstep.recents.viewmodel.TaskContainerViewModel
import com.android.quickstep.task.thumbnail.TaskThumbnailView
-import com.android.quickstep.task.viewmodel.TaskContainerData
import com.android.quickstep.task.viewmodel.TaskThumbnailViewModel
import com.android.systemui.shared.recents.model.Task
+import com.android.systemui.shared.recents.model.ThumbnailData
/** Holder for all Task dependent information. */
class TaskContainer(
@@ -56,20 +54,14 @@
taskOverlayFactory: TaskOverlayFactory,
) {
val overlay: TaskOverlayFactory.TaskOverlay<*> = taskOverlayFactory.createOverlay(this)
- lateinit var taskContainerData: TaskContainerData
+ // TODO(b/390581380): Remove this after this bug is fixed
private val taskThumbnailViewModel: TaskThumbnailViewModel by
RecentsDependencies.inject(snapshotView)
- // TODO(b/335649589): Ideally create and obtain this from DI.
- private val taskContainerViewModel: TaskContainerViewModel by lazy {
- TaskContainerViewModel(splashAlphaUseCase = RecentsDependencies.get())
- }
-
init {
if (enableRefactorTaskThumbnail()) {
require(snapshotView is TaskThumbnailView)
- taskContainerData = RecentsDependencies.get(this)
RecentsDependencies.getScope(snapshotView).apply {
val taskViewScope = RecentsDependencies.getScope(taskView)
linkTo(taskViewScope)
@@ -82,9 +74,11 @@
}
}
- var splitAnimationThumbnail: Bitmap? = null
- get() = if (enableRefactorTaskThumbnail()) field else thumbnailViewDeprecated.thumbnail
- private set
+ internal var thumbnailData: ThumbnailData? = null
+ val splitAnimationThumbnail: Bitmap?
+ get() =
+ if (enableRefactorTaskThumbnail()) thumbnailData?.thumbnail
+ else thumbnailViewDeprecated.thumbnail
val thumbnailView: TaskThumbnailView
get() {
@@ -98,10 +92,12 @@
return snapshotView as TaskThumbnailViewDeprecated
}
+ var isThumbnailValid: Boolean = false
+ internal set
+
val shouldShowSplashView: Boolean
get() =
- if (enableRefactorTaskThumbnail())
- taskContainerViewModel.shouldShowThumbnailSplash(task.key.id)
+ if (enableRefactorTaskThumbnail()) taskView.shouldShowSplash()
else thumbnailViewDeprecated.shouldShowSplashView()
/** Builds proto for logging */
@@ -111,7 +107,7 @@
fun bind() {
digitalWellBeingToast?.bind(task, taskView, snapshotView, stagePosition)
if (enableRefactorTaskThumbnail()) {
- bindThumbnailView()
+ taskThumbnailViewModel.bind(task.key.id)
} else {
thumbnailViewDeprecated.bind(task, overlay, taskView)
}
@@ -126,6 +122,9 @@
if (enableRefactorTaskThumbnail()) {
RecentsDependencies.getInstance().removeScope(snapshotView)
RecentsDependencies.getInstance().removeScope(this)
+ isThumbnailValid = false
+ } else {
+ thumbnailViewDeprecated.setShowSplashForSplitSelection(false)
}
}
@@ -134,10 +133,6 @@
thumbnailView.destroyScopes()
}
- private fun bindThumbnailView() {
- taskThumbnailViewModel.bind(task.key.id)
- }
-
fun setOverlayEnabled(enabled: Boolean) {
if (!enableRefactorTaskThumbnail()) {
thumbnailViewDeprecated.setOverlayEnabled(enabled)
@@ -157,15 +152,41 @@
TaskUiStateMapper.toTaskThumbnailUiState(state, liveTile, hasHeader),
state?.taskId,
)
- splitAnimationThumbnail =
- if (state is TaskData.Data) state.thumbnailData?.thumbnail else null
+ thumbnailData = if (state is TaskData.Data) state.thumbnailData else null
}
fun updateTintAmount(tintAmount: Float) {
thumbnailView.updateTintAmount(tintAmount)
}
+ /**
+ * Updates the progress of the menu opening animation.
+ *
+ * This function propagates the given `progress` value to the `thumbnailView` allowing the
+ * thumbnail view to animate its visual state in sync with the menu's opening/closing
+ * transition.
+ *
+ * @param progress The progress of the menu opening animation (from closed=0 to fully open=1)
+ */
fun updateMenuOpenProgress(progress: Float) {
thumbnailView.updateMenuOpenProgress(progress)
}
+
+ /**
+ * Updates the thumbnail splash progress for a given task.
+ *
+ * This function manages the visual feedback of a "splash" effect that can be displayed over a
+ * thumbnail image, typically during loading or updating. It calculates the alpha (transparency)
+ * of the splash based on the provided progress and then applies this alpha to the thumbnail
+ * view if it should be displayed.
+ *
+ * @param progress The progress of the operation, ranging from 0.0 to 1.0
+ */
+ fun updateThumbnailSplashProgress(progress: Float) {
+ if (enableRefactorTaskThumbnail()) {
+ thumbnailView.updateSplashAlpha(progress)
+ } else {
+ thumbnailViewDeprecated.setSplashAlpha(progress)
+ }
+ }
}
diff --git a/quickstep/src/com/android/quickstep/views/TaskView.kt b/quickstep/src/com/android/quickstep/views/TaskView.kt
index 4b1b8dc..609262f 100644
--- a/quickstep/src/com/android/quickstep/views/TaskView.kt
+++ b/quickstep/src/com/android/quickstep/views/TaskView.kt
@@ -41,6 +41,7 @@
import android.widget.Toast
import androidx.annotation.IntDef
import androidx.annotation.VisibleForTesting
+import androidx.core.view.doOnLayout
import androidx.core.view.updateLayoutParams
import com.android.app.animation.Interpolators
import com.android.launcher3.Flags.enableCursorHoverStates
@@ -80,6 +81,7 @@
import com.android.quickstep.recents.di.RecentsDependencies
import com.android.quickstep.recents.di.get
import com.android.quickstep.recents.di.inject
+import com.android.quickstep.recents.ui.viewmodel.TaskData
import com.android.quickstep.recents.ui.viewmodel.TaskTileUiState
import com.android.quickstep.recents.ui.viewmodel.TaskViewModel
import com.android.quickstep.util.ActiveGestureErrorDetector
@@ -248,8 +250,43 @@
)
private val tempCoordinates = FloatArray(2)
- private val focusBorderAnimator: BorderAnimator?
- private val hoverBorderAnimator: BorderAnimator?
+ private val focusBorderAnimator: BorderAnimator? =
+ focusBorderAnimator
+ ?: createSimpleBorderAnimator(
+ TaskCornerRadius.get(context).toInt(),
+ context.resources.getDimensionPixelSize(R.dimen.keyboard_quick_switch_border_width),
+ this::getThumbnailBounds,
+ this,
+ context
+ .obtainStyledAttributes(attrs, R.styleable.TaskView, defStyleAttr, defStyleRes)
+ .getColor(
+ R.styleable.TaskView_focusBorderColor,
+ BorderAnimator.DEFAULT_BORDER_COLOR,
+ ),
+ )
+
+ private val hoverBorderAnimator: BorderAnimator? =
+ hoverBorderAnimator
+ ?: if (enableCursorHoverStates())
+ createSimpleBorderAnimator(
+ TaskCornerRadius.get(context).toInt(),
+ context.resources.getDimensionPixelSize(R.dimen.task_hover_border_width),
+ this::getThumbnailBounds,
+ this,
+ context
+ .obtainStyledAttributes(
+ attrs,
+ R.styleable.TaskView,
+ defStyleAttr,
+ defStyleRes,
+ )
+ .getColor(
+ R.styleable.TaskView_hoverBorderColor,
+ BorderAnimator.DEFAULT_BORDER_COLOR,
+ ),
+ )
+ else null
+
private val rootViewDisplayId: Int
get() = rootView.display?.displayId ?: Display.DEFAULT_DISPLAY
@@ -295,6 +332,12 @@
onModalnessUpdated(field)
}
+ var splitSplashAlpha = 0f
+ set(value) {
+ field = value
+ applyThumbnailSplashAlpha()
+ }
+
protected var taskThumbnailSplashAlpha = 0f
set(value) {
field = value
@@ -519,40 +562,7 @@
init {
setOnClickListener { _ -> onClick() }
- val cursorHoverStatesEnabled = enableCursorHoverStates()
- setWillNotDraw(!cursorHoverStatesEnabled)
- context.obtainStyledAttributes(attrs, R.styleable.TaskView, defStyleAttr, defStyleRes).use {
- this.focusBorderAnimator =
- focusBorderAnimator
- ?: createSimpleBorderAnimator(
- TaskCornerRadius.get(context).toInt(),
- context.resources.getDimensionPixelSize(
- R.dimen.keyboard_quick_switch_border_width
- ),
- { bounds: Rect -> getThumbnailBounds(bounds) },
- this,
- it.getColor(
- R.styleable.TaskView_focusBorderColor,
- BorderAnimator.DEFAULT_BORDER_COLOR,
- ),
- )
- this.hoverBorderAnimator =
- hoverBorderAnimator
- ?: if (cursorHoverStatesEnabled)
- createSimpleBorderAnimator(
- TaskCornerRadius.get(context).toInt(),
- context.resources.getDimensionPixelSize(
- R.dimen.task_hover_border_width
- ),
- { bounds: Rect -> getThumbnailBounds(bounds) },
- this,
- it.getColor(
- R.styleable.TaskView_hoverBorderColor,
- BorderAnimator.DEFAULT_BORDER_COLOR,
- ),
- )
- else null
- }
+ setWillNotDraw(!enableCursorHoverStates())
}
@VisibleForTesting(otherwise = VisibleForTesting.PROTECTED)
@@ -645,6 +655,8 @@
viewModel = null
attachAlpha = 1f
splitAlpha = 1f
+ splitSplashAlpha = 0f
+ taskThumbnailSplashAlpha = 0f
// Clear any references to the thumbnail (it will be re-read either from the cache or the
// system on next bind)
if (!enableRefactorTaskThumbnail()) {
@@ -762,14 +774,30 @@
// Updating containers
val mapOfTasks = state.tasks.associateBy { it.taskId }
taskContainers.forEach { container ->
+ val containerState = mapOfTasks[container.task.key.id]
container.setState(
- state = mapOfTasks[container.task.key.id],
+ state = containerState,
liveTile = state.isLiveTile,
hasHeader = type == TaskViewType.DESKTOP,
)
+ updateThumbnailValidity(container)
+
+ if (enableOverviewIconMenu()) {
+ setIconState(container, containerState)
+ }
}
}
+ private fun updateThumbnailValidity(container: TaskContainer) {
+ container.isThumbnailValid =
+ viewModel!!.isThumbnailValid(
+ thumbnail = container.thumbnailData,
+ width = container.thumbnailView.width,
+ height = container.thumbnailView.height,
+ )
+ applyThumbnailSplashAlpha()
+ }
+
override fun onDetachedFromWindow() {
super.onDetachedFromWindow()
if (enableRefactorTaskThumbnail()) {
@@ -806,7 +834,7 @@
onBind(orientedState)
}
- open fun onBind(orientedState: RecentsOrientedState) {
+ protected open fun onBind(orientedState: RecentsOrientedState) {
if (enableRefactorTaskThumbnail()) {
viewModel =
TaskViewModel(
@@ -814,20 +842,37 @@
recentsViewData = RecentsDependencies.get(),
getTaskUseCase = RecentsDependencies.get(),
getSysUiStatusNavFlagsUseCase = RecentsDependencies.get(),
+ isThumbnailValidUseCase = RecentsDependencies.get(),
dispatcherProvider = RecentsDependencies.get(),
)
.apply { bind(*taskIds) }
}
- taskContainers.forEach {
- it.bind()
+ taskContainers.forEach { container ->
+ container.bind()
if (enableRefactorTaskThumbnail()) {
- it.thumbnailView.cornerRadius = thumbnailFullscreenParams.currentCornerRadius
+ container.thumbnailView.cornerRadius = thumbnailFullscreenParams.currentCornerRadius
+ container.thumbnailView.doOnLayout { updateThumbnailValidity(container) }
}
}
setOrientationState(orientedState)
}
+ private fun applyThumbnailSplashAlpha() {
+ val alpha = getSplashAlphaProgress()
+ taskContainers.forEach { it.updateThumbnailSplashProgress(alpha) }
+ }
+
+ private fun getSplashAlphaProgress(): Float =
+ when {
+ !enableRefactorTaskThumbnail() -> taskThumbnailSplashAlpha
+ splitSplashAlpha > 0f -> splitSplashAlpha
+ shouldShowSplash() -> taskThumbnailSplashAlpha
+ else -> 0f
+ }
+
+ internal fun shouldShowSplash(): Boolean = taskContainers.any { !it.isThumbnailValid }
+
protected fun createTaskContainer(
task: Task,
@IdRes thumbnailViewId: Int,
@@ -989,7 +1034,7 @@
}
}
}
- if (needsUpdate(changes, FLAG_UPDATE_ICON)) {
+ if (needsUpdate(changes, FLAG_UPDATE_ICON) && !enableOverviewIconMenu()) {
taskContainers.forEach {
if (visible) {
recentsModel.iconCache
@@ -1020,10 +1065,23 @@
pendingIconLoadRequests.clear()
}
+ protected open fun setIconState(container: TaskContainer, state: TaskData?) {
+ if (enableOverviewIconMenu()) {
+ if (state is TaskData.Data) {
+ setIcon(container.iconView, state.icon)
+ container.iconView.setText(state.title)
+ container.digitalWellBeingToast?.initialize()
+ } else {
+ setIcon(container.iconView, null)
+ container.iconView.setText(null)
+ }
+ }
+ }
+
protected open fun onIconLoaded(taskContainer: TaskContainer) {
setIcon(taskContainer.iconView, taskContainer.task.icon)
if (enableOverviewIconMenu()) {
- setText(taskContainer.iconView, taskContainer.task.title)
+ taskContainer.iconView.setText(taskContainer.task.title)
}
taskContainer.digitalWellBeingToast?.initialize()
}
@@ -1031,7 +1089,7 @@
protected open fun onIconUnloaded(taskContainer: TaskContainer) {
setIcon(taskContainer.iconView, null)
if (enableOverviewIconMenu()) {
- setText(taskContainer.iconView, null)
+ taskContainer.iconView.setText(null)
}
}
@@ -1056,10 +1114,6 @@
}
}
- protected fun setText(iconView: TaskViewIcon, text: CharSequence?) {
- iconView.setText(text)
- }
-
@JvmOverloads
open fun setShouldShowScreenshot(
shouldShowScreenshot: Boolean,
@@ -1293,6 +1347,7 @@
if (isQuickSwitch) {
setFreezeRecentTasksReordering()
}
+ // TODO(b/331754864): Update this to use TV.shouldShowSplash
disableStartingWindow = firstTaskContainer.shouldShowSplashView
}
Executors.UI_HELPER_EXECUTOR.execute {
@@ -1583,14 +1638,6 @@
updateFullscreenParams()
}
- protected open fun applyThumbnailSplashAlpha() {
- if (!enableRefactorTaskThumbnail()) {
- taskContainers.forEach {
- it.thumbnailViewDeprecated.setSplashAlpha(taskThumbnailSplashAlpha)
- }
- }
- }
-
private fun applyTranslationX() {
translationX =
dismissTranslationX +
diff --git a/quickstep/tests/multivalentScreenshotTests/src/com/android/quickstep/task/thumbnail/FakeTaskThumbnailViewModel.kt b/quickstep/tests/multivalentScreenshotTests/src/com/android/quickstep/task/thumbnail/FakeTaskThumbnailViewModel.kt
index 3e0c186..1a2b1c3 100644
--- a/quickstep/tests/multivalentScreenshotTests/src/com/android/quickstep/task/thumbnail/FakeTaskThumbnailViewModel.kt
+++ b/quickstep/tests/multivalentScreenshotTests/src/com/android/quickstep/task/thumbnail/FakeTaskThumbnailViewModel.kt
@@ -18,11 +18,8 @@
import android.graphics.Matrix
import com.android.quickstep.task.viewmodel.TaskThumbnailViewModel
-import kotlinx.coroutines.flow.MutableStateFlow
class FakeTaskThumbnailViewModel : TaskThumbnailViewModel {
- override val splashAlpha = MutableStateFlow(0f)
-
override fun bind(taskId: Int) {
// no-op
}
diff --git a/quickstep/tests/multivalentScreenshotTests/src/com/android/quickstep/task/thumbnail/TaskThumbnailViewScreenshotTest.kt b/quickstep/tests/multivalentScreenshotTests/src/com/android/quickstep/task/thumbnail/TaskThumbnailViewScreenshotTest.kt
index 356080a..232a08a 100644
--- a/quickstep/tests/multivalentScreenshotTests/src/com/android/quickstep/task/thumbnail/TaskThumbnailViewScreenshotTest.kt
+++ b/quickstep/tests/multivalentScreenshotTests/src/com/android/quickstep/task/thumbnail/TaskThumbnailViewScreenshotTest.kt
@@ -125,7 +125,6 @@
as TaskThumbnailView
taskThumbnailView.cornerRadius = CORNER_RADIUS
val ttvDiScopeId = di.getScope(taskThumbnailView).scopeId
- di.provide(TaskThumbnailViewData::class.java, ttvDiScopeId) { TaskThumbnailViewData() }
di.provide(TaskThumbnailViewModel::class.java, ttvDiScopeId) { taskThumbnailViewModel }
return taskThumbnailView
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 90c9553..0204b2d 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
@@ -33,6 +33,7 @@
import com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR
import com.android.launcher3.util.TestUtil
import com.android.quickstep.AllAppsActionManager
+import com.android.quickstep.fallback.window.RecentsDisplayModel
import java.lang.reflect.Field
import java.lang.reflect.ParameterizedType
import java.util.Locale
@@ -110,6 +111,7 @@
PendingIntent(IIntentSender.Default())
},
object : TaskbarNavButtonCallbacks {},
+ RecentsDisplayModel.INSTANCE.get(context),
) {
override fun recreateTaskbar() {
super.recreateTaskbar()
diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/OverviewCommandHelperTest.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/OverviewCommandHelperTest.kt
index 0ae710f..56c01f9 100644
--- a/quickstep/tests/multivalentTests/src/com/android/quickstep/OverviewCommandHelperTest.kt
+++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/OverviewCommandHelperTest.kt
@@ -68,7 +68,10 @@
touchInteractionService = mock(),
overviewComponentObserver = mock(),
taskAnimationManager = mock(),
- dispatcherProvider = TestDispatcherProvider(dispatcher)
+ dispatcherProvider = TestDispatcherProvider(dispatcher),
+ recentsDisplayModel = mock(),
+ focusState = mock(),
+ taskbarManager = mock(),
)
)
diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/RecentTasksListTest.java b/quickstep/tests/multivalentTests/src/com/android/quickstep/RecentTasksListTest.java
index c399bdb..8b17958 100644
--- a/quickstep/tests/multivalentTests/src/com/android/quickstep/RecentTasksListTest.java
+++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/RecentTasksListTest.java
@@ -97,6 +97,8 @@
// Set desktop mode supported
when(mContext.getResources()).thenReturn(mResources);
when(mResources.getBoolean(R.bool.config_isDesktopModeSupported)).thenReturn(true);
+ when(mResources.getBoolean(R.bool.config_canInternalDisplayHostDesktops))
+ .thenReturn(true);
mRecentTasksList = new RecentTasksList(mContext, mockMainThreadExecutor,
mockKeyguardManager, mSystemUiProxy, mTopTaskTracker);
diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/domain/usecase/IsThumbnailValidUseCaseTest.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/domain/usecase/IsThumbnailValidUseCaseTest.kt
new file mode 100644
index 0000000..e8bca93
--- /dev/null
+++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/domain/usecase/IsThumbnailValidUseCaseTest.kt
@@ -0,0 +1,99 @@
+/*
+ * Copyright (C) 2025 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.recents.domain.usecase
+
+import android.graphics.Bitmap
+import android.view.Surface
+import android.view.Surface.ROTATION_90
+import com.android.quickstep.recents.data.FakeRecentsRotationStateRepository
+import com.android.systemui.shared.recents.model.ThumbnailData
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.test.runTest
+import org.junit.Test
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.whenever
+
+class IsThumbnailValidUseCaseTest {
+ private val recentsRotationStateRepository = FakeRecentsRotationStateRepository()
+ private val systemUnderTest = IsThumbnailValidUseCase(recentsRotationStateRepository)
+
+ @Test
+ fun withNullThumbnail_returnsInvalid() = runTest {
+ val isThumbnailValid = systemUnderTest(thumbnailData = null, viewWidth = 0, viewHeight = 0)
+ assertThat(isThumbnailValid).isEqualTo(false)
+ }
+
+ @Test
+ fun sameAspectRatio_sameRotation_returnsValid() = runTest {
+ val isThumbnailValid =
+ systemUnderTest.invoke(
+ thumbnailData = createThumbnailData(),
+ viewWidth = THUMBNAIL_WIDTH * 2,
+ viewHeight = THUMBNAIL_HEIGHT * 2,
+ )
+ assertThat(isThumbnailValid).isEqualTo(true)
+ }
+
+ @Test
+ fun differentAspectRatio_sameRotation_returnsInvalid() = runTest {
+ val isThumbnailValid =
+ systemUnderTest.invoke(
+ thumbnailData = createThumbnailData(),
+ viewWidth = THUMBNAIL_WIDTH,
+ viewHeight = THUMBNAIL_HEIGHT * 2,
+ )
+ assertThat(isThumbnailValid).isEqualTo(false)
+ }
+
+ @Test
+ fun sameAspectRatio_differentRotation_returnsInvalid() = runTest {
+ val isThumbnailValid =
+ systemUnderTest.invoke(
+ thumbnailData = createThumbnailData(rotation = ROTATION_90),
+ viewWidth = THUMBNAIL_WIDTH * 2,
+ viewHeight = THUMBNAIL_HEIGHT * 2,
+ )
+ assertThat(isThumbnailValid).isEqualTo(false)
+ }
+
+ @Test
+ fun differentAspectRatio_differentRotation_returnsInvalid() = runTest {
+ val isThumbnailValid =
+ systemUnderTest.invoke(
+ thumbnailData = createThumbnailData(rotation = ROTATION_90),
+ viewWidth = THUMBNAIL_WIDTH,
+ viewHeight = THUMBNAIL_HEIGHT * 2,
+ )
+ assertThat(isThumbnailValid).isEqualTo(false)
+ }
+
+ private fun createThumbnailData(
+ rotation: Int = Surface.ROTATION_0,
+ width: Int = THUMBNAIL_WIDTH,
+ height: Int = THUMBNAIL_HEIGHT,
+ ): ThumbnailData {
+ val bitmap = mock<Bitmap>()
+ whenever(bitmap.width).thenReturn(width)
+ whenever(bitmap.height).thenReturn(height)
+ return ThumbnailData(thumbnail = bitmap, rotation = rotation)
+ }
+
+ companion object {
+ const val THUMBNAIL_WIDTH = 100
+ const val THUMBNAIL_HEIGHT = 200
+ }
+}
diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/ui/viewmodel/TaskViewModelTest.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/ui/viewmodel/TaskViewModelTest.kt
index c031150..08e459b 100644
--- a/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/ui/viewmodel/TaskViewModelTest.kt
+++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/ui/viewmodel/TaskViewModelTest.kt
@@ -25,9 +25,11 @@
import com.android.launcher3.util.SystemUiController.FLAG_LIGHT_NAV
import com.android.launcher3.util.SystemUiController.FLAG_LIGHT_STATUS
import com.android.launcher3.util.TestDispatcherProvider
+import com.android.quickstep.recents.data.FakeRecentsRotationStateRepository
import com.android.quickstep.recents.domain.model.TaskModel
import com.android.quickstep.recents.domain.usecase.GetSysUiStatusNavFlagsUseCase
import com.android.quickstep.recents.domain.usecase.GetTaskUseCase
+import com.android.quickstep.recents.domain.usecase.IsThumbnailValidUseCase
import com.android.quickstep.recents.viewmodel.RecentsViewData
import com.android.quickstep.views.TaskViewType
import com.android.systemui.shared.recents.model.ThumbnailData
@@ -41,6 +43,10 @@
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
+import org.mockito.Mockito.anyInt
+import org.mockito.Mockito.spy
+import org.mockito.Mockito.verify
+import org.mockito.kotlin.anyOrNull
import org.mockito.kotlin.mock
import org.mockito.kotlin.whenever
@@ -52,6 +58,8 @@
private val recentsViewData = RecentsViewData()
private val getTaskUseCase = mock<GetTaskUseCase>()
+ private val isThumbnailValidUseCase =
+ spy(IsThumbnailValidUseCase(FakeRecentsRotationStateRepository()))
private lateinit var sut: TaskViewModel
@Before
@@ -62,6 +70,7 @@
recentsViewData = recentsViewData,
getTaskUseCase = getTaskUseCase,
getSysUiStatusNavFlagsUseCase = GetSysUiStatusNavFlagsUseCase(),
+ isThumbnailValidUseCase = isThumbnailValidUseCase,
dispatcherProvider = TestDispatcherProvider(unconfinedTestDispatcher),
)
whenever(getTaskUseCase.invoke(TASK_MODEL_1.id)).thenReturn(flow { emit(TASK_MODEL_1) })
@@ -102,6 +111,7 @@
recentsViewData = recentsViewData,
getTaskUseCase = getTaskUseCase,
getSysUiStatusNavFlagsUseCase = GetSysUiStatusNavFlagsUseCase(),
+ isThumbnailValidUseCase = isThumbnailValidUseCase,
dispatcherProvider = TestDispatcherProvider(unconfinedTestDispatcher),
)
sut.bind(TASK_MODEL_1.id)
@@ -225,6 +235,12 @@
assertThat(sut.state.first()).isEqualTo(expectedResult)
}
+ @Test
+ fun shouldShowSplash_calls_useCase() {
+ sut.isThumbnailValid(null, 0, 0)
+ verify(isThumbnailValidUseCase).invoke(anyOrNull(), anyInt(), anyInt())
+ }
+
private fun TaskModel.toUiState() =
TaskData.Data(
taskId = id,
diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/task/thumbnail/SplashAlphaUseCaseTest.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/task/thumbnail/SplashAlphaUseCaseTest.kt
deleted file mode 100644
index 0767fb9..0000000
--- a/quickstep/tests/multivalentTests/src/com/android/quickstep/task/thumbnail/SplashAlphaUseCaseTest.kt
+++ /dev/null
@@ -1,149 +0,0 @@
-/*
- * 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.content.ComponentName
-import android.content.Intent
-import android.graphics.Bitmap
-import android.graphics.Color
-import android.graphics.drawable.Drawable
-import android.view.Surface
-import android.view.Surface.ROTATION_90
-import com.android.quickstep.recents.data.FakeRecentsRotationStateRepository
-import com.android.quickstep.recents.data.FakeTasksRepository
-import com.android.quickstep.recents.viewmodel.RecentsViewData
-import com.android.quickstep.task.viewmodel.TaskContainerData
-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.mockito.kotlin.mock
-import org.mockito.kotlin.whenever
-
-class SplashAlphaUseCaseTest {
- private val recentsViewData = RecentsViewData()
- private val taskContainerData = TaskContainerData()
- private val taskThumbnailViewData = TaskThumbnailViewData()
- private val recentTasksRepository = FakeTasksRepository()
- private val recentsRotationStateRepository = FakeRecentsRotationStateRepository()
- private val systemUnderTest =
- SplashAlphaUseCase(
- recentsViewData,
- taskContainerData,
- taskThumbnailViewData,
- recentTasksRepository,
- recentsRotationStateRepository,
- )
-
- @Test
- fun execute_withNullThumbnail_showsSplash() = runTest {
- assertThat(systemUnderTest.execute(0).first()).isEqualTo(SPLASH_HIDDEN)
- }
-
- @Test
- fun execute_withTaskSpecificSplashAlpha_showsSplash() = runTest {
- setupTask(2)
- taskContainerData.thumbnailSplashProgress.value = 0.7f
-
- assertThat(systemUnderTest.execute(2).first()).isEqualTo(0.7f)
- }
-
- @Test
- fun execute_withNoGlobalSplashEnabled_doesntShowSplash() = runTest {
- setupTask(2)
-
- assertThat(systemUnderTest.execute(2).first()).isEqualTo(SPLASH_HIDDEN)
- }
-
- @Test
- fun execute_withSameAspectRatioAndRotation_withGlobalSplashEnabled_doesntShowSplash() =
- runTest {
- setupTask(2)
- recentsViewData.thumbnailSplashProgress.value = 0.5f
- taskThumbnailViewData.width.value = THUMBNAIL_WIDTH * 2
- taskThumbnailViewData.height.value = THUMBNAIL_HEIGHT * 2
-
- assertThat(systemUnderTest.execute(2).first()).isEqualTo(SPLASH_HIDDEN)
- }
-
- @Test
- fun execute_withDifferentAspectRatioAndSameRotation_showsSplash() = runTest {
- setupTask(2)
- recentsViewData.thumbnailSplashProgress.value = 0.5f
- taskThumbnailViewData.width.value = THUMBNAIL_WIDTH
- taskThumbnailViewData.height.value = THUMBNAIL_HEIGHT * 2
-
- assertThat(systemUnderTest.execute(2).first()).isEqualTo(0.5f)
- }
-
- @Test
- fun execute_withSameAspectRatioAndDifferentRotation_showsSplash() = runTest {
- setupTask(2, createThumbnailData(rotation = ROTATION_90))
- recentsViewData.thumbnailSplashProgress.value = 0.5f
- taskThumbnailViewData.width.value = THUMBNAIL_WIDTH * 2
- taskThumbnailViewData.height.value = THUMBNAIL_HEIGHT * 2
-
- assertThat(systemUnderTest.execute(2).first()).isEqualTo(0.5f)
- }
-
- @Test
- fun execute_withDifferentAspectRatioAndRotation_showsSplash() = runTest {
- setupTask(2, createThumbnailData(rotation = ROTATION_90))
- recentsViewData.thumbnailSplashProgress.value = 0.5f
- taskThumbnailViewData.width.value = THUMBNAIL_WIDTH
- taskThumbnailViewData.height.value = THUMBNAIL_HEIGHT * 2
-
- assertThat(systemUnderTest.execute(2).first()).isEqualTo(0.5f)
- }
-
- private val tasks = (0..5).map(::createTaskWithId)
-
- private fun setupTask(taskId: Int, thumbnailData: ThumbnailData = createThumbnailData()) {
- recentTasksRepository.seedThumbnailData(mapOf(taskId to thumbnailData))
- val expectedIconData = mock<Drawable>()
- recentTasksRepository.seedIconData(taskId, "Task $taskId", "", expectedIconData)
- recentTasksRepository.seedTasks(tasks)
- recentTasksRepository.setVisibleTasks(setOf(taskId))
- }
-
- private fun createThumbnailData(
- rotation: Int = Surface.ROTATION_0,
- width: Int = THUMBNAIL_WIDTH,
- height: Int = THUMBNAIL_HEIGHT,
- ): ThumbnailData {
- val bitmap = mock<Bitmap>()
- whenever(bitmap.width).thenReturn(width)
- whenever(bitmap.height).thenReturn(height)
-
- return ThumbnailData(thumbnail = bitmap, rotation = rotation)
- }
-
- private fun createTaskWithId(taskId: Int) =
- Task(Task.TaskKey(taskId, 0, Intent(), ComponentName("", ""), 0, 2000)).apply {
- colorBackground = Color.argb(taskId, taskId, taskId, taskId)
- }
-
- companion object {
- const val THUMBNAIL_WIDTH = 100
- const val THUMBNAIL_HEIGHT = 200
-
- const val SPLASH_HIDDEN = 0f
- const val SPLASH_SHOWN = 1f
- }
-}
diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/task/thumbnail/TaskThumbnailViewModelImplTest.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/task/thumbnail/TaskThumbnailViewModelImplTest.kt
index aec586d..4b4e2eb 100644
--- a/quickstep/tests/multivalentTests/src/com/android/quickstep/task/thumbnail/TaskThumbnailViewModelImplTest.kt
+++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/task/thumbnail/TaskThumbnailViewModelImplTest.kt
@@ -19,7 +19,6 @@
import android.graphics.Matrix
import android.platform.test.flag.junit.SetFlagsRule
import androidx.test.ext.junit.runners.AndroidJUnit4
-import com.android.launcher3.util.TestDispatcherProvider
import com.android.quickstep.recents.usecase.GetThumbnailPositionUseCase
import com.android.quickstep.recents.usecase.ThumbnailPositionState.MatrixScaling
import com.android.quickstep.recents.usecase.ThumbnailPositionState.MissingThumbnail
@@ -42,17 +41,9 @@
private val dispatcher = StandardTestDispatcher()
private val testScope = TestScope(dispatcher)
- private val dispatcherProvider = TestDispatcherProvider(dispatcher)
private val mGetThumbnailPositionUseCase = mock<GetThumbnailPositionUseCase>()
- private val splashAlphaUseCase: SplashAlphaUseCase = mock()
- private val systemUnderTest by lazy {
- TaskThumbnailViewModelImpl(
- dispatcherProvider,
- mGetThumbnailPositionUseCase,
- splashAlphaUseCase,
- )
- }
+ private val systemUnderTest by lazy { TaskThumbnailViewModelImpl(mGetThumbnailPositionUseCase) }
@Test
fun getSnapshotMatrix_MissingThumbnail() =
diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/util/TaskGridNavHelperTest.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/util/TaskGridNavHelperTest.kt
index 7aab75f..f2fa0c5 100644
--- a/quickstep/tests/multivalentTests/src/com/android/quickstep/util/TaskGridNavHelperTest.kt
+++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/util/TaskGridNavHelperTest.kt
@@ -16,6 +16,7 @@
package com.android.quickstep.util
import com.android.launcher3.util.IntArray
+import com.android.quickstep.util.TaskGridNavHelper.ADD_DESK_PLACEHOLDER_ID
import com.android.quickstep.util.TaskGridNavHelper.CLEAR_ALL_PLACEHOLDER_ID
import com.android.quickstep.util.TaskGridNavHelper.DIRECTION_DOWN
import com.android.quickstep.util.TaskGridNavHelper.DIRECTION_LEFT
@@ -446,6 +447,37 @@
}
/*
+ 5 3 [1]
+ CLEAR_ALL
+ 6 4 2
+ */
+ @Test
+ fun equalLengthRows_noFocused_onTop_pressTabWithShift_noCycle_staysOnTop() {
+ assertThat(
+ getNextGridPage(currentPageTaskViewId = 1, DIRECTION_TAB, delta = -1, cycle = false)
+ )
+ .isEqualTo(1)
+ }
+
+ /*
+ 5 3 1
+ [CLEAR_ALL]
+ 6 4 2
+ */
+ @Test
+ fun equalLengthRows_noFocused_onClearAll_pressTab_noCycle_staysOnClearAll() {
+ assertThat(
+ getNextGridPage(
+ currentPageTaskViewId = CLEAR_ALL_PLACEHOLDER_ID,
+ DIRECTION_TAB,
+ delta = 1,
+ cycle = false,
+ )
+ )
+ .isEqualTo(CLEAR_ALL_PLACEHOLDER_ID)
+ }
+
+ /*
5 3 1
CLEAR_ALL FOCUSED_TASK←--DESKTOP
6 4 2
@@ -619,6 +651,161 @@
.isEqualTo(CLEAR_ALL_PLACEHOLDER_ID)
}
+ /*
+ 5 3 1→----|
+ ↓
+ CLEAR_ALL ADD_DESKTOP
+ 6 4 2
+ */
+ @Test
+ fun withAddDesktopButton_pressRightFromTop_goesToAddDesktopButton() {
+ assertThat(
+ getNextGridPage(
+ currentPageTaskViewId = 1,
+ DIRECTION_RIGHT,
+ delta = -1,
+ hasAddDesktopButton = true,
+ )
+ )
+ .isEqualTo(ADD_DESK_PLACEHOLDER_ID)
+ }
+
+ /*
+ 5 3 1
+ CLEAR_ALL ADD_DESKTOP
+ ↑
+ 6 4 2→----↑
+ */
+ @Test
+ fun withAddDesktopButton_pressRightFromBottom_goesToAddDesktopButton() {
+ assertThat(
+ getNextGridPage(
+ currentPageTaskViewId = 2,
+ DIRECTION_RIGHT,
+ delta = -1,
+ hasAddDesktopButton = true,
+ )
+ )
+ .isEqualTo(ADD_DESK_PLACEHOLDER_ID)
+ }
+
+ /*
+ ↓-------------------------------←|
+ | ↑
+ ↓ 5 3 1 |
+ CLEAR_ALL ADD_DESKTOP--→
+ 6 4 2
+ */
+ @Test
+ fun withAddDesktopButton_pressRightFromAddDesktopButton_goesToClearAllButton() {
+ assertThat(
+ getNextGridPage(
+ currentPageTaskViewId = ADD_DESK_PLACEHOLDER_ID,
+ DIRECTION_RIGHT,
+ delta = -1,
+ hasAddDesktopButton = true,
+ )
+ )
+ .isEqualTo(CLEAR_ALL_PLACEHOLDER_ID)
+ }
+
+ /*
+ |→--------------------------------|
+ | |
+ ↑ 5 3 1 ↓
+ ←------CLEAR_ALL ADD_DESKTOP
+
+ 6 4 2
+ */
+ @Test
+ fun withAddDesktopButton_pressLeftFromClearAllButton_goesToAddDesktopButton() {
+ assertThat(
+ getNextGridPage(
+ currentPageTaskViewId = CLEAR_ALL_PLACEHOLDER_ID,
+ DIRECTION_LEFT,
+ delta = 1,
+ hasAddDesktopButton = true,
+ )
+ )
+ .isEqualTo(ADD_DESK_PLACEHOLDER_ID)
+ }
+
+ /*
+ 5 3 1
+ ←--↑
+ CLEAR_ALL ↓-→ADD_DESKTOP
+ 6 4 2
+ */
+ @Test
+ fun withAddDesktopButton_pressUpOnAddDesktop_stayOnAddDesktopButton() {
+ assertThat(
+ getNextGridPage(
+ currentPageTaskViewId = ADD_DESK_PLACEHOLDER_ID,
+ DIRECTION_UP,
+ delta = 1,
+ hasAddDesktopButton = true,
+ )
+ )
+ .isEqualTo(ADD_DESK_PLACEHOLDER_ID)
+ }
+
+ /*
+ 5 3 1
+ CLEAR_ALL ↑--→ADD_DESKTOP
+ ↑←--↓
+ 6 4 2
+ */
+ @Test
+ fun withAddDesktopButton_pressDownOnAddDesktop_stayOnAddDesktopButton() {
+ assertThat(
+ getNextGridPage(
+ currentPageTaskViewId = ADD_DESK_PLACEHOLDER_ID,
+ DIRECTION_DOWN,
+ delta = 1,
+ hasAddDesktopButton = true,
+ )
+ )
+ .isEqualTo(ADD_DESK_PLACEHOLDER_ID)
+ }
+
+ /*
+ 5 3 1
+ CLEAR_ALL DESKTOP--→ADD_DESKTOP
+ 6 4 2
+ */
+ @Test
+ fun withAddDesktopButton_pressRightFromDesktopTask_goesToAddDesktopButton() {
+ assertThat(
+ getNextGridPage(
+ currentPageTaskViewId = ADD_DESK_PLACEHOLDER_ID,
+ DIRECTION_LEFT,
+ delta = 1,
+ largeTileIds = listOf(DESKTOP_TASK_ID),
+ hasAddDesktopButton = true,
+ )
+ )
+ .isEqualTo(DESKTOP_TASK_ID)
+ }
+
+ /*
+ 5 3 1
+ CLEAR_ALL DESKTOP←--ADD_DESKTOP
+ 6 4 2
+ */
+ @Test
+ fun withAddDesktopButton_pressLeftFromAddDesktopButton_goesToDesktopTask() {
+ assertThat(
+ getNextGridPage(
+ currentPageTaskViewId = DESKTOP_TASK_ID,
+ DIRECTION_RIGHT,
+ delta = -1,
+ largeTileIds = listOf(DESKTOP_TASK_ID),
+ hasAddDesktopButton = true,
+ )
+ )
+ .isEqualTo(ADD_DESK_PLACEHOLDER_ID)
+ }
+
private fun getNextGridPage(
currentPageTaskViewId: Int,
direction: Int,
@@ -626,9 +813,12 @@
topIds: IntArray = IntArray.wrap(1, 3, 5),
bottomIds: IntArray = IntArray.wrap(2, 4, 6),
largeTileIds: List<Int> = emptyList(),
+ hasAddDesktopButton: Boolean = false,
+ cycle: Boolean = true,
): Int {
- val taskGridNavHelper = TaskGridNavHelper(topIds, bottomIds, largeTileIds)
- return taskGridNavHelper.getNextGridPage(currentPageTaskViewId, delta, direction, true)
+ val taskGridNavHelper =
+ TaskGridNavHelper(topIds, bottomIds, largeTileIds, hasAddDesktopButton)
+ return taskGridNavHelper.getNextGridPage(currentPageTaskViewId, delta, direction, cycle)
}
private companion object {
diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/util/TransformParamsTest.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/util/TransformParamsTest.kt
new file mode 100644
index 0000000..6dbb667
--- /dev/null
+++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/util/TransformParamsTest.kt
@@ -0,0 +1,191 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.quickstep.util
+
+import android.app.ActivityManager.RunningTaskInfo
+import android.app.WindowConfiguration
+import android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD
+import android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM
+import android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN
+import android.platform.test.annotations.DisableFlags
+import android.platform.test.annotations.EnableFlags
+import android.platform.test.flag.junit.SetFlagsRule
+import android.view.RemoteAnimationTarget
+import android.view.SurfaceControl
+import android.view.WindowManager.TRANSIT_OPEN
+import android.window.TransitionInfo
+import android.window.TransitionInfo.Change
+import android.window.TransitionInfo.FLAG_NONE
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.quickstep.RemoteAnimationTargets
+import com.android.quickstep.util.TransformParams.BuilderProxy.NO_OP
+import com.android.window.flags.Flags.FLAG_ENABLE_DESKTOP_RECENTS_TRANSITIONS_CORNERS_BUGFIX
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.kotlin.anyOrNull
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.never
+import org.mockito.kotlin.verify
+import org.mockito.kotlin.whenever
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class TransformParamsTest {
+ private val surfaceTransaction = mock<SurfaceTransaction>()
+ private val transaction = mock<SurfaceControl.Transaction>()
+ private val transformParams = TransformParams(::surfaceTransaction)
+
+ private val freeformTaskInfo1 =
+ createTaskInfo(taskId = 1, windowingMode = WINDOWING_MODE_FREEFORM)
+ private val freeformTaskInfo2 =
+ createTaskInfo(taskId = 2, windowingMode = WINDOWING_MODE_FREEFORM)
+ private val fullscreenTaskInfo1 =
+ createTaskInfo(taskId = 1, windowingMode = WINDOWING_MODE_FULLSCREEN)
+
+ @get:Rule val setFlagsRule: SetFlagsRule = SetFlagsRule()
+
+ @Before
+ fun setUp() {
+ whenever(surfaceTransaction.transaction).thenReturn(transaction)
+ whenever(surfaceTransaction.forSurface(anyOrNull()))
+ .thenReturn(mock<SurfaceTransaction.SurfaceProperties>())
+ transformParams.setCornerRadius(CORNER_RADIUS)
+ }
+
+ @Test
+ @EnableFlags(FLAG_ENABLE_DESKTOP_RECENTS_TRANSITIONS_CORNERS_BUGFIX)
+ fun createSurfaceParams_freeformTasks_overridesCornerRadius() {
+ val transitionInfo = TransitionInfo(TRANSIT_OPEN, FLAG_NONE)
+ val leash1 = mock<SurfaceControl>()
+ val leash2 = mock<SurfaceControl>()
+ transitionInfo.addChange(createChange(freeformTaskInfo1, leash = leash1))
+ transitionInfo.addChange(createChange(freeformTaskInfo2, leash = leash2))
+ transformParams.setTransitionInfo(transitionInfo)
+ transformParams.setTargetSet(createTargetSet(listOf(freeformTaskInfo1, freeformTaskInfo2)))
+
+ transformParams.createSurfaceParams(NO_OP)
+
+ verify(transaction).setCornerRadius(leash1, 0f)
+ verify(transaction).setCornerRadius(leash2, 0f)
+ }
+
+ @Test
+ @EnableFlags(FLAG_ENABLE_DESKTOP_RECENTS_TRANSITIONS_CORNERS_BUGFIX)
+ fun createSurfaceParams_freeformTasks_overridesCornerRadiusOnlyOnce() {
+ val transitionInfo = TransitionInfo(TRANSIT_OPEN, FLAG_NONE)
+ val leash1 = mock<SurfaceControl>()
+ val leash2 = mock<SurfaceControl>()
+ transitionInfo.addChange(createChange(freeformTaskInfo1, leash = leash1))
+ transitionInfo.addChange(createChange(freeformTaskInfo2, leash = leash2))
+ transformParams.setTransitionInfo(transitionInfo)
+ transformParams.setTargetSet(createTargetSet(listOf(freeformTaskInfo1, freeformTaskInfo2)))
+ transformParams.createSurfaceParams(NO_OP)
+
+ transformParams.createSurfaceParams(NO_OP)
+
+ verify(transaction).setCornerRadius(leash1, 0f)
+ verify(transaction).setCornerRadius(leash2, 0f)
+ }
+
+ @Test
+ @DisableFlags(FLAG_ENABLE_DESKTOP_RECENTS_TRANSITIONS_CORNERS_BUGFIX)
+ fun createSurfaceParams_flagDisabled_doesntOverrideCornerRadius() {
+ val transitionInfo = TransitionInfo(TRANSIT_OPEN, FLAG_NONE)
+ val leash1 = mock<SurfaceControl>()
+ val leash2 = mock<SurfaceControl>()
+ transitionInfo.addChange(createChange(freeformTaskInfo1, leash = leash1))
+ transitionInfo.addChange(createChange(freeformTaskInfo2, leash = leash2))
+ transformParams.setTransitionInfo(transitionInfo)
+ transformParams.setTargetSet(createTargetSet(listOf(freeformTaskInfo1, freeformTaskInfo2)))
+
+ transformParams.createSurfaceParams(NO_OP)
+
+ verify(transaction, never()).setCornerRadius(leash1, 0f)
+ verify(transaction, never()).setCornerRadius(leash2, 0f)
+ }
+
+ @Test
+ @EnableFlags(FLAG_ENABLE_DESKTOP_RECENTS_TRANSITIONS_CORNERS_BUGFIX)
+ fun createSurfaceParams_fullscreenTasks_doesntOverrideCornerRadius() {
+ val transitionInfo = TransitionInfo(TRANSIT_OPEN, FLAG_NONE)
+ val leash = mock<SurfaceControl>()
+ transitionInfo.addChange(createChange(fullscreenTaskInfo1, leash = leash))
+ transformParams.setTransitionInfo(transitionInfo)
+ transformParams.setTargetSet(createTargetSet(listOf(fullscreenTaskInfo1)))
+
+ transformParams.createSurfaceParams(NO_OP)
+
+ verify(transaction, never()).setCornerRadius(leash, 0f)
+ }
+
+ private fun createTargetSet(taskInfos: List<RunningTaskInfo>): RemoteAnimationTargets {
+ val remoteAnimationTargets = mutableListOf<RemoteAnimationTarget>()
+ taskInfos.map { remoteAnimationTargets.add(createRemoteAnimationTarget(it)) }
+ return RemoteAnimationTargets(
+ remoteAnimationTargets.toTypedArray(),
+ /* wallpapers= */ null,
+ /* nonApps= */ null,
+ /* targetMode= */ TRANSIT_OPEN,
+ )
+ }
+
+ private fun createRemoteAnimationTarget(taskInfo: RunningTaskInfo): RemoteAnimationTarget {
+ val windowConfig = mock<WindowConfiguration>()
+ whenever(windowConfig.activityType).thenReturn(ACTIVITY_TYPE_STANDARD)
+ return RemoteAnimationTarget(
+ taskInfo.taskId,
+ /* mode= */ TRANSIT_OPEN,
+ /* leash= */ null,
+ /* isTranslucent= */ false,
+ /* clipRect= */ null,
+ /* contentInsets= */ null,
+ /* prefixOrderIndex= */ 0,
+ /* position= */ null,
+ /* localBounds= */ null,
+ /* screenSpaceBounds= */ null,
+ windowConfig,
+ /* isNotInRecents= */ false,
+ /* startLeash= */ null,
+ /* startBounds= */ null,
+ taskInfo,
+ /* allowEnterPip= */ false,
+ )
+ }
+
+ private fun createTaskInfo(taskId: Int, windowingMode: Int): RunningTaskInfo {
+ val taskInfo = RunningTaskInfo()
+ taskInfo.taskId = taskId
+ taskInfo.configuration.windowConfiguration.windowingMode = windowingMode
+ return taskInfo
+ }
+
+ private fun createChange(taskInfo: RunningTaskInfo, leash: SurfaceControl): Change {
+ val taskInfo = createTaskInfo(taskInfo.taskId, taskInfo.windowingMode)
+ val change = Change(taskInfo.token, mock<SurfaceControl>())
+ change.mode = TRANSIT_OPEN
+ change.taskInfo = taskInfo
+ change.leash = leash
+ return change
+ }
+
+ private companion object {
+ private const val CORNER_RADIUS = 30f
+ }
+}
diff --git a/quickstep/tests/src/com/android/quickstep/TaplTestsQuickstep.java b/quickstep/tests/src/com/android/quickstep/TaplTestsQuickstep.java
index e0560e2..79d3c19 100644
--- a/quickstep/tests/src/com/android/quickstep/TaplTestsQuickstep.java
+++ b/quickstep/tests/src/com/android/quickstep/TaplTestsQuickstep.java
@@ -539,6 +539,24 @@
}
}
+ @Test
+ @PortraitLandscape
+ public void testDismissCancel() throws Exception {
+ startTestAppsWithCheck();
+ Overview overview = mLauncher.goHome().switchToOverview();
+ assertIsInState("Launcher internal state didn't switch to Overview",
+ ExpectedState.OVERVIEW);
+ final Integer numTasks = getFromRecentsView(RecentsView::getTaskViewCount);
+ OverviewTask task = overview.getCurrentTask();
+ assertNotNull("overview.getCurrentTask() returned null (2)", task);
+
+ task.dismissCancel();
+
+ runOnRecentsView(recentsView -> assertEquals(
+ "Canceling dismissing a task removed a task from Overview",
+ numTasks == null ? 0 : numTasks, recentsView.getTaskViewCount()));
+ }
+
private void startTestAppsWithCheck() throws Exception {
startTestApps();
expectLaunchedAppState();
diff --git a/res/values/config.xml b/res/values/config.xml
index a545f0c..07f97bc 100644
--- a/res/values/config.xml
+++ b/res/values/config.xml
@@ -117,6 +117,10 @@
<item name="swipe_up_rect_y_damping_ratio" type="dimen" format="float">0.95</item>
<item name="swipe_up_rect_y_stiffness" type="dimen" format="float">400</item>
+ <!-- Expressive Dismiss -->
+ <item name="expressive_dismiss_task_trans_y_damping_ratio" type="dimen" format="float">0.6</item>
+ <item name="expressive_dismiss_task_trans_y_stiffness" type="dimen" format="float">900</item>
+
<!-- Taskbar -->
<!-- This is a float because it is converted to dp later in DeviceProfile -->
<item name="taskbar_icon_size" type="dimen" format="float">0</item>
diff --git a/res/values/dimens.xml b/res/values/dimens.xml
index c48f140..c3cb31d 100644
--- a/res/values/dimens.xml
+++ b/res/values/dimens.xml
@@ -480,6 +480,7 @@
<dimen name="task_thumbnail_icon_drawable_size_grid">0dp</dimen>
<dimen name="task_thumbnail_icon_menu_drawable_touch_size">0dp</dimen>
<dimen name="task_menu_edge_padding">0dp</dimen>
+ <dimen name="task_dismiss_max_undershoot">0dp</dimen>
<dimen name="overview_task_margin">0dp</dimen>
<dimen name="overview_actions_height">0dp</dimen>
<dimen name="overview_actions_button_spacing">0dp</dimen>
diff --git a/src/com/android/launcher3/BubbleTextView.java b/src/com/android/launcher3/BubbleTextView.java
index d3684b2..fb847f9 100644
--- a/src/com/android/launcher3/BubbleTextView.java
+++ b/src/com/android/launcher3/BubbleTextView.java
@@ -77,7 +77,6 @@
import com.android.launcher3.folder.FolderIcon;
import com.android.launcher3.graphics.IconShape;
import com.android.launcher3.graphics.PreloadIconDrawable;
-import com.android.launcher3.graphics.ThemeManager;
import com.android.launcher3.icons.DotRenderer;
import com.android.launcher3.icons.FastBitmapDrawable;
import com.android.launcher3.icons.IconCache.ItemInfoUpdateReceiver;
@@ -484,9 +483,7 @@
}
private void setNonPendingIcon(ItemInfoWithIcon info) {
- ThemeManager themeManager = ThemeManager.INSTANCE.get(getContext());
- int flags = (shouldUseTheme()
- && themeManager.isMonoThemeEnabled()) ? FLAG_THEMED : 0;
+ int flags = shouldUseTheme() ? FLAG_THEMED : 0;
// Remove badge on icons smaller than 48dp.
if (mHideBadge || mDisplay == DISPLAY_SEARCH_RESULT_SMALL) {
flags |= FLAG_NO_BADGE;
diff --git a/src/com/android/launcher3/InvariantDeviceProfile.java b/src/com/android/launcher3/InvariantDeviceProfile.java
index e47a44a..900f74d 100644
--- a/src/com/android/launcher3/InvariantDeviceProfile.java
+++ b/src/com/android/launcher3/InvariantDeviceProfile.java
@@ -20,6 +20,7 @@
import static com.android.launcher3.LauncherPrefs.ENABLE_TWOLINE_ALLAPPS_TOGGLE;
import static com.android.launcher3.LauncherPrefs.FIXED_LANDSCAPE_MODE;
import static com.android.launcher3.LauncherPrefs.GRID_NAME;
+import static com.android.launcher3.LauncherPrefs.NON_FIXED_LANDSCAPE_GRID_NAME;
import static com.android.launcher3.Utilities.dpiFromPx;
import static com.android.launcher3.testing.shared.ResourceUtils.INVALID_RESOURCE_HANDLE;
import static com.android.launcher3.util.DisplayController.CHANGE_DENSITY;
@@ -93,6 +94,8 @@
new MainThreadInitializedObject<>(InvariantDeviceProfile::new);
public static final String GRID_NAME_PREFS_KEY = "idp_grid_name";
+ public static final String NON_FIXED_LANDSCAPE_GRID_NAME_PREFS_KEY =
+ "idp_non_fixed_landscape_grid_name";
@Retention(RetentionPolicy.SOURCE)
@IntDef({TYPE_PHONE, TYPE_MULTI_DISPLAY, TYPE_TABLET})
@@ -268,7 +271,14 @@
if (FIXED_LANDSCAPE_MODE.getSharedPrefKey().equals(key)
&& isFixedLandscape != FIXED_LANDSCAPE_MODE.get(context)) {
Trace.beginSection("InvariantDeviceProfile#setFixedLandscape");
- onConfigChanged(context);
+ if (isFixedLandscape) {
+ setCurrentGrid(
+ context, LauncherPrefs.get(context).get(NON_FIXED_LANDSCAPE_GRID_NAME));
+ } else {
+ LauncherPrefs.get(context)
+ .put(NON_FIXED_LANDSCAPE_GRID_NAME, getCurrentGridName(context));
+ onConfigChanged(context);
+ }
Trace.endSection();
} else if (ENABLE_TWOLINE_ALLAPPS_TOGGLE.getSharedPrefKey().equals(key)
&& enableTwoLinesInAllApps != ENABLE_TWOLINE_ALLAPPS_TOGGLE.get(context)) {
@@ -399,8 +409,9 @@
private List<DisplayOption> filterByColumnCount(
List<DisplayOption> allOptions, int numColumns) {
- return allOptions.stream().filter(
- option -> option.grid.numColumns == numColumns).toList();
+ return allOptions.stream()
+ .filter(option -> option.grid.numColumns == numColumns)
+ .collect(Collectors.toList());
}
/**
diff --git a/src/com/android/launcher3/Launcher.java b/src/com/android/launcher3/Launcher.java
index 30ef24b..315301a 100644
--- a/src/com/android/launcher3/Launcher.java
+++ b/src/com/android/launcher3/Launcher.java
@@ -29,6 +29,7 @@
import static com.android.launcher3.AbstractFloatingView.getTopOpenViewWithType;
import static com.android.launcher3.Flags.enableAddAppWidgetViaConfigActivityV2;
import static com.android.launcher3.Flags.enableSmartspaceRemovalToggle;
+import static com.android.launcher3.Flags.enableStrictMode;
import static com.android.launcher3.Flags.enableWorkspaceInflation;
import static com.android.launcher3.LauncherAnimUtils.HOTSEAT_SCALE_PROPERTY_FACTORY;
import static com.android.launcher3.LauncherAnimUtils.SCALE_INDEX_WIDGET_TRANSITION;
@@ -287,6 +288,7 @@
import java.util.Set;
import java.util.function.Predicate;
import java.util.function.Supplier;
+import java.util.stream.Collectors;
import java.util.stream.Stream;
/**
@@ -459,7 +461,8 @@
Trace.beginAsyncSection(DISPLAY_ALL_APPS_TRACE_METHOD_NAME,
DISPLAY_ALL_APPS_TRACE_COOKIE);
TraceHelper.INSTANCE.beginSection(ON_CREATE_EVT);
- if (DEBUG_STRICT_MODE) {
+ if (DEBUG_STRICT_MODE
+ || (FeatureFlags.IS_STUDIO_BUILD && enableStrictMode())) {
StrictMode.setThreadPolicy(new StrictMode.ThreadPolicy.Builder()
.detectDiskReads()
.detectDiskWrites()
@@ -2254,8 +2257,9 @@
*/
@Override
public void bindItems(final List<ItemInfo> items, final boolean forceAnimateIcons) {
- bindInflatedItems(items.stream().map(i -> Pair.create(
- i, getItemInflater().inflateItem(i, getModelWriter()))).toList(),
+ bindInflatedItems(items.stream()
+ .map(i -> Pair.create(i, getItemInflater().inflateItem(i, getModelWriter())))
+ .collect(Collectors.toList()),
forceAnimateIcons ? new AnimatorSet() : null);
}
diff --git a/src/com/android/launcher3/LauncherPrefs.kt b/src/com/android/launcher3/LauncherPrefs.kt
index 1120ec8..2a5cd63 100644
--- a/src/com/android/launcher3/LauncherPrefs.kt
+++ b/src/com/android/launcher3/LauncherPrefs.kt
@@ -21,6 +21,7 @@
import androidx.annotation.VisibleForTesting
import com.android.launcher3.BuildConfig.WIDGET_ON_FIRST_SCREEN
import com.android.launcher3.InvariantDeviceProfile.GRID_NAME_PREFS_KEY
+import com.android.launcher3.InvariantDeviceProfile.NON_FIXED_LANDSCAPE_GRID_NAME_PREFS_KEY
import com.android.launcher3.LauncherFiles.DEVICE_PREFERENCES_KEY
import com.android.launcher3.LauncherFiles.SHARED_PREFERENCES_KEY
import com.android.launcher3.dagger.ApplicationContext
@@ -304,6 +305,16 @@
@JvmField
val FIXED_LANDSCAPE_MODE = backedUpItem(SettingsActivity.FIXED_LANDSCAPE_MODE, false)
+ @JvmField
+ val NON_FIXED_LANDSCAPE_GRID_NAME =
+ ConstantItem(
+ NON_FIXED_LANDSCAPE_GRID_NAME_PREFS_KEY,
+ isBackedUp = true,
+ defaultValue = null,
+ encryptionType = EncryptionType.ENCRYPTED,
+ type = String::class.java,
+ )
+
// Preferences for widget configurations
@JvmField
val RECONFIGURABLE_WIDGET_EDUCATION_TIP_SEEN =
diff --git a/src/com/android/launcher3/Utilities.java b/src/com/android/launcher3/Utilities.java
index d93c07f..cb3a0bc 100644
--- a/src/com/android/launcher3/Utilities.java
+++ b/src/com/android/launcher3/Utilities.java
@@ -658,9 +658,9 @@
appState.getInvariantDeviceProfile().fillResIconDpi);
// Only fetch badge if the icon is on workspace
if (info.id != ItemInfo.NO_ID && badge == null) {
- badge = appState.getIconCache().getShortcutInfoBadge(si)
- .newIcon(context, ThemeManager.INSTANCE.get(context)
- .isMonoThemeEnabled() ? FLAG_THEMED : 0);
+ badge = appState.getIconCache().getShortcutInfoBadge(si).newIcon(
+ context, ThemeManager.INSTANCE.get(context).isIconThemeEnabled()
+ ? FLAG_THEMED : 0);
}
}
} else if (info.itemType == LauncherSettings.Favorites.ITEM_TYPE_FOLDER) {
diff --git a/src/com/android/launcher3/allapps/ActivityAllAppsContainerView.java b/src/com/android/launcher3/allapps/ActivityAllAppsContainerView.java
index b0001af..260ff9f 100644
--- a/src/com/android/launcher3/allapps/ActivityAllAppsContainerView.java
+++ b/src/com/android/launcher3/allapps/ActivityAllAppsContainerView.java
@@ -273,12 +273,7 @@
mFastScroller = findViewById(R.id.fast_scroller);
mFastScroller.setPopupView(findViewById(R.id.fast_scroller_popup));
mFastScrollLetterLayout = findViewById(R.id.scroll_letter_layout);
- if (Flags.letterFastScroller()) {
- // Set clip children to false otherwise the scroller letters will be clipped.
- setClipChildren(false);
- } else {
- setClipChildren(true);
- }
+ setClipChildren(false);
mSearchContainer = inflateSearchBar();
if (!isSearchBarFloating()) {
diff --git a/src/com/android/launcher3/celllayout/ReorderAlgorithm.java b/src/com/android/launcher3/celllayout/ReorderAlgorithm.java
index c303783..043c3be 100644
--- a/src/com/android/launcher3/celllayout/ReorderAlgorithm.java
+++ b/src/com/android/launcher3/celllayout/ReorderAlgorithm.java
@@ -26,6 +26,7 @@
import java.util.Comparator;
import java.util.List;
import java.util.Map.Entry;
+import java.util.stream.Collectors;
/**
* Contains the logic of a reorder.
@@ -143,12 +144,14 @@
// and not by the views hash which is "random".
// The views are sorted twice, once for the X position and a second time for the Y position
// to ensure same order everytime.
- Comparator comparator = Comparator.comparing(
- view -> ((CellLayoutLayoutParams) ((View) view).getLayoutParams()).getCellX()
+ Comparator<View> comparator = Comparator.comparing(
+ (View view) -> ((CellLayoutLayoutParams) view.getLayoutParams()).getCellX()
).thenComparing(
- view -> ((CellLayoutLayoutParams) ((View) view).getLayoutParams()).getCellY()
+ (View view) -> ((CellLayoutLayoutParams) view.getLayoutParams()).getCellY()
);
- List<View> views = solution.map.keySet().stream().sorted(comparator).toList();
+ List<View> views = solution.map.keySet().stream()
+ .sorted(comparator)
+ .collect(Collectors.toList());
for (View child : views) {
if (child == ignoreView) continue;
CellAndSpan c = solution.map.get(child);
diff --git a/src/com/android/launcher3/graphics/GridCustomizationsProvider.java b/src/com/android/launcher3/graphics/GridCustomizationsProvider.java
index 80743af..12c65c7 100644
--- a/src/com/android/launcher3/graphics/GridCustomizationsProvider.java
+++ b/src/com/android/launcher3/graphics/GridCustomizationsProvider.java
@@ -55,6 +55,8 @@
import java.lang.ref.WeakReference;
import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
@@ -124,6 +126,8 @@
private static final int MESSAGE_ID_UPDATE_GRID = 7414;
private static final int MESSAGE_ID_UPDATE_COLOR = 856;
+ private static final String DEFAULT_SHAPE_KEY = "circle";
+
// Set of all active previews used to track duplicate memory allocations
private final Set<PreviewLifecycleObserver> mActivePreviews =
Collections.newSetFromMap(new ConcurrentHashMap<>());
@@ -157,7 +161,7 @@
// Handle default for when current shape doesn't match new shapes.
if (selectedShape.isEmpty()) {
selectedShape = Optional.ofNullable(ShapesProvider.INSTANCE.getIconShapes()
- .get("circle"));
+ .get(DEFAULT_SHAPE_KEY));
}
for (IconShapeModel shape : ShapesProvider.INSTANCE.getIconShapes().values()) {
@@ -177,7 +181,13 @@
KEY_NAME, KEY_GRID_TITLE, KEY_ROWS, KEY_COLS, KEY_PREVIEW_COUNT,
KEY_IS_DEFAULT, KEY_GRID_ICON_ID});
InvariantDeviceProfile idp = InvariantDeviceProfile.INSTANCE.get(getContext());
- for (GridOption gridOption : idp.parseAllGridOptions(getContext())) {
+ List<GridOption> gridOptionList = idp.parseAllGridOptions(getContext());
+ if (com.android.launcher3.Flags.oneGridSpecs()) {
+ gridOptionList.sort(Comparator
+ .comparingInt((GridOption option) -> option.numColumns)
+ .reversed());
+ }
+ for (GridOption gridOption : gridOptionList) {
cursor.newRow()
.add(KEY_NAME, gridOption.name)
.add(KEY_GRID_TITLE, gridOption.gridTitle)
diff --git a/src/com/android/launcher3/graphics/ThemeManager.kt b/src/com/android/launcher3/graphics/ThemeManager.kt
index 9f35e4a..242220a 100644
--- a/src/com/android/launcher3/graphics/ThemeManager.kt
+++ b/src/com/android/launcher3/graphics/ThemeManager.kt
@@ -40,8 +40,8 @@
open class ThemeManager
@Inject
constructor(
- @ApplicationContext private val context: Context,
- private val prefs: LauncherPrefs,
+ @ApplicationContext protected val context: Context,
+ protected val prefs: LauncherPrefs,
lifecycle: DaggerSingletonTracker,
) {
@@ -53,9 +53,11 @@
set(value) = prefs.put(THEMED_ICONS, value)
get() = prefs.get(THEMED_ICONS)
- var themeController: IconThemeController? =
- if (isMonoThemeEnabled) MonoIconThemeController() else null
- private set
+ val themeController: IconThemeController?
+ get() = iconState.themeController
+
+ val isIconThemeEnabled: Boolean
+ get() = themeController != null
private val listeners = CopyOnWriteArrayList<ThemeChangeListener>()
@@ -77,12 +79,10 @@
}
}
- private fun verifyIconState() {
+ protected fun verifyIconState() {
val newState = parseIconState()
if (newState == iconState) return
-
iconState = newState
- themeController = if (isMonoThemeEnabled) MonoIconThemeController() else null
listeners.forEach { it.onThemeChanged() }
}
@@ -105,15 +105,19 @@
return IconState(
iconMask = iconMask,
folderShapeMask = shapeModel?.folderPathString ?: iconMask,
- isMonoTheme = isMonoThemeEnabled,
+ themeController = createThemeController(),
)
}
+ protected open fun createThemeController(): IconThemeController? {
+ return if (isMonoThemeEnabled) MONO_THEME_CONTROLLER else null
+ }
+
data class IconState(
val iconMask: String,
val folderShapeMask: String,
- val isMonoTheme: Boolean,
- val themeCode: String = if (isMonoTheme) "with-theme" else "no-theme",
+ val themeController: IconThemeController?,
+ val themeCode: String = themeController?.themeID ?: "no-theme",
) {
fun toUniqueId() = "${iconMask.hashCode()},$themeCode"
}
@@ -135,5 +139,8 @@
private const val ACTION_OVERLAY_CHANGED = "android.intent.action.OVERLAY_CHANGED"
private val CONFIG_ICON_MASK_RES_ID: Int =
Resources.getSystem().getIdentifier("config_icon_mask", "string", "android")
+
+ // Use a constant to allow equality check in verifyIconState
+ private val MONO_THEME_CONTROLLER = MonoIconThemeController()
}
}
diff --git a/src/com/android/launcher3/icons/LauncherIcons.java b/src/com/android/launcher3/icons/LauncherIcons.java
index 59fff62..5c6debe 100644
--- a/src/com/android/launcher3/icons/LauncherIcons.java
+++ b/src/com/android/launcher3/icons/LauncherIcons.java
@@ -16,7 +16,12 @@
package com.android.launcher3.icons;
+import static android.graphics.Color.BLACK;
+
+import static com.android.launcher3.graphics.ThemeManager.PREF_ICON_SHAPE;
+
import android.content.Context;
+import android.graphics.Canvas;
import android.graphics.Path;
import android.graphics.Rect;
import android.graphics.drawable.AdaptiveIconDrawable;
@@ -26,6 +31,7 @@
import com.android.launcher3.Flags;
import com.android.launcher3.InvariantDeviceProfile;
+import com.android.launcher3.LauncherPrefs;
import com.android.launcher3.graphics.IconShape;
import com.android.launcher3.graphics.ThemeManager;
import com.android.launcher3.pm.UserCache;
@@ -41,6 +47,12 @@
*/
public class LauncherIcons extends BaseIconFactory implements AutoCloseable {
+ private static final float SEVEN_SIDED_COOKIE_SCALE = 72f / 80f;
+ private static final float FOUR_SIDED_COOKIE_SCALE = 72f / 83.4f;
+ private static final float VERY_SUNNY_SCALE = 72f / 92f;
+ private static final float DEFAULT_ICON_SCALE = 1f;
+
+
private static final MainThreadInitializedObject<Pool> POOL =
new MainThreadInitializedObject<>(Pool::new);
@@ -87,6 +99,36 @@
}
@Override
+ protected void drawAdaptiveIcon(
+ @NonNull Canvas canvas,
+ @NonNull AdaptiveIconDrawable drawable,
+ @NonNull Path overridePath
+ ) {
+ if (!Flags.enableLauncherIconShapes()) {
+ super.drawAdaptiveIcon(canvas, drawable, overridePath);
+ return;
+ }
+ String shapeKey = LauncherPrefs.get(mContext).get(PREF_ICON_SHAPE);
+ float iconScale = switch (shapeKey) {
+ case "seven_sided_cookie" -> SEVEN_SIDED_COOKIE_SCALE;
+ case "four_sided_cookie" -> FOUR_SIDED_COOKIE_SCALE;
+ case "sunny" -> VERY_SUNNY_SCALE;
+ default -> DEFAULT_ICON_SCALE;
+ };
+ canvas.clipPath(overridePath);
+ canvas.drawColor(BLACK);
+ canvas.save();
+ canvas.scale(iconScale, iconScale, canvas.getWidth() / 2f, canvas.getHeight() / 2f);
+ if (drawable.getBackground() != null) {
+ drawable.getBackground().draw(canvas);
+ }
+ if (drawable.getForeground() != null) {
+ drawable.getForeground().draw(canvas);
+ }
+ canvas.restore();
+ }
+
+ @Override
public void close() {
recycle();
}
diff --git a/src/com/android/launcher3/model/BaseLauncherBinder.java b/src/com/android/launcher3/model/BaseLauncherBinder.java
index de74ae8..003bef3 100644
--- a/src/com/android/launcher3/model/BaseLauncherBinder.java
+++ b/src/com/android/launcher3/model/BaseLauncherBinder.java
@@ -390,8 +390,9 @@
ModelWriter writer = mApp.getModel()
.getWriter(false /* verifyChanges */, CellPosMapper.DEFAULT, null);
- List<Pair<ItemInfo, View>> bindItems = items.stream().map(i ->
- Pair.create(i, inflater.inflateItem(i, writer, null))).toList();
+ List<Pair<ItemInfo, View>> bindItems = items.stream()
+ .map(i -> Pair.create(i, inflater.inflateItem(i, writer, null)))
+ .collect(Collectors.toList());
executeCallbacksTask(c -> c.bindInflatedItems(bindItems), executor);
}
diff --git a/src/com/android/launcher3/model/BgDataModel.java b/src/com/android/launcher3/model/BgDataModel.java
index ddc775d..eab28b7 100644
--- a/src/com/android/launcher3/model/BgDataModel.java
+++ b/src/com/android/launcher3/model/BgDataModel.java
@@ -46,6 +46,7 @@
import com.android.launcher3.BuildConfig;
import com.android.launcher3.Workspace;
import com.android.launcher3.config.FeatureFlags;
+import com.android.launcher3.logging.FileLog;
import com.android.launcher3.model.data.AppInfo;
import com.android.launcher3.model.data.CollectionInfo;
import com.android.launcher3.model.data.ItemInfo;
@@ -210,7 +211,7 @@
}
/**
- * Updates the deep shortucts state in system to match out internal model, pinning any missing
+ * Updates the deep shortcuts state in system to match out internal model, pinning any missing
* shortcuts and unpinning any extra shortcuts.
*/
public void updateShortcutPinnedState(Context context) {
@@ -266,6 +267,8 @@
|| !systemShortcuts.containsAll(modelShortcuts)) {
// Update system state for this package
try {
+ FileLog.d(TAG, "updateShortcutPinnedState:"
+ + " Pinning Shortcuts: " + entry.getKey() + ": " + modelShortcuts);
context.getSystemService(LauncherApps.class).pinShortcuts(
entry.getKey(), new ArrayList<>(modelShortcuts), user);
} catch (SecurityException | IllegalStateException e) {
@@ -278,6 +281,9 @@
systemMap.keySet().forEach(packageName -> {
// Update system state
try {
+ FileLog.d(TAG, "updateShortcutPinnedState:"
+ + " Unpinning extra Shortcuts for package: " + packageName
+ + ": " + systemMap.get(packageName));
context.getSystemService(LauncherApps.class).pinShortcuts(
packageName, Collections.emptyList(), user);
} catch (SecurityException | IllegalStateException e) {
diff --git a/src/com/android/launcher3/model/GridSizeMigrationDBController.java b/src/com/android/launcher3/model/GridSizeMigrationDBController.java
index b291421..47f13bd 100644
--- a/src/com/android/launcher3/model/GridSizeMigrationDBController.java
+++ b/src/com/android/launcher3/model/GridSizeMigrationDBController.java
@@ -193,7 +193,8 @@
List<DbEntry> filteredDstHotseatItems = dstHotseatItems;
if (srcHotseatSize < destHotseatSize) {
filteredDstHotseatItems = filteredDstHotseatItems.stream()
- .filter(entry -> entry.screenId < srcHotseatSize).toList();
+ .filter(entry -> entry.screenId < srcHotseatSize)
+ .collect(Collectors.toList());
}
final List<DbEntry> dstWorkspaceItems = destReader.loadAllWorkspaceEntries();
final List<DbEntry> hotseatToBeAdded = new ArrayList<>(1);
@@ -237,9 +238,12 @@
Collections.sort(hotseatToBeAdded);
Collections.sort(workspaceToBeAdded);
- List<Integer> idsInUse = dstWorkspaceItems.stream().map(entry -> entry.id).collect(
- Collectors.toList());
- idsInUse.addAll(dstHotseatItems.stream().map(entry -> entry.id).toList());
+ List<Integer> idsInUse = dstWorkspaceItems.stream()
+ .map(entry -> entry.id)
+ .collect(Collectors.toList());
+ idsInUse.addAll(dstHotseatItems.stream()
+ .map(entry -> entry.id)
+ .collect(Collectors.toList()));
// Migrate hotseat
solveHotseatPlacement(helper, destHotseatSize,
@@ -269,7 +273,8 @@
int screenId = destReader.mLastScreenId + 1;
while (!workspaceToBeAdded.isEmpty()) {
solveGridPlacement(helper, srcReader, destReader, screenId, trgX, trgY,
- workspaceToBeAdded, srcWorkspaceItems.stream().map(entry -> entry.id).toList());
+ workspaceToBeAdded,
+ srcWorkspaceItems.stream().map(entry -> entry.id).collect(Collectors.toList()));
screenId++;
}
diff --git a/src/com/android/launcher3/model/LoaderCursor.java b/src/com/android/launcher3/model/LoaderCursor.java
index 6a8d86b..bd8c36b 100644
--- a/src/com/android/launcher3/model/LoaderCursor.java
+++ b/src/com/android/launcher3/model/LoaderCursor.java
@@ -24,6 +24,7 @@
import static com.android.launcher3.LauncherSettings.Favorites.TABLE_NAME;
import static com.android.launcher3.Utilities.SHOULD_SHOW_FIRST_PAGE_WIDGET;
import static com.android.launcher3.icons.cache.CacheLookupFlag.DEFAULT_LOOKUP_FLAG;
+import static com.android.launcher3.model.data.ItemInfoWithIcon.FLAG_ARCHIVED;
import android.content.ComponentName;
import android.content.ContentValues;
@@ -307,7 +308,7 @@
* Make an WorkspaceItemInfo object for a restored application or shortcut item that points
* to a package that is not yet installed on the system.
*/
- public WorkspaceItemInfo getRestoredItemInfo(Intent intent) {
+ public WorkspaceItemInfo getRestoredItemInfo(Intent intent, boolean isArchived) {
final WorkspaceItemInfo info = new WorkspaceItemInfo();
info.user = user;
info.intent = intent;
@@ -317,7 +318,7 @@
mIconCache.getTitleAndIcon(info, DEFAULT_LOOKUP_FLAG);
}
- if (hasRestoreFlag(WorkspaceItemInfo.FLAG_RESTORED_ICON)) {
+ if (hasRestoreFlag(WorkspaceItemInfo.FLAG_RESTORED_ICON) || isArchived) {
String title = getTitle();
if (!TextUtils.isEmpty(title)) {
info.title = Utilities.trim(title);
@@ -333,6 +334,7 @@
info.contentDescription = mIconCache.getUserBadgedLabel(info.title, info.user);
info.itemType = itemType;
info.status = restoreFlag;
+ if (isArchived) info.runtimeStatusFlags |= FLAG_ARCHIVED;
return info;
}
diff --git a/src/com/android/launcher3/model/ModelDbController.java b/src/com/android/launcher3/model/ModelDbController.java
index 0138390..3a55aa7 100644
--- a/src/com/android/launcher3/model/ModelDbController.java
+++ b/src/com/android/launcher3/model/ModelDbController.java
@@ -90,6 +90,7 @@
import java.io.InputStream;
import java.io.StringReader;
import java.util.List;
+import java.util.stream.Collectors;
/**
* Utility class which maintains an instance of Launcher database and provides utility methods
@@ -377,7 +378,7 @@
// to run in grid migration based on if that grid already existed before migration or not.
List<String> existingDBs = LauncherFiles.GRID_DB_FILES.stream()
.filter(dbName -> mContext.getDatabasePath(dbName).exists())
- .toList();
+ .collect(Collectors.toList());
mOpenHelper = (mContext instanceof SandboxContext) ? oldHelper
: createDatabaseHelper(true, new DeviceGridState(idp).getDbFile());
@@ -460,7 +461,7 @@
// to run in grid migration based on if that grid already existed before migration or not.
List<String> existingDBs = LauncherFiles.GRID_DB_FILES.stream()
.filter(dbName -> mContext.getDatabasePath(dbName).exists())
- .toList();
+ .collect(Collectors.toList());
mOpenHelper = (mContext instanceof SandboxContext) ? oldHelper
: createDatabaseHelper(true /* forMigration */, targetDbName);
try {
diff --git a/src/com/android/launcher3/model/PackageUpdatedTask.java b/src/com/android/launcher3/model/PackageUpdatedTask.java
index 6bef292..d1eceb9 100644
--- a/src/com/android/launcher3/model/PackageUpdatedTask.java
+++ b/src/com/android/launcher3/model/PackageUpdatedTask.java
@@ -15,6 +15,7 @@
*/
package com.android.launcher3.model;
+import static com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT;
import static com.android.launcher3.model.BgDataModel.Callbacks.FLAG_PRIVATE_PROFILE_QUIET_MODE_ENABLED;
import static com.android.launcher3.model.BgDataModel.Callbacks.FLAG_QUIET_MODE_ENABLED;
import static com.android.launcher3.model.BgDataModel.Callbacks.FLAG_WORK_PROFILE_QUIET_MODE_ENABLED;
@@ -39,7 +40,6 @@
import com.android.launcher3.Flags;
import com.android.launcher3.LauncherAppState;
import com.android.launcher3.LauncherModel.ModelUpdateTask;
-import com.android.launcher3.LauncherSettings;
import com.android.launcher3.LauncherSettings.Favorites;
import com.android.launcher3.config.FeatureFlags;
import com.android.launcher3.icons.IconCache;
@@ -238,19 +238,22 @@
if (itemInfo.isPromise() && isNewApkAvailable) {
boolean isTargetValid = !cn.getClassName().equals(
IconCache.EMPTY_CLASS_NAME);
- if (itemInfo.itemType == Favorites.ITEM_TYPE_DEEP_SHORTCUT) {
+ if (itemInfo.itemType == ITEM_TYPE_DEEP_SHORTCUT) {
List<ShortcutInfo> shortcut =
new ShortcutRequest(context, mUser)
.forPackage(cn.getPackageName(),
itemInfo.getDeepShortcutId())
.query(ShortcutRequest.PINNED);
- if (shortcut.isEmpty()) {
+ if (shortcut.isEmpty()
+ && !(Flags.restoreArchivedShortcuts()
+ && !itemInfo.isArchived())
+ ) {
isTargetValid = false;
if (DEBUG) {
Log.d(TAG, "Pinned Shortcut not found for updated"
+ " package=" + itemInfo.getTargetPackage());
}
- } else {
+ } else if (!shortcut.isEmpty()) {
if (DEBUG) {
Log.d(TAG, "Found pinned shortcut for updated"
+ " package=" + itemInfo.getTargetPackage()
@@ -269,7 +272,7 @@
|| itemInfo.isArchived())) {
if (updateWorkspaceItemIntent(context, itemInfo, packageName)) {
infoUpdated = true;
- } else if (itemInfo.hasPromiseIconUi()) {
+ } else if (shouldRemoveRestoredShortcut(itemInfo)) {
removedShortcuts.add(itemInfo.id);
if (DEBUG) {
FileLog.w(TAG, "Removing restored shortcut promise icon"
@@ -436,7 +439,7 @@
*/
private boolean updateWorkspaceItemIntent(Context context,
WorkspaceItemInfo si, String packageName) {
- if (si.itemType == LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT) {
+ if (si.itemType == ITEM_TYPE_DEEP_SHORTCUT) {
// Do not update intent for deep shortcuts as they contain additional information
// about the shortcut.
return false;
@@ -452,6 +455,15 @@
return false;
}
+ private boolean shouldRemoveRestoredShortcut(WorkspaceItemInfo itemInfo) {
+ if (itemInfo.hasPromiseIconUi() && !Flags.restoreArchivedShortcuts()) {
+ return true;
+ }
+ return Flags.restoreArchivedShortcuts()
+ && !itemInfo.isArchived()
+ && itemInfo.itemType == ITEM_TYPE_DEEP_SHORTCUT;
+ }
+
private String getOpString() {
return switch (mOp) {
case OP_NONE -> "NONE";
diff --git a/src/com/android/launcher3/model/ShortcutsChangedTask.kt b/src/com/android/launcher3/model/ShortcutsChangedTask.kt
index 2e4f75f..56e9e43 100644
--- a/src/com/android/launcher3/model/ShortcutsChangedTask.kt
+++ b/src/com/android/launcher3/model/ShortcutsChangedTask.kt
@@ -17,6 +17,7 @@
import android.content.pm.ShortcutInfo
import android.os.UserHandle
+import com.android.launcher3.Flags
import com.android.launcher3.LauncherModel.ModelUpdateTask
import com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT
import com.android.launcher3.icons.CacheableShortcutInfo
@@ -59,8 +60,11 @@
val infoWrapper = ApplicationInfoWrapper(context, packageName, user)
if (shortcuts.isEmpty()) {
// Verify that the app is indeed installed.
- if (!infoWrapper.isInstalled() && !infoWrapper.isArchived()) {
- // App is not installed or archived, ignoring package events
+ if (
+ (!infoWrapper.isInstalled() && !infoWrapper.isArchived()) ||
+ (Flags.restoreArchivedShortcuts() && infoWrapper.isArchived())
+ ) {
+ // App is not installed or is archived, ignoring package events
return
}
}
@@ -75,7 +79,7 @@
val nonPinnedIds: MutableSet<String> = HashSet(allLauncherKnownIds)
val updatedWorkspaceItemInfos = ArrayList<WorkspaceItemInfo>()
for (fullDetails in shortcuts) {
- if (!fullDetails.isPinned) {
+ if (!fullDetails.isPinned && !Flags.restoreArchivedShortcuts()) {
continue
}
val shortcutId = fullDetails.id
diff --git a/src/com/android/launcher3/model/WorkspaceItemProcessor.kt b/src/com/android/launcher3/model/WorkspaceItemProcessor.kt
index 90f11a3..3919eb7 100644
--- a/src/com/android/launcher3/model/WorkspaceItemProcessor.kt
+++ b/src/com/android/launcher3/model/WorkspaceItemProcessor.kt
@@ -194,27 +194,36 @@
if (intent.`package` == null) {
intent.`package` = targetPkg
}
+ val isPreArchived = appInfoWrapper.isArchived() && c.restoreFlag != 0
+
// else if cn == null => can't infer much, leave it
// else if !validPkg => could be restored icon or missing sd-card
when {
- !TextUtils.isEmpty(targetPkg) && !validTarget -> {
+ !TextUtils.isEmpty(targetPkg) && (!validTarget || isPreArchived) -> {
// Points to a valid app (superset of cn != null) but the apk
// is not available.
when {
- c.restoreFlag != 0 -> {
+ c.restoreFlag != 0 || isPreArchived -> {
// Package is not yet available but might be
// installed later.
- FileLog.d(TAG, "package not yet restored: $targetPkg")
+ FileLog.d(
+ TAG,
+ "package not yet restored: $targetPkg, itemType=${c.itemType}" +
+ "isPreArchived=$isPreArchived, restoreFlag=${c.restoreFlag}",
+ )
tempPackageKey.update(targetPkg, c.user)
when {
c.hasRestoreFlag(WorkspaceItemInfo.FLAG_RESTORE_STARTED) -> {
// Restore has started once.
}
- installingPkgs.containsKey(tempPackageKey) -> {
+ installingPkgs.containsKey(tempPackageKey) || isPreArchived -> {
// App restore has started. Update the flag
c.restoreFlag =
c.restoreFlag or WorkspaceItemInfo.FLAG_RESTORE_STARTED
- FileLog.d(TAG, "restore started for installing app: $targetPkg")
+ FileLog.d(
+ TAG,
+ "restore started for installing app: $targetPkg, itemType=${c.itemType}",
+ )
c.updater().put(Favorites.RESTORED, c.restoreFlag).commit()
}
else -> {
@@ -253,9 +262,18 @@
}
}
if (c.restoreFlag and WorkspaceItemInfo.FLAG_SUPPORTS_WEB_UI != 0) {
+ FileLog.d(
+ TAG,
+ "restore flag set AND WorkspaceItemInfo.FLAG_SUPPORTS_WEB_UI != 0, setting valid target to false: $targetPkg, itemType=${c.itemType}, restoreFlag=${c.restoreFlag}",
+ )
validTarget = false
}
- if (validTarget) {
+ if (validTarget && !isPreArchived) {
+ FileLog.d(
+ TAG,
+ "valid target true, marking restored: $targetPkg," +
+ " itemType=${c.itemType}, restoreFlag=${c.restoreFlag}",
+ )
// The shortcut points to a valid target (either no target
// or something which is ready to be used)
c.markRestored()
@@ -265,7 +283,7 @@
when {
c.restoreFlag != 0 -> {
// Already verified above that user is same as default user
- info = c.getRestoredItemInfo(intent)
+ info = c.getRestoredItemInfo(intent, isPreArchived)
}
c.itemType == Favorites.ITEM_TYPE_APPLICATION ->
info = c.getAppShortcutInfo(intent, allowMissingTarget, useLowResIcon, false)
diff --git a/src/com/android/launcher3/model/data/AppPairInfo.kt b/src/com/android/launcher3/model/data/AppPairInfo.kt
index e620ac9..c0fe4fd 100644
--- a/src/com/android/launcher3/model/data/AppPairInfo.kt
+++ b/src/com/android/launcher3/model/data/AppPairInfo.kt
@@ -23,6 +23,7 @@
import com.android.launcher3.icons.IconCache
import com.android.launcher3.logger.LauncherAtom
import com.android.launcher3.views.ActivityContext
+import java.util.stream.Collectors
/** A type of app collection that launches multiple apps into split screen. */
class AppPairInfo() : CollectionInfo() {
@@ -54,7 +55,7 @@
/** Returns the app pair's member apps as an ArrayList of [ItemInfo]. */
override fun getContents(): ArrayList<ItemInfo> =
- ArrayList(contents.stream().map { it as ItemInfo }.toList())
+ ArrayList(contents.stream().map { it as ItemInfo }.collect(Collectors.toList()))
/** Returns the app pair's member apps as an ArrayList of [WorkspaceItemInfo]. */
override fun getAppContents(): ArrayList<WorkspaceItemInfo> = contents
diff --git a/src/com/android/launcher3/model/data/ItemInfoWithIcon.java b/src/com/android/launcher3/model/data/ItemInfoWithIcon.java
index 7fb0152..ff40f30 100644
--- a/src/com/android/launcher3/model/data/ItemInfoWithIcon.java
+++ b/src/com/android/launcher3/model/data/ItemInfoWithIcon.java
@@ -323,7 +323,7 @@
* Returns a FastBitmapDrawable with the icon and context theme applied
*/
public FastBitmapDrawable newIcon(Context context, @DrawableCreationFlags int creationFlags) {
- if (!ThemeManager.INSTANCE.get(context).isMonoThemeEnabled()) {
+ if (!ThemeManager.INSTANCE.get(context).isIconThemeEnabled()) {
creationFlags &= ~FLAG_THEMED;
}
FastBitmapDrawable drawable = bitmap.newIcon(context, creationFlags);
diff --git a/src/com/android/launcher3/provider/RestoreDbTask.java b/src/com/android/launcher3/provider/RestoreDbTask.java
index f56888b..dc42920 100644
--- a/src/com/android/launcher3/provider/RestoreDbTask.java
+++ b/src/com/android/launcher3/provider/RestoreDbTask.java
@@ -176,7 +176,7 @@
// At this point idp.dbFile contains the name of the dbFile from the previous phone
return LauncherFiles.GRID_DB_FILES.stream()
.filter(dbName -> context.getDatabasePath(dbName).exists())
- .toList();
+ .collect(Collectors.toList());
}
/**
diff --git a/src/com/android/launcher3/shapes/ShapesProvider.kt b/src/com/android/launcher3/shapes/ShapesProvider.kt
index f1ea3a0..7e1f640 100644
--- a/src/com/android/launcher3/shapes/ShapesProvider.kt
+++ b/src/com/android/launcher3/shapes/ShapesProvider.kt
@@ -152,13 +152,20 @@
val iconShapes =
if (Flags.newCustomizationPickerUi() && LauncherFlags.enableLauncherIconShapes()) {
mapOf(
- "arch" to
+ "circle" to
IconShapeModel(
- key = "arch",
- title = "arch",
+ key = "circle",
+ title = "circle",
+ pathString = "M50 0A50 50,0,1,1,50 100A50 50,0,1,1,50 0",
+ folderPathString = folderShapes["clover"]!!,
+ ),
+ "square" to
+ IconShapeModel(
+ key = "square",
+ title = "square",
pathString =
- "M50 0C77.614 0 100 22.386 100 50C100 85.471 100 86.476 99.9 87.321 99.116 93.916 93.916 99.116 87.321 99.9 86.476 100 85.471 100 83.46 100H16.54C14.529 100 13.524 100 12.679 99.9 6.084 99.116 .884 93.916 .1 87.321 0 86.476 0 85.471 0 83.46L0 50C0 22.386 22.386 0 50 0Z",
- folderPathString = folderShapes["arch"]!!,
+ "M53.689 0.82 L53.689 .82 C67.434 .82 74.306 .82 79.758 2.978 87.649 6.103 93.897 12.351 97.022 20.242 99.18 25.694 99.18 32.566 99.18 46.311 V53.689 C99.18 67.434 99.18 74.306 97.022 79.758 93.897 87.649 87.649 93.897 79.758 97.022 74.306 99.18 67.434 99.18 53.689 99.18 H46.311 C32.566 99.18 25.694 99.18 20.242 97.022 12.351 93.897 6.103 87.649 2.978 79.758 .82 74.306 .82 67.434 .82 53.689 L.82 46.311 C.82 32.566 .82 25.694 2.978 20.242 6.103 12.351 12.351 6.103 20.242 2.978 25.694 .82 32.566 .82 46.311 .82Z",
+ folderShapes["square"]!!,
),
"four_sided_cookie" to
IconShapeModel(
@@ -176,6 +183,14 @@
"M35.209 4.878C36.326 3.895 36.884 3.404 37.397 3.006 44.82 -2.742 55.18 -2.742 62.603 3.006 63.116 3.404 63.674 3.895 64.791 4.878 65.164 5.207 65.351 5.371 65.539 5.529 68.167 7.734 71.303 9.248 74.663 9.932 74.902 9.981 75.147 10.025 75.637 10.113 77.1 10.375 77.831 10.506 78.461 10.66 87.573 12.893 94.032 21.011 94.176 30.412 94.186 31.062 94.151 31.805 94.08 33.293 94.057 33.791 94.045 34.04 94.039 34.285 93.958 37.72 94.732 41.121 96.293 44.18 96.404 44.399 96.522 44.618 96.759 45.056 97.467 46.366 97.821 47.021 98.093 47.611 102.032 56.143 99.727 66.266 92.484 72.24 91.983 72.653 91.381 73.089 90.177 73.961 89.774 74.254 89.572 74.4 89.377 74.548 86.647 76.626 84.477 79.353 83.063 82.483 82.962 82.707 82.865 82.936 82.671 83.395 82.091 84.766 81.8 85.451 81.51 86.033 77.31 94.44 67.977 98.945 58.801 96.994 58.166 96.859 57.451 96.659 56.019 96.259 55.54 96.125 55.3 96.058 55.063 95.998 51.74 95.154 48.26 95.154 44.937 95.998 44.699 96.058 44.46 96.125 43.981 96.259 42.549 96.659 41.834 96.859 41.199 96.994 32.023 98.945 22.69 94.44 18.49 86.033 18.2 85.451 17.909 84.766 17.329 83.395 17.135 82.936 17.038 82.707 16.937 82.483 15.523 79.353 13.353 76.626 10.623 74.548 10.428 74.4 10.226 74.254 9.823 73.961 8.619 73.089 8.017 72.653 7.516 72.24 .273 66.266 -2.032 56.143 1.907 47.611 2.179 47.021 2.533 46.366 3.241 45.056 3.478 44.618 3.596 44.399 3.707 44.18 5.268 41.121 6.042 37.72 5.961 34.285 5.955 34.04 5.943 33.791 5.92 33.293 5.849 31.805 5.814 31.062 5.824 30.412 5.968 21.011 12.427 12.893 21.539 10.66 22.169 10.506 22.9 10.375 24.363 10.113 24.853 10.025 25.098 9.981 25.337 9.932 28.697 9.248 31.833 7.734 34.461 5.529 34.649 5.371 34.836 5.207 35.209 4.878Z",
folderPathString = folderShapes["clover"]!!,
),
+ "arch" to
+ IconShapeModel(
+ key = "arch",
+ title = "arch",
+ pathString =
+ "M50 0C77.614 0 100 22.386 100 50C100 85.471 100 86.476 99.9 87.321 99.116 93.916 93.916 99.116 87.321 99.9 86.476 100 85.471 100 83.46 100H16.54C14.529 100 13.524 100 12.679 99.9 6.084 99.116 .884 93.916 .1 87.321 0 86.476 0 85.471 0 83.46L0 50C0 22.386 22.386 0 50 0Z",
+ folderPathString = folderShapes["arch"]!!,
+ ),
"sunny" to
IconShapeModel(
key = "sunny",
@@ -184,21 +199,6 @@
"M42.846 4.873C46.084 -.531 53.916 -.531 57.154 4.873L60.796 10.951C62.685 14.103 66.414 15.647 69.978 14.754L76.851 13.032C82.962 11.5 88.5 17.038 86.968 23.149L85.246 30.022C84.353 33.586 85.897 37.315 89.049 39.204L95.127 42.846C100.531 46.084 100.531 53.916 95.127 57.154L89.049 60.796C85.897 62.685 84.353 66.414 85.246 69.978L86.968 76.851C88.5 82.962 82.962 88.5 76.851 86.968L69.978 85.246C66.414 84.353 62.685 85.898 60.796 89.049L57.154 95.127C53.916 100.531 46.084 100.531 42.846 95.127L39.204 89.049C37.315 85.898 33.586 84.353 30.022 85.246L23.149 86.968C17.038 88.5 11.5 82.962 13.032 76.851L14.754 69.978C15.647 66.414 14.103 62.685 10.951 60.796L4.873 57.154C -.531 53.916 -.531 46.084 4.873 42.846L10.951 39.204C14.103 37.315 15.647 33.586 14.754 30.022L13.032 23.149C11.5 17.038 17.038 11.5 23.149 13.032L30.022 14.754C33.586 15.647 37.315 14.103 39.204 10.951L42.846 4.873Z",
folderPathString = folderShapes["clover"]!!,
),
- "circle" to
- IconShapeModel(
- key = "circle",
- title = "circle",
- pathString = "M50 0A50 50,0,1,1,50 100A50 50,0,1,1,50 0",
- folderPathString = folderShapes["clover"]!!,
- ),
- "square" to
- IconShapeModel(
- key = "square",
- title = "square",
- pathString =
- "M53.689 0.82 L53.689 .82 C67.434 .82 74.306 .82 79.758 2.978 87.649 6.103 93.897 12.351 97.022 20.242 99.18 25.694 99.18 32.566 99.18 46.311 V53.689 C99.18 67.434 99.18 74.306 97.022 79.758 93.897 87.649 87.649 93.897 79.758 97.022 74.306 99.18 67.434 99.18 53.689 99.18 H46.311 C32.566 99.18 25.694 99.18 20.242 97.022 12.351 93.897 6.103 87.649 2.978 79.758 .82 74.306 .82 67.434 .82 53.689 L.82 46.311 C.82 32.566 .82 25.694 2.978 20.242 6.103 12.351 12.351 6.103 20.242 2.978 25.694 .82 32.566 .82 46.311 .82Z",
- folderShapes["square"]!!,
- ),
)
} else {
mapOf(
diff --git a/src/com/android/launcher3/util/DisplayController.java b/src/com/android/launcher3/util/DisplayController.java
index 475dc04..ee1af81 100644
--- a/src/com/android/launcher3/util/DisplayController.java
+++ b/src/com/android/launcher3/util/DisplayController.java
@@ -220,6 +220,14 @@
return INSTANCE.get(context).getInfo().showLockedTaskbarOnHome();
}
+ /**
+ * Returns whether desktop taskbar (pinned taskbar that shows desktop tasks) is to be used
+ * on the display because the display is a freeform display.
+ */
+ public static boolean showDesktopTaskbarForFreeformDisplay(Context context) {
+ return INSTANCE.get(context).getInfo().showDesktopTaskbarForFreeformDisplay();
+ }
+
@Override
public void onDesktopVisibilityChanged(boolean visible) {
notifyConfigChange();
@@ -259,7 +267,9 @@
new PortraitSize(config.screenHeightDp, config.screenWidthDp))
|| mWindowContext.getDisplay().getRotation() != mInfo.rotation
|| mWMProxy.showLockedTaskbarOnHome(mWindowContext)
- != mInfo.showLockedTaskbarOnHome()) {
+ != mInfo.showLockedTaskbarOnHome()
+ || mWMProxy.showDesktopTaskbarForFreeformDisplay(mWindowContext)
+ != mInfo.showDesktopTaskbarForFreeformDisplay()) {
notifyConfigChange();
}
}
@@ -376,6 +386,8 @@
private final boolean mShowLockedTaskbarOnHome;
private final boolean mIsHomeVisible;
+ private final boolean mShowDesktopTaskbarForFreeformDisplay;
+
public Info(Context displayInfoContext) {
/* don't need system overrides for external displays */
this(displayInfoContext, new WindowManagerProxy(), new ArrayMap<>());
@@ -438,6 +450,8 @@
TASKBAR_PINNING_IN_DESKTOP_MODE);
mIsInDesktopMode = wmProxy.isInDesktopMode();
mShowLockedTaskbarOnHome = wmProxy.showLockedTaskbarOnHome(displayInfoContext);
+ mShowDesktopTaskbarForFreeformDisplay = wmProxy.showDesktopTaskbarForFreeformDisplay(
+ displayInfoContext);
mIsHomeVisible = wmProxy.isHomeVisible(displayInfoContext);
}
@@ -455,6 +469,11 @@
return sTransientTaskbarStatusForTests;
}
if (enableTaskbarPinning()) {
+ // If "freeform" display taskbar is enabled, ensure the taskbar is pinned.
+ if (mShowDesktopTaskbarForFreeformDisplay) {
+ return false;
+ }
+
// If Launcher is visible on the freeform display, ensure the taskbar is pinned.
if (mShowLockedTaskbarOnHome && mIsHomeVisible) {
return false;
@@ -533,6 +552,14 @@
public boolean showLockedTaskbarOnHome() {
return mShowLockedTaskbarOnHome;
}
+
+ /**
+ * Returns whether the taskbar should be pinned, and showing desktop tasks, because the
+ * display is a "freeform" display.
+ */
+ public boolean showDesktopTaskbarForFreeformDisplay() {
+ return mShowDesktopTaskbarForFreeformDisplay;
+ }
}
/**
diff --git a/src/com/android/launcher3/util/rects/Rects.kt b/src/com/android/launcher3/util/rects/Rects.kt
index 1e6d717..2f1942a 100644
--- a/src/com/android/launcher3/util/rects/Rects.kt
+++ b/src/com/android/launcher3/util/rects/Rects.kt
@@ -18,6 +18,24 @@
import android.graphics.Rect
import android.view.View
+import com.android.launcher3.Utilities
+
+/**
+ * Linearly interpolate between two rectangles. The result is stored in the rect the function is
+ * called on.
+ *
+ * @param start the starting rectangle
+ * @param end the ending rectangle
+ * @param t the interpolation factor, where 0 is the start and 1 is the end
+ */
+fun Rect.lerpRect(start: Rect, end: Rect, t: Float) {
+ set(
+ Utilities.mapRange(t, start.left.toFloat(), end.left.toFloat()).toInt(),
+ Utilities.mapRange(t, start.top.toFloat(), end.top.toFloat()).toInt(),
+ Utilities.mapRange(t, start.right.toFloat(), end.right.toFloat()).toInt(),
+ Utilities.mapRange(t, start.bottom.toFloat(), end.bottom.toFloat()).toInt(),
+ )
+}
/** Copy the coordinates of the [view] relative to its parent into this rectangle. */
fun Rect.set(view: View) {
diff --git a/src/com/android/launcher3/util/window/WindowManagerProxy.java b/src/com/android/launcher3/util/window/WindowManagerProxy.java
index e568eed..f511ef2 100644
--- a/src/com/android/launcher3/util/window/WindowManagerProxy.java
+++ b/src/com/android/launcher3/util/window/WindowManagerProxy.java
@@ -121,6 +121,14 @@
}
/**
+ * Returns whether the display is a freeform display for which taskbar should be pinned
+ * and showing desktop tasks.
+ */
+ public boolean showDesktopTaskbarForFreeformDisplay(Context displayInfoContext) {
+ return false;
+ }
+
+ /**
* Returns if the home is visible.
*/
public boolean isHomeVisible(Context context) {
diff --git a/src/com/android/launcher3/widget/picker/WidgetRecommendationsView.java b/src/com/android/launcher3/widget/picker/WidgetRecommendationsView.java
index d042b1d..4ccf16b 100644
--- a/src/com/android/launcher3/widget/picker/WidgetRecommendationsView.java
+++ b/src/com/android/launcher3/widget/picker/WidgetRecommendationsView.java
@@ -222,7 +222,8 @@
if (shouldShowFullPageView(recommendations)) {
// Show all widgets in single page with unlimited available height.
return setRecommendations(
- recommendations.values().stream().flatMap(Collection::stream).toList(),
+ recommendations.values().stream().flatMap(Collection::stream)
+ .collect(Collectors.toList()),
deviceProfile, /*availableHeight=*/ Float.MAX_VALUE, availableWidth,
cellPadding);
@@ -369,7 +370,7 @@
// Show only those widgets that were displayed when user first opened the picker.
if (!mDisplayedWidgets.isEmpty()) {
filteredRecommendedWidgets = recommendedWidgets.stream().filter(
- w -> mDisplayedWidgets.contains(w.componentName)).toList();
+ w -> mDisplayedWidgets.contains(w.componentName)).collect(Collectors.toList());
}
Context context = getContext();
LayoutInflater inflater = LayoutInflater.from(context);
diff --git a/src/com/android/launcher3/widget/picker/WidgetsFullSheet.java b/src/com/android/launcher3/widget/picker/WidgetsFullSheet.java
index ab0f9a7..7a218ae 100644
--- a/src/com/android/launcher3/widget/picker/WidgetsFullSheet.java
+++ b/src/com/android/launcher3/widget/picker/WidgetsFullSheet.java
@@ -87,6 +87,7 @@
import java.util.List;
import java.util.Map;
import java.util.function.Predicate;
+import java.util.stream.Collectors;
import java.util.stream.IntStream;
/**
@@ -650,7 +651,7 @@
mRecommendedWidgets = mActivityContext.getWidgetPickerDataProvider().get()
.getRecommendations()
.values().stream()
- .flatMap(Collection::stream).toList();
+ .flatMap(Collection::stream).collect(Collectors.toList());
mRecommendedWidgetsCount = mWidgetRecommendationsView.setRecommendations(
mRecommendedWidgets,
mDeviceProfile,
diff --git a/src/com/android/launcher3/widget/picker/WidgetsRecommendationTableLayout.java b/src/com/android/launcher3/widget/picker/WidgetsRecommendationTableLayout.java
index 0bcab60..216f4d4 100644
--- a/src/com/android/launcher3/widget/picker/WidgetsRecommendationTableLayout.java
+++ b/src/com/android/launcher3/widget/picker/WidgetsRecommendationTableLayout.java
@@ -41,6 +41,7 @@
import java.util.ArrayList;
import java.util.List;
+import java.util.stream.Collectors;
/** A {@link TableLayout} for showing recommended widgets. */
public final class WidgetsRecommendationTableLayout extends TableLayout {
@@ -163,6 +164,7 @@
}
// Perform re-ordering once we have filtered out recommendations that fit.
- return filteredRows.stream().sorted(WIDGETS_TABLE_ROW_COUNT_COMPARATOR).toList();
+ return filteredRows.stream().sorted(WIDGETS_TABLE_ROW_COUNT_COMPARATOR)
+ .collect(Collectors.toList());
}
}
diff --git a/src/com/android/launcher3/widget/util/WidgetsTableUtils.java b/src/com/android/launcher3/widget/util/WidgetsTableUtils.java
index df72f07..1134781 100644
--- a/src/com/android/launcher3/widget/util/WidgetsTableUtils.java
+++ b/src/com/android/launcher3/widget/util/WidgetsTableUtils.java
@@ -95,7 +95,7 @@
List<ArrayList<WidgetItem>> rows = groupWidgetItemsUsingRowPxWithoutReordering(
sortedWidgetItems, context, dp, rowPx,
cellPadding);
- return rows.stream().sorted(WIDGETS_TABLE_ROW_SIZE_COMPARATOR).toList();
+ return rows.stream().sorted(WIDGETS_TABLE_ROW_SIZE_COMPARATOR).collect(Collectors.toList());
}
/**
diff --git a/tests/multivalentTests/src/com/android/launcher3/graphics/ThemeManagerTest.kt b/tests/multivalentTests/src/com/android/launcher3/graphics/ThemeManagerTest.kt
index 43b7b68..85c1156 100644
--- a/tests/multivalentTests/src/com/android/launcher3/graphics/ThemeManagerTest.kt
+++ b/tests/multivalentTests/src/com/android/launcher3/graphics/ThemeManagerTest.kt
@@ -21,11 +21,13 @@
import com.android.launcher3.FakeLauncherPrefs
import com.android.launcher3.dagger.LauncherAppComponent
import com.android.launcher3.dagger.LauncherAppSingleton
+import com.android.launcher3.icons.mono.MonoIconThemeController
import com.android.launcher3.util.AllModulesForTest
import com.android.launcher3.util.Executors.MAIN_EXECUTOR
import com.android.launcher3.util.FakePrefsModule
import com.android.launcher3.util.SandboxApplication
import com.android.launcher3.util.TestUtil
+import com.google.common.truth.Truth.assertThat
import dagger.Component
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
@@ -55,12 +57,13 @@
themeManager.isMonoThemeEnabled = true
TestUtil.runOnExecutorSync(MAIN_EXECUTOR) {}
assertTrue(themeManager.isMonoThemeEnabled)
- assertTrue(themeManager.iconState.isMonoTheme)
+ assertThat(themeManager.iconState.themeController)
+ .isInstanceOf(MonoIconThemeController::class.java)
themeManager.isMonoThemeEnabled = false
TestUtil.runOnExecutorSync(MAIN_EXECUTOR) {}
assertFalse(themeManager.isMonoThemeEnabled)
- assertFalse(themeManager.iconState.isMonoTheme)
+ assertThat(themeManager.iconState.themeController).isNull()
}
@Test
diff --git a/tests/multivalentTests/src/com/android/launcher3/model/ShortcutsChangedTaskTest.kt b/tests/multivalentTests/src/com/android/launcher3/model/ShortcutsChangedTaskTest.kt
index fb6d038..c6863f4 100644
--- a/tests/multivalentTests/src/com/android/launcher3/model/ShortcutsChangedTaskTest.kt
+++ b/tests/multivalentTests/src/com/android/launcher3/model/ShortcutsChangedTaskTest.kt
@@ -24,8 +24,12 @@
import android.content.pm.ShortcutInfo
import android.os.Process.myUserHandle
import android.os.UserHandle
+import android.platform.test.annotations.DisableFlags
+import android.platform.test.annotations.EnableFlags
+import android.platform.test.flag.junit.SetFlagsRule
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
+import com.android.launcher3.Flags
import com.android.launcher3.LauncherAppState
import com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT
import com.android.launcher3.icons.BitmapInfo
@@ -42,6 +46,7 @@
import java.util.function.Predicate
import org.junit.After
import org.junit.Before
+import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.kotlin.any
@@ -55,6 +60,8 @@
@SmallTest
@RunWith(AndroidJUnit4::class)
class ShortcutsChangedTaskTest {
+ @get:Rule val setFlagsRule: SetFlagsRule = SetFlagsRule()
+
private lateinit var shortcutsChangedTask: ShortcutsChangedTask
private lateinit var modelHelper: LauncherModelHelper
private lateinit var context: SandboxModelContext
@@ -131,7 +138,8 @@
}
@Test
- fun `When installed unpinned shortcut is found then remove from workspace`() {
+ @DisableFlags(Flags.FLAG_RESTORE_ARCHIVED_SHORTCUTS)
+ fun `When installed unpinned shortcut is found with Flag off then remove from workspace`() {
// Given
shortcuts =
listOf(
@@ -163,6 +171,37 @@
}
@Test
+ @EnableFlags(Flags.FLAG_RESTORE_ARCHIVED_SHORTCUTS)
+ fun `When installed unpinned shortcut is found with Flag on then keep in workspace`() {
+ // Given
+ shortcuts =
+ listOf(
+ mock<ShortcutInfo>().apply {
+ whenever(isPinned).thenReturn(false)
+ whenever(id).thenReturn(expectedShortcutId)
+ }
+ )
+ val items: IntSparseArrayMap<ItemInfo> = modelHelper.bgDataModel.itemsIdMap
+ items.put(expectedWai.id, expectedWai)
+ doReturn(
+ ApplicationInfo().apply {
+ enabled = true
+ flags = flags or FLAG_INSTALLED
+ isArchived = false
+ }
+ )
+ .whenever(launcherApps)
+ .getApplicationInfo(eq(expectedPackage), any(), eq(user))
+ doReturn(shortcuts).whenever(launcherApps).getShortcuts(any(), eq(user))
+ // When
+ shortcutsChangedTask.execute(mockTaskController, modelHelper.bgDataModel, mockAllApps)
+ // Then
+ verify(mockAppState.iconCache)
+ .getShortcutIcon(eq(expectedWai), any<CacheableShortcutInfo>())
+ verify(mockTaskController).bindUpdatedWorkspaceItems(listOf(expectedWai))
+ }
+
+ @Test
fun `When shortcut app is uninstalled then skip handling`() {
// Given
shortcuts =
@@ -192,7 +231,8 @@
}
@Test
- fun `When archived pinned shortcut is found then keep in workspace`() {
+ @DisableFlags(Flags.FLAG_RESTORE_ARCHIVED_SHORTCUTS)
+ fun `When archived pinned shortcut is found with flag off then keep in workspace`() {
// Given
shortcuts =
listOf(
@@ -222,7 +262,8 @@
}
@Test
- fun `When archived unpinned shortcut is found then keep in workspace`() {
+ @DisableFlags(Flags.FLAG_RESTORE_ARCHIVED_SHORTCUTS)
+ fun `When archived unpinned shortcut is found with flag off then keep in workspace`() {
// Given
shortcuts =
listOf(
@@ -310,4 +351,34 @@
assertThat(modelHelper.bgDataModel.deepShortcutMap).doesNotContainKey(expectedKey)
verify(mockTaskController, times(0)).bindDeepShortcuts(eq(modelHelper.bgDataModel))
}
+
+ @Test
+ @EnableFlags(Flags.FLAG_RESTORE_ARCHIVED_SHORTCUTS)
+ fun `When restoring archived shortcut with flag on then skip handling`() {
+ // Given
+ shortcuts =
+ listOf(
+ mock<ShortcutInfo>().apply {
+ whenever(isPinned).thenReturn(true)
+ whenever(id).thenReturn(expectedShortcutId)
+ }
+ )
+ val items: IntSparseArrayMap<ItemInfo> = modelHelper.bgDataModel.itemsIdMap
+ items.put(expectedWai.id, expectedWai)
+ doReturn(
+ ApplicationInfo().apply {
+ enabled = true
+ flags = flags or FLAG_INSTALLED
+ isArchived = true
+ }
+ )
+ .whenever(launcherApps)
+ .getApplicationInfo(eq(expectedPackage), any(), eq(user))
+ doReturn(shortcuts).whenever(launcherApps).getShortcuts(any(), eq(user))
+ // When
+ shortcutsChangedTask.execute(mockTaskController, modelHelper.bgDataModel, mockAllApps)
+ // Then
+ verify(mockTaskController, times(0)).deleteAndBindComponentsRemoved(any(), any())
+ verify(mockTaskController, times(0)).bindUpdatedWorkspaceItems(any())
+ }
}
diff --git a/tests/multivalentTests/src/com/android/launcher3/model/WorkspaceItemProcessorTest.kt b/tests/multivalentTests/src/com/android/launcher3/model/WorkspaceItemProcessorTest.kt
index da87dfc..7a403e1 100644
--- a/tests/multivalentTests/src/com/android/launcher3/model/WorkspaceItemProcessorTest.kt
+++ b/tests/multivalentTests/src/com/android/launcher3/model/WorkspaceItemProcessorTest.kt
@@ -18,16 +18,19 @@
import android.appwidget.AppWidgetProviderInfo
import android.content.ComponentName
-import android.content.Context
import android.content.Intent
+import android.content.pm.ApplicationInfo
import android.content.pm.LauncherApps
import android.content.pm.PackageInstaller
import android.content.pm.ShortcutInfo
import android.os.Process
import android.os.UserHandle
+import android.platform.test.annotations.DisableFlags
+import android.platform.test.annotations.EnableFlags
+import android.platform.test.flag.junit.SetFlagsRule
import android.util.LongSparseArray
-import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.android.launcher3.Flags
import com.android.launcher3.LauncherAppState
import com.android.launcher3.LauncherSettings.Favorites
import com.android.launcher3.LauncherSettings.Favorites.CONTAINER_DESKTOP
@@ -44,9 +47,14 @@
import com.android.launcher3.model.data.LauncherAppWidgetInfo
import com.android.launcher3.model.data.LauncherAppWidgetInfo.FLAG_UI_NOT_READY
import com.android.launcher3.model.data.WorkspaceItemInfo
+import com.android.launcher3.model.data.WorkspaceItemInfo.FLAG_RESTORED_ICON
+import com.android.launcher3.model.data.WorkspaceItemInfo.FLAG_RESTORE_STARTED
import com.android.launcher3.pm.UserCache
import com.android.launcher3.shortcuts.ShortcutKey
import com.android.launcher3.util.ComponentKey
+import com.android.launcher3.util.ContentWriter
+import com.android.launcher3.util.LauncherModelHelper
+import com.android.launcher3.util.LauncherModelHelper.SandboxModelContext
import com.android.launcher3.util.PackageManagerHelper
import com.android.launcher3.util.PackageUserKey
import com.android.launcher3.util.UserIconInfo
@@ -55,6 +63,7 @@
import com.google.common.truth.Truth.assertThat
import com.google.common.truth.Truth.assertWithMessage
import org.junit.Before
+import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.ArgumentCaptor
@@ -66,6 +75,7 @@
import org.mockito.kotlin.any
import org.mockito.kotlin.anyOrNull
import org.mockito.kotlin.doAnswer
+import org.mockito.kotlin.doReturn
import org.mockito.kotlin.eq
import org.mockito.kotlin.mock
import org.mockito.kotlin.whenever
@@ -73,20 +83,23 @@
@RunWith(AndroidJUnit4::class)
class WorkspaceItemProcessorTest {
+ @get:Rule val setFlagsRule: SetFlagsRule = SetFlagsRule()
+
@Mock private lateinit var mockIconRequestInfo: IconRequestInfo<WorkspaceItemInfo>
@Mock private lateinit var mockWorkspaceInfo: WorkspaceItemInfo
@Mock private lateinit var mockBgDataModel: BgDataModel
- @Mock private lateinit var mockContext: Context
@Mock private lateinit var mockAppState: LauncherAppState
@Mock private lateinit var mockPmHelper: PackageManagerHelper
- @Mock private lateinit var mockLauncherApps: LauncherApps
@Mock private lateinit var mockCursor: LoaderCursor
@Mock private lateinit var mockUserCache: UserCache
@Mock private lateinit var mockUserManagerState: UserManagerState
@Mock private lateinit var mockWidgetInflater: WidgetInflater
- private var intent: Intent = Intent()
- private var mUserHandle: UserHandle = UserHandle(0)
+ lateinit var mModelHelper: LauncherModelHelper
+ lateinit var mContext: SandboxModelContext
+ lateinit var mLauncherApps: LauncherApps
+ private var mIntent: Intent = Intent()
+ private var mUserHandle: UserHandle = Process.myUserHandle()
private var mIconRequestInfos: MutableList<IconRequestInfo<WorkspaceItemInfo>> = mutableListOf()
private var mComponentName: ComponentName = ComponentName("package", "class")
private var mUnlockedUsersArray: LongSparseArray<Boolean> = LongSparseArray()
@@ -101,40 +114,35 @@
@Before
fun setup() {
- mUserHandle = UserHandle(0)
+ mModelHelper = LauncherModelHelper()
+ mContext = mModelHelper.sandboxContext
+ mLauncherApps =
+ mContext.spyService(LauncherApps::class.java).apply {
+ doReturn(true).whenever(this).isPackageEnabled("package", mUserHandle)
+ doReturn(true).whenever(this).isActivityEnabled(mComponentName, mUserHandle)
+ }
+ mUserHandle = Process.myUserHandle()
mockIconRequestInfo = mock<IconRequestInfo<WorkspaceItemInfo>>()
mockWorkspaceInfo = mock<WorkspaceItemInfo>()
mockBgDataModel = mock<BgDataModel>()
mComponentName = ComponentName("package", "class")
mUnlockedUsersArray = LongSparseArray<Boolean>(1).apply { put(101, true) }
- intent =
+ mIntent =
Intent().apply {
component = mComponentName
`package` = "pkg"
putExtra(ShortcutKey.EXTRA_SHORTCUT_ID, "")
}
- mockLauncherApps =
- mock<LauncherApps>().apply {
- whenever(isPackageEnabled("package", mUserHandle)).thenReturn(true)
- whenever(isActivityEnabled(mComponentName, mUserHandle)).thenReturn(true)
- }
- mockContext =
- mock<Context>().apply {
- whenever(packageManager).thenReturn(mock())
- whenever(packageManager.getUserBadgedLabel(any(), any())).thenReturn("")
- whenever(applicationContext).thenReturn(ApplicationProvider.getApplicationContext())
- whenever(getSystemService(LauncherApps::class.java)).thenReturn(mockLauncherApps)
- }
mockAppState =
mock<LauncherAppState>().apply {
- whenever(context).thenReturn(mockContext)
+ whenever(context).thenReturn(mContext)
whenever(iconCache).thenReturn(mock())
whenever(iconCache.getShortcutIcon(any(), any(), any())).then {}
}
mockPmHelper =
mock<PackageManagerHelper>().apply {
whenever(getAppLaunchIntent(mComponentName.packageName, mUserHandle))
- .thenReturn(intent)
+ .thenReturn(mIntent)
}
mockCursor =
mock(LoaderCursor::class.java, RETURNS_DEEP_STUBS).apply {
@@ -143,9 +151,9 @@
id = 1
restoreFlag = 1
serialNumber = 101
- whenever(parseIntent()).thenReturn(intent)
+ whenever(parseIntent()).thenReturn(mIntent)
whenever(markRestored()).doAnswer { restoreFlag = 0 }
- whenever(updater().put(Favorites.INTENT, intent.toUri(0)).commit()).thenReturn(1)
+ whenever(updater().put(Favorites.INTENT, mIntent.toUri(0)).commit()).thenReturn(1)
whenever(getAppShortcutInfo(any(), any(), any(), any()))
.thenReturn(mockWorkspaceInfo)
whenever(createIconRequestInfo(any(), any())).thenReturn(mockIconRequestInfo)
@@ -177,7 +185,7 @@
memoryLogger: LoaderMemoryLogger? = null,
userCache: UserCache = mockUserCache,
userManagerState: UserManagerState = mockUserManagerState,
- launcherApps: LauncherApps = mockLauncherApps,
+ launcherApps: LauncherApps = mLauncherApps,
shortcutKeyToPinnedShortcuts: Map<ShortcutKey, ShortcutInfo> = mKeyToPinnedShortcutsMap,
app: LauncherAppState = mockAppState,
bgDataModel: BgDataModel = mockBgDataModel,
@@ -244,7 +252,7 @@
fun `When app has null target package then mark deleted`() {
// Given
- intent.apply {
+ mIntent.apply {
component = null
`package` = null
}
@@ -264,8 +272,8 @@
// Given
mComponentName = ComponentName("", "")
- intent.component = mComponentName
- intent.`package` = ""
+ mIntent.component = mComponentName
+ mIntent.`package` = ""
// When
itemProcessorUnderTest = createWorkspaceItemProcessorUnderTest()
@@ -298,15 +306,14 @@
fun `When fallback Activity found for app then mark restored`() {
// Given
- mockLauncherApps =
- mock<LauncherApps>().apply {
- whenever(isPackageEnabled("package", mUserHandle)).thenReturn(true)
- whenever(isActivityEnabled(mComponentName, mUserHandle)).thenReturn(false)
- }
+ mLauncherApps.apply {
+ whenever(isPackageEnabled("package", mUserHandle)).thenReturn(true)
+ whenever(isActivityEnabled(mComponentName, mUserHandle)).thenReturn(false)
+ }
mockPmHelper =
mock<PackageManagerHelper>().apply {
whenever(getAppLaunchIntent(mComponentName.packageName, mUserHandle))
- .thenReturn(intent)
+ .thenReturn(mIntent)
}
// When
@@ -317,7 +324,7 @@
assertWithMessage("item restoreFlag should be set to 0")
.that(mockCursor.restoreFlag)
.isEqualTo(0)
- verify(mockCursor.updater().put(Favorites.INTENT, intent.toUri(0))).commit()
+ verify(mockCursor.updater().put(Favorites.INTENT, mIntent.toUri(0))).commit()
assertThat(mIconRequestInfos).containsExactly(mockIconRequestInfo)
verify(mockCursor).checkAndAddItem(mockWorkspaceInfo, mockBgDataModel, null)
}
@@ -326,11 +333,10 @@
fun `When app with disabled activity and no fallback found then mark deleted`() {
// Given
- mockLauncherApps =
- mock<LauncherApps>().apply {
- whenever(isPackageEnabled("package", mUserHandle)).thenReturn(true)
- whenever(isActivityEnabled(mComponentName, mUserHandle)).thenReturn(false)
- }
+ mLauncherApps.apply {
+ whenever(isPackageEnabled("package", mUserHandle)).thenReturn(true)
+ whenever(isActivityEnabled(mComponentName, mUserHandle)).thenReturn(false)
+ }
mockPmHelper =
mock<PackageManagerHelper>().apply {
whenever(getAppLaunchIntent(mComponentName.packageName, mUserHandle))
@@ -358,11 +364,11 @@
@Test
fun `When valid Pinned Deep Shortcut then mark restored`() {
-
// Given
mockCursor.itemType = ITEM_TYPE_DEEP_SHORTCUT
val expectedShortcutInfo =
mock<ShortcutInfo>().apply {
+ whenever(userHandle).thenReturn(mUserHandle)
whenever(id).thenReturn("")
whenever(`package`).thenReturn("")
whenever(activity).thenReturn(mock())
@@ -372,7 +378,7 @@
whenever(disabledReason).thenReturn(0)
whenever(persons).thenReturn(EMPTY_PERSON_ARRAY)
}
- val shortcutKey = ShortcutKey.fromIntent(intent, mockCursor.user)
+ val shortcutKey = ShortcutKey.fromIntent(mIntent, mockCursor.user)
mKeyToPinnedShortcutsMap[shortcutKey] = expectedShortcutInfo
mIconRequestInfos = mutableListOf()
@@ -393,6 +399,67 @@
}
@Test
+ @EnableFlags(Flags.FLAG_RESTORE_ARCHIVED_SHORTCUTS)
+ fun `When Archived Deep Shortcut with flag on then mark restored`() {
+ // Given
+ val mockContentWriter: ContentWriter = mock()
+ val mockAppInfo: ApplicationInfo =
+ mock<ApplicationInfo>().apply {
+ isArchived = true
+ enabled = true
+ }
+ val expectedRestoreFlag = FLAG_RESTORED_ICON or FLAG_RESTORE_STARTED
+ doReturn(mockAppInfo).whenever(mLauncherApps).getApplicationInfo(any(), any(), any())
+ whenever(mockContentWriter.put(Favorites.RESTORED, expectedRestoreFlag))
+ .thenReturn(mockContentWriter)
+ whenever(mockContentWriter.commit()).thenReturn(1)
+ mockCursor.apply {
+ itemType = ITEM_TYPE_DEEP_SHORTCUT
+ restoreFlag = restoreFlag or FLAG_RESTORED_ICON
+ whenever(updater()).thenReturn(mockContentWriter)
+ }
+ mIconRequestInfos = mutableListOf()
+
+ // When
+ itemProcessorUnderTest =
+ createWorkspaceItemProcessorUnderTest(allDeepShortcuts = mAllDeepShortcuts)
+ itemProcessorUnderTest.processItem()
+
+ // Then
+ assertThat(mockCursor.restoreFlag and FLAG_RESTORED_ICON).isEqualTo(FLAG_RESTORED_ICON)
+ assertThat(mockCursor.restoreFlag and FLAG_RESTORE_STARTED).isEqualTo(FLAG_RESTORE_STARTED)
+ assertThat(mIconRequestInfos).isNotEmpty()
+ assertThat(mAllDeepShortcuts).isEmpty()
+ verify(mockContentWriter).put(Favorites.RESTORED, expectedRestoreFlag)
+ verify(mockCursor).checkAndAddItem(any(), eq(mockBgDataModel), eq(null))
+ }
+
+ @Test
+ @DisableFlags(Flags.FLAG_RESTORE_ARCHIVED_SHORTCUTS)
+ fun `When Archived Deep Shortcut with flag off then remove`() {
+ // Given
+ mockCursor.itemType = ITEM_TYPE_DEEP_SHORTCUT
+ mIconRequestInfos = mutableListOf()
+
+ // When
+ itemProcessorUnderTest =
+ createWorkspaceItemProcessorUnderTest(allDeepShortcuts = mAllDeepShortcuts)
+ itemProcessorUnderTest.processItem()
+
+ // Then
+ assertWithMessage("item restoreFlag should be set to 0")
+ .that(mockCursor.restoreFlag)
+ .isEqualTo(0)
+ assertThat(mIconRequestInfos).isEmpty()
+ assertThat(mAllDeepShortcuts).isEmpty()
+ verify(mockCursor)
+ .markDeleted(
+ "Pinned shortcut not found from request. package=pkg, user=UserHandle{0}",
+ "shortcut_not_found",
+ )
+ }
+
+ @Test
fun `When Pinned Deep Shortcut is not stored in ShortcutManager re-query by Shortcut ID`() {
// Given
mockCursor.itemType = ITEM_TYPE_DEEP_SHORTCUT
@@ -406,8 +473,9 @@
whenever(disabledMessage).thenReturn("")
whenever(disabledReason).thenReturn(0)
whenever(persons).thenReturn(EMPTY_PERSON_ARRAY)
+ whenever(userHandle).thenReturn(mUserHandle)
}
- whenever(mockLauncherApps.getShortcuts(any(), any())).thenReturn(listOf(si))
+ doReturn(listOf(si)).whenever(mLauncherApps).getShortcuts(any(), any())
mKeyToPinnedShortcutsMap.clear()
mIconRequestInfos = mutableListOf()
@@ -417,12 +485,12 @@
itemProcessorUnderTest.processItem()
// Then
- verify(mockLauncherApps).getShortcuts(any(), any())
+ verify(mLauncherApps).getShortcuts(any(), any())
assertWithMessage("item restoreFlag should be set to 0")
.that(mockCursor.restoreFlag)
.isEqualTo(0)
verify(mockCursor).markRestored()
- verify(mockCursor).checkAndAddItem(any(), any(), anyOrNull())
+ verify(mockCursor).checkAndAddItem(any(), any(), eq(null))
}
@Test
@@ -469,11 +537,11 @@
}
mIconRequestInfos = mutableListOf()
// Make sure shortcuts map has expected key from expected package
- intent.`package` = mComponentName.packageName
- val shortcutKey = ShortcutKey.fromIntent(intent, mockCursor.user)
+ mIntent.`package` = mComponentName.packageName
+ val shortcutKey = ShortcutKey.fromIntent(mIntent, mockCursor.user)
mKeyToPinnedShortcutsMap[shortcutKey] = expectedShortcutInfo
// set intent package back to null to test scenario
- intent.`package` = null
+ mIntent.`package` = null
// When
itemProcessorUnderTest =
@@ -656,7 +724,7 @@
itemProcessorUnderTest.processItem()
// Then
- verify(mockCursor).checkAndAddItem(any(), any(), anyOrNull())
+ verify(mockCursor).checkAndAddItem(any(), eq(mockBgDataModel), eq(null))
}
@Test
diff --git a/tests/multivalentTests/src/com/android/launcher3/util/DisplayControllerTest.kt b/tests/multivalentTests/src/com/android/launcher3/util/DisplayControllerTest.kt
index 7e76e19..588a668 100644
--- a/tests/multivalentTests/src/com/android/launcher3/util/DisplayControllerTest.kt
+++ b/tests/multivalentTests/src/com/android/launcher3/util/DisplayControllerTest.kt
@@ -32,6 +32,7 @@
import com.android.launcher3.dagger.LauncherAppComponent
import com.android.launcher3.dagger.LauncherAppSingleton
import com.android.launcher3.util.DisplayController.CHANGE_DENSITY
+import com.android.launcher3.util.DisplayController.CHANGE_DESKTOP_MODE
import com.android.launcher3.util.DisplayController.CHANGE_ROTATION
import com.android.launcher3.util.DisplayController.CHANGE_TASKBAR_PINNING
import com.android.launcher3.util.DisplayController.DisplayInfoChangeListener
@@ -229,6 +230,63 @@
.onDisplayInfoChanged(any(), any(), eq(CHANGE_TASKBAR_PINNING))
assertFalse(displayController.getInfo().isTransientTaskbar())
}
+
+ @Test
+ @UiThreadTest
+ fun testTaskbarPinnedForDesktopTaskbar_inDesktopMode() {
+ whenever(windowManagerProxy.showDesktopTaskbarForFreeformDisplay(any())).thenReturn(true)
+ whenever(launcherPrefs.get(TASKBAR_PINNING)).thenReturn(false)
+ whenever(launcherPrefs.get(TASKBAR_PINNING_IN_DESKTOP_MODE)).thenReturn(false)
+ whenever(windowManagerProxy.isInDesktopMode()).thenReturn(true)
+ whenever(windowManagerProxy.isHomeVisible(any())).thenReturn(false)
+ DisplayController.enableTaskbarModePreferenceForTests(true)
+
+ assertTrue(displayController.getInfo().isTransientTaskbar())
+
+ displayController.onConfigurationChanged(configuration)
+
+ verify(displayInfoChangeListener)
+ .onDisplayInfoChanged(any(), any(), eq(CHANGE_TASKBAR_PINNING or CHANGE_DESKTOP_MODE))
+ assertFalse(displayController.getInfo().isTransientTaskbar())
+ }
+
+ @Test
+ @UiThreadTest
+ fun testTaskbarPinnedForDesktopTaskbar_notInDesktopMode() {
+ whenever(windowManagerProxy.showDesktopTaskbarForFreeformDisplay(any())).thenReturn(true)
+ whenever(launcherPrefs.get(TASKBAR_PINNING)).thenReturn(false)
+ whenever(launcherPrefs.get(TASKBAR_PINNING_IN_DESKTOP_MODE)).thenReturn(false)
+ whenever(windowManagerProxy.isInDesktopMode()).thenReturn(false)
+ whenever(windowManagerProxy.isHomeVisible(any())).thenReturn(false)
+ DisplayController.enableTaskbarModePreferenceForTests(true)
+
+ assertTrue(displayController.getInfo().isTransientTaskbar())
+
+ displayController.onConfigurationChanged(configuration)
+
+ verify(displayInfoChangeListener)
+ .onDisplayInfoChanged(any(), any(), eq(CHANGE_TASKBAR_PINNING))
+ assertFalse(displayController.getInfo().isTransientTaskbar())
+ }
+
+ @Test
+ @UiThreadTest
+ fun testTaskbarPinnedForDesktopTaskbar_onHome() {
+ whenever(windowManagerProxy.showDesktopTaskbarForFreeformDisplay(any())).thenReturn(true)
+ whenever(launcherPrefs.get(TASKBAR_PINNING)).thenReturn(false)
+ whenever(launcherPrefs.get(TASKBAR_PINNING_IN_DESKTOP_MODE)).thenReturn(false)
+ whenever(windowManagerProxy.isInDesktopMode()).thenReturn(false)
+ whenever(windowManagerProxy.isHomeVisible(any())).thenReturn(true)
+ DisplayController.enableTaskbarModePreferenceForTests(true)
+
+ assertTrue(displayController.getInfo().isTransientTaskbar())
+
+ displayController.onConfigurationChanged(configuration)
+
+ verify(displayInfoChangeListener)
+ .onDisplayInfoChanged(any(), any(), eq(CHANGE_TASKBAR_PINNING))
+ assertFalse(displayController.getInfo().isTransientTaskbar())
+ }
}
class MyWmProxy : WindowManagerProxy()
diff --git a/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java b/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java
index afc0dd5..16faf14 100644
--- a/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java
+++ b/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java
@@ -1272,8 +1272,6 @@
if (getNavigationModel() == NavigationModel.ZERO_BUTTON
|| isThreeFingerTrackpadGesture) {
final Point displaySize = getRealDisplaySize();
- // TODO(b/225505986): change startY and endY back to displaySize.y / 2 once the
- // issue is solved.
int startX = isThreeFingerTrackpadGesture ? displaySize.x / 4 : 0;
int endX = isThreeFingerTrackpadGesture ? displaySize.x * 3 / 4 : displaySize.x / 2;
linearGesture(startX, displaySize.y / 4, endX, displaySize.y / 4,
diff --git a/tests/tapl/com/android/launcher3/tapl/OverviewTask.java b/tests/tapl/com/android/launcher3/tapl/OverviewTask.java
index 2431ef5..1158521 100644
--- a/tests/tapl/com/android/launcher3/tapl/OverviewTask.java
+++ b/tests/tapl/com/android/launcher3/tapl/OverviewTask.java
@@ -211,6 +211,36 @@
}
/**
+ * Starts dismissing the task by swiping up, then cancels, and task springs back to start.
+ */
+ public void dismissCancel() {
+ try (LauncherInstrumentation.Closable e = mLauncher.eventsCheck();
+ LauncherInstrumentation.Closable c = mLauncher.addContextLayer(
+ "want to start dismissing an overview task then cancel")) {
+ verifyActiveContainer();
+ int taskCountBeforeDismiss = mOverview.getTaskCount();
+ mLauncher.assertNotEquals("Unable to find a task", 0, taskCountBeforeDismiss);
+
+ final Rect taskBounds = mLauncher.getVisibleBounds(mTask);
+ final int centerX = taskBounds.centerX();
+ final int centerY = taskBounds.bottom - 1;
+ final int endCenterY = centerY - (taskBounds.height() / 4);
+ mLauncher.executeAndWaitForLauncherEvent(
+ // Set slowDown to true so we do not fling the task at the end of the drag, as
+ // we want it to cancel and return back to the origin. We use 30 steps to
+ // perform the gesture slowly as well, to avoid flinging.
+ () -> mLauncher.linearGesture(centerX, centerY, centerX, endCenterY,
+ /* steps= */ 30, /* slowDown= */ true,
+ LauncherInstrumentation.GestureScope.DONT_EXPECT_PILFER),
+ event -> TestProtocol.DISMISS_ANIMATION_ENDS_MESSAGE.equals(
+ event.getClassName()),
+ () -> "Canceling swipe to dismiss did not end with task at origin.",
+ "cancel swiping to dismiss");
+
+ }
+ }
+
+ /**
* Clicks the task.
*/
public LaunchedAppState open() {