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