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