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