diff options
10 files changed, 222 insertions, 43 deletions
diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/system/QuickStepContract.java b/packages/SystemUI/shared/src/com/android/systemui/shared/system/QuickStepContract.java index 78cd02fed9eb..ab611901328d 100644 --- a/packages/SystemUI/shared/src/com/android/systemui/shared/system/QuickStepContract.java +++ b/packages/SystemUI/shared/src/com/android/systemui/shared/system/QuickStepContract.java @@ -347,7 +347,12 @@ public class QuickStepContract { } // Disable back gesture on the hub, but not when the shade is showing. if ((sysuiStateFlags & SYSUI_STATE_COMMUNAL_HUB_SHOWING) != 0) { - return (sysuiStateFlags & SYSUI_STATE_NOTIFICATION_PANEL_VISIBLE) == 0; + // Use QS expanded signal as the notification panel is always considered visible + // expanded when on the lock screen and when opening hub over lock screen. This does + // mean that back gesture is disabled when opening shade over hub while in portrait + // mode, since QS is not expanded. + // TODO(b/370108274): allow back gesture on shade over hub in portrait + return (sysuiStateFlags & SYSUI_STATE_QUICK_SETTINGS_EXPANDED) == 0; } if ((sysuiStateFlags & SYSUI_STATE_ALLOW_GESTURE_IGNORING_BAR_VISIBILITY) != 0) { sysuiStateFlags &= ~SYSUI_STATE_NAV_BAR_HIDDEN; diff --git a/packages/SystemUI/tests/src/com/android/systemui/shared/system/QuickStepContractTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shared/system/QuickStepContractTest.kt index 6254fb128859..ef03fab95778 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/shared/system/QuickStepContractTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/shared/system/QuickStepContractTest.kt @@ -21,7 +21,7 @@ import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_BOUNCER_SHOWING import com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_COMMUNAL_HUB_SHOWING -import com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_NOTIFICATION_PANEL_VISIBLE +import com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_QUICK_SETTINGS_EXPANDED import com.google.common.truth.Truth.assertThat import kotlin.test.Test import org.junit.runner.RunWith @@ -41,9 +41,9 @@ class QuickStepContractTest : SysuiTestCase() { } @Test - fun isBackGestureDisabled_hubAndShadeShowing() { + fun isBackGestureDisabled_hubAndQSExpanded() { val sysuiStateFlags = - SYSUI_STATE_COMMUNAL_HUB_SHOWING and SYSUI_STATE_NOTIFICATION_PANEL_VISIBLE + SYSUI_STATE_COMMUNAL_HUB_SHOWING and SYSUI_STATE_QUICK_SETTINGS_EXPANDED // Gestures are enabled because the shade shows over the hub. assertThat( diff --git a/services/core/java/com/android/server/biometrics/BiometricService.java b/services/core/java/com/android/server/biometrics/BiometricService.java index feef5409d14f..4c917894100a 100644 --- a/services/core/java/com/android/server/biometrics/BiometricService.java +++ b/services/core/java/com/android/server/biometrics/BiometricService.java @@ -725,7 +725,7 @@ public class BiometricService extends SystemService { return -1; } - if (!Utils.isValidAuthenticatorConfig(promptInfo)) { + if (!Utils.isValidAuthenticatorConfig(getContext(), promptInfo)) { throw new SecurityException("Invalid authenticator configuration"); } @@ -763,7 +763,7 @@ public class BiometricService extends SystemService { + ", Caller=" + callingUserId + ", Authenticators=" + authenticators); - if (!Utils.isValidAuthenticatorConfig(authenticators)) { + if (!Utils.isValidAuthenticatorConfig(getContext(), authenticators)) { throw new SecurityException("Invalid authenticator configuration"); } @@ -1038,7 +1038,7 @@ public class BiometricService extends SystemService { + ", Caller=" + callingUserId + ", Authenticators=" + authenticators); - if (!Utils.isValidAuthenticatorConfig(authenticators)) { + if (!Utils.isValidAuthenticatorConfig(getContext(), authenticators)) { throw new SecurityException("Invalid authenticator configuration"); } @@ -1060,7 +1060,7 @@ public class BiometricService extends SystemService { Slog.d(TAG, "getSupportedModalities: Authenticators=" + authenticators); - if (!Utils.isValidAuthenticatorConfig(authenticators)) { + if (!Utils.isValidAuthenticatorConfig(getContext(), authenticators)) { throw new SecurityException("Invalid authenticator configuration"); } diff --git a/services/core/java/com/android/server/biometrics/Utils.java b/services/core/java/com/android/server/biometrics/Utils.java index 407ef1e41aa6..87341365355b 100644 --- a/services/core/java/com/android/server/biometrics/Utils.java +++ b/services/core/java/com/android/server/biometrics/Utils.java @@ -16,6 +16,7 @@ package com.android.server.biometrics; +import static android.Manifest.permission.SET_BIOMETRIC_DIALOG_ADVANCED; import static android.Manifest.permission.USE_BIOMETRIC_INTERNAL; import static android.app.ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND_SERVICE; import static android.hardware.biometrics.BiometricManager.Authenticators; @@ -233,17 +234,18 @@ public class Utils { * @param promptInfo * @return */ - static boolean isValidAuthenticatorConfig(PromptInfo promptInfo) { + static boolean isValidAuthenticatorConfig(Context context, PromptInfo promptInfo) { final int authenticators = promptInfo.getAuthenticators(); - return isValidAuthenticatorConfig(authenticators); + return isValidAuthenticatorConfig(context, authenticators); } /** - * Checks if the authenticator configuration is a valid combination of the public APIs - * @param authenticators - * @return + * Checks if the authenticator configuration is a valid combination of the public APIs. + * + * throws {@link SecurityException} if the caller requests for mandatory biometrics without + * {@link SET_BIOMETRIC_DIALOG_ADVANCED} permission */ - static boolean isValidAuthenticatorConfig(int authenticators) { + static boolean isValidAuthenticatorConfig(Context context, int authenticators) { // The caller is not required to set the authenticators. But if they do, check the below. if (authenticators == 0) { return true; @@ -251,9 +253,15 @@ public class Utils { // Check if any of the non-biometric and non-credential bits are set. If so, this is // invalid. - final int testBits = ~(Authenticators.DEVICE_CREDENTIAL - | Authenticators.BIOMETRIC_MIN_STRENGTH - | Authenticators.MANDATORY_BIOMETRICS); + final int testBits; + if (Flags.mandatoryBiometrics()) { + testBits = ~(Authenticators.DEVICE_CREDENTIAL + | Authenticators.BIOMETRIC_MIN_STRENGTH + | Authenticators.MANDATORY_BIOMETRICS); + } else { + testBits = ~(Authenticators.DEVICE_CREDENTIAL + | Authenticators.BIOMETRIC_MIN_STRENGTH); + } if ((authenticators & testBits) != 0) { Slog.e(BiometricService.TAG, "Non-biometric, non-credential bits found." + " Authenticators: " + authenticators); @@ -271,6 +279,9 @@ public class Utils { } else if (biometricBits == Authenticators.BIOMETRIC_WEAK) { return true; } else if (isMandatoryBiometricsRequested(authenticators)) { + //TODO(b/347123256): Update CTS test + context.enforceCallingOrSelfPermission(SET_BIOMETRIC_DIALOG_ADVANCED, + "Must have SET_BIOMETRIC_DIALOG_ADVANCED permission"); return true; } 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 48d24f2e14dd..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,14 +198,23 @@ 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; } - boolean operationActive = mAppOps.isOperationActive(AppOpsManager.OP_PROJECT_MEDIA, - mProjectionGrant.uid, - mProjectionGrant.packageName); - if (operationActive) { + 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() @@ -699,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/core/java/com/android/server/wm/Dimmer.java b/services/core/java/com/android/server/wm/Dimmer.java index c5643ea54623..4940d2ec9c3a 100644 --- a/services/core/java/com/android/server/wm/Dimmer.java +++ b/services/core/java/com/android/server/wm/Dimmer.java @@ -21,6 +21,7 @@ import static com.android.server.wm.WindowManagerDebugConfig.TAG_WITH_CLASS_NAME import static com.android.server.wm.WindowManagerDebugConfig.TAG_WM; import android.annotation.NonNull; +import android.annotation.Nullable; import android.graphics.Rect; import android.util.Log; import android.view.Surface; @@ -128,7 +129,7 @@ class Dimmer { /** * Set the parameters to prepare the dim to be relative parented to the dimming container */ - void prepareReparent(@NonNull WindowContainer<?> geometryParent, + void prepareReparent(@Nullable WindowContainer<?> geometryParent, @NonNull WindowState relativeParent) { mAnimationHelper.setRequestedRelativeParent(relativeParent); mAnimationHelper.setRequestedGeometryParent(geometryParent); @@ -221,7 +222,7 @@ class Dimmer { * @param dimmingContainer The container that is dimming. The dim layer will be rel-z * parented below it */ - public void adjustPosition(@NonNull WindowContainer<?> geometryParent, + public void adjustPosition(@Nullable WindowContainer<?> geometryParent, @NonNull WindowState dimmingContainer) { if (mDimState != null) { mDimState.prepareReparent(geometryParent, dimmingContainer); diff --git a/services/core/java/com/android/server/wm/DimmerAnimationHelper.java b/services/core/java/com/android/server/wm/DimmerAnimationHelper.java index bc188959164d..478bdfeffe79 100644 --- a/services/core/java/com/android/server/wm/DimmerAnimationHelper.java +++ b/services/core/java/com/android/server/wm/DimmerAnimationHelper.java @@ -108,7 +108,7 @@ public class DimmerAnimationHelper { } // Sets the requested layer to reparent the dim to without applying it immediately - void setRequestedGeometryParent(WindowContainer<?> geometryParent) { + void setRequestedGeometryParent(@Nullable WindowContainer<?> geometryParent) { if (geometryParent != null) { mRequestedProperties.mGeometryParent = geometryParent; } diff --git a/services/core/java/com/android/server/wm/WindowState.java b/services/core/java/com/android/server/wm/WindowState.java index 0e3ab63aefb9..9ecaca10b159 100644 --- a/services/core/java/com/android/server/wm/WindowState.java +++ b/services/core/java/com/android/server/wm/WindowState.java @@ -5210,7 +5210,7 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP // but not window manager visible (!isVisibleNow()), it can still be the parent of the // dim, but can not create a new surface or continue a dim alone. Dimmer dimmer; - WindowContainer<?> geometryParent = task; + WindowContainer<?> geometryParent = null; if (Flags.useTasksDimOnly()) { geometryParent = getDimParent(); dimmer = getDimController(); diff --git a/services/tests/servicestests/src/com/android/server/biometrics/UtilsTest.java b/services/tests/servicestests/src/com/android/server/biometrics/UtilsTest.java index 14cb22d7698e..1bea371c5786 100644 --- a/services/tests/servicestests/src/com/android/server/biometrics/UtilsTest.java +++ b/services/tests/servicestests/src/com/android/server/biometrics/UtilsTest.java @@ -16,12 +16,20 @@ package com.android.server.biometrics; +import static android.Manifest.permission.SET_BIOMETRIC_DIALOG_ADVANCED; import static android.hardware.biometrics.BiometricManager.Authenticators; import static junit.framework.Assert.assertEquals; import static junit.framework.Assert.assertFalse; import static junit.framework.Assert.assertTrue; +import static org.junit.Assert.assertThrows; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.doThrow; + +import android.content.Context; import android.hardware.biometrics.BiometricAuthenticator; import android.hardware.biometrics.BiometricConstants; import android.hardware.biometrics.BiometricManager; @@ -36,8 +44,12 @@ import android.platform.test.flag.junit.DeviceFlagsValueProvider; import androidx.test.filters.SmallTest; +import org.junit.Before; import org.junit.Rule; import org.junit.Test; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; @Presubmit @SmallTest @@ -45,6 +57,17 @@ public class UtilsTest { @Rule public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule(); + @Rule + public MockitoRule mockitorule = MockitoJUnit.rule(); + + @Mock + private Context mContext; + + @Before + public void setUp() { + doThrow(SecurityException.class).when(mContext).enforceCallingOrSelfPermission( + eq(SET_BIOMETRIC_DIALOG_ADVANCED), any()); + } @Test public void testCombineAuthenticatorBundles_withKeyDeviceCredential_andKeyAuthenticators() { @@ -162,28 +185,45 @@ public class UtilsTest { @Test public void testIsValidAuthenticatorConfig() { - assertTrue(Utils.isValidAuthenticatorConfig(Authenticators.EMPTY_SET)); + assertTrue(Utils.isValidAuthenticatorConfig(mContext, Authenticators.EMPTY_SET)); - assertTrue(Utils.isValidAuthenticatorConfig(Authenticators.BIOMETRIC_STRONG)); + assertTrue(Utils.isValidAuthenticatorConfig(mContext, Authenticators.BIOMETRIC_STRONG)); - assertTrue(Utils.isValidAuthenticatorConfig(Authenticators.BIOMETRIC_WEAK)); + assertTrue(Utils.isValidAuthenticatorConfig(mContext, Authenticators.BIOMETRIC_WEAK)); - assertTrue(Utils.isValidAuthenticatorConfig(Authenticators.DEVICE_CREDENTIAL)); + assertTrue(Utils.isValidAuthenticatorConfig(mContext, Authenticators.DEVICE_CREDENTIAL)); - assertTrue(Utils.isValidAuthenticatorConfig(Authenticators.DEVICE_CREDENTIAL + assertTrue(Utils.isValidAuthenticatorConfig(mContext, Authenticators.DEVICE_CREDENTIAL | Authenticators.BIOMETRIC_STRONG)); - assertTrue(Utils.isValidAuthenticatorConfig(Authenticators.DEVICE_CREDENTIAL + assertTrue(Utils.isValidAuthenticatorConfig(mContext, Authenticators.DEVICE_CREDENTIAL | Authenticators.BIOMETRIC_WEAK)); - assertFalse(Utils.isValidAuthenticatorConfig(Authenticators.BIOMETRIC_CONVENIENCE)); + assertFalse(Utils.isValidAuthenticatorConfig( + mContext, Authenticators.BIOMETRIC_CONVENIENCE)); - assertFalse(Utils.isValidAuthenticatorConfig(Authenticators.BIOMETRIC_CONVENIENCE + assertFalse(Utils.isValidAuthenticatorConfig(mContext, Authenticators.BIOMETRIC_CONVENIENCE | Authenticators.DEVICE_CREDENTIAL)); - assertFalse(Utils.isValidAuthenticatorConfig(Authenticators.BIOMETRIC_MAX_STRENGTH)); + assertFalse(Utils.isValidAuthenticatorConfig( + mContext, Authenticators.BIOMETRIC_MAX_STRENGTH)); - assertFalse(Utils.isValidAuthenticatorConfig(Authenticators.BIOMETRIC_MIN_STRENGTH)); + assertFalse(Utils.isValidAuthenticatorConfig( + mContext, Authenticators.BIOMETRIC_MIN_STRENGTH)); + + assertThrows(SecurityException.class, () -> Utils.isValidAuthenticatorConfig( + mContext, Authenticators.MANDATORY_BIOMETRICS)); + + doNothing().when(mContext).enforceCallingOrSelfPermission( + eq(SET_BIOMETRIC_DIALOG_ADVANCED), any()); + + if (Flags.mandatoryBiometrics()) { + assertTrue(Utils.isValidAuthenticatorConfig(mContext, + Authenticators.MANDATORY_BIOMETRICS)); + } else { + assertFalse(Utils.isValidAuthenticatorConfig(mContext, + Authenticators.MANDATORY_BIOMETRICS)); + } // The rest of the bits are not allowed to integrate with the public APIs for (int i = 8; i < 32; i++) { @@ -192,7 +232,7 @@ public class UtilsTest { || authenticator == Authenticators.MANDATORY_BIOMETRICS) { continue; } - assertFalse(Utils.isValidAuthenticatorConfig(1 << i)); + assertFalse(Utils.isValidAuthenticatorConfig(mContext, 1 << i)); } } 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 abc9ce3fdc36..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,11 +33,13 @@ 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; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.nullable; import static org.mockito.Mockito.atLeastOnce; import static org.mockito.Mockito.doNothing; import static org.mockito.Mockito.doReturn; @@ -50,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; @@ -67,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; @@ -87,15 +94,18 @@ 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; import org.junit.runner.RunWith; +import org.mockito.Answers; import org.mockito.ArgumentCaptor; 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; @@ -174,8 +184,8 @@ public class MediaProjectionManagerServiceTest { private PackageManager mPackageManager; @Mock private KeyguardManager mKeyguardManager; - @Mock - AppOpsManager mAppOpsManager; + + private AppOpsManager mAppOpsManager; @Mock private IMediaProjectionWatcherCallback mWatcherCallback; @Mock @@ -193,6 +203,7 @@ public class MediaProjectionManagerServiceTest { LocalServices.removeServiceForTest(WindowManagerInternal.class); LocalServices.addService(WindowManagerInternal.class, mWindowManagerInternal); + mAppOpsManager = mockAppOpsManager(); mContext.addMockSystemService(AppOpsManager.class, mAppOpsManager); mContext.addMockSystemService(KeyguardManager.class, mKeyguardManager); mContext.setMockPackageManager(mPackageManager); @@ -206,6 +217,17 @@ public class MediaProjectionManagerServiceTest { mService = new MediaProjectionManagerService(mContext); } + private static AppOpsManager mockAppOpsManager() { + return mock(AppOpsManager.class, invocationOnMock -> { + if (invocationOnMock.getMethod().getName().startsWith("noteOp")) { + // Mockito will return 0 for non-stubbed method which corresponds to MODE_ALLOWED + // and is not what we want. + return AppOpsManager.MODE_IGNORED; + } + return Answers.RETURNS_DEFAULTS.answer(invocationOnMock); + }); + } + @After public void tearDown() { LocalServices.removeServiceForTest(ActivityManagerInternal.class); @@ -298,15 +320,16 @@ public class MediaProjectionManagerServiceTest { assertThat(mService.getActiveProjectionInfo()).isNotNull(); } - @SuppressLint("MissingPermission") @EnableFlags(android.companion.virtualdevice.flags .Flags.FLAG_MEDIA_PROJECTION_KEYGUARD_RESTRICTIONS) @Test public void testCreateProjection_keyguardLocked_AppOpMediaProjection() throws NameNotFoundException { MediaProjectionManagerService.MediaProjection projection = startProjectionPreconditions(); - doReturn(true).when(mAppOpsManager).isOperationActive(eq(AppOpsManager.OP_PROJECT_MEDIA), - eq(projection.uid), eq(projection.packageName)); + doReturn(AppOpsManager.MODE_ALLOWED).when(mAppOpsManager) + .noteOpNoThrow(eq(AppOpsManager.OP_PROJECT_MEDIA), + eq(projection.uid), eq(projection.packageName), nullable(String.class), + nullable(String.class)); doReturn(true).when(mKeyguardManager).isKeyguardLocked(); doReturn(PackageManager.PERMISSION_DENIED).when(mPackageManager).checkPermission( @@ -319,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 { @@ -1159,7 +1212,7 @@ public class MediaProjectionManagerServiceTest { doReturn(mAppInfo).when(mPackageManager).getApplicationInfoAsUser(anyString(), any(ApplicationInfoFlags.class), any(UserHandle.class)); return service.createProjectionInternal(UID, PACKAGE_NAME, - TYPE_MIRRORING, /* isPermanentGrant= */ true, UserHandle.CURRENT); + TYPE_MIRRORING, /* isPermanentGrant= */ false, UserHandle.CURRENT); } // Set up preconditions for starting a projection, with no foreground service requirements. @@ -1186,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 |