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