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:
Jeremy Sim
2024-02-06 16:23:04 +08:00
parent 6474a3b455
commit cf9132e07c
5 changed files with 587 additions and 19 deletions
@@ -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