Merge "Hook up split-launch to legacy transition system" into sc-v2-dev

This commit is contained in:
Evan Rosky
2021-07-08 18:10:40 +00:00
committed by Android (Google) Code Review
11 changed files with 272 additions and 24 deletions

View File

@@ -336,4 +336,12 @@ interface IActivityTaskManager {
* TODO(188595497): Remove this once navbar attachment is in shell.
*/
void detachNavigationBarFromApp(in IBinder transition);
/**
* Marks a process as a delegate for the currently playing remote transition animation. This
* must be called from a process that is already a remote transition player or delegate. Any
* marked delegates are cleaned-up automatically at the end of the transition.
* @param caller is the IApplicationThread representing the calling process.
*/
void setRunningRemoteTransitionDelegate(in IApplicationThread caller);
}

View File

@@ -17,6 +17,7 @@
package android.view;
import android.app.ActivityOptions;
import android.app.IApplicationThread;
import android.compat.annotation.UnsupportedAppUsage;
import android.os.Build;
import android.os.Parcel;
@@ -58,6 +59,9 @@ public class RemoteAnimationAdapter implements Parcelable {
private int mCallingPid;
private int mCallingUid;
/** @see #getCallingApplication */
private IApplicationThread mCallingApplication;
/**
* @param runner The interface that gets notified when we actually need to start the animation.
* @param duration The duration of the animation.
@@ -81,11 +85,19 @@ public class RemoteAnimationAdapter implements Parcelable {
this(runner, duration, statusBarTransitionDelay, false /* changeNeedsSnapshot */);
}
@UnsupportedAppUsage
public RemoteAnimationAdapter(IRemoteAnimationRunner runner, long duration,
long statusBarTransitionDelay, IApplicationThread callingApplication) {
this(runner, duration, statusBarTransitionDelay, false /* changeNeedsSnapshot */);
mCallingApplication = callingApplication;
}
public RemoteAnimationAdapter(Parcel in) {
mRunner = IRemoteAnimationRunner.Stub.asInterface(in.readStrongBinder());
mDuration = in.readLong();
mStatusBarTransitionDelay = in.readLong();
mChangeNeedsSnapshot = in.readBoolean();
mCallingApplication = IApplicationThread.Stub.asInterface(in.readStrongBinder());
}
public IRemoteAnimationRunner getRunner() {
@@ -126,6 +138,15 @@ public class RemoteAnimationAdapter implements Parcelable {
return mCallingUid;
}
/**
* Gets the ApplicationThread that will run the animation. Instead it is intended to pass the
* calling information among client processes (eg. shell + launcher) through one-way binder
* calls (where binder itself doesn't track calling information).
*/
public IApplicationThread getCallingApplication() {
return mCallingApplication;
}
@Override
public int describeContents() {
return 0;
@@ -137,6 +158,7 @@ public class RemoteAnimationAdapter implements Parcelable {
dest.writeLong(mDuration);
dest.writeLong(mStatusBarTransitionDelay);
dest.writeBoolean(mChangeNeedsSnapshot);
dest.writeStrongInterface(mCallingApplication);
}
public static final @android.annotation.NonNull Creator<RemoteAnimationAdapter> CREATOR

View File

@@ -20,6 +20,8 @@ import android.app.PendingIntent;
import android.content.Intent;
import android.os.Bundle;
import android.os.UserHandle;
import android.view.RemoteAnimationAdapter;
import android.view.RemoteAnimationTarget;
import android.window.IRemoteTransition;
import com.android.wm.shell.splitscreen.ISplitScreenListener;
@@ -77,9 +79,15 @@ interface ISplitScreen {
int position, in Bundle options) = 9;
/**
* Starts tasks simultaneously in one transition. The first task in the list will be in the
* main-stage and on the left/top.
* Starts tasks simultaneously in one transition.
*/
oneway void startTasks(int mainTaskId, in Bundle mainOptions, int sideTaskId,
in Bundle sideOptions, int sidePosition, in IRemoteTransition remoteTransition) = 10;
/**
* Version of startTasks using legacy transition system.
*/
oneway void startTasksWithLegacyTransition(int mainTaskId, in Bundle mainOptions,
int sideTaskId, in Bundle sideOptions, int sidePosition,
in RemoteAnimationAdapter adapter) = 11;
}

View File

@@ -39,6 +39,7 @@ import android.os.IBinder;
import android.os.RemoteException;
import android.os.UserHandle;
import android.util.Slog;
import android.view.RemoteAnimationAdapter;
import android.window.IRemoteTransition;
import androidx.annotation.BinderThread;
@@ -427,6 +428,16 @@ public class SplitScreenController implements DragAndDropPolicy.Starter,
});
}
@Override
public void startTasksWithLegacyTransition(int mainTaskId, @Nullable Bundle mainOptions,
int sideTaskId, @Nullable Bundle sideOptions, @SplitPosition int sidePosition,
RemoteAnimationAdapter adapter) {
executeRemoteCallWithTaskPermission(mController, "startTasks",
(controller) -> controller.mStageCoordinator.startTasksWithLegacyTransition(
mainTaskId, mainOptions, sideTaskId, sideOptions, sidePosition,
adapter));
}
@Override
public void startTasks(int mainTaskId, @Nullable Bundle mainOptions,
int sideTaskId, @Nullable Bundle sideOptions,

View File

@@ -20,6 +20,7 @@ import static android.app.ActivityOptions.KEY_LAUNCH_ROOT_TASK_TOKEN;
import static android.app.WindowConfiguration.ACTIVITY_TYPE_HOME;
import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW;
import static android.view.WindowManager.LayoutParams.TYPE_DOCK_DIVIDER;
import static android.view.WindowManager.TRANSIT_OPEN;
import static android.view.WindowManager.TRANSIT_TO_BACK;
import static android.view.WindowManager.TRANSIT_TO_FRONT;
@@ -42,12 +43,21 @@ import static com.android.wm.shell.transition.Transitions.isOpeningType;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.app.ActivityManager;
import android.app.ActivityOptions;
import android.app.ActivityTaskManager;
import android.app.WindowConfiguration;
import android.content.Context;
import android.graphics.Rect;
import android.hardware.devicestate.DeviceStateManager;
import android.os.Bundle;
import android.os.IBinder;
import android.os.RemoteException;
import android.util.Log;
import android.util.Slog;
import android.view.IRemoteAnimationFinishedCallback;
import android.view.IRemoteAnimationRunner;
import android.view.RemoteAnimationAdapter;
import android.view.RemoteAnimationTarget;
import android.view.SurfaceControl;
import android.view.SurfaceSession;
import android.view.WindowManager;
@@ -248,6 +258,75 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler,
TRANSIT_SPLIT_SCREEN_PAIR_OPEN, wct, remoteTransition, this);
}
/** Starts 2 tasks in one legacy transition. */
void startTasksWithLegacyTransition(int mainTaskId, @Nullable Bundle mainOptions,
int sideTaskId, @Nullable Bundle sideOptions, @SplitPosition int sidePosition,
RemoteAnimationAdapter adapter) {
final WindowContainerTransaction wct = new WindowContainerTransaction();
// Need to add another wrapper here in shell so that we can inject the divider bar
// and also manage the process elevation via setRunningRemote
IRemoteAnimationRunner wrapper = new IRemoteAnimationRunner.Stub() {
@Override
public void onAnimationStart(@WindowManager.TransitionOldType int transit,
RemoteAnimationTarget[] apps,
RemoteAnimationTarget[] wallpapers,
RemoteAnimationTarget[] nonApps,
final IRemoteAnimationFinishedCallback finishedCallback) {
RemoteAnimationTarget[] augmentedNonApps =
new RemoteAnimationTarget[nonApps.length + 1];
for (int i = 0; i < nonApps.length; ++i) {
augmentedNonApps[i] = nonApps[i];
}
augmentedNonApps[augmentedNonApps.length - 1] = getDividerBarLegacyTarget();
try {
ActivityTaskManager.getService().setRunningRemoteTransitionDelegate(
adapter.getCallingApplication());
adapter.getRunner().onAnimationStart(transit, apps, wallpapers, nonApps,
finishedCallback);
} catch (RemoteException e) {
Slog.e(TAG, "Error starting remote animation", e);
}
}
@Override
public void onAnimationCancelled() {
try {
adapter.getRunner().onAnimationCancelled();
} catch (RemoteException e) {
Slog.e(TAG, "Error starting remote animation", e);
}
}
};
RemoteAnimationAdapter wrappedAdapter = new RemoteAnimationAdapter(
wrapper, adapter.getDuration(), adapter.getStatusBarTransitionDelay());
if (mainOptions == null) {
mainOptions = ActivityOptions.makeRemoteAnimation(wrappedAdapter).toBundle();
} else {
ActivityOptions mainActivityOptions = ActivityOptions.fromBundle(mainOptions);
mainActivityOptions.update(ActivityOptions.makeRemoteAnimation(wrappedAdapter));
}
sideOptions = sideOptions != null ? sideOptions : new Bundle();
setSideStagePosition(sidePosition);
// Build a request WCT that will launch both apps such that task 0 is on the main stage
// while task 1 is on the side stage.
mMainStage.activate(getMainStageBounds(), wct);
mSideStage.setBounds(getSideStageBounds(), wct);
// Make sure the launch options will put tasks in the corresponding split roots
addActivityOptions(mainOptions, mMainStage);
addActivityOptions(sideOptions, mSideStage);
// Add task launch requests
wct.startTask(mainTaskId, mainOptions);
wct.startTask(sideTaskId, sideOptions);
// Using legacy transitions, so we can't use blast sync since it conflicts.
mTaskOrganizer.applyTransaction(wct);
}
@SplitLayout.SplitPosition
int getSideStagePosition() {
return mSideStagePosition;
@@ -891,6 +970,16 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler,
}
}
private RemoteAnimationTarget getDividerBarLegacyTarget() {
final Rect bounds = mSplitLayout.getDividerBounds();
return new RemoteAnimationTarget(-1 /* taskId */, -1 /* mode */,
mSplitLayout.getDividerLeash(), false /* isTranslucent */, null /* clipRect */,
null /* contentInsets */, Integer.MAX_VALUE /* prefixOrderIndex */,
new android.graphics.Point(0, 0) /* position */, bounds, bounds,
new WindowConfiguration(), true, null /* startLeash */, null /* startBounds */,
null /* taskInfo */, TYPE_DOCK_DIVIDER);
}
@Override
public void dump(@NonNull PrintWriter pw, String prefix) {
final String innerPrefix = prefix + " ";

View File

@@ -69,7 +69,8 @@ public class RemoteAnimationAdapterCompat {
return mRemoteTransition;
}
private static IRemoteAnimationRunner.Stub wrapRemoteAnimationRunner(
/** Wraps a RemoteAnimationRunnerCompat in an IRemoteAnimationRunner. */
public static IRemoteAnimationRunner.Stub wrapRemoteAnimationRunner(
final RemoteAnimationRunnerCompat remoteAnimationAdapter) {
return new IRemoteAnimationRunner.Stub() {
@Override

View File

@@ -5069,6 +5069,34 @@ public class ActivityTaskManagerService extends IActivityTaskManager.Stub {
process.registerDisplayAreaConfigurationListener(imeContainer);
}
@Override
public void setRunningRemoteTransitionDelegate(IApplicationThread caller) {
mAmInternal.enforceCallingPermission(CONTROL_REMOTE_APP_TRANSITION_ANIMATIONS,
"setRunningRemoteTransition");
final int callingPid = Binder.getCallingPid();
final int callingUid = Binder.getCallingUid();
synchronized (mGlobalLock) {
// Also only allow a process which is already runningRemoteAnimation to mark another
// process.
final WindowProcessController callingProc = getProcessController(callingPid,
callingUid);
if (callingProc == null || !callingProc.isRunningRemoteTransition()) {
final String msg = "Can't call setRunningRemoteTransition from a process (pid="
+ callingPid + " uid=" + callingUid + ") which isn't itself running a "
+ "remote transition.";
Slog.e(TAG, msg);
throw new SecurityException(msg);
}
final WindowProcessController wpc = getProcessController(caller);
if (wpc == null) {
Slog.w(TAG, "Unable to find process for application " + caller);
return;
}
wpc.setRunningRemoteAnimation(true /* running */);
callingProc.addRemoteAnimationDelegate(wpc);
}
}
final class H extends Handler {
static final int REPORT_TIME_TRACKER_MSG = 1;
static final int UPDATE_PROCESS_ANIMATING_STATE = 2;

View File

@@ -77,6 +77,18 @@ public class SafeActivityOptions {
: null;
}
/**
* Constructs a new instance from a bundle and provided pid/uid.
*
* @param bOptions The {@link ActivityOptions} as {@link Bundle}.
*/
static SafeActivityOptions fromBundle(Bundle bOptions, int callingPid, int callingUid) {
return bOptions != null
? new SafeActivityOptions(ActivityOptions.fromBundle(bOptions),
callingPid, callingUid)
: null;
}
/**
* Constructs a new instance and records {@link Binder#getCallingPid}/
* {@link Binder#getCallingUid}. Thus, calling identity MUST NOT be cleared when constructing
@@ -90,6 +102,17 @@ public class SafeActivityOptions {
mOriginalOptions = options;
}
/**
* Constructs a new instance.
*
* @param options The options to wrap.
*/
private SafeActivityOptions(@Nullable ActivityOptions options, int callingPid, int callingUid) {
mOriginalCallingPid = callingPid;
mOriginalCallingUid = callingUid;
mOriginalOptions = options;
}
/**
* Overrides options with options from a caller and records {@link Binder#getCallingPid}/
* {@link Binder#getCallingUid}. Thus, calling identity MUST NOT be cleared when calling this

View File

@@ -16,6 +16,7 @@
package com.android.server.wm;
import static android.Manifest.permission.START_TASKS_FROM_RECENTS;
import static android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_CHILDREN_TASKS_REPARENT;
import static android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_CREATE_TASK_FRAGMENT;
import static android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_DELETE_TASK_FRAGMENT;
@@ -142,10 +143,11 @@ class WindowOrganizerController extends IWindowOrganizerController.Stub
if (t == null) {
throw new IllegalArgumentException("Null transaction passed to applySyncTransaction");
}
final CallerInfo caller = new CallerInfo();
final long ident = Binder.clearCallingIdentity();
try {
synchronized (mGlobalLock) {
applyTransaction(t, -1 /*syncId*/, null /*transition*/);
applyTransaction(t, -1 /*syncId*/, null /*transition*/, caller);
}
} finally {
Binder.restoreCallingIdentity(ident);
@@ -159,6 +161,7 @@ class WindowOrganizerController extends IWindowOrganizerController.Stub
if (t == null) {
throw new IllegalArgumentException("Null transaction passed to applySyncTransaction");
}
final CallerInfo caller = new CallerInfo();
final long ident = Binder.clearCallingIdentity();
try {
synchronized (mGlobalLock) {
@@ -178,7 +181,7 @@ class WindowOrganizerController extends IWindowOrganizerController.Stub
if (callback != null) {
syncId = startSyncWithOrganizer(callback);
}
applyTransaction(t, syncId, null /*transition*/);
applyTransaction(t, syncId, null /*transition*/, caller);
if (syncId >= 0) {
setSyncReady(syncId);
}
@@ -193,6 +196,7 @@ class WindowOrganizerController extends IWindowOrganizerController.Stub
public IBinder startTransition(int type, @Nullable IBinder transitionToken,
@Nullable WindowContainerTransaction t) {
enforceTaskPermission("startTransition()");
final CallerInfo caller = new CallerInfo();
final long ident = Binder.clearCallingIdentity();
try {
synchronized (mGlobalLock) {
@@ -212,7 +216,7 @@ class WindowOrganizerController extends IWindowOrganizerController.Stub
throw new IllegalArgumentException("Can't use legacy transitions in"
+ " compatibility mode with no WCT.");
}
applyTransaction(t, -1 /* syncId */, null);
applyTransaction(t, -1 /* syncId */, null, caller);
return null;
}
transition = mTransitionController.createTransition(type);
@@ -221,7 +225,7 @@ class WindowOrganizerController extends IWindowOrganizerController.Stub
if (t == null) {
t = new WindowContainerTransaction();
}
applyTransaction(t, -1 /*syncId*/, transition);
applyTransaction(t, -1 /*syncId*/, transition, caller);
if (needsSetReady) {
transition.setReady();
}
@@ -237,6 +241,7 @@ class WindowOrganizerController extends IWindowOrganizerController.Stub
@Nullable WindowContainerTransaction t,
@Nullable IWindowContainerTransactionCallback callback) {
enforceTaskPermission("finishTransition()");
final CallerInfo caller = new CallerInfo();
final long ident = Binder.clearCallingIdentity();
try {
synchronized (mGlobalLock) {
@@ -247,7 +252,7 @@ class WindowOrganizerController extends IWindowOrganizerController.Stub
// apply the incoming transaction before finish in case it alters the visibility
// of the participants.
if (t != null) {
applyTransaction(t, syncId, null /*transition*/);
applyTransaction(t, syncId, null /*transition*/, caller);
}
getTransitionController().finishTransition(transitionToken);
if (syncId >= 0) {
@@ -263,9 +268,10 @@ class WindowOrganizerController extends IWindowOrganizerController.Stub
/**
* @param syncId If non-null, this will be a sync-transaction.
* @param transition A transition to collect changes into.
* @param caller Info about the calling process.
*/
private void applyTransaction(@NonNull WindowContainerTransaction t, int syncId,
@Nullable Transition transition) {
@Nullable Transition transition, @Nullable CallerInfo caller) {
int effects = 0;
ProtoLog.v(WM_DEBUG_WINDOW_ORGANIZER, "Apply window transaction, syncId=%d", syncId);
mService.deferWindowLayout();
@@ -319,7 +325,7 @@ class WindowOrganizerController extends IWindowOrganizerController.Stub
final boolean isInLockTaskMode = mService.isInLockTaskMode();
for (int i = 0; i < hopSize; ++i) {
effects |= applyHierarchyOp(hops.get(i), effects, syncId, transition,
isInLockTaskMode);
isInLockTaskMode, caller);
}
}
// Queue-up bounds-change transactions for tasks which are now organized. Do
@@ -474,7 +480,8 @@ class WindowOrganizerController extends IWindowOrganizerController.Stub
}
private int applyHierarchyOp(WindowContainerTransaction.HierarchyOp hop, int effects,
int syncId, @Nullable Transition transition, boolean isInLockTaskMode) {
int syncId, @Nullable Transition transition, boolean isInLockTaskMode,
@Nullable CallerInfo caller) {
final int type = hop.getType();
switch (type) {
case HIERARCHY_OP_TYPE_SET_LAUNCH_ROOT: {
@@ -555,11 +562,17 @@ class WindowOrganizerController extends IWindowOrganizerController.Stub
effects |= sanitizeAndApplyHierarchyOp(wc, hop);
break;
case HIERARCHY_OP_TYPE_LAUNCH_TASK:
mService.mAmInternal.enforceCallingPermission(START_TASKS_FROM_RECENTS,
"launchTask HierarchyOp");
final Bundle launchOpts = hop.getLaunchOptions();
final int taskId = launchOpts.getInt(
WindowContainerTransaction.HierarchyOp.LAUNCH_KEY_TASK_ID);
launchOpts.remove(WindowContainerTransaction.HierarchyOp.LAUNCH_KEY_TASK_ID);
mService.startActivityFromRecents(taskId, launchOpts);
final SafeActivityOptions safeOptions = caller != null
? SafeActivityOptions.fromBundle(launchOpts, caller.mPid, caller.mUid)
: SafeActivityOptions.fromBundle(launchOpts);
mService.mTaskSupervisor.startActivityFromRecents(caller.mPid, caller.mUid,
taskId, safeOptions);
break;
case HIERARCHY_OP_TYPE_CREATE_TASK_FRAGMENT:
final TaskFragmentCreationParams taskFragmentCreationOptions =
@@ -936,4 +949,14 @@ class WindowOrganizerController extends IWindowOrganizerController.Stub
mLaunchTaskFragments.removeAt(index);
taskFragment.removeImmediately();
}
static class CallerInfo {
final int mPid;
final int mUid;
CallerInfo() {
mPid = Binder.getCallingPid();
mUid = Binder.getCallingUid();
}
}
}

View File

@@ -75,6 +75,7 @@ import com.android.server.wm.ActivityTaskManagerService.HotPath;
import java.io.IOException;
import java.io.PrintWriter;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.List;
@@ -219,6 +220,10 @@ public class WindowProcessController extends ConfigurationContainer<Configuratio
/** Whether our process is currently running a {@link IRemoteAnimationRunner} */
private boolean mRunningRemoteAnimation;
/** List of "chained" processes that are running remote animations for this process */
private final ArrayList<WeakReference<WindowProcessController>> mRemoteAnimationDelegates =
new ArrayList<>();
// The bits used for mActivityStateFlags.
private static final int ACTIVITY_STATE_FLAG_IS_VISIBLE = 1 << 16;
private static final int ACTIVITY_STATE_FLAG_IS_PAUSING_OR_PAUSED = 1 << 17;
@@ -1596,11 +1601,38 @@ public class WindowProcessController extends ConfigurationContainer<Configuratio
updateRunningRemoteOrRecentsAnimation();
}
/**
* Marks another process as a "delegate" animator. This means that process is doing some part
* of a remote animation on behalf of this process.
*/
void addRemoteAnimationDelegate(WindowProcessController delegate) {
if (!isRunningRemoteTransition()) {
throw new IllegalStateException("Can't add a delegate to a process which isn't itself"
+ " running a remote animation");
}
mRemoteAnimationDelegates.add(new WeakReference<>(delegate));
}
void updateRunningRemoteOrRecentsAnimation() {
if (!isRunningRemoteTransition()) {
// Clean-up any delegates
for (int i = 0; i < mRemoteAnimationDelegates.size(); ++i) {
final WindowProcessController delegate = mRemoteAnimationDelegates.get(i).get();
if (delegate == null) continue;
delegate.setRunningRemoteAnimation(false);
delegate.setRunningRecentsAnimation(false);
}
mRemoteAnimationDelegates.clear();
}
// Posting on handler so WM lock isn't held when we call into AM.
mAtm.mH.sendMessage(PooledLambda.obtainMessage(
WindowProcessListener::setRunningRemoteAnimation, mListener,
mRunningRecentsAnimation || mRunningRemoteAnimation));
isRunningRemoteTransition()));
}
boolean isRunningRemoteTransition() {
return mRunningRecentsAnimation || mRunningRemoteAnimation;
}
/** Adjusts scheduling group for animation. This method MUST NOT be called inside WM lock. */

View File

@@ -61,6 +61,7 @@ import static org.mockito.Mockito.clearInvocations;
import android.app.ActivityManager;
import android.app.ActivityManager.RunningTaskInfo;
import android.app.ActivityOptions;
import android.app.ActivityTaskManager.RootTaskInfo;
import android.app.IRequestFinishCallback;
import android.app.PictureInPictureParams;
@@ -69,7 +70,6 @@ import android.content.pm.ParceledListSlice;
import android.content.res.Configuration;
import android.graphics.Rect;
import android.os.Binder;
import android.os.Bundle;
import android.os.IBinder;
import android.os.RemoteException;
import android.platform.test.annotations.Presubmit;
@@ -1255,21 +1255,24 @@ public class WindowOrganizerTests extends WindowTestsBase {
@Test
public void testStartTasksInTransaction() {
WindowContainerTransaction wct = new WindowContainerTransaction();
Bundle testOptions = new Bundle();
testOptions.putInt("test", 20);
ActivityOptions testOptions = ActivityOptions.makeBasic();
testOptions.setTransientLaunch();
wct.startTask(1, null /* options */);
wct.startTask(2, testOptions);
spyOn(mWm.mAtmService);
doReturn(START_CANCELED).when(mWm.mAtmService).startActivityFromRecents(anyInt(), any());
wct.startTask(2, testOptions.toBundle());
spyOn(mWm.mAtmService.mTaskSupervisor);
doReturn(START_CANCELED).when(mWm.mAtmService.mTaskSupervisor).startActivityFromRecents(
anyInt(), anyInt(), anyInt(), any());
clearInvocations(mWm.mAtmService);
mWm.mAtmService.mWindowOrganizerController.applyTransaction(wct);
final ArgumentCaptor<Bundle> bundleCaptor = ArgumentCaptor.forClass(Bundle.class);
verify(mWm.mAtmService, times(1)).startActivityFromRecents(eq(1), bundleCaptor.capture());
assertTrue(bundleCaptor.getValue().isEmpty());
verify(mWm.mAtmService.mTaskSupervisor, times(1)).startActivityFromRecents(
anyInt(), anyInt(), eq(1), any());
verify(mWm.mAtmService, times(1)).startActivityFromRecents(eq(2), bundleCaptor.capture());
assertEquals(20, bundleCaptor.getValue().getInt("test"));
final ArgumentCaptor<SafeActivityOptions> optionsCaptor =
ArgumentCaptor.forClass(SafeActivityOptions.class);
verify(mWm.mAtmService.mTaskSupervisor, times(1)).startActivityFromRecents(
anyInt(), anyInt(), eq(2), optionsCaptor.capture());
assertTrue(optionsCaptor.getValue().getOriginalOptions().getTransientLaunch());
}
/**