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