From 7626c8e89e1dab59cf0cd23955b2a3879e272136 Mon Sep 17 00:00:00 2001 From: seven332 Date: Mon, 21 Mar 2016 17:02:16 +0800 Subject: [PATCH] Add pattern protection --- app/src/main/AndroidManifest.xml | 6 + .../java/com/hippo/ehviewer/Settings.java | 11 + .../com/hippo/ehviewer/ui/MainActivity.java | 31 +- .../ehviewer/ui/SetSecurityActivity.java | 82 ++ .../ui/fragment/AdvancedFragment.java | 13 + .../ehviewer/ui/scene/SecurityScene.java | 111 ++ .../widget/lockpattern/LockPatternUtils.java | 75 ++ .../widget/lockpattern/LockPatternView.java | 1142 +++++++++++++++++ .../res/layout-land/activity_set_security.xml | 86 ++ .../main/res/layout/activity_set_security.xml | 84 ++ app/src/main/res/layout/scene_security.xml | 38 + app/src/main/res/values-zh-rCN/strings.xml | 8 + app/src/main/res/values-zh-rHK/strings.xml | 8 + app/src/main/res/values-zh-rTW/strings.xml | 8 + app/src/main/res/values/colors.xml | 5 + app/src/main/res/values/dimens.xml | 5 + app/src/main/res/values/strings.xml | 8 + app/src/main/res/xml/advanced_settings.xml | 6 + 18 files changed, 1718 insertions(+), 9 deletions(-) create mode 100644 app/src/main/java/com/hippo/ehviewer/ui/SetSecurityActivity.java create mode 100644 app/src/main/java/com/hippo/ehviewer/ui/scene/SecurityScene.java create mode 100644 app/src/main/java/com/hippo/widget/lockpattern/LockPatternUtils.java create mode 100644 app/src/main/java/com/hippo/widget/lockpattern/LockPatternView.java create mode 100644 app/src/main/res/layout-land/activity_set_security.xml create mode 100644 app/src/main/res/layout/activity_set_security.xml create mode 100644 app/src/main/res/layout/scene_security.xml diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 3031b4f2a..28f5986a2 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -93,6 +93,12 @@ android:theme="@style/AppTheme.NoActionBar" android:label="@string/filter"/> + + pattern) {} + + @Override + public void onPatternDetected(List pattern) { + MainActivity activity = getActivity2(); + if (null == activity || null == mPatternView) { + return; + } + + String enteredPatter = LockPatternUtils.patternToString(pattern); + String targetPatter = Settings.getSecurity(); + + if (ObjectUtils.equal(enteredPatter, targetPatter)) { + activity.startSceneForCheckStep(MainActivity.CHECK_STEP_SECURITY, getArguments()); + finish(); + } else { + mPatternView.setDisplayMode(LockPatternView.DisplayMode.Wrong); + mRetryTimes--; + if (mRetryTimes <= 0) { + finish(); + } + } + } +} diff --git a/app/src/main/java/com/hippo/widget/lockpattern/LockPatternUtils.java b/app/src/main/java/com/hippo/widget/lockpattern/LockPatternUtils.java new file mode 100644 index 000000000..1ecb5d65e --- /dev/null +++ b/app/src/main/java/com/hippo/widget/lockpattern/LockPatternUtils.java @@ -0,0 +1,75 @@ +/* + * 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.hippo.widget.lockpattern; + +import java.util.ArrayList; +import java.util.List; + +/** + * Utilities for the lock pattern and its settings. + */ +public class LockPatternUtils { + + /** + * Deserialize a pattern. + * @param string The pattern serialized with {@link #patternToString} + * @return The pattern. + */ + public static List stringToPattern(String string) { + List result = new ArrayList<>(); + + final byte[] bytes = string.getBytes(); + for (int i = 0; i < bytes.length; i++) { + byte b = bytes[i]; + result.add(LockPatternView.Cell.of(b / 3, b % 3)); + } + return result; + } + + /** + * Serialize a pattern. + * @param pattern The pattern. + * @return The pattern in string form. + */ + public static String patternToString(List pattern) { + if (pattern == null) { + return ""; + } + final int patternSize = pattern.size(); + + byte[] res = new byte[patternSize]; + for (int i = 0; i < patternSize; i++) { + LockPatternView.Cell cell = pattern.get(i); + res[i] = (byte) (cell.getRow() * 3 + cell.getColumn()); + } + return new String(res); + } + + public static boolean isPatternVaild(String pattern) { + byte[] bytes = pattern.getBytes(); + if (bytes.length <= 9 && bytes.length >= 2) { + for (byte b : bytes) { + if (b >= 9 || b < 0) { + return false; + } + } + return true; + } else { + return false; + } + } +} diff --git a/app/src/main/java/com/hippo/widget/lockpattern/LockPatternView.java b/app/src/main/java/com/hippo/widget/lockpattern/LockPatternView.java new file mode 100644 index 000000000..d2167c0b0 --- /dev/null +++ b/app/src/main/java/com/hippo/widget/lockpattern/LockPatternView.java @@ -0,0 +1,1142 @@ +/* + * 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.hippo.widget.lockpattern; + +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.ValueAnimator; +import android.annotation.SuppressLint; +import android.content.Context; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.Path; +import android.graphics.Rect; +import android.os.Debug; +import android.os.Parcel; +import android.os.Parcelable; +import android.os.SystemClock; +import android.support.annotation.NonNull; +import android.support.v4.view.animation.FastOutSlowInInterpolator; +import android.support.v4.view.animation.LinearOutSlowInInterpolator; +import android.util.AttributeSet; +import android.view.HapticFeedbackConstants; +import android.view.MotionEvent; +import android.view.View; +import android.view.accessibility.AccessibilityManager; +import android.view.animation.Interpolator; + +import com.hippo.ehviewer.R; + +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 final CellState[][] mCellStates; + + private final int mDotSize; + private final int mDotSizeActivated; + private final int mPathWidth; + + private boolean mDrawingProfilingStarted = false; + + private final Paint mPaint = new Paint(); + 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; + + private OnPatternListener mOnPatternListener; + 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 DisplayMode mPatternDisplayMode = DisplayMode.Correct; + private boolean mInputEnabled = true; + private boolean mInStealthMode = false; + private boolean mEnableHapticFeedback = true; + private boolean mPatternInProgress = false; + + private final float mHitFactor = 0.6f; + + private float mSquareWidth; + private float mSquareHeight; + + private final Path mCurrentPath = new Path(); + private final Rect mInvalidate = new Rect(); + private final Rect mTmpInvalidateRect = new Rect(); + + private final int mAspect; + private final int mRegularColor; + private final int mErrorColor; + private final int mSuccessColor; + + private final Interpolator mFastOutSlowInInterpolator; + private final Interpolator mLinearOutSlowInInterpolator; + + /** + * Represents a cell in the 3 X 3 matrix of the unlock pattern view. + */ + public static class Cell { + int row; + int column; + + // keep # objects limited to 9 + static Cell[][] sCells = new Cell[3][3]; + static { + for (int i = 0; i < 3; i++) { + for (int j = 0; j < 3; j++) { + sCells[i][j] = new Cell(i, j); + } + } + } + + /** + * @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; + } + + /** + * @param row The row of the cell. + * @param column The column of the cell. + */ + public static synchronized 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"); + } + } + + public String toString() { + return "(row=" + row + ",clmn=" + column + ")"; + } + } + + public static class CellState { + public float scale = 1.0f; + public float translateY = 0.0f; + public float alpha = 1.0f; + public float size; + public float lineEndX = Float.MIN_VALUE; + public float lineEndY = Float.MIN_VALUE; + public ValueAnimator lineAnimator; + } + + /** + * How to display the current pattern. + */ + public enum DisplayMode { + + /** + * The pattern drawn is correct (i.e draw it in a friendly color) + */ + Correct, + + /** + * Animate the pattern (for demo, and help). + */ + Animate, + + /** + * The pattern is wrong (i.e draw a foreboding color) + */ + Wrong + } + + /** + * The call back interface for detecting patterns entered by the user. + */ + public 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); + } + + public LockPatternView(Context context, AttributeSet attrs) { + super(context, attrs); + + //TypedArray a = context.obtainStyledAttributes(attrs, 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 = getResources().getColor(R.color.lock_pattern_view_regular_color); + mErrorColor = getResources().getColor(R.color.lock_pattern_view_error_color); + mSuccessColor = getResources().getColor(R.color.lock_pattern_view_success_color); + + int pathColor = mRegularColor;//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); + + mDotSize = getResources().getDimensionPixelSize(R.dimen.lock_pattern_dot_size); + mDotSizeActivated = getResources().getDimensionPixelSize( + R.dimen.lock_pattern_dot_size_activated); + + 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].size = mDotSize; + } + } + + mFastOutSlowInInterpolator = new FastOutSlowInInterpolator(); + mLinearOutSlowInInterpolator = new LinearOutSlowInInterpolator(); + } + + public CellState[][] getCellStates() { + return mCellStates; + } + + public int getCellSize() { + return mPattern.size(); + } + + public String getPatternString() { + return LockPatternUtils.patternToString(mPattern); + } + + /** + * @return Whether the view is in stealth mode. + */ + public boolean isInStealthMode() { + return mInStealthMode; + } + + /** + * @return Whether the view has tactile feedback enabled. + */ + public boolean isTactileFeedbackEnabled() { + return mEnableHapticFeedback; + } + + /** + * 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. + */ + public void setInStealthMode(boolean inStealthMode) { + mInStealthMode = inStealthMode; + } + + /** + * Set whether the view will use tactile feedback. If true, there will be + * tactile feedback as the user enters the pattern. + * + * @param tactileFeedbackEnabled Whether tactile feedback is enabled + */ + public void setTactileFeedbackEnabled(boolean tactileFeedbackEnabled) { + mEnableHapticFeedback = tactileFeedbackEnabled; + } + + /** + * Set the call back for pattern detection. + * @param onPatternListener The call back. + */ + 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. + */ + 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(); + } + + /** + * Get the display mode of the current pattern. + * @return the display mode of the current pattern + */ + public DisplayMode getDisplayMode() { + return mPatternDisplayMode; + } + + private void notifyCellAdded() { + if (mOnPatternListener != null) { + mOnPatternListener.onPatternCellAdded(mPattern); + } + } + + private void notifyPatternStarted() { + if (mOnPatternListener != null) { + mOnPatternListener.onPatternStart(); + } + } + + private void notifyPatternDetected() { + if (mOnPatternListener != null) { + mOnPatternListener.onPatternDetected(mPattern); + } + } + + private void notifyPatternCleared() { + if (mOnPatternListener != null) { + mOnPatternListener.onPatternCleared(); + } + } + + /** + * Clear the pattern. + */ + public void clearPattern() { + resetPattern(); + } + + /** + * Reset all pattern state. + */ + private void resetPattern() { + mPattern.clear(); + clearPatternDrawLookup(); + mPatternDisplayMode = DisplayMode.Correct; + invalidate(); + } + + /** + * Clear the pattern lookup table. + */ + private void clearPatternDrawLookup() { + for (int i = 0; i < 3; i++) { + for (int j = 0; j < 3; j++) { + mPatternDrawLookup[i][j] = false; + } + } + } + + /** + * Disable input (for instance when displaying a message that will + * timeout so user doesn't get view into messy state). + */ + public void disableInput() { + mInputEnabled = false; + } + + /** + * Enable input. + */ + public void enableInput() { + mInputEnabled = true; + } + + @Override + protected void onSizeChanged(int w, int h, int oldw, int oldh) { + final int width = w - getPaddingLeft() - getPaddingRight(); + mSquareWidth = width / 3.0f; + + final int height = h - getPaddingTop() - getPaddingBottom(); + mSquareHeight = height / 3.0f; + } + + 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; + if (!pattern.isEmpty()) { + final Cell 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); + } + addCellToPattern(cell); + if (mEnableHapticFeedback) { + performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY, + HapticFeedbackConstants.FLAG_IGNORE_VIEW_SETTING + | HapticFeedbackConstants.FLAG_IGNORE_GLOBAL_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 startCellActivatedAnimation(Cell cell) { + final CellState cellState = mCellStates[cell.row][cell.column]; + startSizeAnimation(mDotSize, mDotSizeActivated, 96, mLinearOutSlowInInterpolator, + cellState, new Runnable() { + @Override + public void run() { + startSizeAnimation(mDotSizeActivated, mDotSize, 192, mFastOutSlowInInterpolator, + cellState, null); + } + }); + startLineEndAnimation(cellState, mInProgressX, mInProgressY, + getCenterXForColumn(cell.column), getCenterYForRow(cell.row)); + } + + private void startLineEndAnimation(final CellState state, + final float startX, final float startY, final float targetX, final float targetY) { + ValueAnimator valueAnimator = ValueAnimator.ofFloat(0, 1); + valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { + @Override + public void onAnimationUpdate(ValueAnimator animation) { + float t = (float) animation.getAnimatedValue(); + state.lineEndX = (1 - t) * startX + t * targetX; + state.lineEndY = (1 - t) * startY + t * targetY; + invalidate(); + } + }); + valueAnimator.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + state.lineAnimator = null; + } + }); + valueAnimator.setInterpolator(mFastOutSlowInInterpolator); + valueAnimator.setDuration(100); + valueAnimator.start(); + state.lineAnimator = valueAnimator; + } + + private void startSizeAnimation(float start, float end, long duration, Interpolator interpolator, + final CellState state, final Runnable endRunnable) { + ValueAnimator valueAnimator = ValueAnimator.ofFloat(start, end); + valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { + @Override + public void onAnimationUpdate(ValueAnimator animation) { + state.size = (float) animation.getAnimatedValue(); + invalidate(); + } + }); + if (endRunnable != null) { + valueAnimator.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + endRunnable.run(); + } + }); + } + valueAnimator.setInterpolator(interpolator); + valueAnimator.setDuration(duration); + valueAnimator.start(); + } + + // helper method to find which cell a point maps to + private Cell checkForNewHit(float x, float y) { + + final int rowHit = getRowHit(y); + if (rowHit < 0) { + return null; + } + final int columnHit = getColumnHit(x); + if (columnHit < 0) { + return null; + } + + if (mPatternDrawLookup[rowHit][columnHit]) { + return null; + } + return Cell.of(rowHit, columnHit); + } + + /** + * Helper method to find the row that y falls into. + * @param y The y coordinate + * @return The row that y falls in, or -1 if it falls in no row. + */ + private int getRowHit(float y) { + + final float squareHeight = mSquareHeight; + float hitSize = squareHeight * mHitFactor; + + float offset = getPaddingTop() + (squareHeight - hitSize) / 2f; + for (int i = 0; i < 3; i++) { + + final float hitTop = offset + squareHeight * i; + if (y >= hitTop && y <= hitTop + hitSize) { + return i; + } + } + return -1; + } + + /** + * Helper method to find the column x fallis into. + * @param x The x coordinate. + * @return The column that x falls in, or -1 if it falls in no column. + */ + private int getColumnHit(float x) { + final float squareWidth = mSquareWidth; + float hitSize = squareWidth * mHitFactor; + + float offset = getPaddingLeft() + (squareWidth - hitSize) / 2f; + for (int i = 0; i < 3; i++) { + + final float hitLeft = offset + squareWidth * i; + if (x >= hitLeft && x <= hitLeft + hitSize) { + return i; + } + } + return -1; + } + + @Override + public boolean onHoverEvent(@NonNull MotionEvent event) { + AccessibilityManager accessibilityManager = + (AccessibilityManager) getContext().getSystemService(Context.ACCESSIBILITY_SERVICE); + if (accessibilityManager.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(@NonNull MotionEvent event) { + if (!mInputEnabled || !isEnabled()) { + return false; + } + + switch(event.getAction()) { + case MotionEvent.ACTION_DOWN: + handleActionDown(event); + return true; + case MotionEvent.ACTION_UP: + handleActionUp(event); + return true; + case MotionEvent.ACTION_MOVE: + handleActionMove(event); + return true; + case MotionEvent.ACTION_CANCEL: + if (mPatternInProgress) { + mPatternInProgress = false; + resetPattern(); + notifyPatternCleared(); + } + if (PROFILE_DRAWING) { + if (mDrawingProfilingStarted) { + Debug.stopMethodTracing(); + mDrawingProfilingStarted = false; + } + } + return true; + } + return false; + } + + 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) { + mPatternInProgress = 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 handleActionUp(MotionEvent event) { + // report pattern detected + if (!mPattern.isEmpty()) { + mPatternInProgress = false; + cancelLineAnimations(); + notifyPatternDetected(); + invalidate(); + } + if (PROFILE_DRAWING) { + if (mDrawingProfilingStarted) { + Debug.stopMethodTracing(); + mDrawingProfilingStarted = false; + } + } + } + + private void cancelLineAnimations() { + for (int i = 0; i < 3; i++) { + for (int j = 0; j < 3; j++) { + CellState state = mCellStates[i][j]; + if (state.lineAnimator != null) { + state.lineAnimator.cancel(); + state.lineEndX = Float.MIN_VALUE; + state.lineEndY = Float.MIN_VALUE; + } + } + } + } + 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) { + mPatternInProgress = true; + mPatternDisplayMode = DisplayMode.Correct; + notifyPatternStarted(); + } else if (mPatternInProgress) { + mPatternInProgress = 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; + } + } + } + + private float getCenterXForColumn(int column) { + return getPaddingLeft() + column * mSquareWidth + mSquareWidth / 2f; + } + + private float getCenterYForRow(int row) { + return getPaddingTop() + 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(); + + // 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 size = cellState.size * cellState.scale; + float translationY = cellState.translateY; + drawCircle(canvas, (int) centerX, (int) centerY + translationY, + size, drawLookup[i][j], cellState.alpha); + } + } + + // 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) { + mPathPaint.setColor(getCurrentColor(true /* partOfPattern */)); + + boolean anyCircles = false; + float lastX = 0f; + float lastY = 0f; + 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; + + float centerX = getCenterXForColumn(cell.column); + float centerY = getCenterYForRow(cell.row); + if (i != 0) { + CellState state = mCellStates[cell.row][cell.column]; + currentPath.rewind(); + currentPath.moveTo(lastX, lastY); + if (state.lineEndX != Float.MIN_VALUE && state.lineEndY != Float.MIN_VALUE) { + currentPath.lineTo(state.lineEndX, state.lineEndY); + } else { + currentPath.lineTo(centerX, centerY); + } + canvas.drawPath(currentPath, mPathPaint); + } + 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); + } + } + } + + 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 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 size, + boolean partOfPattern, float alpha) { + mPaint.setColor(getCurrentColor(partOfPattern)); + mPaint.setAlpha((int) (alpha * 255)); + canvas.drawCircle(centerX, centerY, size/2, mPaint); + } + + @Override + protected Parcelable onSaveInstanceState() { + Parcelable superState = super.onSaveInstanceState(); + return new SavedState(superState, + LockPatternUtils.patternToString(mPattern), + mPatternDisplayMode.ordinal(), + mInputEnabled, mInStealthMode, mEnableHapticFeedback); + } + + @Override + protected void onRestoreInstanceState(Parcelable state) { + final SavedState ss = (SavedState) state; + super.onRestoreInstanceState(ss.getSuperState()); + setPattern( + DisplayMode.Correct, + LockPatternUtils.stringToPattern(ss.getSerializedPattern())); + mPatternDisplayMode = DisplayMode.values()[ss.getDisplayMode()]; + mInputEnabled = ss.isInputEnabled(); + mInStealthMode = ss.isInStealthMode(); + mEnableHapticFeedback = ss.isTactileFeedbackEnabled(); + } + + /** + * 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; + private final boolean mTactileFeedbackEnabled; + + /** + * Constructor called from {@link LockPatternView#onSaveInstanceState()} + */ + private SavedState(Parcelable superState, String serializedPattern, int displayMode, + boolean inputEnabled, boolean inStealthMode, boolean tactileFeedbackEnabled) { + super(superState); + mSerializedPattern = serializedPattern; + mDisplayMode = displayMode; + mInputEnabled = inputEnabled; + mInStealthMode = inStealthMode; + mTactileFeedbackEnabled = tactileFeedbackEnabled; + } + + /** + * Constructor called from {@link #CREATOR} + */ + @SuppressLint("ParcelClassLoader") + private SavedState(Parcel in) { + super(in); + mSerializedPattern = in.readString(); + mDisplayMode = in.readInt(); + mInputEnabled = (Boolean) in.readValue(null); + mInStealthMode = (Boolean) in.readValue(null); + mTactileFeedbackEnabled = (Boolean) in.readValue(null); + } + + public String getSerializedPattern() { + return mSerializedPattern; + } + + public int getDisplayMode() { + return mDisplayMode; + } + + public boolean isInputEnabled() { + return mInputEnabled; + } + + public boolean isInStealthMode() { + return mInStealthMode; + } + + public boolean isTactileFeedbackEnabled(){ + return mTactileFeedbackEnabled; + } + + @Override + public void writeToParcel(@NonNull Parcel dest, int flags) { + super.writeToParcel(dest, flags); + dest.writeString(mSerializedPattern); + dest.writeInt(mDisplayMode); + dest.writeValue(mInputEnabled); + dest.writeValue(mInStealthMode); + dest.writeValue(mTactileFeedbackEnabled); + } + + public static final Creator CREATOR = + new Creator() { + @Override + public SavedState createFromParcel(Parcel in) { + return new SavedState(in); + } + + @Override + public SavedState[] newArray(int size) { + return new SavedState[size]; + } + }; + } +} diff --git a/app/src/main/res/layout-land/activity_set_security.xml b/app/src/main/res/layout-land/activity_set_security.xml new file mode 100644 index 000000000..cdc23da78 --- /dev/null +++ b/app/src/main/res/layout-land/activity_set_security.xml @@ -0,0 +1,86 @@ + + + + + + + + + + + + + +