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

Commit 59ce3eac authored by Beverly's avatar Beverly
Browse files

Show low light soft-error on FACE_TIMEOUT

If it meets a threshold of 70% of the acquired
messages received by SysUI.

Test: atest KeyguardIndicationControllerTest
Test: atest BiometricMessageDeferralTest
Test: enroll face auth, go into a low-light area,
attempt face auth and see the "low light" message
Fixes: 244362700

Change-Id: I270c2459a89d087f9a9ea5de52a8c5179d9991ce
parent cb175195
Loading
Loading
Loading
Loading
+89 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2022 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.systemui.biometrics

/**
 * Provides whether an acquired error message should be shown immediately when its received (see
 * [shouldDefer]) or should be shown when the biometric error is received [getDeferredMessage].
 * @property excludedMessages messages that are excluded from counts
 * @property messagesToDefer messages that shouldn't show immediately when received, but may be
 * shown later if the message is the most frequent message processed and meets [THRESHOLD]
 * percentage of all messages (excluding [excludedMessages])
 */
class BiometricMessageDeferral(
    private val excludedMessages: Set<Int>,
    private val messagesToDefer: Set<Int>
) {
    private val msgCounts: MutableMap<Int, Int> = HashMap() // msgId => frequency of msg
    private val msgIdToCharSequence: MutableMap<Int, CharSequence> = HashMap() // msgId => message
    private var totalRelevantMessages = 0
    private var mostFrequentMsgIdToDefer: Int? = null

    /** Reset all saved counts. */
    fun reset() {
        totalRelevantMessages = 0
        msgCounts.clear()
        msgIdToCharSequence.clear()
    }

    /** Whether the given message should be deferred instead of being shown immediately. */
    fun shouldDefer(acquiredMsgId: Int): Boolean {
        return messagesToDefer.contains(acquiredMsgId)
    }

    /**
     * Adds the acquiredMsgId to the counts if it's not in [excludedMessages]. We still count
     * messages that shouldn't be deferred in these counts.
     */
    fun processMessage(acquiredMsgId: Int, helpString: CharSequence) {
        if (excludedMessages.contains(acquiredMsgId)) {
            return
        }

        totalRelevantMessages++
        msgIdToCharSequence[acquiredMsgId] = helpString

        val newAcquiredMsgCount = msgCounts.getOrDefault(acquiredMsgId, 0) + 1
        msgCounts[acquiredMsgId] = newAcquiredMsgCount
        if (
            messagesToDefer.contains(acquiredMsgId) &&
                (mostFrequentMsgIdToDefer == null ||
                    newAcquiredMsgCount > msgCounts.getOrDefault(mostFrequentMsgIdToDefer!!, 0))
        ) {
            mostFrequentMsgIdToDefer = acquiredMsgId
        }
    }

    /**
     * Get the most frequent deferred message that meets the [THRESHOLD] percentage of processed
     * messages excluding [excludedMessages].
     * @return null if no messages have been deferred OR deferred messages didn't meet the
     * [THRESHOLD] percentage of messages to show.
     */
    fun getDeferredMessage(): CharSequence? {
        mostFrequentMsgIdToDefer?.let {
            if (msgCounts.getOrDefault(it, 0) > (THRESHOLD * totalRelevantMessages)) {
                return msgIdToCharSequence[mostFrequentMsgIdToDefer]
            }
        }

        return null
    }
    companion object {
        const val THRESHOLD = .5f
    }
}
+60 −22
Original line number Diff line number Diff line
@@ -19,6 +19,9 @@ package com.android.systemui.statusbar;
import static android.app.admin.DevicePolicyManager.DEVICE_OWNER_TYPE_FINANCED;
import static android.app.admin.DevicePolicyResources.Strings.SystemUi.KEYGUARD_MANAGEMENT_DISCLOSURE;
import static android.app.admin.DevicePolicyResources.Strings.SystemUi.KEYGUARD_NAMED_MANAGEMENT_DISCLOSURE;
import static android.hardware.biometrics.BiometricFaceConstants.FACE_ACQUIRED_FIRST_FRAME_RECEIVED;
import static android.hardware.biometrics.BiometricFaceConstants.FACE_ACQUIRED_GOOD;
import static android.hardware.biometrics.BiometricFaceConstants.FACE_ACQUIRED_START;
import static android.hardware.biometrics.BiometricFaceConstants.FACE_ACQUIRED_TOO_DARK;
import static android.hardware.biometrics.BiometricSourceType.FACE;
import static android.view.View.GONE;
@@ -79,6 +82,7 @@ import com.android.keyguard.KeyguardUpdateMonitorCallback;
import com.android.settingslib.Utils;
import com.android.settingslib.fuelgauge.BatteryStatus;
import com.android.systemui.R;
import com.android.systemui.biometrics.BiometricMessageDeferral;
import com.android.systemui.broadcast.BroadcastDispatcher;
import com.android.systemui.dagger.SysUISingleton;
import com.android.systemui.dagger.qualifiers.Background;
@@ -1049,6 +1053,17 @@ public class KeyguardIndicationController {
                return;
            }

            if (biometricSourceType == FACE) {
                if (msgId == KeyguardUpdateMonitor.BIOMETRIC_HELP_FACE_NOT_RECOGNIZED) {
                    mFaceAcquiredMessageDeferral.reset();
                } else {
                    mFaceAcquiredMessageDeferral.processMessage(msgId, helpString);
                    if (mFaceAcquiredMessageDeferral.shouldDefer(msgId)) {
                        return;
                    }
                }
            }

            final boolean faceAuthSoftError = biometricSourceType == FACE
                    && msgId != BIOMETRIC_HELP_FACE_NOT_RECOGNIZED;
            final boolean faceAuthFailed = biometricSourceType == FACE
@@ -1096,34 +1111,44 @@ public class KeyguardIndicationController {
        @Override
        public void onBiometricError(int msgId, String errString,
                BiometricSourceType biometricSourceType) {
            if (shouldSuppressBiometricError(msgId, biometricSourceType, mKeyguardUpdateMonitor)) {
                return;
            CharSequence deferredFaceMessage = null;
            if (biometricSourceType == FACE) {
                deferredFaceMessage = mFaceAcquiredMessageDeferral.getDeferredMessage();
                mFaceAcquiredMessageDeferral.reset();
            }

            if (biometricSourceType == FACE
                    && msgId == FaceManager.FACE_ERROR_UNABLE_TO_PROCESS) {
                // suppress all face UNABLE_TO_PROCESS errors
            if (shouldSuppressBiometricError(msgId, biometricSourceType, mKeyguardUpdateMonitor)) {
                if (DEBUG) {
                    Log.d(TAG, "skip showing FACE_ERROR_UNABLE_TO_PROCESS errString="
                            + errString);
                    Log.d(TAG, "suppressingBiometricError msgId=" + msgId
                            + " source=" + biometricSourceType);
                }
            } else if (biometricSourceType == FACE
                    && msgId == FaceManager.FACE_ERROR_TIMEOUT) {
            } else if (biometricSourceType == FACE && msgId == FaceManager.FACE_ERROR_TIMEOUT) {
                // Co-ex: show deferred message OR nothing
                if (mKeyguardUpdateMonitor.getCachedIsUnlockWithFingerprintPossible(
                        getCurrentUser())) {
                    // no message if fingerprint is also enrolled
                        KeyguardUpdateMonitor.getCurrentUser())) {
                    // if we're on the lock screen (bouncer isn't showing), show the deferred msg
                    if (deferredFaceMessage != null
                            && !mStatusBarKeyguardViewManager.isBouncerShowing()) {
                        showBiometricMessage(
                                deferredFaceMessage,
                                mContext.getString(R.string.keyguard_suggest_fingerprint)
                        );
                        return;
                    }

                    // otherwise, don't show any message
                    if (DEBUG) {
                        Log.d(TAG, "skip showing FACE_ERROR_TIMEOUT due to co-ex logic");
                    }
                    return;
                }

                // The face timeout message is not very actionable, let's ask the user to
                // Face-only: The face timeout message is not very actionable, let's ask the user to
                // manually retry.
                if (mStatusBarKeyguardViewManager.isShowingAlternateAuth()) {
                    mStatusBarKeyguardViewManager.showBouncerMessage(
                            mContext.getResources().getString(R.string.keyguard_try_fingerprint),
                            mInitialTextColorState
                if (deferredFaceMessage != null) {
                    showBiometricMessage(
                            deferredFaceMessage,
                            mContext.getString(R.string.keyguard_unlock)
                    );
                } else {
                    // suggest swiping up to unlock (try face auth again or swipe up to bouncer)
@@ -1140,8 +1165,9 @@ public class KeyguardIndicationController {

        private boolean shouldSuppressBiometricError(int msgId,
                BiometricSourceType biometricSourceType, KeyguardUpdateMonitor updateMonitor) {
            if (biometricSourceType == BiometricSourceType.FINGERPRINT)
            if (biometricSourceType == BiometricSourceType.FINGERPRINT) {
                return shouldSuppressFingerprintError(msgId, updateMonitor);
            }
            if (biometricSourceType == FACE) {
                return shouldSuppressFaceError(msgId, updateMonitor);
            }
@@ -1167,7 +1193,8 @@ public class KeyguardIndicationController {
            // check of whether non-strong biometric is allowed
            return ((!updateMonitor.isUnlockingWithBiometricAllowed(true /* isStrongBiometric */)
                    && msgId != FaceManager.FACE_ERROR_LOCKOUT_PERMANENT)
                    || msgId == FaceManager.FACE_ERROR_CANCELED);
                    || msgId == FaceManager.FACE_ERROR_CANCELED
                    || msgId == FaceManager.FACE_ERROR_UNABLE_TO_PROCESS);
        }


@@ -1206,12 +1233,13 @@ public class KeyguardIndicationController {
                boolean isStrongBiometric) {
            super.onBiometricAuthenticated(userId, biometricSourceType, isStrongBiometric);
            hideBiometricMessage();

            if (biometricSourceType == FACE
                    && !mKeyguardBypassController.canBypass()) {
            if (biometricSourceType == FACE) {
                mFaceAcquiredMessageDeferral.reset();
                if (!mKeyguardBypassController.canBypass()) {
                    showActionToUnlock();
                }
            }
        }

        @Override
        public void onUserSwitchComplete(int userId) {
@@ -1280,4 +1308,14 @@ public class KeyguardIndicationController {
            }
        }
    };

    private final BiometricMessageDeferral mFaceAcquiredMessageDeferral =
            new BiometricMessageDeferral(
                    Set.of(
                            FACE_ACQUIRED_GOOD,
                            FACE_ACQUIRED_START,
                            FACE_ACQUIRED_FIRST_FRAME_RECEIVED
                    ),
                    Set.of(FACE_ACQUIRED_TOO_DARK)
            );
}
+147 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2022 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.systemui.biometrics

import android.testing.AndroidTestingRunner
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertNull
import org.junit.Assert.assertTrue
import org.junit.Test
import org.junit.runner.RunWith

@SmallTest
@RunWith(AndroidTestingRunner::class)
class BiometricMessageDeferralTest : SysuiTestCase() {

    @Test
    fun testProcessNoMessages_noDeferredMessage() {
        val biometricMessageDeferral = BiometricMessageDeferral(setOf(), setOf())

        assertNull(biometricMessageDeferral.getDeferredMessage())
    }

    @Test
    fun testProcessNonDeferredMessages_noDeferredMessage() {
        val biometricMessageDeferral = BiometricMessageDeferral(setOf(), setOf(1, 2))

        // WHEN there are no deferred messages processed
        for (i in 0..3) {
            biometricMessageDeferral.processMessage(4, "test")
        }

        // THEN getDeferredMessage is null
        assertNull(biometricMessageDeferral.getDeferredMessage())
    }

    @Test
    fun testAllProcessedMessagesWereDeferred() {
        val biometricMessageDeferral = BiometricMessageDeferral(setOf(), setOf(1))

        // WHEN all the processed messages are a deferred message
        for (i in 0..3) {
            biometricMessageDeferral.processMessage(1, "test")
        }

        // THEN deferredMessage will return the string associated with the deferred msgId
        assertEquals("test", biometricMessageDeferral.getDeferredMessage())
    }

    @Test
    fun testReturnsMostFrequentDeferredMessage() {
        val biometricMessageDeferral = BiometricMessageDeferral(setOf(), setOf(1, 2))

        // WHEN there's two msgId=1 processed and one msgId=2 processed
        biometricMessageDeferral.processMessage(1, "msgId-1")
        biometricMessageDeferral.processMessage(1, "msgId-1")
        biometricMessageDeferral.processMessage(1, "msgId-1")
        biometricMessageDeferral.processMessage(2, "msgId-2")

        // THEN the most frequent deferred message is that meets the threshold is returned
        assertEquals("msgId-1", biometricMessageDeferral.getDeferredMessage())
    }

    @Test
    fun testDeferredMessage_mustMeetThreshold() {
        val biometricMessageDeferral = BiometricMessageDeferral(setOf(), setOf(1))

        // WHEN more nonDeferredMessages are shown than the deferred message
        val totalMessages = 10
        val nonDeferredMessagesCount =
            (totalMessages * BiometricMessageDeferral.THRESHOLD).toInt() + 1
        for (i in 0 until nonDeferredMessagesCount) {
            biometricMessageDeferral.processMessage(4, "non-deferred-msg")
        }
        for (i in nonDeferredMessagesCount until totalMessages) {
            biometricMessageDeferral.processMessage(1, "msgId-1")
        }

        // THEN there's no deferred message because it didn't meet the threshold
        assertNull(biometricMessageDeferral.getDeferredMessage())
    }

    @Test
    fun testDeferredMessage_manyExcludedMessages_getDeferredMessage() {
        val biometricMessageDeferral = BiometricMessageDeferral(setOf(3), setOf(1))

        // WHEN more excludedMessages are shown than the deferred message
        val totalMessages = 10
        val excludedMessagesCount = (totalMessages * BiometricMessageDeferral.THRESHOLD).toInt() + 1
        for (i in 0 until excludedMessagesCount) {
            biometricMessageDeferral.processMessage(3, "excluded-msg")
        }
        for (i in excludedMessagesCount until totalMessages) {
            biometricMessageDeferral.processMessage(1, "msgId-1")
        }

        // THEN there IS a deferred message because the deferred msg meets the threshold amongst the
        // non-excluded messages
        assertEquals("msgId-1", biometricMessageDeferral.getDeferredMessage())
    }

    @Test
    fun testResetClearsOutCounts() {
        val biometricMessageDeferral = BiometricMessageDeferral(setOf(), setOf(1, 2))

        // GIVEN two msgId=1 events processed
        biometricMessageDeferral.processMessage(1, "msgId-1")
        biometricMessageDeferral.processMessage(1, "msgId-1")

        // WHEN counts are reset and then a single deferred message is processed (msgId=2)
        biometricMessageDeferral.reset()
        biometricMessageDeferral.processMessage(2, "msgId-2")

        // THEN msgId-2 is the deferred message since the two msgId=1 events were reset
        assertEquals("msgId-2", biometricMessageDeferral.getDeferredMessage())
    }

    @Test
    fun testShouldDefer() {
        // GIVEN should defer msgIds 1 and 2
        val biometricMessageDeferral = BiometricMessageDeferral(setOf(3), setOf(1, 2))

        // THEN shouldDefer returns true for ids 1 & 2
        assertTrue(biometricMessageDeferral.shouldDefer(1))
        assertTrue(biometricMessageDeferral.shouldDefer(2))

        // THEN should defer returns false for ids 3 & 4
        assertFalse(biometricMessageDeferral.shouldDefer(3))
        assertFalse(biometricMessageDeferral.shouldDefer(4))
    }
}
+44 −10
Original line number Diff line number Diff line
@@ -19,6 +19,7 @@ package com.android.systemui.statusbar;
import static android.app.admin.DevicePolicyManager.DEVICE_OWNER_TYPE_DEFAULT;
import static android.app.admin.DevicePolicyManager.DEVICE_OWNER_TYPE_FINANCED;
import static android.content.pm.UserInfo.FLAG_MANAGED_PROFILE;
import static android.hardware.biometrics.BiometricFaceConstants.FACE_ERROR_TIMEOUT;

import static com.android.keyguard.KeyguardUpdateMonitor.BIOMETRIC_HELP_FINGERPRINT_NOT_RECOGNIZED;
import static com.android.systemui.keyguard.KeyguardIndicationRotateTextViewController.INDICATION_TYPE_ALIGNMENT;
@@ -65,7 +66,6 @@ import android.content.pm.UserInfo;
import android.graphics.Color;
import android.hardware.biometrics.BiometricFaceConstants;
import android.hardware.biometrics.BiometricSourceType;
import android.hardware.face.FaceManager;
import android.hardware.fingerprint.FingerprintManager;
import android.os.BatteryManager;
import android.os.Looper;
@@ -602,7 +602,7 @@ public class KeyguardIndicationControllerTest extends SysuiTestCase {
        String message = mContext.getString(R.string.keyguard_unlock);

        mController.setVisible(true);
        mController.getKeyguardCallback().onBiometricError(FaceManager.FACE_ERROR_TIMEOUT,
        mController.getKeyguardCallback().onBiometricError(FACE_ERROR_TIMEOUT,
                "A message", BiometricSourceType.FACE);

        verifyIndicationMessage(INDICATION_TYPE_BIOMETRIC_MESSAGE, message);
@@ -636,7 +636,7 @@ public class KeyguardIndicationControllerTest extends SysuiTestCase {
        when(mKeyguardUpdateMonitor.isFaceEnrolled()).thenReturn(true);

        mController.setVisible(true);
        mController.getKeyguardCallback().onBiometricError(FaceManager.FACE_ERROR_TIMEOUT,
        mController.getKeyguardCallback().onBiometricError(FACE_ERROR_TIMEOUT,
                "A message", BiometricSourceType.FACE);

        verify(mStatusBarKeyguardViewManager).showBouncerMessage(eq(message), any());
@@ -651,7 +651,7 @@ public class KeyguardIndicationControllerTest extends SysuiTestCase {

        mController.setVisible(true);
        mController.getKeyguardCallback().onBiometricError(
                FaceManager.FACE_ERROR_TIMEOUT, message, BiometricSourceType.FACE);
                FACE_ERROR_TIMEOUT, message, BiometricSourceType.FACE);
        verifyNoMessage(INDICATION_TYPE_BIOMETRIC_MESSAGE);
    }

@@ -667,8 +667,7 @@ public class KeyguardIndicationControllerTest extends SysuiTestCase {
        final String helpString = "helpString";
        final int[] msgIds = new int[]{
                BiometricFaceConstants.FACE_ACQUIRED_MOUTH_COVERING_DETECTED,
                BiometricFaceConstants.FACE_ACQUIRED_DARK_GLASSES_DETECTED,
                BiometricFaceConstants.FACE_ACQUIRED_TOO_DARK
                BiometricFaceConstants.FACE_ACQUIRED_DARK_GLASSES_DETECTED
        };
        Set<CharSequence> messages = new HashSet<>();
        for (int msgId : msgIds) {
@@ -728,8 +727,7 @@ public class KeyguardIndicationControllerTest extends SysuiTestCase {
                BiometricFaceConstants.FACE_ACQUIRED_TOO_LEFT,
                BiometricFaceConstants.FACE_ACQUIRED_TOO_HIGH,
                BiometricFaceConstants.FACE_ACQUIRED_TOO_LOW,
                BiometricFaceConstants.FACE_ACQUIRED_TOO_BRIGHT,
                BiometricFaceConstants.FACE_ACQUIRED_TOO_DARK
                BiometricFaceConstants.FACE_ACQUIRED_TOO_BRIGHT
        };
        for (int msgId : msgIds) {
            final String numberedHelpString = helpString + msgId;
@@ -743,7 +741,36 @@ public class KeyguardIndicationControllerTest extends SysuiTestCase {
    }

    @Test
    public void sendTooDarkFaceHelpMessages_fingerprintEnrolled() {
    public void sendTooDarkFaceHelpMessages_onTimeout_noFpEnrolled() {
        createController();

        // GIVEN fingerprint NOT enrolled
        when(mKeyguardUpdateMonitor.getCachedIsUnlockWithFingerprintPossible(
                0)).thenReturn(false);

        // WHEN help message received
        final String helpString = "helpMsg";
        mKeyguardUpdateMonitorCallback.onBiometricHelp(
                BiometricFaceConstants.FACE_ACQUIRED_TOO_DARK,
                helpString,
                BiometricSourceType.FACE
        );

        // THEN help message not shown yet
        verifyNoMessage(INDICATION_TYPE_BIOMETRIC_MESSAGE);

        // WHEN face timeout error received
        mKeyguardUpdateMonitorCallback.onBiometricError(FACE_ERROR_TIMEOUT, "face timeout",
                BiometricSourceType.FACE);

        // THEN the low light message shows with suggestion to swipe up to unlock
        verifyIndicationMessage(INDICATION_TYPE_BIOMETRIC_MESSAGE, helpString);
        verifyIndicationMessage(INDICATION_TYPE_BIOMETRIC_MESSAGE_FOLLOW_UP,
                mContext.getString(R.string.keyguard_unlock));
    }

    @Test
    public void sendTooDarkFaceHelpMessages_onTimeout_fingerprintEnrolled() {
        createController();

        // GIVEN fingerprint enrolled
@@ -758,7 +785,14 @@ public class KeyguardIndicationControllerTest extends SysuiTestCase {
                BiometricSourceType.FACE
        );

        // THEN help message shown and try fingerprint message shown
        // THEN help message not shown yet
        verifyNoMessage(INDICATION_TYPE_BIOMETRIC_MESSAGE);

        // WHEN face timeout error received
        mKeyguardUpdateMonitorCallback.onBiometricError(FACE_ERROR_TIMEOUT, "face timeout",
                BiometricSourceType.FACE);

        // THEN the low light message shows and suggests trying fingerprint
        verifyIndicationMessage(INDICATION_TYPE_BIOMETRIC_MESSAGE, helpString);
        verifyIndicationMessage(INDICATION_TYPE_BIOMETRIC_MESSAGE_FOLLOW_UP,
                mContext.getString(R.string.keyguard_suggest_fingerprint));