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 org.junit.Assume.assumeTrue; 28 29 import android.annotation.NonNull; 30 import android.annotation.Nullable; 31 import android.app.Activity; 32 import android.app.ActivityOptions; 33 import android.content.Context; 34 import android.content.Intent; 35 import android.graphics.Point; 36 import android.graphics.Rect; 37 import android.hardware.display.VirtualDisplay; 38 import android.media.cts.MediaProjectionRule; 39 import android.media.projection.MediaProjection; 40 import android.os.Bundle; 41 import android.os.IBinder; 42 import android.os.UserHandle; 43 import android.server.wm.RotationSession; 44 import android.server.wm.WindowManagerStateHelper; 45 import android.util.Log; 46 import android.view.Surface; 47 import android.view.WindowMetrics; 48 import android.window.WindowInfosListenerForTest.WindowInfo; 49 50 import androidx.test.core.app.ActivityScenario; 51 import androidx.test.platform.app.InstrumentationRegistry; 52 53 import com.android.compatibility.common.util.FrameworkSpecificTest; 54 55 import org.junit.After; 56 import org.junit.Before; 57 import org.junit.Rule; 58 import org.junit.Test; 59 60 import java.time.Duration; 61 import java.util.function.Predicate; 62 import java.util.function.Supplier; 63 64 /** 65 * Test {@link MediaProjection} successfully mirrors the display contents. 66 * 67 * <p>Validate that mirrored views are the expected size, for both full display and single app 68 * capture (if offered). Instead of examining the pixels match exactly (which is historically a 69 * flaky way of validating mirroring), examine the structure of the mirrored hierarchy, to ensure 70 * that mirroring is initiated correctly, and any transformations are applied as expected. 71 * 72 * <p>Run with: 73 * atest CtsMediaProjectionTestCases:MediaProjectionMirroringTest 74 */ 75 @FrameworkSpecificTest 76 public class MediaProjectionMirroringTest { 77 private static final String TAG = "MediaProjectionMirroringTest"; 78 private static final int TOLERANCE = 1; 79 private static final int TIMEOUT_MS = 1000; 80 private Context mContext; 81 82 @Rule public MediaProjectionRule mMediaProjectionRule = new MediaProjectionRule(); 83 84 private final ActivityOptions.LaunchCookie mLaunchCookie = new ActivityOptions.LaunchCookie(); 85 private final WindowManagerStateHelper mWmState = new WindowManagerStateHelper(); 86 /** 87 * Whether to wait for the rotation to be stable state after testing. It can be set if the 88 * display rotation may be changed by test. 89 */ 90 private boolean mWaitForRotationOnTearDown; 91 92 @Before setUp()93 public void setUp() { 94 mContext = InstrumentationRegistry.getInstrumentation().getContext(); 95 runWithShellPermissionIdentity(() -> { 96 mContext.getPackageManager().revokeRuntimePermission( 97 mContext.getPackageName(), 98 android.Manifest.permission.SYSTEM_ALERT_WINDOW, 99 new UserHandle(mContext.getUserId())); 100 }); 101 } 102 103 @After tearDown()104 public void tearDown() { 105 if (mWaitForRotationOnTearDown) { 106 mWmState.waitForDisplayUnfrozen(); 107 } 108 } 109 110 // Validate that the mirrored hierarchy is the expected size. 111 @Test testDisplayCapture()112 public void testDisplayCapture() throws Exception { 113 Intent testActivityIntent = new Intent(mContext, Activity.class); 114 // Start full screen capture. 115 mMediaProjectionRule.startMediaProjection(); 116 117 final WindowMetrics maxWindowMetrics = 118 mMediaProjectionRule.getActivity().getWindowManager().getMaximumWindowMetrics(); 119 120 VirtualDisplay virtualDisplay = 121 mMediaProjectionRule.createVirtualDisplay( 122 maxWindowMetrics.getBounds().width(), 123 maxWindowMetrics.getBounds().height()); 124 125 try (ActivityScenario<Activity> activityScenario = 126 ActivityScenario.launch(testActivityIntent)) { 127 activityScenario.onActivity( 128 activity -> { 129 // Get the bounds of the activity on screen - use getGlobalVisibleRect to 130 // account for 131 // possible insets caused by DisplayCutout 132 final Rect activityRect = new Rect(); 133 activity.getWindow().getDecorView().getGlobalVisibleRect(activityRect); 134 validateMirroredHierarchy( 135 activity, 136 virtualDisplay.getDisplay().getDisplayId(), 137 new Point(activityRect.width(), activityRect.height())); 138 }); 139 } 140 } 141 142 // Validate that the mirrored hierarchy is the expected size after rotating the default display. 143 @Test testDisplayCapture_rotation()144 public void testDisplayCapture_rotation() throws Exception { 145 assumeTrue("Skipping test: no rotation support", supportsRotation()); 146 147 // Start full screen capture. 148 mMediaProjectionRule.startMediaProjection(); 149 Activity activity = mMediaProjectionRule.getActivity(); 150 151 final WindowMetrics maxWindowMetrics = 152 activity.getWindowManager().getMaximumWindowMetrics(); 153 final int initialRotation = activity.getDisplay().getRotation(); 154 155 VirtualDisplay virtualDisplay = 156 mMediaProjectionRule.createVirtualDisplay( 157 maxWindowMetrics.getBounds().width(), 158 maxWindowMetrics.getBounds().height()); 159 160 Intent testActivityIntent = new Intent(mContext, TestRotationActivity.class); 161 162 try (ActivityScenario<TestRotationActivity> activityScenario = 163 ActivityScenario.launch(testActivityIntent); 164 RotationSession rotationSession = createManagedRotationSession(); ) { 165 rotateDeviceAndWaitForActivity(rotationSession, initialRotation); 166 // Re-fetch the activity since reference may have been modified during rotation. 167 activityScenario.onActivity( 168 testActivity -> { 169 // Get the bounds of the activity on screen - use getGlobalVisibleRect to 170 // account for 171 // possible insets caused by DisplayCutout 172 final Rect activityRect = new Rect(); 173 testActivity.getWindow().getDecorView().getGlobalVisibleRect(activityRect); 174 175 final Point mirroredSize = 176 calculateScaledMirroredActivitySize( 177 testActivity.getWindowManager().getCurrentWindowMetrics(), 178 virtualDisplay, 179 new Point(activityRect.width(), activityRect.height())); 180 validateMirroredHierarchy( 181 testActivity, 182 virtualDisplay.getDisplay().getDisplayId(), 183 mirroredSize); 184 }); 185 } 186 } 187 188 // Validate that the mirrored hierarchy is the expected size. 189 @Test testSingleAppCapture()190 public void testSingleAppCapture() throws Exception { 191 // Start full screen capture. 192 mMediaProjectionRule.startMediaProjection(mLaunchCookie); 193 final WindowMetrics maxWindowMetrics = 194 mMediaProjectionRule.getActivity().getWindowManager().getMaximumWindowMetrics(); 195 VirtualDisplay virtualDisplay = 196 mMediaProjectionRule.createVirtualDisplay( 197 maxWindowMetrics.getBounds().width(), 198 maxWindowMetrics.getBounds().height()); 199 200 try (ActivityScenario<Activity> activityScenario = 201 ActivityScenario.launch( 202 new Intent(mContext, Activity.class), 203 createActivityScenarioWithLaunchCookie(mLaunchCookie))) { 204 activityScenario.onActivity( 205 activity -> { 206 // Get the bounds of the activity on screen - use getGlobalVisibleRect to 207 // account for 208 // possible insets caused by DisplayCutout 209 final Rect activityRect = new Rect(); 210 activity.getWindow().getDecorView().getGlobalVisibleRect(activityRect); 211 212 validateMirroredHierarchy( 213 activity, 214 virtualDisplay.getDisplay().getDisplayId(), 215 new Point(activityRect.width(), activityRect.height())); 216 }); 217 } 218 } 219 220 // TODO (b/284968776): test single app capture in split screen 221 222 /** 223 * Returns ActivityOptions with the given launch cookie set. 224 */ createActivityScenarioWithLaunchCookie( @onNull ActivityOptions.LaunchCookie launchCookie)225 private static Bundle createActivityScenarioWithLaunchCookie( 226 @NonNull ActivityOptions.LaunchCookie launchCookie) { 227 ActivityOptions activityOptions = ActivityOptions.makeBasic(); 228 activityOptions.setLaunchCookie(launchCookie); 229 return activityOptions.toBundle(); 230 } 231 232 /** 233 * Rotates the device 90 degrees & waits for the display & activity configuration to stabilize. 234 */ rotateDeviceAndWaitForActivity( @onNull RotationSession rotationSession, @Surface.Rotation int initialRotation)235 private void rotateDeviceAndWaitForActivity( 236 @NonNull RotationSession rotationSession, @Surface.Rotation int initialRotation) { 237 // Rotate the device by 90 degrees 238 rotationSession.set((initialRotation + 1) % (ROTATION_270 + 1), 239 /* waitForDeviceRotation=*/ true); 240 try { 241 waitForStableWindowGeometry(Duration.ofMillis(TIMEOUT_MS)); 242 } catch (InterruptedException e) { 243 Log.e(TAG, "Unable to wait for window to stabilize after rotation: " + e.getMessage()); 244 } 245 } 246 247 /** 248 * Calculate the size of the activity, scaled to fit on the VirtualDisplay. 249 * 250 * @param currentWindowMetrics The size of the source activity, before it is mirrored 251 * @param virtualDisplay The VirtualDisplay the mirrored content is sent to and scaled to 252 * fit 253 * @return The expected size of the mirrored activity on the VirtualDisplay 254 */ calculateScaledMirroredActivitySize( @onNull WindowMetrics currentWindowMetrics, @NonNull VirtualDisplay virtualDisplay, @Nullable Point visibleBounds)255 private static Point calculateScaledMirroredActivitySize( 256 @NonNull WindowMetrics currentWindowMetrics, 257 @NonNull VirtualDisplay virtualDisplay, @Nullable Point visibleBounds) { 258 // Calculate the aspect ratio of the original activity. 259 final Point currentBounds = new Point(currentWindowMetrics.getBounds().width(), 260 currentWindowMetrics.getBounds().height()); 261 final float aspectRatio = currentBounds.x * 1f / currentBounds.y; 262 // Find the size of the surface we are mirroring to. 263 final Point surfaceSize = virtualDisplay.getSurface().getDefaultSize(); 264 int mirroredWidth; 265 int mirroredHeight; 266 267 // Calculate any width & height deltas caused by DisplayCutout insets 268 Point sizeDifference = new Point(); 269 if (visibleBounds != null) { 270 int widthDifference = currentBounds.x - visibleBounds.x; 271 int heightDifference = currentBounds.y - visibleBounds.y; 272 sizeDifference.set(widthDifference, heightDifference); 273 } 274 275 if (surfaceSize.x < surfaceSize.y) { 276 // Output surface is portrait, so its width constrains. The mirrored activity is 277 // scaled down to fill the width entirely, and will have horizontal black bars at the 278 // top and bottom. 279 // Also apply scaled insets, to handle case where device has a display cutout which 280 // shifts the content horizontally when landscape. 281 int adjustedHorizontalInsets = Math.round(sizeDifference.x / aspectRatio); 282 int adjustedVerticalInsets = Math.round(sizeDifference.y / aspectRatio); 283 mirroredWidth = surfaceSize.x - adjustedHorizontalInsets; 284 mirroredHeight = Math.round(surfaceSize.x / aspectRatio) - adjustedVerticalInsets; 285 } else { 286 // Output surface is landscape, so its height constrains. The mirrored activity is 287 // scaled down to fill the height entirely, and will have horizontal black bars on the 288 // left and right. 289 // Also apply scaled insets, to handle case where device has a display cutout which 290 // shifts the content vertically when portrait. 291 int adjustedHorizontalInsets = Math.round(sizeDifference.x * aspectRatio); 292 int adjustedVerticalInsets = Math.round(sizeDifference.y * aspectRatio); 293 mirroredWidth = Math.round(surfaceSize.y * aspectRatio) - adjustedHorizontalInsets; 294 mirroredHeight = surfaceSize.y - adjustedVerticalInsets; 295 } 296 return new Point(mirroredWidth, mirroredHeight); 297 } 298 299 /** 300 * Validate the given activity is in the hierarchy mirrored to the VirtualDisplay. 301 * 302 * <p>Note that the hierarchy is present on the VirtualDisplay because the hierarchy is mirrored 303 * to the Surface provided to #createVirtualDisplay. 304 * 305 * @param activity The activity that we expect to be mirrored 306 * @param virtualDisplayId The id of the virtual display we are mirroring to 307 * @param expectedWindowSize The expected size of the mirrored activity 308 */ validateMirroredHierarchy( Activity activity, int virtualDisplayId, @NonNull Point expectedWindowSize)309 private static void validateMirroredHierarchy( 310 Activity activity, int virtualDisplayId, 311 @NonNull Point expectedWindowSize) { 312 Predicate<WindowInfo> hasExpectedDimensions = windowInfo -> { 313 int widthDiff = Math.abs(windowInfo.bounds.width() - expectedWindowSize.x); 314 int heightDiff = Math.abs(windowInfo.bounds.height() - expectedWindowSize.y); 315 return widthDiff <= TOLERANCE && heightDiff <= TOLERANCE; 316 }; 317 Supplier<IBinder> taskWindowTokenSupplier = 318 activity.getWindow().getDecorView()::getWindowToken; 319 try { 320 Log.e(TAG, "WindowToken: " + taskWindowTokenSupplier.get()); 321 boolean condition = waitForWindowInfo(hasExpectedDimensions, Duration.ofSeconds(5), 322 taskWindowTokenSupplier, virtualDisplayId); 323 assertAndDumpWindowState(TAG, 324 "Mirrored activity isn't the expected size of " + expectedWindowSize, 325 condition); 326 } catch (InterruptedException e) { 327 throw new RuntimeException(e); 328 } 329 } 330 createManagedRotationSession()331 private RotationSession createManagedRotationSession() { 332 mWaitForRotationOnTearDown = true; 333 return new RotationSession(mWmState); 334 } 335 336 /** 337 * Rotation support is indicated by explicitly having both landscape and portrait 338 * features or not listing either at all. 339 */ supportsRotation()340 protected boolean supportsRotation() { 341 final boolean supportsLandscape = hasDeviceFeature(FEATURE_SCREEN_LANDSCAPE); 342 final boolean supportsPortrait = hasDeviceFeature(FEATURE_SCREEN_PORTRAIT); 343 return (supportsLandscape && supportsPortrait) 344 || (!supportsLandscape && !supportsPortrait); 345 } 346 hasDeviceFeature(final String requiredFeature)347 protected boolean hasDeviceFeature(final String requiredFeature) { 348 return mContext.getPackageManager() 349 .hasSystemFeature(requiredFeature); 350 } 351 352 /** 353 * Stub activity for launching an activity meant to be rotated. 354 */ 355 public static class TestRotationActivity extends Activity { 356 // Stub 357 } 358 } 359