1 /* 2 * Copyright (C) 2020 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.ACTIVITY_TYPE_HOME; 20 import static android.app.WindowConfiguration.ACTIVITY_TYPE_RECENTS; 21 import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD; 22 import static android.app.WindowConfiguration.ACTIVITY_TYPE_UNDEFINED; 23 import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN; 24 import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW; 25 import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED; 26 import static android.view.Display.DEFAULT_DISPLAY; 27 import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION; 28 29 import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation; 30 31 import android.app.ActivityManager; 32 import android.app.WindowConfiguration; 33 import android.content.Context; 34 import android.graphics.Rect; 35 import android.hardware.display.DisplayManager; 36 import android.os.Binder; 37 import android.os.IBinder; 38 import android.os.SystemClock; 39 import android.util.ArraySet; 40 import android.util.Log; 41 import android.view.Surface; 42 import android.view.SurfaceControl; 43 import android.view.WindowManager; 44 import android.window.TaskAppearedInfo; 45 import android.window.TaskOrganizer; 46 import android.window.WindowContainerToken; 47 import android.window.WindowContainerTransaction; 48 49 import androidx.annotation.NonNull; 50 import androidx.annotation.Nullable; 51 52 import org.junit.Assert; 53 54 import java.util.HashMap; 55 import java.util.List; 56 import java.util.concurrent.TimeUnit; 57 import java.util.function.Predicate; 58 59 public class TestTaskOrganizer extends TaskOrganizer { 60 private static final String TAG = TestTaskOrganizer.class.getSimpleName(); 61 public static final int INVALID_TASK_ID = -1; 62 63 private boolean mRegistered; 64 private ActivityManager.RunningTaskInfo mRootPrimary; 65 private ActivityManager.RunningTaskInfo mRootSecondary; 66 @Nullable private ActivityManager.RunningTaskInfo mPrePrimarySplitTaskInfo; 67 @Nullable private ActivityManager.RunningTaskInfo mPreSecondarySplitTaskInfo; 68 private IBinder mPrimaryCookie; 69 private IBinder mSecondaryCookie; 70 private final HashMap<Integer, ActivityManager.RunningTaskInfo> mKnownTasks = new HashMap<>(); 71 private final HashMap<Integer, SurfaceControl> mTaskLeashes = new HashMap<>(); 72 private final ArraySet<Integer> mPrimaryChildrenTaskIds = new ArraySet<>(); 73 private final ArraySet<Integer> mSecondaryChildrenTaskIds = new ArraySet<>(); 74 private final Rect mPrimaryBounds = new Rect(); 75 private final Rect mSecondaryBounds = new Rect(); 76 77 private static final int[] CONTROLLED_ACTIVITY_TYPES = { 78 ACTIVITY_TYPE_STANDARD, 79 ACTIVITY_TYPE_HOME, 80 ACTIVITY_TYPE_RECENTS, 81 ACTIVITY_TYPE_UNDEFINED 82 }; 83 private static final int[] CONTROLLED_WINDOWING_MODES = { 84 WINDOWING_MODE_FULLSCREEN, 85 WINDOWING_MODE_MULTI_WINDOW, 86 WINDOWING_MODE_UNDEFINED 87 }; 88 89 @Override registerOrganizer()90 public List<TaskAppearedInfo> registerOrganizer() { 91 final Context context = getInstrumentation().getContext(); 92 final Rect bounds = context.createDisplayContext( 93 context.getSystemService(DisplayManager.class).getDisplay(DEFAULT_DISPLAY)) 94 .createWindowContext(TYPE_APPLICATION, null /* options */) 95 .getSystemService(WindowManager.class) 96 .getCurrentWindowMetrics() 97 .getBounds(); 98 99 final boolean isLandscape = bounds.width() > bounds.height(); 100 if (isLandscape) { 101 bounds.splitVertically(mPrimaryBounds, mSecondaryBounds); 102 } else { 103 bounds.splitHorizontally(mPrimaryBounds, mSecondaryBounds); 104 } 105 Log.i(TAG, "registerOrganizer with PrimaryBounds=" + mPrimaryBounds 106 + " SecondaryBounds=" + mSecondaryBounds); 107 108 synchronized (this) { 109 final List<TaskAppearedInfo> taskInfos = super.registerOrganizer(); 110 for (int i = 0; i < taskInfos.size(); i++) { 111 final TaskAppearedInfo info = taskInfos.get(i); 112 onTaskAppeared(info.getTaskInfo(), info.getLeash()); 113 } 114 createRootTasksIfNeeded(); 115 return taskInfos; 116 } 117 } 118 createRootTasksIfNeeded()119 private void createRootTasksIfNeeded() { 120 synchronized (this) { 121 if (mPrimaryCookie != null) return; 122 mPrimaryCookie = new Binder(); 123 mSecondaryCookie = new Binder(); 124 125 createRootTask(DEFAULT_DISPLAY, WINDOWING_MODE_MULTI_WINDOW, mPrimaryCookie); 126 createRootTask(DEFAULT_DISPLAY, WINDOWING_MODE_MULTI_WINDOW, mSecondaryCookie); 127 128 waitForAndAssert(o -> mRootPrimary != null && mRootSecondary != null, 129 "Failed to get root tasks"); 130 Log.e(TAG, "createRootTasksIfNeeded primary=" + mRootPrimary.taskId 131 + " secondary=" + mRootSecondary.taskId); 132 133 // Set the roots as adjacent to each other. 134 final WindowContainerTransaction wct = new WindowContainerTransaction(); 135 wct.setAdjacentRoots(mRootPrimary.getToken(), mRootSecondary.getToken()); 136 wct.setLaunchAdjacentFlagRoot(mRootSecondary.getToken()); 137 applyTransaction(wct); 138 } 139 } 140 waitForAndAssert(Predicate<Object> condition, String failureMessage)141 private void waitForAndAssert(Predicate<Object> condition, String failureMessage) { 142 waitFor(condition); 143 if (!condition.test(this)) { 144 Assert.fail(failureMessage); 145 } 146 } 147 waitFor(Predicate<Object> condition)148 private void waitFor(Predicate<Object> condition) { 149 final long waitTillTime = SystemClock.elapsedRealtime() + TimeUnit.SECONDS.toMillis(5); 150 while (!condition.test(this) 151 && SystemClock.elapsedRealtime() < waitTillTime) { 152 try { 153 wait(TimeUnit.SECONDS.toMillis(5)); 154 } catch (InterruptedException e) { 155 e.printStackTrace(); 156 } 157 } 158 } 159 notifyOnEnd(Runnable r)160 private void notifyOnEnd(Runnable r) { 161 r.run(); 162 notifyAll(); 163 } 164 registerOrganizerIfNeeded()165 public void registerOrganizerIfNeeded() { 166 synchronized (this) { 167 if (mRegistered) return; 168 169 NestedShellPermission.run(() -> { 170 registerOrganizer(); 171 }); 172 mRegistered = true; 173 } 174 } 175 unregisterOrganizerIfNeeded()176 public void unregisterOrganizerIfNeeded() { 177 synchronized (this) { 178 if (!mRegistered) return; 179 mRegistered = false; 180 181 NestedShellPermission.run(() -> { 182 dismissSplitScreen(); 183 184 deleteRootTask(mRootPrimary.getToken()); 185 mRootPrimary = null; 186 mPrimaryCookie = null; 187 mPrimaryChildrenTaskIds.clear(); 188 deleteRootTask(mRootSecondary.getToken()); 189 mRootSecondary = null; 190 mSecondaryCookie = null; 191 mSecondaryChildrenTaskIds.clear(); 192 mPrePrimarySplitTaskInfo = null; 193 mPreSecondarySplitTaskInfo = null; 194 195 super.unregisterOrganizer(); 196 }); 197 } 198 } 199 putTaskInSplitPrimary(int taskId)200 public void putTaskInSplitPrimary(int taskId) { 201 NestedShellPermission.run(() -> { 202 synchronized (this) { 203 registerOrganizerIfNeeded(); 204 ActivityManager.RunningTaskInfo taskInfo = getTaskInfo(taskId); 205 final WindowContainerTransaction t = new WindowContainerTransaction() 206 .setBounds(mRootPrimary.getToken(), mPrimaryBounds) 207 .setBounds(taskInfo.getToken(), null) 208 .setWindowingMode(taskInfo.getToken(), WINDOWING_MODE_UNDEFINED) 209 .reparent(taskInfo.getToken(), mRootPrimary.getToken(), true /* onTop */) 210 .reorder(mRootPrimary.getToken(), true /* onTop */); 211 applyTransaction(t); 212 mPrePrimarySplitTaskInfo = taskInfo; 213 214 waitForAndAssert( 215 o -> mPrimaryChildrenTaskIds.contains(taskId), 216 "Can't put putTaskInSplitPrimary taskId=" + taskId); 217 218 Log.e(TAG, "putTaskInSplitPrimary taskId=" + taskId); 219 } 220 }); 221 } 222 putTaskInSplitSecondary(int taskId)223 public void putTaskInSplitSecondary(int taskId) { 224 NestedShellPermission.run(() -> { 225 synchronized (this) { 226 registerOrganizerIfNeeded(); 227 ActivityManager.RunningTaskInfo taskInfo = getTaskInfo(taskId); 228 final WindowContainerTransaction t = new WindowContainerTransaction() 229 .setBounds(mRootSecondary.getToken(), mSecondaryBounds) 230 .setBounds(taskInfo.getToken(), null) 231 .setWindowingMode(taskInfo.getToken(), WINDOWING_MODE_UNDEFINED) 232 .reparent(taskInfo.getToken(), mRootSecondary.getToken(), true /* onTop */) 233 .reorder(mRootSecondary.getToken(), true /* onTop */); 234 applyTransaction(t); 235 mPreSecondarySplitTaskInfo = taskInfo; 236 237 waitForAndAssert( 238 o -> mSecondaryChildrenTaskIds.contains(taskId), 239 "Can't put putTaskInSplitSecondary taskId=" + taskId); 240 241 Log.e(TAG, "putTaskInSplitSecondary taskId=" + taskId); 242 } 243 }); 244 } 245 setLaunchRoot(int taskId)246 public void setLaunchRoot(int taskId) { 247 NestedShellPermission.run(() -> { 248 synchronized (this) { 249 final WindowContainerTransaction t = new WindowContainerTransaction() 250 .setLaunchRoot(mKnownTasks.get(taskId).getToken(), 251 CONTROLLED_WINDOWING_MODES, CONTROLLED_ACTIVITY_TYPES); 252 applyTransaction(t); 253 } 254 }); 255 } 256 dismissSplitScreen()257 void dismissSplitScreen() { 258 dismissSplitScreen(false /* primaryOnTop */); 259 } 260 dismissSplitScreen(boolean primaryOnTop)261 public void dismissSplitScreen(boolean primaryOnTop) { 262 dismissSplitScreen(new WindowContainerTransaction(), primaryOnTop); 263 } 264 dismissSplitScreen(WindowContainerTransaction t, boolean primaryOnTop)265 public void dismissSplitScreen(WindowContainerTransaction t, boolean primaryOnTop) { 266 synchronized (this) { 267 NestedShellPermission.run(() -> { 268 t.setLaunchRoot(mRootPrimary.getToken(), null, null) 269 .setLaunchRoot( 270 mRootSecondary.getToken(), 271 null, 272 null) 273 .reparentTasks( 274 primaryOnTop ? mRootSecondary.getToken() : mRootPrimary.getToken(), 275 null /* newParent */, 276 CONTROLLED_WINDOWING_MODES, 277 CONTROLLED_ACTIVITY_TYPES, 278 true /* onTop */) 279 .reparentTasks( 280 primaryOnTop ? mRootPrimary.getToken() : mRootSecondary.getToken(), 281 null /* newParent */, 282 CONTROLLED_WINDOWING_MODES, 283 CONTROLLED_ACTIVITY_TYPES, 284 true /* onTop */); 285 applyPreSplitTaskInfo(t, mPrePrimarySplitTaskInfo); 286 applyPreSplitTaskInfo(t, mPreSecondarySplitTaskInfo); 287 applyTransaction(t); 288 }); 289 } 290 } 291 applyPreSplitTaskInfo(WindowContainerTransaction t, @Nullable ActivityManager.RunningTaskInfo preSplitTaskInfo)292 private void applyPreSplitTaskInfo(WindowContainerTransaction t, 293 @Nullable ActivityManager.RunningTaskInfo preSplitTaskInfo) { 294 if (preSplitTaskInfo == null) { 295 return; 296 } 297 final int restoreWindowingMode = 298 preSplitTaskInfo.getConfiguration().windowConfiguration.getWindowingMode(); 299 // TODO(b/215556258): We should also consider restoring WINDOWING_MODE_FREEFORM and its 300 // bounds. For now, restoring freeform when dismissing split seems to mess up activity 301 // lifecycle especially with shell transitions enabled. 302 if (restoreWindowingMode == WINDOWING_MODE_FULLSCREEN) { 303 t.setWindowingMode(preSplitTaskInfo.getToken(), restoreWindowingMode); 304 t.setBounds(preSplitTaskInfo.getToken(), null); 305 } 306 } 307 setRootPrimaryTaskBounds(Rect bounds)308 public void setRootPrimaryTaskBounds(Rect bounds) { 309 setTaskBounds(mRootPrimary.getToken(), bounds); 310 } 311 setRootSecondaryTaskBounds(Rect bounds)312 void setRootSecondaryTaskBounds(Rect bounds) { 313 setTaskBounds(mRootSecondary.getToken(), bounds); 314 } 315 getPrimaryTaskBounds()316 public Rect getPrimaryTaskBounds() { 317 return mPrimaryBounds; 318 } 319 getSecondaryTaskBounds()320 public Rect getSecondaryTaskBounds() { 321 return mSecondaryBounds; 322 } 323 setTaskBounds(WindowContainerToken container, Rect bounds)324 private void setTaskBounds(WindowContainerToken container, Rect bounds) { 325 synchronized (this) { 326 NestedShellPermission.run(() -> { 327 final WindowContainerTransaction t = new WindowContainerTransaction() 328 .setBounds(container, bounds); 329 applyTransaction(t); 330 }); 331 } 332 } 333 getPrimarySplitTaskCount()334 public int getPrimarySplitTaskCount() { 335 return mPrimaryChildrenTaskIds.size(); 336 } 337 getSecondarySplitTaskCount()338 public int getSecondarySplitTaskCount() { 339 return mSecondaryChildrenTaskIds.size(); 340 } 341 getPrimarySplitTaskId()342 public int getPrimarySplitTaskId() { 343 return mRootPrimary != null ? mRootPrimary.taskId : INVALID_TASK_ID; 344 } 345 getSecondarySplitTaskId()346 public int getSecondarySplitTaskId() { 347 return mRootSecondary != null ? mRootSecondary.taskId : INVALID_TASK_ID; 348 } 349 getTaskInfo(int taskId)350 public ActivityManager.RunningTaskInfo getTaskInfo(int taskId) { 351 synchronized (this) { 352 ActivityManager.RunningTaskInfo taskInfo = mKnownTasks.get(taskId); 353 if (taskInfo != null) return taskInfo; 354 355 final List<ActivityManager.RunningTaskInfo> rootTasks = getRootTasks(DEFAULT_DISPLAY, 356 null); 357 for (ActivityManager.RunningTaskInfo info : rootTasks) { 358 addTask(info); 359 } 360 361 return mKnownTasks.get(taskId); 362 } 363 } 364 365 /** Waits and asserts that given Task to appear. */ waitForAndAssertTaskAppeared(int taskId)366 public void waitForAndAssertTaskAppeared(int taskId) { 367 synchronized (this) { 368 waitForAndAssert(o -> mKnownTasks.containsKey(taskId), "Can't find task=" + taskId); 369 } 370 } 371 372 @Override onTaskAppeared( @onNull ActivityManager.RunningTaskInfo taskInfo, SurfaceControl leash)373 public void onTaskAppeared( 374 @NonNull ActivityManager.RunningTaskInfo taskInfo, SurfaceControl leash) { 375 synchronized (this) { 376 notifyOnEnd(() -> { 377 SurfaceControl.Transaction t = new SurfaceControl.Transaction(); 378 t.setVisibility(leash, true /* visible */); 379 addTask(taskInfo, leash, t); 380 t.apply(); 381 }); 382 } 383 } 384 385 @Override onTaskVanished(@onNull ActivityManager.RunningTaskInfo taskInfo)386 public void onTaskVanished(@NonNull ActivityManager.RunningTaskInfo taskInfo) { 387 synchronized (this) { 388 removeTask(taskInfo); 389 } 390 } 391 392 @Override onTaskInfoChanged(ActivityManager.RunningTaskInfo taskInfo)393 public void onTaskInfoChanged(ActivityManager.RunningTaskInfo taskInfo) { 394 synchronized (this) { 395 notifyOnEnd(() -> { 396 SurfaceControl.Transaction t = new SurfaceControl.Transaction(); 397 addTask(taskInfo, null /* leash */, t); 398 t.apply(); 399 }); 400 } 401 } 402 addTask(ActivityManager.RunningTaskInfo taskInfo)403 private void addTask(ActivityManager.RunningTaskInfo taskInfo) { 404 addTask(taskInfo, null /* SurfaceControl */, null /* Transaction */); 405 } 406 addTask(ActivityManager.RunningTaskInfo taskInfo, SurfaceControl leash, SurfaceControl.Transaction t)407 private void addTask(ActivityManager.RunningTaskInfo taskInfo, SurfaceControl leash, 408 SurfaceControl.Transaction t) { 409 mKnownTasks.put(taskInfo.taskId, taskInfo); 410 if (leash != null) { 411 mTaskLeashes.put(taskInfo.taskId, leash); 412 } else { 413 leash = mTaskLeashes.get(taskInfo.taskId); 414 } 415 if (taskInfo.hasParentTask()) { 416 Rect sourceCrop = null; 417 if (mRootPrimary != null 418 && mRootPrimary.taskId == taskInfo.getParentTaskId()) { 419 sourceCrop = new Rect(mPrimaryBounds); 420 mPrimaryChildrenTaskIds.add(taskInfo.taskId); 421 } else if (mRootSecondary != null 422 && mRootSecondary.taskId == taskInfo.getParentTaskId()) { 423 sourceCrop = new Rect(mSecondaryBounds); 424 mSecondaryChildrenTaskIds.add(taskInfo.taskId); 425 } 426 if (t != null && leash != null && sourceCrop != null) { 427 sourceCrop.offsetTo(0, 0); 428 t.setGeometry(leash, sourceCrop, sourceCrop, Surface.ROTATION_0); 429 } 430 return; 431 } 432 433 if (mRootPrimary == null 434 && mPrimaryCookie != null 435 && taskInfo.containsLaunchCookie(mPrimaryCookie)) { 436 mRootPrimary = taskInfo; 437 if (t != null && leash != null) { 438 Rect sourceCrop = new Rect(mPrimaryBounds); 439 sourceCrop.offsetTo(0, 0); 440 t.setGeometry(leash, sourceCrop, mPrimaryBounds, Surface.ROTATION_0); 441 } 442 return; 443 } 444 445 if (mRootSecondary == null 446 && mSecondaryCookie != null 447 && taskInfo.containsLaunchCookie(mSecondaryCookie)) { 448 mRootSecondary = taskInfo; 449 if (t != null && leash != null) { 450 Rect sourceCrop = new Rect(mSecondaryBounds); 451 sourceCrop.offsetTo(0, 0); 452 t.setGeometry(leash, sourceCrop, mSecondaryBounds, Surface.ROTATION_0); 453 } 454 return; 455 } 456 457 if (t == null || leash == null) { 458 return; 459 } 460 WindowConfiguration config = taskInfo.getConfiguration().windowConfiguration; 461 Rect bounds = config.getBounds(); 462 Rect sourceCrop = null; 463 if (config.getWindowingMode() != WINDOWING_MODE_FULLSCREEN) { 464 sourceCrop = new Rect(bounds); 465 sourceCrop.offsetTo(0, 0); 466 } 467 t.setGeometry(leash, sourceCrop, bounds, Surface.ROTATION_0); 468 } 469 removeTask(ActivityManager.RunningTaskInfo taskInfo)470 private void removeTask(ActivityManager.RunningTaskInfo taskInfo) { 471 final int taskId = taskInfo.taskId; 472 // ignores cleanup on duplicated removal request 473 if (mKnownTasks.remove(taskId) == null) { 474 return; 475 } 476 mTaskLeashes.remove(taskId); 477 mPrimaryChildrenTaskIds.remove(taskId); 478 mSecondaryChildrenTaskIds.remove(taskId); 479 480 if ((mRootPrimary != null && taskId == mRootPrimary.taskId) 481 || (mRootSecondary != null && taskId == mRootSecondary.taskId)) { 482 unregisterOrganizerIfNeeded(); 483 } 484 } 485 } 486