• 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.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      * Clear the pattern lookup table. Also reset the line fade start times for
631      * the next attempt.
632      */
clearPatternDrawLookup()633     private void clearPatternDrawLookup() {
634         for (int i = 0; i < 3; i++) {
635             for (int j = 0; j < 3; j++) {
636                 mPatternDrawLookup[i][j] = false;
637                 mLineFadeStart[i+j*3] = 0;
638             }
639         }
640     }
641 
642     /**
643      * Disable input (for instance when displaying a message that will
644      * timeout so user doesn't get view into messy state).
645      */
646     @UnsupportedAppUsage
disableInput()647     public void disableInput() {
648         mInputEnabled = false;
649     }
650 
651     /**
652      * Enable input.
653      */
654     @UnsupportedAppUsage
enableInput()655     public void enableInput() {
656         mInputEnabled = true;
657     }
658 
659     @Override
onSizeChanged(int w, int h, int oldw, int oldh)660     protected void onSizeChanged(int w, int h, int oldw, int oldh) {
661         final int width = w - mPaddingLeft - mPaddingRight;
662         mSquareWidth = width / 3.0f;
663 
664         if (DEBUG_A11Y) Log.v(TAG, "onSizeChanged(" + w + "," + h + ")");
665         final int height = h - mPaddingTop - mPaddingBottom;
666         mSquareHeight = height / 3.0f;
667         mExploreByTouchHelper.invalidateRoot();
668 
669         if (mUseLockPatternDrawable) {
670             mNotSelectedDrawable.setBounds(mPaddingLeft, mPaddingTop, width, height);
671             mSelectedDrawable.setBounds(mPaddingLeft, mPaddingTop, width, height);
672         }
673     }
674 
resolveMeasured(int measureSpec, int desired)675     private int resolveMeasured(int measureSpec, int desired)
676     {
677         int result = 0;
678         int specSize = MeasureSpec.getSize(measureSpec);
679         switch (MeasureSpec.getMode(measureSpec)) {
680             case MeasureSpec.UNSPECIFIED:
681                 result = desired;
682                 break;
683             case MeasureSpec.AT_MOST:
684                 result = Math.max(specSize, desired);
685                 break;
686             case MeasureSpec.EXACTLY:
687             default:
688                 result = specSize;
689         }
690         return result;
691     }
692 
693     @Override
onMeasure(int widthMeasureSpec, int heightMeasureSpec)694     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
695         final int minimumWidth = getSuggestedMinimumWidth();
696         final int minimumHeight = getSuggestedMinimumHeight();
697         int viewWidth = resolveMeasured(widthMeasureSpec, minimumWidth);
698         int viewHeight = resolveMeasured(heightMeasureSpec, minimumHeight);
699 
700         switch (mAspect) {
701             case ASPECT_SQUARE:
702                 viewWidth = viewHeight = Math.min(viewWidth, viewHeight);
703                 break;
704             case ASPECT_LOCK_WIDTH:
705                 viewHeight = Math.min(viewWidth, viewHeight);
706                 break;
707             case ASPECT_LOCK_HEIGHT:
708                 viewWidth = Math.min(viewWidth, viewHeight);
709                 break;
710         }
711         // Log.v(TAG, "LockPatternView dimensions: " + viewWidth + "x" + viewHeight);
712         setMeasuredDimension(viewWidth, viewHeight);
713     }
714 
715     /**
716      * Determines whether the point x, y will add a new point to the current
717      * pattern (in addition to finding the cell, also makes heuristic choices
718      * such as filling in gaps based on current pattern).
719      * @param x The x coordinate.
720      * @param y The y coordinate.
721      */
detectAndAddHit(float x, float y)722     private Cell detectAndAddHit(float x, float y) {
723         final Cell cell = checkForNewHit(x, y);
724         if (cell != null) {
725 
726             // check for gaps in existing pattern
727             Cell fillInGapCell = null;
728             final ArrayList<Cell> pattern = mPattern;
729             if (!pattern.isEmpty()) {
730                 final Cell lastCell = pattern.get(pattern.size() - 1);
731                 int dRow = cell.row - lastCell.row;
732                 int dColumn = cell.column - lastCell.column;
733 
734                 int fillInRow = lastCell.row;
735                 int fillInColumn = lastCell.column;
736 
737                 if (Math.abs(dRow) == 2 && Math.abs(dColumn) != 1) {
738                     fillInRow = lastCell.row + ((dRow > 0) ? 1 : -1);
739                 }
740 
741                 if (Math.abs(dColumn) == 2 && Math.abs(dRow) != 1) {
742                     fillInColumn = lastCell.column + ((dColumn > 0) ? 1 : -1);
743                 }
744 
745                 fillInGapCell = Cell.of(fillInRow, fillInColumn);
746             }
747 
748             if (fillInGapCell != null &&
749                     !mPatternDrawLookup[fillInGapCell.row][fillInGapCell.column]) {
750                 addCellToPattern(fillInGapCell);
751             }
752             addCellToPattern(cell);
753             if (mEnableHapticFeedback) {
754                 performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY,
755                         HapticFeedbackConstants.FLAG_IGNORE_VIEW_SETTING
756                         | HapticFeedbackConstants.FLAG_IGNORE_GLOBAL_SETTING);
757             }
758             return cell;
759         }
760         return null;
761     }
762 
addCellToPattern(Cell newCell)763     private void addCellToPattern(Cell newCell) {
764         mPatternDrawLookup[newCell.getRow()][newCell.getColumn()] = true;
765         mPattern.add(newCell);
766         if (!mInStealthMode) {
767             startCellActivatedAnimation(newCell);
768         }
769         notifyCellAdded();
770     }
771 
startCellActivatedAnimation(Cell cell)772     private void startCellActivatedAnimation(Cell cell) {
773         final CellState cellState = mCellStates[cell.row][cell.column];
774         startRadiusAnimation(mDotSize/2, mDotSizeActivated/2, 96, mLinearOutSlowInInterpolator,
775                 cellState, new Runnable() {
776                     @Override
777                     public void run() {
778                         startRadiusAnimation(mDotSizeActivated/2, mDotSize/2, 192,
779                                 mFastOutSlowInInterpolator,
780                                 cellState, null);
781                     }
782                 });
783         startLineEndAnimation(cellState, mInProgressX, mInProgressY,
784                 getCenterXForColumn(cell.column), getCenterYForRow(cell.row));
785     }
786 
startLineEndAnimation(final CellState state, final float startX, final float startY, final float targetX, final float targetY)787     private void startLineEndAnimation(final CellState state,
788             final float startX, final float startY, final float targetX, final float targetY) {
789         ValueAnimator valueAnimator = ValueAnimator.ofFloat(0, 1);
790         valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
791             @Override
792             public void onAnimationUpdate(ValueAnimator animation) {
793                 float t = (float) animation.getAnimatedValue();
794                 state.lineEndX = (1 - t) * startX + t * targetX;
795                 state.lineEndY = (1 - t) * startY + t * targetY;
796                 invalidate();
797             }
798         });
799         valueAnimator.addListener(new AnimatorListenerAdapter() {
800             @Override
801             public void onAnimationEnd(Animator animation) {
802                 state.lineAnimator = null;
803             }
804         });
805         valueAnimator.setInterpolator(mFastOutSlowInInterpolator);
806         valueAnimator.setDuration(100);
807         valueAnimator.start();
808         state.lineAnimator = valueAnimator;
809     }
810 
startRadiusAnimation(float start, float end, long duration, Interpolator interpolator, final CellState state, final Runnable endRunnable)811     private void startRadiusAnimation(float start, float end, long duration,
812             Interpolator interpolator, final CellState state, final Runnable endRunnable) {
813         ValueAnimator valueAnimator = ValueAnimator.ofFloat(start, end);
814         valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
815             @Override
816             public void onAnimationUpdate(ValueAnimator animation) {
817                 state.radius = (float) animation.getAnimatedValue();
818                 invalidate();
819             }
820         });
821         if (endRunnable != null) {
822             valueAnimator.addListener(new AnimatorListenerAdapter() {
823                 @Override
824                 public void onAnimationEnd(Animator animation) {
825                     endRunnable.run();
826                 }
827             });
828         }
829         valueAnimator.setInterpolator(interpolator);
830         valueAnimator.setDuration(duration);
831         valueAnimator.start();
832     }
833 
834     // helper method to find which cell a point maps to
checkForNewHit(float x, float y)835     private Cell checkForNewHit(float x, float y) {
836 
837         final int rowHit = getRowHit(y);
838         if (rowHit < 0) {
839             return null;
840         }
841         final int columnHit = getColumnHit(x);
842         if (columnHit < 0) {
843             return null;
844         }
845 
846         if (mPatternDrawLookup[rowHit][columnHit]) {
847             return null;
848         }
849         return Cell.of(rowHit, columnHit);
850     }
851 
852     /**
853      * Helper method to find the row that y falls into.
854      * @param y The y coordinate
855      * @return The row that y falls in, or -1 if it falls in no row.
856      */
getRowHit(float y)857     private int getRowHit(float y) {
858 
859         final float squareHeight = mSquareHeight;
860         float hitSize = squareHeight * mHitFactor;
861 
862         float offset = mPaddingTop + (squareHeight - hitSize) / 2f;
863         for (int i = 0; i < 3; i++) {
864 
865             final float hitTop = offset + squareHeight * i;
866             if (y >= hitTop && y <= hitTop + hitSize) {
867                 return i;
868             }
869         }
870         return -1;
871     }
872 
873     /**
874      * Helper method to find the column x fallis into.
875      * @param x The x coordinate.
876      * @return The column that x falls in, or -1 if it falls in no column.
877      */
getColumnHit(float x)878     private int getColumnHit(float x) {
879         final float squareWidth = mSquareWidth;
880         float hitSize = squareWidth * mHitFactor;
881 
882         float offset = mPaddingLeft + (squareWidth - hitSize) / 2f;
883         for (int i = 0; i < 3; i++) {
884 
885             final float hitLeft = offset + squareWidth * i;
886             if (x >= hitLeft && x <= hitLeft + hitSize) {
887                 return i;
888             }
889         }
890         return -1;
891     }
892 
893     @Override
onHoverEvent(MotionEvent event)894     public boolean onHoverEvent(MotionEvent event) {
895         if (AccessibilityManager.getInstance(mContext).isTouchExplorationEnabled()) {
896             final int action = event.getAction();
897             switch (action) {
898                 case MotionEvent.ACTION_HOVER_ENTER:
899                     event.setAction(MotionEvent.ACTION_DOWN);
900                     break;
901                 case MotionEvent.ACTION_HOVER_MOVE:
902                     event.setAction(MotionEvent.ACTION_MOVE);
903                     break;
904                 case MotionEvent.ACTION_HOVER_EXIT:
905                     event.setAction(MotionEvent.ACTION_UP);
906                     break;
907             }
908             onTouchEvent(event);
909             event.setAction(action);
910         }
911         return super.onHoverEvent(event);
912     }
913 
914     @Override
onTouchEvent(MotionEvent event)915     public boolean onTouchEvent(MotionEvent event) {
916         if (!mInputEnabled || !isEnabled()) {
917             return false;
918         }
919 
920         switch(event.getAction()) {
921             case MotionEvent.ACTION_DOWN:
922                 handleActionDown(event);
923                 return true;
924             case MotionEvent.ACTION_UP:
925                 handleActionUp();
926                 return true;
927             case MotionEvent.ACTION_MOVE:
928                 handleActionMove(event);
929                 return true;
930             case MotionEvent.ACTION_CANCEL:
931                 if (mPatternInProgress) {
932                     setPatternInProgress(false);
933                     resetPattern();
934                     notifyPatternCleared();
935                 }
936                 if (PROFILE_DRAWING) {
937                     if (mDrawingProfilingStarted) {
938                         Debug.stopMethodTracing();
939                         mDrawingProfilingStarted = false;
940                     }
941                 }
942                 return true;
943         }
944         return false;
945     }
946 
setPatternInProgress(boolean progress)947     private void setPatternInProgress(boolean progress) {
948         mPatternInProgress = progress;
949         mExploreByTouchHelper.invalidateRoot();
950     }
951 
handleActionMove(MotionEvent event)952     private void handleActionMove(MotionEvent event) {
953         // Handle all recent motion events so we don't skip any cells even when the device
954         // is busy...
955         final float radius = mPathWidth;
956         final int historySize = event.getHistorySize();
957         mTmpInvalidateRect.setEmpty();
958         boolean invalidateNow = false;
959         for (int i = 0; i < historySize + 1; i++) {
960             final float x = i < historySize ? event.getHistoricalX(i) : event.getX();
961             final float y = i < historySize ? event.getHistoricalY(i) : event.getY();
962             Cell hitCell = detectAndAddHit(x, y);
963             final int patternSize = mPattern.size();
964             if (hitCell != null && patternSize == 1) {
965                 setPatternInProgress(true);
966                 notifyPatternStarted();
967             }
968             // note current x and y for rubber banding of in progress patterns
969             final float dx = Math.abs(x - mInProgressX);
970             final float dy = Math.abs(y - mInProgressY);
971             if (dx > DRAG_THRESHHOLD || dy > DRAG_THRESHHOLD) {
972                 invalidateNow = true;
973             }
974 
975             if (mPatternInProgress && patternSize > 0) {
976                 final ArrayList<Cell> pattern = mPattern;
977                 final Cell lastCell = pattern.get(patternSize - 1);
978                 float lastCellCenterX = getCenterXForColumn(lastCell.column);
979                 float lastCellCenterY = getCenterYForRow(lastCell.row);
980 
981                 // Adjust for drawn segment from last cell to (x,y). Radius accounts for line width.
982                 float left = Math.min(lastCellCenterX, x) - radius;
983                 float right = Math.max(lastCellCenterX, x) + radius;
984                 float top = Math.min(lastCellCenterY, y) - radius;
985                 float bottom = Math.max(lastCellCenterY, y) + radius;
986 
987                 // Invalidate between the pattern's new cell and the pattern's previous cell
988                 if (hitCell != null) {
989                     final float width = mSquareWidth * 0.5f;
990                     final float height = mSquareHeight * 0.5f;
991                     final float hitCellCenterX = getCenterXForColumn(hitCell.column);
992                     final float hitCellCenterY = getCenterYForRow(hitCell.row);
993 
994                     left = Math.min(hitCellCenterX - width, left);
995                     right = Math.max(hitCellCenterX + width, right);
996                     top = Math.min(hitCellCenterY - height, top);
997                     bottom = Math.max(hitCellCenterY + height, bottom);
998                 }
999 
1000                 // Invalidate between the pattern's last cell and the previous location
1001                 mTmpInvalidateRect.union(Math.round(left), Math.round(top),
1002                         Math.round(right), Math.round(bottom));
1003             }
1004         }
1005         mInProgressX = event.getX();
1006         mInProgressY = event.getY();
1007 
1008         // To save updates, we only invalidate if the user moved beyond a certain amount.
1009         if (invalidateNow) {
1010             mInvalidate.union(mTmpInvalidateRect);
1011             invalidate(mInvalidate);
1012             mInvalidate.set(mTmpInvalidateRect);
1013         }
1014     }
1015 
sendAccessEvent(int resId)1016     private void sendAccessEvent(int resId) {
1017         announceForAccessibility(mContext.getString(resId));
1018     }
1019 
handleActionUp()1020     private void handleActionUp() {
1021         // report pattern detected
1022         if (!mPattern.isEmpty()) {
1023             setPatternInProgress(false);
1024             cancelLineAnimations();
1025             notifyPatternDetected();
1026             // Also clear pattern if fading is enabled
1027             if (mFadePattern) {
1028                 clearPatternDrawLookup();
1029                 mPatternDisplayMode = DisplayMode.Correct;
1030             }
1031             invalidate();
1032         }
1033         if (PROFILE_DRAWING) {
1034             if (mDrawingProfilingStarted) {
1035                 Debug.stopMethodTracing();
1036                 mDrawingProfilingStarted = false;
1037             }
1038         }
1039     }
1040 
cancelLineAnimations()1041     private void cancelLineAnimations() {
1042         for (int i = 0; i < 3; i++) {
1043             for (int j = 0; j < 3; j++) {
1044                 CellState state = mCellStates[i][j];
1045                 if (state.lineAnimator != null) {
1046                     state.lineAnimator.cancel();
1047                     state.lineEndX = Float.MIN_VALUE;
1048                     state.lineEndY = Float.MIN_VALUE;
1049                 }
1050             }
1051         }
1052     }
handleActionDown(MotionEvent event)1053     private void handleActionDown(MotionEvent event) {
1054         resetPattern();
1055         final float x = event.getX();
1056         final float y = event.getY();
1057         final Cell hitCell = detectAndAddHit(x, y);
1058         if (hitCell != null) {
1059             setPatternInProgress(true);
1060             mPatternDisplayMode = DisplayMode.Correct;
1061             notifyPatternStarted();
1062         } else if (mPatternInProgress) {
1063             setPatternInProgress(false);
1064             notifyPatternCleared();
1065         }
1066         if (hitCell != null) {
1067             final float startX = getCenterXForColumn(hitCell.column);
1068             final float startY = getCenterYForRow(hitCell.row);
1069 
1070             final float widthOffset = mSquareWidth / 2f;
1071             final float heightOffset = mSquareHeight / 2f;
1072 
1073             invalidate((int) (startX - widthOffset), (int) (startY - heightOffset),
1074                     (int) (startX + widthOffset), (int) (startY + heightOffset));
1075         }
1076         mInProgressX = x;
1077         mInProgressY = y;
1078         if (PROFILE_DRAWING) {
1079             if (!mDrawingProfilingStarted) {
1080                 Debug.startMethodTracing("LockPatternDrawing");
1081                 mDrawingProfilingStarted = true;
1082             }
1083         }
1084     }
1085 
getCenterXForColumn(int column)1086     private float getCenterXForColumn(int column) {
1087         return mPaddingLeft + column * mSquareWidth + mSquareWidth / 2f;
1088     }
1089 
getCenterYForRow(int row)1090     private float getCenterYForRow(int row) {
1091         return mPaddingTop + row * mSquareHeight + mSquareHeight / 2f;
1092     }
1093 
1094     @Override
onDraw(Canvas canvas)1095     protected void onDraw(Canvas canvas) {
1096         final ArrayList<Cell> pattern = mPattern;
1097         final int count = pattern.size();
1098         final boolean[][] drawLookup = mPatternDrawLookup;
1099 
1100         if (mPatternDisplayMode == DisplayMode.Animate) {
1101 
1102             // figure out which circles to draw
1103 
1104             // + 1 so we pause on complete pattern
1105             final int oneCycle = (count + 1) * MILLIS_PER_CIRCLE_ANIMATING;
1106             final int spotInCycle = (int) (SystemClock.elapsedRealtime() -
1107                     mAnimatingPeriodStart) % oneCycle;
1108             final int numCircles = spotInCycle / MILLIS_PER_CIRCLE_ANIMATING;
1109 
1110             clearPatternDrawLookup();
1111             for (int i = 0; i < numCircles; i++) {
1112                 final Cell cell = pattern.get(i);
1113                 drawLookup[cell.getRow()][cell.getColumn()] = true;
1114             }
1115 
1116             // figure out in progress portion of ghosting line
1117 
1118             final boolean needToUpdateInProgressPoint = numCircles > 0
1119                     && numCircles < count;
1120 
1121             if (needToUpdateInProgressPoint) {
1122                 final float percentageOfNextCircle =
1123                         ((float) (spotInCycle % MILLIS_PER_CIRCLE_ANIMATING)) /
1124                                 MILLIS_PER_CIRCLE_ANIMATING;
1125 
1126                 final Cell currentCell = pattern.get(numCircles - 1);
1127                 final float centerX = getCenterXForColumn(currentCell.column);
1128                 final float centerY = getCenterYForRow(currentCell.row);
1129 
1130                 final Cell nextCell = pattern.get(numCircles);
1131                 final float dx = percentageOfNextCircle *
1132                         (getCenterXForColumn(nextCell.column) - centerX);
1133                 final float dy = percentageOfNextCircle *
1134                         (getCenterYForRow(nextCell.row) - centerY);
1135                 mInProgressX = centerX + dx;
1136                 mInProgressY = centerY + dy;
1137             }
1138             // TODO: Infinite loop here...
1139             invalidate();
1140         }
1141 
1142         final Path currentPath = mCurrentPath;
1143         currentPath.rewind();
1144 
1145         // draw the circles
1146         for (int i = 0; i < 3; i++) {
1147             float centerY = getCenterYForRow(i);
1148             for (int j = 0; j < 3; j++) {
1149                 CellState cellState = mCellStates[i][j];
1150                 float centerX = getCenterXForColumn(j);
1151                 float translationY = cellState.translationY;
1152 
1153                 if (mUseLockPatternDrawable) {
1154                     drawCellDrawable(canvas, i, j, cellState.radius, drawLookup[i][j]);
1155                 } else {
1156                     if (isHardwareAccelerated() && cellState.hwAnimating) {
1157                         RecordingCanvas recordingCanvas = (RecordingCanvas) canvas;
1158                         recordingCanvas.drawCircle(cellState.hwCenterX, cellState.hwCenterY,
1159                                 cellState.hwRadius, cellState.hwPaint);
1160                     } else {
1161                         drawCircle(canvas, (int) centerX, (int) centerY + translationY,
1162                                 cellState.radius, drawLookup[i][j], cellState.alpha);
1163                     }
1164                 }
1165             }
1166         }
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 
1240     private float calculateLastSegmentAlpha(float x, float y, float lastX, float lastY) {
1241         float diffX = x - lastX;
1242         float diffY = y - lastY;
1243         float dist = (float) Math.sqrt(diffX*diffX + diffY*diffY);
1244         float frac = dist/mSquareWidth;
1245         return Math.min(1f, Math.max(0f, (frac - 0.3f) * 4f));
1246     }
1247 
1248     private int getCurrentColor(boolean partOfPattern) {
1249         if (!partOfPattern || mInStealthMode || mPatternInProgress) {
1250             // unselected circle
1251             return mRegularColor;
1252         } else if (mPatternDisplayMode == DisplayMode.Wrong) {
1253             // the pattern is wrong
1254             return mErrorColor;
1255         } else if (mPatternDisplayMode == DisplayMode.Correct ||
1256                 mPatternDisplayMode == DisplayMode.Animate) {
1257             return mSuccessColor;
1258         } else {
1259             throw new IllegalStateException("unknown display mode " + mPatternDisplayMode);
1260         }
1261     }
1262 
1263     /**
1264      * @param partOfPattern Whether this circle is part of the pattern.
1265      */
1266     private void drawCircle(Canvas canvas, float centerX, float centerY, float radius,
1267             boolean partOfPattern, float alpha) {
1268         mPaint.setColor(getCurrentColor(partOfPattern));
1269         mPaint.setAlpha((int) (alpha * 255));
1270         canvas.drawCircle(centerX, centerY, radius, mPaint);
1271     }
1272 
1273     /**
1274      * @param partOfPattern Whether this circle is part of the pattern.
1275      */
1276     private void drawCellDrawable(Canvas canvas, int i, int j, float radius,
1277             boolean partOfPattern) {
1278         Rect dst = new Rect(
1279             (int) (mPaddingLeft + j * mSquareWidth),
1280             (int) (mPaddingTop + i * mSquareHeight),
1281             (int) (mPaddingLeft + (j + 1) * mSquareWidth),
1282             (int) (mPaddingTop + (i + 1) * mSquareHeight));
1283         float scale = radius / (mDotSize / 2);
1284 
1285         // Only draw on this square with the appropriate scale.
1286         canvas.save();
1287         canvas.clipRect(dst);
1288         canvas.scale(scale, scale, dst.centerX(), dst.centerY());
1289         if (!partOfPattern || scale > 1) {
1290             mNotSelectedDrawable.draw(canvas);
1291         } else {
1292             mSelectedDrawable.draw(canvas);
1293         }
1294         canvas.restore();
1295     }
1296 
1297     @Override
1298     protected Parcelable onSaveInstanceState() {
1299         Parcelable superState = super.onSaveInstanceState();
1300         byte[] patternBytes = LockPatternUtils.patternToByteArray(mPattern);
1301         String patternString = patternBytes != null ? new String(patternBytes) : null;
1302         return new SavedState(superState,
1303                 patternString,
1304                 mPatternDisplayMode.ordinal(),
1305                 mInputEnabled, mInStealthMode, mEnableHapticFeedback);
1306     }
1307 
1308     @Override
1309     protected void onRestoreInstanceState(Parcelable state) {
1310         final SavedState ss = (SavedState) state;
1311         super.onRestoreInstanceState(ss.getSuperState());
1312         setPattern(
1313                 DisplayMode.Correct,
1314                 LockPatternUtils.stringToPattern(ss.getSerializedPattern()));
1315         mPatternDisplayMode = DisplayMode.values()[ss.getDisplayMode()];
1316         mInputEnabled = ss.isInputEnabled();
1317         mInStealthMode = ss.isInStealthMode();
1318         mEnableHapticFeedback = ss.isTactileFeedbackEnabled();
1319     }
1320 
1321     /**
1322      * The parecelable for saving and restoring a lock pattern view.
1323      */
1324     private static class SavedState extends BaseSavedState {
1325 
1326         private final String mSerializedPattern;
1327         private final int mDisplayMode;
1328         private final boolean mInputEnabled;
1329         private final boolean mInStealthMode;
1330         private final boolean mTactileFeedbackEnabled;
1331 
1332         /**
1333          * Constructor called from {@link LockPatternView#onSaveInstanceState()}
1334          */
1335         @UnsupportedAppUsage
1336         private SavedState(Parcelable superState, String serializedPattern, int displayMode,
1337                 boolean inputEnabled, boolean inStealthMode, boolean tactileFeedbackEnabled) {
1338             super(superState);
1339             mSerializedPattern = serializedPattern;
1340             mDisplayMode = displayMode;
1341             mInputEnabled = inputEnabled;
1342             mInStealthMode = inStealthMode;
1343             mTactileFeedbackEnabled = tactileFeedbackEnabled;
1344         }
1345 
1346         /**
1347          * Constructor called from {@link #CREATOR}
1348          */
1349         @UnsupportedAppUsage
1350         private SavedState(Parcel in) {
1351             super(in);
1352             mSerializedPattern = in.readString();
1353             mDisplayMode = in.readInt();
1354             mInputEnabled = (Boolean) in.readValue(null);
1355             mInStealthMode = (Boolean) in.readValue(null);
1356             mTactileFeedbackEnabled = (Boolean) in.readValue(null);
1357         }
1358 
1359         public String getSerializedPattern() {
1360             return mSerializedPattern;
1361         }
1362 
1363         public int getDisplayMode() {
1364             return mDisplayMode;
1365         }
1366 
1367         public boolean isInputEnabled() {
1368             return mInputEnabled;
1369         }
1370 
1371         public boolean isInStealthMode() {
1372             return mInStealthMode;
1373         }
1374 
1375         public boolean isTactileFeedbackEnabled(){
1376             return mTactileFeedbackEnabled;
1377         }
1378 
1379         @Override
1380         public void writeToParcel(Parcel dest, int flags) {
1381             super.writeToParcel(dest, flags);
1382             dest.writeString(mSerializedPattern);
1383             dest.writeInt(mDisplayMode);
1384             dest.writeValue(mInputEnabled);
1385             dest.writeValue(mInStealthMode);
1386             dest.writeValue(mTactileFeedbackEnabled);
1387         }
1388 
1389         @SuppressWarnings({ "unused", "hiding" }) // Found using reflection
1390         public static final Parcelable.Creator<SavedState> CREATOR =
1391                 new Creator<SavedState>() {
1392             @Override
1393             public SavedState createFromParcel(Parcel in) {
1394                 return new SavedState(in);
1395             }
1396 
1397             @Override
1398             public SavedState[] newArray(int size) {
1399                 return new SavedState[size];
1400             }
1401         };
1402     }
1403 
1404     private final class PatternExploreByTouchHelper extends ExploreByTouchHelper {
1405         private Rect mTempRect = new Rect();
1406         private final SparseArray<VirtualViewContainer> mItems = new SparseArray<>();
1407 
1408         class VirtualViewContainer {
1409             public VirtualViewContainer(CharSequence description) {
1410                 this.description = description;
1411             }
1412             CharSequence description;
1413         };
1414 
1415         public PatternExploreByTouchHelper(View forView) {
1416             super(forView);
1417             for (int i = VIRTUAL_BASE_VIEW_ID; i < VIRTUAL_BASE_VIEW_ID + 9; i++) {
1418                 mItems.put(i, new VirtualViewContainer(getTextForVirtualView(i)));
1419             }
1420         }
1421 
1422         @Override
1423         protected int getVirtualViewAt(float x, float y) {
1424             // This must use the same hit logic for the screen to ensure consistency whether
1425             // accessibility is on or off.
1426             int id = getVirtualViewIdForHit(x, y);
1427             return id;
1428         }
1429 
1430         @Override
1431         protected void getVisibleVirtualViews(IntArray virtualViewIds) {
1432             if (DEBUG_A11Y) Log.v(TAG, "getVisibleVirtualViews(len=" + virtualViewIds.size() + ")");
1433             if (!mPatternInProgress) {
1434                 return;
1435             }
1436             for (int i = VIRTUAL_BASE_VIEW_ID; i < VIRTUAL_BASE_VIEW_ID + 9; i++) {
1437                 // Add all views. As views are added to the pattern, we remove them
1438                 // from notification by making them non-clickable below.
1439                 virtualViewIds.add(i);
1440             }
1441         }
1442 
1443         @Override
1444         protected void onPopulateEventForVirtualView(int virtualViewId, AccessibilityEvent event) {
1445             if (DEBUG_A11Y) Log.v(TAG, "onPopulateEventForVirtualView(" + virtualViewId + ")");
1446             // Announce this view
1447             VirtualViewContainer container = mItems.get(virtualViewId);
1448             if (container != null) {
1449                 event.getText().add(container.description);
1450             }
1451         }
1452 
1453         @Override
1454         public void onPopulateAccessibilityEvent(View host, AccessibilityEvent event) {
1455             super.onPopulateAccessibilityEvent(host, event);
1456             if (!mPatternInProgress) {
1457                 CharSequence contentDescription = getContext().getText(
1458                         com.android.internal.R.string.lockscreen_access_pattern_area);
1459                 event.setContentDescription(contentDescription);
1460             }
1461         }
1462 
1463         @Override
1464         protected void onPopulateNodeForVirtualView(int virtualViewId, AccessibilityNodeInfo node) {
1465             if (DEBUG_A11Y) Log.v(TAG, "onPopulateNodeForVirtualView(view=" + virtualViewId + ")");
1466 
1467             // Node and event text and content descriptions are usually
1468             // identical, so we'll use the exact same string as before.
1469             node.setText(getTextForVirtualView(virtualViewId));
1470             node.setContentDescription(getTextForVirtualView(virtualViewId));
1471 
1472             if (mPatternInProgress) {
1473                 node.setFocusable(true);
1474 
1475                 if (isClickable(virtualViewId)) {
1476                     // Mark this node of interest by making it clickable.
1477                     node.addAction(AccessibilityAction.ACTION_CLICK);
1478                     node.setClickable(isClickable(virtualViewId));
1479                 }
1480             }
1481 
1482             // Compute bounds for this object
1483             final Rect bounds = getBoundsForVirtualView(virtualViewId);
1484             if (DEBUG_A11Y) Log.v(TAG, "bounds:" + bounds.toString());
1485             node.setBoundsInParent(bounds);
1486         }
1487 
1488         private boolean isClickable(int virtualViewId) {
1489             // Dots are clickable if they're not part of the current pattern.
1490             if (virtualViewId != ExploreByTouchHelper.INVALID_ID) {
1491                 int row = (virtualViewId - VIRTUAL_BASE_VIEW_ID) / 3;
1492                 int col = (virtualViewId - VIRTUAL_BASE_VIEW_ID) % 3;
1493                 return !mPatternDrawLookup[row][col];
1494             }
1495             return false;
1496         }
1497 
1498         @Override
1499         protected boolean onPerformActionForVirtualView(int virtualViewId, int action,
1500                 Bundle arguments) {
1501             if (DEBUG_A11Y) Log.v(TAG, "onPerformActionForVirtualView(id=" + virtualViewId
1502                     + ", action=" + action);
1503             switch (action) {
1504                 case AccessibilityNodeInfo.ACTION_CLICK:
1505                     // Click handling should be consistent with
1506                     // onTouchEvent(). This ensures that the view works the
1507                     // same whether accessibility is turned on or off.
1508                     return onItemClicked(virtualViewId);
1509                 default:
1510                     if (DEBUG_A11Y) Log.v(TAG, "*** action not handled in "
1511                             + "onPerformActionForVirtualView(viewId="
1512                             + virtualViewId + "action=" + action + ")");
1513             }
1514             return false;
1515         }
1516 
1517         boolean onItemClicked(int index) {
1518             if (DEBUG_A11Y) Log.v(TAG, "onItemClicked(" + index + ")");
1519 
1520             // Since the item's checked state is exposed to accessibility
1521             // services through its AccessibilityNodeInfo, we need to invalidate
1522             // the item's virtual view. At some point in the future, the
1523             // framework will obtain an updated version of the virtual view.
1524             invalidateVirtualView(index);
1525 
1526             // We need to let the framework know what type of event
1527             // happened. Accessibility services may use this event to provide
1528             // appropriate feedback to the user.
1529             sendEventForVirtualView(index, AccessibilityEvent.TYPE_VIEW_CLICKED);
1530 
1531             return true;
1532         }
1533 
1534         private Rect getBoundsForVirtualView(int virtualViewId) {
1535             int ordinal = virtualViewId - VIRTUAL_BASE_VIEW_ID;
1536             final Rect bounds = mTempRect;
1537             final int row = ordinal / 3;
1538             final int col = ordinal % 3;
1539             final CellState cell = mCellStates[row][col];
1540             float centerX = getCenterXForColumn(col);
1541             float centerY = getCenterYForRow(row);
1542             float cellheight = mSquareHeight * mHitFactor * 0.5f;
1543             float cellwidth = mSquareWidth * mHitFactor * 0.5f;
1544             bounds.left = (int) (centerX - cellwidth);
1545             bounds.right = (int) (centerX + cellwidth);
1546             bounds.top = (int) (centerY - cellheight);
1547             bounds.bottom = (int) (centerY + cellheight);
1548             return bounds;
1549         }
1550 
1551         private CharSequence getTextForVirtualView(int virtualViewId) {
1552             final Resources res = getResources();
1553             return res.getString(R.string.lockscreen_access_pattern_cell_added_verbose,
1554                     virtualViewId);
1555         }
1556 
1557         /**
1558          * Helper method to find which cell a point maps to
1559          *
1560          * if there's no hit.
1561          * @param x touch position x
1562          * @param y touch position y
1563          * @return VIRTUAL_BASE_VIEW_ID+id or 0 if no view was hit
1564          */
1565         private int getVirtualViewIdForHit(float x, float y) {
1566             final int rowHit = getRowHit(y);
1567             if (rowHit < 0) {
1568                 return ExploreByTouchHelper.INVALID_ID;
1569             }
1570             final int columnHit = getColumnHit(x);
1571             if (columnHit < 0) {
1572                 return ExploreByTouchHelper.INVALID_ID;
1573             }
1574             boolean dotAvailable = mPatternDrawLookup[rowHit][columnHit];
1575             int dotId = (rowHit * 3 + columnHit) + VIRTUAL_BASE_VIEW_ID;
1576             int view = dotAvailable ? dotId : ExploreByTouchHelper.INVALID_ID;
1577             if (DEBUG_A11Y) Log.v(TAG, "getVirtualViewIdForHit(" + x + "," + y + ") => "
1578                     + view + "avail =" + dotAvailable);
1579             return view;
1580         }
1581     }
1582 }
1583