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