/* * Copyright (C) 2007 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.internal.widget; import android.animation.Animator; import android.animation.AnimatorListenerAdapter; import android.animation.AnimatorSet; import android.animation.ValueAnimator; import android.annotation.Nullable; import android.compat.annotation.UnsupportedAppUsage; import android.content.Context; import android.content.res.Resources; import android.content.res.TypedArray; import android.graphics.Canvas; import android.graphics.CanvasProperty; import android.graphics.Color; import android.graphics.LinearGradient; import android.graphics.Paint; import android.graphics.Path; import android.graphics.RecordingCanvas; import android.graphics.Rect; import android.graphics.Shader; import android.graphics.drawable.Drawable; import android.os.Bundle; import android.os.Debug; import android.os.Parcel; import android.os.Parcelable; import android.os.SystemClock; import android.util.AttributeSet; import android.util.IntArray; import android.util.Log; import android.util.SparseArray; import android.util.TypedValue; import android.view.HapticFeedbackConstants; import android.view.MotionEvent; import android.view.RenderNodeAnimator; import android.view.View; import android.view.accessibility.AccessibilityEvent; import android.view.accessibility.AccessibilityManager; import android.view.accessibility.AccessibilityNodeInfo; import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction; import android.view.animation.AnimationUtils; import android.view.animation.Interpolator; import com.android.internal.R; import com.android.internal.graphics.ColorUtils; import java.util.ArrayList; import java.util.List; /** * Displays and detects the user's unlock attempt, which is a drag of a finger * across 9 regions of the screen. * * Is also capable of displaying a static pattern in "in progress", "wrong" or * "correct" states. */ public class LockPatternView extends View { // Aspect to use when rendering this view private static final int ASPECT_SQUARE = 0; // View will be the minimum of width/height private static final int ASPECT_LOCK_WIDTH = 1; // Fixed width; height will be minimum of (w,h) private static final int ASPECT_LOCK_HEIGHT = 2; // Fixed height; width will be minimum of (w,h) private static final boolean PROFILE_DRAWING = false; private static final int LINE_END_ANIMATION_DURATION_MILLIS = 50; private static final int DOT_ACTIVATION_DURATION_MILLIS = 50; private static final int DOT_RADIUS_INCREASE_DURATION_MILLIS = 96; private static final int DOT_RADIUS_DECREASE_DURATION_MILLIS = 192; private static final int ALPHA_MAX_VALUE = 255; private static final float MIN_DOT_HIT_FACTOR = 0.2f; private final CellState[][] mCellStates; private static final int CELL_ACTIVATE = 0; private static final int CELL_DEACTIVATE = 1; private final int mDotSize; private final int mDotSizeActivated; private final float mDotHitFactor; private final int mPathWidth; private final int mLineFadeOutAnimationDurationMs; private final int mLineFadeOutAnimationDelayMs; private final int mFadePatternAnimationDurationMs; private final int mFadePatternAnimationDelayMs; private boolean mDrawingProfilingStarted = false; @UnsupportedAppUsage private final Paint mPaint = new Paint(); @UnsupportedAppUsage private final Paint mPathPaint = new Paint(); /** * How many milliseconds we spend animating each circle of a lock pattern * if the animating mode is set. The entire animation should take this * constant * the length of the pattern to complete. */ private static final int MILLIS_PER_CIRCLE_ANIMATING = 700; /** * This can be used to avoid updating the display for very small motions or noisy panels. * It didn't seem to have much impact on the devices tested, so currently set to 0. */ private static final float DRAG_THRESHHOLD = 0.0f; public static final int VIRTUAL_BASE_VIEW_ID = 1; public static final boolean DEBUG_A11Y = false; private static final String TAG = "LockPatternView"; private OnPatternListener mOnPatternListener; @UnsupportedAppUsage private final ArrayList mPattern = new ArrayList(9); /** * Lookup table for the circles of the pattern we are currently drawing. * This will be the cells of the complete pattern unless we are animating, * in which case we use this to hold the cells we are drawing for the in * progress animation. */ private final boolean[][] mPatternDrawLookup = new boolean[3][3]; /** * the in progress point: * - during interaction: where the user's finger is * - during animation: the current tip of the animating line */ private float mInProgressX = -1; private float mInProgressY = -1; private long mAnimatingPeriodStart; private long[] mLineFadeStart = new long[9]; @UnsupportedAppUsage private DisplayMode mPatternDisplayMode = DisplayMode.Correct; private boolean mInputEnabled = true; @UnsupportedAppUsage private boolean mInStealthMode = false; @UnsupportedAppUsage private boolean mPatternInProgress = false; private boolean mFadePattern = true; private boolean mFadeClear = false; private int mFadeAnimationAlpha = ALPHA_MAX_VALUE; private final Path mPatternPath = new Path(); @UnsupportedAppUsage private float mSquareWidth; @UnsupportedAppUsage private float mSquareHeight; private float mDotHitRadius; private float mDotHitMaxRadius; private final LinearGradient mFadeOutGradientShader; private final Path mCurrentPath = new Path(); private final Rect mInvalidate = new Rect(); private final Rect mTmpInvalidateRect = new Rect(); private int mAspect; private int mRegularColor; private int mErrorColor; private int mSuccessColor; private int mDotColor; private int mDotActivatedColor; private boolean mKeepDotActivated; private boolean mEnlargeVertex; private final Interpolator mFastOutSlowInInterpolator; private final Interpolator mLinearOutSlowInInterpolator; private final Interpolator mStandardAccelerateInterpolator; private final PatternExploreByTouchHelper mExploreByTouchHelper; private Drawable mSelectedDrawable; private Drawable mNotSelectedDrawable; private boolean mUseLockPatternDrawable; /** * Represents a cell in the 3 X 3 matrix of the unlock pattern view. */ public static final class Cell { @UnsupportedAppUsage final int row; @UnsupportedAppUsage final int column; // keep # objects limited to 9 private static final Cell[][] sCells = createCells(); private static Cell[][] createCells() { Cell[][] res = new Cell[3][3]; for (int i = 0; i < 3; i++) { for (int j = 0; j < 3; j++) { res[i][j] = new Cell(i, j); } } return res; } /** * @param row The row of the cell. * @param column The column of the cell. */ private Cell(int row, int column) { checkRange(row, column); this.row = row; this.column = column; } public int getRow() { return row; } public int getColumn() { return column; } public static Cell of(int row, int column) { checkRange(row, column); return sCells[row][column]; } private static void checkRange(int row, int column) { if (row < 0 || row > 2) { throw new IllegalArgumentException("row must be in range 0-2"); } if (column < 0 || column > 2) { throw new IllegalArgumentException("column must be in range 0-2"); } } @Override public String toString() { return "(row=" + row + ",clmn=" + column + ")"; } } public static class CellState { int row; int col; boolean hwAnimating; CanvasProperty hwRadius; CanvasProperty hwCenterX; CanvasProperty hwCenterY; CanvasProperty hwPaint; float radius; float translationY; float alpha = 1f; float activationAnimationProgress; public float lineEndX = Float.MIN_VALUE; public float lineEndY = Float.MIN_VALUE; @Nullable Animator activationAnimator; } /** * How to display the current pattern. */ public enum DisplayMode { /** * The pattern drawn is correct (i.e draw it in a friendly color) */ @UnsupportedAppUsage Correct, /** * Animate the pattern (for demo, and help). */ @UnsupportedAppUsage Animate, /** * The pattern is wrong (i.e draw a foreboding color) */ @UnsupportedAppUsage Wrong } /** * The call back interface for detecting patterns entered by the user. */ public static interface OnPatternListener { /** * A new pattern has begun. */ void onPatternStart(); /** * The pattern was cleared. */ void onPatternCleared(); /** * The user extended the pattern currently being drawn by one cell. * @param pattern The pattern with newly added cell. */ void onPatternCellAdded(List pattern); /** * A pattern was detected from the user. * @param pattern The pattern. */ void onPatternDetected(List pattern); } public LockPatternView(Context context) { this(context, null); } @UnsupportedAppUsage public LockPatternView(Context context, AttributeSet attrs) { super(context, attrs); TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.LockPatternView, R.attr.lockPatternStyle, R.style.Widget_LockPatternView); final String aspect = a.getString(R.styleable.LockPatternView_aspect); if ("square".equals(aspect)) { mAspect = ASPECT_SQUARE; } else if ("lock_width".equals(aspect)) { mAspect = ASPECT_LOCK_WIDTH; } else if ("lock_height".equals(aspect)) { mAspect = ASPECT_LOCK_HEIGHT; } else { mAspect = ASPECT_SQUARE; } setClickable(true); mPathPaint.setAntiAlias(true); mPathPaint.setDither(true); mRegularColor = a.getColor(R.styleable.LockPatternView_regularColor, 0); mErrorColor = a.getColor(R.styleable.LockPatternView_errorColor, 0); mSuccessColor = a.getColor(R.styleable.LockPatternView_successColor, 0); mDotColor = a.getColor(R.styleable.LockPatternView_dotColor, mRegularColor); mDotActivatedColor = a.getColor(R.styleable.LockPatternView_dotActivatedColor, mDotColor); mKeepDotActivated = a.getBoolean(R.styleable.LockPatternView_keepDotActivated, false); mEnlargeVertex = a.getBoolean(R.styleable.LockPatternView_enlargeVertexEntryArea, false); int pathColor = a.getColor(R.styleable.LockPatternView_pathColor, mRegularColor); mPathPaint.setColor(pathColor); mPathPaint.setStyle(Paint.Style.STROKE); mPathPaint.setStrokeJoin(Paint.Join.ROUND); mPathPaint.setStrokeCap(Paint.Cap.ROUND); mPathWidth = getResources().getDimensionPixelSize(R.dimen.lock_pattern_dot_line_width); mPathPaint.setStrokeWidth(mPathWidth); mLineFadeOutAnimationDurationMs = getResources().getInteger(R.integer.lock_pattern_line_fade_out_duration); mLineFadeOutAnimationDelayMs = getResources().getInteger(R.integer.lock_pattern_line_fade_out_delay); mFadePatternAnimationDurationMs = getResources().getInteger(R.integer.lock_pattern_fade_pattern_duration); mFadePatternAnimationDelayMs = getResources().getInteger(R.integer.lock_pattern_fade_pattern_delay); mDotSize = getResources().getDimensionPixelSize(R.dimen.lock_pattern_dot_size); mDotSizeActivated = getResources().getDimensionPixelSize( R.dimen.lock_pattern_dot_size_activated); TypedValue outValue = new TypedValue(); getResources().getValue(R.dimen.lock_pattern_dot_hit_factor, outValue, true); mDotHitFactor = Math.max(Math.min(outValue.getFloat(), 1f), MIN_DOT_HIT_FACTOR); mUseLockPatternDrawable = getResources().getBoolean(R.bool.use_lock_pattern_drawable); if (mUseLockPatternDrawable) { mSelectedDrawable = getResources().getDrawable(R.drawable.lockscreen_selected); mNotSelectedDrawable = getResources().getDrawable(R.drawable.lockscreen_notselected); } mPaint.setAntiAlias(true); mPaint.setDither(true); mCellStates = new CellState[3][3]; for (int i = 0; i < 3; i++) { for (int j = 0; j < 3; j++) { mCellStates[i][j] = new CellState(); mCellStates[i][j].radius = mDotSize/2; mCellStates[i][j].row = i; mCellStates[i][j].col = j; } } mFastOutSlowInInterpolator = AnimationUtils.loadInterpolator(context, android.R.interpolator.fast_out_slow_in); mLinearOutSlowInInterpolator = AnimationUtils.loadInterpolator(context, android.R.interpolator.linear_out_slow_in); mStandardAccelerateInterpolator = AnimationUtils.loadInterpolator(context, android.R.interpolator.fast_out_linear_in); mExploreByTouchHelper = new PatternExploreByTouchHelper(this); setAccessibilityDelegate(mExploreByTouchHelper); int fadeAwayGradientWidth = getResources().getDimensionPixelSize( R.dimen.lock_pattern_fade_away_gradient_width); // Set up gradient shader with the middle in point (0, 0). mFadeOutGradientShader = new LinearGradient(/* x0= */ -fadeAwayGradientWidth / 2f, /* y0= */ 0,/* x1= */ fadeAwayGradientWidth / 2f, /* y1= */ 0, Color.TRANSPARENT, pathColor, Shader.TileMode.CLAMP); a.recycle(); } @UnsupportedAppUsage public CellState[][] getCellStates() { return mCellStates; } /** * @return Whether the view is in stealth mode. */ public boolean isInStealthMode() { return mInStealthMode; } /** * Set whether the view is in stealth mode. If true, there will be no * visible feedback as the user enters the pattern. * * @param inStealthMode Whether in stealth mode. */ @UnsupportedAppUsage public void setInStealthMode(boolean inStealthMode) { mInStealthMode = inStealthMode; } /** * Set whether the pattern should fade as it's being drawn. If * true, each segment of the pattern fades over time. */ public void setFadePattern(boolean fadePattern) { mFadePattern = fadePattern; } /** * Set the call back for pattern detection. * @param onPatternListener The call back. */ @UnsupportedAppUsage public void setOnPatternListener( OnPatternListener onPatternListener) { mOnPatternListener = onPatternListener; } /** * Set the pattern explicitely (rather than waiting for the user to input * a pattern). * @param displayMode How to display the pattern. * @param pattern The pattern. */ public void setPattern(DisplayMode displayMode, List pattern) { mPattern.clear(); mPattern.addAll(pattern); clearPatternDrawLookup(); for (Cell cell : pattern) { mPatternDrawLookup[cell.getRow()][cell.getColumn()] = true; } setDisplayMode(displayMode); } /** * Set the display mode of the current pattern. This can be useful, for * instance, after detecting a pattern to tell this view whether change the * in progress result to correct or wrong. * @param displayMode The display mode. */ @UnsupportedAppUsage public void setDisplayMode(DisplayMode displayMode) { mPatternDisplayMode = displayMode; if (displayMode == DisplayMode.Animate) { if (mPattern.size() == 0) { throw new IllegalStateException("you must have a pattern to " + "animate if you want to set the display mode to animate"); } mAnimatingPeriodStart = SystemClock.elapsedRealtime(); final Cell first = mPattern.get(0); mInProgressX = getCenterXForColumn(first.getColumn()); mInProgressY = getCenterYForRow(first.getRow()); clearPatternDrawLookup(); } invalidate(); } public void startCellStateAnimation(CellState cellState, float startAlpha, float endAlpha, float startTranslationY, float endTranslationY, float startScale, float endScale, long delay, long duration, Interpolator interpolator, Runnable finishRunnable) { if (isHardwareAccelerated()) { startCellStateAnimationHw(cellState, startAlpha, endAlpha, startTranslationY, endTranslationY, startScale, endScale, delay, duration, interpolator, finishRunnable); } else { startCellStateAnimationSw(cellState, startAlpha, endAlpha, startTranslationY, endTranslationY, startScale, endScale, delay, duration, interpolator, finishRunnable); } } private void 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) { cellState.alpha = startAlpha; cellState.translationY = startTranslationY; cellState.radius = mDotSize/2 * startScale; ValueAnimator animator = ValueAnimator.ofFloat(0f, 1f); animator.setDuration(duration); animator.setStartDelay(delay); animator.setInterpolator(interpolator); animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator animation) { float t = (float) animation.getAnimatedValue(); cellState.alpha = (1 - t) * startAlpha + t * endAlpha; cellState.translationY = (1 - t) * startTranslationY + t * endTranslationY; cellState.radius = mDotSize/2 * ((1 - t) * startScale + t * endScale); invalidate(); } }); animator.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { if (finishRunnable != null) { finishRunnable.run(); } } }); animator.start(); } private void 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) { cellState.alpha = endAlpha; cellState.translationY = endTranslationY; cellState.radius = mDotSize/2 * endScale; cellState.hwAnimating = true; cellState.hwCenterY = CanvasProperty.createFloat( getCenterYForRow(cellState.row) + startTranslationY); cellState.hwCenterX = CanvasProperty.createFloat(getCenterXForColumn(cellState.col)); cellState.hwRadius = CanvasProperty.createFloat(mDotSize/2 * startScale); mPaint.setColor(getDotColor()); mPaint.setAlpha((int) (startAlpha * 255)); cellState.hwPaint = CanvasProperty.createPaint(new Paint(mPaint)); startRtFloatAnimation(cellState.hwCenterY, getCenterYForRow(cellState.row) + endTranslationY, delay, duration, interpolator); startRtFloatAnimation(cellState.hwRadius, mDotSize/2 * endScale, delay, duration, interpolator); startRtAlphaAnimation(cellState, endAlpha, delay, duration, interpolator, new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { cellState.hwAnimating = false; if (finishRunnable != null) { finishRunnable.run(); } } }); invalidate(); } private void startRtAlphaAnimation(CellState cellState, float endAlpha, long delay, long duration, Interpolator interpolator, Animator.AnimatorListener listener) { RenderNodeAnimator animator = new RenderNodeAnimator(cellState.hwPaint, RenderNodeAnimator.PAINT_ALPHA, (int) (endAlpha * 255)); animator.setDuration(duration); animator.setStartDelay(delay); animator.setInterpolator(interpolator); animator.setTarget(this); animator.addListener(listener); animator.start(); } private void startRtFloatAnimation(CanvasProperty property, float endValue, long delay, long duration, Interpolator interpolator) { RenderNodeAnimator animator = new RenderNodeAnimator(property, endValue); animator.setDuration(duration); animator.setStartDelay(delay); animator.setInterpolator(interpolator); animator.setTarget(this); animator.start(); } private void notifyCellAdded() { // sendAccessEvent(R.string.lockscreen_access_pattern_cell_added); if (mOnPatternListener != null) { mOnPatternListener.onPatternCellAdded(mPattern); } // Disable used cells for accessibility as they get added if (DEBUG_A11Y) Log.v(TAG, "ivnalidating root because cell was added."); mExploreByTouchHelper.invalidateRoot(); } private void notifyPatternStarted() { sendAccessEvent(R.string.lockscreen_access_pattern_start); if (mOnPatternListener != null) { mOnPatternListener.onPatternStart(); } } @UnsupportedAppUsage private void notifyPatternDetected() { sendAccessEvent(R.string.lockscreen_access_pattern_detected); if (mOnPatternListener != null) { mOnPatternListener.onPatternDetected(mPattern); } } private void notifyPatternCleared() { sendAccessEvent(R.string.lockscreen_access_pattern_cleared); if (mOnPatternListener != null) { mOnPatternListener.onPatternCleared(); } } /** * Clear the pattern. */ @UnsupportedAppUsage public void clearPattern() { resetPattern(); } /** * Clear the pattern by fading it out. */ @UnsupportedAppUsage public void fadeClearPattern() { mFadeClear = true; startFadePatternAnimation(); } @Override protected boolean dispatchHoverEvent(MotionEvent event) { // Dispatch to onHoverEvent first so mPatternInProgress is up to date when the // helper gets the event. boolean handled = super.dispatchHoverEvent(event); handled |= mExploreByTouchHelper.dispatchHoverEvent(event); return handled; } /** * Reset all pattern state. */ private void resetPattern() { if (mKeepDotActivated && !mPattern.isEmpty()) { resetLastActivatedCellProgress(); } mPattern.clear(); mPatternPath.reset(); clearPatternDrawLookup(); mPatternDisplayMode = DisplayMode.Correct; invalidate(); } private void resetLastActivatedCellProgress() { final ArrayList pattern = mPattern; final Cell lastCell = pattern.get(pattern.size() - 1); final CellState cellState = mCellStates[lastCell.row][lastCell.column]; if (cellState.activationAnimator != null) { cellState.activationAnimator.cancel(); } cellState.activationAnimationProgress = 0f; } /** * If there are any cells being drawn. */ public boolean isEmpty() { return mPattern.isEmpty(); } /** * Clear the pattern lookup table. Also reset the line fade start times for * the next attempt. */ private void clearPatternDrawLookup() { for (int i = 0; i < 3; i++) { for (int j = 0; j < 3; j++) { mPatternDrawLookup[i][j] = false; mLineFadeStart[i+j*3] = 0; } } } /** * Disable input (for instance when displaying a message that will * timeout so user doesn't get view into messy state). */ @UnsupportedAppUsage public void disableInput() { mInputEnabled = false; } /** * Enable input. */ @UnsupportedAppUsage public void enableInput() { mInputEnabled = true; } @Override protected void onSizeChanged(int w, int h, int oldw, int oldh) { final int width = w - mPaddingLeft - mPaddingRight; mSquareWidth = width / 3.0f; if (DEBUG_A11Y) Log.v(TAG, "onSizeChanged(" + w + "," + h + ")"); final int height = h - mPaddingTop - mPaddingBottom; mSquareHeight = height / 3.0f; mExploreByTouchHelper.invalidateRoot(); mDotHitMaxRadius = Math.min(mSquareHeight / 2, mSquareWidth / 2); mDotHitRadius = mDotHitMaxRadius * mDotHitFactor; if (mUseLockPatternDrawable) { mNotSelectedDrawable.setBounds(mPaddingLeft, mPaddingTop, width, height); mSelectedDrawable.setBounds(mPaddingLeft, mPaddingTop, width, height); } } private int resolveMeasured(int measureSpec, int desired) { int result = 0; int specSize = MeasureSpec.getSize(measureSpec); switch (MeasureSpec.getMode(measureSpec)) { case MeasureSpec.UNSPECIFIED: result = desired; break; case MeasureSpec.AT_MOST: result = Math.max(specSize, desired); break; case MeasureSpec.EXACTLY: default: result = specSize; } return result; } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { final int minimumWidth = getSuggestedMinimumWidth(); final int minimumHeight = getSuggestedMinimumHeight(); int viewWidth = resolveMeasured(widthMeasureSpec, minimumWidth); int viewHeight = resolveMeasured(heightMeasureSpec, minimumHeight); switch (mAspect) { case ASPECT_SQUARE: viewWidth = viewHeight = Math.min(viewWidth, viewHeight); break; case ASPECT_LOCK_WIDTH: viewHeight = Math.min(viewWidth, viewHeight); break; case ASPECT_LOCK_HEIGHT: viewWidth = Math.min(viewWidth, viewHeight); break; } // Log.v(TAG, "LockPatternView dimensions: " + viewWidth + "x" + viewHeight); setMeasuredDimension(viewWidth, viewHeight); } /** * Determines whether the point x, y will add a new point to the current * pattern (in addition to finding the cell, also makes heuristic choices * such as filling in gaps based on current pattern). * @param x The x coordinate. * @param y The y coordinate. */ private Cell detectAndAddHit(float x, float y) { final Cell cell = checkForNewHit(x, y); if (cell != null) { // check for gaps in existing pattern Cell fillInGapCell = null; final ArrayList pattern = mPattern; Cell lastCell = null; if (!pattern.isEmpty()) { lastCell = pattern.get(pattern.size() - 1); int dRow = cell.row - lastCell.row; int dColumn = cell.column - lastCell.column; int fillInRow = lastCell.row; int fillInColumn = lastCell.column; if (Math.abs(dRow) == 2 && Math.abs(dColumn) != 1) { fillInRow = lastCell.row + ((dRow > 0) ? 1 : -1); } if (Math.abs(dColumn) == 2 && Math.abs(dRow) != 1) { fillInColumn = lastCell.column + ((dColumn > 0) ? 1 : -1); } fillInGapCell = Cell.of(fillInRow, fillInColumn); } if (fillInGapCell != null && !mPatternDrawLookup[fillInGapCell.row][fillInGapCell.column]) { addCellToPattern(fillInGapCell); if (mKeepDotActivated) { startCellDeactivatedAnimation(fillInGapCell); } } if (mKeepDotActivated && lastCell != null) { startCellDeactivatedAnimation(lastCell); } addCellToPattern(cell); performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY, HapticFeedbackConstants.FLAG_IGNORE_VIEW_SETTING); return cell; } return null; } private void addCellToPattern(Cell newCell) { mPatternDrawLookup[newCell.getRow()][newCell.getColumn()] = true; mPattern.add(newCell); if (!mInStealthMode) { startCellActivatedAnimation(newCell); } notifyCellAdded(); } private void startFadePatternAnimation() { AnimatorSet animatorSet = new AnimatorSet(); animatorSet.play(createFadePatternAnimation()); animatorSet.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { mFadeAnimationAlpha = ALPHA_MAX_VALUE; mFadeClear = false; resetPattern(); } }); animatorSet.start(); } private Animator createFadePatternAnimation() { ValueAnimator valueAnimator = ValueAnimator.ofInt(ALPHA_MAX_VALUE, 0); valueAnimator.addUpdateListener(animation -> { mFadeAnimationAlpha = (int) animation.getAnimatedValue(); invalidate(); }); valueAnimator.setInterpolator(mStandardAccelerateInterpolator); valueAnimator.setStartDelay(mFadePatternAnimationDelayMs); valueAnimator.setDuration(mFadePatternAnimationDurationMs); return valueAnimator; } private void startCellActivatedAnimation(Cell cell) { startCellActivationAnimation(cell, CELL_ACTIVATE); } private void startCellDeactivatedAnimation(Cell cell) { startCellActivationAnimation(cell, CELL_DEACTIVATE); } private void startCellActivationAnimation(Cell cell, int activate) { final CellState cellState = mCellStates[cell.row][cell.column]; if (cellState.activationAnimator != null) { cellState.activationAnimator.cancel(); } AnimatorSet animatorSet = new AnimatorSet(); // When running the line end animation (see doc for createLineEndAnimation), if cell is in: // - activate state - use finger position at the time of hit detection // - deactivate state - use current position where the end was last during initial animation // Note that deactivate state will only come if mKeepDotActivated is themed true. final float startX = activate == CELL_ACTIVATE ? mInProgressX : cellState.lineEndX; final float startY = activate == CELL_ACTIVATE ? mInProgressY : cellState.lineEndY; AnimatorSet.Builder animatorSetBuilder = animatorSet .play(createLineDisappearingAnimation()) .with(createLineEndAnimation(cellState, startX, startY, getCenterXForColumn(cell.column), getCenterYForRow(cell.row))); if (mDotSize != mDotSizeActivated) { animatorSetBuilder.with(createDotRadiusAnimation(cellState)); } if (mDotColor != mDotActivatedColor) { animatorSetBuilder.with(createDotActivationColorAnimation(cellState, activate)); } animatorSet.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { cellState.activationAnimator = null; invalidate(); } }); cellState.activationAnimator = animatorSet; animatorSet.start(); } private Animator createDotActivationColorAnimation(CellState cellState, int activate) { ValueAnimator.AnimatorUpdateListener updateListener = valueAnimator -> { cellState.activationAnimationProgress = (float) valueAnimator.getAnimatedValue(); invalidate(); }; ValueAnimator activateAnimator = ValueAnimator.ofFloat(0f, 1f); ValueAnimator deactivateAnimator = ValueAnimator.ofFloat(1f, 0f); activateAnimator.addUpdateListener(updateListener); deactivateAnimator.addUpdateListener(updateListener); activateAnimator.setInterpolator(mFastOutSlowInInterpolator); deactivateAnimator.setInterpolator(mLinearOutSlowInInterpolator); // Align dot animation duration with line fade out animation. activateAnimator.setDuration(DOT_ACTIVATION_DURATION_MILLIS); deactivateAnimator.setDuration(DOT_ACTIVATION_DURATION_MILLIS); AnimatorSet set = new AnimatorSet(); if (mKeepDotActivated) { set.play(activate == CELL_ACTIVATE ? activateAnimator : deactivateAnimator); } else { // 'activate' ignored in this case, do full deactivate -> activate cycle set.play(deactivateAnimator) .after(mLineFadeOutAnimationDelayMs + mLineFadeOutAnimationDurationMs - DOT_ACTIVATION_DURATION_MILLIS * 2) .after(activateAnimator); } return set; } /** * On the last frame before cell activates the end point of in progress line is not aligned * with dot center so we execute a short animation moving the end point to exact dot center. */ private Animator createLineEndAnimation(final CellState state, final float startX, final float startY, final float targetX, final float targetY) { ValueAnimator valueAnimator = ValueAnimator.ofFloat(0, 1); valueAnimator.addUpdateListener(animation -> { float t = (float) animation.getAnimatedValue(); state.lineEndX = (1 - t) * startX + t * targetX; state.lineEndY = (1 - t) * startY + t * targetY; invalidate(); }); valueAnimator.setInterpolator(mFastOutSlowInInterpolator); valueAnimator.setDuration(LINE_END_ANIMATION_DURATION_MILLIS); return valueAnimator; } /** * Starts animator to fade out a line segment. It does only invalidate because all the * transitions are applied in {@code onDraw} method. */ private Animator createLineDisappearingAnimation() { ValueAnimator valueAnimator = ValueAnimator.ofFloat(0, 1); valueAnimator.addUpdateListener(animation -> invalidate()); valueAnimator.setStartDelay(mLineFadeOutAnimationDelayMs); valueAnimator.setDuration(mLineFadeOutAnimationDurationMs); return valueAnimator; } private Animator createDotRadiusAnimation(CellState state) { float defaultRadius = mDotSize / 2f; float activatedRadius = mDotSizeActivated / 2f; ValueAnimator.AnimatorUpdateListener animatorUpdateListener = animation -> { state.radius = (float) animation.getAnimatedValue(); invalidate(); }; ValueAnimator activationAnimator = ValueAnimator.ofFloat(defaultRadius, activatedRadius); activationAnimator.addUpdateListener(animatorUpdateListener); activationAnimator.setInterpolator(mLinearOutSlowInInterpolator); activationAnimator.setDuration(DOT_RADIUS_INCREASE_DURATION_MILLIS); ValueAnimator deactivationAnimator = ValueAnimator.ofFloat(activatedRadius, defaultRadius); deactivationAnimator.addUpdateListener(animatorUpdateListener); deactivationAnimator.setInterpolator(mFastOutSlowInInterpolator); deactivationAnimator.setDuration(DOT_RADIUS_DECREASE_DURATION_MILLIS); AnimatorSet set = new AnimatorSet(); set.playSequentially(activationAnimator, deactivationAnimator); return set; } @Nullable private Cell checkForNewHit(float x, float y) { Cell cellHit = detectCellHit(x, y); if (cellHit != null && !mPatternDrawLookup[cellHit.row][cellHit.column]) { return cellHit; } return null; } /** Helper method to find which cell a point maps to. */ @Nullable private Cell detectCellHit(float x, float y) { for (int row = 0; row < 3; row++) { for (int column = 0; column < 3; column++) { float centerY = getCenterYForRow(row); float centerX = getCenterXForColumn(column); float hitRadiusSquared; if (mEnlargeVertex) { // Maximize vertex dots' hit radius for the small screen. // This eases users to draw more patterns with diagnal lines, while keeps // drawing patterns with vertex dots easy. hitRadiusSquared = isVertex(row, column) ? (mDotHitMaxRadius * mDotHitMaxRadius) : (mDotHitRadius * mDotHitRadius); } else { hitRadiusSquared = mDotHitRadius * mDotHitRadius; } if ((x - centerX) * (x - centerX) + (y - centerY) * (y - centerY) < hitRadiusSquared) { return Cell.of(row, column); } } } return null; } private boolean isVertex(int row, int column) { return !(row == 1 || column == 1); } @Override public boolean onHoverEvent(MotionEvent event) { if (AccessibilityManager.getInstance(mContext).isTouchExplorationEnabled()) { final int action = event.getAction(); switch (action) { case MotionEvent.ACTION_HOVER_ENTER: event.setAction(MotionEvent.ACTION_DOWN); break; case MotionEvent.ACTION_HOVER_MOVE: event.setAction(MotionEvent.ACTION_MOVE); break; case MotionEvent.ACTION_HOVER_EXIT: event.setAction(MotionEvent.ACTION_UP); break; } onTouchEvent(event); event.setAction(action); } return super.onHoverEvent(event); } @Override public boolean onTouchEvent(MotionEvent event) { if (!mInputEnabled || !isEnabled()) { return false; } switch(event.getAction()) { case MotionEvent.ACTION_DOWN: handleActionDown(event); return true; case MotionEvent.ACTION_UP: handleActionUp(); return true; case MotionEvent.ACTION_MOVE: handleActionMove(event); return true; case MotionEvent.ACTION_CANCEL: if (mPatternInProgress) { setPatternInProgress(false); resetPattern(); notifyPatternCleared(); } if (PROFILE_DRAWING) { if (mDrawingProfilingStarted) { Debug.stopMethodTracing(); mDrawingProfilingStarted = false; } } return true; } return false; } private void setPatternInProgress(boolean progress) { mPatternInProgress = progress; mExploreByTouchHelper.invalidateRoot(); } private void handleActionMove(MotionEvent event) { // Handle all recent motion events so we don't skip any cells even when the device // is busy... final float radius = mPathWidth; final int historySize = event.getHistorySize(); mTmpInvalidateRect.setEmpty(); boolean invalidateNow = false; for (int i = 0; i < historySize + 1; i++) { final float x = i < historySize ? event.getHistoricalX(i) : event.getX(); final float y = i < historySize ? event.getHistoricalY(i) : event.getY(); Cell hitCell = detectAndAddHit(x, y); final int patternSize = mPattern.size(); if (hitCell != null && patternSize == 1) { setPatternInProgress(true); notifyPatternStarted(); } // note current x and y for rubber banding of in progress patterns final float dx = Math.abs(x - mInProgressX); final float dy = Math.abs(y - mInProgressY); if (dx > DRAG_THRESHHOLD || dy > DRAG_THRESHHOLD) { invalidateNow = true; } if (mPatternInProgress && patternSize > 0) { final ArrayList pattern = mPattern; final Cell lastCell = pattern.get(patternSize - 1); float lastCellCenterX = getCenterXForColumn(lastCell.column); float lastCellCenterY = getCenterYForRow(lastCell.row); // Adjust for drawn segment from last cell to (x,y). Radius accounts for line width. float left = Math.min(lastCellCenterX, x) - radius; float right = Math.max(lastCellCenterX, x) + radius; float top = Math.min(lastCellCenterY, y) - radius; float bottom = Math.max(lastCellCenterY, y) + radius; // Invalidate between the pattern's new cell and the pattern's previous cell if (hitCell != null) { final float width = mSquareWidth * 0.5f; final float height = mSquareHeight * 0.5f; final float hitCellCenterX = getCenterXForColumn(hitCell.column); final float hitCellCenterY = getCenterYForRow(hitCell.row); left = Math.min(hitCellCenterX - width, left); right = Math.max(hitCellCenterX + width, right); top = Math.min(hitCellCenterY - height, top); bottom = Math.max(hitCellCenterY + height, bottom); } // Invalidate between the pattern's last cell and the previous location mTmpInvalidateRect.union(Math.round(left), Math.round(top), Math.round(right), Math.round(bottom)); } } mInProgressX = event.getX(); mInProgressY = event.getY(); // To save updates, we only invalidate if the user moved beyond a certain amount. if (invalidateNow) { mInvalidate.union(mTmpInvalidateRect); invalidate(mInvalidate); mInvalidate.set(mTmpInvalidateRect); } } private void sendAccessEvent(int resId) { announceForAccessibility(mContext.getString(resId)); } private void handleActionUp() { // report pattern detected if (!mPattern.isEmpty()) { setPatternInProgress(false); cancelLineAnimations(); if (mKeepDotActivated) { deactivateLastCell(); } notifyPatternDetected(); // Also clear pattern if fading is enabled if (mFadePattern) { clearPatternDrawLookup(); mPatternDisplayMode = DisplayMode.Correct; } invalidate(); } if (PROFILE_DRAWING) { if (mDrawingProfilingStarted) { Debug.stopMethodTracing(); mDrawingProfilingStarted = false; } } } private void deactivateLastCell() { Cell lastCell = mPattern.get(mPattern.size() - 1); startCellDeactivatedAnimation(lastCell); } private void cancelLineAnimations() { for (int i = 0; i < 3; i++) { for (int j = 0; j < 3; j++) { CellState state = mCellStates[i][j]; if (state.activationAnimator != null) { state.activationAnimator.cancel(); state.activationAnimator = null; state.radius = mDotSize / 2f; state.lineEndX = Float.MIN_VALUE; state.lineEndY = Float.MIN_VALUE; state.activationAnimationProgress = 0f; } } } } private void handleActionDown(MotionEvent event) { resetPattern(); final float x = event.getX(); final float y = event.getY(); final Cell hitCell = detectAndAddHit(x, y); if (hitCell != null) { setPatternInProgress(true); mPatternDisplayMode = DisplayMode.Correct; notifyPatternStarted(); } else if (mPatternInProgress) { setPatternInProgress(false); notifyPatternCleared(); } if (hitCell != null) { final float startX = getCenterXForColumn(hitCell.column); final float startY = getCenterYForRow(hitCell.row); final float widthOffset = mSquareWidth / 2f; final float heightOffset = mSquareHeight / 2f; invalidate((int) (startX - widthOffset), (int) (startY - heightOffset), (int) (startX + widthOffset), (int) (startY + heightOffset)); } mInProgressX = x; mInProgressY = y; if (PROFILE_DRAWING) { if (!mDrawingProfilingStarted) { Debug.startMethodTracing("LockPatternDrawing"); mDrawingProfilingStarted = true; } } } /** * Change theme colors * @param regularColor The dot color * @param successColor Color used when pattern is correct * @param errorColor Color used when authentication fails */ public void setColors(int regularColor, int successColor, int errorColor) { mRegularColor = regularColor; mErrorColor = errorColor; mSuccessColor = successColor; mPathPaint.setColor(regularColor); invalidate(); } private float getCenterXForColumn(int column) { return mPaddingLeft + column * mSquareWidth + mSquareWidth / 2f; } private float getCenterYForRow(int row) { return mPaddingTop + row * mSquareHeight + mSquareHeight / 2f; } @Override protected void onDraw(Canvas canvas) { final ArrayList pattern = mPattern; final int count = pattern.size(); final boolean[][] drawLookup = mPatternDrawLookup; if (mPatternDisplayMode == DisplayMode.Animate) { // figure out which circles to draw // + 1 so we pause on complete pattern final int oneCycle = (count + 1) * MILLIS_PER_CIRCLE_ANIMATING; final int spotInCycle = (int) (SystemClock.elapsedRealtime() - mAnimatingPeriodStart) % oneCycle; final int numCircles = spotInCycle / MILLIS_PER_CIRCLE_ANIMATING; clearPatternDrawLookup(); for (int i = 0; i < numCircles; i++) { final Cell cell = pattern.get(i); drawLookup[cell.getRow()][cell.getColumn()] = true; } // figure out in progress portion of ghosting line final boolean needToUpdateInProgressPoint = numCircles > 0 && numCircles < count; if (needToUpdateInProgressPoint) { final float percentageOfNextCircle = ((float) (spotInCycle % MILLIS_PER_CIRCLE_ANIMATING)) / MILLIS_PER_CIRCLE_ANIMATING; final Cell currentCell = pattern.get(numCircles - 1); final float centerX = getCenterXForColumn(currentCell.column); final float centerY = getCenterYForRow(currentCell.row); final Cell nextCell = pattern.get(numCircles); final float dx = percentageOfNextCircle * (getCenterXForColumn(nextCell.column) - centerX); final float dy = percentageOfNextCircle * (getCenterYForRow(nextCell.row) - centerY); mInProgressX = centerX + dx; mInProgressY = centerY + dy; } // TODO: Infinite loop here... invalidate(); } final Path currentPath = mCurrentPath; currentPath.rewind(); // TODO: the path should be created and cached every time we hit-detect a cell // only the last segment of the path should be computed here // draw the path of the pattern (unless we are in stealth mode) final boolean drawPath = !mInStealthMode; if (drawPath && !mFadeClear) { mPathPaint.setColor(getCurrentColor(true /* partOfPattern */)); boolean anyCircles = false; float lastX = 0f; float lastY = 0f; long elapsedRealtime = SystemClock.elapsedRealtime(); for (int i = 0; i < count; i++) { Cell cell = pattern.get(i); // only draw the part of the pattern stored in // the lookup table (this is only different in the case // of animation). if (!drawLookup[cell.row][cell.column]) { break; } anyCircles = true; if (mLineFadeStart[i] == 0) { mLineFadeStart[i] = SystemClock.elapsedRealtime(); } float centerX = getCenterXForColumn(cell.column); float centerY = getCenterYForRow(cell.row); if (i != 0) { CellState state = mCellStates[cell.row][cell.column]; currentPath.rewind(); float endX; float endY; if (state.lineEndX != Float.MIN_VALUE && state.lineEndY != Float.MIN_VALUE) { endX = state.lineEndX; endY = state.lineEndY; } else { endX = centerX; endY = centerY; } drawLineSegment(canvas, /* startX = */ lastX, /* startY = */ lastY, endX, endY, mLineFadeStart[i], elapsedRealtime); Path tempPath = new Path(); tempPath.moveTo(lastX, lastY); tempPath.lineTo(centerX, centerY); mPatternPath.addPath(tempPath); } lastX = centerX; lastY = centerY; } // draw last in progress section if ((mPatternInProgress || mPatternDisplayMode == DisplayMode.Animate) && anyCircles) { currentPath.rewind(); currentPath.moveTo(lastX, lastY); currentPath.lineTo(mInProgressX, mInProgressY); mPathPaint.setAlpha((int) (calculateLastSegmentAlpha( mInProgressX, mInProgressY, lastX, lastY) * 255f)); canvas.drawPath(currentPath, mPathPaint); } } if (mFadeClear) { mPathPaint.setAlpha(mFadeAnimationAlpha); canvas.drawPath(mPatternPath, mPathPaint); } // draw the circles for (int i = 0; i < 3; i++) { float centerY = getCenterYForRow(i); for (int j = 0; j < 3; j++) { CellState cellState = mCellStates[i][j]; float centerX = getCenterXForColumn(j); float translationY = cellState.translationY; if (mUseLockPatternDrawable) { drawCellDrawable(canvas, i, j, cellState.radius, drawLookup[i][j]); } else { if (isHardwareAccelerated() && cellState.hwAnimating) { RecordingCanvas recordingCanvas = (RecordingCanvas) canvas; recordingCanvas.drawCircle(cellState.hwCenterX, cellState.hwCenterY, cellState.hwRadius, cellState.hwPaint); } else { drawCircle(canvas, (int) centerX, (int) centerY + translationY, cellState.radius, drawLookup[i][j], cellState.alpha, cellState.activationAnimationProgress); } } } } } private void drawLineSegment(Canvas canvas, float startX, float startY, float endX, float endY, long lineFadeStart, long elapsedRealtime) { float fadeAwayProgress; if (mFadePattern) { if (elapsedRealtime - lineFadeStart >= mLineFadeOutAnimationDelayMs + mLineFadeOutAnimationDurationMs) { // Time for this segment animation is out so we don't need to draw it. return; } // Set this line segment to fade away animated. fadeAwayProgress = Math.max( ((float) (elapsedRealtime - lineFadeStart - mLineFadeOutAnimationDelayMs)) / mLineFadeOutAnimationDurationMs, 0f); drawFadingAwayLineSegment(canvas, startX, startY, endX, endY, fadeAwayProgress); } else { mPathPaint.setAlpha(255); canvas.drawLine(startX, startY, endX, endY, mPathPaint); } } private void drawFadingAwayLineSegment(Canvas canvas, float startX, float startY, float endX, float endY, float fadeAwayProgress) { mPathPaint.setAlpha((int) (255 * (1 - fadeAwayProgress))); // To draw gradient segment we use mFadeOutGradientShader which has immutable coordinates // thus we will need to translate and rotate the canvas. mPathPaint.setShader(mFadeOutGradientShader); canvas.save(); // First translate canvas to gradient middle point. float gradientMidX = endX * fadeAwayProgress + startX * (1 - fadeAwayProgress); float gradientMidY = endY * fadeAwayProgress + startY * (1 - fadeAwayProgress); canvas.translate(gradientMidX, gradientMidY); // Then rotate it to the direction of the segment. double segmentAngleRad = Math.atan((endY - startY) / (endX - startX)); float segmentAngleDegrees = (float) Math.toDegrees(segmentAngleRad); if (endX - startX < 0) { // Arc tangent gives us angle degrees [-90; 90] thus to cover [90; 270] degrees we // need this hack. segmentAngleDegrees += 180f; } canvas.rotate(segmentAngleDegrees); // Pythagoras theorem. float segmentLength = (float) Math.hypot(endX - startX, endY - startY); // Draw the segment in coordinates aligned with shader coordinates. canvas.drawLine(/* startX= */ -segmentLength * fadeAwayProgress, /* startY= */ 0,/* stopX= */ segmentLength * (1 - fadeAwayProgress), /* stopY= */ 0, mPathPaint); canvas.restore(); mPathPaint.setShader(null); } private float calculateLastSegmentAlpha(float x, float y, float lastX, float lastY) { float diffX = x - lastX; float diffY = y - lastY; float dist = (float) Math.sqrt(diffX*diffX + diffY*diffY); float frac = dist/mSquareWidth; return Math.min(1f, Math.max(0f, (frac - 0.3f) * 4f)); } private int getDotColor() { if (mInStealthMode) { // Always use the default color in this case return mDotColor; } else if (mPatternDisplayMode == DisplayMode.Wrong) { // the pattern is wrong return mErrorColor; } return mDotColor; } private int getCurrentColor(boolean partOfPattern) { if (!partOfPattern || mInStealthMode || mPatternInProgress) { // unselected circle return mRegularColor; } else if (mPatternDisplayMode == DisplayMode.Wrong) { // the pattern is wrong return mErrorColor; } else if (mPatternDisplayMode == DisplayMode.Correct || mPatternDisplayMode == DisplayMode.Animate) { return mSuccessColor; } else { throw new IllegalStateException("unknown display mode " + mPatternDisplayMode); } } /** * @param partOfPattern Whether this circle is part of the pattern. */ private void drawCircle(Canvas canvas, float centerX, float centerY, float radius, boolean partOfPattern, float alpha, float activationAnimationProgress) { if (mFadePattern && !mInStealthMode) { int resultColor = ColorUtils.blendARGB(mDotColor, mDotActivatedColor, /* ratio= */ activationAnimationProgress); mPaint.setColor(resultColor); } else if (!mFadePattern && partOfPattern){ mPaint.setColor(mDotActivatedColor); } else { mPaint.setColor(getDotColor()); } mPaint.setAlpha((int) (alpha * 255)); canvas.drawCircle(centerX, centerY, radius, mPaint); } /** * @param partOfPattern Whether this circle is part of the pattern. */ private void drawCellDrawable(Canvas canvas, int i, int j, float radius, boolean partOfPattern) { Rect dst = new Rect( (int) (mPaddingLeft + j * mSquareWidth), (int) (mPaddingTop + i * mSquareHeight), (int) (mPaddingLeft + (j + 1) * mSquareWidth), (int) (mPaddingTop + (i + 1) * mSquareHeight)); float scale = radius / (mDotSize / 2); // Only draw on this square with the appropriate scale. canvas.save(); canvas.clipRect(dst); canvas.scale(scale, scale, dst.centerX(), dst.centerY()); if (!partOfPattern || scale > 1) { mNotSelectedDrawable.draw(canvas); } else { mSelectedDrawable.draw(canvas); } canvas.restore(); } @Override protected Parcelable onSaveInstanceState() { Parcelable superState = super.onSaveInstanceState(); byte[] patternBytes = LockPatternUtils.patternToByteArray(mPattern); String patternString = patternBytes != null ? new String(patternBytes) : null; return new SavedState(superState, patternString, mPatternDisplayMode.ordinal(), mInputEnabled, mInStealthMode); } @Override protected void onRestoreInstanceState(Parcelable state) { final SavedState ss = (SavedState) state; super.onRestoreInstanceState(ss.getSuperState()); setPattern( DisplayMode.Correct, LockPatternUtils.byteArrayToPattern(ss.getSerializedPattern().getBytes())); mPatternDisplayMode = DisplayMode.values()[ss.getDisplayMode()]; mInputEnabled = ss.isInputEnabled(); mInStealthMode = ss.isInStealthMode(); } @Override protected void onLayout(boolean changed, int left, int top, int right, int bottom) { super.onLayout(changed, left, top, right, bottom); setSystemGestureExclusionRects(List.of(new Rect(left, top, right, bottom))); } /** * The parecelable for saving and restoring a lock pattern view. */ private static class SavedState extends BaseSavedState { private final String mSerializedPattern; private final int mDisplayMode; private final boolean mInputEnabled; private final boolean mInStealthMode; /** * Constructor called from {@link LockPatternView#onSaveInstanceState()} */ @UnsupportedAppUsage private SavedState(Parcelable superState, String serializedPattern, int displayMode, boolean inputEnabled, boolean inStealthMode) { super(superState); mSerializedPattern = serializedPattern; mDisplayMode = displayMode; mInputEnabled = inputEnabled; mInStealthMode = inStealthMode; } /** * Constructor called from {@link #CREATOR} */ @UnsupportedAppUsage private SavedState(Parcel in) { super(in); mSerializedPattern = in.readString(); mDisplayMode = in.readInt(); mInputEnabled = (Boolean) in.readValue(null); mInStealthMode = (Boolean) in.readValue(null); } public String getSerializedPattern() { return mSerializedPattern; } public int getDisplayMode() { return mDisplayMode; } public boolean isInputEnabled() { return mInputEnabled; } public boolean isInStealthMode() { return mInStealthMode; } @Override public void writeToParcel(Parcel dest, int flags) { super.writeToParcel(dest, flags); dest.writeString(mSerializedPattern); dest.writeInt(mDisplayMode); dest.writeValue(mInputEnabled); dest.writeValue(mInStealthMode); } @SuppressWarnings({ "unused", "hiding" }) // Found using reflection public static final Parcelable.Creator CREATOR = new Creator() { @Override public SavedState createFromParcel(Parcel in) { return new SavedState(in); } @Override public SavedState[] newArray(int size) { return new SavedState[size]; } }; } private final class PatternExploreByTouchHelper extends ExploreByTouchHelper { private Rect mTempRect = new Rect(); private final SparseArray mItems = new SparseArray<>(); class VirtualViewContainer { public VirtualViewContainer(CharSequence description) { this.description = description; } CharSequence description; }; public PatternExploreByTouchHelper(View forView) { super(forView); for (int i = VIRTUAL_BASE_VIEW_ID; i < VIRTUAL_BASE_VIEW_ID + 9; i++) { mItems.put(i, new VirtualViewContainer(getTextForVirtualView(i))); } } @Override protected int getVirtualViewAt(float x, float y) { // This must use the same hit logic for the screen to ensure consistency whether // accessibility is on or off. return getVirtualViewIdForHit(x, y); } @Override protected void getVisibleVirtualViews(IntArray virtualViewIds) { if (DEBUG_A11Y) Log.v(TAG, "getVisibleVirtualViews(len=" + virtualViewIds.size() + ")"); if (!mPatternInProgress) { return; } for (int i = VIRTUAL_BASE_VIEW_ID; i < VIRTUAL_BASE_VIEW_ID + 9; i++) { // Add all views. As views are added to the pattern, we remove them // from notification by making them non-clickable below. virtualViewIds.add(i); } } @Override protected void onPopulateEventForVirtualView(int virtualViewId, AccessibilityEvent event) { if (DEBUG_A11Y) Log.v(TAG, "onPopulateEventForVirtualView(" + virtualViewId + ")"); // Announce this view VirtualViewContainer container = mItems.get(virtualViewId); if (container != null) { event.getText().add(container.description); } } @Override public void onPopulateAccessibilityEvent(View host, AccessibilityEvent event) { super.onPopulateAccessibilityEvent(host, event); if (!mPatternInProgress) { CharSequence contentDescription = getContext().getText( com.android.internal.R.string.lockscreen_access_pattern_area); event.setContentDescription(contentDescription); } } @Override protected void onPopulateNodeForVirtualView(int virtualViewId, AccessibilityNodeInfo node) { if (DEBUG_A11Y) Log.v(TAG, "onPopulateNodeForVirtualView(view=" + virtualViewId + ")"); // Node and event text and content descriptions are usually // identical, so we'll use the exact same string as before. node.setText(getTextForVirtualView(virtualViewId)); node.setContentDescription(getTextForVirtualView(virtualViewId)); if (mPatternInProgress) { node.setFocusable(true); if (isClickable(virtualViewId)) { // Mark this node of interest by making it clickable. node.addAction(AccessibilityAction.ACTION_CLICK); node.setClickable(isClickable(virtualViewId)); } } // Compute bounds for this object final Rect bounds = getBoundsForVirtualView(virtualViewId); if (DEBUG_A11Y) Log.v(TAG, "bounds:" + bounds.toString()); node.setBoundsInParent(bounds); } private boolean isClickable(int virtualViewId) { // Dots are clickable if they're not part of the current pattern. if (virtualViewId != ExploreByTouchHelper.INVALID_ID) { int row = (virtualViewId - VIRTUAL_BASE_VIEW_ID) / 3; int col = (virtualViewId - VIRTUAL_BASE_VIEW_ID) % 3; if (row < 3) { return !mPatternDrawLookup[row][col]; } } return false; } @Override protected boolean onPerformActionForVirtualView(int virtualViewId, int action, Bundle arguments) { if (DEBUG_A11Y) Log.v(TAG, "onPerformActionForVirtualView(id=" + virtualViewId + ", action=" + action); switch (action) { case AccessibilityNodeInfo.ACTION_CLICK: // Click handling should be consistent with // onTouchEvent(). This ensures that the view works the // same whether accessibility is turned on or off. return onItemClicked(virtualViewId); default: if (DEBUG_A11Y) Log.v(TAG, "*** action not handled in " + "onPerformActionForVirtualView(viewId=" + virtualViewId + "action=" + action + ")"); } return false; } boolean onItemClicked(int index) { if (DEBUG_A11Y) Log.v(TAG, "onItemClicked(" + index + ")"); // Since the item's checked state is exposed to accessibility // services through its AccessibilityNodeInfo, we need to invalidate // the item's virtual view. At some point in the future, the // framework will obtain an updated version of the virtual view. invalidateVirtualView(index); // We need to let the framework know what type of event // happened. Accessibility services may use this event to provide // appropriate feedback to the user. sendEventForVirtualView(index, AccessibilityEvent.TYPE_VIEW_CLICKED); return true; } private Rect getBoundsForVirtualView(int virtualViewId) { int ordinal = virtualViewId - VIRTUAL_BASE_VIEW_ID; final Rect bounds = mTempRect; final int row = ordinal / 3; final int col = ordinal % 3; float centerX = getCenterXForColumn(col); float centerY = getCenterYForRow(row); float cellHitRadius = mDotHitRadius; bounds.left = (int) (centerX - cellHitRadius); bounds.right = (int) (centerX + cellHitRadius); bounds.top = (int) (centerY - cellHitRadius); bounds.bottom = (int) (centerY + cellHitRadius); return bounds; } private CharSequence getTextForVirtualView(int virtualViewId) { final Resources res = getResources(); return res.getString(R.string.lockscreen_access_pattern_cell_added_verbose, virtualViewId); } /** * Helper method to find which cell a point maps to * * if there's no hit. * @param x touch position x * @param y touch position y * @return VIRTUAL_BASE_VIEW_ID+id or 0 if no view was hit */ private int getVirtualViewIdForHit(float x, float y) { Cell cellHit = detectCellHit(x, y); if (cellHit == null) { return ExploreByTouchHelper.INVALID_ID; } boolean dotAvailable = mPatternDrawLookup[cellHit.row][cellHit.column]; int dotId = (cellHit.row * 3 + cellHit.column) + VIRTUAL_BASE_VIEW_ID; int view = dotAvailable ? dotId : ExploreByTouchHelper.INVALID_ID; if (DEBUG_A11Y) Log.v(TAG, "getVirtualViewIdForHit(" + x + "," + y + ") => " + view + "avail =" + dotAvailable); return view; } } }