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