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