diff options
| -rw-r--r-- | core/api/current.txt | 4 | ||||
| -rw-r--r-- | media/java/android/media/projection/MediaProjection.java | 192 | ||||
| -rw-r--r-- | media/tests/projection/Android.bp | 2 | ||||
| -rw-r--r-- | media/tests/projection/AndroidManifest.xml | 1 | ||||
| -rw-r--r-- | media/tests/projection/src/android/media/projection/FakeIMediaProjection.java | 82 | ||||
| -rw-r--r-- | media/tests/projection/src/android/media/projection/MediaProjectionTest.java | 197 |
6 files changed, 405 insertions, 73 deletions
diff --git a/core/api/current.txt b/core/api/current.txt index 37480520529b..70800485074c 100644 --- a/core/api/current.txt +++ b/core/api/current.txt @@ -26277,9 +26277,9 @@ package android.media.projection { public final class MediaProjection { method public android.hardware.display.VirtualDisplay createVirtualDisplay(@NonNull String, int, int, int, int, @Nullable android.view.Surface, @Nullable android.hardware.display.VirtualDisplay.Callback, @Nullable android.os.Handler); - method public void registerCallback(android.media.projection.MediaProjection.Callback, android.os.Handler); + method public void registerCallback(@NonNull android.media.projection.MediaProjection.Callback, @Nullable android.os.Handler); method public void stop(); - method public void unregisterCallback(android.media.projection.MediaProjection.Callback); + method public void unregisterCallback(@NonNull android.media.projection.MediaProjection.Callback); } public abstract static class MediaProjection.Callback { diff --git a/media/java/android/media/projection/MediaProjection.java b/media/java/android/media/projection/MediaProjection.java index 178a6d97dff8..9d0662bfb52f 100644 --- a/media/java/android/media/projection/MediaProjection.java +++ b/media/java/android/media/projection/MediaProjection.java @@ -20,43 +20,73 @@ import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION; import android.annotation.NonNull; import android.annotation.Nullable; +import android.app.compat.CompatChanges; +import android.compat.annotation.ChangeId; +import android.compat.annotation.EnabledSince; import android.content.Context; import android.hardware.display.DisplayManager; import android.hardware.display.VirtualDisplay; import android.hardware.display.VirtualDisplayConfig; +import android.os.Build; import android.os.Handler; import android.os.IBinder; import android.os.RemoteException; import android.os.ServiceManager; import android.util.ArrayMap; import android.util.Log; +import android.util.Slog; import android.view.ContentRecordingSession; import android.view.Surface; +import com.android.internal.annotations.VisibleForTesting; + import java.util.Map; +import java.util.Objects; /** * A token granting applications the ability to capture screen contents and/or * record system audio. The exact capabilities granted depend on the type of * MediaProjection. * - * <p> - * A screen capture session can be started through {@link + * <p>A screen capture session can be started through {@link * MediaProjectionManager#createScreenCaptureIntent}. This grants the ability to * capture screen contents, but not system audio. - * </p> */ public final class MediaProjection { private static final String TAG = "MediaProjection"; + /** + * Requires an app registers a {@link Callback} before invoking + * {@link #createVirtualDisplay(String, int, int, int, int, Surface, VirtualDisplay.Callback, + * Handler) createVirtualDisplay}. + * + * <p>Enabled after version 33 (Android T), so applies to target SDK of 34+ (Android U+). + * + * @hide + */ + @VisibleForTesting + @ChangeId + @EnabledSince(targetSdkVersion = Build.VERSION_CODES.UPSIDE_DOWN_CAKE) + static final long MEDIA_PROJECTION_REQUIRES_CALLBACK = 269849258L; // buganizer id + private final IMediaProjection mImpl; private final Context mContext; - private final Map<Callback, CallbackRecord> mCallbacks; - @Nullable private IMediaProjectionManager mProjectionService = null; + private final DisplayManager mDisplayManager; + private final IMediaProjectionManager mProjectionService; + @NonNull + private final Map<Callback, CallbackRecord> mCallbacks = new ArrayMap<>(); /** @hide */ public MediaProjection(Context context, IMediaProjection impl) { - mCallbacks = new ArrayMap<Callback, CallbackRecord>(); + this(context, impl, IMediaProjectionManager.Stub.asInterface( + ServiceManager.getService(Context.MEDIA_PROJECTION_SERVICE)), + context.getSystemService(DisplayManager.class)); + } + + /** @hide */ + @VisibleForTesting + public MediaProjection(Context context, IMediaProjection impl, IMediaProjectionManager service, + DisplayManager displayManager) { mContext = context; mImpl = impl; try { @@ -64,46 +94,44 @@ public final class MediaProjection { } catch (RemoteException e) { throw new RuntimeException("Failed to start media projection", e); } + mProjectionService = service; + mDisplayManager = displayManager; } /** * Register a listener to receive notifications about when the {@link MediaProjection} or * captured content changes state. - * <p> - * The callback should be registered before invoking + * + * <p>The callback must be registered before invoking * {@link #createVirtualDisplay(String, int, int, int, int, Surface, VirtualDisplay.Callback, - * Handler)} - * to ensure that any notifications on the callback are not missed. - * </p> + * Handler)} to ensure that any notifications on the callback are not missed. The client must + * implement {@link Callback#onStop()} and clean up any resources it is holding, e.g. the + * {@link VirtualDisplay} and {@link Surface}. * * @param callback The callback to call. * @param handler The handler on which the callback should be invoked, or * null if the callback should be invoked on the calling thread's looper. - * @throws IllegalArgumentException If the given callback is null. + * @throws NullPointerException If the given callback is null. * @see #unregisterCallback */ - public void registerCallback(Callback callback, Handler handler) { - if (callback == null) { - throw new IllegalArgumentException("callback should not be null"); - } + public void registerCallback(@NonNull Callback callback, @Nullable Handler handler) { + final Callback c = Objects.requireNonNull(callback); if (handler == null) { handler = new Handler(); } - mCallbacks.put(callback, new CallbackRecord(callback, handler)); + mCallbacks.put(c, new CallbackRecord(c, handler)); } /** * Unregister a {@link MediaProjection} listener. * * @param callback The callback to unregister. - * @throws IllegalArgumentException If the given callback is null. + * @throws NullPointerException If the given callback is null. * @see #registerCallback */ - public void unregisterCallback(Callback callback) { - if (callback == null) { - throw new IllegalArgumentException("callback should not be null"); - } - mCallbacks.remove(callback); + public void unregisterCallback(@NonNull Callback callback) { + final Callback c = Objects.requireNonNull(callback); + mCallbacks.remove(c); } /** @@ -122,43 +150,55 @@ public final class MediaProjection { if (surface != null) { builder.setSurface(surface); } - VirtualDisplay virtualDisplay = createVirtualDisplay(builder, callback, handler); - return virtualDisplay; + return createVirtualDisplay(builder, callback, handler); } /** * Creates a {@link android.hardware.display.VirtualDisplay} to capture the * contents of the screen. * - * @param name The name of the virtual display, must be non-empty. - * @param width The width of the virtual display in pixels. Must be - * greater than 0. - * @param height The height of the virtual display in pixels. Must be - * greater than 0. - * @param dpi The density of the virtual display in dpi. Must be greater - * than 0. - * @param surface The surface to which the content of the virtual display - * should be rendered, or null if there is none initially. - * @param flags A combination of virtual display flags. See {@link DisplayManager} for the full - * list of flags. - * @param callback Callback to call when the virtual display's state - * changes, or null if none. - * @param handler The {@link android.os.Handler} on which the callback should be - * invoked, or null if the callback should be invoked on the calling - * thread's main {@link android.os.Looper}. + * <p>To correctly clean up resources associated with a capture, the application must register a + * {@link Callback} before invocation. The app must override {@link Callback#onStop()} to clean + * up (by invoking{@link VirtualDisplay#release()}, {@link Surface#release()} and related + * resources). * - * @see android.hardware.display.VirtualDisplay + * @param name The name of the virtual display, must be non-empty. + * @param width The width of the virtual display in pixels. Must be greater than 0. + * @param height The height of the virtual display in pixels. Must be greater than 0. + * @param dpi The density of the virtual display in dpi. Must be greater than 0. + * @param surface The surface to which the content of the virtual display should be rendered, + * or null if there is none initially. + * @param flags A combination of virtual display flags. See {@link DisplayManager} for the + * full list of flags. + * @param callback Callback invoked when the virtual display's state changes, or null. + * @param handler The {@link android.os.Handler} on which the callback should be invoked, or + * null if the callback should be invoked on the calling thread's main + * {@link android.os.Looper}. + * @throws IllegalStateException If the target SDK is + * {@link android.os.Build.VERSION_CODES#UPSIDE_DOWN_CAKE U} and + * up and no {@link Callback} + * is registered. If the target SDK is less than + * {@link android.os.Build.VERSION_CODES#UPSIDE_DOWN_CAKE U}, no + * exception is thrown. + * @see VirtualDisplay + * @see VirtualDisplay.Callback */ public VirtualDisplay createVirtualDisplay(@NonNull String name, int width, int height, int dpi, int flags, @Nullable Surface surface, @Nullable VirtualDisplay.Callback callback, @Nullable Handler handler) { + if (shouldMediaProjectionRequireCallback()) { + if (mCallbacks.isEmpty()) { + throw new IllegalStateException( + "Must register a callback before starting capture, to manage resources in" + + " response to MediaProjection states."); + } + } final VirtualDisplayConfig.Builder builder = new VirtualDisplayConfig.Builder(name, width, height, dpi).setFlags(flags); if (surface != null) { builder.setSurface(surface); } - VirtualDisplay virtualDisplay = createVirtualDisplay(builder, callback, handler); - return virtualDisplay; + return createVirtualDisplay(builder, callback, handler); } /** @@ -191,13 +231,21 @@ public final class MediaProjection { } else { session = ContentRecordingSession.createTaskSession(launchCookie); } - // Pass in the current session details, so they are guaranteed to only be set in WMS - // AFTER a VirtualDisplay is constructed (assuming there are no errors during set-up). + // Pass in the current session details, so they are guaranteed to only be set in + // WindowManagerService AFTER a VirtualDisplay is constructed (assuming there are no + // errors during set-up). virtualDisplayConfig.setContentRecordingSession(session); virtualDisplayConfig.setWindowManagerMirroringEnabled(true); - final DisplayManager dm = mContext.getSystemService(DisplayManager.class); - final VirtualDisplay virtualDisplay = dm.createVirtualDisplay(this, + final VirtualDisplay virtualDisplay = mDisplayManager.createVirtualDisplay(this, virtualDisplayConfig.build(), callback, handler, windowContext); + if (virtualDisplay == null) { + // Since WindowManager handling a new display and DisplayManager creating a new + // VirtualDisplay is async, WindowManager may have tried to start task recording + // and encountered an error that required stopping recording entirely. The + // VirtualDisplay would then be null and the MediaProjection is no longer active. + Slog.w(TAG, "Failed to create virtual display."); + return null; + } return virtualDisplay; } catch (RemoteException e) { // Can not capture if WMS is not accessible, so bail out. @@ -205,12 +253,14 @@ public final class MediaProjection { } } - private IMediaProjectionManager getProjectionService() { - if (mProjectionService == null) { - mProjectionService = IMediaProjectionManager.Stub.asInterface( - ServiceManager.getService(Context.MEDIA_PROJECTION_SERVICE)); - } - return mProjectionService; + /** + * Returns {@code true} when MediaProjection requires the app registers a callback before + * beginning to capture via + * {@link #createVirtualDisplay(String, int, int, int, int, Surface, VirtualDisplay.Callback, + * Handler)}. + */ + private boolean shouldMediaProjectionRequireCallback() { + return CompatChanges.isChangeEnabled(MEDIA_PROJECTION_REQUIRES_CALLBACK); } /** @@ -238,28 +288,26 @@ public final class MediaProjection { public abstract static class Callback { /** * Called when the MediaProjection session is no longer valid. - * <p> - * Once a MediaProjection has been stopped, it's up to the application to release any - * resources it may be holding (e.g. {@link android.hardware.display.VirtualDisplay}s). - * </p> + * + * <p>Once a MediaProjection has been stopped, it's up to the application to release any + * resources it may be holding (e.g. releasing the {@link VirtualDisplay} and + * {@link Surface}). */ public void onStop() { } /** * Invoked immediately after capture begins or when the size of the captured region changes, * providing the accurate sizing for the streamed capture. - * <p> - * The given width and height, in pixels, corresponds to the same width and height that + * + * <p>The given width and height, in pixels, corresponds to the same width and height that * would be returned from {@link android.view.WindowMetrics#getBounds()} of the captured * region. - * </p> - * <p> - * If the recorded content has a different aspect ratio from either the + * + * <p>If the recorded content has a different aspect ratio from either the * {@link VirtualDisplay} or output {@link Surface}, the captured stream has letterboxing * (black bars) around the recorded content. The application can avoid the letterboxing * around the recorded content by updating the size of both the {@link VirtualDisplay} and * output {@link Surface}: - * </p> * * <pre> * @Override @@ -284,13 +332,12 @@ public final class MediaProjection { /** * Invoked immediately after capture begins or when the visibility of the captured region * changes, providing the current visibility of the captured region. - * <p> - * Applications can take advantage of this callback by showing or hiding the captured + * + * <p>Applications can take advantage of this callback by showing or hiding the captured * content from the output {@link Surface}, based on if the captured region is currently * visible to the user. - * </p> - * <p> - * For example, if the user elected to capture a single app (from the activity shown from + * + * <p>For example, if the user elected to capture a single app (from the activity shown from * {@link MediaProjectionManager#createScreenCaptureIntent()}), the following scenarios * trigger the callback: * <ul> @@ -307,7 +354,6 @@ public final class MediaProjection { * captured app, or the user navigates away from the captured app. * </li> * </ul> - * </p> */ public void onCapturedContentVisibilityChanged(boolean isVisible) { } } @@ -335,7 +381,7 @@ public final class MediaProjection { } } - private final static class CallbackRecord { + private static final class CallbackRecord extends Callback { private final Callback mCallback; private final Handler mHandler; @@ -344,6 +390,8 @@ public final class MediaProjection { mHandler = handler; } + + @Override public void onStop() { mHandler.post(new Runnable() { @Override @@ -353,10 +401,12 @@ public final class MediaProjection { }); } + @Override public void onCapturedContentResize(int width, int height) { mHandler.post(() -> mCallback.onCapturedContentResize(width, height)); } + @Override public void onCapturedContentVisibilityChanged(boolean isVisible) { mHandler.post(() -> mCallback.onCapturedContentVisibilityChanged(isVisible)); } diff --git a/media/tests/projection/Android.bp b/media/tests/projection/Android.bp index 08d950128ce2..e313c46d1973 100644 --- a/media/tests/projection/Android.bp +++ b/media/tests/projection/Android.bp @@ -29,7 +29,9 @@ android_test { "mockito-target-extended-minus-junit4", "platform-test-annotations", "testng", + "testables", "truth-prebuilt", + "platform-compat-test-rules", ], // Needed for mockito-target-extended-minus-junit4 diff --git a/media/tests/projection/AndroidManifest.xml b/media/tests/projection/AndroidManifest.xml index 62f148cfdde1..0c9760400ce0 100644 --- a/media/tests/projection/AndroidManifest.xml +++ b/media/tests/projection/AndroidManifest.xml @@ -19,6 +19,7 @@ package="android.media.projection.mediaprojectiontests" android:sharedUserId="com.android.uid.test"> <uses-permission android:name="android.permission.READ_COMPAT_CHANGE_CONFIG" /> + <uses-permission android:name="android.permission.MANAGE_MEDIA_PROJECTION" /> <application android:debuggable="true" android:testOnly="true"> diff --git a/media/tests/projection/src/android/media/projection/FakeIMediaProjection.java b/media/tests/projection/src/android/media/projection/FakeIMediaProjection.java new file mode 100644 index 000000000000..3cfc0fe5b96e --- /dev/null +++ b/media/tests/projection/src/android/media/projection/FakeIMediaProjection.java @@ -0,0 +1,82 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.media.projection; + +import android.os.IBinder; +import android.os.RemoteException; + +/** + * The connection between MediaProjection and system server is represented by IMediaProjection; + * outside the test it is implemented by the system server. + */ +public final class FakeIMediaProjection extends IMediaProjection.Stub { + boolean mIsStarted = false; + IBinder mLaunchCookie = null; + IMediaProjectionCallback mIMediaProjectionCallback = null; + + @Override + public void start(IMediaProjectionCallback callback) throws RemoteException { + mIMediaProjectionCallback = callback; + mIsStarted = true; + } + + @Override + public void stop() throws RemoteException { + // Pass along to the client's callback wrapper. + mIMediaProjectionCallback.onStop(); + } + + @Override + public boolean canProjectAudio() throws RemoteException { + return false; + } + + @Override + public boolean canProjectVideo() throws RemoteException { + return false; + } + + @Override + public boolean canProjectSecureVideo() throws RemoteException { + return false; + } + + @Override + public int applyVirtualDisplayFlags(int flags) throws RemoteException { + return 0; + } + + @Override + public void registerCallback(IMediaProjectionCallback callback) throws RemoteException { + + } + + @Override + public void unregisterCallback(IMediaProjectionCallback callback) throws RemoteException { + + } + + @Override + public IBinder getLaunchCookie() throws RemoteException { + return mLaunchCookie; + } + + @Override + public void setLaunchCookie(IBinder launchCookie) throws RemoteException { + mLaunchCookie = launchCookie; + } +} diff --git a/media/tests/projection/src/android/media/projection/MediaProjectionTest.java b/media/tests/projection/src/android/media/projection/MediaProjectionTest.java new file mode 100644 index 000000000000..bf616d78cdb6 --- /dev/null +++ b/media/tests/projection/src/android/media/projection/MediaProjectionTest.java @@ -0,0 +1,197 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.media.projection; + + +import static android.media.projection.MediaProjection.MEDIA_PROJECTION_REQUIRES_CALLBACK; +import static android.view.Display.DEFAULT_DISPLAY; + +import static com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession; + +import static com.google.common.truth.Truth.assertThat; + +import static libcore.junit.util.compat.CoreCompatChangeRule.DisableCompatChanges; +import static libcore.junit.util.compat.CoreCompatChangeRule.EnableCompatChanges; + +import static org.junit.Assert.assertThrows; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.nullable; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; + +import android.annotation.Nullable; +import android.compat.testing.PlatformCompatChangeRule; +import android.content.Context; +import android.hardware.display.DisplayManager; +import android.hardware.display.VirtualDisplay; +import android.hardware.display.VirtualDisplayConfig; +import android.os.Handler; +import android.os.IBinder; +import android.os.Looper; +import android.os.RemoteException; +import android.platform.test.annotations.Presubmit; +import android.testing.TestableContext; +import android.view.Display; + +import androidx.test.ext.junit.runners.AndroidJUnit4; +import androidx.test.filters.SmallTest; +import androidx.test.platform.app.InstrumentationRegistry; + +import org.junit.After; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TestRule; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoSession; +import org.mockito.quality.Strictness; + + +/** + * Tests for the {@link MediaProjection} class. + * + * Build/Install/Run: + * atest MediaProjectionTests:MediaProjectionTest + */ +@SmallTest +@Presubmit +@RunWith(AndroidJUnit4.class) +public class MediaProjectionTest { + // Values for creating a VirtualDisplay. + private static final String VIRTUAL_DISPLAY_NAME = "MEDIA_PROJECTION_VIRTUAL_DISPLAY"; + private static final int VIRTUAL_DISPLAY_WIDTH = 500; + private static final int VIRTUAL_DISPLAY_HEIGHT = 600; + private static final int VIRTUAL_DISPLAY_DENSITY = 100; + private static final int VIRTUAL_DISPLAY_FLAGS = 0; + + private final Handler mHandler = new Handler(Looper.getMainLooper()); + // Fake the connection to the system server. + private final FakeIMediaProjection mFakeIMediaProjection = new FakeIMediaProjection(); + // Callback registered by an app. + private MediaProjection mMediaProjection; + + @Mock + private MediaProjection.Callback mMediaProjectionCallback; + @Mock + private IMediaProjectionManager mIMediaProjectionManager; + @Mock + private Display mDisplay; + @Mock + private VirtualDisplay.Callback mVirtualDisplayCallback; + @Mock + private VirtualDisplay mVirtualDisplay; + @Mock + private DisplayManager mDisplayManager; + + @Rule + public TestRule compatChangeRule = new PlatformCompatChangeRule(); + + @Rule + public final TestableContext mTestableContext = new TestableContext( + InstrumentationRegistry.getInstrumentation().getTargetContext()); + + private MockitoSession mMockingSession; + + @Before + public void setup() throws Exception { + mMockingSession = + mockitoSession() + .initMocks(this) + .strictness(Strictness.LENIENT) + .startMocking(); + + doReturn(mock(IBinder.class)).when(mIMediaProjectionManager).asBinder(); + + // Support the MediaProjection instance. + mFakeIMediaProjection.setLaunchCookie(mock(IBinder.class)); + mMediaProjection = new MediaProjection(mTestableContext, mFakeIMediaProjection, + mIMediaProjectionManager, mDisplayManager); + + // Support creation of the VirtualDisplay. + mTestableContext.addMockSystemService(DisplayManager.class, mDisplayManager); + doReturn(mDisplay).when(mVirtualDisplay).getDisplay(); + doReturn(DEFAULT_DISPLAY + 7).when(mDisplay).getDisplayId(); + doReturn(mVirtualDisplay).when(mDisplayManager).createVirtualDisplay( + any(MediaProjection.class), any(VirtualDisplayConfig.class), + nullable(VirtualDisplay.Callback.class), nullable(Handler.class), + nullable(Context.class)); + } + + @After + public void tearDown() { + mMockingSession.finishMocking(); + } + + @Test + public void testConstruction() throws RemoteException { + assertThat(mMediaProjection).isNotNull(); + assertThat(mFakeIMediaProjection.mIsStarted).isTrue(); + } + + @Test + public void testRegisterCallback_null() { + assertThrows(NullPointerException.class, + () -> mMediaProjection.registerCallback(null, mHandler)); + } + + @Test + public void testUnregisterCallback_null() { + mMediaProjection.registerCallback(mMediaProjectionCallback, mHandler); + assertThrows(NullPointerException.class, + () -> mMediaProjection.unregisterCallback(null)); + } + + @Test + @DisableCompatChanges({MEDIA_PROJECTION_REQUIRES_CALLBACK}) + public void createVirtualDisplay_noCallbackRequired_missingMediaProjectionCallback() { + assertThat(createVirtualDisplay(null)).isNotNull(); + assertThat(createVirtualDisplay(mVirtualDisplayCallback)).isNotNull(); + } + + @Test + @DisableCompatChanges({MEDIA_PROJECTION_REQUIRES_CALLBACK}) + public void createVirtualDisplay_noCallbackRequired_givenMediaProjectionCallback() { + mMediaProjection.registerCallback(mMediaProjectionCallback, mHandler); + assertThat(createVirtualDisplay(null)).isNotNull(); + assertThat(createVirtualDisplay(mVirtualDisplayCallback)).isNotNull(); + } + + @Test + @EnableCompatChanges({MEDIA_PROJECTION_REQUIRES_CALLBACK}) + public void createVirtualDisplay_callbackRequired_missingMediaProjectionCallback() { + assertThrows(IllegalStateException.class, + () -> createVirtualDisplay(mVirtualDisplayCallback)); + } + + @Test + @EnableCompatChanges({MEDIA_PROJECTION_REQUIRES_CALLBACK}) + public void createVirtualDisplay_callbackRequired_givenMediaProjectionCallback() { + mMediaProjection.registerCallback(mMediaProjectionCallback, mHandler); + assertThat(createVirtualDisplay(null)).isNotNull(); + assertThat(createVirtualDisplay(mVirtualDisplayCallback)).isNotNull(); + } + + private VirtualDisplay createVirtualDisplay(@Nullable VirtualDisplay.Callback callback) { + // No recording will take place with a null surface. + return mMediaProjection.createVirtualDisplay( + VIRTUAL_DISPLAY_NAME, VIRTUAL_DISPLAY_WIDTH, + VIRTUAL_DISPLAY_HEIGHT, VIRTUAL_DISPLAY_DENSITY, + VIRTUAL_DISPLAY_FLAGS, /* surface = */ null, + callback, /* handler= */ mHandler); + } +} |