Merge "(1/N)[MediaProjection] Throw exception if token re-used" into udc-dev

This commit is contained in:
Naomi Musgrave
2023-04-04 16:08:19 +00:00
committed by Android (Google) Code Review
11 changed files with 887 additions and 110 deletions

View File

@@ -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.
*
* <p>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);
}

View File

@@ -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.
*
* <p>Preconditions:
* <ul>
* <li>{@link IMediaProjection#isValid} returned false, rather than throwing an exception</li>
* <li>Given projection instance is the current projection instance.</li>
* <ul>
*
* <p>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.
*
* <p>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);
}

View File

@@ -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:
* <ol>
* <li>If no {@link Callback} is registered.</li>
* <li>If {@link MediaProjectionManager#getMediaProjection}
* was invoked more than once to get this
* {@code MediaProjection} instance.
* <li>If this instance has already taken a recording through
* {@code #createVirtualDisplay}.
* </ol>
* However, if the target SDK is less than
* {@link android.os.Build.VERSION_CODES#UPSIDE_DOWN_CAKE U}, no
* exception is thrown.
* exception is thrown. In case 1, recording begins even without
* the callback. In case 2 & 3, recording doesn't begin
* until the user re-grants consent in the dialog.
* @throws SecurityException If attempting to create a new virtual display associated with this
* MediaProjection instance after it has been stopped by invoking
* {@link #stop()}.
@@ -216,8 +225,13 @@ public final class MediaProjection {
// 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).
// Do not introduce a separate aidl call here to prevent a race
// condition between setting up the VirtualDisplay and checking token validity.
virtualDisplayConfig.setWindowManagerMirroringEnabled(true);
// Do not declare a display id to mirror; default to the default display.
// DisplayManagerService will ask MediaProjectionManagerService to check if the app
// is re-using consent. Always return the projection instance to keep this call
// non-blocking; no content is sent to the app until the user re-grants consent.
final VirtualDisplay virtualDisplay = mDisplayManager.createVirtualDisplay(this,
virtualDisplayConfig.build(), callback, handler);
if (virtualDisplay == null) {
@@ -339,6 +353,7 @@ public final class MediaProjection {
private final class MediaProjectionCallback extends IMediaProjectionCallback.Stub {
@Override
public void onStop() {
Slog.v(TAG, "Dispatch stop to " + mCallbacks.size() + " callbacks.");
for (CallbackRecord cbr : mCallbacks.values()) {
cbr.onStop();
}

View File

@@ -231,6 +231,8 @@ public final class MediaProjectionManager {
if (projection == null) {
return null;
}
// Don't do anything here if app is re-using the token; we check how often
// IMediaProjection#start is invoked. Fail to the app when they start recording.
return new MediaProjection(mContext, IMediaProjection.Stub.asInterface(projection));
}

View File

@@ -62,12 +62,10 @@ public final class FakeIMediaProjection extends IMediaProjection.Stub {
@Override
public void registerCallback(IMediaProjectionCallback callback) throws RemoteException {
}
@Override
public void unregisterCallback(IMediaProjectionCallback callback) throws RemoteException {
}
@Override
@@ -79,4 +77,14 @@ public final class FakeIMediaProjection extends IMediaProjection.Stub {
public void setLaunchCookie(IBinder launchCookie) throws RemoteException {
mLaunchCookie = launchCookie;
}
@Override
public boolean isValid() throws RemoteException {
return true;
}
@Override
public void notifyVirtualDisplayCreated(int displayId) throws RemoteException {
}
}

View File

@@ -1418,19 +1418,32 @@ public final class DisplayManagerService extends SystemService {
flags |= VIRTUAL_DISPLAY_FLAG_DEVICE_DISPLAY_GROUP;
}
if (projection != null) {
final long firstToken = Binder.clearCallingIdentity();
try {
// Check if the host app is attempting to reuse the token or capture again on the same
// MediaProjection instance. Don't start recording if so; MediaProjectionManagerService
// decides how to respond based on the target SDK.
boolean waitForPermissionConsent = false;
final long firstToken = Binder.clearCallingIdentity();
try {
if (projection != null) {
if (!getProjectionService().isCurrentProjection(projection)) {
throw new SecurityException("Cannot create VirtualDisplay with "
+ "non-current MediaProjection");
}
if (!projection.isValid()) {
// Just log; MediaProjectionManagerService throws an exception.
Slog.w(TAG, "Reusing token: create virtual display for app reusing token");
// If the exception wasn't thrown, we continue and re-show the permission dialog
getProjectionService().requestConsentForInvalidProjection(projection);
// Declare that mirroring shouldn't begin until user reviews the permission
// dialog.
waitForPermissionConsent = true;
}
flags = projection.applyVirtualDisplayFlags(flags);
} catch (RemoteException e) {
throw new SecurityException("unable to validate media projection or flags");
} finally {
Binder.restoreCallingIdentity(firstToken);
}
} catch (RemoteException e) {
throw new SecurityException("Unable to validate media projection or flags", e);
} finally {
Binder.restoreCallingIdentity(firstToken);
}
if (callingUid != Process.SYSTEM_UID
@@ -1548,22 +1561,28 @@ public final class DisplayManagerService extends SystemService {
// Only attempt to set content recording session if there are details to set and a
// VirtualDisplay has been successfully constructed.
session.setVirtualDisplayId(displayId);
// Don't start mirroring until user re-grants consent.
session.setWaitingToRecord(waitForPermissionConsent);
// We set the content recording session here on the server side instead of using
// a second AIDL call in MediaProjection. By ensuring that a virtual display has
// been constructed before calling setContentRecordingSession, we avoid a race
// condition between the DisplayManagerService & WindowManagerService which could
// lead to the MediaProjection being pre-emptively torn down.
if (!mWindowManagerInternal.setContentRecordingSession(session)) {
// Unable to start mirroring, so tear down projection & release VirtualDisplay.
try {
getProjectionService().stopActiveProjection();
} catch (RemoteException e) {
Slog.e(TAG, "Unable to tell MediaProjectionManagerService to stop the "
+ "active projection", e);
try {
if (!getProjectionService().setContentRecordingSession(session, projection)) {
// Unable to start mirroring, so release VirtualDisplay. Projection service
// handles stopping the projection.
releaseVirtualDisplayInternal(callback.asBinder());
return Display.INVALID_DISPLAY;
} else if (projection != null) {
// Indicate that this projection has been used to record, and can't be used
// again.
projection.notifyVirtualDisplayCreated(displayId);
}
releaseVirtualDisplayInternal(callback.asBinder());
return Display.INVALID_DISPLAY;
} catch (RemoteException e) {
Slog.e(TAG, "Unable to tell MediaProjectionManagerService to set the "
+ "content recording session", e);
}
}

View File

@@ -19,6 +19,7 @@ package com.android.server.media.projection;
import static android.Manifest.permission.MANAGE_MEDIA_PROJECTION;
import static android.app.ActivityManagerInternal.MEDIA_PROJECTION_TOKEN_EVENT_CREATED;
import static android.app.ActivityManagerInternal.MEDIA_PROJECTION_TOKEN_EVENT_DESTROYED;
import static android.view.Display.INVALID_DISPLAY;
import android.Manifest;
import android.annotation.NonNull;
@@ -26,10 +27,14 @@ import android.annotation.Nullable;
import android.app.ActivityManagerInternal;
import android.app.AppOpsManager;
import android.app.IProcessObserver;
import android.app.compat.CompatChanges;
import android.compat.annotation.ChangeId;
import android.compat.annotation.EnabledSince;
import android.content.Context;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.ApplicationInfoFlags;
import android.content.pm.PackageManager.NameNotFoundException;
import android.content.pm.ServiceInfo;
import android.hardware.display.DisplayManager;
@@ -46,11 +51,13 @@ import android.os.Handler;
import android.os.IBinder;
import android.os.Looper;
import android.os.RemoteException;
import android.os.SystemClock;
import android.os.UserHandle;
import android.util.ArrayMap;
import android.util.Slog;
import android.view.ContentRecordingSession;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.util.ArrayUtils;
import com.android.internal.util.DumpUtils;
import com.android.server.LocalServices;
@@ -60,6 +67,7 @@ import com.android.server.wm.WindowManagerInternal;
import java.io.FileDescriptor;
import java.io.PrintWriter;
import java.time.Duration;
import java.util.Map;
/**
@@ -75,14 +83,29 @@ public final class MediaProjectionManagerService extends SystemService
private static final boolean REQUIRE_FG_SERVICE_FOR_PROJECTION = true;
private static final String TAG = "MediaProjectionManagerService";
/**
* Determines how to respond to an app re-using a consent token; either failing or allowing the
* user to re-grant consent.
*
* <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_PREVENTS_REUSING_CONSENT = 266201607L; // buganizer id
private final Object mLock = new Object(); // Protects the list of media projections
private final Map<IBinder, IBinder.DeathRecipient> mDeathEaters;
private final CallbackDelegate mCallbackDelegate;
private final Context mContext;
private final Injector mInjector;
private final Clock mClock;
private final AppOpsManager mAppOps;
private final ActivityManagerInternal mActivityManagerInternal;
private final PackageManager mPackageManager;
private final WindowManagerInternal mWmInternal;
private final MediaRouter mMediaRouter;
private final MediaRouterCallback mMediaRouterCallback;
@@ -92,18 +115,53 @@ public final class MediaProjectionManagerService extends SystemService
private MediaProjection mProjectionGrant;
public MediaProjectionManagerService(Context context) {
this(context, new Injector());
}
@VisibleForTesting MediaProjectionManagerService(Context context, Injector injector) {
super(context);
mContext = context;
mInjector = injector;
mClock = injector.createClock();
mDeathEaters = new ArrayMap<IBinder, IBinder.DeathRecipient>();
mCallbackDelegate = new CallbackDelegate();
mAppOps = (AppOpsManager) mContext.getSystemService(Context.APP_OPS_SERVICE);
mActivityManagerInternal = LocalServices.getService(ActivityManagerInternal.class);
mPackageManager = mContext.getPackageManager();
mWmInternal = LocalServices.getService(WindowManagerInternal.class);
mMediaRouter = (MediaRouter) mContext.getSystemService(Context.MEDIA_ROUTER_SERVICE);
mMediaRouterCallback = new MediaRouterCallback();
Watchdog.getInstance().addMonitor(this);
}
/** Functional interface for providing time. */
@VisibleForTesting
interface Clock {
/**
* Returns current time in milliseconds since boot, not counting time spent in deep sleep.
*/
long uptimeMillis();
}
@VisibleForTesting
static class Injector {
/**
* Returns whether we should prevent the calling app from re-using the user's consent, or
* allow the user to re-grant access to the same consent token.
*/
boolean shouldMediaProjectionPreventReusingConsent(MediaProjection projection) {
// TODO(b/269273190): query feature flag directly instead of injecting.
return CompatChanges.isChangeEnabled(MEDIA_PROJECTION_PREVENTS_REUSING_CONSENT,
projection.packageName, UserHandle.getUserHandleForUid(projection.uid));
}
Clock createClock() {
return SystemClock::uptimeMillis;
}
}
@Override
public void onStart() {
publishBinderService(Context.MEDIA_PROJECTION_SERVICE, new BinderService(),
@@ -232,7 +290,31 @@ public final class MediaProjectionManagerService extends SystemService
mCallbackDelegate.dispatchStop(projection);
}
private boolean isCurrentProjection(IBinder token) {
/**
* Returns {@code true} when updating the current mirroring session on WM succeeded, and
* {@code false} otherwise.
*/
@VisibleForTesting
boolean setContentRecordingSession(@Nullable ContentRecordingSession incomingSession) {
synchronized (mLock) {
if (!mWmInternal.setContentRecordingSession(
incomingSession)) {
// Unable to start mirroring, so tear down this projection.
if (mProjectionGrant != null) {
mProjectionGrant.stop();
}
return false;
}
return true;
}
}
/**
* Returns {@code true} when the given token matches the token of the current projection
* instance. Returns {@code false} otherwise.
*/
@VisibleForTesting
boolean isCurrentProjection(IBinder token) {
synchronized (mLock) {
if (mProjectionToken != null) {
return mProjectionToken.equals(token);
@@ -241,7 +323,52 @@ public final class MediaProjectionManagerService extends SystemService
}
}
private MediaProjectionInfo getActiveProjectionInfo() {
/**
* Reshows the permisison dialog for the user to review consent they've already granted in
* the given projection instance.
*
* <p>Preconditions:
* <ul>
* <li>{@link IMediaProjection#isValid} returned false, rather than throwing an exception</li>
* <li>Given projection instance is the current projection instance.</li>
* <ul>
*
* <p>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()) {

View File

@@ -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());

View File

@@ -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",

View File

@@ -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);

View File

@@ -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 {
}
}
}