blob: 5981acafb2742983ada7d1d31a5c0201620efcdf [file] [log] [blame]
/*
* Copyright (C) 2021 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package android.server.wm.taskfragment;
import static android.app.ActivityTaskManager.INVALID_STACK_ID;
import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED;
import static android.content.Intent.FLAG_ACTIVITY_NEW_TASK;
import static android.server.wm.WindowManagerState.STATE_RESUMED;
import static android.window.TaskFragmentOrganizer.TASK_FRAGMENT_TRANSIT_OPEN;
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 com.google.common.truth.Truth.assertThat;
import static com.google.common.truth.Truth.assertWithMessage;
import static org.junit.Assert.fail;
import static org.junit.Assume.assumeTrue;
import android.app.Activity;
import android.app.Instrumentation;
import android.content.ComponentName;
import android.content.Intent;
import android.content.res.Configuration;
import android.graphics.Rect;
import android.os.Binder;
import android.os.Bundle;
import android.os.IBinder;
import android.server.wm.WindowContextTestActivity;
import android.server.wm.WindowManagerState.WindowContainer;
import android.server.wm.WindowManagerTestBase;
import android.util.ArrayMap;
import android.util.Log;
import android.window.TaskFragmentCreationParams;
import android.window.TaskFragmentInfo;
import android.window.TaskFragmentOrganizer;
import android.window.TaskFragmentTransaction;
import android.window.WindowContainerTransaction;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.test.InstrumentationRegistry;
import org.junit.After;
import org.junit.Before;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.function.Predicate;
import javax.annotation.concurrent.GuardedBy;
public class TaskFragmentOrganizerTestBase extends WindowManagerTestBase {
private static final String TAG = "TaskFragmentOrganizerTestBase";
public BasicTaskFragmentOrganizer mTaskFragmentOrganizer;
Activity mOwnerActivity;
IBinder mOwnerToken;
ComponentName mOwnerActivityName;
int mOwnerTaskId;
@Before
@Override
public void setUp() throws Exception {
super.setUp();
assumeTrue(supportsMultiWindow());
mTaskFragmentOrganizer = new BasicTaskFragmentOrganizer();
mTaskFragmentOrganizer.registerOrganizer();
mOwnerActivity = setUpOwnerActivity();
mOwnerToken = getActivityToken(mOwnerActivity);
mOwnerActivityName = mOwnerActivity.getComponentName();
mOwnerTaskId = mOwnerActivity.getTaskId();
// Make sure the activity is launched and resumed, otherwise the window state may not be
// stable.
waitAndAssertResumedActivity(mOwnerActivity.getComponentName(),
"The owner activity must be resumed.");
}
/** Setups the owner activity of the organized TaskFragment. */
Activity setUpOwnerActivity() {
// Launch activities in fullscreen in case the device may use freeform as the default
// windowing mode.
return startActivityInWindowingModeFullScreen(WindowContextTestActivity.class);
}
@After
public void tearDown() {
if (mTaskFragmentOrganizer != null) {
mTaskFragmentOrganizer.unregisterOrganizer();
}
}
public static IBinder getActivityToken(@NonNull Activity activity) {
return activity.getWindow().getAttributes().token;
}
public static void assertEmptyTaskFragment(TaskFragmentInfo info,
IBinder expectedTaskFragToken) {
assertTaskFragmentInfoValidity(info, expectedTaskFragToken);
assertWithMessage("TaskFragment must be empty").that(info.isEmpty()).isTrue();
assertWithMessage("TaskFragmentInfo#getActivities must be empty")
.that(info.getActivities()).isEmpty();
assertWithMessage("TaskFragment must not contain any running Activity")
.that(info.hasRunningActivity()).isFalse();
assertWithMessage("TaskFragment must not be visible").that(info.isVisible()).isFalse();
}
public static void assertNotEmptyTaskFragment(TaskFragmentInfo info,
IBinder expectedTaskFragToken, @Nullable IBinder ... expectedActivityTokens) {
assertTaskFragmentInfoValidity(info, expectedTaskFragToken);
assertWithMessage("TaskFragment must not be empty").that(info.isEmpty()).isFalse();
assertWithMessage("TaskFragment must contain running Activity")
.that(info.hasRunningActivity()).isTrue();
if (expectedActivityTokens != null) {
assertWithMessage("TaskFragmentInfo#getActivities must be empty")
.that(info.getActivities()).containsAtLeastElementsIn(expectedActivityTokens);
}
}
private static void assertTaskFragmentInfoValidity(TaskFragmentInfo info,
IBinder expectedTaskFragToken) {
assertWithMessage("TaskFragmentToken must match the token from "
+ "TaskFragmentCreationParams#getFragmentToken")
.that(info.getFragmentToken()).isEqualTo(expectedTaskFragToken);
assertWithMessage("WindowContainerToken must not be null")
.that(info.getToken()).isNotNull();
assertWithMessage("TaskFragmentInfo#getPositionInParent must not be null")
.that(info.getPositionInParent()).isNotNull();
assertWithMessage("Configuration must not be empty")
.that(info.getConfiguration()).isNotEqualTo(new Configuration());
}
/**
* Verifies whether the window hierarchy is as expected or not.
* <p>
* The sample usage is as follows:
* <pre class="prettyprint">
* assertWindowHierarchy(rootTask, leafTask, taskFragment, activity);
* </pre></p>
*
* @param containers The containers to be verified. It should be put from top to down
*/
public static void assertWindowHierarchy(WindowContainer... containers) {
for (int i = 0; i < containers.length - 2; i++) {
final WindowContainer parent = containers[i];
final WindowContainer child = containers[i + 1];
assertWithMessage(parent + " must contains " + child)
.that(parent.getChildren())
.contains(child);
}
}
/**
* Builds, runs and waits for completion of task fragment creation transaction.
* @param componentName name of the activity to launch in the TF, or {@code null} if none.
* @return token of the created task fragment.
*/
TaskFragmentInfo createTaskFragment(@Nullable ComponentName componentName) {
return createTaskFragment(componentName, new Rect());
}
/**
* Same as {@link #createTaskFragment(ComponentName)}, but allows to specify the bounds for the
* new task fragment.
*/
TaskFragmentInfo createTaskFragment(@Nullable ComponentName componentName,
@NonNull Rect relativeBounds) {
return createTaskFragment(componentName, relativeBounds, new WindowContainerTransaction());
}
/**
* Same as {@link #createTaskFragment(ComponentName, Rect)}, but allows to specify the
* {@link WindowContainerTransaction} to use.
*/
TaskFragmentInfo createTaskFragment(@Nullable ComponentName componentName,
@NonNull Rect relativeBounds, @NonNull WindowContainerTransaction wct) {
final TaskFragmentCreationParams params = generateTaskFragCreationParams(relativeBounds);
final IBinder taskFragToken = params.getFragmentToken();
wct.createTaskFragment(params);
if (componentName != null) {
wct.startActivityInTaskFragment(taskFragToken, mOwnerToken,
new Intent().setComponent(componentName), null /* activityOptions */);
}
mTaskFragmentOrganizer.applyTransaction(wct, TASK_FRAGMENT_TRANSIT_OPEN,
false /* shouldApplyIndependently */);
mTaskFragmentOrganizer.waitForTaskFragmentCreated();
if (componentName != null) {
mWmState.waitForActivityState(componentName, STATE_RESUMED);
}
return mTaskFragmentOrganizer.getTaskFragmentInfo(taskFragToken);
}
@NonNull
TaskFragmentCreationParams generateTaskFragCreationParams() {
return mTaskFragmentOrganizer.generateTaskFragParams(mOwnerToken);
}
@NonNull
TaskFragmentCreationParams generateTaskFragCreationParams(@NonNull Rect relativeBounds) {
return mTaskFragmentOrganizer.generateTaskFragParams(mOwnerToken, relativeBounds,
WINDOWING_MODE_UNDEFINED);
}
static Activity startNewActivity() {
return startNewActivity(WindowContextTestActivity.class);
}
static Activity startNewActivity(Class<?> className) {
final Instrumentation instrumentation = InstrumentationRegistry.getInstrumentation();
final Intent intent = new Intent(instrumentation.getTargetContext(), className)
.addFlags(FLAG_ACTIVITY_NEW_TASK);
return instrumentation.startActivitySync(intent);
}
public static class BasicTaskFragmentOrganizer extends TaskFragmentOrganizer {
private final static int WAIT_TIMEOUT_IN_SECOND = 10;
private final Object mLock = new Object();
@GuardedBy("mLock")
private final Map<IBinder, TaskFragmentInfo> mInfos = new ArrayMap<>();
@GuardedBy("mLock")
private final Map<IBinder, TaskFragmentInfo> mRemovedInfos = new ArrayMap<>();
@GuardedBy("mLock")
private int mParentTaskId = INVALID_STACK_ID;
@Nullable
@GuardedBy("mLock")
private Configuration mParentConfig;
@Nullable
@GuardedBy("mLock")
private IBinder mErrorToken;
@Nullable
@GuardedBy("mLock")
private Throwable mThrowable;
@GuardedBy("mLock")
private boolean mIsRegistered;
private CountDownLatch mAppearedLatch = new CountDownLatch(1);
private CountDownLatch mChangedLatch = new CountDownLatch(1);
private CountDownLatch mVanishedLatch = new CountDownLatch(1);
private CountDownLatch mParentChangedLatch = new CountDownLatch(1);
private CountDownLatch mErrorLatch = new CountDownLatch(1);
BasicTaskFragmentOrganizer() {
super(Runnable::run);
}
public TaskFragmentInfo getTaskFragmentInfo(IBinder taskFragToken) {
synchronized (mLock) {
return mInfos.get(taskFragToken);
}
}
public TaskFragmentInfo getRemovedTaskFragmentInfo(IBinder taskFragToken) {
synchronized (mLock) {
return mRemovedInfos.get(taskFragToken);
}
}
public Throwable getThrowable() {
synchronized (mLock) {
return mThrowable;
}
}
public IBinder getErrorCallbackToken() {
synchronized (mLock) {
return mErrorToken;
}
}
public void resetLatch() {
mAppearedLatch = new CountDownLatch(1);
mChangedLatch = new CountDownLatch(1);
mVanishedLatch = new CountDownLatch(1);
mParentChangedLatch = new CountDownLatch(1);
mErrorLatch = new CountDownLatch(1);
}
/**
* Generates a {@link TaskFragmentCreationParams} with {@code ownerToken} specified.
*
* @param ownerToken The token of {@link Activity} to create a TaskFragment under its parent
* Task
* @return the generated {@link TaskFragmentCreationParams}
*/
@NonNull
public TaskFragmentCreationParams generateTaskFragParams(@NonNull IBinder ownerToken) {
return generateTaskFragParams(ownerToken, new Rect(), WINDOWING_MODE_UNDEFINED);
}
@NonNull
public TaskFragmentCreationParams generateTaskFragParams(@NonNull IBinder ownerToken,
@NonNull Rect relativeBounds, int windowingMode) {
return generateTaskFragParams(new Binder(), ownerToken, relativeBounds, windowingMode);
}
@NonNull
public TaskFragmentCreationParams generateTaskFragParams(@NonNull IBinder fragmentToken,
@NonNull IBinder ownerToken, @NonNull Rect relativeBounds, int windowingMode) {
return new TaskFragmentCreationParams.Builder(getOrganizerToken(), fragmentToken,
ownerToken)
.setInitialRelativeBounds(relativeBounds)
.setWindowingMode(windowingMode)
.build();
}
public void setAppearedCount(int count) {
mAppearedLatch = new CountDownLatch(count);
}
public TaskFragmentInfo waitForAndGetTaskFragmentInfo(IBinder taskFragToken,
Predicate<TaskFragmentInfo> condition, String message) {
final TaskFragmentInfo[] info = new TaskFragmentInfo[1];
waitForOrFail(message, () -> {
info[0] = getTaskFragmentInfo(taskFragToken);
return condition.test(info[0]);
});
return info[0];
}
public void waitForTaskFragmentCreated() {
try {
assertThat(mAppearedLatch.await(WAIT_TIMEOUT_IN_SECOND, TimeUnit.SECONDS)).isTrue();
} catch (InterruptedException e) {
fail("Assertion failed because of" + e);
}
}
public void waitForTaskFragmentInfoChanged() {
try {
assertThat(mChangedLatch.await(WAIT_TIMEOUT_IN_SECOND, TimeUnit.SECONDS)).isTrue();
} catch (InterruptedException e) {
fail("Assertion failed because of" + e);
}
}
public void waitForTaskFragmentRemoved() {
try {
assertThat(mVanishedLatch.await(WAIT_TIMEOUT_IN_SECOND, TimeUnit.SECONDS)).isTrue();
} catch (InterruptedException e) {
fail("Assertion failed because of" + e);
}
}
public void waitForParentConfigChanged() {
try {
assertThat(mParentChangedLatch.await(WAIT_TIMEOUT_IN_SECOND, TimeUnit.SECONDS))
.isTrue();
} catch (InterruptedException e) {
fail("Assertion failed because of" + e);
}
}
public void waitForTaskFragmentError() {
try {
assertThat(mErrorLatch.await(WAIT_TIMEOUT_IN_SECOND, TimeUnit.SECONDS)).isTrue();
} catch (InterruptedException e) {
fail("Assertion failed because of" + e);
}
}
@GuardedBy("mLock")
private void removeAllTaskFragments() {
final WindowContainerTransaction wct = new WindowContainerTransaction();
for (TaskFragmentInfo info : mInfos.values()) {
wct.deleteTaskFragment(info.getFragmentToken());
}
applyTransaction(wct, TASK_FRAGMENT_TRANSIT_CLOSE,
false /* shouldApplyIndependently */);
}
@Override
public void registerOrganizer() {
synchronized (mLock) {
mIsRegistered = true;
}
super.registerOrganizer();
}
@Override
public void unregisterOrganizer() {
synchronized (mLock) {
mIsRegistered = false;
removeAllTaskFragments();
mRemovedInfos.clear();
mInfos.clear();
mParentTaskId = INVALID_STACK_ID;
mParentConfig = null;
mErrorToken = null;
mThrowable = null;
}
super.unregisterOrganizer();
}
@Override
public void onTransactionReady(@NonNull TaskFragmentTransaction transaction) {
synchronized (mLock) {
if (!mIsRegistered) {
// Ignore callback that is invoked after unregister. This can be a racing
// condition before the unregister reaches the server side.
return;
}
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:
onTaskFragmentAppeared(info);
break;
case TYPE_TASK_FRAGMENT_INFO_CHANGED:
onTaskFragmentInfoChanged(info);
break;
case TYPE_TASK_FRAGMENT_VANISHED:
onTaskFragmentVanished(info);
break;
case TYPE_TASK_FRAGMENT_PARENT_INFO_CHANGED:
onTaskFragmentParentInfoChanged(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);
onTaskFragmentError(errorToken, errorTaskFragmentInfo, opType,
exception);
break;
case TYPE_ACTIVITY_REPARENTED_TO_TASK:
onActivityReparentedToTask(
taskId,
change.getActivityIntent(),
change.getActivityToken());
break;
default:
// Log instead of throwing exception in case we will add more types
// between releases.
Log.w(TAG, "Unknown TaskFragmentEvent=" + change.getType());
}
}
onTransactionHandled(transaction.getTransactionToken(),
new WindowContainerTransaction(), TASK_FRAGMENT_TRANSIT_NONE,
false /* shouldApplyIndependently */);
}
}
@GuardedBy("mLock")
private void onTaskFragmentAppeared(@NonNull TaskFragmentInfo taskFragmentInfo) {
mInfos.put(taskFragmentInfo.getFragmentToken(), taskFragmentInfo);
mAppearedLatch.countDown();
}
@GuardedBy("mLock")
private void onTaskFragmentInfoChanged(@NonNull TaskFragmentInfo taskFragmentInfo) {
mInfos.put(taskFragmentInfo.getFragmentToken(), taskFragmentInfo);
mChangedLatch.countDown();
}
@GuardedBy("mLock")
private void onTaskFragmentVanished(@NonNull TaskFragmentInfo taskFragmentInfo) {
mInfos.remove(taskFragmentInfo.getFragmentToken());
mRemovedInfos.put(taskFragmentInfo.getFragmentToken(), taskFragmentInfo);
mVanishedLatch.countDown();
}
@GuardedBy("mLock")
private void onTaskFragmentParentInfoChanged(int taskId,
@NonNull Configuration parentConfig) {
mParentTaskId = taskId;
mParentConfig = parentConfig;
mParentChangedLatch.countDown();
}
@GuardedBy("mLock")
private void onTaskFragmentError(@NonNull IBinder errorCallbackToken,
@Nullable TaskFragmentInfo taskFragmentInfo, int opType,
@NonNull Throwable exception) {
mErrorToken = errorCallbackToken;
if (taskFragmentInfo != null) {
mInfos.put(taskFragmentInfo.getFragmentToken(), taskFragmentInfo);
}
mThrowable = exception;
mErrorLatch.countDown();
}
private void onActivityReparentedToTask(int taskId, @NonNull Intent activityIntent,
@NonNull IBinder activityToken) {
// TODO(b/232476698) Add CTS to verify PIP behavior with ActivityEmbedding
}
}
}