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