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