LockPatternView.java revision 240a295f12a04e888b09f1d815fbd72cffbef974
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
19import android.animation.Animator;
20import android.animation.AnimatorListenerAdapter;
21import android.animation.ValueAnimator;
22import android.content.Context;
23import android.content.res.Resources;
24import android.content.res.TypedArray;
25import android.graphics.Canvas;
26import android.graphics.Paint;
27import android.graphics.Path;
28import android.graphics.Rect;
29import android.graphics.RectF;
30import android.media.AudioManager;
31import android.os.Bundle;
32import android.os.Debug;
33import android.os.Parcel;
34import android.os.Parcelable;
35import android.os.SystemClock;
36import android.os.UserHandle;
37import android.provider.Settings;
38import android.util.AttributeSet;
39import android.util.IntArray;
40import android.util.Log;
41import android.view.HapticFeedbackConstants;
42import android.view.MotionEvent;
43import android.view.View;
44import android.view.accessibility.AccessibilityEvent;
45import android.view.accessibility.AccessibilityManager;
46import android.view.accessibility.AccessibilityNodeInfo;
47import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction;
48import android.view.animation.AnimationUtils;
49import android.view.animation.Interpolator;
50
51import com.android.internal.R;
52
53import java.util.ArrayList;
54import java.util.HashMap;
55import java.util.List;
56
57/**
58 * Displays and detects the user's unlock attempt, which is a drag of a finger
59 * across 9 regions of the screen.
60 *
61 * Is also capable of displaying a static pattern in "in progress", "wrong" or
62 * "correct" states.
63 */
64public class LockPatternView extends View {
65    // Aspect to use when rendering this view
66    private static final int ASPECT_SQUARE = 0; // View will be the minimum of width/height
67    private static final int ASPECT_LOCK_WIDTH = 1; // Fixed width; height will be minimum of (w,h)
68    private static final int ASPECT_LOCK_HEIGHT = 2; // Fixed height; width will be minimum of (w,h)
69
70    private static final boolean PROFILE_DRAWING = false;
71    private final CellState[][] mCellStates;
72
73    private final int mDotSize;
74    private final int mDotSizeActivated;
75    private final int mPathWidth;
76
77    private boolean mDrawingProfilingStarted = false;
78
79    private final Paint mPaint = new Paint();
80    private final Paint mPathPaint = new Paint();
81
82    /**
83     * How many milliseconds we spend animating each circle of a lock pattern
84     * if the animating mode is set.  The entire animation should take this
85     * constant * the length of the pattern to complete.
86     */
87    private static final int MILLIS_PER_CIRCLE_ANIMATING = 700;
88
89    /**
90     * This can be used to avoid updating the display for very small motions or noisy panels.
91     * It didn't seem to have much impact on the devices tested, so currently set to 0.
92     */
93    private static final float DRAG_THRESHHOLD = 0.0f;
94    public static final int VIRTUAL_BASE_VIEW_ID = 1;
95    public static final boolean DEBUG_A11Y = true;
96    private static final String TAG = "LockPatternView";
97
98    private OnPatternListener mOnPatternListener;
99    private final ArrayList<Cell> mPattern = new ArrayList<Cell>(9);
100
101    /**
102     * Lookup table for the circles of the pattern we are currently drawing.
103     * This will be the cells of the complete pattern unless we are animating,
104     * in which case we use this to hold the cells we are drawing for the in
105     * progress animation.
106     */
107    private final boolean[][] mPatternDrawLookup = new boolean[3][3];
108
109    /**
110     * the in progress point:
111     * - during interaction: where the user's finger is
112     * - during animation: the current tip of the animating line
113     */
114    private float mInProgressX = -1;
115    private float mInProgressY = -1;
116
117    private long mAnimatingPeriodStart;
118
119    private DisplayMode mPatternDisplayMode = DisplayMode.Correct;
120    private boolean mInputEnabled = true;
121    private boolean mInStealthMode = false;
122    private boolean mEnableHapticFeedback = true;
123    private boolean mPatternInProgress = false;
124
125    private float mHitFactor = 0.6f;
126
127    private float mSquareWidth;
128    private float mSquareHeight;
129
130    private final Path mCurrentPath = new Path();
131    private final Rect mInvalidate = new Rect();
132    private final Rect mTmpInvalidateRect = new Rect();
133
134    private int mAspect;
135    private int mRegularColor;
136    private int mErrorColor;
137    private int mSuccessColor;
138
139    private final Interpolator mFastOutSlowInInterpolator;
140    private final Interpolator mLinearOutSlowInInterpolator;
141    private PatternExploreByTouchHelper mExploreByTouchHelper;
142    private AudioManager mAudioManager;
143
144    /**
145     * Represents a cell in the 3 X 3 matrix of the unlock pattern view.
146     */
147    public static final class Cell {
148        final int row;
149        final int column;
150
151        // keep # objects limited to 9
152        private static final Cell[][] sCells = createCells();
153
154        private static Cell[][] createCells() {
155            Cell[][] res = new Cell[3][3];
156            for (int i = 0; i < 3; i++) {
157                for (int j = 0; j < 3; j++) {
158                    res[i][j] = new Cell(i, j);
159                }
160            }
161            return res;
162        }
163
164        /**
165         * @param row The row of the cell.
166         * @param column The column of the cell.
167         */
168        private Cell(int row, int column) {
169            checkRange(row, column);
170            this.row = row;
171            this.column = column;
172        }
173
174        public int getRow() {
175            return row;
176        }
177
178        public int getColumn() {
179            return column;
180        }
181
182        public static Cell of(int row, int column) {
183            checkRange(row, column);
184            return sCells[row][column];
185        }
186
187        private static void checkRange(int row, int column) {
188            if (row < 0 || row > 2) {
189                throw new IllegalArgumentException("row must be in range 0-2");
190            }
191            if (column < 0 || column > 2) {
192                throw new IllegalArgumentException("column must be in range 0-2");
193            }
194        }
195
196        @Override
197        public String toString() {
198            return "(row=" + row + ",clmn=" + column + ")";
199        }
200    }
201
202    public static class CellState {
203        public float scale = 1.0f;
204        public float translateY = 0.0f;
205        public float alpha = 1.0f;
206        public float size;
207        public float lineEndX = Float.MIN_VALUE;
208        public float lineEndY = Float.MIN_VALUE;
209        public ValueAnimator lineAnimator;
210     }
211
212    /**
213     * How to display the current pattern.
214     */
215    public enum DisplayMode {
216
217        /**
218         * The pattern drawn is correct (i.e draw it in a friendly color)
219         */
220        Correct,
221
222        /**
223         * Animate the pattern (for demo, and help).
224         */
225        Animate,
226
227        /**
228         * The pattern is wrong (i.e draw a foreboding color)
229         */
230        Wrong
231    }
232
233    /**
234     * The call back interface for detecting patterns entered by the user.
235     */
236    public static interface OnPatternListener {
237
238        /**
239         * A new pattern has begun.
240         */
241        void onPatternStart();
242
243        /**
244         * The pattern was cleared.
245         */
246        void onPatternCleared();
247
248        /**
249         * The user extended the pattern currently being drawn by one cell.
250         * @param pattern The pattern with newly added cell.
251         */
252        void onPatternCellAdded(List<Cell> pattern);
253
254        /**
255         * A pattern was detected from the user.
256         * @param pattern The pattern.
257         */
258        void onPatternDetected(List<Cell> pattern);
259    }
260
261    public LockPatternView(Context context) {
262        this(context, null);
263    }
264
265    public LockPatternView(Context context, AttributeSet attrs) {
266        super(context, attrs);
267
268        TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.LockPatternView);
269
270        final String aspect = a.getString(R.styleable.LockPatternView_aspect);
271
272        if ("square".equals(aspect)) {
273            mAspect = ASPECT_SQUARE;
274        } else if ("lock_width".equals(aspect)) {
275            mAspect = ASPECT_LOCK_WIDTH;
276        } else if ("lock_height".equals(aspect)) {
277            mAspect = ASPECT_LOCK_HEIGHT;
278        } else {
279            mAspect = ASPECT_SQUARE;
280        }
281
282        setClickable(true);
283
284
285        mPathPaint.setAntiAlias(true);
286        mPathPaint.setDither(true);
287
288        mRegularColor = context.getColor(R.color.lock_pattern_view_regular_color);
289        mErrorColor = context.getColor(R.color.lock_pattern_view_error_color);
290        mSuccessColor = context.getColor(R.color.lock_pattern_view_success_color);
291        mRegularColor = a.getColor(R.styleable.LockPatternView_regularColor, mRegularColor);
292        mErrorColor = a.getColor(R.styleable.LockPatternView_errorColor, mErrorColor);
293        mSuccessColor = a.getColor(R.styleable.LockPatternView_successColor, mSuccessColor);
294
295        int pathColor = a.getColor(R.styleable.LockPatternView_pathColor, mRegularColor);
296        mPathPaint.setColor(pathColor);
297
298        mPathPaint.setStyle(Paint.Style.STROKE);
299        mPathPaint.setStrokeJoin(Paint.Join.ROUND);
300        mPathPaint.setStrokeCap(Paint.Cap.ROUND);
301
302        mPathWidth = getResources().getDimensionPixelSize(R.dimen.lock_pattern_dot_line_width);
303        mPathPaint.setStrokeWidth(mPathWidth);
304
305        mDotSize = getResources().getDimensionPixelSize(R.dimen.lock_pattern_dot_size);
306        mDotSizeActivated = getResources().getDimensionPixelSize(
307                R.dimen.lock_pattern_dot_size_activated);
308
309        mPaint.setAntiAlias(true);
310        mPaint.setDither(true);
311
312        mCellStates = new CellState[3][3];
313        for (int i = 0; i < 3; i++) {
314            for (int j = 0; j < 3; j++) {
315                mCellStates[i][j] = new CellState();
316                mCellStates[i][j].size = mDotSize;
317            }
318        }
319
320        mFastOutSlowInInterpolator =
321                AnimationUtils.loadInterpolator(context, android.R.interpolator.fast_out_slow_in);
322        mLinearOutSlowInInterpolator =
323                AnimationUtils.loadInterpolator(context, android.R.interpolator.linear_out_slow_in);
324        mExploreByTouchHelper = new PatternExploreByTouchHelper(this);
325        setAccessibilityDelegate(mExploreByTouchHelper);
326        mAudioManager = (AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE);
327    }
328
329    public CellState[][] getCellStates() {
330        return mCellStates;
331    }
332
333    /**
334     * @return Whether the view is in stealth mode.
335     */
336    public boolean isInStealthMode() {
337        return mInStealthMode;
338    }
339
340    /**
341     * @return Whether the view has tactile feedback enabled.
342     */
343    public boolean isTactileFeedbackEnabled() {
344        return mEnableHapticFeedback;
345    }
346
347    /**
348     * Set whether the view is in stealth mode.  If true, there will be no
349     * visible feedback as the user enters the pattern.
350     *
351     * @param inStealthMode Whether in stealth mode.
352     */
353    public void setInStealthMode(boolean inStealthMode) {
354        mInStealthMode = inStealthMode;
355    }
356
357    /**
358     * Set whether the view will use tactile feedback.  If true, there will be
359     * tactile feedback as the user enters the pattern.
360     *
361     * @param tactileFeedbackEnabled Whether tactile feedback is enabled
362     */
363    public void setTactileFeedbackEnabled(boolean tactileFeedbackEnabled) {
364        mEnableHapticFeedback = tactileFeedbackEnabled;
365    }
366
367    /**
368     * Set the call back for pattern detection.
369     * @param onPatternListener The call back.
370     */
371    public void setOnPatternListener(
372            OnPatternListener onPatternListener) {
373        mOnPatternListener = onPatternListener;
374    }
375
376    /**
377     * Set the pattern explicitely (rather than waiting for the user to input
378     * a pattern).
379     * @param displayMode How to display the pattern.
380     * @param pattern The pattern.
381     */
382    public void setPattern(DisplayMode displayMode, List<Cell> pattern) {
383        mPattern.clear();
384        mPattern.addAll(pattern);
385        clearPatternDrawLookup();
386        for (Cell cell : pattern) {
387            mPatternDrawLookup[cell.getRow()][cell.getColumn()] = true;
388        }
389
390        setDisplayMode(displayMode);
391    }
392
393    /**
394     * Set the display mode of the current pattern.  This can be useful, for
395     * instance, after detecting a pattern to tell this view whether change the
396     * in progress result to correct or wrong.
397     * @param displayMode The display mode.
398     */
399    public void setDisplayMode(DisplayMode displayMode) {
400        mPatternDisplayMode = displayMode;
401        if (displayMode == DisplayMode.Animate) {
402            if (mPattern.size() == 0) {
403                throw new IllegalStateException("you must have a pattern to "
404                        + "animate if you want to set the display mode to animate");
405            }
406            mAnimatingPeriodStart = SystemClock.elapsedRealtime();
407            final Cell first = mPattern.get(0);
408            mInProgressX = getCenterXForColumn(first.getColumn());
409            mInProgressY = getCenterYForRow(first.getRow());
410            clearPatternDrawLookup();
411        }
412        invalidate();
413    }
414
415    private void notifyCellAdded() {
416        // sendAccessEvent(R.string.lockscreen_access_pattern_cell_added);
417        if (mOnPatternListener != null) {
418            mOnPatternListener.onPatternCellAdded(mPattern);
419        }
420        // Disable used cells for accessibility as they get added
421        if (DEBUG_A11Y) Log.v(TAG, "ivnalidating root because cell was added.");
422        mExploreByTouchHelper.invalidateRoot();
423    }
424
425    private void notifyPatternStarted() {
426        sendAccessEvent(R.string.lockscreen_access_pattern_start);
427        if (mOnPatternListener != null) {
428            mOnPatternListener.onPatternStart();
429        }
430    }
431
432    private void notifyPatternDetected() {
433        sendAccessEvent(R.string.lockscreen_access_pattern_detected);
434        if (mOnPatternListener != null) {
435            mOnPatternListener.onPatternDetected(mPattern);
436        }
437    }
438
439    private void notifyPatternCleared() {
440        sendAccessEvent(R.string.lockscreen_access_pattern_cleared);
441        if (mOnPatternListener != null) {
442            mOnPatternListener.onPatternCleared();
443        }
444    }
445
446    /**
447     * Clear the pattern.
448     */
449    public void clearPattern() {
450        resetPattern();
451    }
452
453    @Override
454    protected boolean dispatchHoverEvent(MotionEvent event) {
455        // Give TouchHelper first right of refusal
456        boolean handled = mExploreByTouchHelper.dispatchHoverEvent(event);
457        return super.dispatchHoverEvent(event) || handled;
458    }
459
460    /**
461     * Reset all pattern state.
462     */
463    private void resetPattern() {
464        mPattern.clear();
465        clearPatternDrawLookup();
466        mPatternDisplayMode = DisplayMode.Correct;
467        invalidate();
468    }
469
470    /**
471     * Clear the pattern lookup table.
472     */
473    private void clearPatternDrawLookup() {
474        for (int i = 0; i < 3; i++) {
475            for (int j = 0; j < 3; j++) {
476                mPatternDrawLookup[i][j] = false;
477            }
478        }
479    }
480
481    /**
482     * Disable input (for instance when displaying a message that will
483     * timeout so user doesn't get view into messy state).
484     */
485    public void disableInput() {
486        mInputEnabled = false;
487    }
488
489    /**
490     * Enable input.
491     */
492    public void enableInput() {
493        mInputEnabled = true;
494    }
495
496    @Override
497    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
498        final int width = w - mPaddingLeft - mPaddingRight;
499        mSquareWidth = width / 3.0f;
500
501        if (DEBUG_A11Y) Log.v(TAG, "onSizeChanged(" + w + "," + h + ")");
502        final int height = h - mPaddingTop - mPaddingBottom;
503        mSquareHeight = height / 3.0f;
504        mExploreByTouchHelper.invalidateRoot();
505    }
506
507    private int resolveMeasured(int measureSpec, int desired)
508    {
509        int result = 0;
510        int specSize = MeasureSpec.getSize(measureSpec);
511        switch (MeasureSpec.getMode(measureSpec)) {
512            case MeasureSpec.UNSPECIFIED:
513                result = desired;
514                break;
515            case MeasureSpec.AT_MOST:
516                result = Math.max(specSize, desired);
517                break;
518            case MeasureSpec.EXACTLY:
519            default:
520                result = specSize;
521        }
522        return result;
523    }
524
525    @Override
526    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
527        final int minimumWidth = getSuggestedMinimumWidth();
528        final int minimumHeight = getSuggestedMinimumHeight();
529        int viewWidth = resolveMeasured(widthMeasureSpec, minimumWidth);
530        int viewHeight = resolveMeasured(heightMeasureSpec, minimumHeight);
531
532        switch (mAspect) {
533            case ASPECT_SQUARE:
534                viewWidth = viewHeight = Math.min(viewWidth, viewHeight);
535                break;
536            case ASPECT_LOCK_WIDTH:
537                viewHeight = Math.min(viewWidth, viewHeight);
538                break;
539            case ASPECT_LOCK_HEIGHT:
540                viewWidth = Math.min(viewWidth, viewHeight);
541                break;
542        }
543        // Log.v(TAG, "LockPatternView dimensions: " + viewWidth + "x" + viewHeight);
544        setMeasuredDimension(viewWidth, viewHeight);
545    }
546
547    /**
548     * Determines whether the point x, y will add a new point to the current
549     * pattern (in addition to finding the cell, also makes heuristic choices
550     * such as filling in gaps based on current pattern).
551     * @param x The x coordinate.
552     * @param y The y coordinate.
553     */
554    private Cell detectAndAddHit(float x, float y) {
555        final Cell cell = checkForNewHit(x, y);
556        if (cell != null) {
557
558            // check for gaps in existing pattern
559            Cell fillInGapCell = null;
560            final ArrayList<Cell> pattern = mPattern;
561            if (!pattern.isEmpty()) {
562                final Cell lastCell = pattern.get(pattern.size() - 1);
563                int dRow = cell.row - lastCell.row;
564                int dColumn = cell.column - lastCell.column;
565
566                int fillInRow = lastCell.row;
567                int fillInColumn = lastCell.column;
568
569                if (Math.abs(dRow) == 2 && Math.abs(dColumn) != 1) {
570                    fillInRow = lastCell.row + ((dRow > 0) ? 1 : -1);
571                }
572
573                if (Math.abs(dColumn) == 2 && Math.abs(dRow) != 1) {
574                    fillInColumn = lastCell.column + ((dColumn > 0) ? 1 : -1);
575                }
576
577                fillInGapCell = Cell.of(fillInRow, fillInColumn);
578            }
579
580            if (fillInGapCell != null &&
581                    !mPatternDrawLookup[fillInGapCell.row][fillInGapCell.column]) {
582                addCellToPattern(fillInGapCell);
583            }
584            addCellToPattern(cell);
585            if (mEnableHapticFeedback) {
586                performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY,
587                        HapticFeedbackConstants.FLAG_IGNORE_VIEW_SETTING
588                        | HapticFeedbackConstants.FLAG_IGNORE_GLOBAL_SETTING);
589            }
590            return cell;
591        }
592        return null;
593    }
594
595    private void addCellToPattern(Cell newCell) {
596        mPatternDrawLookup[newCell.getRow()][newCell.getColumn()] = true;
597        mPattern.add(newCell);
598        if (!mInStealthMode) {
599            startCellActivatedAnimation(newCell);
600        }
601        notifyCellAdded();
602    }
603
604    private void startCellActivatedAnimation(Cell cell) {
605        final CellState cellState = mCellStates[cell.row][cell.column];
606        startSizeAnimation(mDotSize, mDotSizeActivated, 96, mLinearOutSlowInInterpolator,
607                cellState, new Runnable() {
608            @Override
609            public void run() {
610                startSizeAnimation(mDotSizeActivated, mDotSize, 192, mFastOutSlowInInterpolator,
611                        cellState, null);
612            }
613        });
614        startLineEndAnimation(cellState, mInProgressX, mInProgressY,
615                getCenterXForColumn(cell.column), getCenterYForRow(cell.row));
616    }
617
618    private void startLineEndAnimation(final CellState state,
619            final float startX, final float startY, final float targetX, final float targetY) {
620        ValueAnimator valueAnimator = ValueAnimator.ofFloat(0, 1);
621        valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
622            @Override
623            public void onAnimationUpdate(ValueAnimator animation) {
624                float t = (float) animation.getAnimatedValue();
625                state.lineEndX = (1 - t) * startX + t * targetX;
626                state.lineEndY = (1 - t) * startY + t * targetY;
627                invalidate();
628            }
629        });
630        valueAnimator.addListener(new AnimatorListenerAdapter() {
631            @Override
632            public void onAnimationEnd(Animator animation) {
633                state.lineAnimator = null;
634            }
635        });
636        valueAnimator.setInterpolator(mFastOutSlowInInterpolator);
637        valueAnimator.setDuration(100);
638        valueAnimator.start();
639        state.lineAnimator = valueAnimator;
640    }
641
642    private void startSizeAnimation(float start, float end, long duration, Interpolator interpolator,
643            final CellState state, final Runnable endRunnable) {
644        ValueAnimator valueAnimator = ValueAnimator.ofFloat(start, end);
645        valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
646            @Override
647            public void onAnimationUpdate(ValueAnimator animation) {
648                state.size = (float) animation.getAnimatedValue();
649                invalidate();
650            }
651        });
652        if (endRunnable != null) {
653            valueAnimator.addListener(new AnimatorListenerAdapter() {
654                @Override
655                public void onAnimationEnd(Animator animation) {
656                    endRunnable.run();
657                }
658            });
659        }
660        valueAnimator.setInterpolator(interpolator);
661        valueAnimator.setDuration(duration);
662        valueAnimator.start();
663    }
664
665    // helper method to find which cell a point maps to
666    private Cell checkForNewHit(float x, float y) {
667
668        final int rowHit = getRowHit(y);
669        if (rowHit < 0) {
670            return null;
671        }
672        final int columnHit = getColumnHit(x);
673        if (columnHit < 0) {
674            return null;
675        }
676
677        if (mPatternDrawLookup[rowHit][columnHit]) {
678            return null;
679        }
680        return Cell.of(rowHit, columnHit);
681    }
682
683    /**
684     * Helper method to find the row that y falls into.
685     * @param y The y coordinate
686     * @return The row that y falls in, or -1 if it falls in no row.
687     */
688    private int getRowHit(float y) {
689
690        final float squareHeight = mSquareHeight;
691        float hitSize = squareHeight * mHitFactor;
692
693        float offset = mPaddingTop + (squareHeight - hitSize) / 2f;
694        for (int i = 0; i < 3; i++) {
695
696            final float hitTop = offset + squareHeight * i;
697            if (y >= hitTop && y <= hitTop + hitSize) {
698                return i;
699            }
700        }
701        return -1;
702    }
703
704    /**
705     * Helper method to find the column x fallis into.
706     * @param x The x coordinate.
707     * @return The column that x falls in, or -1 if it falls in no column.
708     */
709    private int getColumnHit(float x) {
710        final float squareWidth = mSquareWidth;
711        float hitSize = squareWidth * mHitFactor;
712
713        float offset = mPaddingLeft + (squareWidth - hitSize) / 2f;
714        for (int i = 0; i < 3; i++) {
715
716            final float hitLeft = offset + squareWidth * i;
717            if (x >= hitLeft && x <= hitLeft + hitSize) {
718                return i;
719            }
720        }
721        return -1;
722    }
723
724    @Override
725    public boolean onHoverEvent(MotionEvent event) {
726        if (AccessibilityManager.getInstance(mContext).isTouchExplorationEnabled()) {
727            final int action = event.getAction();
728            switch (action) {
729                case MotionEvent.ACTION_HOVER_ENTER:
730                    event.setAction(MotionEvent.ACTION_DOWN);
731                    break;
732                case MotionEvent.ACTION_HOVER_MOVE:
733                    event.setAction(MotionEvent.ACTION_MOVE);
734                    break;
735                case MotionEvent.ACTION_HOVER_EXIT:
736                    event.setAction(MotionEvent.ACTION_UP);
737                    break;
738            }
739            onTouchEvent(event);
740            event.setAction(action);
741        }
742        return super.onHoverEvent(event);
743    }
744
745    @Override
746    public boolean onTouchEvent(MotionEvent event) {
747        if (!mInputEnabled || !isEnabled()) {
748            return false;
749        }
750
751        switch(event.getAction()) {
752            case MotionEvent.ACTION_DOWN:
753                handleActionDown(event);
754                return true;
755            case MotionEvent.ACTION_UP:
756                handleActionUp();
757                return true;
758            case MotionEvent.ACTION_MOVE:
759                handleActionMove(event);
760                return true;
761            case MotionEvent.ACTION_CANCEL:
762                if (mPatternInProgress) {
763                    mPatternInProgress = false;
764                    resetPattern();
765                    notifyPatternCleared();
766                }
767                if (PROFILE_DRAWING) {
768                    if (mDrawingProfilingStarted) {
769                        Debug.stopMethodTracing();
770                        mDrawingProfilingStarted = false;
771                    }
772                }
773                return true;
774        }
775        return false;
776    }
777
778    private void handleActionMove(MotionEvent event) {
779        // Handle all recent motion events so we don't skip any cells even when the device
780        // is busy...
781        final float radius = mPathWidth;
782        final int historySize = event.getHistorySize();
783        mTmpInvalidateRect.setEmpty();
784        boolean invalidateNow = false;
785        for (int i = 0; i < historySize + 1; i++) {
786            final float x = i < historySize ? event.getHistoricalX(i) : event.getX();
787            final float y = i < historySize ? event.getHistoricalY(i) : event.getY();
788            Cell hitCell = detectAndAddHit(x, y);
789            final int patternSize = mPattern.size();
790            if (hitCell != null && patternSize == 1) {
791                mPatternInProgress = true;
792                notifyPatternStarted();
793            }
794            // note current x and y for rubber banding of in progress patterns
795            final float dx = Math.abs(x - mInProgressX);
796            final float dy = Math.abs(y - mInProgressY);
797            if (dx > DRAG_THRESHHOLD || dy > DRAG_THRESHHOLD) {
798                invalidateNow = true;
799            }
800
801            if (mPatternInProgress && patternSize > 0) {
802                final ArrayList<Cell> pattern = mPattern;
803                final Cell lastCell = pattern.get(patternSize - 1);
804                float lastCellCenterX = getCenterXForColumn(lastCell.column);
805                float lastCellCenterY = getCenterYForRow(lastCell.row);
806
807                // Adjust for drawn segment from last cell to (x,y). Radius accounts for line width.
808                float left = Math.min(lastCellCenterX, x) - radius;
809                float right = Math.max(lastCellCenterX, x) + radius;
810                float top = Math.min(lastCellCenterY, y) - radius;
811                float bottom = Math.max(lastCellCenterY, y) + radius;
812
813                // Invalidate between the pattern's new cell and the pattern's previous cell
814                if (hitCell != null) {
815                    final float width = mSquareWidth * 0.5f;
816                    final float height = mSquareHeight * 0.5f;
817                    final float hitCellCenterX = getCenterXForColumn(hitCell.column);
818                    final float hitCellCenterY = getCenterYForRow(hitCell.row);
819
820                    left = Math.min(hitCellCenterX - width, left);
821                    right = Math.max(hitCellCenterX + width, right);
822                    top = Math.min(hitCellCenterY - height, top);
823                    bottom = Math.max(hitCellCenterY + height, bottom);
824                }
825
826                // Invalidate between the pattern's last cell and the previous location
827                mTmpInvalidateRect.union(Math.round(left), Math.round(top),
828                        Math.round(right), Math.round(bottom));
829            }
830        }
831        mInProgressX = event.getX();
832        mInProgressY = event.getY();
833
834        // To save updates, we only invalidate if the user moved beyond a certain amount.
835        if (invalidateNow) {
836            mInvalidate.union(mTmpInvalidateRect);
837            invalidate(mInvalidate);
838            mInvalidate.set(mTmpInvalidateRect);
839        }
840    }
841
842    private void sendAccessEvent(int resId) {
843        announceForAccessibility(mContext.getString(resId));
844    }
845
846    private void handleActionUp() {
847        // report pattern detected
848        if (!mPattern.isEmpty()) {
849            mPatternInProgress = false;
850            cancelLineAnimations();
851            notifyPatternDetected();
852            invalidate();
853        }
854        if (PROFILE_DRAWING) {
855            if (mDrawingProfilingStarted) {
856                Debug.stopMethodTracing();
857                mDrawingProfilingStarted = false;
858            }
859        }
860    }
861
862    private void cancelLineAnimations() {
863        for (int i = 0; i < 3; i++) {
864            for (int j = 0; j < 3; j++) {
865                CellState state = mCellStates[i][j];
866                if (state.lineAnimator != null) {
867                    state.lineAnimator.cancel();
868                    state.lineEndX = Float.MIN_VALUE;
869                    state.lineEndY = Float.MIN_VALUE;
870                }
871            }
872        }
873    }
874    private void handleActionDown(MotionEvent event) {
875        resetPattern();
876        final float x = event.getX();
877        final float y = event.getY();
878        final Cell hitCell = detectAndAddHit(x, y);
879        if (hitCell != null) {
880            mPatternInProgress = true;
881            mPatternDisplayMode = DisplayMode.Correct;
882            notifyPatternStarted();
883        } else if (mPatternInProgress) {
884            mPatternInProgress = false;
885            notifyPatternCleared();
886        }
887        if (hitCell != null) {
888            final float startX = getCenterXForColumn(hitCell.column);
889            final float startY = getCenterYForRow(hitCell.row);
890
891            final float widthOffset = mSquareWidth / 2f;
892            final float heightOffset = mSquareHeight / 2f;
893
894            invalidate((int) (startX - widthOffset), (int) (startY - heightOffset),
895                    (int) (startX + widthOffset), (int) (startY + heightOffset));
896        }
897        mInProgressX = x;
898        mInProgressY = y;
899        if (PROFILE_DRAWING) {
900            if (!mDrawingProfilingStarted) {
901                Debug.startMethodTracing("LockPatternDrawing");
902                mDrawingProfilingStarted = true;
903            }
904        }
905    }
906
907    private float getCenterXForColumn(int column) {
908        return mPaddingLeft + column * mSquareWidth + mSquareWidth / 2f;
909    }
910
911    private float getCenterYForRow(int row) {
912        return mPaddingTop + row * mSquareHeight + mSquareHeight / 2f;
913    }
914
915    @Override
916    protected void onDraw(Canvas canvas) {
917        final ArrayList<Cell> pattern = mPattern;
918        final int count = pattern.size();
919        final boolean[][] drawLookup = mPatternDrawLookup;
920
921        if (mPatternDisplayMode == DisplayMode.Animate) {
922
923            // figure out which circles to draw
924
925            // + 1 so we pause on complete pattern
926            final int oneCycle = (count + 1) * MILLIS_PER_CIRCLE_ANIMATING;
927            final int spotInCycle = (int) (SystemClock.elapsedRealtime() -
928                    mAnimatingPeriodStart) % oneCycle;
929            final int numCircles = spotInCycle / MILLIS_PER_CIRCLE_ANIMATING;
930
931            clearPatternDrawLookup();
932            for (int i = 0; i < numCircles; i++) {
933                final Cell cell = pattern.get(i);
934                drawLookup[cell.getRow()][cell.getColumn()] = true;
935            }
936
937            // figure out in progress portion of ghosting line
938
939            final boolean needToUpdateInProgressPoint = numCircles > 0
940                    && numCircles < count;
941
942            if (needToUpdateInProgressPoint) {
943                final float percentageOfNextCircle =
944                        ((float) (spotInCycle % MILLIS_PER_CIRCLE_ANIMATING)) /
945                                MILLIS_PER_CIRCLE_ANIMATING;
946
947                final Cell currentCell = pattern.get(numCircles - 1);
948                final float centerX = getCenterXForColumn(currentCell.column);
949                final float centerY = getCenterYForRow(currentCell.row);
950
951                final Cell nextCell = pattern.get(numCircles);
952                final float dx = percentageOfNextCircle *
953                        (getCenterXForColumn(nextCell.column) - centerX);
954                final float dy = percentageOfNextCircle *
955                        (getCenterYForRow(nextCell.row) - centerY);
956                mInProgressX = centerX + dx;
957                mInProgressY = centerY + dy;
958            }
959            // TODO: Infinite loop here...
960            invalidate();
961        }
962
963        final Path currentPath = mCurrentPath;
964        currentPath.rewind();
965
966        // draw the circles
967        for (int i = 0; i < 3; i++) {
968            float centerY = getCenterYForRow(i);
969            for (int j = 0; j < 3; j++) {
970                CellState cellState = mCellStates[i][j];
971                float centerX = getCenterXForColumn(j);
972                float size = cellState.size * cellState.scale;
973                float translationY = cellState.translateY;
974                drawCircle(canvas, (int) centerX, (int) centerY + translationY,
975                        size, drawLookup[i][j], cellState.alpha);
976            }
977        }
978
979        // TODO: the path should be created and cached every time we hit-detect a cell
980        // only the last segment of the path should be computed here
981        // draw the path of the pattern (unless we are in stealth mode)
982        final boolean drawPath = !mInStealthMode;
983
984        if (drawPath) {
985            mPathPaint.setColor(getCurrentColor(true /* partOfPattern */));
986
987            boolean anyCircles = false;
988            float lastX = 0f;
989            float lastY = 0f;
990            for (int i = 0; i < count; i++) {
991                Cell cell = pattern.get(i);
992
993                // only draw the part of the pattern stored in
994                // the lookup table (this is only different in the case
995                // of animation).
996                if (!drawLookup[cell.row][cell.column]) {
997                    break;
998                }
999                anyCircles = true;
1000
1001                float centerX = getCenterXForColumn(cell.column);
1002                float centerY = getCenterYForRow(cell.row);
1003                if (i != 0) {
1004                    CellState state = mCellStates[cell.row][cell.column];
1005                    currentPath.rewind();
1006                    currentPath.moveTo(lastX, lastY);
1007                    if (state.lineEndX != Float.MIN_VALUE && state.lineEndY != Float.MIN_VALUE) {
1008                        currentPath.lineTo(state.lineEndX, state.lineEndY);
1009                    } else {
1010                        currentPath.lineTo(centerX, centerY);
1011                    }
1012                    canvas.drawPath(currentPath, mPathPaint);
1013                }
1014                lastX = centerX;
1015                lastY = centerY;
1016            }
1017
1018            // draw last in progress section
1019            if ((mPatternInProgress || mPatternDisplayMode == DisplayMode.Animate)
1020                    && anyCircles) {
1021                currentPath.rewind();
1022                currentPath.moveTo(lastX, lastY);
1023                currentPath.lineTo(mInProgressX, mInProgressY);
1024
1025                mPathPaint.setAlpha((int) (calculateLastSegmentAlpha(
1026                        mInProgressX, mInProgressY, lastX, lastY) * 255f));
1027                canvas.drawPath(currentPath, mPathPaint);
1028            }
1029        }
1030    }
1031
1032    private float calculateLastSegmentAlpha(float x, float y, float lastX, float lastY) {
1033        float diffX = x - lastX;
1034        float diffY = y - lastY;
1035        float dist = (float) Math.sqrt(diffX*diffX + diffY*diffY);
1036        float frac = dist/mSquareWidth;
1037        return Math.min(1f, Math.max(0f, (frac - 0.3f) * 4f));
1038    }
1039
1040    private int getCurrentColor(boolean partOfPattern) {
1041        if (!partOfPattern || mInStealthMode || mPatternInProgress) {
1042            // unselected circle
1043            return mRegularColor;
1044        } else if (mPatternDisplayMode == DisplayMode.Wrong) {
1045            // the pattern is wrong
1046            return mErrorColor;
1047        } else if (mPatternDisplayMode == DisplayMode.Correct ||
1048                mPatternDisplayMode == DisplayMode.Animate) {
1049            return mSuccessColor;
1050        } else {
1051            throw new IllegalStateException("unknown display mode " + mPatternDisplayMode);
1052        }
1053    }
1054
1055    /**
1056     * @param partOfPattern Whether this circle is part of the pattern.
1057     */
1058    private void drawCircle(Canvas canvas, float centerX, float centerY, float size,
1059            boolean partOfPattern, float alpha) {
1060        mPaint.setColor(getCurrentColor(partOfPattern));
1061        mPaint.setAlpha((int) (alpha * 255));
1062        canvas.drawCircle(centerX, centerY, size/2, mPaint);
1063    }
1064
1065    @Override
1066    protected Parcelable onSaveInstanceState() {
1067        Parcelable superState = super.onSaveInstanceState();
1068        return new SavedState(superState,
1069                LockPatternUtils.patternToString(mPattern),
1070                mPatternDisplayMode.ordinal(),
1071                mInputEnabled, mInStealthMode, mEnableHapticFeedback);
1072    }
1073
1074    @Override
1075    protected void onRestoreInstanceState(Parcelable state) {
1076        final SavedState ss = (SavedState) state;
1077        super.onRestoreInstanceState(ss.getSuperState());
1078        setPattern(
1079                DisplayMode.Correct,
1080                LockPatternUtils.stringToPattern(ss.getSerializedPattern()));
1081        mPatternDisplayMode = DisplayMode.values()[ss.getDisplayMode()];
1082        mInputEnabled = ss.isInputEnabled();
1083        mInStealthMode = ss.isInStealthMode();
1084        mEnableHapticFeedback = ss.isTactileFeedbackEnabled();
1085    }
1086
1087    /**
1088     * The parecelable for saving and restoring a lock pattern view.
1089     */
1090    private static class SavedState extends BaseSavedState {
1091
1092        private final String mSerializedPattern;
1093        private final int mDisplayMode;
1094        private final boolean mInputEnabled;
1095        private final boolean mInStealthMode;
1096        private final boolean mTactileFeedbackEnabled;
1097
1098        /**
1099         * Constructor called from {@link LockPatternView#onSaveInstanceState()}
1100         */
1101        private SavedState(Parcelable superState, String serializedPattern, int displayMode,
1102                boolean inputEnabled, boolean inStealthMode, boolean tactileFeedbackEnabled) {
1103            super(superState);
1104            mSerializedPattern = serializedPattern;
1105            mDisplayMode = displayMode;
1106            mInputEnabled = inputEnabled;
1107            mInStealthMode = inStealthMode;
1108            mTactileFeedbackEnabled = tactileFeedbackEnabled;
1109        }
1110
1111        /**
1112         * Constructor called from {@link #CREATOR}
1113         */
1114        private SavedState(Parcel in) {
1115            super(in);
1116            mSerializedPattern = in.readString();
1117            mDisplayMode = in.readInt();
1118            mInputEnabled = (Boolean) in.readValue(null);
1119            mInStealthMode = (Boolean) in.readValue(null);
1120            mTactileFeedbackEnabled = (Boolean) in.readValue(null);
1121        }
1122
1123        public String getSerializedPattern() {
1124            return mSerializedPattern;
1125        }
1126
1127        public int getDisplayMode() {
1128            return mDisplayMode;
1129        }
1130
1131        public boolean isInputEnabled() {
1132            return mInputEnabled;
1133        }
1134
1135        public boolean isInStealthMode() {
1136            return mInStealthMode;
1137        }
1138
1139        public boolean isTactileFeedbackEnabled(){
1140            return mTactileFeedbackEnabled;
1141        }
1142
1143        @Override
1144        public void writeToParcel(Parcel dest, int flags) {
1145            super.writeToParcel(dest, flags);
1146            dest.writeString(mSerializedPattern);
1147            dest.writeInt(mDisplayMode);
1148            dest.writeValue(mInputEnabled);
1149            dest.writeValue(mInStealthMode);
1150            dest.writeValue(mTactileFeedbackEnabled);
1151        }
1152
1153        @SuppressWarnings({ "unused", "hiding" }) // Found using reflection
1154        public static final Parcelable.Creator<SavedState> CREATOR =
1155                new Creator<SavedState>() {
1156            @Override
1157            public SavedState createFromParcel(Parcel in) {
1158                return new SavedState(in);
1159            }
1160
1161            @Override
1162            public SavedState[] newArray(int size) {
1163                return new SavedState[size];
1164            }
1165        };
1166    }
1167
1168    private final class PatternExploreByTouchHelper extends ExploreByTouchHelper {
1169        private Rect mTempRect = new Rect();
1170        private HashMap<Integer, VirtualViewContainer> mItems = new HashMap<Integer,
1171                VirtualViewContainer>();
1172
1173        class VirtualViewContainer {
1174            public VirtualViewContainer(CharSequence description) {
1175                this.description = description;
1176            }
1177            CharSequence description;
1178        };
1179
1180        public PatternExploreByTouchHelper(View forView) {
1181            super(forView);
1182        }
1183
1184        @Override
1185        protected int getVirtualViewAt(float x, float y) {
1186            // This must use the same hit logic for the screen to ensure consistency whether
1187            // accessibility is on or off.
1188            int id = getVirtualViewIdForHit(x, y);
1189            return id;
1190        }
1191
1192        @Override
1193        protected void getVisibleVirtualViews(IntArray virtualViewIds) {
1194            if (DEBUG_A11Y) Log.v(TAG, "getVisibleVirtualViews(len=" + virtualViewIds.size() + ")");
1195            for (int i = VIRTUAL_BASE_VIEW_ID; i < VIRTUAL_BASE_VIEW_ID + 9; i++) {
1196                if (!mItems.containsKey(i)) {
1197                    VirtualViewContainer item = new VirtualViewContainer(getTextForVirtualView(i));
1198                    mItems.put(i, item);
1199                }
1200                // Add all views. As views are added to the pattern, we remove them
1201                // from notification by making them non-clickable below.
1202                virtualViewIds.add(i);
1203            }
1204        }
1205
1206        @Override
1207        protected void onPopulateEventForVirtualView(int virtualViewId, AccessibilityEvent event) {
1208            if (DEBUG_A11Y) Log.v(TAG, "onPopulateEventForVirtualView(" + virtualViewId + ")");
1209            // Announce this view
1210            if (mItems.containsKey(virtualViewId)) {
1211                CharSequence contentDescription = mItems.get(virtualViewId).description;
1212                event.getText().add(contentDescription);
1213            }
1214        }
1215
1216        @Override
1217        protected void onPopulateNodeForVirtualView(int virtualViewId, AccessibilityNodeInfo node) {
1218            if (DEBUG_A11Y) Log.v(TAG, "onPopulateNodeForVirtualView(view=" + virtualViewId + ")");
1219
1220            // Node and event text and content descriptions are usually
1221            // identical, so we'll use the exact same string as before.
1222            node.setText(getTextForVirtualView(virtualViewId));
1223            node.setContentDescription(getTextForVirtualView(virtualViewId));
1224
1225            if (isClickable(virtualViewId)) {
1226                // Mark this node of interest by making it clickable.
1227                node.addAction(AccessibilityAction.ACTION_CLICK);
1228                node.setClickable(isClickable(virtualViewId));
1229            }
1230
1231            // Compute bounds for this object
1232            final Rect bounds = getBoundsForVirtualView(virtualViewId);
1233            if (DEBUG_A11Y) Log.v(TAG, "bounds:" + bounds.toString());
1234            node.setBoundsInParent(bounds);
1235        }
1236
1237        private boolean isClickable(int virtualViewId) {
1238            // Dots are clickable if they're not part of the current pattern.
1239            if (virtualViewId != ExploreByTouchHelper.INVALID_ID) {
1240                int row = (virtualViewId - VIRTUAL_BASE_VIEW_ID) / 3;
1241                int col = (virtualViewId - VIRTUAL_BASE_VIEW_ID) % 3;
1242                return !mPatternDrawLookup[row][col];
1243            }
1244            return false;
1245        }
1246
1247        @Override
1248        protected boolean onPerformActionForVirtualView(int virtualViewId, int action,
1249                Bundle arguments) {
1250            if (DEBUG_A11Y) Log.v(TAG, "onPerformActionForVirtualView(id=" + virtualViewId
1251                    + ", action=" + action);
1252            switch (action) {
1253                case AccessibilityNodeInfo.ACTION_CLICK:
1254                    // Click handling should be consistent with
1255                    // onTouchEvent(). This ensures that the view works the
1256                    // same whether accessibility is turned on or off.
1257                    return onItemClicked(virtualViewId);
1258                default:
1259                    if (DEBUG_A11Y) Log.v(TAG, "*** action not handled in "
1260                            + "onPerformActionForVirtualView(viewId="
1261                            + virtualViewId + "action=" + action + ")");
1262            }
1263            return false;
1264        }
1265
1266        boolean onItemClicked(int index) {
1267            if (DEBUG_A11Y) Log.v(TAG, "onItemClicked(" + index + ")");
1268
1269            // Since the item's checked state is exposed to accessibility
1270            // services through its AccessibilityNodeInfo, we need to invalidate
1271            // the item's virtual view. At some point in the future, the
1272            // framework will obtain an updated version of the virtual view.
1273            invalidateVirtualView(index);
1274
1275            // We need to let the framework know what type of event
1276            // happened. Accessibility services may use this event to provide
1277            // appropriate feedback to the user.
1278            sendEventForVirtualView(index, AccessibilityEvent.TYPE_VIEW_CLICKED);
1279
1280            return true;
1281        }
1282
1283        private Rect getBoundsForVirtualView(int virtualViewId) {
1284            int ordinal = virtualViewId - VIRTUAL_BASE_VIEW_ID;
1285            final Rect bounds = mTempRect;
1286            final int row = ordinal / 3;
1287            final int col = ordinal % 3;
1288            final CellState cell = mCellStates[row][col];
1289            float centerX = getCenterXForColumn(col);
1290            float centerY = getCenterYForRow(row);
1291            float cellheight = mSquareHeight * mHitFactor * 0.5f;
1292            float cellwidth = mSquareWidth * mHitFactor * 0.5f;
1293            float translationY = cell.translateY;
1294            bounds.left = (int) (centerX - cellwidth);
1295            bounds.right = (int) (centerX + cellwidth);
1296            bounds.top = (int) (centerY - cellheight);
1297            bounds.bottom = (int) (centerY + cellheight);
1298            return bounds;
1299        }
1300
1301        private boolean shouldSpeakPassword() {
1302            final boolean speakPassword = Settings.Secure.getIntForUser(
1303                    mContext.getContentResolver(), Settings.Secure.ACCESSIBILITY_SPEAK_PASSWORD, 0,
1304                    UserHandle.USER_CURRENT_OR_SELF) != 0;
1305            final boolean hasHeadphones = mAudioManager != null ?
1306                    (mAudioManager.isWiredHeadsetOn() || mAudioManager.isBluetoothA2dpOn())
1307                    : false;
1308            return speakPassword || hasHeadphones;
1309        }
1310
1311        private CharSequence getTextForVirtualView(int virtualViewId) {
1312            final Resources res = getResources();
1313            return shouldSpeakPassword() ? res.getString(
1314                R.string.lockscreen_access_pattern_cell_added_verbose, virtualViewId)
1315                : res.getString(R.string.lockscreen_access_pattern_cell_added);
1316        }
1317
1318        /**
1319         * Helper method to find which cell a point maps to
1320         *
1321         * if there's no hit.
1322         * @param x touch position x
1323         * @param y touch position y
1324         * @return VIRTUAL_BASE_VIEW_ID+id or 0 if no view was hit
1325         */
1326        private int getVirtualViewIdForHit(float x, float y) {
1327            final int rowHit = getRowHit(y);
1328            if (rowHit < 0) {
1329                return ExploreByTouchHelper.INVALID_ID;
1330            }
1331            final int columnHit = getColumnHit(x);
1332            if (columnHit < 0) {
1333                return ExploreByTouchHelper.INVALID_ID;
1334            }
1335            boolean dotAvailable = mPatternDrawLookup[rowHit][columnHit];
1336            int dotId = (rowHit * 3 + columnHit) + VIRTUAL_BASE_VIEW_ID;
1337            int view = dotAvailable ? dotId : ExploreByTouchHelper.INVALID_ID;
1338            if (DEBUG_A11Y) Log.v(TAG, "getVirtualViewIdForHit(" + x + "," + y + ") => "
1339                    + view + "avail =" + dotAvailable);
1340            return view;
1341        }
1342    }
1343}
1344