Add adaptive authentication service

Adaptive auth aims to provide more secure and better authentication
experience using various signals. In its current version, it detects
repeated failed auth attempts using either primary auth methods (i.e.
PIN/pattern/password) or biometrics, and then decides whether to lock
out the device.

Flag: ACONFIG android.adaptiveauth.enable_adaptive_auth DEVELOPMENT
Bug: 285053096
Test: atest AdaptiveAuthServiceTest
Change-Id: If15115e469f60d20f590c6fb0fde62a75c771b8e
diff --git a/core/java/com/android/internal/widget/LockPatternUtils.java b/core/java/com/android/internal/widget/LockPatternUtils.java
index b5b3a48..e46b8d7 100644
--- a/core/java/com/android/internal/widget/LockPatternUtils.java
+++ b/core/java/com/android/internal/widget/LockPatternUtils.java
@@ -1615,7 +1615,8 @@
                         STRONG_AUTH_REQUIRED_AFTER_TIMEOUT,
                         STRONG_AUTH_REQUIRED_AFTER_USER_LOCKDOWN,
                         STRONG_AUTH_REQUIRED_AFTER_NON_STRONG_BIOMETRICS_TIMEOUT,
-                        SOME_AUTH_REQUIRED_AFTER_TRUSTAGENT_EXPIRED})
+                        SOME_AUTH_REQUIRED_AFTER_TRUSTAGENT_EXPIRED,
+                        SOME_AUTH_REQUIRED_AFTER_ADAPTIVE_AUTH_REQUEST})
         @Retention(RetentionPolicy.SOURCE)
         public @interface StrongAuthFlags {}
 
@@ -1641,7 +1642,8 @@
 
         /**
          * Strong authentication is required because the user has been locked out after too many
-         * attempts.
+         * attempts using primary auth methods (i.e. PIN/pattern/password) from the lock screen,
+         * Android Settings, and BiometricPrompt where user authentication is required.
          */
         public static final int STRONG_AUTH_REQUIRED_AFTER_LOCKOUT = 0x8;
 
@@ -1674,12 +1676,23 @@
         public static final int SOME_AUTH_REQUIRED_AFTER_TRUSTAGENT_EXPIRED = 0x100;
 
         /**
+         * Some authentication is required because adaptive auth has requested to lock device due to
+         * repeated failed primary auth (i.e. PIN/pattern/password) or biometric auth attempts which
+         * can come from Android Settings or BiometricPrompt where user authentication is required,
+         * in addition to from the lock screen. When a risk is determined, adaptive auth will
+         * proactively prompt the lock screen and will require users to re-enter the device with
+         * either primary auth or biometric auth (if not prohibited by other flags).
+         */
+        public static final int SOME_AUTH_REQUIRED_AFTER_ADAPTIVE_AUTH_REQUEST = 0x200;
+
+        /**
          * Strong auth flags that do not prevent biometric methods from being accepted as auth.
          * If any other flags are set, biometric authentication is disabled.
          */
         private static final int ALLOWING_BIOMETRIC = STRONG_AUTH_NOT_REQUIRED
                 | SOME_AUTH_REQUIRED_AFTER_USER_REQUEST
-                | SOME_AUTH_REQUIRED_AFTER_TRUSTAGENT_EXPIRED;
+                | SOME_AUTH_REQUIRED_AFTER_TRUSTAGENT_EXPIRED
+                | SOME_AUTH_REQUIRED_AFTER_ADAPTIVE_AUTH_REQUEST;
 
         private final SparseIntArray mStrongAuthRequiredForUser = new SparseIntArray();
         private final H mHandler;
diff --git a/services/core/java/com/android/server/adaptiveauth/AdaptiveAuthService.java b/services/core/java/com/android/server/adaptiveauth/AdaptiveAuthService.java
new file mode 100644
index 0000000..3312be2
--- /dev/null
+++ b/services/core/java/com/android/server/adaptiveauth/AdaptiveAuthService.java
@@ -0,0 +1,238 @@
+/*
+ * 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.adaptiveauth;
+
+import static com.android.internal.widget.LockPatternUtils.StrongAuthTracker.SOME_AUTH_REQUIRED_AFTER_ADAPTIVE_AUTH_REQUEST;
+
+import android.app.KeyguardManager;
+import android.content.Context;
+import android.hardware.biometrics.AuthenticationStateListener;
+import android.hardware.biometrics.BiometricManager;
+import android.hardware.biometrics.BiometricSourceType;
+import android.os.Build;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.Message;
+import android.os.PowerManager;
+import android.os.SystemClock;
+import android.util.Log;
+import android.util.Slog;
+import android.util.SparseIntArray;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.widget.LockPatternUtils;
+import com.android.internal.widget.LockSettingsInternal;
+import com.android.internal.widget.LockSettingsStateListener;
+import com.android.server.LocalServices;
+import com.android.server.SystemService;
+import com.android.server.pm.UserManagerInternal;
+import com.android.server.wm.WindowManagerInternal;
+
+import java.util.Objects;
+
+/**
+ * @hide
+ */
+public class AdaptiveAuthService extends SystemService {
+    private static final String TAG = "AdaptiveAuthService";
+    private static final boolean DEBUG = Build.IS_DEBUGGABLE && Log.isLoggable(TAG, Log.DEBUG);
+
+    @VisibleForTesting
+    static final int MAX_ALLOWED_FAILED_AUTH_ATTEMPTS = 5;
+    private static final int MSG_REPORT_PRIMARY_AUTH_ATTEMPT = 1;
+    private static final int MSG_REPORT_BIOMETRIC_AUTH_ATTEMPT = 2;
+    private static final int AUTH_SUCCESS = 1;
+    private static final int AUTH_FAILURE = 0;
+
+    private final LockPatternUtils mLockPatternUtils;
+    private final LockSettingsInternal mLockSettings;
+    private final BiometricManager mBiometricManager;
+    private final KeyguardManager mKeyguardManager;
+    private final PowerManager mPowerManager;
+    private final WindowManagerInternal mWindowManager;
+    private final UserManagerInternal mUserManager;
+    @VisibleForTesting
+    final SparseIntArray mFailedAttemptsForUser = new SparseIntArray();
+
+    public AdaptiveAuthService(Context context) {
+        this(context, new LockPatternUtils(context));
+    }
+
+    @VisibleForTesting
+    public AdaptiveAuthService(Context context, LockPatternUtils lockPatternUtils) {
+        super(context);
+        mLockPatternUtils = lockPatternUtils;
+        mLockSettings = Objects.requireNonNull(
+                LocalServices.getService(LockSettingsInternal.class));
+        mBiometricManager = Objects.requireNonNull(
+                context.getSystemService(BiometricManager.class));
+        mKeyguardManager = Objects.requireNonNull(context.getSystemService(KeyguardManager.class));
+        mPowerManager = Objects.requireNonNull(context.getSystemService(PowerManager.class));
+        mWindowManager = Objects.requireNonNull(
+                LocalServices.getService(WindowManagerInternal.class));
+        mUserManager = Objects.requireNonNull(LocalServices.getService(UserManagerInternal.class));
+    }
+
+    @Override
+    public void onStart() {}
+
+    @Override
+    public void onBootPhase(int phase) {
+        if (phase == SystemService.PHASE_SYSTEM_SERVICES_READY) {
+            init();
+        }
+    }
+
+    @VisibleForTesting
+    void init() {
+        mLockSettings.registerLockSettingsStateListener(mLockSettingsStateListener);
+        mBiometricManager.registerAuthenticationStateListener(mAuthenticationStateListener);
+    }
+
+    private final LockSettingsStateListener mLockSettingsStateListener =
+            new LockSettingsStateListener() {
+                @Override
+                public void onAuthenticationSucceeded(int userId) {
+                    if (DEBUG) {
+                        Slog.d(TAG, "LockSettingsStateListener#onAuthenticationSucceeded");
+                    }
+                    mHandler.obtainMessage(MSG_REPORT_PRIMARY_AUTH_ATTEMPT, AUTH_SUCCESS, userId)
+                            .sendToTarget();
+                }
+
+                @Override
+                public void onAuthenticationFailed(int userId) {
+                    Slog.i(TAG, "LockSettingsStateListener#onAuthenticationFailed");
+                    mHandler.obtainMessage(MSG_REPORT_PRIMARY_AUTH_ATTEMPT, AUTH_FAILURE, userId)
+                            .sendToTarget();
+                }
+            };
+
+    private final AuthenticationStateListener mAuthenticationStateListener =
+            new AuthenticationStateListener.Stub() {
+                @Override
+                public void onAuthenticationStarted(int requestReason) {}
+
+                @Override
+                public void onAuthenticationStopped() {}
+
+                @Override
+                public void onAuthenticationSucceeded(int requestReason, int userId) {
+                    if (DEBUG) {
+                        Slog.d(TAG, "AuthenticationStateListener#onAuthenticationSucceeded");
+                    }
+                    mHandler.obtainMessage(MSG_REPORT_BIOMETRIC_AUTH_ATTEMPT, AUTH_SUCCESS, userId)
+                            .sendToTarget();
+                }
+
+                @Override
+                public void onAuthenticationFailed(int requestReason, int userId) {
+                    Slog.i(TAG, "AuthenticationStateListener#onAuthenticationFailed");
+                    mHandler.obtainMessage(MSG_REPORT_BIOMETRIC_AUTH_ATTEMPT, AUTH_FAILURE, userId)
+                            .sendToTarget();
+                }
+
+                @Override
+                public void onAuthenticationAcquired(BiometricSourceType biometricSourceType,
+                        int requestReason, int acquiredInfo) {}
+            };
+
+    private final Handler mHandler = new Handler(Looper.getMainLooper()) {
+        @Override
+        public void handleMessage(Message msg) {
+            switch (msg.what) {
+                case MSG_REPORT_PRIMARY_AUTH_ATTEMPT:
+                    handleReportPrimaryAuthAttempt(msg.arg1 != AUTH_FAILURE, msg.arg2);
+                    break;
+                case MSG_REPORT_BIOMETRIC_AUTH_ATTEMPT:
+                    handleReportBiometricAuthAttempt(msg.arg1 != AUTH_FAILURE, msg.arg2);
+                    break;
+            }
+        }
+    };
+
+    private void handleReportPrimaryAuthAttempt(boolean success, int userId) {
+        if (DEBUG) {
+            Slog.d(TAG, "handleReportPrimaryAuthAttempt: success=" + success
+                    + ", userId=" + userId);
+        }
+        reportAuthAttempt(success, userId);
+    }
+
+    private void handleReportBiometricAuthAttempt(boolean success, int userId) {
+        if (DEBUG) {
+            Slog.d(TAG, "handleReportBiometricAuthAttempt: success=" + success
+                    + ", userId=" + userId);
+        }
+        reportAuthAttempt(success, userId);
+    }
+
+    private void reportAuthAttempt(boolean success, int userId) {
+        if (success) {
+            // Deleting the entry effectively resets the counter of failed attempts for the user
+            mFailedAttemptsForUser.delete(userId);
+            return;
+        }
+
+        final int numFailedAttempts = mFailedAttemptsForUser.get(userId, 0) + 1;
+        Slog.i(TAG, "reportAuthAttempt: numFailedAttempts=" + numFailedAttempts
+                + ", userId=" + userId);
+        mFailedAttemptsForUser.put(userId, numFailedAttempts);
+
+        // Don't lock again if the device is already locked and if Keyguard is already showing and
+        // isn't trivially dismissible
+        if (mKeyguardManager.isDeviceLocked(userId) && mKeyguardManager.isKeyguardLocked()) {
+            Slog.d(TAG, "Not locking the device because the device is already locked.");
+            return;
+        }
+
+        if (numFailedAttempts < MAX_ALLOWED_FAILED_AUTH_ATTEMPTS) {
+            Slog.d(TAG, "Not locking the device because the number of failed attempts is below"
+                    + " the threshold.");
+            return;
+        }
+
+        //TODO: additionally consider the trust signal before locking device
+        lockDevice(userId);
+    }
+
+    /**
+     * Locks the device and requires primary auth or biometric auth for unlocking
+     */
+    private void lockDevice(int userId) {
+        // Require either primary auth or biometric auth to unlock the device again. Keyguard and
+        // bouncer will also check the StrongAuthFlag for the user to display correct strings for
+        // explaining why the device is locked
+        mLockPatternUtils.requireStrongAuth(SOME_AUTH_REQUIRED_AFTER_ADAPTIVE_AUTH_REQUEST, userId);
+
+        // If userId is a profile that has a different parent userId (regardless of its profile
+        // type, or whether it's a profile with unified challenges or not), its parent userId that
+        // owns the Keyguard will also be locked
+        final int parentUserId = mUserManager.getProfileParentId(userId);
+        Slog.i(TAG, "lockDevice: userId=" + userId + ", parentUserId=" + parentUserId);
+        if (parentUserId != userId) {
+            mLockPatternUtils.requireStrongAuth(SOME_AUTH_REQUIRED_AFTER_ADAPTIVE_AUTH_REQUEST,
+                    parentUserId);
+        }
+
+        // Power off the display
+        mPowerManager.goToSleep(SystemClock.uptimeMillis());
+
+        // Lock the device
+        mWindowManager.lockNow();
+    }
+}
diff --git a/services/java/com/android/server/SystemServer.java b/services/java/com/android/server/SystemServer.java
index 1d89d17..7c908cc 100644
--- a/services/java/com/android/server/SystemServer.java
+++ b/services/java/com/android/server/SystemServer.java
@@ -107,6 +107,7 @@
 import com.android.internal.util.FrameworkStatsLog;
 import com.android.internal.widget.ILockSettings;
 import com.android.internal.widget.LockSettingsInternal;
+import com.android.server.adaptiveauth.AdaptiveAuthService;
 import com.android.server.am.ActivityManagerService;
 import com.android.server.appbinding.AppBindingService;
 import com.android.server.appop.AppOpMigrationHelper;
@@ -2615,6 +2616,12 @@
             mSystemServiceManager.startService(AuthService.class);
             t.traceEnd();
 
+            if (android.adaptiveauth.Flags.enableAdaptiveAuth()) {
+                t.traceBegin("StartAdaptiveAuthService");
+                mSystemServiceManager.startService(AdaptiveAuthService.class);
+                t.traceEnd();
+            }
+
             if (!isWatch) {
                 // We don't run this on watches as there are no plans to use the data logged
                 // on watch devices.
diff --git a/services/tests/servicestests/src/com/android/server/adaptiveauth/AdaptiveAuthServiceTest.java b/services/tests/servicestests/src/com/android/server/adaptiveauth/AdaptiveAuthServiceTest.java
new file mode 100644
index 0000000..08a6529
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/adaptiveauth/AdaptiveAuthServiceTest.java
@@ -0,0 +1,333 @@
+/*
+ * 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.adaptiveauth;
+
+import static android.adaptiveauth.Flags.FLAG_ENABLE_ADAPTIVE_AUTH;
+import static android.adaptiveauth.Flags.FLAG_REPORT_BIOMETRIC_AUTH_ATTEMPTS;
+import static android.security.Flags.FLAG_REPORT_PRIMARY_AUTH_ATTEMPTS;
+
+import static com.android.internal.widget.LockPatternUtils.StrongAuthTracker.SOME_AUTH_REQUIRED_AFTER_ADAPTIVE_AUTH_REQUEST;
+import static com.android.server.adaptiveauth.AdaptiveAuthService.MAX_ALLOWED_FAILED_AUTH_ATTEMPTS;
+
+import static org.junit.Assert.assertEquals;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.app.KeyguardManager;
+import android.content.Context;
+import android.hardware.biometrics.AuthenticationStateListener;
+import android.hardware.biometrics.BiometricManager;
+import android.os.RemoteException;
+import android.platform.test.flag.junit.SetFlagsRule;
+
+import androidx.test.InstrumentationRegistry;
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.internal.widget.LockPatternUtils;
+import com.android.internal.widget.LockSettingsInternal;
+import com.android.internal.widget.LockSettingsStateListener;
+import com.android.server.LocalServices;
+import com.android.server.pm.UserManagerInternal;
+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;
+import org.mockito.Captor;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+/**
+ * atest FrameworksServicesTests:AdaptiveAuthServiceTest
+ */
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class AdaptiveAuthServiceTest {
+    @Rule
+    public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
+
+    private static final int PRIMARY_USER_ID = 0;
+    private static final int MANAGED_PROFILE_USER_ID = 12;
+    private static final int DEFAULT_COUNT_FAILED_AUTH_ATTEMPTS = 0;
+    private static final int REASON_UNKNOWN = 0; // BiometricRequestConstants.RequestReason
+
+    private Context mContext;
+    private AdaptiveAuthService mAdaptiveAuthService;
+
+    @Mock
+    LockPatternUtils mLockPatternUtils;
+    @Mock
+    private LockSettingsInternal mLockSettings;
+    @Mock
+    private BiometricManager mBiometricManager;
+    @Mock
+    private KeyguardManager mKeyguardManager;
+    @Mock
+    private WindowManagerInternal mWindowManager;
+    @Mock
+    private UserManagerInternal mUserManager;
+
+    @Captor
+    ArgumentCaptor<LockSettingsStateListener> mLockSettingsStateListenerCaptor;
+    @Captor
+    ArgumentCaptor<AuthenticationStateListener> mAuthenticationStateListenerCaptor;
+
+    @Before
+    public void setUp() {
+        MockitoAnnotations.initMocks(this);
+
+        mSetFlagsRule.enableFlags(FLAG_ENABLE_ADAPTIVE_AUTH);
+        mSetFlagsRule.enableFlags(FLAG_REPORT_PRIMARY_AUTH_ATTEMPTS);
+        mSetFlagsRule.enableFlags(FLAG_REPORT_BIOMETRIC_AUTH_ATTEMPTS);
+
+        mContext = spy(ApplicationProvider.getApplicationContext());
+        when(mContext.getSystemService(BiometricManager.class)).thenReturn(mBiometricManager);
+        when(mContext.getSystemService(KeyguardManager.class)).thenReturn(mKeyguardManager);
+
+        LocalServices.removeServiceForTest(LockSettingsInternal.class);
+        LocalServices.addService(LockSettingsInternal.class, mLockSettings);
+        LocalServices.removeServiceForTest(WindowManagerInternal.class);
+        LocalServices.addService(WindowManagerInternal.class, mWindowManager);
+        LocalServices.removeServiceForTest(UserManagerInternal.class);
+        LocalServices.addService(UserManagerInternal.class, mUserManager);
+
+        mAdaptiveAuthService = new AdaptiveAuthService(mContext, mLockPatternUtils);
+        mAdaptiveAuthService.init();
+
+        verify(mLockSettings).registerLockSettingsStateListener(
+                mLockSettingsStateListenerCaptor.capture());
+        verify(mBiometricManager).registerAuthenticationStateListener(
+                mAuthenticationStateListenerCaptor.capture());
+
+        // Set PRIMARY_USER_ID as the parent of MANAGED_PROFILE_USER_ID
+        when(mUserManager.getProfileParentId(eq(MANAGED_PROFILE_USER_ID)))
+                .thenReturn(PRIMARY_USER_ID);
+    }
+
+    @After
+    public void tearDown() throws Exception {
+        LocalServices.removeServiceForTest(LockSettingsInternal.class);
+        LocalServices.removeServiceForTest(WindowManagerInternal.class);
+        LocalServices.removeServiceForTest(UserManagerInternal.class);
+    }
+
+    @Test
+    public void testReportAuthAttempt_primaryAuthSucceeded()
+            throws RemoteException {
+        mLockSettingsStateListenerCaptor.getValue().onAuthenticationSucceeded(PRIMARY_USER_ID);
+        waitForAuthCompletion();
+
+        verifyNotLockDevice(DEFAULT_COUNT_FAILED_AUTH_ATTEMPTS /* expectedCntFailedAttempts */,
+                PRIMARY_USER_ID);
+    }
+
+    @Test
+    public void testReportAuthAttempt_primaryAuthFailed_once()
+            throws RemoteException {
+        mLockSettingsStateListenerCaptor.getValue().onAuthenticationFailed(PRIMARY_USER_ID);
+        waitForAuthCompletion();
+
+        verifyNotLockDevice(1 /* expectedCntFailedAttempts */, PRIMARY_USER_ID);
+    }
+
+    @Test
+    public void testReportAuthAttempt_primaryAuthFailed_multiple_deviceCurrentlyLocked()
+            throws RemoteException {
+        // Device is currently locked and Keyguard is showing
+        when(mKeyguardManager.isDeviceLocked(PRIMARY_USER_ID)).thenReturn(true);
+        when(mKeyguardManager.isKeyguardLocked()).thenReturn(true);
+
+        for (int i = 0; i < MAX_ALLOWED_FAILED_AUTH_ATTEMPTS; i++) {
+            mLockSettingsStateListenerCaptor.getValue().onAuthenticationFailed(PRIMARY_USER_ID);
+        }
+        waitForAuthCompletion();
+
+        verifyNotLockDevice(MAX_ALLOWED_FAILED_AUTH_ATTEMPTS /* expectedCntFailedAttempts */,
+                PRIMARY_USER_ID);
+    }
+
+    @Test
+    public void testReportAuthAttempt_primaryAuthFailed_multiple_deviceCurrentlyNotLocked()
+            throws RemoteException {
+        // Device is currently not locked and Keyguard is not showing
+        when(mKeyguardManager.isDeviceLocked(PRIMARY_USER_ID)).thenReturn(false);
+        when(mKeyguardManager.isKeyguardLocked()).thenReturn(false);
+
+        for (int i = 0; i < MAX_ALLOWED_FAILED_AUTH_ATTEMPTS; i++) {
+            mLockSettingsStateListenerCaptor.getValue().onAuthenticationFailed(PRIMARY_USER_ID);
+        }
+        waitForAuthCompletion();
+
+        verifyLockDevice(PRIMARY_USER_ID);
+    }
+
+    @Test
+    public void testReportAuthAttempt_biometricAuthSucceeded()
+            throws RemoteException {
+        mAuthenticationStateListenerCaptor.getValue()
+                .onAuthenticationSucceeded(REASON_UNKNOWN, PRIMARY_USER_ID);
+        waitForAuthCompletion();
+
+        verifyNotLockDevice(DEFAULT_COUNT_FAILED_AUTH_ATTEMPTS /* expectedCntFailedAttempts */,
+                PRIMARY_USER_ID);
+    }
+
+    @Test
+    public void testReportAuthAttempt_biometricAuthFailed_once()
+            throws RemoteException {
+        mAuthenticationStateListenerCaptor.getValue()
+                .onAuthenticationFailed(REASON_UNKNOWN, PRIMARY_USER_ID);
+        waitForAuthCompletion();
+
+        verifyNotLockDevice(1 /* expectedCntFailedAttempts */, PRIMARY_USER_ID);
+    }
+
+    @Test
+    public void testReportAuthAttempt_biometricAuthFailed_multiple_deviceCurrentlyLocked()
+            throws RemoteException {
+        // Device is currently locked and Keyguard is showing
+        when(mKeyguardManager.isDeviceLocked(PRIMARY_USER_ID)).thenReturn(true);
+        when(mKeyguardManager.isKeyguardLocked()).thenReturn(true);
+
+        for (int i = 0; i < MAX_ALLOWED_FAILED_AUTH_ATTEMPTS; i++) {
+            mAuthenticationStateListenerCaptor.getValue()
+                    .onAuthenticationFailed(REASON_UNKNOWN, PRIMARY_USER_ID);
+        }
+        waitForAuthCompletion();
+
+        verifyNotLockDevice(MAX_ALLOWED_FAILED_AUTH_ATTEMPTS /* expectedCntFailedAttempts */,
+                PRIMARY_USER_ID);
+    }
+
+    @Test
+    public void testReportAuthAttempt_biometricAuthFailed_multiple_deviceCurrentlyNotLocked()
+            throws RemoteException {
+        // Device is currently not locked and Keyguard is not showing
+        when(mKeyguardManager.isDeviceLocked(PRIMARY_USER_ID)).thenReturn(false);
+        when(mKeyguardManager.isKeyguardLocked()).thenReturn(false);
+
+        for (int i = 0; i < MAX_ALLOWED_FAILED_AUTH_ATTEMPTS; i++) {
+            mAuthenticationStateListenerCaptor.getValue()
+                    .onAuthenticationFailed(REASON_UNKNOWN, PRIMARY_USER_ID);
+        }
+        waitForAuthCompletion();
+
+        verifyLockDevice(PRIMARY_USER_ID);
+    }
+
+    @Test
+    public void testReportAuthAttempt_biometricAuthFailedThenPrimaryAuthSucceeded()
+            throws RemoteException {
+        // Three failed biometric auth attempts
+        for (int i = 0; i < 3; i++) {
+            mAuthenticationStateListenerCaptor.getValue()
+                    .onAuthenticationFailed(REASON_UNKNOWN, PRIMARY_USER_ID);
+        }
+        // One successful primary auth attempt
+        mLockSettingsStateListenerCaptor.getValue().onAuthenticationSucceeded(PRIMARY_USER_ID);
+        waitForAuthCompletion();
+
+        verifyNotLockDevice(DEFAULT_COUNT_FAILED_AUTH_ATTEMPTS /* expectedCntFailedAttempts */,
+                PRIMARY_USER_ID);
+    }
+
+    @Test
+    public void testReportAuthAttempt_primaryAuthFailedThenBiometricAuthSucceeded()
+            throws RemoteException {
+        // Three failed primary auth attempts
+        for (int i = 0; i < 3; i++) {
+            mLockSettingsStateListenerCaptor.getValue().onAuthenticationFailed(PRIMARY_USER_ID);
+        }
+        // One successful biometric auth attempt
+        mAuthenticationStateListenerCaptor.getValue()
+                .onAuthenticationSucceeded(REASON_UNKNOWN, PRIMARY_USER_ID);
+        waitForAuthCompletion();
+
+        verifyNotLockDevice(DEFAULT_COUNT_FAILED_AUTH_ATTEMPTS /* expectedCntFailedAttempts */,
+                PRIMARY_USER_ID);
+    }
+
+    @Test
+    public void testReportAuthAttempt_primaryAuthAndBiometricAuthFailed_primaryUser()
+            throws RemoteException {
+        // Three failed primary auth attempts
+        for (int i = 0; i < 3; i++) {
+            mLockSettingsStateListenerCaptor.getValue().onAuthenticationFailed(PRIMARY_USER_ID);
+        }
+        // Two failed biometric auth attempts
+        for (int i = 0; i < 2; i++) {
+            mAuthenticationStateListenerCaptor.getValue()
+                    .onAuthenticationFailed(REASON_UNKNOWN, PRIMARY_USER_ID);
+        }
+        waitForAuthCompletion();
+
+        verifyLockDevice(PRIMARY_USER_ID);
+    }
+
+    @Test
+    public void testReportAuthAttempt_primaryAuthAndBiometricAuthFailed_profileOfPrimaryUser()
+            throws RemoteException {
+        // Three failed primary auth attempts
+        for (int i = 0; i < 3; i++) {
+            mLockSettingsStateListenerCaptor.getValue()
+                    .onAuthenticationFailed(MANAGED_PROFILE_USER_ID);
+        }
+        // Two failed biometric auth attempts
+        for (int i = 0; i < 2; i++) {
+            mAuthenticationStateListenerCaptor.getValue()
+                    .onAuthenticationFailed(REASON_UNKNOWN, MANAGED_PROFILE_USER_ID);
+        }
+        waitForAuthCompletion();
+
+        verifyLockDevice(MANAGED_PROFILE_USER_ID);
+    }
+
+    private void verifyNotLockDevice(int expectedCntFailedAttempts, int userId) {
+        assertEquals(expectedCntFailedAttempts,
+                mAdaptiveAuthService.mFailedAttemptsForUser.get(userId));
+        verify(mWindowManager, never()).lockNow();
+    }
+
+    private void verifyLockDevice(int userId) {
+        assertEquals(MAX_ALLOWED_FAILED_AUTH_ATTEMPTS,
+                mAdaptiveAuthService.mFailedAttemptsForUser.get(userId));
+        verify(mLockPatternUtils).requireStrongAuth(
+                eq(SOME_AUTH_REQUIRED_AFTER_ADAPTIVE_AUTH_REQUEST), eq(userId));
+        // If userId is MANAGED_PROFILE_USER_ID, the StrongAuthFlag of its parent (PRIMARY_USER_ID)
+        // should also be verified
+        if (userId == MANAGED_PROFILE_USER_ID) {
+            verify(mLockPatternUtils).requireStrongAuth(
+                    eq(SOME_AUTH_REQUIRED_AFTER_ADAPTIVE_AUTH_REQUEST), eq(PRIMARY_USER_ID));
+        }
+        verify(mWindowManager).lockNow();
+    }
+
+    /**
+     * Wait for all auth events to complete before verification
+     */
+    private static void waitForAuthCompletion() {
+        InstrumentationRegistry.getInstrumentation().waitForIdleSync();
+    }
+}
diff --git a/services/tests/servicestests/src/com/android/server/adaptiveauth/OWNERS b/services/tests/servicestests/src/com/android/server/adaptiveauth/OWNERS
new file mode 100644
index 0000000..0218a78
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/adaptiveauth/OWNERS
@@ -0,0 +1 @@
+include /services/core/java/com/android/server/adaptiveauth/OWNERS
\ No newline at end of file