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; 18 19 import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED; 20 import static android.content.Intent.FLAG_ACTIVITY_NEW_TASK; 21 import static android.server.wm.WindowManagerState.STATE_RESUMED; 22 23 import static com.google.common.truth.Truth.assertThat; 24 import static com.google.common.truth.Truth.assertWithMessage; 25 26 import static org.junit.Assert.fail; 27 import static org.junit.Assume.assumeTrue; 28 29 import android.app.Activity; 30 import android.app.Instrumentation; 31 import android.content.ComponentName; 32 import android.content.Intent; 33 import android.content.res.Configuration; 34 import android.graphics.Rect; 35 import android.os.Binder; 36 import android.os.IBinder; 37 import android.server.wm.WindowContextTests.TestActivity; 38 import android.server.wm.WindowManagerState.WindowContainer; 39 import android.server.wm.jetpack.utils.ExtensionUtil; 40 import android.util.ArrayMap; 41 import android.window.TaskFragmentCreationParams; 42 import android.window.TaskFragmentInfo; 43 import android.window.TaskFragmentOrganizer; 44 import android.window.WindowContainerTransaction; 45 46 import androidx.annotation.NonNull; 47 import androidx.annotation.Nullable; 48 import androidx.test.InstrumentationRegistry; 49 50 import org.junit.After; 51 import org.junit.Before; 52 53 import java.util.Map; 54 import java.util.concurrent.CountDownLatch; 55 import java.util.concurrent.TimeUnit; 56 import java.util.function.Predicate; 57 58 public class TaskFragmentOrganizerTestBase extends WindowManagerTestBase { 59 public BasicTaskFragmentOrganizer mTaskFragmentOrganizer; 60 Activity mOwnerActivity; 61 IBinder mOwnerToken; 62 ComponentName mOwnerActivityName; 63 int mOwnerTaskId; 64 65 @Before 66 @Override setUp()67 public void setUp() throws Exception { 68 super.setUp(); 69 mTaskFragmentOrganizer = new BasicTaskFragmentOrganizer(); 70 mTaskFragmentOrganizer.registerOrganizer(); 71 mOwnerActivity = setUpOwnerActivity(); 72 mOwnerToken = getActivityToken(mOwnerActivity); 73 mOwnerActivityName = mOwnerActivity.getComponentName(); 74 mOwnerTaskId = mOwnerActivity.getTaskId(); 75 // Make sure the activity is launched and resumed, otherwise the window state may not be 76 // stable. 77 waitAndAssertResumedActivity(mOwnerActivity.getComponentName(), 78 "The owner activity must be resumed."); 79 } 80 81 /** Setups the owner activity of the organized TaskFragment. */ setUpOwnerActivity()82 Activity setUpOwnerActivity() { 83 // Launch activities in fullscreen in case the device may use freeform as the default 84 // windowing mode. 85 return startActivityInWindowingModeFullScreen(TestActivity.class); 86 } 87 88 @After tearDown()89 public void tearDown() { 90 if (mTaskFragmentOrganizer != null) { 91 mTaskFragmentOrganizer.unregisterOrganizer(); 92 } 93 } 94 getActivityToken(@onNull Activity activity)95 public static IBinder getActivityToken(@NonNull Activity activity) { 96 return activity.getWindow().getAttributes().token; 97 } 98 assertEmptyTaskFragment(TaskFragmentInfo info, IBinder expectedTaskFragToken)99 public static void assertEmptyTaskFragment(TaskFragmentInfo info, 100 IBinder expectedTaskFragToken) { 101 assertTaskFragmentInfoValidity(info, expectedTaskFragToken); 102 assertWithMessage("TaskFragment must be empty").that(info.isEmpty()).isTrue(); 103 assertWithMessage("TaskFragmentInfo#getActivities must be empty") 104 .that(info.getActivities()).isEmpty(); 105 assertWithMessage("TaskFragment must not contain any running Activity") 106 .that(info.hasRunningActivity()).isFalse(); 107 assertWithMessage("TaskFragment must not be visible").that(info.isVisible()).isFalse(); 108 } 109 assertNotEmptyTaskFragment(TaskFragmentInfo info, IBinder expectedTaskFragToken, @Nullable IBinder ... expectedActivityTokens)110 public static void assertNotEmptyTaskFragment(TaskFragmentInfo info, 111 IBinder expectedTaskFragToken, @Nullable IBinder ... expectedActivityTokens) { 112 assertTaskFragmentInfoValidity(info, expectedTaskFragToken); 113 assertWithMessage("TaskFragment must not be empty").that(info.isEmpty()).isFalse(); 114 assertWithMessage("TaskFragment must contain running Activity") 115 .that(info.hasRunningActivity()).isTrue(); 116 if (expectedActivityTokens != null) { 117 assertWithMessage("TaskFragmentInfo#getActivities must be empty") 118 .that(info.getActivities()).containsAtLeastElementsIn(expectedActivityTokens); 119 } 120 } 121 assertTaskFragmentInfoValidity(TaskFragmentInfo info, IBinder expectedTaskFragToken)122 private static void assertTaskFragmentInfoValidity(TaskFragmentInfo info, 123 IBinder expectedTaskFragToken) { 124 assertWithMessage("TaskFragmentToken must match the token from " 125 + "TaskFragmentCreationParams#getFragmentToken") 126 .that(info.getFragmentToken()).isEqualTo(expectedTaskFragToken); 127 assertWithMessage("WindowContainerToken must not be null") 128 .that(info.getToken()).isNotNull(); 129 assertWithMessage("TaskFragmentInfo#getPositionInParent must not be null") 130 .that(info.getPositionInParent()).isNotNull(); 131 assertWithMessage("Configuration must not be empty") 132 .that(info.getConfiguration()).isNotEqualTo(new Configuration()); 133 } 134 135 /** 136 * Verifies whether the window hierarchy is as expected or not. 137 * <p> 138 * The sample usage is as follows: 139 * <pre class="prettyprint"> 140 * assertWindowHierarchy(rootTask, leafTask, taskFragment, activity); 141 * </pre></p> 142 * 143 * @param containers The containers to be verified. It should be put from top to down 144 */ assertWindowHierarchy(WindowContainer... containers)145 public static void assertWindowHierarchy(WindowContainer... containers) { 146 for (int i = 0; i < containers.length - 2; i++) { 147 final WindowContainer parent = containers[i]; 148 final WindowContainer child = containers[i + 1]; 149 assertWithMessage(parent + " must contains " + child) 150 .that(parent.mChildren).contains(child); 151 } 152 } 153 154 /** 155 * Builds, runs and waits for completion of task fragment creation transaction. 156 * @param componentName name of the activity to launch in the TF, or {@code null} if none. 157 * @return token of the created task fragment. 158 */ createTaskFragment(@ullable ComponentName componentName)159 TaskFragmentInfo createTaskFragment(@Nullable ComponentName componentName) { 160 return createTaskFragment(componentName, new Rect()); 161 } 162 163 /** 164 * Same as {@link #createTaskFragment(ComponentName)}, but allows to specify the bounds for the 165 * new task fragment. 166 */ createTaskFragment(@ullable ComponentName componentName, @NonNull Rect bounds)167 TaskFragmentInfo createTaskFragment(@Nullable ComponentName componentName, 168 @NonNull Rect bounds) { 169 return createTaskFragment(componentName, bounds, new WindowContainerTransaction()); 170 } 171 172 /** 173 * Same as {@link #createTaskFragment(ComponentName, Rect)}, but allows to specify the 174 * {@link WindowContainerTransaction} to use. 175 */ createTaskFragment(@ullable ComponentName componentName, @NonNull Rect bounds, @NonNull WindowContainerTransaction wct)176 TaskFragmentInfo createTaskFragment(@Nullable ComponentName componentName, 177 @NonNull Rect bounds, @NonNull WindowContainerTransaction wct) { 178 final TaskFragmentCreationParams params = generateTaskFragCreationParams(bounds); 179 final IBinder taskFragToken = params.getFragmentToken(); 180 wct.createTaskFragment(params); 181 if (componentName != null) { 182 wct.startActivityInTaskFragment(taskFragToken, mOwnerToken, 183 new Intent().setComponent(componentName), null /* activityOptions */); 184 } 185 mTaskFragmentOrganizer.applyTransaction(wct); 186 mTaskFragmentOrganizer.waitForTaskFragmentCreated(); 187 188 if (componentName != null) { 189 mWmState.waitForActivityState(componentName, STATE_RESUMED); 190 } 191 192 return mTaskFragmentOrganizer.getTaskFragmentInfo(taskFragToken); 193 } 194 195 @NonNull generateTaskFragCreationParams()196 TaskFragmentCreationParams generateTaskFragCreationParams() { 197 return mTaskFragmentOrganizer.generateTaskFragParams(mOwnerToken); 198 } 199 200 @NonNull generateTaskFragCreationParams(@onNull Rect bounds)201 TaskFragmentCreationParams generateTaskFragCreationParams(@NonNull Rect bounds) { 202 return mTaskFragmentOrganizer.generateTaskFragParams(mOwnerToken, bounds, 203 WINDOWING_MODE_UNDEFINED); 204 } 205 startNewActivity()206 static Activity startNewActivity() { 207 return startNewActivity(TestActivity.class); 208 } 209 startNewActivity(Class<?> className)210 static Activity startNewActivity(Class<?> className) { 211 final Instrumentation instrumentation = InstrumentationRegistry.getInstrumentation(); 212 final Intent intent = new Intent(instrumentation.getTargetContext(), className) 213 .addFlags(FLAG_ACTIVITY_NEW_TASK); 214 return instrumentation.startActivitySync(intent); 215 } 216 217 /** For API changes that are introduced together with WM Extensions version 2. */ assumeExtensionVersionAtLeast2()218 static void assumeExtensionVersionAtLeast2() { 219 // TODO(b/232476698) Remove in the next Android release. 220 assumeTrue(ExtensionUtil.getExtensionVersion().getMajor() >= 2); 221 } 222 223 public static class BasicTaskFragmentOrganizer extends TaskFragmentOrganizer { 224 private final static int WAIT_TIMEOUT_IN_SECOND = 10; 225 226 private final Map<IBinder, TaskFragmentInfo> mInfos = new ArrayMap<>(); 227 private final Map<IBinder, TaskFragmentInfo> mRemovedInfos = new ArrayMap<>(); 228 private IBinder mTaskFragToken; 229 private Configuration mParentConfig; 230 private IBinder mErrorToken; 231 private Throwable mThrowable; 232 233 private CountDownLatch mAppearedLatch = new CountDownLatch(1); 234 private CountDownLatch mChangedLatch = new CountDownLatch(1); 235 private CountDownLatch mVanishedLatch = new CountDownLatch(1); 236 private CountDownLatch mParentChangedLatch = new CountDownLatch(1); 237 private CountDownLatch mErrorLatch = new CountDownLatch(1); 238 BasicTaskFragmentOrganizer()239 BasicTaskFragmentOrganizer() { 240 super(Runnable::run); 241 } 242 getTaskFragmentInfo(IBinder taskFragToken)243 public TaskFragmentInfo getTaskFragmentInfo(IBinder taskFragToken) { 244 return mInfos.get(taskFragToken); 245 } 246 getRemovedTaskFragmentInfo(IBinder taskFragToken)247 public TaskFragmentInfo getRemovedTaskFragmentInfo(IBinder taskFragToken) { 248 return mRemovedInfos.get(taskFragToken); 249 } 250 getThrowable()251 public Throwable getThrowable() { 252 return mThrowable; 253 } 254 getErrorCallbackToken()255 public IBinder getErrorCallbackToken() { 256 return mErrorToken; 257 } 258 resetLatch()259 public void resetLatch() { 260 mAppearedLatch = new CountDownLatch(1); 261 mChangedLatch = new CountDownLatch(1); 262 mVanishedLatch = new CountDownLatch(1); 263 mParentChangedLatch = new CountDownLatch(1); 264 mErrorLatch = new CountDownLatch(1); 265 } 266 267 /** 268 * Generates a {@link TaskFragmentCreationParams} with {@code ownerToken} specified. 269 * 270 * @param ownerToken The token of {@link Activity} to create a TaskFragment under its parent 271 * Task 272 * @return the generated {@link TaskFragmentCreationParams} 273 */ 274 @NonNull generateTaskFragParams(@onNull IBinder ownerToken)275 public TaskFragmentCreationParams generateTaskFragParams(@NonNull IBinder ownerToken) { 276 return generateTaskFragParams(ownerToken, new Rect(), WINDOWING_MODE_UNDEFINED); 277 } 278 279 @NonNull generateTaskFragParams(@onNull IBinder ownerToken, @NonNull Rect bounds, int windowingMode)280 public TaskFragmentCreationParams generateTaskFragParams(@NonNull IBinder ownerToken, 281 @NonNull Rect bounds, int windowingMode) { 282 return generateTaskFragParams(new Binder(), ownerToken, bounds, windowingMode); 283 } 284 285 @NonNull generateTaskFragParams(@onNull IBinder fragmentToken, @NonNull IBinder ownerToken, @NonNull Rect bounds, int windowingMode)286 public TaskFragmentCreationParams generateTaskFragParams(@NonNull IBinder fragmentToken, 287 @NonNull IBinder ownerToken, @NonNull Rect bounds, int windowingMode) { 288 return new TaskFragmentCreationParams.Builder(getOrganizerToken(), fragmentToken, 289 ownerToken) 290 .setInitialBounds(bounds) 291 .setWindowingMode(windowingMode) 292 .build(); 293 } 294 setAppearedCount(int count)295 public void setAppearedCount(int count) { 296 mAppearedLatch = new CountDownLatch(count); 297 } 298 waitForAndGetTaskFragmentInfo(IBinder taskFragToken, Predicate<TaskFragmentInfo> condition, String message)299 public TaskFragmentInfo waitForAndGetTaskFragmentInfo(IBinder taskFragToken, 300 Predicate<TaskFragmentInfo> condition, String message) { 301 final TaskFragmentInfo[] info = new TaskFragmentInfo[1]; 302 waitForOrFail(message, () -> { 303 info[0] = getTaskFragmentInfo(taskFragToken); 304 return condition.test(info[0]); 305 }); 306 return info[0]; 307 } 308 waitForTaskFragmentCreated()309 public void waitForTaskFragmentCreated() { 310 try { 311 assertThat(mAppearedLatch.await(WAIT_TIMEOUT_IN_SECOND, TimeUnit.SECONDS)).isTrue(); 312 } catch (InterruptedException e) { 313 fail("Assertion failed because of" + e); 314 } 315 } 316 waitForTaskFragmentInfoChanged()317 public void waitForTaskFragmentInfoChanged() { 318 try { 319 assertThat(mChangedLatch.await(WAIT_TIMEOUT_IN_SECOND, TimeUnit.SECONDS)).isTrue(); 320 } catch (InterruptedException e) { 321 fail("Assertion failed because of" + e); 322 } 323 } 324 waitForTaskFragmentRemoved()325 public void waitForTaskFragmentRemoved() { 326 try { 327 assertThat(mVanishedLatch.await(WAIT_TIMEOUT_IN_SECOND, TimeUnit.SECONDS)).isTrue(); 328 } catch (InterruptedException e) { 329 fail("Assertion failed because of" + e); 330 } 331 } 332 waitForParentConfigChanged()333 public void waitForParentConfigChanged() { 334 try { 335 assertThat(mParentChangedLatch.await(WAIT_TIMEOUT_IN_SECOND, TimeUnit.SECONDS)) 336 .isTrue(); 337 } catch (InterruptedException e) { 338 fail("Assertion failed because of" + e); 339 } 340 } 341 waitForTaskFragmentError()342 public void waitForTaskFragmentError() { 343 try { 344 assertThat(mErrorLatch.await(WAIT_TIMEOUT_IN_SECOND, TimeUnit.SECONDS)).isTrue(); 345 } catch (InterruptedException e) { 346 fail("Assertion failed because of" + e); 347 } 348 } 349 removeAllTaskFragments()350 private void removeAllTaskFragments() { 351 final WindowContainerTransaction wct = new WindowContainerTransaction(); 352 for (TaskFragmentInfo info : mInfos.values()) { 353 wct.deleteTaskFragment(info.getToken()); 354 } 355 applyTransaction(wct); 356 } 357 358 @Override unregisterOrganizer()359 public void unregisterOrganizer() { 360 removeAllTaskFragments(); 361 mRemovedInfos.clear(); 362 super.unregisterOrganizer(); 363 } 364 365 @Override onTaskFragmentAppeared(@onNull TaskFragmentInfo taskFragmentInfo)366 public void onTaskFragmentAppeared(@NonNull TaskFragmentInfo taskFragmentInfo) { 367 super.onTaskFragmentAppeared(taskFragmentInfo); 368 mInfos.put(taskFragmentInfo.getFragmentToken(), taskFragmentInfo); 369 mAppearedLatch.countDown(); 370 } 371 372 @Override onTaskFragmentInfoChanged(@onNull TaskFragmentInfo taskFragmentInfo)373 public void onTaskFragmentInfoChanged(@NonNull TaskFragmentInfo taskFragmentInfo) { 374 super.onTaskFragmentInfoChanged(taskFragmentInfo); 375 mInfos.put(taskFragmentInfo.getFragmentToken(), taskFragmentInfo); 376 mChangedLatch.countDown(); 377 } 378 379 @Override onTaskFragmentVanished(@onNull TaskFragmentInfo taskFragmentInfo)380 public void onTaskFragmentVanished(@NonNull TaskFragmentInfo taskFragmentInfo) { 381 super.onTaskFragmentVanished(taskFragmentInfo); 382 mInfos.remove(taskFragmentInfo.getFragmentToken()); 383 mRemovedInfos.put(taskFragmentInfo.getFragmentToken(), taskFragmentInfo); 384 mVanishedLatch.countDown(); 385 } 386 387 @Override onTaskFragmentParentInfoChanged(@onNull IBinder fragmentToken, @NonNull Configuration parentConfig)388 public void onTaskFragmentParentInfoChanged(@NonNull IBinder fragmentToken, 389 @NonNull Configuration parentConfig) { 390 super.onTaskFragmentParentInfoChanged(fragmentToken, parentConfig); 391 mTaskFragToken = fragmentToken; 392 mParentConfig = parentConfig; 393 mParentChangedLatch.countDown(); 394 } 395 396 @Override onTaskFragmentError(@onNull IBinder errorCallbackToken, @NonNull Throwable exception)397 public void onTaskFragmentError(@NonNull IBinder errorCallbackToken, 398 @NonNull Throwable exception) { 399 super.onTaskFragmentError(errorCallbackToken, exception); 400 mErrorToken = errorCallbackToken; 401 mThrowable = exception; 402 mErrorLatch.countDown(); 403 } 404 } 405 } 406