1 /* 2 * Copyright (C) 2014 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 package com.android.keyguard; 17 18 import static android.view.WindowInsets.Type.ime; 19 import static android.view.WindowInsets.Type.systemBars; 20 import static android.view.WindowInsetsAnimation.Callback.DISPATCH_MODE_STOP; 21 22 import static java.lang.Integer.max; 23 24 import android.animation.Animator; 25 import android.animation.AnimatorListenerAdapter; 26 import android.animation.ValueAnimator; 27 import android.app.Activity; 28 import android.app.AlertDialog; 29 import android.content.Context; 30 import android.graphics.Rect; 31 import android.provider.Settings; 32 import android.util.AttributeSet; 33 import android.util.MathUtils; 34 import android.util.TypedValue; 35 import android.view.Gravity; 36 import android.view.MotionEvent; 37 import android.view.VelocityTracker; 38 import android.view.View; 39 import android.view.ViewConfiguration; 40 import android.view.WindowInsets; 41 import android.view.WindowInsetsAnimation; 42 import android.view.WindowManager; 43 import android.view.animation.AnimationUtils; 44 import android.view.animation.Interpolator; 45 import android.widget.FrameLayout; 46 47 import androidx.annotation.Nullable; 48 import androidx.annotation.VisibleForTesting; 49 import androidx.dynamicanimation.animation.DynamicAnimation; 50 import androidx.dynamicanimation.animation.SpringAnimation; 51 52 import com.android.internal.jank.InteractionJankMonitor; 53 import com.android.internal.logging.UiEvent; 54 import com.android.internal.logging.UiEventLogger; 55 import com.android.internal.widget.LockPatternUtils; 56 import com.android.keyguard.KeyguardSecurityModel.SecurityMode; 57 import com.android.systemui.Gefingerpoken; 58 import com.android.systemui.R; 59 import com.android.systemui.animation.Interpolators; 60 import com.android.systemui.shared.system.SysUiStatsLog; 61 62 import java.util.ArrayList; 63 import java.util.List; 64 65 public class KeyguardSecurityContainer extends FrameLayout { 66 static final int USER_TYPE_PRIMARY = 1; 67 static final int USER_TYPE_WORK_PROFILE = 2; 68 static final int USER_TYPE_SECONDARY_USER = 3; 69 70 // Bouncer is dismissed due to no security. 71 static final int BOUNCER_DISMISS_NONE_SECURITY = 0; 72 // Bouncer is dismissed due to pin, password or pattern entered. 73 static final int BOUNCER_DISMISS_PASSWORD = 1; 74 // Bouncer is dismissed due to biometric (face, fingerprint or iris) authenticated. 75 static final int BOUNCER_DISMISS_BIOMETRIC = 2; 76 // Bouncer is dismissed due to extended access granted. 77 static final int BOUNCER_DISMISS_EXTENDED_ACCESS = 3; 78 // Bouncer is dismissed due to sim card unlock code entered. 79 static final int BOUNCER_DISMISS_SIM = 4; 80 81 // Make the view move slower than the finger, as if the spring were applying force. 82 private static final float TOUCH_Y_MULTIPLIER = 0.25f; 83 // How much you need to drag the bouncer to trigger an auth retry (in dps.) 84 private static final float MIN_DRAG_SIZE = 10; 85 // How much to scale the default slop by, to avoid accidental drags. 86 private static final float SLOP_SCALE = 4f; 87 88 private static final long IME_DISAPPEAR_DURATION_MS = 125; 89 90 // The duration of the animation to switch bouncer sides. 91 private static final long BOUNCER_HANDEDNESS_ANIMATION_DURATION_MS = 500; 92 93 // How much of the switch sides animation should be dedicated to fading the bouncer out. The 94 // remainder will fade it back in again. 95 private static final float BOUNCER_HANDEDNESS_ANIMATION_FADE_OUT_PROPORTION = 0.2f; 96 97 @VisibleForTesting 98 KeyguardSecurityViewFlipper mSecurityViewFlipper; 99 private AlertDialog mAlertDialog; 100 private boolean mSwipeUpToRetry; 101 102 private final ViewConfiguration mViewConfiguration; 103 private final SpringAnimation mSpringAnimation; 104 private final VelocityTracker mVelocityTracker = VelocityTracker.obtain(); 105 private final List<Gefingerpoken> mMotionEventListeners = new ArrayList<>(); 106 107 private float mLastTouchY = -1; 108 private int mActivePointerId = -1; 109 private boolean mIsDragging; 110 private float mStartTouchY = -1; 111 private boolean mDisappearAnimRunning; 112 private SwipeListener mSwipeListener; 113 114 private boolean mIsSecurityViewLeftAligned = true; 115 private boolean mOneHandedMode = false; 116 @Nullable private ValueAnimator mRunningOneHandedAnimator; 117 118 private final WindowInsetsAnimation.Callback mWindowInsetsAnimationCallback = 119 new WindowInsetsAnimation.Callback(DISPATCH_MODE_STOP) { 120 121 private final Rect mInitialBounds = new Rect(); 122 private final Rect mFinalBounds = new Rect(); 123 124 @Override 125 public void onPrepare(WindowInsetsAnimation animation) { 126 mSecurityViewFlipper.getBoundsOnScreen(mInitialBounds); 127 } 128 129 @Override 130 public WindowInsetsAnimation.Bounds onStart(WindowInsetsAnimation animation, 131 WindowInsetsAnimation.Bounds bounds) { 132 if (!mDisappearAnimRunning) { 133 beginJankInstrument(InteractionJankMonitor.CUJ_LOCKSCREEN_PASSWORD_APPEAR); 134 } else { 135 beginJankInstrument( 136 InteractionJankMonitor.CUJ_LOCKSCREEN_PASSWORD_DISAPPEAR); 137 } 138 mSecurityViewFlipper.getBoundsOnScreen(mFinalBounds); 139 return bounds; 140 } 141 142 @Override 143 public WindowInsets onProgress(WindowInsets windowInsets, 144 List<WindowInsetsAnimation> list) { 145 float start = mDisappearAnimRunning 146 ? -(mFinalBounds.bottom - mInitialBounds.bottom) 147 : mInitialBounds.bottom - mFinalBounds.bottom; 148 float end = mDisappearAnimRunning 149 ? -((mFinalBounds.bottom - mInitialBounds.bottom) * 0.75f) 150 : 0f; 151 int translationY = 0; 152 float interpolatedFraction = 1f; 153 for (WindowInsetsAnimation animation : list) { 154 if ((animation.getTypeMask() & WindowInsets.Type.ime()) == 0) { 155 continue; 156 } 157 interpolatedFraction = animation.getInterpolatedFraction(); 158 159 final int paddingBottom = (int) MathUtils.lerp( 160 start, end, 161 interpolatedFraction); 162 translationY += paddingBottom; 163 } 164 mSecurityViewFlipper.animateForIme(translationY, interpolatedFraction, 165 !mDisappearAnimRunning); 166 167 return windowInsets; 168 } 169 170 @Override 171 public void onEnd(WindowInsetsAnimation animation) { 172 if (!mDisappearAnimRunning) { 173 endJankInstrument(InteractionJankMonitor.CUJ_LOCKSCREEN_PASSWORD_APPEAR); 174 mSecurityViewFlipper.animateForIme(0, /* interpolatedFraction */ 1f, 175 true /* appearingAnim */); 176 } else { 177 endJankInstrument(InteractionJankMonitor.CUJ_LOCKSCREEN_PASSWORD_DISAPPEAR); 178 } 179 } 180 }; 181 182 // Used to notify the container when something interesting happens. 183 public interface SecurityCallback { dismiss(boolean authenticated, int targetUserId, boolean bypassSecondaryLockScreen)184 boolean dismiss(boolean authenticated, int targetUserId, boolean bypassSecondaryLockScreen); 185 userActivity()186 void userActivity(); 187 onSecurityModeChanged(SecurityMode securityMode, boolean needsInput)188 void onSecurityModeChanged(SecurityMode securityMode, boolean needsInput); 189 190 /** 191 * @param strongAuth wheher the user has authenticated with strong authentication like 192 * pattern, password or PIN but not by trust agents or fingerprint 193 * @param targetUserId a user that needs to be the foreground user at the finish completion. 194 */ finish(boolean strongAuth, int targetUserId)195 void finish(boolean strongAuth, int targetUserId); 196 reset()197 void reset(); 198 onCancelClicked()199 void onCancelClicked(); 200 } 201 202 public interface SwipeListener { onSwipeUp()203 void onSwipeUp(); 204 } 205 206 @VisibleForTesting 207 public enum BouncerUiEvent implements UiEventLogger.UiEventEnum { 208 @UiEvent(doc = "Default UiEvent used for variable initialization.") 209 UNKNOWN(0), 210 211 @UiEvent(doc = "Bouncer is dismissed using extended security access.") 212 BOUNCER_DISMISS_EXTENDED_ACCESS(413), 213 214 @UiEvent(doc = "Bouncer is dismissed using biometric.") 215 BOUNCER_DISMISS_BIOMETRIC(414), 216 217 @UiEvent(doc = "Bouncer is dismissed without security access.") 218 BOUNCER_DISMISS_NONE_SECURITY(415), 219 220 @UiEvent(doc = "Bouncer is dismissed using password security.") 221 BOUNCER_DISMISS_PASSWORD(416), 222 223 @UiEvent(doc = "Bouncer is dismissed using sim security access.") 224 BOUNCER_DISMISS_SIM(417), 225 226 @UiEvent(doc = "Bouncer is successfully unlocked using password.") 227 BOUNCER_PASSWORD_SUCCESS(418), 228 229 @UiEvent(doc = "An attempt to unlock bouncer using password has failed.") 230 BOUNCER_PASSWORD_FAILURE(419); 231 232 private final int mId; 233 BouncerUiEvent(int id)234 BouncerUiEvent(int id) { 235 mId = id; 236 } 237 238 @Override getId()239 public int getId() { 240 return mId; 241 } 242 } 243 KeyguardSecurityContainer(Context context, AttributeSet attrs)244 public KeyguardSecurityContainer(Context context, AttributeSet attrs) { 245 this(context, attrs, 0); 246 } 247 KeyguardSecurityContainer(Context context)248 public KeyguardSecurityContainer(Context context) { 249 this(context, null, 0); 250 } 251 KeyguardSecurityContainer(Context context, AttributeSet attrs, int defStyle)252 public KeyguardSecurityContainer(Context context, AttributeSet attrs, int defStyle) { 253 super(context, attrs, defStyle); 254 mSpringAnimation = new SpringAnimation(this, DynamicAnimation.Y); 255 mViewConfiguration = ViewConfiguration.get(context); 256 } 257 onResume(SecurityMode securityMode, boolean faceAuthEnabled)258 void onResume(SecurityMode securityMode, boolean faceAuthEnabled) { 259 mSecurityViewFlipper.setWindowInsetsAnimationCallback(mWindowInsetsAnimationCallback); 260 updateBiometricRetry(securityMode, faceAuthEnabled); 261 } 262 263 /** 264 * Sets whether this security container is in one handed mode. If so, it will measure its 265 * child SecurityViewFlipper in one half of the screen, and move it when tapping on the opposite 266 * side of the screen. 267 */ setOneHandedMode(boolean oneHandedMode)268 public void setOneHandedMode(boolean oneHandedMode) { 269 mOneHandedMode = oneHandedMode; 270 updateSecurityViewGravity(); 271 updateSecurityViewLocation(false); 272 } 273 274 /** Returns whether this security container is in one-handed mode. */ isOneHandedMode()275 public boolean isOneHandedMode() { 276 return mOneHandedMode; 277 } 278 279 /** 280 * When in one-handed mode, sets if the inner SecurityViewFlipper should be aligned to the 281 * left-hand side of the screen or not, and whether to animate when moving between the two. 282 */ setOneHandedModeLeftAligned(boolean leftAligned, boolean animate)283 public void setOneHandedModeLeftAligned(boolean leftAligned, boolean animate) { 284 mIsSecurityViewLeftAligned = leftAligned; 285 updateSecurityViewLocation(animate); 286 } 287 288 /** Returns whether the inner SecurityViewFlipper is left-aligned when in one-handed mode. */ isOneHandedModeLeftAligned()289 public boolean isOneHandedModeLeftAligned() { 290 return mIsSecurityViewLeftAligned; 291 } 292 updateSecurityViewGravity()293 private void updateSecurityViewGravity() { 294 if (mSecurityViewFlipper == null) { 295 return; 296 } 297 298 FrameLayout.LayoutParams lp = 299 (FrameLayout.LayoutParams) mSecurityViewFlipper.getLayoutParams(); 300 301 if (mOneHandedMode) { 302 lp.gravity = Gravity.LEFT | Gravity.BOTTOM; 303 } else { 304 lp.gravity = Gravity.CENTER_HORIZONTAL; 305 } 306 307 mSecurityViewFlipper.setLayoutParams(lp); 308 } 309 310 /** 311 * Moves the inner security view to the correct location (in one handed mode) with animation. 312 * This is triggered when the user taps on the side of the screen that is not currently occupied 313 * by the security view . 314 */ updateSecurityViewLocation(boolean animate)315 private void updateSecurityViewLocation(boolean animate) { 316 if (mSecurityViewFlipper == null) { 317 return; 318 } 319 320 if (!mOneHandedMode) { 321 mSecurityViewFlipper.setTranslationX(0); 322 return; 323 } 324 325 if (mRunningOneHandedAnimator != null) { 326 mRunningOneHandedAnimator.cancel(); 327 mRunningOneHandedAnimator = null; 328 } 329 330 int targetTranslation = mIsSecurityViewLeftAligned 331 ? 0 : (int) (getMeasuredWidth() - mSecurityViewFlipper.getWidth()); 332 333 if (animate) { 334 // This animation is a bit fun to implement. The bouncer needs to move, and fade in/out 335 // at the same time. The issue is, the bouncer should only move a short amount (120dp or 336 // so), but obviously needs to go from one side of the screen to the other. This needs a 337 // pretty custom animation. 338 // 339 // This works as follows. It uses a ValueAnimation to simply drive the animation 340 // progress. This animator is responsible for both the translation of the bouncer, and 341 // the current fade. It will fade the bouncer out while also moving it along the 120dp 342 // path. Once the bouncer is fully faded out though, it will "snap" the bouncer closer 343 // to its destination, then fade it back in again. The effect is that the bouncer will 344 // move from 0 -> X while fading out, then (destination - X) -> destination while fading 345 // back in again. 346 // TODO(b/195012405): Make this animation properly abortable. 347 Interpolator positionInterpolator = AnimationUtils.loadInterpolator( 348 mContext, android.R.interpolator.fast_out_extra_slow_in); 349 Interpolator fadeOutInterpolator = Interpolators.FAST_OUT_LINEAR_IN; 350 Interpolator fadeInInterpolator = Interpolators.LINEAR_OUT_SLOW_IN; 351 352 mRunningOneHandedAnimator = ValueAnimator.ofFloat(0.0f, 1.0f); 353 mRunningOneHandedAnimator.setDuration(BOUNCER_HANDEDNESS_ANIMATION_DURATION_MS); 354 mRunningOneHandedAnimator.setInterpolator(Interpolators.LINEAR); 355 356 int initialTranslation = (int) mSecurityViewFlipper.getTranslationX(); 357 int totalTranslation = (int) getResources().getDimension( 358 R.dimen.one_handed_bouncer_move_animation_translation); 359 360 final boolean shouldRestoreLayerType = mSecurityViewFlipper.hasOverlappingRendering() 361 && mSecurityViewFlipper.getLayerType() != View.LAYER_TYPE_HARDWARE; 362 if (shouldRestoreLayerType) { 363 mSecurityViewFlipper.setLayerType(View.LAYER_TYPE_HARDWARE, /* paint= */null); 364 } 365 366 float initialAlpha = mSecurityViewFlipper.getAlpha(); 367 368 mRunningOneHandedAnimator.addListener(new AnimatorListenerAdapter() { 369 @Override 370 public void onAnimationEnd(Animator animation) { 371 mRunningOneHandedAnimator = null; 372 } 373 }); 374 mRunningOneHandedAnimator.addUpdateListener(animation -> { 375 float switchPoint = BOUNCER_HANDEDNESS_ANIMATION_FADE_OUT_PROPORTION; 376 boolean isFadingOut = animation.getAnimatedFraction() < switchPoint; 377 378 int currentTranslation = (int) (positionInterpolator.getInterpolation( 379 animation.getAnimatedFraction()) * totalTranslation); 380 int translationRemaining = totalTranslation - currentTranslation; 381 382 // Flip the sign if we're going from right to left. 383 if (mIsSecurityViewLeftAligned) { 384 currentTranslation = -currentTranslation; 385 translationRemaining = -translationRemaining; 386 } 387 388 if (isFadingOut) { 389 // The bouncer fades out over the first X%. 390 float fadeOutFraction = MathUtils.constrainedMap( 391 /* rangeMin= */1.0f, 392 /* rangeMax= */0.0f, 393 /* valueMin= */0.0f, 394 /* valueMax= */switchPoint, 395 animation.getAnimatedFraction()); 396 float opacity = fadeOutInterpolator.getInterpolation(fadeOutFraction); 397 398 // When fading out, the alpha needs to start from the initial opacity of the 399 // view flipper, otherwise we get a weird bit of jank as it ramps back to 100%. 400 mSecurityViewFlipper.setAlpha(opacity * initialAlpha); 401 402 // Animate away from the source. 403 mSecurityViewFlipper.setTranslationX(initialTranslation + currentTranslation); 404 } else { 405 // And in again over the remaining (100-X)%. 406 float fadeInFraction = MathUtils.constrainedMap( 407 /* rangeMin= */0.0f, 408 /* rangeMax= */1.0f, 409 /* valueMin= */switchPoint, 410 /* valueMax= */1.0f, 411 animation.getAnimatedFraction()); 412 413 float opacity = fadeInInterpolator.getInterpolation(fadeInFraction); 414 mSecurityViewFlipper.setAlpha(opacity); 415 416 // Fading back in, animate towards the destination. 417 mSecurityViewFlipper.setTranslationX(targetTranslation - translationRemaining); 418 } 419 420 if (animation.getAnimatedFraction() == 1.0f && shouldRestoreLayerType) { 421 mSecurityViewFlipper.setLayerType(View.LAYER_TYPE_NONE, /* paint= */null); 422 } 423 }); 424 425 mRunningOneHandedAnimator.start(); 426 } else { 427 mSecurityViewFlipper.setTranslationX(targetTranslation); 428 } 429 } 430 onPause()431 public void onPause() { 432 if (mAlertDialog != null) { 433 mAlertDialog.dismiss(); 434 mAlertDialog = null; 435 } 436 mSecurityViewFlipper.setWindowInsetsAnimationCallback(null); 437 } 438 439 @Override shouldDelayChildPressedState()440 public boolean shouldDelayChildPressedState() { 441 return true; 442 } 443 444 @Override onInterceptTouchEvent(MotionEvent event)445 public boolean onInterceptTouchEvent(MotionEvent event) { 446 boolean result = mMotionEventListeners.stream().anyMatch( 447 listener -> listener.onInterceptTouchEvent(event)) 448 || super.onInterceptTouchEvent(event); 449 450 switch (event.getActionMasked()) { 451 case MotionEvent.ACTION_DOWN: 452 int pointerIndex = event.getActionIndex(); 453 mStartTouchY = event.getY(pointerIndex); 454 mActivePointerId = event.getPointerId(pointerIndex); 455 mVelocityTracker.clear(); 456 break; 457 case MotionEvent.ACTION_MOVE: 458 if (mIsDragging) { 459 return true; 460 } 461 if (!mSwipeUpToRetry) { 462 return false; 463 } 464 // Avoid dragging the pattern view 465 if (mSecurityViewFlipper.getSecurityView().disallowInterceptTouch(event)) { 466 return false; 467 } 468 int index = event.findPointerIndex(mActivePointerId); 469 float touchSlop = mViewConfiguration.getScaledTouchSlop() * SLOP_SCALE; 470 if (index != -1 && mStartTouchY - event.getY(index) > touchSlop) { 471 mIsDragging = true; 472 return true; 473 } 474 break; 475 case MotionEvent.ACTION_CANCEL: 476 case MotionEvent.ACTION_UP: 477 mIsDragging = false; 478 break; 479 } 480 return result; 481 } 482 483 @Override onTouchEvent(MotionEvent event)484 public boolean onTouchEvent(MotionEvent event) { 485 final int action = event.getActionMasked(); 486 487 boolean result = mMotionEventListeners.stream() 488 .anyMatch(listener -> listener.onTouchEvent(event)) 489 || super.onTouchEvent(event); 490 491 switch (action) { 492 case MotionEvent.ACTION_MOVE: 493 mVelocityTracker.addMovement(event); 494 int pointerIndex = event.findPointerIndex(mActivePointerId); 495 float y = event.getY(pointerIndex); 496 if (mLastTouchY != -1) { 497 float dy = y - mLastTouchY; 498 setTranslationY(getTranslationY() + dy * TOUCH_Y_MULTIPLIER); 499 } 500 mLastTouchY = y; 501 break; 502 case MotionEvent.ACTION_UP: 503 case MotionEvent.ACTION_CANCEL: 504 mActivePointerId = -1; 505 mLastTouchY = -1; 506 mIsDragging = false; 507 startSpringAnimation(mVelocityTracker.getYVelocity()); 508 break; 509 case MotionEvent.ACTION_POINTER_UP: 510 int index = event.getActionIndex(); 511 int pointerId = event.getPointerId(index); 512 if (pointerId == mActivePointerId) { 513 // This was our active pointer going up. Choose a new 514 // active pointer and adjust accordingly. 515 final int newPointerIndex = index == 0 ? 1 : 0; 516 mLastTouchY = event.getY(newPointerIndex); 517 mActivePointerId = event.getPointerId(newPointerIndex); 518 } 519 break; 520 } 521 if (action == MotionEvent.ACTION_UP) { 522 if (-getTranslationY() > TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 523 MIN_DRAG_SIZE, getResources().getDisplayMetrics())) { 524 if (mSwipeListener != null) { 525 mSwipeListener.onSwipeUp(); 526 } 527 } else { 528 if (!mIsDragging) { 529 handleTap(event); 530 } 531 } 532 } 533 return true; 534 } 535 addMotionEventListener(Gefingerpoken listener)536 void addMotionEventListener(Gefingerpoken listener) { 537 mMotionEventListeners.add(listener); 538 } 539 removeMotionEventListener(Gefingerpoken listener)540 void removeMotionEventListener(Gefingerpoken listener) { 541 mMotionEventListeners.remove(listener); 542 } 543 handleTap(MotionEvent event)544 private void handleTap(MotionEvent event) { 545 // If we're using a fullscreen security mode, skip 546 if (!mOneHandedMode) { 547 return; 548 } 549 550 moveBouncerForXCoordinate(event.getX(), /* animate= */true); 551 } 552 moveBouncerForXCoordinate(float x, boolean animate)553 private void moveBouncerForXCoordinate(float x, boolean animate) { 554 // Did the tap hit the "other" side of the bouncer? 555 if ((mIsSecurityViewLeftAligned && (x > getWidth() / 2f)) 556 || (!mIsSecurityViewLeftAligned && (x < getWidth() / 2f))) { 557 mIsSecurityViewLeftAligned = !mIsSecurityViewLeftAligned; 558 559 Settings.Global.putInt( 560 mContext.getContentResolver(), 561 Settings.Global.ONE_HANDED_KEYGUARD_SIDE, 562 mIsSecurityViewLeftAligned ? Settings.Global.ONE_HANDED_KEYGUARD_SIDE_LEFT 563 : Settings.Global.ONE_HANDED_KEYGUARD_SIDE_RIGHT); 564 565 int keyguardState = mIsSecurityViewLeftAligned 566 ? SysUiStatsLog.KEYGUARD_BOUNCER_STATE_CHANGED__STATE__SWITCH_LEFT 567 : SysUiStatsLog.KEYGUARD_BOUNCER_STATE_CHANGED__STATE__SWITCH_RIGHT; 568 SysUiStatsLog.write(SysUiStatsLog.KEYGUARD_BOUNCER_STATE_CHANGED, keyguardState); 569 570 updateSecurityViewLocation(animate); 571 } 572 } 573 setSwipeListener(SwipeListener swipeListener)574 void setSwipeListener(SwipeListener swipeListener) { 575 mSwipeListener = swipeListener; 576 } 577 startSpringAnimation(float startVelocity)578 private void startSpringAnimation(float startVelocity) { 579 mSpringAnimation 580 .setStartVelocity(startVelocity) 581 .animateToFinalPosition(0); 582 } 583 startDisappearAnimation(SecurityMode securitySelection)584 public void startDisappearAnimation(SecurityMode securitySelection) { 585 mDisappearAnimRunning = true; 586 } 587 beginJankInstrument(int cuj)588 private void beginJankInstrument(int cuj) { 589 KeyguardInputView securityView = mSecurityViewFlipper.getSecurityView(); 590 if (securityView == null) return; 591 InteractionJankMonitor.getInstance().begin(securityView, cuj); 592 } 593 endJankInstrument(int cuj)594 private void endJankInstrument(int cuj) { 595 InteractionJankMonitor.getInstance().end(cuj); 596 } 597 cancelJankInstrument(int cuj)598 private void cancelJankInstrument(int cuj) { 599 InteractionJankMonitor.getInstance().cancel(cuj); 600 } 601 602 /** 603 * Enables/disables swipe up to retry on the bouncer. 604 */ updateBiometricRetry(SecurityMode securityMode, boolean faceAuthEnabled)605 private void updateBiometricRetry(SecurityMode securityMode, boolean faceAuthEnabled) { 606 mSwipeUpToRetry = faceAuthEnabled 607 && securityMode != SecurityMode.SimPin 608 && securityMode != SecurityMode.SimPuk 609 && securityMode != SecurityMode.None; 610 } 611 getTitle()612 public CharSequence getTitle() { 613 return mSecurityViewFlipper.getTitle(); 614 } 615 616 617 @Override onFinishInflate()618 public void onFinishInflate() { 619 super.onFinishInflate(); 620 mSecurityViewFlipper = findViewById(R.id.view_flipper); 621 } 622 623 @Override onApplyWindowInsets(WindowInsets insets)624 public WindowInsets onApplyWindowInsets(WindowInsets insets) { 625 626 // Consume bottom insets because we're setting the padding locally (for IME and navbar.) 627 int bottomInset = insets.getInsetsIgnoringVisibility(systemBars()).bottom; 628 int imeInset = insets.getInsets(ime()).bottom; 629 int inset = max(bottomInset, imeInset); 630 setPadding(getPaddingLeft(), getPaddingTop(), getPaddingRight(), inset); 631 return insets.inset(0, 0, 0, inset); 632 } 633 showDialog(String title, String message)634 private void showDialog(String title, String message) { 635 if (mAlertDialog != null) { 636 mAlertDialog.dismiss(); 637 } 638 639 mAlertDialog = new AlertDialog.Builder(mContext) 640 .setTitle(title) 641 .setMessage(message) 642 .setCancelable(false) 643 .setNeutralButton(R.string.ok, null) 644 .create(); 645 if (!(mContext instanceof Activity)) { 646 mAlertDialog.getWindow().setType(WindowManager.LayoutParams.TYPE_KEYGUARD_DIALOG); 647 } 648 mAlertDialog.show(); 649 } 650 showTimeoutDialog(int userId, int timeoutMs, LockPatternUtils lockPatternUtils, SecurityMode securityMode)651 void showTimeoutDialog(int userId, int timeoutMs, LockPatternUtils lockPatternUtils, 652 SecurityMode securityMode) { 653 int timeoutInSeconds = timeoutMs / 1000; 654 int messageId = 0; 655 656 switch (securityMode) { 657 case Pattern: 658 messageId = R.string.kg_too_many_failed_pattern_attempts_dialog_message; 659 break; 660 case PIN: 661 messageId = R.string.kg_too_many_failed_pin_attempts_dialog_message; 662 break; 663 case Password: 664 messageId = R.string.kg_too_many_failed_password_attempts_dialog_message; 665 break; 666 // These don't have timeout dialogs. 667 case Invalid: 668 case None: 669 case SimPin: 670 case SimPuk: 671 break; 672 } 673 674 if (messageId != 0) { 675 final String message = mContext.getString(messageId, 676 lockPatternUtils.getCurrentFailedPasswordAttempts(userId), 677 timeoutInSeconds); 678 showDialog(null, message); 679 } 680 } 681 682 @Override onMeasure(int widthMeasureSpec, int heightMeasureSpec)683 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 684 int maxHeight = 0; 685 int maxWidth = 0; 686 int childState = 0; 687 688 int halfWidthMeasureSpec = MeasureSpec.makeMeasureSpec( 689 MeasureSpec.getSize(widthMeasureSpec) / 2, 690 MeasureSpec.getMode(widthMeasureSpec)); 691 692 for (int i = 0; i < getChildCount(); i++) { 693 final View view = getChildAt(i); 694 if (view.getVisibility() != GONE) { 695 if (mOneHandedMode && view == mSecurityViewFlipper) { 696 measureChildWithMargins(view, halfWidthMeasureSpec, 0, 697 heightMeasureSpec, 0); 698 } else { 699 measureChildWithMargins(view, widthMeasureSpec, 0, 700 heightMeasureSpec, 0); 701 } 702 final LayoutParams lp = (LayoutParams) view.getLayoutParams(); 703 maxWidth = Math.max(maxWidth, 704 view.getMeasuredWidth() + lp.leftMargin + lp.rightMargin); 705 maxHeight = Math.max(maxHeight, 706 view.getMeasuredHeight() + lp.topMargin + lp.bottomMargin); 707 childState = combineMeasuredStates(childState, view.getMeasuredState()); 708 } 709 } 710 711 maxWidth += getPaddingLeft() + getPaddingRight(); 712 maxHeight += getPaddingTop() + getPaddingBottom(); 713 714 // Check against our minimum height and width 715 maxHeight = Math.max(maxHeight, getSuggestedMinimumHeight()); 716 maxWidth = Math.max(maxWidth, getSuggestedMinimumWidth()); 717 718 setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState), 719 resolveSizeAndState(maxHeight, heightMeasureSpec, 720 childState << MEASURED_HEIGHT_STATE_SHIFT)); 721 } 722 723 @Override onLayout(boolean changed, int left, int top, int right, int bottom)724 protected void onLayout(boolean changed, int left, int top, int right, int bottom) { 725 super.onLayout(changed, left, top, right, bottom); 726 727 // After a layout pass, we need to re-place the inner bouncer, as our bounds may have 728 // changed. 729 updateSecurityViewLocation(/* animate= */false); 730 } 731 showAlmostAtWipeDialog(int attempts, int remaining, int userType)732 void showAlmostAtWipeDialog(int attempts, int remaining, int userType) { 733 String message = null; 734 switch (userType) { 735 case USER_TYPE_PRIMARY: 736 message = mContext.getString(R.string.kg_failed_attempts_almost_at_wipe, 737 attempts, remaining); 738 break; 739 case USER_TYPE_SECONDARY_USER: 740 message = mContext.getString(R.string.kg_failed_attempts_almost_at_erase_user, 741 attempts, remaining); 742 break; 743 case USER_TYPE_WORK_PROFILE: 744 message = mContext.getString(R.string.kg_failed_attempts_almost_at_erase_profile, 745 attempts, remaining); 746 break; 747 } 748 showDialog(null, message); 749 } 750 showWipeDialog(int attempts, int userType)751 void showWipeDialog(int attempts, int userType) { 752 String message = null; 753 switch (userType) { 754 case USER_TYPE_PRIMARY: 755 message = mContext.getString(R.string.kg_failed_attempts_now_wiping, 756 attempts); 757 break; 758 case USER_TYPE_SECONDARY_USER: 759 message = mContext.getString(R.string.kg_failed_attempts_now_erasing_user, 760 attempts); 761 break; 762 case USER_TYPE_WORK_PROFILE: 763 message = mContext.getString(R.string.kg_failed_attempts_now_erasing_profile, 764 attempts); 765 break; 766 } 767 showDialog(null, message); 768 } 769 reset()770 public void reset() { 771 mDisappearAnimRunning = false; 772 } 773 } 774 775