1 /* 2 * Copyright (C) 2024 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 package android.media.projection.cts; 17 18 import static android.content.pm.PackageManager.FEATURE_SCREEN_LANDSCAPE; 19 import static android.content.pm.PackageManager.FEATURE_SCREEN_PORTRAIT; 20 import static android.server.wm.CtsWindowInfoUtils.assertAndDumpWindowState; 21 import static android.server.wm.CtsWindowInfoUtils.waitForStableWindowGeometry; 22 import static android.server.wm.CtsWindowInfoUtils.waitForWindowInfo; 23 import static android.view.Surface.ROTATION_270; 24 25 import static com.android.compatibility.common.util.SystemUtil.runWithShellPermissionIdentity; 26 27 import static com.google.common.truth.Truth.assertThat; 28 29 import static org.junit.Assume.assumeTrue; 30 31 import android.annotation.NonNull; 32 import android.annotation.Nullable; 33 import android.app.Activity; 34 import android.app.ActivityManager; 35 import android.app.ActivityOptions; 36 import android.content.Context; 37 import android.content.Intent; 38 import android.graphics.Bitmap; 39 import android.graphics.PixelFormat; 40 import android.graphics.Point; 41 import android.graphics.Rect; 42 import android.hardware.display.VirtualDisplay; 43 import android.media.Image; 44 import android.media.ImageReader; 45 import android.media.projection.MediaProjection; 46 import android.os.Bundle; 47 import android.os.Environment; 48 import android.os.Handler; 49 import android.os.IBinder; 50 import android.os.Looper; 51 import android.os.UserHandle; 52 import android.server.wm.MediaProjectionHelper; 53 import android.server.wm.RotationSession; 54 import android.server.wm.WindowManagerStateHelper; 55 import android.util.DisplayMetrics; 56 import android.util.Log; 57 import android.view.Surface; 58 import android.view.WindowMetrics; 59 import android.window.WindowInfosListenerForTest.WindowInfo; 60 61 import androidx.test.core.app.ActivityScenario; 62 import androidx.test.platform.app.InstrumentationRegistry; 63 64 import com.android.compatibility.common.util.NonMainlineTest; 65 66 import org.junit.After; 67 import org.junit.Before; 68 import org.junit.Test; 69 70 import java.io.File; 71 import java.io.FileOutputStream; 72 import java.nio.ByteBuffer; 73 import java.util.concurrent.CountDownLatch; 74 import java.util.concurrent.TimeUnit; 75 import java.util.function.Predicate; 76 import java.util.function.Supplier; 77 78 /** 79 * Test {@link MediaProjection} successfully mirrors the display contents. 80 * 81 * <p>Validate that mirrored views are the expected size, for both full display and single app 82 * capture (if offered). Instead of examining the pixels match exactly (which is historically a 83 * flaky way of validating mirroring), examine the structure of the mirrored hierarchy, to ensure 84 * that mirroring is initiated correctly, and any transformations are applied as expected. 85 * 86 * <p>Run with: 87 * atest CtsMediaProjectionTestCases:MediaProjectionMirroringTest 88 */ 89 @NonMainlineTest 90 public class MediaProjectionMirroringTest { 91 private static final String TAG = "MediaProjectionMirroringTest-FOO"; 92 private static final int SCREENSHOT_TIMEOUT_MS = 1000; 93 // Enable debug mode to save screenshots from MediaProjection session. 94 private static final boolean DEBUG_MODE = false; 95 private static final String VIRTUAL_DISPLAY = "MirroringTestVD"; 96 private Context mContext; 97 // Manage a MediaProjection capture session. 98 private final MediaProjectionHelper mMediaProjectionHelper = new MediaProjectionHelper(); 99 100 private MediaProjection mMediaProjection; 101 private MediaProjection.Callback mMediaProjectionCallback = 102 new MediaProjection.Callback() { 103 @Override 104 public void onStop() { 105 super.onStop(); 106 } 107 108 @Override 109 public void onCapturedContentResize(int width, int height) { 110 super.onCapturedContentResize(width, height); 111 } 112 113 @Override 114 public void onCapturedContentVisibilityChanged(boolean isVisible) { 115 super.onCapturedContentVisibilityChanged(isVisible); 116 } 117 }; 118 private ImageReader mImageReader; 119 private CountDownLatch mScreenshotCountDownLatch; 120 private VirtualDisplay mVirtualDisplay; 121 private final ActivityOptions.LaunchCookie mLaunchCookie = new ActivityOptions.LaunchCookie(); 122 public ActivityScenario<TestRotationActivity> mTestRotationActivityActivityScenario; 123 private Activity mActivity; 124 private final WindowManagerStateHelper mWmState = new WindowManagerStateHelper(); 125 /** 126 * Whether to wait for the rotation to be stable state after testing. It can be set if the 127 * display rotation may be changed by test. 128 */ 129 private boolean mWaitForRotationOnTearDown; 130 131 @Before setUp()132 public void setUp() { 133 mContext = InstrumentationRegistry.getInstrumentation().getContext(); 134 runWithShellPermissionIdentity(() -> { 135 mContext.getPackageManager().revokeRuntimePermission( 136 mContext.getPackageName(), 137 android.Manifest.permission.SYSTEM_ALERT_WINDOW, 138 new UserHandle(ActivityManager.getCurrentUser())); 139 }); 140 mMediaProjection = null; 141 if (DEBUG_MODE) { 142 mScreenshotCountDownLatch = new CountDownLatch(1); 143 } 144 } 145 146 @After tearDown()147 public void tearDown() { 148 if (mMediaProjection != null) { 149 if (mMediaProjectionCallback != null) { 150 mMediaProjection.unregisterCallback(mMediaProjectionCallback); 151 mMediaProjectionCallback = null; 152 } 153 mMediaProjection.stop(); 154 mMediaProjection = null; 155 } 156 if (mImageReader != null) { 157 mImageReader = null; 158 } 159 if (mVirtualDisplay != null) { 160 mVirtualDisplay.getSurface().release(); 161 mVirtualDisplay.release(); 162 mVirtualDisplay = null; 163 } 164 if (mWaitForRotationOnTearDown) { 165 mWmState.waitForDisplayUnfrozen(); 166 } 167 } 168 169 // Validate that the mirrored hierarchy is the expected size. 170 @Test testDisplayCapture()171 public void testDisplayCapture() { 172 ActivityScenario<Activity> activityScenario = 173 ActivityScenario.launch(new Intent(mContext, Activity.class)); 174 activityScenario.onActivity(activity -> mActivity = activity); 175 176 final WindowMetrics maxWindowMetrics = 177 mActivity.getWindowManager().getMaximumWindowMetrics(); 178 final Rect activityRect = new Rect(); 179 180 // Select full screen capture. 181 mMediaProjectionHelper.authorizeMediaProjection(); 182 183 // Start capture of the entire display. 184 mMediaProjection = mMediaProjectionHelper.startMediaProjection(); 185 mVirtualDisplay = createVirtualDisplay(maxWindowMetrics.getBounds(), "testDisplayCapture"); 186 waitForLatestScreenshot(); 187 188 // Get the bounds of the activity on screen - use getGlobalVisibleRect to account for 189 // possible insets caused by DisplayCutout 190 mActivity.getWindow().getDecorView().getGlobalVisibleRect(activityRect); 191 192 validateMirroredHierarchy(mActivity, 193 mVirtualDisplay.getDisplay().getDisplayId(), 194 new Point(activityRect.width(), activityRect.height())); 195 activityScenario.close(); 196 } 197 198 // Validate that the mirrored hierarchy is the expected size after rotating the default display. 199 @Test testDisplayCapture_rotation()200 public void testDisplayCapture_rotation() { 201 assumeTrue("Skipping test: no rotation support", supportsRotation()); 202 203 mTestRotationActivityActivityScenario = 204 ActivityScenario.launch(new Intent(mContext, TestRotationActivity.class)); 205 mTestRotationActivityActivityScenario.onActivity(activity -> mActivity = activity); 206 207 final RotationSession rotationSession = createManagedRotationSession(); 208 final WindowMetrics maxWindowMetrics = 209 mActivity.getWindowManager().getMaximumWindowMetrics(); 210 final Rect activityRect = new Rect(); 211 final int initialRotation = mActivity.getDisplay().getRotation(); 212 213 // Select full screen capture. 214 mMediaProjectionHelper.authorizeMediaProjection(); 215 216 // Start capture of the entire display. 217 mMediaProjection = mMediaProjectionHelper.startMediaProjection(); 218 mVirtualDisplay = createVirtualDisplay(maxWindowMetrics.getBounds(), 219 "testDisplayCapture_rotation"); 220 221 rotateDeviceAndWaitForActivity(rotationSession, initialRotation); 222 223 // Get the bounds of the activity on screen - use getGlobalVisibleRect to account for 224 // possible insets caused by DisplayCutout 225 mActivity.getWindow().getDecorView().getGlobalVisibleRect(activityRect); 226 227 final Point mirroredSize = calculateScaledMirroredActivitySize( 228 mActivity.getWindowManager().getCurrentWindowMetrics(), mVirtualDisplay, 229 new Point(activityRect.width(), activityRect.height())); 230 validateMirroredHierarchy(mActivity, mVirtualDisplay.getDisplay().getDisplayId(), 231 mirroredSize); 232 233 rotationSession.close(); 234 mTestRotationActivityActivityScenario.close(); 235 } 236 237 // Validate that the mirrored hierarchy is the expected size. 238 @Test testSingleAppCapture()239 public void testSingleAppCapture() { 240 final ActivityScenario<Activity> activityScenario = ActivityScenario.launch( 241 new Intent(mContext, Activity.class), 242 createActivityScenarioWithLaunchCookie(mLaunchCookie) 243 ); 244 activityScenario.onActivity(activity -> mActivity = activity); 245 final WindowMetrics maxWindowMetrics = 246 mActivity.getWindowManager().getMaximumWindowMetrics(); 247 final Rect activityRect = new Rect(); 248 249 // Select single app capture if supported. 250 mMediaProjectionHelper.authorizeMediaProjection(mLaunchCookie); 251 252 // Start capture of the single app. 253 mMediaProjection = mMediaProjectionHelper.startMediaProjection(); 254 mVirtualDisplay = createVirtualDisplay(maxWindowMetrics.getBounds(), 255 "testSingleAppCapture"); 256 waitForLatestScreenshot(); 257 258 // Get the bounds of the activity on screen - use getGlobalVisibleRect to account for 259 // possible insets caused by DisplayCutout 260 mActivity.getWindow().getDecorView().getGlobalVisibleRect(activityRect); 261 262 validateMirroredHierarchy(mActivity, 263 mVirtualDisplay.getDisplay().getDisplayId(), 264 new Point(activityRect.width(), activityRect.height())); 265 activityScenario.close(); 266 } 267 268 // TODO (b/284968776): test single app capture in split screen 269 270 /** 271 * Returns ActivityOptions with the given launch cookie set. 272 */ createActivityScenarioWithLaunchCookie( @onNull ActivityOptions.LaunchCookie launchCookie)273 private static Bundle createActivityScenarioWithLaunchCookie( 274 @NonNull ActivityOptions.LaunchCookie launchCookie) { 275 ActivityOptions activityOptions = ActivityOptions.makeBasic(); 276 activityOptions.setLaunchCookie(launchCookie); 277 return activityOptions.toBundle(); 278 } 279 createVirtualDisplay(Rect displayBounds, String methodName)280 private VirtualDisplay createVirtualDisplay(Rect displayBounds, String methodName) { 281 mImageReader = ImageReader.newInstance(displayBounds.width(), displayBounds.height(), 282 PixelFormat.RGBA_8888, /* maxImages= */ 1); 283 if (DEBUG_MODE) { 284 ScreenshotListener screenshotListener = new ScreenshotListener(methodName, 285 mScreenshotCountDownLatch); 286 mImageReader.setOnImageAvailableListener(screenshotListener, 287 new Handler(Looper.getMainLooper())); 288 } 289 mMediaProjection.registerCallback(mMediaProjectionCallback, 290 new Handler(Looper.getMainLooper())); 291 return mMediaProjection.createVirtualDisplay(VIRTUAL_DISPLAY + "_" + methodName, 292 displayBounds.width(), displayBounds.height(), 293 DisplayMetrics.DENSITY_HIGH, /* flags= */ 0, 294 mImageReader.getSurface(), /* callback= */ 295 null, new Handler(Looper.getMainLooper())); 296 } 297 298 /** 299 * Rotates the device 90 degrees & waits for the display & activity configuration to stabilize. 300 */ rotateDeviceAndWaitForActivity( @onNull RotationSession rotationSession, @Surface.Rotation int initialRotation)301 private void rotateDeviceAndWaitForActivity( 302 @NonNull RotationSession rotationSession, @Surface.Rotation int initialRotation) { 303 // Rotate the device by 90 degrees 304 rotationSession.set((initialRotation + 1) % (ROTATION_270 + 1), 305 /* waitForDeviceRotation=*/ true); 306 waitForLatestScreenshot(); 307 try { 308 waitForStableWindowGeometry(SCREENSHOT_TIMEOUT_MS, TimeUnit.MILLISECONDS); 309 } catch (InterruptedException e) { 310 Log.e(TAG, "Unable to wait for window to stabilize after rotation: " + e.getMessage()); 311 } 312 // Re-fetch the activity since reference may have been modified during rotation. 313 mTestRotationActivityActivityScenario.onActivity(activity -> mActivity = activity); 314 } 315 316 /** 317 * Calculate the size of the activity, scaled to fit on the VirtualDisplay. 318 * 319 * @param currentWindowMetrics The size of the source activity, before it is mirrored 320 * @param virtualDisplay The VirtualDisplay the mirrored content is sent to and scaled to 321 * fit 322 * @return The expected size of the mirrored activity on the VirtualDisplay 323 */ calculateScaledMirroredActivitySize( @onNull WindowMetrics currentWindowMetrics, @NonNull VirtualDisplay virtualDisplay, @Nullable Point visibleBounds)324 private static Point calculateScaledMirroredActivitySize( 325 @NonNull WindowMetrics currentWindowMetrics, 326 @NonNull VirtualDisplay virtualDisplay, @Nullable Point visibleBounds) { 327 // Calculate the aspect ratio of the original activity. 328 final Point currentBounds = new Point(currentWindowMetrics.getBounds().width(), 329 currentWindowMetrics.getBounds().height()); 330 final float aspectRatio = currentBounds.x * 1f / currentBounds.y; 331 // Find the size of the surface we are mirroring to. 332 final Point surfaceSize = virtualDisplay.getSurface().getDefaultSize(); 333 int mirroredWidth; 334 int mirroredHeight; 335 336 // Calculate any width & height deltas caused by DisplayCutout insets 337 Point sizeDifference = new Point(); 338 if (visibleBounds != null) { 339 int widthDifference = currentBounds.x - visibleBounds.x; 340 int heightDifference = currentBounds.y - visibleBounds.y; 341 sizeDifference.set(widthDifference, heightDifference); 342 } 343 344 if (surfaceSize.x < surfaceSize.y) { 345 // Output surface is portrait, so its width constrains. The mirrored activity is 346 // scaled down to fill the width entirely, and will have horizontal black bars at the 347 // top and bottom. 348 // Also apply scaled insets, to handle case where device has a display cutout which 349 // shifts the content horizontally when landscape. 350 int adjustedHorizontalInsets = Math.round(sizeDifference.x / aspectRatio); 351 int adjustedVerticalInsets = Math.round(sizeDifference.y / aspectRatio); 352 mirroredWidth = surfaceSize.x - adjustedHorizontalInsets; 353 mirroredHeight = Math.round(surfaceSize.x / aspectRatio) - adjustedVerticalInsets; 354 } else { 355 // Output surface is landscape, so its height constrains. The mirrored activity is 356 // scaled down to fill the height entirely, and will have horizontal black bars on the 357 // left and right. 358 // Also apply scaled insets, to handle case where device has a display cutout which 359 // shifts the content vertically when portrait. 360 int adjustedHorizontalInsets = Math.round(sizeDifference.x * aspectRatio); 361 int adjustedVerticalInsets = Math.round(sizeDifference.y * aspectRatio); 362 mirroredWidth = Math.round(surfaceSize.y * aspectRatio) - adjustedHorizontalInsets; 363 mirroredHeight = surfaceSize.y - adjustedVerticalInsets; 364 } 365 return new Point(mirroredWidth, mirroredHeight); 366 } 367 368 /** 369 * Validate the given activity is in the hierarchy mirrored to the VirtualDisplay. 370 * 371 * <p>Note that the hierarchy is present on the VirtualDisplay because the hierarchy is mirrored 372 * to the Surface provided to #createVirtualDisplay. 373 * 374 * @param activity The activity that we expect to be mirrored 375 * @param virtualDisplayId The id of the virtual display we are mirroring to 376 * @param expectedWindowSize The expected size of the mirrored activity 377 */ validateMirroredHierarchy( Activity activity, int virtualDisplayId, @NonNull Point expectedWindowSize)378 private static void validateMirroredHierarchy( 379 Activity activity, int virtualDisplayId, 380 @NonNull Point expectedWindowSize) { 381 Predicate<WindowInfo> hasExpectedDimensions = 382 windowInfo -> windowInfo.bounds.width() == expectedWindowSize.x 383 && windowInfo.bounds.height() == expectedWindowSize.y; 384 Supplier<IBinder> taskWindowTokenSupplier = 385 activity.getWindow().getDecorView()::getWindowToken; 386 try { 387 boolean condition = waitForWindowInfo(hasExpectedDimensions, 5, TimeUnit.SECONDS, 388 taskWindowTokenSupplier, virtualDisplayId); 389 assertAndDumpWindowState(TAG, 390 "Mirrored activity isn't the expected size of " + expectedWindowSize, 391 condition); 392 } catch (InterruptedException e) { 393 throw new RuntimeException(e); 394 } 395 } 396 createManagedRotationSession()397 private RotationSession createManagedRotationSession() { 398 mWaitForRotationOnTearDown = true; 399 return new RotationSession(mWmState); 400 } 401 402 /** 403 * Rotation support is indicated by explicitly having both landscape and portrait 404 * features or not listing either at all. 405 */ supportsRotation()406 protected boolean supportsRotation() { 407 final boolean supportsLandscape = hasDeviceFeature(FEATURE_SCREEN_LANDSCAPE); 408 final boolean supportsPortrait = hasDeviceFeature(FEATURE_SCREEN_PORTRAIT); 409 return (supportsLandscape && supportsPortrait) 410 || (!supportsLandscape && !supportsPortrait); 411 } 412 hasDeviceFeature(final String requiredFeature)413 protected boolean hasDeviceFeature(final String requiredFeature) { 414 return mContext.getPackageManager() 415 .hasSystemFeature(requiredFeature); 416 } 417 418 /** 419 * Stub activity for launching an activity meant to be rotated. 420 */ 421 public static class TestRotationActivity extends Activity { 422 // Stub 423 } 424 425 /** 426 * Wait for any screenshot that has been received already. Assumes that the countdown 427 * latch is already set. 428 */ waitForLatestScreenshot()429 private void waitForLatestScreenshot() { 430 if (DEBUG_MODE) { 431 // wait until we've received a screenshot 432 try { 433 assertThat(mScreenshotCountDownLatch.await(SCREENSHOT_TIMEOUT_MS, 434 TimeUnit.MILLISECONDS)).isTrue(); 435 } catch (InterruptedException e) { 436 Log.e(TAG, e.toString()); 437 } 438 } 439 } 440 441 /** 442 * Save MediaProjection's screenshots to the device to help debug test failures. 443 */ 444 public static class ScreenshotListener implements ImageReader.OnImageAvailableListener { 445 private final CountDownLatch mCountDownLatch; 446 private final String mMethodName; 447 private int mCurrentScreenshot = 0; 448 // How often to save an image 449 private static final int SCREENSHOT_FREQUENCY = 5; 450 ScreenshotListener(@onNull String methodName, @NonNull CountDownLatch latch)451 public ScreenshotListener(@NonNull String methodName, 452 @NonNull CountDownLatch latch) { 453 mMethodName = methodName; 454 mCountDownLatch = latch; 455 } 456 457 @Override onImageAvailable(ImageReader reader)458 public void onImageAvailable(ImageReader reader) { 459 if (mCurrentScreenshot % SCREENSHOT_FREQUENCY != 0) { 460 Log.d(TAG, "onImageAvailable - skip this one"); 461 return; 462 } 463 Log.d(TAG, "onImageAvailable - processing"); 464 if (mCountDownLatch != null) { 465 mCountDownLatch.countDown(); 466 } 467 mCurrentScreenshot++; 468 469 final Image image = reader.acquireLatestImage(); 470 471 assertThat(image).isNotNull(); 472 473 final Image.Plane plane = image.getPlanes()[0]; 474 475 assertThat(plane).isNotNull(); 476 477 final int rowPadding = 478 plane.getRowStride() - plane.getPixelStride() * image.getWidth(); 479 final Bitmap bitmap = Bitmap.createBitmap( 480 /* width= */ image.getWidth() + rowPadding / plane.getPixelStride(), 481 /* height= */ image.getHeight(), Bitmap.Config.ARGB_8888); 482 final ByteBuffer buffer = plane.getBuffer(); 483 484 assertThat(buffer).isNotNull(); 485 assertThat(bitmap).isNotNull(); // why null? 486 487 bitmap.copyPixelsFromBuffer(plane.getBuffer()); 488 assertThat(bitmap).isNotNull(); // why null? 489 490 try { 491 // save to virtual sdcard 492 final File outputDirectory = new File(Environment.getExternalStorageDirectory(), 493 "cts." + TAG); 494 Log.d(TAG, "Had to create the directory? " + outputDirectory.mkdir()); 495 final File screenshot = new File(outputDirectory, 496 mMethodName + "_screenshot_" + mCurrentScreenshot + "_" 497 + System.currentTimeMillis() + ".jpg"); 498 final FileOutputStream stream = new FileOutputStream(screenshot); 499 assertThat(stream).isNotNull(); 500 bitmap.compress(Bitmap.CompressFormat.JPEG, 100, stream); 501 stream.close(); 502 image.close(); 503 } catch (Exception e) { 504 Log.e(TAG, "Unable to write out screenshot", e); 505 } 506 } 507 } 508 } 509