• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright 2025 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.cts;
17 
18 import static android.content.Intent.FLAG_ACTIVITY_NEW_TASK;
19 import static android.hardware.display.DisplayManager.VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR;
20 
21 import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
22 
23 import static com.google.common.truth.Truth.assertThat;
24 import static com.google.common.truth.Truth.assertWithMessage;
25 
26 import android.annotation.NonNull;
27 import android.annotation.Nullable;
28 import android.app.ActivityOptions;
29 import android.content.Context;
30 import android.content.Intent;
31 import android.graphics.Bitmap;
32 import android.graphics.PixelFormat;
33 import android.hardware.display.DisplayManager;
34 import android.hardware.display.VirtualDisplay;
35 import android.media.Image;
36 import android.media.ImageReader;
37 import android.media.projection.MediaProjection;
38 import android.media.projection.MediaProjectionConfig;
39 import android.media.projection.MediaProjectionManager;
40 import android.os.Environment;
41 import android.os.Handler;
42 import android.os.Looper;
43 import android.platform.test.flag.junit.DeviceFlagsValueProvider;
44 import android.util.ArraySet;
45 import android.util.Log;
46 import android.view.Display;
47 import android.view.Surface;
48 
49 import androidx.test.core.app.ActivityScenario;
50 
51 import org.junit.rules.ExternalResource;
52 import org.junit.rules.RuleChain;
53 import org.junit.rules.TestRule;
54 import org.junit.runner.Description;
55 import org.junit.runners.model.Statement;
56 
57 import java.io.File;
58 import java.io.FileOutputStream;
59 import java.nio.ByteBuffer;
60 import java.util.Set;
61 import java.util.concurrent.CountDownLatch;
62 import java.util.concurrent.TimeUnit;
63 
64 /** A test rule for testing MediaProjection and related components */
65 public class MediaProjectionRule implements TestRule {
66     private static final String TAG = "MediaProjectionRule";
67     // Enable debug mode to save screenshots from MediaProjection session.
68     private static final boolean DEBUG_MODE = false;
69     private static final int RECORDING_WIDTH = 500;
70     private static final int RECORDING_HEIGHT = 700;
71     private static final int RECORDING_DENSITY = 200;
72     private static final int TIMEOUT_MS = 3000;
73     private static final int SCREENSHOT_TIMEOUT_MS = 1000;
74 
75     private final MediaProjection.Callback mEmptyMediaProjectionCallback =
76             new MediaProjection.Callback() {};
77     private final MediaProjectionTrackerRule mMediaProjectionTrackerRule =
78             new MediaProjectionTrackerRule();
79     private final Context mContext = getInstrumentation().getTargetContext();
80     private final MediaProjectionManager mMediaProjectionManager =
81             mContext.getSystemService(MediaProjectionManager.class);
82     private final Handler mCallbackHandler = new Handler(Looper.getMainLooper());
83 
84     private android.media.cts.MediaProjectionActivity mActivity;
85     private String mTestName;
86 
87     @Override
apply(Statement base, Description description)88     public Statement apply(Statement base, Description description) {
89         assertThat(mMediaProjectionManager).isNotNull();
90         mTestName = String.format("%s#%s", description.getClassName(), description.getMethodName());
91         return RuleChain.outerRule(DeviceFlagsValueProvider.createCheckFlagsRule())
92                 .around(mMediaProjectionTrackerRule)
93                 .apply(base, description);
94     }
95 
96     /** Get an instance of MediaProjectionManager. */
getMediaProjectionManager()97     public MediaProjectionManager getMediaProjectionManager() {
98         return mMediaProjectionManager;
99     }
100 
101     /**
102      * Starts the MediaProjection consent flow but doesn't actually start a MediaProjection session.
103      */
showMediaProjectionConsent(@ullable MediaProjectionConfig config)104     public void showMediaProjectionConsent(@Nullable MediaProjectionConfig config)
105             throws Exception {
106         showMediaProjectionConsent(config, null, null);
107     }
108 
showMediaProjectionConsent( @ullable MediaProjectionConfig config, @Nullable ActivityOptions.LaunchCookie launchCookie, @Nullable String foregroundServiceClass)109     private void showMediaProjectionConsent(
110             @Nullable MediaProjectionConfig config,
111             @Nullable ActivityOptions.LaunchCookie launchCookie,
112             @Nullable String foregroundServiceClass)
113             throws Exception {
114         CountDownLatch latch = new CountDownLatch(1);
115         Intent intent =
116                 new Intent(mContext, android.media.cts.MediaProjectionActivity.class)
117                         .addFlags(FLAG_ACTIVITY_NEW_TASK);
118 
119         if (config != null) {
120             intent.putExtra(android.media.cts.MediaProjectionActivity.EXTRA_MP_CONFIG, config);
121         }
122         if (launchCookie != null) {
123             intent.putExtra(
124                     android.media.cts.MediaProjectionActivity.EXTRA_LAUNCH_COOKIE, launchCookie);
125         }
126         if (foregroundServiceClass != null) {
127             intent.putExtra(
128                     android.media.cts.MediaProjectionActivity.EXTRA_FOREGROUND_SERVICE_CLASS,
129                     foregroundServiceClass);
130         }
131 
132         mMediaProjectionTrackerRule.mActivityScenario = ActivityScenario.launch(intent);
133         mMediaProjectionTrackerRule.mActivityScenario.onActivity(
134                 activity -> {
135                     mActivity = activity;
136                     latch.countDown();
137                 });
138         assertWithMessage("MediaProjectionActivity not started")
139                 .that(latch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS))
140                 .isTrue();
141     }
142 
143     /** Start a MediaProjection session. */
startMediaProjection()144     public MediaProjection startMediaProjection() throws Exception {
145         return startMediaProjection(null, null, null, false);
146     }
147 
148     /** Start a MediaProjection session (with a custom foregroundService class). */
startMediaProjection(String foregroundServiceClass)149     public MediaProjection startMediaProjection(String foregroundServiceClass) throws Exception {
150         return startMediaProjection(null, null, foregroundServiceClass, false);
151     }
152 
153     /** Start a MediaProjection session (with a custom MediaProjectionConfig). */
startMediaProjection(MediaProjectionConfig config)154     public MediaProjection startMediaProjection(MediaProjectionConfig config) throws Exception {
155         return startMediaProjection(config, null, null, false);
156     }
157 
158     /** Start a MediaProjection session for a specific LaunchCookie. */
startMediaProjection(ActivityOptions.LaunchCookie launchCookie)159     public MediaProjection startMediaProjection(ActivityOptions.LaunchCookie launchCookie)
160             throws Exception {
161         return startMediaProjection(null, launchCookie, null, false);
162     }
163 
164     /** Start a MediaProjection session for a specific LaunchCookie. */
startMediaProjection(boolean skipDefaultCallback)165     public MediaProjection startMediaProjection(boolean skipDefaultCallback) throws Exception {
166         return startMediaProjection(null, null, null, skipDefaultCallback);
167     }
168 
startMediaProjection( @ullable MediaProjectionConfig config, @Nullable ActivityOptions.LaunchCookie launchCookie, @Nullable String foregroundServiceClass, boolean skipDefaultCallback)169     private MediaProjection startMediaProjection(
170             @Nullable MediaProjectionConfig config,
171             @Nullable ActivityOptions.LaunchCookie launchCookie,
172             @Nullable String foregroundServiceClass,
173             boolean skipDefaultCallback)
174             throws Exception {
175         showMediaProjectionConsent(config, launchCookie, foregroundServiceClass);
176         mMediaProjectionTrackerRule.mMediaProjection = mActivity.waitForMediaProjection();
177         mMediaProjectionTrackerRule.mActivityScenario.close();
178         if (!skipDefaultCallback) {
179             // Register an empty callback. Tests that want to validate callback behavior can still
180             // register individual callbacks.
181             registerCallback(mEmptyMediaProjectionCallback);
182         }
183         return mMediaProjectionTrackerRule.mMediaProjection;
184     }
185 
186     /** Register a MediaProjection.Callback. */
registerCallback(MediaProjection.Callback callback)187     public void registerCallback(MediaProjection.Callback callback) {
188         if (mMediaProjectionTrackerRule.mMediaProjection == null) {
189             throw new IllegalStateException("MediaProjection not yet started.");
190         }
191         mMediaProjectionTrackerRule.mCallbacks.add(callback);
192         mMediaProjectionTrackerRule.mMediaProjection.registerCallback(callback, mCallbackHandler);
193     }
194 
195     /** Create a VirtualDisplay for the MediaProjection. */
createVirtualDisplay()196     public VirtualDisplay createVirtualDisplay() throws InterruptedException {
197         return createVirtualDisplay(
198                 mMediaProjectionTrackerRule.mMediaProjection, RECORDING_WIDTH, RECORDING_HEIGHT);
199     }
200 
201     /** Create a VirtualDisplay for a MediaProjection. */
createVirtualDisplay(MediaProjection mediaProjection)202     public VirtualDisplay createVirtualDisplay(MediaProjection mediaProjection)
203             throws InterruptedException {
204         return createVirtualDisplay(mediaProjection, RECORDING_WIDTH, RECORDING_HEIGHT);
205     }
206 
207     /** Create a VirtualDisplay for a MediaProjection. */
createVirtualDisplay(int width, int height)208     public VirtualDisplay createVirtualDisplay(int width, int height) throws InterruptedException {
209         return createVirtualDisplay(mMediaProjectionTrackerRule.mMediaProjection, width, height);
210     }
211 
212     /** Create a VirtualDisplay for the MediaProjection with specific dimensions. */
createVirtualDisplay( MediaProjection mediaProjection, int width, int height)213     public VirtualDisplay createVirtualDisplay(
214             MediaProjection mediaProjection, int width, int height) throws InterruptedException {
215         if (mediaProjection == null) {
216             throw new IllegalStateException("MediaProjection not yet started.");
217         }
218 
219         CountDownLatch latch = new CountDownLatch(1);
220         DisplayManager dm = mContext.getSystemService(DisplayManager.class);
221         dm.registerDisplayListener(
222                 new DisplayManager.DisplayListener() {
223                     @Override
224                     public void onDisplayAdded(int displayId) {
225                         checkDisplayState(displayId);
226                     }
227 
228                     @Override
229                     public void onDisplayRemoved(int displayId) {}
230 
231                     @Override
232                     public void onDisplayChanged(int displayId) {
233                         checkDisplayState(displayId);
234                     }
235 
236                     private void checkDisplayState(int displayId) {
237                         Display display = dm.getDisplay(displayId);
238                         if (display != null && display.getState() == Display.STATE_ON) {
239                             latch.countDown();
240                         }
241                     }
242                 },
243                 null);
244 
245         ImageReader imageReader =
246                 ImageReader.newInstance(width, height, PixelFormat.RGBA_8888, /* maxImages= */ 1);
247         mMediaProjectionTrackerRule.mImageReaders.add(imageReader);
248         CountDownLatch mScreenshotCountDownLatch = new CountDownLatch(1);
249         if (DEBUG_MODE) {
250             ScreenshotListener screenshotListener =
251                     new ScreenshotListener(mTestName, mScreenshotCountDownLatch);
252             imageReader.setOnImageAvailableListener(screenshotListener, mCallbackHandler);
253         }
254         VirtualDisplay virtualDisplay =
255                 mediaProjection.createVirtualDisplay(
256                         mTestName,
257                         width,
258                         height,
259                         RECORDING_DENSITY,
260                         VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR,
261                         imageReader.getSurface(),
262                         new VirtualDisplay.Callback() {
263                             @Override
264                             public void onStopped() {
265                                 super.onStopped();
266                                 // VirtualDisplay stopped by the system; no more frames incoming.
267                                 // Must release VirtualDisplay.
268                                 mMediaProjectionTrackerRule.cleanupVirtualDisplay();
269                             }
270                         },
271                         mCallbackHandler);
272         mMediaProjectionTrackerRule.mVirtualDisplays.add(virtualDisplay);
273 
274         if (DEBUG_MODE) {
275             // wait until we've received a screenshot
276             try {
277                 assertThat(
278                                 mScreenshotCountDownLatch.await(
279                                         SCREENSHOT_TIMEOUT_MS, TimeUnit.MILLISECONDS))
280                         .isTrue();
281             } catch (InterruptedException e) {
282                 Log.e(TAG, e.toString());
283             }
284         }
285 
286         assertWithMessage("VirtualDisplay should be created and on")
287                 .that(latch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS))
288                 .isTrue();
289         return virtualDisplay;
290     }
291 
292     /** Get the Activity hosting the MediaProjection consent flow. */
getActivity()293     public android.media.cts.MediaProjectionActivity getActivity() {
294         return mActivity;
295     }
296 
297     /** Save MediaProjection's screenshots to the device to help debug test failures. */
298     public static class ScreenshotListener implements ImageReader.OnImageAvailableListener {
299         private final CountDownLatch mCountDownLatch;
300         private final String mMethodName;
301         private int mCurrentScreenshot = 0;
302         // How often to save an image
303         private static final int SCREENSHOT_FREQUENCY = 5;
304 
ScreenshotListener(@onNull String methodName, @NonNull CountDownLatch latch)305         public ScreenshotListener(@NonNull String methodName, @NonNull CountDownLatch latch) {
306             mMethodName = methodName;
307             mCountDownLatch = latch;
308         }
309 
310         @Override
onImageAvailable(ImageReader reader)311         public void onImageAvailable(ImageReader reader) {
312             if (mCurrentScreenshot % SCREENSHOT_FREQUENCY != 0) {
313                 Log.d(TAG, "onImageAvailable - skip this one");
314                 return;
315             }
316             Log.d(TAG, "onImageAvailable - processing");
317             mCountDownLatch.countDown();
318             mCurrentScreenshot++;
319 
320             final Image image = reader.acquireLatestImage();
321 
322             assertThat(image).isNotNull();
323 
324             final Image.Plane plane = image.getPlanes()[0];
325 
326             assertThat(plane).isNotNull();
327 
328             final int rowPadding = plane.getRowStride() - plane.getPixelStride() * image.getWidth();
329             final Bitmap bitmap =
330                     Bitmap.createBitmap(
331                             /* width= */ image.getWidth() + rowPadding / plane.getPixelStride(),
332                             /* height= */ image.getHeight(),
333                             Bitmap.Config.ARGB_8888);
334             final ByteBuffer buffer = plane.getBuffer();
335 
336             assertThat(buffer).isNotNull();
337             assertThat(bitmap).isNotNull(); // why null?
338 
339             bitmap.copyPixelsFromBuffer(plane.getBuffer());
340             assertThat(bitmap).isNotNull(); // why null?
341 
342             try {
343                 // save to virtual sdcard
344                 final File outputDirectory =
345                         new File(Environment.getExternalStorageDirectory(), "cts." + TAG);
346                 Log.d(TAG, "Had to create the directory? " + outputDirectory.mkdir());
347                 final File screenshot =
348                         new File(
349                                 outputDirectory,
350                                 mMethodName
351                                         + "_screenshot_"
352                                         + mCurrentScreenshot
353                                         + "_"
354                                         + System.currentTimeMillis()
355                                         + ".jpg");
356                 final FileOutputStream stream = new FileOutputStream(screenshot);
357                 assertThat(stream).isNotNull();
358                 bitmap.compress(Bitmap.CompressFormat.JPEG, 100, stream);
359                 stream.close();
360             } catch (Exception e) {
361                 Log.e(TAG, "Unable to write out screenshot", e);
362             } finally {
363                 image.close();
364             }
365         }
366     }
367 
368     /**
369      * Internal rule that tracks the Active MediaProjection session and resources and ensures they
370      * are properly closed and released after the test.
371      */
372     private static final class MediaProjectionTrackerRule extends ExternalResource {
373 
374         final Set<MediaProjection.Callback> mCallbacks = new ArraySet<>();
375         final Set<ImageReader> mImageReaders = new ArraySet<>();
376         final Set<VirtualDisplay> mVirtualDisplays = new ArraySet<>();
377         ActivityScenario<android.media.cts.MediaProjectionActivity> mActivityScenario;
378         MediaProjection mMediaProjection;
379 
380         @Override
after()381         protected void after() {
382             if (mMediaProjection != null) {
383                 for (MediaProjection.Callback callback : mCallbacks) {
384                     mMediaProjection.unregisterCallback(callback);
385                 }
386                 mMediaProjection.stop();
387                 mMediaProjection = null;
388             }
389 
390             cleanupVirtualDisplay();
391 
392             if (mActivityScenario != null) {
393                 mActivityScenario.close();
394             }
395         }
396 
cleanupVirtualDisplay()397         void cleanupVirtualDisplay() {
398             for (ImageReader imageReader : mImageReaders) {
399                 imageReader.close();
400             }
401 
402             for (VirtualDisplay virtualDisplay : mVirtualDisplays) {
403                 final Surface surface = virtualDisplay.getSurface();
404                 if (surface != null) {
405                     surface.release();
406                 }
407                 virtualDisplay.release();
408             }
409         }
410     }
411 }
412