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