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