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