1 /* <lambda>null2 * Copyright (C) 2024 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.systemui.biometrics 18 19 import android.util.Log 20 import com.android.systemui.deviceentry.shared.model.HelpFaceAuthenticationStatus 21 22 /** 23 * Debounces face help messages with parameters: 24 * - window: Window of time (in milliseconds) to analyze face acquired messages) 25 * - startWindow: Window of time on start required before showing the first help message 26 * - shownFaceMessageFrequencyBoost: Frequency boost given to messages that are currently shown to 27 * the user 28 * - threshold: minimum percentage of frames a message must appear in order to show it 29 */ 30 class FaceHelpMessageDebouncer( 31 private val window: Long = DEFAULT_WINDOW_MS, 32 private val startWindow: Long = window, 33 private val shownFaceMessageFrequencyBoost: Int = 4, 34 private val threshold: Float = 0f, 35 ) { 36 private val TAG = "FaceHelpMessageDebouncer" 37 private var startTime = 0L 38 private var helpFaceAuthStatuses: MutableList<HelpFaceAuthenticationStatus> = mutableListOf() 39 private var lastMessageIdShown: Int? = null 40 41 /** Remove messages that are outside of the time [window]. */ 42 private fun removeOldMessages(currTimestamp: Long) { 43 var numToRemove = 0 44 // This works under the assumption that timestamps are ordered from first to last 45 // in chronological order 46 for (index in helpFaceAuthStatuses.indices) { 47 if ((helpFaceAuthStatuses[index].createdAt + window) >= currTimestamp) { 48 break // all timestamps from here and on are within the window 49 } 50 numToRemove += 1 51 } 52 53 // Remove all outside time window 54 repeat(numToRemove) { helpFaceAuthStatuses.removeFirst() } 55 56 if (numToRemove > 0) { 57 Log.v(TAG, "removedFirst=$numToRemove") 58 } 59 } 60 61 private fun getMostFrequentHelpMessageSurpassingThreshold(): HelpFaceAuthenticationStatus? { 62 // freqMap: msgId => frequency 63 val freqMap = helpFaceAuthStatuses.groupingBy { it.msgId }.eachCount().toMutableMap() 64 65 // Give shownFaceMessageFrequencyBoost to lastMessageIdShown 66 if (lastMessageIdShown != null) { 67 freqMap.computeIfPresent(lastMessageIdShown!!) { _, value -> 68 value + shownFaceMessageFrequencyBoost 69 } 70 } 71 // Go through all msgId keys & find the highest frequency msgId 72 val msgIdWithHighestFrequency = 73 freqMap.entries 74 .maxWithOrNull { (msgId1, freq1), (msgId2, freq2) -> 75 // ties are broken by more recent message 76 if (freq1 == freq2) { 77 helpFaceAuthStatuses 78 .findLast { it.msgId == msgId1 }!! 79 .createdAt 80 .compareTo( 81 helpFaceAuthStatuses.findLast { it.msgId == msgId2 }!!.createdAt 82 ) 83 } else { 84 freq1.compareTo(freq2) 85 } 86 } 87 ?.key 88 89 if (msgIdWithHighestFrequency == null) { 90 return null 91 } 92 93 val freq = 94 if (msgIdWithHighestFrequency == lastMessageIdShown) { 95 freqMap[msgIdWithHighestFrequency]!! - shownFaceMessageFrequencyBoost 96 } else { 97 freqMap[msgIdWithHighestFrequency]!! 98 } 99 .toFloat() 100 101 return if ((freq / helpFaceAuthStatuses.size.toFloat()) >= threshold) { 102 helpFaceAuthStatuses.findLast { it.msgId == msgIdWithHighestFrequency } 103 } else { 104 Log.v(TAG, "most frequent helpFaceAuthStatus didn't make the threshold: $threshold") 105 null 106 } 107 } 108 109 fun addMessage(helpFaceAuthStatus: HelpFaceAuthenticationStatus) { 110 helpFaceAuthStatuses.add(helpFaceAuthStatus) 111 Log.v(TAG, "added message=$helpFaceAuthStatus") 112 } 113 114 fun getMessageToShow(atTimestamp: Long): HelpFaceAuthenticationStatus? { 115 if (helpFaceAuthStatuses.isEmpty() || (atTimestamp - startTime) < startWindow) { 116 // there's not enough time that has passed to determine whether to show anything yet 117 Log.v(TAG, "No message; haven't made initial threshold window OR no messages") 118 return null 119 } 120 removeOldMessages(atTimestamp) 121 val messageToShow = getMostFrequentHelpMessageSurpassingThreshold() 122 if (lastMessageIdShown != messageToShow?.msgId) { 123 Log.v( 124 TAG, 125 "showMessage previousLastMessageId=$lastMessageIdShown" + 126 "\n\tmessageToShow=$messageToShow " + 127 "\n\thelpFaceAuthStatusesSize=${helpFaceAuthStatuses.size}" + 128 "\n\thelpFaceAuthStatuses=$helpFaceAuthStatuses" + 129 "\n\tthreshold=$threshold" 130 ) 131 lastMessageIdShown = messageToShow?.msgId 132 } 133 return messageToShow 134 } 135 136 fun startNewFaceAuthSession(faceAuthStartedTime: Long) { 137 Log.d(TAG, "startNewFaceAuthSession at startTime=$startTime") 138 startTime = faceAuthStartedTime 139 helpFaceAuthStatuses.clear() 140 lastMessageIdShown = null 141 } 142 143 companion object { 144 const val DEFAULT_WINDOW_MS = 200L 145 } 146 } 147