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