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 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 int getPrimarySplitTaskCount() { 335 return mPrimaryChildrenTaskIds.size(); 336 } 337 getSecondarySplitTaskCount()338 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 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 @Override onTaskAppeared(@onNull ActivityManager.RunningTaskInfo taskInfo, SurfaceControl leash)366 public void onTaskAppeared(@NonNull ActivityManager.RunningTaskInfo taskInfo, 367 SurfaceControl leash) { 368 synchronized (this) { 369 notifyOnEnd(() -> { 370 SurfaceControl.Transaction t = new SurfaceControl.Transaction(); 371 t.setVisibility(leash, true /* visible */); 372 addTask(taskInfo, leash, t); 373 t.apply(); 374 }); 375 } 376 } 377 378 @Override onTaskVanished(@onNull ActivityManager.RunningTaskInfo taskInfo)379 public void onTaskVanished(@NonNull ActivityManager.RunningTaskInfo taskInfo) { 380 synchronized (this) { 381 removeTask(taskInfo); 382 } 383 } 384 385 @Override onTaskInfoChanged(ActivityManager.RunningTaskInfo taskInfo)386 public void onTaskInfoChanged(ActivityManager.RunningTaskInfo taskInfo) { 387 synchronized (this) { 388 notifyOnEnd(() -> { 389 SurfaceControl.Transaction t = new SurfaceControl.Transaction(); 390 addTask(taskInfo, null /* leash */, t); 391 t.apply(); 392 }); 393 } 394 } 395 addTask(ActivityManager.RunningTaskInfo taskInfo)396 private void addTask(ActivityManager.RunningTaskInfo taskInfo) { 397 addTask(taskInfo, null /* SurfaceControl */, null /* Transaction */); 398 } 399 addTask(ActivityManager.RunningTaskInfo taskInfo, SurfaceControl leash, SurfaceControl.Transaction t)400 private void addTask(ActivityManager.RunningTaskInfo taskInfo, SurfaceControl leash, 401 SurfaceControl.Transaction t) { 402 mKnownTasks.put(taskInfo.taskId, taskInfo); 403 if (leash != null) { 404 mTaskLeashes.put(taskInfo.taskId, leash); 405 } else { 406 leash = mTaskLeashes.get(taskInfo.taskId); 407 } 408 if (taskInfo.hasParentTask()) { 409 Rect sourceCrop = null; 410 if (mRootPrimary != null 411 && mRootPrimary.taskId == taskInfo.getParentTaskId()) { 412 sourceCrop = new Rect(mPrimaryBounds); 413 mPrimaryChildrenTaskIds.add(taskInfo.taskId); 414 } else if (mRootSecondary != null 415 && mRootSecondary.taskId == taskInfo.getParentTaskId()) { 416 sourceCrop = new Rect(mSecondaryBounds); 417 mSecondaryChildrenTaskIds.add(taskInfo.taskId); 418 } 419 if (t != null && leash != null && sourceCrop != null) { 420 sourceCrop.offsetTo(0, 0); 421 t.setGeometry(leash, sourceCrop, sourceCrop, Surface.ROTATION_0); 422 } 423 return; 424 } 425 426 if (mRootPrimary == null 427 && mPrimaryCookie != null 428 && taskInfo.containsLaunchCookie(mPrimaryCookie)) { 429 mRootPrimary = taskInfo; 430 if (t != null && leash != null) { 431 Rect sourceCrop = new Rect(mPrimaryBounds); 432 sourceCrop.offsetTo(0, 0); 433 t.setGeometry(leash, sourceCrop, mPrimaryBounds, Surface.ROTATION_0); 434 } 435 return; 436 } 437 438 if (mRootSecondary == null 439 && mSecondaryCookie != null 440 && taskInfo.containsLaunchCookie(mSecondaryCookie)) { 441 mRootSecondary = taskInfo; 442 if (t != null && leash != null) { 443 Rect sourceCrop = new Rect(mSecondaryBounds); 444 sourceCrop.offsetTo(0, 0); 445 t.setGeometry(leash, sourceCrop, mSecondaryBounds, Surface.ROTATION_0); 446 } 447 return; 448 } 449 450 if (t == null || leash == null) { 451 return; 452 } 453 WindowConfiguration config = taskInfo.getConfiguration().windowConfiguration; 454 Rect bounds = config.getBounds(); 455 Rect sourceCrop = null; 456 if (config.getWindowingMode() != WINDOWING_MODE_FULLSCREEN) { 457 sourceCrop = new Rect(bounds); 458 sourceCrop.offsetTo(0, 0); 459 } 460 t.setGeometry(leash, sourceCrop, bounds, Surface.ROTATION_0); 461 } 462 removeTask(ActivityManager.RunningTaskInfo taskInfo)463 private void removeTask(ActivityManager.RunningTaskInfo taskInfo) { 464 final int taskId = taskInfo.taskId; 465 // ignores cleanup on duplicated removal request 466 if (mKnownTasks.remove(taskId) == null) { 467 return; 468 } 469 mTaskLeashes.remove(taskId); 470 mPrimaryChildrenTaskIds.remove(taskId); 471 mSecondaryChildrenTaskIds.remove(taskId); 472 473 if ((mRootPrimary != null && taskId == mRootPrimary.taskId) 474 || (mRootSecondary != null && taskId == mRootSecondary.taskId)) { 475 unregisterOrganizerIfNeeded(); 476 } 477 } 478 } 479