Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 0a4f6aa9 authored by Vadim Caen's avatar Vadim Caen Committed by Android Build Coastguard Worker
Browse files

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
(cherry picked from commit a1b5763f)
(cherry picked from https://googleplex-android-review.googlesource.com/q/commit:869074c0ba83e914538ee4082f0fa6ec2079e7bd)
Merged-In: Iccb22027040315514c73bbb228d118dd03182635
Change-Id: Iccb22027040315514c73bbb228d118dd03182635
parent 57cda4b3
Loading
Loading
Loading
Loading
+30 −1
Original line number Diff line number Diff line
@@ -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) {
+79 −1
Original line number Diff line number Diff line
@@ -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