summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--core/api/current.txt4
-rw-r--r--media/java/android/media/projection/MediaProjection.java192
-rw-r--r--media/tests/projection/Android.bp2
-rw-r--r--media/tests/projection/AndroidManifest.xml1
-rw-r--r--media/tests/projection/src/android/media/projection/FakeIMediaProjection.java82
-rw-r--r--media/tests/projection/src/android/media/projection/MediaProjectionTest.java197
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>
* &#x40;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);
+ }
+}