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