1 /* 2 * Copyright (C) 2018 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 com.android.server.wm; 18 19 import static android.hardware.display.DisplayManager.VIRTUAL_DISPLAY_FLAG_OWN_CONTENT_ONLY; 20 import static android.hardware.display.DisplayManager.VIRTUAL_DISPLAY_FLAG_PRESENTATION; 21 import static android.hardware.display.DisplayManager.VIRTUAL_DISPLAY_FLAG_PUBLIC; 22 23 import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation; 24 25 import static com.android.server.wm.utils.CommonUtils.runWithShellPermissionIdentity; 26 27 import static org.junit.Assert.assertEquals; 28 import static org.junit.Assert.assertNotNull; 29 30 import android.app.Activity; 31 import android.app.ActivityManager.RunningTaskInfo; 32 import android.app.ActivityManager.TaskDescription; 33 import android.app.ActivityOptions; 34 import android.app.ActivityTaskManager; 35 import android.app.ITaskStackListener; 36 import android.app.Instrumentation.ActivityMonitor; 37 import android.app.TaskStackListener; 38 import android.content.ComponentName; 39 import android.content.Context; 40 import android.content.Intent; 41 import android.content.pm.ActivityInfo; 42 import android.graphics.PixelFormat; 43 import android.hardware.display.DisplayManager; 44 import android.hardware.display.VirtualDisplay; 45 import android.media.ImageReader; 46 import android.os.Bundle; 47 import android.os.RemoteException; 48 import android.os.SystemClock; 49 import android.platform.test.annotations.Presubmit; 50 import android.text.TextUtils; 51 import android.view.Display; 52 import android.view.ViewGroup; 53 import android.widget.LinearLayout; 54 55 import androidx.test.filters.MediumTest; 56 57 import org.junit.After; 58 import org.junit.Test; 59 60 import java.util.Arrays; 61 import java.util.concurrent.ArrayBlockingQueue; 62 import java.util.concurrent.CountDownLatch; 63 import java.util.concurrent.TimeUnit; 64 import java.util.function.Predicate; 65 66 /** 67 * Build/Install/Run: 68 * atest WmTests:TaskStackChangedListenerTest 69 */ 70 @MediumTest 71 public class TaskStackChangedListenerTest { 72 73 private ITaskStackListener mTaskStackListener; 74 private VirtualDisplay mVirtualDisplay; 75 private ImageReader mImageReader; 76 77 private static final int WAIT_TIMEOUT_MS = 5000; 78 private static final Object sLock = new Object(); 79 80 @After tearDown()81 public void tearDown() throws Exception { 82 if (mTaskStackListener != null) { 83 ActivityTaskManager.getService().unregisterTaskStackListener(mTaskStackListener); 84 } 85 if (mVirtualDisplay != null) { 86 mVirtualDisplay.release(); 87 mImageReader.close(); 88 } 89 } 90 createVirtualDisplay()91 private VirtualDisplay createVirtualDisplay() { 92 final int width = 800; 93 final int height = 600; 94 final int density = 160; 95 final DisplayManager displayManager = getInstrumentation().getContext().getSystemService( 96 DisplayManager.class); 97 mImageReader = ImageReader.newInstance(width, height, PixelFormat.RGBA_8888, 98 2 /* maxImages */); 99 final int flags = VIRTUAL_DISPLAY_FLAG_PRESENTATION | VIRTUAL_DISPLAY_FLAG_OWN_CONTENT_ONLY 100 | VIRTUAL_DISPLAY_FLAG_PUBLIC; 101 final String name = getClass().getSimpleName() + "_VirtualDisplay"; 102 mVirtualDisplay = displayManager.createVirtualDisplay(name, width, height, density, 103 mImageReader.getSurface(), flags); 104 mVirtualDisplay.setSurface(mImageReader.getSurface()); 105 assertNotNull("display must be registered", 106 Arrays.stream(displayManager.getDisplays()).filter( 107 d -> d.getName().equals(name)).findAny()); 108 return mVirtualDisplay; 109 } 110 111 @Test 112 @Presubmit testTaskStackChanged_afterFinish()113 public void testTaskStackChanged_afterFinish() throws Exception { 114 final TestActivity activity = startTestActivity(ActivityA.class); 115 final CountDownLatch latch = new CountDownLatch(1); 116 registerTaskStackChangedListener(new TaskStackListener() { 117 @Override 118 public void onTaskStackChanged() throws RemoteException { 119 latch.countDown(); 120 } 121 }); 122 123 activity.finish(); 124 waitForCallback(latch); 125 } 126 127 @Test 128 @Presubmit testTaskStackChanged_resumeWhilePausing()129 public void testTaskStackChanged_resumeWhilePausing() throws Exception { 130 final CountDownLatch latch = new CountDownLatch(1); 131 registerTaskStackChangedListener(new TaskStackListener() { 132 @Override 133 public void onTaskStackChanged() throws RemoteException { 134 latch.countDown(); 135 } 136 }); 137 138 startTestActivity(ResumeWhilePausingActivity.class); 139 waitForCallback(latch); 140 } 141 142 @Test 143 @Presubmit testTaskDescriptionChanged()144 public void testTaskDescriptionChanged() throws Exception { 145 final Object[] params = new Object[2]; 146 final CountDownLatch latch = new CountDownLatch(1); 147 registerTaskStackChangedListener(new TaskStackListener() { 148 int mTaskId = -1; 149 150 @Override 151 public void onTaskCreated(int taskId, ComponentName componentName) 152 throws RemoteException { 153 mTaskId = taskId; 154 } 155 @Override 156 public void onTaskDescriptionChanged(RunningTaskInfo info) { 157 if (mTaskId == info.taskId && !TextUtils.isEmpty(info.taskDescription.getLabel())) { 158 params[0] = info.taskId; 159 params[1] = info.taskDescription; 160 latch.countDown(); 161 } 162 } 163 }); 164 165 int taskId; 166 synchronized (sLock) { 167 taskId = startTestActivity(ActivityTaskDescriptionChange.class).getTaskId(); 168 } 169 waitForCallback(latch); 170 assertEquals(taskId, params[0]); 171 assertEquals("Test Label", ((TaskDescription) params[1]).getLabel()); 172 } 173 174 @Test 175 @Presubmit testActivityRequestedOrientationChanged()176 public void testActivityRequestedOrientationChanged() throws Exception { 177 final int[] params = new int[2]; 178 final CountDownLatch latch = new CountDownLatch(1); 179 registerTaskStackChangedListener(new TaskStackListener() { 180 @Override 181 public void onActivityRequestedOrientationChanged(int taskId, 182 int requestedOrientation) { 183 params[0] = taskId; 184 params[1] = requestedOrientation; 185 latch.countDown(); 186 } 187 }); 188 int taskId; 189 synchronized (sLock) { 190 taskId = startTestActivity(ActivityRequestedOrientationChange.class).getTaskId(); 191 } 192 waitForCallback(latch); 193 assertEquals(taskId, params[0]); 194 assertEquals(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT, params[1]); 195 } 196 197 /** 198 * Tests for onTaskCreated, onTaskMovedToFront, onTaskRemoved and onTaskRemovalStarted. 199 */ 200 @Test 201 @Presubmit testTaskChangeCallBacks()202 public void testTaskChangeCallBacks() throws Exception { 203 final CountDownLatch taskCreatedLaunchLatch = new CountDownLatch(1); 204 final CountDownLatch taskMovedToFrontLatch = new CountDownLatch(1); 205 final CountDownLatch taskRemovedLatch = new CountDownLatch(1); 206 final CountDownLatch taskRemovalStartedLatch = new CountDownLatch(1); 207 final int[] expectedTaskId = { -1 }; 208 final int[] receivedTaskId = { -1 }; 209 final ComponentName expectedName = new ComponentName(getInstrumentation().getContext(), 210 ActivityTaskChangeCallbacks.class); 211 registerTaskStackChangedListener(new TaskStackListener() { 212 @Override 213 public void onTaskCreated(int taskId, ComponentName componentName) { 214 receivedTaskId[0] = taskId; 215 if (expectedName.equals(componentName)) { 216 taskCreatedLaunchLatch.countDown(); 217 } 218 } 219 220 @Override 221 public void onTaskMovedToFront(RunningTaskInfo info) { 222 receivedTaskId[0] = info.taskId; 223 taskMovedToFrontLatch.countDown(); 224 } 225 226 @Override 227 public void onTaskRemovalStarted(RunningTaskInfo info) { 228 if (expectedTaskId[0] == info.taskId) { 229 taskRemovalStartedLatch.countDown(); 230 } 231 } 232 233 @Override 234 public void onTaskRemoved(int taskId) { 235 if (expectedTaskId[0] == taskId) { 236 taskRemovedLatch.countDown(); 237 } 238 } 239 }); 240 241 final ActivityTaskChangeCallbacks activity = 242 (ActivityTaskChangeCallbacks) startTestActivity(ActivityTaskChangeCallbacks.class); 243 expectedTaskId[0] = activity.getTaskId(); 244 245 // Test for onTaskCreated and onTaskMovedToFront 246 waitForCallback(taskMovedToFrontLatch); 247 assertEquals(0, taskCreatedLaunchLatch.getCount()); 248 assertEquals(expectedTaskId[0], receivedTaskId[0]); 249 250 // Ensure that the window is attached before removal so there will be a detached callback. 251 waitForCallback(activity.mOnAttachedToWindowCountDownLatch); 252 // Test for onTaskRemovalStarted. 253 assertEquals(1, taskRemovalStartedLatch.getCount()); 254 assertEquals(1, taskRemovedLatch.getCount()); 255 activity.finishAndRemoveTask(); 256 waitForCallback(taskRemovalStartedLatch); 257 // onTaskRemovalStarted happens before the activity's window is removed. 258 assertEquals(1, activity.mOnDetachedFromWindowCountDownLatch.getCount()); 259 260 // Test for onTaskRemoved. 261 waitForCallback(taskRemovedLatch); 262 waitForCallback(activity.mOnDetachedFromWindowCountDownLatch); 263 } 264 265 @Test testTaskDisplayChanged()266 public void testTaskDisplayChanged() throws Exception { 267 final int virtualDisplayId = createVirtualDisplay().getDisplay().getDisplayId(); 268 269 // Launch a Activity inside VirtualDisplay 270 CountDownLatch displayChangedLatch1 = new CountDownLatch(1); 271 final Object[] params1 = new Object[1]; 272 registerTaskStackChangedListener(new TaskDisplayChangedListener( 273 virtualDisplayId, params1, displayChangedLatch1)); 274 ActivityOptions options1 = ActivityOptions.makeBasic().setLaunchDisplayId(virtualDisplayId); 275 int taskId1; 276 synchronized (sLock) { 277 taskId1 = startTestActivity(ActivityInVirtualDisplay.class, options1).getTaskId(); 278 } 279 waitForCallback(displayChangedLatch1); 280 281 assertEquals(taskId1, params1[0]); 282 283 // Launch the Activity in the default display, expects that reparenting happens. 284 final Object[] params2 = new Object[1]; 285 final CountDownLatch displayChangedLatch2 = new CountDownLatch(1); 286 registerTaskStackChangedListener( 287 new TaskDisplayChangedListener( 288 Display.DEFAULT_DISPLAY, params2, displayChangedLatch2)); 289 int taskId2; 290 ActivityOptions options2 = ActivityOptions.makeBasic() 291 .setLaunchDisplayId(Display.DEFAULT_DISPLAY); 292 synchronized (sLock) { 293 taskId2 = startTestActivity(ActivityInVirtualDisplay.class, options2).getTaskId(); 294 } 295 waitForCallback(displayChangedLatch2); 296 297 assertEquals(taskId2, params2[0]); 298 assertEquals(taskId1, taskId2); // TaskId should be same since reparenting happens. 299 } 300 301 private static class TaskDisplayChangedListener extends TaskStackListener { 302 private int mDisplayId; 303 private final Object[] mParams; 304 private final CountDownLatch mDisplayChangedLatch; TaskDisplayChangedListener( int displayId, Object[] params, CountDownLatch displayChangedLatch)305 TaskDisplayChangedListener( 306 int displayId, Object[] params, CountDownLatch displayChangedLatch) { 307 mDisplayId = displayId; 308 mParams = params; 309 mDisplayChangedLatch = displayChangedLatch; 310 } 311 @Override onTaskDisplayChanged(int taskId, int displayId)312 public void onTaskDisplayChanged(int taskId, int displayId) throws RemoteException { 313 // Filter out the events for the uninterested displays. 314 // if (displayId != mDisplayId) return; 315 mParams[0] = taskId; 316 mDisplayChangedLatch.countDown(); 317 } 318 }; 319 320 @Presubmit 321 @Test testNotifyTaskRequestedOrientationChanged()322 public void testNotifyTaskRequestedOrientationChanged() throws Exception { 323 final ArrayBlockingQueue<int[]> taskIdAndOrientationQueue = new ArrayBlockingQueue<>(10); 324 registerTaskStackChangedListener(new TaskStackListener() { 325 @Override 326 public void onTaskRequestedOrientationChanged(int taskId, int requestedOrientation) { 327 int[] taskIdAndOrientation = new int[2]; 328 taskIdAndOrientation[0] = taskId; 329 taskIdAndOrientation[1] = requestedOrientation; 330 taskIdAndOrientationQueue.offer(taskIdAndOrientation); 331 } 332 }); 333 334 final LandscapeActivity activity = 335 (LandscapeActivity) startTestActivity(LandscapeActivity.class); 336 337 int[] taskIdAndOrientation = waitForResult(taskIdAndOrientationQueue, 338 candidate -> candidate[0] == activity.getTaskId()); 339 assertNotNull(taskIdAndOrientation); 340 assertEquals(ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE, taskIdAndOrientation[1]); 341 342 activity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_SENSOR_PORTRAIT); 343 taskIdAndOrientation = waitForResult(taskIdAndOrientationQueue, 344 candidate -> candidate[0] == activity.getTaskId()); 345 assertNotNull(taskIdAndOrientation); 346 assertEquals(ActivityInfo.SCREEN_ORIENTATION_SENSOR_PORTRAIT, taskIdAndOrientation[1]); 347 348 activity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED); 349 taskIdAndOrientation = waitForResult(taskIdAndOrientationQueue, 350 candidate -> candidate[0] == activity.getTaskId()); 351 assertNotNull(taskIdAndOrientation); 352 assertEquals(ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED, taskIdAndOrientation[1]); 353 } 354 355 /** 356 * Starts the provided activity and returns the started instance. 357 */ startTestActivity(Class<?> activityClass)358 private TestActivity startTestActivity(Class<?> activityClass) throws InterruptedException { 359 return startTestActivity(activityClass, ActivityOptions.makeBasic()); 360 } 361 startTestActivity(Class<?> activityClass, ActivityOptions options)362 private TestActivity startTestActivity(Class<?> activityClass, ActivityOptions options) 363 throws InterruptedException { 364 final ActivityMonitor monitor = new ActivityMonitor(activityClass.getName(), null, false); 365 getInstrumentation().addMonitor(monitor); 366 final Context context = getInstrumentation().getContext(); 367 runWithShellPermissionIdentity(() -> context.startActivity( 368 new Intent(context, activityClass).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK), 369 options.toBundle())); 370 final TestActivity activity = 371 (TestActivity) monitor.waitForActivityWithTimeout(WAIT_TIMEOUT_MS); 372 if (activity == null) { 373 throw new RuntimeException("Timed out waiting for Activity"); 374 } 375 activity.waitForResumeStateChange(true); 376 return activity; 377 } 378 registerTaskStackChangedListener(ITaskStackListener listener)379 private void registerTaskStackChangedListener(ITaskStackListener listener) throws Exception { 380 if (mTaskStackListener != null) { 381 ActivityTaskManager.getService().unregisterTaskStackListener(mTaskStackListener); 382 } 383 mTaskStackListener = listener; 384 ActivityTaskManager.getService().registerTaskStackListener(listener); 385 } 386 waitForCallback(CountDownLatch latch)387 private void waitForCallback(CountDownLatch latch) { 388 try { 389 final boolean result = latch.await(WAIT_TIMEOUT_MS, TimeUnit.MILLISECONDS); 390 if (!result) { 391 throw new AssertionError("Timed out waiting for task stack change notification"); 392 } 393 } catch (InterruptedException e) { 394 } 395 } 396 waitForResult(ArrayBlockingQueue<T> queue, Predicate<T> predicate)397 private <T> T waitForResult(ArrayBlockingQueue<T> queue, Predicate<T> predicate) { 398 try { 399 final long timeout = SystemClock.uptimeMillis() + TimeUnit.SECONDS.toMillis(15); 400 T result; 401 do { 402 result = queue.poll(timeout - SystemClock.uptimeMillis(), TimeUnit.MILLISECONDS); 403 } while (result != null && !predicate.test(result)); 404 return result; 405 } catch (InterruptedException e) { 406 return null; 407 } 408 } 409 410 public static class TestActivity extends Activity { 411 boolean mIsResumed = false; 412 413 @Override onPostResume()414 protected void onPostResume() { 415 super.onPostResume(); 416 synchronized (this) { 417 mIsResumed = true; 418 notifyAll(); 419 } 420 } 421 422 @Override onPause()423 protected void onPause() { 424 super.onPause(); 425 synchronized (this) { 426 mIsResumed = false; 427 notifyAll(); 428 } 429 } 430 431 /** 432 * If isResumed is {@code true}, sleep the thread until the activity is resumed. 433 * if {@code false}, sleep the thread until the activity is paused. 434 */ 435 @SuppressWarnings("WaitNotInLoop") waitForResumeStateChange(boolean isResumed)436 public void waitForResumeStateChange(boolean isResumed) throws InterruptedException { 437 synchronized (this) { 438 if (mIsResumed == isResumed) { 439 return; 440 } 441 wait(WAIT_TIMEOUT_MS); 442 } 443 assertEquals("The activity resume state change timed out", isResumed, mIsResumed); 444 } 445 } 446 447 public static class ActivityA extends TestActivity {} 448 449 public static class ActivityB extends TestActivity { 450 451 @Override onPostResume()452 protected void onPostResume() { 453 super.onPostResume(); 454 finish(); 455 } 456 } 457 458 public static class ActivityRequestedOrientationChange extends TestActivity { 459 @Override onPostResume()460 protected void onPostResume() { 461 super.onPostResume(); 462 setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT); 463 synchronized (sLock) { 464 // Hold the lock to ensure no one is trying to access fields of this Activity in 465 // this test. 466 finish(); 467 } 468 } 469 } 470 471 public static class ActivityTaskDescriptionChange extends TestActivity { 472 @Override onPostResume()473 protected void onPostResume() { 474 super.onPostResume(); 475 setTaskDescription(new TaskDescription("Test Label")); 476 synchronized (sLock) { 477 // Hold the lock to ensure no one is trying to access fields of this Activity in 478 // this test. 479 finish(); 480 } 481 } 482 } 483 484 public static class ActivityTaskChangeCallbacks extends TestActivity { 485 final CountDownLatch mOnAttachedToWindowCountDownLatch = new CountDownLatch(1); 486 final CountDownLatch mOnDetachedFromWindowCountDownLatch = new CountDownLatch(1); 487 488 @Override onAttachedToWindow()489 public void onAttachedToWindow() { 490 mOnAttachedToWindowCountDownLatch.countDown(); 491 } 492 493 @Override onDetachedFromWindow()494 public void onDetachedFromWindow() { 495 mOnDetachedFromWindowCountDownLatch.countDown(); 496 } 497 } 498 499 public static class ActivityInVirtualDisplay extends TestActivity { 500 501 @Override onCreate(Bundle savedInstanceState)502 public void onCreate(Bundle savedInstanceState) { 503 super.onCreate(savedInstanceState); 504 505 LinearLayout layout = new LinearLayout(this); 506 layout.setLayoutParams(new ViewGroup.LayoutParams( 507 ViewGroup.LayoutParams.MATCH_PARENT, 508 ViewGroup.LayoutParams.MATCH_PARENT)); 509 setContentView(layout); 510 } 511 } 512 513 public static class ResumeWhilePausingActivity extends TestActivity {} 514 515 public static class LandscapeActivity extends TestActivity {} 516 } 517