1 /* 2 * Copyright (C) 2021 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package android.server.wm.taskfragment; 18 19 import static android.app.ActivityTaskManager.INVALID_STACK_ID; 20 import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED; 21 import static android.content.Intent.FLAG_ACTIVITY_NEW_TASK; 22 import static android.server.wm.WindowManagerState.STATE_RESUMED; 23 import static android.window.TaskFragmentOrganizer.TASK_FRAGMENT_TRANSIT_OPEN; 24 import static android.window.TaskFragmentTransaction.TYPE_ACTIVITY_REPARENTED_TO_TASK; 25 import static android.window.TaskFragmentTransaction.TYPE_TASK_FRAGMENT_APPEARED; 26 import static android.window.TaskFragmentTransaction.TYPE_TASK_FRAGMENT_ERROR; 27 import static android.window.TaskFragmentTransaction.TYPE_TASK_FRAGMENT_INFO_CHANGED; 28 import static android.window.TaskFragmentTransaction.TYPE_TASK_FRAGMENT_PARENT_INFO_CHANGED; 29 import static android.window.TaskFragmentTransaction.TYPE_TASK_FRAGMENT_VANISHED; 30 31 import static com.google.common.truth.Truth.assertThat; 32 import static com.google.common.truth.Truth.assertWithMessage; 33 34 import static org.junit.Assert.fail; 35 import static org.junit.Assume.assumeTrue; 36 37 import android.app.Activity; 38 import android.app.Instrumentation; 39 import android.content.ComponentName; 40 import android.content.Intent; 41 import android.content.res.Configuration; 42 import android.graphics.Rect; 43 import android.os.Binder; 44 import android.os.Bundle; 45 import android.os.IBinder; 46 import android.server.wm.WindowContextTestActivity; 47 import android.server.wm.WindowManagerState.WindowContainer; 48 import android.server.wm.WindowManagerTestBase; 49 import android.util.ArrayMap; 50 import android.util.Log; 51 import android.window.TaskFragmentCreationParams; 52 import android.window.TaskFragmentInfo; 53 import android.window.TaskFragmentOrganizer; 54 import android.window.TaskFragmentParentInfo; 55 import android.window.TaskFragmentTransaction; 56 import android.window.WindowContainerTransaction; 57 58 import androidx.annotation.NonNull; 59 import androidx.annotation.Nullable; 60 import androidx.test.InstrumentationRegistry; 61 62 import org.junit.After; 63 import org.junit.Before; 64 65 import java.util.List; 66 import java.util.Map; 67 import java.util.concurrent.CountDownLatch; 68 import java.util.concurrent.TimeUnit; 69 import java.util.function.Predicate; 70 71 import javax.annotation.concurrent.GuardedBy; 72 73 public class TaskFragmentOrganizerTestBase extends WindowManagerTestBase { 74 private static final String TAG = "TaskFragmentOrganizerTestBase"; 75 76 public BasicTaskFragmentOrganizer mTaskFragmentOrganizer; 77 Activity mOwnerActivity; 78 IBinder mOwnerToken; 79 ComponentName mOwnerActivityName; 80 int mOwnerTaskId; 81 82 @Before 83 @Override setUp()84 public void setUp() throws Exception { 85 super.setUp(); 86 assumeTrue(supportsMultiWindow()); 87 mTaskFragmentOrganizer = new BasicTaskFragmentOrganizer(); 88 mTaskFragmentOrganizer.registerOrganizer(); 89 mOwnerActivity = setUpOwnerActivity(); 90 mOwnerToken = getActivityToken(mOwnerActivity); 91 mOwnerActivityName = mOwnerActivity.getComponentName(); 92 mOwnerTaskId = mOwnerActivity.getTaskId(); 93 // Make sure the activity is launched and resumed, otherwise the window state may not be 94 // stable. 95 waitAndAssertResumedActivity(mOwnerActivity.getComponentName(), 96 "The owner activity must be resumed."); 97 } 98 99 /** Setups the owner activity of the organized TaskFragment. */ setUpOwnerActivity()100 Activity setUpOwnerActivity() { 101 // Launch activities in fullscreen in case the device may use freeform as the default 102 // windowing mode. 103 return startActivityInWindowingModeFullScreen(WindowContextTestActivity.class); 104 } 105 106 @After tearDown()107 public void tearDown() { 108 if (mTaskFragmentOrganizer != null) { 109 mTaskFragmentOrganizer.unregisterOrganizer(); 110 } 111 } 112 getActivityToken(@onNull Activity activity)113 public static IBinder getActivityToken(@NonNull Activity activity) { 114 return activity.getWindow().getAttributes().token; 115 } 116 assertEmptyTaskFragment(TaskFragmentInfo info, IBinder expectedTaskFragToken)117 public static void assertEmptyTaskFragment(TaskFragmentInfo info, 118 IBinder expectedTaskFragToken) { 119 assertTaskFragmentInfoValidity(info, expectedTaskFragToken); 120 assertWithMessage("TaskFragment must be empty").that(info.isEmpty()).isTrue(); 121 assertWithMessage("TaskFragmentInfo#getActivities must be empty") 122 .that(info.getActivities()).isEmpty(); 123 assertWithMessage("TaskFragment must not contain any running Activity") 124 .that(info.hasRunningActivity()).isFalse(); 125 assertWithMessage("TaskFragment must not be visible").that(info.isVisible()).isFalse(); 126 } 127 assertNotEmptyTaskFragment(TaskFragmentInfo info, IBinder expectedTaskFragToken, @Nullable IBinder ... expectedActivityTokens)128 public static void assertNotEmptyTaskFragment(TaskFragmentInfo info, 129 IBinder expectedTaskFragToken, @Nullable IBinder ... expectedActivityTokens) { 130 assertTaskFragmentInfoValidity(info, expectedTaskFragToken); 131 assertWithMessage("TaskFragment must not be empty").that(info.isEmpty()).isFalse(); 132 assertWithMessage("TaskFragment must contain running Activity") 133 .that(info.hasRunningActivity()).isTrue(); 134 if (expectedActivityTokens != null) { 135 assertWithMessage("TaskFragmentInfo#getActivities must be empty") 136 .that(info.getActivities()).containsAtLeastElementsIn(expectedActivityTokens); 137 } 138 } 139 assertTaskFragmentInfoValidity(TaskFragmentInfo info, IBinder expectedTaskFragToken)140 private static void assertTaskFragmentInfoValidity(TaskFragmentInfo info, 141 IBinder expectedTaskFragToken) { 142 assertWithMessage("TaskFragmentToken must match the token from " 143 + "TaskFragmentCreationParams#getFragmentToken") 144 .that(info.getFragmentToken()).isEqualTo(expectedTaskFragToken); 145 assertWithMessage("WindowContainerToken must not be null") 146 .that(info.getToken()).isNotNull(); 147 assertWithMessage("TaskFragmentInfo#getPositionInParent must not be null") 148 .that(info.getPositionInParent()).isNotNull(); 149 assertWithMessage("Configuration must not be empty") 150 .that(info.getConfiguration()).isNotEqualTo(new Configuration()); 151 } 152 153 /** 154 * Verifies whether the window hierarchy is as expected or not. 155 * <p> 156 * The sample usage is as follows: 157 * <pre class="prettyprint"> 158 * assertWindowHierarchy(rootTask, leafTask, taskFragment, activity); 159 * </pre></p> 160 * 161 * @param containers The containers to be verified. It should be put from top to down 162 */ assertWindowHierarchy(WindowContainer... containers)163 public static void assertWindowHierarchy(WindowContainer... containers) { 164 for (int i = 0; i < containers.length - 2; i++) { 165 final WindowContainer parent = containers[i]; 166 final WindowContainer child = containers[i + 1]; 167 assertWithMessage(parent + " must contains " + child) 168 .that(parent.getChildren()) 169 .contains(child); 170 } 171 } 172 173 /** 174 * Builds, runs and waits for completion of task fragment creation transaction. 175 * @param componentName name of the activity to launch in the TF, or {@code null} if none. 176 * @return token of the created task fragment. 177 */ createTaskFragment(@ullable ComponentName componentName)178 TaskFragmentInfo createTaskFragment(@Nullable ComponentName componentName) { 179 return createTaskFragment(componentName, new Rect()); 180 } 181 182 /** 183 * Same as {@link #createTaskFragment(ComponentName)}, but allows to specify the bounds for the 184 * new task fragment. 185 */ createTaskFragment(@ullable ComponentName componentName, @NonNull Rect relativeBounds)186 TaskFragmentInfo createTaskFragment(@Nullable ComponentName componentName, 187 @NonNull Rect relativeBounds) { 188 return createTaskFragment(componentName, relativeBounds, new WindowContainerTransaction()); 189 } 190 191 /** 192 * Same as {@link #createTaskFragment(ComponentName, Rect)}, but allows to specify the 193 * {@link WindowContainerTransaction} to use. 194 */ createTaskFragment(@ullable ComponentName componentName, @NonNull Rect relativeBounds, @NonNull WindowContainerTransaction wct)195 TaskFragmentInfo createTaskFragment(@Nullable ComponentName componentName, 196 @NonNull Rect relativeBounds, @NonNull WindowContainerTransaction wct) { 197 final TaskFragmentCreationParams params = generateTaskFragCreationParams(relativeBounds); 198 final IBinder taskFragToken = params.getFragmentToken(); 199 wct.createTaskFragment(params); 200 if (componentName != null) { 201 wct.startActivityInTaskFragment(taskFragToken, mOwnerToken, 202 new Intent().setComponent(componentName), null /* activityOptions */); 203 } 204 mTaskFragmentOrganizer.applyTransaction(wct, TASK_FRAGMENT_TRANSIT_OPEN, 205 false /* shouldApplyIndependently */); 206 mTaskFragmentOrganizer.waitForTaskFragmentCreated(); 207 208 if (componentName != null) { 209 mWmState.waitForActivityState(componentName, STATE_RESUMED); 210 } 211 212 return mTaskFragmentOrganizer.getTaskFragmentInfo(taskFragToken); 213 } 214 215 @NonNull generateTaskFragCreationParams()216 TaskFragmentCreationParams generateTaskFragCreationParams() { 217 return mTaskFragmentOrganizer.generateTaskFragParams(mOwnerToken); 218 } 219 220 @NonNull generateTaskFragCreationParams(@onNull Rect relativeBounds)221 TaskFragmentCreationParams generateTaskFragCreationParams(@NonNull Rect relativeBounds) { 222 return mTaskFragmentOrganizer.generateTaskFragParams(mOwnerToken, relativeBounds, 223 WINDOWING_MODE_UNDEFINED); 224 } 225 startNewActivity()226 static Activity startNewActivity() { 227 return startNewActivity(WindowContextTestActivity.class); 228 } 229 startNewActivity(Class<?> className)230 static Activity startNewActivity(Class<?> className) { 231 final Instrumentation instrumentation = InstrumentationRegistry.getInstrumentation(); 232 final Intent intent = new Intent(instrumentation.getTargetContext(), className) 233 .addFlags(FLAG_ACTIVITY_NEW_TASK); 234 return instrumentation.startActivitySync(intent); 235 } 236 237 public static class BasicTaskFragmentOrganizer extends TaskFragmentOrganizer { 238 private final static int WAIT_TIMEOUT_IN_SECOND = 10; 239 240 private final Object mLock = new Object(); 241 242 @GuardedBy("mLock") 243 private final Map<IBinder, TaskFragmentInfo> mInfos = new ArrayMap<>(); 244 @GuardedBy("mLock") 245 private final Map<IBinder, TaskFragmentInfo> mRemovedInfos = new ArrayMap<>(); 246 @GuardedBy("mLock") 247 private int mParentTaskId = INVALID_STACK_ID; 248 @Nullable 249 @GuardedBy("mLock") 250 private TaskFragmentParentInfo mParentInfo; 251 @Nullable 252 @GuardedBy("mLock") 253 private IBinder mErrorToken; 254 @Nullable 255 @GuardedBy("mLock") 256 private Throwable mThrowable; 257 @GuardedBy("mLock") 258 private boolean mIsRegistered; 259 260 private CountDownLatch mAppearedLatch = new CountDownLatch(1); 261 private CountDownLatch mChangedLatch = new CountDownLatch(1); 262 private CountDownLatch mVanishedLatch = new CountDownLatch(1); 263 private CountDownLatch mParentChangedLatch = new CountDownLatch(1); 264 private CountDownLatch mErrorLatch = new CountDownLatch(1); 265 BasicTaskFragmentOrganizer()266 BasicTaskFragmentOrganizer() { 267 super(Runnable::run); 268 } 269 getTaskFragmentInfo(IBinder taskFragToken)270 public TaskFragmentInfo getTaskFragmentInfo(IBinder taskFragToken) { 271 synchronized (mLock) { 272 return mInfos.get(taskFragToken); 273 } 274 } 275 getRemovedTaskFragmentInfo(IBinder taskFragToken)276 public TaskFragmentInfo getRemovedTaskFragmentInfo(IBinder taskFragToken) { 277 synchronized (mLock) { 278 return mRemovedInfos.get(taskFragToken); 279 } 280 } 281 getThrowable()282 public Throwable getThrowable() { 283 synchronized (mLock) { 284 return mThrowable; 285 } 286 } 287 getErrorCallbackToken()288 public IBinder getErrorCallbackToken() { 289 synchronized (mLock) { 290 return mErrorToken; 291 } 292 } 293 resetLatch()294 public void resetLatch() { 295 mAppearedLatch = new CountDownLatch(1); 296 mChangedLatch = new CountDownLatch(1); 297 mVanishedLatch = new CountDownLatch(1); 298 mParentChangedLatch = new CountDownLatch(1); 299 mErrorLatch = new CountDownLatch(1); 300 } 301 302 /** 303 * Generates a {@link TaskFragmentCreationParams} with {@code ownerToken} specified. 304 * 305 * @param ownerToken The token of {@link Activity} to create a TaskFragment under its parent 306 * Task 307 * @return the generated {@link TaskFragmentCreationParams} 308 */ 309 @NonNull generateTaskFragParams(@onNull IBinder ownerToken)310 public TaskFragmentCreationParams generateTaskFragParams(@NonNull IBinder ownerToken) { 311 return generateTaskFragParams(ownerToken, new Rect(), WINDOWING_MODE_UNDEFINED); 312 } 313 314 @NonNull generateTaskFragParams(@onNull IBinder ownerToken, @NonNull Rect relativeBounds, int windowingMode)315 public TaskFragmentCreationParams generateTaskFragParams(@NonNull IBinder ownerToken, 316 @NonNull Rect relativeBounds, int windowingMode) { 317 return generateTaskFragParams(new Binder(), ownerToken, relativeBounds, windowingMode); 318 } 319 320 @NonNull generateTaskFragParams(@onNull IBinder fragmentToken, @NonNull IBinder ownerToken, @NonNull Rect relativeBounds, int windowingMode)321 public TaskFragmentCreationParams generateTaskFragParams(@NonNull IBinder fragmentToken, 322 @NonNull IBinder ownerToken, @NonNull Rect relativeBounds, int windowingMode) { 323 return new TaskFragmentCreationParams.Builder(getOrganizerToken(), fragmentToken, 324 ownerToken) 325 .setInitialRelativeBounds(relativeBounds) 326 .setWindowingMode(windowingMode) 327 .build(); 328 } 329 setAppearedCount(int count)330 public void setAppearedCount(int count) { 331 mAppearedLatch = new CountDownLatch(count); 332 } 333 waitForAndGetTaskFragmentInfo(IBinder taskFragToken, Predicate<TaskFragmentInfo> condition, String message)334 public TaskFragmentInfo waitForAndGetTaskFragmentInfo(IBinder taskFragToken, 335 Predicate<TaskFragmentInfo> condition, String message) { 336 final TaskFragmentInfo[] info = new TaskFragmentInfo[1]; 337 waitForOrFail(message, () -> { 338 info[0] = getTaskFragmentInfo(taskFragToken); 339 return condition.test(info[0]); 340 }); 341 return info[0]; 342 } 343 waitForTaskFragmentCreated()344 public void waitForTaskFragmentCreated() { 345 try { 346 assertThat(mAppearedLatch.await(WAIT_TIMEOUT_IN_SECOND, TimeUnit.SECONDS)).isTrue(); 347 } catch (InterruptedException e) { 348 fail("Assertion failed because of" + e); 349 } 350 } 351 waitForTaskFragmentInfoChanged()352 public void waitForTaskFragmentInfoChanged() { 353 try { 354 assertThat(mChangedLatch.await(WAIT_TIMEOUT_IN_SECOND, TimeUnit.SECONDS)).isTrue(); 355 } catch (InterruptedException e) { 356 fail("Assertion failed because of" + e); 357 } 358 } 359 waitForTaskFragmentRemoved()360 public void waitForTaskFragmentRemoved() { 361 try { 362 assertThat(mVanishedLatch.await(WAIT_TIMEOUT_IN_SECOND, TimeUnit.SECONDS)).isTrue(); 363 } catch (InterruptedException e) { 364 fail("Assertion failed because of" + e); 365 } 366 } 367 waitForParentConfigChanged()368 public void waitForParentConfigChanged() { 369 try { 370 assertThat(mParentChangedLatch.await(WAIT_TIMEOUT_IN_SECOND, TimeUnit.SECONDS)) 371 .isTrue(); 372 } catch (InterruptedException e) { 373 fail("Assertion failed because of" + e); 374 } 375 } 376 waitForTaskFragmentError()377 public void waitForTaskFragmentError() { 378 try { 379 assertThat(mErrorLatch.await(WAIT_TIMEOUT_IN_SECOND, TimeUnit.SECONDS)).isTrue(); 380 } catch (InterruptedException e) { 381 fail("Assertion failed because of" + e); 382 } 383 } 384 385 @GuardedBy("mLock") removeAllTaskFragments()386 private void removeAllTaskFragments() { 387 final WindowContainerTransaction wct = new WindowContainerTransaction(); 388 for (TaskFragmentInfo info : mInfos.values()) { 389 wct.deleteTaskFragment(info.getFragmentToken()); 390 } 391 applyTransaction(wct, TASK_FRAGMENT_TRANSIT_CLOSE, 392 false /* shouldApplyIndependently */); 393 } 394 395 @Override registerOrganizer()396 public void registerOrganizer() { 397 synchronized (mLock) { 398 mIsRegistered = true; 399 } 400 super.registerOrganizer(); 401 } 402 403 @Override unregisterOrganizer()404 public void unregisterOrganizer() { 405 synchronized (mLock) { 406 mIsRegistered = false; 407 removeAllTaskFragments(); 408 mRemovedInfos.clear(); 409 mInfos.clear(); 410 mParentTaskId = INVALID_STACK_ID; 411 mParentInfo = null; 412 mErrorToken = null; 413 mThrowable = null; 414 } 415 super.unregisterOrganizer(); 416 } 417 418 @Override onTransactionReady(@onNull TaskFragmentTransaction transaction)419 public void onTransactionReady(@NonNull TaskFragmentTransaction transaction) { 420 synchronized (mLock) { 421 if (!mIsRegistered) { 422 // Ignore callback that is invoked after unregister. This can be a racing 423 // condition before the unregister reaches the server side. 424 return; 425 } 426 final List<TaskFragmentTransaction.Change> changes = transaction.getChanges(); 427 for (TaskFragmentTransaction.Change change : changes) { 428 final int taskId = change.getTaskId(); 429 final TaskFragmentInfo info = change.getTaskFragmentInfo(); 430 switch (change.getType()) { 431 case TYPE_TASK_FRAGMENT_APPEARED: 432 onTaskFragmentAppeared(info); 433 break; 434 case TYPE_TASK_FRAGMENT_INFO_CHANGED: 435 onTaskFragmentInfoChanged(info); 436 break; 437 case TYPE_TASK_FRAGMENT_VANISHED: 438 onTaskFragmentVanished(info); 439 break; 440 case TYPE_TASK_FRAGMENT_PARENT_INFO_CHANGED: 441 onTaskFragmentParentInfoChanged(taskId, 442 change.getTaskFragmentParentInfo()); 443 break; 444 case TYPE_TASK_FRAGMENT_ERROR: 445 final Bundle errorBundle = change.getErrorBundle(); 446 final IBinder errorToken = change.getErrorCallbackToken(); 447 final TaskFragmentInfo errorTaskFragmentInfo = 448 errorBundle.getParcelable( 449 KEY_ERROR_CALLBACK_TASK_FRAGMENT_INFO, 450 TaskFragmentInfo.class); 451 final int opType = errorBundle.getInt(KEY_ERROR_CALLBACK_OP_TYPE); 452 final Throwable exception = errorBundle.getSerializable( 453 KEY_ERROR_CALLBACK_THROWABLE, Throwable.class); 454 onTaskFragmentError(errorToken, errorTaskFragmentInfo, opType, 455 exception); 456 break; 457 case TYPE_ACTIVITY_REPARENTED_TO_TASK: 458 onActivityReparentedToTask( 459 taskId, 460 change.getActivityIntent(), 461 change.getActivityToken()); 462 break; 463 default: 464 // Log instead of throwing exception in case we will add more types 465 // between releases. 466 Log.w(TAG, "Unknown TaskFragmentEvent=" + change.getType()); 467 } 468 } 469 onTransactionHandled(transaction.getTransactionToken(), 470 new WindowContainerTransaction(), TASK_FRAGMENT_TRANSIT_NONE, 471 false /* shouldApplyIndependently */); 472 } 473 } 474 475 @GuardedBy("mLock") onTaskFragmentAppeared(@onNull TaskFragmentInfo taskFragmentInfo)476 private void onTaskFragmentAppeared(@NonNull TaskFragmentInfo taskFragmentInfo) { 477 mInfos.put(taskFragmentInfo.getFragmentToken(), taskFragmentInfo); 478 mAppearedLatch.countDown(); 479 } 480 481 @GuardedBy("mLock") onTaskFragmentInfoChanged(@onNull TaskFragmentInfo taskFragmentInfo)482 private void onTaskFragmentInfoChanged(@NonNull TaskFragmentInfo taskFragmentInfo) { 483 mInfos.put(taskFragmentInfo.getFragmentToken(), taskFragmentInfo); 484 mChangedLatch.countDown(); 485 } 486 487 @GuardedBy("mLock") onTaskFragmentVanished(@onNull TaskFragmentInfo taskFragmentInfo)488 private void onTaskFragmentVanished(@NonNull TaskFragmentInfo taskFragmentInfo) { 489 mInfos.remove(taskFragmentInfo.getFragmentToken()); 490 mRemovedInfos.put(taskFragmentInfo.getFragmentToken(), taskFragmentInfo); 491 mVanishedLatch.countDown(); 492 } 493 494 @GuardedBy("mLock") onTaskFragmentParentInfoChanged(int taskId, @NonNull TaskFragmentParentInfo parentInfo)495 private void onTaskFragmentParentInfoChanged(int taskId, 496 @NonNull TaskFragmentParentInfo parentInfo) { 497 mParentTaskId = taskId; 498 mParentInfo = parentInfo; 499 mParentChangedLatch.countDown(); 500 } 501 502 @GuardedBy("mLock") onTaskFragmentError(@onNull IBinder errorCallbackToken, @Nullable TaskFragmentInfo taskFragmentInfo, int opType, @NonNull Throwable exception)503 private void onTaskFragmentError(@NonNull IBinder errorCallbackToken, 504 @Nullable TaskFragmentInfo taskFragmentInfo, int opType, 505 @NonNull Throwable exception) { 506 mErrorToken = errorCallbackToken; 507 if (taskFragmentInfo != null) { 508 mInfos.put(taskFragmentInfo.getFragmentToken(), taskFragmentInfo); 509 } 510 mThrowable = exception; 511 mErrorLatch.countDown(); 512 } 513 onActivityReparentedToTask(int taskId, @NonNull Intent activityIntent, @NonNull IBinder activityToken)514 private void onActivityReparentedToTask(int taskId, @NonNull Intent activityIntent, 515 @NonNull IBinder activityToken) { 516 // TODO(b/232476698) Add CTS to verify PIP behavior with ActivityEmbedding 517 } 518 } 519 } 520