/*
 * Copyright (C) 2021 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 android.server.biometrics;

import static android.server.biometrics.Components.CLASS_2_BIOMETRIC_ACTIVITY;
import static android.server.biometrics.Components.CLASS_3_BIOMETRIC_ACTIVITY;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import static org.junit.Assume.assumeTrue;

import android.content.ComponentName;
import android.hardware.biometrics.BiometricManager;
import android.hardware.biometrics.BiometricManager.Authenticators;
import android.hardware.biometrics.BiometricTestSession;
import android.hardware.biometrics.SensorProperties;
import android.platform.test.annotations.Presubmit;
import android.server.wm.TestJournalProvider.TestJournal;
import android.server.wm.TestJournalProvider.TestJournalContainer;
import android.util.Log;

import org.junit.After;
import org.junit.BeforeClass;
import org.junit.Test;

import java.util.List;

/**
 * Tests for security related functionality such as downgrading and upgrading biometric strength.
 */
@Presubmit
public class BiometricSecurityTests extends BiometricTestBase {
    private static final String TAG = "BiometricTests/Security";

    private static final String DEVICE_CONFIG_NAMESPACE = "biometrics";
    private static final String DEVICE_CONFIG_BIO_STRENGTH_KEY = "biometric_strengths";

    @BeforeClass
    public static void ensureReset() throws Exception {
        revertSensorStrengths();
    }

    @After
    public void teardown() throws Exception {
        revertSensorStrengths();
    }

    /**
     * A strong biometric should be able to perform auth with any requested strength, since it is
     * the highest biometric class. For example,
     * +-------------------+--------------------+----------+
     * | Original Strength | Requested Strength | Result   |
     * +-------------------+--------------------+----------+
     * | BIOMETRIC_STRONG  | BIOMETRIC_STRONG   | Accepted |
     * +-------------------+--------------------+----------+
     * | BIOMETRIC_STRONG  | BIOMETRIC_WEAK     | Accepted |
     * +-------------------+--------------------+----------+
     * Note that since BiometricPrompt does not support Convenience biometrics, currently we don't
     * have a way to test cases where the requested strength is BIOMETRIC_CONVENIENCE.
     */
    @Test
    public void testBiometricStrength_StrongSensor() throws Exception {
        assumeTrue(Utils.isFirstApiLevel29orGreater());
        final List<Integer> sensors = getSensorsOfTargetStrength(SensorProperties.STRENGTH_STRONG);
        assumeTrue("testBiometricStrength_StrongSensor: numSensors=" + sensors.size(),
                sensors.size() > 0);

        // Tuple of originalStrength and requestedStrength
        final int[][] testCases = {
                // Request Strong auth
                {Authenticators.BIOMETRIC_STRONG, Authenticators.BIOMETRIC_STRONG},

                // Request Weak auth
                {Authenticators.BIOMETRIC_STRONG, Authenticators.BIOMETRIC_WEAK}
        };

        for (Integer sensorId : sensors) {
            for (int i = 0; i < testCases.length; i++) {
                testBiometricStrength_forSensor_authAllowed(sensorId,
                        testCases[i][0] /* originalStrength */,
                        testCases[i][1] /* requestedStrength */);
            }
        }
    }

    /**
     * A weak biometric may or may not be able to perform auth, depending on the requested strength.
     * For example,
     * +-------------------+--------------------+----------+
     * | Original Strength | Requested Strength | Result   |
     * +-------------------+--------------------+----------+
     * | BIOMETRIC_WEAK    | BIOMETRIC_STRONG   | Error    |
     * +-------------------+--------------------+----------+
     * | BIOMETRIC_WEAK    | BIOMETRIC_WEAK     | Accepted |
     * +-------------------+--------------------+----------+
     * Note that since BiometricPrompt does not support Convenience biometrics, currently we don't
     * have a way to test cases where the requested strength is BIOMETRIC_CONVENIENCE.
     */
    @Test
    public void testBiometricStrength_WeakSensor() throws Exception {
        assumeTrue(Utils.isFirstApiLevel29orGreater());
        final List<Integer> sensors = getSensorsOfTargetStrength(SensorProperties.STRENGTH_WEAK);
        assumeTrue("testBiometricStrength_WeakSensor: numSensors: " + sensors.size(),
                sensors.size() > 0);

        for (Integer sensorId : sensors) {
            testBiometricStrength_forSensor_authDisallowed(sensorId,
                    Authenticators.BIOMETRIC_WEAK /* originalStrength */,
                    Authenticators.BIOMETRIC_STRONG /* requestedStrength */,
                    mSensorProperties.size() > 1 /* hasMultiSensors */);

            testBiometricStrength_forSensor_authAllowed(sensorId,
                    Authenticators.BIOMETRIC_WEAK /* originalStrength */,
                    Authenticators.BIOMETRIC_WEAK /* requestedStrength */);
        }
    }

    /**
     * A convenience biometric should not be able to perform auth with the following requested
     * strength, due to insufficient strength.
     * +-----------------------+--------------------+--------+
     * | Original Strength     | Requested Strength | Result |
     * +-----------------------+--------------------+--------+
     * | BIOMETRIC_CONVENIENCE | BIOMETRIC_STRONG   | Error  |
     * +-----------------------+--------------------+--------+
     * | BIOMETRIC_CONVENIENCE | BIOMETRIC_WEAK     | Error  |
     * +-----------------------+--------------------+--------+
     * Note that since BiometricPrompt does not support Convenience biometrics, currently we don't
     * have a way to test cases where the requested strength is BIOMETRIC_CONVENIENCE.
     */
    @Test
    public void testBiometricStrength_ConvenienceSensor() throws Exception {
        assumeTrue(Utils.isFirstApiLevel29orGreater());
        final List<Integer> sensors =
                getSensorsOfTargetStrength(SensorProperties.STRENGTH_CONVENIENCE);
        assumeTrue("testBiometricStrength_ConvenienceSensor: numSensors=" + sensors.size(),
                sensors.size() > 0);

        // Tuple of originalStrength and requestedStrength
        final int[][] testCases = {
                // Request Strong auth
                {Authenticators.BIOMETRIC_CONVENIENCE, Authenticators.BIOMETRIC_STRONG},

                // Request Weak auth
                {Authenticators.BIOMETRIC_CONVENIENCE, Authenticators.BIOMETRIC_WEAK}
        };

        for (Integer sensorId : sensors) {
            for (int i = 0; i < testCases.length; i++) {
                testBiometricStrength_forSensor_authDisallowed(sensorId,
                        testCases[i][0] /* originalStrength */,
                        testCases[i][1] /* requestedStrength */,
                        mSensorProperties.size() > 1 /* hasMultiSensors */);
            }
        }
    }

    private void testBiometricStrength_forSensor_authAllowed(int sensorId, int originalStrength,
            int requestedStrength) throws Exception {
        Log.d(TAG, "testBiometricStrength_forSensor_authAllowed: "
                + ", sensorId=" + sensorId
                + ", originalStrength=" + originalStrength
                + ", requestedStrength=" + requestedStrength);

        final ComponentName componentName = getComponentName(requestedStrength);

        // Reset to the original strength in case it's ever changed before the test
        updateStrengthAndVerify(sensorId, originalStrength);

        try (BiometricTestSession session = mBiometricManager.createTestSession(sensorId);
             ActivitySession activitySession = new ActivitySession(this, componentName)) {
            final int userId = 0;
            waitForAllUnenrolled();
            enrollForSensor(session, sensorId);
            final TestJournal journal =
                    TestJournalContainer.get(activitySession.getComponentName());

            // No error code should be returned for the requested strength
            int errCode = mBiometricManager.canAuthenticate(requestedStrength);
            assertEquals("Device should allow auth with the requested biometric",
                    BiometricManager.BIOMETRIC_SUCCESS, errCode);

            // Launch test activity
            launchActivityAndWaitForResumed(activitySession);

            BiometricCallbackHelper.State callbackState = getCallbackState(journal);
            assertNotNull(callbackState);

            BiometricServiceState state = getCurrentState();
            assertTrue(state.toString(), state.mSensorStates.sensorStates.get(sensorId).isBusy());

            // Auth should work
            successfullyAuthenticate(session, userId);
            mInstrumentation.waitForIdleSync();
            callbackState = getCallbackState(journal);
            assertNotNull(callbackState);
            assertEquals(callbackState.toString(), 0, callbackState.mNumAuthRejected);
            assertEquals(callbackState.toString(), 1, callbackState.mNumAuthAccepted);
            assertEquals(callbackState.toString(), 0, callbackState.mAcquiredReceived.size());
            assertEquals(callbackState.toString(), 0, callbackState.mErrorsReceived.size());
        }
    }

    private void testBiometricStrength_forSensor_authDisallowed(int sensorId, int originalStrength,
            int requestedStrength, boolean hasMultiSensors) throws Exception {
        Log.d(TAG, "testBiometricStrength_forSensor_authDisallowed: "
                + ", sensorId=" + sensorId
                + ", originalStrength=" + originalStrength
                + ", requestedStrength=" + requestedStrength
                + ", hasMultiSensors=" + hasMultiSensors);

        final ComponentName componentName = getComponentName(requestedStrength);

        // Reset to the original strength in case it's ever changed before the test
        updateStrengthAndVerify(sensorId, originalStrength);

        try (BiometricTestSession session = mBiometricManager.createTestSession(sensorId);
             ActivitySession activitySession = new ActivitySession(this, componentName)) {
            waitForAllUnenrolled();
            enrollForSensor(session, sensorId);
            final TestJournal journal =
                    TestJournalContainer.get(activitySession.getComponentName());

            // Error code should be returned for the requested strength due to insufficient strength
            int errCode = mBiometricManager.canAuthenticate(requestedStrength);
            checkErrCode("Device shouldn't allow auth with biometrics that have insufficient"
                            + " strength. errCode: " + errCode,
                    errCode, BiometricManager.BIOMETRIC_ERROR_NO_HARDWARE,
                    hasMultiSensors);

            // Launch test activity
            launchActivityAndWaitForResumed(activitySession);

            // Auth shouldn't work and error code should be returned
            mInstrumentation.waitForIdleSync();
            BiometricCallbackHelper.State callbackState = getCallbackState(journal);
            assertNotNull(callbackState);
            assertEquals(callbackState.toString(), 0, callbackState.mNumAuthRejected);
            assertEquals(callbackState.toString(), 0, callbackState.mNumAuthAccepted);
            assertEquals(callbackState.toString(), 0, callbackState.mAcquiredReceived.size());
            assertEquals(callbackState.toString(), 1, callbackState.mErrorsReceived.size());
            checkErrCode(callbackState.toString(), (int) callbackState.mErrorsReceived.get(0),
                    BiometricManager.BIOMETRIC_ERROR_NO_HARDWARE, hasMultiSensors);
        }
    }

    /**
     * The strength of a Strong biometric may need to be downgraded to a weaker one if the biometric
     * requires a security update. After downgrading, the biometric may or may not be able to
     * perform auth with the requested strength. For example,
     * +-------------------+-----------------------+--------------------+----------+
     * | Original Strength | Target Strength       | Requested Strength | Result   |
     * +-------------------+-----------------------+--------------------+----------+
     * | BIOMETRIC_STRONG  | BIOMETRIC_WEAK        | BIOMETRIC_STRONG   | Error    |
     * +-------------------+-----------------------+--------------------+----------+
     * | BIOMETRIC_STRONG  | BIOMETRIC_WEAK        | BIOMETRIC_WEAK     | Accepted |
     * +-------------------+-----------------------+--------------------+----------+
     * | BIOMETRIC_STRONG  | BIOMETRIC_CONVENIENCE | BIOMETRIC_STRONG   | Error    |
     * +-------------------+-----------------------+--------------------+----------+
     * | BIOMETRIC_STRONG  | BIOMETRIC_CONVENIENCE | BIOMETRIC_WEAK     | Error    |
     * +-------------------+-----------------------+--------------------+----------+
     * Note that since BiometricPrompt does not support Convenience biometrics, currently we don't
     * have a way to test cases where the requested strength is BIOMETRIC_CONVENIENCE.
     */
    @Test
    public void testBiometricStrengthDowngraded_StrongSensor() throws Exception {
        assumeTrue(Utils.isFirstApiLevel29orGreater());
        final List<Integer> sensors = getSensorsOfTargetStrength(SensorProperties.STRENGTH_STRONG);
        assumeTrue("testBiometricStrengthDowngraded_StrongSensor: numSensors=" + sensors.size(),
                sensors.size() > 0);

        // Tuple of originalStrength, targetStrength, and requestedStrength
        final int[][] testCases = {
                // Downgrade Strong to Weak, and request Strong auth
                {Authenticators.BIOMETRIC_STRONG, Authenticators.BIOMETRIC_WEAK,
                        Authenticators.BIOMETRIC_STRONG},

                // Downgrade Strong to Weak, and request Weak auth
                {Authenticators.BIOMETRIC_STRONG, Authenticators.BIOMETRIC_WEAK,
                        Authenticators.BIOMETRIC_WEAK},

                // Downgrade Strong to Convenience, and request Strong auth
                {Authenticators.BIOMETRIC_STRONG, Authenticators.BIOMETRIC_CONVENIENCE,
                        Authenticators.BIOMETRIC_STRONG},

                // Downgrade Strong to Convenience, and request Weak auth
                {Authenticators.BIOMETRIC_STRONG, Authenticators.BIOMETRIC_CONVENIENCE,
                        Authenticators.BIOMETRIC_WEAK}
        };

        for (Integer sensorId : sensors) {
            for (int i = 0; i < testCases.length; i++) {
                testBiometricStrengthDowngraded_forSensor(sensorId,
                        testCases[i][0] /* originalStrength */,
                        testCases[i][1] /* targetStrength */,
                        testCases[i][2] /* requestedStrength */,
                        mSensorProperties.size() > 1 /* hasMultiSensors */);
            }
        }
    }

    /**
     * The strength of a Weak biometric may need to be downgraded to a weaker one if the biometric
     * requires a security update. After downgrading, the biometric may or may not be able to
     * perform auth with the requested strength. For example,
     * +-------------------+-----------------------+--------------------+--------+
     * | Original Strength | Target Strength       | Requested Strength | Result |
     * +-------------------+-----------------------+--------------------+--------+
     * | BIOMETRIC_WEAK    | BIOMETRIC_CONVENIENCE | BIOMETRIC_WEAK     | Error  |
     * +-------------------+-----------------------+--------------------+--------+
     * Note that since BiometricPrompt does not support Convenience biometrics, currently we don't
     * have a way to test cases where the requested strength is BIOMETRIC_CONVENIENCE.
     */
    @Test
    public void testBiometricStrengthDowngraded_WeakSensor() throws Exception {
        assumeTrue(Utils.isFirstApiLevel29orGreater());
        final List<Integer> sensors = getSensorsOfTargetStrength(SensorProperties.STRENGTH_WEAK);
        assumeTrue("testBiometricStrengthDowngraded_WeakSensor: numSensors: " + sensors.size(),
                sensors.size() > 0);

        // Tuple of originalStrength, targetStrength, and requestedStrength
        final int[][] testCases = {
                // Downgrade Weak to Convenience, and request Weak auth
                {Authenticators.BIOMETRIC_WEAK, Authenticators.BIOMETRIC_CONVENIENCE,
                        Authenticators.BIOMETRIC_WEAK}
        };

        for (Integer sensorId : sensors) {
            for (int i = 0; i < testCases.length; i++) {
                testBiometricStrengthDowngraded_forSensor(sensorId,
                        testCases[i][0] /* originalStrength */,
                        testCases[i][1] /* targetStrength */,
                        testCases[i][2] /* requestedStrength */,
                        mSensorProperties.size() > 1 /* hasMultiSensors */);
            }
        }
    }

    private void testBiometricStrengthDowngraded_forSensor(int sensorId, int originalStrength,
            int targetStrength, int requestedStrength, boolean hasMultiSensors) throws Exception {
        Log.d(TAG, "testBiometricStrengthDowngraded_forSensor: "
                + ", sensorId=" + sensorId
                + ", originalStrength=" + originalStrength
                + ", targetStrength=" + targetStrength
                + ", requestedStrength=" + requestedStrength
                + ", hasMultiSensors=" + hasMultiSensors);

        final ComponentName componentName = getComponentName(requestedStrength);

        try (BiometricTestSession session = mBiometricManager.createTestSession(sensorId);
             ActivitySession activitySession = new ActivitySession(this, componentName)) {
            final int userId = 0;
            waitForAllUnenrolled();
            enrollForSensor(session, sensorId);
            final TestJournal journal =
                    TestJournalContainer.get(activitySession.getComponentName());

            BiometricCallbackHelper.State callbackState;
            BiometricServiceState state;

            // Downgrade the biometric strength to the target strength
            updateStrengthAndVerify(sensorId, targetStrength);

            // After downgrading, check whether auth works
            // TODO: should check if targetStrength is at least as strong as the requestedStrength,
            // but some strength constants that are needed for the calculation are not exposed in
            // BiometricManager.
            if (targetStrength == requestedStrength) {
                Log.d(TAG, "The targetStrength is as strong as the requestedStrength");
                // No error code should be returned since biometric has sufficient strength if
                // request weak auth
                int errCode = mBiometricManager.canAuthenticate(requestedStrength);
                assertEquals("Device should allow auth with the requested biometric",
                        BiometricManager.BIOMETRIC_SUCCESS, errCode);

                // Launch test activity
                launchActivityAndWaitForResumed(activitySession);

                state = getCurrentState();
                assertTrue(state.toString(),
                        state.mSensorStates.sensorStates.get(sensorId).isBusy());

                // Auth should work
                successfullyAuthenticate(session, userId);
                mInstrumentation.waitForIdleSync();
                callbackState = getCallbackState(journal);
                assertNotNull(callbackState);
                assertEquals(callbackState.toString(), 0, callbackState.mNumAuthRejected);
                assertEquals(callbackState.toString(), 1, callbackState.mNumAuthAccepted);
                assertEquals(callbackState.toString(), 0, callbackState.mAcquiredReceived.size());
                assertEquals(callbackState.toString(), 0, callbackState.mErrorsReceived.size());
            } else {
                Log.d(TAG, "The targetStrength is not strong enough");
                // Error code should be returned
                int errCode = mBiometricManager.canAuthenticate(requestedStrength);
                checkErrCode("Device shouldn't allow auth with biometrics that require security"
                                + " update. errCode: " + errCode,
                        errCode, BiometricManager.BIOMETRIC_ERROR_SECURITY_UPDATE_REQUIRED,
                        hasMultiSensors);

                // Launch test activity
                launchActivityAndWaitForResumed(activitySession);

                // Auth shouldn't work and error code should be returned
                mInstrumentation.waitForIdleSync();
                callbackState = getCallbackState(journal);
                assertNotNull(callbackState);
                assertEquals(callbackState.toString(), 0, callbackState.mNumAuthRejected);
                assertEquals(callbackState.toString(), 0, callbackState.mNumAuthAccepted);
                assertEquals(callbackState.toString(), 0, callbackState.mAcquiredReceived.size());
                assertEquals(callbackState.toString(), 1, callbackState.mErrorsReceived.size());
                checkErrCode(callbackState.toString(), (int) callbackState.mErrorsReceived.get(0),
                        BiometricManager.BIOMETRIC_ERROR_SECURITY_UPDATE_REQUIRED, hasMultiSensors);
            }
        }
    }

    /**
     * Trying to upgrade the strength of a Weak biometric to a stronger strength will not
     * succeed (ie, it's no-op and the biometric strength is still Weak), since the biometric's
     * actual strength can't go past its original strength. After upgrading, the biometric without
     * sufficient strength should not be able to perform the requested auth. For example,
     * +-------------------+------------------+--------------------+--------+
     * | Original Strength | Target Strength  | Requested Strength | Result |
     * +-------------------+------------------+--------------------+--------+
     * | BIOMETRIC_WEAK    | BIOMETRIC_STRONG | BIOMETRIC_STRONG   | Error  |
     * +-------------------+------------------+--------------------+--------+
     */
    @Test
    public void testBiometricStrengthUpgraded_WeakSensor() throws Exception {
        assumeTrue(Utils.isFirstApiLevel29orGreater());
        final List<Integer> sensors = getSensorsOfTargetStrength(SensorProperties.STRENGTH_WEAK);
        assumeTrue("testBiometricStrengthUpgraded_WeakSensor: numSensors: " + sensors.size(),
                sensors.size() > 0);

        // Tuple of originalStrength, targetStrength, and requestedStrength
        final int[][] testCases = {
                // Upgrade Weak to Strong, and request Strong auth
                {Authenticators.BIOMETRIC_WEAK, Authenticators.BIOMETRIC_STRONG,
                        Authenticators.BIOMETRIC_STRONG}
        };

        for (Integer sensorId : sensors) {
            for (int i = 0; i < testCases.length; i++) {
                testBiometricStrengthUpgraded_forSensor(sensorId,
                        testCases[i][0] /* originalStrength */,
                        testCases[i][1] /* targetStrength */,
                        testCases[i][2] /* requestedStrength */,
                        mSensorProperties.size() > 1 /* hasMultiSensors */);
            }
        }
    }

    /**
     * Trying to upgrade the strength of a Convenience biometric to a stronger strength will not
     * succeed (ie, it's no-op and the biometric strength is still Convenience), since the
     * biometric's actual strength can't go past its original strength. After upgrading, the
     * biometric without sufficient strength should not be able to perform the requested auth.
     * For example,
     * +-----------------------+------------------+--------------------+--------+
     * | Original Strength     | Target Strength  | Requested Strength | Result |
     * +-----------------------+------------------+--------------------+--------+
     * | BIOMETRIC_CONVENIENCE | BIOMETRIC_STRONG | BIOMETRIC_STRONG   | Error  |
     * +-----------------------+------------------+--------------------+--------+
     * | BIOMETRIC_CONVENIENCE | BIOMETRIC_STRONG | BIOMETRIC_WEAK     | Error  |
     * +-----------------------+------------------+--------------------+--------+
     * | BIOMETRIC_CONVENIENCE | BIOMETRIC_WEAK   | BIOMETRIC_WEAK     | Error  |
     * +-----------------------+------------------+--------------------+--------+
     */
    @Test
    public void testBiometricStrengthUpgraded_ConvenienceSensor() throws Exception {
        assumeTrue(Utils.isFirstApiLevel29orGreater());
        final List<Integer> sensors =
                getSensorsOfTargetStrength(SensorProperties.STRENGTH_CONVENIENCE);
        assumeTrue("testBiometricStrengthUpgraded_ConvenienceSensor: numSensors=" + sensors.size(),
                sensors.size() > 0);

        // Tuple of originalStrength, targetStrength, and requestedStrength
        final int[][] testCases = {
                // Upgrade Convenience to Strong, and request Strong auth
                {Authenticators.BIOMETRIC_CONVENIENCE, Authenticators.BIOMETRIC_STRONG,
                        Authenticators.BIOMETRIC_STRONG},

                // Upgrade Convenience to Strong, and request Weak auth
                {Authenticators.BIOMETRIC_CONVENIENCE, Authenticators.BIOMETRIC_STRONG,
                        Authenticators.BIOMETRIC_WEAK},

                // Upgrade Convenience to Weak, and request Weak auth
                {Authenticators.BIOMETRIC_CONVENIENCE, Authenticators.BIOMETRIC_WEAK,
                        Authenticators.BIOMETRIC_WEAK}
        };

        for (Integer sensorId : sensors) {
            for (int i = 0; i < testCases.length; i++) {
                testBiometricStrengthUpgraded_forSensor(sensorId,
                        testCases[i][0] /* originalStrength */,
                        testCases[i][1] /* targetStrength */,
                        testCases[i][2] /* requestedStrength */,
                        mSensorProperties.size() > 1 /* hasMultiSensors */);
            }
        }
    }

    private void testBiometricStrengthUpgraded_forSensor(int sensorId, int originalStrength,
            int targetStrength, int requestedStrength, boolean hasMultiSensors) throws Exception {
        Log.d(TAG, "testBiometricStrengthUpgraded_forSensor: "
                + "sensorId=" + sensorId
                + ", originalStrength=" + originalStrength
                + ", targetStrength=" + targetStrength
                + ", requestedStrength=" + requestedStrength
                + ", hasMultiSensors=" + hasMultiSensors);

        final ComponentName componentName = getComponentName(requestedStrength);

        // Reset to the original strength in case it's ever changed before the test
        updateStrengthAndVerify(sensorId, originalStrength);

        try (BiometricTestSession session = mBiometricManager.createTestSession(sensorId);
             ActivitySession activitySession = new ActivitySession(this, componentName)) {
            waitForAllUnenrolled();
            enrollForSensor(session, sensorId);
            final TestJournal journal =
                    TestJournalContainer.get(activitySession.getComponentName());

            // Try to upgrade the biometric strength to the target strength. The upgrading operation
            // is no-op since the biometric can't be upgraded past its original strength.
            updateStrengthAndIdle(sensorId, targetStrength);
            final int currentStrength = getCurrentStrength(sensorId);
            assertTrue("currentStrength: " + currentStrength, currentStrength == originalStrength);

            // After upgrading, check whether auth works
            // Error code should be returned
            int errCode = mBiometricManager.canAuthenticate(requestedStrength);
            checkErrCode("Device shouldn't allow auth with biometrics without sufficient strength."
                            + " errCode: " + errCode,
                    errCode, BiometricManager.BIOMETRIC_ERROR_NO_HARDWARE, hasMultiSensors);

            // Launch test activity
            launchActivityAndWaitForResumed(activitySession);

            // Auth shouldn't work and error code should be returned
            mInstrumentation.waitForIdleSync();
            BiometricCallbackHelper.State callbackState = getCallbackState(journal);
            assertNotNull(callbackState);
            assertEquals(callbackState.toString(), 0, callbackState.mNumAuthRejected);
            assertEquals(callbackState.toString(), 0, callbackState.mNumAuthAccepted);
            assertEquals(callbackState.toString(), 0, callbackState.mAcquiredReceived.size());
            assertEquals(callbackState.toString(), 1, callbackState.mErrorsReceived.size());
            checkErrCode(callbackState.toString(), (int) callbackState.mErrorsReceived.get(0),
                    BiometricManager.BIOMETRIC_ERROR_NO_HARDWARE, hasMultiSensors);
        }
    }

    private void checkErrCode(String msg, int errCode, int expectedErrCode,
            boolean hasMultiSensors) {
        if (!hasMultiSensors) {
            assertTrue(msg, errCode == expectedErrCode);
        } else {
            // In the multi-sensor case, error code for the first ineligible sensor may be
            // returned so the following error codes are accepted
            assertTrue(msg, errCode == expectedErrCode
                    || errCode == BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED
                    || errCode == BiometricManager.BIOMETRIC_ERROR_NO_HARDWARE);
        }
    }

    private static ComponentName getComponentName(int requestedStrength) {
        assertTrue("requestedStrength: " + requestedStrength,
                requestedStrength == Authenticators.BIOMETRIC_STRONG ||
                        requestedStrength == Authenticators.BIOMETRIC_WEAK);

        if (requestedStrength == Authenticators.BIOMETRIC_STRONG) {
            return CLASS_3_BIOMETRIC_ACTIVITY;
        }
        return CLASS_2_BIOMETRIC_ACTIVITY;
    }

    private static void revertSensorStrengths() throws Exception {
        Log.d(TAG, "revertSensorStrengths");
        Utils.executeShellCommand("device_config delete " +
                DEVICE_CONFIG_NAMESPACE + " " +
                DEVICE_CONFIG_BIO_STRENGTH_KEY);
        // this is probably not needed, but there are not too many tests so pause to ensure
        // the settings have settled
        Thread.sleep(1000);
    }

    private void updateStrengthAndVerify(int sensorId, int targetStrength) throws Exception {
        updateSensorStrength(sensorId, targetStrength, /* verify */ true);
    }

    private void updateStrengthAndIdle(int sensorId, int targetStrength) throws Exception {
        updateSensorStrength(sensorId, targetStrength, /* verify */ false);
    }

    private void updateSensorStrength(int sensorId, int targetStrength, boolean verify)
            throws Exception {
        Log.d(TAG, "updateStrength: update sensorId=" + sensorId + " to targetStrength="
                + targetStrength);
        Utils.executeShellCommand("device_config put " +
                DEVICE_CONFIG_NAMESPACE + " " +
                DEVICE_CONFIG_BIO_STRENGTH_KEY + " " +
                String.format("%s:%s", sensorId, targetStrength));

        final boolean matchesTarget = waitForSensorToBecomeStrength(sensorId, targetStrength);
        if (verify && !matchesTarget) {
            fail("Timed out waiting for sensorId " + sensorId + " to become target strength: "
                    + targetStrength);
        }
    }

    private boolean waitForSensorToBecomeStrength(int sensorId, int targetStrength)
            throws Exception {
        for (int i = 0; i < 20; i++) {
            final int currentStrength = getCurrentStrength(sensorId);
            if (currentStrength == targetStrength) {
                return true;
            }
            Log.d(TAG, "Not at target strength yet, current: " + currentStrength);
            Thread.sleep(300);
        }
        return false;
    }
}
