diff --git a/media/java/android/media/projection/IMediaProjection.aidl b/media/java/android/media/projection/IMediaProjection.aidl index 5f7d636fdd1eb..e3829e6a30210 100644 --- a/media/java/android/media/projection/IMediaProjection.aidl +++ b/media/java/android/media/projection/IMediaProjection.aidl @@ -51,4 +51,26 @@ interface IMediaProjection { @JavaPassthrough(annotation = "@android.annotation.RequiresPermission(android.Manifest" + ".permission.MANAGE_MEDIA_PROJECTION)") void setLaunchCookie(in IBinder launchCookie); + + /** + * Returns {@code true} if this token is still valid. A token is valid as long as the token + * hasn't timed out before it was used, and the token is only used once. + * + *
If the {@link IMediaProjection} is not valid, then either throws an exception if the + * target SDK is at least {@code U}, or returns {@code false} for target SDK below {@code U}. + * + * @throws IllegalStateException If the caller's target SDK is at least {@code U} and the + * projection is not valid. + */ + @JavaPassthrough(annotation = "@android.annotation.RequiresPermission(android.Manifest" + + ".permission.MANAGE_MEDIA_PROJECTION)") + boolean isValid(); + + /** + * Sets that {@link MediaProjection#createVirtualDisplay} has been invoked with this token (it + * should only be called once). + */ + @JavaPassthrough(annotation = "@android.annotation.RequiresPermission(android.Manifest" + + ".permission.MANAGE_MEDIA_PROJECTION)") + void notifyVirtualDisplayCreated(int displayId); } diff --git a/media/java/android/media/projection/IMediaProjectionManager.aidl b/media/java/android/media/projection/IMediaProjectionManager.aidl index c97265d4939dd..835e4c3ee4f60 100644 --- a/media/java/android/media/projection/IMediaProjectionManager.aidl +++ b/media/java/android/media/projection/IMediaProjectionManager.aidl @@ -44,6 +44,22 @@ interface IMediaProjectionManager { + ".permission.MANAGE_MEDIA_PROJECTION)") boolean isCurrentProjection(IMediaProjection projection); + /** + * Reshows the permisison dialog for the user to review consent they've already granted in + * the given projection instance. + * + *
Preconditions: + *
Returns immediately but waits to start recording until user has reviewed their consent. + */ + @JavaPassthrough(annotation = "@android.annotation.RequiresPermission(android.Manifest" + + ".permission.MANAGE_MEDIA_PROJECTION)") + void requestConsentForInvalidProjection(IMediaProjection projection); + @JavaPassthrough(annotation = "@android.annotation.RequiresPermission(android.Manifest" + ".permission.MANAGE_MEDIA_PROJECTION)") MediaProjectionInfo getActiveProjectionInfo(); @@ -69,15 +85,18 @@ interface IMediaProjectionManager { void removeCallback(IMediaProjectionWatcherCallback callback); /** - * Updates the content recording session. If a different session is already in progress, then - * the pre-existing session is stopped, and the new incoming session takes over. Only updates - * the session if the given projection is valid. + * Returns {@code true} if it successfully updates the content recording session. Returns + * {@code false} otherwise, and stops the current projection. + * + *
If a different session is already in progress, then the pre-existing session is stopped, + * and the new incoming session takes over. Only updates the session if the given projection is + * valid. * * @param incomingSession the nullable incoming content recording session * @param projection the non-null projection the session describes */ @JavaPassthrough(annotation = "@android.annotation.RequiresPermission(android.Manifest" + ".permission.MANAGE_MEDIA_PROJECTION)") - void setContentRecordingSession(in ContentRecordingSession incomingSession, + boolean setContentRecordingSession(in ContentRecordingSession incomingSession, in IMediaProjection projection); } diff --git a/media/java/android/media/projection/MediaProjection.java b/media/java/android/media/projection/MediaProjection.java index e040bf4967231..f1cffb63af07d 100644 --- a/media/java/android/media/projection/MediaProjection.java +++ b/media/java/android/media/projection/MediaProjection.java @@ -164,12 +164,21 @@ public final class MediaProjection { * @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 + * @throws IllegalStateException In the following scenarios, if the target SDK is {@link + * android.os.Build.VERSION_CODES#UPSIDE_DOWN_CAKE U} and up: + *
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_PREVENTS_REUSING_CONSENT = 266201607L; // buganizer id
+
private final Object mLock = new Object(); // Protects the list of media projections
private final Map Preconditions:
+ * Returns immediately but waits to start recording until user has reviewed their consent.
+ */
+ @VisibleForTesting
+ void requestConsentForInvalidProjection(IMediaProjection projection) {
+ synchronized (mLock) {
+ Slog.v(TAG, "Reusing token: Reshow dialog for due to invalid projection.");
+ // TODO(b/274790702): Trigger the permission dialog again in SysUI.
+ }
+ }
+
+ // TODO(b/261563516): Remove internal method and test aidl directly, here and elsewhere.
+ @VisibleForTesting
+ MediaProjection createProjectionInternal(int uid, String packageName, int type,
+ boolean isPermanentGrant, UserHandle callingUser,
+ boolean packageAttemptedReusingGrantedConsent) {
+ MediaProjection projection;
+ ApplicationInfo ai;
+ try {
+ ai = mPackageManager.getApplicationInfoAsUser(packageName, ApplicationInfoFlags.of(0),
+ callingUser);
+ } catch (NameNotFoundException e) {
+ throw new IllegalArgumentException("No package matching :" + packageName);
+ }
+
+ projection = new MediaProjection(type, uid, packageName, ai.targetSdkVersion,
+ ai.isPrivilegedApp());
+ if (isPermanentGrant) {
+ mAppOps.setMode(AppOpsManager.OP_PROJECT_MEDIA,
+ projection.uid, projection.packageName, AppOpsManager.MODE_ALLOWED);
+ }
+ return projection;
+ }
+
+ @VisibleForTesting
+ MediaProjectionInfo getActiveProjectionInfo() {
synchronized (mLock) {
if (mProjectionGrant == null) {
return null;
@@ -291,22 +418,12 @@ public final class MediaProjectionManagerService extends SystemService
if (packageName == null || packageName.isEmpty()) {
throw new IllegalArgumentException("package name must not be empty");
}
- final ApplicationInfo ai;
- try {
- ai = mPackageManager.getApplicationInfo(packageName, 0);
- } catch (NameNotFoundException e) {
- throw new IllegalArgumentException("No package matching :" + packageName);
- }
-
MediaProjection projection;
+ final UserHandle callingUser = Binder.getCallingUserHandle();
final long callingToken = Binder.clearCallingIdentity();
try {
- projection = new MediaProjection(type, uid, packageName, ai.targetSdkVersion,
- ai.isPrivilegedApp());
- if (isPermanentGrant) {
- mAppOps.setMode(AppOpsManager.OP_PROJECT_MEDIA,
- projection.uid, projection.packageName, AppOpsManager.MODE_ALLOWED);
- }
+ projection = createProjectionInternal(uid, packageName, type, isPermanentGrant,
+ callingUser, false);
} finally {
Binder.restoreCallingIdentity(callingToken);
}
@@ -426,33 +543,49 @@ public final class MediaProjectionManagerService extends SystemService
}
}
- /**
- * Updates the current content mirroring session.
- */
@Override
- public void setContentRecordingSession(@Nullable ContentRecordingSession incomingSession,
+ public boolean setContentRecordingSession(@Nullable ContentRecordingSession incomingSession,
@NonNull IMediaProjection projection) {
+ if (mContext.checkCallingOrSelfPermission(Manifest.permission.MANAGE_MEDIA_PROJECTION)
+ != PackageManager.PERMISSION_GRANTED) {
+ throw new SecurityException("Requires MANAGE_MEDIA_PROJECTION to set session "
+ + "details.");
+ }
+ if (!isCurrentProjection(projection)) {
+ throw new SecurityException("Unable to set ContentRecordingSession on "
+ + "non-current MediaProjection");
+ }
final long origId = Binder.clearCallingIdentity();
try {
- synchronized (mLock) {
- if (!isCurrentProjection(projection)) {
- throw new SecurityException("Unable to set ContentRecordingSession on "
- + "non-current MediaProjection");
- }
- if (!LocalServices.getService(
- WindowManagerInternal.class).setContentRecordingSession(
- incomingSession)) {
- // Unable to start mirroring, so tear down this projection.
- if (mProjectionGrant != null) {
- mProjectionGrant.stop();
- }
- }
- }
+ return MediaProjectionManagerService.this.setContentRecordingSession(
+ incomingSession);
} finally {
Binder.restoreCallingIdentity(origId);
}
}
+ @Override
+ public void requestConsentForInvalidProjection(IMediaProjection projection) {
+ if (mContext.checkCallingOrSelfPermission(Manifest.permission.MANAGE_MEDIA_PROJECTION)
+ != PackageManager.PERMISSION_GRANTED) {
+ throw new SecurityException("Requires MANAGE_MEDIA_PROJECTION to check if the given"
+ + "projection is valid.");
+ }
+ if (!isCurrentProjection(projection)) {
+ Slog.v(TAG, "Reusing token: Won't request consent again for a token that "
+ + "isn't current");
+ return;
+ }
+
+ // Remove calling app identity before performing any privileged operations.
+ final long token = Binder.clearCallingIdentity();
+ try {
+ MediaProjectionManagerService.this.requestConsentForInvalidProjection(projection);
+ } finally {
+ Binder.restoreCallingIdentity(token);
+ }
+ }
+
@Override // Binder call
public void dump(FileDescriptor fd, final PrintWriter pw, String[] args) {
if (!DumpUtils.checkDumpPermission(mContext, TAG, pw)) return;
@@ -471,7 +604,14 @@ public final class MediaProjectionManagerService extends SystemService
}
}
- private final class MediaProjection extends IMediaProjection.Stub {
+ @VisibleForTesting
+ final class MediaProjection extends IMediaProjection.Stub {
+ // Host app has 5 minutes to begin using the token before it is invalid.
+ // Some apps show a dialog for the user to interact with (selecting recording resolution)
+ // before starting capture, but after requesting consent.
+ final long mDefaultTimeoutMs = Duration.ofMinutes(5).toMillis();
+ // The creation timestamp in milliseconds, measured by {@link SystemClock#uptimeMillis}.
+ private final long mCreateTimeMs;
public final int uid;
public final String packageName;
public final UserHandle userHandle;
@@ -485,6 +625,15 @@ public final class MediaProjectionManagerService extends SystemService
private boolean mRestoreSystemAlertWindow;
private IBinder mLaunchCookie = null;
+ // Values for tracking token validity.
+ // Timeout value to compare creation time against.
+ private long mTimeoutMs = mDefaultTimeoutMs;
+ // Count of number of times IMediaProjection#start is invoked.
+ private int mCountStarts = 0;
+ // Set if MediaProjection#createVirtualDisplay has been invoked previously (it
+ // should only be called once).
+ private int mVirtualDisplayId = INVALID_DISPLAY;
+
MediaProjection(int type, int uid, String packageName, int targetSdkVersion,
boolean isPrivileged) {
mType = type;
@@ -493,6 +642,7 @@ public final class MediaProjectionManagerService extends SystemService
userHandle = new UserHandle(UserHandle.getUserId(uid));
mTargetSdkVersion = targetSdkVersion;
mIsPrivileged = isPrivileged;
+ mCreateTimeMs = mClock.uptimeMillis();
// TODO(b/267740338): Add unit test.
mActivityManagerInternal.notifyMediaProjectionEvent(uid, asBinder(),
MEDIA_PROJECTION_TOKEN_EVENT_CREATED);
@@ -554,6 +704,9 @@ public final class MediaProjectionManagerService extends SystemService
if (isCurrentProjection(asBinder())) {
Slog.w(TAG, "UID " + Binder.getCallingUid()
+ " attempted to start already started MediaProjection");
+ // It is possible the app didn't explicitly invoke stop before trying to start
+ // again; ensure this start is counted in case they are re-using this token.
+ mCountStarts++;
return;
}
@@ -612,6 +765,8 @@ public final class MediaProjectionManagerService extends SystemService
}
}
startProjectionLocked(this);
+ // Mark this token as used when the app gets the MediaProjection instance.
+ mCountStarts++;
}
}
@@ -689,6 +844,51 @@ public final class MediaProjectionManagerService extends SystemService
return mLaunchCookie;
}
+ @Override
+ public boolean isValid() {
+ if (mContext.checkCallingOrSelfPermission(Manifest.permission.MANAGE_MEDIA_PROJECTION)
+ != PackageManager.PERMISSION_GRANTED) {
+ throw new SecurityException("Requires MANAGE_MEDIA_PROJECTION to check if this"
+ + "projection is valid.");
+ }
+ synchronized (mLock) {
+ final long curMs = mClock.uptimeMillis();
+ final boolean hasTimedOut = curMs - mCreateTimeMs > mTimeoutMs;
+ final boolean virtualDisplayCreated = mVirtualDisplayId != INVALID_DISPLAY;
+ final boolean isValid =
+ !hasTimedOut && (mCountStarts <= 1) && !virtualDisplayCreated;
+ if (isValid) {
+ return true;
+ }
+
+ // Can safely use mProjectionGrant since we know this is the current projection.
+ if (mInjector.shouldMediaProjectionPreventReusingConsent(mProjectionGrant)) {
+ Slog.v(TAG, "Reusing token: Throw exception due to invalid projection.");
+ // Tear down projection here; necessary to ensure (among other reasons) that
+ // stop is dispatched to client and cast icon disappears from status bar.
+ mProjectionGrant.stop();
+ throw new IllegalStateException("Don't re-use the resultData to retrieve "
+ + "the same projection instance, and don't use a token that has "
+ + "timed out. Don't take multiple captures by invoking "
+ + "MediaProjection#createVirtualDisplay multiple times on the "
+ + "same instance.");
+ }
+ return false;
+ }
+ }
+
+ @Override
+ public void notifyVirtualDisplayCreated(int displayId) {
+ if (mContext.checkCallingOrSelfPermission(Manifest.permission.MANAGE_MEDIA_PROJECTION)
+ != PackageManager.PERMISSION_GRANTED) {
+ throw new SecurityException("Requires MANAGE_MEDIA_PROJECTION to notify virtual "
+ + "display created.");
+ }
+ synchronized (mLock) {
+ mVirtualDisplayId = displayId;
+ }
+ }
+
public MediaProjectionInfo getProjectionInfo() {
return new MediaProjectionInfo(packageName, userHandle);
}
@@ -804,7 +1004,7 @@ public final class MediaProjectionManagerService extends SystemService
return;
}
synchronized (mLock) {
- // TODO(b/249827847) Currently the service assumes there is only one projection
+ // TODO(b/249827847): Currently the service assumes there is only one projection
// at once - need to find the callback for the given projection, when there are
// multiple sessions.
for (IMediaProjectionCallback callback : mClientCallbacks.values()) {
@@ -832,7 +1032,7 @@ public final class MediaProjectionManagerService extends SystemService
return;
}
synchronized (mLock) {
- // TODO(b/249827847) Currently the service assumes there is only one projection
+ // TODO(b/249827847): Currently the service assumes there is only one projection
// at once - need to find the callback for the given projection, when there are
// multiple sessions.
for (IMediaProjectionCallback callback : mClientCallbacks.values()) {
diff --git a/services/core/java/com/android/server/wm/ContentRecorder.java b/services/core/java/com/android/server/wm/ContentRecorder.java
index f1c5f91146ab3..b808a55d29526 100644
--- a/services/core/java/com/android/server/wm/ContentRecorder.java
+++ b/services/core/java/com/android/server/wm/ContentRecorder.java
@@ -296,6 +296,9 @@ final class ContentRecorder implements WindowContainerListener {
+ "state %d",
mDisplayContent.getDisplayId(), mDisplayContent.getDisplayInfo().state);
+ // TODO(b/274790702): Do not start recording if waiting for consent - for now,
+ // go ahead.
+
// Create a mirrored hierarchy for the SurfaceControl of the DisplayArea to capture.
mRecordedSurface = SurfaceControl.mirrorSurface(
mRecordedWindowContainer.getSurfaceControl());
diff --git a/services/tests/servicestests/Android.bp b/services/tests/servicestests/Android.bp
index 6f26a5fe89709..cfeaf0b545522 100644
--- a/services/tests/servicestests/Android.bp
+++ b/services/tests/servicestests/Android.bp
@@ -42,6 +42,7 @@ android_test {
"androidx.test.ext.truth",
"androidx.test.runner",
"androidx.test.rules",
+ "androidx.test.ext.junit",
"cts-wm-util",
"platform-compat-test-rules",
"mockito-target-minus-junit4",
diff --git a/services/tests/servicestests/src/com/android/server/display/DisplayManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/display/DisplayManagerServiceTest.java
index b5237a5b34fd8..acfc0736282e2 100644
--- a/services/tests/servicestests/src/com/android/server/display/DisplayManagerServiceTest.java
+++ b/services/tests/servicestests/src/com/android/server/display/DisplayManagerServiceTest.java
@@ -37,6 +37,7 @@ import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.ArgumentMatchers.nullable;
import static org.mockito.Mockito.any;
import static org.mockito.Mockito.atLeastOnce;
import static org.mockito.Mockito.doReturn;
@@ -289,7 +290,7 @@ public class DisplayManagerServiceTest {
}
@Test
- public void testCreateVirtualDisplay_sentToInputManager() {
+ public void testCreateVirtualDisplay_sentToInputManager() throws RemoteException {
// This is to update the display device config such that DisplayManagerService can ignore
// the usage of SensorManager, which is available only after the PowerManagerService
// is ready.
@@ -316,7 +317,8 @@ public class DisplayManagerServiceTest {
builder.setFlags(flags);
int displayId = bs.createVirtualDisplay(builder.build(), mMockAppToken /* callback */,
null /* projection */, PACKAGE_NAME);
- verify(mMockWindowManagerInternal, never()).setContentRecordingSession(Mockito.any());
+ verify(mMockProjectionService, never()).setContentRecordingSession(any(),
+ nullable(IMediaProjection.class));
displayManager.performTraversalInternal(mock(SurfaceControl.Transaction.class));
@@ -442,7 +444,8 @@ public class DisplayManagerServiceTest {
builder.setUniqueId(uniqueId);
int displayId = bs.createVirtualDisplay(builder.build(), mMockAppToken /* callback */,
null /* projection */, PACKAGE_NAME);
- verify(mMockWindowManagerInternal, never()).setContentRecordingSession(Mockito.any());
+ verify(mMockProjectionService, never()).setContentRecordingSession(any(),
+ nullable(IMediaProjection.class));
displayManager.performTraversalInternal(mock(SurfaceControl.Transaction.class));
@@ -455,7 +458,7 @@ public class DisplayManagerServiceTest {
}
@Test
- public void testCreateVirtualDisplayOwnFocus() {
+ public void testCreateVirtualDisplayOwnFocus() throws RemoteException {
DisplayManagerService displayManager =
new DisplayManagerService(mContext, mBasicInjector);
registerDefaultDisplays(displayManager);
@@ -479,7 +482,8 @@ public class DisplayManagerServiceTest {
builder.setUniqueId(uniqueId);
int displayId = bs.createVirtualDisplay(builder.build(), /* callback= */ mMockAppToken,
/* projection= */ null, PACKAGE_NAME);
- verify(mMockWindowManagerInternal, never()).setContentRecordingSession(Mockito.any());
+ verify(mMockProjectionService, never()).setContentRecordingSession(any(),
+ nullable(IMediaProjection.class));
displayManager.performTraversalInternal(mock(SurfaceControl.Transaction.class));
@@ -492,7 +496,7 @@ public class DisplayManagerServiceTest {
}
@Test
- public void testCreateVirtualDisplayOwnFocus_nonTrustedDisplay() {
+ public void testCreateVirtualDisplayOwnFocus_nonTrustedDisplay() throws RemoteException {
DisplayManagerService displayManager =
new DisplayManagerService(mContext, mBasicInjector);
registerDefaultDisplays(displayManager);
@@ -513,7 +517,8 @@ public class DisplayManagerServiceTest {
builder.setUniqueId(uniqueId);
int displayId = bs.createVirtualDisplay(builder.build(), /* callback= */ mMockAppToken,
/* projection= */ null, PACKAGE_NAME);
- verify(mMockWindowManagerInternal, never()).setContentRecordingSession(Mockito.any());
+ verify(mMockProjectionService, never()).setContentRecordingSession(any(),
+ nullable(IMediaProjection.class));
displayManager.performTraversalInternal(mock(SurfaceControl.Transaction.class));
@@ -755,7 +760,8 @@ public class DisplayManagerServiceTest {
builder.setUniqueId(uniqueId);
final int firstDisplayId = binderService.createVirtualDisplay(builder.build(),
mMockAppToken /* callback */, null /* projection */, PACKAGE_NAME);
- verify(mMockWindowManagerInternal, never()).setContentRecordingSession(Mockito.any());
+ verify(mMockProjectionService, never()).setContentRecordingSession(any(),
+ nullable(IMediaProjection.class));
// The second virtual display requests to mirror the first virtual display.
final String uniqueId2 = "uniqueId --- displayIdToMirrorTest #2";
@@ -767,7 +773,8 @@ public class DisplayManagerServiceTest {
final int secondDisplayId = binderService.createVirtualDisplay(builder2.build(),
mMockAppToken2 /* callback */, null /* projection */,
PACKAGE_NAME);
- verify(mMockWindowManagerInternal, never()).setContentRecordingSession(Mockito.any());
+ verify(mMockProjectionService, never()).setContentRecordingSession(any(),
+ nullable(IMediaProjection.class));
displayManager.performTraversalInternal(mock(SurfaceControl.Transaction.class));
// flush the handler
@@ -805,7 +812,8 @@ public class DisplayManagerServiceTest {
virtualDevice /* virtualDeviceToken */,
mock(DisplayWindowPolicyController.class),
PACKAGE_NAME);
- verify(mMockWindowManagerInternal, never()).setContentRecordingSession(Mockito.any());
+ verify(mMockProjectionService, never()).setContentRecordingSession(any(),
+ nullable(IMediaProjection.class));
int displayGroupId1 = localService.getDisplayInfo(displayId1).displayGroupId;
// Create a second virtual display. This should be added to the previously created display
@@ -821,7 +829,8 @@ public class DisplayManagerServiceTest {
virtualDevice /* virtualDeviceToken */,
mock(DisplayWindowPolicyController.class),
PACKAGE_NAME);
- verify(mMockWindowManagerInternal, never()).setContentRecordingSession(Mockito.any());
+ verify(mMockProjectionService, never()).setContentRecordingSession(any(),
+ nullable(IMediaProjection.class));
int displayGroupId2 = localService.getDisplayInfo(displayId2).displayGroupId;
assertEquals(
@@ -859,7 +868,8 @@ public class DisplayManagerServiceTest {
virtualDevice /* virtualDeviceToken */,
mock(DisplayWindowPolicyController.class),
PACKAGE_NAME);
- verify(mMockWindowManagerInternal, never()).setContentRecordingSession(Mockito.any());
+ verify(mMockProjectionService, never()).setContentRecordingSession(any(),
+ nullable(IMediaProjection.class));
int displayGroupId1 = localService.getDisplayInfo(displayId1).displayGroupId;
// Create a second virtual display. With the flag VIRTUAL_DISPLAY_FLAG_OWN_DISPLAY_GROUP,
@@ -878,7 +888,8 @@ public class DisplayManagerServiceTest {
virtualDevice /* virtualDeviceToken */,
mock(DisplayWindowPolicyController.class),
PACKAGE_NAME);
- verify(mMockWindowManagerInternal, never()).setContentRecordingSession(Mockito.any());
+ verify(mMockProjectionService, never()).setContentRecordingSession(any(),
+ nullable(IMediaProjection.class));
int displayGroupId2 = localService.getDisplayInfo(displayId2).displayGroupId;
assertNotEquals(
@@ -922,7 +933,8 @@ public class DisplayManagerServiceTest {
virtualDevice /* virtualDeviceToken */,
mock(DisplayWindowPolicyController.class),
PACKAGE_NAME);
- verify(mMockWindowManagerInternal, never()).setContentRecordingSession(Mockito.any());
+ verify(mMockProjectionService, never()).setContentRecordingSession(any(),
+ nullable(IMediaProjection.class));
// Check that FLAG_ALWAYS_UNLOCKED is set.
assertNotEquals(
@@ -948,7 +960,8 @@ public class DisplayManagerServiceTest {
virtualDevice /* virtualDeviceToken */,
mock(DisplayWindowPolicyController.class),
PACKAGE_NAME);
- verify(mMockWindowManagerInternal, never()).setContentRecordingSession(Mockito.any());
+ verify(mMockProjectionService, never()).setContentRecordingSession(any(),
+ nullable(IMediaProjection.class));
// Check that FLAG_ALWAYS_UNLOCKED is set.
assertNotEquals(
@@ -972,7 +985,8 @@ public class DisplayManagerServiceTest {
null /* virtualDeviceToken */,
mock(DisplayWindowPolicyController.class),
PACKAGE_NAME);
- verify(mMockWindowManagerInternal, never()).setContentRecordingSession(Mockito.any());
+ verify(mMockProjectionService, never()).setContentRecordingSession(any(),
+ nullable(IMediaProjection.class));
// Check that FLAG_ALWAYS_UNLOCKED is not set.
assertEquals(
@@ -1004,7 +1018,8 @@ public class DisplayManagerServiceTest {
.setFlags(VIRTUAL_DISPLAY_FLAG_OWN_CONTENT_ONLY);
final int firstDisplayId = binderService.createVirtualDisplay(builder.build(),
mMockAppToken /* callback */, null /* projection */, PACKAGE_NAME);
- verify(mMockWindowManagerInternal, never()).setContentRecordingSession(Mockito.any());
+ verify(mMockProjectionService, never()).setContentRecordingSession(any(),
+ nullable(IMediaProjection.class));
// The second virtual display requests to mirror the first virtual display.
final String uniqueId2 = "uniqueId --- displayIdToMirrorTest #2";
@@ -1016,7 +1031,8 @@ public class DisplayManagerServiceTest {
final int secondDisplayId = binderService.createVirtualDisplay(builder2.build(),
mMockAppToken2 /* callback */, null /* projection */,
PACKAGE_NAME);
- verify(mMockWindowManagerInternal, never()).setContentRecordingSession(Mockito.any());
+ verify(mMockProjectionService, never()).setContentRecordingSession(any(),
+ nullable(IMediaProjection.class));
displayManager.performTraversalInternal(mock(SurfaceControl.Transaction.class));
// flush the handler
@@ -1031,15 +1047,51 @@ public class DisplayManagerServiceTest {
Display.INVALID_DISPLAY);
}
+ @Test
+ public void testCreateVirtualDisplay_isValidProjection_notValid()
+ throws RemoteException {
+ when(mMockAppToken.asBinder()).thenReturn(mMockAppToken);
+ IMediaProjection projection = mock(IMediaProjection.class);
+ doReturn(false).when(projection).isValid();
+ when(mMockProjectionService
+ .setContentRecordingSession(any(ContentRecordingSession.class), eq(projection)))
+ .thenReturn(true);
+ doReturn(true).when(mMockProjectionService).isCurrentProjection(eq(projection));
+
+ final VirtualDisplayConfig.Builder builder = new VirtualDisplayConfig.Builder(
+ VIRTUAL_DISPLAY_NAME, 600, 800, 320);
+ builder.setUniqueId("uniqueId --- isValid false");
+
+ DisplayManagerService displayManager = new DisplayManagerService(mContext, mBasicInjector);
+ registerDefaultDisplays(displayManager);
+ displayManager.windowManagerAndInputReady();
+
+ // Pass in a non-null projection.
+ DisplayManagerService.BinderService binderService = displayManager.new BinderService();
+ final int displayId = binderService.createVirtualDisplay(builder.build(),
+ mMockAppToken /* callback */, projection, PACKAGE_NAME);
+
+ // VirtualDisplay is created for mirroring.
+ assertThat(displayId).isNotEqualTo(Display.INVALID_DISPLAY);
+ verify(mMockProjectionService, atLeastOnce()).setContentRecordingSession(
+ any(ContentRecordingSession.class), nullable(IMediaProjection.class));
+ // But mirroring doesn't begin.
+ verify(mMockProjectionService, atLeastOnce()).setContentRecordingSession(
+ mContentRecordingSessionCaptor.capture(), nullable(IMediaProjection.class));
+ ContentRecordingSession session = mContentRecordingSessionCaptor.getValue();
+ assertThat(session.isWaitingToRecord()).isTrue();
+ }
+
@Test
public void testCreateVirtualDisplay_setContentRecordingSessionSuccess()
throws RemoteException {
final int displayToRecord = 50;
when(mMockAppToken.asBinder()).thenReturn(mMockAppToken);
- when(mMockWindowManagerInternal
- .setContentRecordingSession(any(ContentRecordingSession.class)))
- .thenReturn(true);
IMediaProjection projection = mock(IMediaProjection.class);
+ doReturn(true).when(projection).isValid();
+ when(mMockProjectionService
+ .setContentRecordingSession(any(ContentRecordingSession.class), eq(projection)))
+ .thenReturn(true);
doReturn(true).when(mMockProjectionService).isCurrentProjection(eq(projection));
final VirtualDisplayConfig.Builder builder = new VirtualDisplayConfig.Builder(
@@ -1056,21 +1108,23 @@ public class DisplayManagerServiceTest {
mMockAppToken /* callback */, projection, PACKAGE_NAME);
assertThat(displayId).isNotEqualTo(Display.INVALID_DISPLAY);
- verify(mMockWindowManagerInternal, atLeastOnce()).setContentRecordingSession(
- mContentRecordingSessionCaptor.capture());
+ verify(mMockProjectionService, atLeastOnce()).setContentRecordingSession(
+ mContentRecordingSessionCaptor.capture(), nullable(IMediaProjection.class));
ContentRecordingSession session = mContentRecordingSessionCaptor.getValue();
assertThat(session.getContentToRecord()).isEqualTo(RECORD_CONTENT_DISPLAY);
assertThat(session.getVirtualDisplayId()).isEqualTo(displayId);
assertThat(session.getDisplayToRecord()).isEqualTo(displayToRecord);
+ assertThat(session.isWaitingToRecord()).isFalse();
}
@Test
public void testCreateVirtualDisplay_setContentRecordingSessionFail() throws RemoteException {
when(mMockAppToken.asBinder()).thenReturn(mMockAppToken);
- when(mMockWindowManagerInternal
- .setContentRecordingSession(any(ContentRecordingSession.class)))
- .thenReturn(false);
IMediaProjection projection = mock(IMediaProjection.class);
+ doReturn(true).when(projection).isValid();
+ when(mMockProjectionService
+ .setContentRecordingSession(any(ContentRecordingSession.class), eq(projection)))
+ .thenReturn(false);
doReturn(true).when(mMockProjectionService).isCurrentProjection(eq(projection));
final VirtualDisplayConfig.Builder builder = new VirtualDisplayConfig.Builder(
@@ -1093,12 +1147,12 @@ public class DisplayManagerServiceTest {
throws RemoteException {
final int displayToRecord = 50;
when(mMockAppToken.asBinder()).thenReturn(mMockAppToken);
- when(mMockWindowManagerInternal
- .setContentRecordingSession(any(ContentRecordingSession.class)))
- .thenReturn(true);
IMediaProjection projection = mock(IMediaProjection.class);
+ doReturn(true).when(projection).isValid();
+ when(mMockProjectionService
+ .setContentRecordingSession(any(ContentRecordingSession.class), eq(projection)))
+ .thenReturn(true);
doReturn(mock(IBinder.class)).when(projection).getLaunchCookie();
-
doReturn(true).when(mMockProjectionService).isCurrentProjection(eq(projection));
final VirtualDisplayConfig.Builder builder = new VirtualDisplayConfig.Builder(
@@ -1115,8 +1169,8 @@ public class DisplayManagerServiceTest {
mMockAppToken /* callback */, projection, PACKAGE_NAME);
assertThat(displayId).isNotEqualTo(Display.INVALID_DISPLAY);
- verify(mMockWindowManagerInternal, atLeastOnce()).setContentRecordingSession(
- mContentRecordingSessionCaptor.capture());
+ verify(mMockProjectionService, atLeastOnce()).setContentRecordingSession(
+ mContentRecordingSessionCaptor.capture(), nullable(IMediaProjection.class));
ContentRecordingSession session = mContentRecordingSessionCaptor.getValue();
assertThat(session.getContentToRecord()).isEqualTo(RECORD_CONTENT_TASK);
assertThat(session.getVirtualDisplayId()).isEqualTo(displayId);
@@ -1124,7 +1178,8 @@ public class DisplayManagerServiceTest {
}
@Test
- public void testCreateVirtualDisplay_setContentRecordingSession_noProjection_noFlags() {
+ public void testCreateVirtualDisplay_setContentRecordingSession_noProjection_noFlags()
+ throws RemoteException {
when(mMockAppToken.asBinder()).thenReturn(mMockAppToken);
// Set no flags for the VirtualDisplay.
@@ -1143,12 +1198,13 @@ public class DisplayManagerServiceTest {
// VirtualDisplay is created but not for mirroring.
assertThat(displayId).isNotEqualTo(Display.INVALID_DISPLAY);
- verify(mMockWindowManagerInternal, never()).setContentRecordingSession(
- any(ContentRecordingSession.class));
+ verify(mMockProjectionService, never()).setContentRecordingSession(
+ any(ContentRecordingSession.class), nullable(IMediaProjection.class));
}
@Test
- public void testCreateVirtualDisplay_setContentRecordingSession_noProjection_noMirroringFlag() {
+ public void testCreateVirtualDisplay_setContentRecordingSession_noProjection_noMirroringFlag()
+ throws RemoteException {
when(mMockAppToken.asBinder()).thenReturn(mMockAppToken);
// Set a non-mirroring flag for the VirtualDisplay.
@@ -1168,18 +1224,19 @@ public class DisplayManagerServiceTest {
// VirtualDisplay is created but not for mirroring.
assertThat(displayId).isNotEqualTo(Display.INVALID_DISPLAY);
- verify(mMockWindowManagerInternal, never()).setContentRecordingSession(
- any(ContentRecordingSession.class));
+ verify(mMockProjectionService, never()).setContentRecordingSession(
+ any(ContentRecordingSession.class), nullable(IMediaProjection.class));
}
@Test
public void testCreateVirtualDisplay_setContentRecordingSession_projection_noMirroringFlag()
throws RemoteException {
when(mMockAppToken.asBinder()).thenReturn(mMockAppToken);
- when(mMockWindowManagerInternal
- .setContentRecordingSession(any(ContentRecordingSession.class)))
- .thenReturn(true);
IMediaProjection projection = mock(IMediaProjection.class);
+ doReturn(true).when(projection).isValid();
+ when(mMockProjectionService
+ .setContentRecordingSession(any(ContentRecordingSession.class), eq(projection)))
+ .thenReturn(true);
doReturn(true).when(mMockProjectionService).isCurrentProjection(eq(projection));
// Set no flags for the VirtualDisplay.
@@ -1198,8 +1255,8 @@ public class DisplayManagerServiceTest {
// VirtualDisplay is created for mirroring.
assertThat(displayId).isNotEqualTo(Display.INVALID_DISPLAY);
- verify(mMockWindowManagerInternal, atLeastOnce()).setContentRecordingSession(
- any(ContentRecordingSession.class));
+ verify(mMockProjectionService, atLeastOnce()).setContentRecordingSession(
+ any(ContentRecordingSession.class), nullable(IMediaProjection.class));
}
/**
@@ -1228,7 +1285,8 @@ public class DisplayManagerServiceTest {
builder.setUniqueId(uniqueId);
final int displayId = binderService.createVirtualDisplay(builder.build(),
mMockAppToken /* callback */, null /* projection */, PACKAGE_NAME);
- verify(mMockWindowManagerInternal, never()).setContentRecordingSession(Mockito.any());
+ verify(mMockProjectionService, never()).setContentRecordingSession(any(),
+ nullable(IMediaProjection.class));
displayManager.performTraversalInternal(mock(SurfaceControl.Transaction.class));
@@ -1243,7 +1301,8 @@ public class DisplayManagerServiceTest {
* ADD_TRUSTED_DISPLAY is granted.
*/
@Test
- public void testOwnDisplayGroup_allowCreationWithAddTrustedDisplayPermission() {
+ public void testOwnDisplayGroup_allowCreationWithAddTrustedDisplayPermission()
+ throws RemoteException {
DisplayManagerService displayManager =
new DisplayManagerService(mContext, mBasicInjector);
registerDefaultDisplays(displayManager);
@@ -1261,7 +1320,8 @@ public class DisplayManagerServiceTest {
int displayId = bs.createVirtualDisplay(builder.build(), mMockAppToken /* callback */,
null /* projection */, PACKAGE_NAME);
- verify(mMockWindowManagerInternal, never()).setContentRecordingSession(Mockito.any());
+ verify(mMockProjectionService, never()).setContentRecordingSession(any(),
+ nullable(IMediaProjection.class));
displayManager.performTraversalInternal(mock(SurfaceControl.Transaction.class));
displayManager.getDisplayHandler().runWithScissors(() -> {}, 0 /* now */);
DisplayDeviceInfo ddi = displayManager.getDisplayDeviceInfoInternal(displayId);
@@ -1329,7 +1389,8 @@ public class DisplayManagerServiceTest {
int displayId = localService.createVirtualDisplay(builder.build(),
mMockAppToken /* callback */, virtualDevice /* virtualDeviceToken */,
mock(DisplayWindowPolicyController.class), PACKAGE_NAME);
- verify(mMockWindowManagerInternal, never()).setContentRecordingSession(Mockito.any());
+ verify(mMockProjectionService, never()).setContentRecordingSession(any(),
+ nullable(IMediaProjection.class));
displayManager.performTraversalInternal(mock(SurfaceControl.Transaction.class));
displayManager.getDisplayHandler().runWithScissors(() -> {}, 0 /* now */);
DisplayDeviceInfo ddi = displayManager.getDisplayDeviceInfoInternal(displayId);
diff --git a/services/tests/servicestests/src/com/android/server/media/projection/MediaProjectionManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/media/projection/MediaProjectionManagerServiceTest.java
new file mode 100644
index 0000000000000..36c200188fcfa
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/media/projection/MediaProjectionManagerServiceTest.java
@@ -0,0 +1,427 @@
+/*
+ * 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 com.android.server.media.projection;
+
+
+import static android.content.pm.ApplicationInfo.PRIVATE_FLAG_PRIVILEGED;
+import static android.media.projection.MediaProjectionManager.TYPE_MIRRORING;
+import static android.view.Display.DEFAULT_DISPLAY;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.spy;
+import static org.testng.Assert.assertThrows;
+
+import android.app.ActivityManagerInternal;
+import android.content.Context;
+import android.content.ContextWrapper;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.PackageManager.ApplicationInfoFlags;
+import android.content.pm.PackageManager.NameNotFoundException;
+import android.media.projection.IMediaProjectionCallback;
+import android.os.IBinder;
+import android.os.RemoteException;
+import android.os.UserHandle;
+import android.platform.test.annotations.Presubmit;
+import android.view.ContentRecordingSession;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+import androidx.test.platform.app.InstrumentationRegistry;
+
+import com.android.server.LocalServices;
+import com.android.server.testutils.OffsettableClock;
+import com.android.server.wm.WindowManagerInternal;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+/**
+ * Tests for the {@link MediaProjectionManagerService} class.
+ *
+ * Build/Install/Run:
+ * atest FrameworksServicesTests:MediaProjectionManagerServiceTest
+ */
+@SmallTest
+@Presubmit
+@RunWith(AndroidJUnit4.class)
+public class MediaProjectionManagerServiceTest {
+ private static final int UID = 10;
+ private static final String PACKAGE_NAME = "test.package";
+ private final ApplicationInfo mAppInfo = new ApplicationInfo();
+ private static final ContentRecordingSession DISPLAY_SESSION =
+ ContentRecordingSession.createDisplaySession(DEFAULT_DISPLAY);
+ // Callback registered by an app on a MediaProjection instance.
+ private final FakeIMediaProjectionCallback mIMediaProjectionCallback =
+ new FakeIMediaProjectionCallback();
+
+ private final MediaProjectionManagerService.Injector mPreventReusedTokenEnabledInjector =
+ new MediaProjectionManagerService.Injector() {
+ @Override
+ boolean shouldMediaProjectionPreventReusingConsent(
+ MediaProjectionManagerService.MediaProjection projection) {
+ return true;
+ }
+ };
+
+ private final MediaProjectionManagerService.Injector mPreventReusedTokenDisabledInjector =
+ new MediaProjectionManagerService.Injector() {
+ @Override
+ boolean shouldMediaProjectionPreventReusingConsent(
+ MediaProjectionManagerService.MediaProjection projection) {
+ return false;
+ }
+ };
+
+ private Context mContext;
+ private MediaProjectionManagerService mService;
+ private OffsettableClock mClock;
+ private ContentRecordingSession mWaitingDisplaySession =
+ ContentRecordingSession.createDisplaySession(DEFAULT_DISPLAY);
+
+ @Mock
+ private ActivityManagerInternal mAmInternal;
+ @Mock
+ private WindowManagerInternal mWindowManagerInternal;
+ @Mock
+ private PackageManager mPackageManager;
+
+ @Before
+ public void setup() throws Exception {
+ MockitoAnnotations.initMocks(this);
+
+ LocalServices.removeServiceForTest(ActivityManagerInternal.class);
+ LocalServices.addService(ActivityManagerInternal.class, mAmInternal);
+ LocalServices.removeServiceForTest(WindowManagerInternal.class);
+ LocalServices.addService(WindowManagerInternal.class, mWindowManagerInternal);
+
+ mContext = spy(new ContextWrapper(
+ InstrumentationRegistry.getInstrumentation().getTargetContext()));
+ doReturn(mPackageManager).when(mContext).getPackageManager();
+
+ mClock = new OffsettableClock.Stopped();
+ mWaitingDisplaySession.setWaitingToRecord(true);
+ mWaitingDisplaySession.setVirtualDisplayId(5);
+
+ mAppInfo.targetSdkVersion = 32;
+
+ mService = new MediaProjectionManagerService(mContext);
+ }
+
+ @After
+ public void tearDown() {
+ LocalServices.removeServiceForTest(ActivityManagerInternal.class);
+ LocalServices.removeServiceForTest(WindowManagerInternal.class);
+ }
+
+ @Test
+ public void testGetActiveProjectionInfoInternal() throws NameNotFoundException {
+ assertThat(mService.getActiveProjectionInfo()).isNull();
+
+ MediaProjectionManagerService.MediaProjection projection = startProjectionPreconditions();
+
+ // Create a projection, active is still null.
+ assertThat(projection).isNotNull();
+ assertThat(mService.getActiveProjectionInfo()).isNull();
+
+ // Start the projection, active is now not null.
+ projection.start(mIMediaProjectionCallback);
+ assertThat(mService.getActiveProjectionInfo()).isNotNull();
+ }
+
+ @Test
+ public void testCreateProjection() throws NameNotFoundException {
+ MediaProjectionManagerService.MediaProjection projection =
+ startProjectionPreconditions(/* packageAttemptedReusingGrantedConsent= */ false);
+ projection.start(mIMediaProjectionCallback);
+
+ MediaProjectionManagerService.MediaProjection secondProjection =
+ startProjectionPreconditions(/* packageAttemptedReusingGrantedConsent= */ false);
+ assertThat(secondProjection).isNotNull();
+ assertThat(secondProjection).isNotEqualTo(projection);
+ }
+
+ @Test
+ public void testCreateProjection_attemptReuse_noPriorProjectionGrant()
+ throws NameNotFoundException {
+ MediaProjectionManagerService.MediaProjection projection =
+ startProjectionPreconditions(/* packageAttemptedReusingGrantedConsent= */ false);
+ projection.start(mIMediaProjectionCallback);
+
+ MediaProjectionManagerService.MediaProjection secondProjection =
+ startProjectionPreconditions(/* packageAttemptedReusingGrantedConsent= */ true);
+
+ assertThat(secondProjection).isNotNull();
+ assertThat(secondProjection).isNotEqualTo(projection);
+ }
+
+ @Test
+ public void testCreateProjection_attemptReuse_priorProjectionGrant_notWaiting()
+ throws NameNotFoundException {
+ MediaProjectionManagerService.MediaProjection projection =
+ startProjectionPreconditions(/* packageAttemptedReusingGrantedConsent= */ false);
+ projection.start(mIMediaProjectionCallback);
+
+ // Mark this projection as not waiting.
+ doReturn(true).when(mWindowManagerInternal).setContentRecordingSession(
+ any(ContentRecordingSession.class));
+ mService.setContentRecordingSession(DISPLAY_SESSION);
+
+ // We are allowed to create another projection.
+ MediaProjectionManagerService.MediaProjection secondProjection =
+ startProjectionPreconditions(/* packageAttemptedReusingGrantedConsent= */ true);
+
+ assertThat(secondProjection).isNotNull();
+
+ // But this is a new projection.
+ assertThat(secondProjection).isNotEqualTo(projection);
+ }
+
+ @Test
+ public void testCreateProjection_attemptReuse_priorProjectionGrant_waiting_differentPackage()
+ throws NameNotFoundException {
+ MediaProjectionManagerService.MediaProjection projection =
+ startProjectionPreconditions(/* packageAttemptedReusingGrantedConsent= */ false);
+ projection.start(mIMediaProjectionCallback);
+
+ // Mark this projection as not waiting.
+ mService.setContentRecordingSession(mWaitingDisplaySession);
+
+ // We are allowed to create another projection.
+ MediaProjectionManagerService.MediaProjection secondProjection =
+ mService.createProjectionInternal(UID + 10, PACKAGE_NAME + "foo",
+ TYPE_MIRRORING, /* isPermanentGrant= */ true,
+ UserHandle.CURRENT, /* packageAttemptedReusingGrantedConsent= */ true);
+
+ assertThat(secondProjection).isNotNull();
+
+ // But this is a new projection.
+ assertThat(secondProjection).isNotEqualTo(projection);
+ }
+
+ @Test
+ public void testIsValid_multipleStarts_preventionDisabled() throws NameNotFoundException {
+ MediaProjectionManagerService service = new MediaProjectionManagerService(mContext,
+ mPreventReusedTokenDisabledInjector);
+ MediaProjectionManagerService.MediaProjection projection = startProjectionPreconditions(
+ service);
+ // No starts yet, and not timed out yet - so still valid.
+ assertThat(projection.isValid()).isTrue();
+
+ // Only one start - so still valid.
+ projection.start(mIMediaProjectionCallback);
+ assertThat(projection.isValid()).isTrue();
+
+ // Second start - technically allowed to start again, without stopping in between.
+ // Token should no longer be valid.
+ projection.start(mIMediaProjectionCallback);
+ assertThat(projection.isValid()).isFalse();
+ }
+
+ @Test
+ public void testIsValid_restart() throws NameNotFoundException {
+ MediaProjectionManagerService service = new MediaProjectionManagerService(mContext,
+ mPreventReusedTokenDisabledInjector);
+ MediaProjectionManagerService.MediaProjection projection = startProjectionPreconditions(
+ service);
+ // No starts yet, and not timed out yet - so still valid.
+ assertThat(projection.isValid()).isTrue();
+
+ // Only one start - so still valid.
+ projection.start(mIMediaProjectionCallback);
+ assertThat(projection.isValid()).isTrue();
+
+ projection.stop();
+
+ // Second start - so not valid.
+ projection.start(mIMediaProjectionCallback);
+ assertThat(projection.isValid()).isFalse();
+ }
+
+ @Test
+ public void testIsValid_timeout() throws NameNotFoundException {
+ final MediaProjectionManagerService.Injector mClockInjector =
+ new MediaProjectionManagerService.Injector() {
+ @Override
+ MediaProjectionManagerService.Clock createClock() {
+ // Always return the same value for elapsed time.
+ return () -> mClock.now();
+ }
+ @Override
+ boolean shouldMediaProjectionPreventReusingConsent(
+ MediaProjectionManagerService.MediaProjection projection) {
+ return false;
+ }
+ };
+ final MediaProjectionManagerService service = new MediaProjectionManagerService(mContext,
+ mClockInjector);
+ MediaProjectionManagerService.MediaProjection projection = createProjectionPreconditions(
+ service);
+ mClock.fastForward(projection.mDefaultTimeoutMs + 10);
+
+ // Immediate timeout - so no longer valid.
+ assertThat(projection.isValid()).isFalse();
+ }
+
+ @Test
+ public void testIsValid_virtualDisplayAlreadyCreated() throws NameNotFoundException {
+ MediaProjectionManagerService service = new MediaProjectionManagerService(mContext,
+ mPreventReusedTokenDisabledInjector);
+ MediaProjectionManagerService.MediaProjection projection = startProjectionPreconditions(
+ service);
+ // Simulate MediaProjection#createVirtualDisplay being invoked previously.
+ projection.notifyVirtualDisplayCreated(10);
+
+ // Trying to re-use token on another MediaProjection#createVirtualDisplay - no longer valid.
+ assertThat(projection.isValid()).isFalse();
+ }
+
+ // TODO(269273190): Test flag using compat annotations instead.
+ @Test
+ public void testIsValid_invalid_preventionEnabled()
+ throws NameNotFoundException {
+ MediaProjectionManagerService service = new MediaProjectionManagerService(mContext,
+ mPreventReusedTokenEnabledInjector);
+ MediaProjectionManagerService.MediaProjection projection = startProjectionPreconditions(
+ service);
+ projection.start(mIMediaProjectionCallback);
+ projection.stop();
+ // Second start - so not valid.
+ projection.start(mIMediaProjectionCallback);
+
+ assertThrows(IllegalStateException.class, projection::isValid);
+ }
+
+ // TODO(269273190): Test flag using compat annotations instead.
+ @Test
+ public void testIsValid_invalid_preventionDisabled()
+ throws NameNotFoundException {
+ MediaProjectionManagerService service = new MediaProjectionManagerService(mContext,
+ mPreventReusedTokenDisabledInjector);
+ MediaProjectionManagerService.MediaProjection projection = startProjectionPreconditions(
+ service);
+ projection.start(mIMediaProjectionCallback);
+ projection.stop();
+
+ // Second start - so not valid.
+ projection.start(mIMediaProjectionCallback);
+
+ assertThat(projection.isValid()).isFalse();
+ }
+
+ @Test
+ public void testIsCurrentProjectionInternal_invalid() throws NameNotFoundException {
+ IBinder iBinder = mock(IBinder.class);
+ MediaProjectionManagerService.MediaProjection projection = startProjectionPreconditions();
+
+ // Create a projection, current is false.
+ assertThat(projection).isNotNull();
+ assertThat(mService.isCurrentProjection(iBinder)).isFalse();
+
+ // Start the projection, and test a random token.
+ projection.start(mIMediaProjectionCallback);
+ assertThat(mService.isCurrentProjection(iBinder)).isFalse();
+ }
+
+ @Test
+ public void testIsCurrentProjectionInternal_noProjection() {
+ IBinder iBinder = mock(IBinder.class);
+ assertThat(mService.isCurrentProjection(iBinder)).isFalse();
+ }
+
+ @Test
+ public void testIsCurrentProjectionInternal_currentProjection()
+ throws NameNotFoundException {
+ MediaProjectionManagerService.MediaProjection projection = startProjectionPreconditions();
+
+ // Create a projection, current is false.
+ assertThat(projection).isNotNull();
+ assertThat(mService.isCurrentProjection(projection.asBinder())).isFalse();
+
+ // Start the projection, is current is now true.
+ projection.start(mIMediaProjectionCallback);
+ assertThat(mService.isCurrentProjection(projection.asBinder())).isTrue();
+ }
+
+ // Set up preconditions for creating a projection.
+ private MediaProjectionManagerService.MediaProjection createProjectionPreconditions(
+ MediaProjectionManagerService service)
+ throws NameNotFoundException {
+ doReturn(mAppInfo).when(mPackageManager).getApplicationInfoAsUser(anyString(),
+ any(ApplicationInfoFlags.class), any(UserHandle.class));
+ return service.createProjectionInternal(UID, PACKAGE_NAME,
+ TYPE_MIRRORING, /* isPermanentGrant= */ true, UserHandle.CURRENT,
+ /* packageAttemptedReusingGrantedConsent= */ false);
+ }
+
+ // Set up preconditions for creating a projection.
+ private MediaProjectionManagerService.MediaProjection createProjectionPreconditions()
+ throws NameNotFoundException {
+ return createProjectionPreconditions(mService);
+ }
+
+ // Set up preconditions for starting a projection, with no foreground service requirements.
+ private MediaProjectionManagerService.MediaProjection startProjectionPreconditions(
+ MediaProjectionManagerService service)
+ throws NameNotFoundException {
+ mAppInfo.privateFlags |= PRIVATE_FLAG_PRIVILEGED;
+ return createProjectionPreconditions(service);
+ }
+
+ // Set up preconditions for starting a projection, specifying if it is possible to reuse the
+ // the current projection.
+ private MediaProjectionManagerService.MediaProjection startProjectionPreconditions(
+ boolean packageAttemptedReusingGrantedConsent)
+ throws NameNotFoundException {
+ mAppInfo.privateFlags |= PRIVATE_FLAG_PRIVILEGED;
+ doReturn(mAppInfo).when(mPackageManager).getApplicationInfoAsUser(anyString(),
+ any(ApplicationInfoFlags.class), any(UserHandle.class));
+ return mService.createProjectionInternal(UID, PACKAGE_NAME,
+ TYPE_MIRRORING, /* isPermanentGrant= */ true, UserHandle.CURRENT,
+ packageAttemptedReusingGrantedConsent);
+ }
+
+ // Set up preconditions for starting a projection, with no foreground service requirements.
+ private MediaProjectionManagerService.MediaProjection startProjectionPreconditions()
+ throws NameNotFoundException {
+ mAppInfo.privateFlags |= PRIVATE_FLAG_PRIVILEGED;
+ return createProjectionPreconditions(mService);
+ }
+
+ private static class FakeIMediaProjectionCallback extends IMediaProjectionCallback.Stub {
+ @Override
+ public void onStop() throws RemoteException {
+ }
+
+ @Override
+ public void onCapturedContentResize(int width, int height) throws RemoteException {
+ }
+
+ @Override
+ public void onCapturedContentVisibilityChanged(boolean isVisible) throws RemoteException {
+ }
+ }
+}
+ *
+ *
+ *