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