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