• 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 
20 import android.content.Context;
21 import android.content.res.Resources;
22 import android.content.res.TypedArray;
23 import android.graphics.Bitmap;
24 import android.graphics.BitmapFactory;
25 import android.graphics.Canvas;
26 import android.graphics.Color;
27 import android.graphics.Matrix;
28 import android.graphics.Paint;
29 import android.graphics.Path;
30 import android.graphics.Rect;
31 import android.os.Debug;
32 import android.os.Parcel;
33 import android.os.Parcelable;
34 import android.os.SystemClock;
35 import android.util.AttributeSet;
36 import android.util.Log;
37 import android.view.HapticFeedbackConstants;
38 import android.view.MotionEvent;
39 import android.view.View;
40 import android.view.accessibility.AccessibilityEvent;
41 import android.view.accessibility.AccessibilityManager;
42 
43 import com.android.internal.R;
44 
45 import java.util.ArrayList;
46 import java.util.List;
47 
48 /**
49  * Displays and detects the user's unlock attempt, which is a drag of a finger
50  * across 9 regions of the screen.
51  *
52  * Is also capable of displaying a static pattern in "in progress", "wrong" or
53  * "correct" states.
54  */
55 public class LockPatternView extends View {
56     private static final String TAG = "LockPatternView";
57     // Aspect to use when rendering this view
58     private static final int ASPECT_SQUARE = 0; // View will be the minimum of width/height
59     private static final int ASPECT_LOCK_WIDTH = 1; // Fixed width; height will be minimum of (w,h)
60     private static final int ASPECT_LOCK_HEIGHT = 2; // Fixed height; width will be minimum of (w,h)
61 
62     private static final boolean PROFILE_DRAWING = false;
63     private boolean mDrawingProfilingStarted = false;
64 
65     private Paint mPaint = new Paint();
66     private Paint mPathPaint = new Paint();
67 
68     // TODO: make this common with PhoneWindow
69     static final int STATUS_BAR_HEIGHT = 25;
70 
71     /**
72      * How many milliseconds we spend animating each circle of a lock pattern
73      * if the animating mode is set.  The entire animation should take this
74      * constant * the length of the pattern to complete.
75      */
76     private static final int MILLIS_PER_CIRCLE_ANIMATING = 700;
77 
78     private OnPatternListener mOnPatternListener;
79     private ArrayList<Cell> mPattern = new ArrayList<Cell>(9);
80 
81     /**
82      * Lookup table for the circles of the pattern we are currently drawing.
83      * This will be the cells of the complete pattern unless we are animating,
84      * in which case we use this to hold the cells we are drawing for the in
85      * progress animation.
86      */
87     private boolean[][] mPatternDrawLookup = new boolean[3][3];
88 
89     /**
90      * the in progress point:
91      * - during interaction: where the user's finger is
92      * - during animation: the current tip of the animating line
93      */
94     private float mInProgressX = -1;
95     private float mInProgressY = -1;
96 
97     private long mAnimatingPeriodStart;
98 
99     private DisplayMode mPatternDisplayMode = DisplayMode.Correct;
100     private boolean mInputEnabled = true;
101     private boolean mInStealthMode = false;
102     private boolean mEnableHapticFeedback = true;
103     private boolean mPatternInProgress = false;
104 
105     private float mDiameterFactor = 0.10f; // TODO: move to attrs
106     private final int mStrokeAlpha = 128;
107     private float mHitFactor = 0.6f;
108 
109     private float mSquareWidth;
110     private float mSquareHeight;
111 
112     private Bitmap mBitmapBtnDefault;
113     private Bitmap mBitmapBtnTouched;
114     private Bitmap mBitmapCircleDefault;
115     private Bitmap mBitmapCircleGreen;
116     private Bitmap mBitmapCircleRed;
117 
118     private Bitmap mBitmapArrowGreenUp;
119     private Bitmap mBitmapArrowRedUp;
120 
121     private final Path mCurrentPath = new Path();
122     private final Rect mInvalidate = new Rect();
123 
124     private int mBitmapWidth;
125     private int mBitmapHeight;
126 
127     private int mAspect;
128     private final Matrix mArrowMatrix = new Matrix();
129     private final Matrix mCircleMatrix = new Matrix();
130 
131 
132     /**
133      * Represents a cell in the 3 X 3 matrix of the unlock pattern view.
134      */
135     public static class Cell {
136         int row;
137         int column;
138 
139         // keep # objects limited to 9
140         static Cell[][] sCells = new Cell[3][3];
141         static {
142             for (int i = 0; i < 3; i++) {
143                 for (int j = 0; j < 3; j++) {
144                     sCells[i][j] = new Cell(i, j);
145                 }
146             }
147         }
148 
149         /**
150          * @param row The row of the cell.
151          * @param column The column of the cell.
152          */
Cell(int row, int column)153         private Cell(int row, int column) {
154             checkRange(row, column);
155             this.row = row;
156             this.column = column;
157         }
158 
getRow()159         public int getRow() {
160             return row;
161         }
162 
getColumn()163         public int getColumn() {
164             return column;
165         }
166 
167         /**
168          * @param row The row of the cell.
169          * @param column The column of the cell.
170          */
of(int row, int column)171         public static synchronized Cell of(int row, int column) {
172             checkRange(row, column);
173             return sCells[row][column];
174         }
175 
checkRange(int row, int column)176         private static void checkRange(int row, int column) {
177             if (row < 0 || row > 2) {
178                 throw new IllegalArgumentException("row must be in range 0-2");
179             }
180             if (column < 0 || column > 2) {
181                 throw new IllegalArgumentException("column must be in range 0-2");
182             }
183         }
184 
toString()185         public String toString() {
186             return "(row=" + row + ",clmn=" + column + ")";
187         }
188     }
189 
190     /**
191      * How to display the current pattern.
192      */
193     public enum DisplayMode {
194 
195         /**
196          * The pattern drawn is correct (i.e draw it in a friendly color)
197          */
198         Correct,
199 
200         /**
201          * Animate the pattern (for demo, and help).
202          */
203         Animate,
204 
205         /**
206          * The pattern is wrong (i.e draw a foreboding color)
207          */
208         Wrong
209     }
210 
211     /**
212      * The call back interface for detecting patterns entered by the user.
213      */
214     public static interface OnPatternListener {
215 
216         /**
217          * A new pattern has begun.
218          */
onPatternStart()219         void onPatternStart();
220 
221         /**
222          * The pattern was cleared.
223          */
onPatternCleared()224         void onPatternCleared();
225 
226         /**
227          * The user extended the pattern currently being drawn by one cell.
228          * @param pattern The pattern with newly added cell.
229          */
onPatternCellAdded(List<Cell> pattern)230         void onPatternCellAdded(List<Cell> pattern);
231 
232         /**
233          * A pattern was detected from the user.
234          * @param pattern The pattern.
235          */
onPatternDetected(List<Cell> pattern)236         void onPatternDetected(List<Cell> pattern);
237     }
238 
LockPatternView(Context context)239     public LockPatternView(Context context) {
240         this(context, null);
241     }
242 
LockPatternView(Context context, AttributeSet attrs)243     public LockPatternView(Context context, AttributeSet attrs) {
244         super(context, attrs);
245 
246         TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.LockPatternView);
247 
248         final String aspect = a.getString(R.styleable.LockPatternView_aspect);
249 
250         if ("square".equals(aspect)) {
251             mAspect = ASPECT_SQUARE;
252         } else if ("lock_width".equals(aspect)) {
253             mAspect = ASPECT_LOCK_WIDTH;
254         } else if ("lock_height".equals(aspect)) {
255             mAspect = ASPECT_LOCK_HEIGHT;
256         } else {
257             mAspect = ASPECT_SQUARE;
258         }
259 
260         setClickable(true);
261 
262         mPathPaint.setAntiAlias(true);
263         mPathPaint.setDither(true);
264         mPathPaint.setColor(Color.WHITE);   // TODO this should be from the style
265         mPathPaint.setAlpha(mStrokeAlpha);
266         mPathPaint.setStyle(Paint.Style.STROKE);
267         mPathPaint.setStrokeJoin(Paint.Join.ROUND);
268         mPathPaint.setStrokeCap(Paint.Cap.ROUND);
269 
270         // lot's of bitmaps!
271         mBitmapBtnDefault = getBitmapFor(R.drawable.btn_code_lock_default_holo);
272         mBitmapBtnTouched = getBitmapFor(R.drawable.btn_code_lock_touched_holo);
273         mBitmapCircleDefault = getBitmapFor(R.drawable.indicator_code_lock_point_area_default_holo);
274         mBitmapCircleGreen = getBitmapFor(R.drawable.indicator_code_lock_point_area_green_holo);
275         mBitmapCircleRed = getBitmapFor(R.drawable.indicator_code_lock_point_area_red_holo);
276 
277         mBitmapArrowGreenUp = getBitmapFor(R.drawable.indicator_code_lock_drag_direction_green_up);
278         mBitmapArrowRedUp = getBitmapFor(R.drawable.indicator_code_lock_drag_direction_red_up);
279 
280         // bitmaps have the size of the largest bitmap in this group
281         final Bitmap bitmaps[] = { mBitmapBtnDefault, mBitmapBtnTouched, mBitmapCircleDefault,
282                 mBitmapCircleGreen, mBitmapCircleRed };
283 
284         for (Bitmap bitmap : bitmaps) {
285             mBitmapWidth = Math.max(mBitmapWidth, bitmap.getWidth());
286             mBitmapHeight = Math.max(mBitmapHeight, bitmap.getHeight());
287         }
288 
289     }
290 
getBitmapFor(int resId)291     private Bitmap getBitmapFor(int resId) {
292         return BitmapFactory.decodeResource(getContext().getResources(), resId);
293     }
294 
295     /**
296      * @return Whether the view is in stealth mode.
297      */
isInStealthMode()298     public boolean isInStealthMode() {
299         return mInStealthMode;
300     }
301 
302     /**
303      * @return Whether the view has tactile feedback enabled.
304      */
isTactileFeedbackEnabled()305     public boolean isTactileFeedbackEnabled() {
306         return mEnableHapticFeedback;
307     }
308 
309     /**
310      * Set whether the view is in stealth mode.  If true, there will be no
311      * visible feedback as the user enters the pattern.
312      *
313      * @param inStealthMode Whether in stealth mode.
314      */
setInStealthMode(boolean inStealthMode)315     public void setInStealthMode(boolean inStealthMode) {
316         mInStealthMode = inStealthMode;
317     }
318 
319     /**
320      * Set whether the view will use tactile feedback.  If true, there will be
321      * tactile feedback as the user enters the pattern.
322      *
323      * @param tactileFeedbackEnabled Whether tactile feedback is enabled
324      */
setTactileFeedbackEnabled(boolean tactileFeedbackEnabled)325     public void setTactileFeedbackEnabled(boolean tactileFeedbackEnabled) {
326         mEnableHapticFeedback = tactileFeedbackEnabled;
327     }
328 
329     /**
330      * Set the call back for pattern detection.
331      * @param onPatternListener The call back.
332      */
setOnPatternListener( OnPatternListener onPatternListener)333     public void setOnPatternListener(
334             OnPatternListener onPatternListener) {
335         mOnPatternListener = onPatternListener;
336     }
337 
338     /**
339      * Set the pattern explicitely (rather than waiting for the user to input
340      * a pattern).
341      * @param displayMode How to display the pattern.
342      * @param pattern The pattern.
343      */
setPattern(DisplayMode displayMode, List<Cell> pattern)344     public void setPattern(DisplayMode displayMode, List<Cell> pattern) {
345         mPattern.clear();
346         mPattern.addAll(pattern);
347         clearPatternDrawLookup();
348         for (Cell cell : pattern) {
349             mPatternDrawLookup[cell.getRow()][cell.getColumn()] = true;
350         }
351 
352         setDisplayMode(displayMode);
353     }
354 
355     /**
356      * Set the display mode of the current pattern.  This can be useful, for
357      * instance, after detecting a pattern to tell this view whether change the
358      * in progress result to correct or wrong.
359      * @param displayMode The display mode.
360      */
setDisplayMode(DisplayMode displayMode)361     public void setDisplayMode(DisplayMode displayMode) {
362         mPatternDisplayMode = displayMode;
363         if (displayMode == DisplayMode.Animate) {
364             if (mPattern.size() == 0) {
365                 throw new IllegalStateException("you must have a pattern to "
366                         + "animate if you want to set the display mode to animate");
367             }
368             mAnimatingPeriodStart = SystemClock.elapsedRealtime();
369             final Cell first = mPattern.get(0);
370             mInProgressX = getCenterXForColumn(first.getColumn());
371             mInProgressY = getCenterYForRow(first.getRow());
372             clearPatternDrawLookup();
373         }
374         invalidate();
375     }
376 
notifyCellAdded()377     private void notifyCellAdded() {
378         if (mOnPatternListener != null) {
379             mOnPatternListener.onPatternCellAdded(mPattern);
380         }
381         sendAccessEvent(R.string.lockscreen_access_pattern_cell_added);
382     }
383 
notifyPatternStarted()384     private void notifyPatternStarted() {
385         if (mOnPatternListener != null) {
386             mOnPatternListener.onPatternStart();
387         }
388         sendAccessEvent(R.string.lockscreen_access_pattern_start);
389     }
390 
notifyPatternDetected()391     private void notifyPatternDetected() {
392         if (mOnPatternListener != null) {
393             mOnPatternListener.onPatternDetected(mPattern);
394         }
395         sendAccessEvent(R.string.lockscreen_access_pattern_detected);
396     }
397 
notifyPatternCleared()398     private void notifyPatternCleared() {
399         if (mOnPatternListener != null) {
400             mOnPatternListener.onPatternCleared();
401         }
402         sendAccessEvent(R.string.lockscreen_access_pattern_cleared);
403     }
404 
405     /**
406      * Clear the pattern.
407      */
clearPattern()408     public void clearPattern() {
409         resetPattern();
410     }
411 
412     /**
413      * Reset all pattern state.
414      */
resetPattern()415     private void resetPattern() {
416         mPattern.clear();
417         clearPatternDrawLookup();
418         mPatternDisplayMode = DisplayMode.Correct;
419         invalidate();
420     }
421 
422     /**
423      * Clear the pattern lookup table.
424      */
clearPatternDrawLookup()425     private void clearPatternDrawLookup() {
426         for (int i = 0; i < 3; i++) {
427             for (int j = 0; j < 3; j++) {
428                 mPatternDrawLookup[i][j] = false;
429             }
430         }
431     }
432 
433     /**
434      * Disable input (for instance when displaying a message that will
435      * timeout so user doesn't get view into messy state).
436      */
disableInput()437     public void disableInput() {
438         mInputEnabled = false;
439     }
440 
441     /**
442      * Enable input.
443      */
enableInput()444     public void enableInput() {
445         mInputEnabled = true;
446     }
447 
448     @Override
onSizeChanged(int w, int h, int oldw, int oldh)449     protected void onSizeChanged(int w, int h, int oldw, int oldh) {
450         final int width = w - mPaddingLeft - mPaddingRight;
451         mSquareWidth = width / 3.0f;
452 
453         final int height = h - mPaddingTop - mPaddingBottom;
454         mSquareHeight = height / 3.0f;
455     }
456 
resolveMeasured(int measureSpec, int desired)457     private int resolveMeasured(int measureSpec, int desired)
458     {
459         int result = 0;
460         int specSize = MeasureSpec.getSize(measureSpec);
461         switch (MeasureSpec.getMode(measureSpec)) {
462             case MeasureSpec.UNSPECIFIED:
463                 result = desired;
464                 break;
465             case MeasureSpec.AT_MOST:
466                 result = Math.min(specSize, desired);
467                 break;
468             case MeasureSpec.EXACTLY:
469             default:
470                 result = specSize;
471         }
472         return result;
473     }
474 
475     @Override
getSuggestedMinimumWidth()476     protected int getSuggestedMinimumWidth() {
477         // View should be large enough to contain 3 side-by-side target bitmaps
478         return 3 * mBitmapWidth;
479     }
480 
481     @Override
getSuggestedMinimumHeight()482     protected int getSuggestedMinimumHeight() {
483         // View should be large enough to contain 3 side-by-side target bitmaps
484         return 3 * mBitmapWidth;
485     }
486 
487     @Override
onMeasure(int widthMeasureSpec, int heightMeasureSpec)488     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
489         final int minimumWidth = getSuggestedMinimumWidth();
490         final int minimumHeight = getSuggestedMinimumHeight();
491         int viewWidth = resolveMeasured(widthMeasureSpec, minimumWidth);
492         int viewHeight = resolveMeasured(heightMeasureSpec, minimumHeight);
493 
494         switch (mAspect) {
495             case ASPECT_SQUARE:
496                 viewWidth = viewHeight = Math.min(viewWidth, viewHeight);
497                 break;
498             case ASPECT_LOCK_WIDTH:
499                 viewHeight = Math.min(viewWidth, viewHeight);
500                 break;
501             case ASPECT_LOCK_HEIGHT:
502                 viewWidth = Math.min(viewWidth, viewHeight);
503                 break;
504         }
505         // Log.v(TAG, "LockPatternView dimensions: " + viewWidth + "x" + viewHeight);
506         setMeasuredDimension(viewWidth, viewHeight);
507     }
508 
509     /**
510      * Determines whether the point x, y will add a new point to the current
511      * pattern (in addition to finding the cell, also makes heuristic choices
512      * such as filling in gaps based on current pattern).
513      * @param x The x coordinate.
514      * @param y The y coordinate.
515      */
detectAndAddHit(float x, float y)516     private Cell detectAndAddHit(float x, float y) {
517         final Cell cell = checkForNewHit(x, y);
518         if (cell != null) {
519 
520             // check for gaps in existing pattern
521             Cell fillInGapCell = null;
522             final ArrayList<Cell> pattern = mPattern;
523             if (!pattern.isEmpty()) {
524                 final Cell lastCell = pattern.get(pattern.size() - 1);
525                 int dRow = cell.row - lastCell.row;
526                 int dColumn = cell.column - lastCell.column;
527 
528                 int fillInRow = lastCell.row;
529                 int fillInColumn = lastCell.column;
530 
531                 if (Math.abs(dRow) == 2 && Math.abs(dColumn) != 1) {
532                     fillInRow = lastCell.row + ((dRow > 0) ? 1 : -1);
533                 }
534 
535                 if (Math.abs(dColumn) == 2 && Math.abs(dRow) != 1) {
536                     fillInColumn = lastCell.column + ((dColumn > 0) ? 1 : -1);
537                 }
538 
539                 fillInGapCell = Cell.of(fillInRow, fillInColumn);
540             }
541 
542             if (fillInGapCell != null &&
543                     !mPatternDrawLookup[fillInGapCell.row][fillInGapCell.column]) {
544                 addCellToPattern(fillInGapCell);
545             }
546             addCellToPattern(cell);
547             if (mEnableHapticFeedback) {
548                 performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY,
549                         HapticFeedbackConstants.FLAG_IGNORE_VIEW_SETTING
550                         | HapticFeedbackConstants.FLAG_IGNORE_GLOBAL_SETTING);
551             }
552             return cell;
553         }
554         return null;
555     }
556 
addCellToPattern(Cell newCell)557     private void addCellToPattern(Cell newCell) {
558         mPatternDrawLookup[newCell.getRow()][newCell.getColumn()] = true;
559         mPattern.add(newCell);
560         notifyCellAdded();
561     }
562 
563     // helper method to find which cell a point maps to
checkForNewHit(float x, float y)564     private Cell checkForNewHit(float x, float y) {
565 
566         final int rowHit = getRowHit(y);
567         if (rowHit < 0) {
568             return null;
569         }
570         final int columnHit = getColumnHit(x);
571         if (columnHit < 0) {
572             return null;
573         }
574 
575         if (mPatternDrawLookup[rowHit][columnHit]) {
576             return null;
577         }
578         return Cell.of(rowHit, columnHit);
579     }
580 
581     /**
582      * Helper method to find the row that y falls into.
583      * @param y The y coordinate
584      * @return The row that y falls in, or -1 if it falls in no row.
585      */
getRowHit(float y)586     private int getRowHit(float y) {
587 
588         final float squareHeight = mSquareHeight;
589         float hitSize = squareHeight * mHitFactor;
590 
591         float offset = mPaddingTop + (squareHeight - hitSize) / 2f;
592         for (int i = 0; i < 3; i++) {
593 
594             final float hitTop = offset + squareHeight * i;
595             if (y >= hitTop && y <= hitTop + hitSize) {
596                 return i;
597             }
598         }
599         return -1;
600     }
601 
602     /**
603      * Helper method to find the column x fallis into.
604      * @param x The x coordinate.
605      * @return The column that x falls in, or -1 if it falls in no column.
606      */
getColumnHit(float x)607     private int getColumnHit(float x) {
608         final float squareWidth = mSquareWidth;
609         float hitSize = squareWidth * mHitFactor;
610 
611         float offset = mPaddingLeft + (squareWidth - hitSize) / 2f;
612         for (int i = 0; i < 3; i++) {
613 
614             final float hitLeft = offset + squareWidth * i;
615             if (x >= hitLeft && x <= hitLeft + hitSize) {
616                 return i;
617             }
618         }
619         return -1;
620     }
621 
622     @Override
onHoverEvent(MotionEvent event)623     public boolean onHoverEvent(MotionEvent event) {
624         if (AccessibilityManager.getInstance(mContext).isTouchExplorationEnabled()) {
625             final int action = event.getAction();
626             switch (action) {
627                 case MotionEvent.ACTION_HOVER_ENTER:
628                     event.setAction(MotionEvent.ACTION_DOWN);
629                     break;
630                 case MotionEvent.ACTION_HOVER_MOVE:
631                     event.setAction(MotionEvent.ACTION_MOVE);
632                     break;
633                 case MotionEvent.ACTION_HOVER_EXIT:
634                     event.setAction(MotionEvent.ACTION_UP);
635                     break;
636             }
637             onTouchEvent(event);
638             event.setAction(action);
639         }
640         return super.onHoverEvent(event);
641     }
642 
643     @Override
onTouchEvent(MotionEvent event)644     public boolean onTouchEvent(MotionEvent event) {
645         if (!mInputEnabled || !isEnabled()) {
646             return false;
647         }
648 
649         switch(event.getAction()) {
650             case MotionEvent.ACTION_DOWN:
651                 handleActionDown(event);
652                 return true;
653             case MotionEvent.ACTION_UP:
654                 handleActionUp(event);
655                 return true;
656             case MotionEvent.ACTION_MOVE:
657                 handleActionMove(event);
658                 return true;
659             case MotionEvent.ACTION_CANCEL:
660                 resetPattern();
661                 mPatternInProgress = false;
662                 notifyPatternCleared();
663                 if (PROFILE_DRAWING) {
664                     if (mDrawingProfilingStarted) {
665                         Debug.stopMethodTracing();
666                         mDrawingProfilingStarted = false;
667                     }
668                 }
669                 return true;
670         }
671         return false;
672     }
673 
handleActionMove(MotionEvent event)674     private void handleActionMove(MotionEvent event) {
675         // Handle all recent motion events so we don't skip any cells even when the device
676         // is busy...
677         final int historySize = event.getHistorySize();
678         for (int i = 0; i < historySize + 1; i++) {
679             final float x = i < historySize ? event.getHistoricalX(i) : event.getX();
680             final float y = i < historySize ? event.getHistoricalY(i) : event.getY();
681             final int patternSizePreHitDetect = mPattern.size();
682             Cell hitCell = detectAndAddHit(x, y);
683             final int patternSize = mPattern.size();
684             if (hitCell != null && patternSize == 1) {
685                 mPatternInProgress = true;
686                 notifyPatternStarted();
687             }
688             // note current x and y for rubber banding of in progress patterns
689             final float dx = Math.abs(x - mInProgressX);
690             final float dy = Math.abs(y - mInProgressY);
691             if (dx + dy > mSquareWidth * 0.01f) {
692                 float oldX = mInProgressX;
693                 float oldY = mInProgressY;
694 
695                 mInProgressX = x;
696                 mInProgressY = y;
697 
698                 if (mPatternInProgress && patternSize > 0) {
699                     final ArrayList<Cell> pattern = mPattern;
700                     final float radius = mSquareWidth * mDiameterFactor * 0.5f;
701 
702                     final Cell lastCell = pattern.get(patternSize - 1);
703 
704                     float startX = getCenterXForColumn(lastCell.column);
705                     float startY = getCenterYForRow(lastCell.row);
706 
707                     float left;
708                     float top;
709                     float right;
710                     float bottom;
711 
712                     final Rect invalidateRect = mInvalidate;
713 
714                     if (startX < x) {
715                         left = startX;
716                         right = x;
717                     } else {
718                         left = x;
719                         right = startX;
720                     }
721 
722                     if (startY < y) {
723                         top = startY;
724                         bottom = y;
725                     } else {
726                         top = y;
727                         bottom = startY;
728                     }
729 
730                     // Invalidate between the pattern's last cell and the current location
731                     invalidateRect.set((int) (left - radius), (int) (top - radius),
732                             (int) (right + radius), (int) (bottom + radius));
733 
734                     if (startX < oldX) {
735                         left = startX;
736                         right = oldX;
737                     } else {
738                         left = oldX;
739                         right = startX;
740                     }
741 
742                     if (startY < oldY) {
743                         top = startY;
744                         bottom = oldY;
745                     } else {
746                         top = oldY;
747                         bottom = startY;
748                     }
749 
750                     // Invalidate between the pattern's last cell and the previous location
751                     invalidateRect.union((int) (left - radius), (int) (top - radius),
752                             (int) (right + radius), (int) (bottom + radius));
753 
754                     // Invalidate between the pattern's new cell and the pattern's previous cell
755                     if (hitCell != null) {
756                         startX = getCenterXForColumn(hitCell.column);
757                         startY = getCenterYForRow(hitCell.row);
758 
759                         if (patternSize >= 2) {
760                             // (re-using hitcell for old cell)
761                             hitCell = pattern.get(patternSize - 1 - (patternSize - patternSizePreHitDetect));
762                             oldX = getCenterXForColumn(hitCell.column);
763                             oldY = getCenterYForRow(hitCell.row);
764 
765                             if (startX < oldX) {
766                                 left = startX;
767                                 right = oldX;
768                             } else {
769                                 left = oldX;
770                                 right = startX;
771                             }
772 
773                             if (startY < oldY) {
774                                 top = startY;
775                                 bottom = oldY;
776                             } else {
777                                 top = oldY;
778                                 bottom = startY;
779                             }
780                         } else {
781                             left = right = startX;
782                             top = bottom = startY;
783                         }
784 
785                         final float widthOffset = mSquareWidth / 2f;
786                         final float heightOffset = mSquareHeight / 2f;
787 
788                         invalidateRect.set((int) (left - widthOffset),
789                                 (int) (top - heightOffset), (int) (right + widthOffset),
790                                 (int) (bottom + heightOffset));
791                     }
792 
793                     invalidate(invalidateRect);
794                 } else {
795                     invalidate();
796                 }
797             }
798         }
799     }
800 
801     private void sendAccessEvent(int resId) {
802         setContentDescription(mContext.getString(resId));
803         sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_SELECTED);
804         setContentDescription(null);
805     }
806 
807     private void handleActionUp(MotionEvent event) {
808         // report pattern detected
809         if (!mPattern.isEmpty()) {
810             mPatternInProgress = false;
811             notifyPatternDetected();
812             invalidate();
813         }
814         if (PROFILE_DRAWING) {
815             if (mDrawingProfilingStarted) {
816                 Debug.stopMethodTracing();
817                 mDrawingProfilingStarted = false;
818             }
819         }
820     }
821 
822     private void handleActionDown(MotionEvent event) {
823         resetPattern();
824         final float x = event.getX();
825         final float y = event.getY();
826         final Cell hitCell = detectAndAddHit(x, y);
827         if (hitCell != null) {
828             mPatternInProgress = true;
829             mPatternDisplayMode = DisplayMode.Correct;
830             notifyPatternStarted();
831         } else {
832             mPatternInProgress = false;
833             notifyPatternCleared();
834         }
835         if (hitCell != null) {
836             final float startX = getCenterXForColumn(hitCell.column);
837             final float startY = getCenterYForRow(hitCell.row);
838 
839             final float widthOffset = mSquareWidth / 2f;
840             final float heightOffset = mSquareHeight / 2f;
841 
842             invalidate((int) (startX - widthOffset), (int) (startY - heightOffset),
843                     (int) (startX + widthOffset), (int) (startY + heightOffset));
844         }
845         mInProgressX = x;
846         mInProgressY = y;
847         if (PROFILE_DRAWING) {
848             if (!mDrawingProfilingStarted) {
849                 Debug.startMethodTracing("LockPatternDrawing");
850                 mDrawingProfilingStarted = true;
851             }
852         }
853     }
854 
855     private float getCenterXForColumn(int column) {
856         return mPaddingLeft + column * mSquareWidth + mSquareWidth / 2f;
857     }
858 
859     private float getCenterYForRow(int row) {
860         return mPaddingTop + row * mSquareHeight + mSquareHeight / 2f;
861     }
862 
863     @Override
864     protected void onDraw(Canvas canvas) {
865         final ArrayList<Cell> pattern = mPattern;
866         final int count = pattern.size();
867         final boolean[][] drawLookup = mPatternDrawLookup;
868 
869         if (mPatternDisplayMode == DisplayMode.Animate) {
870 
871             // figure out which circles to draw
872 
873             // + 1 so we pause on complete pattern
874             final int oneCycle = (count + 1) * MILLIS_PER_CIRCLE_ANIMATING;
875             final int spotInCycle = (int) (SystemClock.elapsedRealtime() -
876                     mAnimatingPeriodStart) % oneCycle;
877             final int numCircles = spotInCycle / MILLIS_PER_CIRCLE_ANIMATING;
878 
879             clearPatternDrawLookup();
880             for (int i = 0; i < numCircles; i++) {
881                 final Cell cell = pattern.get(i);
882                 drawLookup[cell.getRow()][cell.getColumn()] = true;
883             }
884 
885             // figure out in progress portion of ghosting line
886 
887             final boolean needToUpdateInProgressPoint = numCircles > 0
888                     && numCircles < count;
889 
890             if (needToUpdateInProgressPoint) {
891                 final float percentageOfNextCircle =
892                         ((float) (spotInCycle % MILLIS_PER_CIRCLE_ANIMATING)) /
893                                 MILLIS_PER_CIRCLE_ANIMATING;
894 
895                 final Cell currentCell = pattern.get(numCircles - 1);
896                 final float centerX = getCenterXForColumn(currentCell.column);
897                 final float centerY = getCenterYForRow(currentCell.row);
898 
899                 final Cell nextCell = pattern.get(numCircles);
900                 final float dx = percentageOfNextCircle *
901                         (getCenterXForColumn(nextCell.column) - centerX);
902                 final float dy = percentageOfNextCircle *
903                         (getCenterYForRow(nextCell.row) - centerY);
904                 mInProgressX = centerX + dx;
905                 mInProgressY = centerY + dy;
906             }
907             // TODO: Infinite loop here...
908             invalidate();
909         }
910 
911         final float squareWidth = mSquareWidth;
912         final float squareHeight = mSquareHeight;
913 
914         float radius = (squareWidth * mDiameterFactor * 0.5f);
915         mPathPaint.setStrokeWidth(radius);
916 
917         final Path currentPath = mCurrentPath;
918         currentPath.rewind();
919 
920         // draw the circles
921         final int paddingTop = mPaddingTop;
922         final int paddingLeft = mPaddingLeft;
923 
924         for (int i = 0; i < 3; i++) {
925             float topY = paddingTop + i * squareHeight;
926             //float centerY = mPaddingTop + i * mSquareHeight + (mSquareHeight / 2);
927             for (int j = 0; j < 3; j++) {
928                 float leftX = paddingLeft + j * squareWidth;
929                 drawCircle(canvas, (int) leftX, (int) topY, drawLookup[i][j]);
930             }
931         }
932 
933         // TODO: the path should be created and cached every time we hit-detect a cell
934         // only the last segment of the path should be computed here
935         // draw the path of the pattern (unless the user is in progress, and
936         // we are in stealth mode)
937         final boolean drawPath = (!mInStealthMode || mPatternDisplayMode == DisplayMode.Wrong);
938 
939         // draw the arrows associated with the path (unless the user is in progress, and
940         // we are in stealth mode)
941         boolean oldFlag = (mPaint.getFlags() & Paint.FILTER_BITMAP_FLAG) != 0;
942         mPaint.setFilterBitmap(true); // draw with higher quality since we render with transforms
943         if (drawPath) {
944             for (int i = 0; i < count - 1; i++) {
945                 Cell cell = pattern.get(i);
946                 Cell next = pattern.get(i + 1);
947 
948                 // only draw the part of the pattern stored in
949                 // the lookup table (this is only different in the case
950                 // of animation).
951                 if (!drawLookup[next.row][next.column]) {
952                     break;
953                 }
954 
955                 float leftX = paddingLeft + cell.column * squareWidth;
956                 float topY = paddingTop + cell.row * squareHeight;
957 
958                 drawArrow(canvas, leftX, topY, cell, next);
959             }
960         }
961 
962         if (drawPath) {
963             boolean anyCircles = false;
964             for (int i = 0; i < count; i++) {
965                 Cell cell = pattern.get(i);
966 
967                 // only draw the part of the pattern stored in
968                 // the lookup table (this is only different in the case
969                 // of animation).
970                 if (!drawLookup[cell.row][cell.column]) {
971                     break;
972                 }
973                 anyCircles = true;
974 
975                 float centerX = getCenterXForColumn(cell.column);
976                 float centerY = getCenterYForRow(cell.row);
977                 if (i == 0) {
978                     currentPath.moveTo(centerX, centerY);
979                 } else {
980                     currentPath.lineTo(centerX, centerY);
981                 }
982             }
983 
984             // add last in progress section
985             if ((mPatternInProgress || mPatternDisplayMode == DisplayMode.Animate)
986                     && anyCircles) {
987                 currentPath.lineTo(mInProgressX, mInProgressY);
988             }
989             canvas.drawPath(currentPath, mPathPaint);
990         }
991 
992         mPaint.setFilterBitmap(oldFlag); // restore default flag
993     }
994 
995     private void drawArrow(Canvas canvas, float leftX, float topY, Cell start, Cell end) {
996         boolean green = mPatternDisplayMode != DisplayMode.Wrong;
997 
998         final int endRow = end.row;
999         final int startRow = start.row;
1000         final int endColumn = end.column;
1001         final int startColumn = start.column;
1002 
1003         // offsets for centering the bitmap in the cell
1004         final int offsetX = ((int) mSquareWidth - mBitmapWidth) / 2;
1005         final int offsetY = ((int) mSquareHeight - mBitmapHeight) / 2;
1006 
1007         // compute transform to place arrow bitmaps at correct angle inside circle.
1008         // This assumes that the arrow image is drawn at 12:00 with it's top edge
1009         // coincident with the circle bitmap's top edge.
1010         Bitmap arrow = green ? mBitmapArrowGreenUp : mBitmapArrowRedUp;
1011         final int cellWidth = mBitmapWidth;
1012         final int cellHeight = mBitmapHeight;
1013 
1014         // the up arrow bitmap is at 12:00, so find the rotation from x axis and add 90 degrees.
1015         final float theta = (float) Math.atan2(
1016                 (double) (endRow - startRow), (double) (endColumn - startColumn));
1017         final float angle = (float) Math.toDegrees(theta) + 90.0f;
1018 
1019         // compose matrix
1020         float sx = Math.min(mSquareWidth / mBitmapWidth, 1.0f);
1021         float sy = Math.min(mSquareHeight / mBitmapHeight, 1.0f);
1022         mArrowMatrix.setTranslate(leftX + offsetX, topY + offsetY); // transform to cell position
1023         mArrowMatrix.preTranslate(mBitmapWidth/2, mBitmapHeight/2);
1024         mArrowMatrix.preScale(sx, sy);
1025         mArrowMatrix.preTranslate(-mBitmapWidth/2, -mBitmapHeight/2);
1026         mArrowMatrix.preRotate(angle, cellWidth / 2.0f, cellHeight / 2.0f);  // rotate about cell center
1027         mArrowMatrix.preTranslate((cellWidth - arrow.getWidth()) / 2.0f, 0.0f); // translate to 12:00 pos
1028         canvas.drawBitmap(arrow, mArrowMatrix, mPaint);
1029     }
1030 
1031     /**
1032      * @param canvas
1033      * @param leftX
1034      * @param topY
1035      * @param partOfPattern Whether this circle is part of the pattern.
1036      */
1037     private void drawCircle(Canvas canvas, int leftX, int topY, boolean partOfPattern) {
1038         Bitmap outerCircle;
1039         Bitmap innerCircle;
1040 
1041         if (!partOfPattern || (mInStealthMode && mPatternDisplayMode != DisplayMode.Wrong)) {
1042             // unselected circle
1043             outerCircle = mBitmapCircleDefault;
1044             innerCircle = mBitmapBtnDefault;
1045         } else if (mPatternInProgress) {
1046             // user is in middle of drawing a pattern
1047             outerCircle = mBitmapCircleGreen;
1048             innerCircle = mBitmapBtnTouched;
1049         } else if (mPatternDisplayMode == DisplayMode.Wrong) {
1050             // the pattern is wrong
1051             outerCircle = mBitmapCircleRed;
1052             innerCircle = mBitmapBtnDefault;
1053         } else if (mPatternDisplayMode == DisplayMode.Correct ||
1054                 mPatternDisplayMode == DisplayMode.Animate) {
1055             // the pattern is correct
1056             outerCircle = mBitmapCircleGreen;
1057             innerCircle = mBitmapBtnDefault;
1058         } else {
1059             throw new IllegalStateException("unknown display mode " + mPatternDisplayMode);
1060         }
1061 
1062         final int width = mBitmapWidth;
1063         final int height = mBitmapHeight;
1064 
1065         final float squareWidth = mSquareWidth;
1066         final float squareHeight = mSquareHeight;
1067 
1068         int offsetX = (int) ((squareWidth - width) / 2f);
1069         int offsetY = (int) ((squareHeight - height) / 2f);
1070 
1071         // Allow circles to shrink if the view is too small to hold them.
1072         float sx = Math.min(mSquareWidth / mBitmapWidth, 1.0f);
1073         float sy = Math.min(mSquareHeight / mBitmapHeight, 1.0f);
1074 
1075         mCircleMatrix.setTranslate(leftX + offsetX, topY + offsetY);
1076         mCircleMatrix.preTranslate(mBitmapWidth/2, mBitmapHeight/2);
1077         mCircleMatrix.preScale(sx, sy);
1078         mCircleMatrix.preTranslate(-mBitmapWidth/2, -mBitmapHeight/2);
1079 
1080         canvas.drawBitmap(outerCircle, mCircleMatrix, mPaint);
1081         canvas.drawBitmap(innerCircle, mCircleMatrix, mPaint);
1082     }
1083 
1084     @Override
1085     protected Parcelable onSaveInstanceState() {
1086         Parcelable superState = super.onSaveInstanceState();
1087         return new SavedState(superState,
1088                 LockPatternUtils.patternToString(mPattern),
1089                 mPatternDisplayMode.ordinal(),
1090                 mInputEnabled, mInStealthMode, mEnableHapticFeedback);
1091     }
1092 
1093     @Override
1094     protected void onRestoreInstanceState(Parcelable state) {
1095         final SavedState ss = (SavedState) state;
1096         super.onRestoreInstanceState(ss.getSuperState());
1097         setPattern(
1098                 DisplayMode.Correct,
1099                 LockPatternUtils.stringToPattern(ss.getSerializedPattern()));
1100         mPatternDisplayMode = DisplayMode.values()[ss.getDisplayMode()];
1101         mInputEnabled = ss.isInputEnabled();
1102         mInStealthMode = ss.isInStealthMode();
1103         mEnableHapticFeedback = ss.isTactileFeedbackEnabled();
1104     }
1105 
1106     /**
1107      * The parecelable for saving and restoring a lock pattern view.
1108      */
1109     private static class SavedState extends BaseSavedState {
1110 
1111         private final String mSerializedPattern;
1112         private final int mDisplayMode;
1113         private final boolean mInputEnabled;
1114         private final boolean mInStealthMode;
1115         private final boolean mTactileFeedbackEnabled;
1116 
1117         /**
1118          * Constructor called from {@link LockPatternView#onSaveInstanceState()}
1119          */
1120         private SavedState(Parcelable superState, String serializedPattern, int displayMode,
1121                 boolean inputEnabled, boolean inStealthMode, boolean tactileFeedbackEnabled) {
1122             super(superState);
1123             mSerializedPattern = serializedPattern;
1124             mDisplayMode = displayMode;
1125             mInputEnabled = inputEnabled;
1126             mInStealthMode = inStealthMode;
1127             mTactileFeedbackEnabled = tactileFeedbackEnabled;
1128         }
1129 
1130         /**
1131          * Constructor called from {@link #CREATOR}
1132          */
1133         private SavedState(Parcel in) {
1134             super(in);
1135             mSerializedPattern = in.readString();
1136             mDisplayMode = in.readInt();
1137             mInputEnabled = (Boolean) in.readValue(null);
1138             mInStealthMode = (Boolean) in.readValue(null);
1139             mTactileFeedbackEnabled = (Boolean) in.readValue(null);
1140         }
1141 
1142         public String getSerializedPattern() {
1143             return mSerializedPattern;
1144         }
1145 
1146         public int getDisplayMode() {
1147             return mDisplayMode;
1148         }
1149 
1150         public boolean isInputEnabled() {
1151             return mInputEnabled;
1152         }
1153 
1154         public boolean isInStealthMode() {
1155             return mInStealthMode;
1156         }
1157 
1158         public boolean isTactileFeedbackEnabled(){
1159             return mTactileFeedbackEnabled;
1160         }
1161 
1162         @Override
1163         public void writeToParcel(Parcel dest, int flags) {
1164             super.writeToParcel(dest, flags);
1165             dest.writeString(mSerializedPattern);
1166             dest.writeInt(mDisplayMode);
1167             dest.writeValue(mInputEnabled);
1168             dest.writeValue(mInStealthMode);
1169             dest.writeValue(mTactileFeedbackEnabled);
1170         }
1171 
1172         public static final Parcelable.Creator<SavedState> CREATOR =
1173                 new Creator<SavedState>() {
1174                     public SavedState createFromParcel(Parcel in) {
1175                         return new SavedState(in);
1176                     }
1177 
1178                     public SavedState[] newArray(int size) {
1179                         return new SavedState[size];
1180                     }
1181                 };
1182     }
1183 }
1184