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

Commit a1b5763f authored by Vadim Caen's avatar Vadim Caen
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
Change-Id: Iccb22027040315514c73bbb228d118dd03182635
parent 0d4d5181
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