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