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