Merge "(1/N)[MediaProjection] Throw exception if token re-used" into udc-dev
This commit is contained in:
committed by
Android (Google) Code Review
commit
04388ab4e5
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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()) {
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 {
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user