Merge "Fix issue where hotseat apps don't migrate to the new grid" into main
diff --git a/quickstep/res/layout/keyboard_quick_switch_desktop_taskview.xml b/quickstep/res/layout/keyboard_quick_switch_desktop_taskview.xml
index 71c782d..db47ff0 100644
--- a/quickstep/res/layout/keyboard_quick_switch_desktop_taskview.xml
+++ b/quickstep/res/layout/keyboard_quick_switch_desktop_taskview.xml
@@ -48,13 +48,13 @@
app:layout_constraintVertical_chainStyle="packed"
app:layout_constraintTop_toTopOf="parent"
- app:layout_constraintBottom_toTopOf="@id/text"
+ app:layout_constraintBottom_toTopOf="@id/small_text"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"/>
<TextView
style="@style/KeyboardQuickSwitchText.OnTaskView"
- android:id="@+id/text"
+ android:id="@+id/small_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textAlignment="center"
diff --git a/quickstep/src/com/android/launcher3/desktop/DesktopAppLaunchAnimatorHelper.kt b/quickstep/src/com/android/launcher3/desktop/DesktopAppLaunchAnimatorHelper.kt
new file mode 100644
index 0000000..adbcc75
--- /dev/null
+++ b/quickstep/src/com/android/launcher3/desktop/DesktopAppLaunchAnimatorHelper.kt
@@ -0,0 +1,192 @@
+/*
+ * 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.launcher3.desktop
+
+import android.animation.Animator
+import android.animation.AnimatorSet
+import android.animation.ValueAnimator
+import android.content.Context
+import android.graphics.Rect
+import android.view.Choreographer
+import android.view.SurfaceControl.Transaction
+import android.view.WindowManager.TRANSIT_CLOSE
+import android.view.WindowManager.TRANSIT_OPEN
+import android.view.WindowManager.TRANSIT_TO_BACK
+import android.window.DesktopModeFlags
+import android.window.TransitionInfo
+import android.window.TransitionInfo.Change
+import androidx.core.animation.addListener
+import androidx.core.util.Supplier
+import com.android.app.animation.Interpolators
+import com.android.internal.jank.Cuj
+import com.android.internal.jank.InteractionJankMonitor
+import com.android.internal.policy.ScreenDecorationsUtils
+import com.android.launcher3.desktop.DesktopAppLaunchTransition.AppLaunchType
+import com.android.launcher3.desktop.DesktopAppLaunchTransition.Companion.LAUNCH_CHANGE_MODES
+import com.android.wm.shell.shared.animation.MinimizeAnimator
+import com.android.wm.shell.shared.animation.WindowAnimator
+
+/**
+ * Helper class responsible for creating and managing animators for desktop app launch and related
+ * transitions.
+ *
+ * <p>This class handles the complex logic of creating various animators, including launch,
+ * minimize, and trampoline close animations, based on the provided transition information and
+ * launch type. It also utilizes {@link InteractionJankMonitor} to monitor animation jank.
+ *
+ * @param context The application context.
+ * @param launchType The type of app launch, containing animation parameters.
+ * @param cujType The CUJ (Critical User Journey) type for jank monitoring.
+ */
+class DesktopAppLaunchAnimatorHelper(
+ private val context: Context,
+ private val launchType: AppLaunchType,
+ @Cuj.CujType private val cujType: Int,
+ private val transactionSupplier: Supplier<Transaction>,
+) {
+
+ private val interactionJankMonitor = InteractionJankMonitor.getInstance()
+
+ fun createAnimators(info: TransitionInfo, finishCallback: (Animator) -> Unit): List<Animator> {
+ val launchChange = getLaunchChange(info)
+ requireNotNull(launchChange) { "expected an app launch Change" }
+
+ val transaction = transactionSupplier.get()
+
+ val minimizeChange = getMinimizeChange(info)
+ val trampolineCloseChange = getTrampolineCloseChange(info)
+
+ val launchAnimator =
+ createLaunchAnimator(
+ launchChange,
+ transaction,
+ finishCallback,
+ isTrampoline = trampolineCloseChange != null,
+ )
+ val animatorsList = mutableListOf(launchAnimator)
+ if (minimizeChange != null) {
+ val minimizeAnimator =
+ createMinimizeAnimator(minimizeChange, transaction, finishCallback)
+ animatorsList.add(minimizeAnimator)
+ }
+ if (trampolineCloseChange != null) {
+ val trampolineCloseAnimator =
+ createTrampolineCloseAnimator(trampolineCloseChange, transaction)
+ animatorsList.add(trampolineCloseAnimator)
+ }
+ return animatorsList
+ }
+
+ private fun getLaunchChange(info: TransitionInfo): Change? =
+ info.changes.firstOrNull { change -> change.mode in LAUNCH_CHANGE_MODES }
+
+ private fun getMinimizeChange(info: TransitionInfo): Change? =
+ info.changes.firstOrNull { change -> change.mode == TRANSIT_TO_BACK }
+
+ private fun getTrampolineCloseChange(info: TransitionInfo): Change? {
+ if (
+ info.changes.size < 2 ||
+ !DesktopModeFlags.ENABLE_DESKTOP_TRAMPOLINE_CLOSE_ANIMATION_BUGFIX.isTrue
+ ) {
+ return null
+ }
+ val openChange =
+ info.changes.firstOrNull { change ->
+ change.mode == TRANSIT_OPEN && change.taskInfo?.isFreeform == true
+ }
+ val closeChange =
+ info.changes.firstOrNull { change ->
+ change.mode == TRANSIT_CLOSE && change.taskInfo?.isFreeform == true
+ }
+ val openPackage = openChange?.taskInfo?.baseIntent?.component?.packageName
+ val closePackage = closeChange?.taskInfo?.baseIntent?.component?.packageName
+ return if (openPackage != null && closePackage != null && openPackage == closePackage) {
+ closeChange
+ } else {
+ null
+ }
+ }
+
+ private fun createLaunchAnimator(
+ change: Change,
+ transaction: Transaction,
+ onAnimFinish: (Animator) -> Unit,
+ isTrampoline: Boolean,
+ ): Animator {
+ val boundsAnimator =
+ WindowAnimator.createBoundsAnimator(
+ context.resources.displayMetrics,
+ launchType.boundsAnimationParams,
+ change,
+ transaction,
+ )
+ val alphaAnimator =
+ ValueAnimator.ofFloat(0f, 1f).apply {
+ duration = launchType.alphaDurationMs
+ interpolator = Interpolators.LINEAR
+ addUpdateListener { animation ->
+ transaction
+ .setAlpha(change.leash, animation.animatedValue as Float)
+ .setFrameTimeline(Choreographer.getInstance().vsyncId)
+ .apply()
+ }
+ }
+ val clipRect = Rect(change.endAbsBounds).apply { offsetTo(0, 0) }
+ transaction.setCrop(change.leash, clipRect)
+ transaction.setCornerRadius(
+ change.leash,
+ ScreenDecorationsUtils.getWindowCornerRadius(context),
+ )
+ return AnimatorSet().apply {
+ interactionJankMonitor.begin(change.leash, context, context.mainThreadHandler, cujType)
+ if (isTrampoline) {
+ play(alphaAnimator)
+ } else {
+ playTogether(boundsAnimator, alphaAnimator)
+ }
+ addListener(
+ onEnd = { animation ->
+ onAnimFinish(animation)
+ interactionJankMonitor.end(cujType)
+ }
+ )
+ }
+ }
+
+ private fun createMinimizeAnimator(
+ change: Change,
+ transaction: Transaction,
+ onAnimFinish: (Animator) -> Unit,
+ ): Animator {
+ return MinimizeAnimator.create(
+ context.resources.displayMetrics,
+ change,
+ transaction,
+ onAnimFinish,
+ )
+ }
+
+ private fun createTrampolineCloseAnimator(change: Change, transaction: Transaction): Animator {
+ return ValueAnimator.ofFloat(1f, 0f).apply {
+ duration = 100L
+ interpolator = Interpolators.LINEAR
+ addUpdateListener { animation ->
+ transaction.setAlpha(change.leash, animation.animatedValue as Float).apply()
+ }
+ }
+ }
+}
diff --git a/quickstep/src/com/android/launcher3/desktop/DesktopAppLaunchTransition.kt b/quickstep/src/com/android/launcher3/desktop/DesktopAppLaunchTransition.kt
index 2406fb6..578bba5 100644
--- a/quickstep/src/com/android/launcher3/desktop/DesktopAppLaunchTransition.kt
+++ b/quickstep/src/com/android/launcher3/desktop/DesktopAppLaunchTransition.kt
@@ -17,27 +17,18 @@
package com.android.launcher3.desktop
import android.animation.Animator
-import android.animation.AnimatorSet
-import android.animation.ValueAnimator
import android.content.Context
-import android.graphics.Rect
import android.os.IBinder
-import android.view.Choreographer
import android.view.SurfaceControl.Transaction
import android.view.WindowManager.TRANSIT_OPEN
-import android.view.WindowManager.TRANSIT_TO_BACK
import android.view.WindowManager.TRANSIT_TO_FRONT
import android.window.IRemoteTransitionFinishedCallback
import android.window.RemoteTransitionStub
import android.window.TransitionInfo
-import android.window.TransitionInfo.Change
-import androidx.core.animation.addListener
+import androidx.core.util.Supplier
import com.android.app.animation.Interpolators
import com.android.internal.jank.Cuj
-import com.android.internal.jank.InteractionJankMonitor
-import com.android.internal.policy.ScreenDecorationsUtils
import com.android.quickstep.RemoteRunnable
-import com.android.wm.shell.shared.animation.MinimizeAnimator
import com.android.wm.shell.shared.animation.WindowAnimator
import java.util.concurrent.Executor
@@ -48,14 +39,18 @@
* ([android.view.WindowManager.TRANSIT_TO_BACK]) this transition will apply a minimize animation to
* that window.
*/
-class DesktopAppLaunchTransition(
- private val context: Context,
- private val mainExecutor: Executor,
- private val launchType: AppLaunchType,
+class DesktopAppLaunchTransition
+@JvmOverloads
+constructor(
+ context: Context,
+ launchType: AppLaunchType,
@Cuj.CujType private val cujType: Int,
+ private val mainExecutor: Executor,
+ transactionSupplier: Supplier<Transaction> = Supplier { Transaction() },
) : RemoteTransitionStub() {
- private val interactionJankMonitor = InteractionJankMonitor.getInstance()
+ private val animatorHelper: DesktopAppLaunchAnimatorHelper =
+ DesktopAppLaunchAnimatorHelper(context, launchType, cujType, transactionSupplier)
enum class AppLaunchType(
val boundsAnimationParams: WindowAnimator.BoundsAnimationParams,
@@ -68,7 +63,7 @@
override fun startAnimation(
token: IBinder,
info: TransitionInfo,
- t: Transaction,
+ transaction: Transaction,
transitionFinishedCallback: IRemoteTransitionFinishedCallback,
) {
val safeTransitionFinishedCallback = RemoteRunnable {
@@ -76,7 +71,7 @@
}
mainExecutor.execute {
runAnimators(info, safeTransitionFinishedCallback)
- t.apply()
+ transaction.apply()
}
}
@@ -86,77 +81,10 @@
animators -= animator
if (animators.isEmpty()) finishedCallback.run()
}
- animators += createAnimators(info, animatorFinishedCallback)
+ animators += animatorHelper.createAnimators(info, animatorFinishedCallback)
animators.forEach { it.start() }
}
- private fun createAnimators(
- info: TransitionInfo,
- finishCallback: (Animator) -> Unit,
- ): List<Animator> {
- val transaction = Transaction()
- val launchAnimator =
- createLaunchAnimator(getLaunchChange(info), transaction, finishCallback)
- val minimizeChange = getMinimizeChange(info) ?: return listOf(launchAnimator)
- val minimizeAnimator =
- MinimizeAnimator.create(
- context.resources.displayMetrics,
- minimizeChange,
- transaction,
- finishCallback,
- )
- return listOf(launchAnimator, minimizeAnimator)
- }
-
- private fun getLaunchChange(info: TransitionInfo): Change =
- requireNotNull(info.changes.firstOrNull { change -> change.mode in LAUNCH_CHANGE_MODES }) {
- "expected an app launch Change"
- }
-
- private fun getMinimizeChange(info: TransitionInfo): Change? =
- info.changes.firstOrNull { change -> change.mode == TRANSIT_TO_BACK }
-
- private fun createLaunchAnimator(
- change: Change,
- transaction: Transaction,
- onAnimFinish: (Animator) -> Unit,
- ): Animator {
- val boundsAnimator =
- WindowAnimator.createBoundsAnimator(
- context.resources.displayMetrics,
- launchType.boundsAnimationParams,
- change,
- transaction,
- )
- val alphaAnimator =
- ValueAnimator.ofFloat(0f, 1f).apply {
- duration = launchType.alphaDurationMs
- interpolator = Interpolators.LINEAR
- addUpdateListener { animation ->
- transaction
- .setAlpha(change.leash, animation.animatedValue as Float)
- .setFrameTimeline(Choreographer.getInstance().vsyncId)
- .apply()
- }
- }
- val clipRect = Rect(change.endAbsBounds).apply { offsetTo(0, 0) }
- transaction.setCrop(change.leash, clipRect)
- transaction.setCornerRadius(
- change.leash,
- ScreenDecorationsUtils.getWindowCornerRadius(context),
- )
- return AnimatorSet().apply {
- interactionJankMonitor.begin(change.leash, context, context.mainThreadHandler, cujType)
- playTogether(boundsAnimator, alphaAnimator)
- addListener(
- onEnd = { animation ->
- onAnimFinish(animation)
- interactionJankMonitor.end(cujType)
- }
- )
- }
- }
-
companion object {
/** Change modes that represent a task becoming visible / launching in Desktop mode. */
val LAUNCH_CHANGE_MODES = intArrayOf(TRANSIT_OPEN, TRANSIT_TO_FRONT)
diff --git a/quickstep/src/com/android/launcher3/desktop/DesktopAppLaunchTransitionManager.kt b/quickstep/src/com/android/launcher3/desktop/DesktopAppLaunchTransitionManager.kt
index 36c5fba..a72b5c4 100644
--- a/quickstep/src/com/android/launcher3/desktop/DesktopAppLaunchTransitionManager.kt
+++ b/quickstep/src/com/android/launcher3/desktop/DesktopAppLaunchTransitionManager.kt
@@ -48,9 +48,9 @@
RemoteTransition(
DesktopAppLaunchTransition(
context,
- MAIN_EXECUTOR,
AppLaunchType.UNMINIMIZE,
Cuj.CUJ_DESKTOP_MODE_APP_LAUNCH_FROM_INTENT,
+ MAIN_EXECUTOR,
),
"DesktopWindowLimitUnminimize",
)
diff --git a/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchTaskView.java b/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchTaskView.java
index ce96556..bf5c0c8 100644
--- a/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchTaskView.java
+++ b/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchTaskView.java
@@ -104,9 +104,18 @@
mIcon2 = findViewById(R.id.icon_2);
mContent = findViewById(R.id.content);
- Resources resources = mContext.getResources();
-
Preconditions.assertNotNull(mContent);
+
+ TypefaceUtils.setTypeface(
+ mContent.findViewById(R.id.large_text),
+ TypefaceUtils.FONT_FAMILY_HEADLINE_LARGE_EMPHASIZED
+ );
+ TypefaceUtils.setTypeface(
+ mContent.findViewById(R.id.small_text),
+ TypefaceUtils.FONT_FAMILY_LABEL_LARGE_BASELINE
+ );
+
+ Resources resources = mContext.getResources();
mBorderAnimator = BorderAnimator.createScalingBorderAnimator(
/* borderRadiusPx= */ mBorderRadius != INVALID_BORDER_RADIUS
? mBorderRadius
diff --git a/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchView.java b/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchView.java
index 4581119..4b4d68d 100644
--- a/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchView.java
+++ b/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchView.java
@@ -163,6 +163,10 @@
R.dimen.keyboard_quick_switch_view_small_spacing);
mOutlineRadius = resources.getDimensionPixelSize(R.dimen.keyboard_quick_switch_view_radius);
mIsRtl = Utilities.isRtl(resources);
+
+ TypefaceUtils.setTypeface(
+ mNoRecentItemsPane.findViewById(R.id.no_recent_items_text),
+ TypefaceUtils.FONT_FAMILY_LABEL_LARGE_BASELINE);
}
private void registerOnBackInvokedCallback() {
@@ -310,7 +314,7 @@
layoutInflater,
previousTaskView);
- desktopButton.<TextView>findViewById(R.id.text).setText(
+ desktopButton.<TextView>findViewById(R.id.small_text).setText(
resources.getString(R.string.quick_switch_desktop));
}
mDisplayingRecentTasks = !groupTasks.isEmpty() || useDesktopTaskView;
diff --git a/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchViewController.java b/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchViewController.java
index 5af7ff8..5f7a026 100644
--- a/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchViewController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchViewController.java
@@ -290,9 +290,9 @@
remoteTransition = new RemoteTransition(
new DesktopAppLaunchTransition(
context,
- MAIN_EXECUTOR,
UNMINIMIZE,
- Cuj.CUJ_DESKTOP_MODE_KEYBOARD_QUICK_SWITCH_APP_LAUNCH
+ Cuj.CUJ_DESKTOP_MODE_KEYBOARD_QUICK_SWITCH_APP_LAUNCH,
+ MAIN_EXECUTOR
),
"DesktopKeyboardQuickSwitchUnminimize");
}
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java
index b9d1275..e62288f 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java
@@ -47,7 +47,6 @@
import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_NOTIFICATION_PANEL_VISIBLE;
import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_VOICE_INTERACTION_WINDOW_SHOWING;
import static com.android.window.flags.Flags.enableStartLaunchTransitionFromTaskbarBugfix;
-import static com.android.window.flags.Flags.enableTaskbarConnectedDisplays;
import static com.android.wm.shell.Flags.enableTinyTaskbar;
import static java.lang.invoke.MethodHandles.Lookup.PROTECTED;
@@ -77,6 +76,7 @@
import android.view.WindowManager;
import android.widget.FrameLayout;
import android.widget.Toast;
+import android.window.DesktopExperienceFlags;
import android.window.DesktopModeFlags;
import android.window.RemoteTransition;
@@ -434,8 +434,9 @@
.setIsTransientTaskbar(true)
.build();
}
- mNavMode = (enableTaskbarConnectedDisplays() && !mIsPrimaryDisplay)
- ? NavigationMode.THREE_BUTTONS : DisplayController.getNavigationMode(this);
+ mNavMode = (DesktopExperienceFlags.ENABLE_TASKBAR_CONNECTED_DISPLAYS.isTrue()
+ && !mIsPrimaryDisplay) ? NavigationMode.THREE_BUTTONS
+ : DisplayController.getNavigationMode(this);
}
@@ -1533,9 +1534,9 @@
return new RemoteTransition(
new DesktopAppLaunchTransition(
this,
- getMainExecutor(),
appLaunchType,
- cujType
+ cujType,
+ getMainExecutor()
),
"TaskbarDesktopAppLaunch");
}
@@ -1558,7 +1559,10 @@
*/
private void launchFromInAppTaskbar(@Nullable RecentsView recents,
@Nullable View launchingIconView, List<? extends ItemInfo> itemInfos) {
- if (recents == null) {
+ boolean launchedFromExternalDisplay =
+ DesktopExperienceFlags.ENABLE_TASKBAR_CONNECTED_DISPLAYS.isTrue()
+ && !mIsPrimaryDisplay;
+ if (recents == null && !launchedFromExternalDisplay) {
return;
}
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarManager.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarManager.java
index 96bcffd..46802c3 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarManager.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarManager.java
@@ -32,7 +32,6 @@
import static com.android.launcher3.util.FlagDebugUtils.formatFlagChange;
import static com.android.quickstep.util.SystemActionConstants.ACTION_SHOW_TASKBAR;
import static com.android.quickstep.util.SystemActionConstants.SYSTEM_ACTION_ID_TASKBAR;
-import static com.android.window.flags.Flags.enableTaskbarConnectedDisplays;
import android.annotation.SuppressLint;
import android.app.PendingIntent;
@@ -53,6 +52,7 @@
import android.view.MotionEvent;
import android.view.WindowManager;
import android.widget.FrameLayout;
+import android.window.DesktopExperienceFlags;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
@@ -118,11 +118,12 @@
Settings.Secure.NAV_BAR_KIDS_MODE);
private final Context mBaseContext;
+ private TaskbarNavButtonCallbacks mNavCallbacks;
// TODO: Remove this during the connected displays lifecycle refactor.
private final Context mPrimaryWindowContext;
private WindowManager mPrimaryWindowManager;
- private final TaskbarNavButtonController mDefaultNavButtonController;
- private final ComponentCallbacks mDefaultComponentCallbacks;
+ private TaskbarNavButtonController mPrimaryNavButtonController;
+ private ComponentCallbacks mPrimaryComponentCallbacks;
private final SimpleBroadcastReceiver mShutdownReceiver;
@@ -140,6 +141,11 @@
private final SparseArray<FrameLayout> mRootLayouts = new SparseArray<>();
/** DisplayId - {@link Boolean} map indicating if RootLayout was added to window. */
private final SparseBooleanArray mAddedRootLayouts = new SparseBooleanArray();
+ /** DisplayId - {@link TaskbarNavButtonController} map for Connected Display. */
+ private final SparseArray<TaskbarNavButtonController> mNavButtonControllers =
+ new SparseArray<>();
+ /** DisplayId - {@link ComponentCallbacks} map for Connected Display. */
+ private final SparseArray<ComponentCallbacks> mComponentCallbacks = new SparseArray<>();
private StatefulActivity mActivity;
private RecentsViewContainer mRecentsViewContainer;
@@ -158,27 +164,35 @@
private class RecreationListener implements DisplayController.DisplayInfoChangeListener {
@Override
public void onDisplayInfoChanged(Context context, DisplayController.Info info, int flags) {
-
if ((flags & CHANGE_DENSITY) != 0) {
- Log.d(NULL_TASKBAR_ROOT_LAYOUT_TAG, "Display density changed");
+ debugTaskbarManager("onDisplayInfoChanged - Display density changed",
+ context.getDisplayId());
}
if ((flags & CHANGE_NAVIGATION_MODE) != 0) {
- Log.d(NULL_TASKBAR_ROOT_LAYOUT_TAG, "Navigation mode changed");
+ debugTaskbarManager("onDisplayInfoChanged - Navigation mode changed",
+ context.getDisplayId());
}
if ((flags & CHANGE_DESKTOP_MODE) != 0) {
- Log.d(NULL_TASKBAR_ROOT_LAYOUT_TAG, "Desktop mode changed");
+ debugTaskbarManager("onDisplayInfoChanged - Desktop mode changed",
+ context.getDisplayId());
}
if ((flags & CHANGE_TASKBAR_PINNING) != 0) {
- Log.d(NULL_TASKBAR_ROOT_LAYOUT_TAG, "Taskbar pinning changed");
+ debugTaskbarManager("onDisplayInfoChanged - Taskbar pinning changed",
+ context.getDisplayId());
}
if ((flags & (CHANGE_DENSITY | CHANGE_NAVIGATION_MODE | CHANGE_DESKTOP_MODE
| CHANGE_TASKBAR_PINNING)) != 0) {
+ debugTaskbarManager("onDisplayInfoChanged - Recreating Taskbar!",
+ context.getDisplayId());
recreateTaskbar();
}
}
}
- private final SettingsCache.OnChangeListener mOnSettingsChangeListener = c -> recreateTaskbar();
+ private final SettingsCache.OnChangeListener mOnSettingsChangeListener = c -> {
+ debugTaskbarManager("Settings changed! Recreating Taskbar!");
+ recreateTaskbar();
+ };
private boolean mUserUnlocked = false;
@@ -191,6 +205,7 @@
@Override
public void run() {
int displayId = getDefaultDisplayId();
+ debugTaskbarManager("mActivityOnDestroyCallback running!", displayId);
if (mActivity != null) {
displayId = mActivity.getDisplayId();
mActivity.removeOnDeviceProfileChangeListener(
@@ -204,7 +219,6 @@
mRecentsViewContainer = null;
}
mActivity = null;
- debugWhyTaskbarNotDestroyed("clearActivity");
TaskbarActivityContext taskbar = getTaskbarForDisplay(displayId);
if (taskbar != null) {
taskbar.setUIController(TaskbarUIController.DEFAULT);
@@ -217,28 +231,27 @@
new UnfoldTransitionProgressProvider.TransitionProgressListener() {
@Override
public void onTransitionStarted() {
- Log.d(TASKBAR_NOT_DESTROYED_TAG,
- "fold/unfold transition started getting called.");
+ debugTaskbarManager("fold/unfold transition started getting called.");
}
@Override
public void onTransitionProgress(float progress) {
- Log.d(TASKBAR_NOT_DESTROYED_TAG,
- "fold/unfold transition progress : " + progress);
+ debugTaskbarManager(
+ "fold/unfold transition progress getting called. | progress="
+ + progress);
}
@Override
public void onTransitionFinishing() {
- Log.d(TASKBAR_NOT_DESTROYED_TAG,
+ debugTaskbarManager(
"fold/unfold transition finishing getting called.");
}
@Override
public void onTransitionFinished() {
- Log.d(TASKBAR_NOT_DESTROYED_TAG,
+ debugTaskbarManager(
"fold/unfold transition finished getting called.");
-
}
};
@@ -250,20 +263,22 @@
RecentsDisplayModel recentsDisplayModel) {
mBaseContext = context;
mAllAppsActionManager = allAppsActionManager;
+ mNavCallbacks = navCallbacks;
mRecentsDisplayModel = recentsDisplayModel;
- mPrimaryWindowContext = createWindowContext(getDefaultDisplayId());
- if (enableTaskbarNoRecreate()) {
- mPrimaryWindowManager = mPrimaryWindowContext.getSystemService(WindowManager.class);
- createTaskbarRootLayout(getDefaultDisplayId());
- }
- mDefaultNavButtonController = createDefaultNavButtonController(context, navCallbacks);
- mDefaultComponentCallbacks = createDefaultComponentCallbacks();
+
+ // Set up primary display.
+ int primaryDisplayId = getDefaultDisplayId();
+ debugTaskbarManager("TaskbarManager constructor", primaryDisplayId);
+ mPrimaryWindowContext = createWindowContext(primaryDisplayId);
+ mPrimaryWindowManager = mPrimaryWindowContext.getSystemService(WindowManager.class);
+ createTaskbarRootLayout(primaryDisplayId);
+ createNavButtonController(primaryDisplayId);
+ createAndRegisterComponentCallbacks(primaryDisplayId);
+
SettingsCache.INSTANCE.get(mPrimaryWindowContext)
.register(USER_SETUP_COMPLETE_URI, mOnSettingsChangeListener);
SettingsCache.INSTANCE.get(mPrimaryWindowContext)
.register(NAV_BAR_KIDS_MODE, mOnSettingsChangeListener);
- Log.d(TASKBAR_NOT_DESTROYED_TAG, "registering component callbacks from constructor.");
- mPrimaryWindowContext.registerComponentCallbacks(mDefaultComponentCallbacks);
mShutdownReceiver =
new SimpleBroadcastReceiver(
mPrimaryWindowContext, UI_HELPER_EXECUTOR, i -> destroyAllTaskbars());
@@ -281,82 +296,10 @@
mTaskbarBroadcastReceiver.register(RECEIVER_NOT_EXPORTED, ACTION_SHOW_TASKBAR);
});
- debugWhyTaskbarNotDestroyed("TaskbarManager created");
+ debugTaskbarManager("TaskbarManager created");
recreateTaskbar();
}
- @NonNull
- private TaskbarNavButtonController createDefaultNavButtonController(Context context,
- TaskbarNavButtonCallbacks navCallbacks) {
- return new TaskbarNavButtonController(
- context,
- navCallbacks,
- SystemUiProxy.INSTANCE.get(mPrimaryWindowContext),
- new Handler(),
- new ContextualSearchInvoker(mPrimaryWindowContext));
- }
-
- private ComponentCallbacks createDefaultComponentCallbacks() {
- return new ComponentCallbacks() {
- private Configuration mOldConfig =
- mPrimaryWindowContext.getResources().getConfiguration();
-
- @Override
- public void onConfigurationChanged(Configuration newConfig) {
- Trace.instantForTrack(Trace.TRACE_TAG_APP, "TaskbarManager",
- "onConfigurationChanged: " + newConfig);
- debugWhyTaskbarNotDestroyed(
- "TaskbarManager#mComponentCallbacks.onConfigurationChanged: " + newConfig);
- // TODO: adapt this logic to be specific to different displays.
- DeviceProfile dp = mUserUnlocked
- ? LauncherAppState.getIDP(mPrimaryWindowContext).getDeviceProfile(
- mPrimaryWindowContext)
- : null;
- int configDiff = mOldConfig.diff(newConfig) & ~SKIP_RECREATE_CONFIG_CHANGES;
-
- if ((configDiff & ActivityInfo.CONFIG_UI_MODE) != 0) {
- Log.d(ILLEGAL_ARGUMENT_WM_ADD_VIEW, "onConfigurationChanged: theme changed");
- // Only recreate for theme changes, not other UI mode changes such as docking.
- int oldUiNightMode = (mOldConfig.uiMode & Configuration.UI_MODE_NIGHT_MASK);
- int newUiNightMode = (newConfig.uiMode & Configuration.UI_MODE_NIGHT_MASK);
- if (oldUiNightMode == newUiNightMode) {
- configDiff &= ~ActivityInfo.CONFIG_UI_MODE;
- }
- }
-
- debugWhyTaskbarNotDestroyed("ComponentCallbacks#onConfigurationChanged() "
- + "configDiff=" + Configuration.configurationDiffToString(configDiff));
- if (configDiff != 0 || getCurrentActivityContext() == null) {
- recreateTaskbar();
- } else {
- // Config change might be handled without re-creating the taskbar
- if (dp != null && !isTaskbarEnabled(dp)) {
- destroyDefaultTaskbar();
- } else {
- if (dp != null && isTaskbarEnabled(dp)) {
- if (ENABLE_TASKBAR_NAVBAR_UNIFICATION) {
- // Re-initialize for screen size change? Should this be done
- // by looking at screen-size change flag in configDiff in the
- // block above?
- recreateTaskbar();
- } else {
- getCurrentActivityContext().updateDeviceProfile(dp);
- }
- }
- getCurrentActivityContext().onConfigurationChanged(configDiff);
- }
- }
- mOldConfig = new Configuration(newConfig);
- // reset taskbar was pinned value, so we don't automatically unstash taskbar upon
- // user unfolding the device.
- mSharedState.setTaskbarWasPinned(false);
- }
-
- @Override
- public void onLowMemory() { }
- };
- }
-
private void destroyAllTaskbars() {
for (int i = 0; i < mTaskbars.size(); i++) {
int displayId = mTaskbars.keyAt(i);
@@ -365,23 +308,18 @@
}
}
- private void destroyDefaultTaskbar() {
- destroyTaskbarForDisplay(getDefaultDisplayId());
- }
-
private void destroyTaskbarForDisplay(int displayId) {
Log.d(ILLEGAL_ARGUMENT_WM_ADD_VIEW, "destroyTaskbarForDisplay: " + displayId);
TaskbarActivityContext taskbar = getTaskbarForDisplay(displayId);
- debugWhyTaskbarNotDestroyed("destroyTaskbarForDisplay: " + taskbar, displayId);
+ debugTaskbarManager("destroyTaskbarForDisplay: " + taskbar, displayId);
if (taskbar != null) {
taskbar.onDestroy();
// remove all defaults that we store
removeTaskbarFromMap(displayId);
}
- // make this display-specific
- DeviceProfile dp = mUserUnlocked ?
- LauncherAppState.getIDP(getWindowContext(displayId)).getDeviceProfile(
- getWindowContext(displayId)) : null;
+ // TODO (b/381113004): make this display-specific via getWindowContext()
+ DeviceProfile dp = mUserUnlocked ? LauncherAppState.getIDP(
+ mPrimaryWindowContext).getDeviceProfile(mPrimaryWindowContext) : null;
if (dp == null || !isTaskbarEnabled(dp)) {
removeTaskbarRootViewFromWindow(displayId);
}
@@ -431,7 +369,10 @@
DisplayController.INSTANCE.get(mPrimaryWindowContext).addChangeListener(
mRecreationListener);
recreateTaskbar();
- addTaskbarRootViewToWindow(getDefaultDisplayId());
+ for (int i = 0; i < mTaskbars.size(); i++) {
+ int displayId = mTaskbars.keyAt(i);
+ addTaskbarRootViewToWindow(displayId);
+ }
}
/**
@@ -443,7 +384,7 @@
}
removeActivityCallbacksAndListeners();
mActivity = activity;
- debugWhyTaskbarNotDestroyed("Set mActivity=" + mActivity);
+ debugTaskbarManager("Set mActivity=" + mActivity);
mActivity.addOnDeviceProfileChangeListener(mDebugActivityDeviceProfileChanged);
Log.d(TASKBAR_NOT_DESTROYED_TAG,
"registering activity lifecycle callbacks from setActivity().");
@@ -492,7 +433,7 @@
return ql.getUnfoldTransitionProgressProvider();
}
} else {
- return SystemUiProxy.INSTANCE.get(mPrimaryWindowContext).getUnfoldTransitionProvider();
+ return SystemUiProxy.INSTANCE.get(mBaseContext).getUnfoldTransitionProvider();
}
return null;
}
@@ -530,13 +471,21 @@
/**
* This method is called multiple times (ex. initial init, then when user unlocks) in which case
- * we fully want to destroy the existing default display's taskbar and create a new one.
+ * we fully want to destroy existing taskbars and create all desired new ones.
* In other case (folding/unfolding) we don't need to remove and add window.
*/
@VisibleForTesting
public synchronized void recreateTaskbar() {
- // TODO: make this recreate all taskbars in map.
- recreateTaskbarForDisplay(getDefaultDisplayId());
+ // Handles initial creation case.
+ if (mTaskbars.size() == 0) {
+ recreateTaskbarForDisplay(getDefaultDisplayId());
+ return;
+ }
+
+ for (int i = 0; i < mTaskbars.size(); i++) {
+ int displayId = mTaskbars.keyAt(i);
+ recreateTaskbarForDisplay(displayId);
+ }
}
/**
@@ -545,13 +494,12 @@
* In other case (folding/unfolding) we don't need to remove and add window.
*/
private void recreateTaskbarForDisplay(int displayId) {
- Trace.beginSection("recreateTaskbar");
+ Trace.beginSection("recreateTaskbarForDisplay");
try {
Log.d(ILLEGAL_ARGUMENT_WM_ADD_VIEW, "recreateTaskbarForDisplay: " + displayId);
- // TODO: make this code display specific
- DeviceProfile dp = mUserUnlocked ?
- LauncherAppState.getIDP(getWindowContext(displayId)).getDeviceProfile(
- getWindowContext(displayId)) : null;
+ // TODO (b/381113004): make this display-specific via getWindowContext()
+ DeviceProfile dp = mUserUnlocked ? LauncherAppState.getIDP(
+ mPrimaryWindowContext).getDeviceProfile(mPrimaryWindowContext) : null;
// All Apps action is unrelated to navbar unification, so we only need to check DP.
final boolean isLargeScreenTaskbar = dp != null && dp.isTaskbarPresent;
@@ -559,15 +507,17 @@
destroyTaskbarForDisplay(displayId);
+ boolean displayExists = getDisplay(displayId) != null;
boolean isTaskbarEnabled = dp != null && isTaskbarEnabled(dp);
- debugWhyTaskbarNotDestroyed("recreateTaskbar: isTaskbarEnabled=" + isTaskbarEnabled
+ debugTaskbarManager("recreateTaskbarForDisplay: isTaskbarEnabled=" + isTaskbarEnabled
+ " [dp != null (i.e. mUserUnlocked)]=" + (dp != null)
+ " FLAG_HIDE_NAVBAR_WINDOW=" + ENABLE_TASKBAR_NAVBAR_UNIFICATION
- + " dp.isTaskbarPresent=" + (dp == null ? "null" : dp.isTaskbarPresent));
- if (!isTaskbarEnabled || !isLargeScreenTaskbar) {
- SystemUiProxy.INSTANCE.get(mPrimaryWindowContext)
+ + " dp.isTaskbarPresent=" + (dp == null ? "null" : dp.isTaskbarPresent)
+ + " displayExists=" + displayExists);
+ if (!isTaskbarEnabled || !isLargeScreenTaskbar || !displayExists) {
+ SystemUiProxy.INSTANCE.get(mBaseContext)
.notifyTaskbarStatus(/* visible */ false, /* stashed */ false);
- if (!isTaskbarEnabled) {
+ if (!isTaskbarEnabled || !displayExists) {
return;
}
}
@@ -575,6 +525,11 @@
TaskbarActivityContext taskbar = getTaskbarForDisplay(displayId);
if (enableTaskbarNoRecreate() || taskbar == null) {
taskbar = createTaskbarActivityContext(dp, displayId);
+ if (taskbar == null) {
+ debugTaskbarManager(
+ "recreateTaskbarForDisplay: new taskbar instance is null!", displayId);
+ return;
+ }
} else {
taskbar.updateDeviceProfile(dp);
}
@@ -585,7 +540,8 @@
// Non default displays should not use LauncherTaskbarUIController as they shouldn't
// have access to the Launcher activity.
- if (enableTaskbarConnectedDisplays() && !isDefaultDisplay(displayId)) {
+ if (!isDefaultDisplay(displayId)
+ && DesktopExperienceFlags.ENABLE_TASKBAR_CONNECTED_DISPLAYS.isTrue()) {
taskbar.setUIController(createTaskbarUIControllerForNonDefaultDisplay(displayId));
} else if (mRecentsViewContainer != null) {
taskbar.setUIController(
@@ -622,8 +578,8 @@
}
public void onLongPressHomeEnabled(boolean assistantLongPressEnabled) {
- if (mDefaultNavButtonController != null) {
- mDefaultNavButtonController.setAssistantLongPressEnabled(assistantLongPressEnabled);
+ if (mPrimaryNavButtonController != null) {
+ mPrimaryNavButtonController.setAssistantLongPressEnabled(assistantLongPressEnabled);
}
}
@@ -746,13 +702,26 @@
* primary device or a previously mirroring display is switched to extended mode.
*/
public void onDisplayAddSystemDecorations(int displayId) {
- if (isDefaultDisplay(displayId) || !enableTaskbarConnectedDisplays()) {
+ Display display = getDisplay(displayId);
+ if (!DesktopExperienceFlags.ENABLE_TASKBAR_CONNECTED_DISPLAYS.isTrue() || isDefaultDisplay(
+ displayId) || display == null) {
+ debugTaskbarManager("onDisplayAddSystemDecorations: not adding display");
+ return;
+ }
+
+ // TODO (b/391965805): remove once onDisplayAddSystemDecorations is working.
+ WindowManager wm = getWindowManager(displayId);
+ if (wm == null || !wm.shouldShowSystemDecors(displayId)) {
return;
}
Context newWindowContext = createWindowContext(displayId);
if (newWindowContext != null) {
addWindowContextToMap(displayId, newWindowContext);
+ createTaskbarRootLayout(displayId);
+ createNavButtonController(displayId);
+ createAndRegisterComponentCallbacks(displayId);
+ recreateTaskbarForDisplay(displayId);
}
}
@@ -761,12 +730,16 @@
* removed from the primary device.
*/
public void onDisplayRemoved(int displayId) {
- if (isDefaultDisplay(displayId) || !enableTaskbarConnectedDisplays()) {
+ if (!DesktopExperienceFlags.ENABLE_TASKBAR_CONNECTED_DISPLAYS.isTrue() || isDefaultDisplay(
+ displayId)) {
return;
}
Context windowContext = getWindowContext(displayId);
if (windowContext != null) {
+ removeNavButtonController(displayId);
+ removeAndUnregisterComponentCallbacks(displayId);
+ destroyTaskbarForDisplay(displayId);
removeWindowContextFromMap(displayId);
}
}
@@ -796,7 +769,7 @@
*/
public void destroy() {
mRecentsViewContainer = null;
- debugWhyTaskbarNotDestroyed("TaskbarManager#destroy()");
+ debugTaskbarManager("TaskbarManager#destroy()");
removeActivityCallbacksAndListeners();
mTaskbarBroadcastReceiver.unregisterReceiverSafely();
@@ -809,7 +782,7 @@
SettingsCache.INSTANCE.get(mPrimaryWindowContext)
.unregister(NAV_BAR_KIDS_MODE, mOnSettingsChangeListener);
Log.d(TASKBAR_NOT_DESTROYED_TAG, "unregistering component callbacks from destroy().");
- mPrimaryWindowContext.unregisterComponentCallbacks(mDefaultComponentCallbacks);
+ removeAndUnregisterComponentCallbacks(getDefaultDisplayId());
mShutdownReceiver.unregisterReceiverSafely();
destroyAllTaskbars();
}
@@ -836,15 +809,20 @@
private void addTaskbarRootViewToWindow(int displayId) {
TaskbarActivityContext taskbar = getTaskbarForDisplay(displayId);
if (!enableTaskbarNoRecreate() || taskbar == null) {
- Log.d(NULL_TASKBAR_ROOT_LAYOUT_TAG,
- "addTaskbarRootViewToWindow - taskbar null | displayId=" + displayId);
+ debugTaskbarManager("addTaskbarRootViewToWindow - taskbar null", displayId);
+ return;
+ }
+
+ if (getDisplay(displayId) == null) {
+ debugTaskbarManager("addTaskbarRootViewToWindow - display null", displayId);
return;
}
if (!isTaskbarRootLayoutAddedForDisplay(displayId)) {
FrameLayout rootLayout = getTaskbarRootLayoutForDisplay(displayId);
- if (rootLayout != null) {
- getWindowManager(displayId).addView(rootLayout, taskbar.getWindowLayoutParams());
+ WindowManager windowManager = getWindowManager(displayId);
+ if (rootLayout != null && windowManager != null) {
+ windowManager.addView(rootLayout, taskbar.getWindowLayoutParams());
mAddedRootLayouts.put(displayId, true);
} else {
Log.d(ILLEGAL_ARGUMENT_WM_ADD_VIEW,
@@ -864,10 +842,14 @@
return;
}
- if (isTaskbarRootLayoutAddedForDisplay(displayId)) {
- getWindowManager(displayId).removeViewImmediate(rootLayout);
+ WindowManager windowManager = getWindowManager(displayId);
+ if (isTaskbarRootLayoutAddedForDisplay(displayId) && windowManager != null) {
+ windowManager.removeViewImmediate(rootLayout);
mAddedRootLayouts.put(displayId, false);
removeTaskbarRootLayoutFromMap(displayId);
+ } else {
+ debugTaskbarManager("removeTaskbarRootViewFromWindow - WindowManager is null",
+ displayId);
}
}
@@ -913,24 +895,164 @@
/**
* Creates a {@link TaskbarActivityContext} for the given display and adds it to the map.
+ * @param dp The {@link DeviceProfile} for the display.
+ * @param displayId The ID of the display.
*/
- private TaskbarActivityContext createTaskbarActivityContext(DeviceProfile dp, int displayId) {
- Display display = mBaseContext.getSystemService(DisplayManager.class).getDisplay(
- displayId);
- Context navigationBarPanelContext = ENABLE_TASKBAR_NAVBAR_UNIFICATION
- ? mBaseContext.createWindowContext(display, TYPE_NAVIGATION_BAR_PANEL, null)
- : null;
+ private @Nullable TaskbarActivityContext createTaskbarActivityContext(DeviceProfile dp,
+ int displayId) {
+ Display display = getDisplay(displayId);
+ if (display == null) {
+ debugTaskbarManager("createTaskbarActivityContext: display null", displayId);
+ return null;
+ }
+
+ Context navigationBarPanelContext = null;
+ if (ENABLE_TASKBAR_NAVBAR_UNIFICATION) {
+ navigationBarPanelContext = mBaseContext.createWindowContext(display,
+ TYPE_NAVIGATION_BAR_PANEL, null);
+ }
+
+ boolean isPrimaryDisplay = isDefaultDisplay(displayId)
+ || !DesktopExperienceFlags.ENABLE_TASKBAR_CONNECTED_DISPLAYS.isTrue();
TaskbarActivityContext newTaskbar = new TaskbarActivityContext(getWindowContext(displayId),
- navigationBarPanelContext, dp, mDefaultNavButtonController,
- mUnfoldProgressProvider, isDefaultDisplay(displayId),
- SystemUiProxy.INSTANCE.get(mPrimaryWindowContext));
+ navigationBarPanelContext, dp, getNavButtonController(displayId),
+ mUnfoldProgressProvider, isPrimaryDisplay,
+ SystemUiProxy.INSTANCE.get(mBaseContext));
addTaskbarToMap(displayId, newTaskbar);
return newTaskbar;
}
/**
+ * Create {@link ComponentCallbacks} for the given display and register it to the relevant
+ * WindowContext. For external displays, populate maps.
+ * @param displayId The ID of the display.
+ */
+ private void createAndRegisterComponentCallbacks(int displayId) {
+ ComponentCallbacks callbacks = new ComponentCallbacks() {
+ private Configuration mOldConfig =
+ getWindowContext(displayId).getResources().getConfiguration();
+
+ @Override
+ public void onConfigurationChanged(Configuration newConfig) {
+ Trace.instantForTrack(Trace.TRACE_TAG_APP, "TaskbarManager",
+ "onConfigurationChanged: " + newConfig);
+ debugTaskbarManager(
+ "TaskbarManager#mComponentCallbacks.onConfigurationChanged: " + newConfig);
+ // TODO (b/381113004): make this display-specific via getWindowContext()
+ DeviceProfile dp = mUserUnlocked ? LauncherAppState.getIDP(
+ mPrimaryWindowContext).getDeviceProfile(mPrimaryWindowContext) : null;
+ int configDiff = mOldConfig.diff(newConfig) & ~SKIP_RECREATE_CONFIG_CHANGES;
+
+ if ((configDiff & ActivityInfo.CONFIG_UI_MODE) != 0) {
+ Log.d(ILLEGAL_ARGUMENT_WM_ADD_VIEW, "onConfigurationChanged: theme changed");
+ // Only recreate for theme changes, not other UI mode changes such as docking.
+ int oldUiNightMode = (mOldConfig.uiMode & Configuration.UI_MODE_NIGHT_MASK);
+ int newUiNightMode = (newConfig.uiMode & Configuration.UI_MODE_NIGHT_MASK);
+ if (oldUiNightMode == newUiNightMode) {
+ configDiff &= ~ActivityInfo.CONFIG_UI_MODE;
+ }
+ }
+
+ debugTaskbarManager("ComponentCallbacks#onConfigurationChanged() "
+ + "configDiff=" + Configuration.configurationDiffToString(configDiff));
+ if (configDiff != 0 || getCurrentActivityContext() == null) {
+ recreateTaskbar();
+ } else {
+ // Config change might be handled without re-creating the taskbar
+ if (dp != null && !isTaskbarEnabled(dp)) {
+ destroyTaskbarForDisplay(getDefaultDisplayId());
+ } else {
+ if (dp != null && isTaskbarEnabled(dp)) {
+ if (ENABLE_TASKBAR_NAVBAR_UNIFICATION) {
+ // Re-initialize for screen size change? Should this be done
+ // by looking at screen-size change flag in configDiff in the
+ // block above?
+ recreateTaskbar();
+ } else {
+ getCurrentActivityContext().updateDeviceProfile(dp);
+ }
+ }
+ getCurrentActivityContext().onConfigurationChanged(configDiff);
+ }
+ }
+ mOldConfig = new Configuration(newConfig);
+ // reset taskbar was pinned value, so we don't automatically unstash taskbar upon
+ // user unfolding the device.
+ mSharedState.setTaskbarWasPinned(false);
+ }
+
+ @Override
+ public void onLowMemory() { }
+ };
+ if (isDefaultDisplay(displayId)
+ || !DesktopExperienceFlags.ENABLE_TASKBAR_CONNECTED_DISPLAYS.isTrue()) {
+ mPrimaryComponentCallbacks = callbacks;
+ mPrimaryWindowContext.registerComponentCallbacks(callbacks);
+ } else {
+ mComponentCallbacks.put(displayId, callbacks);
+ getWindowContext(displayId).registerComponentCallbacks(callbacks);
+ }
+ }
+
+ /**
+ * Unregister {@link ComponentCallbacks} for the given display from its WindowContext. For
+ * external displays, remove from the map.
+ * @param displayId The ID of the display.
+ */
+ private void removeAndUnregisterComponentCallbacks(int displayId) {
+ if (isDefaultDisplay(displayId)
+ || !DesktopExperienceFlags.ENABLE_TASKBAR_CONNECTED_DISPLAYS.isTrue()) {
+ mPrimaryWindowContext.unregisterComponentCallbacks(mPrimaryComponentCallbacks);
+ } else {
+ ComponentCallbacks callbacks = mComponentCallbacks.get(displayId);
+ getWindowContext(displayId).unregisterComponentCallbacks(callbacks);
+ mComponentCallbacks.delete(displayId);
+ }
+ }
+
+ /**
+ * Creates a {@link TaskbarNavButtonController} for the given display and adds it to the map
+ * if it doesn't already exist.
+ * @param displayId The ID of the display
+ */
+ private void createNavButtonController(int displayId) {
+ if (isDefaultDisplay(displayId)
+ || !DesktopExperienceFlags.ENABLE_TASKBAR_CONNECTED_DISPLAYS.isTrue()) {
+ mPrimaryNavButtonController = new TaskbarNavButtonController(
+ mPrimaryWindowContext,
+ mNavCallbacks,
+ SystemUiProxy.INSTANCE.get(mBaseContext),
+ new Handler(),
+ new ContextualSearchInvoker(mBaseContext));
+ } else {
+ TaskbarNavButtonController navButtonController = new TaskbarNavButtonController(
+ getWindowContext(displayId),
+ mNavCallbacks,
+ SystemUiProxy.INSTANCE.get(mBaseContext),
+ new Handler(),
+ new ContextualSearchInvoker(mBaseContext));
+ mNavButtonControllers.put(displayId, navButtonController);
+ }
+ }
+
+ private TaskbarNavButtonController getNavButtonController(int displayId) {
+ return (isDefaultDisplay(displayId)
+ || !DesktopExperienceFlags.ENABLE_TASKBAR_CONNECTED_DISPLAYS.isTrue())
+ ? mPrimaryNavButtonController : mNavButtonControllers.get(displayId);
+ }
+
+ private void removeNavButtonController(int displayId) {
+ if (isDefaultDisplay(displayId)
+ || !DesktopExperienceFlags.ENABLE_TASKBAR_CONNECTED_DISPLAYS.isTrue()) {
+ mPrimaryNavButtonController = null;
+ } else {
+ mNavButtonControllers.delete(displayId);
+ }
+ }
+
+ /**
* Adds the {@link TaskbarActivityContext} associated with the given display ID to taskbar
* map if there is not already a taskbar mapped to that displayId.
*
@@ -958,12 +1080,16 @@
*/
private void createTaskbarRootLayout(int displayId) {
Log.d(ILLEGAL_ARGUMENT_WM_ADD_VIEW, "createTaskbarRootLayout: " + displayId);
+ if (!enableTaskbarNoRecreate()) {
+ return;
+ }
+
FrameLayout newTaskbarRootLayout = new FrameLayout(getWindowContext(displayId)) {
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
// The motion events can be outside the view bounds of task bar, and hence
// manually dispatching them to the drag layer here.
- TaskbarActivityContext taskbar = getTaskbarForDisplay(getDefaultDisplayId());
+ TaskbarActivityContext taskbar = getTaskbarForDisplay(displayId);
if (taskbar != null && taskbar.getDragLayer().isAttachedToWindow()) {
return taskbar.getDragLayer().dispatchTouchEvent(ev);
}
@@ -1029,13 +1155,10 @@
* @param displayId The ID of the display for which to create the window context.
*/
private @Nullable Context createWindowContext(int displayId) {
- DisplayManager displayManager = mBaseContext.getSystemService(DisplayManager.class);
- if (displayManager == null) {
- return null;
- }
-
- Display display = displayManager.getDisplay(displayId);
+ debugTaskbarManager("createWindowContext: " + displayId);
+ Display display = getDisplay(displayId);
if (display == null) {
+ debugTaskbarManager("createWindowContext: display null", displayId);
return null;
}
@@ -1043,10 +1166,29 @@
if (ENABLE_TASKBAR_NAVBAR_UNIFICATION && isDefaultDisplay(displayId)) {
windowType = TYPE_NAVIGATION_BAR;
}
+ debugTaskbarManager(
+ "createWindowContext: windowType=" + ((windowType == TYPE_NAVIGATION_BAR)
+ ? "TYPE_NAVIGATION_BAR" : "TYPE_NAVIGATION_BAR_PANEL"), displayId);
return mBaseContext.createWindowContext(display, windowType, null);
}
+ private @Nullable Display getDisplay(int displayId) {
+ DisplayManager displayManager = mBaseContext.getSystemService(DisplayManager.class);
+ if (displayManager == null) {
+ debugTaskbarManager("cannot get DisplayManager", displayId);
+ return null;
+ }
+
+ Display display = displayManager.getDisplay(displayId);
+ if (display == null) {
+ debugTaskbarManager("Cannot get display!", displayId);
+ return null;
+ }
+
+ return displayManager.getDisplay(displayId);
+ }
+
/**
* Retrieves the window context of the taskbar for the specified display.
*
@@ -1054,7 +1196,8 @@
* @return The Window Context {@link Context} for a given display or {@code null}.
*/
private Context getWindowContext(int displayId) {
- return (isDefaultDisplay(displayId) || !enableTaskbarConnectedDisplays())
+ return (isDefaultDisplay(displayId)
+ || !DesktopExperienceFlags.ENABLE_TASKBAR_CONNECTED_DISPLAYS.isTrue())
? mPrimaryWindowContext : mWindowContexts.get(displayId);
}
@@ -1070,12 +1213,15 @@
* @return The window manager {@link WindowManager} for a given display or {@code null}.
*/
private @Nullable WindowManager getWindowManager(int displayId) {
- if (isDefaultDisplay(displayId) || !enableTaskbarConnectedDisplays()) {
+ if (isDefaultDisplay(displayId)
+ || !DesktopExperienceFlags.ENABLE_TASKBAR_CONNECTED_DISPLAYS.isTrue()) {
+ debugTaskbarManager("cannot get mPrimaryWindowManager", displayId);
return mPrimaryWindowManager;
}
Context externalDisplayContext = getWindowContext(displayId);
if (externalDisplayContext == null) {
+ debugTaskbarManager("cannot get externalDisplayContext", displayId);
return null;
}
@@ -1110,18 +1256,20 @@
}
/** Temp logs for b/254119092. */
- public void debugWhyTaskbarNotDestroyed(String debugReason) {
- debugWhyTaskbarNotDestroyed(debugReason, getDefaultDisplayId());
+ public void debugTaskbarManager(String debugReason) {
+ debugTaskbarManager(debugReason, getDefaultDisplayId());
}
/** Temp logs for b/254119092. */
- public void debugWhyTaskbarNotDestroyed(String debugReason, int displayId) {
+ public void debugTaskbarManager(String debugReason, int displayId) {
StringJoiner log = new StringJoiner("\n");
- log.add(debugReason + " displayId=" + displayId);
+ log.add(debugReason + " displayId=" + displayId + " isDefaultDisplay=" + isDefaultDisplay(
+ displayId));
boolean activityTaskbarPresent = mActivity != null
&& mActivity.getDeviceProfile().isTaskbarPresent;
- Context windowContext = getWindowContext(displayId);
+ // TODO (b/381113004): make this display-specific via getWindowContext()
+ Context windowContext = mPrimaryWindowContext;
if (windowContext == null) {
log.add("window context for displayId" + displayId);
return;
@@ -1160,6 +1308,6 @@
}
private final DeviceProfile.OnDeviceProfileChangeListener mDebugActivityDeviceProfileChanged =
- dp -> debugWhyTaskbarNotDestroyed("mActivity onDeviceProfileChanged");
+ dp -> debugTaskbarManager("mActivity onDeviceProfileChanged");
}
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarUIController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarUIController.java
index e5d642d..89bcb41 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarUIController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarUIController.java
@@ -283,7 +283,7 @@
foundTask,
taskContainer.getIconView().getDrawable(),
taskContainer.getSnapshotView(),
- taskContainer.getSplitAnimationThumbnail(),
+ taskContainer.getThumbnail(),
null /* intent */,
null /* user */,
info);
diff --git a/quickstep/src/com/android/launcher3/taskbar/TypefaceUtils.kt b/quickstep/src/com/android/launcher3/taskbar/TypefaceUtils.kt
new file mode 100644
index 0000000..fa551b8
--- /dev/null
+++ b/quickstep/src/com/android/launcher3/taskbar/TypefaceUtils.kt
@@ -0,0 +1,47 @@
+/*
+ * 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.launcher3.taskbar
+
+import android.graphics.Typeface
+import android.widget.TextView
+import com.android.launcher3.Flags
+
+/**
+ * Helper util class to set pre-defined typefaces to textviews
+ *
+ * If the typeface font family is already defined here, you can just reuse it directly. Otherwise,
+ * please define it here for future use. You do not need to define the font style. If you need
+ * anything other than [Typeface.NORMAL], pass it inline when calling [setTypeface]
+ */
+class TypefaceUtils {
+
+ companion object {
+ const val FONT_FAMILY_HEADLINE_LARGE_EMPHASIZED = "variable-headline-large-emphasized"
+ const val FONT_FAMILY_LABEL_LARGE_BASELINE = "variable-label-large"
+
+ @JvmStatic
+ @JvmOverloads
+ fun setTypeface(
+ textView: TextView?,
+ fontFamilyName: String,
+ fontStyle: Int = Typeface.NORMAL,
+ ) {
+ if (!Flags.expressiveThemeInTaskbarAndNavigation()) return
+ textView?.typeface = Typeface.create(fontFamilyName, fontStyle)
+ }
+ }
+}
diff --git a/quickstep/src/com/android/launcher3/uioverrides/PredictedAppIcon.java b/quickstep/src/com/android/launcher3/uioverrides/PredictedAppIcon.java
index b25f999..37b8dc7 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/PredictedAppIcon.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/PredictedAppIcon.java
@@ -51,7 +51,7 @@
import com.android.launcher3.anim.AnimatorListeners;
import com.android.launcher3.celllayout.CellLayoutLayoutParams;
import com.android.launcher3.celllayout.DelegatedCellDrawing;
-import com.android.launcher3.graphics.IconShape;
+import com.android.launcher3.graphics.ThemeManager;
import com.android.launcher3.icons.FastBitmapDrawable;
import com.android.launcher3.icons.IconNormalizer;
import com.android.launcher3.icons.LauncherIcons;
@@ -142,7 +142,7 @@
int shadowSize = context.getResources().getDimensionPixelSize(
R.dimen.blur_size_thin_outline);
mShadowFilter = new BlurMaskFilter(shadowSize, BlurMaskFilter.Blur.OUTER);
- mShapePath = IconShape.INSTANCE.get(context).getShape().getPath(mNormalizedIconSize);
+ mShapePath = ThemeManager.INSTANCE.get(context).getIconShape().getPath(mNormalizedIconSize);
}
@Override
diff --git a/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java b/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java
index bb57d6e..019e746 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java
@@ -1380,7 +1380,7 @@
SystemUiProxy.INSTANCE.get(this).setLauncherAppIconSize(mDeviceProfile.iconSizePx);
TaskbarManager taskbarManager = mTISBindHelper.getTaskbarManager();
if (taskbarManager != null) {
- taskbarManager.debugWhyTaskbarNotDestroyed("QuickstepLauncher#onDeviceProfileChanged");
+ taskbarManager.debugTaskbarManager("QuickstepLauncher#onDeviceProfileChanged");
}
}
diff --git a/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/TaskViewDismissTouchController.kt b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/TaskViewDismissTouchController.kt
index 98737a5..88b7155 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/TaskViewDismissTouchController.kt
+++ b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/TaskViewDismissTouchController.kt
@@ -16,9 +16,11 @@
package com.android.launcher3.uioverrides.touchcontrollers
import android.content.Context
+import android.graphics.Rect
import android.view.MotionEvent
import androidx.dynamicanimation.animation.SpringAnimation
import com.android.app.animation.Interpolators.DECELERATE
+import com.android.app.animation.Interpolators.LINEAR
import com.android.launcher3.AbstractFloatingView
import com.android.launcher3.R
import com.android.launcher3.Utilities.EDGE_NAV_BAR
@@ -29,6 +31,7 @@
import com.android.launcher3.util.MSDLPlayerWrapper
import com.android.launcher3.util.TouchController
import com.android.quickstep.views.RecentsView
+import com.android.quickstep.views.RecentsView.RECENTS_SCALE_PROPERTY
import com.android.quickstep.views.RecentsViewContainer
import com.android.quickstep.views.TaskView
import com.google.android.msdl.data.model.MSDLToken
@@ -50,6 +53,7 @@
recentsView.pagedOrientationHandler.upDownSwipeDirection,
)
private val isRtl = isRtl(container.resources)
+ private val tempTaskThumbnailBounds = Rect()
private var taskBeingDragged: TaskView? = null
private var springAnimation: SpringAnimation? = null
@@ -57,6 +61,7 @@
private var verticalFactor: Int = 0
private var hasDismissThresholdHapticRun = false
private var initialDisplacement: Float = 0f
+ private var recentsScaleAnimation: SpringAnimation? = null
private fun canInterceptTouch(ev: MotionEvent): Boolean =
when {
@@ -98,6 +103,7 @@
private fun onActionDown(ev: MotionEvent): Boolean {
springAnimation?.cancel()
+ recentsScaleAnimation?.cancel()
if (!canInterceptTouch(ev)) {
return false
}
@@ -108,7 +114,9 @@
recentsView.isTaskViewVisible(it) && container.dragLayer.isEventOverView(it, ev)
}
?.also {
- dismissLength = recentsView.pagedOrientationHandler.getSecondaryDimension(it)
+ // Dismiss length as bottom of task so it is fully off screen when dismissed.
+ it.getThumbnailBounds(tempTaskThumbnailBounds, relativeToDragLayer = true)
+ dismissLength = tempTaskThumbnailBounds.bottom
verticalFactor =
recentsView.pagedOrientationHandler.secondaryTranslationDirectionFactor
}
@@ -162,6 +170,8 @@
}
recentsView.redrawLiveTile()
}
+ val dismissFraction = displacement / (dismissLength * verticalFactor).toFloat()
+ RECENTS_SCALE_PROPERTY.setValue(recentsView, getRecentsScale(dismissFraction))
playDismissThresholdHaptic(displacement)
return true
}
@@ -216,6 +226,10 @@
if (isDismissing) (dismissLength * verticalFactor).toFloat() else 0f
)
}
+ recentsScaleAnimation =
+ recentsView.animateRecentsScale(RECENTS_SCALE_DEFAULT).addEndListener { _, _, _, _ ->
+ recentsScaleAnimation = null
+ }
}
// Returns if the current task being dragged is towards "positive" (e.g. dismissal).
@@ -230,8 +244,54 @@
springAnimation = null
}
+ private fun getRecentsScale(dismissFraction: Float): Float {
+ return when {
+ // Do not scale recents when dragging below origin.
+ dismissFraction <= 0 -> {
+ RECENTS_SCALE_DEFAULT
+ }
+ // Initially scale recents as the drag begins, up to the first threshold.
+ dismissFraction < RECENTS_SCALE_FIRST_THRESHOLD_FRACTION -> {
+ mapToRange(
+ dismissFraction,
+ 0f,
+ RECENTS_SCALE_FIRST_THRESHOLD_FRACTION,
+ RECENTS_SCALE_DEFAULT,
+ RECENTS_SCALE_ON_DISMISS_CANCEL,
+ LINEAR,
+ )
+ }
+ // Keep scale consistent until dragging to the dismiss threshold.
+ dismissFraction < RECENTS_SCALE_DISMISS_THRESHOLD_FRACTION -> {
+ RECENTS_SCALE_ON_DISMISS_CANCEL
+ }
+ // Scale beyond the dismiss threshold again, to indicate dismiss will occur on release.
+ dismissFraction < RECENTS_SCALE_SECOND_THRESHOLD_FRACTION -> {
+ mapToRange(
+ dismissFraction,
+ RECENTS_SCALE_DISMISS_THRESHOLD_FRACTION,
+ RECENTS_SCALE_SECOND_THRESHOLD_FRACTION,
+ RECENTS_SCALE_ON_DISMISS_CANCEL,
+ RECENTS_SCALE_ON_DISMISS_SUCCESS,
+ LINEAR,
+ )
+ }
+ // Keep scale beyond the dismiss threshold scaling consistent.
+ else -> {
+ RECENTS_SCALE_ON_DISMISS_SUCCESS
+ }
+ }
+ }
+
companion object {
private const val DISMISS_THRESHOLD_FRACTION = 0.5f
private const val DISMISS_THRESHOLD_HAPTIC_RANGE = 10f
+
+ private const val RECENTS_SCALE_ON_DISMISS_CANCEL = 0.9875f
+ private const val RECENTS_SCALE_ON_DISMISS_SUCCESS = 0.975f
+ private const val RECENTS_SCALE_DEFAULT = 1f
+ private const val RECENTS_SCALE_FIRST_THRESHOLD_FRACTION = 0.2f
+ private const val RECENTS_SCALE_DISMISS_THRESHOLD_FRACTION = 0.5f
+ private const val RECENTS_SCALE_SECOND_THRESHOLD_FRACTION = 0.575f
}
}
diff --git a/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java b/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java
index b43c3ac..3640e1f 100644
--- a/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java
+++ b/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java
@@ -1112,6 +1112,9 @@
public void onGestureEnded(float endVelocityPxPerMs, PointF velocityPxPerMs) {
float flingThreshold = mContext.getResources()
.getDimension(R.dimen.quickstep_fling_threshold_speed);
+ Log.d(TAG, "onGestureEnded: mGestureStarted=" + mGestureStarted
+ + ", mIsMotionPaused=" + mIsMotionPaused
+ + ", flingThresholdPassed=" + (Math.abs(endVelocityPxPerMs) > flingThreshold));
boolean isFling = mGestureStarted && !mIsMotionPaused
&& Math.abs(endVelocityPxPerMs) > flingThreshold;
mStateCallback.setStateOnUiThread(STATE_GESTURE_COMPLETED);
@@ -1260,12 +1263,12 @@
dpiFromPx(velocityPxPerMs.x),
dpiFromPx(velocityPxPerMs.y),
Math.toDegrees(Math.atan2(-velocityPxPerMs.y, velocityPxPerMs.x)));
-
if (mGestureState.isHandlingAtomicEvent()) {
// Button mode, this is only used to go to recents.
return RECENTS;
}
+ Log.d(TAG, "calculateEndTarget: isCancel=" + isCancel + ", isFlingY=" + isFlingY);
GestureEndTarget endTarget;
if (isCancel) {
endTarget = LAST_TASK;
@@ -1275,6 +1278,7 @@
endTarget = calculateEndTargetForNonFling(velocityPxPerMs);
}
+ Log.d(TAG, "calculateEndTarget: endTarget(1)=" + endTarget);
if (mDeviceState.isOverviewDisabled() && endTarget == RECENTS) {
return LAST_TASK;
}
@@ -1293,6 +1297,7 @@
return LAST_TASK;
}
}
+ Log.d(TAG, "calculateEndTarget: endTarget(2)=" + endTarget);
return endTarget;
}
@@ -1301,9 +1306,12 @@
final boolean willGoToNewTask =
isScrollingToNewTask() && Math.abs(velocity.x) > Math.abs(endVelocity);
final boolean isSwipeUp = endVelocity < 0;
+ Log.d(TAG, "calculateEndTargetForFlingY: willGoToNewTask=" + willGoToNewTask
+ + ", isSwipeUp=" + isSwipeUp);
if (!isSwipeUp) {
final boolean isCenteredOnNewTask = mRecentsView != null
&& mRecentsView.getDestinationPage() != mRecentsView.getRunningTaskIndex();
+ Log.d(TAG, "calculateEndTargetForFlingY: isCenteredOnNewTask=" + isCenteredOnNewTask);
return willGoToNewTask || isCenteredOnNewTask ? NEW_TASK : LAST_TASK;
}
@@ -1316,6 +1324,9 @@
// Fully gestural mode.
final boolean isFlingX = Math.abs(velocity.x) > mContext.getResources()
.getDimension(R.dimen.quickstep_fling_threshold_speed);
+ Log.d(TAG, "calculateEndTargetForNonFling: isScrollingToNewTask=" + isScrollingToNewTask
+ + ", isFlingX=" + isFlingX
+ + ", mIsMotionPaused=" + mIsMotionPaused);
if (isScrollingToNewTask && isFlingX) {
// Flinging towards new task takes precedence over mIsMotionPaused (which only
// checks y-velocity).
@@ -1325,6 +1336,7 @@
} else if (isScrollingToNewTask) {
return NEW_TASK;
}
+ Log.d(TAG, "calculateEndTargetForNonFling: mCanSlowSwipeGoHome=" + mCanSlowSwipeGoHome);
return velocity.y < 0 && mCanSlowSwipeGoHome ? HOME : LAST_TASK;
}
diff --git a/quickstep/src/com/android/quickstep/RecentsAnimationCallbacks.java b/quickstep/src/com/android/quickstep/RecentsAnimationCallbacks.java
index c276447..d7152b5 100644
--- a/quickstep/src/com/android/quickstep/RecentsAnimationCallbacks.java
+++ b/quickstep/src/com/android/quickstep/RecentsAnimationCallbacks.java
@@ -21,6 +21,7 @@
import static android.view.WindowManager.LayoutParams.TYPE_DOCK_DIVIDER;
import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
+import static com.android.wm.shell.shared.TransitionUtil.TYPE_SPLIT_SCREEN_DIM_LAYER;
import android.annotation.Nullable;
import android.graphics.Rect;
@@ -190,7 +191,8 @@
ArrayList<RemoteAnimationTarget> apps, ArrayList<RemoteAnimationTarget> nonApps) {
for (int i = 0; i < appTargets.length; i++) {
RemoteAnimationTarget target = appTargets[i];
- if (target.windowType == TYPE_DOCK_DIVIDER) {
+ if (target.windowType == TYPE_DOCK_DIVIDER
+ || target.windowType == TYPE_SPLIT_SCREEN_DIM_LAYER) {
nonApps.add(target);
} else {
apps.add(target);
diff --git a/quickstep/src/com/android/quickstep/TaskShortcutFactory.java b/quickstep/src/com/android/quickstep/TaskShortcutFactory.java
index 7990aae..a69d472 100644
--- a/quickstep/src/com/android/quickstep/TaskShortcutFactory.java
+++ b/quickstep/src/com/android/quickstep/TaskShortcutFactory.java
@@ -236,15 +236,14 @@
position[0] + width, position[1] + height);
// Take the thumbnail of the task without a scrim and apply it back after
- // TODO(b/348643341) add ability to get override the scrim for this Bitmap retrieval
- float alpha = 0f;
- if (!enableRefactorTaskThumbnail()) {
- alpha = mTaskContainer.getThumbnailViewDeprecated().getDimAlpha();
+ Bitmap thumbnail;
+ if (enableRefactorTaskThumbnail()) {
+ thumbnail = mTaskContainer.getThumbnail();
+ } else {
+ float alpha = mTaskContainer.getThumbnailViewDeprecated().getDimAlpha();
mTaskContainer.getThumbnailViewDeprecated().setDimAlpha(0);
- }
- Bitmap thumbnail = RecentsTransition.drawViewIntoHardwareBitmap(
- taskBounds.width(), taskBounds.height(), snapShotView, 1f, Color.BLACK);
- if (!enableRefactorTaskThumbnail()) {
+ thumbnail = RecentsTransition.drawViewIntoHardwareBitmap(
+ taskBounds.width(), taskBounds.height(), snapShotView, 1f, Color.BLACK);
mTaskContainer.getThumbnailViewDeprecated().setDimAlpha(alpha);
}
diff --git a/quickstep/src/com/android/quickstep/TaskViewUtils.java b/quickstep/src/com/android/quickstep/TaskViewUtils.java
index 37841e8..35a6e72 100644
--- a/quickstep/src/com/android/quickstep/TaskViewUtils.java
+++ b/quickstep/src/com/android/quickstep/TaskViewUtils.java
@@ -39,6 +39,7 @@
import static com.android.launcher3.Utilities.getDescendantCoordRelativeToAncestor;
import static com.android.launcher3.util.MultiPropertyFactory.MULTI_PROPERTY_VALUE;
import static com.android.quickstep.util.AnimUtils.clampToDuration;
+import static com.android.wm.shell.shared.TransitionUtil.TYPE_SPLIT_SCREEN_DIM_LAYER;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
@@ -732,7 +733,9 @@
List<SurfaceControl> auxiliarySurfaces = new ArrayList<>();
for (RemoteAnimationTarget target : nonApps) {
final SurfaceControl leash = target.leash;
- if (target.windowType == TYPE_DOCK_DIVIDER && leash != null && leash.isValid()) {
+ if ((target.windowType == TYPE_DOCK_DIVIDER
+ || target.windowType == TYPE_SPLIT_SCREEN_DIM_LAYER)
+ && leash != null && leash.isValid()) {
auxiliarySurfaces.add(leash);
}
}
diff --git a/quickstep/src/com/android/quickstep/recents/di/RecentsDependencies.kt b/quickstep/src/com/android/quickstep/recents/di/RecentsDependencies.kt
index d2f10b6..4b3bef3 100644
--- a/quickstep/src/com/android/quickstep/recents/di/RecentsDependencies.kt
+++ b/quickstep/src/com/android/quickstep/recents/di/RecentsDependencies.kt
@@ -28,15 +28,14 @@
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.GetThumbnailPositionUseCase
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.viewmodel.TaskOverlayViewModel
-import com.android.quickstep.task.viewmodel.TaskThumbnailViewModel
-import com.android.quickstep.task.viewmodel.TaskThumbnailViewModelImpl
import com.android.systemui.shared.recents.model.Task
+import com.android.systemui.shared.recents.utilities.PreviewPositionHelper
import kotlinx.coroutines.CoroutineName
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.SupervisorJob
@@ -173,8 +172,6 @@
val instance: Any =
when (modelClass) {
RecentsViewData::class.java -> RecentsViewData()
- TaskThumbnailViewModel::class.java ->
- TaskThumbnailViewModelImpl(getThumbnailPositionUseCase = inject())
TaskOverlayViewModel::class.java -> {
val task = extras["Task"] as Task
TaskOverlayViewModel(
@@ -194,7 +191,7 @@
GetThumbnailPositionUseCase(
deviceProfileRepository = inject(),
rotationStateRepository = inject(),
- tasksRepository = inject(),
+ previewPositionHelper = PreviewPositionHelper(),
)
OrganizeDesktopTasksUseCase::class.java -> OrganizeDesktopTasksUseCase()
else -> {
diff --git a/quickstep/src/com/android/quickstep/recents/usecase/GetThumbnailPositionUseCase.kt b/quickstep/src/com/android/quickstep/recents/domain/usecase/GetThumbnailPositionUseCase.kt
similarity index 62%
rename from quickstep/src/com/android/quickstep/recents/usecase/GetThumbnailPositionUseCase.kt
rename to quickstep/src/com/android/quickstep/recents/domain/usecase/GetThumbnailPositionUseCase.kt
index bea1d07..8501382 100644
--- a/quickstep/src/com/android/quickstep/recents/usecase/GetThumbnailPositionUseCase.kt
+++ b/quickstep/src/com/android/quickstep/recents/domain/usecase/GetThumbnailPositionUseCase.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,28 +14,30 @@
* limitations under the License.
*/
-package com.android.quickstep.recents.usecase
+package com.android.quickstep.recents.domain.usecase
import android.graphics.Matrix
import android.graphics.Rect
-import com.android.quickstep.recents.data.RecentTasksRepository
import com.android.quickstep.recents.data.RecentsDeviceProfileRepository
import com.android.quickstep.recents.data.RecentsRotationStateRepository
-import com.android.quickstep.recents.usecase.ThumbnailPositionState.MatrixScaling
-import com.android.quickstep.recents.usecase.ThumbnailPositionState.MissingThumbnail
+import com.android.systemui.shared.recents.model.ThumbnailData
import com.android.systemui.shared.recents.utilities.PreviewPositionHelper
/** Use case for retrieving [Matrix] for positioning Thumbnail in a View */
class GetThumbnailPositionUseCase(
private val deviceProfileRepository: RecentsDeviceProfileRepository,
private val rotationStateRepository: RecentsRotationStateRepository,
- private val tasksRepository: RecentTasksRepository,
- private val previewPositionHelper: PreviewPositionHelper = PreviewPositionHelper(),
+ private val previewPositionHelper: PreviewPositionHelper,
) {
- fun run(taskId: Int, width: Int, height: Int, isRtl: Boolean): ThumbnailPositionState {
- val thumbnailData =
- tasksRepository.getCurrentThumbnailById(taskId) ?: return MissingThumbnail
- val thumbnail = thumbnailData.thumbnail ?: return MissingThumbnail
+ operator fun invoke(
+ thumbnailData: ThumbnailData?,
+ width: Int,
+ height: Int,
+ isRtl: Boolean,
+ ): ThumbnailPosition {
+ val thumbnail =
+ thumbnailData?.thumbnail ?: return ThumbnailPosition(Matrix.IDENTITY_MATRIX, false)
+
previewPositionHelper.updateThumbnailMatrix(
Rect(0, 0, thumbnail.width, thumbnail.height),
thumbnailData,
@@ -45,9 +47,11 @@
rotationStateRepository.getRecentsRotationState().activityRotation,
isRtl,
)
- return MatrixScaling(
- previewPositionHelper.matrix,
- previewPositionHelper.isOrientationChanged,
+ return ThumbnailPosition(
+ matrix = previewPositionHelper.matrix,
+ isRotated = previewPositionHelper.isOrientationChanged,
)
}
}
+
+data class ThumbnailPosition(val matrix: Matrix, val isRotated: Boolean)
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 0b299ee..3c4a384 100644
--- a/quickstep/src/com/android/quickstep/recents/ui/viewmodel/TaskViewModel.kt
+++ b/quickstep/src/com/android/quickstep/recents/ui/viewmodel/TaskViewModel.kt
@@ -24,7 +24,9 @@
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.GetThumbnailPositionUseCase
import com.android.quickstep.recents.domain.usecase.IsThumbnailValidUseCase
+import com.android.quickstep.recents.domain.usecase.ThumbnailPosition
import com.android.quickstep.recents.viewmodel.RecentsViewData
import com.android.quickstep.views.TaskViewType
import com.android.systemui.shared.recents.model.ThumbnailData
@@ -48,6 +50,7 @@
private val getTaskUseCase: GetTaskUseCase,
private val getSysUiStatusNavFlagsUseCase: GetSysUiStatusNavFlagsUseCase,
private val isThumbnailValidUseCase: IsThumbnailValidUseCase,
+ private val getThumbnailPositionUseCase: GetThumbnailPositionUseCase,
dispatcherProvider: DispatcherProvider,
) {
private var taskIds = MutableStateFlow(emptySet<Int>())
@@ -83,6 +86,19 @@
fun isThumbnailValid(thumbnail: ThumbnailData?, width: Int, height: Int): Boolean =
isThumbnailValidUseCase(thumbnail, width, height)
+ fun getThumbnailPosition(
+ thumbnail: ThumbnailData?,
+ width: Int,
+ height: Int,
+ isRtl: Boolean,
+ ): ThumbnailPosition =
+ getThumbnailPositionUseCase(
+ thumbnailData = thumbnail,
+ width = width,
+ height = height,
+ isRtl = isRtl,
+ )
+
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/usecase/ThumbnailPositionState.kt b/quickstep/src/com/android/quickstep/recents/usecase/ThumbnailPositionState.kt
deleted file mode 100644
index 1a1bef7..0000000
--- a/quickstep/src/com/android/quickstep/recents/usecase/ThumbnailPositionState.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.usecase
-
-import android.graphics.Matrix
-
-/** State on how a task Thumbnail can fit on given canvas */
-sealed class ThumbnailPositionState {
- data object MissingThumbnail : ThumbnailPositionState()
-
- data class MatrixScaling(val matrix: Matrix, val isRotated: Boolean) : ThumbnailPositionState()
-}
diff --git a/quickstep/src/com/android/quickstep/task/thumbnail/TaskThumbnailView.kt b/quickstep/src/com/android/quickstep/task/thumbnail/TaskThumbnailView.kt
index 63e93ba..e672ec4 100644
--- a/quickstep/src/com/android/quickstep/task/thumbnail/TaskThumbnailView.kt
+++ b/quickstep/src/com/android/quickstep/task/thumbnail/TaskThumbnailView.kt
@@ -18,6 +18,7 @@
import android.content.Context
import android.graphics.Color
+import android.graphics.Matrix
import android.graphics.Outline
import android.graphics.Rect
import android.graphics.drawable.ShapeDrawable
@@ -34,34 +35,15 @@
import com.android.launcher3.R
import com.android.launcher3.util.MultiPropertyFactory
import com.android.launcher3.util.ViewPool
-import com.android.launcher3.util.coroutines.DispatcherProvider
-import com.android.quickstep.recents.di.RecentsDependencies
-import com.android.quickstep.recents.di.get
import com.android.quickstep.task.thumbnail.TaskThumbnailUiState.BackgroundOnly
import com.android.quickstep.task.thumbnail.TaskThumbnailUiState.LiveTile
import com.android.quickstep.task.thumbnail.TaskThumbnailUiState.Snapshot
import com.android.quickstep.task.thumbnail.TaskThumbnailUiState.SnapshotSplash
import com.android.quickstep.task.thumbnail.TaskThumbnailUiState.Uninitialized
-import com.android.quickstep.task.viewmodel.TaskThumbnailViewModel
import com.android.quickstep.views.FixedSizeImageView
import com.android.quickstep.views.TaskThumbnailViewHeader
-import kotlinx.coroutines.CoroutineName
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.SupervisorJob
-import kotlinx.coroutines.cancel
-import kotlinx.coroutines.launch
class TaskThumbnailView : FrameLayout, ViewPool.Reusable {
- private val recentsCoroutineScope: CoroutineScope = RecentsDependencies.get()
- private val dispatcherProvider: DispatcherProvider = RecentsDependencies.get()
-
- // 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 lateinit var viewModel: TaskThumbnailViewModel
-
- private lateinit var viewAttachedScope: CoroutineScope
-
private val scrimView: View by lazy { findViewById(R.id.task_thumbnail_scrim) }
private val liveTileView: LiveTileView by lazy { findViewById(R.id.task_thumbnail_live_tile) }
private val thumbnailView: FixedSizeImageView by lazy { findViewById(R.id.task_thumbnail) }
@@ -70,10 +52,21 @@
private val dimAlpha: MultiPropertyFactory<View> by lazy {
MultiPropertyFactory(scrimView, VIEW_ALPHA, ScrimViewAlpha.entries.size, ::maxOf)
}
+ private var onSizeChanged: ((width: Int, height: Int) -> Unit)? = null
private var taskThumbnailViewHeader: TaskThumbnailViewHeader? = null
private var uiState: TaskThumbnailUiState = Uninitialized
+
+ /**
+ * Sets the outline bounds of the view. Default to use view's bound as outline when set to null.
+ */
+ var outlineBounds: Rect? = null
+ set(value) {
+ field = value
+ invalidateOutline()
+ }
+
private val bounds = Rect()
var cornerRadius: Float = 0f
@@ -94,36 +87,24 @@
override fun onFinishInflate() {
super.onFinishInflate()
-
maybeCreateHeader()
}
override fun onAttachedToWindow() {
super.onAttachedToWindow()
- viewAttachedScope =
- CoroutineScope(
- SupervisorJob() + Dispatchers.Main.immediate + CoroutineName("TaskThumbnailView")
- )
- viewModel = RecentsDependencies.get(this)
clipToOutline = true
outlineProvider =
object : ViewOutlineProvider() {
override fun getOutline(view: View, outline: Outline) {
- outline.setRoundRect(bounds, cornerRadius)
+ outline.setRoundRect(outlineBounds ?: bounds, cornerRadius)
}
}
}
- // TODO(b/391842220): Cancel scope in onDetach instead of having a specific method for this.
- fun destroyScopes() {
- val scopeToCancel = viewAttachedScope
- recentsCoroutineScope.launch(dispatcherProvider.background) {
- scopeToCancel.cancel("TaskThumbnailView detaching from window")
- }
- }
-
override fun onRecycle() {
uiState = Uninitialized
+ onSizeChanged = null
+ outlineBounds = null
resetViews()
}
@@ -159,11 +140,13 @@
splashIcon.alpha = value
}
+ fun doOnSizeChange(action: (width: Int, height: Int) -> Unit) {
+ onSizeChanged = action
+ }
+
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
super.onSizeChanged(w, h, oldw, oldh)
- if (uiState is SnapshotSplash) {
- setImageMatrix()
- }
+ onSizeChanged?.invoke(width, height)
bounds.set(0, 0, w, h)
invalidateOutline()
}
@@ -220,11 +203,12 @@
drawBackground(snapshot.backgroundColor)
thumbnailView.setImageBitmap(snapshot.bitmap)
thumbnailView.isInvisible = false
- setImageMatrix()
}
- private fun setImageMatrix() {
- thumbnailView.imageMatrix = viewModel.getThumbnailPositionState(width, height, isLayoutRtl)
+ fun setImageMatrix(matrix: Matrix) {
+ if (uiState is SnapshotSplash) {
+ thumbnailView.imageMatrix = matrix
+ }
}
private fun logDebug(message: String) {
diff --git a/quickstep/src/com/android/quickstep/task/viewmodel/TaskOverlayViewModel.kt b/quickstep/src/com/android/quickstep/task/viewmodel/TaskOverlayViewModel.kt
index 81a904b..9bff3ac 100644
--- a/quickstep/src/com/android/quickstep/task/viewmodel/TaskOverlayViewModel.kt
+++ b/quickstep/src/com/android/quickstep/task/viewmodel/TaskOverlayViewModel.kt
@@ -19,9 +19,7 @@
import android.graphics.Matrix
import com.android.launcher3.util.coroutines.DispatcherProvider
import com.android.quickstep.recents.data.RecentTasksRepository
-import com.android.quickstep.recents.usecase.GetThumbnailPositionUseCase
-import com.android.quickstep.recents.usecase.ThumbnailPositionState.MatrixScaling
-import com.android.quickstep.recents.usecase.ThumbnailPositionState.MissingThumbnail
+import com.android.quickstep.recents.domain.usecase.GetThumbnailPositionUseCase
import com.android.quickstep.recents.viewmodel.RecentsViewData
import com.android.quickstep.task.thumbnail.TaskOverlayUiState.Disabled
import com.android.quickstep.task.thumbnail.TaskOverlayUiState.Enabled
@@ -36,7 +34,7 @@
private val task: Task,
recentsViewData: RecentsViewData,
private val getThumbnailPositionUseCase: GetThumbnailPositionUseCase,
- recentTasksRepository: RecentTasksRepository,
+ private val recentTasksRepository: RecentTasksRepository,
dispatcherProvider: DispatcherProvider,
) {
val overlayState =
@@ -60,22 +58,17 @@
.flowOn(dispatcherProvider.background)
fun getThumbnailPositionState(width: Int, height: Int, isRtl: Boolean): ThumbnailPositionState {
- val matrix: Matrix
- val isRotated: Boolean
- when (
- val thumbnailPositionState =
- getThumbnailPositionUseCase.run(task.key.id, width, height, isRtl)
- ) {
- is MatrixScaling -> {
- matrix = thumbnailPositionState.matrix
- isRotated = thumbnailPositionState.isRotated
- }
- is MissingThumbnail -> {
- matrix = Matrix.IDENTITY_MATRIX
- isRotated = false
- }
- }
- return ThumbnailPositionState(matrix, isRotated)
+ val thumbnailPositionState =
+ getThumbnailPositionUseCase(
+ thumbnailData = recentTasksRepository.getCurrentThumbnailById(task.key.id),
+ width = width,
+ height = height,
+ isRtl = isRtl,
+ )
+ return ThumbnailPositionState(
+ thumbnailPositionState.matrix,
+ thumbnailPositionState.isRotated,
+ )
}
data class ThumbnailPositionState(val matrix: Matrix, val isRotated: Boolean)
diff --git a/quickstep/src/com/android/quickstep/task/viewmodel/TaskThumbnailViewModel.kt b/quickstep/src/com/android/quickstep/task/viewmodel/TaskThumbnailViewModel.kt
deleted file mode 100644
index e641737..0000000
--- a/quickstep/src/com/android/quickstep/task/viewmodel/TaskThumbnailViewModel.kt
+++ /dev/null
@@ -1,28 +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.viewmodel
-
-import android.graphics.Matrix
-
-/** ViewModel for representing TaskThumbnails */
-interface TaskThumbnailViewModel {
- /** Attaches this ViewModel to a specific task id for it to provide data from. */
- fun bind(taskId: Int)
-
- /** Returns a Matrix which can be applied to the snapshot */
- fun getThumbnailPositionState(width: Int, height: Int, isRtl: Boolean): Matrix
-}
diff --git a/quickstep/src/com/android/quickstep/task/viewmodel/TaskThumbnailViewModelImpl.kt b/quickstep/src/com/android/quickstep/task/viewmodel/TaskThumbnailViewModelImpl.kt
deleted file mode 100644
index 94c40d1..0000000
--- a/quickstep/src/com/android/quickstep/task/viewmodel/TaskThumbnailViewModelImpl.kt
+++ /dev/null
@@ -1,47 +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 goveryning permissions and
- * limitations under the License.
- */
-
-package com.android.quickstep.task.viewmodel
-
-import android.app.ActivityTaskManager.INVALID_TASK_ID
-import android.graphics.Matrix
-import android.util.Log
-import com.android.quickstep.recents.usecase.GetThumbnailPositionUseCase
-import com.android.quickstep.recents.usecase.ThumbnailPositionState
-
-class TaskThumbnailViewModelImpl(
- private val getThumbnailPositionUseCase: GetThumbnailPositionUseCase
-) : TaskThumbnailViewModel {
- private var taskId: Int = INVALID_TASK_ID
-
- override fun bind(taskId: Int) {
- Log.d(TAG, "bind taskId: $taskId")
- this.taskId = taskId
- }
-
- override fun getThumbnailPositionState(width: Int, height: Int, isRtl: Boolean): Matrix =
- when (
- val thumbnailPositionState =
- getThumbnailPositionUseCase.run(taskId, width, height, isRtl)
- ) {
- is ThumbnailPositionState.MatrixScaling -> thumbnailPositionState.matrix
- is ThumbnailPositionState.MissingThumbnail -> Matrix.IDENTITY_MATRIX
- }
-
- private companion object {
- const val TAG = "TaskThumbnailViewModel"
- }
-}
diff --git a/quickstep/src/com/android/quickstep/util/SplitAnimationController.kt b/quickstep/src/com/android/quickstep/util/SplitAnimationController.kt
index 88d14b7..a9dbbf2 100644
--- a/quickstep/src/com/android/quickstep/util/SplitAnimationController.kt
+++ b/quickstep/src/com/android/quickstep/util/SplitAnimationController.kt
@@ -127,7 +127,7 @@
val drawable = getDrawable(container.iconView, splitSelectSource)
return SplitAnimInitProps(
container.snapshotView,
- container.splitAnimationThumbnail,
+ container.thumbnail,
drawable,
fadeWithThumbnail = true,
isStagedTask = true,
@@ -147,7 +147,7 @@
val drawable = getDrawable(it.iconView, splitSelectSource)
return SplitAnimInitProps(
it.snapshotView,
- it.splitAnimationThumbnail,
+ it.thumbnail,
drawable,
fadeWithThumbnail = true,
isStagedTask = true,
@@ -215,13 +215,13 @@
if (enableOverviewIconMenu()) {
builder.add(
ObjectAnimator.ofFloat(
- (iconView as IconAppChipView).splitTranslationX,
+ (iconView as IconAppChipView).getSplitTranslationX(),
MULTI_PROPERTY_VALUE,
0f,
)
)
builder.add(
- ObjectAnimator.ofFloat(iconView.splitTranslationY, MULTI_PROPERTY_VALUE, 0f)
+ ObjectAnimator.ofFloat(iconView.getSplitTranslationY(), MULTI_PROPERTY_VALUE, 0f)
)
}
@@ -985,12 +985,11 @@
val splitTree: Pair<Change, List<Change>>? = extractTopParentAndChildren(transitionInfo)
check(splitTree != null) { "Could not find a split root candidate" }
val rootCandidate = splitTree.first
- val stageRootTaskIds: Set<Int> = splitTree.second
- .map { it.taskInfo!!.taskId }
- .toSet()
- val leafTasks: List<Change> = transitionInfo.changes
- .filter { it.taskInfo != null && it.taskInfo!!.parentTaskId in stageRootTaskIds}
- .toList()
+ val stageRootTaskIds: Set<Int> = splitTree.second.map { it.taskInfo!!.taskId }.toSet()
+ val leafTasks: List<Change> =
+ transitionInfo.changes
+ .filter { it.taskInfo != null && it.taskInfo!!.parentTaskId in stageRootTaskIds }
+ .toList()
// Starting position is a 34% size tile centered in the middle of the screen.
// Ending position is the full device screen.
@@ -1031,8 +1030,13 @@
val endAbsBounds = leaf.endAbsBounds
t.setAlpha(leaf.leash, 1f)
- t.setCrop(leaf.leash, 0f, 0f,
- endAbsBounds.width().toFloat(), endAbsBounds.height().toFloat())
+ t.setCrop(
+ leaf.leash,
+ 0f,
+ 0f,
+ endAbsBounds.width().toFloat(),
+ endAbsBounds.height().toFloat(),
+ )
t.setPosition(leaf.leash, 0f, 0f)
}
@@ -1040,10 +1044,18 @@
val endAbsBounds = stageRoot.endAbsBounds
t.setAlpha(stageRoot.leash, 1f)
- t.setCrop(stageRoot.leash, 0f, 0f,
- endAbsBounds.width().toFloat(), endAbsBounds.height().toFloat())
- t.setPosition(stageRoot.leash, endAbsBounds.left.toFloat(),
- endAbsBounds.top.toFloat())
+ t.setCrop(
+ stageRoot.leash,
+ 0f,
+ 0f,
+ endAbsBounds.width().toFloat(),
+ endAbsBounds.height().toFloat(),
+ )
+ t.setPosition(
+ stageRoot.leash,
+ endAbsBounds.left.toFloat(),
+ endAbsBounds.top.toFloat(),
+ )
}
t.apply()
}
diff --git a/quickstep/src/com/android/quickstep/util/TaskGridNavHelper.java b/quickstep/src/com/android/quickstep/util/TaskGridNavHelper.java
deleted file mode 100644
index 35e90f2..0000000
--- a/quickstep/src/com/android/quickstep/util/TaskGridNavHelper.java
+++ /dev/null
@@ -1,148 +0,0 @@
-/*
- * Copyright (C) 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.quickstep.util;
-
-import static java.lang.annotation.RetentionPolicy.SOURCE;
-
-import androidx.annotation.IntDef;
-
-import com.android.launcher3.util.IntArray;
-
-import java.lang.annotation.Retention;
-import java.util.List;
-
-/**
- * Helper class for navigating RecentsView grid tasks via arrow keys and tab.
- */
-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;
- public static final int DIRECTION_LEFT = 2;
- public static final int DIRECTION_RIGHT = 3;
- public static final int DIRECTION_TAB = 4;
-
- @Retention(SOURCE)
- @IntDef({DIRECTION_UP, DIRECTION_DOWN, DIRECTION_LEFT, DIRECTION_RIGHT, DIRECTION_TAB})
- public @interface TASK_NAV_DIRECTION {}
-
- private final IntArray mOriginalTopRowIds;
- private final IntArray mTopRowIds = new IntArray();
- private final IntArray mBottomRowIds = new IntArray();
-
- public TaskGridNavHelper(IntArray topIds, IntArray bottomIds,
- List<Integer> largeTileIds, boolean hasAddDesktopButton) {
- mOriginalTopRowIds = topIds.clone();
- generateTaskViewIdGrid(topIds, bottomIds, largeTileIds, hasAddDesktopButton);
- }
-
- private void generateTaskViewIdGrid(IntArray topRowIdArray, IntArray bottomRowIdArray,
- 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.
- 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.
- mTopRowIds.add(CLEAR_ALL_PLACEHOLDER_ID);
- mBottomRowIds.add(CLEAR_ALL_PLACEHOLDER_ID);
- }
-
- /**
- * Returns the id of the next page in the grid or -1 for the clear all button.
- */
- public int getNextGridPage(int currentPageTaskViewId, int delta,
- @TASK_NAV_DIRECTION int direction, boolean cycle) {
- boolean inTop = mTopRowIds.contains(currentPageTaskViewId);
- int index = inTop ? mTopRowIds.indexOf(currentPageTaskViewId)
- : mBottomRowIds.indexOf(currentPageTaskViewId);
- int maxSize = Math.max(mTopRowIds.size(), mBottomRowIds.size());
- int nextIndex = index + delta;
-
- switch (direction) {
- case DIRECTION_UP:
- case DIRECTION_DOWN: {
- return inTop ? mBottomRowIds.get(index) : mTopRowIds.get(index);
- }
- case DIRECTION_LEFT: {
- int boundedIndex = cycle ? nextIndex % maxSize : Math.min(nextIndex, maxSize - 1);
- return inTop ? mTopRowIds.get(boundedIndex)
- : mBottomRowIds.get(boundedIndex);
- }
- case DIRECTION_RIGHT: {
- int boundedIndex =
- cycle ? (nextIndex < 0 ? maxSize - 1 : nextIndex)
- : Math.max(nextIndex, 0);
- boolean inOriginalTop = mOriginalTopRowIds.contains(currentPageTaskViewId);
- return inOriginalTop ? mTopRowIds.get(boundedIndex)
- : mBottomRowIds.get(boundedIndex);
- }
- case DIRECTION_TAB: {
- int boundedIndex =
- cycle ? (nextIndex < 0 ? maxSize - 1 : nextIndex % maxSize)
- : Math.min(nextIndex, maxSize - 1);
- if (delta >= 0) {
- return inTop && mTopRowIds.get(index) != mBottomRowIds.get(index)
- ? mBottomRowIds.get(index)
- : mTopRowIds.get(boundedIndex);
- } else {
- if (mTopRowIds.contains(currentPageTaskViewId)) {
- if (boundedIndex < 0) {
- // If no cycling, always return the first task.
- return mTopRowIds.get(0);
- } else {
- return mBottomRowIds.get(boundedIndex);
- }
- } else {
- // Go up to top if there is task above
- return mTopRowIds.get(index) != mBottomRowIds.get(index)
- ? mTopRowIds.get(index)
- : mBottomRowIds.get(boundedIndex);
- }
- }
- }
- default:
- return currentPageTaskViewId;
- }
- }
-
- /**
- * Returns the column of a task's id in the grid.
- */
- public int getColumn(int taskViewId) {
- return mTopRowIds.contains(taskViewId) ? mTopRowIds.indexOf(taskViewId)
- : mBottomRowIds.indexOf(taskViewId);
- }
-}
diff --git a/quickstep/src/com/android/quickstep/util/TaskGridNavHelper.kt b/quickstep/src/com/android/quickstep/util/TaskGridNavHelper.kt
new file mode 100644
index 0000000..0e78801
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/util/TaskGridNavHelper.kt
@@ -0,0 +1,159 @@
+/*
+ * 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 com.android.launcher3.util.IntArray
+import kotlin.math.abs
+import kotlin.math.max
+
+/** Helper class for navigating RecentsView grid tasks via arrow keys and tab. */
+class TaskGridNavHelper(
+ private val topIds: IntArray,
+ bottomIds: IntArray,
+ largeTileIds: List<Int>,
+ hasAddDesktopButton: Boolean,
+) {
+ private val topRowIds = mutableListOf<Int>()
+ private val bottomRowIds = mutableListOf<Int>()
+
+ init {
+ // Add AddDesktopButton and lage tiles to both rows.
+ if (hasAddDesktopButton) {
+ topRowIds += ADD_DESK_PLACEHOLDER_ID
+ bottomRowIds += ADD_DESK_PLACEHOLDER_ID
+ }
+ topRowIds += largeTileIds
+ bottomRowIds += largeTileIds
+
+ // Add row ids to their respective rows.
+ topRowIds += topIds
+ bottomRowIds += bottomIds
+
+ // Fill in the shorter array with the ids from the longer one.
+ topRowIds += bottomRowIds.takeLast(max(bottomRowIds.size - topRowIds.size, 0))
+ bottomRowIds += topRowIds.takeLast(max(topRowIds.size - bottomRowIds.size, 0))
+
+ // Add the clear all button to the end of both arrays.
+ topRowIds += CLEAR_ALL_PLACEHOLDER_ID
+ bottomRowIds += CLEAR_ALL_PLACEHOLDER_ID
+ }
+
+ /** Returns the id of the next page in the grid or -1 for the clear all button. */
+ fun getNextGridPage(
+ currentPageTaskViewId: Int,
+ delta: Int,
+ direction: TaskNavDirection,
+ cycle: Boolean,
+ ): Int {
+ val inTop = topRowIds.contains(currentPageTaskViewId)
+ val index =
+ if (inTop) topRowIds.indexOf(currentPageTaskViewId)
+ else bottomRowIds.indexOf(currentPageTaskViewId)
+ val maxSize = max(topRowIds.size, bottomRowIds.size)
+ val nextIndex = index + delta
+
+ return when (direction) {
+ TaskNavDirection.UP,
+ TaskNavDirection.DOWN -> {
+ if (inTop) bottomRowIds[index] else topRowIds[index]
+ }
+ TaskNavDirection.LEFT -> {
+ val boundedIndex =
+ if (cycle) nextIndex % maxSize else nextIndex.coerceAtMost(maxSize - 1)
+ if (inTop) topRowIds[boundedIndex] else bottomRowIds[boundedIndex]
+ }
+ TaskNavDirection.RIGHT -> {
+ val boundedIndex =
+ if (cycle) (if (nextIndex < 0) maxSize - 1 else nextIndex)
+ else nextIndex.coerceAtLeast(0)
+ val inOriginalTop = topIds.contains(currentPageTaskViewId)
+ if (inOriginalTop) topRowIds[boundedIndex] else bottomRowIds[boundedIndex]
+ }
+ TaskNavDirection.TAB -> {
+ val boundedIndex =
+ if (cycle) (if (nextIndex < 0) maxSize - 1 else nextIndex % maxSize)
+ else nextIndex.coerceAtMost(maxSize - 1)
+ if (delta >= 0) {
+ if (inTop && topRowIds[index] != bottomRowIds[index]) bottomRowIds[index]
+ else topRowIds[boundedIndex]
+ } else {
+ if (topRowIds.contains(currentPageTaskViewId)) {
+ if (boundedIndex < 0) {
+ // If no cycling, always return the first task.
+ topRowIds[0]
+ } else {
+ bottomRowIds[boundedIndex]
+ }
+ } else {
+ // Go up to top if there is task above
+ if (topRowIds[index] != bottomRowIds[index]) topRowIds[index]
+ else bottomRowIds[boundedIndex]
+ }
+ }
+ }
+ else -> currentPageTaskViewId
+ }
+ }
+
+ /**
+ * Returns a sequence of pairs of (TaskView ID, offset) in the grid, ordered according to tab
+ * navigation, starting from the initial TaskView ID, towards the start or end of the grid.
+ *
+ * <p>A positive delta moves forward in the tab order towards the end of the grid, while a
+ * negative value moves backward towards the beginning. The offset is the distance between
+ * columns the tasks are in.
+ */
+ fun gridTaskViewIdOffsetPairInTabOrderSequence(
+ initialTaskViewId: Int,
+ towardsStart: Boolean,
+ ): Sequence<Pair<Int, Int>> = sequence {
+ val draggedTaskViewColumn = getColumn(initialTaskViewId)
+ var nextTaskViewId: Int = initialTaskViewId
+ var previousTaskViewId: Int = Int.MIN_VALUE
+ while (nextTaskViewId != previousTaskViewId && nextTaskViewId >= 0) {
+ previousTaskViewId = nextTaskViewId
+ nextTaskViewId =
+ getNextGridPage(
+ nextTaskViewId,
+ if (towardsStart) -1 else 1,
+ TaskNavDirection.TAB,
+ cycle = false,
+ )
+ if (nextTaskViewId >= 0 && nextTaskViewId != previousTaskViewId) {
+ val columnOffset = abs(getColumn(nextTaskViewId) - draggedTaskViewColumn)
+ yield(Pair(nextTaskViewId, columnOffset))
+ }
+ }
+ }
+
+ /** Returns the column of a task's id in the grid. */
+ private fun getColumn(taskViewId: Int): Int =
+ if (topRowIds.contains(taskViewId)) topRowIds.indexOf(taskViewId)
+ else bottomRowIds.indexOf(taskViewId)
+
+ enum class TaskNavDirection {
+ UP,
+ DOWN,
+ LEFT,
+ RIGHT,
+ TAB,
+ }
+
+ companion object {
+ const val CLEAR_ALL_PLACEHOLDER_ID: Int = -1
+ const val ADD_DESK_PLACEHOLDER_ID: Int = -2
+ }
+}
diff --git a/quickstep/src/com/android/quickstep/util/TransformParams.java b/quickstep/src/com/android/quickstep/util/TransformParams.java
index 1c1fbd8..716803a 100644
--- a/quickstep/src/com/android/quickstep/util/TransformParams.java
+++ b/quickstep/src/com/android/quickstep/util/TransformParams.java
@@ -222,7 +222,7 @@
return null;
}
- // Pubic getters so outside packages can read the values.
+ // Public getters so outside packages can read the values.
public float getProgress() {
return mProgress;
diff --git a/quickstep/src/com/android/quickstep/views/DesktopTaskView.kt b/quickstep/src/com/android/quickstep/views/DesktopTaskView.kt
index da160f1..fbda3b3 100644
--- a/quickstep/src/com/android/quickstep/views/DesktopTaskView.kt
+++ b/quickstep/src/com/android/quickstep/views/DesktopTaskView.kt
@@ -21,6 +21,7 @@
import android.graphics.Matrix
import android.graphics.PointF
import android.graphics.Rect
+import android.graphics.Rect.intersects
import android.graphics.RectF
import android.util.AttributeSet
import android.util.Log
@@ -30,6 +31,7 @@
import android.view.ViewStub
import androidx.core.content.res.ResourcesCompat
import androidx.core.view.updateLayoutParams
+import com.android.internal.hidden_from_bootclasspath.com.android.window.flags.Flags.enableDesktopRecentsTransitionsCornersBugfix
import com.android.launcher3.Flags.enableDesktopExplodedView
import com.android.launcher3.Flags.enableOverviewIconMenu
import com.android.launcher3.Flags.enableRefactorTaskThumbnail
@@ -57,6 +59,7 @@
import com.android.quickstep.task.thumbnail.TaskThumbnailView
import com.android.quickstep.util.DesktopTask
import com.android.quickstep.util.RecentsOrientedState
+import kotlin.math.roundToInt
/** TaskView that contains all tasks that are part of the desktop. */
class DesktopTaskView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) :
@@ -102,7 +105,7 @@
* 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()
+ private var fullscreenTaskPositions: List<DesktopTaskBoundsData> = emptyList()
/**
* When enableDesktopExplodedView is enabled, this controls the gradual transition from the
@@ -172,42 +175,44 @@
val thumbnailTopMarginPx = container.deviceProfile.overviewTaskThumbnailTopMarginPx
- val containerWidth = layoutParams.width
- val containerHeight = layoutParams.height - thumbnailTopMarginPx
+ val taskViewWidth = layoutParams.width
+ val taskViewHeight = 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()
+ val screenWidth = tempPointF.x.toInt()
+ val screenHeight = tempPointF.y.toInt()
+ val screenRect = Rect(0, 0, screenWidth, screenHeight)
+ val scaleWidth = taskViewWidth / screenWidth.toFloat()
+ val scaleHeight = taskViewHeight / screenHeight.toFloat()
taskContainers.forEach {
val taskId = it.task.key.id
- val defaultPosition = defaultTaskPositions.firstOrNull { it.taskId == taskId } ?: return
- val position =
+ val fullscreenTaskPosition =
+ fullscreenTaskPositions.firstOrNull { it.taskId == taskId } ?: return
+ val overviewTaskPosition =
if (enableDesktopExplodedView()) {
viewModel!!
.organizedDesktopTaskPositions
.firstOrNull { it.taskId == taskId }
?.let { organizedPosition ->
- TEMP_RECT.apply {
+ TEMP_OVERVIEW_TASK_POSITION.apply {
lerpRect(
- defaultPosition.bounds,
+ fullscreenTaskPosition.bounds,
organizedPosition.bounds,
explodeProgress,
)
}
- } ?: defaultPosition.bounds
+ } ?: fullscreenTaskPosition.bounds
} else {
- defaultPosition.bounds
+ fullscreenTaskPosition.bounds
}
if (enableDesktopExplodedView()) {
getRemoteTargetHandle(taskId)?.let { remoteTargetHandle ->
val fromRect =
- TEMP_RECTF1.apply {
- set(defaultPosition.bounds)
+ TEMP_FROM_RECTF.apply {
+ set(fullscreenTaskPosition.bounds)
scale(scaleWidth)
offset(
lastComputedTaskSize.left.toFloat(),
@@ -215,8 +220,8 @@
)
}
val toRect =
- TEMP_RECTF2.apply {
- set(position)
+ TEMP_TO_RECTF.apply {
+ set(overviewTaskPosition)
scale(scaleWidth)
offset(
lastComputedTaskSize.left.toFloat(),
@@ -230,10 +235,10 @@
}
}
- val taskLeft = position.left * scaleWidth
- val taskTop = position.top * scaleHeight
- val taskWidth = position.width() * scaleWidth
- val taskHeight = position.height() * scaleHeight
+ val taskLeft = overviewTaskPosition.left * scaleWidth
+ val taskTop = overviewTaskPosition.top * scaleHeight
+ val taskWidth = overviewTaskPosition.width() * scaleWidth
+ val taskHeight = overviewTaskPosition.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.
@@ -254,6 +259,24 @@
leftMargin = taskLeft.toInt()
topMargin = taskTop.toInt()
}
+
+ if (
+ enableDesktopRecentsTransitionsCornersBugfix() && enableRefactorTaskThumbnail()
+ ) {
+ it.thumbnailView.outlineBounds =
+ if (intersects(overviewTaskPosition, screenRect))
+ Rect(overviewTaskPosition).apply {
+ intersectUnchecked(screenRect)
+ // Offset to 0,0 to transform into TaskThumbnailView's coordinate
+ // system.
+ offset(-overviewTaskPosition.left, -overviewTaskPosition.top)
+ left = (left * scaleWidth).roundToInt()
+ top = (top * scaleHeight).roundToInt()
+ right = (right * scaleWidth).roundToInt()
+ bottom = (bottom * scaleHeight).roundToInt()
+ }
+ else null
+ }
} else {
// During the animation, apply translation and scale such that the view is
// transformed to where we want, without triggering layout.
@@ -346,13 +369,13 @@
val desktopSize = Size(tempPointF.x.toInt(), tempPointF.y.toInt())
DEFAULT_BOUNDS.set(0, 0, desktopSize.width / 4, desktopSize.height / 4)
- defaultTaskPositions =
+ fullscreenTaskPositions =
taskContainers.map {
DesktopTaskBoundsData(it.task.key.id, it.task.appBounds ?: DEFAULT_BOUNDS)
}
if (enableDesktopExplodedView()) {
- viewModel?.organizeDesktopTasks(desktopSize, defaultTaskPositions)
+ viewModel?.organizeDesktopTasks(desktopSize, fullscreenTaskPositions)
}
positionTaskWindows()
}
@@ -449,8 +472,8 @@
private const val VIEW_POOL_INITIAL_SIZE = 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()
+ private val TEMP_OVERVIEW_TASK_POSITION = Rect()
+ private val TEMP_FROM_RECTF = RectF()
+ private val TEMP_TO_RECTF = RectF()
}
}
diff --git a/quickstep/src/com/android/quickstep/views/GroupedTaskView.kt b/quickstep/src/com/android/quickstep/views/GroupedTaskView.kt
index 229c8f5..2abfb13 100644
--- a/quickstep/src/com/android/quickstep/views/GroupedTaskView.kt
+++ b/quickstep/src/com/android/quickstep/views/GroupedTaskView.kt
@@ -171,12 +171,10 @@
val iconMargins = (iconViewMarginStart + iconViewBackgroundMarginStart) * 2
// setMaxWidth() needs to be called before mIconView.setIconOrientation which is
// called in the super below.
- (leftTopTaskContainer.iconView as IconAppChipView).setMaxWidth(
+ (leftTopTaskContainer.iconView as IconAppChipView).maxWidth =
groupedTaskViewSizes.first.x - iconMargins
- )
- (rightBottomTaskContainer.iconView as IconAppChipView).setMaxWidth(
+ (rightBottomTaskContainer.iconView as IconAppChipView).maxWidth =
groupedTaskViewSizes.second.x - iconMargins
- )
}
}
super.setOrientationState(orientationState)
diff --git a/quickstep/src/com/android/quickstep/views/IconAppChipView.java b/quickstep/src/com/android/quickstep/views/IconAppChipView.java
deleted file mode 100644
index 5270477..0000000
--- a/quickstep/src/com/android/quickstep/views/IconAppChipView.java
+++ /dev/null
@@ -1,471 +0,0 @@
-/*
- * Copyright (C) 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.quickstep.views;
-
-import static com.android.app.animation.Interpolators.EMPHASIZED;
-import static com.android.app.animation.Interpolators.LINEAR;
-import static com.android.launcher3.LauncherAnimUtils.SCALE_PROPERTY;
-import static com.android.launcher3.LauncherAnimUtils.VIEW_TRANSLATE_X;
-import static com.android.launcher3.LauncherAnimUtils.VIEW_TRANSLATE_Y;
-
-import android.animation.Animator;
-import android.animation.AnimatorSet;
-import android.animation.ObjectAnimator;
-import android.animation.RectEvaluator;
-import android.animation.ValueAnimator;
-import android.content.Context;
-import android.content.res.Resources;
-import android.graphics.Outline;
-import android.graphics.Rect;
-import android.graphics.drawable.Drawable;
-import android.util.AttributeSet;
-import android.view.View;
-import android.view.ViewAnimationUtils;
-import android.view.ViewOutlineProvider;
-import android.widget.FrameLayout;
-import android.widget.ImageView;
-import android.widget.TextView;
-
-import androidx.annotation.Nullable;
-
-import com.android.launcher3.R;
-import com.android.launcher3.Utilities;
-import com.android.launcher3.util.MultiPropertyFactory;
-import com.android.launcher3.util.MultiValueAlpha;
-import com.android.quickstep.orientation.RecentsPagedOrientationHandler;
-import com.android.quickstep.util.RecentsOrientedState;
-
-/**
- * An icon app menu view which can be used in place of an IconView in overview TaskViews.
- */
-public class IconAppChipView extends FrameLayout implements TaskViewIcon {
-
- private static final int MENU_BACKGROUND_REVEAL_DURATION = 417;
- private static final int MENU_BACKGROUND_HIDE_DURATION = 333;
-
- private static final int NUM_ALPHA_CHANNELS = 4;
- private static final int INDEX_CONTENT_ALPHA = 0;
- private static final int INDEX_COLOR_FILTER_ALPHA = 1;
- private static final int INDEX_MODAL_ALPHA = 2;
- /** Used to hide the app chip for 90:10 flex split. */
- private static final int INDEX_MINIMUM_RATIO_ALPHA = 3;
-
- private final MultiValueAlpha mMultiValueAlpha;
-
- private View mMenuAnchorView;
- private IconView mIconView;
- // Two textview so we can ellipsize the collapsed view and crossfade on expand to the full name.
- private TextView mIconTextCollapsedView;
- private TextView mIconTextExpandedView;
- private ImageView mIconArrowView;
- private final Rect mBackgroundRelativeLtrLocation = new Rect();
- final RectEvaluator mBackgroundAnimationRectEvaluator =
- new RectEvaluator(mBackgroundRelativeLtrLocation);
- private final int mCollapsedMenuDefaultWidth;
- private final int mExpandedMenuDefaultWidth;
- private final int mCollapsedMenuDefaultHeight;
- private final int mExpandedMenuDefaultHeight;
- private final int mIconMenuMarginTopStart;
- private final int mMenuToChipGap;
- private final int mBackgroundMarginTopStart;
- private final int mAppNameHorizontalMargin;
- private final int mIconViewMarginStart;
- private final int mAppIconSize;
- private final int mArrowSize;
- private final int mIconViewDrawableExpandedSize;
- private final int mArrowMarginEnd;
- private AnimatorSet mAnimator;
-
- private int mMaxWidth = Integer.MAX_VALUE;
-
- private static final int INDEX_SPLIT_TRANSLATION = 0;
- private static final int INDEX_MENU_TRANSLATION = 1;
- private static final int INDEX_COUNT_TRANSLATION = 2;
-
- private final MultiPropertyFactory<View> mViewTranslationX;
- private final MultiPropertyFactory<View> mViewTranslationY;
-
- /**
- * Gets the view split x-axis translation
- */
- public MultiPropertyFactory<View>.MultiProperty getSplitTranslationX() {
- return mViewTranslationX.get(INDEX_SPLIT_TRANSLATION);
- }
-
- /**
- * Sets the view split x-axis translation
- * @param translationX x-axis translation
- */
- public void setSplitTranslationX(float translationX) {
- getSplitTranslationX().setValue(translationX);
- }
-
- /**
- * Gets the view split y-axis translation
- */
- public MultiPropertyFactory<View>.MultiProperty getSplitTranslationY() {
- return mViewTranslationY.get(INDEX_SPLIT_TRANSLATION);
- }
-
- /**
- * Sets the view split y-axis translation
- * @param translationY y-axis translation
- */
- public void setSplitTranslationY(float translationY) {
- getSplitTranslationY().setValue(translationY);
- }
-
- /**
- * Gets the menu x-axis translation for split task
- */
- public MultiPropertyFactory<View>.MultiProperty getMenuTranslationX() {
- return mViewTranslationX.get(INDEX_MENU_TRANSLATION);
- }
-
- /**
- * Gets the menu y-axis translation for split task
- */
- public MultiPropertyFactory<View>.MultiProperty getMenuTranslationY() {
- return mViewTranslationY.get(INDEX_MENU_TRANSLATION);
- }
-
- public IconAppChipView(Context context) {
- this(context, null);
- }
-
- public IconAppChipView(Context context, AttributeSet attrs) {
- this(context, attrs, 0);
- }
-
- public IconAppChipView(Context context, AttributeSet attrs, int defStyleAttr) {
- this(context, attrs, defStyleAttr, 0);
- }
-
- public IconAppChipView(Context context, @Nullable AttributeSet attrs, int defStyleAttr,
- int defStyleRes) {
- super(context, attrs, defStyleAttr, defStyleRes);
- Resources res = getResources();
- mMultiValueAlpha = new MultiValueAlpha(this, NUM_ALPHA_CHANNELS);
- mMultiValueAlpha.setUpdateVisibility(/* updateVisibility= */ true);
-
- // Menu dimensions
- mCollapsedMenuDefaultWidth =
- res.getDimensionPixelSize(R.dimen.task_thumbnail_icon_menu_collapsed_width);
- mExpandedMenuDefaultWidth =
- res.getDimensionPixelSize(R.dimen.task_thumbnail_icon_menu_expanded_width);
- mCollapsedMenuDefaultHeight =
- res.getDimensionPixelSize(R.dimen.task_thumbnail_icon_menu_collapsed_height);
- mExpandedMenuDefaultHeight =
- res.getDimensionPixelSize(R.dimen.task_thumbnail_icon_menu_expanded_height);
- mIconMenuMarginTopStart = res.getDimensionPixelSize(
- R.dimen.task_thumbnail_icon_menu_expanded_top_start_margin);
- mMenuToChipGap = res.getDimensionPixelSize(
- R.dimen.task_thumbnail_icon_menu_expanded_gap);
-
- // Background dimensions
- mBackgroundMarginTopStart = res.getDimensionPixelSize(
- R.dimen.task_thumbnail_icon_menu_background_margin_top_start);
-
- // Contents dimensions
- mAppNameHorizontalMargin = res.getDimensionPixelSize(
- R.dimen.task_thumbnail_icon_menu_app_name_margin_horizontal_collapsed);
- mArrowMarginEnd = res.getDimensionPixelSize(R.dimen.task_thumbnail_icon_menu_arrow_margin);
- mIconViewMarginStart = res.getDimensionPixelSize(
- R.dimen.task_thumbnail_icon_view_start_margin);
- mAppIconSize = res.getDimensionPixelSize(
- R.dimen.task_thumbnail_icon_menu_app_icon_collapsed_size);
- mArrowSize = res.getDimensionPixelSize(
- R.dimen.task_thumbnail_icon_menu_arrow_size);
- mIconViewDrawableExpandedSize = res.getDimensionPixelSize(
- R.dimen.task_thumbnail_icon_menu_app_icon_expanded_size);
-
- mViewTranslationX = new MultiPropertyFactory<>(this, VIEW_TRANSLATE_X,
- INDEX_COUNT_TRANSLATION,
- Float::sum);
- mViewTranslationY = new MultiPropertyFactory<>(this, VIEW_TRANSLATE_Y,
- INDEX_COUNT_TRANSLATION,
- Float::sum);
- }
-
- @Override
- protected void onFinishInflate() {
- super.onFinishInflate();
- mIconView = findViewById(R.id.icon_view);
- mIconTextCollapsedView = findViewById(R.id.icon_text_collapsed);
- mIconTextExpandedView = findViewById(R.id.icon_text_expanded);
- mIconArrowView = findViewById(R.id.icon_arrow);
- mMenuAnchorView = findViewById(R.id.icon_view_menu_anchor);
- }
-
- protected IconView getIconView() {
- return mIconView;
- }
-
- @Override
- public void setText(CharSequence text) {
- if (mIconTextCollapsedView != null) {
- mIconTextCollapsedView.setText(text);
- }
- if (mIconTextExpandedView != null) {
- mIconTextExpandedView.setText(text);
- }
- }
-
- @Override
- public Drawable getDrawable() {
- return mIconView == null ? null : mIconView.getDrawable();
- }
-
- @Override
- public void setDrawable(Drawable icon) {
- if (mIconView != null) {
- mIconView.setDrawable(icon);
- }
- }
-
- @Override
- public void setDrawableSize(int iconWidth, int iconHeight) {
- if (mIconView != null) {
- mIconView.setDrawableSize(iconWidth, iconHeight);
- }
- }
-
- /**
- * Sets the maximum width of this Icon Menu. This is usually used when space is limited for
- * split screen.
- */
- public void setMaxWidth(int maxWidth) {
- // Width showing only the app icon and arrow. Max width should not be set to less than this.
- int minimumMaxWidth = mIconViewMarginStart + mAppIconSize + mArrowSize + mArrowMarginEnd;
- mMaxWidth = Math.max(maxWidth, minimumMaxWidth);
- }
-
- @Override
- public void setIconOrientation(RecentsOrientedState orientationState, boolean isGridTask) {
- RecentsPagedOrientationHandler orientationHandler =
- orientationState.getOrientationHandler();
- // Layout params for anchor view
- LayoutParams anchorLayoutParams = (LayoutParams) mMenuAnchorView.getLayoutParams();
- anchorLayoutParams.topMargin = mExpandedMenuDefaultHeight + mMenuToChipGap;
- mMenuAnchorView.setLayoutParams(anchorLayoutParams);
-
- // Layout Params for the Menu View (this)
- LayoutParams iconMenuParams = (LayoutParams) getLayoutParams();
- iconMenuParams.width = mExpandedMenuDefaultWidth;
- iconMenuParams.height = mExpandedMenuDefaultHeight;
- orientationHandler.setIconAppChipMenuParams(this, iconMenuParams, mIconMenuMarginTopStart,
- mIconMenuMarginTopStart);
- setLayoutParams(iconMenuParams);
-
- // Layout params for the background
- Rect collapsedBackgroundBounds = getCollapsedBackgroundLtrBounds();
- mBackgroundRelativeLtrLocation.set(collapsedBackgroundBounds);
- setOutlineProvider(new ViewOutlineProvider() {
- final Rect mRtlAppliedOutlineBounds = new Rect();
- @Override
- public void getOutline(View view, Outline outline) {
- mRtlAppliedOutlineBounds.set(mBackgroundRelativeLtrLocation);
- if (isLayoutRtl()) {
- int width = getWidth();
- mRtlAppliedOutlineBounds.left = width - mBackgroundRelativeLtrLocation.right;
- mRtlAppliedOutlineBounds.right = width - mBackgroundRelativeLtrLocation.left;
- }
- outline.setRoundRect(
- mRtlAppliedOutlineBounds, mRtlAppliedOutlineBounds.height() / 2f);
- }
- });
-
- // Layout Params for the Icon View
- LayoutParams iconParams = (LayoutParams) mIconView.getLayoutParams();
- int iconMarginStartRelativeToParent = mIconViewMarginStart + mBackgroundMarginTopStart;
- orientationHandler.setIconAppChipChildrenParams(
- iconParams, iconMarginStartRelativeToParent);
-
- mIconView.setLayoutParams(iconParams);
- mIconView.setDrawableSize(mAppIconSize, mAppIconSize);
-
- // Layout Params for the collapsed Icon Text View
- int textMarginStart =
- iconMarginStartRelativeToParent + mAppIconSize + mAppNameHorizontalMargin;
- LayoutParams iconTextCollapsedParams =
- (LayoutParams) mIconTextCollapsedView.getLayoutParams();
- orientationHandler.setIconAppChipChildrenParams(iconTextCollapsedParams, textMarginStart);
- int collapsedTextWidth = collapsedBackgroundBounds.width() - mIconViewMarginStart
- - mAppIconSize - mArrowSize - mAppNameHorizontalMargin - mArrowMarginEnd;
- iconTextCollapsedParams.width = collapsedTextWidth;
- mIconTextCollapsedView.setLayoutParams(iconTextCollapsedParams);
- mIconTextCollapsedView.setAlpha(1f);
-
- // Layout Params for the expanded Icon Text View
- LayoutParams iconTextExpandedParams =
- (LayoutParams) mIconTextExpandedView.getLayoutParams();
- orientationHandler.setIconAppChipChildrenParams(iconTextExpandedParams, textMarginStart);
- mIconTextExpandedView.setLayoutParams(iconTextExpandedParams);
- mIconTextExpandedView.setAlpha(0f);
- mIconTextExpandedView.setRevealClip(true, 0, mAppIconSize / 2f, collapsedTextWidth);
-
- // Layout Params for the Icon Arrow View
- LayoutParams iconArrowParams = (LayoutParams) mIconArrowView.getLayoutParams();
- int arrowMarginStart = collapsedBackgroundBounds.right - mArrowMarginEnd - mArrowSize;
- orientationHandler.setIconAppChipChildrenParams(iconArrowParams, arrowMarginStart);
- mIconArrowView.setPivotY(iconArrowParams.height / 2f);
- mIconArrowView.setLayoutParams(iconArrowParams);
-
- // This method is called twice sometimes (like when rotating split tasks). It is called
- // once before onMeasure and onLayout, and again after onMeasure but before onLayout with
- // a new width. This happens because we update widths on rotation and on measure of
- // grouped task views. Calling requestLayout() does not guarantee a call to onMeasure if
- // it has just measured, so we explicitly call it here.
- measure(MeasureSpec.makeMeasureSpec(getLayoutParams().width, MeasureSpec.EXACTLY),
- MeasureSpec.makeMeasureSpec(getLayoutParams().height, MeasureSpec.EXACTLY));
- }
-
- @Override
- public void setIconColorTint(int color, float amount) {
- // RecentsView's COLOR_TINT animates between 0 and 0.5f, we want to hide the app chip menu.
- float colorTintAlpha = Utilities.mapToRange(amount, 0f, 0.5f, 1f, 0f, LINEAR);
- mMultiValueAlpha.get(INDEX_COLOR_FILTER_ALPHA).setValue(colorTintAlpha);
- }
-
- @Override
- public void setContentAlpha(float alpha) {
- mMultiValueAlpha.get(INDEX_CONTENT_ALPHA).setValue(alpha);
- }
-
- @Override
- public void setModalAlpha(float alpha) {
- mMultiValueAlpha.get(INDEX_MODAL_ALPHA).setValue(alpha);
- }
-
- @Override
- public void setFlexSplitAlpha(float alpha) {
- mMultiValueAlpha.get(INDEX_MINIMUM_RATIO_ALPHA).setValue(alpha);
- }
-
- @Override
- public int getDrawableWidth() {
- return mIconView == null ? 0 : mIconView.getDrawableWidth();
- }
-
- @Override
- public int getDrawableHeight() {
- return mIconView == null ? 0 : mIconView.getDrawableHeight();
- }
-
- protected void revealAnim(boolean isRevealing) {
- cancelInProgressAnimations();
- final Rect collapsedBackgroundBounds = getCollapsedBackgroundLtrBounds();
- final Rect expandedBackgroundBounds = getExpandedBackgroundLtrBounds();
- final Rect initialBackground = new Rect(mBackgroundRelativeLtrLocation);
- mAnimator = new AnimatorSet();
-
- if (isRevealing) {
- boolean isRtl = isLayoutRtl();
- bringToFront();
- // Clip expanded text with reveal animation so it doesn't go beyond the edge of the menu
- Animator expandedTextRevealAnim = ViewAnimationUtils.createCircularReveal(
- mIconTextExpandedView, 0, mIconTextExpandedView.getHeight() / 2,
- mIconTextCollapsedView.getWidth(), mIconTextExpandedView.getWidth());
- // Animate background clipping
- ValueAnimator backgroundAnimator = ValueAnimator.ofObject(
- mBackgroundAnimationRectEvaluator,
- initialBackground,
- expandedBackgroundBounds);
- backgroundAnimator.addUpdateListener(valueAnimator -> invalidateOutline());
-
- float iconViewScaling = mIconViewDrawableExpandedSize / (float) mAppIconSize;
- float arrowTranslationX =
- expandedBackgroundBounds.right - collapsedBackgroundBounds.right;
- float iconCenterToTextCollapsed = mAppIconSize / 2f + mAppNameHorizontalMargin;
- float iconCenterToTextExpanded =
- mIconViewDrawableExpandedSize / 2f + mAppNameHorizontalMargin;
- float textTranslationX = iconCenterToTextExpanded - iconCenterToTextCollapsed;
-
- float textTranslationXWithRtl = isRtl ? -textTranslationX : textTranslationX;
- float arrowTranslationWithRtl = isRtl ? -arrowTranslationX : arrowTranslationX;
-
- mAnimator.playTogether(
- expandedTextRevealAnim,
- backgroundAnimator,
- ObjectAnimator.ofFloat(mIconView, SCALE_X, iconViewScaling),
- ObjectAnimator.ofFloat(mIconView, SCALE_Y, iconViewScaling),
- ObjectAnimator.ofFloat(mIconTextCollapsedView, TRANSLATION_X,
- textTranslationXWithRtl),
- ObjectAnimator.ofFloat(mIconTextExpandedView, TRANSLATION_X,
- textTranslationXWithRtl),
- ObjectAnimator.ofFloat(mIconTextCollapsedView, ALPHA, 0),
- ObjectAnimator.ofFloat(mIconTextExpandedView, ALPHA, 1),
- ObjectAnimator.ofFloat(mIconArrowView, TRANSLATION_X, arrowTranslationWithRtl),
- ObjectAnimator.ofFloat(mIconArrowView, SCALE_Y, -1));
- mAnimator.setDuration(MENU_BACKGROUND_REVEAL_DURATION);
- } else {
- // Clip expanded text with reveal animation so it doesn't go beyond the edge of the menu
- Animator expandedTextClipAnim = ViewAnimationUtils.createCircularReveal(
- mIconTextExpandedView, 0, mIconTextExpandedView.getHeight() / 2,
- mIconTextExpandedView.getWidth(), mIconTextCollapsedView.getWidth());
-
- // Animate background clipping
- ValueAnimator backgroundAnimator = ValueAnimator.ofObject(
- mBackgroundAnimationRectEvaluator,
- initialBackground,
- collapsedBackgroundBounds);
- backgroundAnimator.addUpdateListener(valueAnimator -> invalidateOutline());
-
- mAnimator.playTogether(
- expandedTextClipAnim,
- backgroundAnimator,
- ObjectAnimator.ofFloat(mIconView, SCALE_PROPERTY, 1),
- ObjectAnimator.ofFloat(mIconTextCollapsedView, TRANSLATION_X, 0),
- ObjectAnimator.ofFloat(mIconTextExpandedView, TRANSLATION_X, 0),
- ObjectAnimator.ofFloat(mIconTextCollapsedView, ALPHA, 1),
- ObjectAnimator.ofFloat(mIconTextExpandedView, ALPHA, 0),
- ObjectAnimator.ofFloat(mIconArrowView, TRANSLATION_X, 0),
- ObjectAnimator.ofFloat(mIconArrowView, SCALE_Y, 1));
- mAnimator.setDuration(MENU_BACKGROUND_HIDE_DURATION);
- }
-
- mAnimator.setInterpolator(EMPHASIZED);
- mAnimator.start();
- }
-
- private Rect getCollapsedBackgroundLtrBounds() {
- Rect bounds = new Rect(
- 0,
- 0,
- Math.min(mMaxWidth, mCollapsedMenuDefaultWidth),
- mCollapsedMenuDefaultHeight);
- bounds.offset(mBackgroundMarginTopStart, mBackgroundMarginTopStart);
- return bounds;
- }
-
- private Rect getExpandedBackgroundLtrBounds() {
- return new Rect(0, 0, mExpandedMenuDefaultWidth, mExpandedMenuDefaultHeight);
- }
-
- private void cancelInProgressAnimations() {
- // We null the `AnimatorSet` because it holds references to the `Animators` which aren't
- // expecting to be mutable and will cause a crash if they are re-used.
- if (mAnimator != null && mAnimator.isStarted()) {
- mAnimator.cancel();
- mAnimator = null;
- }
- }
-
- @Override
- public View asView() {
- return this;
- }
-}
diff --git a/quickstep/src/com/android/quickstep/views/IconAppChipView.kt b/quickstep/src/com/android/quickstep/views/IconAppChipView.kt
new file mode 100644
index 0000000..85d14cc
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/views/IconAppChipView.kt
@@ -0,0 +1,438 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.quickstep.views
+
+import android.animation.AnimatorSet
+import android.animation.ObjectAnimator
+import android.animation.RectEvaluator
+import android.animation.ValueAnimator
+import android.content.Context
+import android.graphics.Outline
+import android.graphics.Rect
+import android.graphics.drawable.Drawable
+import android.util.AttributeSet
+import android.view.View
+import android.view.ViewAnimationUtils
+import android.view.ViewOutlineProvider
+import android.widget.FrameLayout
+import android.widget.ImageView
+import android.widget.TextView
+import com.android.app.animation.Interpolators
+import com.android.launcher3.LauncherAnimUtils.SCALE_PROPERTY
+import com.android.launcher3.LauncherAnimUtils.VIEW_TRANSLATE_X
+import com.android.launcher3.LauncherAnimUtils.VIEW_TRANSLATE_Y
+import com.android.launcher3.R
+import com.android.launcher3.Utilities
+import com.android.launcher3.util.MultiPropertyFactory
+import com.android.launcher3.util.MultiPropertyFactory.FloatBiFunction
+import com.android.launcher3.util.MultiValueAlpha
+import com.android.quickstep.util.RecentsOrientedState
+import kotlin.math.max
+import kotlin.math.min
+
+/** An icon app menu view which can be used in place of an IconView in overview TaskViews. */
+class IconAppChipView
+@JvmOverloads
+constructor(
+ context: Context,
+ attrs: AttributeSet? = null,
+ defStyleAttr: Int = 0,
+ defStyleRes: Int = 0,
+) : FrameLayout(context, attrs, defStyleAttr, defStyleRes), TaskViewIcon {
+
+ private var iconView: IconView? = null
+ private var iconArrowView: ImageView? = null
+ private var menuAnchorView: View? = null
+ // Two textview so we can ellipsize the collapsed view and crossfade on expand to the full name.
+ private var iconTextCollapsedView: TextView? = null
+ private var iconTextExpandedView: TextView? = null
+
+ private val backgroundRelativeLtrLocation = Rect()
+ private val backgroundAnimationRectEvaluator = RectEvaluator(backgroundRelativeLtrLocation)
+
+ // Menu dimensions
+ private val collapsedMenuDefaultWidth: Int =
+ resources.getDimensionPixelSize(R.dimen.task_thumbnail_icon_menu_collapsed_width)
+ private val expandedMenuDefaultWidth: Int =
+ resources.getDimensionPixelSize(R.dimen.task_thumbnail_icon_menu_expanded_width)
+ private val collapsedMenuDefaultHeight =
+ resources.getDimensionPixelSize(R.dimen.task_thumbnail_icon_menu_collapsed_height)
+ private val expandedMenuDefaultHeight =
+ resources.getDimensionPixelSize(R.dimen.task_thumbnail_icon_menu_expanded_height)
+ private val iconMenuMarginTopStart =
+ resources.getDimensionPixelSize(R.dimen.task_thumbnail_icon_menu_expanded_top_start_margin)
+ private val menuToChipGap: Int =
+ resources.getDimensionPixelSize(R.dimen.task_thumbnail_icon_menu_expanded_gap)
+
+ // Background dimensions
+ private val backgroundMarginTopStart: Int =
+ resources.getDimensionPixelSize(
+ R.dimen.task_thumbnail_icon_menu_background_margin_top_start
+ )
+
+ // Contents dimensions
+ private val appNameHorizontalMargin =
+ resources.getDimensionPixelSize(
+ R.dimen.task_thumbnail_icon_menu_app_name_margin_horizontal_collapsed
+ )
+ private val arrowMarginEnd =
+ resources.getDimensionPixelSize(R.dimen.task_thumbnail_icon_menu_arrow_margin)
+ private val iconViewMarginStart =
+ resources.getDimensionPixelSize(R.dimen.task_thumbnail_icon_view_start_margin)
+ private val appIconSize =
+ resources.getDimensionPixelSize(R.dimen.task_thumbnail_icon_menu_app_icon_collapsed_size)
+ private val arrowSize =
+ resources.getDimensionPixelSize(R.dimen.task_thumbnail_icon_menu_arrow_size)
+ private val iconViewDrawableExpandedSize =
+ resources.getDimensionPixelSize(R.dimen.task_thumbnail_icon_menu_app_icon_expanded_size)
+
+ private var animator: AnimatorSet? = null
+
+ private val multiValueAlpha: MultiValueAlpha =
+ MultiValueAlpha(this, NUM_ALPHA_CHANNELS).apply { setUpdateVisibility(true) }
+
+ private val viewTranslationX: MultiPropertyFactory<View> =
+ MultiPropertyFactory(this, VIEW_TRANSLATE_X, INDEX_COUNT_TRANSLATION, SUM_AGGREGATOR)
+
+ private val viewTranslationY: MultiPropertyFactory<View> =
+ MultiPropertyFactory(this, VIEW_TRANSLATE_Y, INDEX_COUNT_TRANSLATION, SUM_AGGREGATOR)
+
+ var maxWidth = Int.MAX_VALUE
+ /**
+ * Sets the maximum width of this Icon Menu. This is usually used when space is limited for
+ * split screen.
+ */
+ set(value) {
+ // Width showing only the app icon and arrow. Max width should not be set to less than
+ // this.
+ val minMaxWidth = iconViewMarginStart + appIconSize + arrowSize + arrowMarginEnd
+ field = max(value, minMaxWidth)
+ }
+
+ override fun onFinishInflate() {
+ super.onFinishInflate()
+ iconView = findViewById(R.id.icon_view)
+ iconTextCollapsedView = findViewById(R.id.icon_text_collapsed)
+ iconTextExpandedView = findViewById(R.id.icon_text_expanded)
+ iconArrowView = findViewById(R.id.icon_arrow)
+ menuAnchorView = findViewById(R.id.icon_view_menu_anchor)
+ }
+
+ override fun setText(text: CharSequence?) {
+ iconTextCollapsedView?.text = text
+ iconTextExpandedView?.text = text
+ }
+
+ override fun getDrawable(): Drawable? = iconView?.drawable
+
+ override fun setDrawable(icon: Drawable?) {
+ iconView?.drawable = icon
+ }
+
+ override fun setDrawableSize(iconWidth: Int, iconHeight: Int) {
+ iconView?.setDrawableSize(iconWidth, iconHeight)
+ }
+
+ override fun setIconOrientation(orientationState: RecentsOrientedState, isGridTask: Boolean) {
+ val orientationHandler = orientationState.orientationHandler
+ // Layout params for anchor view
+ val anchorLayoutParams = menuAnchorView!!.layoutParams as LayoutParams
+ anchorLayoutParams.topMargin = expandedMenuDefaultHeight + menuToChipGap
+ menuAnchorView!!.layoutParams = anchorLayoutParams
+
+ // Layout Params for the Menu View (this)
+ val iconMenuParams = layoutParams as LayoutParams
+ iconMenuParams.width = expandedMenuDefaultWidth
+ iconMenuParams.height = expandedMenuDefaultHeight
+ orientationHandler.setIconAppChipMenuParams(
+ this,
+ iconMenuParams,
+ iconMenuMarginTopStart,
+ iconMenuMarginTopStart,
+ )
+ layoutParams = iconMenuParams
+
+ // Layout params for the background
+ val collapsedBackgroundBounds = getCollapsedBackgroundLtrBounds()
+ backgroundRelativeLtrLocation.set(collapsedBackgroundBounds)
+ outlineProvider =
+ object : ViewOutlineProvider() {
+ val mRtlAppliedOutlineBounds: Rect = Rect()
+
+ override fun getOutline(view: View, outline: Outline) {
+ mRtlAppliedOutlineBounds.set(backgroundRelativeLtrLocation)
+ if (isLayoutRtl) {
+ val width = width
+ mRtlAppliedOutlineBounds.left = width - backgroundRelativeLtrLocation.right
+ mRtlAppliedOutlineBounds.right = width - backgroundRelativeLtrLocation.left
+ }
+ outline.setRoundRect(
+ mRtlAppliedOutlineBounds,
+ mRtlAppliedOutlineBounds.height() / 2f,
+ )
+ }
+ }
+
+ // Layout Params for the Icon View
+ val iconParams = iconView!!.layoutParams as LayoutParams
+ val iconMarginStartRelativeToParent = iconViewMarginStart + backgroundMarginTopStart
+ orientationHandler.setIconAppChipChildrenParams(iconParams, iconMarginStartRelativeToParent)
+
+ iconView!!.layoutParams = iconParams
+ iconView!!.setDrawableSize(appIconSize, appIconSize)
+
+ // Layout Params for the collapsed Icon Text View
+ val textMarginStart =
+ iconMarginStartRelativeToParent + appIconSize + appNameHorizontalMargin
+ val iconTextCollapsedParams = iconTextCollapsedView!!.layoutParams as LayoutParams
+ orientationHandler.setIconAppChipChildrenParams(iconTextCollapsedParams, textMarginStart)
+ val collapsedTextWidth =
+ (collapsedBackgroundBounds.width() -
+ iconViewMarginStart -
+ appIconSize -
+ arrowSize -
+ appNameHorizontalMargin -
+ arrowMarginEnd)
+ iconTextCollapsedParams.width = collapsedTextWidth
+ iconTextCollapsedView!!.layoutParams = iconTextCollapsedParams
+ iconTextCollapsedView!!.alpha = 1f
+
+ // Layout Params for the expanded Icon Text View
+ val iconTextExpandedParams = iconTextExpandedView!!.layoutParams as LayoutParams
+ orientationHandler.setIconAppChipChildrenParams(iconTextExpandedParams, textMarginStart)
+ iconTextExpandedView!!.layoutParams = iconTextExpandedParams
+ iconTextExpandedView!!.alpha = 0f
+ iconTextExpandedView!!.setRevealClip(
+ true,
+ 0f,
+ appIconSize / 2f,
+ collapsedTextWidth.toFloat(),
+ )
+
+ // Layout Params for the Icon Arrow View
+ val iconArrowParams = iconArrowView!!.layoutParams as LayoutParams
+ val arrowMarginStart = collapsedBackgroundBounds.right - arrowMarginEnd - arrowSize
+ orientationHandler.setIconAppChipChildrenParams(iconArrowParams, arrowMarginStart)
+ iconArrowView!!.pivotY = iconArrowParams.height / 2f
+ iconArrowView!!.layoutParams = iconArrowParams
+
+ // This method is called twice sometimes (like when rotating split tasks). It is called
+ // once before onMeasure and onLayout, and again after onMeasure but before onLayout with
+ // a new width. This happens because we update widths on rotation and on measure of
+ // grouped task views. Calling requestLayout() does not guarantee a call to onMeasure if
+ // it has just measured, so we explicitly call it here.
+ measure(
+ MeasureSpec.makeMeasureSpec(layoutParams.width, MeasureSpec.EXACTLY),
+ MeasureSpec.makeMeasureSpec(layoutParams.height, MeasureSpec.EXACTLY),
+ )
+ }
+
+ override fun setIconColorTint(color: Int, amount: Float) {
+ // RecentsView's COLOR_TINT animates between 0 and 0.5f, we want to hide the app chip menu.
+ val colorTintAlpha = Utilities.mapToRange(amount, 0f, 0.5f, 1f, 0f, Interpolators.LINEAR)
+ multiValueAlpha[INDEX_COLOR_FILTER_ALPHA].value = colorTintAlpha
+ }
+
+ override fun setContentAlpha(alpha: Float) {
+ multiValueAlpha[INDEX_CONTENT_ALPHA].value = alpha
+ }
+
+ override fun setModalAlpha(alpha: Float) {
+ multiValueAlpha[INDEX_MODAL_ALPHA].value = alpha
+ }
+
+ override fun setFlexSplitAlpha(alpha: Float) {
+ multiValueAlpha[INDEX_MINIMUM_RATIO_ALPHA].value = alpha
+ }
+
+ override fun getDrawableWidth(): Int = iconView?.drawableWidth ?: 0
+
+ override fun getDrawableHeight(): Int = iconView?.drawableHeight ?: 0
+
+ /** Gets the view split x-axis translation */
+ fun getSplitTranslationX(): MultiPropertyFactory<View>.MultiProperty =
+ viewTranslationX.get(INDEX_SPLIT_TRANSLATION)
+
+ /**
+ * Sets the view split x-axis translation
+ *
+ * @param value x-axis translation
+ */
+ fun setSplitTranslationX(value: Float) {
+ getSplitTranslationX().value = value
+ }
+
+ /** Gets the view split y-axis translation */
+ fun getSplitTranslationY(): MultiPropertyFactory<View>.MultiProperty =
+ viewTranslationY[INDEX_SPLIT_TRANSLATION]
+
+ /**
+ * Sets the view split y-axis translation
+ *
+ * @param value y-axis translation
+ */
+ fun setSplitTranslationY(value: Float) {
+ getSplitTranslationY().value = value
+ }
+
+ /** Gets the menu x-axis translation for split task */
+ fun getMenuTranslationX(): MultiPropertyFactory<View>.MultiProperty =
+ viewTranslationX[INDEX_MENU_TRANSLATION]
+
+ /** Gets the menu y-axis translation for split task */
+ fun getMenuTranslationY(): MultiPropertyFactory<View>.MultiProperty =
+ viewTranslationY[INDEX_MENU_TRANSLATION]
+
+ internal fun revealAnim(isRevealing: Boolean) {
+ cancelInProgressAnimations()
+ val collapsedBackgroundBounds = getCollapsedBackgroundLtrBounds()
+ val expandedBackgroundBounds = getExpandedBackgroundLtrBounds()
+ val initialBackground = Rect(backgroundRelativeLtrLocation)
+ animator = AnimatorSet()
+
+ if (isRevealing) {
+ val isRtl = isLayoutRtl
+ bringToFront()
+ // Clip expanded text with reveal animation so it doesn't go beyond the edge of the menu
+ val expandedTextRevealAnim =
+ ViewAnimationUtils.createCircularReveal(
+ iconTextExpandedView,
+ 0,
+ iconTextExpandedView!!.height / 2,
+ iconTextCollapsedView!!.width.toFloat(),
+ iconTextExpandedView!!.width.toFloat(),
+ )
+ // Animate background clipping
+ val backgroundAnimator =
+ ValueAnimator.ofObject(
+ backgroundAnimationRectEvaluator,
+ initialBackground,
+ expandedBackgroundBounds,
+ )
+ backgroundAnimator.addUpdateListener { invalidateOutline() }
+
+ val iconViewScaling = iconViewDrawableExpandedSize / appIconSize.toFloat()
+ val arrowTranslationX =
+ (expandedBackgroundBounds.right - collapsedBackgroundBounds.right).toFloat()
+ val iconCenterToTextCollapsed = appIconSize / 2f + appNameHorizontalMargin
+ val iconCenterToTextExpanded =
+ iconViewDrawableExpandedSize / 2f + appNameHorizontalMargin
+ val textTranslationX = iconCenterToTextExpanded - iconCenterToTextCollapsed
+
+ val textTranslationXWithRtl = if (isRtl) -textTranslationX else textTranslationX
+ val arrowTranslationWithRtl = if (isRtl) -arrowTranslationX else arrowTranslationX
+
+ animator!!.playTogether(
+ expandedTextRevealAnim,
+ backgroundAnimator,
+ ObjectAnimator.ofFloat(iconView, SCALE_X, iconViewScaling),
+ ObjectAnimator.ofFloat(iconView, SCALE_Y, iconViewScaling),
+ ObjectAnimator.ofFloat(
+ iconTextCollapsedView,
+ TRANSLATION_X,
+ textTranslationXWithRtl,
+ ),
+ ObjectAnimator.ofFloat(
+ iconTextExpandedView,
+ TRANSLATION_X,
+ textTranslationXWithRtl,
+ ),
+ ObjectAnimator.ofFloat(iconTextCollapsedView, ALPHA, 0f),
+ ObjectAnimator.ofFloat(iconTextExpandedView, ALPHA, 1f),
+ ObjectAnimator.ofFloat(iconArrowView, TRANSLATION_X, arrowTranslationWithRtl),
+ ObjectAnimator.ofFloat(iconArrowView, SCALE_Y, -1f),
+ )
+ animator!!.setDuration(MENU_BACKGROUND_REVEAL_DURATION.toLong())
+ } else {
+ // Clip expanded text with reveal animation so it doesn't go beyond the edge of the menu
+ val expandedTextClipAnim =
+ ViewAnimationUtils.createCircularReveal(
+ iconTextExpandedView,
+ 0,
+ iconTextExpandedView!!.height / 2,
+ iconTextExpandedView!!.width.toFloat(),
+ iconTextCollapsedView!!.width.toFloat(),
+ )
+
+ // Animate background clipping
+ val backgroundAnimator =
+ ValueAnimator.ofObject(
+ backgroundAnimationRectEvaluator,
+ initialBackground,
+ collapsedBackgroundBounds,
+ )
+ backgroundAnimator.addUpdateListener { valueAnimator: ValueAnimator? ->
+ invalidateOutline()
+ }
+
+ animator!!.playTogether(
+ expandedTextClipAnim,
+ backgroundAnimator,
+ ObjectAnimator.ofFloat(iconView, SCALE_PROPERTY, 1f),
+ ObjectAnimator.ofFloat(iconTextCollapsedView, TRANSLATION_X, 0f),
+ ObjectAnimator.ofFloat(iconTextExpandedView, TRANSLATION_X, 0f),
+ ObjectAnimator.ofFloat(iconTextCollapsedView, ALPHA, 1f),
+ ObjectAnimator.ofFloat(iconTextExpandedView, ALPHA, 0f),
+ ObjectAnimator.ofFloat(iconArrowView, TRANSLATION_X, 0f),
+ ObjectAnimator.ofFloat(iconArrowView, SCALE_Y, 1f),
+ )
+ animator!!.setDuration(MENU_BACKGROUND_HIDE_DURATION.toLong())
+ }
+
+ animator!!.interpolator = Interpolators.EMPHASIZED
+ animator!!.start()
+ }
+
+ private fun getCollapsedBackgroundLtrBounds(): Rect {
+ val bounds =
+ Rect(0, 0, min(maxWidth, collapsedMenuDefaultWidth), collapsedMenuDefaultHeight)
+ bounds.offset(backgroundMarginTopStart, backgroundMarginTopStart)
+ return bounds
+ }
+
+ private fun getExpandedBackgroundLtrBounds() =
+ Rect(0, 0, expandedMenuDefaultWidth, expandedMenuDefaultHeight)
+
+ private fun cancelInProgressAnimations() {
+ // We null the `AnimatorSet` because it holds references to the `Animators` which aren't
+ // expecting to be mutable and will cause a crash if they are re-used.
+ if (animator != null && animator!!.isStarted) {
+ animator!!.cancel()
+ animator = null
+ }
+ }
+
+ override fun asView(): View = this
+
+ private companion object {
+ private val SUM_AGGREGATOR = FloatBiFunction { a: Float, b: Float -> a + b }
+
+ private const val MENU_BACKGROUND_REVEAL_DURATION = 417
+ private const val MENU_BACKGROUND_HIDE_DURATION = 333
+
+ private const val NUM_ALPHA_CHANNELS = 4
+ private const val INDEX_CONTENT_ALPHA = 0
+ private const val INDEX_COLOR_FILTER_ALPHA = 1
+ private const val INDEX_MODAL_ALPHA = 2
+
+ /** Used to hide the app chip for 90:10 flex split. */
+ private const val INDEX_MINIMUM_RATIO_ALPHA = 3
+
+ private const val INDEX_SPLIT_TRANSLATION = 0
+ private const val INDEX_MENU_TRANSLATION = 1
+ private const val INDEX_COUNT_TRANSLATION = 2
+ }
+}
diff --git a/quickstep/src/com/android/quickstep/views/RecentsView.java b/quickstep/src/com/android/quickstep/views/RecentsView.java
index 424271a..0218a58 100644
--- a/quickstep/src/com/android/quickstep/views/RecentsView.java
+++ b/quickstep/src/com/android/quickstep/views/RecentsView.java
@@ -65,11 +65,6 @@
import static com.android.quickstep.BaseContainerInterface.getTaskDimension;
import static com.android.quickstep.TaskUtils.checkCurrentOrManagedUserId;
import static com.android.quickstep.util.LogUtils.splitFailureMessage;
-import static com.android.quickstep.util.TaskGridNavHelper.DIRECTION_DOWN;
-import static com.android.quickstep.util.TaskGridNavHelper.DIRECTION_LEFT;
-import static com.android.quickstep.util.TaskGridNavHelper.DIRECTION_RIGHT;
-import static com.android.quickstep.util.TaskGridNavHelper.DIRECTION_TAB;
-import static com.android.quickstep.util.TaskGridNavHelper.DIRECTION_UP;
import static com.android.quickstep.views.ClearAllButton.DISMISS_ALPHA;
import static com.android.quickstep.views.OverviewActionsView.HIDDEN_ACTIONS_IN_MENU;
import static com.android.quickstep.views.OverviewActionsView.HIDDEN_DESKTOP;
@@ -2696,8 +2691,6 @@
}
if (enableRefactorTaskThumbnail()) {
mRecentsViewModel.onReset();
- // TODO(b/391842220) Remove TaskViews rather than calling specific logic to cancel scope
- getTaskViews().forEach(TaskView::destroyScopes);
}
}
@@ -4540,7 +4533,7 @@
}
private boolean snapToPageRelative(int delta, boolean cycle,
- @TaskGridNavHelper.TASK_NAV_DIRECTION int direction) {
+ TaskGridNavHelper.TaskNavDirection direction) {
// Set next page if scroll animation is still running, otherwise cannot snap to the
// next page on successive key presses. Setting the current page aborts the scroll.
if (!mScroller.isFinished()) {
@@ -4559,7 +4552,7 @@
return true;
}
- private int getNextPageInternal(int delta, @TaskGridNavHelper.TASK_NAV_DIRECTION int direction,
+ private int getNextPageInternal(int delta, TaskGridNavHelper.TaskNavDirection direction,
boolean cycle) {
if (!showAsGrid()) {
return getNextPage() + delta;
@@ -4647,15 +4640,19 @@
switch (event.getKeyCode()) {
case KeyEvent.KEYCODE_TAB:
return snapToPageRelative(event.isShiftPressed() ? -1 : 1, true /* cycle */,
- DIRECTION_TAB);
+ TaskGridNavHelper.TaskNavDirection.TAB);
case KeyEvent.KEYCODE_DPAD_RIGHT:
- return snapToPageRelative(mIsRtl ? -1 : 1, true /* cycle */, DIRECTION_RIGHT);
+ return snapToPageRelative(mIsRtl ? -1 : 1, true /* cycle */,
+ TaskGridNavHelper.TaskNavDirection.RIGHT);
case KeyEvent.KEYCODE_DPAD_LEFT:
- return snapToPageRelative(mIsRtl ? 1 : -1, true /* cycle */, DIRECTION_LEFT);
+ return snapToPageRelative(mIsRtl ? 1 : -1, true /* cycle */,
+ TaskGridNavHelper.TaskNavDirection.LEFT);
case KeyEvent.KEYCODE_DPAD_UP:
- return snapToPageRelative(1, false /* cycle */, DIRECTION_UP);
+ return snapToPageRelative(1, false /* cycle */,
+ TaskGridNavHelper.TaskNavDirection.UP);
case KeyEvent.KEYCODE_DPAD_DOWN:
- return snapToPageRelative(1, false /* cycle */, DIRECTION_DOWN);
+ return snapToPageRelative(1, false /* cycle */,
+ TaskGridNavHelper.TaskNavDirection.DOWN);
case KeyEvent.KEYCODE_DEL:
case KeyEvent.KEYCODE_FORWARD_DEL:
dismissCurrentTask();
@@ -6950,6 +6947,13 @@
isDismissing, detector, dismissLength, onEndRunnable);
}
+ /**
+ * Animates RecentsView's scale to the provided value, using spring animations.
+ */
+ public SpringAnimation animateRecentsScale(float scale) {
+ return mUtils.animateRecentsScale(scale);
+ }
+
public interface TaskLaunchListener {
void onTaskLaunched();
}
diff --git a/quickstep/src/com/android/quickstep/views/RecentsViewUtils.kt b/quickstep/src/com/android/quickstep/views/RecentsViewUtils.kt
index d18a5f7..0e350bb 100644
--- a/quickstep/src/com/android/quickstep/views/RecentsViewUtils.kt
+++ b/quickstep/src/com/android/quickstep/views/RecentsViewUtils.kt
@@ -35,6 +35,7 @@
import com.android.quickstep.util.GroupTask
import com.android.quickstep.util.TaskGridNavHelper
import com.android.quickstep.util.isExternalDisplay
+import com.android.quickstep.views.RecentsView.RECENTS_SCALE_PROPERTY
import com.android.quickstep.views.RecentsView.RUNNING_TASK_ATTACH_ALPHA
import com.android.systemui.shared.recents.model.ThumbnailData
import com.google.android.msdl.data.model.MSDLToken
@@ -335,8 +336,6 @@
.setStartVelocity(if (detector.isFling(velocity)) velocity else 0f)
.addUpdateListener { animation, value, _ ->
if (isDismissing && abs(value) >= abs(dismissLength)) {
- // TODO(b/393553524): Remove 0 alpha, instead animate task fully off screen.
- draggedTaskView.alpha = 0f
animation.cancel()
} else if (draggedTaskView.isRunningTask && recentsView.enableDrawingLiveTile) {
recentsView.runActionOnRemoteHandles { remoteTargetHandle ->
@@ -362,7 +361,6 @@
addNeighboringSpringAnimationsForDismissCancel(
draggedTaskView,
draggedTaskViewSpringAnimation,
- recentsView.pageCount,
)
}
return draggedTaskViewSpringAnimation
@@ -371,7 +369,6 @@
private fun addNeighboringSpringAnimationsForDismissCancel(
draggedTaskView: TaskView,
draggedTaskViewSpringAnimation: SpringAnimation,
- taskCount: Int,
) {
// Empty spring animation exists for conditional start, and to drive neighboring springs.
val neighborsToSettle =
@@ -432,7 +429,23 @@
towardsStart: Boolean,
): Sequence<Pair<TaskView, Int>> {
if (recentsView.showAsGrid()) {
- return gridTaskOffsetPairInTabOrderSequence(draggedTaskView, towardsStart)
+ val taskGridNavHelper =
+ TaskGridNavHelper(
+ recentsView.topRowIdArray,
+ recentsView.bottomRowIdArray,
+ getLargeTaskViewIds(),
+ hasAddDesktopButton = false,
+ )
+ return taskGridNavHelper
+ .gridTaskViewIdOffsetPairInTabOrderSequence(
+ draggedTaskView.taskViewId,
+ towardsStart,
+ )
+ .mapNotNull { (taskViewId, columnOffset) ->
+ recentsView.getTaskViewFromTaskViewId(taskViewId)?.let { taskView ->
+ Pair(taskView, columnOffset)
+ }
+ }
} else {
val taskViewList = taskViews.toList()
val draggedTaskViewIndex = taskViewList.indexOf(draggedTaskView)
@@ -452,49 +465,6 @@
}
}
- /**
- * Returns a sequence of pairs of (TaskViews, offsets) in the grid, ordered according to tab
- * navigation, starting from the dragged TaskView, towards the start or end of the grid.
- *
- * <p>A positive delta moves forward in the tab order towards the end of the grid, while a
- * negative value moves backward towards the beginning. The offset is the distance between
- * columns the tasks are in.
- */
- private fun gridTaskOffsetPairInTabOrderSequence(
- draggedTaskView: TaskView,
- towardsStart: Boolean,
- ): Sequence<Pair<TaskView, Int>> = sequence {
- val taskGridNavHelper =
- TaskGridNavHelper(
- recentsView.topRowIdArray,
- recentsView.bottomRowIdArray,
- getLargeTaskViewIds(),
- /* hasAddDesktopButton= */ false,
- )
- val draggedTaskViewColumn = taskGridNavHelper.getColumn(draggedTaskView.taskViewId)
- var nextTaskView: TaskView? = draggedTaskView
- var previousTaskView: TaskView? = null
- while (nextTaskView != previousTaskView && nextTaskView != null) {
- previousTaskView = nextTaskView
- nextTaskView =
- recentsView.getTaskViewFromTaskViewId(
- taskGridNavHelper.getNextGridPage(
- nextTaskView.taskViewId,
- if (towardsStart) -1 else 1,
- TaskGridNavHelper.DIRECTION_TAB,
- /* cycle = */ false,
- )
- )
- if (nextTaskView != null && nextTaskView != previousTaskView) {
- val columnOffset =
- abs(
- taskGridNavHelper.getColumn(nextTaskView.taskViewId) - draggedTaskViewColumn
- )
- yield(Pair(nextTaskView, columnOffset))
- }
- }
- }
-
/** Creates a neighboring task view spring, driven by the spring of its neighbor. */
private fun createNeighboringTaskViewSpringAnimation(
taskView: TaskView,
@@ -559,10 +529,39 @@
)
}
+ /** Animates RecentsView's scale to the provided value, using spring animations. */
+ fun animateRecentsScale(scale: Float): SpringAnimation {
+ val resourceProvider = DynamicResource.provider(recentsView.mContainer)
+ val dampingRatio = resourceProvider.getFloat(R.dimen.swipe_up_rect_scale_damping_ratio)
+ val stiffness = resourceProvider.getFloat(R.dimen.swipe_up_rect_scale_stiffness)
+
+ // Spring which sets the Recents scale on update. This is needed, as the SpringAnimation
+ // struggles to animate small values like changing recents scale from 0.9 to 1. So
+ // we animate over a larger range (e.g. 900 to 1000) and convert back to the required value.
+ // (This is instead of converting RECENTS_SCALE_PROPERTY to a FloatPropertyCompat and
+ // animating it directly via springs.)
+ val initialRecentsScaleSpringValue =
+ RECENTS_SCALE_SPRING_MULTIPLIER * RECENTS_SCALE_PROPERTY.get(recentsView)
+ return SpringAnimation(FloatValueHolder(initialRecentsScaleSpringValue))
+ .setSpring(
+ SpringForce(initialRecentsScaleSpringValue)
+ .setDampingRatio(dampingRatio)
+ .setStiffness(stiffness)
+ )
+ .addUpdateListener { _, value, _ ->
+ RECENTS_SCALE_PROPERTY.setValue(
+ recentsView,
+ value / RECENTS_SCALE_SPRING_MULTIPLIER,
+ )
+ }
+ .apply { animateToFinalPosition(RECENTS_SCALE_SPRING_MULTIPLIER * scale) }
+ }
+
companion object {
val TEMP_RECT = Rect()
// The additional damping to apply to tasks further from the dismissed task.
- const val ADDITIONAL_DISMISS_DAMPING_RATIO = 0.15f
+ private const val ADDITIONAL_DISMISS_DAMPING_RATIO = 0.15f
+ private const val RECENTS_SCALE_SPRING_MULTIPLIER = 1000f
}
}
diff --git a/quickstep/src/com/android/quickstep/views/TaskContainer.kt b/quickstep/src/com/android/quickstep/views/TaskContainer.kt
index bbe1af4..ce20bf9 100644
--- a/quickstep/src/com/android/quickstep/views/TaskContainer.kt
+++ b/quickstep/src/com/android/quickstep/views/TaskContainer.kt
@@ -17,6 +17,7 @@
package com.android.quickstep.views
import android.graphics.Bitmap
+import android.graphics.Matrix
import android.view.View
import com.android.launcher3.Flags.enableRefactorTaskThumbnail
import com.android.launcher3.model.data.TaskViewItemInfo
@@ -26,11 +27,9 @@
import com.android.quickstep.ViewUtils.addAccessibleChildToList
import com.android.quickstep.recents.di.RecentsDependencies
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.task.thumbnail.TaskThumbnailView
-import com.android.quickstep.task.viewmodel.TaskThumbnailViewModel
import com.android.systemui.shared.recents.model.Task
import com.android.systemui.shared.recents.model.ThumbnailData
@@ -55,10 +54,6 @@
) {
val overlay: TaskOverlayFactory.TaskOverlay<*> = taskOverlayFactory.createOverlay(this)
- // TODO(b/390581380): Remove this after this bug is fixed
- private val taskThumbnailViewModel: TaskThumbnailViewModel by
- RecentsDependencies.inject(snapshotView)
-
init {
if (enableRefactorTaskThumbnail()) {
require(snapshotView is TaskThumbnailView)
@@ -75,7 +70,10 @@
}
internal var thumbnailData: ThumbnailData? = null
- val splitAnimationThumbnail: Bitmap?
+ private set
+
+ val thumbnail: Bitmap?
+ /** If possible don't use this. It should be replaced as part of b/331753115. */
get() =
if (enableRefactorTaskThumbnail()) thumbnailData?.thumbnail
else thumbnailViewDeprecated.thumbnail
@@ -106,9 +104,7 @@
fun bind() {
digitalWellBeingToast?.bind(task, taskView, snapshotView, stagePosition)
- if (enableRefactorTaskThumbnail()) {
- taskThumbnailViewModel.bind(task.key.id)
- } else {
+ if (!enableRefactorTaskThumbnail()) {
thumbnailViewDeprecated.bind(task, overlay, taskView)
}
overlay.init()
@@ -128,11 +124,6 @@
}
}
- // TODO(b/391842220): Cancel scope in onDetach instead of having a specific method for this.
- fun destroyScopes() {
- thumbnailView.destroyScopes()
- }
-
fun setOverlayEnabled(enabled: Boolean) {
if (!enableRefactorTaskThumbnail()) {
thumbnailViewDeprecated.setOverlayEnabled(enabled)
@@ -189,4 +180,8 @@
thumbnailViewDeprecated.setSplashAlpha(progress)
}
}
+
+ fun updateThumbnailMatrix(matrix: Matrix) {
+ thumbnailView.setImageMatrix(matrix)
+ }
}
diff --git a/quickstep/src/com/android/quickstep/views/TaskView.kt b/quickstep/src/com/android/quickstep/views/TaskView.kt
index 56a35bd..49ec31d 100644
--- a/quickstep/src/com/android/quickstep/views/TaskView.kt
+++ b/quickstep/src/com/android/quickstep/views/TaskView.kt
@@ -41,7 +41,6 @@
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
@@ -671,11 +670,6 @@
taskContainers.forEach { it.destroy() }
}
- fun destroyScopes() {
- // TODO(b/391842220): Cancel scope in onDetach instead of having a specific method for this.
- taskContainers.forEach { it.destroyScopes() }
- }
-
// TODO: Clip-out the icon region from the thumbnail, since they are overlapping.
override fun hasOverlappingRendering() = false
@@ -778,9 +772,14 @@
container.setState(
state = containerState,
liveTile = state.isLiveTile,
- hasHeader = type == TaskViewType.DESKTOP,
+ hasHeader = state.hasHeader,
)
updateThumbnailValidity(container)
+ updateThumbnailMatrix(
+ container = container,
+ width = container.thumbnailView.width,
+ height = container.thumbnailView.height,
+ )
if (enableOverviewIconMenu()) {
setIconState(container, containerState)
@@ -798,6 +797,23 @@
applyThumbnailSplashAlpha()
}
+ /**
+ * Updates the thumbnail's transformation matrix and rotation state within a TaskContainer.
+ *
+ * This function is called to reposition the thumbnail in the following scenarios:
+ * - When the TTV's size changes (onSizeChanged), and it's displaying a SnapshotSplash.
+ * - When drawing a snapshot (drawSnapshot).
+ *
+ * @param container The TaskContainer holding the thumbnail to be updated.
+ * @param width The desired width of the thumbnail's container.
+ * @param height The desired height of the thumbnail's container.
+ */
+ private fun updateThumbnailMatrix(container: TaskContainer, width: Int, height: Int) {
+ val thumbnailPosition =
+ viewModel!!.getThumbnailPosition(container.thumbnailData, width, height, isLayoutRtl)
+ container.updateThumbnailMatrix(thumbnailPosition.matrix)
+ }
+
override fun onDetachedFromWindow() {
super.onDetachedFromWindow()
if (enableRefactorTaskThumbnail()) {
@@ -843,6 +859,7 @@
getTaskUseCase = RecentsDependencies.get(),
getSysUiStatusNavFlagsUseCase = RecentsDependencies.get(),
isThumbnailValidUseCase = RecentsDependencies.get(),
+ getThumbnailPositionUseCase = RecentsDependencies.get(),
dispatcherProvider = RecentsDependencies.get(),
)
.apply { bind(*taskIds) }
@@ -852,7 +869,10 @@
container.bind()
if (enableRefactorTaskThumbnail()) {
container.thumbnailView.cornerRadius = thumbnailFullscreenParams.currentCornerRadius
- container.thumbnailView.doOnLayout { updateThumbnailValidity(container) }
+ container.thumbnailView.doOnSizeChange { width, height ->
+ updateThumbnailValidity(container)
+ updateThumbnailMatrix(container, width, height)
+ }
}
}
setOrientationState(orientedState)
@@ -1397,7 +1417,7 @@
container.task,
container.iconView.drawable,
container.snapshotView,
- container.splitAnimationThumbnail,
+ container.thumbnail,
/* intent */ null,
/* user */ null,
container.itemInfo,
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
deleted file mode 100644
index 1a2b1c3..0000000
--- a/quickstep/tests/multivalentScreenshotTests/src/com/android/quickstep/task/thumbnail/FakeTaskThumbnailViewModel.kt
+++ /dev/null
@@ -1,29 +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.Matrix
-import com.android.quickstep.task.viewmodel.TaskThumbnailViewModel
-
-class FakeTaskThumbnailViewModel : TaskThumbnailViewModel {
- override fun bind(taskId: Int) {
- // no-op
- }
-
- override fun getThumbnailPositionState(width: Int, height: Int, isRtl: Boolean) =
- Matrix.IDENTITY_MATRIX
-}
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 c3b4d15..b9d2fed 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
@@ -19,12 +19,9 @@
import android.graphics.Color
import android.view.LayoutInflater
import com.android.launcher3.R
-import com.android.quickstep.recents.di.RecentsDependencies
import com.android.quickstep.task.thumbnail.TaskThumbnailUiState.BackgroundOnly
import com.android.quickstep.task.thumbnail.TaskThumbnailUiState.Uninitialized
-import com.android.quickstep.task.viewmodel.TaskThumbnailViewModel
import com.google.android.apps.nexuslauncher.imagecomparison.goldenpathmanager.ViewScreenshotGoldenPathManager
-import org.junit.After
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
@@ -46,13 +43,6 @@
ViewScreenshotGoldenPathManager(getEmulatedDevicePathConfig(emulationSpec)),
)
- private val taskThumbnailViewModel = FakeTaskThumbnailViewModel()
-
- @After
- fun tearDown() {
- RecentsDependencies.destroy()
- }
-
@Test
fun taskThumbnailView_uninitializedByDefault() {
screenshotRule.screenshotTest("taskThumbnailView_uninitialized") { activity ->
@@ -125,14 +115,10 @@
}
private fun createTaskThumbnailView(context: Context): TaskThumbnailView {
- val di = RecentsDependencies.initialize(context)
val taskThumbnailView =
LayoutInflater.from(context).inflate(R.layout.task_thumbnail, null, false)
as TaskThumbnailView
taskThumbnailView.cornerRadius = CORNER_RADIUS
- val ttvDiScopeId = di.getScope(taskThumbnailView).scopeId
- di.provide(TaskThumbnailViewModel::class.java, ttvDiScopeId) { taskThumbnailViewModel }
-
return taskThumbnailView
}
diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/usecase/GetThumbnailPositionUseCaseTest.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/domain/usecase/GetThumbnailPositionUseCaseTest.kt
similarity index 61%
rename from quickstep/tests/multivalentTests/src/com/android/quickstep/recents/usecase/GetThumbnailPositionUseCaseTest.kt
rename to quickstep/tests/multivalentTests/src/com/android/quickstep/recents/domain/usecase/GetThumbnailPositionUseCaseTest.kt
index bd7d970..a253280 100644
--- a/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/usecase/GetThumbnailPositionUseCaseTest.kt
+++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/domain/usecase/GetThumbnailPositionUseCaseTest.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,22 +14,15 @@
* limitations under the License.
*/
-package com.android.quickstep.recents.usecase
+package com.android.quickstep.recents.domain.usecase
-import android.content.ComponentName
-import android.content.Intent
import android.graphics.Bitmap
-import android.graphics.Color
import android.graphics.Matrix
import android.graphics.Rect
import android.view.Surface.ROTATION_90
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.android.quickstep.recents.data.FakeRecentsDeviceProfileRepository
import com.android.quickstep.recents.data.FakeRecentsRotationStateRepository
-import com.android.quickstep.recents.data.FakeTasksRepository
-import com.android.quickstep.recents.usecase.ThumbnailPositionState.MatrixScaling
-import com.android.quickstep.recents.usecase.ThumbnailPositionState.MissingThumbnail
-import com.android.systemui.shared.recents.model.Task
import com.android.systemui.shared.recents.model.ThumbnailData
import com.android.systemui.shared.recents.utilities.PreviewPositionHelper
import com.google.common.truth.Truth.assertThat
@@ -43,55 +36,34 @@
/** Test for [GetThumbnailPositionUseCase] */
@RunWith(AndroidJUnit4::class)
class GetThumbnailPositionUseCaseTest {
- private val task =
- Task(Task.TaskKey(TASK_ID, 0, Intent(), ComponentName("", ""), 0, 2000)).apply {
- colorBackground = Color.BLACK
- }
- private val thumbnailData =
- ThumbnailData(
- thumbnail =
- mock<Bitmap>().apply {
- whenever(width).thenReturn(THUMBNAIL_WIDTH)
- whenever(height).thenReturn(THUMBNAIL_HEIGHT)
- }
- )
-
private val deviceProfileRepository = FakeRecentsDeviceProfileRepository()
private val rotationStateRepository = FakeRecentsRotationStateRepository()
- private val tasksRepository = FakeTasksRepository()
private val previewPositionHelper = mock<PreviewPositionHelper>()
private val systemUnderTest =
GetThumbnailPositionUseCase(
deviceProfileRepository,
rotationStateRepository,
- tasksRepository,
previewPositionHelper,
)
@Test
- fun invisibleTask_returnsIdentityMatrix() = runTest {
- tasksRepository.seedTasks(listOf(task))
-
- assertThat(systemUnderTest.run(TASK_ID, CANVAS_WIDTH, CANVAS_HEIGHT, isRtl = true))
- .isInstanceOf(MissingThumbnail::class.java)
+ fun nullThumbnailData_returnsIdentityMatrix() = runTest {
+ val expectedResult = ThumbnailPosition(Matrix.IDENTITY_MATRIX, false)
+ val result = systemUnderTest.invoke(null, CANVAS_WIDTH, CANVAS_HEIGHT, isRtl = true)
+ assertThat(result).isEqualTo(expectedResult)
}
@Test
- fun visibleTaskWithoutThumbnailData_returnsIdentityMatrix() = runTest {
- tasksRepository.seedTasks(listOf(task))
- tasksRepository.setVisibleTasks(setOf(TASK_ID))
-
- assertThat(systemUnderTest.run(TASK_ID, CANVAS_WIDTH, CANVAS_HEIGHT, isRtl = true))
- .isInstanceOf(MissingThumbnail::class.java)
+ fun withoutThumbnail_returnsIdentityMatrix() = runTest {
+ val expectedResult = ThumbnailPosition(Matrix.IDENTITY_MATRIX, false)
+ val result =
+ systemUnderTest.invoke(ThumbnailData(), CANVAS_WIDTH, CANVAS_HEIGHT, isRtl = true)
+ assertThat(result).isEqualTo(expectedResult)
}
@Test
fun visibleTaskWithThumbnailData_returnsTransformedMatrix() = runTest {
- tasksRepository.seedThumbnailData(mapOf(TASK_ID to thumbnailData))
- tasksRepository.seedTasks(listOf(task))
- tasksRepository.setVisibleTasks(setOf(TASK_ID))
-
val isLargeScreen = true
deviceProfileRepository.setRecentsDeviceProfile(
deviceProfileRepository.getRecentsDeviceProfile().copy(isLargeScreen = isLargeScreen)
@@ -108,13 +80,14 @@
whenever(previewPositionHelper.matrix).thenReturn(MATRIX)
whenever(previewPositionHelper.isOrientationChanged).thenReturn(isRotated)
- assertThat(systemUnderTest.run(TASK_ID, CANVAS_WIDTH, CANVAS_HEIGHT, isRtl))
- .isEqualTo(MatrixScaling(MATRIX, isRotated))
+ val result = systemUnderTest.invoke(THUMBNAIL_DATA, CANVAS_WIDTH, CANVAS_HEIGHT, isRtl)
+ val expectedResult = ThumbnailPosition(MATRIX, isRotated)
+ assertThat(result).isEqualTo(expectedResult)
verify(previewPositionHelper)
.updateThumbnailMatrix(
Rect(0, 0, THUMBNAIL_WIDTH, THUMBNAIL_HEIGHT),
- thumbnailData,
+ THUMBNAIL_DATA,
CANVAS_WIDTH,
CANVAS_HEIGHT,
isLargeScreen,
@@ -123,8 +96,7 @@
)
}
- companion object {
- const val TASK_ID = 2
+ private companion object {
const val THUMBNAIL_WIDTH = 100
const val THUMBNAIL_HEIGHT = 200
const val CANVAS_WIDTH = 300
@@ -133,5 +105,14 @@
Matrix().apply {
setValues(floatArrayOf(2.3f, 4.5f, 2.6f, 7.4f, 3.4f, 2.3f, 2.5f, 6.0f, 3.4f))
}
+
+ val THUMBNAIL_DATA =
+ ThumbnailData(
+ thumbnail =
+ mock<Bitmap>().apply {
+ whenever(width).thenReturn(THUMBNAIL_WIDTH)
+ whenever(height).thenReturn(THUMBNAIL_HEIGHT)
+ }
+ )
}
}
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 08e459b..a97ef0c 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
@@ -29,6 +29,7 @@
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.GetThumbnailPositionUseCase
import com.android.quickstep.recents.domain.usecase.IsThumbnailValidUseCase
import com.android.quickstep.recents.viewmodel.RecentsViewData
import com.android.quickstep.views.TaskViewType
@@ -58,6 +59,7 @@
private val recentsViewData = RecentsViewData()
private val getTaskUseCase = mock<GetTaskUseCase>()
+ private val getThumbnailPositionUseCase = mock<GetThumbnailPositionUseCase>()
private val isThumbnailValidUseCase =
spy(IsThumbnailValidUseCase(FakeRecentsRotationStateRepository()))
private lateinit var sut: TaskViewModel
@@ -71,6 +73,7 @@
getTaskUseCase = getTaskUseCase,
getSysUiStatusNavFlagsUseCase = GetSysUiStatusNavFlagsUseCase(),
isThumbnailValidUseCase = isThumbnailValidUseCase,
+ getThumbnailPositionUseCase = getThumbnailPositionUseCase,
dispatcherProvider = TestDispatcherProvider(unconfinedTestDispatcher),
)
whenever(getTaskUseCase.invoke(TASK_MODEL_1.id)).thenReturn(flow { emit(TASK_MODEL_1) })
@@ -112,6 +115,7 @@
getTaskUseCase = getTaskUseCase,
getSysUiStatusNavFlagsUseCase = GetSysUiStatusNavFlagsUseCase(),
isThumbnailValidUseCase = isThumbnailValidUseCase,
+ getThumbnailPositionUseCase = getThumbnailPositionUseCase,
dispatcherProvider = TestDispatcherProvider(unconfinedTestDispatcher),
)
sut.bind(TASK_MODEL_1.id)
diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/usecase/GetThumbnailUseCaseTest.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/usecase/GetThumbnailUseCaseTest.kt
index 0044631..90ef61d 100644
--- a/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/usecase/GetThumbnailUseCaseTest.kt
+++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/usecase/GetThumbnailUseCaseTest.kt
@@ -21,7 +21,6 @@
import android.graphics.Bitmap
import android.graphics.Color
import com.android.quickstep.recents.data.FakeTasksRepository
-import com.android.quickstep.task.viewmodel.TaskOverlayViewModelTest
import com.android.systemui.shared.recents.model.Task
import com.android.systemui.shared.recents.model.ThumbnailData
import com.google.common.truth.Truth.assertThat
@@ -62,7 +61,7 @@
@Test
fun taskNotVisible_returnsNull() {
tasksRepository.seedTasks(listOf(task))
- tasksRepository.seedThumbnailData(mapOf(TaskOverlayViewModelTest.TASK_ID to thumbnailData))
+ tasksRepository.seedThumbnailData(mapOf(TASK_ID to thumbnailData))
assertThat(systemUnderTest.run(TASK_ID)).isNull()
}
@@ -70,8 +69,8 @@
@Test
fun taskVisible_returnsThumbnail() {
tasksRepository.seedTasks(listOf(task))
- tasksRepository.seedThumbnailData(mapOf(TaskOverlayViewModelTest.TASK_ID to thumbnailData))
- tasksRepository.setVisibleTasks(setOf(TaskOverlayViewModelTest.TASK_ID))
+ tasksRepository.seedThumbnailData(mapOf(TASK_ID to thumbnailData))
+ tasksRepository.setVisibleTasks(setOf(TASK_ID))
assertThat(systemUnderTest.run(TASK_ID)).isEqualTo(thumbnailData.thumbnail)
}
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
deleted file mode 100644
index 4b4e2eb..0000000
--- a/quickstep/tests/multivalentTests/src/com/android/quickstep/task/thumbnail/TaskThumbnailViewModelImplTest.kt
+++ /dev/null
@@ -1,88 +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.Matrix
-import android.platform.test.flag.junit.SetFlagsRule
-import androidx.test.ext.junit.runners.AndroidJUnit4
-import com.android.quickstep.recents.usecase.GetThumbnailPositionUseCase
-import com.android.quickstep.recents.usecase.ThumbnailPositionState.MatrixScaling
-import com.android.quickstep.recents.usecase.ThumbnailPositionState.MissingThumbnail
-import com.android.quickstep.task.viewmodel.TaskThumbnailViewModelImpl
-import com.google.common.truth.Truth.assertThat
-import kotlinx.coroutines.test.StandardTestDispatcher
-import kotlinx.coroutines.test.TestScope
-import kotlinx.coroutines.test.runTest
-import org.junit.Rule
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.mockito.kotlin.mock
-import org.mockito.kotlin.whenever
-
-/** Test for [TaskThumbnailView] */
-@RunWith(AndroidJUnit4::class)
-class TaskThumbnailViewModelImplTest {
- @get:Rule val setFlagsRule = SetFlagsRule()
-
- private val dispatcher = StandardTestDispatcher()
- private val testScope = TestScope(dispatcher)
-
- private val mGetThumbnailPositionUseCase = mock<GetThumbnailPositionUseCase>()
-
- private val systemUnderTest by lazy { TaskThumbnailViewModelImpl(mGetThumbnailPositionUseCase) }
-
- @Test
- fun getSnapshotMatrix_MissingThumbnail() =
- testScope.runTest {
- val taskId = 2
- val isRtl = true
-
- whenever(mGetThumbnailPositionUseCase.run(taskId, CANVAS_WIDTH, CANVAS_HEIGHT, isRtl))
- .thenReturn(MissingThumbnail)
-
- systemUnderTest.bind(taskId)
- assertThat(
- systemUnderTest.getThumbnailPositionState(CANVAS_WIDTH, CANVAS_HEIGHT, isRtl)
- )
- .isEqualTo(Matrix.IDENTITY_MATRIX)
- }
-
- @Test
- fun getSnapshotMatrix_MatrixScaling() =
- testScope.runTest {
- val taskId = 2
- val isRtl = true
-
- whenever(mGetThumbnailPositionUseCase.run(taskId, CANVAS_WIDTH, CANVAS_HEIGHT, isRtl))
- .thenReturn(MatrixScaling(MATRIX, isRotated = false))
-
- systemUnderTest.bind(taskId)
- assertThat(
- systemUnderTest.getThumbnailPositionState(CANVAS_WIDTH, CANVAS_HEIGHT, isRtl)
- )
- .isEqualTo(MATRIX)
- }
-
- private companion object {
- const val CANVAS_WIDTH = 300
- const val CANVAS_HEIGHT = 600
- val MATRIX =
- Matrix().apply {
- setValues(floatArrayOf(2.3f, 4.5f, 2.6f, 7.4f, 3.4f, 2.3f, 2.5f, 6.0f, 3.4f))
- }
- }
-}
diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/task/viewmodel/TaskOverlayViewModelTest.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/task/viewmodel/TaskOverlayViewModelTest.kt
deleted file mode 100644
index 95504af..0000000
--- a/quickstep/tests/multivalentTests/src/com/android/quickstep/task/viewmodel/TaskOverlayViewModelTest.kt
+++ /dev/null
@@ -1,195 +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.viewmodel
-
-import android.content.ComponentName
-import android.content.Intent
-import android.graphics.Bitmap
-import android.graphics.Color
-import android.graphics.Matrix
-import androidx.test.ext.junit.runners.AndroidJUnit4
-import com.android.launcher3.util.TestDispatcherProvider
-import com.android.quickstep.recents.data.FakeTasksRepository
-import com.android.quickstep.recents.usecase.GetThumbnailPositionUseCase
-import com.android.quickstep.recents.usecase.ThumbnailPositionState.MatrixScaling
-import com.android.quickstep.recents.usecase.ThumbnailPositionState.MissingThumbnail
-import com.android.quickstep.recents.viewmodel.RecentsViewData
-import com.android.quickstep.task.thumbnail.TaskOverlayUiState.Disabled
-import com.android.quickstep.task.thumbnail.TaskOverlayUiState.Enabled
-import com.android.quickstep.task.viewmodel.TaskOverlayViewModel.ThumbnailPositionState
-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.ExperimentalCoroutinesApi
-import kotlinx.coroutines.flow.first
-import kotlinx.coroutines.test.TestScope
-import kotlinx.coroutines.test.UnconfinedTestDispatcher
-import kotlinx.coroutines.test.runTest
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.mockito.kotlin.mock
-import org.mockito.kotlin.whenever
-
-/** Test for [TaskOverlayViewModel] */
-@OptIn(ExperimentalCoroutinesApi::class)
-@RunWith(AndroidJUnit4::class)
-class TaskOverlayViewModelTest {
- private val task =
- Task(Task.TaskKey(TASK_ID, 0, Intent(), ComponentName("", ""), 0, 2000)).apply {
- colorBackground = Color.BLACK
- }
- private val thumbnailData =
- ThumbnailData(
- thumbnail =
- mock<Bitmap>().apply {
- whenever(width).thenReturn(THUMBNAIL_WIDTH)
- whenever(height).thenReturn(THUMBNAIL_HEIGHT)
- }
- )
- private val recentsViewData = RecentsViewData()
- private val tasksRepository = FakeTasksRepository()
- private val mGetThumbnailPositionUseCase = mock<GetThumbnailPositionUseCase>()
- private val dispatcher = UnconfinedTestDispatcher()
- private val testScope = TestScope(dispatcher)
- private val systemUnderTest =
- TaskOverlayViewModel(
- task,
- recentsViewData,
- mGetThumbnailPositionUseCase,
- tasksRepository,
- TestDispatcherProvider(dispatcher),
- )
-
- @Test
- fun initialStateIsDisabled() =
- testScope.runTest { assertThat(systemUnderTest.overlayState.first()).isEqualTo(Disabled) }
-
- @Test
- fun recentsViewOverlayDisabled_Disabled() =
- testScope.runTest {
- recentsViewData.overlayEnabled.value = false
- recentsViewData.settledFullyVisibleTaskIds.value = setOf(TASK_ID)
-
- assertThat(systemUnderTest.overlayState.first()).isEqualTo(Disabled)
- }
-
- @Test
- fun taskNotFullyVisible_Disabled() =
- testScope.runTest {
- recentsViewData.overlayEnabled.value = true
- recentsViewData.settledFullyVisibleTaskIds.value = setOf()
-
- assertThat(systemUnderTest.overlayState.first()).isEqualTo(Disabled)
- }
-
- @Test
- fun noThumbnail_Enabled() =
- testScope.runTest {
- recentsViewData.overlayEnabled.value = true
- recentsViewData.settledFullyVisibleTaskIds.value = setOf(TASK_ID)
- task.isLocked = false
-
- assertThat(systemUnderTest.overlayState.first())
- .isEqualTo(Enabled(isRealSnapshot = false, thumbnail = null))
- }
-
- @Test
- fun withThumbnail_RealSnapshot_NotLocked_Enabled() =
- testScope.runTest {
- recentsViewData.overlayEnabled.value = true
- recentsViewData.settledFullyVisibleTaskIds.value = setOf(TASK_ID)
- tasksRepository.seedTasks(listOf(task))
- tasksRepository.seedThumbnailData(mapOf(TASK_ID to thumbnailData))
- tasksRepository.setVisibleTasks(setOf(TASK_ID))
- thumbnailData.isRealSnapshot = true
- task.isLocked = false
-
- assertThat(systemUnderTest.overlayState.first())
- .isEqualTo(Enabled(isRealSnapshot = true, thumbnail = thumbnailData.thumbnail))
- }
-
- @Test
- fun withThumbnail_RealSnapshot_Locked_Enabled() =
- testScope.runTest {
- recentsViewData.overlayEnabled.value = true
- recentsViewData.settledFullyVisibleTaskIds.value = setOf(TASK_ID)
- tasksRepository.seedTasks(listOf(task))
- tasksRepository.seedThumbnailData(mapOf(TASK_ID to thumbnailData))
- tasksRepository.setVisibleTasks(setOf(TASK_ID))
- thumbnailData.isRealSnapshot = true
- task.isLocked = true
-
- assertThat(systemUnderTest.overlayState.first())
- .isEqualTo(Enabled(isRealSnapshot = false, thumbnail = thumbnailData.thumbnail))
- }
-
- @Test
- fun withThumbnail_FakeSnapshot_Enabled() =
- testScope.runTest {
- recentsViewData.overlayEnabled.value = true
- recentsViewData.settledFullyVisibleTaskIds.value = setOf(TASK_ID)
- tasksRepository.seedTasks(listOf(task))
- tasksRepository.seedThumbnailData(mapOf(TASK_ID to thumbnailData))
- tasksRepository.setVisibleTasks(setOf(TASK_ID))
- thumbnailData.isRealSnapshot = false
- task.isLocked = false
-
- assertThat(systemUnderTest.overlayState.first())
- .isEqualTo(Enabled(isRealSnapshot = false, thumbnail = thumbnailData.thumbnail))
- }
-
- @Test
- fun getThumbnailMatrix_MissingThumbnail() =
- testScope.runTest {
- val isRtl = true
-
- whenever(mGetThumbnailPositionUseCase.run(TASK_ID, CANVAS_WIDTH, CANVAS_HEIGHT, isRtl))
- .thenReturn(MissingThumbnail)
-
- assertThat(
- systemUnderTest.getThumbnailPositionState(CANVAS_WIDTH, CANVAS_HEIGHT, isRtl)
- )
- .isEqualTo(ThumbnailPositionState(Matrix.IDENTITY_MATRIX, isRotated = false))
- }
-
- @Test
- fun getThumbnailMatrix_MatrixScaling() =
- testScope.runTest {
- val isRtl = true
- val isRotated = true
-
- whenever(mGetThumbnailPositionUseCase.run(TASK_ID, CANVAS_WIDTH, CANVAS_HEIGHT, isRtl))
- .thenReturn(MatrixScaling(MATRIX, isRotated))
-
- assertThat(
- systemUnderTest.getThumbnailPositionState(CANVAS_WIDTH, CANVAS_HEIGHT, isRtl)
- )
- .isEqualTo(ThumbnailPositionState(MATRIX, isRotated))
- }
-
- companion object {
- const val TASK_ID = 0
- const val THUMBNAIL_WIDTH = 100
- const val THUMBNAIL_HEIGHT = 200
- const val CANVAS_WIDTH = 300
- const val CANVAS_HEIGHT = 600
- val MATRIX =
- Matrix().apply {
- setValues(floatArrayOf(2.3f, 4.5f, 2.6f, 7.4f, 3.4f, 2.3f, 2.5f, 6.0f, 3.4f))
- }
- }
-}
diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/util/SplitAnimationControllerTest.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/util/SplitAnimationControllerTest.kt
index 5051251..9d000a4 100644
--- a/quickstep/tests/multivalentTests/src/com/android/quickstep/util/SplitAnimationControllerTest.kt
+++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/util/SplitAnimationControllerTest.kt
@@ -89,7 +89,7 @@
@Before
fun setup() {
whenever(mockTaskContainer.snapshotView).thenReturn(mockSnapshotView)
- whenever(mockTaskContainer.splitAnimationThumbnail).thenReturn(mockBitmap)
+ whenever(mockTaskContainer.thumbnail).thenReturn(mockBitmap)
whenever(mockTaskContainer.iconView).thenReturn(mockIconView)
whenever(mockTaskContainer.task).thenReturn(mockTask)
whenever(mockIconView.drawable).thenReturn(mockTaskViewDrawable)
@@ -117,7 +117,7 @@
assertEquals(
"Did not fallback to use splitSource icon drawable",
mockSplitSourceDrawable,
- splitAnimInitProps.iconDrawable
+ splitAnimInitProps.iconDrawable,
)
}
@@ -133,7 +133,7 @@
assertEquals(
"Did not use taskView icon drawable",
mockTaskViewDrawable,
- splitAnimInitProps.iconDrawable
+ splitAnimInitProps.iconDrawable,
)
}
@@ -152,7 +152,7 @@
assertEquals(
"Did not use taskView icon drawable",
mockTaskViewDrawable,
- splitAnimInitProps.iconDrawable
+ splitAnimInitProps.iconDrawable,
)
}
@@ -168,7 +168,7 @@
assertEquals(
"Did not use splitSource icon drawable",
mockSplitSourceDrawable,
- splitAnimInitProps.iconDrawable
+ splitAnimInitProps.iconDrawable,
)
}
@@ -190,13 +190,13 @@
val splitAnimInitProps: SplitAnimationController.Companion.SplitAnimInitProps =
splitAnimationController.getFirstAnimInitViews(
{ mockGroupedTaskView },
- { splitSelectSource }
+ { splitSelectSource },
)
assertEquals(
"Did not use splitSource icon drawable",
mockSplitSourceDrawable,
- splitAnimInitProps.iconDrawable
+ splitAnimInitProps.iconDrawable,
)
}
@@ -214,7 +214,7 @@
any(),
any(),
any(),
- any()
+ any(),
)
spySplitAnimationController.playSplitLaunchAnimation(
@@ -230,7 +230,7 @@
null /* info */,
null /* t */,
{} /* finishCallback */,
- 1f /* cornerRadius */
+ 1f, /* cornerRadius */
)
verify(spySplitAnimationController)
@@ -243,7 +243,7 @@
any(),
any(),
any(),
- any()
+ any(),
)
}
@@ -267,7 +267,7 @@
transitionInfo,
transaction,
{} /* finishCallback */,
- 1f /* cornerRadius */
+ 1f, /* cornerRadius */
)
verify(spySplitAnimationController)
@@ -296,7 +296,7 @@
transitionInfo,
transaction,
{} /* finishCallback */,
- 1f /* cornerRadius */
+ 1f, /* cornerRadius */
)
verify(spySplitAnimationController)
@@ -325,7 +325,7 @@
transitionInfo,
transaction,
{} /* finishCallback */,
- 1f /* cornerRadius */
+ 1f, /* cornerRadius */
)
verify(spySplitAnimationController)
@@ -353,7 +353,7 @@
transitionInfo,
transaction,
{} /* finishCallback */,
- 1f /* cornerRadius */
+ 1f, /* cornerRadius */
)
verify(spySplitAnimationController)
@@ -381,7 +381,7 @@
transitionInfo,
transaction,
{} /* finishCallback */,
- 1f /* cornerRadius */
+ 1f, /* cornerRadius */
)
verify(spySplitAnimationController)
@@ -408,7 +408,7 @@
transitionInfo,
transaction,
{} /* finishCallback */,
- 1f /* cornerRadius */
+ 1f, /* cornerRadius */
)
verify(spySplitAnimationController)
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 f2fa0c5..cb088fd 100644
--- a/quickstep/tests/multivalentTests/src/com/android/quickstep/util/TaskGridNavHelperTest.kt
+++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/util/TaskGridNavHelperTest.kt
@@ -16,13 +16,13 @@
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
-import com.android.quickstep.util.TaskGridNavHelper.DIRECTION_RIGHT
-import com.android.quickstep.util.TaskGridNavHelper.DIRECTION_TAB
-import com.android.quickstep.util.TaskGridNavHelper.DIRECTION_UP
+import com.android.quickstep.util.TaskGridNavHelper.Companion.ADD_DESK_PLACEHOLDER_ID
+import com.android.quickstep.util.TaskGridNavHelper.Companion.CLEAR_ALL_PLACEHOLDER_ID
+import com.android.quickstep.util.TaskGridNavHelper.TaskNavDirection.DOWN
+import com.android.quickstep.util.TaskGridNavHelper.TaskNavDirection.LEFT
+import com.android.quickstep.util.TaskGridNavHelper.TaskNavDirection.RIGHT
+import com.android.quickstep.util.TaskGridNavHelper.TaskNavDirection.TAB
+import com.android.quickstep.util.TaskGridNavHelper.TaskNavDirection.UP
import com.google.common.truth.Truth.assertThat
import org.junit.Test
@@ -35,8 +35,7 @@
*/
@Test
fun equalLengthRows_noFocused_onTop_pressDown_goesToBottom() {
- assertThat(getNextGridPage(currentPageTaskViewId = 1, DIRECTION_DOWN, delta = 1))
- .isEqualTo(2)
+ assertThat(getNextGridPage(currentPageTaskViewId = 1, DOWN, delta = 1)).isEqualTo(2)
}
/* ↑----→
@@ -46,7 +45,7 @@
*/
@Test
fun equalLengthRows_noFocused_onTop_pressUp_goesToBottom() {
- assertThat(getNextGridPage(currentPageTaskViewId = 1, DIRECTION_UP, delta = 1)).isEqualTo(2)
+ assertThat(getNextGridPage(currentPageTaskViewId = 1, UP, delta = 1)).isEqualTo(2)
}
/* ↓----↑
@@ -57,8 +56,7 @@
*/
@Test
fun equalLengthRows_noFocused_onBottom_pressDown_goesToTop() {
- assertThat(getNextGridPage(currentPageTaskViewId = 2, DIRECTION_DOWN, delta = 1))
- .isEqualTo(1)
+ assertThat(getNextGridPage(currentPageTaskViewId = 2, DOWN, delta = 1)).isEqualTo(1)
}
/*
@@ -68,7 +66,7 @@
*/
@Test
fun equalLengthRows_noFocused_onBottom_pressUp_goesToTop() {
- assertThat(getNextGridPage(currentPageTaskViewId = 2, DIRECTION_UP, delta = 1)).isEqualTo(1)
+ assertThat(getNextGridPage(currentPageTaskViewId = 2, UP, delta = 1)).isEqualTo(1)
}
/*
@@ -78,8 +76,7 @@
*/
@Test
fun equalLengthRows_noFocused_onTop_pressLeft_goesLeft() {
- assertThat(getNextGridPage(currentPageTaskViewId = 1, DIRECTION_LEFT, delta = 1))
- .isEqualTo(3)
+ assertThat(getNextGridPage(currentPageTaskViewId = 1, LEFT, delta = 1)).isEqualTo(3)
}
/*
@@ -89,8 +86,7 @@
*/
@Test
fun equalLengthRows_noFocused_onBottom_pressLeft_goesLeft() {
- assertThat(getNextGridPage(currentPageTaskViewId = 2, DIRECTION_LEFT, delta = 1))
- .isEqualTo(4)
+ assertThat(getNextGridPage(currentPageTaskViewId = 2, LEFT, delta = 1)).isEqualTo(4)
}
/*
@@ -100,8 +96,7 @@
*/
@Test
fun equalLengthRows_noFocused_onTop_secondItem_pressRight_goesRight() {
- assertThat(getNextGridPage(currentPageTaskViewId = 3, DIRECTION_RIGHT, delta = -1))
- .isEqualTo(1)
+ assertThat(getNextGridPage(currentPageTaskViewId = 3, RIGHT, delta = -1)).isEqualTo(1)
}
/*
@@ -111,8 +106,7 @@
*/
@Test
fun equalLengthRows_noFocused_onBottom_secondItem_pressRight_goesRight() {
- assertThat(getNextGridPage(currentPageTaskViewId = 4, DIRECTION_RIGHT, delta = -1))
- .isEqualTo(2)
+ assertThat(getNextGridPage(currentPageTaskViewId = 4, RIGHT, delta = -1)).isEqualTo(2)
}
/*
@@ -124,7 +118,7 @@
*/
@Test
fun equalLengthRows_noFocused_onTop_pressRight_cycleToClearAll() {
- assertThat(getNextGridPage(currentPageTaskViewId = 1, DIRECTION_RIGHT, delta = -1))
+ assertThat(getNextGridPage(currentPageTaskViewId = 1, RIGHT, delta = -1))
.isEqualTo(CLEAR_ALL_PLACEHOLDER_ID)
}
@@ -137,7 +131,7 @@
*/
@Test
fun equalLengthRows_noFocused_onBottom_pressRight_cycleToClearAll() {
- assertThat(getNextGridPage(currentPageTaskViewId = 2, DIRECTION_RIGHT, delta = -1))
+ assertThat(getNextGridPage(currentPageTaskViewId = 2, RIGHT, delta = -1))
.isEqualTo(CLEAR_ALL_PLACEHOLDER_ID)
}
@@ -149,7 +143,7 @@
*/
@Test
fun equalLengthRows_noFocused_onTop_lastItem_pressLeft_toClearAll() {
- assertThat(getNextGridPage(currentPageTaskViewId = 5, DIRECTION_LEFT, delta = 1))
+ assertThat(getNextGridPage(currentPageTaskViewId = 5, LEFT, delta = 1))
.isEqualTo(CLEAR_ALL_PLACEHOLDER_ID)
}
@@ -161,7 +155,7 @@
*/
@Test
fun equalLengthRows_noFocused_onBottom_lastItem_pressLeft_toClearAll() {
- assertThat(getNextGridPage(currentPageTaskViewId = 6, DIRECTION_LEFT, delta = 1))
+ assertThat(getNextGridPage(currentPageTaskViewId = 6, LEFT, delta = 1))
.isEqualTo(CLEAR_ALL_PLACEHOLDER_ID)
}
@@ -176,11 +170,7 @@
@Test
fun equalLengthRows_noFocused_onClearAll_pressLeft_cycleToFirst() {
assertThat(
- getNextGridPage(
- currentPageTaskViewId = CLEAR_ALL_PLACEHOLDER_ID,
- DIRECTION_LEFT,
- delta = 1,
- )
+ getNextGridPage(currentPageTaskViewId = CLEAR_ALL_PLACEHOLDER_ID, LEFT, delta = 1)
)
.isEqualTo(1)
}
@@ -194,11 +184,7 @@
@Test
fun equalLengthRows_noFocused_onClearAll_pressRight_toLastInBottom() {
assertThat(
- getNextGridPage(
- currentPageTaskViewId = CLEAR_ALL_PLACEHOLDER_ID,
- DIRECTION_RIGHT,
- delta = -1,
- )
+ getNextGridPage(currentPageTaskViewId = CLEAR_ALL_PLACEHOLDER_ID, RIGHT, delta = -1)
)
.isEqualTo(6)
}
@@ -214,7 +200,7 @@
assertThat(
getNextGridPage(
currentPageTaskViewId = FOCUSED_TASK_ID,
- DIRECTION_LEFT,
+ LEFT,
delta = 1,
largeTileIds = listOf(FOCUSED_TASK_ID),
)
@@ -233,7 +219,7 @@
assertThat(
getNextGridPage(
currentPageTaskViewId = FOCUSED_TASK_ID,
- DIRECTION_UP,
+ UP,
delta = 1,
largeTileIds = listOf(FOCUSED_TASK_ID),
)
@@ -254,7 +240,7 @@
assertThat(
getNextGridPage(
currentPageTaskViewId = FOCUSED_TASK_ID,
- DIRECTION_DOWN,
+ DOWN,
delta = 1,
largeTileIds = listOf(FOCUSED_TASK_ID),
)
@@ -275,7 +261,7 @@
assertThat(
getNextGridPage(
currentPageTaskViewId = FOCUSED_TASK_ID,
- DIRECTION_RIGHT,
+ RIGHT,
delta = -1,
largeTileIds = listOf(FOCUSED_TASK_ID),
)
@@ -297,7 +283,7 @@
assertThat(
getNextGridPage(
currentPageTaskViewId = CLEAR_ALL_PLACEHOLDER_ID,
- DIRECTION_LEFT,
+ LEFT,
delta = 1,
largeTileIds = listOf(FOCUSED_TASK_ID),
)
@@ -316,7 +302,7 @@
assertThat(
getNextGridPage(
currentPageTaskViewId = 7,
- DIRECTION_DOWN,
+ DOWN,
delta = 1,
topIds = IntArray.wrap(1, 3, 5, 7),
)
@@ -335,7 +321,7 @@
assertThat(
getNextGridPage(
/* topIds = */ currentPageTaskViewId = 7,
- DIRECTION_UP,
+ UP,
delta = 1,
topIds = IntArray.wrap(1, 3, 5, 7),
)
@@ -353,7 +339,7 @@
assertThat(
getNextGridPage(
/* topIds = */ currentPageTaskViewId = 6,
- DIRECTION_LEFT,
+ LEFT,
delta = 1,
topIds = IntArray.wrap(1, 3, 5, 7),
)
@@ -372,7 +358,7 @@
assertThat(
getNextGridPage(
/* topIds = */ currentPageTaskViewId = CLEAR_ALL_PLACEHOLDER_ID,
- DIRECTION_RIGHT,
+ RIGHT,
delta = -1,
topIds = IntArray.wrap(1, 3, 5, 7),
)
@@ -391,7 +377,7 @@
assertThat(
getNextGridPage(
currentPageTaskViewId = CLEAR_ALL_PLACEHOLDER_ID,
- DIRECTION_RIGHT,
+ RIGHT,
delta = -1,
bottomIds = IntArray.wrap(2, 4, 6, 7),
)
@@ -406,8 +392,7 @@
*/
@Test
fun equalLengthRows_noFocused_onTop_pressTab_goesToBottom() {
- assertThat(getNextGridPage(currentPageTaskViewId = 1, DIRECTION_TAB, delta = 1))
- .isEqualTo(2)
+ assertThat(getNextGridPage(currentPageTaskViewId = 1, TAB, delta = 1)).isEqualTo(2)
}
/*
@@ -418,8 +403,7 @@
*/
@Test
fun equalLengthRows_noFocused_onBottom_pressTab_goesToNextTop() {
- assertThat(getNextGridPage(currentPageTaskViewId = 2, DIRECTION_TAB, delta = 1))
- .isEqualTo(3)
+ assertThat(getNextGridPage(currentPageTaskViewId = 2, TAB, delta = 1)).isEqualTo(3)
}
/*
@@ -431,8 +415,7 @@
*/
@Test
fun equalLengthRows_noFocused_onTop_pressTabWithShift_goesToPreviousBottom() {
- assertThat(getNextGridPage(currentPageTaskViewId = 3, DIRECTION_TAB, delta = -1))
- .isEqualTo(2)
+ assertThat(getNextGridPage(currentPageTaskViewId = 3, TAB, delta = -1)).isEqualTo(2)
}
/*
@@ -442,8 +425,7 @@
*/
@Test
fun equalLengthRows_noFocused_onBottom_pressTabWithShift_goesToTop() {
- assertThat(getNextGridPage(currentPageTaskViewId = 2, DIRECTION_TAB, delta = -1))
- .isEqualTo(1)
+ assertThat(getNextGridPage(currentPageTaskViewId = 2, TAB, delta = -1)).isEqualTo(1)
}
/*
@@ -453,9 +435,7 @@
*/
@Test
fun equalLengthRows_noFocused_onTop_pressTabWithShift_noCycle_staysOnTop() {
- assertThat(
- getNextGridPage(currentPageTaskViewId = 1, DIRECTION_TAB, delta = -1, cycle = false)
- )
+ assertThat(getNextGridPage(currentPageTaskViewId = 1, TAB, delta = -1, cycle = false))
.isEqualTo(1)
}
@@ -469,7 +449,7 @@
assertThat(
getNextGridPage(
currentPageTaskViewId = CLEAR_ALL_PLACEHOLDER_ID,
- DIRECTION_TAB,
+ TAB,
delta = 1,
cycle = false,
)
@@ -487,7 +467,7 @@
assertThat(
getNextGridPage(
currentPageTaskViewId = DESKTOP_TASK_ID,
- DIRECTION_LEFT,
+ LEFT,
delta = 1,
largeTileIds = listOf(DESKTOP_TASK_ID, FOCUSED_TASK_ID),
)
@@ -505,7 +485,7 @@
assertThat(
getNextGridPage(
currentPageTaskViewId = FOCUSED_TASK_ID,
- DIRECTION_RIGHT,
+ RIGHT,
delta = -1,
largeTileIds = listOf(DESKTOP_TASK_ID, FOCUSED_TASK_ID),
)
@@ -525,7 +505,7 @@
assertThat(
getNextGridPage(
currentPageTaskViewId = DESKTOP_TASK_ID,
- DIRECTION_RIGHT,
+ RIGHT,
delta = -1,
largeTileIds = listOf(DESKTOP_TASK_ID, FOCUSED_TASK_ID),
)
@@ -546,7 +526,7 @@
assertThat(
getNextGridPage(
currentPageTaskViewId = CLEAR_ALL_PLACEHOLDER_ID,
- DIRECTION_LEFT,
+ LEFT,
delta = 1,
largeTileIds = listOf(DESKTOP_TASK_ID, FOCUSED_TASK_ID),
)
@@ -565,7 +545,7 @@
assertThat(
getNextGridPage(
currentPageTaskViewId = 2,
- DIRECTION_RIGHT,
+ RIGHT,
delta = -1,
largeTileIds = listOf(DESKTOP_TASK_ID, FOCUSED_TASK_ID),
)
@@ -584,7 +564,7 @@
assertThat(
getNextGridPage(
currentPageTaskViewId = 1,
- DIRECTION_RIGHT,
+ RIGHT,
delta = -1,
largeTileIds = listOf(DESKTOP_TASK_ID, FOCUSED_TASK_ID),
)
@@ -603,7 +583,7 @@
assertThat(
getNextGridPage(
currentPageTaskViewId = DESKTOP_TASK_ID,
- DIRECTION_TAB,
+ TAB,
delta = 1,
largeTileIds = listOf(DESKTOP_TASK_ID, FOCUSED_TASK_ID),
)
@@ -621,7 +601,7 @@
assertThat(
getNextGridPage(
currentPageTaskViewId = FOCUSED_TASK_ID,
- DIRECTION_LEFT,
+ LEFT,
delta = 1,
topIds = IntArray(),
bottomIds = IntArray.wrap(2),
@@ -643,7 +623,7 @@
assertThat(
getNextGridPage(
currentPageTaskViewId = DESKTOP_TASK_ID,
- DIRECTION_TAB,
+ TAB,
delta = -1,
largeTileIds = listOf(DESKTOP_TASK_ID, FOCUSED_TASK_ID),
)
@@ -662,7 +642,7 @@
assertThat(
getNextGridPage(
currentPageTaskViewId = 1,
- DIRECTION_RIGHT,
+ RIGHT,
delta = -1,
hasAddDesktopButton = true,
)
@@ -681,7 +661,7 @@
assertThat(
getNextGridPage(
currentPageTaskViewId = 2,
- DIRECTION_RIGHT,
+ RIGHT,
delta = -1,
hasAddDesktopButton = true,
)
@@ -701,7 +681,7 @@
assertThat(
getNextGridPage(
currentPageTaskViewId = ADD_DESK_PLACEHOLDER_ID,
- DIRECTION_RIGHT,
+ RIGHT,
delta = -1,
hasAddDesktopButton = true,
)
@@ -722,7 +702,7 @@
assertThat(
getNextGridPage(
currentPageTaskViewId = CLEAR_ALL_PLACEHOLDER_ID,
- DIRECTION_LEFT,
+ LEFT,
delta = 1,
hasAddDesktopButton = true,
)
@@ -741,7 +721,7 @@
assertThat(
getNextGridPage(
currentPageTaskViewId = ADD_DESK_PLACEHOLDER_ID,
- DIRECTION_UP,
+ UP,
delta = 1,
hasAddDesktopButton = true,
)
@@ -760,7 +740,7 @@
assertThat(
getNextGridPage(
currentPageTaskViewId = ADD_DESK_PLACEHOLDER_ID,
- DIRECTION_DOWN,
+ DOWN,
delta = 1,
hasAddDesktopButton = true,
)
@@ -778,7 +758,7 @@
assertThat(
getNextGridPage(
currentPageTaskViewId = ADD_DESK_PLACEHOLDER_ID,
- DIRECTION_LEFT,
+ LEFT,
delta = 1,
largeTileIds = listOf(DESKTOP_TASK_ID),
hasAddDesktopButton = true,
@@ -797,7 +777,7 @@
assertThat(
getNextGridPage(
currentPageTaskViewId = DESKTOP_TASK_ID,
- DIRECTION_RIGHT,
+ RIGHT,
delta = -1,
largeTileIds = listOf(DESKTOP_TASK_ID),
hasAddDesktopButton = true,
@@ -806,9 +786,43 @@
.isEqualTo(ADD_DESK_PLACEHOLDER_ID)
}
+ // Col offset: 0 1 2
+ // -----------
+ // ID grid: 4 2 0 start
+ // end [5] 3 1
+ @Test
+ fun gridTaskViewIdOffsetPairInTabOrderSequence_towardsStart() {
+ val expected = listOf(Pair(4, 0), Pair(3, 1), Pair(2, 1), Pair(1, 2), Pair(0, 2))
+ assertThat(
+ gridTaskViewIdOffsetPairInTabOrderSequence(
+ initialTaskViewId = 5,
+ towardsStart = true,
+ )
+ .toList()
+ )
+ .isEqualTo(expected)
+ }
+
+ // Col offset: 2 1 0
+ // -----------
+ // ID grid: 4 2 [0] start
+ // end 5 3 1
+ @Test
+ fun gridTaskViewIdOffsetPairInTabOrderSequence_towardsEnd() {
+ val expected = listOf(Pair(1, 0), Pair(2, 1), Pair(3, 1), Pair(4, 2), Pair(5, 2))
+ assertThat(
+ gridTaskViewIdOffsetPairInTabOrderSequence(
+ initialTaskViewId = 0,
+ towardsStart = false,
+ )
+ .toList()
+ )
+ .isEqualTo(expected)
+ }
+
private fun getNextGridPage(
currentPageTaskViewId: Int,
- direction: Int,
+ direction: TaskGridNavHelper.TaskNavDirection,
delta: Int,
topIds: IntArray = IntArray.wrap(1, 3, 5),
bottomIds: IntArray = IntArray.wrap(2, 4, 6),
@@ -821,6 +835,22 @@
return taskGridNavHelper.getNextGridPage(currentPageTaskViewId, delta, direction, cycle)
}
+ private fun gridTaskViewIdOffsetPairInTabOrderSequence(
+ initialTaskViewId: Int,
+ towardsStart: Boolean,
+ topIds: IntArray = IntArray.wrap(0, 2, 4),
+ bottomIds: IntArray = IntArray.wrap(1, 3, 5),
+ largeTileIds: List<Int> = emptyList(),
+ hasAddDesktopButton: Boolean = false,
+ ): Sequence<Pair<Int, Int>> {
+ val taskGridNavHelper =
+ TaskGridNavHelper(topIds, bottomIds, largeTileIds, hasAddDesktopButton)
+ return taskGridNavHelper.gridTaskViewIdOffsetPairInTabOrderSequence(
+ initialTaskViewId,
+ towardsStart,
+ )
+ }
+
private companion object {
const val FOCUSED_TASK_ID = 99
const val DESKTOP_TASK_ID = 100
diff --git a/quickstep/tests/src/com/android/quickstep/TaplTestsQuickstep.java b/quickstep/tests/src/com/android/quickstep/TaplTestsQuickstep.java
index 79d3c19..aaaa0e4 100644
--- a/quickstep/tests/src/com/android/quickstep/TaplTestsQuickstep.java
+++ b/quickstep/tests/src/com/android/quickstep/TaplTestsQuickstep.java
@@ -62,6 +62,7 @@
import org.junit.Test;
import org.junit.runner.RunWith;
+import java.util.Comparator;
import java.util.function.Consumer;
import java.util.function.Function;
@@ -557,6 +558,27 @@
numTasks == null ? 0 : numTasks, recentsView.getTaskViewCount()));
}
+ @Test
+ @PortraitLandscape
+ public void testDismissBottomRow() throws Exception {
+ assumeTrue(mLauncher.isTablet());
+ mLauncher.goHome().switchToOverview().dismissAllTasks();
+ startTestAppsWithCheck();
+ Overview overview = mLauncher.goHome().switchToOverview();
+ assertIsInState("Launcher internal state didn't switch to Overview",
+ ExpectedState.OVERVIEW);
+ final Integer numTasks = getFromRecentsView(RecentsView::getTaskViewCount);
+ OverviewTask bottomTask = overview.getCurrentTasksForTablet().stream().max(
+ Comparator.comparingInt(OverviewTask::getTaskCenterY)).get();
+ assertNotNull("bottomTask null", bottomTask);
+
+ bottomTask.dismiss();
+
+ runOnRecentsView(recentsView -> assertEquals(
+ "Dismissing a bottomTask didn't remove 1 bottomTask from Overview",
+ numTasks - 1, recentsView.getTaskViewCount()));
+ }
+
private void startTestAppsWithCheck() throws Exception {
startTestApps();
expectLaunchedAppState();
diff --git a/quickstep/tests/src/com/android/quickstep/desktop/DesktopAppLaunchAnimatorHelperTest.kt b/quickstep/tests/src/com/android/quickstep/desktop/DesktopAppLaunchAnimatorHelperTest.kt
new file mode 100644
index 0000000..b4d9f5b
--- /dev/null
+++ b/quickstep/tests/src/com/android/quickstep/desktop/DesktopAppLaunchAnimatorHelperTest.kt
@@ -0,0 +1,279 @@
+/*
+ * 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.desktop
+
+import android.animation.Animator
+import android.animation.AnimatorSet
+import android.animation.ValueAnimator
+import android.app.ActivityManager
+import android.app.WindowConfiguration
+import android.content.ComponentName
+import android.content.Context
+import android.content.Intent
+import android.content.res.Resources
+import android.platform.test.annotations.DisableFlags
+import android.platform.test.annotations.EnableFlags
+import android.platform.test.flag.junit.SetFlagsRule
+import android.util.DisplayMetrics
+import android.view.SurfaceControl
+import android.view.WindowManager
+import android.window.TransitionInfo
+import androidx.core.util.Supplier
+import com.android.app.animation.Interpolators
+import com.android.internal.jank.Cuj
+import com.android.launcher3.desktop.DesktopAppLaunchAnimatorHelper
+import com.android.launcher3.desktop.DesktopAppLaunchTransition.AppLaunchType
+import com.android.launcher3.util.Executors.MAIN_EXECUTOR
+import com.android.window.flags.Flags
+import com.google.common.truth.Truth.assertThat
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.mockito.kotlin.any
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.whenever
+
+class DesktopAppLaunchAnimatorHelperTest {
+
+ @get:Rule val setFlagsRule = SetFlagsRule()
+
+ private val context = mock<Context>()
+ private val resources = mock<Resources>()
+ private val transaction = mock<SurfaceControl.Transaction>()
+ private val transactionSupplier = mock<Supplier<SurfaceControl.Transaction>>()
+
+ private lateinit var helper: DesktopAppLaunchAnimatorHelper
+
+ @Before
+ fun setUp() {
+ helper =
+ DesktopAppLaunchAnimatorHelper(
+ context = context,
+ launchType = AppLaunchType.LAUNCH,
+ cujType = Cuj.CUJ_DESKTOP_MODE_APP_LAUNCH_FROM_INTENT,
+ transactionSupplier = transactionSupplier,
+ )
+ whenever(transactionSupplier.get()).thenReturn(transaction)
+ whenever(transaction.setCrop(any(), any())).thenReturn(transaction)
+ whenever(transaction.setCornerRadius(any(), any())).thenReturn(transaction)
+
+ whenever(context.resources).thenReturn(resources)
+ whenever(resources.displayMetrics).thenReturn(DisplayMetrics())
+ whenever(context.mainThreadHandler).thenReturn(MAIN_EXECUTOR.handler)
+ }
+
+ @Test
+ fun launchTransition_returnsLaunchAnimator() {
+ val openChange =
+ TransitionInfo.Change(mock(), mock()).apply {
+ mode = WindowManager.TRANSIT_OPEN
+ taskInfo = TASK_INFO_FREEFORM
+ }
+ val transitionInfo = TransitionInfo(WindowManager.TRANSIT_NONE, 0)
+ transitionInfo.addChange(openChange)
+
+ val actual = helper.createAnimators(transitionInfo, finishCallback = {})
+
+ assertThat(actual).hasSize(1)
+ assertLaunchAnimator(actual[0])
+ }
+
+ @Test
+ fun minimizeTransition_returnsLaunchAndMinimizeAnimator() {
+ val openChange =
+ TransitionInfo.Change(mock(), mock()).apply {
+ mode = WindowManager.TRANSIT_OPEN
+ taskInfo = TASK_INFO_FREEFORM
+ }
+ val minimizeChange =
+ TransitionInfo.Change(mock(), mock()).apply {
+ mode = WindowManager.TRANSIT_TO_BACK
+ taskInfo = TASK_INFO_FREEFORM
+ }
+ val transitionInfo = TransitionInfo(WindowManager.TRANSIT_NONE, 0)
+ transitionInfo.addChange(openChange)
+ transitionInfo.addChange(minimizeChange)
+
+ val actual = helper.createAnimators(transitionInfo, finishCallback = {})
+
+ assertThat(actual).hasSize(2)
+ assertLaunchAnimator(actual[0])
+ assertMinimizeAnimator(actual[1])
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_TRAMPOLINE_CLOSE_ANIMATION_BUGFIX)
+ fun trampolineTransition_flagEnabled_returnsLaunchAndCloseAnimator() {
+ val openChange =
+ TransitionInfo.Change(mock(), mock()).apply {
+ mode = WindowManager.TRANSIT_OPEN
+ taskInfo = TASK_INFO_FREEFORM
+ }
+ val closeChange =
+ TransitionInfo.Change(mock(), mock()).apply {
+ mode = WindowManager.TRANSIT_CLOSE
+ taskInfo = TASK_INFO_FREEFORM
+ }
+ val transitionInfo = TransitionInfo(WindowManager.TRANSIT_NONE, 0)
+ transitionInfo.addChange(openChange)
+ transitionInfo.addChange(closeChange)
+
+ val actual = helper.createAnimators(transitionInfo, finishCallback = {})
+
+ assertThat(actual).hasSize(2)
+ assertTrampolineLaunchAnimator(actual[0])
+ assertCloseAnimator(actual[1])
+ }
+
+ @Test
+ @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_TRAMPOLINE_CLOSE_ANIMATION_BUGFIX)
+ fun trampolineTransition_flagDisabled_returnsLaunchAnimator() {
+ val openChange =
+ TransitionInfo.Change(mock(), mock()).apply {
+ mode = WindowManager.TRANSIT_OPEN
+ taskInfo = TASK_INFO_FREEFORM
+ }
+ val closeChange =
+ TransitionInfo.Change(mock(), mock()).apply {
+ mode = WindowManager.TRANSIT_CLOSE
+ taskInfo = TASK_INFO_FREEFORM
+ }
+ val transitionInfo = TransitionInfo(WindowManager.TRANSIT_NONE, 0)
+ transitionInfo.addChange(openChange)
+ transitionInfo.addChange(closeChange)
+
+ val actual = helper.createAnimators(transitionInfo, finishCallback = {})
+
+ assertThat(actual).hasSize(1)
+ assertLaunchAnimator(actual[0])
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_TRAMPOLINE_CLOSE_ANIMATION_BUGFIX)
+ fun trampolineTransition_flagEnabled_hitDesktopWindowLimit_returnsLaunchMinimizeCloseAnimator() {
+ val openChange =
+ TransitionInfo.Change(mock(), mock()).apply {
+ mode = WindowManager.TRANSIT_OPEN
+ taskInfo = TASK_INFO_FREEFORM
+ }
+ val minimizeChange =
+ TransitionInfo.Change(mock(), mock()).apply {
+ mode = WindowManager.TRANSIT_TO_BACK
+ taskInfo = TASK_INFO_FREEFORM
+ }
+ val closeChange =
+ TransitionInfo.Change(mock(), mock()).apply {
+ mode = WindowManager.TRANSIT_CLOSE
+ taskInfo = TASK_INFO_FREEFORM
+ }
+ val transitionInfo = TransitionInfo(WindowManager.TRANSIT_NONE, 0)
+ transitionInfo.addChange(openChange)
+ transitionInfo.addChange(minimizeChange)
+ transitionInfo.addChange(closeChange)
+
+ val actual = helper.createAnimators(transitionInfo, finishCallback = {})
+
+ assertThat(actual).hasSize(3)
+ assertTrampolineLaunchAnimator(actual[0])
+ assertMinimizeAnimator(actual[1])
+ assertCloseAnimator(actual[2])
+ }
+
+ @Test
+ @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_TRAMPOLINE_CLOSE_ANIMATION_BUGFIX)
+ fun trampolineTransition_flagDisabled_hitDesktopWindowLimit_returnsLaunchMinimizeAnimator() {
+ val openChange =
+ TransitionInfo.Change(mock(), mock()).apply {
+ mode = WindowManager.TRANSIT_OPEN
+ taskInfo = TASK_INFO_FREEFORM
+ }
+ val minimizeChange =
+ TransitionInfo.Change(mock(), mock()).apply {
+ mode = WindowManager.TRANSIT_TO_BACK
+ taskInfo = TASK_INFO_FREEFORM
+ }
+ val closeChange =
+ TransitionInfo.Change(mock(), mock()).apply {
+ mode = WindowManager.TRANSIT_CLOSE
+ taskInfo = TASK_INFO_FREEFORM
+ }
+ val transitionInfo = TransitionInfo(WindowManager.TRANSIT_NONE, 0)
+ transitionInfo.addChange(openChange)
+ transitionInfo.addChange(minimizeChange)
+ transitionInfo.addChange(closeChange)
+
+ val actual = helper.createAnimators(transitionInfo, finishCallback = {})
+
+ assertThat(actual).hasSize(2)
+ assertLaunchAnimator(actual[0])
+ assertMinimizeAnimator(actual[1])
+ }
+
+ private fun assertLaunchAnimator(animator: Animator) {
+ assertThat(animator).isInstanceOf(AnimatorSet::class.java)
+ assertThat((animator as AnimatorSet).childAnimations.size).isEqualTo(2)
+ assertThat(animator.childAnimations[0]).isInstanceOf(ValueAnimator::class.java)
+ assertThat(animator.childAnimations[0].interpolator)
+ .isEqualTo(AppLaunchType.LAUNCH.boundsAnimationParams.interpolator)
+ assertThat(animator.childAnimations[0].duration)
+ .isEqualTo(AppLaunchType.LAUNCH.boundsAnimationParams.durationMs)
+ assertThat(animator.childAnimations[1]).isInstanceOf(ValueAnimator::class.java)
+ assertThat(animator.childAnimations[1].interpolator).isEqualTo(Interpolators.LINEAR)
+ assertThat(animator.childAnimations[1].duration)
+ .isEqualTo(AppLaunchType.LAUNCH.alphaDurationMs)
+ }
+
+ private fun assertTrampolineLaunchAnimator(animator: Animator) {
+ assertThat(animator).isInstanceOf(AnimatorSet::class.java)
+ assertThat((animator as AnimatorSet).childAnimations.size).isEqualTo(1)
+ assertThat(animator.childAnimations[0]).isInstanceOf(ValueAnimator::class.java)
+ assertThat(animator.childAnimations[0].interpolator).isEqualTo(Interpolators.LINEAR)
+ assertThat(animator.childAnimations[0].duration)
+ .isEqualTo(AppLaunchType.LAUNCH.alphaDurationMs)
+ }
+
+ private fun assertMinimizeAnimator(animator: Animator) {
+ assertThat(animator).isInstanceOf(AnimatorSet::class.java)
+ assertThat((animator as AnimatorSet).childAnimations.size).isEqualTo(2)
+ assertThat(animator.childAnimations[0]).isInstanceOf(ValueAnimator::class.java)
+ assertThat(animator.childAnimations[0].interpolator)
+ .isInstanceOf(Interpolators.STANDARD_ACCELERATE::class.java)
+ assertThat(animator.childAnimations[0].duration).isEqualTo(200)
+ assertThat(animator.childAnimations[1]).isInstanceOf(ValueAnimator::class.java)
+ assertThat(animator.childAnimations[1].interpolator)
+ .isInstanceOf(Interpolators.LINEAR::class.java)
+ assertThat(animator.childAnimations[1].duration).isEqualTo(100)
+ }
+
+ private fun assertCloseAnimator(animator: Animator) {
+ assertThat(animator).isInstanceOf(ValueAnimator::class.java)
+ assertThat(animator.interpolator).isInstanceOf(Interpolators.LINEAR::class.java)
+ assertThat(animator.duration).isEqualTo(100)
+ }
+
+ private companion object {
+ val TASK_INFO_FREEFORM =
+ ActivityManager.RunningTaskInfo().apply {
+ baseIntent =
+ Intent().apply {
+ component = ComponentName("com.example.app", "com.example.app.MainActivity")
+ }
+ configuration.windowConfiguration.windowingMode =
+ WindowConfiguration.WINDOWING_MODE_FREEFORM
+ }
+ }
+}
diff --git a/src/com/android/launcher3/BubbleTextView.java b/src/com/android/launcher3/BubbleTextView.java
index e6b3457..03dd943 100644
--- a/src/com/android/launcher3/BubbleTextView.java
+++ b/src/com/android/launcher3/BubbleTextView.java
@@ -75,8 +75,8 @@
import com.android.launcher3.dragndrop.DragOptions.PreDragCondition;
import com.android.launcher3.dragndrop.DraggableView;
import com.android.launcher3.folder.FolderIcon;
-import com.android.launcher3.graphics.IconShape;
import com.android.launcher3.graphics.PreloadIconDrawable;
+import com.android.launcher3.graphics.ShapeDelegate;
import com.android.launcher3.icons.DotRenderer;
import com.android.launcher3.icons.FastBitmapDrawable;
import com.android.launcher3.icons.IconCache.ItemInfoUpdateReceiver;
@@ -724,7 +724,7 @@
if (!mForceHideDot && (hasDot() || mDotParams.scale > 0)) {
getIconBounds(mDotParams.iconBounds);
Utilities.scaleRectAboutCenter(mDotParams.iconBounds,
- IconShape.INSTANCE.get(getContext()).getNormalizationScale());
+ ShapeDelegate.getNormalizationScale());
final int scrollX = getScrollX();
final int scrollY = getScrollY();
canvas.translate(scrollX, scrollY);
@@ -775,7 +775,7 @@
getIconBounds(mRunningAppIconBounds);
Utilities.scaleRectAboutCenter(
mRunningAppIconBounds,
- IconShape.INSTANCE.get(getContext()).getNormalizationScale());
+ ShapeDelegate.getNormalizationScale());
final boolean isMinimized = mRunningAppState == RunningAppState.MINIMIZED;
final int indicatorTop = mRunningAppIconBounds.bottom + mRunningAppIndicatorTopMargin;
diff --git a/src/com/android/launcher3/DeviceProfile.java b/src/com/android/launcher3/DeviceProfile.java
index 3b283c3..f34add8 100644
--- a/src/com/android/launcher3/DeviceProfile.java
+++ b/src/com/android/launcher3/DeviceProfile.java
@@ -52,7 +52,7 @@
import com.android.launcher3.CellLayout.ContainerType;
import com.android.launcher3.DevicePaddings.DevicePadding;
import com.android.launcher3.folder.ClippedFolderIconLayoutRule;
-import com.android.launcher3.graphics.IconShape;
+import com.android.launcher3.graphics.ThemeManager;
import com.android.launcher3.icons.DotRenderer;
import com.android.launcher3.icons.IconNormalizer;
import com.android.launcher3.model.data.ItemInfo;
@@ -388,7 +388,7 @@
/** TODO: Once we fully migrate to staged split, remove "isMultiWindowMode" */
DeviceProfile(Context context, InvariantDeviceProfile inv, Info info,
- WindowManagerProxy wmProxy, IconShape iconShape, WindowBounds windowBounds,
+ WindowManagerProxy wmProxy, ThemeManager themeManager, WindowBounds windowBounds,
SparseArray<DotRenderer> dotRendererCache, boolean isMultiWindowMode,
boolean transposeLayoutWithOrientation, boolean isMultiDisplay, boolean isGestureMode,
@NonNull final ViewScaleProvider viewScaleProvider,
@@ -846,8 +846,8 @@
dimensionOverrideProvider.accept(this);
// This is done last, after iconSizePx is calculated above.
- mDotRendererWorkSpace = createDotRenderer(iconShape, iconSizePx, dotRendererCache);
- mDotRendererAllApps = createDotRenderer(iconShape, allAppsIconSizePx, dotRendererCache);
+ mDotRendererWorkSpace = createDotRenderer(themeManager, iconSizePx, dotRendererCache);
+ mDotRendererAllApps = createDotRenderer(themeManager, allAppsIconSizePx, dotRendererCache);
}
/**
@@ -869,12 +869,12 @@
}
private static DotRenderer createDotRenderer(
- @NonNull IconShape iconShape, int size, @NonNull SparseArray<DotRenderer> cache) {
+ @NonNull ThemeManager themeManager, int size, @NonNull SparseArray<DotRenderer> cache) {
DotRenderer renderer = cache.get(size);
if (renderer == null) {
renderer = new DotRenderer(
size,
- iconShape.getShape().getPath(DEFAULT_DOT_SIZE),
+ themeManager.getIconShape().getPath(DEFAULT_DOT_SIZE),
DEFAULT_DOT_SIZE);
cache.put(size, renderer);
}
@@ -2478,7 +2478,7 @@
private final InvariantDeviceProfile mInv;
private final Info mInfo;
private final WindowManagerProxy mWMProxy;
- private final IconShape mIconShape;
+ private final ThemeManager mThemeManager;
private WindowBounds mWindowBounds;
private boolean mIsMultiDisplay;
@@ -2495,12 +2495,12 @@
private boolean mIsTransientTaskbar;
public Builder(Context context, InvariantDeviceProfile inv, Info info,
- WindowManagerProxy wmProxy, IconShape iconShape) {
+ WindowManagerProxy wmProxy, ThemeManager themeManager) {
mContext = context;
mInv = inv;
mInfo = info;
mWMProxy = wmProxy;
- mIconShape = iconShape;
+ mThemeManager = themeManager;
mIsTransientTaskbar = info.isTransientTaskbar();
}
@@ -2581,7 +2581,7 @@
if (mOverrideProvider == null) {
mOverrideProvider = DEFAULT_DIMENSION_PROVIDER;
}
- return new DeviceProfile(mContext, mInv, mInfo, mWMProxy, mIconShape,
+ return new DeviceProfile(mContext, mInv, mInfo, mWMProxy, mThemeManager,
mWindowBounds, mDotRendererCache,
mIsMultiWindowMode, mTransposeLayoutWithOrientation, mIsMultiDisplay,
mIsGestureMode, mViewScaleProvider, mOverrideProvider, mIsTransientTaskbar);
diff --git a/src/com/android/launcher3/InvariantDeviceProfile.java b/src/com/android/launcher3/InvariantDeviceProfile.java
index 4107c8f..c2d6df5 100644
--- a/src/com/android/launcher3/InvariantDeviceProfile.java
+++ b/src/com/android/launcher3/InvariantDeviceProfile.java
@@ -57,7 +57,7 @@
import com.android.launcher3.dagger.ApplicationContext;
import com.android.launcher3.dagger.LauncherAppComponent;
import com.android.launcher3.dagger.LauncherAppSingleton;
-import com.android.launcher3.graphics.IconShape;
+import com.android.launcher3.graphics.ThemeManager;
import com.android.launcher3.icons.DotRenderer;
import com.android.launcher3.logging.FileLog;
import com.android.launcher3.model.DeviceGridState;
@@ -136,7 +136,7 @@
private final DisplayController mDisplayController;
private final WindowManagerProxy mWMProxy;
private final LauncherPrefs mPrefs;
- private final IconShape mIconShape;
+ private final ThemeManager mThemeManager;
/**
* Number of icons per row and column in the workspace.
@@ -260,12 +260,12 @@
LauncherPrefs prefs,
DisplayController dc,
WindowManagerProxy wmProxy,
- IconShape iconShape,
+ ThemeManager themeManager,
DaggerSingletonTracker lifeCycle) {
mDisplayController = dc;
mWMProxy = wmProxy;
mPrefs = prefs;
- mIconShape = iconShape;
+ mThemeManager = themeManager;
String gridName = prefs.get(GRID_NAME);
initGrid(context, gridName);
@@ -490,7 +490,7 @@
}
DeviceProfile.Builder newDPBuilder(Context context, Info info) {
- return new DeviceProfile.Builder(context, this, info, mWMProxy, mIconShape);
+ return new DeviceProfile.Builder(context, this, info, mWMProxy, mThemeManager);
}
public void addOnChangeListener(OnIDPChangeListener listener) {
diff --git a/src/com/android/launcher3/Launcher.java b/src/com/android/launcher3/Launcher.java
index 315301a..3edba99 100644
--- a/src/com/android/launcher3/Launcher.java
+++ b/src/com/android/launcher3/Launcher.java
@@ -71,6 +71,7 @@
import static com.android.launcher3.LauncherState.NO_SCALE;
import static com.android.launcher3.LauncherState.SPRING_LOADED;
import static com.android.launcher3.Utilities.postAsyncCallback;
+import static com.android.launcher3.anim.AnimatorListeners.forEndCallback;
import static com.android.launcher3.config.FeatureFlags.FOLDABLE_SINGLE_PAGE;
import static com.android.launcher3.config.FeatureFlags.MULTI_SELECT_EDIT_MODE;
import static com.android.launcher3.logging.KeyboardStateManager.KeyboardState.HIDE;
@@ -182,8 +183,8 @@
import com.android.launcher3.celllayout.CellPosMapper.TwoPanelCellPosMapper;
import com.android.launcher3.compat.AccessibilityManagerCompat;
import com.android.launcher3.config.FeatureFlags;
-import com.android.launcher3.debug.TestEvent;
import com.android.launcher3.debug.TestEventEmitter;
+import com.android.launcher3.debug.TestEventEmitter.TestEvent;
import com.android.launcher3.dot.DotInfo;
import com.android.launcher3.dragndrop.DragController;
import com.android.launcher3.dragndrop.DragLayer;
@@ -610,7 +611,7 @@
RuleController.getInstance(this).setRules(
RuleController.parseRules(this, R.xml.split_configuration));
}
- TestEventEmitter.INSTANCE.get(this).sendEvent(TestEvent.LAUNCHER_ON_CREATE);
+ TestEventEmitter.sendEvent(TestEvent.LAUNCHER_ON_CREATE);
}
protected ModelCallbacks createModelCallbacks() {
@@ -1466,7 +1467,10 @@
}
getModelWriter().addItemToDatabase(info, container, screenId, cellXY[0], cellXY[1]);
- mWorkspace.addInScreen(view, info);
+ AnimatorSet anim = new AnimatorSet();
+ anim.addListener(forEndCallback(() ->
+ view.sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_FOCUSED)));
+ bindInflatedItems(Collections.singletonList(Pair.create(info, view)), anim);
} else {
// Adding a shortcut to a Folder.
FolderIcon folderIcon = findFolderIcon(container);
diff --git a/src/com/android/launcher3/ModelCallbacks.kt b/src/com/android/launcher3/ModelCallbacks.kt
index 5338fb4..d01f35d 100644
--- a/src/com/android/launcher3/ModelCallbacks.kt
+++ b/src/com/android/launcher3/ModelCallbacks.kt
@@ -11,8 +11,8 @@
import com.android.launcher3.WorkspaceLayoutManager.FIRST_SCREEN_ID
import com.android.launcher3.allapps.AllAppsStore
import com.android.launcher3.config.FeatureFlags
-import com.android.launcher3.debug.TestEvent
import com.android.launcher3.debug.TestEventEmitter
+import com.android.launcher3.debug.TestEventEmitter.TestEvent
import com.android.launcher3.model.BgDataModel
import com.android.launcher3.model.StringCache
import com.android.launcher3.model.data.AppInfo
@@ -154,7 +154,7 @@
/*pause=*/ false,
deviceProfile.isTwoPanels,
)
- TestEventEmitter.INSTANCE.get(launcher).sendEvent(TestEvent.WORKSPACE_FINISH_LOADING)
+ TestEventEmitter.sendEvent(TestEvent.WORKSPACE_FINISH_LOADING)
}
/**
diff --git a/src/com/android/launcher3/Workspace.java b/src/com/android/launcher3/Workspace.java
index b41a425..cfb1161 100644
--- a/src/com/android/launcher3/Workspace.java
+++ b/src/com/android/launcher3/Workspace.java
@@ -80,8 +80,8 @@
import com.android.launcher3.celllayout.CellPosMapper;
import com.android.launcher3.celllayout.CellPosMapper.CellPos;
import com.android.launcher3.config.FeatureFlags;
-import com.android.launcher3.debug.TestEvent;
import com.android.launcher3.debug.TestEventEmitter;
+import com.android.launcher3.debug.TestEventEmitter.TestEvent;
import com.android.launcher3.dot.FolderDotInfo;
import com.android.launcher3.dragndrop.DragController;
import com.android.launcher3.dragndrop.DragLayer;
@@ -2255,7 +2255,7 @@
if (d.stateAnnouncer != null && !droppedOnOriginalCell) {
d.stateAnnouncer.completeAction(R.string.item_moved);
}
- TestEventEmitter.INSTANCE.get(getContext()).sendEvent(TestEvent.WORKSPACE_ON_DROP);
+ TestEventEmitter.sendEvent(TestEvent.WORKSPACE_ON_DROP);
}
@Nullable
diff --git a/src/com/android/launcher3/allapps/ActivityAllAppsContainerView.java b/src/com/android/launcher3/allapps/ActivityAllAppsContainerView.java
index 260ff9f..fafa60b 100644
--- a/src/com/android/launcher3/allapps/ActivityAllAppsContainerView.java
+++ b/src/com/android/launcher3/allapps/ActivityAllAppsContainerView.java
@@ -116,6 +116,7 @@
ScrimView.ScrimDrawingController {
+ private static final String TAG = "ActivityAllAppsContainerView";
public static final float PULL_MULTIPLIER = .02f;
public static final float FLING_VELOCITY_MULTIPLIER = 1200f;
protected static final String BUNDLE_KEY_CURRENT_PAGE = "launcher.allapps.current_page";
@@ -600,6 +601,7 @@
mViewPager.getPageIndicator().setActiveMarker(AdapterHolder.MAIN);
findViewById(R.id.tab_personal)
.setOnClickListener((View view) -> {
+ Log.d(TAG, "rebindAdapters: " + "Clicked personal tab.");
if (mViewPager.snapToPage(AdapterHolder.MAIN)) {
mActivityContext.getStatsLogManager().logger()
.log(LAUNCHER_ALLAPPS_TAP_ON_PERSONAL_TAB);
@@ -607,6 +609,7 @@
});
findViewById(R.id.tab_work)
.setOnClickListener((View view) -> {
+ Log.d(TAG, "rebindAdapters: " + "Clicked work tab.");
if (mViewPager.snapToPage(AdapterHolder.WORK)) {
mActivityContext.getStatsLogManager().logger()
.log(LAUNCHER_ALLAPPS_TAP_ON_WORK_TAB);
diff --git a/src/com/android/launcher3/allapps/WorkPausedCard.java b/src/com/android/launcher3/allapps/WorkPausedCard.java
index a14ac98..864ede8 100644
--- a/src/com/android/launcher3/allapps/WorkPausedCard.java
+++ b/src/com/android/launcher3/allapps/WorkPausedCard.java
@@ -20,6 +20,7 @@
import android.content.Context;
import android.content.res.Configuration;
import android.util.AttributeSet;
+import android.util.Log;
import android.view.View;
import android.widget.Button;
import android.widget.LinearLayout;
@@ -34,6 +35,7 @@
*/
public class WorkPausedCard extends LinearLayout implements View.OnClickListener {
+ private static final String TAG = "WorkPausedCard";
private final ActivityContext mActivityContext;
private Button mBtn;
@@ -79,6 +81,7 @@
@Override
public void onClick(View view) {
+ Log.d(TAG, "WorkPausedCard clicked.");
mActivityContext.getAppsView().getWorkManager().setWorkProfileEnabled(true);
mActivityContext.getStatsLogManager().logger().log(LAUNCHER_TURN_ON_WORK_APPS_TAP);
}
diff --git a/src/com/android/launcher3/allapps/WorkProfileManager.java b/src/com/android/launcher3/allapps/WorkProfileManager.java
index 6d7d193..920efa4 100644
--- a/src/com/android/launcher3/allapps/WorkProfileManager.java
+++ b/src/com/android/launcher3/allapps/WorkProfileManager.java
@@ -200,6 +200,7 @@
private void onWorkFabClicked(View view) {
if (getCurrentState() == STATE_ENABLED && mWorkUtilityView.isEnabled()) {
+ Log.d(TAG, "Work FAB clicked.");
logEvents(LAUNCHER_TURN_OFF_WORK_APPS_TAP);
setWorkProfileEnabled(false);
}
diff --git a/src/com/android/launcher3/allapps/WorkUtilityView.java b/src/com/android/launcher3/allapps/WorkUtilityView.java
index e42a6b9..20ceb15 100644
--- a/src/com/android/launcher3/allapps/WorkUtilityView.java
+++ b/src/com/android/launcher3/allapps/WorkUtilityView.java
@@ -30,6 +30,7 @@
import android.graphics.Rect;
import android.text.TextUtils;
import android.util.AttributeSet;
+import android.util.Log;
import android.view.View;
import android.view.ViewGroup;
import android.view.WindowInsets;
@@ -63,6 +64,7 @@
public class WorkUtilityView extends LinearLayout implements Insettable,
KeyboardInsetAnimationCallback.KeyboardInsetListener {
+ private static final String TAG = "WorkUtilityView";
private static final int TEXT_EXPAND_OPACITY_DURATION = 300;
private static final int TEXT_COLLAPSE_OPACITY_DURATION = 50;
private static final int EXPAND_COLLAPSE_DURATION = 300;
@@ -143,9 +145,11 @@
mSchedulerButton.setOnClickListener(null);
if (shouldUseScheduler()) {
mSchedulerButton.setVisibility(VISIBLE);
- mSchedulerButton.setOnClickListener(view ->
- mActivityContext.startActivitySafely(view,
- new Intent(mWorkSchedulerIntentAction), null /* itemInfo */));
+ mSchedulerButton.setOnClickListener(view -> {
+ Log.d(TAG, "WorkScheduler button clicked.");
+ mActivityContext.startActivitySafely(view,
+ new Intent(mWorkSchedulerIntentAction), null /* itemInfo */);
+ });
}
}
diff --git a/src/com/android/launcher3/dagger/LauncherBaseAppComponent.java b/src/com/android/launcher3/dagger/LauncherBaseAppComponent.java
index 87b5459..31d0da0 100644
--- a/src/com/android/launcher3/dagger/LauncherBaseAppComponent.java
+++ b/src/com/android/launcher3/dagger/LauncherBaseAppComponent.java
@@ -20,8 +20,8 @@
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.icons.LauncherIcons.IconPool;
import com.android.launcher3.model.ItemInstallQueue;
import com.android.launcher3.pm.InstallSessionHelper;
import com.android.launcher3.pm.UserCache;
@@ -56,7 +56,6 @@
ApiWrapper getApiWrapper();
CustomWidgetManager getCustomWidgetManager();
DynamicResource getDynamicResource();
- IconShape getIconShape();
InstallSessionHelper getInstallSessionHelper();
ItemInstallQueue getItemInstallQueue();
RefreshRateTracker getRefreshRateTracker();
@@ -74,6 +73,7 @@
WallpaperColorHints getWallpaperColorHints();
LockedUserState getLockedUserState();
InvariantDeviceProfile getIDP();
+ IconPool getIconPool();
/** Builder for LauncherBaseAppComponent. */
interface Builder {
diff --git a/src/com/android/launcher3/debug/TestEventEmitter.java b/src/com/android/launcher3/debug/TestEventEmitter.java
new file mode 100644
index 0000000..ed3b4bb
--- /dev/null
+++ b/src/com/android/launcher3/debug/TestEventEmitter.java
@@ -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.launcher3.debug;
+
+/**
+ * TestEventsEmitter shouldn't do anything since it runs on the launcher code and not on
+ * tests. This is just a placeholder and test should mock the static sendEvent method.
+ * See "EventsRule.kt" in tests folder where sendEvent is statically mocked to change the
+ * behavior in tests.
+ */
+public class TestEventEmitter {
+ public static void sendEvent(TestEvent event) {
+ }
+
+ /** Events fired by the launcher. */
+ public enum TestEvent {
+
+ LAUNCHER_ON_CREATE("LAUNCHER_ON_CREATE"),
+ WORKSPACE_ON_DROP("WORKSPACE_ON_DROP"),
+ RESIZE_FRAME_SHOWING("RESIZE_FRAME_SHOWING"),
+ WORKSPACE_FINISH_LOADING("WORKSPACE_FINISH_LOADING"),
+ SPRING_LOADED_STATE_STARTED("SPRING_LOADED_STATE_STARTED"),
+ SPRING_LOADED_STATE_COMPLETED("SPRING_LOADED_STATE_COMPLETED");
+
+ TestEvent(String event) {
+ }
+
+ }
+}
+
+
diff --git a/src/com/android/launcher3/debug/TestEventsEmitterProduction.kt b/src/com/android/launcher3/debug/TestEventsEmitterProduction.kt
deleted file mode 100644
index 52b454f..0000000
--- a/src/com/android/launcher3/debug/TestEventsEmitterProduction.kt
+++ /dev/null
@@ -1,59 +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.launcher3.debug
-
-import android.content.Context
-import android.util.Log
-import com.android.launcher3.util.MainThreadInitializedObject
-import com.android.launcher3.util.SafeCloseable
-
-/** Events fired by the launcher. */
-enum class TestEvent(val event: String) {
- LAUNCHER_ON_CREATE("LAUNCHER_ON_CREATE"),
- WORKSPACE_ON_DROP("WORKSPACE_ON_DROP"),
- RESIZE_FRAME_SHOWING("RESIZE_FRAME_SHOWING"),
- WORKSPACE_FINISH_LOADING("WORKSPACE_FINISH_LOADING"),
- SPRING_LOADED_STATE_STARTED("SPRING_LOADED_STATE_STARTED"),
- SPRING_LOADED_STATE_COMPLETED("SPRING_LOADED_STATE_COMPLETED"),
-}
-
-/** Interface to create TestEventEmitters. */
-interface TestEventEmitter : SafeCloseable {
-
- companion object {
- @JvmField
- val INSTANCE =
- MainThreadInitializedObject<TestEventEmitter> { _: Context? ->
- TestEventsEmitterProduction()
- }
- }
-
- fun sendEvent(event: TestEvent)
-}
-
-/**
- * TestEventsEmitterProduction shouldn't do anything since it runs on the launcher code and not on
- * tests. This is just a placeholder and test should override this class.
- */
-class TestEventsEmitterProduction : TestEventEmitter {
-
- override fun close() {}
-
- override fun sendEvent(event: TestEvent) {
- Log.d("TestEventsEmitterProduction", "Event sent ${event.event}")
- }
-}
diff --git a/src/com/android/launcher3/dragndrop/DragView.java b/src/com/android/launcher3/dragndrop/DragView.java
index 072673d..bff323c 100644
--- a/src/com/android/launcher3/dragndrop/DragView.java
+++ b/src/com/android/launcher3/dragndrop/DragView.java
@@ -60,7 +60,7 @@
import com.android.app.animation.Interpolators;
import com.android.launcher3.R;
import com.android.launcher3.Utilities;
-import com.android.launcher3.graphics.IconShape;
+import com.android.launcher3.graphics.ThemeManager;
import com.android.launcher3.icons.FastBitmapDrawable;
import com.android.launcher3.icons.LauncherIcons;
import com.android.launcher3.model.data.ItemInfo;
@@ -274,9 +274,9 @@
Utilities.scaleRectAboutCenter(shrunkBounds, 0.98f);
adaptiveIcon.setBounds(shrunkBounds);
- IconShape iconShape = IconShape.INSTANCE.get(getContext());
+ ThemeManager themeManager = ThemeManager.INSTANCE.get(getContext());
final Path mask = (adaptiveIcon instanceof FolderAdaptiveIcon
- ? iconShape.getFolderShape() : iconShape.getShape())
+ ? themeManager.getFolderShape() : themeManager.getIconShape())
.getPath(shrunkBounds);
mTranslateX = new SpringFloatValue(DragView.this,
diff --git a/src/com/android/launcher3/folder/FolderAnimationManager.java b/src/com/android/launcher3/folder/FolderAnimationManager.java
index 2157610..d2354c1 100644
--- a/src/com/android/launcher3/folder/FolderAnimationManager.java
+++ b/src/com/android/launcher3/folder/FolderAnimationManager.java
@@ -46,8 +46,8 @@
import com.android.launcher3.anim.PropertyResetListener;
import com.android.launcher3.apppairs.AppPairIcon;
import com.android.launcher3.celllayout.CellLayoutLayoutParams;
-import com.android.launcher3.graphics.IconShape;
-import com.android.launcher3.graphics.IconShape.ShapeDelegate;
+import com.android.launcher3.graphics.ShapeDelegate;
+import com.android.launcher3.graphics.ThemeManager;
import com.android.launcher3.util.Themes;
import com.android.launcher3.views.BaseDragLayer;
@@ -237,7 +237,7 @@
}
play(a, getAnimator(mFolder.mFooter, ALPHA, 0, 1f), footerStartDelay, footerAlphaDuration);
- ShapeDelegate shapeDelegate = IconShape.INSTANCE.get(mContext).getFolderShape();
+ ShapeDelegate shapeDelegate = ThemeManager.INSTANCE.get(mContext).getFolderShape();
// Create reveal animator for the folder background
play(a, shapeDelegate.createRevealAnimator(
mFolder, startRect, endRect, finalRadius, !mIsOpening));
diff --git a/src/com/android/launcher3/folder/PreviewBackground.java b/src/com/android/launcher3/folder/PreviewBackground.java
index 77fa355..ba8a290 100644
--- a/src/com/android/launcher3/folder/PreviewBackground.java
+++ b/src/com/android/launcher3/folder/PreviewBackground.java
@@ -47,8 +47,8 @@
import com.android.launcher3.DeviceProfile;
import com.android.launcher3.R;
import com.android.launcher3.celllayout.DelegatedCellDrawing;
-import com.android.launcher3.graphics.IconShape;
-import com.android.launcher3.graphics.IconShape.ShapeDelegate;
+import com.android.launcher3.graphics.ShapeDelegate;
+import com.android.launcher3.graphics.ThemeManager;
import com.android.launcher3.util.Themes;
import com.android.launcher3.views.ActivityContext;
@@ -260,7 +260,7 @@
}
private ShapeDelegate getShape() {
- return IconShape.INSTANCE.get(mContext).getFolderShape();
+ return ThemeManager.INSTANCE.get(mContext).getFolderShape();
}
public void drawShadow(Canvas canvas) {
diff --git a/src/com/android/launcher3/graphics/PreloadIconDrawable.java b/src/com/android/launcher3/graphics/PreloadIconDrawable.java
index 50d6d1c..3bd9fb5 100644
--- a/src/com/android/launcher3/graphics/PreloadIconDrawable.java
+++ b/src/com/android/launcher3/graphics/PreloadIconDrawable.java
@@ -119,7 +119,7 @@
IconPalette.getPreloadProgressColor(context, info.bitmap.color),
getPreloadColors(context),
Utilities.isDarkTheme(context),
- IconShape.INSTANCE.get(context).getShape().getPath(DEFAULT_PATH_SIZE)
+ ThemeManager.INSTANCE.get(context).getIconShape().getPath(DEFAULT_PATH_SIZE)
);
}
diff --git a/src/com/android/launcher3/graphics/PreviewSurfaceRenderer.java b/src/com/android/launcher3/graphics/PreviewSurfaceRenderer.java
index 686024d..f0d670e 100644
--- a/src/com/android/launcher3/graphics/PreviewSurfaceRenderer.java
+++ b/src/com/android/launcher3/graphics/PreviewSurfaceRenderer.java
@@ -40,6 +40,7 @@
import android.util.SparseIntArray;
import android.view.ContextThemeWrapper;
import android.view.Display;
+import android.view.Surface;
import android.view.SurfaceControlViewHost;
import android.view.SurfaceControlViewHost.SurfacePackage;
import android.view.View;
@@ -286,6 +287,20 @@
}
context = context.createConfigurationContext(configuration);
}
+ if (InvariantDeviceProfile.INSTANCE.get(context).isFixedLandscape) {
+ Configuration configuration = new Configuration(
+ context.getResources().getConfiguration()
+ );
+ int width = configuration.screenWidthDp;
+ int height = configuration.screenHeightDp;
+ if (configuration.screenHeightDp > configuration.screenWidthDp) {
+ configuration.screenWidthDp = height;
+ configuration.screenHeightDp = width;
+ configuration.orientation = Surface.ROTATION_90;
+ }
+ context = context.createConfigurationContext(configuration);
+ }
+
if (Flags.newCustomizationPickerUi()) {
if (mPreviewColorOverride != null) {
LocalColorExtractor.newInstance(context)
@@ -396,15 +411,28 @@
}
renderer.hideBottomRow(mHideQsb);
View view = renderer.getRenderedView(dataModel, widgetProviderInfoMap);
- // This aspect scales the view to fit in the surface and centers it
- final float scale = Math.min(mWidth / (float) view.getMeasuredWidth(),
- mHeight / (float) view.getMeasuredHeight());
- view.setScaleX(scale);
- view.setScaleY(scale);
+
view.setPivotX(0);
view.setPivotY(0);
- view.setTranslationX((mWidth - scale * view.getWidth()) / 2);
- view.setTranslationY((mHeight - scale * view.getHeight()) / 2);
+ if (idp.isFixedLandscape) {
+ final float scale = Math.min(mHeight / (float) view.getMeasuredWidth(),
+ mWidth / (float) view.getMeasuredHeight());
+ view.setScaleX(scale);
+ view.setScaleY(scale);
+ view.setRotation(90);
+ view.setTranslationX((mHeight - scale * view.getWidth()) / 2 + mWidth);
+ view.setTranslationY((mWidth - scale * view.getHeight()) / 2);
+ } else {
+ // This aspect scales the view to fit in the surface and centers it
+ final float scale = Math.min(mWidth / (float) view.getMeasuredWidth(),
+ mHeight / (float) view.getMeasuredHeight());
+ view.setScaleX(scale);
+ view.setScaleY(scale);
+ view.setTranslationX((mWidth - scale * view.getWidth()) / 2);
+ view.setTranslationY((mHeight - scale * view.getHeight()) / 2);
+ }
+
+
if (!Flags.newCustomizationPickerUi()) {
view.setAlpha(0);
view.animate().alpha(1)
diff --git a/src/com/android/launcher3/graphics/IconShape.kt b/src/com/android/launcher3/graphics/ShapeDelegate.kt
similarity index 81%
rename from src/com/android/launcher3/graphics/IconShape.kt
rename to src/com/android/launcher3/graphics/ShapeDelegate.kt
index eac3440..df0c8f9 100644
--- a/src/com/android/launcher3/graphics/IconShape.kt
+++ b/src/com/android/launcher3/graphics/ShapeDelegate.kt
@@ -42,78 +42,38 @@
import androidx.graphics.shapes.toPath
import androidx.graphics.shapes.transformed
import com.android.launcher3.anim.RoundedRectRevealOutlineProvider
-import com.android.launcher3.dagger.LauncherAppComponent
-import com.android.launcher3.dagger.LauncherAppSingleton
-import com.android.launcher3.graphics.ThemeManager.ThemeChangeListener
import com.android.launcher3.icons.GraphicsUtils
import com.android.launcher3.icons.IconNormalizer.normalizeAdaptiveIcon
-import com.android.launcher3.util.DaggerSingletonObject
-import com.android.launcher3.util.DaggerSingletonTracker
import com.android.launcher3.views.ClipPathView
-import javax.inject.Inject
/** Abstract representation of the shape of an icon shape */
-@LauncherAppSingleton
-class IconShape
-@Inject
-constructor(private val themeManager: ThemeManager, lifeCycle: DaggerSingletonTracker) {
+interface ShapeDelegate {
- val normalizationScale =
- normalizeAdaptiveIcon(
- AdaptiveIconDrawable(null, ColorDrawable(Color.BLACK)),
- AREA_CALC_SIZE,
- )
+ fun getPath(pathSize: Float = DEFAULT_PATH_SIZE) =
+ Path().apply { addToPath(this, 0f, 0f, pathSize / 2) }
- var shape: ShapeDelegate = pickBestShape(themeManager.iconState.iconMask)
- private set
-
- var folderShape: ShapeDelegate =
- themeManager.iconState.run {
- if (folderShapeMask == iconMask || folderShapeMask.isEmpty()) shape
- else pickBestShape(folderShapeMask)
+ fun getPath(bounds: Rect) =
+ Path().apply {
+ addToPath(
+ this,
+ bounds.left.toFloat(),
+ bounds.top.toFloat(),
+ // Radius is half of the average size of the icon
+ (bounds.width() + bounds.height()) / 4f,
+ )
}
- private set
- init {
- val changeListener = ThemeChangeListener {
- shape = pickBestShape(themeManager.iconState.iconMask)
- folderShape =
- themeManager.iconState.run {
- if (folderShapeMask == iconMask || folderShapeMask.isEmpty()) shape
- else pickBestShape(folderShapeMask)
- }
- }
- themeManager.addChangeListener(changeListener)
- lifeCycle.addCloseable { themeManager.removeChangeListener(changeListener) }
- }
+ fun drawShape(canvas: Canvas, offsetX: Float, offsetY: Float, radius: Float, paint: Paint)
- interface ShapeDelegate {
- fun getPath(pathSize: Float = DEFAULT_PATH_SIZE) =
- Path().apply { addToPath(this, 0f, 0f, pathSize / 2) }
+ fun addToPath(path: Path, offsetX: Float, offsetY: Float, radius: Float)
- fun getPath(bounds: Rect) =
- Path().apply {
- addToPath(
- this,
- bounds.left.toFloat(),
- bounds.top.toFloat(),
- // Radius is half of the average size of the icon
- (bounds.width() + bounds.height()) / 4f,
- )
- }
-
- fun drawShape(canvas: Canvas, offsetX: Float, offsetY: Float, radius: Float, paint: Paint)
-
- fun addToPath(path: Path, offsetX: Float, offsetY: Float, radius: Float)
-
- fun <T> createRevealAnimator(
- target: T,
- startRect: Rect,
- endRect: Rect,
- endRadius: Float,
- isReversed: Boolean,
- ): ValueAnimator where T : View, T : ClipPathView
- }
+ fun <T> createRevealAnimator(
+ target: T,
+ startRect: Rect,
+ endRect: Rect,
+ endRadius: Float,
+ isReversed: Boolean,
+ ): ValueAnimator where T : View, T : ClipPathView
class Circle : RoundedSquare(1f) {
@@ -179,10 +139,15 @@
}
.createRevealAnimator(target, isReversed)
}
+
+ override fun equals(other: Any?) =
+ other is RoundedSquare && other.radiusRatio == radiusRatio
+
+ override fun hashCode() = radiusRatio.hashCode()
}
/** Generic shape delegate with pathString in bounds [0, 0, 100, 100] */
- class GenericPathShape(pathString: String) : ShapeDelegate {
+ data class GenericPathShape(private val pathString: String) : ShapeDelegate {
private val poly =
RoundedPolygon(
features = SvgPathParser.parseFeatures(pathString),
@@ -287,7 +252,6 @@
}
companion object {
- @JvmField var INSTANCE = DaggerSingletonObject(LauncherAppComponent::getIconShape)
const val TAG = "IconShape"
const val DEFAULT_PATH_SIZE = 100f
@@ -312,7 +276,6 @@
}
}
- @VisibleForTesting
fun pickBestShape(shapeStr: String): ShapeDelegate {
val baseShape =
if (shapeStr.isNotEmpty()) {
@@ -332,7 +295,6 @@
return pickBestShape(baseShape, shapeStr)
}
- @VisibleForTesting
fun pickBestShape(baseShape: Path, shapeStr: String): ShapeDelegate {
val calcAreaDiff = areaDiffCalculator(baseShape)
@@ -380,5 +342,12 @@
centerY = (bottom - top) / 2,
rounding = CornerRounding(cornerR),
)
+
+ @JvmStatic
+ val normalizationScale =
+ normalizeAdaptiveIcon(
+ AdaptiveIconDrawable(null, ColorDrawable(Color.BLACK)),
+ AREA_CALC_SIZE,
+ )
}
}
diff --git a/src/com/android/launcher3/graphics/ThemeManager.kt b/src/com/android/launcher3/graphics/ThemeManager.kt
index 1636da8..4a0ff8c 100644
--- a/src/com/android/launcher3/graphics/ThemeManager.kt
+++ b/src/com/android/launcher3/graphics/ThemeManager.kt
@@ -25,6 +25,7 @@
import com.android.launcher3.dagger.ApplicationContext
import com.android.launcher3.dagger.LauncherAppComponent
import com.android.launcher3.dagger.LauncherAppSingleton
+import com.android.launcher3.graphics.ShapeDelegate.Companion.pickBestShape
import com.android.launcher3.icons.IconThemeController
import com.android.launcher3.icons.mono.MonoIconThemeController
import com.android.launcher3.shapes.ShapesProvider
@@ -46,19 +47,25 @@
) {
/** Representation of the current icon state */
- var iconState = parseIconState()
+ var iconState = parseIconState(null)
private set
var isMonoThemeEnabled
set(value) = prefs.put(THEMED_ICONS, value)
get() = prefs.get(THEMED_ICONS)
- val themeController: IconThemeController?
+ val themeController
get() = iconState.themeController
- val isIconThemeEnabled: Boolean
+ val isIconThemeEnabled
get() = themeController != null
+ val iconShape
+ get() = iconState.iconShape
+
+ val folderShape
+ get() = iconState.folderShape
+
private val listeners = CopyOnWriteArrayList<ThemeChangeListener>()
init {
@@ -80,7 +87,7 @@
}
protected fun verifyIconState() {
- val newState = parseIconState()
+ val newState = parseIconState(iconState)
if (newState == iconState) return
iconState = newState
@@ -91,7 +98,7 @@
fun removeChangeListener(listener: ThemeChangeListener) = listeners.remove(listener)
- private fun parseIconState(): IconState {
+ private fun parseIconState(oldState: IconState?): IconState {
val shapeModel =
prefs.get(PREF_ICON_SHAPE).let { shapeOverride ->
ShapesProvider.iconShapes.values.firstOrNull { it.key == shapeOverride }
@@ -102,10 +109,27 @@
CONFIG_ICON_MASK_RES_ID == Resources.ID_NULL -> ""
else -> context.resources.getString(CONFIG_ICON_MASK_RES_ID)
}
+
+ val iconShape =
+ if (oldState != null && oldState.iconMask == iconMask) oldState.iconShape
+ else pickBestShape(iconMask)
+
+ val folderShapeMask = shapeModel?.folderPathString ?: iconMask
+ val folderShape =
+ when {
+ oldState != null && oldState.folderShapeMask == folderShapeMask ->
+ oldState.folderShape
+ folderShapeMask == iconMask || folderShapeMask.isEmpty() -> iconShape
+ else -> pickBestShape(folderShapeMask)
+ }
+
return IconState(
iconMask = iconMask,
- folderShapeMask = shapeModel?.folderPathString ?: iconMask,
+ folderShapeMask = folderShapeMask,
themeController = createThemeController(),
+ iconScale = shapeModel?.iconScale ?: 1f,
+ iconShape = iconShape,
+ folderShape = folderShape,
)
}
@@ -118,6 +142,9 @@
val folderShapeMask: String,
val themeController: IconThemeController?,
val themeCode: String = themeController?.themeID ?: "no-theme",
+ val iconScale: Float = 1f,
+ val iconShape: ShapeDelegate,
+ val folderShape: ShapeDelegate,
) {
fun toUniqueId() = "${iconMask.hashCode()},$themeCode"
}
diff --git a/src/com/android/launcher3/icons/LauncherIconProvider.java b/src/com/android/launcher3/icons/LauncherIconProvider.java
index 482360c..836b7d1 100644
--- a/src/com/android/launcher3/icons/LauncherIconProvider.java
+++ b/src/com/android/launcher3/icons/LauncherIconProvider.java
@@ -30,7 +30,7 @@
import com.android.launcher3.R;
import com.android.launcher3.config.FeatureFlags;
-import com.android.launcher3.graphics.IconShape;
+import com.android.launcher3.graphics.ShapeDelegate;
import com.android.launcher3.graphics.ThemeManager;
import com.android.launcher3.util.ApiWrapper;
@@ -54,13 +54,13 @@
private Map<String, ThemeData> mThemedIconMap;
private final ApiWrapper mApiWrapper;
- private final IconShape mIconShape;
+ private final ThemeManager mThemeManager;
public LauncherIconProvider(Context context) {
super(context);
- setIconThemeSupported(ThemeManager.INSTANCE.get(context).isMonoThemeEnabled());
+ mThemeManager = ThemeManager.INSTANCE.get(context);
mApiWrapper = ApiWrapper.INSTANCE.get(context);
- mIconShape = IconShape.INSTANCE.get(context);
+ setIconThemeSupported(mThemeManager.isMonoThemeEnabled());
}
/**
@@ -91,7 +91,7 @@
@Override
protected Drawable loadAppInfoIcon(ApplicationInfo info, Resources resources, int density) {
// Tries to load the round icon res, if the app defines it as an adaptive icon
- if (mIconShape.getShape() instanceof IconShape.Circle) {
+ if (mThemeManager.getIconShape() instanceof ShapeDelegate.Circle) {
int roundIconRes = mApiWrapper.getRoundIconRes(info);
if (roundIconRes != 0 && roundIconRes != info.icon) {
try {
diff --git a/src/com/android/launcher3/icons/LauncherIcons.kt b/src/com/android/launcher3/icons/LauncherIcons.kt
index 518f29d..6c018e8 100644
--- a/src/com/android/launcher3/icons/LauncherIcons.kt
+++ b/src/com/android/launcher3/icons/LauncherIcons.kt
@@ -24,29 +24,36 @@
import android.os.UserHandle
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.dagger.ApplicationContext
+import com.android.launcher3.dagger.LauncherAppSingleton
+import com.android.launcher3.dagger.LauncherComponentProvider.appComponent
import com.android.launcher3.graphics.ThemeManager
import com.android.launcher3.pm.UserCache
-import com.android.launcher3.util.MainThreadInitializedObject
-import com.android.launcher3.util.SafeCloseable
import com.android.launcher3.util.UserIconInfo
+import dagger.assisted.Assisted
+import dagger.assisted.AssistedFactory
+import dagger.assisted.AssistedInject
import java.util.concurrent.ConcurrentLinkedQueue
+import javax.inject.Inject
/**
* Wrapper class to provide access to [BaseIconFactory] and also to provide pool of this class that
* are threadsafe.
*/
class LauncherIcons
-protected constructor(
- context: Context,
- fillResIconDpi: Int,
- iconBitmapSize: Int,
- private val pool: ConcurrentLinkedQueue<LauncherIcons>,
-) : BaseIconFactory(context, fillResIconDpi, iconBitmapSize), AutoCloseable {
+@AssistedInject
+internal constructor(
+ @ApplicationContext context: Context,
+ idp: InvariantDeviceProfile,
+ private var themeManager: ThemeManager,
+ private var userCache: UserCache,
+ @Assisted private val pool: ConcurrentLinkedQueue<LauncherIcons>,
+) : BaseIconFactory(context, idp.fillResIconDpi, idp.iconBitmapSize), AutoCloseable {
+
+ private val iconScale = themeManager.iconState.iconScale
init {
- mThemeController = ThemeManager.INSTANCE[context].themeController
+ mThemeController = themeManager.themeController
}
/** Recycles a LauncherIcons that may be in-use. */
@@ -56,12 +63,12 @@
}
override fun getUserInfo(user: UserHandle): UserIconInfo {
- return UserCache.INSTANCE[mContext].getUserInfo(user)
+ return userCache.getUserInfo(user)
}
public override fun getShapePath(drawable: AdaptiveIconDrawable, iconBounds: Rect): Path {
if (!Flags.enableLauncherIconShapes()) return drawable.iconMask
- return IconShape.INSTANCE[mContext].shape.getPath(iconBounds)
+ return themeManager.iconShape.getPath(iconBounds)
}
override fun drawAdaptiveIcon(
@@ -73,14 +80,6 @@
super.drawAdaptiveIcon(canvas, drawable, overridePath)
return
}
- val shapeKey = LauncherPrefs.get(mContext).get(ThemeManager.PREF_ICON_SHAPE)
- val iconScale =
- when (shapeKey) {
- "seven_sided_cookie" -> SEVEN_SIDED_COOKIE_SCALE
- "four_sided_cookie" -> FOUR_SIDED_COOKIE_SCALE
- "sunny" -> VERY_SUNNY_SCALE
- else -> DEFAULT_ICON_SCALE
- }
canvas.clipPath(overridePath)
canvas.drawColor(Color.BLACK)
canvas.save()
@@ -98,42 +97,31 @@
recycle()
}
- private class Pool(private val context: Context) : SafeCloseable {
+ @AssistedFactory
+ internal interface LauncherIconsFactory {
+ fun create(pool: ConcurrentLinkedQueue<LauncherIcons>): LauncherIcons
+ }
+
+ @LauncherAppSingleton
+ class IconPool @Inject internal constructor(private val factory: LauncherIconsFactory) {
private var pool = ConcurrentLinkedQueue<LauncherIcons>()
- fun obtain(): LauncherIcons {
- val pool = pool
- return pool.poll()
- ?: InvariantDeviceProfile.INSTANCE[context].let {
- LauncherIcons(context, it.fillResIconDpi, it.iconBitmapSize, pool)
- }
- }
+ fun obtain(): LauncherIcons = pool.let { it.poll() ?: factory.create(it) }
- override fun close() {
+ fun clear() {
pool = ConcurrentLinkedQueue()
}
}
companion object {
- private const val SEVEN_SIDED_COOKIE_SCALE = 72f / 80f
- private const val FOUR_SIDED_COOKIE_SCALE = 72f / 83.4f
- private const val VERY_SUNNY_SCALE = 72f / 92f
- private const val DEFAULT_ICON_SCALE = 1f
-
- private val POOL = MainThreadInitializedObject { Pool(it) }
/**
- * Return a new Message instance from the global pool. Allows us to avoid allocating new
- * objects in many cases.
+ * Return a new LauncherIcons instance from the global pool. Allows us to avoid allocating
+ * new objects in many cases.
*/
@JvmStatic
- fun obtain(context: Context): LauncherIcons {
- return POOL[context].obtain()
- }
+ fun obtain(context: Context): LauncherIcons = context.appComponent.iconPool.obtain()
- @JvmStatic
- fun clearPool(context: Context) {
- POOL[context].close()
- }
+ @JvmStatic fun clearPool(context: Context) = context.appComponent.iconPool.clear()
}
}
diff --git a/src/com/android/launcher3/shapes/IconShapeModel.kt b/src/com/android/launcher3/shapes/IconShapeModel.kt
index dd6c432..fc49adc 100644
--- a/src/com/android/launcher3/shapes/IconShapeModel.kt
+++ b/src/com/android/launcher3/shapes/IconShapeModel.kt
@@ -21,4 +21,5 @@
val title: String,
val pathString: String,
val folderPathString: String = pathString,
+ val iconScale: Float = 1f,
)
diff --git a/src/com/android/launcher3/shapes/ShapesProvider.kt b/src/com/android/launcher3/shapes/ShapesProvider.kt
index 7e1f640..463c816 100644
--- a/src/com/android/launcher3/shapes/ShapesProvider.kt
+++ b/src/com/android/launcher3/shapes/ShapesProvider.kt
@@ -174,6 +174,7 @@
pathString =
"M39.888,4.517C46.338 7.319 53.662 7.319 60.112 4.517L63.605 3C84.733 -6.176 106.176 15.268 97 36.395L95.483 39.888C92.681 46.338 92.681 53.662 95.483 60.112L97 63.605C106.176 84.732 84.733 106.176 63.605 97L60.112 95.483C53.662 92.681 46.338 92.681 39.888 95.483L36.395 97C15.267 106.176 -6.176 84.732 3 63.605L4.517 60.112C7.319 53.662 7.319 46.338 4.517 39.888L3 36.395C -6.176 15.268 15.267 -6.176 36.395 3Z",
folderPathString = folderShapes["complexClover"]!!,
+ iconScale = 72f / 83.4f,
),
"seven_sided_cookie" to
IconShapeModel(
@@ -182,6 +183,7 @@
pathString =
"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"]!!,
+ iconScale = 72f / 80f,
),
"arch" to
IconShapeModel(
@@ -198,6 +200,7 @@
pathString =
"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"]!!,
+ iconScale = 72f / 92f,
),
)
} else {
diff --git a/src/com/android/launcher3/views/ClipIconView.java b/src/com/android/launcher3/views/ClipIconView.java
index f90a3e4..ddf18df 100644
--- a/src/com/android/launcher3/views/ClipIconView.java
+++ b/src/com/android/launcher3/views/ClipIconView.java
@@ -47,7 +47,8 @@
import com.android.launcher3.R;
import com.android.launcher3.Utilities;
import com.android.launcher3.dragndrop.FolderAdaptiveIcon;
-import com.android.launcher3.graphics.IconShape;
+import com.android.launcher3.graphics.ShapeDelegate;
+import com.android.launcher3.graphics.ThemeManager;
/**
* A view used to draw both layers of an {@link AdaptiveIconDrawable}.
@@ -174,7 +175,7 @@
if (mIsAdaptiveIcon) {
if (!isOpening && progress >= shapeProgressStart) {
if (mRevealAnimator == null) {
- mRevealAnimator = IconShape.INSTANCE.get(getContext()).getShape()
+ mRevealAnimator = ThemeManager.INSTANCE.get(getContext()).getIconShape()
.createRevealAnimator(this, mStartRevealRect,
mOutline, mTaskCornerRadius, !isOpening);
mRevealAnimator.addListener(forEndCallback(() -> mRevealAnimator = null));
@@ -259,7 +260,7 @@
if (!isFolderIcon) {
Utilities.scaleRectAboutCenter(mStartRevealRect,
- IconShape.INSTANCE.get(getContext()).getNormalizationScale());
+ ShapeDelegate.getNormalizationScale());
}
if (dp.isLandscape) {
diff --git a/tests/Android.bp b/tests/Android.bp
index 4bc654c..fc08e86 100644
--- a/tests/Android.bp
+++ b/tests/Android.bp
@@ -168,6 +168,7 @@
"src/**/*Test.java",
"src/**/*Test.kt",
"src/**/RoboApiWrapper.kt",
+ "src/**/EventsRule.kt",
"multivalentTests/src/**/*Test.java",
"multivalentTests/src/**/*Test.kt",
],
diff --git a/tests/multivalentTests/src/com/android/launcher3/FakeInvariantDeviceProfileTest.kt b/tests/multivalentTests/src/com/android/launcher3/FakeInvariantDeviceProfileTest.kt
index 060c28c..f855c51 100644
--- a/tests/multivalentTests/src/com/android/launcher3/FakeInvariantDeviceProfileTest.kt
+++ b/tests/multivalentTests/src/com/android/launcher3/FakeInvariantDeviceProfileTest.kt
@@ -71,7 +71,7 @@
inv,
info,
context.appComponent.wmProxy,
- context.appComponent.iconShape,
+ context.appComponent.themeManager,
windowBounds,
SparseArray(),
/*isMultiWindowMode=*/ false,
diff --git a/tests/multivalentTests/src/com/android/launcher3/graphics/IconShapeTest.kt b/tests/multivalentTests/src/com/android/launcher3/graphics/ShapeDelegateTest.kt
similarity index 93%
rename from tests/multivalentTests/src/com/android/launcher3/graphics/IconShapeTest.kt
rename to tests/multivalentTests/src/com/android/launcher3/graphics/ShapeDelegateTest.kt
index 311676a..7e38f0e 100644
--- a/tests/multivalentTests/src/com/android/launcher3/graphics/IconShapeTest.kt
+++ b/tests/multivalentTests/src/com/android/launcher3/graphics/ShapeDelegateTest.kt
@@ -28,13 +28,13 @@
import androidx.core.graphics.PathParser
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
-import com.android.launcher3.graphics.IconShape.Circle
-import com.android.launcher3.graphics.IconShape.Companion.AREA_CALC_SIZE
-import com.android.launcher3.graphics.IconShape.Companion.AREA_DIFF_THRESHOLD
-import com.android.launcher3.graphics.IconShape.Companion.areaDiffCalculator
-import com.android.launcher3.graphics.IconShape.Companion.pickBestShape
-import com.android.launcher3.graphics.IconShape.GenericPathShape
-import com.android.launcher3.graphics.IconShape.RoundedSquare
+import com.android.launcher3.graphics.ShapeDelegate.Circle
+import com.android.launcher3.graphics.ShapeDelegate.Companion.AREA_CALC_SIZE
+import com.android.launcher3.graphics.ShapeDelegate.Companion.AREA_DIFF_THRESHOLD
+import com.android.launcher3.graphics.ShapeDelegate.Companion.areaDiffCalculator
+import com.android.launcher3.graphics.ShapeDelegate.Companion.pickBestShape
+import com.android.launcher3.graphics.ShapeDelegate.GenericPathShape
+import com.android.launcher3.graphics.ShapeDelegate.RoundedSquare
import com.android.launcher3.icons.GraphicsUtils
import com.android.launcher3.views.ClipPathView
import com.google.common.truth.Truth.assertThat
@@ -43,7 +43,7 @@
@SmallTest
@RunWith(AndroidJUnit4::class)
-class IconShapeTest {
+class ShapeDelegateTest {
@Test
fun `areaDiffCalculator increases with outwards shape`() {
diff --git a/tests/multivalentTests/src/com/android/launcher3/shapes/ShapesProviderTest.kt b/tests/multivalentTests/src/com/android/launcher3/shapes/ShapesProviderTest.kt
index 2b8896e..508c9a4 100644
--- a/tests/multivalentTests/src/com/android/launcher3/shapes/ShapesProviderTest.kt
+++ b/tests/multivalentTests/src/com/android/launcher3/shapes/ShapesProviderTest.kt
@@ -22,7 +22,7 @@
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.launcher3.Flags.FLAG_ENABLE_LAUNCHER_ICON_SHAPES
-import com.android.launcher3.graphics.IconShape.GenericPathShape
+import com.android.launcher3.graphics.ShapeDelegate.GenericPathShape
import com.android.systemui.shared.Flags.FLAG_NEW_CUSTOMIZATION_PICKER_UI
import org.junit.Rule
import org.junit.Test
diff --git a/tests/src/com/android/launcher3/celllayout/integrationtest/events/EventWaiter.kt b/tests/src/com/android/launcher3/celllayout/integrationtest/events/EventWaiter.kt
new file mode 100644
index 0000000..20ad60f
--- /dev/null
+++ b/tests/src/com/android/launcher3/celllayout/integrationtest/events/EventWaiter.kt
@@ -0,0 +1,52 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.launcher3.celllayout.integrationtest.events
+
+import com.android.launcher3.debug.TestEventEmitter.TestEvent
+import java.util.concurrent.TimeUnit
+import kotlinx.coroutines.CompletableDeferred
+import kotlinx.coroutines.runBlocking
+import kotlinx.coroutines.withTimeoutOrNull
+
+enum class EventStatus() {
+ SUCCESS,
+ FAILURE,
+ TIMEOUT,
+}
+
+class EventWaiter(val eventToWait: TestEvent) {
+ private val deferrable = CompletableDeferred<EventStatus>()
+
+ companion object {
+ private const val TAG = "EventWaiter"
+ private val SIGNAL_TIMEOUT = TimeUnit.SECONDS.toMillis(5)
+ }
+
+ fun waitForSignal(timeout: Long = SIGNAL_TIMEOUT) = runBlocking {
+ var status = withTimeoutOrNull(timeout) { deferrable.await() }
+ if (status == null) {
+ status = EventStatus.TIMEOUT
+ }
+ if (status != EventStatus.SUCCESS) {
+ throw Exception("Failure waiting for event $eventToWait, failure = $status")
+ }
+ }
+
+ fun terminate() {
+ deferrable.complete(EventStatus.SUCCESS)
+ }
+}
diff --git a/tests/src/com/android/launcher3/celllayout/integrationtest/events/EventsRule.kt b/tests/src/com/android/launcher3/celllayout/integrationtest/events/EventsRule.kt
index fb61ced..45eb5e1 100644
--- a/tests/src/com/android/launcher3/celllayout/integrationtest/events/EventsRule.kt
+++ b/tests/src/com/android/launcher3/celllayout/integrationtest/events/EventsRule.kt
@@ -17,11 +17,15 @@
package com.android.launcher3.celllayout.integrationtest.events
import android.content.Context
-import com.android.launcher3.debug.TestEvent
+import android.util.Log
+import com.android.dx.mockito.inline.extended.ExtendedMockito.*
+import com.android.dx.mockito.inline.extended.StaticMockitoSession
import com.android.launcher3.debug.TestEventEmitter
+import com.android.launcher3.debug.TestEventEmitter.TestEvent
import org.junit.rules.TestRule
import org.junit.runner.Description
import org.junit.runners.model.Statement
+import org.mockito.quality.Strictness
/**
* Rule to create EventWaiters to wait for events that happens on the Launcher. For reference look
@@ -30,35 +34,65 @@
* Waiting for event should be used to prevent race conditions, it provides a more precise way of
* waiting for events compared to [AbstractLauncherUiTest#waitForLauncherCondition].
*
- * This class overrides the [TestEventEmitter] with [TestEventsEmitterImplementation] and makes sure
- * to return the [TestEventEmitter] to the previous value when finished.
+ * This class mocks the static method [TestEventEmitter.sendEvent]
*/
class EventsRule(val context: Context) : TestRule {
- private var prevEventEmitter: TestEventEmitter? = null
+ private val expectedEvents: ArrayDeque<EventWaiter> = ArrayDeque()
- private val eventEmitter = TestEventsEmitterImplementation()
+ private lateinit var mockitoSession: StaticMockitoSession
override fun apply(base: Statement, description: Description?): Statement {
return object : Statement() {
override fun evaluate() {
- beforeTest()
- base.evaluate()
- afterTest()
+ try {
+ beforeTest()
+ base.evaluate()
+ } finally {
+ afterTest()
+ }
}
}
}
fun createEventWaiter(expectedEvent: TestEvent): EventWaiter {
- return eventEmitter.createEventWaiter(expectedEvent)
+ val eventWaiter = EventWaiter(expectedEvent)
+ expectedEvents.add(eventWaiter)
+ return eventWaiter
}
private fun beforeTest() {
- prevEventEmitter = TestEventEmitter.INSTANCE.get(context)
- TestEventEmitter.INSTANCE.initializeForTesting(eventEmitter)
+ mockitoSession =
+ mockitoSession()
+ .strictness(Strictness.LENIENT)
+ .spyStatic(TestEventEmitter::class.java)
+ .startMocking()
+
+ doAnswer { invocation ->
+ val event = (invocation.arguments[0] as TestEvent)
+ Log.d(TAG, "Signal received $event")
+ Log.d(TAG, "Total expected events ${expectedEvents.size}")
+ if (!expectedEvents.isEmpty()) {
+ val eventWaiter = expectedEvents.last()
+ if (eventWaiter.eventToWait == event) {
+ Log.d(TAG, "Removing $event")
+ expectedEvents.removeLast()
+ eventWaiter.terminate()
+ } else {
+ Log.d(TAG, "Not matching $event")
+ }
+ }
+ null
+ }
+ .`when` { TestEventEmitter.sendEvent(any()) }
}
private fun afterTest() {
- TestEventEmitter.INSTANCE.initializeForTesting(prevEventEmitter)
+ mockitoSession.finishMocking()
+ expectedEvents.clear()
+ }
+
+ companion object {
+ private const val TAG = "TestEvents"
}
}
diff --git a/tests/src/com/android/launcher3/celllayout/integrationtest/events/TestEventsEmitterImplementation.kt b/tests/src/com/android/launcher3/celllayout/integrationtest/events/TestEventsEmitterImplementation.kt
deleted file mode 100644
index 5e062d0..0000000
--- a/tests/src/com/android/launcher3/celllayout/integrationtest/events/TestEventsEmitterImplementation.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.launcher3.celllayout.integrationtest.events
-
-import android.util.Log
-import com.android.launcher3.debug.TestEvent
-import com.android.launcher3.debug.TestEventEmitter
-import java.util.concurrent.TimeUnit
-import kotlinx.coroutines.CompletableDeferred
-import kotlinx.coroutines.runBlocking
-import kotlinx.coroutines.withTimeoutOrNull
-
-enum class EventStatus() {
- SUCCESS,
- FAILURE,
- TIMEOUT,
-}
-
-class EventWaiter(val eventToWait: TestEvent) {
- private val deferrable = CompletableDeferred<EventStatus>()
-
- companion object {
- private const val TAG = "EventWaiter"
- private val SIGNAL_TIMEOUT = TimeUnit.SECONDS.toMillis(5)
- }
-
- fun waitForSignal(timeout: Long = SIGNAL_TIMEOUT) = runBlocking {
- var status = withTimeoutOrNull(timeout) { deferrable.await() }
- if (status == null) {
- status = EventStatus.TIMEOUT
- }
- if (status != EventStatus.SUCCESS) {
- throw Exception("Failure waiting for event $eventToWait, failure = $status")
- }
- }
-
- fun terminate() {
- deferrable.complete(EventStatus.SUCCESS)
- }
-}
-
-class TestEventsEmitterImplementation() : TestEventEmitter {
- companion object {
- private const val TAG = "TestEvents"
- }
-
- private val expectedEvents: ArrayDeque<EventWaiter> = ArrayDeque()
-
- fun createEventWaiter(expectedEvent: TestEvent): EventWaiter {
- val eventWaiter = EventWaiter(expectedEvent)
- expectedEvents.add(eventWaiter)
- return eventWaiter
- }
-
- private fun clearQueue() {
- expectedEvents.clear()
- }
-
- override fun sendEvent(event: TestEvent) {
- Log.d(TAG, "Signal received $event")
- Log.d(TAG, "Total expected events ${expectedEvents.size}")
- if (expectedEvents.isEmpty()) return
- val eventWaiter = expectedEvents.last()
- if (eventWaiter.eventToWait == event) {
- Log.d(TAG, "Removing $event")
- expectedEvents.removeLast()
- eventWaiter.terminate()
- } else {
- Log.d(TAG, "Not matching $event")
- }
- }
-
- override fun close() {
- clearQueue()
- }
-}
diff --git a/tests/src/com/android/launcher3/nonquickstep/DeviceProfileDumpTest.kt b/tests/src/com/android/launcher3/nonquickstep/DeviceProfileDumpTest.kt
index 05cf926..f6e45a3 100644
--- a/tests/src/com/android/launcher3/nonquickstep/DeviceProfileDumpTest.kt
+++ b/tests/src/com/android/launcher3/nonquickstep/DeviceProfileDumpTest.kt
@@ -40,6 +40,7 @@
Flags.FLAG_ENABLE_SCALING_REVEAL_HOME_ANIMATION,
)
setFlagsRule.setFlags(false, Flags.FLAG_ONE_GRID_SPECS)
+ setFlagsRule.setFlags(false, Flags.FLAG_ENABLE_LAUNCHER_ICON_SHAPES)
}
@Test
diff --git a/tests/tapl/com/android/launcher3/tapl/OverviewTask.java b/tests/tapl/com/android/launcher3/tapl/OverviewTask.java
index 1158521..4e94e1e 100644
--- a/tests/tapl/com/android/launcher3/tapl/OverviewTask.java
+++ b/tests/tapl/com/android/launcher3/tapl/OverviewTask.java
@@ -129,7 +129,7 @@
return mTask.getVisibleCenter().x;
}
- int getTaskCenterY() {
+ public int getTaskCenterY() {
return mTask.getVisibleCenter().y;
}