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