1 /* 2 * Copyright (C) 2022 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.settings.biometrics.fingerprint; 18 19 import android.hardware.fingerprint.FingerprintManager; 20 import android.os.Handler; 21 22 import androidx.annotation.NonNull; 23 import androidx.annotation.Nullable; 24 25 import java.time.Clock; 26 import java.util.ArrayDeque; 27 import java.util.Deque; 28 import java.util.HashMap; 29 30 /** 31 * Processes message provided from the enrollment callback and filters them based 32 * on the below configurable flags. This is primarily used to reduce the rate 33 * at which messages come through, which in turns eliminates UI flicker. 34 */ 35 public class MessageDisplayController extends FingerprintManager.EnrollmentCallback { 36 37 private final int mHelpMinimumDisplayTime; 38 private final int mProgressMinimumDisplayTime; 39 private final boolean mProgressPriorityOverHelp; 40 private final boolean mPrioritizeAcquireMessages; 41 private final int mCollectTime; 42 @NonNull 43 private final Deque<HelpMessage> mHelpMessageList; 44 @NonNull 45 private final Deque<ProgressMessage> mProgressMessageList; 46 @NonNull 47 private final Handler mHandler; 48 @NonNull 49 private final Clock mClock; 50 @NonNull 51 private final Runnable mDisplayMessageRunnable; 52 53 @Nullable 54 private ProgressMessage mLastProgressMessageDisplayed; 55 private boolean mMustDisplayProgress; 56 private boolean mWaitingForMessage; 57 @NonNull FingerprintManager.EnrollmentCallback mEnrollmentCallback; 58 59 private abstract static class Message { 60 long mTimeStamp = 0; display()61 abstract void display(); 62 } 63 64 private class HelpMessage extends Message { 65 private final int mHelpMsgId; 66 private final CharSequence mHelpString; 67 HelpMessage(int helpMsgId, CharSequence helpString)68 HelpMessage(int helpMsgId, CharSequence helpString) { 69 mHelpMsgId = helpMsgId; 70 mHelpString = helpString; 71 mTimeStamp = mClock.millis(); 72 } 73 74 @Override display()75 void display() { 76 mEnrollmentCallback.onEnrollmentHelp(mHelpMsgId, mHelpString); 77 mHandler.postDelayed(mDisplayMessageRunnable, mHelpMinimumDisplayTime); 78 } 79 } 80 81 private class ProgressMessage extends Message { 82 private final int mRemaining; 83 ProgressMessage(int remaining)84 ProgressMessage(int remaining) { 85 mRemaining = remaining; 86 mTimeStamp = mClock.millis(); 87 } 88 89 @Override display()90 void display() { 91 mEnrollmentCallback.onEnrollmentProgress(mRemaining); 92 mLastProgressMessageDisplayed = this; 93 mHandler.postDelayed(mDisplayMessageRunnable, mProgressMinimumDisplayTime); 94 } 95 } 96 97 /** 98 * Creating a MessageDisplayController object. 99 * @param handler main handler to run message queue 100 * @param enrollmentCallback callback to display messages 101 * @param clock real time system clock 102 * @param helpMinimumDisplayTime the minimum duration (in millis) that 103 * a help message needs to be displayed for 104 * @param progressMinimumDisplayTime the minimum duration (in millis) that 105 * a progress message needs to be displayed for 106 * @param progressPriorityOverHelp if true, then progress message is displayed 107 * when both help and progress message APIs have been called 108 * @param prioritizeAcquireMessages if true, then displays the help message 109 * which has occurred the most after the last display message 110 * @param collectTime the waiting time (in millis) to collect messages when it is idle 111 */ MessageDisplayController(@onNull Handler handler, FingerprintManager.EnrollmentCallback enrollmentCallback, @NonNull Clock clock, int helpMinimumDisplayTime, int progressMinimumDisplayTime, boolean progressPriorityOverHelp, boolean prioritizeAcquireMessages, int collectTime)112 public MessageDisplayController(@NonNull Handler handler, 113 FingerprintManager.EnrollmentCallback enrollmentCallback, 114 @NonNull Clock clock, int helpMinimumDisplayTime, int progressMinimumDisplayTime, 115 boolean progressPriorityOverHelp, boolean prioritizeAcquireMessages, 116 int collectTime) { 117 mClock = clock; 118 mWaitingForMessage = false; 119 mHelpMessageList = new ArrayDeque<>(); 120 mProgressMessageList = new ArrayDeque<>(); 121 mHandler = handler; 122 mEnrollmentCallback = enrollmentCallback; 123 124 mHelpMinimumDisplayTime = helpMinimumDisplayTime; 125 mProgressMinimumDisplayTime = progressMinimumDisplayTime; 126 mProgressPriorityOverHelp = progressPriorityOverHelp; 127 mPrioritizeAcquireMessages = prioritizeAcquireMessages; 128 mCollectTime = collectTime; 129 130 mDisplayMessageRunnable = () -> { 131 long timeStamp = mClock.millis(); 132 Message messageToDisplay = getMessageToDisplay(timeStamp); 133 134 if (messageToDisplay != null) { 135 messageToDisplay.display(); 136 } else { 137 mWaitingForMessage = true; 138 } 139 }; 140 141 mHandler.postDelayed(mDisplayMessageRunnable, 0); 142 } 143 144 /** 145 * Adds help message to the queue to be processed later. 146 * 147 * @param helpMsgId message Id associated with the help message 148 * @param helpString string associated with the help message 149 */ 150 @Override onEnrollmentHelp(int helpMsgId, CharSequence helpString)151 public void onEnrollmentHelp(int helpMsgId, CharSequence helpString) { 152 mHelpMessageList.add(new HelpMessage(helpMsgId, helpString)); 153 154 if (mWaitingForMessage) { 155 mWaitingForMessage = false; 156 mHandler.postDelayed(mDisplayMessageRunnable, mCollectTime); 157 } 158 } 159 160 /** 161 * Adds progress change message to the queue to be processed later. 162 * 163 * @param remaining remaining number of steps to complete enrollment 164 */ 165 @Override onEnrollmentProgress(int remaining)166 public void onEnrollmentProgress(int remaining) { 167 mProgressMessageList.add(new ProgressMessage(remaining)); 168 169 if (mWaitingForMessage) { 170 mWaitingForMessage = false; 171 mHandler.postDelayed(mDisplayMessageRunnable, mCollectTime); 172 } 173 } 174 175 @Override onEnrollmentError(int errMsgId, CharSequence errString)176 public void onEnrollmentError(int errMsgId, CharSequence errString) { 177 mEnrollmentCallback.onEnrollmentError(errMsgId, errString); 178 } 179 180 @Override onAcquired(boolean isAcquiredGood)181 public void onAcquired(boolean isAcquiredGood) { 182 mEnrollmentCallback.onAcquired(isAcquiredGood); 183 } 184 getMessageToDisplay(long timeStamp)185 private Message getMessageToDisplay(long timeStamp) { 186 ProgressMessage progressMessageToDisplay = getProgressMessageToDisplay(timeStamp); 187 if (mMustDisplayProgress) { 188 mMustDisplayProgress = false; 189 if (progressMessageToDisplay != null) { 190 return progressMessageToDisplay; 191 } 192 if (mLastProgressMessageDisplayed != null) { 193 return mLastProgressMessageDisplayed; 194 } 195 } 196 197 Message helpMessageToDisplay = getHelpMessageToDisplay(timeStamp); 198 if (helpMessageToDisplay != null || progressMessageToDisplay != null) { 199 if (mProgressPriorityOverHelp && progressMessageToDisplay != null) { 200 return progressMessageToDisplay; 201 } else if (helpMessageToDisplay != null) { 202 if (progressMessageToDisplay != null) { 203 mMustDisplayProgress = true; 204 mLastProgressMessageDisplayed = progressMessageToDisplay; 205 } 206 return helpMessageToDisplay; 207 } else { 208 return progressMessageToDisplay; 209 } 210 } else { 211 return null; 212 } 213 } 214 getProgressMessageToDisplay(long timeStamp)215 private ProgressMessage getProgressMessageToDisplay(long timeStamp) { 216 ProgressMessage finalProgressMessage = null; 217 while (mProgressMessageList != null && !mProgressMessageList.isEmpty()) { 218 Message message = mProgressMessageList.peekFirst(); 219 if (message.mTimeStamp <= timeStamp) { 220 ProgressMessage progressMessage = mProgressMessageList.pollFirst(); 221 if (mLastProgressMessageDisplayed != null 222 && mLastProgressMessageDisplayed.mRemaining == progressMessage.mRemaining) { 223 continue; 224 } 225 finalProgressMessage = progressMessage; 226 } else { 227 break; 228 } 229 } 230 231 return finalProgressMessage; 232 } 233 getHelpMessageToDisplay(long timeStamp)234 private HelpMessage getHelpMessageToDisplay(long timeStamp) { 235 HashMap<CharSequence, Integer> messageCount = new HashMap<>(); 236 HelpMessage finalHelpMessage = null; 237 238 while (mHelpMessageList != null && !mHelpMessageList.isEmpty()) { 239 Message message = mHelpMessageList.peekFirst(); 240 if (message.mTimeStamp <= timeStamp) { 241 finalHelpMessage = mHelpMessageList.pollFirst(); 242 CharSequence errString = finalHelpMessage.mHelpString; 243 messageCount.put(errString, messageCount.getOrDefault(errString, 0) + 1); 244 } else { 245 break; 246 } 247 } 248 if (mPrioritizeAcquireMessages) { 249 finalHelpMessage = prioritizeHelpMessageByCount(messageCount); 250 } 251 252 return finalHelpMessage; 253 } 254 prioritizeHelpMessageByCount(HashMap<CharSequence, Integer> messageCount)255 private HelpMessage prioritizeHelpMessageByCount(HashMap<CharSequence, Integer> messageCount) { 256 int maxCount = 0; 257 CharSequence maxCountMessage = null; 258 259 for (CharSequence key : 260 messageCount.keySet()) { 261 if (maxCount < messageCount.get(key)) { 262 maxCountMessage = key; 263 maxCount = messageCount.get(key); 264 } 265 } 266 267 return maxCountMessage != null ? new HelpMessage(0 /* errMsgId */, 268 maxCountMessage) : null; 269 } 270 } 271