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:
@@ -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) {
|
||||
|
||||
+107
-5
@@ -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 }
|
||||
|
||||
|
||||
Reference in New Issue
Block a user