diff options
| author | 2023-03-29 15:40:18 +0000 | |
|---|---|---|
| committer | 2023-04-22 11:08:43 +0000 | |
| commit | 1398b3d058a2672c52d6ad96d698baface0e9c66 (patch) | |
| tree | af8a440fd77b50efdf1b01eb0535dd1f4a8ed3c6 | |
| parent | 3d3fc71b2189f302cc3bc82f683e3f180ae0b266 (diff) | |
(2/N)[MediaProjection] Show dialog when token is reused
Validate if the IMediaProjection token (representing
the user's consent) is used to get more than one
MediaProjection instance, or if client app is trying to
invoke MediaProjection#createVirtualDisplay more than
once.
Re-show the permission dialog for target SDK below U.
Follow-on CLs will:
* Black out recording when waiting for consent
Bug: 274790702
Test: atest FrameworksServicesTests:MediaProjectionManagerServiceTest
Change-Id: I30c96d9a9afd69fba29314caeeb867f665bb83ad
6 files changed, 615 insertions, 57 deletions
diff --git a/data/etc/services.core.protolog.json b/data/etc/services.core.protolog.json index eb2412c0d598..596f3515dda5 100644 --- a/data/etc/services.core.protolog.json +++ b/data/etc/services.core.protolog.json @@ -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", diff --git a/media/java/android/media/projection/IMediaProjectionManager.aidl b/media/java/android/media/projection/IMediaProjectionManager.aidl index 835e4c3ee4f6..a3cd623d627e 100644 --- a/media/java/android/media/projection/IMediaProjectionManager.aidl +++ b/media/java/android/media/projection/IMediaProjectionManager.aidl @@ -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); @@ -37,6 +49,21 @@ interface IMediaProjectionManager { 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); } diff --git a/media/java/android/media/projection/ReviewGrantedConsentResult.aidl b/media/java/android/media/projection/ReviewGrantedConsentResult.aidl new file mode 100644 index 000000000000..4f25be75ab29 --- /dev/null +++ b/media/java/android/media/projection/ReviewGrantedConsentResult.aidl @@ -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, +} diff --git a/services/core/java/com/android/server/media/projection/MediaProjectionManagerService.java b/services/core/java/com/android/server/media/projection/MediaProjectionManagerService.java index 94d5aabe24e5..7a51126eff2d 100644 --- a/services/core/java/com/android/server/media/projection/MediaProjectionManagerService.java +++ b/services/core/java/com/android/server/media/projection/MediaProjectionManagerService.java @@ -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. + } + } } } diff --git a/services/core/java/com/android/server/wm/ContentRecordingController.java b/services/core/java/com/android/server/wm/ContentRecordingController.java index a41dcc66ff52..040da8862c74 100644 --- a/services/core/java/com/android/server/wm/ContentRecordingController.java +++ b/services/core/java/com/android/server/wm/ContentRecordingController.java @@ -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. diff --git a/services/tests/servicestests/src/com/android/server/media/projection/MediaProjectionManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/media/projection/MediaProjectionManagerServiceTest.java index 36c200188fcf..5751db0727e7 100644 --- a/services/tests/servicestests/src/com/android/server/media/projection/MediaProjectionManagerServiceTest.java +++ b/services/tests/servicestests/src/com/android/server/media/projection/MediaProjectionManagerServiceTest.java @@ -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,24 +677,20 @@ 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) + // Set up preconditions for starting a projection, with no foreground service requirements. + private MediaProjectionManagerService.MediaProjection startProjectionPreconditions() 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); + return createProjectionPreconditions(mService); } - // Set up preconditions for starting a projection, with no foreground service requirements. - private MediaProjectionManagerService.MediaProjection startProjectionPreconditions() + // Set up preconditions for starting a projection, retrieving a pre-existing projection. + private MediaProjectionManagerService.MediaProjection startReusedProjectionPreconditions() throws NameNotFoundException { mAppInfo.privateFlags |= PRIVATE_FLAG_PRIVILEGED; - return createProjectionPreconditions(mService); + 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 { |