• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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