1 /*
2  * Copyright (C) 2007 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.internal.widget;
18 
19 import android.animation.Animator;
20 import android.animation.AnimatorListenerAdapter;
21 import android.animation.AnimatorSet;
22 import android.animation.ValueAnimator;
23 import android.annotation.Nullable;
24 import android.compat.annotation.UnsupportedAppUsage;
25 import android.content.Context;
26 import android.content.res.Resources;
27 import android.content.res.TypedArray;
28 import android.graphics.Canvas;
29 import android.graphics.CanvasProperty;
30 import android.graphics.Color;
31 import android.graphics.LinearGradient;
32 import android.graphics.Paint;
33 import android.graphics.Path;
34 import android.graphics.RecordingCanvas;
35 import android.graphics.Rect;
36 import android.graphics.Shader;
37 import android.graphics.drawable.Drawable;
38 import android.os.Bundle;
39 import android.os.Debug;
40 import android.os.Parcel;
41 import android.os.Parcelable;
42 import android.os.SystemClock;
43 import android.util.AttributeSet;
44 import android.util.IntArray;
45 import android.util.Log;
46 import android.util.SparseArray;
47 import android.util.TypedValue;
48 import android.view.HapticFeedbackConstants;
49 import android.view.MotionEvent;
50 import android.view.RenderNodeAnimator;
51 import android.view.View;
52 import android.view.accessibility.AccessibilityEvent;
53 import android.view.accessibility.AccessibilityManager;
54 import android.view.accessibility.AccessibilityNodeInfo;
55 import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction;
56 import android.view.animation.AnimationUtils;
57 import android.view.animation.Interpolator;
58 
59 import com.android.internal.R;
60 import com.android.internal.graphics.ColorUtils;
61 
62 import java.util.ArrayList;
63 import java.util.List;
64 
65 /**
66  * Displays and detects the user's unlock attempt, which is a drag of a finger
67  * across 9 regions of the screen.
68  *
69  * Is also capable of displaying a static pattern in "in progress", "wrong" or
70  * "correct" states.
71  */
72 public class LockPatternView extends View {
73     // Aspect to use when rendering this view
74     private static final int ASPECT_SQUARE = 0; // View will be the minimum of width/height
75     private static final int ASPECT_LOCK_WIDTH = 1; // Fixed width; height will be minimum of (w,h)
76     private static final int ASPECT_LOCK_HEIGHT = 2; // Fixed height; width will be minimum of (w,h)
77 
78     private static final boolean PROFILE_DRAWING = false;
79     private static final int LINE_END_ANIMATION_DURATION_MILLIS = 50;
80     private static final int DOT_ACTIVATION_DURATION_MILLIS = 50;
81     private static final int DOT_RADIUS_INCREASE_DURATION_MILLIS = 96;
82     private static final int DOT_RADIUS_DECREASE_DURATION_MILLIS = 192;
83     private static final float MIN_DOT_HIT_FACTOR = 0.2f;
84     private final CellState[][] mCellStates;
85 
86     private final int mDotSize;
87     private final int mDotSizeActivated;
88     private final float mDotHitFactor;
89     private final int mPathWidth;
90     private final int mLineFadeOutAnimationDurationMs;
91     private final int mLineFadeOutAnimationDelayMs;
92 
93     private boolean mDrawingProfilingStarted = false;
94 
95     @UnsupportedAppUsage
96     private final Paint mPaint = new Paint();
97     @UnsupportedAppUsage
98     private final Paint mPathPaint = new Paint();
99 
100     /**
101      * How many milliseconds we spend animating each circle of a lock pattern
102      * if the animating mode is set.  The entire animation should take this
103      * constant * the length of the pattern to complete.
104      */
105     private static final int MILLIS_PER_CIRCLE_ANIMATING = 700;
106 
107     /**
108      * This can be used to avoid updating the display for very small motions or noisy panels.
109      * It didn't seem to have much impact on the devices tested, so currently set to 0.
110      */
111     private static final float DRAG_THRESHHOLD = 0.0f;
112     public static final int VIRTUAL_BASE_VIEW_ID = 1;
113     public static final boolean DEBUG_A11Y = false;
114     private static final String TAG = "LockPatternView";
115 
116     private OnPatternListener mOnPatternListener;
117     @UnsupportedAppUsage
118     private final ArrayList<Cell> mPattern = new ArrayList<Cell>(9);
119 
120     /**
121      * Lookup table for the circles of the pattern we are currently drawing.
122      * This will be the cells of the complete pattern unless we are animating,
123      * in which case we use this to hold the cells we are drawing for the in
124      * progress animation.
125      */
126     private final boolean[][] mPatternDrawLookup = new boolean[3][3];
127 
128     /**
129      * the in progress point:
130      * - during interaction: where the user's finger is
131      * - during animation: the current tip of the animating line
132      */
133     private float mInProgressX = -1;
134     private float mInProgressY = -1;
135 
136     private long mAnimatingPeriodStart;
137     private long[] mLineFadeStart = new long[9];
138 
139     @UnsupportedAppUsage
140     private DisplayMode mPatternDisplayMode = DisplayMode.Correct;
141     private boolean mInputEnabled = true;
142     @UnsupportedAppUsage
143     private boolean mInStealthMode = false;
144     @UnsupportedAppUsage
145     private boolean mPatternInProgress = false;
146     private boolean mFadePattern = true;
147 
148     @UnsupportedAppUsage
149     private float mSquareWidth;
150     @UnsupportedAppUsage
151     private float mSquareHeight;
152     private float mDotHitRadius;
153     private final LinearGradient mFadeOutGradientShader;
154 
155     private final Path mCurrentPath = new Path();
156     private final Rect mInvalidate = new Rect();
157     private final Rect mTmpInvalidateRect = new Rect();
158 
159     private int mAspect;
160     private int mRegularColor;
161     private int mErrorColor;
162     private int mSuccessColor;
163     private int mDotColor;
164     private int mDotActivatedColor;
165 
166     private final Interpolator mFastOutSlowInInterpolator;
167     private final Interpolator mLinearOutSlowInInterpolator;
168     private final PatternExploreByTouchHelper mExploreByTouchHelper;
169 
170     private Drawable mSelectedDrawable;
171     private Drawable mNotSelectedDrawable;
172     private boolean mUseLockPatternDrawable;
173 
174     /**
175      * Represents a cell in the 3 X 3 matrix of the unlock pattern view.
176      */
177     public static final class Cell {
178         @UnsupportedAppUsage
179         final int row;
180         @UnsupportedAppUsage
181         final int column;
182 
183         // keep # objects limited to 9
184         private static final Cell[][] sCells = createCells();
185 
createCells()186         private static Cell[][] createCells() {
187             Cell[][] res = new Cell[3][3];
188             for (int i = 0; i < 3; i++) {
189                 for (int j = 0; j < 3; j++) {
190                     res[i][j] = new Cell(i, j);
191                 }
192             }
193             return res;
194         }
195 
196         /**
197          * @param row The row of the cell.
198          * @param column The column of the cell.
199          */
Cell(int row, int column)200         private Cell(int row, int column) {
201             checkRange(row, column);
202             this.row = row;
203             this.column = column;
204         }
205 
getRow()206         public int getRow() {
207             return row;
208         }
209 
getColumn()210         public int getColumn() {
211             return column;
212         }
213 
of(int row, int column)214         public static Cell of(int row, int column) {
215             checkRange(row, column);
216             return sCells[row][column];
217         }
218 
checkRange(int row, int column)219         private static void checkRange(int row, int column) {
220             if (row < 0 || row > 2) {
221                 throw new IllegalArgumentException("row must be in range 0-2");
222             }
223             if (column < 0 || column > 2) {
224                 throw new IllegalArgumentException("column must be in range 0-2");
225             }
226         }
227 
228         @Override
toString()229         public String toString() {
230             return "(row=" + row + ",clmn=" + column + ")";
231         }
232     }
233 
234     public static class CellState {
235         int row;
236         int col;
237         boolean hwAnimating;
238         CanvasProperty<Float> hwRadius;
239         CanvasProperty<Float> hwCenterX;
240         CanvasProperty<Float> hwCenterY;
241         CanvasProperty<Paint> hwPaint;
242         float radius;
243         float translationY;
244         float alpha = 1f;
245         float activationAnimationProgress;
246         public float lineEndX = Float.MIN_VALUE;
247         public float lineEndY = Float.MIN_VALUE;
248         @Nullable
249         Animator activationAnimator;
250      }
251 
252     /**
253      * How to display the current pattern.
254      */
255     public enum DisplayMode {
256 
257         /**
258          * The pattern drawn is correct (i.e draw it in a friendly color)
259          */
260         @UnsupportedAppUsage
261         Correct,
262 
263         /**
264          * Animate the pattern (for demo, and help).
265          */
266         @UnsupportedAppUsage
267         Animate,
268 
269         /**
270          * The pattern is wrong (i.e draw a foreboding color)
271          */
272         @UnsupportedAppUsage
273         Wrong
274     }
275 
276     /**
277      * The call back interface for detecting patterns entered by the user.
278      */
279     public static interface OnPatternListener {
280 
281         /**
282          * A new pattern has begun.
283          */
onPatternStart()284         void onPatternStart();
285 
286         /**
287          * The pattern was cleared.
288          */
onPatternCleared()289         void onPatternCleared();
290 
291         /**
292          * The user extended the pattern currently being drawn by one cell.
293          * @param pattern The pattern with newly added cell.
294          */
onPatternCellAdded(List<Cell> pattern)295         void onPatternCellAdded(List<Cell> pattern);
296 
297         /**
298          * A pattern was detected from the user.
299          * @param pattern The pattern.
300          */
onPatternDetected(List<Cell> pattern)301         void onPatternDetected(List<Cell> pattern);
302     }
303 
LockPatternView(Context context)304     public LockPatternView(Context context) {
305         this(context, null);
306     }
307 
308     @UnsupportedAppUsage
LockPatternView(Context context, AttributeSet attrs)309     public LockPatternView(Context context, AttributeSet attrs) {
310         super(context, attrs);
311 
312         TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.LockPatternView,
313                 R.attr.lockPatternStyle, R.style.Widget_LockPatternView);
314 
315         final String aspect = a.getString(R.styleable.LockPatternView_aspect);
316 
317         if ("square".equals(aspect)) {
318             mAspect = ASPECT_SQUARE;
319         } else if ("lock_width".equals(aspect)) {
320             mAspect = ASPECT_LOCK_WIDTH;
321         } else if ("lock_height".equals(aspect)) {
322             mAspect = ASPECT_LOCK_HEIGHT;
323         } else {
324             mAspect = ASPECT_SQUARE;
325         }
326 
327         setClickable(true);
328 
329 
330         mPathPaint.setAntiAlias(true);
331         mPathPaint.setDither(true);
332 
333         mRegularColor = a.getColor(R.styleable.LockPatternView_regularColor, 0);
334         mErrorColor = a.getColor(R.styleable.LockPatternView_errorColor, 0);
335         mSuccessColor = a.getColor(R.styleable.LockPatternView_successColor, 0);
336         mDotColor = a.getColor(R.styleable.LockPatternView_dotColor, mRegularColor);
337         mDotActivatedColor = a.getColor(R.styleable.LockPatternView_dotActivatedColor, mDotColor);
338 
339         int pathColor = a.getColor(R.styleable.LockPatternView_pathColor, mRegularColor);
340         mPathPaint.setColor(pathColor);
341 
342         mPathPaint.setStyle(Paint.Style.STROKE);
343         mPathPaint.setStrokeJoin(Paint.Join.ROUND);
344         mPathPaint.setStrokeCap(Paint.Cap.ROUND);
345 
346         mPathWidth = getResources().getDimensionPixelSize(R.dimen.lock_pattern_dot_line_width);
347         mPathPaint.setStrokeWidth(mPathWidth);
348 
349         mLineFadeOutAnimationDurationMs =
350             getResources().getInteger(R.integer.lock_pattern_line_fade_out_duration);
351         mLineFadeOutAnimationDelayMs =
352             getResources().getInteger(R.integer.lock_pattern_line_fade_out_delay);
353 
354         mDotSize = getResources().getDimensionPixelSize(R.dimen.lock_pattern_dot_size);
355         mDotSizeActivated = getResources().getDimensionPixelSize(
356                 R.dimen.lock_pattern_dot_size_activated);
357         TypedValue outValue = new TypedValue();
358         getResources().getValue(R.dimen.lock_pattern_dot_hit_factor, outValue, true);
359         mDotHitFactor = Math.max(Math.min(outValue.getFloat(), 1f), MIN_DOT_HIT_FACTOR);
360 
361         mUseLockPatternDrawable = getResources().getBoolean(R.bool.use_lock_pattern_drawable);
362         if (mUseLockPatternDrawable) {
363             mSelectedDrawable = getResources().getDrawable(R.drawable.lockscreen_selected);
364             mNotSelectedDrawable = getResources().getDrawable(R.drawable.lockscreen_notselected);
365         }
366 
367         mPaint.setAntiAlias(true);
368         mPaint.setDither(true);
369 
370         mCellStates = new CellState[3][3];
371         for (int i = 0; i < 3; i++) {
372             for (int j = 0; j < 3; j++) {
373                 mCellStates[i][j] = new CellState();
374                 mCellStates[i][j].radius = mDotSize/2;
375                 mCellStates[i][j].row = i;
376                 mCellStates[i][j].col = j;
377             }
378         }
379 
380         mFastOutSlowInInterpolator =
381                 AnimationUtils.loadInterpolator(context, android.R.interpolator.fast_out_slow_in);
382         mLinearOutSlowInInterpolator =
383                 AnimationUtils.loadInterpolator(context, android.R.interpolator.linear_out_slow_in);
384         mExploreByTouchHelper = new PatternExploreByTouchHelper(this);
385         setAccessibilityDelegate(mExploreByTouchHelper);
386 
387         int fadeAwayGradientWidth = getResources().getDimensionPixelSize(
388                 R.dimen.lock_pattern_fade_away_gradient_width);
389         // Set up gradient shader with the middle in point (0, 0).
390         mFadeOutGradientShader = new LinearGradient(/* x0= */ -fadeAwayGradientWidth / 2f,
391                 /* y0= */ 0,/* x1= */ fadeAwayGradientWidth / 2f, /* y1= */ 0,
392                 Color.TRANSPARENT, pathColor, Shader.TileMode.CLAMP);
393 
394         a.recycle();
395     }
396 
397     @UnsupportedAppUsage
getCellStates()398     public CellState[][] getCellStates() {
399         return mCellStates;
400     }
401 
402     /**
403      * @return Whether the view is in stealth mode.
404      */
isInStealthMode()405     public boolean isInStealthMode() {
406         return mInStealthMode;
407     }
408 
409     /**
410      * Set whether the view is in stealth mode.  If true, there will be no
411      * visible feedback as the user enters the pattern.
412      *
413      * @param inStealthMode Whether in stealth mode.
414      */
415     @UnsupportedAppUsage
setInStealthMode(boolean inStealthMode)416     public void setInStealthMode(boolean inStealthMode) {
417         mInStealthMode = inStealthMode;
418     }
419 
420     /**
421      * Set whether the pattern should fade as it's being drawn. If
422      * true, each segment of the pattern fades over time.
423      */
setFadePattern(boolean fadePattern)424     public void setFadePattern(boolean fadePattern) {
425         mFadePattern = fadePattern;
426     }
427 
428     /**
429      * Set the call back for pattern detection.
430      * @param onPatternListener The call back.
431      */
432     @UnsupportedAppUsage
setOnPatternListener( OnPatternListener onPatternListener)433     public void setOnPatternListener(
434             OnPatternListener onPatternListener) {
435         mOnPatternListener = onPatternListener;
436     }
437 
438     /**
439      * Set the pattern explicitely (rather than waiting for the user to input
440      * a pattern).
441      * @param displayMode How to display the pattern.
442      * @param pattern The pattern.
443      */
setPattern(DisplayMode displayMode, List<Cell> pattern)444     public void setPattern(DisplayMode displayMode, List<Cell> pattern) {
445         mPattern.clear();
446         mPattern.addAll(pattern);
447         clearPatternDrawLookup();
448         for (Cell cell : pattern) {
449             mPatternDrawLookup[cell.getRow()][cell.getColumn()] = true;
450         }
451 
452         setDisplayMode(displayMode);
453     }
454 
455     /**
456      * Set the display mode of the current pattern.  This can be useful, for
457      * instance, after detecting a pattern to tell this view whether change the
458      * in progress result to correct or wrong.
459      * @param displayMode The display mode.
460      */
461     @UnsupportedAppUsage
setDisplayMode(DisplayMode displayMode)462     public void setDisplayMode(DisplayMode displayMode) {
463         mPatternDisplayMode = displayMode;
464         if (displayMode == DisplayMode.Animate) {
465             if (mPattern.size() == 0) {
466                 throw new IllegalStateException("you must have a pattern to "
467                         + "animate if you want to set the display mode to animate");
468             }
469             mAnimatingPeriodStart = SystemClock.elapsedRealtime();
470             final Cell first = mPattern.get(0);
471             mInProgressX = getCenterXForColumn(first.getColumn());
472             mInProgressY = getCenterYForRow(first.getRow());
473             clearPatternDrawLookup();
474         }
475         invalidate();
476     }
477 
startCellStateAnimation(CellState cellState, float startAlpha, float endAlpha, float startTranslationY, float endTranslationY, float startScale, float endScale, long delay, long duration, Interpolator interpolator, Runnable finishRunnable)478     public void startCellStateAnimation(CellState cellState, float startAlpha, float endAlpha,
479             float startTranslationY, float endTranslationY, float startScale, float endScale,
480             long delay, long duration,
481             Interpolator interpolator, Runnable finishRunnable) {
482         if (isHardwareAccelerated()) {
483             startCellStateAnimationHw(cellState, startAlpha, endAlpha, startTranslationY,
484                     endTranslationY, startScale, endScale, delay, duration, interpolator,
485                     finishRunnable);
486         } else {
487             startCellStateAnimationSw(cellState, startAlpha, endAlpha, startTranslationY,
488                     endTranslationY, startScale, endScale, delay, duration, interpolator,
489                     finishRunnable);
490         }
491     }
492 
startCellStateAnimationSw(final CellState cellState, final float startAlpha, final float endAlpha, final float startTranslationY, final float endTranslationY, final float startScale, final float endScale, long delay, long duration, Interpolator interpolator, final Runnable finishRunnable)493     private void startCellStateAnimationSw(final CellState cellState,
494             final float startAlpha, final float endAlpha,
495             final float startTranslationY, final float endTranslationY,
496             final float startScale, final float endScale,
497             long delay, long duration, Interpolator interpolator, final Runnable finishRunnable) {
498         cellState.alpha = startAlpha;
499         cellState.translationY = startTranslationY;
500         cellState.radius = mDotSize/2 * startScale;
501         ValueAnimator animator = ValueAnimator.ofFloat(0f, 1f);
502         animator.setDuration(duration);
503         animator.setStartDelay(delay);
504         animator.setInterpolator(interpolator);
505         animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
506             @Override
507             public void onAnimationUpdate(ValueAnimator animation) {
508                 float t = (float) animation.getAnimatedValue();
509                 cellState.alpha = (1 - t) * startAlpha + t * endAlpha;
510                 cellState.translationY = (1 - t) * startTranslationY + t * endTranslationY;
511                 cellState.radius = mDotSize/2 * ((1 - t) * startScale + t * endScale);
512                 invalidate();
513             }
514         });
515         animator.addListener(new AnimatorListenerAdapter() {
516             @Override
517             public void onAnimationEnd(Animator animation) {
518                 if (finishRunnable != null) {
519                     finishRunnable.run();
520                 }
521             }
522         });
523         animator.start();
524     }
525 
startCellStateAnimationHw(final CellState cellState, float startAlpha, float endAlpha, float startTranslationY, float endTranslationY, float startScale, float endScale, long delay, long duration, Interpolator interpolator, final Runnable finishRunnable)526     private void startCellStateAnimationHw(final CellState cellState,
527             float startAlpha, float endAlpha,
528             float startTranslationY, float endTranslationY,
529             float startScale, float endScale,
530             long delay, long duration, Interpolator interpolator, final Runnable finishRunnable) {
531         cellState.alpha = endAlpha;
532         cellState.translationY = endTranslationY;
533         cellState.radius = mDotSize/2 * endScale;
534         cellState.hwAnimating = true;
535         cellState.hwCenterY = CanvasProperty.createFloat(
536                 getCenterYForRow(cellState.row) + startTranslationY);
537         cellState.hwCenterX = CanvasProperty.createFloat(getCenterXForColumn(cellState.col));
538         cellState.hwRadius = CanvasProperty.createFloat(mDotSize/2 * startScale);
539         mPaint.setColor(getDotColor());
540         mPaint.setAlpha((int) (startAlpha * 255));
541         cellState.hwPaint = CanvasProperty.createPaint(new Paint(mPaint));
542 
543         startRtFloatAnimation(cellState.hwCenterY,
544                 getCenterYForRow(cellState.row) + endTranslationY, delay, duration, interpolator);
545         startRtFloatAnimation(cellState.hwRadius, mDotSize/2 * endScale, delay, duration,
546                 interpolator);
547         startRtAlphaAnimation(cellState, endAlpha, delay, duration, interpolator,
548                 new AnimatorListenerAdapter() {
549                     @Override
550                     public void onAnimationEnd(Animator animation) {
551                         cellState.hwAnimating = false;
552                         if (finishRunnable != null) {
553                             finishRunnable.run();
554                         }
555                     }
556                 });
557 
558         invalidate();
559     }
560 
startRtAlphaAnimation(CellState cellState, float endAlpha, long delay, long duration, Interpolator interpolator, Animator.AnimatorListener listener)561     private void startRtAlphaAnimation(CellState cellState, float endAlpha,
562             long delay, long duration, Interpolator interpolator,
563             Animator.AnimatorListener listener) {
564         RenderNodeAnimator animator = new RenderNodeAnimator(cellState.hwPaint,
565                 RenderNodeAnimator.PAINT_ALPHA, (int) (endAlpha * 255));
566         animator.setDuration(duration);
567         animator.setStartDelay(delay);
568         animator.setInterpolator(interpolator);
569         animator.setTarget(this);
570         animator.addListener(listener);
571         animator.start();
572     }
573 
startRtFloatAnimation(CanvasProperty<Float> property, float endValue, long delay, long duration, Interpolator interpolator)574     private void startRtFloatAnimation(CanvasProperty<Float> property, float endValue,
575             long delay, long duration, Interpolator interpolator) {
576         RenderNodeAnimator animator = new RenderNodeAnimator(property, endValue);
577         animator.setDuration(duration);
578         animator.setStartDelay(delay);
579         animator.setInterpolator(interpolator);
580         animator.setTarget(this);
581         animator.start();
582     }
583 
notifyCellAdded()584     private void notifyCellAdded() {
585         // sendAccessEvent(R.string.lockscreen_access_pattern_cell_added);
586         if (mOnPatternListener != null) {
587             mOnPatternListener.onPatternCellAdded(mPattern);
588         }
589         // Disable used cells for accessibility as they get added
590         if (DEBUG_A11Y) Log.v(TAG, "ivnalidating root because cell was added.");
591         mExploreByTouchHelper.invalidateRoot();
592     }
593 
notifyPatternStarted()594     private void notifyPatternStarted() {
595         sendAccessEvent(R.string.lockscreen_access_pattern_start);
596         if (mOnPatternListener != null) {
597             mOnPatternListener.onPatternStart();
598         }
599     }
600 
601     @UnsupportedAppUsage
notifyPatternDetected()602     private void notifyPatternDetected() {
603         sendAccessEvent(R.string.lockscreen_access_pattern_detected);
604         if (mOnPatternListener != null) {
605             mOnPatternListener.onPatternDetected(mPattern);
606         }
607     }
608 
notifyPatternCleared()609     private void notifyPatternCleared() {
610         sendAccessEvent(R.string.lockscreen_access_pattern_cleared);
611         if (mOnPatternListener != null) {
612             mOnPatternListener.onPatternCleared();
613         }
614     }
615 
616     /**
617      * Clear the pattern.
618      */
619     @UnsupportedAppUsage
clearPattern()620     public void clearPattern() {
621         resetPattern();
622     }
623 
624     @Override
dispatchHoverEvent(MotionEvent event)625     protected boolean dispatchHoverEvent(MotionEvent event) {
626         // Dispatch to onHoverEvent first so mPatternInProgress is up to date when the
627         // helper gets the event.
628         boolean handled = super.dispatchHoverEvent(event);
629         handled |= mExploreByTouchHelper.dispatchHoverEvent(event);
630         return handled;
631     }
632 
633     /**
634      * Reset all pattern state.
635      */
resetPattern()636     private void resetPattern() {
637         mPattern.clear();
638         clearPatternDrawLookup();
639         mPatternDisplayMode = DisplayMode.Correct;
640         invalidate();
641     }
642 
643     /**
644      * If there are any cells being drawn.
645      */
isEmpty()646     public boolean isEmpty() {
647         return mPattern.isEmpty();
648     }
649 
650     /**
651      * Clear the pattern lookup table. Also reset the line fade start times for
652      * the next attempt.
653      */
clearPatternDrawLookup()654     private void clearPatternDrawLookup() {
655         for (int i = 0; i < 3; i++) {
656             for (int j = 0; j < 3; j++) {
657                 mPatternDrawLookup[i][j] = false;
658                 mLineFadeStart[i+j*3] = 0;
659             }
660         }
661     }
662 
663     /**
664      * Disable input (for instance when displaying a message that will
665      * timeout so user doesn't get view into messy state).
666      */
667     @UnsupportedAppUsage
disableInput()668     public void disableInput() {
669         mInputEnabled = false;
670     }
671 
672     /**
673      * Enable input.
674      */
675     @UnsupportedAppUsage
enableInput()676     public void enableInput() {
677         mInputEnabled = true;
678     }
679 
680     @Override
onSizeChanged(int w, int h, int oldw, int oldh)681     protected void onSizeChanged(int w, int h, int oldw, int oldh) {
682         final int width = w - mPaddingLeft - mPaddingRight;
683         mSquareWidth = width / 3.0f;
684 
685         if (DEBUG_A11Y) Log.v(TAG, "onSizeChanged(" + w + "," + h + ")");
686         final int height = h - mPaddingTop - mPaddingBottom;
687         mSquareHeight = height / 3.0f;
688         mExploreByTouchHelper.invalidateRoot();
689         mDotHitRadius = Math.min(mSquareHeight / 2, mSquareWidth / 2) * mDotHitFactor;
690 
691         if (mUseLockPatternDrawable) {
692             mNotSelectedDrawable.setBounds(mPaddingLeft, mPaddingTop, width, height);
693             mSelectedDrawable.setBounds(mPaddingLeft, mPaddingTop, width, height);
694         }
695     }
696 
resolveMeasured(int measureSpec, int desired)697     private int resolveMeasured(int measureSpec, int desired)
698     {
699         int result = 0;
700         int specSize = MeasureSpec.getSize(measureSpec);
701         switch (MeasureSpec.getMode(measureSpec)) {
702             case MeasureSpec.UNSPECIFIED:
703                 result = desired;
704                 break;
705             case MeasureSpec.AT_MOST:
706                 result = Math.max(specSize, desired);
707                 break;
708             case MeasureSpec.EXACTLY:
709             default:
710                 result = specSize;
711         }
712         return result;
713     }
714 
715     @Override
onMeasure(int widthMeasureSpec, int heightMeasureSpec)716     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
717         final int minimumWidth = getSuggestedMinimumWidth();
718         final int minimumHeight = getSuggestedMinimumHeight();
719         int viewWidth = resolveMeasured(widthMeasureSpec, minimumWidth);
720         int viewHeight = resolveMeasured(heightMeasureSpec, minimumHeight);
721 
722         switch (mAspect) {
723             case ASPECT_SQUARE:
724                 viewWidth = viewHeight = Math.min(viewWidth, viewHeight);
725                 break;
726             case ASPECT_LOCK_WIDTH:
727                 viewHeight = Math.min(viewWidth, viewHeight);
728                 break;
729             case ASPECT_LOCK_HEIGHT:
730                 viewWidth = Math.min(viewWidth, viewHeight);
731                 break;
732         }
733         // Log.v(TAG, "LockPatternView dimensions: " + viewWidth + "x" + viewHeight);
734         setMeasuredDimension(viewWidth, viewHeight);
735     }
736 
737     /**
738      * Determines whether the point x, y will add a new point to the current
739      * pattern (in addition to finding the cell, also makes heuristic choices
740      * such as filling in gaps based on current pattern).
741      * @param x The x coordinate.
742      * @param y The y coordinate.
743      */
detectAndAddHit(float x, float y)744     private Cell detectAndAddHit(float x, float y) {
745         final Cell cell = checkForNewHit(x, y);
746         if (cell != null) {
747 
748             // check for gaps in existing pattern
749             Cell fillInGapCell = null;
750             final ArrayList<Cell> pattern = mPattern;
751             if (!pattern.isEmpty()) {
752                 final Cell lastCell = pattern.get(pattern.size() - 1);
753                 int dRow = cell.row - lastCell.row;
754                 int dColumn = cell.column - lastCell.column;
755 
756                 int fillInRow = lastCell.row;
757                 int fillInColumn = lastCell.column;
758 
759                 if (Math.abs(dRow) == 2 && Math.abs(dColumn) != 1) {
760                     fillInRow = lastCell.row + ((dRow > 0) ? 1 : -1);
761                 }
762 
763                 if (Math.abs(dColumn) == 2 && Math.abs(dRow) != 1) {
764                     fillInColumn = lastCell.column + ((dColumn > 0) ? 1 : -1);
765                 }
766 
767                 fillInGapCell = Cell.of(fillInRow, fillInColumn);
768             }
769 
770             if (fillInGapCell != null &&
771                     !mPatternDrawLookup[fillInGapCell.row][fillInGapCell.column]) {
772                 addCellToPattern(fillInGapCell);
773             }
774             addCellToPattern(cell);
775             performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY,
776                     HapticFeedbackConstants.FLAG_IGNORE_VIEW_SETTING);
777             return cell;
778         }
779         return null;
780     }
781 
addCellToPattern(Cell newCell)782     private void addCellToPattern(Cell newCell) {
783         mPatternDrawLookup[newCell.getRow()][newCell.getColumn()] = true;
784         mPattern.add(newCell);
785         if (!mInStealthMode) {
786             startCellActivatedAnimation(newCell);
787         }
788         notifyCellAdded();
789     }
790 
startCellActivatedAnimation(Cell cell)791     private void startCellActivatedAnimation(Cell cell) {
792         final CellState cellState = mCellStates[cell.row][cell.column];
793 
794         if (cellState.activationAnimator != null) {
795             cellState.activationAnimator.cancel();
796         }
797         AnimatorSet animatorSet = new AnimatorSet();
798         AnimatorSet.Builder animatorSetBuilder = animatorSet
799                 .play(createLineDisappearingAnimation())
800                 .with(createLineEndAnimation(cellState, mInProgressX, mInProgressY,
801                         getCenterXForColumn(cell.column), getCenterYForRow(cell.row)));
802         if (mDotSize != mDotSizeActivated) {
803             animatorSetBuilder.with(createDotRadiusAnimation(cellState));
804         }
805         if (mDotColor != mDotActivatedColor) {
806             animatorSetBuilder.with(createDotActivationColorAnimation(cellState));
807         }
808 
809         animatorSet.addListener(new AnimatorListenerAdapter() {
810             @Override
811             public void onAnimationEnd(Animator animation) {
812                 cellState.activationAnimator = null;
813                 invalidate();
814             }
815         });
816         cellState.activationAnimator = animatorSet;
817         animatorSet.start();
818     }
819 
createDotActivationColorAnimation(CellState cellState)820     private Animator createDotActivationColorAnimation(CellState cellState) {
821         ValueAnimator.AnimatorUpdateListener updateListener =
822                 valueAnimator -> {
823                     cellState.activationAnimationProgress =
824                             (float) valueAnimator.getAnimatedValue();
825                     invalidate();
826                 };
827         ValueAnimator activateAnimator = ValueAnimator.ofFloat(0f, 1f);
828         ValueAnimator deactivateAnimator = ValueAnimator.ofFloat(1f, 0f);
829         activateAnimator.addUpdateListener(updateListener);
830         deactivateAnimator.addUpdateListener(updateListener);
831         activateAnimator.setInterpolator(mFastOutSlowInInterpolator);
832         deactivateAnimator.setInterpolator(mLinearOutSlowInInterpolator);
833 
834         // Align dot animation duration with line fade out animation.
835         activateAnimator.setDuration(DOT_ACTIVATION_DURATION_MILLIS);
836         deactivateAnimator.setDuration(DOT_ACTIVATION_DURATION_MILLIS);
837         AnimatorSet set = new AnimatorSet();
838         set.play(deactivateAnimator)
839                 .after(mLineFadeOutAnimationDelayMs + mLineFadeOutAnimationDurationMs
840                         - DOT_ACTIVATION_DURATION_MILLIS * 2)
841                 .after(activateAnimator);
842         return set;
843     }
844 
845     /**
846      * On the last frame before cell activates the end point of in progress line is not aligned
847      * with dot center so we execute a short animation moving the end point to exact dot center.
848      */
createLineEndAnimation(final CellState state, final float startX, final float startY, final float targetX, final float targetY)849     private Animator createLineEndAnimation(final CellState state,
850             final float startX, final float startY, final float targetX, final float targetY) {
851         ValueAnimator valueAnimator = ValueAnimator.ofFloat(0, 1);
852         valueAnimator.addUpdateListener(animation -> {
853             float t = (float) animation.getAnimatedValue();
854             state.lineEndX = (1 - t) * startX + t * targetX;
855             state.lineEndY = (1 - t) * startY + t * targetY;
856             invalidate();
857         });
858         valueAnimator.setInterpolator(mFastOutSlowInInterpolator);
859         valueAnimator.setDuration(LINE_END_ANIMATION_DURATION_MILLIS);
860         return valueAnimator;
861     }
862 
863     /**
864      * Starts animator to fade out a line segment. It does only invalidate because all the
865      * transitions are applied in {@code onDraw} method.
866      */
createLineDisappearingAnimation()867     private Animator createLineDisappearingAnimation() {
868         ValueAnimator valueAnimator = ValueAnimator.ofFloat(0, 1);
869         valueAnimator.addUpdateListener(animation -> invalidate());
870         valueAnimator.setStartDelay(mLineFadeOutAnimationDelayMs);
871         valueAnimator.setDuration(mLineFadeOutAnimationDurationMs);
872         return valueAnimator;
873     }
874 
createDotRadiusAnimation(CellState state)875     private Animator createDotRadiusAnimation(CellState state) {
876         float defaultRadius = mDotSize / 2f;
877         float activatedRadius = mDotSizeActivated / 2f;
878 
879         ValueAnimator.AnimatorUpdateListener animatorUpdateListener =
880                 animation -> {
881                     state.radius = (float) animation.getAnimatedValue();
882                     invalidate();
883                 };
884 
885         ValueAnimator activationAnimator = ValueAnimator.ofFloat(defaultRadius, activatedRadius);
886         activationAnimator.addUpdateListener(animatorUpdateListener);
887         activationAnimator.setInterpolator(mLinearOutSlowInInterpolator);
888         activationAnimator.setDuration(DOT_RADIUS_INCREASE_DURATION_MILLIS);
889 
890         ValueAnimator deactivationAnimator = ValueAnimator.ofFloat(activatedRadius, defaultRadius);
891         deactivationAnimator.addUpdateListener(animatorUpdateListener);
892         deactivationAnimator.setInterpolator(mFastOutSlowInInterpolator);
893         deactivationAnimator.setDuration(DOT_RADIUS_DECREASE_DURATION_MILLIS);
894 
895         AnimatorSet set = new AnimatorSet();
896         set.playSequentially(activationAnimator, deactivationAnimator);
897         return set;
898     }
899 
900     @Nullable
checkForNewHit(float x, float y)901     private Cell checkForNewHit(float x, float y) {
902         Cell cellHit = detectCellHit(x, y);
903         if (cellHit != null && !mPatternDrawLookup[cellHit.row][cellHit.column]) {
904             return cellHit;
905         }
906         return null;
907     }
908 
909     /** Helper method to find which cell a point maps to. */
910     @Nullable
detectCellHit(float x, float y)911     private Cell detectCellHit(float x, float y) {
912         final float hitRadiusSquared = mDotHitRadius * mDotHitRadius;
913         for (int row = 0; row < 3; row++) {
914             for (int column = 0; column < 3; column++) {
915                 float centerY = getCenterYForRow(row);
916                 float centerX = getCenterXForColumn(column);
917                 if ((x - centerX) * (x - centerX) + (y - centerY) * (y - centerY)
918                         < hitRadiusSquared) {
919                     return Cell.of(row, column);
920                 }
921             }
922         }
923         return null;
924     }
925 
926     @Override
onHoverEvent(MotionEvent event)927     public boolean onHoverEvent(MotionEvent event) {
928         if (AccessibilityManager.getInstance(mContext).isTouchExplorationEnabled()) {
929             final int action = event.getAction();
930             switch (action) {
931                 case MotionEvent.ACTION_HOVER_ENTER:
932                     event.setAction(MotionEvent.ACTION_DOWN);
933                     break;
934                 case MotionEvent.ACTION_HOVER_MOVE:
935                     event.setAction(MotionEvent.ACTION_MOVE);
936                     break;
937                 case MotionEvent.ACTION_HOVER_EXIT:
938                     event.setAction(MotionEvent.ACTION_UP);
939                     break;
940             }
941             onTouchEvent(event);
942             event.setAction(action);
943         }
944         return super.onHoverEvent(event);
945     }
946 
947     @Override
onTouchEvent(MotionEvent event)948     public boolean onTouchEvent(MotionEvent event) {
949         if (!mInputEnabled || !isEnabled()) {
950             return false;
951         }
952 
953         switch(event.getAction()) {
954             case MotionEvent.ACTION_DOWN:
955                 handleActionDown(event);
956                 return true;
957             case MotionEvent.ACTION_UP:
958                 handleActionUp();
959                 return true;
960             case MotionEvent.ACTION_MOVE:
961                 handleActionMove(event);
962                 return true;
963             case MotionEvent.ACTION_CANCEL:
964                 if (mPatternInProgress) {
965                     setPatternInProgress(false);
966                     resetPattern();
967                     notifyPatternCleared();
968                 }
969                 if (PROFILE_DRAWING) {
970                     if (mDrawingProfilingStarted) {
971                         Debug.stopMethodTracing();
972                         mDrawingProfilingStarted = false;
973                     }
974                 }
975                 return true;
976         }
977         return false;
978     }
979 
setPatternInProgress(boolean progress)980     private void setPatternInProgress(boolean progress) {
981         mPatternInProgress = progress;
982         mExploreByTouchHelper.invalidateRoot();
983     }
984 
handleActionMove(MotionEvent event)985     private void handleActionMove(MotionEvent event) {
986         // Handle all recent motion events so we don't skip any cells even when the device
987         // is busy...
988         final float radius = mPathWidth;
989         final int historySize = event.getHistorySize();
990         mTmpInvalidateRect.setEmpty();
991         boolean invalidateNow = false;
992         for (int i = 0; i < historySize + 1; i++) {
993             final float x = i < historySize ? event.getHistoricalX(i) : event.getX();
994             final float y = i < historySize ? event.getHistoricalY(i) : event.getY();
995             Cell hitCell = detectAndAddHit(x, y);
996             final int patternSize = mPattern.size();
997             if (hitCell != null && patternSize == 1) {
998                 setPatternInProgress(true);
999                 notifyPatternStarted();
1000             }
1001             // note current x and y for rubber banding of in progress patterns
1002             final float dx = Math.abs(x - mInProgressX);
1003             final float dy = Math.abs(y - mInProgressY);
1004             if (dx > DRAG_THRESHHOLD || dy > DRAG_THRESHHOLD) {
1005                 invalidateNow = true;
1006             }
1007 
1008             if (mPatternInProgress && patternSize > 0) {
1009                 final ArrayList<Cell> pattern = mPattern;
1010                 final Cell lastCell = pattern.get(patternSize - 1);
1011                 float lastCellCenterX = getCenterXForColumn(lastCell.column);
1012                 float lastCellCenterY = getCenterYForRow(lastCell.row);
1013 
1014                 // Adjust for drawn segment from last cell to (x,y). Radius accounts for line width.
1015                 float left = Math.min(lastCellCenterX, x) - radius;
1016                 float right = Math.max(lastCellCenterX, x) + radius;
1017                 float top = Math.min(lastCellCenterY, y) - radius;
1018                 float bottom = Math.max(lastCellCenterY, y) + radius;
1019 
1020                 // Invalidate between the pattern's new cell and the pattern's previous cell
1021                 if (hitCell != null) {
1022                     final float width = mSquareWidth * 0.5f;
1023                     final float height = mSquareHeight * 0.5f;
1024                     final float hitCellCenterX = getCenterXForColumn(hitCell.column);
1025                     final float hitCellCenterY = getCenterYForRow(hitCell.row);
1026 
1027                     left = Math.min(hitCellCenterX - width, left);
1028                     right = Math.max(hitCellCenterX + width, right);
1029                     top = Math.min(hitCellCenterY - height, top);
1030                     bottom = Math.max(hitCellCenterY + height, bottom);
1031                 }
1032 
1033                 // Invalidate between the pattern's last cell and the previous location
1034                 mTmpInvalidateRect.union(Math.round(left), Math.round(top),
1035                         Math.round(right), Math.round(bottom));
1036             }
1037         }
1038         mInProgressX = event.getX();
1039         mInProgressY = event.getY();
1040 
1041         // To save updates, we only invalidate if the user moved beyond a certain amount.
1042         if (invalidateNow) {
1043             mInvalidate.union(mTmpInvalidateRect);
1044             invalidate(mInvalidate);
1045             mInvalidate.set(mTmpInvalidateRect);
1046         }
1047     }
1048 
sendAccessEvent(int resId)1049     private void sendAccessEvent(int resId) {
1050         announceForAccessibility(mContext.getString(resId));
1051     }
1052 
handleActionUp()1053     private void handleActionUp() {
1054         // report pattern detected
1055         if (!mPattern.isEmpty()) {
1056             setPatternInProgress(false);
1057             cancelLineAnimations();
1058             notifyPatternDetected();
1059             // Also clear pattern if fading is enabled
1060             if (mFadePattern) {
1061                 clearPatternDrawLookup();
1062                 mPatternDisplayMode = DisplayMode.Correct;
1063             }
1064             invalidate();
1065         }
1066         if (PROFILE_DRAWING) {
1067             if (mDrawingProfilingStarted) {
1068                 Debug.stopMethodTracing();
1069                 mDrawingProfilingStarted = false;
1070             }
1071         }
1072     }
1073 
cancelLineAnimations()1074     private void cancelLineAnimations() {
1075         for (int i = 0; i < 3; i++) {
1076             for (int j = 0; j < 3; j++) {
1077                 CellState state = mCellStates[i][j];
1078                 if (state.activationAnimator != null) {
1079                     state.activationAnimator.cancel();
1080                     state.activationAnimator = null;
1081                     state.radius = mDotSize / 2f;
1082                     state.activationAnimationProgress = 0f;
1083                     state.lineEndX = Float.MIN_VALUE;
1084                     state.lineEndY = Float.MIN_VALUE;
1085                 }
1086             }
1087         }
1088     }
handleActionDown(MotionEvent event)1089     private void handleActionDown(MotionEvent event) {
1090         resetPattern();
1091         final float x = event.getX();
1092         final float y = event.getY();
1093         final Cell hitCell = detectAndAddHit(x, y);
1094         if (hitCell != null) {
1095             setPatternInProgress(true);
1096             mPatternDisplayMode = DisplayMode.Correct;
1097             notifyPatternStarted();
1098         } else if (mPatternInProgress) {
1099             setPatternInProgress(false);
1100             notifyPatternCleared();
1101         }
1102         if (hitCell != null) {
1103             final float startX = getCenterXForColumn(hitCell.column);
1104             final float startY = getCenterYForRow(hitCell.row);
1105 
1106             final float widthOffset = mSquareWidth / 2f;
1107             final float heightOffset = mSquareHeight / 2f;
1108 
1109             invalidate((int) (startX - widthOffset), (int) (startY - heightOffset),
1110                     (int) (startX + widthOffset), (int) (startY + heightOffset));
1111         }
1112         mInProgressX = x;
1113         mInProgressY = y;
1114         if (PROFILE_DRAWING) {
1115             if (!mDrawingProfilingStarted) {
1116                 Debug.startMethodTracing("LockPatternDrawing");
1117                 mDrawingProfilingStarted = true;
1118             }
1119         }
1120     }
1121 
1122     /**
1123      * Change theme colors
1124      * @param regularColor The dot color
1125      * @param successColor Color used when pattern is correct
1126      * @param errorColor Color used when authentication fails
1127      */
setColors(int regularColor, int successColor, int errorColor)1128     public void setColors(int regularColor, int successColor, int errorColor) {
1129         mRegularColor = regularColor;
1130         mErrorColor = errorColor;
1131         mSuccessColor = successColor;
1132         mPathPaint.setColor(regularColor);
1133         invalidate();
1134     }
1135 
getCenterXForColumn(int column)1136     private float getCenterXForColumn(int column) {
1137         return mPaddingLeft + column * mSquareWidth + mSquareWidth / 2f;
1138     }
1139 
getCenterYForRow(int row)1140     private float getCenterYForRow(int row) {
1141         return mPaddingTop + row * mSquareHeight + mSquareHeight / 2f;
1142     }
1143 
1144     @Override
onDraw(Canvas canvas)1145     protected void onDraw(Canvas canvas) {
1146         final ArrayList<Cell> pattern = mPattern;
1147         final int count = pattern.size();
1148         final boolean[][] drawLookup = mPatternDrawLookup;
1149 
1150         if (mPatternDisplayMode == DisplayMode.Animate) {
1151 
1152             // figure out which circles to draw
1153 
1154             // + 1 so we pause on complete pattern
1155             final int oneCycle = (count + 1) * MILLIS_PER_CIRCLE_ANIMATING;
1156             final int spotInCycle = (int) (SystemClock.elapsedRealtime() -
1157                     mAnimatingPeriodStart) % oneCycle;
1158             final int numCircles = spotInCycle / MILLIS_PER_CIRCLE_ANIMATING;
1159 
1160             clearPatternDrawLookup();
1161             for (int i = 0; i < numCircles; i++) {
1162                 final Cell cell = pattern.get(i);
1163                 drawLookup[cell.getRow()][cell.getColumn()] = true;
1164             }
1165 
1166             // figure out in progress portion of ghosting line
1167 
1168             final boolean needToUpdateInProgressPoint = numCircles > 0
1169                     && numCircles < count;
1170 
1171             if (needToUpdateInProgressPoint) {
1172                 final float percentageOfNextCircle =
1173                         ((float) (spotInCycle % MILLIS_PER_CIRCLE_ANIMATING)) /
1174                                 MILLIS_PER_CIRCLE_ANIMATING;
1175 
1176                 final Cell currentCell = pattern.get(numCircles - 1);
1177                 final float centerX = getCenterXForColumn(currentCell.column);
1178                 final float centerY = getCenterYForRow(currentCell.row);
1179 
1180                 final Cell nextCell = pattern.get(numCircles);
1181                 final float dx = percentageOfNextCircle *
1182                         (getCenterXForColumn(nextCell.column) - centerX);
1183                 final float dy = percentageOfNextCircle *
1184                         (getCenterYForRow(nextCell.row) - centerY);
1185                 mInProgressX = centerX + dx;
1186                 mInProgressY = centerY + dy;
1187             }
1188             // TODO: Infinite loop here...
1189             invalidate();
1190         }
1191 
1192         final Path currentPath = mCurrentPath;
1193         currentPath.rewind();
1194 
1195         // TODO: the path should be created and cached every time we hit-detect a cell
1196         // only the last segment of the path should be computed here
1197         // draw the path of the pattern (unless we are in stealth mode)
1198         final boolean drawPath = !mInStealthMode;
1199 
1200         if (drawPath) {
1201             mPathPaint.setColor(getCurrentColor(true /* partOfPattern */));
1202 
1203             boolean anyCircles = false;
1204             float lastX = 0f;
1205             float lastY = 0f;
1206             long elapsedRealtime = SystemClock.elapsedRealtime();
1207            for (int i = 0; i < count; i++) {
1208                 Cell cell = pattern.get(i);
1209 
1210                 // only draw the part of the pattern stored in
1211                 // the lookup table (this is only different in the case
1212                 // of animation).
1213                 if (!drawLookup[cell.row][cell.column]) {
1214                     break;
1215                 }
1216                 anyCircles = true;
1217 
1218                 if (mLineFadeStart[i] == 0) {
1219                   mLineFadeStart[i] = SystemClock.elapsedRealtime();
1220                 }
1221 
1222                 float centerX = getCenterXForColumn(cell.column);
1223                 float centerY = getCenterYForRow(cell.row);
1224                 if (i != 0) {
1225                     CellState state = mCellStates[cell.row][cell.column];
1226                     currentPath.rewind();
1227                     float endX;
1228                     float endY;
1229                     if (state.lineEndX != Float.MIN_VALUE && state.lineEndY != Float.MIN_VALUE) {
1230                         endX = state.lineEndX;
1231                         endY = state.lineEndY;
1232                     } else {
1233                         endX = centerX;
1234                         endY = centerY;
1235                     }
1236                     drawLineSegment(canvas, /* startX = */ lastX, /* startY = */ lastY, endX, endY,
1237                             mLineFadeStart[i], elapsedRealtime);
1238                 }
1239                 lastX = centerX;
1240                 lastY = centerY;
1241             }
1242 
1243             // draw last in progress section
1244             if ((mPatternInProgress || mPatternDisplayMode == DisplayMode.Animate)
1245                     && anyCircles) {
1246                 currentPath.rewind();
1247                 currentPath.moveTo(lastX, lastY);
1248                 currentPath.lineTo(mInProgressX, mInProgressY);
1249 
1250                 mPathPaint.setAlpha((int) (calculateLastSegmentAlpha(
1251                         mInProgressX, mInProgressY, lastX, lastY) * 255f));
1252                 canvas.drawPath(currentPath, mPathPaint);
1253             }
1254         }
1255 
1256         // draw the circles
1257         for (int i = 0; i < 3; i++) {
1258             float centerY = getCenterYForRow(i);
1259             for (int j = 0; j < 3; j++) {
1260                 CellState cellState = mCellStates[i][j];
1261                 float centerX = getCenterXForColumn(j);
1262                 float translationY = cellState.translationY;
1263 
1264                 if (mUseLockPatternDrawable) {
1265                     drawCellDrawable(canvas, i, j, cellState.radius, drawLookup[i][j]);
1266                 } else {
1267                     if (isHardwareAccelerated() && cellState.hwAnimating) {
1268                         RecordingCanvas recordingCanvas = (RecordingCanvas) canvas;
1269                         recordingCanvas.drawCircle(cellState.hwCenterX, cellState.hwCenterY,
1270                                 cellState.hwRadius, cellState.hwPaint);
1271                     } else {
1272                         drawCircle(canvas, (int) centerX, (int) centerY + translationY,
1273                                 cellState.radius, drawLookup[i][j], cellState.alpha,
1274                                 cellState.activationAnimationProgress);
1275                     }
1276                 }
1277             }
1278         }
1279     }
1280 
1281     private void drawLineSegment(Canvas canvas, float startX, float startY, float endX, float endY,
1282             long lineFadeStart, long elapsedRealtime) {
1283         float fadeAwayProgress;
1284         if (mFadePattern) {
1285             if (elapsedRealtime - lineFadeStart
1286                     >= mLineFadeOutAnimationDelayMs + mLineFadeOutAnimationDurationMs) {
1287                 // Time for this segment animation is out so we don't need to draw it.
1288                 return;
1289             }
1290             // Set this line segment to fade away animated.
1291             fadeAwayProgress = Math.max(
1292                     ((float) (elapsedRealtime - lineFadeStart - mLineFadeOutAnimationDelayMs))
1293                             / mLineFadeOutAnimationDurationMs, 0f);
1294             drawFadingAwayLineSegment(canvas, startX, startY, endX, endY, fadeAwayProgress);
1295         } else {
1296             mPathPaint.setAlpha(255);
1297             canvas.drawLine(startX, startY, endX, endY, mPathPaint);
1298         }
1299     }
1300 
1301     private void drawFadingAwayLineSegment(Canvas canvas, float startX, float startY, float endX,
1302             float endY, float fadeAwayProgress) {
1303         mPathPaint.setAlpha((int) (255 * (1 - fadeAwayProgress)));
1304 
1305         // To draw gradient segment we use mFadeOutGradientShader which has immutable coordinates
1306         // thus we will need to translate and rotate the canvas.
1307         mPathPaint.setShader(mFadeOutGradientShader);
1308         canvas.save();
1309 
1310         // First translate canvas to gradient middle point.
1311         float gradientMidX = endX * fadeAwayProgress + startX * (1 - fadeAwayProgress);
1312         float gradientMidY = endY * fadeAwayProgress + startY * (1 - fadeAwayProgress);
1313         canvas.translate(gradientMidX, gradientMidY);
1314 
1315         // Then rotate it to the direction of the segment.
1316         double segmentAngleRad = Math.atan((endY - startY) / (endX - startX));
1317         float segmentAngleDegrees = (float) Math.toDegrees(segmentAngleRad);
1318         if (endX - startX < 0) {
1319             // Arc tangent gives us angle degrees [-90; 90] thus to cover [90; 270] degrees we
1320             // need this hack.
1321             segmentAngleDegrees += 180f;
1322         }
1323         canvas.rotate(segmentAngleDegrees);
1324 
1325         // Pythagoras theorem.
1326         float segmentLength = (float) Math.hypot(endX - startX, endY - startY);
1327 
1328         // Draw the segment in coordinates aligned with shader coordinates.
1329         canvas.drawLine(/* startX= */ -segmentLength * fadeAwayProgress, /* startY= */
1330                 0,/* stopX= */ segmentLength * (1 - fadeAwayProgress), /* stopY= */ 0, mPathPaint);
1331 
1332         canvas.restore();
1333         mPathPaint.setShader(null);
1334     }
1335 
1336     private float calculateLastSegmentAlpha(float x, float y, float lastX, float lastY) {
1337         float diffX = x - lastX;
1338         float diffY = y - lastY;
1339         float dist = (float) Math.sqrt(diffX*diffX + diffY*diffY);
1340         float frac = dist/mSquareWidth;
1341         return Math.min(1f, Math.max(0f, (frac - 0.3f) * 4f));
1342     }
1343 
1344     private int getDotColor() {
1345         if (mInStealthMode) {
1346             // Always use the default color in this case
1347             return mDotColor;
1348         } else if (mPatternDisplayMode == DisplayMode.Wrong) {
1349             // the pattern is wrong
1350             return mErrorColor;
1351         }
1352         return mDotColor;
1353     }
1354 
1355     private int getCurrentColor(boolean partOfPattern) {
1356         if (!partOfPattern || mInStealthMode || mPatternInProgress) {
1357             // unselected circle
1358             return mRegularColor;
1359         } else if (mPatternDisplayMode == DisplayMode.Wrong) {
1360             // the pattern is wrong
1361             return mErrorColor;
1362         } else if (mPatternDisplayMode == DisplayMode.Correct ||
1363                 mPatternDisplayMode == DisplayMode.Animate) {
1364             return mSuccessColor;
1365         } else {
1366             throw new IllegalStateException("unknown display mode " + mPatternDisplayMode);
1367         }
1368     }
1369 
1370     /**
1371      * @param partOfPattern Whether this circle is part of the pattern.
1372      */
1373     private void drawCircle(Canvas canvas, float centerX, float centerY, float radius,
1374             boolean partOfPattern, float alpha, float activationAnimationProgress) {
1375         if (mFadePattern && !mInStealthMode) {
1376             int resultColor = ColorUtils.blendARGB(mDotColor, mDotActivatedColor,
1377                     /* ratio= */ activationAnimationProgress);
1378             mPaint.setColor(resultColor);
1379         } else {
1380             mPaint.setColor(getDotColor());
1381         }
1382         mPaint.setAlpha((int) (alpha * 255));
1383         canvas.drawCircle(centerX, centerY, radius, mPaint);
1384     }
1385 
1386     /**
1387      * @param partOfPattern Whether this circle is part of the pattern.
1388      */
1389     private void drawCellDrawable(Canvas canvas, int i, int j, float radius,
1390             boolean partOfPattern) {
1391         Rect dst = new Rect(
1392             (int) (mPaddingLeft + j * mSquareWidth),
1393             (int) (mPaddingTop + i * mSquareHeight),
1394             (int) (mPaddingLeft + (j + 1) * mSquareWidth),
1395             (int) (mPaddingTop + (i + 1) * mSquareHeight));
1396         float scale = radius / (mDotSize / 2);
1397 
1398         // Only draw on this square with the appropriate scale.
1399         canvas.save();
1400         canvas.clipRect(dst);
1401         canvas.scale(scale, scale, dst.centerX(), dst.centerY());
1402         if (!partOfPattern || scale > 1) {
1403             mNotSelectedDrawable.draw(canvas);
1404         } else {
1405             mSelectedDrawable.draw(canvas);
1406         }
1407         canvas.restore();
1408     }
1409 
1410     @Override
1411     protected Parcelable onSaveInstanceState() {
1412         Parcelable superState = super.onSaveInstanceState();
1413         byte[] patternBytes = LockPatternUtils.patternToByteArray(mPattern);
1414         String patternString = patternBytes != null ? new String(patternBytes) : null;
1415         return new SavedState(superState,
1416                 patternString,
1417                 mPatternDisplayMode.ordinal(),
1418                 mInputEnabled, mInStealthMode);
1419     }
1420 
1421     @Override
1422     protected void onRestoreInstanceState(Parcelable state) {
1423         final SavedState ss = (SavedState) state;
1424         super.onRestoreInstanceState(ss.getSuperState());
1425         setPattern(
1426                 DisplayMode.Correct,
1427                 LockPatternUtils.byteArrayToPattern(ss.getSerializedPattern().getBytes()));
1428         mPatternDisplayMode = DisplayMode.values()[ss.getDisplayMode()];
1429         mInputEnabled = ss.isInputEnabled();
1430         mInStealthMode = ss.isInStealthMode();
1431     }
1432 
1433     /**
1434      * The parecelable for saving and restoring a lock pattern view.
1435      */
1436     private static class SavedState extends BaseSavedState {
1437 
1438         private final String mSerializedPattern;
1439         private final int mDisplayMode;
1440         private final boolean mInputEnabled;
1441         private final boolean mInStealthMode;
1442 
1443         /**
1444          * Constructor called from {@link LockPatternView#onSaveInstanceState()}
1445          */
1446         @UnsupportedAppUsage
1447         private SavedState(Parcelable superState, String serializedPattern, int displayMode,
1448                 boolean inputEnabled, boolean inStealthMode) {
1449             super(superState);
1450             mSerializedPattern = serializedPattern;
1451             mDisplayMode = displayMode;
1452             mInputEnabled = inputEnabled;
1453             mInStealthMode = inStealthMode;
1454         }
1455 
1456         /**
1457          * Constructor called from {@link #CREATOR}
1458          */
1459         @UnsupportedAppUsage
1460         private SavedState(Parcel in) {
1461             super(in);
1462             mSerializedPattern = in.readString();
1463             mDisplayMode = in.readInt();
1464             mInputEnabled = (Boolean) in.readValue(null);
1465             mInStealthMode = (Boolean) in.readValue(null);
1466         }
1467 
1468         public String getSerializedPattern() {
1469             return mSerializedPattern;
1470         }
1471 
1472         public int getDisplayMode() {
1473             return mDisplayMode;
1474         }
1475 
1476         public boolean isInputEnabled() {
1477             return mInputEnabled;
1478         }
1479 
1480         public boolean isInStealthMode() {
1481             return mInStealthMode;
1482         }
1483 
1484         @Override
1485         public void writeToParcel(Parcel dest, int flags) {
1486             super.writeToParcel(dest, flags);
1487             dest.writeString(mSerializedPattern);
1488             dest.writeInt(mDisplayMode);
1489             dest.writeValue(mInputEnabled);
1490             dest.writeValue(mInStealthMode);
1491         }
1492 
1493         @SuppressWarnings({ "unused", "hiding" }) // Found using reflection
1494         public static final Parcelable.Creator<SavedState> CREATOR =
1495                 new Creator<SavedState>() {
1496             @Override
1497             public SavedState createFromParcel(Parcel in) {
1498                 return new SavedState(in);
1499             }
1500 
1501             @Override
1502             public SavedState[] newArray(int size) {
1503                 return new SavedState[size];
1504             }
1505         };
1506     }
1507 
1508     private final class PatternExploreByTouchHelper extends ExploreByTouchHelper {
1509         private Rect mTempRect = new Rect();
1510         private final SparseArray<VirtualViewContainer> mItems = new SparseArray<>();
1511 
1512         class VirtualViewContainer {
1513             public VirtualViewContainer(CharSequence description) {
1514                 this.description = description;
1515             }
1516             CharSequence description;
1517         };
1518 
1519         public PatternExploreByTouchHelper(View forView) {
1520             super(forView);
1521             for (int i = VIRTUAL_BASE_VIEW_ID; i < VIRTUAL_BASE_VIEW_ID + 9; i++) {
1522                 mItems.put(i, new VirtualViewContainer(getTextForVirtualView(i)));
1523             }
1524         }
1525 
1526         @Override
1527         protected int getVirtualViewAt(float x, float y) {
1528             // This must use the same hit logic for the screen to ensure consistency whether
1529             // accessibility is on or off.
1530             return getVirtualViewIdForHit(x, y);
1531         }
1532 
1533         @Override
1534         protected void getVisibleVirtualViews(IntArray virtualViewIds) {
1535             if (DEBUG_A11Y) Log.v(TAG, "getVisibleVirtualViews(len=" + virtualViewIds.size() + ")");
1536             if (!mPatternInProgress) {
1537                 return;
1538             }
1539             for (int i = VIRTUAL_BASE_VIEW_ID; i < VIRTUAL_BASE_VIEW_ID + 9; i++) {
1540                 // Add all views. As views are added to the pattern, we remove them
1541                 // from notification by making them non-clickable below.
1542                 virtualViewIds.add(i);
1543             }
1544         }
1545 
1546         @Override
1547         protected void onPopulateEventForVirtualView(int virtualViewId, AccessibilityEvent event) {
1548             if (DEBUG_A11Y) Log.v(TAG, "onPopulateEventForVirtualView(" + virtualViewId + ")");
1549             // Announce this view
1550             VirtualViewContainer container = mItems.get(virtualViewId);
1551             if (container != null) {
1552                 event.getText().add(container.description);
1553             }
1554         }
1555 
1556         @Override
1557         public void onPopulateAccessibilityEvent(View host, AccessibilityEvent event) {
1558             super.onPopulateAccessibilityEvent(host, event);
1559             if (!mPatternInProgress) {
1560                 CharSequence contentDescription = getContext().getText(
1561                         com.android.internal.R.string.lockscreen_access_pattern_area);
1562                 event.setContentDescription(contentDescription);
1563             }
1564         }
1565 
1566         @Override
1567         protected void onPopulateNodeForVirtualView(int virtualViewId, AccessibilityNodeInfo node) {
1568             if (DEBUG_A11Y) Log.v(TAG, "onPopulateNodeForVirtualView(view=" + virtualViewId + ")");
1569 
1570             // Node and event text and content descriptions are usually
1571             // identical, so we'll use the exact same string as before.
1572             node.setText(getTextForVirtualView(virtualViewId));
1573             node.setContentDescription(getTextForVirtualView(virtualViewId));
1574 
1575             if (mPatternInProgress) {
1576                 node.setFocusable(true);
1577 
1578                 if (isClickable(virtualViewId)) {
1579                     // Mark this node of interest by making it clickable.
1580                     node.addAction(AccessibilityAction.ACTION_CLICK);
1581                     node.setClickable(isClickable(virtualViewId));
1582                 }
1583             }
1584 
1585             // Compute bounds for this object
1586             final Rect bounds = getBoundsForVirtualView(virtualViewId);
1587             if (DEBUG_A11Y) Log.v(TAG, "bounds:" + bounds.toString());
1588             node.setBoundsInParent(bounds);
1589         }
1590 
1591         private boolean isClickable(int virtualViewId) {
1592             // Dots are clickable if they're not part of the current pattern.
1593             if (virtualViewId != ExploreByTouchHelper.INVALID_ID) {
1594                 int row = (virtualViewId - VIRTUAL_BASE_VIEW_ID) / 3;
1595                 int col = (virtualViewId - VIRTUAL_BASE_VIEW_ID) % 3;
1596                 if (row < 3) {
1597                     return !mPatternDrawLookup[row][col];
1598                 }
1599             }
1600             return false;
1601         }
1602 
1603         @Override
1604         protected boolean onPerformActionForVirtualView(int virtualViewId, int action,
1605                 Bundle arguments) {
1606             if (DEBUG_A11Y) Log.v(TAG, "onPerformActionForVirtualView(id=" + virtualViewId
1607                     + ", action=" + action);
1608             switch (action) {
1609                 case AccessibilityNodeInfo.ACTION_CLICK:
1610                     // Click handling should be consistent with
1611                     // onTouchEvent(). This ensures that the view works the
1612                     // same whether accessibility is turned on or off.
1613                     return onItemClicked(virtualViewId);
1614                 default:
1615                     if (DEBUG_A11Y) Log.v(TAG, "*** action not handled in "
1616                             + "onPerformActionForVirtualView(viewId="
1617                             + virtualViewId + "action=" + action + ")");
1618             }
1619             return false;
1620         }
1621 
1622         boolean onItemClicked(int index) {
1623             if (DEBUG_A11Y) Log.v(TAG, "onItemClicked(" + index + ")");
1624 
1625             // Since the item's checked state is exposed to accessibility
1626             // services through its AccessibilityNodeInfo, we need to invalidate
1627             // the item's virtual view. At some point in the future, the
1628             // framework will obtain an updated version of the virtual view.
1629             invalidateVirtualView(index);
1630 
1631             // We need to let the framework know what type of event
1632             // happened. Accessibility services may use this event to provide
1633             // appropriate feedback to the user.
1634             sendEventForVirtualView(index, AccessibilityEvent.TYPE_VIEW_CLICKED);
1635 
1636             return true;
1637         }
1638 
1639         private Rect getBoundsForVirtualView(int virtualViewId) {
1640             int ordinal = virtualViewId - VIRTUAL_BASE_VIEW_ID;
1641             final Rect bounds = mTempRect;
1642             final int row = ordinal / 3;
1643             final int col = ordinal % 3;
1644             float centerX = getCenterXForColumn(col);
1645             float centerY = getCenterYForRow(row);
1646             float cellHitRadius = mDotHitRadius;
1647             bounds.left = (int) (centerX - cellHitRadius);
1648             bounds.right = (int) (centerX + cellHitRadius);
1649             bounds.top = (int) (centerY - cellHitRadius);
1650             bounds.bottom = (int) (centerY + cellHitRadius);
1651             return bounds;
1652         }
1653 
1654         private CharSequence getTextForVirtualView(int virtualViewId) {
1655             final Resources res = getResources();
1656             return res.getString(R.string.lockscreen_access_pattern_cell_added_verbose,
1657                     virtualViewId);
1658         }
1659 
1660         /**
1661          * Helper method to find which cell a point maps to
1662          *
1663          * if there's no hit.
1664          * @param x touch position x
1665          * @param y touch position y
1666          * @return VIRTUAL_BASE_VIEW_ID+id or 0 if no view was hit
1667          */
1668         private int getVirtualViewIdForHit(float x, float y) {
1669             Cell cellHit = detectCellHit(x, y);
1670             if (cellHit == null) {
1671                 return ExploreByTouchHelper.INVALID_ID;
1672             }
1673             boolean dotAvailable = mPatternDrawLookup[cellHit.row][cellHit.column];
1674             int dotId = (cellHit.row * 3 + cellHit.column) + VIRTUAL_BASE_VIEW_ID;
1675             int view = dotAvailable ? dotId : ExploreByTouchHelper.INVALID_ID;
1676             if (DEBUG_A11Y) Log.v(TAG, "getVirtualViewIdForHit(" + x + "," + y + ") => "
1677                     + view + "avail =" + dotAvailable);
1678             return view;
1679         }
1680     }
1681 }
1682