1 /* 2 * Copyright (C) 2020 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.keyguard; 18 19 import static android.hardware.biometrics.BiometricSourceType.FINGERPRINT; 20 21 import static com.android.systemui.classifier.Classifier.LOCK_ICON; 22 23 import android.content.Context; 24 import android.content.res.Configuration; 25 import android.graphics.PointF; 26 import android.graphics.Rect; 27 import android.graphics.drawable.AnimatedVectorDrawable; 28 import android.graphics.drawable.Drawable; 29 import android.hardware.biometrics.BiometricSourceType; 30 import android.hardware.fingerprint.FingerprintSensorPropertiesInternal; 31 import android.media.AudioAttributes; 32 import android.os.Process; 33 import android.os.Vibrator; 34 import android.util.DisplayMetrics; 35 import android.view.GestureDetector; 36 import android.view.GestureDetector.SimpleOnGestureListener; 37 import android.view.MotionEvent; 38 import android.view.View; 39 import android.view.accessibility.AccessibilityManager; 40 import android.view.accessibility.AccessibilityNodeInfo; 41 42 import androidx.annotation.NonNull; 43 import androidx.annotation.Nullable; 44 import androidx.core.view.accessibility.AccessibilityNodeInfoCompat; 45 46 import com.android.systemui.Dumpable; 47 import com.android.systemui.R; 48 import com.android.systemui.biometrics.AuthController; 49 import com.android.systemui.biometrics.UdfpsController; 50 import com.android.systemui.dagger.qualifiers.Main; 51 import com.android.systemui.dump.DumpManager; 52 import com.android.systemui.plugins.FalsingManager; 53 import com.android.systemui.plugins.statusbar.StatusBarStateController; 54 import com.android.systemui.statusbar.StatusBarState; 55 import com.android.systemui.statusbar.phone.dagger.StatusBarComponent; 56 import com.android.systemui.statusbar.policy.ConfigurationController; 57 import com.android.systemui.statusbar.policy.KeyguardStateController; 58 import com.android.systemui.util.ViewController; 59 import com.android.systemui.util.concurrency.DelayableExecutor; 60 61 import java.io.FileDescriptor; 62 import java.io.PrintWriter; 63 import java.util.Objects; 64 65 import javax.inject.Inject; 66 67 /** 68 * Controls when to show the LockIcon affordance (lock/unlocked icon or circle) on lock screen. 69 * 70 * This view will only be shown if the user has UDFPS or FaceAuth enrolled 71 */ 72 @StatusBarComponent.StatusBarScope 73 public class LockIconViewController extends ViewController<LockIconView> implements Dumpable { 74 private static final float sDefaultDensity = 75 (float) DisplayMetrics.DENSITY_DEVICE_STABLE / (float) DisplayMetrics.DENSITY_DEFAULT; 76 private static final int sLockIconRadiusPx = (int) (sDefaultDensity * 36); 77 private static final float sDistAboveKgBottomAreaPx = sDefaultDensity * 12; 78 private static final AudioAttributes VIBRATION_SONIFICATION_ATTRIBUTES = 79 new AudioAttributes.Builder() 80 .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION) 81 .setUsage(AudioAttributes.USAGE_ASSISTANCE_SONIFICATION) 82 .build(); 83 84 @NonNull private final KeyguardUpdateMonitor mKeyguardUpdateMonitor; 85 @NonNull private final KeyguardViewController mKeyguardViewController; 86 @NonNull private final StatusBarStateController mStatusBarStateController; 87 @NonNull private final KeyguardStateController mKeyguardStateController; 88 @NonNull private final FalsingManager mFalsingManager; 89 @NonNull private final AuthController mAuthController; 90 @NonNull private final AccessibilityManager mAccessibilityManager; 91 @NonNull private final ConfigurationController mConfigurationController; 92 @NonNull private final DelayableExecutor mExecutor; 93 private boolean mUdfpsEnrolled; 94 95 @NonNull private final AnimatedVectorDrawable mFpToUnlockIcon; 96 @NonNull private final AnimatedVectorDrawable mLockToUnlockIcon; 97 @NonNull private final Drawable mLockIcon; 98 @NonNull private final Drawable mUnlockIcon; 99 @NonNull private CharSequence mUnlockedLabel; 100 @NonNull private CharSequence mLockedLabel; 101 @Nullable private final Vibrator mVibrator; 102 103 private boolean mIsDozing; 104 private boolean mIsBouncerShowing; 105 private boolean mRunningFPS; 106 private boolean mCanDismissLockScreen; 107 private boolean mQsExpanded; 108 private int mStatusBarState; 109 private boolean mIsKeyguardShowing; 110 private boolean mUserUnlockedWithBiometric; 111 private Runnable mCancelDelayedUpdateVisibilityRunnable; 112 113 private boolean mUdfpsSupported; 114 private float mHeightPixels; 115 private float mWidthPixels; 116 private int mBottomPadding; // in pixels 117 118 private boolean mShowUnlockIcon; 119 private boolean mShowLockIcon; 120 121 private boolean mDownDetected; 122 private boolean mDetectedLongPress; 123 private final Rect mSensorTouchLocation = new Rect(); 124 125 @Inject LockIconViewController( @ullable LockIconView view, @NonNull StatusBarStateController statusBarStateController, @NonNull KeyguardUpdateMonitor keyguardUpdateMonitor, @NonNull KeyguardViewController keyguardViewController, @NonNull KeyguardStateController keyguardStateController, @NonNull FalsingManager falsingManager, @NonNull AuthController authController, @NonNull DumpManager dumpManager, @NonNull AccessibilityManager accessibilityManager, @NonNull ConfigurationController configurationController, @NonNull @Main DelayableExecutor executor, @Nullable Vibrator vibrator )126 public LockIconViewController( 127 @Nullable LockIconView view, 128 @NonNull StatusBarStateController statusBarStateController, 129 @NonNull KeyguardUpdateMonitor keyguardUpdateMonitor, 130 @NonNull KeyguardViewController keyguardViewController, 131 @NonNull KeyguardStateController keyguardStateController, 132 @NonNull FalsingManager falsingManager, 133 @NonNull AuthController authController, 134 @NonNull DumpManager dumpManager, 135 @NonNull AccessibilityManager accessibilityManager, 136 @NonNull ConfigurationController configurationController, 137 @NonNull @Main DelayableExecutor executor, 138 @Nullable Vibrator vibrator 139 ) { 140 super(view); 141 mStatusBarStateController = statusBarStateController; 142 mKeyguardUpdateMonitor = keyguardUpdateMonitor; 143 mAuthController = authController; 144 mKeyguardViewController = keyguardViewController; 145 mKeyguardStateController = keyguardStateController; 146 mFalsingManager = falsingManager; 147 mAccessibilityManager = accessibilityManager; 148 mConfigurationController = configurationController; 149 mExecutor = executor; 150 mVibrator = vibrator; 151 152 final Context context = view.getContext(); 153 mUnlockIcon = mView.getContext().getResources().getDrawable( 154 R.drawable.ic_unlock, 155 mView.getContext().getTheme()); 156 mLockIcon = mView.getContext().getResources().getDrawable( 157 R.anim.lock_to_unlock, 158 mView.getContext().getTheme()); 159 mFpToUnlockIcon = (AnimatedVectorDrawable) mView.getContext().getResources().getDrawable( 160 R.anim.fp_to_unlock, mView.getContext().getTheme()); 161 mLockToUnlockIcon = (AnimatedVectorDrawable) mView.getContext().getResources().getDrawable( 162 R.anim.lock_to_unlock, 163 mView.getContext().getTheme()); 164 mUnlockedLabel = context.getResources().getString(R.string.accessibility_unlock_button); 165 mLockedLabel = context.getResources().getString(R.string.accessibility_lock_icon); 166 dumpManager.registerDumpable("LockIconViewController", this); 167 } 168 169 @Override onInit()170 protected void onInit() { 171 mView.setAccessibilityDelegate(mAccessibilityDelegate); 172 } 173 174 @Override onViewAttached()175 protected void onViewAttached() { 176 // we check this here instead of onInit since the FingerprintManager + FaceManager may not 177 // have started up yet onInit 178 mUdfpsSupported = mAuthController.getUdfpsSensorLocation() != null; 179 180 updateConfiguration(); 181 updateKeyguardShowing(); 182 mUserUnlockedWithBiometric = false; 183 mIsBouncerShowing = mKeyguardViewController.isBouncerShowing(); 184 mIsDozing = mStatusBarStateController.isDozing(); 185 mRunningFPS = mKeyguardUpdateMonitor.isFingerprintDetectionRunning(); 186 mCanDismissLockScreen = mKeyguardStateController.canDismissLockScreen(); 187 mStatusBarState = mStatusBarStateController.getState(); 188 189 updateColors(); 190 mConfigurationController.addCallback(mConfigurationListener); 191 192 mKeyguardUpdateMonitor.registerCallback(mKeyguardUpdateMonitorCallback); 193 mStatusBarStateController.addCallback(mStatusBarStateListener); 194 mKeyguardStateController.addCallback(mKeyguardStateCallback); 195 mDownDetected = false; 196 updateVisibility(); 197 } 198 199 @Override onViewDetached()200 protected void onViewDetached() { 201 mConfigurationController.removeCallback(mConfigurationListener); 202 mKeyguardUpdateMonitor.removeCallback(mKeyguardUpdateMonitorCallback); 203 mStatusBarStateController.removeCallback(mStatusBarStateListener); 204 mKeyguardStateController.removeCallback(mKeyguardStateCallback); 205 206 if (mCancelDelayedUpdateVisibilityRunnable != null) { 207 mCancelDelayedUpdateVisibilityRunnable.run(); 208 mCancelDelayedUpdateVisibilityRunnable = null; 209 } 210 } 211 getTop()212 public float getTop() { 213 return mView.getLocationTop(); 214 } 215 216 /** 217 * Set whether qs is expanded. When QS is expanded, don't show a DisabledUdfps affordance. 218 */ setQsExpanded(boolean expanded)219 public void setQsExpanded(boolean expanded) { 220 mQsExpanded = expanded; 221 updateVisibility(); 222 } 223 updateVisibility()224 private void updateVisibility() { 225 if (mCancelDelayedUpdateVisibilityRunnable != null) { 226 mCancelDelayedUpdateVisibilityRunnable.run(); 227 mCancelDelayedUpdateVisibilityRunnable = null; 228 } 229 230 if (!mIsKeyguardShowing) { 231 mView.setVisibility(View.INVISIBLE); 232 return; 233 } 234 235 boolean wasShowingFpIcon = mUdfpsEnrolled && !mShowUnlockIcon && !mShowLockIcon; 236 boolean wasShowingLockIcon = mShowLockIcon; 237 boolean wasShowingUnlockIcon = mShowUnlockIcon; 238 mShowLockIcon = !mCanDismissLockScreen && !mUserUnlockedWithBiometric && isLockScreen() 239 && (!mUdfpsEnrolled || !mRunningFPS); 240 mShowUnlockIcon = mCanDismissLockScreen && isLockScreen(); 241 242 final CharSequence prevContentDescription = mView.getContentDescription(); 243 if (mShowLockIcon) { 244 mView.setImageDrawable(mLockIcon); 245 mView.setVisibility(View.VISIBLE); 246 mView.setContentDescription(mLockedLabel); 247 } else if (mShowUnlockIcon) { 248 if (!wasShowingUnlockIcon) { 249 if (wasShowingFpIcon) { 250 mView.setImageDrawable(mFpToUnlockIcon); 251 mFpToUnlockIcon.forceAnimationOnUI(); 252 mFpToUnlockIcon.start(); 253 } else if (wasShowingLockIcon) { 254 mView.setImageDrawable(mLockToUnlockIcon); 255 mLockToUnlockIcon.forceAnimationOnUI(); 256 mLockToUnlockIcon.start(); 257 } else { 258 mView.setImageDrawable(mUnlockIcon); 259 } 260 } 261 mView.setVisibility(View.VISIBLE); 262 mView.setContentDescription(mUnlockedLabel); 263 } else { 264 mView.setVisibility(View.INVISIBLE); 265 mView.setContentDescription(null); 266 } 267 if (!Objects.equals(prevContentDescription, mView.getContentDescription()) 268 && mView.getContentDescription() != null) { 269 mView.announceForAccessibility(mView.getContentDescription()); 270 } 271 } 272 273 private final View.AccessibilityDelegate mAccessibilityDelegate = 274 new View.AccessibilityDelegate() { 275 private final AccessibilityNodeInfo.AccessibilityAction mAccessibilityAuthenticateHint = 276 new AccessibilityNodeInfo.AccessibilityAction( 277 AccessibilityNodeInfoCompat.ACTION_CLICK, 278 getResources().getString(R.string.accessibility_authenticate_hint)); 279 private final AccessibilityNodeInfo.AccessibilityAction mAccessibilityEnterHint = 280 new AccessibilityNodeInfo.AccessibilityAction( 281 AccessibilityNodeInfoCompat.ACTION_CLICK, 282 getResources().getString(R.string.accessibility_enter_hint)); 283 public void onInitializeAccessibilityNodeInfo(View v, AccessibilityNodeInfo info) { 284 super.onInitializeAccessibilityNodeInfo(v, info); 285 if (isClickable()) { 286 if (mShowLockIcon) { 287 info.addAction(mAccessibilityAuthenticateHint); 288 } else if (mShowUnlockIcon) { 289 info.addAction(mAccessibilityEnterHint); 290 } 291 } 292 } 293 }; 294 isLockScreen()295 private boolean isLockScreen() { 296 return !mIsDozing 297 && !mIsBouncerShowing 298 && !mQsExpanded 299 && mStatusBarState == StatusBarState.KEYGUARD; 300 } 301 updateKeyguardShowing()302 private void updateKeyguardShowing() { 303 mIsKeyguardShowing = mKeyguardStateController.isShowing() 304 && !mKeyguardStateController.isKeyguardGoingAway(); 305 } 306 updateColors()307 private void updateColors() { 308 mView.updateColorAndBackgroundVisibility(mUdfpsSupported); 309 } 310 updateConfiguration()311 private void updateConfiguration() { 312 final DisplayMetrics metrics = mView.getContext().getResources().getDisplayMetrics(); 313 mWidthPixels = metrics.widthPixels; 314 mHeightPixels = metrics.heightPixels; 315 mBottomPadding = mView.getContext().getResources().getDimensionPixelSize( 316 R.dimen.lock_icon_margin_bottom); 317 318 mUnlockedLabel = mView.getContext().getResources().getString( 319 R.string.accessibility_unlock_button); 320 mLockedLabel = mView.getContext() 321 .getResources().getString(R.string.accessibility_lock_icon); 322 323 updateLockIconLocation(); 324 } 325 updateLockIconLocation()326 private void updateLockIconLocation() { 327 if (mUdfpsSupported) { 328 FingerprintSensorPropertiesInternal props = mAuthController.getUdfpsProps().get(0); 329 mView.setCenterLocation(new PointF(props.sensorLocationX, props.sensorLocationY), 330 props.sensorRadius); 331 } else { 332 mView.setCenterLocation( 333 new PointF(mWidthPixels / 2, 334 mHeightPixels - mBottomPadding - sDistAboveKgBottomAreaPx 335 - sLockIconRadiusPx), sLockIconRadiusPx); 336 } 337 338 mView.getHitRect(mSensorTouchLocation); 339 } 340 341 @Override dump(@onNull FileDescriptor fd, @NonNull PrintWriter pw, @NonNull String[] args)342 public void dump(@NonNull FileDescriptor fd, @NonNull PrintWriter pw, @NonNull String[] args) { 343 pw.println("mUdfpsEnrolled: " + mUdfpsEnrolled); 344 pw.println("mIsKeyguardShowing: " + mIsKeyguardShowing); 345 pw.println(" mShowUnlockIcon: " + mShowUnlockIcon); 346 pw.println(" mShowLockIcon: " + mShowLockIcon); 347 pw.println(" mIsDozing: " + mIsDozing); 348 pw.println(" mIsBouncerShowing: " + mIsBouncerShowing); 349 pw.println(" mUserUnlockedWithBiometric: " + mUserUnlockedWithBiometric); 350 pw.println(" mRunningFPS: " + mRunningFPS); 351 pw.println(" mCanDismissLockScreen: " + mCanDismissLockScreen); 352 pw.println(" mStatusBarState: " + StatusBarState.toShortString(mStatusBarState)); 353 pw.println(" mQsExpanded: " + mQsExpanded); 354 355 if (mView != null) { 356 mView.dump(fd, pw, args); 357 } 358 } 359 360 private StatusBarStateController.StateListener mStatusBarStateListener = 361 new StatusBarStateController.StateListener() { 362 @Override 363 public void onDozingChanged(boolean isDozing) { 364 mIsDozing = isDozing; 365 updateVisibility(); 366 } 367 368 @Override 369 public void onStateChanged(int statusBarState) { 370 mStatusBarState = statusBarState; 371 updateVisibility(); 372 } 373 }; 374 375 private final KeyguardUpdateMonitorCallback mKeyguardUpdateMonitorCallback = 376 new KeyguardUpdateMonitorCallback() { 377 @Override 378 public void onKeyguardVisibilityChanged(boolean showing) { 379 // reset mIsBouncerShowing state in case it was preemptively set 380 // onAffordanceClick 381 mIsBouncerShowing = mKeyguardViewController.isBouncerShowing(); 382 updateVisibility(); 383 } 384 385 @Override 386 public void onKeyguardBouncerChanged(boolean bouncer) { 387 mIsBouncerShowing = bouncer; 388 updateVisibility(); 389 } 390 391 @Override 392 public void onBiometricRunningStateChanged(boolean running, 393 BiometricSourceType biometricSourceType) { 394 mUserUnlockedWithBiometric = 395 mKeyguardUpdateMonitor.getUserUnlockedWithBiometric( 396 KeyguardUpdateMonitor.getCurrentUser()); 397 398 if (biometricSourceType == FINGERPRINT) { 399 mRunningFPS = running; 400 if (!mRunningFPS) { 401 if (mCancelDelayedUpdateVisibilityRunnable != null) { 402 mCancelDelayedUpdateVisibilityRunnable.run(); 403 } 404 405 // For some devices, auth is cancelled immediately on screen off but 406 // before dozing state is set. We want to avoid briefly showing the 407 // button in this case, so we delay updating the visibility by 50ms. 408 mCancelDelayedUpdateVisibilityRunnable = 409 mExecutor.executeDelayed(() -> updateVisibility(), 50); 410 } else { 411 updateVisibility(); 412 } 413 } 414 } 415 }; 416 417 private final KeyguardStateController.Callback mKeyguardStateCallback = 418 new KeyguardStateController.Callback() { 419 @Override 420 public void onUnlockedChanged() { 421 mCanDismissLockScreen = mKeyguardStateController.canDismissLockScreen(); 422 updateKeyguardShowing(); 423 updateVisibility(); 424 } 425 426 @Override 427 public void onKeyguardShowingChanged() { 428 // Reset values in case biometrics were removed (ie: pin/pattern/password => swipe). 429 // If biometrics were removed, local vars mCanDismissLockScreen and 430 // mUserUnlockedWithBiometric may not be updated. 431 mCanDismissLockScreen = mKeyguardStateController.canDismissLockScreen(); 432 updateKeyguardShowing(); 433 if (mIsKeyguardShowing) { 434 mUserUnlockedWithBiometric = 435 mKeyguardUpdateMonitor.getUserUnlockedWithBiometric( 436 KeyguardUpdateMonitor.getCurrentUser()); 437 } 438 mUdfpsEnrolled = mKeyguardUpdateMonitor.isUdfpsEnrolled(); 439 updateVisibility(); 440 } 441 442 @Override 443 public void onKeyguardFadingAwayChanged() { 444 updateKeyguardShowing(); 445 updateVisibility(); 446 } 447 }; 448 449 private final ConfigurationController.ConfigurationListener mConfigurationListener = 450 new ConfigurationController.ConfigurationListener() { 451 @Override 452 public void onUiModeChanged() { 453 updateColors(); 454 } 455 456 @Override 457 public void onThemeChanged() { 458 updateColors(); 459 } 460 461 @Override 462 public void onOverlayChanged() { 463 updateColors(); 464 } 465 466 @Override 467 public void onConfigChanged(Configuration newConfig) { 468 updateConfiguration(); 469 updateColors(); 470 } 471 }; 472 473 private final GestureDetector mGestureDetector = 474 new GestureDetector(new SimpleOnGestureListener() { 475 public boolean onDown(MotionEvent e) { 476 mDetectedLongPress = false; 477 if (!isClickable()) { 478 mDownDetected = false; 479 return false; 480 } 481 482 // intercept all following touches until we see MotionEvent.ACTION_CANCEL UP or 483 // MotionEvent.ACTION_UP (see #onTouchEvent) 484 mDownDetected = true; 485 if (mVibrator != null) { 486 mVibrator.vibrate( 487 Process.myUid(), 488 getContext().getOpPackageName(), 489 UdfpsController.EFFECT_CLICK, 490 "lockIcon-onDown", 491 VIBRATION_SONIFICATION_ATTRIBUTES); 492 } 493 return true; 494 } 495 496 public void onLongPress(MotionEvent e) { 497 if (!wasClickableOnDownEvent()) { 498 return; 499 } 500 501 if (mVibrator != null) { 502 mVibrator.vibrate( 503 Process.myUid(), 504 getContext().getOpPackageName(), 505 UdfpsController.EFFECT_CLICK, 506 "lockIcon-onLongPress", 507 VIBRATION_SONIFICATION_ATTRIBUTES); 508 } 509 mDetectedLongPress = true; 510 onAffordanceClick(); 511 } 512 513 public boolean onSingleTapUp(MotionEvent e) { 514 if (!wasClickableOnDownEvent()) { 515 return false; 516 } 517 onAffordanceClick(); 518 return true; 519 } 520 521 public boolean onFling(MotionEvent e1, MotionEvent e2, 522 float velocityX, float velocityY) { 523 if (!wasClickableOnDownEvent()) { 524 return false; 525 } 526 onAffordanceClick(); 527 return true; 528 } 529 530 private boolean wasClickableOnDownEvent() { 531 return mDownDetected; 532 } 533 534 private void onAffordanceClick() { 535 if (mFalsingManager.isFalseTouch(LOCK_ICON)) { 536 return; 537 } 538 539 // pre-emptively set to true to hide view 540 mIsBouncerShowing = true; 541 updateVisibility(); 542 mKeyguardViewController.showBouncer(/* scrim */ true); 543 } 544 }); 545 546 /** 547 * Send touch events to this view and handles it if the touch is within this view and we are 548 * in a 'clickable' state 549 * @return whether to intercept the touch event 550 */ onTouchEvent(MotionEvent event)551 public boolean onTouchEvent(MotionEvent event) { 552 if (mSensorTouchLocation.contains((int) event.getX(), (int) event.getY()) 553 && mView.getVisibility() == View.VISIBLE) { 554 mGestureDetector.onTouchEvent(event); 555 } 556 557 // we continue to intercept all following touches until we see MotionEvent.ACTION_CANCEL UP 558 // or MotionEvent.ACTION_UP. this is to avoid passing the touch to NPV 559 // after the lock icon disappears on device entry 560 if (mDownDetected && mDetectedLongPress) { 561 if (event.getAction() == MotionEvent.ACTION_CANCEL 562 || event.getAction() == MotionEvent.ACTION_UP) { 563 mDownDetected = false; 564 } 565 return true; 566 } 567 return false; 568 } 569 isClickable()570 private boolean isClickable() { 571 return mUdfpsSupported || mShowUnlockIcon; 572 } 573 574 /** 575 * Set the alpha of this view. 576 */ setAlpha(float alpha)577 public void setAlpha(float alpha) { 578 mView.setAlpha(alpha); 579 } 580 } 581