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

Commit 27863594 authored by Marvin Ramin's avatar Marvin Ramin
Browse files

Refactor MediaProjection stop-on-lock

Encapsulate all the MediaProjection stop behavior in a new stop
controller. This helps add more tests and keeping the callbacks more
isolated to help clean up MediaProjectionManagerService.

Bug: 377877538
Test: atest MediaProjectionStopControllerTest
Flag: EXEMPT refactor
Change-Id: I0667776e6fa58830dd6ef278c586c7f3b06dfe88
parent 7bb5abb4
Loading
Loading
Loading
Loading
+24 −97
Original line number Diff line number Diff line
@@ -17,7 +17,6 @@
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;
@@ -28,7 +27,6 @@ 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.provider.Settings.Global.DISABLE_SCREEN_SHARE_PROTECTIONS_FOR_APPS_AND_NOTIFICATIONS;
import static android.view.Display.DEFAULT_DISPLAY;
import static android.view.Display.INVALID_DISPLAY;

@@ -41,10 +39,7 @@ 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.app.role.RoleManager;
import android.companion.AssociationRequest;
import android.compat.annotation.ChangeId;
import android.compat.annotation.EnabledSince;
import android.content.ComponentName;
@@ -74,7 +69,6 @@ import android.os.PermissionEnforcer;
import android.os.RemoteException;
import android.os.SystemClock;
import android.os.UserHandle;
import android.provider.Settings;
import android.util.ArrayMap;
import android.util.Slog;
import android.view.ContentRecordingSession;
@@ -85,7 +79,6 @@ 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;
@@ -140,12 +133,12 @@ public final class MediaProjectionManagerService extends SystemService
    private final ActivityManagerInternal mActivityManagerInternal;
    private final PackageManager mPackageManager;
    private final WindowManagerInternal mWmInternal;
    private final KeyguardManager mKeyguardManager;
    private final RoleManager mRoleManager;


    private final MediaRouter mMediaRouter;
    private final MediaRouterCallback mMediaRouterCallback;
    private final MediaProjectionMetricsLogger mMediaProjectionMetricsLogger;
    private final MediaProjectionStopController mMediaProjectionStopController;
    private MediaRouter.RouteInfo mMediaRouteInfo;

    @GuardedBy("mLock")
@@ -175,72 +168,17 @@ 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);
        mRoleManager = mContext.getSystemService(RoleManager.class);
        mMediaProjectionStopController = new MediaProjectionStopController(context,
                this::maybeStopMediaProjection);
        mMediaProjectionStopController.startTrackingStopReasons(context);
        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 allowlisted packages, or
     *   - hold the OP_PROJECT_MEDIA AppOp.
     */
    @SuppressWarnings("BooleanMethodIsAlwaysInverted")
    private boolean canCaptureKeyguard() {
        if (!android.companion.virtualdevice.flags.Flags.mediaProjectionKeyguardRestrictions()) {
            return true;
        }
    private void maybeStopMediaProjection(int reason) {
        synchronized (mLock) {
            if (mProjectionGrant == null || mProjectionGrant.packageName == null) {
                return false;
            }
            boolean disableScreenShareProtections = Settings.Global.getInt(
                    getContext().getContentResolver(),
                    DISABLE_SCREEN_SHARE_PROTECTIONS_FOR_APPS_AND_NOTIFICATIONS, 0) != 0;
            if (disableScreenShareProtections) {
                Slog.v(TAG,
                        "Allowing keyguard capture as screenshare protections are disabled.");
                return true;
            }

            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,
                    mProjectionGrant.uid, mProjectionGrant.packageName, /* attributionTag= */ null,
                    "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()
                    .contains(mProjectionGrant.packageName);
        }
    }

    @VisibleForTesting
    void onKeyguardLockedStateChanged(boolean isKeyguardLocked) {
        if (!isKeyguardLocked) return;
        synchronized (mLock) {
            if (mProjectionGrant != null && !canCaptureKeyguard()
                    && mProjectionGrant.mVirtualDisplayId != INVALID_DISPLAY) {
                Slog.d(TAG, "Content Recording: Stopped MediaProjection"
                        + " due to keyguard lock");
            if (!mMediaProjectionStopController.isExemptFromStopping(mProjectionGrant)) {
                Slog.d(TAG, "Content Recording: Stopping MediaProjection due to "
                        + MediaProjectionStopController.stopReasonToString(reason));
                mProjectionGrant.stop();
            }
        }
@@ -736,20 +674,6 @@ 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) {
@@ -957,18 +881,19 @@ public final class MediaProjectionManagerService extends SystemService
        public void requestConsentForInvalidProjection(@NonNull IMediaProjection projection) {
            requestConsentForInvalidProjection_enforcePermission();

            if (android.companion.virtualdevice.flags.Flags.mediaProjectionKeyguardRestrictions()
                    && mKeyguardManager.isKeyguardLocked()) {
                Slog.v(TAG, "Reusing token: Won't request consent while the keyguard is locked");
                return;
            }

            synchronized (mLock) {
                if (!isCurrentProjection(projection)) {
                    Slog.v(TAG, "Reusing token: Won't request consent again for a token that "
                            + "isn't current");
                    return;
                }

                if (mMediaProjectionStopController.isStartForbidden(mProjectionGrant)) {
                    Slog.v(TAG,
                            "Reusing token: Won't request consent while MediaProjection is "
                                    + "restricted");
                    return;
                }
            }

            // Remove calling app identity before performing any privileged operations.
@@ -1076,7 +1001,6 @@ public final class MediaProjectionManagerService extends SystemService
        }
    }

    @VisibleForTesting
    final class MediaProjection extends IMediaProjection.Stub {
        // Host app has 5 minutes to begin using the token before it is invalid.
        // Some apps show a dialog for the user to interact with (selecting recording resolution)
@@ -1381,12 +1305,15 @@ 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");
            synchronized (mLock) {
                if (mMediaProjectionStopController.isStartForbidden(mProjectionGrant)) {
                    Slog.w(TAG,
                            "Content Recording: MediaProjection start disallowed, aborting "
                                    + "MediaProjection");
                    stop();
                    return;
                }
            synchronized (mLock) {

                mVirtualDisplayId = displayId;

                // If prior session was does not have a valid display id, then update the display
+179 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2024 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 com.android.server.media.projection;

import static android.Manifest.permission.RECORD_SENSITIVE_CONTENT;
import static android.provider.Settings.Global.DISABLE_SCREEN_SHARE_PROTECTIONS_FOR_APPS_AND_NOTIFICATIONS;

import android.app.AppOpsManager;
import android.app.KeyguardManager;
import android.app.role.RoleManager;
import android.companion.AssociationRequest;
import android.content.ContentResolver;
import android.content.Context;
import android.content.pm.PackageManager;
import android.os.Binder;
import android.provider.Settings;
import android.util.Slog;
import android.view.Display;

import com.android.internal.annotations.VisibleForTesting;
import com.android.server.SystemConfig;

import java.util.function.Consumer;

/**
 * Tracks events that should cause MediaProjection to stop
 */
public class MediaProjectionStopController {

    private static final String TAG = "MediaProjectionStopController";
    @VisibleForTesting
    static final int STOP_REASON_KEYGUARD = 1;

    private final Consumer<Integer> mStopReasonConsumer;
    private final KeyguardManager mKeyguardManager;
    private final AppOpsManager mAppOpsManager;
    private final PackageManager mPackageManager;
    private final RoleManager mRoleManager;
    private final ContentResolver mContentResolver;

    public MediaProjectionStopController(Context context, Consumer<Integer> stopReasonConsumer) {
        mStopReasonConsumer = stopReasonConsumer;
        mKeyguardManager = context.getSystemService(KeyguardManager.class);
        mAppOpsManager = context.getSystemService(AppOpsManager.class);
        mPackageManager = context.getPackageManager();
        mRoleManager = context.getSystemService(RoleManager.class);
        mContentResolver = context.getContentResolver();
    }

    /**
     * Start tracking stop reasons that may interrupt a MediaProjection session.
     */
    public void startTrackingStopReasons(Context context) {
        final long token = Binder.clearCallingIdentity();
        try {
            mKeyguardManager.addKeyguardLockedStateListener(context.getMainExecutor(),
                    this::onKeyguardLockedStateChanged);
        } finally {
            Binder.restoreCallingIdentity(token);
        }
    }

    /**
     * Checks whether the given projection grant is exempt from stopping restrictions.
     */
    public boolean isExemptFromStopping(
            MediaProjectionManagerService.MediaProjection projectionGrant) {
        return isExempt(projectionGrant, false);
    }

    /**
     * Apps may disregard recording restrictions via MediaProjection for any stop reason if:
     * - the "Disable Screenshare protections" developer option is enabled
     * - the app is a holder of RECORD_SENSITIVE_CONTENT permission
     * - the app holds the OP_PROJECT_MEDIA AppOp
     * - the app holds the COMPANION_DEVICE_APP_STREAMING role
     * - the app is one of the bugreport allowlisted packages
     * - the current projection does not have an active VirtualDisplay associated with the
     * MediaProjection session
     */
    private boolean isExempt(
            MediaProjectionManagerService.MediaProjection projectionGrant, boolean forStart) {
        if (projectionGrant == null || projectionGrant.packageName == null) {
            return true;
        }
        boolean disableScreenShareProtections = Settings.Global.getInt(mContentResolver,
                DISABLE_SCREEN_SHARE_PROTECTIONS_FOR_APPS_AND_NOTIFICATIONS, 0) != 0;
        if (disableScreenShareProtections) {
            Slog.v(TAG, "Continuing MediaProjection as screenshare protections are disabled.");
            return true;
        }

        if (mPackageManager.checkPermission(RECORD_SENSITIVE_CONTENT, projectionGrant.packageName)
                == PackageManager.PERMISSION_GRANTED) {
            Slog.v(TAG,
                    "Continuing MediaProjection for package with RECORD_SENSITIVE_CONTENT "
                            + "permission");
            return true;
        }
        if (AppOpsManager.MODE_ALLOWED == mAppOpsManager.noteOpNoThrow(
                AppOpsManager.OP_PROJECT_MEDIA, projectionGrant.uid,
                projectionGrant.packageName, /* attributionTag= */ null, "recording lockscreen")) {
            // Some tools use media projection by granting the OP_PROJECT_MEDIA app
            // op via a shell command.
            Slog.v(TAG, "Continuing MediaProjection for package with OP_PROJECT_MEDIA AppOp ");
            return true;
        }
        if (mRoleManager.getRoleHoldersAsUser(AssociationRequest.DEVICE_PROFILE_APP_STREAMING,
                projectionGrant.userHandle).contains(projectionGrant.packageName)) {
            Slog.v(TAG, "Continuing MediaProjection for package holding app streaming role.");
            return true;
        }
        if (SystemConfig.getInstance().getBugreportWhitelistedPackages().contains(
                projectionGrant.packageName)) {
            Slog.v(TAG, "Continuing MediaProjection for package allowlisted for bugreporting.");
            return true;
        }
        if (!forStart && projectionGrant.getVirtualDisplayId() == Display.INVALID_DISPLAY) {
            Slog.v(TAG, "Continuing MediaProjection as current projection has no VirtualDisplay.");
            return true;
        }

        return false;
    }

    /**
     * @return {@code true} if a MediaProjection session is currently in a restricted state.
     */
    public boolean isStartForbidden(
            MediaProjectionManagerService.MediaProjection projectionGrant) {
        if (!android.companion.virtualdevice.flags.Flags.mediaProjectionKeyguardRestrictions()) {
            return false;
        }

        if (!mKeyguardManager.isKeyguardLocked()) {
            return false;
        }

        if (isExempt(projectionGrant, true)) {
            return false;
        }
        return true;
    }

    @VisibleForTesting
    void onKeyguardLockedStateChanged(boolean isKeyguardLocked) {
        if (!isKeyguardLocked) return;
        if (!android.companion.virtualdevice.flags.Flags.mediaProjectionKeyguardRestrictions()) {
            return;
        }
        mStopReasonConsumer.accept(STOP_REASON_KEYGUARD);
    }

    /**
     * @return a String representation of the stop reason interrupting MediaProjection.
     */
    public static String stopReasonToString(int stopReason) {
        switch (stopReason) {
            case STOP_REASON_KEYGUARD -> {
                return "STOP_REASON_KEYGUARD";
            }
        }
        return "";
    }
}
+2 −44
Original line number Diff line number Diff line
@@ -532,6 +532,8 @@ public class MediaProjectionManagerServiceTest {
        MediaProjectionManagerService.MediaProjection projection = startProjectionPreconditions();
        projection.start(mIMediaProjectionCallback);

        doReturn(PackageManager.PERMISSION_DENIED).when(mPackageManager).checkPermission(
                RECORD_SENSITIVE_CONTENT, projection.packageName);
        doReturn(true).when(mKeyguardManager).isKeyguardLocked();
        MediaProjectionManagerService.BinderService mediaProjectionBinderService =
                mService.new BinderService(mContext);
@@ -540,50 +542,6 @@ public class MediaProjectionManagerServiceTest {
        verify(mContext, never()).startActivityAsUser(any(), any());
    }

    @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);
        projection.notifyVirtualDisplayCreated(10);

        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);
        projection.notifyVirtualDisplayCreated(10);

        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 =
+364 −0

File added.

Preview size limit exceeded, changes collapsed.