Handle Desktop app launches from Taskbar/AllApps icon clicks.
- Provide both launch and minimize animations from
DesktopAppLaunchTransition
- Add support to TaskbarRecentAppsController to check whether an app is
visible vs. minimized vs. not showing at all in Desktop Mode
- Use DesktopAppLaunchTransition when clicking a Taskbar/AllApps app
icon (when in Desktop Mode and the app is not visible) to animate the
app launch
- The animation / transition is passed to Shell through ActivityOptions
Note: this CL does not add animations for launching managed/work
profile apps. That will be handled in b/376819104.
Test: launch apps in Desktop from Taskbar/AllApps
Bug: 327428659
Flag: com.android.window.flags.enable_desktop_app_launch_transitions
Change-Id: I44be96b9c53718b2082d2f630e1921356785fc4b
diff --git a/quickstep/src/com/android/launcher3/desktop/DesktopAppLaunchTransition.kt b/quickstep/src/com/android/launcher3/desktop/DesktopAppLaunchTransition.kt
index 6916a1d..e160f82 100644
--- a/quickstep/src/com/android/launcher3/desktop/DesktopAppLaunchTransition.kt
+++ b/quickstep/src/com/android/launcher3/desktop/DesktopAppLaunchTransition.kt
@@ -20,6 +20,7 @@
import android.animation.AnimatorSet
import android.animation.ValueAnimator
import android.content.Context
+import android.graphics.Rect
import android.os.IBinder
import android.view.SurfaceControl.Transaction
import android.view.WindowManager.TRANSIT_OPEN
@@ -31,6 +32,7 @@
import android.window.TransitionInfo.Change
import androidx.core.animation.addListener
import com.android.app.animation.Interpolators
+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
@@ -43,8 +45,19 @@
* ([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) :
- RemoteTransitionStub() {
+class DesktopAppLaunchTransition(
+ private val context: Context,
+ private val mainExecutor: Executor,
+ private val launchType: AppLaunchType,
+) : RemoteTransitionStub() {
+
+ enum class AppLaunchType(
+ val boundsAnimationParams: WindowAnimator.BoundsAnimationParams,
+ val alphaDurationMs: Long,
+ ) {
+ LAUNCH(launchBoundsAnimationDef, /* alphaDurationMs= */ 200L),
+ UNMINIMIZE(unminimizeBoundsAnimationDef, /* alphaDurationMs= */ 100L),
+ }
override fun startAnimation(
token: IBinder,
@@ -105,18 +118,24 @@
val boundsAnimator =
WindowAnimator.createBoundsAnimator(
context.resources.displayMetrics,
- launchBoundsAnimationDef,
+ launchType.boundsAnimationParams,
change,
transaction,
)
val alphaAnimator =
ValueAnimator.ofFloat(0f, 1f).apply {
- duration = LAUNCH_ANIM_ALPHA_DURATION_MS
+ duration = launchType.alphaDurationMs
interpolator = Interpolators.LINEAR
addUpdateListener { animation ->
transaction.setAlpha(change.leash, animation.animatedValue as Float).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 {
playTogether(boundsAnimator, alphaAnimator)
addListener(onEnd = { animation -> onAnimFinish(animation) })
@@ -124,13 +143,18 @@
}
companion object {
- private val LAUNCH_CHANGE_MODES = intArrayOf(TRANSIT_OPEN, TRANSIT_TO_FRONT)
-
- private const val LAUNCH_ANIM_ALPHA_DURATION_MS = 100L
- private const val MINIMIZE_ANIM_ALPHA_DURATION_MS = 100L
+ val LAUNCH_CHANGE_MODES = intArrayOf(TRANSIT_OPEN, TRANSIT_TO_FRONT)
private val launchBoundsAnimationDef =
WindowAnimator.BoundsAnimationParams(
+ durationMs = 600,
+ startOffsetYDp = 36f,
+ startScale = 0.95f,
+ interpolator = Interpolators.STANDARD_DECELERATE,
+ )
+
+ private val unminimizeBoundsAnimationDef =
+ WindowAnimator.BoundsAnimationParams(
durationMs = 300,
startOffsetYDp = 12f,
startScale = 0.97f,
diff --git a/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchViewController.java b/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchViewController.java
index 5e11601..390112e 100644
--- a/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchViewController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchViewController.java
@@ -36,6 +36,7 @@
import com.android.launcher3.Utilities;
import com.android.launcher3.anim.AnimatorListeners;
import com.android.launcher3.desktop.DesktopAppLaunchTransition;
+import com.android.launcher3.desktop.DesktopAppLaunchTransition.AppLaunchType;
import com.android.launcher3.taskbar.overlay.TaskbarOverlayContext;
import com.android.launcher3.taskbar.overlay.TaskbarOverlayDragLayer;
import com.android.launcher3.util.DisplayController;
@@ -251,7 +252,8 @@
) {
// This app is being unminimized - use our own transition runner.
remoteTransition = new RemoteTransition(
- new DesktopAppLaunchTransition(context, MAIN_EXECUTOR));
+ new DesktopAppLaunchTransition(
+ context, MAIN_EXECUTOR, AppLaunchType.UNMINIMIZE));
}
mControllers.taskbarActivityContext.handleGroupTaskLaunch(
task,
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java
index 5b9381c..82acc0c 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java
@@ -59,6 +59,7 @@
import android.graphics.PixelFormat;
import android.graphics.Rect;
import android.hardware.display.DisplayManager;
+import android.os.Bundle;
import android.os.IRemoteCallback;
import android.os.Process;
import android.os.Trace;
@@ -91,6 +92,7 @@
import com.android.launcher3.apppairs.AppPairIcon;
import com.android.launcher3.config.FeatureFlags;
import com.android.launcher3.desktop.DesktopAppLaunchTransition;
+import com.android.launcher3.desktop.DesktopAppLaunchTransition.AppLaunchType;
import com.android.launcher3.dot.DotInfo;
import com.android.launcher3.folder.Folder;
import com.android.launcher3.folder.FolderIcon;
@@ -862,6 +864,33 @@
return makeDefaultActivityOptions(SPLASH_SCREEN_STYLE_UNDEFINED);
}
+ private ActivityOptionsWrapper getActivityLaunchDesktopOptions(ItemInfo info) {
+ if (!DesktopModeFlags.ENABLE_DESKTOP_APP_LAUNCH_TRANSITIONS.isTrue()) {
+ return null;
+ }
+ if (!areDesktopTasksVisible()) {
+ return null;
+ }
+ BubbleTextView.RunningAppState appState =
+ mControllers.taskbarRecentAppsController.getDesktopItemState(info);
+ AppLaunchType launchType = null;
+ switch (appState) {
+ case RUNNING:
+ return null;
+ case MINIMIZED:
+ launchType = AppLaunchType.UNMINIMIZE;
+ break;
+ case NOT_RUNNING:
+ launchType = AppLaunchType.LAUNCH;
+ break;
+ }
+ ActivityOptions options = ActivityOptions.makeRemoteTransition(
+ new RemoteTransition(
+ new DesktopAppLaunchTransition(
+ /* context= */ this, getMainExecutor(), launchType)));
+ return new ActivityOptionsWrapper(options, new RunnableList());
+ }
+
/**
* Sets a new data-source for this taskbar instance
*/
@@ -1402,7 +1431,9 @@
}
private RemoteTransition createUnminimizeRemoteTransition() {
- return new RemoteTransition(new DesktopAppLaunchTransition(this, getMainExecutor()));
+ return new RemoteTransition(
+ new DesktopAppLaunchTransition(
+ this, getMainExecutor(), AppLaunchType.UNMINIMIZE));
}
/**
@@ -1503,25 +1534,31 @@
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
try {
TestLogging.recordEvent(TestProtocol.SEQUENCE_MAIN, "start: taskbarAppIcon");
- if (info.user.equals(Process.myUserHandle())) {
- // TODO(b/216683257): Use startActivityForResult for search results that require it.
- if (taskInRecents != null) {
- // Re launch instance from recents
- ActivityOptionsWrapper opts = getActivityLaunchOptions(null, info);
- opts.options.setLaunchDisplayId(
- getDisplay() == null ? DEFAULT_DISPLAY : getDisplay().getDisplayId());
- if (ActivityManagerWrapper.getInstance()
- .startActivityFromRecents(taskInRecents.key, opts.options)) {
- mControllers.uiController.getRecentsView()
- .addSideTaskLaunchCallback(opts.onEndCallback);
- return;
- }
- }
- startActivity(intent);
- } else {
+ if (!info.user.equals(Process.myUserHandle())) {
+ // TODO b/376819104: support Desktop launch animations for apps in managed profiles
getSystemService(LauncherApps.class).startMainActivity(
intent.getComponent(), info.user, intent.getSourceBounds(), null);
+ return;
}
+ // TODO(b/216683257): Use startActivityForResult for search results that require it.
+ if (taskInRecents != null) {
+ // Re launch instance from recents
+ ActivityOptionsWrapper opts = getActivityLaunchOptions(null, info);
+ opts.options.setLaunchDisplayId(
+ getDisplay() == null ? DEFAULT_DISPLAY : getDisplay().getDisplayId());
+ if (ActivityManagerWrapper.getInstance()
+ .startActivityFromRecents(taskInRecents.key, opts.options)) {
+ mControllers.uiController.getRecentsView()
+ .addSideTaskLaunchCallback(opts.onEndCallback);
+ return;
+ }
+ }
+ ActivityOptionsWrapper opts = null;
+ if (areDesktopTasksVisible()) {
+ opts = getActivityLaunchDesktopOptions(info);
+ }
+ Bundle optionsBundle = opts == null ? null : opts.options.toBundle();
+ startActivity(intent, optionsBundle);
} catch (NullPointerException | ActivityNotFoundException | SecurityException e) {
Toast.makeText(this, R.string.activity_not_found, Toast.LENGTH_SHORT)
.show();
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarRecentAppsController.kt b/quickstep/src/com/android/launcher3/taskbar/TaskbarRecentAppsController.kt
index 7b05043..3d57de4 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarRecentAppsController.kt
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarRecentAppsController.kt
@@ -73,6 +73,33 @@
var shownTasks: List<GroupTask> = emptyList()
private set
+ /**
+ * Returns the state of the most active Desktop task represented by the given [ItemInfo].
+ *
+ * If there are several tasks represented by the same [ItemInfo] we return the most active one,
+ * i.e. we return [DesktopAppState.RUNNING] over [DesktopAppState.MINIMIZED], and
+ * [DesktopAppState.MINIMIZED] over [DesktopAppState.NOT_RUNNING].
+ */
+ fun getDesktopItemState(itemInfo: ItemInfo?): RunningAppState {
+ val packageName = itemInfo?.getTargetPackage() ?: return RunningAppState.NOT_RUNNING
+ return getDesktopAppState(packageName, itemInfo.user.identifier)
+ }
+
+ private fun getDesktopAppState(packageName: String, userId: Int): RunningAppState {
+ val tasks = desktopTask?.tasks ?: return RunningAppState.NOT_RUNNING
+ val appTasks =
+ tasks.filter { task ->
+ packageName == task.key.packageName && task.key.userId == userId
+ }
+ if (appTasks.find { getRunningAppState(it.key.id) == RunningAppState.RUNNING } != null) {
+ return RunningAppState.RUNNING
+ }
+ if (appTasks.find { getRunningAppState(it.key.id) == RunningAppState.MINIMIZED } != null) {
+ return RunningAppState.MINIMIZED
+ }
+ return RunningAppState.NOT_RUNNING
+ }
+
/** Get the [RunningAppState] for the given task. */
fun getRunningAppState(taskId: Int): RunningAppState {
return when (taskId) {
diff --git a/quickstep/tests/src/com/android/launcher3/taskbar/TaskbarRecentAppsControllerTest.kt b/quickstep/tests/src/com/android/launcher3/taskbar/TaskbarRecentAppsControllerTest.kt
index 59413d3..066ddc0 100644
--- a/quickstep/tests/src/com/android/launcher3/taskbar/TaskbarRecentAppsControllerTest.kt
+++ b/quickstep/tests/src/com/android/launcher3/taskbar/TaskbarRecentAppsControllerTest.kt
@@ -32,6 +32,7 @@
import com.android.launcher3.model.data.AppInfo
import com.android.launcher3.model.data.ItemInfo
import com.android.launcher3.model.data.TaskItemInfo
+import com.android.launcher3.model.data.WorkspaceItemInfo
import com.android.quickstep.RecentsModel
import com.android.quickstep.RecentsModel.RecentTasksChangedListener
import com.android.quickstep.TaskIconCache
@@ -78,7 +79,9 @@
private var taskListChangeId: Int = 1
private lateinit var recentAppsController: TaskbarRecentAppsController
- private lateinit var userHandle: UserHandle
+ private lateinit var myUserHandle: UserHandle
+ private val USER_HANDLE_1 = UserHandle.of(1)
+ private val USER_HANDLE_2 = UserHandle.of(2)
private var canShowRunningAndRecentAppsAtInit = true
private var recentTasksChangedListener: RecentTasksChangedListener? = null
@@ -86,7 +89,7 @@
@Before
fun setUp() {
super.setup()
- userHandle = Process.myUserHandle()
+ myUserHandle = Process.myUserHandle()
// Set desktop mode supported
whenever(mockContext.getResources()).thenReturn(mockResources)
@@ -149,6 +152,84 @@
}
@Test
+ fun getDesktopItemState_nullItemInfo_returnsNotRunning() {
+ setInDesktopMode(true)
+ assertThat(recentAppsController.getDesktopItemState(/* itemInfo= */ null))
+ .isEqualTo(RunningAppState.NOT_RUNNING)
+ }
+
+ @Test
+ fun getDesktopItemState_noItemPackage_returnsNotRunning() {
+ setInDesktopMode(true)
+ assertThat(recentAppsController.getDesktopItemState(ItemInfo()))
+ .isEqualTo(RunningAppState.NOT_RUNNING)
+ }
+
+ @Test
+ fun getDesktopItemState_noMatchingTasks_returnsNotRunning() {
+ setInDesktopMode(true)
+ val itemInfo = createItemInfo("package")
+ assertThat(recentAppsController.getDesktopItemState(itemInfo))
+ .isEqualTo(RunningAppState.NOT_RUNNING)
+ }
+
+ @Test
+ fun getDesktopItemState_matchingVisibleTask_returnsVisible() {
+ setInDesktopMode(true)
+ val visibleTask = createTask(id = 1, "visiblePackage", isVisible = true)
+ updateRecentTasks(runningTasks = listOf(visibleTask), recentTaskPackages = emptyList())
+ val itemInfo = createItemInfo("visiblePackage")
+
+ assertThat(recentAppsController.getDesktopItemState(itemInfo))
+ .isEqualTo(RunningAppState.RUNNING)
+ }
+
+ @Test
+ fun getDesktopItemState_matchingMinimizedTask_returnsMinimized() {
+ setInDesktopMode(true)
+ val minimizedTask = createTask(id = 1, "minimizedPackage", isVisible = false)
+ updateRecentTasks(runningTasks = listOf(minimizedTask), recentTaskPackages = emptyList())
+ val itemInfo = createItemInfo("minimizedPackage")
+
+ assertThat(recentAppsController.getDesktopItemState(itemInfo))
+ .isEqualTo(RunningAppState.MINIMIZED)
+ }
+
+ @Test
+ fun getDesktopItemState_matchingMinimizedAndRunningTask_returnsVisible() {
+ setInDesktopMode(true)
+ updateRecentTasks(
+ runningTasks =
+ listOf(
+ createTask(id = 1, "package", isVisible = false),
+ createTask(id = 2, "package", isVisible = true),
+ ),
+ recentTaskPackages = emptyList(),
+ )
+ val itemInfo = createItemInfo("package")
+
+ assertThat(recentAppsController.getDesktopItemState(itemInfo))
+ .isEqualTo(RunningAppState.RUNNING)
+ }
+
+ @Test
+ fun getDesktopItemState_noMatchingUserId_returnsNotRunning() {
+ setInDesktopMode(true)
+ updateRecentTasks(
+ runningTasks =
+ listOf(
+ createTask(id = 1, "package", isVisible = false, USER_HANDLE_1),
+ createTask(id = 2, "package", isVisible = true, USER_HANDLE_1),
+ ),
+ recentTaskPackages = emptyList(),
+ )
+ val itemInfo = createItemInfo("package", USER_HANDLE_2)
+
+ assertThat(recentAppsController.getDesktopItemState(itemInfo))
+ .isEqualTo(RunningAppState.NOT_RUNNING)
+ }
+
+ @Test
fun getRunningAppState_taskNotRunningOrMinimized_returnsNotRunning() {
setInDesktopMode(true)
updateRecentTasks(runningTasks = emptyList(), recentTaskPackages = emptyList())
@@ -814,7 +895,13 @@
private fun createTestAppInfo(
packageName: String = "testPackageName",
className: String = "testClassName",
- ) = AppInfo(ComponentName(packageName, className), className /* title */, userHandle, Intent())
+ ) =
+ AppInfo(
+ ComponentName(packageName, className),
+ className /* title */,
+ myUserHandle,
+ Intent(),
+ )
private fun createRecentTasksFromPackageNames(packageNames: List<String>): List<GroupTask> {
return packageNames.map { packageName ->
@@ -833,14 +920,19 @@
}
}
- private fun createTask(id: Int, packageName: String, isVisible: Boolean = true): Task {
+ private fun createTask(
+ id: Int,
+ packageName: String,
+ isVisible: Boolean = true,
+ localUserHandle: UserHandle? = null,
+ ): Task {
return Task(
Task.TaskKey(
id,
WINDOWING_MODE_FREEFORM,
Intent().apply { `package` = packageName },
ComponentName(packageName, "TestActivity"),
- userHandle.identifier,
+ localUserHandle?.identifier ?: myUserHandle.identifier,
0,
)
)
@@ -852,6 +944,16 @@
.thenReturn(inDesktopMode)
}
+ private fun createItemInfo(
+ packageName: String,
+ userHandle: UserHandle = myUserHandle,
+ ): ItemInfo {
+ val appInfo = AppInfo()
+ appInfo.intent = Intent().setComponent(ComponentName(packageName, "className"))
+ appInfo.user = userHandle
+ return WorkspaceItemInfo(appInfo)
+ }
+
private val GroupTask.packageNames: List<String>
get() = tasks.map { task -> task.key.packageName }