Loading data/etc/services.core.protolog.json +6 −0 Original line number Original line Diff line number Diff line Loading @@ -1495,6 +1495,12 @@ "group": "WM_DEBUG_CONFIGURATION", "group": "WM_DEBUG_CONFIGURATION", "at": "com\/android\/server\/wm\/ActivityRecord.java" "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": { "-732715767": { "message": "Unable to retrieve window container to start recording for display %d", "message": "Unable to retrieve window container to start recording for display %d", "level": "VERBOSE", "level": "VERBOSE", Loading media/java/android/media/projection/IMediaProjectionManager.aidl +51 −1 Original line number Original line Diff line number Diff line Loading @@ -20,11 +20,23 @@ import android.media.projection.IMediaProjection; import android.media.projection.IMediaProjectionCallback; import android.media.projection.IMediaProjectionCallback; import android.media.projection.IMediaProjectionWatcherCallback; import android.media.projection.IMediaProjectionWatcherCallback; import android.media.projection.MediaProjectionInfo; import android.media.projection.MediaProjectionInfo; import android.media.projection.ReviewGrantedConsentResult; import android.os.IBinder; import android.os.IBinder; import android.view.ContentRecordingSession; import android.view.ContentRecordingSession; /** {@hide} */ /** {@hide} */ interface IMediaProjectionManager { 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 @UnsupportedAppUsage boolean hasProjectionPermission(int uid, String packageName); boolean hasProjectionPermission(int uid, String packageName); Loading @@ -36,6 +48,21 @@ interface IMediaProjectionManager { IMediaProjection createProjection(int uid, String packageName, int type, IMediaProjection createProjection(int uid, String packageName, int type, boolean permanentGrant); 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 * Returns {@code true} if the given {@link IMediaProjection} corresponds to the current * projection, or {@code false} otherwise. * projection, or {@code false} otherwise. Loading @@ -58,7 +85,7 @@ interface IMediaProjectionManager { */ */ @JavaPassthrough(annotation = "@android.annotation.RequiresPermission(android.Manifest" @JavaPassthrough(annotation = "@android.annotation.RequiresPermission(android.Manifest" + ".permission.MANAGE_MEDIA_PROJECTION)") + ".permission.MANAGE_MEDIA_PROJECTION)") void requestConsentForInvalidProjection(IMediaProjection projection); void requestConsentForInvalidProjection(in IMediaProjection projection); @JavaPassthrough(annotation = "@android.annotation.RequiresPermission(android.Manifest" @JavaPassthrough(annotation = "@android.annotation.RequiresPermission(android.Manifest" + ".permission.MANAGE_MEDIA_PROJECTION)") + ".permission.MANAGE_MEDIA_PROJECTION)") Loading Loading @@ -94,9 +121,32 @@ interface IMediaProjectionManager { * * * @param incomingSession the nullable incoming content recording session * @param incomingSession the nullable incoming content recording session * @param projection the non-null projection the session describes * @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" @JavaPassthrough(annotation = "@android.annotation.RequiresPermission(android.Manifest" + ".permission.MANAGE_MEDIA_PROJECTION)") + ".permission.MANAGE_MEDIA_PROJECTION)") boolean setContentRecordingSession(in ContentRecordingSession incomingSession, boolean setContentRecordingSession(in ContentRecordingSession incomingSession, in IMediaProjection projection); 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); } } media/java/android/media/projection/ReviewGrantedConsentResult.aidl 0 → 100644 +31 −0 Original line number Original line Diff line number Diff line /* * 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, } services/core/java/com/android/server/media/projection/MediaProjectionManagerService.java +201 −12 Original line number Original line Diff line number Diff line Loading @@ -19,9 +19,19 @@ package com.android.server.media.projection; import static android.Manifest.permission.MANAGE_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_CREATED; import static android.app.ActivityManagerInternal.MEDIA_PROJECTION_TOKEN_EVENT_DESTROYED; 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 static android.view.Display.INVALID_DISPLAY; import android.Manifest; import android.Manifest; import android.annotation.EnforcePermission; import android.annotation.NonNull; import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.Nullable; import android.app.ActivityManagerInternal; import android.app.ActivityManagerInternal; Loading @@ -30,7 +40,9 @@ import android.app.IProcessObserver; import android.app.compat.CompatChanges; import android.app.compat.CompatChanges; import android.compat.annotation.ChangeId; import android.compat.annotation.ChangeId; import android.compat.annotation.EnabledSince; import android.compat.annotation.EnabledSince; import android.content.ComponentName; import android.content.Context; import android.content.Context; import android.content.Intent; import android.content.pm.ApplicationInfo; import android.content.pm.ApplicationInfo; import android.content.pm.PackageInfo; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import android.content.pm.PackageManager; Loading @@ -45,11 +57,13 @@ import android.media.projection.IMediaProjectionManager; import android.media.projection.IMediaProjectionWatcherCallback; import android.media.projection.IMediaProjectionWatcherCallback; import android.media.projection.MediaProjectionInfo; import android.media.projection.MediaProjectionInfo; import android.media.projection.MediaProjectionManager; import android.media.projection.MediaProjectionManager; import android.media.projection.ReviewGrantedConsentResult; import android.os.Binder; import android.os.Binder; import android.os.Build; import android.os.Build; import android.os.Handler; import android.os.Handler; import android.os.IBinder; import android.os.IBinder; import android.os.Looper; import android.os.Looper; import android.os.PermissionEnforcer; import android.os.RemoteException; import android.os.RemoteException; import android.os.SystemClock; import android.os.SystemClock; import android.os.UserHandle; import android.os.UserHandle; Loading @@ -57,6 +71,7 @@ import android.util.ArrayMap; import android.util.Slog; import android.util.Slog; import android.view.ContentRecordingSession; import android.view.ContentRecordingSession; import com.android.internal.R; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.util.ArrayUtils; import com.android.internal.util.ArrayUtils; import com.android.internal.util.DumpUtils; import com.android.internal.util.DumpUtils; Loading @@ -69,6 +84,7 @@ import java.io.FileDescriptor; import java.io.PrintWriter; import java.io.PrintWriter; import java.time.Duration; import java.time.Duration; import java.util.Map; import java.util.Map; import java.util.Objects; /** /** * Manages MediaProjection sessions. * Manages MediaProjection sessions. Loading Loading @@ -161,10 +177,9 @@ public final class MediaProjectionManagerService extends SystemService } } } } @Override @Override public void onStart() { public void onStart() { publishBinderService(Context.MEDIA_PROJECTION_SERVICE, new BinderService(), publishBinderService(Context.MEDIA_PROJECTION_SERVICE, new BinderService(mContext), false /*allowIsolated*/); false /*allowIsolated*/); mMediaRouter.addCallback(MediaRouter.ROUTE_TYPE_REMOTE_DISPLAY, mMediaRouterCallback, mMediaRouter.addCallback(MediaRouter.ROUTE_TYPE_REMOTE_DISPLAY, mMediaRouterCallback, MediaRouter.CALLBACK_FLAG_PASSIVE_DISCOVERY); MediaRouter.CALLBACK_FLAG_PASSIVE_DISCOVERY); Loading Loading @@ -305,6 +320,10 @@ public final class MediaProjectionManagerService extends SystemService } } return false; return false; } } if (mProjectionGrant != null) { // Cache the session details. mProjectionGrant.mSession = incomingSession; } return true; return true; } } } } Loading @@ -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. * the given projection instance. * * * <p>Preconditions: * <p>Preconditions: Loading @@ -337,18 +355,111 @@ public final class MediaProjectionManagerService extends SystemService * <p>Returns immediately but waits to start recording until user has reviewed their consent. * <p>Returns immediately but waits to start recording until user has reviewed their consent. */ */ @VisibleForTesting @VisibleForTesting void requestConsentForInvalidProjection(IMediaProjection projection) { void requestConsentForInvalidProjection() { synchronized (mLock) { synchronized (mLock) { Slog.v(TAG, "Reusing token: Reshow dialog for due to invalid projection."); 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. // TODO(b/261563516): Remove internal method and test aidl directly, here and elsewhere. @VisibleForTesting @VisibleForTesting MediaProjection createProjectionInternal(int uid, String packageName, int type, MediaProjection createProjectionInternal(int uid, String packageName, int type, boolean isPermanentGrant, UserHandle callingUser, boolean isPermanentGrant, UserHandle callingUser) { boolean packageAttemptedReusingGrantedConsent) { MediaProjection projection; MediaProjection projection; ApplicationInfo ai; ApplicationInfo ai; try { try { Loading @@ -371,6 +482,34 @@ public final class MediaProjectionManagerService extends SystemService return projection; 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 @VisibleForTesting MediaProjectionInfo getActiveProjectionInfo() { MediaProjectionInfo getActiveProjectionInfo() { synchronized (mLock) { synchronized (mLock) { Loading @@ -395,6 +534,10 @@ public final class MediaProjectionManagerService extends SystemService private final class BinderService extends IMediaProjectionManager.Stub { private final class BinderService extends IMediaProjectionManager.Stub { BinderService(Context context) { super(PermissionEnforcer.fromContext(context)); } @Override // Binder call @Override // Binder call public boolean hasProjectionPermission(int uid, String packageName) { public boolean hasProjectionPermission(int uid, String packageName) { final long token = Binder.clearCallingIdentity(); final long token = Binder.clearCallingIdentity(); Loading Loading @@ -424,7 +567,25 @@ public final class MediaProjectionManagerService extends SystemService } } final UserHandle callingUser = Binder.getCallingUserHandle(); final UserHandle callingUser = Binder.getCallingUserHandle(); return createProjectionInternal(uid, packageName, type, isPermanentGrant, 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 @Override // Binder call Loading Loading @@ -562,7 +723,7 @@ public final class MediaProjectionManagerService extends SystemService } } @Override @Override public void requestConsentForInvalidProjection(IMediaProjection projection) { public void requestConsentForInvalidProjection(@NonNull IMediaProjection projection) { if (mContext.checkCallingOrSelfPermission(Manifest.permission.MANAGE_MEDIA_PROJECTION) if (mContext.checkCallingOrSelfPermission(Manifest.permission.MANAGE_MEDIA_PROJECTION) != PackageManager.PERMISSION_GRANTED) { != PackageManager.PERMISSION_GRANTED) { throw new SecurityException("Requires MANAGE_MEDIA_PROJECTION to check if the given" throw new SecurityException("Requires MANAGE_MEDIA_PROJECTION to check if the given" Loading @@ -577,7 +738,22 @@ public final class MediaProjectionManagerService extends SystemService // Remove calling app identity before performing any privileged operations. // Remove calling app identity before performing any privileged operations. final long token = Binder.clearCallingIdentity(); final long token = Binder.clearCallingIdentity(); try { 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 { } finally { Binder.restoreCallingIdentity(token); Binder.restoreCallingIdentity(token); } } Loading @@ -594,7 +770,6 @@ public final class MediaProjectionManagerService extends SystemService } } } } private boolean checkPermission(String packageName, String permission) { private boolean checkPermission(String packageName, String permission) { return mContext.getPackageManager().checkPermission(permission, packageName) return mContext.getPackageManager().checkPermission(permission, packageName) == PackageManager.PERMISSION_GRANTED; == PackageManager.PERMISSION_GRANTED; Loading Loading @@ -630,6 +805,8 @@ public final class MediaProjectionManagerService extends SystemService // Set if MediaProjection#createVirtualDisplay has been invoked previously (it // Set if MediaProjection#createVirtualDisplay has been invoked previously (it // should only be called once). // should only be called once). private int mVirtualDisplayId = INVALID_DISPLAY; 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, MediaProjection(int type, int uid, String packageName, int targetSdkVersion, boolean isPrivileged) { boolean isPrivileged) { Loading Loading @@ -883,6 +1060,18 @@ public final class MediaProjectionManagerService extends SystemService } } synchronized (mLock) { synchronized (mLock) { mVirtualDisplayId = displayId; 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. } } } } } } Loading services/core/java/com/android/server/wm/ContentRecordingController.java +2 −2 Original line number Original line Diff line number Diff line Loading @@ -80,7 +80,7 @@ final class ContentRecordingController { } } // Invalid scenario: ignore identical incoming session. // Invalid scenario: ignore identical incoming session. if (ContentRecordingSession.isProjectionOnSameDisplay(mSession, incomingSession)) { 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. // the update through. ProtoLog.v(WM_DEBUG_CONTENT_RECORDING, ProtoLog.v(WM_DEBUG_CONTENT_RECORDING, Loading @@ -99,7 +99,7 @@ final class ContentRecordingController { incomingDisplayContent = wmService.mRoot.getDisplayContentOrCreate( incomingDisplayContent = wmService.mRoot.getDisplayContentOrCreate( incomingSession.getVirtualDisplayId()); incomingSession.getVirtualDisplayId()); incomingDisplayContent.setContentRecordingSession(incomingSession); 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. // to update, since no config/display change arrives. Mark recording as black. } } // Takeover and stopping scenario: stop recording on the pre-existing session. // Takeover and stopping scenario: stop recording on the pre-existing session. Loading Loading
data/etc/services.core.protolog.json +6 −0 Original line number Original line Diff line number Diff line Loading @@ -1495,6 +1495,12 @@ "group": "WM_DEBUG_CONFIGURATION", "group": "WM_DEBUG_CONFIGURATION", "at": "com\/android\/server\/wm\/ActivityRecord.java" "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": { "-732715767": { "message": "Unable to retrieve window container to start recording for display %d", "message": "Unable to retrieve window container to start recording for display %d", "level": "VERBOSE", "level": "VERBOSE", Loading
media/java/android/media/projection/IMediaProjectionManager.aidl +51 −1 Original line number Original line Diff line number Diff line Loading @@ -20,11 +20,23 @@ import android.media.projection.IMediaProjection; import android.media.projection.IMediaProjectionCallback; import android.media.projection.IMediaProjectionCallback; import android.media.projection.IMediaProjectionWatcherCallback; import android.media.projection.IMediaProjectionWatcherCallback; import android.media.projection.MediaProjectionInfo; import android.media.projection.MediaProjectionInfo; import android.media.projection.ReviewGrantedConsentResult; import android.os.IBinder; import android.os.IBinder; import android.view.ContentRecordingSession; import android.view.ContentRecordingSession; /** {@hide} */ /** {@hide} */ interface IMediaProjectionManager { 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 @UnsupportedAppUsage boolean hasProjectionPermission(int uid, String packageName); boolean hasProjectionPermission(int uid, String packageName); Loading @@ -36,6 +48,21 @@ interface IMediaProjectionManager { IMediaProjection createProjection(int uid, String packageName, int type, IMediaProjection createProjection(int uid, String packageName, int type, boolean permanentGrant); 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 * Returns {@code true} if the given {@link IMediaProjection} corresponds to the current * projection, or {@code false} otherwise. * projection, or {@code false} otherwise. Loading @@ -58,7 +85,7 @@ interface IMediaProjectionManager { */ */ @JavaPassthrough(annotation = "@android.annotation.RequiresPermission(android.Manifest" @JavaPassthrough(annotation = "@android.annotation.RequiresPermission(android.Manifest" + ".permission.MANAGE_MEDIA_PROJECTION)") + ".permission.MANAGE_MEDIA_PROJECTION)") void requestConsentForInvalidProjection(IMediaProjection projection); void requestConsentForInvalidProjection(in IMediaProjection projection); @JavaPassthrough(annotation = "@android.annotation.RequiresPermission(android.Manifest" @JavaPassthrough(annotation = "@android.annotation.RequiresPermission(android.Manifest" + ".permission.MANAGE_MEDIA_PROJECTION)") + ".permission.MANAGE_MEDIA_PROJECTION)") Loading Loading @@ -94,9 +121,32 @@ interface IMediaProjectionManager { * * * @param incomingSession the nullable incoming content recording session * @param incomingSession the nullable incoming content recording session * @param projection the non-null projection the session describes * @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" @JavaPassthrough(annotation = "@android.annotation.RequiresPermission(android.Manifest" + ".permission.MANAGE_MEDIA_PROJECTION)") + ".permission.MANAGE_MEDIA_PROJECTION)") boolean setContentRecordingSession(in ContentRecordingSession incomingSession, boolean setContentRecordingSession(in ContentRecordingSession incomingSession, in IMediaProjection projection); 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); } }
media/java/android/media/projection/ReviewGrantedConsentResult.aidl 0 → 100644 +31 −0 Original line number Original line Diff line number Diff line /* * 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, }
services/core/java/com/android/server/media/projection/MediaProjectionManagerService.java +201 −12 Original line number Original line Diff line number Diff line Loading @@ -19,9 +19,19 @@ package com.android.server.media.projection; import static android.Manifest.permission.MANAGE_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_CREATED; import static android.app.ActivityManagerInternal.MEDIA_PROJECTION_TOKEN_EVENT_DESTROYED; 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 static android.view.Display.INVALID_DISPLAY; import android.Manifest; import android.Manifest; import android.annotation.EnforcePermission; import android.annotation.NonNull; import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.Nullable; import android.app.ActivityManagerInternal; import android.app.ActivityManagerInternal; Loading @@ -30,7 +40,9 @@ import android.app.IProcessObserver; import android.app.compat.CompatChanges; import android.app.compat.CompatChanges; import android.compat.annotation.ChangeId; import android.compat.annotation.ChangeId; import android.compat.annotation.EnabledSince; import android.compat.annotation.EnabledSince; import android.content.ComponentName; import android.content.Context; import android.content.Context; import android.content.Intent; import android.content.pm.ApplicationInfo; import android.content.pm.ApplicationInfo; import android.content.pm.PackageInfo; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import android.content.pm.PackageManager; Loading @@ -45,11 +57,13 @@ import android.media.projection.IMediaProjectionManager; import android.media.projection.IMediaProjectionWatcherCallback; import android.media.projection.IMediaProjectionWatcherCallback; import android.media.projection.MediaProjectionInfo; import android.media.projection.MediaProjectionInfo; import android.media.projection.MediaProjectionManager; import android.media.projection.MediaProjectionManager; import android.media.projection.ReviewGrantedConsentResult; import android.os.Binder; import android.os.Binder; import android.os.Build; import android.os.Build; import android.os.Handler; import android.os.Handler; import android.os.IBinder; import android.os.IBinder; import android.os.Looper; import android.os.Looper; import android.os.PermissionEnforcer; import android.os.RemoteException; import android.os.RemoteException; import android.os.SystemClock; import android.os.SystemClock; import android.os.UserHandle; import android.os.UserHandle; Loading @@ -57,6 +71,7 @@ import android.util.ArrayMap; import android.util.Slog; import android.util.Slog; import android.view.ContentRecordingSession; import android.view.ContentRecordingSession; import com.android.internal.R; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.util.ArrayUtils; import com.android.internal.util.ArrayUtils; import com.android.internal.util.DumpUtils; import com.android.internal.util.DumpUtils; Loading @@ -69,6 +84,7 @@ import java.io.FileDescriptor; import java.io.PrintWriter; import java.io.PrintWriter; import java.time.Duration; import java.time.Duration; import java.util.Map; import java.util.Map; import java.util.Objects; /** /** * Manages MediaProjection sessions. * Manages MediaProjection sessions. Loading Loading @@ -161,10 +177,9 @@ public final class MediaProjectionManagerService extends SystemService } } } } @Override @Override public void onStart() { public void onStart() { publishBinderService(Context.MEDIA_PROJECTION_SERVICE, new BinderService(), publishBinderService(Context.MEDIA_PROJECTION_SERVICE, new BinderService(mContext), false /*allowIsolated*/); false /*allowIsolated*/); mMediaRouter.addCallback(MediaRouter.ROUTE_TYPE_REMOTE_DISPLAY, mMediaRouterCallback, mMediaRouter.addCallback(MediaRouter.ROUTE_TYPE_REMOTE_DISPLAY, mMediaRouterCallback, MediaRouter.CALLBACK_FLAG_PASSIVE_DISCOVERY); MediaRouter.CALLBACK_FLAG_PASSIVE_DISCOVERY); Loading Loading @@ -305,6 +320,10 @@ public final class MediaProjectionManagerService extends SystemService } } return false; return false; } } if (mProjectionGrant != null) { // Cache the session details. mProjectionGrant.mSession = incomingSession; } return true; return true; } } } } Loading @@ -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. * the given projection instance. * * * <p>Preconditions: * <p>Preconditions: Loading @@ -337,18 +355,111 @@ public final class MediaProjectionManagerService extends SystemService * <p>Returns immediately but waits to start recording until user has reviewed their consent. * <p>Returns immediately but waits to start recording until user has reviewed their consent. */ */ @VisibleForTesting @VisibleForTesting void requestConsentForInvalidProjection(IMediaProjection projection) { void requestConsentForInvalidProjection() { synchronized (mLock) { synchronized (mLock) { Slog.v(TAG, "Reusing token: Reshow dialog for due to invalid projection."); 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. // TODO(b/261563516): Remove internal method and test aidl directly, here and elsewhere. @VisibleForTesting @VisibleForTesting MediaProjection createProjectionInternal(int uid, String packageName, int type, MediaProjection createProjectionInternal(int uid, String packageName, int type, boolean isPermanentGrant, UserHandle callingUser, boolean isPermanentGrant, UserHandle callingUser) { boolean packageAttemptedReusingGrantedConsent) { MediaProjection projection; MediaProjection projection; ApplicationInfo ai; ApplicationInfo ai; try { try { Loading @@ -371,6 +482,34 @@ public final class MediaProjectionManagerService extends SystemService return projection; 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 @VisibleForTesting MediaProjectionInfo getActiveProjectionInfo() { MediaProjectionInfo getActiveProjectionInfo() { synchronized (mLock) { synchronized (mLock) { Loading @@ -395,6 +534,10 @@ public final class MediaProjectionManagerService extends SystemService private final class BinderService extends IMediaProjectionManager.Stub { private final class BinderService extends IMediaProjectionManager.Stub { BinderService(Context context) { super(PermissionEnforcer.fromContext(context)); } @Override // Binder call @Override // Binder call public boolean hasProjectionPermission(int uid, String packageName) { public boolean hasProjectionPermission(int uid, String packageName) { final long token = Binder.clearCallingIdentity(); final long token = Binder.clearCallingIdentity(); Loading Loading @@ -424,7 +567,25 @@ public final class MediaProjectionManagerService extends SystemService } } final UserHandle callingUser = Binder.getCallingUserHandle(); final UserHandle callingUser = Binder.getCallingUserHandle(); return createProjectionInternal(uid, packageName, type, isPermanentGrant, 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 @Override // Binder call Loading Loading @@ -562,7 +723,7 @@ public final class MediaProjectionManagerService extends SystemService } } @Override @Override public void requestConsentForInvalidProjection(IMediaProjection projection) { public void requestConsentForInvalidProjection(@NonNull IMediaProjection projection) { if (mContext.checkCallingOrSelfPermission(Manifest.permission.MANAGE_MEDIA_PROJECTION) if (mContext.checkCallingOrSelfPermission(Manifest.permission.MANAGE_MEDIA_PROJECTION) != PackageManager.PERMISSION_GRANTED) { != PackageManager.PERMISSION_GRANTED) { throw new SecurityException("Requires MANAGE_MEDIA_PROJECTION to check if the given" throw new SecurityException("Requires MANAGE_MEDIA_PROJECTION to check if the given" Loading @@ -577,7 +738,22 @@ public final class MediaProjectionManagerService extends SystemService // Remove calling app identity before performing any privileged operations. // Remove calling app identity before performing any privileged operations. final long token = Binder.clearCallingIdentity(); final long token = Binder.clearCallingIdentity(); try { 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 { } finally { Binder.restoreCallingIdentity(token); Binder.restoreCallingIdentity(token); } } Loading @@ -594,7 +770,6 @@ public final class MediaProjectionManagerService extends SystemService } } } } private boolean checkPermission(String packageName, String permission) { private boolean checkPermission(String packageName, String permission) { return mContext.getPackageManager().checkPermission(permission, packageName) return mContext.getPackageManager().checkPermission(permission, packageName) == PackageManager.PERMISSION_GRANTED; == PackageManager.PERMISSION_GRANTED; Loading Loading @@ -630,6 +805,8 @@ public final class MediaProjectionManagerService extends SystemService // Set if MediaProjection#createVirtualDisplay has been invoked previously (it // Set if MediaProjection#createVirtualDisplay has been invoked previously (it // should only be called once). // should only be called once). private int mVirtualDisplayId = INVALID_DISPLAY; 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, MediaProjection(int type, int uid, String packageName, int targetSdkVersion, boolean isPrivileged) { boolean isPrivileged) { Loading Loading @@ -883,6 +1060,18 @@ public final class MediaProjectionManagerService extends SystemService } } synchronized (mLock) { synchronized (mLock) { mVirtualDisplayId = displayId; 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. } } } } } } Loading
services/core/java/com/android/server/wm/ContentRecordingController.java +2 −2 Original line number Original line Diff line number Diff line Loading @@ -80,7 +80,7 @@ final class ContentRecordingController { } } // Invalid scenario: ignore identical incoming session. // Invalid scenario: ignore identical incoming session. if (ContentRecordingSession.isProjectionOnSameDisplay(mSession, incomingSession)) { 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. // the update through. ProtoLog.v(WM_DEBUG_CONTENT_RECORDING, ProtoLog.v(WM_DEBUG_CONTENT_RECORDING, Loading @@ -99,7 +99,7 @@ final class ContentRecordingController { incomingDisplayContent = wmService.mRoot.getDisplayContentOrCreate( incomingDisplayContent = wmService.mRoot.getDisplayContentOrCreate( incomingSession.getVirtualDisplayId()); incomingSession.getVirtualDisplayId()); incomingDisplayContent.setContentRecordingSession(incomingSession); 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. // to update, since no config/display change arrives. Mark recording as black. } } // Takeover and stopping scenario: stop recording on the pre-existing session. // Takeover and stopping scenario: stop recording on the pre-existing session. Loading