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 true /* moveTogether */); 137 wct.setLaunchAdjacentFlagRoot(mRootSecondary.getToken()); 138 applyTransaction(wct); 139 } 140 } 141 waitForAndAssert(Predicate<Object> condition, String failureMessage)142 private void waitForAndAssert(Predicate<Object> condition, String failureMessage) { 143 waitFor(condition); 144 if (!condition.test(this)) { 145 Assert.fail(failureMessage); 146 } 147 } 148 waitFor(Predicate<Object> condition)149 private void waitFor(Predicate<Object> condition) { 150 final long waitTillTime = SystemClock.elapsedRealtime() + TimeUnit.SECONDS.toMillis(5); 151 while (!condition.test(this) 152 && SystemClock.elapsedRealtime() < waitTillTime) { 153 try { 154 wait(TimeUnit.SECONDS.toMillis(5)); 155 } catch (InterruptedException e) { 156 e.printStackTrace(); 157 } 158 } 159 } 160 notifyOnEnd(Runnable r)161 private void notifyOnEnd(Runnable r) { 162 r.run(); 163 notifyAll(); 164 } 165 registerOrganizerIfNeeded()166 private void registerOrganizerIfNeeded() { 167 if (mRegistered) return; 168 169 registerOrganizer(); 170 mRegistered = true; 171 } 172 unregisterOrganizerIfNeeded()173 public void unregisterOrganizerIfNeeded() { 174 synchronized (this) { 175 if (!mRegistered) return; 176 mRegistered = false; 177 178 NestedShellPermission.run(() -> { 179 dismissSplitScreen(); 180 181 deleteRootTask(mRootPrimary.getToken()); 182 mRootPrimary = null; 183 mPrimaryCookie = null; 184 mPrimaryChildrenTaskIds.clear(); 185 deleteRootTask(mRootSecondary.getToken()); 186 mRootSecondary = null; 187 mSecondaryCookie = null; 188 mSecondaryChildrenTaskIds.clear(); 189 mPrePrimarySplitTaskInfo = null; 190 mPreSecondarySplitTaskInfo = null; 191 192 super.unregisterOrganizer(); 193 }); 194 } 195 } 196 putTaskInSplitPrimary(int taskId)197 public void putTaskInSplitPrimary(int taskId) { 198 NestedShellPermission.run(() -> { 199 synchronized (this) { 200 registerOrganizerIfNeeded(); 201 ActivityManager.RunningTaskInfo taskInfo = getTaskInfo(taskId); 202 final WindowContainerTransaction t = new WindowContainerTransaction() 203 .setBounds(mRootPrimary.getToken(), mPrimaryBounds) 204 .setBounds(taskInfo.getToken(), null) 205 .setWindowingMode(taskInfo.getToken(), WINDOWING_MODE_UNDEFINED) 206 .reparent(taskInfo.getToken(), mRootPrimary.getToken(), true /* onTop */) 207 .reorder(mRootPrimary.getToken(), true /* onTop */); 208 applyTransaction(t); 209 mPrePrimarySplitTaskInfo = taskInfo; 210 211 waitForAndAssert( 212 o -> mPrimaryChildrenTaskIds.contains(taskId), 213 "Can't put putTaskInSplitPrimary taskId=" + taskId); 214 215 Log.e(TAG, "putTaskInSplitPrimary taskId=" + taskId); 216 } 217 }); 218 } 219 putTaskInSplitSecondary(int taskId)220 public void putTaskInSplitSecondary(int taskId) { 221 NestedShellPermission.run(() -> { 222 synchronized (this) { 223 registerOrganizerIfNeeded(); 224 ActivityManager.RunningTaskInfo taskInfo = getTaskInfo(taskId); 225 final WindowContainerTransaction t = new WindowContainerTransaction() 226 .setBounds(mRootSecondary.getToken(), mSecondaryBounds) 227 .setBounds(taskInfo.getToken(), null) 228 .setWindowingMode(taskInfo.getToken(), WINDOWING_MODE_UNDEFINED) 229 .reparent(taskInfo.getToken(), mRootSecondary.getToken(), true /* onTop */) 230 .reorder(mRootSecondary.getToken(), true /* onTop */); 231 applyTransaction(t); 232 mPreSecondarySplitTaskInfo = taskInfo; 233 234 waitForAndAssert( 235 o -> mSecondaryChildrenTaskIds.contains(taskId), 236 "Can't put putTaskInSplitSecondary taskId=" + taskId); 237 238 Log.e(TAG, "putTaskInSplitSecondary taskId=" + taskId); 239 } 240 }); 241 } 242 setLaunchRoot(int taskId)243 public void setLaunchRoot(int taskId) { 244 NestedShellPermission.run(() -> { 245 synchronized (this) { 246 final WindowContainerTransaction t = new WindowContainerTransaction() 247 .setLaunchRoot(mKnownTasks.get(taskId).getToken(), 248 CONTROLLED_WINDOWING_MODES, CONTROLLED_ACTIVITY_TYPES); 249 applyTransaction(t); 250 } 251 }); 252 } 253 dismissSplitScreen()254 void dismissSplitScreen() { 255 dismissSplitScreen(false /* primaryOnTop */); 256 } 257 dismissSplitScreen(boolean primaryOnTop)258 void dismissSplitScreen(boolean primaryOnTop) { 259 dismissSplitScreen(new WindowContainerTransaction(), primaryOnTop); 260 } 261 dismissSplitScreen(WindowContainerTransaction t, boolean primaryOnTop)262 void dismissSplitScreen(WindowContainerTransaction t, boolean primaryOnTop) { 263 synchronized (this) { 264 NestedShellPermission.run(() -> { 265 t.setLaunchRoot(mRootPrimary.getToken(), null, null) 266 .setLaunchRoot( 267 mRootSecondary.getToken(), 268 null, 269 null) 270 .reparentTasks( 271 primaryOnTop ? mRootSecondary.getToken() : mRootPrimary.getToken(), 272 null /* newParent */, 273 CONTROLLED_WINDOWING_MODES, 274 CONTROLLED_ACTIVITY_TYPES, 275 true /* onTop */) 276 .reparentTasks( 277 primaryOnTop ? mRootPrimary.getToken() : mRootSecondary.getToken(), 278 null /* newParent */, 279 CONTROLLED_WINDOWING_MODES, 280 CONTROLLED_ACTIVITY_TYPES, 281 true /* onTop */); 282 applyPreSplitTaskInfo(t, mPrePrimarySplitTaskInfo); 283 applyPreSplitTaskInfo(t, mPreSecondarySplitTaskInfo); 284 applyTransaction(t); 285 }); 286 } 287 } 288 applyPreSplitTaskInfo(WindowContainerTransaction t, @Nullable ActivityManager.RunningTaskInfo preSplitTaskInfo)289 private void applyPreSplitTaskInfo(WindowContainerTransaction t, 290 @Nullable ActivityManager.RunningTaskInfo preSplitTaskInfo) { 291 if (preSplitTaskInfo == null) { 292 return; 293 } 294 final int restoreWindowingMode = 295 preSplitTaskInfo.getConfiguration().windowConfiguration.getWindowingMode(); 296 // TODO(b/215556258): We should also consider restoring WINDOWING_MODE_FREEFORM and its 297 // bounds. For now, restoring freeform when dismissing split seems to mess up activity 298 // lifecycle especially with shell transitions enabled. 299 if (restoreWindowingMode == WINDOWING_MODE_FULLSCREEN) { 300 t.setWindowingMode(preSplitTaskInfo.getToken(), restoreWindowingMode); 301 t.setBounds(preSplitTaskInfo.getToken(), null); 302 } 303 } 304 setRootPrimaryTaskBounds(Rect bounds)305 void setRootPrimaryTaskBounds(Rect bounds) { 306 setTaskBounds(mRootPrimary.getToken(), bounds); 307 } 308 setRootSecondaryTaskBounds(Rect bounds)309 void setRootSecondaryTaskBounds(Rect bounds) { 310 setTaskBounds(mRootSecondary.getToken(), bounds); 311 } 312 getPrimaryTaskBounds()313 public Rect getPrimaryTaskBounds() { 314 return mPrimaryBounds; 315 } 316 getSecondaryTaskBounds()317 public Rect getSecondaryTaskBounds() { 318 return mSecondaryBounds; 319 } 320 setTaskBounds(WindowContainerToken container, Rect bounds)321 private void setTaskBounds(WindowContainerToken container, Rect bounds) { 322 synchronized (this) { 323 NestedShellPermission.run(() -> { 324 final WindowContainerTransaction t = new WindowContainerTransaction() 325 .setBounds(container, bounds); 326 applyTransaction(t); 327 }); 328 } 329 } 330 getPrimarySplitTaskCount()331 int getPrimarySplitTaskCount() { 332 return mPrimaryChildrenTaskIds.size(); 333 } 334 getSecondarySplitTaskCount()335 int getSecondarySplitTaskCount() { 336 return mSecondaryChildrenTaskIds.size(); 337 } 338 getPrimarySplitTaskId()339 public int getPrimarySplitTaskId() { 340 return mRootPrimary != null ? mRootPrimary.taskId : INVALID_TASK_ID; 341 } 342 getSecondarySplitTaskId()343 public int getSecondarySplitTaskId() { 344 return mRootSecondary != null ? mRootSecondary.taskId : INVALID_TASK_ID; 345 } 346 getTaskInfo(int taskId)347 ActivityManager.RunningTaskInfo getTaskInfo(int taskId) { 348 synchronized (this) { 349 ActivityManager.RunningTaskInfo taskInfo = mKnownTasks.get(taskId); 350 if (taskInfo != null) return taskInfo; 351 352 final List<ActivityManager.RunningTaskInfo> rootTasks = getRootTasks(DEFAULT_DISPLAY, 353 null); 354 for (ActivityManager.RunningTaskInfo info : rootTasks) { 355 addTask(info); 356 } 357 358 return mKnownTasks.get(taskId); 359 } 360 } 361 362 @Override onTaskAppeared(@onNull ActivityManager.RunningTaskInfo taskInfo, SurfaceControl leash)363 public void onTaskAppeared(@NonNull ActivityManager.RunningTaskInfo taskInfo, 364 SurfaceControl leash) { 365 synchronized (this) { 366 notifyOnEnd(() -> { 367 SurfaceControl.Transaction t = new SurfaceControl.Transaction(); 368 t.setVisibility(leash, true /* visible */); 369 addTask(taskInfo, leash, t); 370 t.apply(); 371 }); 372 } 373 } 374 375 @Override onTaskVanished(@onNull ActivityManager.RunningTaskInfo taskInfo)376 public void onTaskVanished(@NonNull ActivityManager.RunningTaskInfo taskInfo) { 377 synchronized (this) { 378 removeTask(taskInfo); 379 } 380 } 381 382 @Override onTaskInfoChanged(ActivityManager.RunningTaskInfo taskInfo)383 public void onTaskInfoChanged(ActivityManager.RunningTaskInfo taskInfo) { 384 synchronized (this) { 385 notifyOnEnd(() -> { 386 SurfaceControl.Transaction t = new SurfaceControl.Transaction(); 387 addTask(taskInfo, null /* leash */, t); 388 t.apply(); 389 }); 390 } 391 } 392 addTask(ActivityManager.RunningTaskInfo taskInfo)393 private void addTask(ActivityManager.RunningTaskInfo taskInfo) { 394 addTask(taskInfo, null /* SurfaceControl */, null /* Transaction */); 395 } 396 addTask(ActivityManager.RunningTaskInfo taskInfo, SurfaceControl leash, SurfaceControl.Transaction t)397 private void addTask(ActivityManager.RunningTaskInfo taskInfo, SurfaceControl leash, 398 SurfaceControl.Transaction t) { 399 mKnownTasks.put(taskInfo.taskId, taskInfo); 400 if (leash != null) { 401 mTaskLeashes.put(taskInfo.taskId, leash); 402 } else { 403 leash = mTaskLeashes.get(taskInfo.taskId); 404 } 405 if (taskInfo.hasParentTask()) { 406 Rect sourceCrop = null; 407 if (mRootPrimary != null 408 && mRootPrimary.taskId == taskInfo.getParentTaskId()) { 409 sourceCrop = new Rect(mPrimaryBounds); 410 mPrimaryChildrenTaskIds.add(taskInfo.taskId); 411 } else if (mRootSecondary != null 412 && mRootSecondary.taskId == taskInfo.getParentTaskId()) { 413 sourceCrop = new Rect(mSecondaryBounds); 414 mSecondaryChildrenTaskIds.add(taskInfo.taskId); 415 } 416 if (t != null && leash != null && sourceCrop != null) { 417 sourceCrop.offsetTo(0, 0); 418 t.setGeometry(leash, sourceCrop, sourceCrop, Surface.ROTATION_0); 419 } 420 return; 421 } 422 423 if (mRootPrimary == null 424 && mPrimaryCookie != null 425 && taskInfo.containsLaunchCookie(mPrimaryCookie)) { 426 mRootPrimary = taskInfo; 427 if (t != null && leash != null) { 428 Rect sourceCrop = new Rect(mPrimaryBounds); 429 sourceCrop.offsetTo(0, 0); 430 t.setGeometry(leash, sourceCrop, mPrimaryBounds, Surface.ROTATION_0); 431 } 432 return; 433 } 434 435 if (mRootSecondary == null 436 && mSecondaryCookie != null 437 && taskInfo.containsLaunchCookie(mSecondaryCookie)) { 438 mRootSecondary = taskInfo; 439 if (t != null && leash != null) { 440 Rect sourceCrop = new Rect(mSecondaryBounds); 441 sourceCrop.offsetTo(0, 0); 442 t.setGeometry(leash, sourceCrop, mSecondaryBounds, Surface.ROTATION_0); 443 } 444 return; 445 } 446 447 if (t == null || leash == null) { 448 return; 449 } 450 WindowConfiguration config = taskInfo.getConfiguration().windowConfiguration; 451 Rect bounds = config.getBounds(); 452 Rect sourceCrop = null; 453 if (config.getWindowingMode() != WINDOWING_MODE_FULLSCREEN) { 454 sourceCrop = new Rect(bounds); 455 sourceCrop.offsetTo(0, 0); 456 } 457 t.setGeometry(leash, sourceCrop, bounds, Surface.ROTATION_0); 458 } 459 removeTask(ActivityManager.RunningTaskInfo taskInfo)460 private void removeTask(ActivityManager.RunningTaskInfo taskInfo) { 461 final int taskId = taskInfo.taskId; 462 // ignores cleanup on duplicated removal request 463 if (mKnownTasks.remove(taskId) == null) { 464 return; 465 } 466 mTaskLeashes.remove(taskId); 467 mPrimaryChildrenTaskIds.remove(taskId); 468 mSecondaryChildrenTaskIds.remove(taskId); 469 470 if ((mRootPrimary != null && taskId == mRootPrimary.taskId) 471 || (mRootSecondary != null && taskId == mRootSecondary.taskId)) { 472 unregisterOrganizerIfNeeded(); 473 } 474 } 475 } 476