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 if (mCurrIndicationType == type 192 && !hasNewIndication 193 && showAsap) { 194 if (currMsgShownForMinTime) { 195 if (mShowNextIndicationRunnable != null) { 196 mShowNextIndicationRunnable.runImmediately(); 197 } else { 198 showIndication(INDICATION_TYPE_NONE); 199 } 200 } else { 201 scheduleShowNextIndication(minShowDuration - timeSinceLastIndicationSwitch); 202 } 203 } 204 } 205 206 /** 207 * Stop showing the following indication type. 208 * 209 * If the current indication is of this type, immediately stops showing the message. 210 */ hideIndication(@ndicationType int type)211 public void hideIndication(@IndicationType int type) { 212 if (!mIndicationMessages.containsKey(type) 213 || TextUtils.isEmpty(mIndicationMessages.get(type).getMessage())) { 214 return; 215 } 216 updateIndication(type, null, true); 217 } 218 219 /** 220 * Show a transient message. 221 * Transient messages: 222 * - show immediately 223 * - will continue to be in the rotation of messages shown until hideTransient is called. 224 */ showTransient(CharSequence newIndication)225 public void showTransient(CharSequence newIndication) { 226 updateIndication(INDICATION_TYPE_TRANSIENT, 227 new KeyguardIndication.Builder() 228 .setMessage(newIndication) 229 .setMinVisibilityMillis(IMPORTANT_MSG_MIN_DURATION) 230 .setTextColor(mInitialTextColorState) 231 .build(), 232 /* showImmediately */true); 233 } 234 235 /** 236 * Hide a transient message immediately. 237 */ hideTransient()238 public void hideTransient() { 239 hideIndication(INDICATION_TYPE_TRANSIENT); 240 } 241 242 /** 243 * @return true if there are available indications to show 244 */ hasIndications()245 public boolean hasIndications() { 246 return mIndicationMessages.keySet().size() > 0; 247 } 248 249 /** 250 * Clears all messages in the queue and sets the current message to an empty string. 251 */ clearMessages()252 public void clearMessages() { 253 mCurrIndicationType = INDICATION_TYPE_NONE; 254 mIndicationQueue.clear(); 255 mIndicationMessages.clear(); 256 mView.clearMessages(); 257 } 258 259 /** 260 * Immediately show the passed indication type and schedule the next indication to show. 261 * Will re-add this indication to be re-shown after all other indications have been 262 * rotated through. 263 */ showIndication(@ndicationType int type)264 private void showIndication(@IndicationType int type) { 265 cancelScheduledIndication(); 266 267 final CharSequence previousMessage = mCurrMessage; 268 final @IndicationType int previousIndicationType = mCurrIndicationType; 269 mCurrIndicationType = type; 270 mCurrMessage = mIndicationMessages.get(type) != null 271 ? mIndicationMessages.get(type).getMessage() 272 : null; 273 274 mIndicationQueue.removeIf(x -> x == type); 275 if (mCurrIndicationType != INDICATION_TYPE_NONE) { 276 mIndicationQueue.add(type); // re-add to show later 277 } 278 279 mLastIndicationSwitch = SystemClock.uptimeMillis(); 280 if (!TextUtils.equals(previousMessage, mCurrMessage) 281 || previousIndicationType != mCurrIndicationType) { 282 mLogger.logKeyguardSwitchIndication(type, 283 mCurrMessage != null ? mCurrMessage.toString() : null); 284 mView.switchIndication(mIndicationMessages.get(type)); 285 } 286 287 // only schedule next indication if there's more than just this indication in the queue 288 if (mCurrIndicationType != INDICATION_TYPE_NONE && mIndicationQueue.size() > 1) { 289 scheduleShowNextIndication(Math.max( 290 getMinVisibilityMillis(mIndicationMessages.get(type)), 291 DEFAULT_INDICATION_SHOW_LENGTH)); 292 } 293 } 294 getMinVisibilityMillis(KeyguardIndication indication)295 private long getMinVisibilityMillis(KeyguardIndication indication) { 296 if (indication == null) { 297 return 0; 298 } 299 300 if (indication.getMinVisibilityMillis() == null) { 301 return 0; 302 } 303 304 return indication.getMinVisibilityMillis(); 305 } 306 isNextIndicationScheduled()307 protected boolean isNextIndicationScheduled() { 308 return mShowNextIndicationRunnable != null; 309 } 310 311 scheduleShowNextIndication(long msUntilShowNextMsg)312 private void scheduleShowNextIndication(long msUntilShowNextMsg) { 313 cancelScheduledIndication(); 314 mShowNextIndicationRunnable = new ShowNextIndication(msUntilShowNextMsg); 315 } 316 cancelScheduledIndication()317 private void cancelScheduledIndication() { 318 if (mShowNextIndicationRunnable != null) { 319 mShowNextIndicationRunnable.cancelDelayedExecution(); 320 mShowNextIndicationRunnable = null; 321 } 322 } 323 324 private StatusBarStateController.StateListener mStatusBarStateListener = 325 new StatusBarStateController.StateListener() { 326 @Override 327 public void onDozeAmountChanged(float linear, float eased) { 328 mView.setAlpha((1 - linear) * mMaxAlpha); 329 } 330 331 @Override 332 public void onDozingChanged(boolean isDozing) { 333 if (isDozing == mIsDozing) return; 334 mIsDozing = isDozing; 335 if (mIsDozing) { 336 showIndication(INDICATION_TYPE_NONE); 337 } else if (mIndicationQueue.size() > 0) { 338 showIndication(mIndicationQueue.get(0)); 339 } 340 } 341 }; 342 343 /** 344 * Shows the next indication in the IndicationQueue after an optional delay. 345 * This wrapper has the ability to cancel itself (remove runnable from DelayableExecutor) or 346 * immediately run itself (which also removes itself from the DelayableExecutor). 347 */ 348 class ShowNextIndication { 349 private final Runnable mShowIndicationRunnable; 350 private Runnable mCancelDelayedRunnable; 351 ShowNextIndication(long delay)352 ShowNextIndication(long delay) { 353 mShowIndicationRunnable = () -> { 354 int type = mIndicationQueue.size() == 0 355 ? INDICATION_TYPE_NONE : mIndicationQueue.get(0); 356 showIndication(type); 357 }; 358 mCancelDelayedRunnable = mExecutor.executeDelayed(mShowIndicationRunnable, delay); 359 } 360 runImmediately()361 public void runImmediately() { 362 cancelDelayedExecution(); 363 mShowIndicationRunnable.run(); 364 } 365 cancelDelayedExecution()366 public void cancelDelayedExecution() { 367 if (mCancelDelayedRunnable != null) { 368 mCancelDelayedRunnable.run(); 369 mCancelDelayedRunnable = null; 370 } 371 } 372 } 373 374 @Override dump(PrintWriter pw, String[] args)375 public void dump(PrintWriter pw, String[] args) { 376 pw.println("KeyguardIndicationRotatingTextViewController:"); 377 pw.println(" currentTextViewMessage=" + mView.getText()); 378 pw.println(" currentStoredMessage=" + mView.getMessage()); 379 pw.println(" dozing:" + mIsDozing); 380 pw.println(" queue:" + mIndicationQueue); 381 pw.println(" showNextIndicationRunnable:" + mShowNextIndicationRunnable); 382 383 if (hasIndications()) { 384 pw.println(" All messages:"); 385 for (int type : mIndicationMessages.keySet()) { 386 pw.println(" type=" + type + " " + mIndicationMessages.get(type)); 387 } 388 } 389 } 390 391 // only used locally to stop showing any messages & stop the rotating messages 392 static final int INDICATION_TYPE_NONE = -1; 393 394 public static final int INDICATION_TYPE_OWNER_INFO = 0; 395 public static final int INDICATION_TYPE_DISCLOSURE = 1; 396 public static final int INDICATION_TYPE_LOGOUT = 2; 397 public static final int INDICATION_TYPE_BATTERY = 3; 398 public static final int INDICATION_TYPE_ALIGNMENT = 4; 399 public static final int INDICATION_TYPE_TRANSIENT = 5; 400 public static final int INDICATION_TYPE_TRUST = 6; 401 public static final int INDICATION_TYPE_PERSISTENT_UNLOCK_MESSAGE = 7; 402 public static final int INDICATION_TYPE_USER_LOCKED = 8; 403 public static final int INDICATION_TYPE_REVERSE_CHARGING = 10; 404 public static final int INDICATION_TYPE_BIOMETRIC_MESSAGE = 11; 405 public static final int INDICATION_TYPE_BIOMETRIC_MESSAGE_FOLLOW_UP = 12; 406 407 @IntDef({ 408 INDICATION_TYPE_NONE, 409 INDICATION_TYPE_DISCLOSURE, 410 INDICATION_TYPE_OWNER_INFO, 411 INDICATION_TYPE_LOGOUT, 412 INDICATION_TYPE_BATTERY, 413 INDICATION_TYPE_ALIGNMENT, 414 INDICATION_TYPE_TRANSIENT, 415 INDICATION_TYPE_TRUST, 416 INDICATION_TYPE_PERSISTENT_UNLOCK_MESSAGE, 417 INDICATION_TYPE_USER_LOCKED, 418 INDICATION_TYPE_REVERSE_CHARGING, 419 INDICATION_TYPE_BIOMETRIC_MESSAGE, 420 INDICATION_TYPE_BIOMETRIC_MESSAGE_FOLLOW_UP 421 }) 422 @Retention(RetentionPolicy.SOURCE) 423 public @interface IndicationType{} 424 425 /** 426 * Get human-readable string representation of the indication type. 427 */ indicationTypeToString(@ndicationType int type)428 public static String indicationTypeToString(@IndicationType int type) { 429 switch (type) { 430 case INDICATION_TYPE_NONE: 431 return "none"; 432 case INDICATION_TYPE_DISCLOSURE: 433 return "disclosure"; 434 case INDICATION_TYPE_OWNER_INFO: 435 return "owner_info"; 436 case INDICATION_TYPE_LOGOUT: 437 return "logout"; 438 case INDICATION_TYPE_BATTERY: 439 return "battery"; 440 case INDICATION_TYPE_ALIGNMENT: 441 return "alignment"; 442 case INDICATION_TYPE_TRANSIENT: 443 return "transient"; 444 case INDICATION_TYPE_TRUST: 445 return "trust"; 446 case INDICATION_TYPE_PERSISTENT_UNLOCK_MESSAGE: 447 return "persistent_unlock_message"; 448 case INDICATION_TYPE_USER_LOCKED: 449 return "user_locked"; 450 case INDICATION_TYPE_REVERSE_CHARGING: 451 return "reverse_charging"; 452 case INDICATION_TYPE_BIOMETRIC_MESSAGE: 453 return "biometric_message"; 454 case INDICATION_TYPE_BIOMETRIC_MESSAGE_FOLLOW_UP: 455 return "biometric_message_followup"; 456 default: 457 return "unknown[" + type + "]"; 458 } 459 } 460 } 461