1 /* 2 * Copyright (C) 2015 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.statusbar.phone; 18 19 import static com.android.systemui.Dependency.MAIN_HANDLER_NAME; 20 import static com.android.systemui.util.InjectionInflationController.VIEW_CONTEXT; 21 22 import android.content.Context; 23 import android.content.res.ColorStateList; 24 import android.content.res.Configuration; 25 import android.content.res.TypedArray; 26 import android.graphics.Color; 27 import android.graphics.drawable.Animatable2; 28 import android.graphics.drawable.AnimatedVectorDrawable; 29 import android.graphics.drawable.Drawable; 30 import android.hardware.biometrics.BiometricSourceType; 31 import android.os.Handler; 32 import android.os.Trace; 33 import android.util.AttributeSet; 34 import android.view.ViewGroup; 35 import android.view.accessibility.AccessibilityNodeInfo; 36 37 import androidx.annotation.Nullable; 38 39 import com.android.internal.graphics.ColorUtils; 40 import com.android.internal.telephony.IccCardConstants; 41 import com.android.keyguard.KeyguardUpdateMonitor; 42 import com.android.keyguard.KeyguardUpdateMonitorCallback; 43 import com.android.systemui.R; 44 import com.android.systemui.dock.DockManager; 45 import com.android.systemui.plugins.statusbar.StatusBarStateController; 46 import com.android.systemui.statusbar.KeyguardAffordanceView; 47 import com.android.systemui.statusbar.phone.ScrimController.ScrimVisibility; 48 import com.android.systemui.statusbar.policy.AccessibilityController; 49 import com.android.systemui.statusbar.policy.ConfigurationController; 50 import com.android.systemui.statusbar.policy.KeyguardMonitor; 51 import com.android.systemui.statusbar.policy.UserInfoController.OnUserInfoChangedListener; 52 53 import javax.inject.Inject; 54 import javax.inject.Named; 55 56 /** 57 * Manages the different states and animations of the unlock icon. 58 */ 59 public class LockIcon extends KeyguardAffordanceView implements OnUserInfoChangedListener, 60 StatusBarStateController.StateListener, ConfigurationController.ConfigurationListener, 61 UnlockMethodCache.OnUnlockMethodChangedListener { 62 63 private static final int STATE_LOCKED = 0; 64 private static final int STATE_LOCK_OPEN = 1; 65 private static final int STATE_SCANNING_FACE = 2; 66 private static final int STATE_BIOMETRICS_ERROR = 3; 67 private final ConfigurationController mConfigurationController; 68 private final StatusBarStateController mStatusBarStateController; 69 private final UnlockMethodCache mUnlockMethodCache; 70 private final KeyguardUpdateMonitor mKeyguardUpdateMonitor; 71 private final AccessibilityController mAccessibilityController; 72 private final DockManager mDockManager; 73 private final Handler mMainHandler; 74 private final KeyguardMonitor mKeyguardMonitor; 75 76 private int mLastState = 0; 77 private boolean mTransientBiometricsError; 78 private boolean mIsFaceUnlockState; 79 private boolean mSimLocked; 80 private int mDensity; 81 private boolean mPulsing; 82 private boolean mDozing; 83 private boolean mBouncerVisible; 84 private boolean mDocked; 85 private boolean mLastDozing; 86 private boolean mLastPulsing; 87 private boolean mLastBouncerVisible; 88 private int mIconColor; 89 private float mDozeAmount; 90 private int mIconRes; 91 private boolean mWasPulsingOnThisFrame; 92 private boolean mWakeAndUnlockRunning; 93 private boolean mKeyguardShowing; 94 private boolean mShowingLaunchAffordance; 95 96 private final KeyguardMonitor.Callback mKeyguardMonitorCallback = 97 new KeyguardMonitor.Callback() { 98 @Override 99 public void onKeyguardShowingChanged() { 100 mKeyguardShowing = mKeyguardMonitor.isShowing(); 101 update(false /* force */); 102 } 103 }; 104 private final DockManager.DockEventListener mDockEventListener = 105 new DockManager.DockEventListener() { 106 @Override 107 public void onEvent(int event) { 108 boolean docked = event == DockManager.STATE_DOCKED 109 || event == DockManager.STATE_DOCKED_HIDE; 110 if (docked != mDocked) { 111 mDocked = docked; 112 update(true /* force */); 113 } 114 } 115 }; 116 117 private final KeyguardUpdateMonitorCallback mUpdateMonitorCallback = 118 new KeyguardUpdateMonitorCallback() { 119 @Override 120 public void onSimStateChanged(int subId, int slotId, 121 IccCardConstants.State simState) { 122 boolean oldSimLocked = mSimLocked; 123 mSimLocked = mKeyguardUpdateMonitor.isSimPinSecure(); 124 update(oldSimLocked != mSimLocked); 125 } 126 127 @Override 128 public void onKeyguardVisibilityChanged(boolean showing) { 129 update(); 130 } 131 132 @Override 133 public void onBiometricRunningStateChanged(boolean running, 134 BiometricSourceType biometricSourceType) { 135 update(); 136 } 137 138 @Override 139 public void onStrongAuthStateChanged(int userId) { 140 update(); 141 } 142 }; 143 144 @Inject LockIcon(@amedVIEW_CONTEXT) Context context, AttributeSet attrs, StatusBarStateController statusBarStateController, ConfigurationController configurationController, AccessibilityController accessibilityController, KeyguardMonitor keyguardMonitor, @Nullable DockManager dockManager, @Named(MAIN_HANDLER_NAME) Handler mainHandler)145 public LockIcon(@Named(VIEW_CONTEXT) Context context, AttributeSet attrs, 146 StatusBarStateController statusBarStateController, 147 ConfigurationController configurationController, 148 AccessibilityController accessibilityController, 149 KeyguardMonitor keyguardMonitor, 150 @Nullable DockManager dockManager, 151 @Named(MAIN_HANDLER_NAME) Handler mainHandler) { 152 super(context, attrs); 153 mContext = context; 154 mUnlockMethodCache = UnlockMethodCache.getInstance(context); 155 mKeyguardUpdateMonitor = KeyguardUpdateMonitor.getInstance(mContext); 156 mAccessibilityController = accessibilityController; 157 mConfigurationController = configurationController; 158 mStatusBarStateController = statusBarStateController; 159 mKeyguardMonitor = keyguardMonitor; 160 mDockManager = dockManager; 161 mMainHandler = mainHandler; 162 } 163 164 @Override onAttachedToWindow()165 protected void onAttachedToWindow() { 166 super.onAttachedToWindow(); 167 mStatusBarStateController.addCallback(this); 168 mConfigurationController.addCallback(this); 169 mKeyguardMonitor.addCallback(mKeyguardMonitorCallback); 170 mKeyguardUpdateMonitor.registerCallback(mUpdateMonitorCallback); 171 mUnlockMethodCache.addListener(this); 172 mSimLocked = mKeyguardUpdateMonitor.isSimPinSecure(); 173 if (mDockManager != null) { 174 mDockManager.addListener(mDockEventListener); 175 } 176 onThemeChanged(); 177 } 178 179 @Override onDetachedFromWindow()180 protected void onDetachedFromWindow() { 181 super.onDetachedFromWindow(); 182 mStatusBarStateController.removeCallback(this); 183 mConfigurationController.removeCallback(this); 184 mKeyguardUpdateMonitor.removeCallback(mUpdateMonitorCallback); 185 mKeyguardMonitor.removeCallback(mKeyguardMonitorCallback); 186 mUnlockMethodCache.removeListener(this); 187 if (mDockManager != null) { 188 mDockManager.removeListener(mDockEventListener); 189 } 190 } 191 192 @Override onThemeChanged()193 public void onThemeChanged() { 194 TypedArray typedArray = mContext.getTheme().obtainStyledAttributes( 195 null, new int[]{ R.attr.wallpaperTextColor }, 0, 0); 196 mIconColor = typedArray.getColor(0, Color.WHITE); 197 typedArray.recycle(); 198 updateDarkTint(); 199 } 200 201 @Override onUserInfoChanged(String name, Drawable picture, String userAccount)202 public void onUserInfoChanged(String name, Drawable picture, String userAccount) { 203 update(); 204 } 205 206 /** 207 * If we're currently presenting an authentication error message. 208 */ setTransientBiometricsError(boolean transientBiometricsError)209 public void setTransientBiometricsError(boolean transientBiometricsError) { 210 mTransientBiometricsError = transientBiometricsError; 211 update(); 212 } 213 214 @Override onConfigurationChanged(Configuration newConfig)215 protected void onConfigurationChanged(Configuration newConfig) { 216 super.onConfigurationChanged(newConfig); 217 final int density = newConfig.densityDpi; 218 if (density != mDensity) { 219 mDensity = density; 220 update(); 221 } 222 } 223 update()224 public void update() { 225 update(false /* force */); 226 } 227 update(boolean force)228 public void update(boolean force) { 229 int state = getState(); 230 mIsFaceUnlockState = state == STATE_SCANNING_FACE; 231 if (state != mLastState || mLastDozing != mDozing || mLastPulsing != mPulsing 232 || mLastBouncerVisible != mBouncerVisible || force) { 233 int iconAnimRes = getAnimationResForTransition(mLastState, state, mLastPulsing, 234 mPulsing, mLastDozing, mDozing, mBouncerVisible); 235 boolean isAnim = iconAnimRes != -1; 236 237 int iconRes = isAnim ? iconAnimRes : getIconForState(state); 238 if (iconRes != mIconRes) { 239 mIconRes = iconRes; 240 241 Drawable icon = mContext.getDrawable(iconRes); 242 final AnimatedVectorDrawable animation = icon instanceof AnimatedVectorDrawable 243 ? (AnimatedVectorDrawable) icon 244 : null; 245 setImageDrawable(icon, false); 246 if (mIsFaceUnlockState) { 247 announceForAccessibility(getContext().getString( 248 R.string.accessibility_scanning_face)); 249 } 250 251 if (animation != null && isAnim) { 252 animation.forceAnimationOnUI(); 253 animation.clearAnimationCallbacks(); 254 animation.registerAnimationCallback(new Animatable2.AnimationCallback() { 255 @Override 256 public void onAnimationEnd(Drawable drawable) { 257 if (getDrawable() == animation && state == getState() 258 && doesAnimationLoop(iconAnimRes)) { 259 animation.start(); 260 } else { 261 Trace.endAsyncSection("LockIcon#Animation", state); 262 } 263 } 264 }); 265 Trace.beginAsyncSection("LockIcon#Animation", state); 266 animation.start(); 267 } 268 } 269 updateDarkTint(); 270 271 mLastState = state; 272 mLastDozing = mDozing; 273 mLastPulsing = mPulsing; 274 mLastBouncerVisible = mBouncerVisible; 275 } 276 277 boolean onAodNotPulsingOrDocked = mDozing && (!mPulsing || mDocked); 278 boolean invisible = onAodNotPulsingOrDocked || mWakeAndUnlockRunning 279 || mShowingLaunchAffordance; 280 setVisibility(invisible ? INVISIBLE : VISIBLE); 281 updateClickability(); 282 } 283 updateClickability()284 private void updateClickability() { 285 if (mAccessibilityController == null) { 286 return; 287 } 288 boolean canLock = mUnlockMethodCache.isMethodSecure() 289 && mUnlockMethodCache.canSkipBouncer(); 290 boolean clickToUnlock = mAccessibilityController.isAccessibilityEnabled(); 291 setClickable(clickToUnlock); 292 setLongClickable(canLock && !clickToUnlock); 293 setFocusable(mAccessibilityController.isAccessibilityEnabled()); 294 } 295 296 @Override onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info)297 public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { 298 super.onInitializeAccessibilityNodeInfo(info); 299 boolean fingerprintRunning = mKeyguardUpdateMonitor.isFingerprintDetectionRunning(); 300 boolean unlockingAllowed = mKeyguardUpdateMonitor.isUnlockingWithBiometricAllowed(); 301 if (fingerprintRunning && unlockingAllowed) { 302 AccessibilityNodeInfo.AccessibilityAction unlock 303 = new AccessibilityNodeInfo.AccessibilityAction( 304 AccessibilityNodeInfo.ACTION_CLICK, 305 getContext().getString(R.string.accessibility_unlock_without_fingerprint)); 306 info.addAction(unlock); 307 info.setHintText(getContext().getString( 308 R.string.accessibility_waiting_for_fingerprint)); 309 } else if (mIsFaceUnlockState) { 310 //Avoid 'button' to be spoken for scanning face 311 info.setClassName(LockIcon.class.getName()); 312 info.setContentDescription(getContext().getString( 313 R.string.accessibility_scanning_face)); 314 } 315 } 316 getIconForState(int state)317 private int getIconForState(int state) { 318 int iconRes; 319 switch (state) { 320 case STATE_LOCKED: 321 // Scanning animation is a pulsing padlock. This means that the resting state is 322 // just a padlock. 323 case STATE_SCANNING_FACE: 324 // Error animation also starts and ands on the padlock. 325 case STATE_BIOMETRICS_ERROR: 326 iconRes = com.android.internal.R.drawable.ic_lock; 327 break; 328 case STATE_LOCK_OPEN: 329 iconRes = com.android.internal.R.drawable.ic_lock_open; 330 break; 331 default: 332 throw new IllegalArgumentException(); 333 } 334 335 return iconRes; 336 } 337 doesAnimationLoop(int resourceId)338 private boolean doesAnimationLoop(int resourceId) { 339 return resourceId == com.android.internal.R.anim.lock_scanning; 340 } 341 getAnimationResForTransition(int oldState, int newState, boolean wasPulsing, boolean pulsing, boolean wasDozing, boolean dozing, boolean bouncerVisible)342 private int getAnimationResForTransition(int oldState, int newState, 343 boolean wasPulsing, boolean pulsing, boolean wasDozing, boolean dozing, 344 boolean bouncerVisible) { 345 346 // Never animate when screen is off 347 if (dozing && !pulsing && !mWasPulsingOnThisFrame) { 348 return -1; 349 } 350 351 boolean isError = oldState != STATE_BIOMETRICS_ERROR && newState == STATE_BIOMETRICS_ERROR; 352 boolean justUnlocked = oldState != STATE_LOCK_OPEN && newState == STATE_LOCK_OPEN; 353 boolean justLocked = oldState == STATE_LOCK_OPEN && newState == STATE_LOCKED; 354 boolean nowPulsing = !wasPulsing && pulsing; 355 boolean turningOn = wasDozing && !dozing && !mWasPulsingOnThisFrame; 356 357 if (isError) { 358 return com.android.internal.R.anim.lock_to_error; 359 } else if (justUnlocked) { 360 return com.android.internal.R.anim.lock_unlock; 361 } else if (justLocked) { 362 return com.android.internal.R.anim.lock_lock; 363 } else if (newState == STATE_SCANNING_FACE && bouncerVisible) { 364 return com.android.internal.R.anim.lock_scanning; 365 } else if ((nowPulsing || turningOn) && newState != STATE_LOCK_OPEN) { 366 return com.android.internal.R.anim.lock_in; 367 } 368 return -1; 369 } 370 getState()371 private int getState() { 372 KeyguardUpdateMonitor updateMonitor = KeyguardUpdateMonitor.getInstance(mContext); 373 if (mTransientBiometricsError) { 374 return STATE_BIOMETRICS_ERROR; 375 } else if ((mUnlockMethodCache.canSkipBouncer() || !mKeyguardShowing) && !mSimLocked) { 376 return STATE_LOCK_OPEN; 377 } else if (updateMonitor.isFaceDetectionRunning()) { 378 return STATE_SCANNING_FACE; 379 } else { 380 return STATE_LOCKED; 381 } 382 } 383 384 @Override onDozeAmountChanged(float linear, float eased)385 public void onDozeAmountChanged(float linear, float eased) { 386 mDozeAmount = eased; 387 updateDarkTint(); 388 } 389 390 /** 391 * When keyguard is in pulsing (AOD2) state. 392 * @param pulsing {@code true} when pulsing. 393 */ setPulsing(boolean pulsing)394 public void setPulsing(boolean pulsing) { 395 mPulsing = pulsing; 396 if (!mPulsing) { 397 mWasPulsingOnThisFrame = true; 398 mMainHandler.post(() -> { 399 mWasPulsingOnThisFrame = false; 400 }); 401 } 402 update(); 403 } 404 405 /** 406 * Sets the dozing state of the keyguard. 407 */ 408 @Override onDozingChanged(boolean dozing)409 public void onDozingChanged(boolean dozing) { 410 mDozing = dozing; 411 update(); 412 } 413 updateDarkTint()414 private void updateDarkTint() { 415 int color = ColorUtils.blendARGB(mIconColor, Color.WHITE, mDozeAmount); 416 setImageTintList(ColorStateList.valueOf(color)); 417 } 418 419 /** 420 * If bouncer is visible or not. 421 */ setBouncerVisible(boolean bouncerVisible)422 public void setBouncerVisible(boolean bouncerVisible) { 423 if (mBouncerVisible == bouncerVisible) { 424 return; 425 } 426 mBouncerVisible = bouncerVisible; 427 update(); 428 } 429 430 @Override onDensityOrFontScaleChanged()431 public void onDensityOrFontScaleChanged() { 432 ViewGroup.LayoutParams lp = getLayoutParams(); 433 if (lp == null) { 434 return; 435 } 436 lp.width = getResources().getDimensionPixelSize(R.dimen.keyguard_lock_width); 437 lp.height = getResources().getDimensionPixelSize(R.dimen.keyguard_lock_height); 438 setLayoutParams(lp); 439 update(true /* force */); 440 } 441 442 @Override onLocaleListChanged()443 public void onLocaleListChanged() { 444 setContentDescription(getContext().getText(R.string.accessibility_unlock_button)); 445 update(true /* force */); 446 } 447 448 @Override onUnlockMethodStateChanged()449 public void onUnlockMethodStateChanged() { 450 update(); 451 } 452 453 /** 454 * We need to hide the lock whenever there's a fingerprint unlock, otherwise you'll see the 455 * icon on top of the black front scrim. 456 */ onBiometricAuthModeChanged(boolean wakeAndUnlock)457 public void onBiometricAuthModeChanged(boolean wakeAndUnlock) { 458 if (wakeAndUnlock) { 459 mWakeAndUnlockRunning = true; 460 } 461 update(); 462 } 463 464 /** 465 * When we're launching an affordance, like double pressing power to open camera. 466 */ onShowingLaunchAffordanceChanged(boolean showing)467 public void onShowingLaunchAffordanceChanged(boolean showing) { 468 mShowingLaunchAffordance = showing; 469 update(); 470 } 471 472 /** 473 * Called whenever the scrims become opaque, transparent or semi-transparent. 474 */ onScrimVisibilityChanged(@crimVisibility int scrimsVisible)475 public void onScrimVisibilityChanged(@ScrimVisibility int scrimsVisible) { 476 if (mWakeAndUnlockRunning 477 && scrimsVisible == ScrimController.VISIBILITY_FULLY_TRANSPARENT) { 478 mWakeAndUnlockRunning = false; 479 update(); 480 } 481 } 482 } 483