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