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
This commit is contained in:
Beverly
2022-09-01 00:50:02 +00:00
parent cb175195b8
commit 59ce3eac82
4 changed files with 340 additions and 32 deletions

View File

@@ -0,0 +1,89 @@
/*
* 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
}
}

View File

@@ -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,10 +1233,11 @@ public class KeyguardIndicationController {
boolean isStrongBiometric) {
super.onBiometricAuthenticated(userId, biometricSourceType, isStrongBiometric);
hideBiometricMessage();
if (biometricSourceType == FACE
&& !mKeyguardBypassController.canBypass()) {
showActionToUnlock();
if (biometricSourceType == FACE) {
mFaceAcquiredMessageDeferral.reset();
if (!mKeyguardBypassController.canBypass()) {
showActionToUnlock();
}
}
}
@@ -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)
);
}

View File

@@ -0,0 +1,147 @@
/*
* 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))
}
}

View File

@@ -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));