diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index a78c16ae7..353cdb621 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -135,6 +135,22 @@
android:theme="@style/AppTheme.NoActionBar.Transparent" >
+
+
+
+
+
+
@@ -144,6 +160,13 @@
android:authorities="com.hippo.ehviewer.SimpleSuggestionProvider"
android:exported="false" />
+
+
+
+
+
+
+
diff --git a/app/src/main/java/com/hippo/ehviewer/StartActivity.java b/app/src/main/java/com/hippo/ehviewer/StartActivity.java
index a44299299..b9ba57059 100644
--- a/app/src/main/java/com/hippo/ehviewer/StartActivity.java
+++ b/app/src/main/java/com/hippo/ehviewer/StartActivity.java
@@ -27,14 +27,16 @@ import android.widget.ImageView;
import android.widget.TextView;
import com.hippo.ehviewer.app.MaterialAlertDialog;
+import com.hippo.ehviewer.miscellaneous.ActivityHelper;
import com.hippo.ehviewer.network.Network;
import com.hippo.ehviewer.ui.AbsActivity;
+import com.hippo.ehviewer.ui.EnterPatternActivity;
import com.hippo.ehviewer.ui.GalleryListActivity;
-import com.hippo.ehviewer.miscellaneous.ActivityHelper;
import com.hippo.ehviewer.util.Config;
import com.hippo.ehviewer.util.Crash;
import com.hippo.ehviewer.util.Secret;
import com.hippo.ehviewer.util.ViewUtils;
+import com.hippo.ehviewer.widget.LockPatternUtils;
import com.larvalabs.svgandroid.SVGBuilder;
public class StartActivity extends AbsActivity {
@@ -42,6 +44,8 @@ public class StartActivity extends AbsActivity {
@SuppressWarnings("unused")
private static final String TAG = StartActivity.class.getSimpleName();
+ private static final int REQUEST_CODE_PATTERN = 0;
+
private static final int CHECK_WARING = 0;
private static final int CHECK_ANALYTICS = 1;
private static final int CHECK_NETWORK = 2;
@@ -51,6 +55,7 @@ public class StartActivity extends AbsActivity {
private static final String KEY_SET_ANALYTICS = "set_analyics";
private String lastCrash;
+ private boolean isEnterPattern = false;
private boolean isAnimationOver = false;
private boolean isCheckOver = false;
@@ -196,12 +201,37 @@ public class StartActivity extends AbsActivity {
AppHandler.getInstance().postDelayed(new Runnable() {
@Override
public void run() {
- if (isCheckOver)
- redirectTo();
- isAnimationOver = true;
+ if (!StartActivity.this.isFinishing()) {
+ if (isCheckOver && isEnterPattern)
+ redirectTo();
+ isAnimationOver = true;
+ }
}
}, 3000);
check(CHECK_WARING);
+
+ // Enter Pattern
+ String pattern = Config.getPatternProtection();
+ if (LockPatternUtils.isPatternVaild(pattern)) {
+ // Start activity to enter pattern
+ Intent intent = new Intent(this, EnterPatternActivity.class);
+ startActivityForResult(intent, REQUEST_CODE_PATTERN);
+ } else {
+ isEnterPattern = true;
+ }
+ }
+
+ @Override
+ protected void onActivityResult(int requestCode, int resultCode, Intent data) {
+ if (requestCode == REQUEST_CODE_PATTERN) {
+ if (resultCode == RESULT_OK) {
+ if (isAnimationOver && isCheckOver)
+ redirectTo();
+ isEnterPattern = true;
+ } else {
+ finish();
+ }
+ }
}
/**
@@ -239,7 +269,7 @@ public class StartActivity extends AbsActivity {
}
private void checkOver() {
- if (isAnimationOver)
+ if (isAnimationOver && isEnterPattern)
redirectTo();
isCheckOver = true;
}
diff --git a/app/src/main/java/com/hippo/ehviewer/receiver/RestPatternReceiver.java b/app/src/main/java/com/hippo/ehviewer/receiver/RestPatternReceiver.java
new file mode 100644
index 000000000..7d7541609
--- /dev/null
+++ b/app/src/main/java/com/hippo/ehviewer/receiver/RestPatternReceiver.java
@@ -0,0 +1,18 @@
+package com.hippo.ehviewer.receiver;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+
+import com.hippo.ehviewer.R;
+import com.hippo.ehviewer.util.Config;
+import com.hippo.ehviewer.widget.MaterialToast;
+
+public class RestPatternReceiver extends BroadcastReceiver {
+
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ Config.setPatternProtection("");
+ MaterialToast.showToast(R.string.reset_pattern_protection);
+ }
+}
diff --git a/app/src/main/java/com/hippo/ehviewer/ui/EnterPatternActivity.java b/app/src/main/java/com/hippo/ehviewer/ui/EnterPatternActivity.java
new file mode 100644
index 000000000..873e405b5
--- /dev/null
+++ b/app/src/main/java/com/hippo/ehviewer/ui/EnterPatternActivity.java
@@ -0,0 +1,95 @@
+package com.hippo.ehviewer.ui;
+
+import android.graphics.Color;
+import android.graphics.drawable.ColorDrawable;
+import android.os.Bundle;
+import android.view.MenuItem;
+import android.widget.TextView;
+
+import com.hippo.ehviewer.R;
+import com.hippo.ehviewer.drawable.MaterialIndicatorDrawable;
+import com.hippo.ehviewer.util.Config;
+import com.hippo.ehviewer.util.Ui;
+import com.hippo.ehviewer.widget.LockPatternUtils;
+import com.hippo.ehviewer.widget.LockPatternView;
+
+import java.util.List;
+
+public class EnterPatternActivity extends AbsActivity {
+
+ private static final String KEY_LAST_BAD_PATTERN_RETRY = "last_bad_pattern_retry";
+
+ private static final int MAX_RETRY_TIMES = 5;
+
+ private int mRetryTimes = 0;
+
+ @SuppressWarnings("ConstantConditions")
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_enter_pattern);
+
+ // Set random color
+ int themeColor = Config.getCustomThemeColor() ? Config.getThemeColor() : Ui.THEME_COLOR;
+ getActionBar().setBackgroundDrawable(new ColorDrawable(themeColor));
+ Ui.colorStatusBarL(this, themeColor);
+
+ // Menu
+ MaterialIndicatorDrawable materialIndicator = new MaterialIndicatorDrawable(this, Color.WHITE, MaterialIndicatorDrawable.Stroke.THIN);
+ materialIndicator.setIconState(MaterialIndicatorDrawable.IconState.ARROW);
+ Ui.setMaterialIndicator(getActionBar(), materialIndicator);
+
+ // Can't try if bad retry happened in 10 mins
+ long lastBadRetry = Config.getLong(KEY_LAST_BAD_PATTERN_RETRY, Long.MAX_VALUE);
+ if (System.currentTimeMillis() - lastBadRetry < 1000 * 60 * 10) {
+ mRetryTimes = 10;
+ }
+
+ final TextView textView = (TextView) findViewById(R.id.text);
+ LockPatternView patternView = (LockPatternView) findViewById(R.id.pattern_view);
+ patternView.setOnPatternListener(new LockPatternView.OnPatternListener() {
+ @Override
+ public void onPatternStart() {
+
+ }
+
+ @Override
+ public void onPatternCleared() {
+
+ }
+
+ @Override
+ public void onPatternCellAdded(List pattern) {
+
+ }
+
+ @Override
+ public void onPatternDetected(List pattern) {
+ if (mRetryTimes > MAX_RETRY_TIMES) {
+ textView.setText(R.string.no_more_retries);
+ Config.setLong(KEY_LAST_BAD_PATTERN_RETRY, System.currentTimeMillis());
+ } else {
+ String enterPatter = LockPatternUtils.patternToString(pattern);
+ if (enterPatter.equals(Config.getPatternProtection())) {
+ setResult(RESULT_OK);
+ finish();
+ } else {
+ textView.setText(R.string.error_pattern);
+ }
+ }
+ mRetryTimes++;
+ }
+ });
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ switch (item.getItemId()) {
+ case android.R.id.home:
+ finish();
+ return true;
+ }
+
+ return super.onOptionsItemSelected(item);
+ }
+}
diff --git a/app/src/main/java/com/hippo/ehviewer/ui/PatternProtectionActivity.java b/app/src/main/java/com/hippo/ehviewer/ui/PatternProtectionActivity.java
new file mode 100644
index 000000000..ac4622e14
--- /dev/null
+++ b/app/src/main/java/com/hippo/ehviewer/ui/PatternProtectionActivity.java
@@ -0,0 +1,82 @@
+package com.hippo.ehviewer.ui;
+
+import android.content.res.Resources;
+import android.graphics.Color;
+import android.graphics.drawable.ColorDrawable;
+import android.os.Bundle;
+import android.view.MenuItem;
+import android.view.View;
+import android.widget.TextView;
+
+import com.hippo.ehviewer.R;
+import com.hippo.ehviewer.drawable.MaterialIndicatorDrawable;
+import com.hippo.ehviewer.util.Config;
+import com.hippo.ehviewer.util.Ui;
+import com.hippo.ehviewer.widget.LockPatternUtils;
+import com.hippo.ehviewer.widget.LockPatternView;
+
+public class PatternProtectionActivity extends AbsActivity implements View.OnClickListener{
+
+ private TextView mTextView;
+ private LockPatternView mPatternView;
+ private View mCancel;
+ private View mOK;
+
+ @SuppressWarnings("ConstantConditions")
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_pattern_protection);
+
+ // Set random color
+ int themeColor = Config.getCustomThemeColor() ? Config.getThemeColor() : Ui.THEME_COLOR;
+ getActionBar().setBackgroundDrawable(new ColorDrawable(themeColor));
+ Ui.colorStatusBarL(this, themeColor);
+
+ // Menu
+ MaterialIndicatorDrawable materialIndicator = new MaterialIndicatorDrawable(this, Color.WHITE, MaterialIndicatorDrawable.Stroke.THIN);
+ materialIndicator.setIconState(MaterialIndicatorDrawable.IconState.ARROW);
+ Ui.setMaterialIndicator(getActionBar(), materialIndicator);
+
+ mTextView = (TextView) findViewById(R.id.text);
+ mPatternView = (LockPatternView) findViewById(R.id.pattern_view);
+ mCancel = findViewById(R.id.cancel);
+ mOK = findViewById(R.id.ok);
+
+ mOK.setOnClickListener(this);
+ mCancel.setOnClickListener(this);
+
+ String pattern = Config.getPatternProtection();
+ boolean setPattern = LockPatternUtils.isPatternVaild(pattern);
+ Resources resources = getResources();
+ int patternProtectionStateId = setPattern ?
+ R.string.pattern_protection_set : R.string.pattern_protection_not_set;
+ mTextView.setText(resources.getString(patternProtectionStateId) + "\n\n" +
+ getString(R.string.pattern_protection_warning));
+
+ if (setPattern) {
+ mPatternView.setPattern(LockPatternView.DisplayMode.Correct, LockPatternUtils.stringToPattern(pattern));
+ }
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ switch (item.getItemId()) {
+ case android.R.id.home:
+ finish();
+ return true;
+ }
+
+ return super.onOptionsItemSelected(item);
+ }
+
+ @Override
+ public void onClick(View v) {
+ if (v == mOK) {
+ Config.setPatternProtection(mPatternView.getPatternString());
+ finish();
+ } else if (v == mCancel) {
+ finish();
+ }
+ }
+}
diff --git a/app/src/main/java/com/hippo/ehviewer/ui/SettingsActivity.java b/app/src/main/java/com/hippo/ehviewer/ui/SettingsActivity.java
index 3b233e0eb..90f65e638 100644
--- a/app/src/main/java/com/hippo/ehviewer/ui/SettingsActivity.java
+++ b/app/src/main/java/com/hippo/ehviewer/ui/SettingsActivity.java
@@ -688,9 +688,11 @@ public class SettingsActivity extends AbsPreferenceActivity {
private static final String KEY_FIX_DIRNAME = "fix_dirname";
private static final String KEY_LOGIN_VIA_COOKIE = "login_via_cookie";
+ private static final String KEY_PATTERN_PROTECTION = "pattern_protection";
private Preference mFixDirname;
private Preference mLoginViaCookie;
+ private Preference mPatternProtection;
@Override
public void onCreate(Bundle savedInstanceState) {
@@ -701,6 +703,8 @@ public class SettingsActivity extends AbsPreferenceActivity {
mFixDirname.setOnPreferenceClickListener(this);
mLoginViaCookie = findPreference(KEY_LOGIN_VIA_COOKIE);
mLoginViaCookie.setOnPreferenceClickListener(this);
+ mPatternProtection = findPreference(KEY_PATTERN_PROTECTION);
+ mPatternProtection.setOnPreferenceClickListener(this);
}
@Override
@@ -820,6 +824,9 @@ public class SettingsActivity extends AbsPreferenceActivity {
return true;
}
}).show();
+ } else if (KEY_PATTERN_PROTECTION.equals(key)) {
+ Intent intent = new Intent(getActivity(), PatternProtectionActivity.class);
+ getActivity().startActivity(intent);
}
return true;
}
diff --git a/app/src/main/java/com/hippo/ehviewer/util/Config.java b/app/src/main/java/com/hippo/ehviewer/util/Config.java
index 98e760d57..7581d25bd 100644
--- a/app/src/main/java/com/hippo/ehviewer/util/Config.java
+++ b/app/src/main/java/com/hippo/ehviewer/util/Config.java
@@ -96,6 +96,14 @@ public final class Config {
mConfigPre.edit().putInt(key, value).apply();
}
+ public static long getLong(String key, long defValue) {
+ return mConfigPre.getLong(key, defValue);
+ }
+
+ public static void setLong(String key, long value) {
+ mConfigPre.edit().putLong(key, value).apply();
+ }
+
public static String getString(String key, String defValue) {
return mConfigPre.getString(key, defValue);
}
@@ -563,6 +571,9 @@ public final class Config {
private static final String KEY_EH_MIN_INTERVAL = "eh_min_interval";
private static final int DEFAULT_EH_MIN_INTERVAL = 0;
+ private static final String KEY_PATTERN_PROTECTION = "pattern_protection";
+ private static final String DEFAULT_PATTERN_PROTECTION = "";
+
public static int getHttpRetry() {
return getIntFromStr(KEY_HTTP_RETRY, DEFAULT_HTTP_RETRY);
}
@@ -579,6 +590,14 @@ public final class Config {
return getIntFromStr(KEY_EH_MIN_INTERVAL, DEFAULT_EH_MIN_INTERVAL);
}
+ public static String getPatternProtection() {
+ return getString(KEY_PATTERN_PROTECTION, DEFAULT_PATTERN_PROTECTION);
+ }
+
+ public static void setPatternProtection(String value) {
+ setString(KEY_PATTERN_PROTECTION, value);
+ }
+
/****** About ******/
diff --git a/app/src/main/java/com/hippo/ehviewer/widget/LockPatternUtils.java b/app/src/main/java/com/hippo/ehviewer/widget/LockPatternUtils.java
new file mode 100644
index 000000000..2be16ab03
--- /dev/null
+++ b/app/src/main/java/com/hippo/ehviewer/widget/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.ehviewer.widget;
+
+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/ehviewer/widget/LockPatternView.java b/app/src/main/java/com/hippo/ehviewer/widget/LockPatternView.java
new file mode 100644
index 000000000..5550c2775
--- /dev/null
+++ b/app/src/main/java/com/hippo/ehviewer/widget/LockPatternView.java
@@ -0,0 +1,1138 @@
+/*
+ * 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.ehviewer.widget;
+
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.ValueAnimator;
+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 Paint mPaint = new Paint();
+ private 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 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 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 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 int mAspect;
+ private int mRegularColor;
+ private int mErrorColor;
+ private int mSuccessColor;
+
+ private Interpolator mFastOutSlowInInterpolator;
+ private 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);
+ //mRegularColor = a.getColor(R.styleable.LockPatternView_regularColor, mRegularColor);
+ //mErrorColor = a.getColor(R.styleable.LockPatternView_errorColor, mErrorColor);
+ //mSuccessColor = a.getColor(R.styleable.LockPatternView_successColor, mSuccessColor);
+
+ 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 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}
+ */
+ 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() {
+ public SavedState createFromParcel(Parcel in) {
+ return new SavedState(in);
+ }
+
+ public SavedState[] newArray(int size) {
+ return new SavedState[size];
+ }
+ };
+ }
+}
diff --git a/app/src/main/res/layout/activity_enter_pattern.xml b/app/src/main/res/layout/activity_enter_pattern.xml
new file mode 100644
index 000000000..99a892a7f
--- /dev/null
+++ b/app/src/main/res/layout/activity_enter_pattern.xml
@@ -0,0 +1,23 @@
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/activity_pattern_protection.xml b/app/src/main/res/layout/activity_pattern_protection.xml
new file mode 100644
index 000000000..3927b3f43
--- /dev/null
+++ b/app/src/main/res/layout/activity_pattern_protection.xml
@@ -0,0 +1,55 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml
index 3a3c61a48..5768ab889 100644
--- a/app/src/main/res/values/colors.xml
+++ b/app/src/main/res/values/colors.xml
@@ -90,4 +90,8 @@
@color/material_deep_teal_500
@color/material_deep_teal_200
-
\ No newline at end of file
+ #8a000000
+ #8a000000
+ #fff4511e
+
+
diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml
index 4ce55d11f..20b3f9759 100644
--- a/app/src/main/res/values/dimens.xml
+++ b/app/src/main/res/values/dimens.xml
@@ -37,4 +37,8 @@
14sp
12sp
+ 3dp
+ 12dp
+ 28dp
+
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index d18c92d2f..de6aa9b20 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -105,6 +105,16 @@
Click to retry
Click to go to first page
+
+ Pattern protection has not been set.
+ Pattern protection has been set.
+ NO guarantee for anything.\nOpen a dial application and enter *#*#110722#*#* if you forget password.
+ Pattern protection has been reset
+
+
+ Error pattern
+ No more retries
+
%d pages leaved
Download over
@@ -295,6 +305,7 @@
No need to fix
%d item(s) fixed
Login via cookie
+ Pattern protection
About
Author
diff --git a/app/src/main/res/xml/advanced_settings.xml b/app/src/main/res/xml/advanced_settings.xml
index e1ab0c8c9..e4aa9c7d6 100644
--- a/app/src/main/res/xml/advanced_settings.xml
+++ b/app/src/main/res/xml/advanced_settings.xml
@@ -81,5 +81,9 @@
android:key="login_via_cookie"
android:title="@string/login_via_cookie_title"/>
+
+
| | | | | | |