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