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