Merge "(2/N)[MediaProjection] Show dialog when token is reused" into udc-dev

This commit is contained in:
Naomi Musgrave
2023-04-22 13:31:56 +00:00
committed by Android (Google) Code Review
6 changed files with 616 additions and 58 deletions

View File

@@ -1495,6 +1495,12 @@
"group": "WM_DEBUG_CONFIGURATION",
"at": "com\/android\/server\/wm\/ActivityRecord.java"
},
"-741766551": {
"message": "Content Recording: Ignoring session on invalid virtual display",
"level": "VERBOSE",
"group": "WM_DEBUG_CONTENT_RECORDING",
"at": "com\/android\/server\/wm\/ContentRecordingController.java"
},
"-732715767": {
"message": "Unable to retrieve window container to start recording for display %d",
"level": "VERBOSE",

View File

@@ -20,11 +20,23 @@ import android.media.projection.IMediaProjection;
import android.media.projection.IMediaProjectionCallback;
import android.media.projection.IMediaProjectionWatcherCallback;
import android.media.projection.MediaProjectionInfo;
import android.media.projection.ReviewGrantedConsentResult;
import android.os.IBinder;
import android.view.ContentRecordingSession;
/** {@hide} */
interface IMediaProjectionManager {
/**
* Intent extra indicating if user must review access to the consent token already granted.
*/
const String EXTRA_USER_REVIEW_GRANTED_CONSENT = "extra_media_projection_user_consent_required";
/**
* Intent extra indicating the package attempting to re-use granted consent.
*/
const String EXTRA_PACKAGE_REUSING_GRANTED_CONSENT =
"extra_media_projection_package_reusing_consent";
@UnsupportedAppUsage
boolean hasProjectionPermission(int uid, String packageName);
@@ -36,6 +48,21 @@ interface IMediaProjectionManager {
IMediaProjection createProjection(int uid, String packageName, int type,
boolean permanentGrant);
/**
* Returns the current {@link IMediaProjection} instance associated with the given
* package, or {@code null} if it is not possible to re-use the current projection.
*
* <p>Should only be invoked when the user has reviewed consent for a re-used projection token.
* Requires that there is a prior session waiting for the user to review consent, and the given
* package details match those on the current projection.
*
* @see {@link #isCurrentProjection}
*/
@EnforcePermission("android.Manifest.permission.MANAGE_MEDIA_PROJECTION")
@JavaPassthrough(annotation = "@android.annotation.RequiresPermission(android.Manifest"
+ ".permission.MANAGE_MEDIA_PROJECTION)")
IMediaProjection getProjection(int uid, String packageName);
/**
* Returns {@code true} if the given {@link IMediaProjection} corresponds to the current
* projection, or {@code false} otherwise.
@@ -58,7 +85,7 @@ interface IMediaProjectionManager {
*/
@JavaPassthrough(annotation = "@android.annotation.RequiresPermission(android.Manifest"
+ ".permission.MANAGE_MEDIA_PROJECTION)")
void requestConsentForInvalidProjection(IMediaProjection projection);
void requestConsentForInvalidProjection(in IMediaProjection projection);
@JavaPassthrough(annotation = "@android.annotation.RequiresPermission(android.Manifest"
+ ".permission.MANAGE_MEDIA_PROJECTION)")
@@ -94,9 +121,32 @@ interface IMediaProjectionManager {
*
* @param incomingSession the nullable incoming content recording session
* @param projection the non-null projection the session describes
* @throws SecurityException If the provided projection is not current.
*/
@JavaPassthrough(annotation = "@android.annotation.RequiresPermission(android.Manifest"
+ ".permission.MANAGE_MEDIA_PROJECTION)")
boolean setContentRecordingSession(in ContentRecordingSession incomingSession,
in IMediaProjection projection);
/**
* Sets the result of the user reviewing the recording permission, when the host app is re-using
* the consent token.
*
* <p>Ignores the provided result if the given projection is not the current projection.
*
* <p>Based on the given result:
* <ul>
* <li>If UNKNOWN or RECORD_CANCEL, then tear down the recording.</li>
* <li>If RECORD_CONTENT_DISPLAY, then record the default display.</li>
* <li>If RECORD_CONTENT_TASK, record the task indicated by
* {@link IMediaProjection#getLaunchCookie}.</li>
* </ul>
* @param projection The projection associated with the consent result. Must be the current
* projection instance, unless the given result is RECORD_CANCEL.
*/
@EnforcePermission("android.Manifest.permission.MANAGE_MEDIA_PROJECTION")
@JavaPassthrough(annotation = "@android.annotation.RequiresPermission(android.Manifest"
+ ".permission.MANAGE_MEDIA_PROJECTION)")
void setUserReviewGrantedConsentResult(ReviewGrantedConsentResult consentResult,
in @nullable IMediaProjection projection);
}

View File

@@ -0,0 +1,31 @@
/*
* Copyright (C) 2023 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package android.media.projection;
/**
* Indicates result of user interacting with consent dialog, when their review is required due to
* app re-using the token.
* @hide
*/
@Backing(type="int")
enum ReviewGrantedConsentResult {
UNKNOWN = -1,
RECORD_CANCEL = 0,
RECORD_CONTENT_DISPLAY = 1,
RECORD_CONTENT_TASK = 2,
}

View File

@@ -19,9 +19,19 @@ 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.content.Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS;
import static android.content.Intent.FLAG_ACTIVITY_NEW_TASK;
import static android.media.projection.IMediaProjectionManager.EXTRA_PACKAGE_REUSING_GRANTED_CONSENT;
import static android.media.projection.IMediaProjectionManager.EXTRA_USER_REVIEW_GRANTED_CONSENT;
import static android.media.projection.ReviewGrantedConsentResult.RECORD_CANCEL;
import static android.media.projection.ReviewGrantedConsentResult.RECORD_CONTENT_DISPLAY;
import static android.media.projection.ReviewGrantedConsentResult.RECORD_CONTENT_TASK;
import static android.media.projection.ReviewGrantedConsentResult.UNKNOWN;
import static android.view.Display.DEFAULT_DISPLAY;
import static android.view.Display.INVALID_DISPLAY;
import android.Manifest;
import android.annotation.EnforcePermission;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.app.ActivityManagerInternal;
@@ -30,7 +40,9 @@ import android.app.IProcessObserver;
import android.app.compat.CompatChanges;
import android.compat.annotation.ChangeId;
import android.compat.annotation.EnabledSince;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
@@ -45,11 +57,13 @@ import android.media.projection.IMediaProjectionManager;
import android.media.projection.IMediaProjectionWatcherCallback;
import android.media.projection.MediaProjectionInfo;
import android.media.projection.MediaProjectionManager;
import android.media.projection.ReviewGrantedConsentResult;
import android.os.Binder;
import android.os.Build;
import android.os.Handler;
import android.os.IBinder;
import android.os.Looper;
import android.os.PermissionEnforcer;
import android.os.RemoteException;
import android.os.SystemClock;
import android.os.UserHandle;
@@ -57,6 +71,7 @@ import android.util.ArrayMap;
import android.util.Slog;
import android.view.ContentRecordingSession;
import com.android.internal.R;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.util.ArrayUtils;
import com.android.internal.util.DumpUtils;
@@ -69,6 +84,7 @@ import java.io.FileDescriptor;
import java.io.PrintWriter;
import java.time.Duration;
import java.util.Map;
import java.util.Objects;
/**
* Manages MediaProjection sessions.
@@ -161,10 +177,9 @@ public final class MediaProjectionManagerService extends SystemService
}
}
@Override
public void onStart() {
publishBinderService(Context.MEDIA_PROJECTION_SERVICE, new BinderService(),
publishBinderService(Context.MEDIA_PROJECTION_SERVICE, new BinderService(mContext),
false /*allowIsolated*/);
mMediaRouter.addCallback(MediaRouter.ROUTE_TYPE_REMOTE_DISPLAY, mMediaRouterCallback,
MediaRouter.CALLBACK_FLAG_PASSIVE_DISCOVERY);
@@ -305,6 +320,10 @@ public final class MediaProjectionManagerService extends SystemService
}
return false;
}
if (mProjectionGrant != null) {
// Cache the session details.
mProjectionGrant.mSession = incomingSession;
}
return true;
}
}
@@ -323,9 +342,8 @@ public final class MediaProjectionManagerService extends SystemService
}
}
/**
* Reshows the permisison dialog for the user to review consent they've already granted in
* Re-shows the permission dialog for the user to review consent they've already granted in
* the given projection instance.
*
* <p>Preconditions:
@@ -337,18 +355,111 @@ public final class MediaProjectionManagerService extends SystemService
* <p>Returns immediately but waits to start recording until user has reviewed their consent.
*/
@VisibleForTesting
void requestConsentForInvalidProjection(IMediaProjection projection) {
void requestConsentForInvalidProjection() {
synchronized (mLock) {
Slog.v(TAG, "Reusing token: Reshow dialog for due to invalid projection.");
// TODO(b/274790702): Trigger the permission dialog again in SysUI.
// Trigger the permission dialog again in SysUI
// Do not handle the result; SysUI will update us when the user has consented.
mContext.startActivityAsUser(buildReviewGrantedConsentIntent(),
UserHandle.getUserHandleForUid(mProjectionGrant.uid));
}
}
/**
* Returns an intent to re-show the consent dialog in SysUI. Should only be used for the
* scenario where the host app has re-used the consent token.
*
* <p>Consent dialog result handled in
* {@link BinderService#setUserReviewGrantedConsentResult(int)}.
*/
private Intent buildReviewGrantedConsentIntent() {
final String permissionDialogString = mContext.getResources().getString(
R.string.config_mediaProjectionPermissionDialogComponent);
final ComponentName mediaProjectionPermissionDialogComponent =
ComponentName.unflattenFromString(permissionDialogString);
// We can use mProjectionGrant since we already checked that it matches the given token.
return new Intent().setComponent(mediaProjectionPermissionDialogComponent)
.putExtra(EXTRA_USER_REVIEW_GRANTED_CONSENT, true)
.putExtra(EXTRA_PACKAGE_REUSING_GRANTED_CONSENT, mProjectionGrant.packageName)
.setFlags(FLAG_ACTIVITY_NEW_TASK | FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS);
}
/**
* Handles result of dialog shown from {@link BinderService#buildReviewGrantedConsentIntent()}.
*
* <p>Tears down session if user did not consent, or starts mirroring if user did consent.
*/
@VisibleForTesting
void setUserReviewGrantedConsentResult(@ReviewGrantedConsentResult int consentResult,
@Nullable IMediaProjection projection) {
synchronized (mLock) {
final boolean consentGranted =
consentResult == RECORD_CONTENT_DISPLAY || consentResult == RECORD_CONTENT_TASK;
if (consentGranted && projection == null || !isCurrentProjection(
projection.asBinder())) {
Slog.v(TAG, "Reusing token: Ignore consent result of " + consentResult + " for a "
+ "token that isn't current");
return;
}
if (mProjectionGrant == null) {
Slog.w(TAG, "Reusing token: Can't review consent with no ongoing projection.");
return;
}
if (mProjectionGrant.mSession == null
|| !mProjectionGrant.mSession.isWaitingToRecord()) {
Slog.w(TAG, "Reusing token: Ignore consent result " + consentResult
+ " if not waiting for the result.");
return;
}
Slog.v(TAG, "Reusing token: Handling user consent result " + consentResult);
switch (consentResult) {
case UNKNOWN:
case RECORD_CANCEL:
// Pass in null to stop mirroring.
setReviewedConsentSessionLocked(/* session= */ null);
// The grant may now be null if setting the session failed.
if (mProjectionGrant != null) {
// Always stop the projection.
mProjectionGrant.stop();
}
break;
case RECORD_CONTENT_DISPLAY:
// TODO(270118861) The app may have specified a particular id in the virtual
// display config. However - below will always return INVALID since it checks
// that window manager mirroring is not enabled (it is always enabled for MP).
setReviewedConsentSessionLocked(ContentRecordingSession.createDisplaySession(
DEFAULT_DISPLAY));
break;
case RECORD_CONTENT_TASK:
setReviewedConsentSessionLocked(ContentRecordingSession.createTaskSession(
mProjectionGrant.getLaunchCookie()));
break;
}
}
}
/**
* Updates the session after the user has reviewed consent. There must be a current session.
*
* @param session The new session details, or {@code null} to stop recording.
*/
private void setReviewedConsentSessionLocked(@Nullable ContentRecordingSession session) {
if (session != null) {
session.setWaitingToRecord(false);
session.setVirtualDisplayId(mProjectionGrant.mVirtualDisplayId);
}
Slog.v(TAG, "Reusing token: Processed consent so set the session " + session);
if (!setContentRecordingSession(session)) {
Slog.e(TAG, "Reusing token: Failed to set session for reused consent, so stop");
// Do not need to invoke stop; updating the session does it for us.
}
}
// 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) {
boolean isPermanentGrant, UserHandle callingUser) {
MediaProjection projection;
ApplicationInfo ai;
try {
@@ -371,6 +482,34 @@ public final class MediaProjectionManagerService extends SystemService
return projection;
}
// TODO(b/261563516): Remove internal method and test aidl directly, here and elsewhere.
@VisibleForTesting
MediaProjection getProjectionInternal(int uid, String packageName) {
final long callingToken = Binder.clearCallingIdentity();
try {
// Supposedly the package has re-used the user's consent; confirm the provided details
// against the current projection token before re-using the current projection.
if (mProjectionGrant == null || mProjectionGrant.mSession == null
|| !mProjectionGrant.mSession.isWaitingToRecord()) {
Slog.e(TAG, "Reusing token: Not possible to reuse the current projection "
+ "instance");
return null;
}
// The package matches, go ahead and re-use the token for this request.
if (mProjectionGrant.uid == uid
&& Objects.equals(mProjectionGrant.packageName, packageName)) {
Slog.v(TAG, "Reusing token: getProjection can reuse the current projection");
return mProjectionGrant;
} else {
Slog.e(TAG, "Reusing token: Not possible to reuse the current projection "
+ "instance due to package details mismatching");
return null;
}
} finally {
Binder.restoreCallingIdentity(callingToken);
}
}
@VisibleForTesting
MediaProjectionInfo getActiveProjectionInfo() {
synchronized (mLock) {
@@ -395,6 +534,10 @@ public final class MediaProjectionManagerService extends SystemService
private final class BinderService extends IMediaProjectionManager.Stub {
BinderService(Context context) {
super(PermissionEnforcer.fromContext(context));
}
@Override // Binder call
public boolean hasProjectionPermission(int uid, String packageName) {
final long token = Binder.clearCallingIdentity();
@@ -424,7 +567,25 @@ public final class MediaProjectionManagerService extends SystemService
}
final UserHandle callingUser = Binder.getCallingUserHandle();
return createProjectionInternal(uid, packageName, type, isPermanentGrant,
callingUser, false);
callingUser);
}
@Override // Binder call
@EnforcePermission(MANAGE_MEDIA_PROJECTION)
public IMediaProjection getProjection(int uid, String packageName) {
getProjection_enforcePermission();
if (packageName == null || packageName.isEmpty()) {
throw new IllegalArgumentException("package name must not be empty");
}
MediaProjection projection;
final long callingToken = Binder.clearCallingIdentity();
try {
projection = getProjectionInternal(uid, packageName);
} finally {
Binder.restoreCallingIdentity(callingToken);
}
return projection;
}
@Override // Binder call
@@ -562,7 +723,7 @@ public final class MediaProjectionManagerService extends SystemService
}
@Override
public void requestConsentForInvalidProjection(IMediaProjection projection) {
public void requestConsentForInvalidProjection(@NonNull 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"
@@ -577,7 +738,22 @@ public final class MediaProjectionManagerService extends SystemService
// Remove calling app identity before performing any privileged operations.
final long token = Binder.clearCallingIdentity();
try {
MediaProjectionManagerService.this.requestConsentForInvalidProjection(projection);
MediaProjectionManagerService.this.requestConsentForInvalidProjection();
} finally {
Binder.restoreCallingIdentity(token);
}
}
@Override // Binder call
@EnforcePermission(MANAGE_MEDIA_PROJECTION)
public void setUserReviewGrantedConsentResult(@ReviewGrantedConsentResult int consentResult,
@Nullable IMediaProjection projection) {
setUserReviewGrantedConsentResult_enforcePermission();
// Remove calling app identity before performing any privileged operations.
final long token = Binder.clearCallingIdentity();
try {
MediaProjectionManagerService.this.setUserReviewGrantedConsentResult(consentResult,
projection);
} finally {
Binder.restoreCallingIdentity(token);
}
@@ -594,7 +770,6 @@ public final class MediaProjectionManagerService extends SystemService
}
}
private boolean checkPermission(String packageName, String permission) {
return mContext.getPackageManager().checkPermission(permission, packageName)
== PackageManager.PERMISSION_GRANTED;
@@ -630,6 +805,8 @@ public final class MediaProjectionManagerService extends SystemService
// Set if MediaProjection#createVirtualDisplay has been invoked previously (it
// should only be called once).
private int mVirtualDisplayId = INVALID_DISPLAY;
// The associated session details already sent to WindowManager.
private ContentRecordingSession mSession;
MediaProjection(int type, int uid, String packageName, int targetSdkVersion,
boolean isPrivileged) {
@@ -883,6 +1060,18 @@ public final class MediaProjectionManagerService extends SystemService
}
synchronized (mLock) {
mVirtualDisplayId = displayId;
// If prior session was does not have a valid display id, then update the display
// so recording can start.
if (mSession != null && mSession.getVirtualDisplayId() == INVALID_DISPLAY) {
Slog.v(TAG, "Virtual display now created, so update session with the virtual "
+ "display id");
mSession.setVirtualDisplayId(mVirtualDisplayId);
if (!setContentRecordingSession(mSession)) {
Slog.e(TAG, "Failed to set session for virtual display id");
// Do not need to invoke stop; updating the session does it for us.
}
}
}
}

View File

@@ -80,7 +80,7 @@ final class ContentRecordingController {
}
// Invalid scenario: ignore identical incoming session.
if (ContentRecordingSession.isProjectionOnSameDisplay(mSession, incomingSession)) {
// TODO(242833866) if incoming session is no longer waiting to record, allow
// TODO(242833866): if incoming session is no longer waiting to record, allow
// the update through.
ProtoLog.v(WM_DEBUG_CONTENT_RECORDING,
@@ -99,7 +99,7 @@ final class ContentRecordingController {
incomingDisplayContent = wmService.mRoot.getDisplayContentOrCreate(
incomingSession.getVirtualDisplayId());
incomingDisplayContent.setContentRecordingSession(incomingSession);
// TODO(b/270118861) When user grants consent to re-use, explicitly ask ContentRecorder
// TODO(b/270118861): When user grants consent to re-use, explicitly ask ContentRecorder
// to update, since no config/display change arrives. Mark recording as black.
}
// Takeover and stopping scenario: stop recording on the pre-existing session.

View File

@@ -19,15 +19,25 @@ 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.media.projection.ReviewGrantedConsentResult.RECORD_CANCEL;
import static android.media.projection.ReviewGrantedConsentResult.RECORD_CONTENT_DISPLAY;
import static android.media.projection.ReviewGrantedConsentResult.RECORD_CONTENT_TASK;
import static android.media.projection.ReviewGrantedConsentResult.UNKNOWN;
import static android.view.Display.DEFAULT_DISPLAY;
import static android.view.Display.INVALID_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.ArgumentMatchers.eq;
import static org.mockito.Mockito.atLeastOnce;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.testng.Assert.assertThrows;
import android.app.ActivityManagerInternal;
@@ -37,7 +47,9 @@ 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.IMediaProjection;
import android.media.projection.IMediaProjectionCallback;
import android.media.projection.ReviewGrantedConsentResult;
import android.os.IBinder;
import android.os.RemoteException;
import android.os.UserHandle;
@@ -56,6 +68,8 @@ import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
@@ -108,6 +122,8 @@ public class MediaProjectionManagerServiceTest {
private WindowManagerInternal mWindowManagerInternal;
@Mock
private PackageManager mPackageManager;
@Captor
private ArgumentCaptor<ContentRecordingSession> mSessionCaptor;
@Before
public void setup() throws Exception {
@@ -154,12 +170,15 @@ public class MediaProjectionManagerServiceTest {
@Test
public void testCreateProjection() throws NameNotFoundException {
MediaProjectionManagerService.MediaProjection projection =
startProjectionPreconditions(/* packageAttemptedReusingGrantedConsent= */ false);
// Create a first projection.
MediaProjectionManagerService.MediaProjection projection = startProjectionPreconditions();
projection.start(mIMediaProjectionCallback);
// We are allowed to create a new projection.
MediaProjectionManagerService.MediaProjection secondProjection =
startProjectionPreconditions(/* packageAttemptedReusingGrantedConsent= */ false);
startProjectionPreconditions();
// This is a new projection.
assertThat(secondProjection).isNotNull();
assertThat(secondProjection).isNotEqualTo(projection);
}
@@ -167,44 +186,58 @@ public class MediaProjectionManagerServiceTest {
@Test
public void testCreateProjection_attemptReuse_noPriorProjectionGrant()
throws NameNotFoundException {
MediaProjectionManagerService.MediaProjection projection =
startProjectionPreconditions(/* packageAttemptedReusingGrantedConsent= */ false);
// Create a first projection.
MediaProjectionManagerService.MediaProjection projection = startProjectionPreconditions();
projection.start(mIMediaProjectionCallback);
MediaProjectionManagerService.MediaProjection secondProjection =
startProjectionPreconditions(/* packageAttemptedReusingGrantedConsent= */ true);
assertThat(secondProjection).isNotNull();
assertThat(secondProjection).isNotEqualTo(projection);
// We are not allowed to retrieve the prior projection, since we are not waiting for the
// user's consent.
assertThat(startReusedProjectionPreconditions()).isNull();
}
@Test
public void testCreateProjection_attemptReuse_priorProjectionGrant_notWaiting()
throws NameNotFoundException {
MediaProjectionManagerService.MediaProjection projection =
startProjectionPreconditions(/* packageAttemptedReusingGrantedConsent= */ false);
// Create a first projection.
MediaProjectionManagerService.MediaProjection projection = startProjectionPreconditions();
projection.start(mIMediaProjectionCallback);
// Mark this projection as not waiting.
// Mark this projection as not waiting for the user to review consent.
doReturn(true).when(mWindowManagerInternal).setContentRecordingSession(
any(ContentRecordingSession.class));
mService.setContentRecordingSession(DISPLAY_SESSION);
// We are allowed to create another projection.
// We are not allowed to retrieve the prior projection, since we are not waiting for the
// user's consent.
assertThat(startReusedProjectionPreconditions()).isNull();
}
@Test
public void testCreateProjection_attemptReuse_priorProjectionGrant_waiting()
throws NameNotFoundException {
// Create a first projection.
MediaProjectionManagerService.MediaProjection projection = startProjectionPreconditions();
projection.start(mIMediaProjectionCallback);
// Mark this projection as waiting for the user to review consent.
doReturn(true).when(mWindowManagerInternal).setContentRecordingSession(
any(ContentRecordingSession.class));
mService.setContentRecordingSession(mWaitingDisplaySession);
// We are allowed to create another projection, reusing a prior grant if necessary.
MediaProjectionManagerService.MediaProjection secondProjection =
startProjectionPreconditions(/* packageAttemptedReusingGrantedConsent= */ true);
startReusedProjectionPreconditions();
// This is a new projection, since we are waiting for the user's consent; simply provide
// the projection grant from before.
assertThat(secondProjection).isNotNull();
// But this is a new projection.
assertThat(secondProjection).isNotEqualTo(projection);
assertThat(secondProjection).isEqualTo(projection);
}
@Test
public void testCreateProjection_attemptReuse_priorProjectionGrant_waiting_differentPackage()
throws NameNotFoundException {
MediaProjectionManagerService.MediaProjection projection =
startProjectionPreconditions(/* packageAttemptedReusingGrantedConsent= */ false);
MediaProjectionManagerService.MediaProjection projection = startProjectionPreconditions();
projection.start(mIMediaProjectionCallback);
// Mark this projection as not waiting.
@@ -213,8 +246,7 @@ public class MediaProjectionManagerServiceTest {
// 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);
TYPE_MIRRORING, /* isPermanentGrant= */ true, UserHandle.CURRENT);
assertThat(secondProjection).isNotNull();
@@ -366,6 +398,267 @@ public class MediaProjectionManagerServiceTest {
assertThat(mService.isCurrentProjection(projection.asBinder())).isTrue();
}
@Test
public void testSetUserReviewGrantedConsentResult_noCurrentProjection() {
// Gracefully handle invocation without a current projection.
mService.setUserReviewGrantedConsentResult(RECORD_CONTENT_DISPLAY,
mock(IMediaProjection.class));
assertThat(mService.getActiveProjectionInfo()).isNull();
verify(mWindowManagerInternal, never()).setContentRecordingSession(any(
ContentRecordingSession.class));
}
@Test
public void testSetUserReviewGrantedConsentResult_projectionNotCurrent() throws Exception {
MediaProjectionManagerService.MediaProjection projection = startProjectionPreconditions();
projection.start(mIMediaProjectionCallback);
assertThat(mService.isCurrentProjection(projection)).isTrue();
doReturn(true).when(mWindowManagerInternal).setContentRecordingSession(
any(ContentRecordingSession.class));
// Some other token.
final IMediaProjection otherProjection = mock(IMediaProjection.class);
doReturn(mock(IBinder.class)).when(otherProjection).asBinder();
// Waiting for user to review consent.
mService.setContentRecordingSession(mWaitingDisplaySession);
mService.setUserReviewGrantedConsentResult(RECORD_CONTENT_DISPLAY, otherProjection);
// Display result is ignored; only the first session is set.
verify(mWindowManagerInternal, times(1)).setContentRecordingSession(
eq(mWaitingDisplaySession));
}
@Test
public void testSetUserReviewGrantedConsentResult_projectionNull() throws Exception {
MediaProjectionManagerService.MediaProjection projection = startProjectionPreconditions();
projection.start(mIMediaProjectionCallback);
assertThat(mService.isCurrentProjection(projection)).isTrue();
doReturn(true).when(mWindowManagerInternal).setContentRecordingSession(
any(ContentRecordingSession.class));
// Some other token.
final IMediaProjection otherProjection = null;
// Waiting for user to review consent.
mService.setContentRecordingSession(mWaitingDisplaySession);
mService.setUserReviewGrantedConsentResult(RECORD_CONTENT_DISPLAY, otherProjection);
// Display result is ignored; only the first session is set.
verify(mWindowManagerInternal, times(1)).setContentRecordingSession(
eq(mWaitingDisplaySession));
}
@Test
public void testSetUserReviewGrantedConsentResult_noVirtualDisplay() throws Exception {
MediaProjectionManagerService.MediaProjection projection = startProjectionPreconditions();
projection.start(mIMediaProjectionCallback);
// Do not indicate that the virtual display was created.
ContentRecordingSession session = mWaitingDisplaySession;
session.setVirtualDisplayId(INVALID_DISPLAY);
doReturn(true).when(mWindowManagerInternal).setContentRecordingSession(
any(ContentRecordingSession.class));
// Waiting for user to review consent.
assertThat(mService.isCurrentProjection(projection)).isTrue();
mService.setContentRecordingSession(session);
mService.setUserReviewGrantedConsentResult(RECORD_CONTENT_DISPLAY, projection);
// A session is sent, indicating consent is granted to record but the virtual display isn't
// ready yet.
verify(mWindowManagerInternal, times(2)).setContentRecordingSession(
mSessionCaptor.capture());
// Examine latest value.
final ContentRecordingSession capturedSession = mSessionCaptor.getValue();
assertThat(capturedSession.isWaitingToRecord()).isFalse();
assertThat(capturedSession.getVirtualDisplayId()).isEqualTo(INVALID_DISPLAY);
}
@Test
public void testSetUserReviewGrantedConsentResult_thenVirtualDisplayCreated() throws Exception {
MediaProjectionManagerService.MediaProjection projection = startProjectionPreconditions();
projection.start(mIMediaProjectionCallback);
assertThat(mService.isCurrentProjection(projection)).isTrue();
doReturn(true).when(mWindowManagerInternal).setContentRecordingSession(
any(ContentRecordingSession.class));
// Waiting for user to review consent.
mService.setContentRecordingSession(mWaitingDisplaySession);
mService.setUserReviewGrantedConsentResult(RECORD_CONTENT_DISPLAY, projection);
// Virtual Display is finally created.
projection.notifyVirtualDisplayCreated(10);
verifySetSessionWithContent(ContentRecordingSession.RECORD_CONTENT_DISPLAY);
}
@Test
public void testSetUserReviewGrantedConsentResult_unknown_updatedSession() throws Exception {
testSetUserReviewGrantedConsentResult_userCancelsSession(
/* isSetSessionSuccessful= */ true, UNKNOWN);
}
@Test
public void testSetUserReviewGrantedConsentResult_unknown_failedToUpdateSession()
throws Exception {
testSetUserReviewGrantedConsentResult_userCancelsSession(
/* isSetSessionSuccessful= */ false, UNKNOWN);
}
@Test
public void testSetUserReviewGrantedConsentResult_cancel_updatedSession() throws Exception {
testSetUserReviewGrantedConsentResult_userCancelsSession(
/* isSetSessionSuccessful= */ true, RECORD_CANCEL);
}
@Test
public void testSetUserReviewGrantedConsentResult_cancel_failedToUpdateSession()
throws Exception {
testSetUserReviewGrantedConsentResult_userCancelsSession(
/* isSetSessionSuccessful= */ false, RECORD_CANCEL);
}
/**
* Executes and validates scenario where the consent result indicates the projection ends.
*/
private void testSetUserReviewGrantedConsentResult_userCancelsSession(
boolean isSetSessionSuccessful, @ReviewGrantedConsentResult int consentResult)
throws Exception {
MediaProjectionManagerService.MediaProjection projection = startProjectionPreconditions();
projection.start(mIMediaProjectionCallback);
projection.notifyVirtualDisplayCreated(10);
// Waiting for user to review consent.
assertThat(mService.isCurrentProjection(projection)).isTrue();
doReturn(true).when(mWindowManagerInternal).setContentRecordingSession(
any(ContentRecordingSession.class));
mService.setContentRecordingSession(mWaitingDisplaySession);
doReturn(isSetSessionSuccessful).when(mWindowManagerInternal).setContentRecordingSession(
any(ContentRecordingSession.class));
mService.setUserReviewGrantedConsentResult(consentResult, projection);
verify(mWindowManagerInternal, atLeastOnce()).setContentRecordingSession(
mSessionCaptor.capture());
// Null value to stop session.
assertThat(mSessionCaptor.getValue()).isNull();
assertThat(mService.isCurrentProjection(projection.asBinder())).isFalse();
}
@Test
public void testSetUserReviewGrantedConsentResult_displayMirroring_startedSession()
throws NameNotFoundException {
testSetUserReviewGrantedConsentResult_startedSession(RECORD_CONTENT_DISPLAY,
ContentRecordingSession.RECORD_CONTENT_DISPLAY);
}
@Test
public void testSetUserReviewGrantedConsentResult_displayMirroring_failedToStartSession()
throws NameNotFoundException {
testSetUserReviewGrantedConsentResult_failedToStartSession(RECORD_CONTENT_DISPLAY,
ContentRecordingSession.RECORD_CONTENT_DISPLAY);
}
@Test
public void testSetUserReviewGrantedConsentResult_taskMirroring_startedSession()
throws NameNotFoundException {
testSetUserReviewGrantedConsentResult_startedSession(RECORD_CONTENT_TASK,
ContentRecordingSession.RECORD_CONTENT_TASK);
}
@Test
public void testSetUserReviewGrantedConsentResult_taskMirroring_failedToStartSession()
throws NameNotFoundException {
testSetUserReviewGrantedConsentResult_failedToStartSession(RECORD_CONTENT_TASK,
ContentRecordingSession.RECORD_CONTENT_TASK);
}
/**
* Executes and validates scenario where the consent result indicates the projection continues,
* and successfully started projection.
*/
private void testSetUserReviewGrantedConsentResult_startedSession(
@ReviewGrantedConsentResult int consentResult,
@ContentRecordingSession.RecordContent int recordedContent)
throws NameNotFoundException {
MediaProjectionManagerService.MediaProjection projection = startProjectionPreconditions();
projection.setLaunchCookie(mock(IBinder.class));
projection.start(mIMediaProjectionCallback);
projection.notifyVirtualDisplayCreated(10);
// Waiting for user to review consent.
doReturn(true).when(mWindowManagerInternal).setContentRecordingSession(
any(ContentRecordingSession.class));
mService.setContentRecordingSession(mWaitingDisplaySession);
mService.setUserReviewGrantedConsentResult(consentResult, projection);
verifySetSessionWithContent(recordedContent);
assertThat(mService.isCurrentProjection(projection)).isTrue();
}
/**
* Executes and validates scenario where the consent result indicates the projection continues,
* but unable to continue projection.
*/
private void testSetUserReviewGrantedConsentResult_failedToStartSession(
@ReviewGrantedConsentResult int consentResult,
@ContentRecordingSession.RecordContent int recordedContent)
throws NameNotFoundException {
MediaProjectionManagerService.MediaProjection projection = startProjectionPreconditions();
projection.start(mIMediaProjectionCallback);
projection.notifyVirtualDisplayCreated(10);
// Waiting for user to review consent.
doReturn(true).when(mWindowManagerInternal).setContentRecordingSession(
eq(mWaitingDisplaySession));
mService.setContentRecordingSession(mWaitingDisplaySession);
doReturn(false).when(mWindowManagerInternal).setContentRecordingSession(
any(ContentRecordingSession.class));
mService.setUserReviewGrantedConsentResult(consentResult, projection);
verifySetSessionWithContent(recordedContent);
assertThat(mService.isCurrentProjection(projection.asBinder())).isFalse();
}
@Test
public void testSetUserReviewGrantedConsentResult_displayMirroring_noPriorSession()
throws NameNotFoundException {
MediaProjectionManagerService.MediaProjection projection = startProjectionPreconditions();
projection.setLaunchCookie(mock(IBinder.class));
projection.start(mIMediaProjectionCallback);
// Skip setting the prior session details.
doReturn(true).when(mWindowManagerInternal).setContentRecordingSession(
any(ContentRecordingSession.class));
mService.setUserReviewGrantedConsentResult(RECORD_CONTENT_DISPLAY, projection);
// Result is ignored & session not updated.
verify(mWindowManagerInternal, never()).setContentRecordingSession(any(
ContentRecordingSession.class));
// Current session continues.
assertThat(mService.isCurrentProjection(projection)).isTrue();
}
@Test
public void testSetUserReviewGrantedConsentResult_displayMirroring_sessionNotWaiting()
throws NameNotFoundException {
MediaProjectionManagerService.MediaProjection projection = startProjectionPreconditions();
projection.setLaunchCookie(mock(IBinder.class));
projection.start(mIMediaProjectionCallback);
// Session is not waiting for user's consent.
doReturn(true).when(mWindowManagerInternal).setContentRecordingSession(
any(ContentRecordingSession.class));
mService.setContentRecordingSession(DISPLAY_SESSION);
doReturn(true).when(mWindowManagerInternal).setContentRecordingSession(
any(ContentRecordingSession.class));
mService.setUserReviewGrantedConsentResult(RECORD_CONTENT_DISPLAY, projection);
// Result is ignored; only the original session was ever sent.
verify(mWindowManagerInternal).setContentRecordingSession(eq(
DISPLAY_SESSION));
// Current session continues.
assertThat(mService.isCurrentProjection(projection)).isTrue();
}
private void verifySetSessionWithContent(@ContentRecordingSession.RecordContent int content) {
verify(mWindowManagerInternal, atLeastOnce()).setContentRecordingSession(
mSessionCaptor.capture());
assertThat(mSessionCaptor.getValue()).isNotNull();
assertThat(mSessionCaptor.getValue().getContentToRecord()).isEqualTo(content);
}
// Set up preconditions for creating a projection.
private MediaProjectionManagerService.MediaProjection createProjectionPreconditions(
MediaProjectionManagerService service)
@@ -373,14 +666,7 @@ public class MediaProjectionManagerServiceTest {
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);
TYPE_MIRRORING, /* isPermanentGrant= */ true, UserHandle.CURRENT);
}
// Set up preconditions for starting a projection, with no foreground service requirements.
@@ -391,19 +677,6 @@ public class MediaProjectionManagerServiceTest {
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 {
@@ -411,6 +684,15 @@ public class MediaProjectionManagerServiceTest {
return createProjectionPreconditions(mService);
}
// Set up preconditions for starting a projection, retrieving a pre-existing projection.
private MediaProjectionManagerService.MediaProjection startReusedProjectionPreconditions()
throws NameNotFoundException {
mAppInfo.privateFlags |= PRIVATE_FLAG_PRIVILEGED;
doReturn(mAppInfo).when(mPackageManager).getApplicationInfoAsUser(anyString(),
any(ApplicationInfoFlags.class), any(UserHandle.class));
return mService.getProjectionInternal(UID, PACKAGE_NAME);
}
private static class FakeIMediaProjectionCallback extends IMediaProjectionCallback.Stub {
@Override
public void onStop() throws RemoteException {