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