LockPatternView.java revision 613f55fbbb23249d7c65e3f1fe8c943c4459b41a
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 17package com.android.internal.widget; 18 19import android.animation.Animator; 20import android.animation.AnimatorListenerAdapter; 21import android.animation.ValueAnimator; 22import android.content.Context; 23import android.content.res.Resources; 24import android.content.res.TypedArray; 25import android.graphics.Canvas; 26import android.graphics.CanvasProperty; 27import android.graphics.Paint; 28import android.graphics.Path; 29import android.graphics.Rect; 30import android.media.AudioManager; 31import android.os.Bundle; 32import android.os.Debug; 33import android.os.Parcel; 34import android.os.Parcelable; 35import android.os.SystemClock; 36import android.os.UserHandle; 37import android.provider.Settings; 38import android.util.AttributeSet; 39import android.util.IntArray; 40import android.util.Log; 41import android.view.DisplayListCanvas; 42import android.view.HapticFeedbackConstants; 43import android.view.MotionEvent; 44import android.view.RenderNodeAnimator; 45import android.view.View; 46import android.view.accessibility.AccessibilityEvent; 47import android.view.accessibility.AccessibilityManager; 48import android.view.accessibility.AccessibilityNodeInfo; 49import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction; 50import android.view.animation.AnimationUtils; 51import android.view.animation.Interpolator; 52 53import com.android.internal.R; 54 55import java.util.ArrayList; 56import java.util.HashMap; 57import 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 */ 66public 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 = true; 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 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 */ 170 private Cell(int row, int column) { 171 checkRange(row, column); 172 this.row = row; 173 this.column = column; 174 } 175 176 public int getRow() { 177 return row; 178 } 179 180 public int getColumn() { 181 return column; 182 } 183 184 public static Cell of(int row, int column) { 185 checkRange(row, column); 186 return sCells[row][column]; 187 } 188 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 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 */ 249 void onPatternStart(); 250 251 /** 252 * The pattern was cleared. 253 */ 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 */ 260 void onPatternCellAdded(List<Cell> pattern); 261 262 /** 263 * A pattern was detected from the user. 264 * @param pattern The pattern. 265 */ 266 void onPatternDetected(List<Cell> pattern); 267 } 268 269 public LockPatternView(Context context) { 270 this(context, null); 271 } 272 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 339 public CellState[][] getCellStates() { 340 return mCellStates; 341 } 342 343 /** 344 * @return Whether the view is in stealth mode. 345 */ 346 public boolean isInStealthMode() { 347 return mInStealthMode; 348 } 349 350 /** 351 * @return Whether the view has tactile feedback enabled. 352 */ 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 */ 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 */ 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 */ 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 */ 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 */ 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 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 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 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 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 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 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 541 private void notifyPatternStarted() { 542 sendAccessEvent(R.string.lockscreen_access_pattern_start); 543 if (mOnPatternListener != null) { 544 mOnPatternListener.onPatternStart(); 545 } 546 } 547 548 private void notifyPatternDetected() { 549 sendAccessEvent(R.string.lockscreen_access_pattern_detected); 550 if (mOnPatternListener != null) { 551 mOnPatternListener.onPatternDetected(mPattern); 552 } 553 } 554 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 */ 565 public void clearPattern() { 566 resetPattern(); 567 } 568 569 @Override 570 protected boolean dispatchHoverEvent(MotionEvent event) { 571 // Give TouchHelper first right of refusal 572 boolean handled = mExploreByTouchHelper.dispatchHoverEvent(event); 573 return super.dispatchHoverEvent(event) || handled; 574 } 575 576 /** 577 * Reset all pattern state. 578 */ 579 private void resetPattern() { 580 mPattern.clear(); 581 clearPatternDrawLookup(); 582 mPatternDisplayMode = DisplayMode.Correct; 583 invalidate(); 584 } 585 586 /** 587 * Clear the pattern lookup table. 588 */ 589 private void clearPatternDrawLookup() { 590 for (int i = 0; i < 3; i++) { 591 for (int j = 0; j < 3; j++) { 592 mPatternDrawLookup[i][j] = false; 593 } 594 } 595 } 596 597 /** 598 * Disable input (for instance when displaying a message that will 599 * timeout so user doesn't get view into messy state). 600 */ 601 public void disableInput() { 602 mInputEnabled = false; 603 } 604 605 /** 606 * Enable input. 607 */ 608 public void enableInput() { 609 mInputEnabled = true; 610 } 611 612 @Override 613 protected void onSizeChanged(int w, int h, int oldw, int oldh) { 614 final int width = w - mPaddingLeft - mPaddingRight; 615 mSquareWidth = width / 3.0f; 616 617 if (DEBUG_A11Y) Log.v(TAG, "onSizeChanged(" + w + "," + h + ")"); 618 final int height = h - mPaddingTop - mPaddingBottom; 619 mSquareHeight = height / 3.0f; 620 mExploreByTouchHelper.invalidateRoot(); 621 } 622 623 private int resolveMeasured(int measureSpec, int desired) 624 { 625 int result = 0; 626 int specSize = MeasureSpec.getSize(measureSpec); 627 switch (MeasureSpec.getMode(measureSpec)) { 628 case MeasureSpec.UNSPECIFIED: 629 result = desired; 630 break; 631 case MeasureSpec.AT_MOST: 632 result = Math.max(specSize, desired); 633 break; 634 case MeasureSpec.EXACTLY: 635 default: 636 result = specSize; 637 } 638 return result; 639 } 640 641 @Override 642 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 643 final int minimumWidth = getSuggestedMinimumWidth(); 644 final int minimumHeight = getSuggestedMinimumHeight(); 645 int viewWidth = resolveMeasured(widthMeasureSpec, minimumWidth); 646 int viewHeight = resolveMeasured(heightMeasureSpec, minimumHeight); 647 648 switch (mAspect) { 649 case ASPECT_SQUARE: 650 viewWidth = viewHeight = Math.min(viewWidth, viewHeight); 651 break; 652 case ASPECT_LOCK_WIDTH: 653 viewHeight = Math.min(viewWidth, viewHeight); 654 break; 655 case ASPECT_LOCK_HEIGHT: 656 viewWidth = Math.min(viewWidth, viewHeight); 657 break; 658 } 659 // Log.v(TAG, "LockPatternView dimensions: " + viewWidth + "x" + viewHeight); 660 setMeasuredDimension(viewWidth, viewHeight); 661 } 662 663 /** 664 * Determines whether the point x, y will add a new point to the current 665 * pattern (in addition to finding the cell, also makes heuristic choices 666 * such as filling in gaps based on current pattern). 667 * @param x The x coordinate. 668 * @param y The y coordinate. 669 */ 670 private Cell detectAndAddHit(float x, float y) { 671 final Cell cell = checkForNewHit(x, y); 672 if (cell != null) { 673 674 // check for gaps in existing pattern 675 Cell fillInGapCell = null; 676 final ArrayList<Cell> pattern = mPattern; 677 if (!pattern.isEmpty()) { 678 final Cell lastCell = pattern.get(pattern.size() - 1); 679 int dRow = cell.row - lastCell.row; 680 int dColumn = cell.column - lastCell.column; 681 682 int fillInRow = lastCell.row; 683 int fillInColumn = lastCell.column; 684 685 if (Math.abs(dRow) == 2 && Math.abs(dColumn) != 1) { 686 fillInRow = lastCell.row + ((dRow > 0) ? 1 : -1); 687 } 688 689 if (Math.abs(dColumn) == 2 && Math.abs(dRow) != 1) { 690 fillInColumn = lastCell.column + ((dColumn > 0) ? 1 : -1); 691 } 692 693 fillInGapCell = Cell.of(fillInRow, fillInColumn); 694 } 695 696 if (fillInGapCell != null && 697 !mPatternDrawLookup[fillInGapCell.row][fillInGapCell.column]) { 698 addCellToPattern(fillInGapCell); 699 } 700 addCellToPattern(cell); 701 if (mEnableHapticFeedback) { 702 performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY, 703 HapticFeedbackConstants.FLAG_IGNORE_VIEW_SETTING 704 | HapticFeedbackConstants.FLAG_IGNORE_GLOBAL_SETTING); 705 } 706 return cell; 707 } 708 return null; 709 } 710 711 private void addCellToPattern(Cell newCell) { 712 mPatternDrawLookup[newCell.getRow()][newCell.getColumn()] = true; 713 mPattern.add(newCell); 714 if (!mInStealthMode) { 715 startCellActivatedAnimation(newCell); 716 } 717 notifyCellAdded(); 718 } 719 720 private void startCellActivatedAnimation(Cell cell) { 721 final CellState cellState = mCellStates[cell.row][cell.column]; 722 startRadiusAnimation(mDotSize/2, mDotSizeActivated/2, 96, mLinearOutSlowInInterpolator, 723 cellState, new Runnable() { 724 @Override 725 public void run() { 726 startRadiusAnimation(mDotSizeActivated/2, mDotSize/2, 192, 727 mFastOutSlowInInterpolator, 728 cellState, null); 729 } 730 }); 731 startLineEndAnimation(cellState, mInProgressX, mInProgressY, 732 getCenterXForColumn(cell.column), getCenterYForRow(cell.row)); 733 } 734 735 private void startLineEndAnimation(final CellState state, 736 final float startX, final float startY, final float targetX, final float targetY) { 737 ValueAnimator valueAnimator = ValueAnimator.ofFloat(0, 1); 738 valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { 739 @Override 740 public void onAnimationUpdate(ValueAnimator animation) { 741 float t = (float) animation.getAnimatedValue(); 742 state.lineEndX = (1 - t) * startX + t * targetX; 743 state.lineEndY = (1 - t) * startY + t * targetY; 744 invalidate(); 745 } 746 }); 747 valueAnimator.addListener(new AnimatorListenerAdapter() { 748 @Override 749 public void onAnimationEnd(Animator animation) { 750 state.lineAnimator = null; 751 } 752 }); 753 valueAnimator.setInterpolator(mFastOutSlowInInterpolator); 754 valueAnimator.setDuration(100); 755 valueAnimator.start(); 756 state.lineAnimator = valueAnimator; 757 } 758 759 private void startRadiusAnimation(float start, float end, long duration, 760 Interpolator interpolator, final CellState state, final Runnable endRunnable) { 761 ValueAnimator valueAnimator = ValueAnimator.ofFloat(start, end); 762 valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { 763 @Override 764 public void onAnimationUpdate(ValueAnimator animation) { 765 state.radius = (float) animation.getAnimatedValue(); 766 invalidate(); 767 } 768 }); 769 if (endRunnable != null) { 770 valueAnimator.addListener(new AnimatorListenerAdapter() { 771 @Override 772 public void onAnimationEnd(Animator animation) { 773 endRunnable.run(); 774 } 775 }); 776 } 777 valueAnimator.setInterpolator(interpolator); 778 valueAnimator.setDuration(duration); 779 valueAnimator.start(); 780 } 781 782 // helper method to find which cell a point maps to 783 private Cell checkForNewHit(float x, float y) { 784 785 final int rowHit = getRowHit(y); 786 if (rowHit < 0) { 787 return null; 788 } 789 final int columnHit = getColumnHit(x); 790 if (columnHit < 0) { 791 return null; 792 } 793 794 if (mPatternDrawLookup[rowHit][columnHit]) { 795 return null; 796 } 797 return Cell.of(rowHit, columnHit); 798 } 799 800 /** 801 * Helper method to find the row that y falls into. 802 * @param y The y coordinate 803 * @return The row that y falls in, or -1 if it falls in no row. 804 */ 805 private int getRowHit(float y) { 806 807 final float squareHeight = mSquareHeight; 808 float hitSize = squareHeight * mHitFactor; 809 810 float offset = mPaddingTop + (squareHeight - hitSize) / 2f; 811 for (int i = 0; i < 3; i++) { 812 813 final float hitTop = offset + squareHeight * i; 814 if (y >= hitTop && y <= hitTop + hitSize) { 815 return i; 816 } 817 } 818 return -1; 819 } 820 821 /** 822 * Helper method to find the column x fallis into. 823 * @param x The x coordinate. 824 * @return The column that x falls in, or -1 if it falls in no column. 825 */ 826 private int getColumnHit(float x) { 827 final float squareWidth = mSquareWidth; 828 float hitSize = squareWidth * mHitFactor; 829 830 float offset = mPaddingLeft + (squareWidth - hitSize) / 2f; 831 for (int i = 0; i < 3; i++) { 832 833 final float hitLeft = offset + squareWidth * i; 834 if (x >= hitLeft && x <= hitLeft + hitSize) { 835 return i; 836 } 837 } 838 return -1; 839 } 840 841 @Override 842 public boolean onHoverEvent(MotionEvent event) { 843 if (AccessibilityManager.getInstance(mContext).isTouchExplorationEnabled()) { 844 final int action = event.getAction(); 845 switch (action) { 846 case MotionEvent.ACTION_HOVER_ENTER: 847 event.setAction(MotionEvent.ACTION_DOWN); 848 break; 849 case MotionEvent.ACTION_HOVER_MOVE: 850 event.setAction(MotionEvent.ACTION_MOVE); 851 break; 852 case MotionEvent.ACTION_HOVER_EXIT: 853 event.setAction(MotionEvent.ACTION_UP); 854 break; 855 } 856 onTouchEvent(event); 857 event.setAction(action); 858 } 859 return super.onHoverEvent(event); 860 } 861 862 @Override 863 public boolean onTouchEvent(MotionEvent event) { 864 if (!mInputEnabled || !isEnabled()) { 865 return false; 866 } 867 868 switch(event.getAction()) { 869 case MotionEvent.ACTION_DOWN: 870 handleActionDown(event); 871 return true; 872 case MotionEvent.ACTION_UP: 873 handleActionUp(); 874 return true; 875 case MotionEvent.ACTION_MOVE: 876 handleActionMove(event); 877 return true; 878 case MotionEvent.ACTION_CANCEL: 879 if (mPatternInProgress) { 880 mPatternInProgress = false; 881 resetPattern(); 882 notifyPatternCleared(); 883 } 884 if (PROFILE_DRAWING) { 885 if (mDrawingProfilingStarted) { 886 Debug.stopMethodTracing(); 887 mDrawingProfilingStarted = false; 888 } 889 } 890 return true; 891 } 892 return false; 893 } 894 895 private void handleActionMove(MotionEvent event) { 896 // Handle all recent motion events so we don't skip any cells even when the device 897 // is busy... 898 final float radius = mPathWidth; 899 final int historySize = event.getHistorySize(); 900 mTmpInvalidateRect.setEmpty(); 901 boolean invalidateNow = false; 902 for (int i = 0; i < historySize + 1; i++) { 903 final float x = i < historySize ? event.getHistoricalX(i) : event.getX(); 904 final float y = i < historySize ? event.getHistoricalY(i) : event.getY(); 905 Cell hitCell = detectAndAddHit(x, y); 906 final int patternSize = mPattern.size(); 907 if (hitCell != null && patternSize == 1) { 908 mPatternInProgress = true; 909 notifyPatternStarted(); 910 } 911 // note current x and y for rubber banding of in progress patterns 912 final float dx = Math.abs(x - mInProgressX); 913 final float dy = Math.abs(y - mInProgressY); 914 if (dx > DRAG_THRESHHOLD || dy > DRAG_THRESHHOLD) { 915 invalidateNow = true; 916 } 917 918 if (mPatternInProgress && patternSize > 0) { 919 final ArrayList<Cell> pattern = mPattern; 920 final Cell lastCell = pattern.get(patternSize - 1); 921 float lastCellCenterX = getCenterXForColumn(lastCell.column); 922 float lastCellCenterY = getCenterYForRow(lastCell.row); 923 924 // Adjust for drawn segment from last cell to (x,y). Radius accounts for line width. 925 float left = Math.min(lastCellCenterX, x) - radius; 926 float right = Math.max(lastCellCenterX, x) + radius; 927 float top = Math.min(lastCellCenterY, y) - radius; 928 float bottom = Math.max(lastCellCenterY, y) + radius; 929 930 // Invalidate between the pattern's new cell and the pattern's previous cell 931 if (hitCell != null) { 932 final float width = mSquareWidth * 0.5f; 933 final float height = mSquareHeight * 0.5f; 934 final float hitCellCenterX = getCenterXForColumn(hitCell.column); 935 final float hitCellCenterY = getCenterYForRow(hitCell.row); 936 937 left = Math.min(hitCellCenterX - width, left); 938 right = Math.max(hitCellCenterX + width, right); 939 top = Math.min(hitCellCenterY - height, top); 940 bottom = Math.max(hitCellCenterY + height, bottom); 941 } 942 943 // Invalidate between the pattern's last cell and the previous location 944 mTmpInvalidateRect.union(Math.round(left), Math.round(top), 945 Math.round(right), Math.round(bottom)); 946 } 947 } 948 mInProgressX = event.getX(); 949 mInProgressY = event.getY(); 950 951 // To save updates, we only invalidate if the user moved beyond a certain amount. 952 if (invalidateNow) { 953 mInvalidate.union(mTmpInvalidateRect); 954 invalidate(mInvalidate); 955 mInvalidate.set(mTmpInvalidateRect); 956 } 957 } 958 959 private void sendAccessEvent(int resId) { 960 announceForAccessibility(mContext.getString(resId)); 961 } 962 963 private void handleActionUp() { 964 // report pattern detected 965 if (!mPattern.isEmpty()) { 966 mPatternInProgress = false; 967 cancelLineAnimations(); 968 notifyPatternDetected(); 969 invalidate(); 970 } 971 if (PROFILE_DRAWING) { 972 if (mDrawingProfilingStarted) { 973 Debug.stopMethodTracing(); 974 mDrawingProfilingStarted = false; 975 } 976 } 977 } 978 979 private void cancelLineAnimations() { 980 for (int i = 0; i < 3; i++) { 981 for (int j = 0; j < 3; j++) { 982 CellState state = mCellStates[i][j]; 983 if (state.lineAnimator != null) { 984 state.lineAnimator.cancel(); 985 state.lineEndX = Float.MIN_VALUE; 986 state.lineEndY = Float.MIN_VALUE; 987 } 988 } 989 } 990 } 991 private void handleActionDown(MotionEvent event) { 992 resetPattern(); 993 final float x = event.getX(); 994 final float y = event.getY(); 995 final Cell hitCell = detectAndAddHit(x, y); 996 if (hitCell != null) { 997 mPatternInProgress = true; 998 mPatternDisplayMode = DisplayMode.Correct; 999 notifyPatternStarted(); 1000 } else if (mPatternInProgress) { 1001 mPatternInProgress = false; 1002 notifyPatternCleared(); 1003 } 1004 if (hitCell != null) { 1005 final float startX = getCenterXForColumn(hitCell.column); 1006 final float startY = getCenterYForRow(hitCell.row); 1007 1008 final float widthOffset = mSquareWidth / 2f; 1009 final float heightOffset = mSquareHeight / 2f; 1010 1011 invalidate((int) (startX - widthOffset), (int) (startY - heightOffset), 1012 (int) (startX + widthOffset), (int) (startY + heightOffset)); 1013 } 1014 mInProgressX = x; 1015 mInProgressY = y; 1016 if (PROFILE_DRAWING) { 1017 if (!mDrawingProfilingStarted) { 1018 Debug.startMethodTracing("LockPatternDrawing"); 1019 mDrawingProfilingStarted = true; 1020 } 1021 } 1022 } 1023 1024 private float getCenterXForColumn(int column) { 1025 return mPaddingLeft + column * mSquareWidth + mSquareWidth / 2f; 1026 } 1027 1028 private float getCenterYForRow(int row) { 1029 return mPaddingTop + row * mSquareHeight + mSquareHeight / 2f; 1030 } 1031 1032 @Override 1033 protected void onDraw(Canvas canvas) { 1034 final ArrayList<Cell> pattern = mPattern; 1035 final int count = pattern.size(); 1036 final boolean[][] drawLookup = mPatternDrawLookup; 1037 1038 if (mPatternDisplayMode == DisplayMode.Animate) { 1039 1040 // figure out which circles to draw 1041 1042 // + 1 so we pause on complete pattern 1043 final int oneCycle = (count + 1) * MILLIS_PER_CIRCLE_ANIMATING; 1044 final int spotInCycle = (int) (SystemClock.elapsedRealtime() - 1045 mAnimatingPeriodStart) % oneCycle; 1046 final int numCircles = spotInCycle / MILLIS_PER_CIRCLE_ANIMATING; 1047 1048 clearPatternDrawLookup(); 1049 for (int i = 0; i < numCircles; i++) { 1050 final Cell cell = pattern.get(i); 1051 drawLookup[cell.getRow()][cell.getColumn()] = true; 1052 } 1053 1054 // figure out in progress portion of ghosting line 1055 1056 final boolean needToUpdateInProgressPoint = numCircles > 0 1057 && numCircles < count; 1058 1059 if (needToUpdateInProgressPoint) { 1060 final float percentageOfNextCircle = 1061 ((float) (spotInCycle % MILLIS_PER_CIRCLE_ANIMATING)) / 1062 MILLIS_PER_CIRCLE_ANIMATING; 1063 1064 final Cell currentCell = pattern.get(numCircles - 1); 1065 final float centerX = getCenterXForColumn(currentCell.column); 1066 final float centerY = getCenterYForRow(currentCell.row); 1067 1068 final Cell nextCell = pattern.get(numCircles); 1069 final float dx = percentageOfNextCircle * 1070 (getCenterXForColumn(nextCell.column) - centerX); 1071 final float dy = percentageOfNextCircle * 1072 (getCenterYForRow(nextCell.row) - centerY); 1073 mInProgressX = centerX + dx; 1074 mInProgressY = centerY + dy; 1075 } 1076 // TODO: Infinite loop here... 1077 invalidate(); 1078 } 1079 1080 final Path currentPath = mCurrentPath; 1081 currentPath.rewind(); 1082 1083 // draw the circles 1084 for (int i = 0; i < 3; i++) { 1085 float centerY = getCenterYForRow(i); 1086 for (int j = 0; j < 3; j++) { 1087 CellState cellState = mCellStates[i][j]; 1088 float centerX = getCenterXForColumn(j); 1089 float translationY = cellState.translationY; 1090 if (isHardwareAccelerated() && cellState.hwAnimating) { 1091 DisplayListCanvas displayListCanvas = (DisplayListCanvas) canvas; 1092 displayListCanvas.drawCircle(cellState.hwCenterX, cellState.hwCenterY, 1093 cellState.hwRadius, cellState.hwPaint); 1094 } else { 1095 drawCircle(canvas, (int) centerX, (int) centerY + translationY, 1096 cellState.radius, drawLookup[i][j], cellState.alpha); 1097 1098 } 1099 } 1100 } 1101 1102 // TODO: the path should be created and cached every time we hit-detect a cell 1103 // only the last segment of the path should be computed here 1104 // draw the path of the pattern (unless we are in stealth mode) 1105 final boolean drawPath = !mInStealthMode; 1106 1107 if (drawPath) { 1108 mPathPaint.setColor(getCurrentColor(true /* partOfPattern */)); 1109 1110 boolean anyCircles = false; 1111 float lastX = 0f; 1112 float lastY = 0f; 1113 for (int i = 0; i < count; i++) { 1114 Cell cell = pattern.get(i); 1115 1116 // only draw the part of the pattern stored in 1117 // the lookup table (this is only different in the case 1118 // of animation). 1119 if (!drawLookup[cell.row][cell.column]) { 1120 break; 1121 } 1122 anyCircles = true; 1123 1124 float centerX = getCenterXForColumn(cell.column); 1125 float centerY = getCenterYForRow(cell.row); 1126 if (i != 0) { 1127 CellState state = mCellStates[cell.row][cell.column]; 1128 currentPath.rewind(); 1129 currentPath.moveTo(lastX, lastY); 1130 if (state.lineEndX != Float.MIN_VALUE && state.lineEndY != Float.MIN_VALUE) { 1131 currentPath.lineTo(state.lineEndX, state.lineEndY); 1132 } else { 1133 currentPath.lineTo(centerX, centerY); 1134 } 1135 canvas.drawPath(currentPath, mPathPaint); 1136 } 1137 lastX = centerX; 1138 lastY = centerY; 1139 } 1140 1141 // draw last in progress section 1142 if ((mPatternInProgress || mPatternDisplayMode == DisplayMode.Animate) 1143 && anyCircles) { 1144 currentPath.rewind(); 1145 currentPath.moveTo(lastX, lastY); 1146 currentPath.lineTo(mInProgressX, mInProgressY); 1147 1148 mPathPaint.setAlpha((int) (calculateLastSegmentAlpha( 1149 mInProgressX, mInProgressY, lastX, lastY) * 255f)); 1150 canvas.drawPath(currentPath, mPathPaint); 1151 } 1152 } 1153 } 1154 1155 private float calculateLastSegmentAlpha(float x, float y, float lastX, float lastY) { 1156 float diffX = x - lastX; 1157 float diffY = y - lastY; 1158 float dist = (float) Math.sqrt(diffX*diffX + diffY*diffY); 1159 float frac = dist/mSquareWidth; 1160 return Math.min(1f, Math.max(0f, (frac - 0.3f) * 4f)); 1161 } 1162 1163 private int getCurrentColor(boolean partOfPattern) { 1164 if (!partOfPattern || mInStealthMode || mPatternInProgress) { 1165 // unselected circle 1166 return mRegularColor; 1167 } else if (mPatternDisplayMode == DisplayMode.Wrong) { 1168 // the pattern is wrong 1169 return mErrorColor; 1170 } else if (mPatternDisplayMode == DisplayMode.Correct || 1171 mPatternDisplayMode == DisplayMode.Animate) { 1172 return mSuccessColor; 1173 } else { 1174 throw new IllegalStateException("unknown display mode " + mPatternDisplayMode); 1175 } 1176 } 1177 1178 /** 1179 * @param partOfPattern Whether this circle is part of the pattern. 1180 */ 1181 private void drawCircle(Canvas canvas, float centerX, float centerY, float radius, 1182 boolean partOfPattern, float alpha) { 1183 mPaint.setColor(getCurrentColor(partOfPattern)); 1184 mPaint.setAlpha((int) (alpha * 255)); 1185 canvas.drawCircle(centerX, centerY, radius, mPaint); 1186 } 1187 1188 @Override 1189 protected Parcelable onSaveInstanceState() { 1190 Parcelable superState = super.onSaveInstanceState(); 1191 return new SavedState(superState, 1192 LockPatternUtils.patternToString(mPattern), 1193 mPatternDisplayMode.ordinal(), 1194 mInputEnabled, mInStealthMode, mEnableHapticFeedback); 1195 } 1196 1197 @Override 1198 protected void onRestoreInstanceState(Parcelable state) { 1199 final SavedState ss = (SavedState) state; 1200 super.onRestoreInstanceState(ss.getSuperState()); 1201 setPattern( 1202 DisplayMode.Correct, 1203 LockPatternUtils.stringToPattern(ss.getSerializedPattern())); 1204 mPatternDisplayMode = DisplayMode.values()[ss.getDisplayMode()]; 1205 mInputEnabled = ss.isInputEnabled(); 1206 mInStealthMode = ss.isInStealthMode(); 1207 mEnableHapticFeedback = ss.isTactileFeedbackEnabled(); 1208 } 1209 1210 /** 1211 * The parecelable for saving and restoring a lock pattern view. 1212 */ 1213 private static class SavedState extends BaseSavedState { 1214 1215 private final String mSerializedPattern; 1216 private final int mDisplayMode; 1217 private final boolean mInputEnabled; 1218 private final boolean mInStealthMode; 1219 private final boolean mTactileFeedbackEnabled; 1220 1221 /** 1222 * Constructor called from {@link LockPatternView#onSaveInstanceState()} 1223 */ 1224 private SavedState(Parcelable superState, String serializedPattern, int displayMode, 1225 boolean inputEnabled, boolean inStealthMode, boolean tactileFeedbackEnabled) { 1226 super(superState); 1227 mSerializedPattern = serializedPattern; 1228 mDisplayMode = displayMode; 1229 mInputEnabled = inputEnabled; 1230 mInStealthMode = inStealthMode; 1231 mTactileFeedbackEnabled = tactileFeedbackEnabled; 1232 } 1233 1234 /** 1235 * Constructor called from {@link #CREATOR} 1236 */ 1237 private SavedState(Parcel in) { 1238 super(in); 1239 mSerializedPattern = in.readString(); 1240 mDisplayMode = in.readInt(); 1241 mInputEnabled = (Boolean) in.readValue(null); 1242 mInStealthMode = (Boolean) in.readValue(null); 1243 mTactileFeedbackEnabled = (Boolean) in.readValue(null); 1244 } 1245 1246 public String getSerializedPattern() { 1247 return mSerializedPattern; 1248 } 1249 1250 public int getDisplayMode() { 1251 return mDisplayMode; 1252 } 1253 1254 public boolean isInputEnabled() { 1255 return mInputEnabled; 1256 } 1257 1258 public boolean isInStealthMode() { 1259 return mInStealthMode; 1260 } 1261 1262 public boolean isTactileFeedbackEnabled(){ 1263 return mTactileFeedbackEnabled; 1264 } 1265 1266 @Override 1267 public void writeToParcel(Parcel dest, int flags) { 1268 super.writeToParcel(dest, flags); 1269 dest.writeString(mSerializedPattern); 1270 dest.writeInt(mDisplayMode); 1271 dest.writeValue(mInputEnabled); 1272 dest.writeValue(mInStealthMode); 1273 dest.writeValue(mTactileFeedbackEnabled); 1274 } 1275 1276 @SuppressWarnings({ "unused", "hiding" }) // Found using reflection 1277 public static final Parcelable.Creator<SavedState> CREATOR = 1278 new Creator<SavedState>() { 1279 @Override 1280 public SavedState createFromParcel(Parcel in) { 1281 return new SavedState(in); 1282 } 1283 1284 @Override 1285 public SavedState[] newArray(int size) { 1286 return new SavedState[size]; 1287 } 1288 }; 1289 } 1290 1291 private final class PatternExploreByTouchHelper extends ExploreByTouchHelper { 1292 private Rect mTempRect = new Rect(); 1293 private HashMap<Integer, VirtualViewContainer> mItems = new HashMap<Integer, 1294 VirtualViewContainer>(); 1295 1296 class VirtualViewContainer { 1297 public VirtualViewContainer(CharSequence description) { 1298 this.description = description; 1299 } 1300 CharSequence description; 1301 }; 1302 1303 public PatternExploreByTouchHelper(View forView) { 1304 super(forView); 1305 } 1306 1307 @Override 1308 protected int getVirtualViewAt(float x, float y) { 1309 // This must use the same hit logic for the screen to ensure consistency whether 1310 // accessibility is on or off. 1311 int id = getVirtualViewIdForHit(x, y); 1312 return id; 1313 } 1314 1315 @Override 1316 protected void getVisibleVirtualViews(IntArray virtualViewIds) { 1317 if (DEBUG_A11Y) Log.v(TAG, "getVisibleVirtualViews(len=" + virtualViewIds.size() + ")"); 1318 for (int i = VIRTUAL_BASE_VIEW_ID; i < VIRTUAL_BASE_VIEW_ID + 9; i++) { 1319 if (!mItems.containsKey(i)) { 1320 VirtualViewContainer item = new VirtualViewContainer(getTextForVirtualView(i)); 1321 mItems.put(i, item); 1322 } 1323 // Add all views. As views are added to the pattern, we remove them 1324 // from notification by making them non-clickable below. 1325 virtualViewIds.add(i); 1326 } 1327 } 1328 1329 @Override 1330 protected void onPopulateEventForVirtualView(int virtualViewId, AccessibilityEvent event) { 1331 if (DEBUG_A11Y) Log.v(TAG, "onPopulateEventForVirtualView(" + virtualViewId + ")"); 1332 // Announce this view 1333 if (mItems.containsKey(virtualViewId)) { 1334 CharSequence contentDescription = mItems.get(virtualViewId).description; 1335 event.getText().add(contentDescription); 1336 } 1337 } 1338 1339 @Override 1340 protected void onPopulateNodeForVirtualView(int virtualViewId, AccessibilityNodeInfo node) { 1341 if (DEBUG_A11Y) Log.v(TAG, "onPopulateNodeForVirtualView(view=" + virtualViewId + ")"); 1342 1343 // Node and event text and content descriptions are usually 1344 // identical, so we'll use the exact same string as before. 1345 node.setText(getTextForVirtualView(virtualViewId)); 1346 node.setContentDescription(getTextForVirtualView(virtualViewId)); 1347 1348 if (isClickable(virtualViewId)) { 1349 // Mark this node of interest by making it clickable. 1350 node.addAction(AccessibilityAction.ACTION_CLICK); 1351 node.setClickable(isClickable(virtualViewId)); 1352 } 1353 1354 // Compute bounds for this object 1355 final Rect bounds = getBoundsForVirtualView(virtualViewId); 1356 if (DEBUG_A11Y) Log.v(TAG, "bounds:" + bounds.toString()); 1357 node.setBoundsInParent(bounds); 1358 } 1359 1360 private boolean isClickable(int virtualViewId) { 1361 // Dots are clickable if they're not part of the current pattern. 1362 if (virtualViewId != ExploreByTouchHelper.INVALID_ID) { 1363 int row = (virtualViewId - VIRTUAL_BASE_VIEW_ID) / 3; 1364 int col = (virtualViewId - VIRTUAL_BASE_VIEW_ID) % 3; 1365 return !mPatternDrawLookup[row][col]; 1366 } 1367 return false; 1368 } 1369 1370 @Override 1371 protected boolean onPerformActionForVirtualView(int virtualViewId, int action, 1372 Bundle arguments) { 1373 if (DEBUG_A11Y) Log.v(TAG, "onPerformActionForVirtualView(id=" + virtualViewId 1374 + ", action=" + action); 1375 switch (action) { 1376 case AccessibilityNodeInfo.ACTION_CLICK: 1377 // Click handling should be consistent with 1378 // onTouchEvent(). This ensures that the view works the 1379 // same whether accessibility is turned on or off. 1380 return onItemClicked(virtualViewId); 1381 default: 1382 if (DEBUG_A11Y) Log.v(TAG, "*** action not handled in " 1383 + "onPerformActionForVirtualView(viewId=" 1384 + virtualViewId + "action=" + action + ")"); 1385 } 1386 return false; 1387 } 1388 1389 boolean onItemClicked(int index) { 1390 if (DEBUG_A11Y) Log.v(TAG, "onItemClicked(" + index + ")"); 1391 1392 // Since the item's checked state is exposed to accessibility 1393 // services through its AccessibilityNodeInfo, we need to invalidate 1394 // the item's virtual view. At some point in the future, the 1395 // framework will obtain an updated version of the virtual view. 1396 invalidateVirtualView(index); 1397 1398 // We need to let the framework know what type of event 1399 // happened. Accessibility services may use this event to provide 1400 // appropriate feedback to the user. 1401 sendEventForVirtualView(index, AccessibilityEvent.TYPE_VIEW_CLICKED); 1402 1403 return true; 1404 } 1405 1406 private Rect getBoundsForVirtualView(int virtualViewId) { 1407 int ordinal = virtualViewId - VIRTUAL_BASE_VIEW_ID; 1408 final Rect bounds = mTempRect; 1409 final int row = ordinal / 3; 1410 final int col = ordinal % 3; 1411 final CellState cell = mCellStates[row][col]; 1412 float centerX = getCenterXForColumn(col); 1413 float centerY = getCenterYForRow(row); 1414 float cellheight = mSquareHeight * mHitFactor * 0.5f; 1415 float cellwidth = mSquareWidth * mHitFactor * 0.5f; 1416 bounds.left = (int) (centerX - cellwidth); 1417 bounds.right = (int) (centerX + cellwidth); 1418 bounds.top = (int) (centerY - cellheight); 1419 bounds.bottom = (int) (centerY + cellheight); 1420 return bounds; 1421 } 1422 1423 private boolean shouldSpeakPassword() { 1424 final boolean speakPassword = Settings.Secure.getIntForUser( 1425 mContext.getContentResolver(), Settings.Secure.ACCESSIBILITY_SPEAK_PASSWORD, 0, 1426 UserHandle.USER_CURRENT_OR_SELF) != 0; 1427 final boolean hasHeadphones = mAudioManager != null ? 1428 (mAudioManager.isWiredHeadsetOn() || mAudioManager.isBluetoothA2dpOn()) 1429 : false; 1430 return speakPassword || hasHeadphones; 1431 } 1432 1433 private CharSequence getTextForVirtualView(int virtualViewId) { 1434 final Resources res = getResources(); 1435 return shouldSpeakPassword() ? res.getString( 1436 R.string.lockscreen_access_pattern_cell_added_verbose, virtualViewId) 1437 : res.getString(R.string.lockscreen_access_pattern_cell_added); 1438 } 1439 1440 /** 1441 * Helper method to find which cell a point maps to 1442 * 1443 * if there's no hit. 1444 * @param x touch position x 1445 * @param y touch position y 1446 * @return VIRTUAL_BASE_VIEW_ID+id or 0 if no view was hit 1447 */ 1448 private int getVirtualViewIdForHit(float x, float y) { 1449 final int rowHit = getRowHit(y); 1450 if (rowHit < 0) { 1451 return ExploreByTouchHelper.INVALID_ID; 1452 } 1453 final int columnHit = getColumnHit(x); 1454 if (columnHit < 0) { 1455 return ExploreByTouchHelper.INVALID_ID; 1456 } 1457 boolean dotAvailable = mPatternDrawLookup[rowHit][columnHit]; 1458 int dotId = (rowHit * 3 + columnHit) + VIRTUAL_BASE_VIEW_ID; 1459 int view = dotAvailable ? dotId : ExploreByTouchHelper.INVALID_ID; 1460 if (DEBUG_A11Y) Log.v(TAG, "getVirtualViewIdForHit(" + x + "," + y + ") => " 1461 + view + "avail =" + dotAvailable); 1462 return view; 1463 } 1464 } 1465} 1466