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