LockPatternView.java revision aef555bcf26e770e37f2065913084588fb92c6fb
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 android.content.Context;
21import android.content.res.Resources;
22import android.content.res.TypedArray;
23import android.graphics.Bitmap;
24import android.graphics.BitmapFactory;
25import android.graphics.Canvas;
26import android.graphics.Color;
27import android.graphics.Matrix;
28import android.graphics.Paint;
29import android.graphics.Path;
30import android.graphics.Rect;
31import android.os.Debug;
32import android.os.Parcel;
33import android.os.Parcelable;
34import android.os.SystemClock;
35import android.util.AttributeSet;
36import android.util.Log;
37import android.view.HapticFeedbackConstants;
38import android.view.MotionEvent;
39import android.view.View;
40import android.view.accessibility.AccessibilityEvent;
41import android.view.accessibility.AccessibilityManager;
42
43import com.android.internal.R;
44
45import java.util.ArrayList;
46import 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 */
55public 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         */
153        private Cell(int row, int column) {
154            checkRange(row, column);
155            this.row = row;
156            this.column = column;
157        }
158
159        public int getRow() {
160            return row;
161        }
162
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         */
171        public static synchronized Cell of(int row, int column) {
172            checkRange(row, column);
173            return sCells[row][column];
174        }
175
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
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         */
219        void onPatternStart();
220
221        /**
222         * The pattern was cleared.
223         */
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         */
230        void onPatternCellAdded(List<Cell> pattern);
231
232        /**
233         * A pattern was detected from the user.
234         * @param pattern The pattern.
235         */
236        void onPatternDetected(List<Cell> pattern);
237    }
238
239    public LockPatternView(Context context) {
240        this(context, null);
241    }
242
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
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     */
298    public boolean isInStealthMode() {
299        return mInStealthMode;
300    }
301
302    /**
303     * @return Whether the view has tactile feedback enabled.
304     */
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     */
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     */
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     */
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     */
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     */
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
377    private void notifyCellAdded() {
378        if (mOnPatternListener != null) {
379            mOnPatternListener.onPatternCellAdded(mPattern);
380        }
381        sendAccessEvent(R.string.lockscreen_access_pattern_cell_added);
382    }
383
384    private void notifyPatternStarted() {
385        if (mOnPatternListener != null) {
386            mOnPatternListener.onPatternStart();
387        }
388        sendAccessEvent(R.string.lockscreen_access_pattern_start);
389    }
390
391    private void notifyPatternDetected() {
392        if (mOnPatternListener != null) {
393            mOnPatternListener.onPatternDetected(mPattern);
394        }
395        sendAccessEvent(R.string.lockscreen_access_pattern_detected);
396    }
397
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     */
408    public void clearPattern() {
409        resetPattern();
410    }
411
412    /**
413     * Reset all pattern state.
414     */
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     */
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     */
437    public void disableInput() {
438        mInputEnabled = false;
439    }
440
441    /**
442     * Enable input.
443     */
444    public void enableInput() {
445        mInputEnabled = true;
446    }
447
448    @Override
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
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
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
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
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     */
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
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
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     */
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     */
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
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
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
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