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