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
This commit is contained in:
Gustav Sennton
2024-10-31 14:29:25 +00:00
parent a2603e829a
commit da2c2c0f7f
5 changed files with 223 additions and 31 deletions
@@ -20,6 +20,7 @@ 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.SurfaceControl.Transaction
import android.view.WindowManager.TRANSIT_OPEN
@@ -31,6 +32,7 @@ import android.window.TransitionInfo
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 @@ import java.util.concurrent.Executor
* ([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 @@ class DesktopAppLaunchTransition(private val context: Context, private val mainE
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,12 +143,17 @@ class DesktopAppLaunchTransition(private val context: Context, private val mainE
}
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,
@@ -36,6 +36,7 @@ import com.android.launcher3.R;
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 @@ public class KeyboardQuickSwitchViewController {
) {
// 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,
@@ -59,6 +59,7 @@ import android.content.res.Resources;
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.anim.AnimatorPlaybackController;
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 @@ public class TaskbarActivityContext extends BaseTaskbarContext {
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 @@ public class TaskbarActivityContext extends BaseTaskbarContext {
}
private RemoteTransition createUnminimizeRemoteTransition() {
return new RemoteTransition(new DesktopAppLaunchTransition(this, getMainExecutor()));
return new RemoteTransition(
new DesktopAppLaunchTransition(
this, getMainExecutor(), AppLaunchType.UNMINIMIZE));
}
/**
@@ -1503,25 +1534,31 @@ public class TaskbarActivityContext extends BaseTaskbarContext {
.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();
@@ -73,6 +73,33 @@ class TaskbarRecentAppsController(context: Context, private val recentsModel: Re
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) {
@@ -32,6 +32,7 @@ import com.android.launcher3.LauncherSettings.Favorites.CONTAINER_HOTSEAT_PREDIC
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 @@ class TaskbarRecentAppsControllerTest : TaskbarBaseTestCase() {
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 @@ class TaskbarRecentAppsControllerTest : TaskbarBaseTestCase() {
@Before
fun setUp() {
super.setup()
userHandle = Process.myUserHandle()
myUserHandle = Process.myUserHandle()
// Set desktop mode supported
whenever(mockContext.getResources()).thenReturn(mockResources)
@@ -148,6 +151,84 @@ class TaskbarRecentAppsControllerTest : TaskbarBaseTestCase() {
verify(mockRecentsModel, times(1)).getTasks(any<Consumer<List<GroupTask>>>())
}
@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)
@@ -814,7 +895,13 @@ class TaskbarRecentAppsControllerTest : TaskbarBaseTestCase() {
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 @@ class TaskbarRecentAppsControllerTest : TaskbarBaseTestCase() {
}
}
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 @@ class TaskbarRecentAppsControllerTest : TaskbarBaseTestCase() {
.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 }