diff options
| author | 2023-04-22 13:31:56 +0000 | |
|---|---|---|
| committer | 2023-04-22 13:31:56 +0000 | |
| commit | 1d535bc391c0ea1a7fec94b55a9ea9f608f58767 (patch) | |
| tree | 2bda47ad987770ea5ea632fa9ed4cc6e763adece | |
| parent | 0a66fbb609a373ac59ee61274038359b75e6708d (diff) | |
| parent | 1398b3d058a2672c52d6ad96d698baface0e9c66 (diff) | |
Merge "(2/N)[MediaProjection] Show dialog when token is reused" into udc-dev
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 { |