Loading quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java +40 −6 Original line number Diff line number Diff line Loading @@ -1063,7 +1063,7 @@ public class TaskbarActivityContext extends BaseTaskbarContext { Toast.LENGTH_SHORT).show(); } else { // Else launch the selected app pair launchFromTaskbarPreservingSplitIfVisible(recents, view, fi.contents); launchFromTaskbar(recents, view, fi.contents); mControllers.uiController.onTaskbarIconLaunched(fi); mControllers.taskbarStashController.updateAndAnimateTransientTaskbar(true); } Loading Loading @@ -1097,8 +1097,7 @@ public class TaskbarActivityContext extends BaseTaskbarContext { getSystemService(LauncherApps.class) .startShortcut(packageName, id, null, null, info.user); } else { launchFromTaskbarPreservingSplitIfVisible( recents, view, Collections.singletonList(info)); launchFromTaskbar(recents, view, Collections.singletonList(info)); } } catch (NullPointerException Loading Loading @@ -1136,8 +1135,7 @@ public class TaskbarActivityContext extends BaseTaskbarContext { // If we are selecting a second app for split, launch the split tasks taskbarUIController.triggerSecondAppForSplit(info, info.intent, view); } else { launchFromTaskbarPreservingSplitIfVisible( recents, view, Collections.singletonList(info)); launchFromTaskbar(recents, view, Collections.singletonList(info)); } mControllers.uiController.onTaskbarIconLaunched(info); mControllers.taskbarStashController.updateAndAnimateTransientTaskbar(true); Loading @@ -1152,13 +1150,49 @@ public class TaskbarActivityContext extends BaseTaskbarContext { } } /** * Runs when the user taps a Taskbar icon in TaskbarActivityContext (Overview or inside an app), * and calls the appropriate method to animate and launch. */ private void launchFromTaskbar(@Nullable RecentsView recents, @Nullable View launchingIconView, List<? extends ItemInfo> itemInfos) { if (isInApp()) { launchFromInAppTaskbar(recents, launchingIconView, itemInfos); } else { launchFromOverviewTaskbar(recents, launchingIconView, itemInfos); } } /** * Runs when the user taps a Taskbar icon while inside an app. */ private void launchFromInAppTaskbar(@Nullable RecentsView recents, @Nullable View launchingIconView, List<? extends ItemInfo> itemInfos) { if (recents == null) { return; } boolean tappedAppPair = itemInfos.size() == 2; if (tappedAppPair) { // If the icon is an app pair, the logic gets a bit complicated because we play // different animations depending on which app (or app pair) is currently running on // screen, so delegate logic to appPairsController. recents.getSplitSelectController().getAppPairsController() .handleAppPairLaunchInApp((AppPairIcon) launchingIconView, itemInfos); } else { // Tapped a single app, nothing complicated here. startItemInfoActivity(itemInfos.get(0)); } } /** * Run when the user taps a Taskbar icon while in Overview. If the tapped app is currently * visible to the user in Overview, or is part of a visible split pair, we expand the TaskView * as if the user tapped on it (preserving the split pair). Otherwise, launch it normally * (potentially breaking a split pair). */ private void launchFromTaskbarPreservingSplitIfVisible(@Nullable RecentsView recents, private void launchFromOverviewTaskbar(@Nullable RecentsView recents, @Nullable View launchingIconView, List<? extends ItemInfo> itemInfos) { if (recents == null) { return; Loading quickstep/src/com/android/quickstep/util/AppPairsController.java +186 −2 Original line number Diff line number Diff line Loading @@ -17,17 +17,22 @@ package com.android.quickstep.util; import static android.app.ActivityTaskManager.INVALID_TASK_ID; import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_APP_PAIR_LAUNCH; import static com.android.launcher3.util.Executors.MAIN_EXECUTOR; import static com.android.launcher3.util.Executors.MODEL_EXECUTOR; import static com.android.launcher3.util.SplitConfigurationOptions.STAGE_POSITION_BOTTOM_OR_RIGHT; import static com.android.launcher3.util.SplitConfigurationOptions.STAGE_POSITION_TOP_OR_LEFT; import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_BOTTOM_OR_RIGHT; import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_TOP_OR_LEFT; import static com.android.wm.shell.common.split.SplitScreenConstants.isPersistentSnapPosition; import android.app.ActivityTaskManager; import android.content.Context; import android.content.Intent; import android.content.pm.LauncherApps; import android.util.Log; import android.util.Pair; import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; Loading @@ -39,16 +44,23 @@ import com.android.launcher3.R; import com.android.launcher3.accessibility.LauncherAccessibilityDelegate; import com.android.launcher3.apppairs.AppPairIcon; import com.android.launcher3.icons.IconCache; import com.android.launcher3.logging.InstanceId; import com.android.launcher3.logging.StatsLogManager; import com.android.launcher3.model.data.FolderInfo; import com.android.launcher3.model.data.ItemInfo; import com.android.launcher3.model.data.WorkspaceItemInfo; import com.android.launcher3.taskbar.TaskbarActivityContext; import com.android.launcher3.util.ComponentKey; import com.android.launcher3.util.SplitConfigurationOptions.StagePosition; import com.android.quickstep.SystemUiProxy; import com.android.quickstep.TopTaskTracker; import com.android.quickstep.views.GroupedTaskView; import com.android.quickstep.views.TaskView; import com.android.systemui.shared.recents.model.Task; import com.android.wm.shell.common.split.SplitScreenConstants.PersistentSnapPosition; import java.util.Arrays; import java.util.List; /** * Controller class that handles app pair interactions: saving, modifying, deleting, etc. Loading Loading @@ -150,7 +162,7 @@ public class AppPairsController { task1Id = foundTask1.key.id; task1Intent = null; } else { task1Id = ActivityTaskManager.INVALID_TASK_ID; task1Id = INVALID_TASK_ID; task1Intent = app1.intent; } Loading @@ -176,6 +188,170 @@ public class AppPairsController { ); } /** * Handles the complicated logic for how to animate an app pair entrance when already inside an * app or app pair. * * If the user tapped on an app pair while already in an app pair, there are 4 general cases: * a) Clicked app pair A|B, but both apps are already running on screen. * b) App A is already on-screen, but App B isn't. * c) App B is on-screen, but App A isn't. * d) Neither is on-screen. * * If the user tapped an app pair while inside a single app, there are 3 cases: * a) The on-screen app is App A of the app pair. * b) The on-screen app is App B of the app pair. * c) It is neither. * * For each case, we call the appropriate animation and split launch type. */ public void handleAppPairLaunchInApp(AppPairIcon launchingIconView, List<? extends ItemInfo> itemInfos) { TaskbarActivityContext context = (TaskbarActivityContext) launchingIconView.getContext(); List<ComponentKey> componentKeys = itemInfos.stream().map(ItemInfo::getComponentKey).toList(); // Use TopTaskTracker to find the currently running app (or apps) TopTaskTracker topTaskTracker = getTopTaskTracker(context); // getRunningSplitTasksIds() will return a pair of ids if we are currently running a // split pair, or an empty array with zero length if we are running a single app. int[] runningSplitTasks = topTaskTracker.getRunningSplitTaskIds(); if (runningSplitTasks != null && runningSplitTasks.length == 2) { // Tapped an app pair while in an app pair int runningTaskId1 = runningSplitTasks[0]; int runningTaskId2 = runningSplitTasks[1]; mSplitSelectStateController.findLastActiveTasksAndRunCallback( componentKeys, false /* findExactPairMatch */, foundTasks -> { // If our clicked app pair has already-running Tasks, we grab the // taskIds here so we can see if those ids are already on-screen now List<Integer> lastActiveTasksOfAppPair = Arrays.stream(foundTasks).map((Task task) -> { if (task != null) { return task.getKey().getId(); } else { return INVALID_TASK_ID; } }).toList(); if (lastActiveTasksOfAppPair.contains(runningTaskId1) && lastActiveTasksOfAppPair.contains(runningTaskId2)) { // App A and App B are already on-screen, so do nothing. } else if (!lastActiveTasksOfAppPair.contains(runningTaskId1) && !lastActiveTasksOfAppPair.contains(runningTaskId2)) { // Neither A nor B are on screen, so just launch a new app pair // normally. launchAppPair(launchingIconView); } else { // Exactly one app (A or B) is on-screen, so we have to launch the other // on the appropriate side. ItemInfo app1 = itemInfos.get(0); ItemInfo app2 = itemInfos.get(1); int task1 = lastActiveTasksOfAppPair.get(0); int task2 = lastActiveTasksOfAppPair.get(1); // If task1 is one of the running on-screen tasks, we launch app2. // If not, task2 must be the running task, and we launch app1. ItemInfo appToLaunch = task1 == runningTaskId1 || task1 == runningTaskId2 ? app2 : app1; // If the on-screen task is on the bottom/right position, we launch to // the top/left. If not, we launch to the bottom/right. @StagePosition int sideToLaunch = task1 == runningTaskId2 || task2 == runningTaskId2 ? STAGE_POSITION_TOP_OR_LEFT : STAGE_POSITION_BOTTOM_OR_RIGHT; launchToSide(context, launchingIconView.getInfo(), appToLaunch, sideToLaunch); } } ); } else { // Tapped an app pair while in a single app int runningTaskId = topTaskTracker .getCachedTopTask(false /* filterOnlyVisibleRecents */).getTaskId(); mSplitSelectStateController.findLastActiveTasksAndRunCallback( componentKeys, false /* findExactPairMatch */, foundTasks -> { Task foundTask1 = foundTasks[0]; Task foundTask2 = foundTasks[1]; boolean task1IsOnScreen = foundTask1 != null && foundTask1.getKey().getId() == runningTaskId; boolean task2IsOnScreen = foundTask2 != null && foundTask2.getKey().getId() == runningTaskId; if (!task1IsOnScreen && !task2IsOnScreen) { // Neither App A nor App B are on-screen, launch the app pair normally. launchAppPair(launchingIconView); } else { // Either A or B is on-screen, so launch the other on the appropriate // side. ItemInfo app1 = itemInfos.get(0); ItemInfo app2 = itemInfos.get(1); // If task1 is the running on-screen task, we launch app2 on the // bottom/right. If task2 is on-screen, launch app1 on the top/left. ItemInfo appToLaunch = task1IsOnScreen ? app2 : app1; @StagePosition int sideToLaunch = task1IsOnScreen ? STAGE_POSITION_BOTTOM_OR_RIGHT : STAGE_POSITION_TOP_OR_LEFT; launchToSide(context, launchingIconView.getInfo(), appToLaunch, sideToLaunch); } } ); } } /** * Executes a split launch by launching an app to the side of an existing app. * @param context The TaskbarActivityContext that we are launching the app pair from. * @param launchingItemInfo The itemInfo of the icon that was tapped. * @param app The app that will launch to the side of the existing running app (not necessarily * the same as the previous parameter; e.g. we tap an app pair but launch an app). * @param side A @StagePosition, either STAGE_POSITION_TOP_OR_LEFT or * STAGE_POSITION_BOTTOM_OR_RIGHT. */ @VisibleForTesting public void launchToSide( TaskbarActivityContext context, ItemInfo launchingItemInfo, ItemInfo app, @StagePosition int side ) { LauncherApps launcherApps = context.getSystemService(LauncherApps.class); // Set up to log app pair launch event Pair<com.android.internal.logging.InstanceId, InstanceId> instanceIds = LogUtils.getShellShareableInstanceId(); context.getStatsLogManager() .logger() .withItemInfo(launchingItemInfo) .withInstanceId(instanceIds.second) .log(LAUNCHER_APP_PAIR_LAUNCH); SystemUiProxy.INSTANCE.get(context) .startIntent( launcherApps.getMainActivityLaunchIntent( app.getIntent().getComponent(), null, app.user ), app.user.getIdentifier(), new Intent(), side, null, instanceIds.first ); } /** * App pair members have a "rank" attribute that contains information about the split position * and ratio. We implement this by splitting the int in half (e.g. 16 bits each), then use one Loading Loading @@ -209,4 +385,12 @@ public class AppPairsController { public String getDefaultTitle(CharSequence app1, CharSequence app2) { return mContext.getString(R.string.app_pair_default_title, app1, app2); } /** * Gets the TopTaskTracker, which is a cached record of the top running Task. */ @VisibleForTesting public TopTaskTracker getTopTaskTracker(Context context) { return TopTaskTracker.INSTANCE.get(context); } } quickstep/src/com/android/quickstep/util/SplitAnimationController.kt +97 −8 Original line number Diff line number Diff line Loading @@ -391,14 +391,6 @@ class SplitAnimationController(val splitSelectStateController: SplitSelectStateC "trying to launch an app pair icon, but encountered an unexpected null" } // If launching an app pair from Taskbar inside of an app context, use fade-in animation // TODO (b/316485863): Replace with desired app pair launch animation if (launchingIconView.context is TaskbarActivityContext) { composeFadeInSplitLaunchAnimator( initialTaskId, secondTaskId, info, t, finishCallback) return } composeIconSplitLaunchAnimator(launchingIconView, info, t, finishCallback) } else { // Fallback case: simple fade-in animation Loading Loading @@ -483,6 +475,10 @@ class SplitAnimationController(val splitSelectStateController: SplitSelectStateC * We want to animate the Root (grandparent) so that it affects both apps and the divider. * To do this, we find one of the nodes with WINDOWING_MODE_MULTI_WINDOW (one of the * left-side ones, for simplicity) and traverse the tree until we find the grandparent. * * This function is only called when we are animating the app pair in from scratch. It is NOT * called when we are animating in from an existing visible TaskView tile or an app that is * already on screen. */ @VisibleForTesting fun composeIconSplitLaunchAnimator( Loading @@ -491,6 +487,14 @@ class SplitAnimationController(val splitSelectStateController: SplitSelectStateC t: Transaction, finishCallback: Runnable ) { // If launching an app pair from Taskbar inside of an app context (no access to Launcher), // use the scale-up animation if (launchingIconView.context is TaskbarActivityContext) { composeScaleUpLaunchAnimation(transitionInfo, t, finishCallback) return } // Else we are in Launcher and can launch with the full icon stretch-and-split animation. val launcher = Launcher.getLauncher(launchingIconView.context) val dp = launcher.deviceProfile Loading Loading @@ -649,6 +653,91 @@ class SplitAnimationController(val splitSelectStateController: SplitSelectStateC launchAnimation.start() } /** * This is a scale-up-and-fade-in animation (34% to 100%) for launching an app in Overview when * there is no visible associated tile to expand from. */ @VisibleForTesting fun composeScaleUpLaunchAnimation( transitionInfo: TransitionInfo, t: Transaction, finishCallback: Runnable ) { val launchAnimation = AnimatorSet() val progressUpdater = ValueAnimator.ofFloat(0f, 1f) progressUpdater.setDuration(QuickstepTransitionManager.APP_LAUNCH_DURATION) progressUpdater.interpolator = Interpolators.EMPHASIZED var rootCandidate: Change? = null for (change in transitionInfo.changes) { val taskInfo: RunningTaskInfo = change.taskInfo ?: continue // TODO (b/316490565): Replace this logic when SplitBounds is available to // startAnimation() and we can know the precise taskIds of launching tasks. // Find a change that has WINDOWING_MODE_MULTI_WINDOW. if ( taskInfo.windowingMode == WINDOWING_MODE_MULTI_WINDOW && (change.mode == TRANSIT_OPEN || change.mode == TRANSIT_TO_FRONT) ) { // Found one! rootCandidate = change break } } // If we could not find a proper root candidate, something went wrong. check(rootCandidate != null) { "Could not find a split root candidate" } // Recurse up the tree until parent is null, then we've found our root. var parentToken: WindowContainerToken? = rootCandidate.parent while (parentToken != null) { rootCandidate = transitionInfo.getChange(parentToken) ?: break parentToken = rootCandidate.parent } // Make sure nothing weird happened, like getChange() returning null. check(rootCandidate != null) { "Failed to find a root leash" } // Starting position is a 34% size tile centered in the middle of the screen. // Ending position is the full device screen. val screenBounds = rootCandidate.endAbsBounds val startingScale = 0.34f val startX = screenBounds.left + ((screenBounds.right - screenBounds.left) * ((1 - startingScale) / 2f)) val startY = screenBounds.top + ((screenBounds.bottom - screenBounds.top) * ((1 - startingScale) / 2f)) val endX = screenBounds.left val endY = screenBounds.top progressUpdater.addUpdateListener { valueAnimator: ValueAnimator -> val progress = valueAnimator.animatedFraction val x = startX + ((endX - startX) * progress) val y = startY + ((endY - startY) * progress) val scale = startingScale + ((1 - startingScale) * progress) t.setPosition(rootCandidate.leash, x, y) t.setScale(rootCandidate.leash, scale, scale) t.setAlpha(rootCandidate.leash, progress) t.apply() } // When animation ends, run finishCallback progressUpdater.addListener( object : AnimatorListenerAdapter() { override fun onAnimationEnd(animation: Animator) { finishCallback.run() } } ) launchAnimation.play(progressUpdater) launchAnimation.start() } /** * If we are launching split screen without any special animation from a starting View, we * simply fade in the starting apps and fade out launcher. Loading quickstep/tests/src/com/android/quickstep/util/AppPairsControllerTest.kt +262 −0 File changed.Preview size limit exceeded, changes collapsed. Show changes quickstep/tests/src/com/android/quickstep/util/SplitAnimationControllerTest.kt +2 −3 Original line number Diff line number Diff line Loading @@ -281,7 +281,7 @@ class SplitAnimationControllerTest { whenever(mockAppPairIcon.context).thenReturn(mockTaskbarActivityContext) doNothing() .whenever(spySplitAnimationController) .composeFadeInSplitLaunchAnimator(any(), any(), any(), any(), any()) .composeScaleUpLaunchAnimation(any(), any(), any()) spySplitAnimationController.playSplitLaunchAnimation( null /* launchingTaskView */, Loading @@ -298,8 +298,7 @@ class SplitAnimationControllerTest { {} /* finishCallback */ ) verify(spySplitAnimationController) .composeFadeInSplitLaunchAnimator(any(), any(), any(), any(), any()) verify(spySplitAnimationController).composeScaleUpLaunchAnimation(any(), any(), any()) } @Test Loading Loading
quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java +40 −6 Original line number Diff line number Diff line Loading @@ -1063,7 +1063,7 @@ public class TaskbarActivityContext extends BaseTaskbarContext { Toast.LENGTH_SHORT).show(); } else { // Else launch the selected app pair launchFromTaskbarPreservingSplitIfVisible(recents, view, fi.contents); launchFromTaskbar(recents, view, fi.contents); mControllers.uiController.onTaskbarIconLaunched(fi); mControllers.taskbarStashController.updateAndAnimateTransientTaskbar(true); } Loading Loading @@ -1097,8 +1097,7 @@ public class TaskbarActivityContext extends BaseTaskbarContext { getSystemService(LauncherApps.class) .startShortcut(packageName, id, null, null, info.user); } else { launchFromTaskbarPreservingSplitIfVisible( recents, view, Collections.singletonList(info)); launchFromTaskbar(recents, view, Collections.singletonList(info)); } } catch (NullPointerException Loading Loading @@ -1136,8 +1135,7 @@ public class TaskbarActivityContext extends BaseTaskbarContext { // If we are selecting a second app for split, launch the split tasks taskbarUIController.triggerSecondAppForSplit(info, info.intent, view); } else { launchFromTaskbarPreservingSplitIfVisible( recents, view, Collections.singletonList(info)); launchFromTaskbar(recents, view, Collections.singletonList(info)); } mControllers.uiController.onTaskbarIconLaunched(info); mControllers.taskbarStashController.updateAndAnimateTransientTaskbar(true); Loading @@ -1152,13 +1150,49 @@ public class TaskbarActivityContext extends BaseTaskbarContext { } } /** * Runs when the user taps a Taskbar icon in TaskbarActivityContext (Overview or inside an app), * and calls the appropriate method to animate and launch. */ private void launchFromTaskbar(@Nullable RecentsView recents, @Nullable View launchingIconView, List<? extends ItemInfo> itemInfos) { if (isInApp()) { launchFromInAppTaskbar(recents, launchingIconView, itemInfos); } else { launchFromOverviewTaskbar(recents, launchingIconView, itemInfos); } } /** * Runs when the user taps a Taskbar icon while inside an app. */ private void launchFromInAppTaskbar(@Nullable RecentsView recents, @Nullable View launchingIconView, List<? extends ItemInfo> itemInfos) { if (recents == null) { return; } boolean tappedAppPair = itemInfos.size() == 2; if (tappedAppPair) { // If the icon is an app pair, the logic gets a bit complicated because we play // different animations depending on which app (or app pair) is currently running on // screen, so delegate logic to appPairsController. recents.getSplitSelectController().getAppPairsController() .handleAppPairLaunchInApp((AppPairIcon) launchingIconView, itemInfos); } else { // Tapped a single app, nothing complicated here. startItemInfoActivity(itemInfos.get(0)); } } /** * Run when the user taps a Taskbar icon while in Overview. If the tapped app is currently * visible to the user in Overview, or is part of a visible split pair, we expand the TaskView * as if the user tapped on it (preserving the split pair). Otherwise, launch it normally * (potentially breaking a split pair). */ private void launchFromTaskbarPreservingSplitIfVisible(@Nullable RecentsView recents, private void launchFromOverviewTaskbar(@Nullable RecentsView recents, @Nullable View launchingIconView, List<? extends ItemInfo> itemInfos) { if (recents == null) { return; Loading
quickstep/src/com/android/quickstep/util/AppPairsController.java +186 −2 Original line number Diff line number Diff line Loading @@ -17,17 +17,22 @@ package com.android.quickstep.util; import static android.app.ActivityTaskManager.INVALID_TASK_ID; import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_APP_PAIR_LAUNCH; import static com.android.launcher3.util.Executors.MAIN_EXECUTOR; import static com.android.launcher3.util.Executors.MODEL_EXECUTOR; import static com.android.launcher3.util.SplitConfigurationOptions.STAGE_POSITION_BOTTOM_OR_RIGHT; import static com.android.launcher3.util.SplitConfigurationOptions.STAGE_POSITION_TOP_OR_LEFT; import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_BOTTOM_OR_RIGHT; import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_TOP_OR_LEFT; import static com.android.wm.shell.common.split.SplitScreenConstants.isPersistentSnapPosition; import android.app.ActivityTaskManager; import android.content.Context; import android.content.Intent; import android.content.pm.LauncherApps; import android.util.Log; import android.util.Pair; import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; Loading @@ -39,16 +44,23 @@ import com.android.launcher3.R; import com.android.launcher3.accessibility.LauncherAccessibilityDelegate; import com.android.launcher3.apppairs.AppPairIcon; import com.android.launcher3.icons.IconCache; import com.android.launcher3.logging.InstanceId; import com.android.launcher3.logging.StatsLogManager; import com.android.launcher3.model.data.FolderInfo; import com.android.launcher3.model.data.ItemInfo; import com.android.launcher3.model.data.WorkspaceItemInfo; import com.android.launcher3.taskbar.TaskbarActivityContext; import com.android.launcher3.util.ComponentKey; import com.android.launcher3.util.SplitConfigurationOptions.StagePosition; import com.android.quickstep.SystemUiProxy; import com.android.quickstep.TopTaskTracker; import com.android.quickstep.views.GroupedTaskView; import com.android.quickstep.views.TaskView; import com.android.systemui.shared.recents.model.Task; import com.android.wm.shell.common.split.SplitScreenConstants.PersistentSnapPosition; import java.util.Arrays; import java.util.List; /** * Controller class that handles app pair interactions: saving, modifying, deleting, etc. Loading Loading @@ -150,7 +162,7 @@ public class AppPairsController { task1Id = foundTask1.key.id; task1Intent = null; } else { task1Id = ActivityTaskManager.INVALID_TASK_ID; task1Id = INVALID_TASK_ID; task1Intent = app1.intent; } Loading @@ -176,6 +188,170 @@ public class AppPairsController { ); } /** * Handles the complicated logic for how to animate an app pair entrance when already inside an * app or app pair. * * If the user tapped on an app pair while already in an app pair, there are 4 general cases: * a) Clicked app pair A|B, but both apps are already running on screen. * b) App A is already on-screen, but App B isn't. * c) App B is on-screen, but App A isn't. * d) Neither is on-screen. * * If the user tapped an app pair while inside a single app, there are 3 cases: * a) The on-screen app is App A of the app pair. * b) The on-screen app is App B of the app pair. * c) It is neither. * * For each case, we call the appropriate animation and split launch type. */ public void handleAppPairLaunchInApp(AppPairIcon launchingIconView, List<? extends ItemInfo> itemInfos) { TaskbarActivityContext context = (TaskbarActivityContext) launchingIconView.getContext(); List<ComponentKey> componentKeys = itemInfos.stream().map(ItemInfo::getComponentKey).toList(); // Use TopTaskTracker to find the currently running app (or apps) TopTaskTracker topTaskTracker = getTopTaskTracker(context); // getRunningSplitTasksIds() will return a pair of ids if we are currently running a // split pair, or an empty array with zero length if we are running a single app. int[] runningSplitTasks = topTaskTracker.getRunningSplitTaskIds(); if (runningSplitTasks != null && runningSplitTasks.length == 2) { // Tapped an app pair while in an app pair int runningTaskId1 = runningSplitTasks[0]; int runningTaskId2 = runningSplitTasks[1]; mSplitSelectStateController.findLastActiveTasksAndRunCallback( componentKeys, false /* findExactPairMatch */, foundTasks -> { // If our clicked app pair has already-running Tasks, we grab the // taskIds here so we can see if those ids are already on-screen now List<Integer> lastActiveTasksOfAppPair = Arrays.stream(foundTasks).map((Task task) -> { if (task != null) { return task.getKey().getId(); } else { return INVALID_TASK_ID; } }).toList(); if (lastActiveTasksOfAppPair.contains(runningTaskId1) && lastActiveTasksOfAppPair.contains(runningTaskId2)) { // App A and App B are already on-screen, so do nothing. } else if (!lastActiveTasksOfAppPair.contains(runningTaskId1) && !lastActiveTasksOfAppPair.contains(runningTaskId2)) { // Neither A nor B are on screen, so just launch a new app pair // normally. launchAppPair(launchingIconView); } else { // Exactly one app (A or B) is on-screen, so we have to launch the other // on the appropriate side. ItemInfo app1 = itemInfos.get(0); ItemInfo app2 = itemInfos.get(1); int task1 = lastActiveTasksOfAppPair.get(0); int task2 = lastActiveTasksOfAppPair.get(1); // If task1 is one of the running on-screen tasks, we launch app2. // If not, task2 must be the running task, and we launch app1. ItemInfo appToLaunch = task1 == runningTaskId1 || task1 == runningTaskId2 ? app2 : app1; // If the on-screen task is on the bottom/right position, we launch to // the top/left. If not, we launch to the bottom/right. @StagePosition int sideToLaunch = task1 == runningTaskId2 || task2 == runningTaskId2 ? STAGE_POSITION_TOP_OR_LEFT : STAGE_POSITION_BOTTOM_OR_RIGHT; launchToSide(context, launchingIconView.getInfo(), appToLaunch, sideToLaunch); } } ); } else { // Tapped an app pair while in a single app int runningTaskId = topTaskTracker .getCachedTopTask(false /* filterOnlyVisibleRecents */).getTaskId(); mSplitSelectStateController.findLastActiveTasksAndRunCallback( componentKeys, false /* findExactPairMatch */, foundTasks -> { Task foundTask1 = foundTasks[0]; Task foundTask2 = foundTasks[1]; boolean task1IsOnScreen = foundTask1 != null && foundTask1.getKey().getId() == runningTaskId; boolean task2IsOnScreen = foundTask2 != null && foundTask2.getKey().getId() == runningTaskId; if (!task1IsOnScreen && !task2IsOnScreen) { // Neither App A nor App B are on-screen, launch the app pair normally. launchAppPair(launchingIconView); } else { // Either A or B is on-screen, so launch the other on the appropriate // side. ItemInfo app1 = itemInfos.get(0); ItemInfo app2 = itemInfos.get(1); // If task1 is the running on-screen task, we launch app2 on the // bottom/right. If task2 is on-screen, launch app1 on the top/left. ItemInfo appToLaunch = task1IsOnScreen ? app2 : app1; @StagePosition int sideToLaunch = task1IsOnScreen ? STAGE_POSITION_BOTTOM_OR_RIGHT : STAGE_POSITION_TOP_OR_LEFT; launchToSide(context, launchingIconView.getInfo(), appToLaunch, sideToLaunch); } } ); } } /** * Executes a split launch by launching an app to the side of an existing app. * @param context The TaskbarActivityContext that we are launching the app pair from. * @param launchingItemInfo The itemInfo of the icon that was tapped. * @param app The app that will launch to the side of the existing running app (not necessarily * the same as the previous parameter; e.g. we tap an app pair but launch an app). * @param side A @StagePosition, either STAGE_POSITION_TOP_OR_LEFT or * STAGE_POSITION_BOTTOM_OR_RIGHT. */ @VisibleForTesting public void launchToSide( TaskbarActivityContext context, ItemInfo launchingItemInfo, ItemInfo app, @StagePosition int side ) { LauncherApps launcherApps = context.getSystemService(LauncherApps.class); // Set up to log app pair launch event Pair<com.android.internal.logging.InstanceId, InstanceId> instanceIds = LogUtils.getShellShareableInstanceId(); context.getStatsLogManager() .logger() .withItemInfo(launchingItemInfo) .withInstanceId(instanceIds.second) .log(LAUNCHER_APP_PAIR_LAUNCH); SystemUiProxy.INSTANCE.get(context) .startIntent( launcherApps.getMainActivityLaunchIntent( app.getIntent().getComponent(), null, app.user ), app.user.getIdentifier(), new Intent(), side, null, instanceIds.first ); } /** * App pair members have a "rank" attribute that contains information about the split position * and ratio. We implement this by splitting the int in half (e.g. 16 bits each), then use one Loading Loading @@ -209,4 +385,12 @@ public class AppPairsController { public String getDefaultTitle(CharSequence app1, CharSequence app2) { return mContext.getString(R.string.app_pair_default_title, app1, app2); } /** * Gets the TopTaskTracker, which is a cached record of the top running Task. */ @VisibleForTesting public TopTaskTracker getTopTaskTracker(Context context) { return TopTaskTracker.INSTANCE.get(context); } }
quickstep/src/com/android/quickstep/util/SplitAnimationController.kt +97 −8 Original line number Diff line number Diff line Loading @@ -391,14 +391,6 @@ class SplitAnimationController(val splitSelectStateController: SplitSelectStateC "trying to launch an app pair icon, but encountered an unexpected null" } // If launching an app pair from Taskbar inside of an app context, use fade-in animation // TODO (b/316485863): Replace with desired app pair launch animation if (launchingIconView.context is TaskbarActivityContext) { composeFadeInSplitLaunchAnimator( initialTaskId, secondTaskId, info, t, finishCallback) return } composeIconSplitLaunchAnimator(launchingIconView, info, t, finishCallback) } else { // Fallback case: simple fade-in animation Loading Loading @@ -483,6 +475,10 @@ class SplitAnimationController(val splitSelectStateController: SplitSelectStateC * We want to animate the Root (grandparent) so that it affects both apps and the divider. * To do this, we find one of the nodes with WINDOWING_MODE_MULTI_WINDOW (one of the * left-side ones, for simplicity) and traverse the tree until we find the grandparent. * * This function is only called when we are animating the app pair in from scratch. It is NOT * called when we are animating in from an existing visible TaskView tile or an app that is * already on screen. */ @VisibleForTesting fun composeIconSplitLaunchAnimator( Loading @@ -491,6 +487,14 @@ class SplitAnimationController(val splitSelectStateController: SplitSelectStateC t: Transaction, finishCallback: Runnable ) { // If launching an app pair from Taskbar inside of an app context (no access to Launcher), // use the scale-up animation if (launchingIconView.context is TaskbarActivityContext) { composeScaleUpLaunchAnimation(transitionInfo, t, finishCallback) return } // Else we are in Launcher and can launch with the full icon stretch-and-split animation. val launcher = Launcher.getLauncher(launchingIconView.context) val dp = launcher.deviceProfile Loading Loading @@ -649,6 +653,91 @@ class SplitAnimationController(val splitSelectStateController: SplitSelectStateC launchAnimation.start() } /** * This is a scale-up-and-fade-in animation (34% to 100%) for launching an app in Overview when * there is no visible associated tile to expand from. */ @VisibleForTesting fun composeScaleUpLaunchAnimation( transitionInfo: TransitionInfo, t: Transaction, finishCallback: Runnable ) { val launchAnimation = AnimatorSet() val progressUpdater = ValueAnimator.ofFloat(0f, 1f) progressUpdater.setDuration(QuickstepTransitionManager.APP_LAUNCH_DURATION) progressUpdater.interpolator = Interpolators.EMPHASIZED var rootCandidate: Change? = null for (change in transitionInfo.changes) { val taskInfo: RunningTaskInfo = change.taskInfo ?: continue // TODO (b/316490565): Replace this logic when SplitBounds is available to // startAnimation() and we can know the precise taskIds of launching tasks. // Find a change that has WINDOWING_MODE_MULTI_WINDOW. if ( taskInfo.windowingMode == WINDOWING_MODE_MULTI_WINDOW && (change.mode == TRANSIT_OPEN || change.mode == TRANSIT_TO_FRONT) ) { // Found one! rootCandidate = change break } } // If we could not find a proper root candidate, something went wrong. check(rootCandidate != null) { "Could not find a split root candidate" } // Recurse up the tree until parent is null, then we've found our root. var parentToken: WindowContainerToken? = rootCandidate.parent while (parentToken != null) { rootCandidate = transitionInfo.getChange(parentToken) ?: break parentToken = rootCandidate.parent } // Make sure nothing weird happened, like getChange() returning null. check(rootCandidate != null) { "Failed to find a root leash" } // Starting position is a 34% size tile centered in the middle of the screen. // Ending position is the full device screen. val screenBounds = rootCandidate.endAbsBounds val startingScale = 0.34f val startX = screenBounds.left + ((screenBounds.right - screenBounds.left) * ((1 - startingScale) / 2f)) val startY = screenBounds.top + ((screenBounds.bottom - screenBounds.top) * ((1 - startingScale) / 2f)) val endX = screenBounds.left val endY = screenBounds.top progressUpdater.addUpdateListener { valueAnimator: ValueAnimator -> val progress = valueAnimator.animatedFraction val x = startX + ((endX - startX) * progress) val y = startY + ((endY - startY) * progress) val scale = startingScale + ((1 - startingScale) * progress) t.setPosition(rootCandidate.leash, x, y) t.setScale(rootCandidate.leash, scale, scale) t.setAlpha(rootCandidate.leash, progress) t.apply() } // When animation ends, run finishCallback progressUpdater.addListener( object : AnimatorListenerAdapter() { override fun onAnimationEnd(animation: Animator) { finishCallback.run() } } ) launchAnimation.play(progressUpdater) launchAnimation.start() } /** * If we are launching split screen without any special animation from a starting View, we * simply fade in the starting apps and fade out launcher. Loading
quickstep/tests/src/com/android/quickstep/util/AppPairsControllerTest.kt +262 −0 File changed.Preview size limit exceeded, changes collapsed. Show changes
quickstep/tests/src/com/android/quickstep/util/SplitAnimationControllerTest.kt +2 −3 Original line number Diff line number Diff line Loading @@ -281,7 +281,7 @@ class SplitAnimationControllerTest { whenever(mockAppPairIcon.context).thenReturn(mockTaskbarActivityContext) doNothing() .whenever(spySplitAnimationController) .composeFadeInSplitLaunchAnimator(any(), any(), any(), any(), any()) .composeScaleUpLaunchAnimation(any(), any(), any()) spySplitAnimationController.playSplitLaunchAnimation( null /* launchingTaskView */, Loading @@ -298,8 +298,7 @@ class SplitAnimationControllerTest { {} /* finishCallback */ ) verify(spySplitAnimationController) .composeFadeInSplitLaunchAnimator(any(), any(), any(), any(), any()) verify(spySplitAnimationController).composeScaleUpLaunchAnimation(any(), any(), any()) } @Test Loading