Home | History | Annotate | Download | only in widget
      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 
     17 package com.android.internal.widget;
     18 
     19 import android.animation.Animator;
     20 import android.animation.AnimatorListenerAdapter;
     21 import android.animation.ValueAnimator;
     22 import android.content.Context;
     23 import android.content.res.Resources;
     24 import android.content.res.TypedArray;
     25 import android.graphics.Canvas;
     26 import android.graphics.CanvasProperty;
     27 import android.graphics.Paint;
     28 import android.graphics.Path;
     29 import android.graphics.Rect;
     30 import android.media.AudioManager;
     31 import android.os.Bundle;
     32 import android.os.Debug;
     33 import android.os.Parcel;
     34 import android.os.Parcelable;
     35 import android.os.SystemClock;
     36 import android.os.UserHandle;
     37 import android.provider.Settings;
     38 import android.util.AttributeSet;
     39 import android.util.IntArray;
     40 import android.util.Log;
     41 import android.view.DisplayListCanvas;
     42 import android.view.HapticFeedbackConstants;
     43 import android.view.MotionEvent;
     44 import android.view.RenderNodeAnimator;
     45 import android.view.View;
     46 import android.view.accessibility.AccessibilityEvent;
     47 import android.view.accessibility.AccessibilityManager;
     48 import android.view.accessibility.AccessibilityNodeInfo;
     49 import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction;
     50 import android.view.animation.AnimationUtils;
     51 import android.view.animation.Interpolator;
     52 
     53 import com.android.internal.R;
     54 
     55 import java.util.ArrayList;
     56 import java.util.HashMap;
     57 import 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  */
     66 public 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