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