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 = false;
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        // Dispatch to onHoverEvent first so mPatternInProgress is up to date when the
572        // helper gets the event.
573        boolean handled = super.dispatchHoverEvent(event);
574        handled |= mExploreByTouchHelper.dispatchHoverEvent(event);
575        return handled;
576    }
577
578    /**
579     * Reset all pattern state.
580     */
581    private void resetPattern() {
582        mPattern.clear();
583        clearPatternDrawLookup();
584        mPatternDisplayMode = DisplayMode.Correct;
585        invalidate();
586    }
587
588    /**
589     * Clear the pattern lookup table.
590     */
591    private void clearPatternDrawLookup() {
592        for (int i = 0; i < 3; i++) {
593            for (int j = 0; j < 3; j++) {
594                mPatternDrawLookup[i][j] = false;
595            }
596        }
597    }
598
599    /**
600     * Disable input (for instance when displaying a message that will
601     * timeout so user doesn't get view into messy state).
602     */
603    public void disableInput() {
604        mInputEnabled = false;
605    }
606
607    /**
608     * Enable input.
609     */
610    public void enableInput() {
611        mInputEnabled = true;
612    }
613
614    @Override
615    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
616        final int width = w - mPaddingLeft - mPaddingRight;
617        mSquareWidth = width / 3.0f;
618
619        if (DEBUG_A11Y) Log.v(TAG, "onSizeChanged(" + w + "," + h + ")");
620        final int height = h - mPaddingTop - mPaddingBottom;
621        mSquareHeight = height / 3.0f;
622        mExploreByTouchHelper.invalidateRoot();
623    }
624
625    private int resolveMeasured(int measureSpec, int desired)
626    {
627        int result = 0;
628        int specSize = MeasureSpec.getSize(measureSpec);
629        switch (MeasureSpec.getMode(measureSpec)) {
630            case MeasureSpec.UNSPECIFIED:
631                result = desired;
632                break;
633            case MeasureSpec.AT_MOST:
634                result = Math.max(specSize, desired);
635                break;
636            case MeasureSpec.EXACTLY:
637            default:
638                result = specSize;
639        }
640        return result;
641    }
642
643    @Override
644    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
645        final int minimumWidth = getSuggestedMinimumWidth();
646        final int minimumHeight = getSuggestedMinimumHeight();
647        int viewWidth = resolveMeasured(widthMeasureSpec, minimumWidth);
648        int viewHeight = resolveMeasured(heightMeasureSpec, minimumHeight);
649
650        switch (mAspect) {
651            case ASPECT_SQUARE:
652                viewWidth = viewHeight = Math.min(viewWidth, viewHeight);
653                break;
654            case ASPECT_LOCK_WIDTH:
655                viewHeight = Math.min(viewWidth, viewHeight);
656                break;
657            case ASPECT_LOCK_HEIGHT:
658                viewWidth = Math.min(viewWidth, viewHeight);
659                break;
660        }
661        // Log.v(TAG, "LockPatternView dimensions: " + viewWidth + "x" + viewHeight);
662        setMeasuredDimension(viewWidth, viewHeight);
663    }
664
665    /**
666     * Determines whether the point x, y will add a new point to the current
667     * pattern (in addition to finding the cell, also makes heuristic choices
668     * such as filling in gaps based on current pattern).
669     * @param x The x coordinate.
670     * @param y The y coordinate.
671     */
672    private Cell detectAndAddHit(float x, float y) {
673        final Cell cell = checkForNewHit(x, y);
674        if (cell != null) {
675
676            // check for gaps in existing pattern
677            Cell fillInGapCell = null;
678            final ArrayList<Cell> pattern = mPattern;
679            if (!pattern.isEmpty()) {
680                final Cell lastCell = pattern.get(pattern.size() - 1);
681                int dRow = cell.row - lastCell.row;
682                int dColumn = cell.column - lastCell.column;
683
684                int fillInRow = lastCell.row;
685                int fillInColumn = lastCell.column;
686
687                if (Math.abs(dRow) == 2 && Math.abs(dColumn) != 1) {
688                    fillInRow = lastCell.row + ((dRow > 0) ? 1 : -1);
689                }
690
691                if (Math.abs(dColumn) == 2 && Math.abs(dRow) != 1) {
692                    fillInColumn = lastCell.column + ((dColumn > 0) ? 1 : -1);
693                }
694
695                fillInGapCell = Cell.of(fillInRow, fillInColumn);
696            }
697
698            if (fillInGapCell != null &&
699                    !mPatternDrawLookup[fillInGapCell.row][fillInGapCell.column]) {
700                addCellToPattern(fillInGapCell);
701            }
702            addCellToPattern(cell);
703            if (mEnableHapticFeedback) {
704                performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY,
705                        HapticFeedbackConstants.FLAG_IGNORE_VIEW_SETTING
706                        | HapticFeedbackConstants.FLAG_IGNORE_GLOBAL_SETTING);
707            }
708            return cell;
709        }
710        return null;
711    }
712
713    private void addCellToPattern(Cell newCell) {
714        mPatternDrawLookup[newCell.getRow()][newCell.getColumn()] = true;
715        mPattern.add(newCell);
716        if (!mInStealthMode) {
717            startCellActivatedAnimation(newCell);
718        }
719        notifyCellAdded();
720    }
721
722    private void startCellActivatedAnimation(Cell cell) {
723        final CellState cellState = mCellStates[cell.row][cell.column];
724        startRadiusAnimation(mDotSize/2, mDotSizeActivated/2, 96, mLinearOutSlowInInterpolator,
725                cellState, new Runnable() {
726                    @Override
727                    public void run() {
728                        startRadiusAnimation(mDotSizeActivated/2, mDotSize/2, 192,
729                                mFastOutSlowInInterpolator,
730                                cellState, null);
731                    }
732                });
733        startLineEndAnimation(cellState, mInProgressX, mInProgressY,
734                getCenterXForColumn(cell.column), getCenterYForRow(cell.row));
735    }
736
737    private void startLineEndAnimation(final CellState state,
738            final float startX, final float startY, final float targetX, final float targetY) {
739        ValueAnimator valueAnimator = ValueAnimator.ofFloat(0, 1);
740        valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
741            @Override
742            public void onAnimationUpdate(ValueAnimator animation) {
743                float t = (float) animation.getAnimatedValue();
744                state.lineEndX = (1 - t) * startX + t * targetX;
745                state.lineEndY = (1 - t) * startY + t * targetY;
746                invalidate();
747            }
748        });
749        valueAnimator.addListener(new AnimatorListenerAdapter() {
750            @Override
751            public void onAnimationEnd(Animator animation) {
752                state.lineAnimator = null;
753            }
754        });
755        valueAnimator.setInterpolator(mFastOutSlowInInterpolator);
756        valueAnimator.setDuration(100);
757        valueAnimator.start();
758        state.lineAnimator = valueAnimator;
759    }
760
761    private void startRadiusAnimation(float start, float end, long duration,
762            Interpolator interpolator, final CellState state, final Runnable endRunnable) {
763        ValueAnimator valueAnimator = ValueAnimator.ofFloat(start, end);
764        valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
765            @Override
766            public void onAnimationUpdate(ValueAnimator animation) {
767                state.radius = (float) animation.getAnimatedValue();
768                invalidate();
769            }
770        });
771        if (endRunnable != null) {
772            valueAnimator.addListener(new AnimatorListenerAdapter() {
773                @Override
774                public void onAnimationEnd(Animator animation) {
775                    endRunnable.run();
776                }
777            });
778        }
779        valueAnimator.setInterpolator(interpolator);
780        valueAnimator.setDuration(duration);
781        valueAnimator.start();
782    }
783
784    // helper method to find which cell a point maps to
785    private Cell checkForNewHit(float x, float y) {
786
787        final int rowHit = getRowHit(y);
788        if (rowHit < 0) {
789            return null;
790        }
791        final int columnHit = getColumnHit(x);
792        if (columnHit < 0) {
793            return null;
794        }
795
796        if (mPatternDrawLookup[rowHit][columnHit]) {
797            return null;
798        }
799        return Cell.of(rowHit, columnHit);
800    }
801
802    /**
803     * Helper method to find the row that y falls into.
804     * @param y The y coordinate
805     * @return The row that y falls in, or -1 if it falls in no row.
806     */
807    private int getRowHit(float y) {
808
809        final float squareHeight = mSquareHeight;
810        float hitSize = squareHeight * mHitFactor;
811
812        float offset = mPaddingTop + (squareHeight - hitSize) / 2f;
813        for (int i = 0; i < 3; i++) {
814
815            final float hitTop = offset + squareHeight * i;
816            if (y >= hitTop && y <= hitTop + hitSize) {
817                return i;
818            }
819        }
820        return -1;
821    }
822
823    /**
824     * Helper method to find the column x fallis into.
825     * @param x The x coordinate.
826     * @return The column that x falls in, or -1 if it falls in no column.
827     */
828    private int getColumnHit(float x) {
829        final float squareWidth = mSquareWidth;
830        float hitSize = squareWidth * mHitFactor;
831
832        float offset = mPaddingLeft + (squareWidth - hitSize) / 2f;
833        for (int i = 0; i < 3; i++) {
834
835            final float hitLeft = offset + squareWidth * i;
836            if (x >= hitLeft && x <= hitLeft + hitSize) {
837                return i;
838            }
839        }
840        return -1;
841    }
842
843    @Override
844    public boolean onHoverEvent(MotionEvent event) {
845        if (AccessibilityManager.getInstance(mContext).isTouchExplorationEnabled()) {
846            final int action = event.getAction();
847            switch (action) {
848                case MotionEvent.ACTION_HOVER_ENTER:
849                    event.setAction(MotionEvent.ACTION_DOWN);
850                    break;
851                case MotionEvent.ACTION_HOVER_MOVE:
852                    event.setAction(MotionEvent.ACTION_MOVE);
853                    break;
854                case MotionEvent.ACTION_HOVER_EXIT:
855                    event.setAction(MotionEvent.ACTION_UP);
856                    break;
857            }
858            onTouchEvent(event);
859            event.setAction(action);
860        }
861        return super.onHoverEvent(event);
862    }
863
864    @Override
865    public boolean onTouchEvent(MotionEvent event) {
866        if (!mInputEnabled || !isEnabled()) {
867            return false;
868        }
869
870        switch(event.getAction()) {
871            case MotionEvent.ACTION_DOWN:
872                handleActionDown(event);
873                return true;
874            case MotionEvent.ACTION_UP:
875                handleActionUp();
876                return true;
877            case MotionEvent.ACTION_MOVE:
878                handleActionMove(event);
879                return true;
880            case MotionEvent.ACTION_CANCEL:
881                if (mPatternInProgress) {
882                    setPatternInProgress(false);
883                    resetPattern();
884                    notifyPatternCleared();
885                }
886                if (PROFILE_DRAWING) {
887                    if (mDrawingProfilingStarted) {
888                        Debug.stopMethodTracing();
889                        mDrawingProfilingStarted = false;
890                    }
891                }
892                return true;
893        }
894        return false;
895    }
896
897    private void setPatternInProgress(boolean progress) {
898        mPatternInProgress = progress;
899        mExploreByTouchHelper.invalidateRoot();
900    }
901
902    private void handleActionMove(MotionEvent event) {
903        // Handle all recent motion events so we don't skip any cells even when the device
904        // is busy...
905        final float radius = mPathWidth;
906        final int historySize = event.getHistorySize();
907        mTmpInvalidateRect.setEmpty();
908        boolean invalidateNow = false;
909        for (int i = 0; i < historySize + 1; i++) {
910            final float x = i < historySize ? event.getHistoricalX(i) : event.getX();
911            final float y = i < historySize ? event.getHistoricalY(i) : event.getY();
912            Cell hitCell = detectAndAddHit(x, y);
913            final int patternSize = mPattern.size();
914            if (hitCell != null && patternSize == 1) {
915                setPatternInProgress(true);
916                notifyPatternStarted();
917            }
918            // note current x and y for rubber banding of in progress patterns
919            final float dx = Math.abs(x - mInProgressX);
920            final float dy = Math.abs(y - mInProgressY);
921            if (dx > DRAG_THRESHHOLD || dy > DRAG_THRESHHOLD) {
922                invalidateNow = true;
923            }
924
925            if (mPatternInProgress && patternSize > 0) {
926                final ArrayList<Cell> pattern = mPattern;
927                final Cell lastCell = pattern.get(patternSize - 1);
928                float lastCellCenterX = getCenterXForColumn(lastCell.column);
929                float lastCellCenterY = getCenterYForRow(lastCell.row);
930
931                // Adjust for drawn segment from last cell to (x,y). Radius accounts for line width.
932                float left = Math.min(lastCellCenterX, x) - radius;
933                float right = Math.max(lastCellCenterX, x) + radius;
934                float top = Math.min(lastCellCenterY, y) - radius;
935                float bottom = Math.max(lastCellCenterY, y) + radius;
936
937                // Invalidate between the pattern's new cell and the pattern's previous cell
938                if (hitCell != null) {
939                    final float width = mSquareWidth * 0.5f;
940                    final float height = mSquareHeight * 0.5f;
941                    final float hitCellCenterX = getCenterXForColumn(hitCell.column);
942                    final float hitCellCenterY = getCenterYForRow(hitCell.row);
943
944                    left = Math.min(hitCellCenterX - width, left);
945                    right = Math.max(hitCellCenterX + width, right);
946                    top = Math.min(hitCellCenterY - height, top);
947                    bottom = Math.max(hitCellCenterY + height, bottom);
948                }
949
950                // Invalidate between the pattern's last cell and the previous location
951                mTmpInvalidateRect.union(Math.round(left), Math.round(top),
952                        Math.round(right), Math.round(bottom));
953            }
954        }
955        mInProgressX = event.getX();
956        mInProgressY = event.getY();
957
958        // To save updates, we only invalidate if the user moved beyond a certain amount.
959        if (invalidateNow) {
960            mInvalidate.union(mTmpInvalidateRect);
961            invalidate(mInvalidate);
962            mInvalidate.set(mTmpInvalidateRect);
963        }
964    }
965
966    private void sendAccessEvent(int resId) {
967        announceForAccessibility(mContext.getString(resId));
968    }
969
970    private void handleActionUp() {
971        // report pattern detected
972        if (!mPattern.isEmpty()) {
973            setPatternInProgress(false);
974            cancelLineAnimations();
975            notifyPatternDetected();
976            invalidate();
977        }
978        if (PROFILE_DRAWING) {
979            if (mDrawingProfilingStarted) {
980                Debug.stopMethodTracing();
981                mDrawingProfilingStarted = false;
982            }
983        }
984    }
985
986    private void cancelLineAnimations() {
987        for (int i = 0; i < 3; i++) {
988            for (int j = 0; j < 3; j++) {
989                CellState state = mCellStates[i][j];
990                if (state.lineAnimator != null) {
991                    state.lineAnimator.cancel();
992                    state.lineEndX = Float.MIN_VALUE;
993                    state.lineEndY = Float.MIN_VALUE;
994                }
995            }
996        }
997    }
998    private void handleActionDown(MotionEvent event) {
999        resetPattern();
1000        final float x = event.getX();
1001        final float y = event.getY();
1002        final Cell hitCell = detectAndAddHit(x, y);
1003        if (hitCell != null) {
1004            setPatternInProgress(true);
1005            mPatternDisplayMode = DisplayMode.Correct;
1006            notifyPatternStarted();
1007        } else if (mPatternInProgress) {
1008            setPatternInProgress(false);
1009            notifyPatternCleared();
1010        }
1011        if (hitCell != null) {
1012            final float startX = getCenterXForColumn(hitCell.column);
1013            final float startY = getCenterYForRow(hitCell.row);
1014
1015            final float widthOffset = mSquareWidth / 2f;
1016            final float heightOffset = mSquareHeight / 2f;
1017
1018            invalidate((int) (startX - widthOffset), (int) (startY - heightOffset),
1019                    (int) (startX + widthOffset), (int) (startY + heightOffset));
1020        }
1021        mInProgressX = x;
1022        mInProgressY = y;
1023        if (PROFILE_DRAWING) {
1024            if (!mDrawingProfilingStarted) {
1025                Debug.startMethodTracing("LockPatternDrawing");
1026                mDrawingProfilingStarted = true;
1027            }
1028        }
1029    }
1030
1031    private float getCenterXForColumn(int column) {
1032        return mPaddingLeft + column * mSquareWidth + mSquareWidth / 2f;
1033    }
1034
1035    private float getCenterYForRow(int row) {
1036        return mPaddingTop + row * mSquareHeight + mSquareHeight / 2f;
1037    }
1038
1039    @Override
1040    protected void onDraw(Canvas canvas) {
1041        final ArrayList<Cell> pattern = mPattern;
1042        final int count = pattern.size();
1043        final boolean[][] drawLookup = mPatternDrawLookup;
1044
1045        if (mPatternDisplayMode == DisplayMode.Animate) {
1046
1047            // figure out which circles to draw
1048
1049            // + 1 so we pause on complete pattern
1050            final int oneCycle = (count + 1) * MILLIS_PER_CIRCLE_ANIMATING;
1051            final int spotInCycle = (int) (SystemClock.elapsedRealtime() -
1052                    mAnimatingPeriodStart) % oneCycle;
1053            final int numCircles = spotInCycle / MILLIS_PER_CIRCLE_ANIMATING;
1054
1055            clearPatternDrawLookup();
1056            for (int i = 0; i < numCircles; i++) {
1057                final Cell cell = pattern.get(i);
1058                drawLookup[cell.getRow()][cell.getColumn()] = true;
1059            }
1060
1061            // figure out in progress portion of ghosting line
1062
1063            final boolean needToUpdateInProgressPoint = numCircles > 0
1064                    && numCircles < count;
1065
1066            if (needToUpdateInProgressPoint) {
1067                final float percentageOfNextCircle =
1068                        ((float) (spotInCycle % MILLIS_PER_CIRCLE_ANIMATING)) /
1069                                MILLIS_PER_CIRCLE_ANIMATING;
1070
1071                final Cell currentCell = pattern.get(numCircles - 1);
1072                final float centerX = getCenterXForColumn(currentCell.column);
1073                final float centerY = getCenterYForRow(currentCell.row);
1074
1075                final Cell nextCell = pattern.get(numCircles);
1076                final float dx = percentageOfNextCircle *
1077                        (getCenterXForColumn(nextCell.column) - centerX);
1078                final float dy = percentageOfNextCircle *
1079                        (getCenterYForRow(nextCell.row) - centerY);
1080                mInProgressX = centerX + dx;
1081                mInProgressY = centerY + dy;
1082            }
1083            // TODO: Infinite loop here...
1084            invalidate();
1085        }
1086
1087        final Path currentPath = mCurrentPath;
1088        currentPath.rewind();
1089
1090        // draw the circles
1091        for (int i = 0; i < 3; i++) {
1092            float centerY = getCenterYForRow(i);
1093            for (int j = 0; j < 3; j++) {
1094                CellState cellState = mCellStates[i][j];
1095                float centerX = getCenterXForColumn(j);
1096                float translationY = cellState.translationY;
1097                if (isHardwareAccelerated() && cellState.hwAnimating) {
1098                    DisplayListCanvas displayListCanvas = (DisplayListCanvas) canvas;
1099                    displayListCanvas.drawCircle(cellState.hwCenterX, cellState.hwCenterY,
1100                            cellState.hwRadius, cellState.hwPaint);
1101                } else {
1102                    drawCircle(canvas, (int) centerX, (int) centerY + translationY,
1103                            cellState.radius, drawLookup[i][j], cellState.alpha);
1104
1105                }
1106            }
1107        }
1108
1109        // TODO: the path should be created and cached every time we hit-detect a cell
1110        // only the last segment of the path should be computed here
1111        // draw the path of the pattern (unless we are in stealth mode)
1112        final boolean drawPath = !mInStealthMode;
1113
1114        if (drawPath) {
1115            mPathPaint.setColor(getCurrentColor(true /* partOfPattern */));
1116
1117            boolean anyCircles = false;
1118            float lastX = 0f;
1119            float lastY = 0f;
1120            for (int i = 0; i < count; i++) {
1121                Cell cell = pattern.get(i);
1122
1123                // only draw the part of the pattern stored in
1124                // the lookup table (this is only different in the case
1125                // of animation).
1126                if (!drawLookup[cell.row][cell.column]) {
1127                    break;
1128                }
1129                anyCircles = true;
1130
1131                float centerX = getCenterXForColumn(cell.column);
1132                float centerY = getCenterYForRow(cell.row);
1133                if (i != 0) {
1134                    CellState state = mCellStates[cell.row][cell.column];
1135                    currentPath.rewind();
1136                    currentPath.moveTo(lastX, lastY);
1137                    if (state.lineEndX != Float.MIN_VALUE && state.lineEndY != Float.MIN_VALUE) {
1138                        currentPath.lineTo(state.lineEndX, state.lineEndY);
1139                    } else {
1140                        currentPath.lineTo(centerX, centerY);
1141                    }
1142                    canvas.drawPath(currentPath, mPathPaint);
1143                }
1144                lastX = centerX;
1145                lastY = centerY;
1146            }
1147
1148            // draw last in progress section
1149            if ((mPatternInProgress || mPatternDisplayMode == DisplayMode.Animate)
1150                    && anyCircles) {
1151                currentPath.rewind();
1152                currentPath.moveTo(lastX, lastY);
1153                currentPath.lineTo(mInProgressX, mInProgressY);
1154
1155                mPathPaint.setAlpha((int) (calculateLastSegmentAlpha(
1156                        mInProgressX, mInProgressY, lastX, lastY) * 255f));
1157                canvas.drawPath(currentPath, mPathPaint);
1158            }
1159        }
1160    }
1161
1162    private float calculateLastSegmentAlpha(float x, float y, float lastX, float lastY) {
1163        float diffX = x - lastX;
1164        float diffY = y - lastY;
1165        float dist = (float) Math.sqrt(diffX*diffX + diffY*diffY);
1166        float frac = dist/mSquareWidth;
1167        return Math.min(1f, Math.max(0f, (frac - 0.3f) * 4f));
1168    }
1169
1170    private int getCurrentColor(boolean partOfPattern) {
1171        if (!partOfPattern || mInStealthMode || mPatternInProgress) {
1172            // unselected circle
1173            return mRegularColor;
1174        } else if (mPatternDisplayMode == DisplayMode.Wrong) {
1175            // the pattern is wrong
1176            return mErrorColor;
1177        } else if (mPatternDisplayMode == DisplayMode.Correct ||
1178                mPatternDisplayMode == DisplayMode.Animate) {
1179            return mSuccessColor;
1180        } else {
1181            throw new IllegalStateException("unknown display mode " + mPatternDisplayMode);
1182        }
1183    }
1184
1185    /**
1186     * @param partOfPattern Whether this circle is part of the pattern.
1187     */
1188    private void drawCircle(Canvas canvas, float centerX, float centerY, float radius,
1189            boolean partOfPattern, float alpha) {
1190        mPaint.setColor(getCurrentColor(partOfPattern));
1191        mPaint.setAlpha((int) (alpha * 255));
1192        canvas.drawCircle(centerX, centerY, radius, mPaint);
1193    }
1194
1195    @Override
1196    protected Parcelable onSaveInstanceState() {
1197        Parcelable superState = super.onSaveInstanceState();
1198        return new SavedState(superState,
1199                LockPatternUtils.patternToString(mPattern),
1200                mPatternDisplayMode.ordinal(),
1201                mInputEnabled, mInStealthMode, mEnableHapticFeedback);
1202    }
1203
1204    @Override
1205    protected void onRestoreInstanceState(Parcelable state) {
1206        final SavedState ss = (SavedState) state;
1207        super.onRestoreInstanceState(ss.getSuperState());
1208        setPattern(
1209                DisplayMode.Correct,
1210                LockPatternUtils.stringToPattern(ss.getSerializedPattern()));
1211        mPatternDisplayMode = DisplayMode.values()[ss.getDisplayMode()];
1212        mInputEnabled = ss.isInputEnabled();
1213        mInStealthMode = ss.isInStealthMode();
1214        mEnableHapticFeedback = ss.isTactileFeedbackEnabled();
1215    }
1216
1217    /**
1218     * The parecelable for saving and restoring a lock pattern view.
1219     */
1220    private static class SavedState extends BaseSavedState {
1221
1222        private final String mSerializedPattern;
1223        private final int mDisplayMode;
1224        private final boolean mInputEnabled;
1225        private final boolean mInStealthMode;
1226        private final boolean mTactileFeedbackEnabled;
1227
1228        /**
1229         * Constructor called from {@link LockPatternView#onSaveInstanceState()}
1230         */
1231        private SavedState(Parcelable superState, String serializedPattern, int displayMode,
1232                boolean inputEnabled, boolean inStealthMode, boolean tactileFeedbackEnabled) {
1233            super(superState);
1234            mSerializedPattern = serializedPattern;
1235            mDisplayMode = displayMode;
1236            mInputEnabled = inputEnabled;
1237            mInStealthMode = inStealthMode;
1238            mTactileFeedbackEnabled = tactileFeedbackEnabled;
1239        }
1240
1241        /**
1242         * Constructor called from {@link #CREATOR}
1243         */
1244        private SavedState(Parcel in) {
1245            super(in);
1246            mSerializedPattern = in.readString();
1247            mDisplayMode = in.readInt();
1248            mInputEnabled = (Boolean) in.readValue(null);
1249            mInStealthMode = (Boolean) in.readValue(null);
1250            mTactileFeedbackEnabled = (Boolean) in.readValue(null);
1251        }
1252
1253        public String getSerializedPattern() {
1254            return mSerializedPattern;
1255        }
1256
1257        public int getDisplayMode() {
1258            return mDisplayMode;
1259        }
1260
1261        public boolean isInputEnabled() {
1262            return mInputEnabled;
1263        }
1264
1265        public boolean isInStealthMode() {
1266            return mInStealthMode;
1267        }
1268
1269        public boolean isTactileFeedbackEnabled(){
1270            return mTactileFeedbackEnabled;
1271        }
1272
1273        @Override
1274        public void writeToParcel(Parcel dest, int flags) {
1275            super.writeToParcel(dest, flags);
1276            dest.writeString(mSerializedPattern);
1277            dest.writeInt(mDisplayMode);
1278            dest.writeValue(mInputEnabled);
1279            dest.writeValue(mInStealthMode);
1280            dest.writeValue(mTactileFeedbackEnabled);
1281        }
1282
1283        @SuppressWarnings({ "unused", "hiding" }) // Found using reflection
1284        public static final Parcelable.Creator<SavedState> CREATOR =
1285                new Creator<SavedState>() {
1286            @Override
1287            public SavedState createFromParcel(Parcel in) {
1288                return new SavedState(in);
1289            }
1290
1291            @Override
1292            public SavedState[] newArray(int size) {
1293                return new SavedState[size];
1294            }
1295        };
1296    }
1297
1298    private final class PatternExploreByTouchHelper extends ExploreByTouchHelper {
1299        private Rect mTempRect = new Rect();
1300        private HashMap<Integer, VirtualViewContainer> mItems = new HashMap<Integer,
1301                VirtualViewContainer>();
1302
1303        class VirtualViewContainer {
1304            public VirtualViewContainer(CharSequence description) {
1305                this.description = description;
1306            }
1307            CharSequence description;
1308        };
1309
1310        public PatternExploreByTouchHelper(View forView) {
1311            super(forView);
1312        }
1313
1314        @Override
1315        protected int getVirtualViewAt(float x, float y) {
1316            // This must use the same hit logic for the screen to ensure consistency whether
1317            // accessibility is on or off.
1318            int id = getVirtualViewIdForHit(x, y);
1319            return id;
1320        }
1321
1322        @Override
1323        protected void getVisibleVirtualViews(IntArray virtualViewIds) {
1324            if (DEBUG_A11Y) Log.v(TAG, "getVisibleVirtualViews(len=" + virtualViewIds.size() + ")");
1325            if (!mPatternInProgress) {
1326                return;
1327            }
1328            for (int i = VIRTUAL_BASE_VIEW_ID; i < VIRTUAL_BASE_VIEW_ID + 9; i++) {
1329                if (!mItems.containsKey(i)) {
1330                    VirtualViewContainer item = new VirtualViewContainer(getTextForVirtualView(i));
1331                    mItems.put(i, item);
1332                }
1333                // Add all views. As views are added to the pattern, we remove them
1334                // from notification by making them non-clickable below.
1335                virtualViewIds.add(i);
1336            }
1337        }
1338
1339        @Override
1340        protected void onPopulateEventForVirtualView(int virtualViewId, AccessibilityEvent event) {
1341            if (DEBUG_A11Y) Log.v(TAG, "onPopulateEventForVirtualView(" + virtualViewId + ")");
1342            // Announce this view
1343            if (mItems.containsKey(virtualViewId)) {
1344                CharSequence contentDescription = mItems.get(virtualViewId).description;
1345                event.getText().add(contentDescription);
1346            }
1347        }
1348
1349        @Override
1350        public void onPopulateAccessibilityEvent(View host, AccessibilityEvent event) {
1351            super.onPopulateAccessibilityEvent(host, event);
1352            if (!mPatternInProgress) {
1353                CharSequence contentDescription = getContext().getText(
1354                        com.android.internal.R.string.lockscreen_access_pattern_area);
1355                event.setContentDescription(contentDescription);
1356            }
1357        }
1358
1359        @Override
1360        protected void onPopulateNodeForVirtualView(int virtualViewId, AccessibilityNodeInfo node) {
1361            if (DEBUG_A11Y) Log.v(TAG, "onPopulateNodeForVirtualView(view=" + virtualViewId + ")");
1362
1363            // Node and event text and content descriptions are usually
1364            // identical, so we'll use the exact same string as before.
1365            node.setText(getTextForVirtualView(virtualViewId));
1366            node.setContentDescription(getTextForVirtualView(virtualViewId));
1367
1368            if (mPatternInProgress) {
1369                node.setFocusable(true);
1370
1371                if (isClickable(virtualViewId)) {
1372                    // Mark this node of interest by making it clickable.
1373                    node.addAction(AccessibilityAction.ACTION_CLICK);
1374                    node.setClickable(isClickable(virtualViewId));
1375                }
1376            }
1377
1378            // Compute bounds for this object
1379            final Rect bounds = getBoundsForVirtualView(virtualViewId);
1380            if (DEBUG_A11Y) Log.v(TAG, "bounds:" + bounds.toString());
1381            node.setBoundsInParent(bounds);
1382        }
1383
1384        private boolean isClickable(int virtualViewId) {
1385            // Dots are clickable if they're not part of the current pattern.
1386            if (virtualViewId != ExploreByTouchHelper.INVALID_ID) {
1387                int row = (virtualViewId - VIRTUAL_BASE_VIEW_ID) / 3;
1388                int col = (virtualViewId - VIRTUAL_BASE_VIEW_ID) % 3;
1389                return !mPatternDrawLookup[row][col];
1390            }
1391            return false;
1392        }
1393
1394        @Override
1395        protected boolean onPerformActionForVirtualView(int virtualViewId, int action,
1396                Bundle arguments) {
1397            if (DEBUG_A11Y) Log.v(TAG, "onPerformActionForVirtualView(id=" + virtualViewId
1398                    + ", action=" + action);
1399            switch (action) {
1400                case AccessibilityNodeInfo.ACTION_CLICK:
1401                    // Click handling should be consistent with
1402                    // onTouchEvent(). This ensures that the view works the
1403                    // same whether accessibility is turned on or off.
1404                    return onItemClicked(virtualViewId);
1405                default:
1406                    if (DEBUG_A11Y) Log.v(TAG, "*** action not handled in "
1407                            + "onPerformActionForVirtualView(viewId="
1408                            + virtualViewId + "action=" + action + ")");
1409            }
1410            return false;
1411        }
1412
1413        boolean onItemClicked(int index) {
1414            if (DEBUG_A11Y) Log.v(TAG, "onItemClicked(" + index + ")");
1415
1416            // Since the item's checked state is exposed to accessibility
1417            // services through its AccessibilityNodeInfo, we need to invalidate
1418            // the item's virtual view. At some point in the future, the
1419            // framework will obtain an updated version of the virtual view.
1420            invalidateVirtualView(index);
1421
1422            // We need to let the framework know what type of event
1423            // happened. Accessibility services may use this event to provide
1424            // appropriate feedback to the user.
1425            sendEventForVirtualView(index, AccessibilityEvent.TYPE_VIEW_CLICKED);
1426
1427            return true;
1428        }
1429
1430        private Rect getBoundsForVirtualView(int virtualViewId) {
1431            int ordinal = virtualViewId - VIRTUAL_BASE_VIEW_ID;
1432            final Rect bounds = mTempRect;
1433            final int row = ordinal / 3;
1434            final int col = ordinal % 3;
1435            final CellState cell = mCellStates[row][col];
1436            float centerX = getCenterXForColumn(col);
1437            float centerY = getCenterYForRow(row);
1438            float cellheight = mSquareHeight * mHitFactor * 0.5f;
1439            float cellwidth = mSquareWidth * mHitFactor * 0.5f;
1440            bounds.left = (int) (centerX - cellwidth);
1441            bounds.right = (int) (centerX + cellwidth);
1442            bounds.top = (int) (centerY - cellheight);
1443            bounds.bottom = (int) (centerY + cellheight);
1444            return bounds;
1445        }
1446
1447        private boolean shouldSpeakPassword() {
1448            final boolean speakPassword = Settings.Secure.getIntForUser(
1449                    mContext.getContentResolver(), Settings.Secure.ACCESSIBILITY_SPEAK_PASSWORD, 0,
1450                    UserHandle.USER_CURRENT_OR_SELF) != 0;
1451            final boolean hasHeadphones = mAudioManager != null ?
1452                    (mAudioManager.isWiredHeadsetOn() || mAudioManager.isBluetoothA2dpOn())
1453                    : false;
1454            return speakPassword || hasHeadphones;
1455        }
1456
1457        private CharSequence getTextForVirtualView(int virtualViewId) {
1458            final Resources res = getResources();
1459            return shouldSpeakPassword() ? res.getString(
1460                R.string.lockscreen_access_pattern_cell_added_verbose, virtualViewId)
1461                : res.getString(R.string.lockscreen_access_pattern_cell_added);
1462        }
1463
1464        /**
1465         * Helper method to find which cell a point maps to
1466         *
1467         * if there's no hit.
1468         * @param x touch position x
1469         * @param y touch position y
1470         * @return VIRTUAL_BASE_VIEW_ID+id or 0 if no view was hit
1471         */
1472        private int getVirtualViewIdForHit(float x, float y) {
1473            final int rowHit = getRowHit(y);
1474            if (rowHit < 0) {
1475                return ExploreByTouchHelper.INVALID_ID;
1476            }
1477            final int columnHit = getColumnHit(x);
1478            if (columnHit < 0) {
1479                return ExploreByTouchHelper.INVALID_ID;
1480            }
1481            boolean dotAvailable = mPatternDrawLookup[rowHit][columnHit];
1482            int dotId = (rowHit * 3 + columnHit) + VIRTUAL_BASE_VIEW_ID;
1483            int view = dotAvailable ? dotId : ExploreByTouchHelper.INVALID_ID;
1484            if (DEBUG_A11Y) Log.v(TAG, "getVirtualViewIdForHit(" + x + "," + y + ") => "
1485                    + view + "avail =" + dotAvailable);
1486            return view;
1487        }
1488    }
1489}
1490