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