diff options
5 files changed, 232 insertions, 32 deletions
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/FaceHelpMessageDebouncerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/FaceHelpMessageDebouncerTest.kt index baef620ad556..a36b0bca42b5 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/FaceHelpMessageDebouncerTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/FaceHelpMessageDebouncerTest.kt @@ -218,4 +218,60 @@ class FaceHelpMessageDebouncerTest : SysuiTestCase() { assertThat(underTest.getMessageToShow(startWindow)?.msgId) .isEqualTo(BiometricFaceConstants.FACE_ACQUIRED_TOO_CLOSE) } + + @Test + fun messageMustMeetThreshold() { + underTest = + FaceHelpMessageDebouncer( + window = window, + startWindow = 0, + shownFaceMessageFrequencyBoost = 0, + threshold = .8f, + ) + + underTest.addMessage( + HelpFaceAuthenticationStatus( + BiometricFaceConstants.FACE_ACQUIRED_TOO_CLOSE, + "tooClose", + 0 + ) + ) + underTest.addMessage( + HelpFaceAuthenticationStatus( + BiometricFaceConstants.FACE_ACQUIRED_TOO_CLOSE, + "tooClose", + 0 + ) + ) + underTest.addMessage( + HelpFaceAuthenticationStatus( + BiometricFaceConstants.FACE_ACQUIRED_TOO_BRIGHT, + "tooBright", + 0 + ) + ) + + // although tooClose message is the majority, it doesn't meet the 80% threshold + assertThat(underTest.getMessageToShow(startWindow)).isNull() + + underTest.addMessage( + HelpFaceAuthenticationStatus( + BiometricFaceConstants.FACE_ACQUIRED_TOO_CLOSE, + "tooClose", + 0 + ) + ) + underTest.addMessage( + HelpFaceAuthenticationStatus( + BiometricFaceConstants.FACE_ACQUIRED_TOO_CLOSE, + "tooClose", + 0 + ) + ) + + // message shows once it meets the threshold + assertThat(underTest.getMessageToShow(startWindow)?.msgId) + .isEqualTo(BiometricFaceConstants.FACE_ACQUIRED_TOO_CLOSE) + assertThat(underTest.getMessageToShow(startWindow)?.msg).isEqualTo("tooClose") + } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/FaceHelpMessageDeferralTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/FaceHelpMessageDeferralTest.kt index b31f6f5b096b..add7a7fbb8a1 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/FaceHelpMessageDeferralTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/FaceHelpMessageDeferralTest.kt @@ -16,11 +16,15 @@ package com.android.systemui.biometrics -import androidx.test.ext.junit.runners.AndroidJUnit4 +import android.platform.test.annotations.DisableFlags +import android.platform.test.annotations.EnableFlags +import android.platform.test.flag.junit.FlagsParameterization import androidx.test.filters.SmallTest import com.android.keyguard.logging.BiometricMessageDeferralLogger +import com.android.systemui.Flags import com.android.systemui.SysuiTestCase import com.android.systemui.dump.DumpManager +import com.android.systemui.util.time.FakeSystemClock import org.junit.Assert.assertEquals import org.junit.Assert.assertFalse import org.junit.Assert.assertNull @@ -31,14 +35,29 @@ import org.junit.runner.RunWith import org.mockito.Mock import org.mockito.Mockito.verify import org.mockito.MockitoAnnotations +import platform.test.runner.parameterized.ParameterizedAndroidJunit4 +import platform.test.runner.parameterized.Parameters @SmallTest -@RunWith(AndroidJUnit4::class) +@RunWith(ParameterizedAndroidJunit4::class) @android.platform.test.annotations.EnabledOnRavenwood -class FaceHelpMessageDeferralTest : SysuiTestCase() { +class FaceHelpMessageDeferralTest(flags: FlagsParameterization) : SysuiTestCase() { val threshold = .75f @Mock lateinit var logger: BiometricMessageDeferralLogger @Mock lateinit var dumpManager: DumpManager + val systemClock = FakeSystemClock() + + companion object { + @JvmStatic + @Parameters(name = "{0}") + fun getParams(): List<FlagsParameterization> { + return FlagsParameterization.allCombinationsOf(Flags.FLAG_FACE_MESSAGE_DEFER_UPDATE) + } + } + + init { + mSetFlagsRule.setFlagsParameterization(flags) + } @Before fun setUp() { @@ -111,10 +130,11 @@ class FaceHelpMessageDeferralTest : SysuiTestCase() { } @Test + @DisableFlags(Flags.FLAG_FACE_MESSAGE_DEFER_UPDATE) fun testReturnsMostFrequentDeferredMessage() { val biometricMessageDeferral = createMsgDeferral(setOf(1, 2)) - // WHEN there's 80%of the messages are msgId=1 and 20% is msgId=2 + // WHEN there's 80% of the messages are msgId=1 and 20% is msgId=2 biometricMessageDeferral.processFrame(1) biometricMessageDeferral.processFrame(1) biometricMessageDeferral.processFrame(1) @@ -124,7 +144,41 @@ class FaceHelpMessageDeferralTest : SysuiTestCase() { biometricMessageDeferral.processFrame(2) biometricMessageDeferral.updateMessage(2, "msgId-2") - // THEN the most frequent deferred message is that meets the threshold is returned + // THEN the most frequent deferred message that meets the threshold is returned + assertEquals("msgId-1", biometricMessageDeferral.getDeferredMessage()) + } + + @Test + @EnableFlags(Flags.FLAG_FACE_MESSAGE_DEFER_UPDATE) + fun testReturnsMostFrequentDeferredMessage_onlyAnalyzesLastNWindow() { + val biometricMessageDeferral = createMsgDeferral(setOf(1, 2)) + + // WHEN there's 80% of the messages are msgId=1 and 20% is msgId=2, but the last + // N window only contains messages with msgId=2 + repeat(80) { biometricMessageDeferral.processFrame(1) } + biometricMessageDeferral.updateMessage(1, "msgId-1") + systemClock.setElapsedRealtime(systemClock.elapsedRealtime() + 501L) + repeat(20) { biometricMessageDeferral.processFrame(2) } + biometricMessageDeferral.updateMessage(2, "msgId-2") + + // THEN the most frequent deferred message in the last N window (500L) is returned + assertEquals("msgId-2", biometricMessageDeferral.getDeferredMessage()) + } + + @Test + @DisableFlags(Flags.FLAG_FACE_MESSAGE_DEFER_UPDATE) + fun testReturnsMostFrequentDeferredMessage_analyzesAllFrames() { + val biometricMessageDeferral = createMsgDeferral(setOf(1, 2)) + + // WHEN there's 80% of the messages are msgId=1 and 20% is msgId=2, but the last + // N window only contains messages with msgId=2 + repeat(80) { biometricMessageDeferral.processFrame(1) } + biometricMessageDeferral.updateMessage(1, "msgId-1") + systemClock.setElapsedRealtime(systemClock.elapsedRealtime() + 501L) + repeat(20) { biometricMessageDeferral.processFrame(2) } + biometricMessageDeferral.updateMessage(2, "msgId-2") + + // THEN the most frequent deferred message is returned assertEquals("msgId-1", biometricMessageDeferral.getDeferredMessage()) } @@ -213,14 +267,17 @@ class FaceHelpMessageDeferralTest : SysuiTestCase() { private fun createMsgDeferral( messagesToDefer: Set<Int>, acquiredInfoToIgnore: Set<Int> = emptySet(), + windowToAnalyzeLastNFrames: Long = 500L, ): BiometricMessageDeferral { return BiometricMessageDeferral( - messagesToDefer, - acquiredInfoToIgnore, - threshold, - logger, - dumpManager, - "0", + messagesToDefer = messagesToDefer, + acquiredInfoToIgnore = acquiredInfoToIgnore, + threshold = threshold, + windowToAnalyzeLastNFrames = windowToAnalyzeLastNFrames, + logBuffer = logger, + dumpManager = dumpManager, + id = "0", + systemClock = { systemClock }, ) } } diff --git a/packages/SystemUI/res/values/config.xml b/packages/SystemUI/res/values/config.xml index 823ff9f54be3..e8fd2ef6eafa 100644 --- a/packages/SystemUI/res/values/config.xml +++ b/packages/SystemUI/res/values/config.xml @@ -727,6 +727,10 @@ .75 </item> + <!-- The last x ms of face acquired info messages to analyze to determine + whether to show a deferred face auth help message. --> + <integer name="config_face_help_msgs_defer_analyze_timeframe">500</integer> + <!-- 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"> diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/FaceHelpMessageDebouncer.kt b/packages/SystemUI/src/com/android/systemui/biometrics/FaceHelpMessageDebouncer.kt index 1685f49e4f3e..4731ebba7124 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/FaceHelpMessageDebouncer.kt +++ b/packages/SystemUI/src/com/android/systemui/biometrics/FaceHelpMessageDebouncer.kt @@ -25,11 +25,13 @@ import com.android.systemui.deviceentry.shared.model.HelpFaceAuthenticationStatu * - startWindow: Window of time on start required before showing the first help message * - shownFaceMessageFrequencyBoost: Frequency boost given to messages that are currently shown to * the user + * - threshold: minimum percentage of frames a message must appear in order to show it */ class FaceHelpMessageDebouncer( private val window: Long = DEFAULT_WINDOW_MS, private val startWindow: Long = window, private val shownFaceMessageFrequencyBoost: Int = 4, + private val threshold: Float = 0f, ) { private val TAG = "FaceHelpMessageDebouncer" private var startTime = 0L @@ -56,7 +58,7 @@ class FaceHelpMessageDebouncer( } } - private fun getMostFrequentHelpMessage(): HelpFaceAuthenticationStatus? { + private fun getMostFrequentHelpMessageSurpassingThreshold(): HelpFaceAuthenticationStatus? { // freqMap: msgId => frequency val freqMap = helpFaceAuthStatuses.groupingBy { it.msgId }.eachCount().toMutableMap() @@ -83,7 +85,25 @@ class FaceHelpMessageDebouncer( } } ?.key - return helpFaceAuthStatuses.findLast { it.msgId == msgIdWithHighestFrequency } + + if (msgIdWithHighestFrequency == null) { + return null + } + + val freq = + if (msgIdWithHighestFrequency == lastMessageIdShown) { + freqMap[msgIdWithHighestFrequency]!! - shownFaceMessageFrequencyBoost + } else { + freqMap[msgIdWithHighestFrequency]!! + } + .toFloat() + + return if ((freq / helpFaceAuthStatuses.size.toFloat()) >= threshold) { + helpFaceAuthStatuses.findLast { it.msgId == msgIdWithHighestFrequency } + } else { + Log.v(TAG, "most frequent helpFaceAuthStatus didn't make the threshold: $threshold") + null + } } fun addMessage(helpFaceAuthStatus: HelpFaceAuthenticationStatus) { @@ -98,14 +118,15 @@ class FaceHelpMessageDebouncer( return null } removeOldMessages(atTimestamp) - val messageToShow = getMostFrequentHelpMessage() + val messageToShow = getMostFrequentHelpMessageSurpassingThreshold() if (lastMessageIdShown != messageToShow?.msgId) { Log.v( TAG, "showMessage previousLastMessageId=$lastMessageIdShown" + "\n\tmessageToShow=$messageToShow " + "\n\thelpFaceAuthStatusesSize=${helpFaceAuthStatuses.size}" + - "\n\thelpFaceAuthStatuses=$helpFaceAuthStatuses" + "\n\thelpFaceAuthStatuses=$helpFaceAuthStatuses" + + "\n\tthreshold=$threshold" ) lastMessageIdShown = messageToShow?.msgId } diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/FaceHelpMessageDeferral.kt b/packages/SystemUI/src/com/android/systemui/biometrics/FaceHelpMessageDeferral.kt index 90d06fb0bec1..d382adaff955 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/FaceHelpMessageDeferral.kt +++ b/packages/SystemUI/src/com/android/systemui/biometrics/FaceHelpMessageDeferral.kt @@ -17,14 +17,19 @@ package com.android.systemui.biometrics import android.content.res.Resources +import android.os.SystemClock.elapsedRealtime import com.android.keyguard.logging.BiometricMessageDeferralLogger import com.android.systemui.Dumpable +import com.android.systemui.Flags.faceMessageDeferUpdate import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Main +import com.android.systemui.deviceentry.shared.model.HelpFaceAuthenticationStatus import com.android.systemui.dump.DumpManager import com.android.systemui.log.LogBuffer import com.android.systemui.log.dagger.BiometricLog import com.android.systemui.res.R +import com.android.systemui.util.time.SystemClock +import dagger.Lazy import java.io.PrintWriter import java.util.Objects import java.util.UUID @@ -36,7 +41,8 @@ class FaceHelpMessageDeferralFactory constructor( @Main private val resources: Resources, @BiometricLog private val logBuffer: LogBuffer, - private val dumpManager: DumpManager + private val dumpManager: DumpManager, + private val systemClock: Lazy<SystemClock>, ) { fun create(): FaceHelpMessageDeferral { val id = UUID.randomUUID().toString() @@ -45,6 +51,7 @@ constructor( logBuffer = BiometricMessageDeferralLogger(logBuffer, "FaceHelpMessageDeferral[$id]"), dumpManager = dumpManager, id = id, + systemClock, ) } } @@ -58,14 +65,17 @@ class FaceHelpMessageDeferral( logBuffer: BiometricMessageDeferralLogger, dumpManager: DumpManager, val id: String, + val systemClock: Lazy<SystemClock>, ) : BiometricMessageDeferral( resources.getIntArray(R.array.config_face_help_msgs_defer_until_timeout).toHashSet(), resources.getIntArray(R.array.config_face_help_msgs_ignore).toHashSet(), resources.getFloat(R.dimen.config_face_help_msgs_defer_until_timeout_threshold), + resources.getInteger(R.integer.config_face_help_msgs_defer_analyze_timeframe).toLong(), logBuffer, dumpManager, id, + systemClock, ) /** @@ -77,10 +87,24 @@ open class BiometricMessageDeferral( private val messagesToDefer: Set<Int>, private val acquiredInfoToIgnore: Set<Int>, private val threshold: Float, + private val windowToAnalyzeLastNFrames: Long, private val logBuffer: BiometricMessageDeferralLogger, dumpManager: DumpManager, id: String, + private val systemClock: Lazy<SystemClock>, ) : Dumpable { + + private val faceHelpMessageDebouncer: FaceHelpMessageDebouncer? = + if (faceMessageDeferUpdate()) { + FaceHelpMessageDebouncer( + window = windowToAnalyzeLastNFrames, + startWindow = 0L, + shownFaceMessageFrequencyBoost = 0, + threshold = threshold, + ) + } else { + null + } private val acquiredInfoToFrequency: MutableMap<Int, Int> = HashMap() private val acquiredInfoToHelpString: MutableMap<Int, String> = HashMap() private var mostFrequentAcquiredInfoToDefer: Int? = null @@ -97,13 +121,20 @@ open class BiometricMessageDeferral( pw.println("messagesToDefer=$messagesToDefer") pw.println("totalFrames=$totalFrames") pw.println("threshold=$threshold") + pw.println("faceMessageDeferUpdateFlagEnabled=${faceMessageDeferUpdate()}") + if (faceMessageDeferUpdate()) { + pw.println("windowToAnalyzeLastNFrames(ms)=$windowToAnalyzeLastNFrames") + } } /** Reset all saved counts. */ fun reset() { totalFrames = 0 - mostFrequentAcquiredInfoToDefer = null - acquiredInfoToFrequency.clear() + if (!faceMessageDeferUpdate()) { + mostFrequentAcquiredInfoToDefer = null + acquiredInfoToFrequency.clear() + } + acquiredInfoToHelpString.clear() logBuffer.reset() } @@ -137,24 +168,48 @@ open class BiometricMessageDeferral( logBuffer.logFrameIgnored(acquiredInfo) return } - totalFrames++ - val newAcquiredInfoCount = acquiredInfoToFrequency.getOrDefault(acquiredInfo, 0) + 1 - acquiredInfoToFrequency[acquiredInfo] = newAcquiredInfoCount - if ( - messagesToDefer.contains(acquiredInfo) && - (mostFrequentAcquiredInfoToDefer == null || - newAcquiredInfoCount > - acquiredInfoToFrequency.getOrDefault(mostFrequentAcquiredInfoToDefer!!, 0)) - ) { - mostFrequentAcquiredInfoToDefer = acquiredInfo + if (faceMessageDeferUpdate()) { + faceHelpMessageDebouncer?.let { + val helpFaceAuthStatus = + HelpFaceAuthenticationStatus( + msgId = acquiredInfo, + msg = null, + systemClock.get().elapsedRealtime() + ) + if (totalFrames == 1) { // first frame + it.startNewFaceAuthSession(helpFaceAuthStatus.createdAt) + } + it.addMessage(helpFaceAuthStatus) + } + } else { + val newAcquiredInfoCount = acquiredInfoToFrequency.getOrDefault(acquiredInfo, 0) + 1 + acquiredInfoToFrequency[acquiredInfo] = newAcquiredInfoCount + if ( + messagesToDefer.contains(acquiredInfo) && + (mostFrequentAcquiredInfoToDefer == null || + newAcquiredInfoCount > + acquiredInfoToFrequency.getOrDefault( + mostFrequentAcquiredInfoToDefer!!, + 0 + )) + ) { + mostFrequentAcquiredInfoToDefer = acquiredInfo + } } logBuffer.logFrameProcessed( acquiredInfo, totalFrames, - mostFrequentAcquiredInfoToDefer?.toString() + if (faceMessageDeferUpdate()) { + faceHelpMessageDebouncer + ?.getMessageToShow(systemClock.get().elapsedRealtime()) + ?.msgId + .toString() + } else { + mostFrequentAcquiredInfoToDefer?.toString() + } ) } @@ -166,9 +221,16 @@ open class BiometricMessageDeferral( * [threshold] percentage. */ fun getDeferredMessage(): CharSequence? { - mostFrequentAcquiredInfoToDefer?.let { - if (acquiredInfoToFrequency.getOrDefault(it, 0) > (threshold * totalFrames)) { - return acquiredInfoToHelpString[it] + if (faceMessageDeferUpdate()) { + faceHelpMessageDebouncer?.let { + val helpFaceAuthStatus = it.getMessageToShow(systemClock.get().elapsedRealtime()) + return acquiredInfoToHelpString[helpFaceAuthStatus?.msgId] + } + } else { + mostFrequentAcquiredInfoToDefer?.let { + if (acquiredInfoToFrequency.getOrDefault(it, 0) > (threshold * totalFrames)) { + return acquiredInfoToHelpString[it] + } } } return null |