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