1 /*
2  * Copyright (C) 2021 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.keyguard;
18 
19 import android.annotation.Nullable;
20 import android.content.res.ColorStateList;
21 import android.graphics.Color;
22 import android.os.SystemClock;
23 import android.text.TextUtils;
24 
25 import androidx.annotation.IntDef;
26 
27 import com.android.systemui.Dumpable;
28 import com.android.systemui.dagger.qualifiers.Main;
29 import com.android.systemui.plugins.statusbar.StatusBarStateController;
30 import com.android.systemui.statusbar.phone.KeyguardIndicationTextView;
31 import com.android.systemui.util.ViewController;
32 import com.android.systemui.util.concurrency.DelayableExecutor;
33 
34 import java.io.FileDescriptor;
35 import java.io.PrintWriter;
36 import java.lang.annotation.Retention;
37 import java.lang.annotation.RetentionPolicy;
38 import java.util.HashMap;
39 import java.util.LinkedList;
40 import java.util.List;
41 import java.util.Map;
42 
43 /**
44  * Animates through messages to show on the keyguard bottom area on the lock screen.
45  * Utilizes a {@link KeyguardIndicationTextView} for animations. This class handles the rotating
46  * nature of the messages including:
47  *  - ensuring a message is shown for its minimum amount of time. Minimum time is determined by
48  *  {@link KeyguardIndication#getMinVisibilityMillis()}
49  *  - showing the next message after a default of 3.5 seconds before animating to the next
50  *  - statically showing a single message if there is only one message to show
51  *  - showing certain messages immediately, assuming te current message has been shown for
52  *  at least {@link KeyguardIndication#getMinVisibilityMillis()}. For example, transient and
53  *  biometric messages are meant to be shown immediately.
54  *  - ending animations when dozing begins, and resuming when dozing ends. Rotating messages on
55  *  AoD is undesirable since it wakes up the AP too often.
56  */
57 public class KeyguardIndicationRotateTextViewController extends
58         ViewController<KeyguardIndicationTextView> implements Dumpable {
59     public static String TAG = "KgIndicationRotatingCtrl";
60     private static final long DEFAULT_INDICATION_SHOW_LENGTH = 3500; // milliseconds
61     public static final long IMPORTANT_MSG_MIN_DURATION = 2000L + 600L; // 2000ms + [Y in duration]
62 
63     private final StatusBarStateController mStatusBarStateController;
64     private final float mMaxAlpha;
65     private final ColorStateList mInitialTextColorState;
66 
67     // Stores @IndicationType => KeyguardIndication messages
68     private final Map<Integer, KeyguardIndication> mIndicationMessages = new HashMap<>();
69 
70     // Executor that will show the next message after a delay
71     private final DelayableExecutor mExecutor;
72     @Nullable private ShowNextIndication mShowNextIndicationRunnable;
73 
74     // List of indication types to show. The next indication to show is always at index 0
75     private final List<Integer> mIndicationQueue = new LinkedList<>();
76     private @IndicationType int mCurrIndicationType = INDICATION_TYPE_NONE;
77     private CharSequence mCurrMessage;
78     private long mLastIndicationSwitch;
79 
80     private boolean mIsDozing;
81 
KeyguardIndicationRotateTextViewController( KeyguardIndicationTextView view, @Main DelayableExecutor executor, StatusBarStateController statusBarStateController )82     public KeyguardIndicationRotateTextViewController(
83             KeyguardIndicationTextView view,
84             @Main DelayableExecutor executor,
85             StatusBarStateController statusBarStateController
86     ) {
87         super(view);
88         mMaxAlpha = view.getAlpha();
89         mExecutor = executor;
90         mInitialTextColorState = mView != null
91                 ? mView.getTextColors() : ColorStateList.valueOf(Color.WHITE);
92         mStatusBarStateController = statusBarStateController;
93         init();
94     }
95 
96     @Override
onViewAttached()97     protected void onViewAttached() {
98         mStatusBarStateController.addCallback(mStatusBarStateListener);
99     }
100 
101     @Override
onViewDetached()102     protected void onViewDetached() {
103         mStatusBarStateController.removeCallback(mStatusBarStateListener);
104         cancelScheduledIndication();
105     }
106 
107     /**
108      * Update the indication type with the given String.
109      * @param type of indication
110      * @param newIndication message to associate with this indication type
111      * @param showAsap if true: shows this indication message as soon as possible. If false,
112      *                   the text associated with this type is updated and will show when its turn
113      *                   in the IndicationQueue comes around.
114      */
updateIndication(@ndicationType int type, KeyguardIndication newIndication, boolean showAsap)115     public void updateIndication(@IndicationType int type, KeyguardIndication newIndication,
116             boolean showAsap) {
117         if (type == INDICATION_TYPE_REVERSE_CHARGING) {
118             // temporarily don't show here, instead use AmbientContainer b/181049781
119             return;
120         }
121         long minShowDuration = getMinVisibilityMillis(mIndicationMessages.get(mCurrIndicationType));
122         final boolean hasPreviousIndication = mIndicationMessages.get(type) != null
123                 && !TextUtils.isEmpty(mIndicationMessages.get(type).getMessage());
124         final boolean hasNewIndication = newIndication != null;
125         if (!hasNewIndication) {
126             mIndicationMessages.remove(type);
127             mIndicationQueue.removeIf(x -> x == type);
128         } else {
129             if (!hasPreviousIndication) {
130                 mIndicationQueue.add(type);
131             }
132 
133             mIndicationMessages.put(type, newIndication);
134         }
135 
136         if (mIsDozing) {
137             return;
138         }
139 
140         long currTime = SystemClock.uptimeMillis();
141         long timeSinceLastIndicationSwitch = currTime - mLastIndicationSwitch;
142         boolean currMsgShownForMinTime = timeSinceLastIndicationSwitch >= minShowDuration;
143         if (hasNewIndication) {
144             if (mCurrIndicationType == INDICATION_TYPE_NONE || mCurrIndicationType == type) {
145                 showIndication(type);
146             } else if (showAsap) {
147                 if (currMsgShownForMinTime) {
148                     showIndication(type);
149                 } else {
150                     mIndicationQueue.removeIf(x -> x == type);
151                     mIndicationQueue.add(0 /* index */, type /* type */);
152                     scheduleShowNextIndication(minShowDuration - timeSinceLastIndicationSwitch);
153                 }
154             } else if (!isNextIndicationScheduled()) {
155                 long nextShowTime = Math.max(
156                         getMinVisibilityMillis(mIndicationMessages.get(type)),
157                         DEFAULT_INDICATION_SHOW_LENGTH);
158                 if (timeSinceLastIndicationSwitch >= nextShowTime) {
159                     showIndication(type);
160                 } else {
161                     scheduleShowNextIndication(
162                             nextShowTime - timeSinceLastIndicationSwitch);
163                 }
164             }
165             return;
166         }
167 
168         // current indication is updated to empty
169         if (mCurrIndicationType == type
170                 && !hasNewIndication
171                 && showAsap) {
172             if (currMsgShownForMinTime) {
173                 if (mShowNextIndicationRunnable != null) {
174                     mShowNextIndicationRunnable.runImmediately();
175                 } else {
176                     showIndication(INDICATION_TYPE_NONE);
177                 }
178             } else {
179                 scheduleShowNextIndication(minShowDuration - timeSinceLastIndicationSwitch);
180             }
181         }
182     }
183 
184     /**
185      * Stop showing the following indication type.
186      *
187      * If the current indication is of this type, immediately stops showing the message.
188      */
hideIndication(@ndicationType int type)189     public void hideIndication(@IndicationType int type) {
190         if (!mIndicationMessages.containsKey(type)
191                 || TextUtils.isEmpty(mIndicationMessages.get(type).getMessage())) {
192             return;
193         }
194         updateIndication(type, null, true);
195     }
196 
197     /**
198      * Show a transient message.
199      * Transient messages:
200      * - show immediately
201      * - will continue to be in the rotation of messages shown until hideTransient is called.
202      */
showTransient(CharSequence newIndication)203     public void showTransient(CharSequence newIndication) {
204         updateIndication(INDICATION_TYPE_TRANSIENT,
205                 new KeyguardIndication.Builder()
206                         .setMessage(newIndication)
207                         .setMinVisibilityMillis(IMPORTANT_MSG_MIN_DURATION)
208                         .setTextColor(mInitialTextColorState)
209                         .build(),
210                 /* showImmediately */true);
211     }
212 
213     /**
214      * Hide a transient message immediately.
215      */
hideTransient()216     public void hideTransient() {
217         hideIndication(INDICATION_TYPE_TRANSIENT);
218     }
219 
220     /**
221      * @return true if there are available indications to show
222      */
hasIndications()223     public boolean hasIndications() {
224         return mIndicationMessages.keySet().size() > 0;
225     }
226 
227     /**
228      * Clears all messages in the queue and sets the current message to an empty string.
229      */
clearMessages()230     public void clearMessages() {
231         mCurrIndicationType = INDICATION_TYPE_NONE;
232         mIndicationQueue.clear();
233         mView.clearMessages();
234     }
235 
236     /**
237      * Immediately show the passed indication type and schedule the next indication to show.
238      * Will re-add this indication to be re-shown after all other indications have been
239      * rotated through.
240      */
showIndication(@ndicationType int type)241     private void showIndication(@IndicationType int type) {
242         cancelScheduledIndication();
243 
244         final CharSequence previousMessage = mCurrMessage;
245         final @IndicationType int previousIndicationType = mCurrIndicationType;
246         mCurrIndicationType = type;
247         mCurrMessage = mIndicationMessages.get(type) != null
248                 ? mIndicationMessages.get(type).getMessage()
249                 : null;
250 
251         mIndicationQueue.removeIf(x -> x == type);
252         if (mCurrIndicationType != INDICATION_TYPE_NONE) {
253             mIndicationQueue.add(type); // re-add to show later
254         }
255 
256         mLastIndicationSwitch = SystemClock.uptimeMillis();
257         if (!TextUtils.equals(previousMessage, mCurrMessage)
258                 || previousIndicationType != mCurrIndicationType) {
259             mView.switchIndication(mIndicationMessages.get(type));
260         }
261 
262         // only schedule next indication if there's more than just this indication in the queue
263         if (mCurrIndicationType != INDICATION_TYPE_NONE && mIndicationQueue.size() > 1) {
264             scheduleShowNextIndication(Math.max(
265                     getMinVisibilityMillis(mIndicationMessages.get(type)),
266                     DEFAULT_INDICATION_SHOW_LENGTH));
267         }
268     }
269 
getMinVisibilityMillis(KeyguardIndication indication)270     private long getMinVisibilityMillis(KeyguardIndication indication) {
271         if (indication == null) {
272             return 0;
273         }
274 
275         if (indication.getMinVisibilityMillis() == null) {
276             return 0;
277         }
278 
279         return indication.getMinVisibilityMillis();
280     }
281 
isNextIndicationScheduled()282     protected boolean isNextIndicationScheduled() {
283         return mShowNextIndicationRunnable != null;
284     }
285 
286 
scheduleShowNextIndication(long msUntilShowNextMsg)287     private void scheduleShowNextIndication(long msUntilShowNextMsg) {
288         cancelScheduledIndication();
289         mShowNextIndicationRunnable = new ShowNextIndication(msUntilShowNextMsg);
290     }
291 
cancelScheduledIndication()292     private void cancelScheduledIndication() {
293         if (mShowNextIndicationRunnable != null) {
294             mShowNextIndicationRunnable.cancelDelayedExecution();
295             mShowNextIndicationRunnable = null;
296         }
297     }
298 
299     private StatusBarStateController.StateListener mStatusBarStateListener =
300             new StatusBarStateController.StateListener() {
301                 @Override
302                 public void onDozeAmountChanged(float linear, float eased) {
303                     mView.setAlpha((1 - linear) * mMaxAlpha);
304                 }
305 
306                 @Override
307                 public void onDozingChanged(boolean isDozing) {
308                     if (isDozing == mIsDozing) return;
309                     mIsDozing = isDozing;
310                     if (mIsDozing) {
311                         showIndication(INDICATION_TYPE_NONE);
312                     } else if (mIndicationQueue.size() > 0) {
313                         showIndication(mIndicationQueue.remove(0));
314                     }
315                 }
316             };
317 
318     /**
319      * Shows the next indication in the IndicationQueue after an optional delay.
320      * This wrapper has the ability to cancel itself (remove runnable from DelayableExecutor) or
321      * immediately run itself (which also removes itself from the DelayableExecutor).
322      */
323     class ShowNextIndication {
324         private final Runnable mShowIndicationRunnable;
325         private Runnable mCancelDelayedRunnable;
326 
ShowNextIndication(long delay)327         ShowNextIndication(long delay) {
328             mShowIndicationRunnable = () -> {
329                 int type = mIndicationQueue.size() == 0
330                         ? INDICATION_TYPE_NONE : mIndicationQueue.remove(0);
331                 showIndication(type);
332             };
333             mCancelDelayedRunnable = mExecutor.executeDelayed(mShowIndicationRunnable, delay);
334         }
335 
runImmediately()336         public void runImmediately() {
337             cancelDelayedExecution();
338             mShowIndicationRunnable.run();
339         }
340 
cancelDelayedExecution()341         public void cancelDelayedExecution() {
342             if (mCancelDelayedRunnable != null) {
343                 mCancelDelayedRunnable.run();
344                 mCancelDelayedRunnable = null;
345             }
346         }
347     }
348 
349     @Override
dump(FileDescriptor fd, PrintWriter pw, String[] args)350     public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
351         pw.println("KeyguardIndicationRotatingTextViewController:");
352         pw.println("    currentMessage=" + mView.getText());
353         pw.println("    dozing:" + mIsDozing);
354         pw.println("    queue:" + mIndicationQueue.toString());
355         pw.println("    showNextIndicationRunnable:" + mShowNextIndicationRunnable);
356 
357         if (hasIndications()) {
358             pw.println("    All messages:");
359             for (int type : mIndicationMessages.keySet()) {
360                 pw.println("        type=" + type + " " + mIndicationMessages.get(type));
361             }
362         }
363     }
364 
365     // only used locally to stop showing any messages & stop the rotating messages
366     static final int INDICATION_TYPE_NONE = -1;
367 
368     public static final int INDICATION_TYPE_OWNER_INFO = 0;
369     public static final int INDICATION_TYPE_DISCLOSURE = 1;
370     public static final int INDICATION_TYPE_LOGOUT = 2;
371     public static final int INDICATION_TYPE_BATTERY = 3;
372     public static final int INDICATION_TYPE_ALIGNMENT = 4;
373     public static final int INDICATION_TYPE_TRANSIENT = 5;
374     public static final int INDICATION_TYPE_TRUST = 6;
375     public static final int INDICATION_TYPE_RESTING = 7;
376     public static final int INDICATION_TYPE_USER_LOCKED = 8;
377     public static final int INDICATION_TYPE_REVERSE_CHARGING = 10;
378     public static final int INDICATION_TYPE_BIOMETRIC_MESSAGE = 11;
379 
380     @IntDef({
381             INDICATION_TYPE_NONE,
382             INDICATION_TYPE_DISCLOSURE,
383             INDICATION_TYPE_OWNER_INFO,
384             INDICATION_TYPE_LOGOUT,
385             INDICATION_TYPE_BATTERY,
386             INDICATION_TYPE_ALIGNMENT,
387             INDICATION_TYPE_TRANSIENT,
388             INDICATION_TYPE_TRUST,
389             INDICATION_TYPE_RESTING,
390             INDICATION_TYPE_USER_LOCKED,
391             INDICATION_TYPE_REVERSE_CHARGING,
392             INDICATION_TYPE_BIOMETRIC_MESSAGE
393     })
394     @Retention(RetentionPolicy.SOURCE)
395     public @interface IndicationType{}
396 }
397