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