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