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