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

Commit a85f3392 authored by Vladimir Komsiyski's avatar Vladimir Komsiyski Committed by Android (Google) Code Review
Browse files

Merge "Auto-stop MediaProjection when screen is locked" into main

parents c41d00bc da18f328
Loading
Loading
Loading
Loading
+51 −1
Original line number Diff line number Diff line
@@ -17,6 +17,7 @@
package com.android.server.media.projection;

import static android.Manifest.permission.MANAGE_MEDIA_PROJECTION;
import static android.Manifest.permission.RECORD_SENSITIVE_CONTENT;
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;
@@ -34,10 +35,12 @@ import android.Manifest;
import android.annotation.EnforcePermission;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.RequiresPermission;
import android.app.ActivityManagerInternal;
import android.app.ActivityOptions.LaunchCookie;
import android.app.AppOpsManager;
import android.app.IProcessObserver;
import android.app.KeyguardManager;
import android.app.compat.CompatChanges;
import android.compat.annotation.ChangeId;
import android.compat.annotation.EnabledSince;
@@ -78,6 +81,7 @@ import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.util.ArrayUtils;
import com.android.internal.util.DumpUtils;
import com.android.server.LocalServices;
import com.android.server.SystemConfig;
import com.android.server.SystemService;
import com.android.server.Watchdog;
import com.android.server.wm.WindowManagerInternal;
@@ -132,6 +136,7 @@ public final class MediaProjectionManagerService extends SystemService
    private final ActivityManagerInternal mActivityManagerInternal;
    private final PackageManager mPackageManager;
    private final WindowManagerInternal mWmInternal;
    private final KeyguardManager mKeyguardManager;

    private final MediaRouter mMediaRouter;
    private final MediaRouterCallback mMediaRouterCallback;
@@ -147,7 +152,9 @@ public final class MediaProjectionManagerService extends SystemService
        this(context, new Injector());
    }

    @VisibleForTesting MediaProjectionManagerService(Context context, Injector injector) {
    @RequiresPermission(Manifest.permission.SUBSCRIBE_TO_KEYGUARD_LOCKED_STATE)
    @VisibleForTesting
    MediaProjectionManagerService(Context context, Injector injector) {
        super(context);
        mContext = context;
        mInjector = injector;
@@ -163,9 +170,47 @@ public final class MediaProjectionManagerService extends SystemService
        mMediaRouter = (MediaRouter) mContext.getSystemService(Context.MEDIA_ROUTER_SERVICE);
        mMediaRouterCallback = new MediaRouterCallback();
        mMediaProjectionMetricsLogger = injector.mediaProjectionMetricsLogger(context);
        mKeyguardManager = (KeyguardManager) mContext.getSystemService(Context.KEYGUARD_SERVICE);
        mKeyguardManager.addKeyguardLockedStateListener(
                mContext.getMainExecutor(), this::onKeyguardLockedStateChanged);
        Watchdog.getInstance().addMonitor(this);
    }

    /**
     * In order to record the keyguard, the MediaProjection package must be either:
     *   - a holder of RECORD_SENSITIVE_CONTENT permission, or
     *   - be one of the bugreport whitelisted packages
     */
    private boolean canCaptureKeyguard() {
        if (!android.companion.virtualdevice.flags.Flags.mediaProjectionKeyguardRestrictions()) {
            return true;
        }
        synchronized (mLock) {
            if (mProjectionGrant == null || mProjectionGrant.packageName == null) {
                return false;
            }
            if (mPackageManager.checkPermission(RECORD_SENSITIVE_CONTENT,
                    mProjectionGrant.packageName)
                    == PackageManager.PERMISSION_GRANTED) {
                return true;
            }
            return SystemConfig.getInstance().getBugreportWhitelistedPackages()
                    .contains(mProjectionGrant.packageName);
        }
    }

    @VisibleForTesting
    void onKeyguardLockedStateChanged(boolean isKeyguardLocked) {
        if (!isKeyguardLocked) return;
        synchronized (mLock) {
            if (mProjectionGrant != null && !canCaptureKeyguard()) {
                Slog.d(TAG, "Content Recording: Stopped MediaProjection"
                        + " due to keyguard lock");
                mProjectionGrant.stop();
            }
        }
    }

    /** Functional interface for providing time. */
    @VisibleForTesting
    interface Clock {
@@ -1252,6 +1297,11 @@ public final class MediaProjectionManagerService extends SystemService
        @Override
        public void notifyVirtualDisplayCreated(int displayId) {
            notifyVirtualDisplayCreated_enforcePermission();
            if (mKeyguardManager.isKeyguardLocked() && !canCaptureKeyguard()) {
                Slog.w(TAG, "Content Recording: Keyguard locked, aborting MediaProjection");
                stop();
                return;
            }
            synchronized (mLock) {
                mVirtualDisplayId = displayId;

+86 −0
Original line number Diff line number Diff line
@@ -17,6 +17,7 @@
package com.android.server.media.projection;


import static android.Manifest.permission.RECORD_SENSITIVE_CONTENT;
import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW;
import static android.content.pm.ApplicationInfo.PRIVATE_FLAG_PRIVILEGED;
import static android.media.projection.MediaProjectionManager.TYPE_MIRRORING;
@@ -50,6 +51,7 @@ import static org.testng.Assert.assertThrows;

import android.app.ActivityManagerInternal;
import android.app.ActivityOptions.LaunchCookie;
import android.app.KeyguardManager;
import android.content.Context;
import android.content.ContextWrapper;
import android.content.pm.ApplicationInfo;
@@ -66,7 +68,9 @@ import android.os.Looper;
import android.os.RemoteException;
import android.os.UserHandle;
import android.os.test.TestLooper;
import android.platform.test.annotations.EnableFlags;
import android.platform.test.annotations.Presubmit;
import android.platform.test.flag.junit.SetFlagsRule;
import android.view.ContentRecordingSession;
import android.view.ContentRecordingSession.RecordContent;

@@ -81,6 +85,7 @@ import com.android.server.wm.WindowManagerInternal;

import org.junit.After;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
@@ -151,6 +156,9 @@ public class MediaProjectionManagerServiceTest {
    private ContentRecordingSession mWaitingDisplaySession =
            createDisplaySession(DEFAULT_DISPLAY);

    @Rule
    public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();

    @Mock
    private ActivityManagerInternal mAmInternal;
    @Mock
@@ -158,6 +166,8 @@ public class MediaProjectionManagerServiceTest {
    @Mock
    private PackageManager mPackageManager;
    @Mock
    private KeyguardManager mKeyguardManager;
    @Mock
    private IMediaProjectionWatcherCallback mWatcherCallback;
    @Mock
    private MediaProjectionMetricsLogger mMediaProjectionMetricsLogger;
@@ -177,6 +187,7 @@ public class MediaProjectionManagerServiceTest {
        mContext = spy(new ContextWrapper(
                InstrumentationRegistry.getInstrumentation().getTargetContext()));
        doReturn(mPackageManager).when(mContext).getPackageManager();
        doReturn(mKeyguardManager).when(mContext).getSystemService(eq(Context.KEYGUARD_SERVICE));

        mClock = new OffsettableClock.Stopped();
        mWaitingDisplaySession.setWaitingForConsent(true);
@@ -246,6 +257,39 @@ public class MediaProjectionManagerServiceTest {
        assertThat(stoppedCallback2).isFalse();
    }

    @EnableFlags(android.companion.virtualdevice.flags
            .Flags.FLAG_MEDIA_PROJECTION_KEYGUARD_RESTRICTIONS)
    @Test
    public void testCreateProjection_keyguardLocked() throws Exception {
        MediaProjectionManagerService.MediaProjection projection = startProjectionPreconditions();

        doReturn(true).when(mKeyguardManager).isKeyguardLocked();
        doReturn(PackageManager.PERMISSION_DENIED).when(mPackageManager)
                .checkPermission(RECORD_SENSITIVE_CONTENT, projection.packageName);
        projection.start(mIMediaProjectionCallback);
        projection.notifyVirtualDisplayCreated(10);

        assertThat(mService.getActiveProjectionInfo()).isNull();
        assertThat(mIMediaProjectionCallback.mLatch.await(5, TimeUnit.SECONDS)).isTrue();
    }

    @EnableFlags(android.companion.virtualdevice.flags
            .Flags.FLAG_MEDIA_PROJECTION_KEYGUARD_RESTRICTIONS)
    @Test
    public void testCreateProjection_keyguardLocked_packageAllowlisted()
            throws NameNotFoundException {
        MediaProjectionManagerService.MediaProjection projection = startProjectionPreconditions();

        doReturn(true).when(mKeyguardManager).isKeyguardLocked();
        doReturn(PackageManager.PERMISSION_GRANTED).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.
        assertThat(mService.getActiveProjectionInfo()).isNotNull();
    }

    @Test
    public void testCreateProjection_attemptReuse_noPriorProjectionGrant()
            throws NameNotFoundException {
@@ -317,6 +361,48 @@ public class MediaProjectionManagerServiceTest {
        assertThat(secondProjection).isNotEqualTo(projection);
    }

    @EnableFlags(android.companion.virtualdevice.flags
            .Flags.FLAG_MEDIA_PROJECTION_KEYGUARD_RESTRICTIONS)
    @Test
    public void testKeyguardLocked_stopsActiveProjection() throws Exception {
        MediaProjectionManagerService service =
                new MediaProjectionManagerService(mContext, mMediaProjectionMetricsLoggerInjector);
        MediaProjectionManagerService.MediaProjection projection =
                startProjectionPreconditions(service);
        projection.start(mIMediaProjectionCallback);

        assertThat(service.getActiveProjectionInfo()).isNotNull();

        doReturn(PackageManager.PERMISSION_DENIED).when(mPackageManager)
                .checkPermission(RECORD_SENSITIVE_CONTENT, projection.packageName);
        service.onKeyguardLockedStateChanged(true);

        verify(mMediaProjectionMetricsLogger).logStopped(UID, TARGET_UID_UNKNOWN);
        assertThat(service.getActiveProjectionInfo()).isNull();
        assertThat(mIMediaProjectionCallback.mLatch.await(5, TimeUnit.SECONDS)).isTrue();
    }

    @EnableFlags(android.companion.virtualdevice.flags
            .Flags.FLAG_MEDIA_PROJECTION_KEYGUARD_RESTRICTIONS)
    @Test
    public void testKeyguardLocked_packageAllowlisted_doesNotStopActiveProjection()
            throws NameNotFoundException {
        MediaProjectionManagerService service =
                new MediaProjectionManagerService(mContext, mMediaProjectionMetricsLoggerInjector);
        MediaProjectionManagerService.MediaProjection projection =
                startProjectionPreconditions(service);
        projection.start(mIMediaProjectionCallback);

        assertThat(service.getActiveProjectionInfo()).isNotNull();

        doReturn(PackageManager.PERMISSION_GRANTED).when(mPackageManager).checkPermission(
                RECORD_SENSITIVE_CONTENT, projection.packageName);
        service.onKeyguardLockedStateChanged(true);

        verifyZeroInteractions(mMediaProjectionMetricsLogger);
        assertThat(service.getActiveProjectionInfo()).isNotNull();
    }

    @Test
    public void stop_noActiveProjections_doesNotLog() throws Exception {
        MediaProjectionManagerService service =