Merge changes If104c67d,Id59fb236 into main
* changes:
[CD][Alt+Tab] Show apps from all desktops for KQS on CD
[CD][Alt+Tab] Implement skeleton code for KQS on CD
diff --git a/aconfig/launcher.aconfig b/aconfig/launcher.aconfig
index 9fa2f50..ed370ec 100644
--- a/aconfig/launcher.aconfig
+++ b/aconfig/launcher.aconfig
@@ -601,3 +601,10 @@
purpose: PURPOSE_BUGFIX
}
}
+
+flag {
+ name: "enable_strict_mode"
+ namespace: "launcher"
+ description: "Enable Strict Mode for the Launcher app"
+ bug: "394651876"
+}
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..f2f1ebd 100644
--- a/quickstep/res/values/dimens.xml
+++ b/quickstep/res/values/dimens.xml
@@ -108,6 +108,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/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/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/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/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/RecentsAnimationDeviceState.java b/quickstep/src/com/android/quickstep/RecentsAnimationDeviceState.java
index e1e962a..090ccdc 100644
--- a/quickstep/src/com/android/quickstep/RecentsAnimationDeviceState.java
+++ b/quickstep/src/com/android/quickstep/RecentsAnimationDeviceState.java
@@ -114,7 +114,6 @@
private final RotationTouchHelper mRotationTouchHelper;
private final TaskStackChangeListener mPipListener;
- private final DaggerSingletonTracker mLifeCycle;
// Cache for better performance since it doesn't change at runtime.
private final boolean mCanImeRenderGesturalNavButtons =
InputMethodService.canImeRenderGesturalNavButtons();
@@ -152,16 +151,15 @@
mExclusionManager = exclusionManager;
mContextualSearchStateManager = contextualSearchStateManager;
mRotationTouchHelper = rotationTouchHelper;
- mLifeCycle = lifeCycle;
mIsOneHandedModeSupported = SystemProperties.getBoolean(SUPPORT_ONE_HANDED_MODE, false);
// Register for exclusion updates
- mLifeCycle.addCloseable(this::unregisterExclusionListener);
+ lifeCycle.addCloseable(this::unregisterExclusionListener);
// Register for display changes changes
mDisplayController.addChangeListener(this);
onDisplayInfoChanged(context, mDisplayController.getInfo(), CHANGE_ALL);
- mLifeCycle.addCloseable(() -> mDisplayController.removeChangeListener(this));
+ lifeCycle.addCloseable(() -> mDisplayController.removeChangeListener(this));
if (mIsOneHandedModeSupported) {
Uri oneHandedUri = Settings.Secure.getUriFor(ONE_HANDED_ENABLED);
@@ -169,7 +167,7 @@
enabled -> mIsOneHandedModeEnabled = enabled;
settingsCache.register(oneHandedUri, onChangeListener);
mIsOneHandedModeEnabled = settingsCache.getValue(oneHandedUri);
- mLifeCycle.addCloseable(() -> settingsCache.unregister(oneHandedUri, onChangeListener));
+ lifeCycle.addCloseable(() -> settingsCache.unregister(oneHandedUri, onChangeListener));
} else {
mIsOneHandedModeEnabled = false;
}
@@ -180,7 +178,7 @@
enabled -> mIsSwipeToNotificationEnabled = enabled;
settingsCache.register(swipeBottomNotificationUri, onChangeListener);
mIsSwipeToNotificationEnabled = settingsCache.getValue(swipeBottomNotificationUri);
- mLifeCycle.addCloseable(
+ lifeCycle.addCloseable(
() -> settingsCache.unregister(swipeBottomNotificationUri, onChangeListener));
Uri setupCompleteUri = Settings.Secure.getUriFor(Settings.Secure.USER_SETUP_COMPLETE);
@@ -188,7 +186,7 @@
if (!mIsUserSetupComplete) {
SettingsCache.OnChangeListener userSetupChangeListener = e -> mIsUserSetupComplete = e;
settingsCache.register(setupCompleteUri, userSetupChangeListener);
- mLifeCycle.addCloseable(
+ lifeCycle.addCloseable(
() -> settingsCache.unregister(setupCompleteUri, userSetupChangeListener));
}
@@ -210,15 +208,19 @@
}
};
TaskStackChangeListeners.getInstance().registerTaskStackListener(mPipListener);
- mLifeCycle.addCloseable(() ->
+ lifeCycle.addCloseable(() ->
TaskStackChangeListeners.getInstance().unregisterTaskStackListener(mPipListener));
}
/**
* Adds a listener for the nav mode change, guaranteed to be called after the device state's
* mode has changed.
+ *
+ * @return Added {@link DisplayInfoChangeListener} so that caller is
+ * responsible for removing the listener from {@link DisplayController} to avoid memory leak.
*/
- public void addNavigationModeChangedCallback(Runnable callback) {
+ public DisplayController.DisplayInfoChangeListener addNavigationModeChangedCallback(
+ Runnable callback) {
DisplayController.DisplayInfoChangeListener listener = (context, info, flags) -> {
if ((flags & CHANGE_NAVIGATION_MODE) != 0) {
callback.run();
@@ -226,7 +228,16 @@
};
mDisplayController.addChangeListener(listener);
callback.run();
- mLifeCycle.addCloseable(() -> mDisplayController.removeChangeListener(listener));
+ return listener;
+ }
+
+ /**
+ * Remove the {DisplayController.DisplayInfoChangeListener} added from
+ * {@link #addNavigationModeChangedCallback} when {@link TouchInteractionService} is destroyed.
+ */
+ public void removeDisplayInfoChangeListener(
+ DisplayController.DisplayInfoChangeListener listener) {
+ mDisplayController.removeChangeListener(listener);
}
@Override
diff --git a/quickstep/src/com/android/quickstep/TaskIconCache.kt b/quickstep/src/com/android/quickstep/TaskIconCache.kt
index bf94d41..b82c110 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
@@ -206,6 +207,7 @@
TaskCacheEntry(
entryIcon,
getBadgedContentDescription(
+ context,
activityInfo,
task.key.userId,
task.taskDescription,
@@ -215,7 +217,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) }
}
@@ -224,28 +231,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 8422d4c..ba4c65a 100644
--- a/quickstep/src/com/android/quickstep/TouchInteractionService.java
+++ b/quickstep/src/com/android/quickstep/TouchInteractionService.java
@@ -562,6 +562,8 @@
private DesktopAppLaunchTransitionManager mDesktopAppLaunchTransitionManager;
+ private DisplayController.DisplayInfoChangeListener mDisplayInfoChangeListener;
+
@Override
public void onCreate() {
super.onCreate();
@@ -592,7 +594,8 @@
// Call runOnUserUnlocked() before any other callbacks to ensure everything is initialized.
LockedUserState.get(this).runOnUserUnlocked(mUserUnlockedRunnable);
- mDeviceState.addNavigationModeChangedCallback(this::onNavigationModeChanged);
+ mDisplayInfoChangeListener =
+ mDeviceState.addNavigationModeChangedCallback(this::onNavigationModeChanged);
ScreenOnTracker.INSTANCE.get(this).addListener(mScreenOnListener);
}
@@ -746,7 +749,7 @@
mDesktopAppLaunchTransitionManager.unregisterTransitions();
}
mDesktopAppLaunchTransitionManager = null;
-
+ mDeviceState.removeDisplayInfoChangeListener(mDisplayInfoChangeListener);
LockedUserState.get(this).removeOnUserUnlockedRunnable(mUserUnlockedRunnable);
ScreenOnTracker.INSTANCE.get(this).removeListener(mScreenOnListener);
super.onDestroy();
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/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..162d14d 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>())
@@ -78,6 +81,9 @@
taskIds.value = taskId.toSet()
}
+ 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..99255e8 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
@@ -94,7 +94,7 @@
val fadeWithThumbnail: Boolean,
val isStagedTask: Boolean,
val iconView: View?,
- val contentDescription: CharSequence?
+ val contentDescription: CharSequence?,
)
}
@@ -104,7 +104,7 @@
*/
fun getFirstAnimInitViews(
taskViewSupplier: Supplier<TaskView>,
- splitSelectSourceSupplier: Supplier<SplitSelectSource?>
+ splitSelectSourceSupplier: Supplier<SplitSelectSource?>,
): SplitAnimInitProps {
val splitSelectSource = splitSelectSourceSupplier.get()
if (!splitSelectStateController.isAnimateCurrentTaskDismissal) {
@@ -116,7 +116,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 +131,7 @@
fadeWithThumbnail = true,
isStagedTask = true,
iconView = container.iconView.asView(),
- container.task.titleDescription
+ container.task.titleDescription,
)
}
}
@@ -151,7 +151,7 @@
fadeWithThumbnail = true,
isStagedTask = true,
iconView = it.iconView.asView(),
- it.task.titleDescription
+ it.task.titleDescription,
)
}
}
@@ -189,29 +189,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 +216,7 @@
ObjectAnimator.ofFloat(
(iconView as IconAppChipView).splitTranslationX,
MULTI_PROPERTY_VALUE,
- 0f
+ 0f,
)
)
builder.add(
@@ -306,7 +302,7 @@
fun addScrimBehindAnim(
pendingAnimation: PendingAnimation,
container: RecentsViewContainer,
- context: Context
+ context: Context,
): View {
val scrim = View(context)
val recentsView = container.getOverviewPanel<RecentsView<*, *>>()
@@ -334,8 +330,8 @@
Interpolators.clampToProgress(
timings.backingScrimFadeInterpolator,
timings.backingScrimFadeInStartOffset,
- timings.backingScrimFadeInEndOffset
- )
+ timings.backingScrimFadeInEndOffset,
+ ),
)
return scrim
@@ -358,7 +354,7 @@
fun createPlaceholderDismissAnim(
container: RecentsViewContainer,
splitDismissEvent: EventEnum,
- duration: Long?
+ duration: Long?,
): AnimatorSet {
val animatorSet = AnimatorSet()
duration?.let { animatorSet.duration = it }
@@ -375,7 +371,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 +379,7 @@
dragLayer.left.toFloat(),
dragLayer.top.toFloat(),
dragLayer.right.toFloat(),
- dragLayer.bottom.toFloat()
+ dragLayer.bottom.toFloat(),
)
animatorSet.play(
ObjectAnimator.ofFloat(
@@ -393,8 +389,8 @@
floatingTask,
onScreenRectF,
floatingTask.stagePosition,
- container.deviceProfile
- )
+ container.deviceProfile,
+ ),
)
)
animatorSet.addListener(
@@ -403,7 +399,7 @@
splitSelectStateController.resetState()
safeRemoveViewFromDragLayer(
container,
- splitSelectStateController.splitInstructionsView
+ splitSelectStateController.splitInstructionsView,
)
}
}
@@ -429,8 +425,8 @@
Interpolators.clampToProgress(
Interpolators.LINEAR,
timings.instructionsContainerFadeInStartOffset,
- timings.instructionsContainerFadeInEndOffset
- )
+ timings.instructionsContainerFadeInEndOffset,
+ ),
)
anim.addFloat(
splitInstructionsView,
@@ -440,8 +436,8 @@
Interpolators.clampToProgress(
Interpolators.EMPHASIZED_DECELERATE,
timings.instructionsUnfoldStartOffset,
- timings.instructionsUnfoldEndOffset
- )
+ timings.instructionsUnfoldEndOffset,
+ ),
)
return anim
}
@@ -459,7 +455,7 @@
fun playAnimPlaceholderToFullscreen(
container: RecentsViewContainer,
view: View,
- resetCallback: Optional<Runnable>
+ resetCallback: Optional<Runnable>,
) {
val stagedTaskView = view as FloatingTaskView
@@ -481,7 +477,7 @@
RectF(firstTaskStartingBounds),
firstTaskEndingBounds,
false /* fadeWithThumbnail */,
- true /* isStagedTask */
+ true, /* isStagedTask */
)
pendingAnimation.addEndListener {
@@ -511,7 +507,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 +526,7 @@
nonApps,
stateManager,
depthController,
- finishCallback
+ finishCallback,
)
return
@@ -548,7 +544,7 @@
depthController,
info,
t,
- finishCallback
+ finishCallback,
)
} else if (launchingIconView != null) {
// Tapping an app pair icon
@@ -563,7 +559,7 @@
info,
t,
finishCallback,
- cornerRadius
+ cornerRadius,
)
} else {
composeFullscreenIconSplitLaunchAnimator(
@@ -571,7 +567,7 @@
info,
t,
finishCallback,
- appPairLaunchingAppIndex
+ appPairLaunchingAppIndex,
)
}
} else {
@@ -587,7 +583,7 @@
info,
t,
finishCallback,
- cornerRadius
+ cornerRadius,
)
}
}
@@ -603,7 +599,7 @@
depthController: DepthController?,
info: TransitionInfo,
t: Transaction,
- finishCallback: Runnable
+ finishCallback: Runnable,
) {
TaskViewUtils.composeRecentsSplitLaunchAnimator(
launchingTaskView,
@@ -611,7 +607,7 @@
depthController,
info,
t,
- finishCallback
+ finishCallback,
)
}
@@ -629,7 +625,7 @@
nonApps: Array<RemoteAnimationTarget>,
stateManager: StateManager<*, *>,
depthController: DepthController?,
- finishCallback: Runnable
+ finishCallback: Runnable,
) {
TaskViewUtils.composeRecentsSplitLaunchAnimatorLegacy(
launchingTaskView,
@@ -640,7 +636,7 @@
nonApps,
stateManager,
depthController,
- finishCallback
+ finishCallback,
)
}
@@ -651,7 +647,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 +708,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 +717,7 @@
transitionInfo,
t,
finishCallback,
- WINDOWING_MODE_MULTI_WINDOW
+ WINDOWING_MODE_MULTI_WINDOW,
)
return
}
@@ -753,8 +749,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 +764,7 @@
drawableArea,
appIcon1,
appIcon2,
- dividerPos
+ dividerPos,
)
floatingView.bringToFront()
@@ -780,7 +775,7 @@
finishCallback,
launcher,
floatingView,
- mainRootCandidate
+ mainRootCandidate,
)
iconLaunchValueAnimator.addListener(
object : AnimatorListenerAdapter() {
@@ -806,7 +801,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 +810,7 @@
transitionInfo,
t,
finishCallback,
- WINDOWING_MODE_FULLSCREEN
+ WINDOWING_MODE_FULLSCREEN,
)
return
}
@@ -867,7 +862,7 @@
drawableArea,
appIcon,
null /*appIcon2*/,
- 0 /*dividerPos*/
+ 0, /*dividerPos*/
)
floatingView.bringToFront()
launchAnimation.play(
@@ -882,7 +877,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 +891,7 @@
Interpolators.LINEAR,
valueAnimator.animatedFraction,
timings.appRevealStartOffset,
- timings.appRevealEndOffset
+ timings.appRevealEndOffset,
)
// Set the alpha of the shell layer (2 apps + divider)
@@ -913,8 +908,8 @@
Interpolators.clampToProgress(
timings.getStagedRectXInterpolator(),
timings.stagedRectSlideStartOffset,
- timings.stagedRectSlideEndOffset
- )
+ timings.stagedRectSlideEndOffset,
+ ),
)
var mDy =
FloatProp(
@@ -923,8 +918,8 @@
Interpolators.clampToProgress(
Interpolators.EMPHASIZED,
timings.stagedRectSlideStartOffset,
- timings.stagedRectSlideEndOffset
- )
+ timings.stagedRectSlideEndOffset,
+ ),
)
var mScaleX =
FloatProp(
@@ -933,8 +928,8 @@
Interpolators.clampToProgress(
Interpolators.EMPHASIZED,
timings.stagedRectSlideStartOffset,
- timings.stagedRectSlideEndOffset
- )
+ timings.stagedRectSlideEndOffset,
+ ),
)
var mScaleY =
FloatProp(
@@ -943,8 +938,8 @@
Interpolators.clampToProgress(
Interpolators.EMPHASIZED,
timings.stagedRectSlideStartOffset,
- timings.stagedRectSlideEndOffset
- )
+ timings.stagedRectSlideEndOffset,
+ ),
)
override fun onUpdate(percent: Float, initOnly: Boolean) {
@@ -979,7 +974,7 @@
transitionInfo: TransitionInfo,
t: Transaction,
finishCallback: Runnable,
- windowingMode: Int
+ windowingMode: Int,
) {
val launchAnimation = AnimatorSet()
val progressUpdater = ValueAnimator.ofFloat(0f, 1f)
@@ -1066,7 +1061,7 @@
transitionInfo: TransitionInfo,
t: Transaction,
finishCallback: Runnable,
- cornerRadius: Float
+ cornerRadius: Float,
) {
var splitRoot1: Change? = null
var splitRoot2: Change? = null
@@ -1131,7 +1126,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/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..ceffbe4 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);
}
/**
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..bb6829a 100644
--- a/quickstep/src/com/android/quickstep/views/DesktopTaskView.kt
+++ b/quickstep/src/com/android/quickstep/views/DesktopTaskView.kt
@@ -15,33 +15,44 @@
*/
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.task.thumbnail.TaskThumbnailView
import com.android.quickstep.util.RecentsOrientedState
import com.android.systemui.shared.recents.model.Task
@@ -79,11 +90,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 +161,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 +280,7 @@
tasks.forEach { sb.append(" key=${it.key}\n") }
Log.d(TAG, sb.toString())
}
+
cancelPendingLoadTasks()
val backgroundViewIndex = contentView.indexOfChild(backgroundView)
taskContainers =
@@ -160,8 +308,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 +335,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) {
@@ -319,6 +438,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..a76ebdb 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();
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..5093259 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
@@ -248,8 +249,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 +331,12 @@
onModalnessUpdated(field)
}
+ var splitSplashAlpha = 0f
+ set(value) {
+ field = value
+ applyThumbnailSplashAlpha()
+ }
+
protected var taskThumbnailSplashAlpha = 0f
set(value) {
field = value
@@ -519,40 +561,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 +654,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()) {
@@ -767,9 +778,20 @@
liveTile = state.isLiveTile,
hasHeader = type == TaskViewType.DESKTOP,
)
+ updateThumbnailValidity(container)
}
}
+ 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 +828,7 @@
onBind(orientedState)
}
- open fun onBind(orientedState: RecentsOrientedState) {
+ protected open fun onBind(orientedState: RecentsOrientedState) {
if (enableRefactorTaskThumbnail()) {
viewModel =
TaskViewModel(
@@ -814,20 +836,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,
@@ -1293,6 +1332,7 @@
if (isQuickSwitch) {
setFreezeRecentTasksReordering()
}
+ // TODO(b/331754864): Update this to use TV.shouldShowSplash
disableStartingWindow = firstTaskContainer.shouldShowSplashView
}
Executors.UI_HELPER_EXECUTOR.execute {
@@ -1583,14 +1623,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/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..7066d21 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
@@ -619,6 +620,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,8 +782,10 @@
topIds: IntArray = IntArray.wrap(1, 3, 5),
bottomIds: IntArray = IntArray.wrap(2, 4, 6),
largeTileIds: List<Int> = emptyList(),
+ hasAddDesktopButton: Boolean = false,
): Int {
- val taskGridNavHelper = TaskGridNavHelper(topIds, bottomIds, largeTileIds)
+ val taskGridNavHelper =
+ TaskGridNavHelper(topIds, bottomIds, largeTileIds, hasAddDesktopButton)
return taskGridNavHelper.getNextGridPage(currentPageTaskViewId, delta, direction, true)
}
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/src/com/android/launcher3/InvariantDeviceProfile.java b/src/com/android/launcher3/InvariantDeviceProfile.java
index e47a44a..add8a05 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)) {
diff --git a/src/com/android/launcher3/Launcher.java b/src/com/android/launcher3/Launcher.java
index 30ef24b..58fd154 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;
@@ -459,7 +460,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()
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/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/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/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/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,