diff options
author | 2024-09-24 17:23:30 +0200 | |
---|---|---|
committer | 2024-10-03 14:54:05 +0000 | |
commit | a1b5763f22284ac5b3ea56cc3d50cd9ff3161e68 (patch) | |
tree | 0f6a446bc82b42d9940dbbf92d4b0d495255a055 | |
parent | 0d4d51818f0a1cde68e453f9cd0531a971b6c918 (diff) |
MediaProjection lockscreen recording Roles
Allow apps holding the android.app.role.COMPANION_DEVICE_APP_STREAMING
role to record the lockscreen
Bug: 367301791
Test: com.android.server.media.projection.MediaProjectionManagerServiceTest#testCreateProjection_keyguardLocked_RoleHeld
Flag: android.companion.virtualdevice.flags.media_projection_keyguard_restrictions
Change-Id: Iccb22027040315514c73bbb228d118dd03182635
2 files changed, 109 insertions, 2 deletions
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 47f579db604f..e7e519ede768 100644 --- a/services/core/java/com/android/server/media/projection/MediaProjectionManagerService.java +++ b/services/core/java/com/android/server/media/projection/MediaProjectionManagerService.java @@ -42,6 +42,8 @@ import android.app.AppOpsManager; import android.app.IProcessObserver; import android.app.KeyguardManager; import android.app.compat.CompatChanges; +import android.app.role.RoleManager; +import android.companion.AssociationRequest; import android.compat.annotation.ChangeId; import android.compat.annotation.EnabledSince; import android.content.ComponentName; @@ -94,7 +96,7 @@ import java.util.Objects; /** * Manages MediaProjection sessions. - * + * <p> * The {@link MediaProjectionManagerService} manages the creation and lifetime of MediaProjections, * as well as the capabilities they grant. Any service using MediaProjection tokens as permission * grants <b>must</b> validate the token before use by calling {@link @@ -137,6 +139,7 @@ public final class MediaProjectionManagerService extends SystemService private final PackageManager mPackageManager; private final WindowManagerInternal mWmInternal; private final KeyguardManager mKeyguardManager; + private final RoleManager mRoleManager; private final MediaRouter mMediaRouter; private final MediaRouterCallback mMediaRouterCallback; @@ -173,6 +176,7 @@ public final class MediaProjectionManagerService extends SystemService mKeyguardManager = (KeyguardManager) mContext.getSystemService(Context.KEYGUARD_SERVICE); mKeyguardManager.addKeyguardLockedStateListener( mContext.getMainExecutor(), this::onKeyguardLockedStateChanged); + mRoleManager = mContext.getSystemService(RoleManager.class); Watchdog.getInstance().addMonitor(this); } @@ -182,6 +186,7 @@ public final class MediaProjectionManagerService extends SystemService * - be one of the bugreport allowlisted packages, or * - hold the OP_PROJECT_MEDIA AppOp. */ + @SuppressWarnings("BooleanMethodIsAlwaysInverted") private boolean canCaptureKeyguard() { if (!android.companion.virtualdevice.flags.Flags.mediaProjectionKeyguardRestrictions()) { return true; @@ -193,6 +198,9 @@ public final class MediaProjectionManagerService extends SystemService if (mPackageManager.checkPermission(RECORD_SENSITIVE_CONTENT, mProjectionGrant.packageName) == PackageManager.PERMISSION_GRANTED) { + Slog.v(TAG, + "Allowing keyguard capture for package with RECORD_SENSITIVE_CONTENT " + + "permission"); return true; } if (AppOpsManager.MODE_ALLOWED == mAppOps.noteOpNoThrow(AppOpsManager.OP_PROJECT_MEDIA, @@ -200,6 +208,13 @@ public final class MediaProjectionManagerService extends SystemService "recording lockscreen")) { // Some tools use media projection by granting the OP_PROJECT_MEDIA app // op via a shell command. Those tools can be granted keyguard capture + Slog.v(TAG, + "Allowing keyguard capture for package with OP_PROJECT_MEDIA AppOp "); + return true; + } + if (isProjectionAppHoldingAppStreamingRoleLocked()) { + Slog.v(TAG, + "Allowing keyguard capture for package holding app streaming role."); return true; } return SystemConfig.getInstance().getBugreportWhitelistedPackages() @@ -698,6 +713,20 @@ public final class MediaProjectionManagerService extends SystemService } } + /** + * Application holding the app streaming role + * ({@value AssociationRequest#DEVICE_PROFILE_APP_STREAMING}) are allowed to record the + * lockscreen. + * + * @return true if the is held by the recording application. + */ + @GuardedBy("mLock") + private boolean isProjectionAppHoldingAppStreamingRoleLocked() { + return mRoleManager.getRoleHoldersAsUser(AssociationRequest.DEVICE_PROFILE_APP_STREAMING, + mContext.getUser()) + .contains(mProjectionGrant.packageName); + } + private void dump(final PrintWriter pw) { pw.println("MEDIA PROJECTION MANAGER (dumpsys media_projection)"); synchronized (mLock) { 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 ee63d5d32ff1..425bb158f997 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 @@ -33,6 +33,7 @@ import static android.view.Display.DEFAULT_DISPLAY; import static android.view.Display.INVALID_DISPLAY; import static com.google.common.truth.Truth.assertThat; +import static com.google.common.truth.Truth.assertWithMessage; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyInt; @@ -51,11 +52,15 @@ import static org.mockito.Mockito.verifyZeroInteractions; import static org.mockito.Mockito.when; import static org.testng.Assert.assertThrows; +import android.Manifest; import android.annotation.SuppressLint; import android.app.ActivityManagerInternal; import android.app.ActivityOptions.LaunchCookie; import android.app.AppOpsManager; +import android.app.Instrumentation; import android.app.KeyguardManager; +import android.app.role.RoleManager; +import android.companion.AssociationRequest; import android.content.Context; import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager; @@ -68,6 +73,7 @@ import android.media.projection.ReviewGrantedConsentResult; import android.os.Binder; import android.os.IBinder; import android.os.Looper; +import android.os.Process; import android.os.RemoteException; import android.os.UserHandle; import android.os.test.TestLooper; @@ -88,6 +94,7 @@ import com.android.server.testutils.OffsettableClock; import com.android.server.wm.WindowManagerInternal; import org.junit.After; +import org.junit.Assert; import org.junit.Before; import org.junit.Rule; import org.junit.Test; @@ -98,6 +105,7 @@ import org.mockito.Captor; import org.mockito.Mock; import org.mockito.MockitoAnnotations; +import java.util.Objects; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; @@ -312,7 +320,6 @@ public class MediaProjectionManagerServiceTest { assertThat(mService.getActiveProjectionInfo()).isNotNull(); } - @SuppressLint("MissingPermission") @EnableFlags(android.companion.virtualdevice.flags .Flags.FLAG_MEDIA_PROJECTION_KEYGUARD_RESTRICTIONS) @Test @@ -335,6 +342,36 @@ public class MediaProjectionManagerServiceTest { assertThat(mService.getActiveProjectionInfo()).isNotNull(); } + @EnableFlags(android.companion.virtualdevice.flags + .Flags.FLAG_MEDIA_PROJECTION_KEYGUARD_RESTRICTIONS) + @Test + public void testCreateProjection_keyguardLocked_RoleHeld() { + runWithRole(AssociationRequest.DEVICE_PROFILE_APP_STREAMING, () -> { + try { + mAppInfo.privateFlags |= PRIVATE_FLAG_PRIVILEGED; + doReturn(mAppInfo).when(mPackageManager).getApplicationInfoAsUser(anyString(), + any(ApplicationInfoFlags.class), any(UserHandle.class)); + MediaProjectionManagerService.MediaProjection projection = + mService.createProjectionInternal(Process.myUid(), + mContext.getPackageName(), + TYPE_MIRRORING, /* isPermanentGrant= */ false, UserHandle.CURRENT); + doReturn(true).when(mKeyguardManager).isKeyguardLocked(); + doReturn(PackageManager.PERMISSION_DENIED).when( + mPackageManager).checkPermission( + RECORD_SENSITIVE_CONTENT, projection.packageName); + + projection.start(mIMediaProjectionCallback); + projection.notifyVirtualDisplayCreated(10); + + // The projection was started because it was allowed to capture the keyguard. + assertWithMessage("Failed to run projection") + .that(mService.getActiveProjectionInfo()).isNotNull(); + } catch (NameNotFoundException e) { + throw new RuntimeException(e); + } + }); + } + @Test public void testCreateProjection_attemptReuse_noPriorProjectionGrant() throws NameNotFoundException { @@ -1202,6 +1239,47 @@ public class MediaProjectionManagerServiceTest { return mService.getProjectionInternal(UID, PACKAGE_NAME); } + /** + * Run the provided block giving the current context's package the provided role. + */ + @SuppressWarnings("SameParameterValue") + private void runWithRole(String role, Runnable block) { + Instrumentation instrumentation = InstrumentationRegistry.getInstrumentation(); + String packageName = mContext.getPackageName(); + UserHandle user = instrumentation.getTargetContext().getUser(); + RoleManager roleManager = Objects.requireNonNull( + mContext.getSystemService(RoleManager.class)); + try { + CountDownLatch latch = new CountDownLatch(1); + instrumentation.getUiAutomation().adoptShellPermissionIdentity( + Manifest.permission.MANAGE_ROLE_HOLDERS, + Manifest.permission.BYPASS_ROLE_QUALIFICATION); + + roleManager.setBypassingRoleQualification(true); + roleManager.addRoleHolderAsUser(role, packageName, /* flags = */ 0, user, + mContext.getMainExecutor(), success -> { + if (success) { + latch.countDown(); + } else { + Assert.fail("Couldn't set role for test (failure) " + role); + } + }); + assertWithMessage("Couldn't set role for test (timeout) : " + role) + .that(latch.await(1, TimeUnit.SECONDS)).isTrue(); + block.run(); + + } catch (InterruptedException e) { + throw new RuntimeException(e); + } finally { + roleManager.removeRoleHolderAsUser(role, packageName, 0, user, + mContext.getMainExecutor(), (aBool) -> { + }); + roleManager.setBypassingRoleQualification(false); + instrumentation.getUiAutomation() + .dropShellPermissionIdentity(); + } + } + private static class FakeIMediaProjectionCallback extends IMediaProjectionCallback.Stub { CountDownLatch mLatch = new CountDownLatch(1); @Override |