Merge "Handle TaskFragmentOrganizer#onTransactionReady in sync" into tm-qpr-dev

This commit is contained in:
Chris Li
2022-08-27 10:15:16 +00:00
committed by Android (Google) Code Review
7 changed files with 582 additions and 285 deletions

View File

@@ -47,11 +47,24 @@ import java.util.concurrent.Executor;
public class TaskFragmentOrganizer extends WindowOrganizer {
/**
* Key to the exception in {@link Bundle} in {@link ITaskFragmentOrganizer#onTaskFragmentError}.
* Key to the {@link Throwable} in {@link TaskFragmentTransaction.Change#getErrorBundle()}.
* @hide
*/
private static final String KEY_ERROR_CALLBACK_EXCEPTION = "fragment_exception";
private static final String KEY_ERROR_CALLBACK_TASK_FRAGMENT_INFO = "task_fragment_info";
private static final String KEY_ERROR_CALLBACK_OP_TYPE = "operation_type";
public static final String KEY_ERROR_CALLBACK_THROWABLE = "fragment_throwable";
/**
* Key to the {@link TaskFragmentInfo} in
* {@link TaskFragmentTransaction.Change#getErrorBundle()}.
* @hide
*/
public static final String KEY_ERROR_CALLBACK_TASK_FRAGMENT_INFO = "task_fragment_info";
/**
* Key to the {@link WindowContainerTransaction.HierarchyOp} in
* {@link TaskFragmentTransaction.Change#getErrorBundle()}.
* @hide
*/
public static final String KEY_ERROR_CALLBACK_OP_TYPE = "operation_type";
/**
* Creates a {@link Bundle} with an exception, operation type and TaskFragmentInfo (if any)
@@ -61,7 +74,7 @@ public class TaskFragmentOrganizer extends WindowOrganizer {
public static @NonNull Bundle putErrorInfoInBundle(@NonNull Throwable exception,
@Nullable TaskFragmentInfo info, int opType) {
final Bundle errorBundle = new Bundle();
errorBundle.putSerializable(KEY_ERROR_CALLBACK_EXCEPTION, exception);
errorBundle.putSerializable(KEY_ERROR_CALLBACK_THROWABLE, exception);
if (info != null) {
errorBundle.putParcelable(KEY_ERROR_CALLBACK_TASK_FRAGMENT_INFO, info);
}
@@ -342,6 +355,9 @@ public class TaskFragmentOrganizer extends WindowOrganizer {
* @hide
*/
public void onTransactionReady(@NonNull TaskFragmentTransaction transaction) {
// TODO(b/240519866): move to SplitController#onTransactionReady to make sure the whole
// transaction is handled in one sync block. Keep the implementation below to keep CTS
// compatibility. Remove in the next release.
final WindowContainerTransaction wct = new WindowContainerTransaction();
final List<TaskFragmentTransaction.Change> changes = transaction.getChanges();
for (TaskFragmentTransaction.Change change : changes) {
@@ -391,7 +407,7 @@ public class TaskFragmentOrganizer extends WindowOrganizer {
errorBundle.getParcelable(
KEY_ERROR_CALLBACK_TASK_FRAGMENT_INFO, TaskFragmentInfo.class),
errorBundle.getInt(KEY_ERROR_CALLBACK_OP_TYPE),
errorBundle.getSerializable(KEY_ERROR_CALLBACK_EXCEPTION,
errorBundle.getSerializable(KEY_ERROR_CALLBACK_THROWABLE,
java.lang.Throwable.class));
break;
case TYPE_ACTIVITY_REPARENTED_TO_TASK:

View File

@@ -21,7 +21,6 @@ import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED;
import android.app.Activity;
import android.app.WindowConfiguration.WindowingMode;
import android.content.Intent;
import android.content.res.Configuration;
import android.graphics.Rect;
import android.os.Bundle;
import android.os.IBinder;
@@ -29,6 +28,7 @@ import android.util.ArrayMap;
import android.window.TaskFragmentCreationParams;
import android.window.TaskFragmentInfo;
import android.window.TaskFragmentOrganizer;
import android.window.TaskFragmentTransaction;
import android.window.WindowContainerTransaction;
import androidx.annotation.NonNull;
@@ -62,18 +62,7 @@ class JetpackTaskFragmentOrganizer extends TaskFragmentOrganizer {
* Callback that notifies the controller about changes to task fragments.
*/
interface TaskFragmentCallback {
void onTaskFragmentAppeared(@NonNull WindowContainerTransaction wct,
@NonNull TaskFragmentInfo taskFragmentInfo);
void onTaskFragmentInfoChanged(@NonNull WindowContainerTransaction wct,
@NonNull TaskFragmentInfo taskFragmentInfo);
void onTaskFragmentVanished(@NonNull WindowContainerTransaction wct,
@NonNull TaskFragmentInfo taskFragmentInfo);
void onTaskFragmentParentInfoChanged(@NonNull WindowContainerTransaction wct,
int taskId, @NonNull Configuration parentConfig);
void onActivityReparentedToTask(@NonNull WindowContainerTransaction wct,
int taskId, @NonNull Intent activityIntent, @NonNull IBinder activityToken);
void onTaskFragmentError(@NonNull WindowContainerTransaction wct,
@Nullable TaskFragmentInfo taskFragmentInfo, int opType);
void onTransactionReady(@NonNull TaskFragmentTransaction transaction);
}
/**
@@ -270,50 +259,16 @@ class JetpackTaskFragmentOrganizer extends TaskFragmentOrganizer {
wct.deleteTaskFragment(mFragmentInfos.get(fragmentToken).getToken());
}
@Override
public void onTaskFragmentAppeared(@NonNull WindowContainerTransaction wct,
@NonNull TaskFragmentInfo taskFragmentInfo) {
final IBinder fragmentToken = taskFragmentInfo.getFragmentToken();
mFragmentInfos.put(fragmentToken, taskFragmentInfo);
mCallback.onTaskFragmentAppeared(wct, taskFragmentInfo);
void updateTaskFragmentInfo(@NonNull TaskFragmentInfo taskFragmentInfo) {
mFragmentInfos.put(taskFragmentInfo.getFragmentToken(), taskFragmentInfo);
}
@Override
public void onTaskFragmentInfoChanged(@NonNull WindowContainerTransaction wct,
@NonNull TaskFragmentInfo taskFragmentInfo) {
final IBinder fragmentToken = taskFragmentInfo.getFragmentToken();
mFragmentInfos.put(fragmentToken, taskFragmentInfo);
mCallback.onTaskFragmentInfoChanged(wct, taskFragmentInfo);
}
@Override
public void onTaskFragmentVanished(@NonNull WindowContainerTransaction wct,
@NonNull TaskFragmentInfo taskFragmentInfo) {
void removeTaskFragmentInfo(@NonNull TaskFragmentInfo taskFragmentInfo) {
mFragmentInfos.remove(taskFragmentInfo.getFragmentToken());
mCallback.onTaskFragmentVanished(wct, taskFragmentInfo);
}
@Override
public void onTaskFragmentParentInfoChanged(@NonNull WindowContainerTransaction wct,
int taskId, @NonNull Configuration parentConfig) {
mCallback.onTaskFragmentParentInfoChanged(wct, taskId, parentConfig);
}
@Override
public void onActivityReparentedToTask(@NonNull WindowContainerTransaction wct,
int taskId, @NonNull Intent activityIntent, @NonNull IBinder activityToken) {
mCallback.onActivityReparentedToTask(wct, taskId, activityIntent, activityToken);
}
@Override
public void onTaskFragmentError(@NonNull WindowContainerTransaction wct,
@NonNull IBinder errorCallbackToken,
@Nullable TaskFragmentInfo taskFragmentInfo,
int opType, @NonNull Throwable exception) {
if (taskFragmentInfo != null) {
final IBinder fragmentToken = taskFragmentInfo.getFragmentToken();
mFragmentInfos.put(fragmentToken, taskFragmentInfo);
}
mCallback.onTaskFragmentError(wct, taskFragmentInfo, opType);
public void onTransactionReady(@NonNull TaskFragmentTransaction transaction) {
mCallback.onTransactionReady(transaction);
}
}

View File

@@ -19,6 +19,15 @@ package androidx.window.extensions.embedding;
import static android.app.ActivityManager.START_SUCCESS;
import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED;
import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED;
import static android.window.TaskFragmentOrganizer.KEY_ERROR_CALLBACK_OP_TYPE;
import static android.window.TaskFragmentOrganizer.KEY_ERROR_CALLBACK_TASK_FRAGMENT_INFO;
import static android.window.TaskFragmentOrganizer.KEY_ERROR_CALLBACK_THROWABLE;
import static android.window.TaskFragmentTransaction.TYPE_ACTIVITY_REPARENTED_TO_TASK;
import static android.window.TaskFragmentTransaction.TYPE_TASK_FRAGMENT_APPEARED;
import static android.window.TaskFragmentTransaction.TYPE_TASK_FRAGMENT_ERROR;
import static android.window.TaskFragmentTransaction.TYPE_TASK_FRAGMENT_INFO_CHANGED;
import static android.window.TaskFragmentTransaction.TYPE_TASK_FRAGMENT_PARENT_INFO_CHANGED;
import static android.window.TaskFragmentTransaction.TYPE_TASK_FRAGMENT_VANISHED;
import static android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_REPARENT_ACTIVITY_TO_TASK_FRAGMENT;
import static android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_START_ACTIVITY_IN_TASK_FRAGMENT;
@@ -53,6 +62,7 @@ import android.util.Pair;
import android.util.Size;
import android.util.SparseArray;
import android.window.TaskFragmentInfo;
import android.window.TaskFragmentTransaction;
import android.window.WindowContainerTransaction;
import androidx.annotation.GuardedBy;
@@ -144,203 +154,312 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen
}
}
/**
* Called when the transaction is ready so that the organizer can update the TaskFragments based
* on the changes in transaction.
*/
@Override
public void onTaskFragmentAppeared(@NonNull WindowContainerTransaction wct,
@NonNull TaskFragmentInfo taskFragmentInfo) {
public void onTransactionReady(@NonNull TaskFragmentTransaction transaction) {
synchronized (mLock) {
TaskFragmentContainer container = getContainer(taskFragmentInfo.getFragmentToken());
if (container == null) {
return;
final WindowContainerTransaction wct = new WindowContainerTransaction();
final List<TaskFragmentTransaction.Change> changes = transaction.getChanges();
for (TaskFragmentTransaction.Change change : changes) {
final int taskId = change.getTaskId();
final TaskFragmentInfo info = change.getTaskFragmentInfo();
switch (change.getType()) {
case TYPE_TASK_FRAGMENT_APPEARED:
mPresenter.updateTaskFragmentInfo(info);
onTaskFragmentAppeared(wct, info);
break;
case TYPE_TASK_FRAGMENT_INFO_CHANGED:
mPresenter.updateTaskFragmentInfo(info);
onTaskFragmentInfoChanged(wct, info);
break;
case TYPE_TASK_FRAGMENT_VANISHED:
mPresenter.removeTaskFragmentInfo(info);
onTaskFragmentVanished(wct, info);
break;
case TYPE_TASK_FRAGMENT_PARENT_INFO_CHANGED:
onTaskFragmentParentInfoChanged(wct, taskId, change.getTaskConfiguration());
break;
case TYPE_TASK_FRAGMENT_ERROR:
final Bundle errorBundle = change.getErrorBundle();
final IBinder errorToken = change.getErrorCallbackToken();
final TaskFragmentInfo errorTaskFragmentInfo = errorBundle.getParcelable(
KEY_ERROR_CALLBACK_TASK_FRAGMENT_INFO, TaskFragmentInfo.class);
final int opType = errorBundle.getInt(KEY_ERROR_CALLBACK_OP_TYPE);
final Throwable exception = errorBundle.getSerializable(
KEY_ERROR_CALLBACK_THROWABLE, Throwable.class);
if (errorTaskFragmentInfo != null) {
mPresenter.updateTaskFragmentInfo(errorTaskFragmentInfo);
}
onTaskFragmentError(wct, errorToken, errorTaskFragmentInfo, opType,
exception);
break;
case TYPE_ACTIVITY_REPARENTED_TO_TASK:
onActivityReparentedToTask(
wct,
taskId,
change.getActivityIntent(),
change.getActivityToken());
break;
default:
throw new IllegalArgumentException(
"Unknown TaskFragmentEvent=" + change.getType());
}
}
container.setInfo(wct, taskFragmentInfo);
if (container.isFinished()) {
mPresenter.cleanupContainer(wct, container, false /* shouldFinishDependent */);
} else {
// Update with the latest Task configuration.
updateContainer(wct, container);
}
// Notify the server, and the server should apply the WindowContainerTransaction.
mPresenter.onTransactionHandled(transaction.getTransactionToken(), wct);
updateCallbackIfNecessary();
}
}
@Override
public void onTaskFragmentInfoChanged(@NonNull WindowContainerTransaction wct,
/**
* Called when a TaskFragment is created and organized by this organizer.
*
* @param wct The {@link WindowContainerTransaction} to make any changes with if needed.
* @param taskFragmentInfo Info of the TaskFragment that is created.
*/
@VisibleForTesting
@GuardedBy("mLock")
void onTaskFragmentAppeared(@NonNull WindowContainerTransaction wct,
@NonNull TaskFragmentInfo taskFragmentInfo) {
synchronized (mLock) {
TaskFragmentContainer container = getContainer(taskFragmentInfo.getFragmentToken());
if (container == null) {
return;
}
final TaskFragmentContainer container = getContainer(taskFragmentInfo.getFragmentToken());
if (container == null) {
return;
}
final boolean wasInPip = isInPictureInPicture(container);
container.setInfo(wct, taskFragmentInfo);
final boolean isInPip = isInPictureInPicture(container);
// Check if there are no running activities - consider the container empty if there are
// no non-finishing activities left.
if (!taskFragmentInfo.hasRunningActivity()) {
if (taskFragmentInfo.isTaskFragmentClearedForPip()) {
// Do not finish the dependents if the last activity is reparented to PiP.
// Instead, the original split should be cleanup, and the dependent may be
// expanded to fullscreen.
cleanupForEnterPip(wct, container);
mPresenter.cleanupContainer(wct, container, false /* shouldFinishDependent */);
} else if (taskFragmentInfo.isTaskClearedForReuse()) {
// Do not finish the dependents if this TaskFragment was cleared due to
// launching activity in the Task.
mPresenter.cleanupContainer(wct, container, false /* shouldFinishDependent */);
} else if (!container.isWaitingActivityAppear()) {
// Do not finish the container before the expected activity appear until
// timeout.
mPresenter.cleanupContainer(wct, container, true /* shouldFinishDependent */);
}
} else if (wasInPip && isInPip) {
// No update until exit PIP.
return;
} else if (isInPip) {
// Enter PIP.
// All overrides will be cleanup.
container.setLastRequestedBounds(null /* bounds */);
container.setLastRequestedWindowingMode(WINDOWING_MODE_UNDEFINED);
container.setInfo(wct, taskFragmentInfo);
if (container.isFinished()) {
mPresenter.cleanupContainer(wct, container, false /* shouldFinishDependent */);
} else {
// Update with the latest Task configuration.
updateContainer(wct, container);
}
}
/**
* Called when the status of an organized TaskFragment is changed.
*
* @param wct The {@link WindowContainerTransaction} to make any changes with if needed.
* @param taskFragmentInfo Info of the TaskFragment that is changed.
*/
@VisibleForTesting
@GuardedBy("mLock")
void onTaskFragmentInfoChanged(@NonNull WindowContainerTransaction wct,
@NonNull TaskFragmentInfo taskFragmentInfo) {
final TaskFragmentContainer container = getContainer(taskFragmentInfo.getFragmentToken());
if (container == null) {
return;
}
final boolean wasInPip = isInPictureInPicture(container);
container.setInfo(wct, taskFragmentInfo);
final boolean isInPip = isInPictureInPicture(container);
// Check if there are no running activities - consider the container empty if there are
// no non-finishing activities left.
if (!taskFragmentInfo.hasRunningActivity()) {
if (taskFragmentInfo.isTaskFragmentClearedForPip()) {
// Do not finish the dependents if the last activity is reparented to PiP.
// Instead, the original split should be cleanup, and the dependent may be
// expanded to fullscreen.
cleanupForEnterPip(wct, container);
} else if (wasInPip) {
// Exit PIP.
// Updates the presentation of the container. Expand or launch placeholder if
// needed.
mPresenter.cleanupContainer(wct, container, false /* shouldFinishDependent */);
} else if (taskFragmentInfo.isTaskClearedForReuse()) {
// Do not finish the dependents if this TaskFragment was cleared due to
// launching activity in the Task.
mPresenter.cleanupContainer(wct, container, false /* shouldFinishDependent */);
} else if (!container.isWaitingActivityAppear()) {
// Do not finish the container before the expected activity appear until
// timeout.
mPresenter.cleanupContainer(wct, container, true /* shouldFinishDependent */);
}
} else if (wasInPip && isInPip) {
// No update until exit PIP.
return;
} else if (isInPip) {
// Enter PIP.
// All overrides will be cleanup.
container.setLastRequestedBounds(null /* bounds */);
container.setLastRequestedWindowingMode(WINDOWING_MODE_UNDEFINED);
cleanupForEnterPip(wct, container);
} else if (wasInPip) {
// Exit PIP.
// Updates the presentation of the container. Expand or launch placeholder if
// needed.
updateContainer(wct, container);
}
}
/**
* Called when an organized TaskFragment is removed.
*
* @param wct The {@link WindowContainerTransaction} to make any changes with if needed.
* @param taskFragmentInfo Info of the TaskFragment that is removed.
*/
@VisibleForTesting
@GuardedBy("mLock")
void onTaskFragmentVanished(@NonNull WindowContainerTransaction wct,
@NonNull TaskFragmentInfo taskFragmentInfo) {
final TaskFragmentContainer container = getContainer(taskFragmentInfo.getFragmentToken());
if (container != null) {
// Cleanup if the TaskFragment vanished is not requested by the organizer.
removeContainer(container);
// Make sure the top container is updated.
final TaskFragmentContainer newTopContainer = getTopActiveContainer(
container.getTaskId());
if (newTopContainer != null) {
updateContainer(wct, newTopContainer);
}
}
cleanupTaskFragment(taskFragmentInfo.getFragmentToken());
}
/**
* Called when the parent leaf Task of organized TaskFragments is changed.
* When the leaf Task is changed, the organizer may want to update the TaskFragments in one
* transaction.
*
* For case like screen size change, it will trigger {@link #onTaskFragmentParentInfoChanged}
* with new Task bounds, but may not trigger {@link #onTaskFragmentInfoChanged} because there
* can be an override bounds.
*
* @param wct The {@link WindowContainerTransaction} to make any changes with if needed.
* @param taskId Id of the parent Task that is changed.
* @param parentConfig Config of the parent Task.
*/
@VisibleForTesting
@GuardedBy("mLock")
void onTaskFragmentParentInfoChanged(@NonNull WindowContainerTransaction wct,
int taskId, @NonNull Configuration parentConfig) {
onTaskConfigurationChanged(taskId, parentConfig);
if (isInPictureInPicture(parentConfig)) {
// No need to update presentation in PIP until the Task exit PIP.
return;
}
final TaskContainer taskContainer = getTaskContainer(taskId);
if (taskContainer == null || taskContainer.isEmpty()) {
Log.e(TAG, "onTaskFragmentParentInfoChanged on empty Task id=" + taskId);
return;
}
// Update all TaskFragments in the Task. Make a copy of the list since some may be
// removed on updating.
final List<TaskFragmentContainer> containers =
new ArrayList<>(taskContainer.mContainers);
for (int i = containers.size() - 1; i >= 0; i--) {
final TaskFragmentContainer container = containers.get(i);
// Wait until onTaskFragmentAppeared to update new container.
if (!container.isFinished() && !container.isWaitingActivityAppear()) {
updateContainer(wct, container);
}
updateCallbackIfNecessary();
}
}
@Override
public void onTaskFragmentVanished(@NonNull WindowContainerTransaction wct,
@NonNull TaskFragmentInfo taskFragmentInfo) {
synchronized (mLock) {
final TaskFragmentContainer container = getContainer(
taskFragmentInfo.getFragmentToken());
if (container != null) {
// Cleanup if the TaskFragment vanished is not requested by the organizer.
removeContainer(container);
// Make sure the top container is updated.
final TaskFragmentContainer newTopContainer = getTopActiveContainer(
container.getTaskId());
if (newTopContainer != null) {
updateContainer(wct, newTopContainer);
}
updateCallbackIfNecessary();
}
cleanupTaskFragment(taskFragmentInfo.getFragmentToken());
}
}
@Override
public void onTaskFragmentParentInfoChanged(@NonNull WindowContainerTransaction wct,
int taskId, @NonNull Configuration parentConfig) {
synchronized (mLock) {
onTaskConfigurationChanged(taskId, parentConfig);
if (isInPictureInPicture(parentConfig)) {
// No need to update presentation in PIP until the Task exit PIP.
return;
}
final TaskContainer taskContainer = getTaskContainer(taskId);
if (taskContainer == null || taskContainer.isEmpty()) {
Log.e(TAG, "onTaskFragmentParentInfoChanged on empty Task id=" + taskId);
return;
}
// Update all TaskFragments in the Task. Make a copy of the list since some may be
// removed on updating.
final List<TaskFragmentContainer> containers =
new ArrayList<>(taskContainer.mContainers);
for (int i = containers.size() - 1; i >= 0; i--) {
final TaskFragmentContainer container = containers.get(i);
// Wait until onTaskFragmentAppeared to update new container.
if (!container.isFinished() && !container.isWaitingActivityAppear()) {
updateContainer(wct, container);
}
}
updateCallbackIfNecessary();
}
}
@Override
public void onActivityReparentedToTask(@NonNull WindowContainerTransaction wct,
/**
* Called when an Activity is reparented to the Task with organized TaskFragment. For example,
* when an Activity enters and then exits Picture-in-picture, it will be reparented back to its
* original Task. In this case, we need to notify the organizer so that it can check if the
* Activity matches any split rule.
*
* @param wct The {@link WindowContainerTransaction} to make any changes with if needed.
* @param taskId The Task that the activity is reparented to.
* @param activityIntent The intent that the activity is original launched with.
* @param activityToken If the activity belongs to the same process as the organizer, this
* will be the actual activity token; if the activity belongs to a
* different process, the server will generate a temporary token that
* the organizer can use to reparent the activity through
* {@link WindowContainerTransaction} if needed.
*/
@VisibleForTesting
@GuardedBy("mLock")
void onActivityReparentedToTask(@NonNull WindowContainerTransaction wct,
int taskId, @NonNull Intent activityIntent,
@NonNull IBinder activityToken) {
synchronized (mLock) {
// If the activity belongs to the current app process, we treat it as a new activity
// launch.
final Activity activity = getActivity(activityToken);
if (activity != null) {
// We don't allow split as primary for new launch because we currently only support
// launching to top. We allow split as primary for activity reparent because the
// activity may be split as primary before it is reparented out. In that case, we
// want to show it as primary again when it is reparented back.
if (!resolveActivityToContainer(wct, activity, true /* isOnReparent */)) {
// When there is no embedding rule matched, try to place it in the top container
// like a normal launch.
placeActivityInTopContainer(wct, activity);
}
updateCallbackIfNecessary();
return;
}
final TaskContainer taskContainer = getTaskContainer(taskId);
if (taskContainer == null || taskContainer.isInPictureInPicture()) {
// We don't embed activity when it is in PIP.
return;
}
// If the activity belongs to a different app process, we treat it as starting new
// intent, since both actions might result in a new activity that should appear in an
// organized TaskFragment.
TaskFragmentContainer targetContainer = resolveStartActivityIntent(wct, taskId,
activityIntent, null /* launchingActivity */);
if (targetContainer == null) {
// If the activity belongs to the current app process, we treat it as a new activity
// launch.
final Activity activity = getActivity(activityToken);
if (activity != null) {
// We don't allow split as primary for new launch because we currently only support
// launching to top. We allow split as primary for activity reparent because the
// activity may be split as primary before it is reparented out. In that case, we
// want to show it as primary again when it is reparented back.
if (!resolveActivityToContainer(wct, activity, true /* isOnReparent */)) {
// When there is no embedding rule matched, try to place it in the top container
// like a normal launch.
targetContainer = taskContainer.getTopTaskFragmentContainer();
placeActivityInTopContainer(wct, activity);
}
if (targetContainer == null) {
return;
}
wct.reparentActivityToTaskFragment(targetContainer.getTaskFragmentToken(),
activityToken);
// Because the activity does not belong to the organizer process, we wait until
// onTaskFragmentAppeared to trigger updateCallbackIfNecessary().
return;
}
final TaskContainer taskContainer = getTaskContainer(taskId);
if (taskContainer == null || taskContainer.isInPictureInPicture()) {
// We don't embed activity when it is in PIP.
return;
}
// If the activity belongs to a different app process, we treat it as starting new
// intent, since both actions might result in a new activity that should appear in an
// organized TaskFragment.
TaskFragmentContainer targetContainer = resolveStartActivityIntent(wct, taskId,
activityIntent, null /* launchingActivity */);
if (targetContainer == null) {
// When there is no embedding rule matched, try to place it in the top container
// like a normal launch.
targetContainer = taskContainer.getTopTaskFragmentContainer();
}
if (targetContainer == null) {
return;
}
wct.reparentActivityToTaskFragment(targetContainer.getTaskFragmentToken(),
activityToken);
// Because the activity does not belong to the organizer process, we wait until
// onTaskFragmentAppeared to trigger updateCallbackIfNecessary().
}
@Override
public void onTaskFragmentError(@NonNull WindowContainerTransaction wct,
@Nullable TaskFragmentInfo taskFragmentInfo, int opType) {
synchronized (mLock) {
switch (opType) {
case HIERARCHY_OP_TYPE_START_ACTIVITY_IN_TASK_FRAGMENT:
case HIERARCHY_OP_TYPE_REPARENT_ACTIVITY_TO_TASK_FRAGMENT: {
final TaskFragmentContainer container;
if (taskFragmentInfo != null) {
container = getContainer(taskFragmentInfo.getFragmentToken());
} else {
container = null;
}
if (container == null) {
break;
}
// Update the latest taskFragmentInfo and perform necessary clean-up
container.setInfo(wct, taskFragmentInfo);
container.clearPendingAppearedActivities();
if (container.isEmpty()) {
mPresenter.cleanupContainer(wct, container,
false /* shouldFinishDependent */);
}
/**
* Called when the {@link WindowContainerTransaction} created with
* {@link WindowContainerTransaction#setErrorCallbackToken(IBinder)} failed on the server side.
*
* @param wct The {@link WindowContainerTransaction} to make any changes with if needed.
* @param errorCallbackToken token set in
* {@link WindowContainerTransaction#setErrorCallbackToken(IBinder)}
* @param taskFragmentInfo The {@link TaskFragmentInfo}. This could be {@code null} if no
* TaskFragment created.
* @param opType The {@link WindowContainerTransaction.HierarchyOp} of the failed
* transaction operation.
* @param exception exception from the server side.
*/
@VisibleForTesting
@GuardedBy("mLock")
void onTaskFragmentError(@NonNull WindowContainerTransaction wct,
@Nullable IBinder errorCallbackToken, @Nullable TaskFragmentInfo taskFragmentInfo,
int opType, @NonNull Throwable exception) {
Log.e(TAG, "onTaskFragmentError=" + exception.getMessage());
switch (opType) {
case HIERARCHY_OP_TYPE_START_ACTIVITY_IN_TASK_FRAGMENT:
case HIERARCHY_OP_TYPE_REPARENT_ACTIVITY_TO_TASK_FRAGMENT: {
final TaskFragmentContainer container;
if (taskFragmentInfo != null) {
container = getContainer(taskFragmentInfo.getFragmentToken());
} else {
container = null;
}
if (container == null) {
break;
}
default:
Log.e(TAG, "onTaskFragmentError: taskFragmentInfo = " + taskFragmentInfo
+ ", opType = " + opType);
// Update the latest taskFragmentInfo and perform necessary clean-up
container.setInfo(wct, taskFragmentInfo);
container.clearPendingAppearedActivities();
if (container.isEmpty()) {
mPresenter.cleanupContainer(wct, container, false /* shouldFinishDependent */);
}
break;
}
default:
Log.e(TAG, "onTaskFragmentError: taskFragmentInfo = " + taskFragmentInfo
+ ", opType = " + opType);
}
}

View File

@@ -36,6 +36,7 @@ import android.graphics.Point;
import android.os.Handler;
import android.platform.test.annotations.Presubmit;
import android.window.TaskFragmentInfo;
import android.window.TaskFragmentTransaction;
import android.window.WindowContainerToken;
import android.window.WindowContainerTransaction;
@@ -129,6 +130,14 @@ public class JetpackTaskFragmentOrganizerTest {
WINDOWING_MODE_UNDEFINED);
}
@Test
public void testOnTransactionReady() {
final TaskFragmentTransaction transaction = new TaskFragmentTransaction();
mOrganizer.onTransactionReady(transaction);
verify(mCallback).onTransactionReady(transaction);
}
private TaskFragmentInfo createMockInfo(TaskFragmentContainer container) {
return new TaskFragmentInfo(container.getTaskFragmentToken(),
mock(WindowContainerToken.class), new Configuration(), 0 /* runningActivityCount */,

View File

@@ -19,6 +19,13 @@ package androidx.window.extensions.embedding;
import static android.app.ActivityManager.START_CANCELED;
import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW;
import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED;
import static android.window.TaskFragmentTransaction.TYPE_ACTIVITY_REPARENTED_TO_TASK;
import static android.window.TaskFragmentTransaction.TYPE_TASK_FRAGMENT_APPEARED;
import static android.window.TaskFragmentTransaction.TYPE_TASK_FRAGMENT_ERROR;
import static android.window.TaskFragmentTransaction.TYPE_TASK_FRAGMENT_INFO_CHANGED;
import static android.window.TaskFragmentTransaction.TYPE_TASK_FRAGMENT_PARENT_INFO_CHANGED;
import static android.window.TaskFragmentTransaction.TYPE_TASK_FRAGMENT_VANISHED;
import static android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_CREATE_TASK_FRAGMENT;
import static androidx.window.extensions.embedding.EmbeddingTestUtils.SPLIT_RATIO;
import static androidx.window.extensions.embedding.EmbeddingTestUtils.TASK_BOUNDS;
@@ -67,6 +74,8 @@ import android.os.Handler;
import android.os.IBinder;
import android.platform.test.annotations.Presubmit;
import android.window.TaskFragmentInfo;
import android.window.TaskFragmentOrganizer;
import android.window.TaskFragmentTransaction;
import android.window.WindowContainerTransaction;
import androidx.test.core.app.ApplicationProvider;
@@ -980,6 +989,98 @@ public class SplitControllerTest {
assertTrue(taskContainer.mSplitContainers.isEmpty());
}
@Test
public void testOnTransactionReady_taskFragmentAppeared() {
final TaskFragmentTransaction transaction = new TaskFragmentTransaction();
final TaskFragmentInfo info = mock(TaskFragmentInfo.class);
transaction.addChange(new TaskFragmentTransaction.Change(TYPE_TASK_FRAGMENT_APPEARED)
.setTaskId(TASK_ID)
.setTaskFragmentToken(new Binder())
.setTaskFragmentInfo(info));
mSplitController.onTransactionReady(transaction);
verify(mSplitController).onTaskFragmentAppeared(any(), eq(info));
verify(mSplitPresenter).onTransactionHandled(eq(transaction.getTransactionToken()), any());
}
@Test
public void testOnTransactionReady_taskFragmentInfoChanged() {
final TaskFragmentTransaction transaction = new TaskFragmentTransaction();
final TaskFragmentInfo info = mock(TaskFragmentInfo.class);
transaction.addChange(new TaskFragmentTransaction.Change(TYPE_TASK_FRAGMENT_INFO_CHANGED)
.setTaskId(TASK_ID)
.setTaskFragmentToken(new Binder())
.setTaskFragmentInfo(info));
mSplitController.onTransactionReady(transaction);
verify(mSplitController).onTaskFragmentInfoChanged(any(), eq(info));
verify(mSplitPresenter).onTransactionHandled(eq(transaction.getTransactionToken()), any());
}
@Test
public void testOnTransactionReady_taskFragmentVanished() {
final TaskFragmentTransaction transaction = new TaskFragmentTransaction();
final TaskFragmentInfo info = mock(TaskFragmentInfo.class);
transaction.addChange(new TaskFragmentTransaction.Change(TYPE_TASK_FRAGMENT_VANISHED)
.setTaskId(TASK_ID)
.setTaskFragmentToken(new Binder())
.setTaskFragmentInfo(info));
mSplitController.onTransactionReady(transaction);
verify(mSplitController).onTaskFragmentVanished(any(), eq(info));
verify(mSplitPresenter).onTransactionHandled(eq(transaction.getTransactionToken()), any());
}
@Test
public void testOnTransactionReady_taskFragmentParentInfoChanged() {
final TaskFragmentTransaction transaction = new TaskFragmentTransaction();
final Configuration taskConfig = new Configuration();
transaction.addChange(new TaskFragmentTransaction.Change(
TYPE_TASK_FRAGMENT_PARENT_INFO_CHANGED)
.setTaskId(TASK_ID)
.setTaskConfiguration(taskConfig));
mSplitController.onTransactionReady(transaction);
verify(mSplitController).onTaskFragmentParentInfoChanged(any(), eq(TASK_ID),
eq(taskConfig));
verify(mSplitPresenter).onTransactionHandled(eq(transaction.getTransactionToken()), any());
}
@Test
public void testOnTransactionReady_taskFragmentParentError() {
final TaskFragmentTransaction transaction = new TaskFragmentTransaction();
final IBinder errorToken = new Binder();
final TaskFragmentInfo info = mock(TaskFragmentInfo.class);
final int opType = HIERARCHY_OP_TYPE_CREATE_TASK_FRAGMENT;
final Exception exception = new SecurityException("test");
final Bundle errorBundle = TaskFragmentOrganizer.putErrorInfoInBundle(exception, info,
opType);
transaction.addChange(new TaskFragmentTransaction.Change(TYPE_TASK_FRAGMENT_ERROR)
.setErrorCallbackToken(errorToken)
.setErrorBundle(errorBundle));
mSplitController.onTransactionReady(transaction);
verify(mSplitController).onTaskFragmentError(any(), eq(errorToken), eq(info), eq(opType),
eq(exception));
verify(mSplitPresenter).onTransactionHandled(eq(transaction.getTransactionToken()), any());
}
@Test
public void testOnTransactionReady_activityReparentedToTask() {
final TaskFragmentTransaction transaction = new TaskFragmentTransaction();
final Intent intent = mock(Intent.class);
final IBinder activityToken = new Binder();
transaction.addChange(new TaskFragmentTransaction.Change(TYPE_ACTIVITY_REPARENTED_TO_TASK)
.setTaskId(TASK_ID)
.setActivityIntent(intent)
.setActivityToken(activityToken));
mSplitController.onTransactionReady(transaction);
verify(mSplitController).onActivityReparentedToTask(any(), eq(TASK_ID), eq(intent),
eq(activityToken));
verify(mSplitPresenter).onTransactionHandled(eq(transaction.getTransactionToken()), any());
}
/** Creates a mock activity in the organizer process. */
private Activity createMockActivity() {
final Activity activity = mock(Activity.class);

View File

@@ -234,10 +234,8 @@ public class SplitPresenterTest {
assertEquals(RESULT_EXPANDED, mPresenter.expandSplitContainerIfNeeded(mTransaction,
splitContainer, mActivity, secondaryActivity, null /* secondaryIntent */));
verify(mPresenter).expandTaskFragment(eq(mTransaction),
eq(primaryTf.getTaskFragmentToken()));
verify(mPresenter).expandTaskFragment(eq(mTransaction),
eq(secondaryTf.getTaskFragmentToken()));
verify(mPresenter).expandTaskFragment(mTransaction, primaryTf.getTaskFragmentToken());
verify(mPresenter).expandTaskFragment(mTransaction, secondaryTf.getTaskFragmentToken());
clearInvocations(mPresenter);
@@ -245,10 +243,8 @@ public class SplitPresenterTest {
splitContainer, mActivity, null /* secondaryActivity */,
new Intent(ApplicationProvider.getApplicationContext(),
MinimumDimensionActivity.class)));
verify(mPresenter).expandTaskFragment(eq(mTransaction),
eq(primaryTf.getTaskFragmentToken()));
verify(mPresenter).expandTaskFragment(eq(mTransaction),
eq(secondaryTf.getTaskFragmentToken()));
verify(mPresenter).expandTaskFragment(mTransaction, primaryTf.getTaskFragmentToken());
verify(mPresenter).expandTaskFragment(mTransaction, secondaryTf.getTaskFragmentToken());
}
private Activity createMockActivity() {

View File

@@ -18,10 +18,19 @@ package com.android.server.wm;
import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD;
import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED;
import static android.window.TaskFragmentOrganizer.KEY_ERROR_CALLBACK_OP_TYPE;
import static android.window.TaskFragmentOrganizer.KEY_ERROR_CALLBACK_THROWABLE;
import static android.window.TaskFragmentTransaction.TYPE_ACTIVITY_REPARENTED_TO_TASK;
import static android.window.TaskFragmentTransaction.TYPE_TASK_FRAGMENT_APPEARED;
import static android.window.TaskFragmentTransaction.TYPE_TASK_FRAGMENT_ERROR;
import static android.window.TaskFragmentTransaction.TYPE_TASK_FRAGMENT_INFO_CHANGED;
import static android.window.TaskFragmentTransaction.TYPE_TASK_FRAGMENT_PARENT_INFO_CHANGED;
import static android.window.TaskFragmentTransaction.TYPE_TASK_FRAGMENT_VANISHED;
import static android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_CREATE_TASK_FRAGMENT;
import static android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_DELETE_TASK_FRAGMENT;
import static android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_REPARENT_ACTIVITY_TO_TASK_FRAGMENT;
import static android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_REPARENT_CHILDREN;
import static android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_SET_ADJACENT_ROOTS;
import static android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_SET_ADJACENT_TASK_FRAGMENTS;
import static android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_START_ACTIVITY_IN_TASK_FRAGMENT;
@@ -47,14 +56,15 @@ import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.clearInvocations;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import android.annotation.NonNull;
import android.content.Intent;
import android.content.pm.ActivityInfo;
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;
@@ -76,9 +86,12 @@ 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 java.util.List;
/**
* Build/Install/Run:
* atest WmTests:TaskFragmentOrganizerControllerTest
@@ -107,6 +120,8 @@ public class TaskFragmentOrganizerControllerTest extends WindowTestsBase {
private TaskFragmentInfo mTaskFragmentInfo;
@Mock
private Task mTask;
@Captor
private ArgumentCaptor<TaskFragmentTransaction> mTransactionCaptor;
@Before
public void setup() throws RemoteException {
@@ -166,7 +181,7 @@ public class TaskFragmentOrganizerControllerTest extends WindowTestsBase {
mController.onTaskFragmentAppeared(mTaskFragment.getTaskFragmentOrganizer(), mTaskFragment);
mController.dispatchPendingEvents();
verify(mOrganizer, never()).onTaskFragmentAppeared(any(), any());
verify(mOrganizer, never()).onTransactionReady(any());
// Send callback when the TaskFragment is attached.
setupMockParent(mTaskFragment, mTask);
@@ -174,7 +189,7 @@ public class TaskFragmentOrganizerControllerTest extends WindowTestsBase {
mController.onTaskFragmentAppeared(mTaskFragment.getTaskFragmentOrganizer(), mTaskFragment);
mController.dispatchPendingEvents();
verify(mOrganizer).onTaskFragmentAppeared(any(), any());
assertTaskFragmentAppearedTransaction();
}
@Test
@@ -187,15 +202,16 @@ public class TaskFragmentOrganizerControllerTest extends WindowTestsBase {
mTaskFragment);
mController.dispatchPendingEvents();
verify(mOrganizer, never()).onTaskFragmentInfoChanged(any(), any());
verify(mOrganizer, never()).onTransactionReady(any());
// Call onTaskFragmentAppeared first.
mController.onTaskFragmentAppeared(mTaskFragment.getTaskFragmentOrganizer(), mTaskFragment);
mController.dispatchPendingEvents();
verify(mOrganizer).onTaskFragmentAppeared(any(), any());
verify(mOrganizer).onTransactionReady(any());
// No callback if the info is not changed.
clearInvocations(mOrganizer);
doReturn(true).when(mTaskFragmentInfo).equalsForTaskFragmentOrganizer(any());
doReturn(new Configuration()).when(mTaskFragmentInfo).getConfiguration();
@@ -203,7 +219,7 @@ public class TaskFragmentOrganizerControllerTest extends WindowTestsBase {
mTaskFragment);
mController.dispatchPendingEvents();
verify(mOrganizer, never()).onTaskFragmentInfoChanged(any(), any());
verify(mOrganizer, never()).onTransactionReady(any());
// Trigger callback if the info is changed.
doReturn(false).when(mTaskFragmentInfo).equalsForTaskFragmentOrganizer(any());
@@ -212,7 +228,7 @@ public class TaskFragmentOrganizerControllerTest extends WindowTestsBase {
mTaskFragment);
mController.dispatchPendingEvents();
verify(mOrganizer).onTaskFragmentInfoChanged(any(), eq(mTaskFragmentInfo));
assertTaskFragmentInfoChangedTransaction();
}
@Test
@@ -223,7 +239,7 @@ public class TaskFragmentOrganizerControllerTest extends WindowTestsBase {
mController.onTaskFragmentVanished(mTaskFragment.getTaskFragmentOrganizer(), mTaskFragment);
mController.dispatchPendingEvents();
verify(mOrganizer).onTaskFragmentVanished(any(), any());
assertTaskFragmentVanishedTransaction();
}
@Test
@@ -236,10 +252,7 @@ public class TaskFragmentOrganizerControllerTest extends WindowTestsBase {
mController.onTaskFragmentVanished(mTaskFragment.getTaskFragmentOrganizer(), mTaskFragment);
mController.dispatchPendingEvents();
verify(mOrganizer, never()).onTaskFragmentAppeared(any(), any());
verify(mOrganizer, never()).onTaskFragmentInfoChanged(any(), any());
verify(mOrganizer, never()).onTaskFragmentParentInfoChanged(any(), anyInt(), any());
verify(mOrganizer).onTaskFragmentVanished(any(), eq(mTaskFragmentInfo));
assertTaskFragmentVanishedTransaction();
// Not trigger onTaskFragmentInfoChanged.
// Call onTaskFragmentAppeared before calling onTaskFragmentInfoChanged.
@@ -252,10 +265,7 @@ public class TaskFragmentOrganizerControllerTest extends WindowTestsBase {
mController.onTaskFragmentVanished(mTaskFragment.getTaskFragmentOrganizer(), mTaskFragment);
mController.dispatchPendingEvents();
verify(mOrganizer, never()).onTaskFragmentAppeared(any(), any());
verify(mOrganizer, never()).onTaskFragmentInfoChanged(any(), any());
verify(mOrganizer, never()).onTaskFragmentParentInfoChanged(any(), anyInt(), any());
verify(mOrganizer).onTaskFragmentVanished(any(), eq(mTaskFragmentInfo));
assertTaskFragmentVanishedTransaction();
}
@Test
@@ -268,24 +278,30 @@ public class TaskFragmentOrganizerControllerTest extends WindowTestsBase {
mTaskFragment.getTaskFragmentOrganizer(), mTaskFragment);
mController.dispatchPendingEvents();
verify(mOrganizer).onTaskFragmentParentInfoChanged(any(), eq(mTask.mTaskId), any());
assertTaskFragmentParentInfoChangedTransaction(mTask);
// No extra callback if the info is not changed.
// No extra parent info changed callback if the info is not changed.
clearInvocations(mOrganizer);
mController.onTaskFragmentInfoChanged(
mTaskFragment.getTaskFragmentOrganizer(), mTaskFragment);
mController.dispatchPendingEvents();
verify(mOrganizer, never()).onTaskFragmentParentInfoChanged(any(), anyInt(), any());
verify(mOrganizer).onTransactionReady(mTransactionCaptor.capture());
final TaskFragmentTransaction transaction = mTransactionCaptor.getValue();
final List<TaskFragmentTransaction.Change> changes = transaction.getChanges();
assertEquals(1, changes.size());
final TaskFragmentTransaction.Change change = changes.get(0);
assertEquals(TYPE_TASK_FRAGMENT_INFO_CHANGED, change.getType());
// Trigger callback if the size is changed.
clearInvocations(mOrganizer);
mTask.getConfiguration().smallestScreenWidthDp = 100;
mController.onTaskFragmentInfoChanged(
mTaskFragment.getTaskFragmentOrganizer(), mTaskFragment);
mController.dispatchPendingEvents();
verify(mOrganizer).onTaskFragmentParentInfoChanged(any(), eq(mTask.mTaskId), any());
assertTaskFragmentParentInfoChangedTransaction(mTask);
// Trigger callback if the windowing mode is changed.
clearInvocations(mOrganizer);
@@ -294,7 +310,7 @@ public class TaskFragmentOrganizerControllerTest extends WindowTestsBase {
mTaskFragment.getTaskFragmentOrganizer(), mTaskFragment);
mController.dispatchPendingEvents();
verify(mOrganizer).onTaskFragmentParentInfoChanged(any(), eq(mTask.mTaskId), any());
assertTaskFragmentParentInfoChangedTransaction(mTask);
}
@Test
@@ -303,11 +319,12 @@ public class TaskFragmentOrganizerControllerTest extends WindowTestsBase {
mController.registerOrganizer(mIOrganizer);
mController.onTaskFragmentError(mTaskFragment.getTaskFragmentOrganizer(),
mErrorToken, null /* taskFragment */, -1 /* opType */, exception);
mErrorToken, null /* taskFragment */, HIERARCHY_OP_TYPE_SET_ADJACENT_ROOTS,
exception);
mController.dispatchPendingEvents();
verify(mOrganizer).onTaskFragmentError(any(), eq(mErrorToken), eq(null), eq(-1),
eq(exception));
assertTaskFragmentErrorTransaction(HIERARCHY_OP_TYPE_SET_ADJACENT_ROOTS,
exception.getClass());
}
@Test
@@ -326,17 +343,17 @@ public class TaskFragmentOrganizerControllerTest extends WindowTestsBase {
mController.onActivityReparentedToTask(activity);
mController.dispatchPendingEvents();
verify(mOrganizer, never()).onActivityReparentedToTask(any(), anyInt(), any(), any());
verify(mOrganizer, never()).onTransactionReady(any());
// Notify organizer if it was embedded before entered Pip.
activity.mLastTaskFragmentOrganizerBeforePip = mIOrganizer;
mController.onActivityReparentedToTask(activity);
mController.dispatchPendingEvents();
verify(mOrganizer).onActivityReparentedToTask(any(), eq(task.mTaskId), eq(activity.intent),
eq(activity.token));
assertActivityReparentedToTaskTransaction(task.mTaskId, activity.intent, activity.token);
// Notify organizer if there is any embedded in the Task.
clearInvocations(mOrganizer);
final TaskFragment taskFragment = new TaskFragmentBuilder(mAtm)
.setParentTask(task)
.setOrganizer(mOrganizer)
@@ -348,9 +365,7 @@ public class TaskFragmentOrganizerControllerTest extends WindowTestsBase {
mController.onActivityReparentedToTask(activity);
mController.dispatchPendingEvents();
verify(mOrganizer, times(2))
.onActivityReparentedToTask(any(), eq(task.mTaskId), eq(activity.intent),
eq(activity.token));
assertActivityReparentedToTaskTransaction(task.mTaskId, activity.intent, activity.token);
}
@Test
@@ -370,25 +385,31 @@ public class TaskFragmentOrganizerControllerTest extends WindowTestsBase {
activity.info.applicationInfo.uid = uid;
doReturn(pid + 1).when(activity).getPid();
task.effectiveUid = uid;
final ArgumentCaptor<IBinder> token = ArgumentCaptor.forClass(IBinder.class);
// Notify organizer if it was embedded before entered Pip.
// Create a temporary token since the activity doesn't belong to the same process.
clearInvocations(mOrganizer);
activity.mLastTaskFragmentOrganizerBeforePip = mIOrganizer;
mController.onActivityReparentedToTask(activity);
mController.dispatchPendingEvents();
// Allow organizer to reparent activity in other process using the temporary token.
verify(mOrganizer).onActivityReparentedToTask(any(), eq(task.mTaskId), eq(activity.intent),
token.capture());
final IBinder temporaryToken = token.getValue();
assertNotEquals(activity.token, temporaryToken);
mTransaction.reparentActivityToTaskFragment(mFragmentToken, temporaryToken);
verify(mOrganizer).onTransactionReady(mTransactionCaptor.capture());
final TaskFragmentTransaction transaction = mTransactionCaptor.getValue();
final List<TaskFragmentTransaction.Change> changes = transaction.getChanges();
assertFalse(changes.isEmpty());
final TaskFragmentTransaction.Change change = changes.get(0);
assertEquals(TYPE_ACTIVITY_REPARENTED_TO_TASK, change.getType());
assertEquals(task.mTaskId, change.getTaskId());
assertEquals(activity.intent, change.getActivityIntent());
assertNotEquals(activity.token, change.getActivityToken());
mTransaction.reparentActivityToTaskFragment(mFragmentToken, change.getActivityToken());
mWindowOrganizerController.applyTransaction(mTransaction);
assertEquals(mTaskFragment, activity.getTaskFragment());
// The temporary token can only be used once.
assertNull(mController.getReparentActivityFromTemporaryToken(mIOrganizer, temporaryToken));
assertNull(mController.getReparentActivityFromTemporaryToken(mIOrganizer,
change.getActivityToken()));
}
@Test
@@ -901,7 +922,7 @@ public class TaskFragmentOrganizerControllerTest extends WindowTestsBase {
mController.dispatchPendingEvents();
// Verifies that event was not sent
verify(mOrganizer, never()).onTaskFragmentInfoChanged(any(), any());
verify(mOrganizer, never()).onTransactionReady(any());
}
@Test
@@ -927,7 +948,7 @@ public class TaskFragmentOrganizerControllerTest extends WindowTestsBase {
mController.dispatchPendingEvents();
// Verifies that event was not sent
verify(mOrganizer, never()).onTaskFragmentInfoChanged(any(), any());
verify(mOrganizer, never()).onTransactionReady(any());
// Mock the task becomes visible, and activity resumed
doReturn(true).when(task).shouldBeVisible(any());
@@ -935,7 +956,7 @@ public class TaskFragmentOrganizerControllerTest extends WindowTestsBase {
// Verifies that event is sent.
mController.dispatchPendingEvents();
verify(mOrganizer).onTaskFragmentInfoChanged(any(), any());
verify(mOrganizer).onTransactionReady(any());
}
/**
@@ -969,7 +990,7 @@ public class TaskFragmentOrganizerControllerTest extends WindowTestsBase {
clearInvocations(mOrganizer);
mController.onTaskFragmentInfoChanged(mIOrganizer, taskFragment);
mController.dispatchPendingEvents();
verify(mOrganizer).onTaskFragmentInfoChanged(any(), any());
verify(mOrganizer).onTransactionReady(any());
}
/**
@@ -996,21 +1017,21 @@ public class TaskFragmentOrganizerControllerTest extends WindowTestsBase {
taskFragment.mTaskFragmentAppearedSent = true;
mController.onTaskFragmentInfoChanged(mIOrganizer, taskFragment);
mController.dispatchPendingEvents();
verify(mOrganizer).onTaskFragmentInfoChanged(any(), any());
verify(mOrganizer).onTransactionReady(any());
// Verify the info changed callback is not called when the task is invisible
clearInvocations(mOrganizer);
doReturn(false).when(task).shouldBeVisible(any());
mController.onTaskFragmentInfoChanged(mIOrganizer, taskFragment);
mController.dispatchPendingEvents();
verify(mOrganizer, never()).onTaskFragmentInfoChanged(any(), any());
verify(mOrganizer, never()).onTransactionReady(any());
// Finish the embedded activity, and verify the info changed callback is called because the
// TaskFragment is becoming empty.
embeddedActivity.finishing = true;
mController.onTaskFragmentInfoChanged(mIOrganizer, taskFragment);
mController.dispatchPendingEvents();
verify(mOrganizer).onTaskFragmentInfoChanged(any(), any());
verify(mOrganizer).onTransactionReady(any());
}
/**
@@ -1120,9 +1141,8 @@ public class TaskFragmentOrganizerControllerTest extends WindowTestsBase {
// The pending event will be dispatched on the handler (from requestTraversal).
waitHandlerIdle(mWm.mAnimationHandler);
verify(mOrganizer).onTaskFragmentError(any(), eq(mErrorToken), any(),
eq(HIERARCHY_OP_TYPE_REPARENT_ACTIVITY_TO_TASK_FRAGMENT),
any(SecurityException.class));
assertTaskFragmentErrorTransaction(HIERARCHY_OP_TYPE_REPARENT_ACTIVITY_TO_TASK_FRAGMENT,
SecurityException.class);
}
@Test
@@ -1159,8 +1179,8 @@ public class TaskFragmentOrganizerControllerTest extends WindowTestsBase {
// The pending event will be dispatched on the handler (from requestTraversal).
waitHandlerIdle(mWm.mAnimationHandler);
verify(mOrganizer).onTaskFragmentError(any(), eq(mErrorToken), any(),
eq(HIERARCHY_OP_TYPE_REPARENT_CHILDREN), any(SecurityException.class));
assertTaskFragmentErrorTransaction(HIERARCHY_OP_TYPE_REPARENT_CHILDREN,
SecurityException.class);
}
@Test
@@ -1264,6 +1284,87 @@ public class TaskFragmentOrganizerControllerTest extends WindowTestsBase {
}
}
/** Asserts that there will be a transaction for TaskFragment appeared. */
private void assertTaskFragmentAppearedTransaction() {
verify(mOrganizer).onTransactionReady(mTransactionCaptor.capture());
final TaskFragmentTransaction transaction = mTransactionCaptor.getValue();
final List<TaskFragmentTransaction.Change> changes = transaction.getChanges();
assertFalse(changes.isEmpty());
// Appeared will come with parent info changed.
final TaskFragmentTransaction.Change change = changes.get(changes.size() - 1);
assertEquals(TYPE_TASK_FRAGMENT_APPEARED, change.getType());
assertEquals(mTaskFragmentInfo, change.getTaskFragmentInfo());
assertEquals(mFragmentToken, change.getTaskFragmentToken());
}
/** Asserts that there will be a transaction for TaskFragment info changed. */
private void assertTaskFragmentInfoChangedTransaction() {
verify(mOrganizer).onTransactionReady(mTransactionCaptor.capture());
final TaskFragmentTransaction transaction = mTransactionCaptor.getValue();
final List<TaskFragmentTransaction.Change> changes = transaction.getChanges();
assertFalse(changes.isEmpty());
// InfoChanged may come with parent info changed.
final TaskFragmentTransaction.Change change = changes.get(changes.size() - 1);
assertEquals(TYPE_TASK_FRAGMENT_INFO_CHANGED, change.getType());
assertEquals(mTaskFragmentInfo, change.getTaskFragmentInfo());
assertEquals(mFragmentToken, change.getTaskFragmentToken());
}
/** Asserts that there will be a transaction for TaskFragment vanished. */
private void assertTaskFragmentVanishedTransaction() {
verify(mOrganizer).onTransactionReady(mTransactionCaptor.capture());
final TaskFragmentTransaction transaction = mTransactionCaptor.getValue();
final List<TaskFragmentTransaction.Change> changes = transaction.getChanges();
assertFalse(changes.isEmpty());
final TaskFragmentTransaction.Change change = changes.get(0);
assertEquals(TYPE_TASK_FRAGMENT_VANISHED, change.getType());
assertEquals(mTaskFragmentInfo, change.getTaskFragmentInfo());
assertEquals(mFragmentToken, change.getTaskFragmentToken());
}
/** Asserts that there will be a transaction for TaskFragment vanished. */
private void assertTaskFragmentParentInfoChangedTransaction(@NonNull Task task) {
verify(mOrganizer).onTransactionReady(mTransactionCaptor.capture());
final TaskFragmentTransaction transaction = mTransactionCaptor.getValue();
final List<TaskFragmentTransaction.Change> changes = transaction.getChanges();
assertFalse(changes.isEmpty());
final TaskFragmentTransaction.Change change = changes.get(0);
assertEquals(TYPE_TASK_FRAGMENT_PARENT_INFO_CHANGED, change.getType());
assertEquals(task.mTaskId, change.getTaskId());
assertEquals(task.getConfiguration(), change.getTaskConfiguration());
}
/** Asserts that there will be a transaction for TaskFragment error. */
private void assertTaskFragmentErrorTransaction(int opType, @NonNull Class<?> exceptionClass) {
verify(mOrganizer).onTransactionReady(mTransactionCaptor.capture());
final TaskFragmentTransaction transaction = mTransactionCaptor.getValue();
final List<TaskFragmentTransaction.Change> changes = transaction.getChanges();
assertFalse(changes.isEmpty());
final TaskFragmentTransaction.Change change = changes.get(0);
assertEquals(TYPE_TASK_FRAGMENT_ERROR, change.getType());
assertEquals(mErrorToken, change.getErrorCallbackToken());
final Bundle errorBundle = change.getErrorBundle();
assertEquals(opType, errorBundle.getInt(KEY_ERROR_CALLBACK_OP_TYPE));
assertEquals(exceptionClass, errorBundle.getSerializable(
KEY_ERROR_CALLBACK_THROWABLE, Throwable.class).getClass());
}
/** Asserts that there will be a transaction for activity reparented to Task. */
private void assertActivityReparentedToTaskTransaction(int taskId, @NonNull Intent intent,
@NonNull IBinder activityToken) {
verify(mOrganizer).onTransactionReady(mTransactionCaptor.capture());
final TaskFragmentTransaction transaction = mTransactionCaptor.getValue();
final List<TaskFragmentTransaction.Change> changes = transaction.getChanges();
assertFalse(changes.isEmpty());
final TaskFragmentTransaction.Change change = changes.get(0);
assertEquals(TYPE_ACTIVITY_REPARENTED_TO_TASK, change.getType());
assertEquals(taskId, change.getTaskId());
assertEquals(intent, change.getActivityIntent());
assertEquals(activityToken, change.getActivityToken());
}
/** Setups an embedded TaskFragment in a PIP Task. */
private void setupTaskFragmentInPip() {
mOrganizer.applyTransaction(mTransaction);