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