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

Commit a12fe48a authored by TreeHugger Robot's avatar TreeHugger Robot Committed by Android (Google) Code Review
Browse files

Merge changes from topic "too_dark_face_acquisition_message_enable" into tm-qpr-dev

* changes:
  Show low light soft-error on FACE_TIMEOUT
  [Co-ex] Show 'too_dark' face acquired messages
parents e04e312e 59ce3eac
Loading
Loading
Loading
Loading
+1 −1
Original line number Diff line number Diff line
@@ -1804,7 +1804,7 @@
    <!-- Message shown during face acquisition when the image is too bright [CHAR LIMIT=50] -->
    <string name="face_acquired_too_bright">Too bright. Try gentler lighting.</string>
    <!-- Message shown during face acquisition when the image is too dark [CHAR LIMIT=50] -->
    <string name="face_acquired_too_dark">Try brighter lighting</string>
    <string name="face_acquired_too_dark">Not enough light</string>
    <!-- Message shown during face acquisition when the user is too close to sensor [CHAR LIMIT=50] -->
    <string name="face_acquired_too_close">Move phone farther away</string>
    <!-- Message shown during face acquisition when the user is too far from sensor [CHAR LIMIT=50] -->
+3 −2
Original line number Diff line number Diff line
@@ -617,8 +617,9 @@
    <!-- Which face help messages to surface when fingerprint is also enrolled.
         Message ids correspond with the acquired ids in BiometricFaceConstants -->
    <integer-array name="config_face_help_msgs_when_fingerprint_enrolled">
        <item>25</item>
        <item>26</item>
        <item>3</item> <!-- TOO_DARK -->
        <item>25</item> <!-- DARK_GLASSES -->
        <item>26</item> <!-- MOUTH_COVERING_DETECTED -->
    </integer-array>

    <!-- Whether the communal service should be enabled -->
+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
    }
}
+74 −30
Original line number Diff line number Diff line
@@ -19,6 +19,10 @@ 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;
import static android.view.View.VISIBLE;
@@ -78,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;
@@ -178,7 +183,7 @@ public class KeyguardIndicationController {
    private boolean mBatteryPresent = true;
    private long mChargingTimeRemaining;
    private String mBiometricErrorMessageToShowOnScreenOn;
    private final Set<Integer> mCoExFaceHelpMsgIdsToShow;
    private final Set<Integer> mCoExFaceAcquisitionMsgIdsToShow;
    private boolean mInited;

    private KeyguardUpdateMonitorCallback mUpdateMonitorCallback;
@@ -249,11 +254,11 @@ public class KeyguardIndicationController {
        mScreenLifecycle = screenLifecycle;
        mScreenLifecycle.addObserver(mScreenObserver);

        mCoExFaceHelpMsgIdsToShow = new HashSet<>();
        mCoExFaceAcquisitionMsgIdsToShow = new HashSet<>();
        int[] msgIds = context.getResources().getIntArray(
                com.android.systemui.R.array.config_face_help_msgs_when_fingerprint_enrolled);
        for (int msgId : msgIds) {
            mCoExFaceHelpMsgIdsToShow.add(msgId);
            mCoExFaceAcquisitionMsgIdsToShow.add(msgId);
        }

        mHandler = new Handler(mainLooper) {
@@ -990,7 +995,7 @@ public class KeyguardIndicationController {
                mTopIndicationView == null ? null : mTopIndicationView.getText()));
        pw.println("  computePowerIndication(): " + computePowerIndication());
        pw.println("  trustGrantedIndication: " + getTrustGrantedIndication());
        pw.println("    mCoExFaceHelpMsgIdsToShow=" + mCoExFaceHelpMsgIdsToShow);
        pw.println("    mCoExFaceHelpMsgIdsToShow=" + mCoExFaceAcquisitionMsgIdsToShow);
        mRotateTextViewController.dump(pw, args);
    }

@@ -1048,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
@@ -1055,9 +1071,9 @@ public class KeyguardIndicationController {
            final boolean isUnlockWithFingerprintPossible =
                    mKeyguardUpdateMonitor.getCachedIsUnlockWithFingerprintPossible(
                            getCurrentUser());
            if (faceAuthSoftError
                    && isUnlockWithFingerprintPossible
                    && !mCoExFaceHelpMsgIdsToShow.contains(msgId)) {
            final boolean isCoExFaceAcquisitionMessage =
                    faceAuthSoftError && isUnlockWithFingerprintPossible;
            if (isCoExFaceAcquisitionMessage && !mCoExFaceAcquisitionMsgIdsToShow.contains(msgId)) {
                if (DEBUG) {
                    Log.d(TAG, "skip showing msgId=" + msgId + " helpString=" + helpString
                            + ", due to co-ex logic");
@@ -1067,7 +1083,12 @@ public class KeyguardIndicationController {
                mStatusBarKeyguardViewManager.showBouncerMessage(helpString,
                        mInitialTextColorState);
            } else if (mScreenLifecycle.getScreenState() == SCREEN_ON) {
                if (faceAuthFailed && isUnlockWithFingerprintPossible) {
                if (isCoExFaceAcquisitionMessage && msgId == FACE_ACQUIRED_TOO_DARK) {
                    showBiometricMessage(
                            helpString,
                            mContext.getString(R.string.keyguard_suggest_fingerprint)
                    );
                } else if (faceAuthFailed && isUnlockWithFingerprintPossible) {
                    showBiometricMessage(
                            mContext.getString(R.string.keyguard_face_failed),
                            mContext.getString(R.string.keyguard_suggest_fingerprint)
@@ -1090,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)
@@ -1134,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);
            }
@@ -1161,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);
        }


@@ -1200,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) {
@@ -1274,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))
    }
}
Loading