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 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/activity_set_security.xml b/app/src/main/res/layout/activity_set_security.xml
new file mode 100644
index 000000000..374a0ee30
--- /dev/null
+++ b/app/src/main/res/layout/activity_set_security.xml
@@ -0,0 +1,84 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/scene_security.xml b/app/src/main/res/layout/scene_security.xml
new file mode 100644
index 000000000..b488a17c5
--- /dev/null
+++ b/app/src/main/res/layout/scene_security.xml
@@ -0,0 +1,38 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml
index e19ae98d8..461b08d9c 100644
--- a/app/src/main/res/values-zh-rCN/strings.xml
+++ b/app/src/main/res/values-zh-rCN/strings.xml
@@ -300,6 +300,11 @@
删除过滤项:%s?
标题过滤项:排除标题含有该关键字的画廊。\n\n上传者过滤项:排除该上传者上传的画廊。\n\n标签过滤项:排除包含该关键字的画廊,这会使获取画廊列表花费更多时间。
+
+ 设置图案保护
+ 绘制图案来设置图案保护\n留空来取消图案保护
+ 设置
+
第 %d 页
@string/refresh
@@ -385,6 +390,9 @@
清除画廊下载路径缓存?
清理内存缓存
清理缩略图、画廊信息的内存缓存
+ 图案保护
+ 未设置图案保护
+ 已设置图案保护
关于
作者
diff --git a/app/src/main/res/values-zh-rHK/strings.xml b/app/src/main/res/values-zh-rHK/strings.xml
index b871519dd..4fa4ac74a 100644
--- a/app/src/main/res/values-zh-rHK/strings.xml
+++ b/app/src/main/res/values-zh-rHK/strings.xml
@@ -300,6 +300,11 @@
刪除過濾項:%s?
標題過濾項:排除標題含有該關鍵字的畫廊。\n\n上傳者過濾項:排除該上傳者上傳的畫廊。\n\n標籤過濾項:排除包含該關鍵字的畫廊,這會使獲取畫廊列表花費更多時間。
+
+ 設置圖案保護
+ 繪製圖案來設置圖案保護\n留空來取消圖案保護
+ 設置
+
第 %d 頁
@string/refresh
@@ -385,6 +390,9 @@
清除畫廊下載路徑緩存?
清理內存緩存
清理縮略圖、畫廊信息的內存緩存
+ 圖案保護
+ 未設置圖案保護
+ 已設置圖案保護
關於
作者
diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml
index d9e8f0cc9..f22d57077 100644
--- a/app/src/main/res/values-zh-rTW/strings.xml
+++ b/app/src/main/res/values-zh-rTW/strings.xml
@@ -300,6 +300,11 @@
刪除過濾項:%s?
標題過濾項:排除標題含有該關鍵字的畫廊。\n\n上傳者過濾項:排除該上傳者上傳的畫廊。\n\n標籤過濾項:排除包含該關鍵字的畫廊,這會使獲取畫廊列表花費更多時間。
+
+ 設定圖案保護
+ 繪製圖案來設定圖案保護\n留空來取消圖案保護
+ 設定
+
第 %d 頁
@string/refresh
@@ -385,6 +390,9 @@
清除畫廊下載路徑快取?
清理記憶體快取
清理縮圖、畫廊資訊的記憶體快取
+ 圖案保護
+ 未設定圖案保護
+ 已設定圖案保護
關於
作者
diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml
index f5229f26a..5bb42c6ac 100644
--- a/app/src/main/res/values/colors.xml
+++ b/app/src/main/res/values/colors.xml
@@ -109,4 +109,9 @@
#e5009688
+
+ #8a000000
+ @color/colorPrimary
+ @color/red_500
+
diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml
index f1229c0af..858b27954 100644
--- a/app/src/main/res/values/dimens.xml
+++ b/app/src/main/res/values/dimens.xml
@@ -102,4 +102,9 @@
3dp
+
+ 3dp
+ 12dp
+ 28dp
+
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index cd8a111d7..57e156436 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -343,6 +343,11 @@
Filter text
Title Filter: exclude the gallery whose title contains the word.\n\nUploader Filter: exclude the gallery which was uploaded by the uploader.\n\nTag Filter: exclude the gallery which contain the tag, it takes more time to get gallery list.
+
+ Set pattern protection
+ Draw a pattern to set pattern protection\nLeave blank to clear pattern protection
+ Set
+
Page %d
@string/refresh
@@ -427,6 +432,9 @@
Clear download path cache?
Clear memory cache
Clear thumb, gallery info memory cache
+ Pattern protection
+ Pattern protection has not been set
+ Pattern protection has been set
About
Author
diff --git a/app/src/main/res/xml/advanced_settings.xml b/app/src/main/res/xml/advanced_settings.xml
index 5f4d903c5..85464d365 100644
--- a/app/src/main/res/xml/advanced_settings.xml
+++ b/app/src/main/res/xml/advanced_settings.xml
@@ -39,4 +39,10 @@
android:title="@string/settings_advanced_clear_memory_cache"
android:summary="@string/settings_advanced_clear_memory_cache_summary"/>
+
+
| | | | | | |