From 513a9ffcbb547adc84c6904dc1bf7003ffa7b812 Mon Sep 17 00:00:00 2001 From: Chris Li Date: Mon, 22 Aug 2022 14:31:54 +0800 Subject: [PATCH] Handle TaskFragmentOrganizer#onTransactionReady in sync Refactor TaskFragmentOrganizer#onTransactionReady to SplitController#onTransactionReady to make sure that the transaction is handled in one sync block. Bug: 240519866 Test: pass existing for refactor Change-Id: I9e7191e2b71ba262e1a3f8c947f9354698382972 --- .../android/window/TaskFragmentOrganizer.java | 28 +- .../JetpackTaskFragmentOrganizer.java | 59 +-- .../extensions/embedding/SplitController.java | 459 +++++++++++------- .../JetpackTaskFragmentOrganizerTest.java | 9 + .../embedding/SplitControllerTest.java | 101 ++++ .../embedding/SplitPresenterTest.java | 12 +- .../TaskFragmentOrganizerControllerTest.java | 199 ++++++-- 7 files changed, 582 insertions(+), 285 deletions(-) diff --git a/core/java/android/window/TaskFragmentOrganizer.java b/core/java/android/window/TaskFragmentOrganizer.java index 7b6139fbcc2fa..19b1374ae767d 100644 --- a/core/java/android/window/TaskFragmentOrganizer.java +++ b/core/java/android/window/TaskFragmentOrganizer.java @@ -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 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: diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/JetpackTaskFragmentOrganizer.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/JetpackTaskFragmentOrganizer.java index 1335e5ea051f5..febd7917dff9f 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/JetpackTaskFragmentOrganizer.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/JetpackTaskFragmentOrganizer.java @@ -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); } } diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java index 0597809f8e36f..02af9160301c5 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java @@ -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 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 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 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); } } diff --git a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/JetpackTaskFragmentOrganizerTest.java b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/JetpackTaskFragmentOrganizerTest.java index 21cf7a6272a7c..d0eaf34274aad 100644 --- a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/JetpackTaskFragmentOrganizerTest.java +++ b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/JetpackTaskFragmentOrganizerTest.java @@ -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 */, diff --git a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitControllerTest.java b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitControllerTest.java index 07758d24ad94e..f7436108d3e3b 100644 --- a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitControllerTest.java +++ b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitControllerTest.java @@ -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); diff --git a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitPresenterTest.java b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitPresenterTest.java index 3fdf8e5d4c4dd..da724d9d9311d 100644 --- a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitPresenterTest.java +++ b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitPresenterTest.java @@ -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() { diff --git a/services/tests/wmtests/src/com/android/server/wm/TaskFragmentOrganizerControllerTest.java b/services/tests/wmtests/src/com/android/server/wm/TaskFragmentOrganizerControllerTest.java index 90ac5880506e6..24cdc0fb329fc 100644 --- a/services/tests/wmtests/src/com/android/server/wm/TaskFragmentOrganizerControllerTest.java +++ b/services/tests/wmtests/src/com/android/server/wm/TaskFragmentOrganizerControllerTest.java @@ -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 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 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 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 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 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 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 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 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 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 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);