Fix app pairs launch from in-app Taskbar
This CL adds a handler function in SplitAnimationController that manages the complicated logic flow for launching an app pair when already inside of an app. To give an idea of the complicated logic: 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 a different animation and launch the app pair in a different way. When merged, this patch will fix all animation glitches that are currently happening in these situations, and get us 90% of the way to having the ideal animation in all cases. There are still a few complicated cases that need a polished animation (like when you launch app pairs with custom ratios), which will be implemented in a following patch soon (I thought this CL was big enough already as is). Bug: 316485863 Fixes: 315190686 Test: Manual testing of all the different launch combinations Flag: ACONFIG com.android.wm.shell.enable_app_pairs TEAMFOOD Change-Id: I5c0e03512bb706360c575d833cac6ed02a5de936
This commit is contained in:
@@ -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);
|
||||
}
|
||||
@@ -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
|
||||
@@ -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);
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
@@ -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.
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
@@ -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(
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -18,18 +18,37 @@ package com.android.quickstep.util
|
||||
|
||||
import android.content.Context
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import com.android.launcher3.apppairs.AppPairIcon
|
||||
import com.android.launcher3.logging.StatsLogManager
|
||||
import com.android.launcher3.model.data.ItemInfo
|
||||
import com.android.launcher3.taskbar.TaskbarActivityContext
|
||||
import com.android.launcher3.util.SplitConfigurationOptions.STAGE_POSITION_BOTTOM_OR_RIGHT
|
||||
import com.android.launcher3.util.SplitConfigurationOptions.STAGE_POSITION_TOP_OR_LEFT
|
||||
import com.android.quickstep.TopTaskTracker
|
||||
import com.android.quickstep.TopTaskTracker.CachedTaskInfo
|
||||
import com.android.systemui.shared.recents.model.Task
|
||||
import com.android.systemui.shared.recents.model.Task.TaskKey
|
||||
import com.android.wm.shell.common.split.SplitScreenConstants.SNAP_TO_30_70
|
||||
import com.android.wm.shell.common.split.SplitScreenConstants.SNAP_TO_50_50
|
||||
import com.android.wm.shell.common.split.SplitScreenConstants.SNAP_TO_70_30
|
||||
import java.util.function.Consumer
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.mockito.ArgumentCaptor
|
||||
import org.mockito.Captor
|
||||
import org.mockito.Mock
|
||||
import org.mockito.MockitoAnnotations
|
||||
import org.mockito.kotlin.any
|
||||
import org.mockito.kotlin.anyOrNull
|
||||
import org.mockito.kotlin.doNothing
|
||||
import org.mockito.kotlin.eq
|
||||
import org.mockito.kotlin.never
|
||||
import org.mockito.kotlin.spy
|
||||
import org.mockito.kotlin.times
|
||||
import org.mockito.kotlin.verify
|
||||
import org.mockito.kotlin.whenever
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class AppPairsControllerTest {
|
||||
@@ -58,11 +77,38 @@ class AppPairsControllerTest {
|
||||
appPairsController.encodeRank(STAGE_POSITION_BOTTOM_OR_RIGHT, SNAP_TO_70_30)
|
||||
}
|
||||
|
||||
@Mock lateinit var mockAppPairIcon: AppPairIcon
|
||||
@Mock lateinit var mockTaskbarActivityContext: TaskbarActivityContext
|
||||
@Mock lateinit var mockTopTaskTracker: TopTaskTracker
|
||||
@Mock lateinit var mockCachedTaskInfo: CachedTaskInfo
|
||||
@Mock lateinit var mockItemInfo1: ItemInfo
|
||||
@Mock lateinit var mockItemInfo2: ItemInfo
|
||||
@Mock lateinit var mockTask1: Task
|
||||
@Mock lateinit var mockTask2: Task
|
||||
@Mock lateinit var mockTaskKey1: TaskKey
|
||||
@Mock lateinit var mockTaskKey2: TaskKey
|
||||
@Captor lateinit var callbackCaptor: ArgumentCaptor<Consumer<Array<Task>>>
|
||||
|
||||
private lateinit var spyAppPairsController: AppPairsController
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
MockitoAnnotations.initMocks(this)
|
||||
appPairsController =
|
||||
AppPairsController(context, splitSelectStateController, statsLogManager)
|
||||
|
||||
// Stub methods on appPairsController so that they return mocks
|
||||
spyAppPairsController = spy(appPairsController)
|
||||
whenever(mockAppPairIcon.context).thenReturn(mockTaskbarActivityContext)
|
||||
whenever(spyAppPairsController.getTopTaskTracker(mockTaskbarActivityContext))
|
||||
.thenReturn(mockTopTaskTracker)
|
||||
whenever(mockTopTaskTracker.getCachedTopTask(any())).thenReturn(mockCachedTaskInfo)
|
||||
whenever(mockTask1.getKey()).thenReturn(mockTaskKey1)
|
||||
whenever(mockTask2.getKey()).thenReturn(mockTaskKey2)
|
||||
doNothing().whenever(spyAppPairsController).launchAppPair(any())
|
||||
doNothing()
|
||||
.whenever(spyAppPairsController)
|
||||
.launchToSide(anyOrNull(), anyOrNull(), anyOrNull(), anyOrNull())
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -144,4 +190,220 @@ class AppPairsControllerTest {
|
||||
AppPairsController.convertRankToSnapPosition(right70),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun handleAppPairLaunchInApp_shouldDoNothingWhenAppsAreAlreadyRunning() {
|
||||
// Test launching apps 1 and 2 from app pair
|
||||
whenever(mockTaskKey1.getId()).thenReturn(1)
|
||||
whenever(mockTaskKey2.getId()).thenReturn(2)
|
||||
// ... with apps 1 and 2 already on screen
|
||||
whenever(mockTopTaskTracker.runningSplitTaskIds).thenReturn(arrayListOf(1, 2).toIntArray())
|
||||
|
||||
// Trigger app pair launch, capture and run callback from findLastActiveTasksAndRunCallback
|
||||
spyAppPairsController.handleAppPairLaunchInApp(
|
||||
mockAppPairIcon,
|
||||
listOf(mockItemInfo1, mockItemInfo2)
|
||||
)
|
||||
verify(splitSelectStateController)
|
||||
.findLastActiveTasksAndRunCallback(any(), any(), callbackCaptor.capture())
|
||||
val callback: Consumer<Array<Task>> = callbackCaptor.value
|
||||
callback.accept(arrayOf(mockTask1, mockTask2))
|
||||
|
||||
// Verify that launchAppPair and launchToSide were never called
|
||||
verify(spyAppPairsController, never()).launchAppPair(any())
|
||||
verify(spyAppPairsController, never())
|
||||
.launchToSide(anyOrNull(), anyOrNull(), anyOrNull(), anyOrNull())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun handleAppPairLaunchInApp_shouldLaunchApp2ToRightWhenApp1IsOnLeft() {
|
||||
// Test launching apps 1 and 2 from app pair
|
||||
whenever(mockTaskKey1.getId()).thenReturn(1)
|
||||
whenever(mockTaskKey2.getId()).thenReturn(2)
|
||||
// ... with apps 1 and 3 already on screen
|
||||
whenever(mockTopTaskTracker.runningSplitTaskIds).thenReturn(arrayListOf(1, 3).toIntArray())
|
||||
|
||||
// Trigger app pair launch, capture and run callback from findLastActiveTasksAndRunCallback
|
||||
spyAppPairsController.handleAppPairLaunchInApp(
|
||||
mockAppPairIcon,
|
||||
listOf(mockItemInfo1, mockItemInfo2)
|
||||
)
|
||||
verify(splitSelectStateController)
|
||||
.findLastActiveTasksAndRunCallback(any(), any(), callbackCaptor.capture())
|
||||
val callback: Consumer<Array<Task>> = callbackCaptor.value
|
||||
callback.accept(arrayOf(mockTask1, mockTask2))
|
||||
|
||||
// Verify that launchToSide was called with the correct arguments
|
||||
verify(spyAppPairsController, never()).launchAppPair(any())
|
||||
verify(spyAppPairsController, times(1))
|
||||
.launchToSide(anyOrNull(), anyOrNull(), anyOrNull(), eq(STAGE_POSITION_BOTTOM_OR_RIGHT))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun handleAppPairLaunchInApp_shouldLaunchApp2ToLeftWhenApp1IsOnRight() {
|
||||
// Test launching apps 1 and 2 from app pair
|
||||
whenever(mockTaskKey1.getId()).thenReturn(1)
|
||||
whenever(mockTaskKey2.getId()).thenReturn(2)
|
||||
// ... with apps 3 and 1 already on screen
|
||||
whenever(mockTopTaskTracker.runningSplitTaskIds).thenReturn(arrayListOf(3, 1).toIntArray())
|
||||
|
||||
// Trigger app pair launch, capture and run callback from findLastActiveTasksAndRunCallback
|
||||
spyAppPairsController.handleAppPairLaunchInApp(
|
||||
mockAppPairIcon,
|
||||
listOf(mockItemInfo1, mockItemInfo2)
|
||||
)
|
||||
verify(splitSelectStateController)
|
||||
.findLastActiveTasksAndRunCallback(any(), any(), callbackCaptor.capture())
|
||||
val callback: Consumer<Array<Task>> = callbackCaptor.value
|
||||
callback.accept(arrayOf(mockTask1, mockTask2))
|
||||
|
||||
// Verify that launchToSide was called with the correct arguments
|
||||
verify(spyAppPairsController, never()).launchAppPair(any())
|
||||
verify(spyAppPairsController, times(1))
|
||||
.launchToSide(anyOrNull(), anyOrNull(), anyOrNull(), eq(STAGE_POSITION_TOP_OR_LEFT))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun handleAppPairLaunchInApp_shouldLaunchApp1ToRightWhenApp2IsOnLeft() {
|
||||
// Test launching apps 1 and 2 from app pair
|
||||
whenever(mockTaskKey1.getId()).thenReturn(1)
|
||||
whenever(mockTaskKey2.getId()).thenReturn(2)
|
||||
// ... with apps 2 and 3 already on screen
|
||||
whenever(mockTopTaskTracker.runningSplitTaskIds).thenReturn(arrayListOf(2, 3).toIntArray())
|
||||
|
||||
// Trigger app pair launch, capture and run callback from findLastActiveTasksAndRunCallback
|
||||
spyAppPairsController.handleAppPairLaunchInApp(
|
||||
mockAppPairIcon,
|
||||
listOf(mockItemInfo1, mockItemInfo2)
|
||||
)
|
||||
verify(splitSelectStateController)
|
||||
.findLastActiveTasksAndRunCallback(any(), any(), callbackCaptor.capture())
|
||||
val callback: Consumer<Array<Task>> = callbackCaptor.value
|
||||
callback.accept(arrayOf(mockTask1, mockTask2))
|
||||
|
||||
// Verify that launchToSide was called with the correct arguments
|
||||
verify(spyAppPairsController, never()).launchAppPair(any())
|
||||
verify(spyAppPairsController, times(1))
|
||||
.launchToSide(anyOrNull(), anyOrNull(), anyOrNull(), eq(STAGE_POSITION_BOTTOM_OR_RIGHT))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun handleAppPairLaunchInApp_shouldLaunchApp1ToLeftWhenApp2IsOnRight() {
|
||||
// Test launching apps 1 and 2 from app pair
|
||||
whenever(mockTaskKey1.getId()).thenReturn(1)
|
||||
whenever(mockTaskKey2.getId()).thenReturn(2)
|
||||
// ... with apps 3 and 2 already on screen
|
||||
whenever(mockTopTaskTracker.runningSplitTaskIds).thenReturn(arrayListOf(3, 2).toIntArray())
|
||||
|
||||
// Trigger app pair launch, capture and run callback from findLastActiveTasksAndRunCallback
|
||||
spyAppPairsController.handleAppPairLaunchInApp(
|
||||
mockAppPairIcon,
|
||||
listOf(mockItemInfo1, mockItemInfo2)
|
||||
)
|
||||
verify(splitSelectStateController)
|
||||
.findLastActiveTasksAndRunCallback(any(), any(), callbackCaptor.capture())
|
||||
val callback: Consumer<Array<Task>> = callbackCaptor.value
|
||||
callback.accept(arrayOf(mockTask1, mockTask2))
|
||||
|
||||
// Verify that launchToSide was called with the correct arguments
|
||||
verify(spyAppPairsController, never()).launchAppPair(any())
|
||||
verify(spyAppPairsController, times(1))
|
||||
.launchToSide(anyOrNull(), anyOrNull(), anyOrNull(), eq(STAGE_POSITION_TOP_OR_LEFT))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun handleAppPairLaunchInApp_shouldLaunchAppPairNormallyWhenUnrelatedPairIsOnScreen() {
|
||||
// Test launching apps 1 and 2 from app pair
|
||||
whenever(mockTaskKey1.getId()).thenReturn(1)
|
||||
whenever(mockTaskKey2.getId()).thenReturn(2)
|
||||
// ... with apps 3 and 4 already on screen
|
||||
whenever(mockTopTaskTracker.runningSplitTaskIds).thenReturn(arrayListOf(3, 4).toIntArray())
|
||||
|
||||
// Trigger app pair launch, capture and run callback from findLastActiveTasksAndRunCallback
|
||||
spyAppPairsController.handleAppPairLaunchInApp(
|
||||
mockAppPairIcon,
|
||||
listOf(mockItemInfo1, mockItemInfo2)
|
||||
)
|
||||
verify(splitSelectStateController)
|
||||
.findLastActiveTasksAndRunCallback(any(), any(), callbackCaptor.capture())
|
||||
val callback: Consumer<Array<Task>> = callbackCaptor.value
|
||||
callback.accept(arrayOf(mockTask1, mockTask2))
|
||||
|
||||
// Verify that launchAppPair was called
|
||||
verify(spyAppPairsController, times(1)).launchAppPair(any())
|
||||
verify(spyAppPairsController, never())
|
||||
.launchToSide(anyOrNull(), anyOrNull(), anyOrNull(), anyOrNull())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun handleAppPairLaunchInApp_shouldLaunchApp2ToRightWhenApp1IsFullscreen() {
|
||||
/// Test launching apps 1 and 2 from app pair
|
||||
whenever(mockTaskKey1.getId()).thenReturn(1)
|
||||
whenever(mockTaskKey2.getId()).thenReturn(2)
|
||||
// ... with app 1 already on screen
|
||||
whenever(mockCachedTaskInfo.taskId).thenReturn(1)
|
||||
|
||||
// Trigger app pair launch, capture and run callback from findLastActiveTasksAndRunCallback
|
||||
spyAppPairsController.handleAppPairLaunchInApp(
|
||||
mockAppPairIcon,
|
||||
listOf(mockItemInfo1, mockItemInfo2)
|
||||
)
|
||||
verify(splitSelectStateController)
|
||||
.findLastActiveTasksAndRunCallback(any(), any(), callbackCaptor.capture())
|
||||
val callback: Consumer<Array<Task>> = callbackCaptor.value
|
||||
callback.accept(arrayOf(mockTask1, mockTask2))
|
||||
|
||||
// Verify that launchToSide was called with the correct arguments
|
||||
verify(spyAppPairsController, never()).launchAppPair(any())
|
||||
verify(spyAppPairsController, times(1))
|
||||
.launchToSide(anyOrNull(), anyOrNull(), anyOrNull(), eq(STAGE_POSITION_BOTTOM_OR_RIGHT))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun handleAppPairLaunchInApp_shouldLaunchApp1ToLeftWhenApp2IsFullscreen() {
|
||||
/// Test launching apps 1 and 2 from app pair
|
||||
whenever(mockTaskKey1.getId()).thenReturn(1)
|
||||
whenever(mockTaskKey2.getId()).thenReturn(2)
|
||||
// ... with app 2 already on screen
|
||||
whenever(mockCachedTaskInfo.taskId).thenReturn(2)
|
||||
|
||||
// Trigger app pair launch, capture and run callback from findLastActiveTasksAndRunCallback
|
||||
spyAppPairsController.handleAppPairLaunchInApp(
|
||||
mockAppPairIcon,
|
||||
listOf(mockItemInfo1, mockItemInfo2)
|
||||
)
|
||||
verify(splitSelectStateController)
|
||||
.findLastActiveTasksAndRunCallback(any(), any(), callbackCaptor.capture())
|
||||
val callback: Consumer<Array<Task>> = callbackCaptor.value
|
||||
callback.accept(arrayOf(mockTask1, mockTask2))
|
||||
|
||||
// Verify that launchToSide was called with the correct arguments
|
||||
verify(spyAppPairsController, never()).launchAppPair(any())
|
||||
verify(spyAppPairsController, times(1))
|
||||
.launchToSide(anyOrNull(), anyOrNull(), anyOrNull(), eq(STAGE_POSITION_TOP_OR_LEFT))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun handleAppPairLaunchInApp_shouldLaunchAppPairNormallyWhenUnrelatedSingleAppIsFullscreen() {
|
||||
// Test launching apps 1 and 2 from app pair
|
||||
whenever(mockTaskKey1.getId()).thenReturn(1)
|
||||
whenever(mockTaskKey2.getId()).thenReturn(2)
|
||||
// ... with app 3 already on screen
|
||||
whenever(mockCachedTaskInfo.taskId).thenReturn(3)
|
||||
|
||||
// Trigger app pair launch, capture and run callback from findLastActiveTasksAndRunCallback
|
||||
spyAppPairsController.handleAppPairLaunchInApp(
|
||||
mockAppPairIcon,
|
||||
listOf(mockItemInfo1, mockItemInfo2)
|
||||
)
|
||||
verify(splitSelectStateController)
|
||||
.findLastActiveTasksAndRunCallback(any(), any(), callbackCaptor.capture())
|
||||
val callback: Consumer<Array<Task>> = callbackCaptor.value
|
||||
callback.accept(arrayOf(mockTask1, mockTask2))
|
||||
|
||||
// Verify that launchAppPair was called
|
||||
verify(spyAppPairsController, times(1)).launchAppPair(any())
|
||||
verify(spyAppPairsController, never())
|
||||
.launchToSide(anyOrNull(), anyOrNull(), anyOrNull(), anyOrNull())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 */,
|
||||
@@ -298,8 +298,7 @@ class SplitAnimationControllerTest {
|
||||
{} /* finishCallback */
|
||||
)
|
||||
|
||||
verify(spySplitAnimationController)
|
||||
.composeFadeInSplitLaunchAnimator(any(), any(), any(), any(), any())
|
||||
verify(spySplitAnimationController).composeScaleUpLaunchAnimation(any(), any(), any())
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
Reference in New Issue
Block a user