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