From 6d013b328d46bf4baa2a874053e1a737bfc9fb33 Mon Sep 17 00:00:00 2001
From: whale
Date: Mon, 19 Jan 2026 20:04:19 +0800
Subject: [PATCH] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E5=AF=86=E7=A0=81=E9=94=81?=
=?UTF-8?q?=E5=8A=9F=E8=83=BD?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
src/main/AndroidManifest.xml | 22 +-
.../java/net/micode/notes/data/Notes.java | 24 ++
.../notes/data/NotesDatabaseHelper.java | 45 ++-
.../net/micode/notes/model/WorkingNote.java | 110 ++++++-
.../micode/notes/tool/LockPasswordUtils.java | 158 ++++++++++
.../net/micode/notes/ui/LockPatternView.java | 266 ++++++++++++++++
.../net/micode/notes/ui/NoteEditActivity.java | 143 ++++++---
.../net/micode/notes/ui/NoteItemData.java | 18 ++
.../micode/notes/ui/NotesListActivity.java | 14 +-
.../net/micode/notes/ui/NotesListItem.java | 11 +
.../notes/ui/NumericPasswordActivity.java | 287 ++++++++++++++++++
.../net/micode/notes/ui/SetLockActivity.java | 273 +++++++++++++++++
.../net/micode/notes/ui/UnlockActivity.java | 213 +++++++++++++
.../res/layout/activity_numeric_password.xml | 183 +++++++++++
src/main/res/layout/activity_set_lock.xml | 82 +++++
src/main/res/layout/activity_unlock.xml | 45 +++
src/main/res/layout/note_item.xml | 8 +
src/main/res/menu/note_edit.xml | 17 +-
src/main/res/values/strings.xml | 44 +++
19 files changed, 1896 insertions(+), 67 deletions(-)
create mode 100644 src/main/java/net/micode/notes/tool/LockPasswordUtils.java
create mode 100644 src/main/java/net/micode/notes/ui/LockPatternView.java
create mode 100644 src/main/java/net/micode/notes/ui/NumericPasswordActivity.java
create mode 100644 src/main/java/net/micode/notes/ui/SetLockActivity.java
create mode 100644 src/main/java/net/micode/notes/ui/UnlockActivity.java
create mode 100644 src/main/res/layout/activity_numeric_password.xml
create mode 100644 src/main/res/layout/activity_set_lock.xml
create mode 100644 src/main/res/layout/activity_unlock.xml
diff --git a/src/main/AndroidManifest.xml b/src/main/AndroidManifest.xml
index cb43220..ed21bae 100644
--- a/src/main/AndroidManifest.xml
+++ b/src/main/AndroidManifest.xml
@@ -127,12 +127,32 @@
+
+
+
+
+
+
+
+
+
diff --git a/src/main/java/net/micode/notes/data/Notes.java b/src/main/java/net/micode/notes/data/Notes.java
index 1cf1b3f..b148c5b 100644
--- a/src/main/java/net/micode/notes/data/Notes.java
+++ b/src/main/java/net/micode/notes/data/Notes.java
@@ -223,6 +223,30 @@ public class Notes {
* 类型 : INTEGER (long),值越大优先级越高
*/
public static final String PIN_PRIORITY = "pin_priority";
+
+ /**
+ * 锁定状态,用于标识便签是否被密码锁定
+ * 类型 : INTEGER (0: 未锁定, 1: 已锁定)
+ */
+ public static final String IS_LOCKED = "is_locked";
+
+ /**
+ * 锁定密码,存储加密后的手势密码
+ * 类型 : TEXT
+ */
+ public static final String LOCK_PASSWORD = "lock_password";
+
+ /**
+ * 密码类型,标识便签使用的密码类型
+ * 类型 : TEXT ("gesture" 或 "numeric")
+ */
+ public static final String PASSWORD_TYPE = "password_type";
+
+ /**
+ * 数字密码,存储加密后的6位数字密码
+ * 类型 : TEXT
+ */
+ public static final String NUMERIC_PASSWORD = "numeric_password";
}
/**
diff --git a/src/main/java/net/micode/notes/data/NotesDatabaseHelper.java b/src/main/java/net/micode/notes/data/NotesDatabaseHelper.java
index 7ff4ec2..4be5e25 100644
--- a/src/main/java/net/micode/notes/data/NotesDatabaseHelper.java
+++ b/src/main/java/net/micode/notes/data/NotesDatabaseHelper.java
@@ -36,7 +36,7 @@ public class NotesDatabaseHelper extends SQLiteOpenHelper {
private static final String DB_NAME = "note.db";
// 数据库版本号
- private static final int DB_VERSION = 5;
+ private static final int DB_VERSION = 7;
// 数据库表名定义
public interface TABLE {
@@ -92,7 +92,9 @@ public class NotesDatabaseHelper extends SQLiteOpenHelper {
NoteColumns.GTASK_ID + " TEXT NOT NULL DEFAULT ''," +
NoteColumns.VERSION + " INTEGER NOT NULL DEFAULT 0," +
NoteColumns.IS_PINNED + " INTEGER NOT NULL DEFAULT 0," +
- NoteColumns.PIN_PRIORITY + " INTEGER NOT NULL DEFAULT 0" +
+ NoteColumns.PIN_PRIORITY + " INTEGER NOT NULL DEFAULT 0," +
+ NoteColumns.IS_LOCKED + " INTEGER NOT NULL DEFAULT 0," +
+ NoteColumns.LOCK_PASSWORD + " TEXT NOT NULL DEFAULT ''" +
")";
// 创建数据表的SQL语句
@@ -420,7 +422,16 @@ public class NotesDatabaseHelper extends SQLiteOpenHelper {
oldVersion++;
}
- // 如果需要,重新创建触发器
+ if (oldVersion == 5) {
+ upgradeToV6(db);
+ oldVersion++;
+ }
+
+ if (oldVersion == 6) {
+ upgradeToV7(db);
+ oldVersion++;
+ }
+
if (reCreateTriggers) {
reCreateNoteTableTriggers(db);
reCreateDataTableTriggers(db);
@@ -491,4 +502,32 @@ public class NotesDatabaseHelper extends SQLiteOpenHelper {
db.execSQL("ALTER TABLE " + TABLE.NOTE + " ADD COLUMN " + NoteColumns.PIN_PRIORITY
+ " INTEGER NOT NULL DEFAULT 0");
}
+
+ /**
+ * 将数据库从v5升级到v6
+ * 此版本升级添加了便签锁定状态和密码字段,用于支持便签密码锁功能
+ * @param db SQLite数据库实例
+ */
+ private void upgradeToV6(SQLiteDatabase db) {
+ // 为笔记表添加锁定状态字段
+ db.execSQL("ALTER TABLE " + TABLE.NOTE + " ADD COLUMN " + NoteColumns.IS_LOCKED
+ + " INTEGER NOT NULL DEFAULT 0");
+ // 为笔记表添加密码字段
+ db.execSQL("ALTER TABLE " + TABLE.NOTE + " ADD COLUMN " + NoteColumns.LOCK_PASSWORD
+ + " TEXT NOT NULL DEFAULT ''");
+ }
+
+ /**
+ * 将数据库从v6升级到v7
+ * 此版本升级添加了密码类型字段,用于支持手势密码和数字密码
+ * @param db SQLite数据库实例
+ */
+ private void upgradeToV7(SQLiteDatabase db) {
+ // 为笔记表添加密码类型字段
+ db.execSQL("ALTER TABLE " + TABLE.NOTE + " ADD COLUMN " + NoteColumns.PASSWORD_TYPE
+ + " TEXT NOT NULL DEFAULT 'gesture'");
+ // 为笔记表添加数字密码字段
+ db.execSQL("ALTER TABLE " + TABLE.NOTE + " ADD COLUMN " + NoteColumns.NUMERIC_PASSWORD
+ + " TEXT NOT NULL DEFAULT ''");
+ }
}
diff --git a/src/main/java/net/micode/notes/model/WorkingNote.java b/src/main/java/net/micode/notes/model/WorkingNote.java
index 91c98fd..5b111d3 100644
--- a/src/main/java/net/micode/notes/model/WorkingNote.java
+++ b/src/main/java/net/micode/notes/model/WorkingNote.java
@@ -108,6 +108,26 @@ public class WorkingNote {
*/
private NoteSettingChangedListener mNoteSettingStatusListener;
+ /**
+ * 便签锁定状态,标识便签是否被密码锁定
+ */
+ private boolean mIsLocked;
+
+ /**
+ * 便签锁定密码,存储加密后的手势密码
+ */
+ private String mLockPassword;
+
+ /**
+ * 密码类型,标识便签使用的密码类型
+ */
+ private String mPasswordType;
+
+ /**
+ * 数字密码,存储加密后的6位数字密码
+ */
+ private String mNumericPassword;
+
/**
* 数据投影数组,用于从数据库中查询便签数据
*/
@@ -130,7 +150,11 @@ public class WorkingNote {
NoteColumns.BG_COLOR_ID, // 背景颜色ID
NoteColumns.WIDGET_ID, // 小部件ID
NoteColumns.WIDGET_TYPE, // 小部件类型
- NoteColumns.MODIFIED_DATE // 修改日期
+ NoteColumns.MODIFIED_DATE, // 修改日期
+ NoteColumns.IS_LOCKED, // 锁定状态
+ NoteColumns.LOCK_PASSWORD, // 锁定密码
+ NoteColumns.PASSWORD_TYPE, // 密码类型
+ NoteColumns.NUMERIC_PASSWORD // 数字密码
};
/**
@@ -150,6 +174,10 @@ public class WorkingNote {
private static final int NOTE_WIDGET_ID_COLUMN = 3; // 小部件ID列索引
private static final int NOTE_WIDGET_TYPE_COLUMN = 4; // 小部件类型列索引
private static final int NOTE_MODIFIED_DATE_COLUMN = 5; // 修改日期列索引
+ private static final int NOTE_IS_LOCKED_COLUMN = 6; // 锁定状态列索引
+ private static final int NOTE_LOCK_PASSWORD_COLUMN = 7; // 锁定密码列索引
+ private static final int NOTE_PASSWORD_TYPE_COLUMN = 8; // 密码类型列索引
+ private static final int NOTE_NUMERIC_PASSWORD_COLUMN = 9; // 数字密码列索引
/**
* 构造方法,创建一个新的便签
@@ -202,6 +230,10 @@ public class WorkingNote {
mWidgetType = cursor.getInt(NOTE_WIDGET_TYPE_COLUMN);
mAlertDate = cursor.getLong(NOTE_ALERTED_DATE_COLUMN);
mModifiedDate = cursor.getLong(NOTE_MODIFIED_DATE_COLUMN);
+ mIsLocked = cursor.getInt(NOTE_IS_LOCKED_COLUMN) > 0;
+ mLockPassword = cursor.getString(NOTE_LOCK_PASSWORD_COLUMN);
+ mPasswordType = cursor.getString(NOTE_PASSWORD_TYPE_COLUMN);
+ mNumericPassword = cursor.getString(NOTE_NUMERIC_PASSWORD_COLUMN);
}
cursor.close();
} else {
@@ -537,6 +569,82 @@ public class WorkingNote {
return mWidgetType;
}
+ /**
+ * 设置便签锁定状态
+ * @param locked 是否锁定
+ */
+ public void setLocked(boolean locked) {
+ if (mIsLocked != locked) {
+ mIsLocked = locked;
+ mNote.setNoteValue(NoteColumns.IS_LOCKED, String.valueOf(locked ? 1 : 0));
+ }
+ }
+
+ /**
+ * 检查便签是否锁定
+ * @return 是否锁定
+ */
+ public boolean isLocked() {
+ return mIsLocked;
+ }
+
+ /**
+ * 设置锁定密码
+ * @param password 加密后的密码
+ */
+ public void setLockPassword(String password) {
+ if (!TextUtils.equals(mLockPassword, password)) {
+ mLockPassword = password;
+ mNote.setNoteValue(NoteColumns.LOCK_PASSWORD, password);
+ }
+ }
+
+ /**
+ * 获取锁定密码
+ * @return 加密后的密码
+ */
+ public String getLockPassword() {
+ return mLockPassword;
+ }
+
+ /**
+ * 设置密码类型
+ * @param type 密码类型 ("gesture" 或 "numeric")
+ */
+ public void setPasswordType(String type) {
+ if (!TextUtils.equals(mPasswordType, type)) {
+ mPasswordType = type;
+ mNote.setNoteValue(NoteColumns.PASSWORD_TYPE, type);
+ }
+ }
+
+ /**
+ * 获取密码类型
+ * @return 密码类型
+ */
+ public String getPasswordType() {
+ return mPasswordType;
+ }
+
+ /**
+ * 设置数字密码
+ * @param password 加密后的数字密码
+ */
+ public void setNumericPassword(String password) {
+ if (!TextUtils.equals(mNumericPassword, password)) {
+ mNumericPassword = password;
+ mNote.setNoteValue(NoteColumns.NUMERIC_PASSWORD, password);
+ }
+ }
+
+ /**
+ * 获取数字密码
+ * @return 加密后的数字密码
+ */
+ public String getNumericPassword() {
+ return mNumericPassword;
+ }
+
/**
* 便签设置变化监听器接口
* 用于监听便签设置的变化,如背景颜色、提醒时间、小部件等
diff --git a/src/main/java/net/micode/notes/tool/LockPasswordUtils.java b/src/main/java/net/micode/notes/tool/LockPasswordUtils.java
new file mode 100644
index 0000000..577d70d
--- /dev/null
+++ b/src/main/java/net/micode/notes/tool/LockPasswordUtils.java
@@ -0,0 +1,158 @@
+/*
+ * Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net)
+ *
+ * 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 net.micode.notes.tool;
+
+import android.text.TextUtils;
+
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+
+/**
+ * 密码加密工具类
+ * 用于加密和解密手势密码
+ */
+public class LockPasswordUtils {
+ private static final String TAG = "LockPasswordUtils";
+
+ private static final String SALT = "micode_notes_lock_salt";
+
+ /**
+ * 密码类型:手势密码
+ */
+ public static final String TYPE_GESTURE = "gesture";
+
+ /**
+ * 密码类型:数字密码
+ */
+ public static final String TYPE_NUMERIC = "numeric";
+
+ /**
+ * 加密密码
+ * @param password 原始密码
+ * @return 加密后的密码
+ */
+ public static String encryptPassword(String password) {
+ if (TextUtils.isEmpty(password)) {
+ return "";
+ }
+ try {
+ MessageDigest md = MessageDigest.getInstance("SHA-256");
+ String saltedPassword = password + SALT;
+ byte[] hash = md.digest(saltedPassword.getBytes());
+ return bytesToHex(hash);
+ } catch (NoSuchAlgorithmException e) {
+ e.printStackTrace();
+ return "";
+ }
+ }
+
+ /**
+ * 验证密码
+ * @param password 原始密码
+ * @param encryptedPassword 加密后的密码
+ * @return 是否匹配
+ */
+ public static boolean verifyPassword(String password, String encryptedPassword) {
+ if (TextUtils.isEmpty(password) || TextUtils.isEmpty(encryptedPassword)) {
+ return false;
+ }
+ String encrypted = encryptPassword(password);
+ return encrypted.equals(encryptedPassword);
+ }
+
+ /**
+ * 将字节数组转换为十六进制字符串
+ * @param bytes 字节数组
+ * @return 十六进制字符串
+ */
+ private static String bytesToHex(byte[] bytes) {
+ StringBuilder sb = new StringBuilder();
+ for (byte b : bytes) {
+ sb.append(String.format("%02x", b));
+ }
+ return sb.toString();
+ }
+
+ /**
+ * 将手势路径转换为密码字符串
+ * @param pattern 手势路径数组
+ * @return 密码字符串
+ */
+ public static String patternToString(int[] pattern) {
+ if (pattern == null || pattern.length == 0) {
+ return "";
+ }
+ StringBuilder sb = new StringBuilder();
+ for (int i = 0; i < pattern.length; i++) {
+ sb.append(pattern[i]);
+ if (i < pattern.length - 1) {
+ sb.append(",");
+ }
+ }
+ return sb.toString();
+ }
+
+ /**
+ * 将密码字符串转换为手势路径
+ * @param patternString 密码字符串
+ * @return 手势路径数组
+ */
+ public static int[] stringToPattern(String patternString) {
+ if (TextUtils.isEmpty(patternString)) {
+ return new int[0];
+ }
+ String[] parts = patternString.split(",");
+ int[] pattern = new int[parts.length];
+ for (int i = 0; i < parts.length; i++) {
+ try {
+ pattern[i] = Integer.parseInt(parts[i].trim());
+ } catch (NumberFormatException e) {
+ pattern[i] = 0;
+ }
+ }
+ return pattern;
+ }
+
+ /**
+ * 检查手势是否有效
+ * @param pattern 手势路径
+ * @return 是否有效(至少连接4个点)
+ */
+ public static boolean isValidPattern(int[] pattern) {
+ return pattern != null && pattern.length >= 4;
+ }
+
+ /**
+ * 检查数字密码是否有效
+ * @param password 数字密码
+ * @return 是否有效(必须是6位数字)
+ */
+ public static boolean isValidNumericPassword(String password) {
+ if (TextUtils.isEmpty(password)) {
+ return false;
+ }
+ if (password.length() != 6) {
+ return false;
+ }
+ for (char c : password.toCharArray()) {
+ if (!Character.isDigit(c)) {
+ return false;
+ }
+ }
+ return true;
+ }
+}
diff --git a/src/main/java/net/micode/notes/ui/LockPatternView.java b/src/main/java/net/micode/notes/ui/LockPatternView.java
new file mode 100644
index 0000000..dd4b385
--- /dev/null
+++ b/src/main/java/net/micode/notes/ui/LockPatternView.java
@@ -0,0 +1,266 @@
+/*
+ * Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net)
+ *
+ * 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 net.micode.notes.ui;
+
+import android.content.Context;
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.graphics.Path;
+import android.util.AttributeSet;
+import android.view.MotionEvent;
+import android.view.View;
+
+import net.micode.notes.R;
+import net.micode.notes.tool.LockPasswordUtils;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * 九宫格手势密码视图
+ * 用于绘制 3x3 九宫格,处理手势输入
+ */
+public class LockPatternView extends View {
+ private static final String TAG = "LockPatternView";
+
+ private static final int GRID_SIZE = 3;
+ private static final int DOT_RADIUS = 10;
+ private static final int LINE_WIDTH = 8;
+
+ private Paint mDotPaint;
+ private Paint mLinePaint;
+ private Paint mSelectedDotPaint;
+
+ private float[] mDotPositions;
+ private List mPattern;
+ private Path mPath;
+ private boolean mIsDrawing;
+ private float mLastX;
+ private float mLastY;
+
+ private OnPatternListener mListener;
+
+ /**
+ * 手势监听器接口
+ */
+ public interface OnPatternListener {
+ void onPatternStart();
+
+ void onPatternCleared();
+
+ void onPatternDetected(List pattern);
+ }
+
+ public LockPatternView(Context context) {
+ super(context);
+ init();
+ }
+
+ public LockPatternView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ init();
+ }
+
+ public LockPatternView(Context context, AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ init();
+ }
+
+ private void init() {
+ mDotPaint = new Paint();
+ mDotPaint.setColor(getResources().getColor(R.color.secondary_text_dark));
+ mDotPaint.setAntiAlias(true);
+ mDotPaint.setStyle(Paint.Style.FILL);
+
+ mSelectedDotPaint = new Paint();
+ mSelectedDotPaint.setColor(getResources().getColor(android.R.color.holo_blue_dark));
+ mSelectedDotPaint.setAntiAlias(true);
+ mSelectedDotPaint.setStyle(Paint.Style.FILL);
+
+ mLinePaint = new Paint();
+ mLinePaint.setColor(getResources().getColor(android.R.color.holo_blue_dark));
+ mLinePaint.setAntiAlias(true);
+ mLinePaint.setStyle(Paint.Style.STROKE);
+ mLinePaint.setStrokeWidth(LINE_WIDTH);
+ mLinePaint.setStrokeCap(Paint.Cap.ROUND);
+
+ mPattern = new ArrayList<>();
+ mPath = new Path();
+ mDotPositions = new float[GRID_SIZE * GRID_SIZE * 2];
+ }
+
+ @Override
+ protected void onSizeChanged(int w, int h, int oldw, int oldh) {
+ super.onSizeChanged(w, h, oldw, oldh);
+ calculateDotPositions(w, h);
+ }
+
+ private void calculateDotPositions(int width, int height) {
+ int padding = Math.min(width, height) / 10;
+ int availableWidth = width - 2 * padding;
+ int availableHeight = height - 2 * padding;
+ int cellWidth = availableWidth / (GRID_SIZE - 1);
+ int cellHeight = availableHeight / (GRID_SIZE - 1);
+
+ for (int row = 0; row < GRID_SIZE; row++) {
+ for (int col = 0; col < GRID_SIZE; col++) {
+ int index = (row * GRID_SIZE + col) * 2;
+ mDotPositions[index] = padding + col * cellWidth;
+ mDotPositions[index + 1] = padding + row * cellHeight;
+ }
+ }
+ }
+
+ @Override
+ protected void onDraw(Canvas canvas) {
+ super.onDraw(canvas);
+
+ drawDots(canvas);
+ drawPattern(canvas);
+ }
+
+ private void drawDots(Canvas canvas) {
+ for (int row = 0; row < GRID_SIZE; row++) {
+ for (int col = 0; col < GRID_SIZE; col++) {
+ int dotIndex = row * GRID_SIZE + col;
+ int index = dotIndex * 2;
+ float x = mDotPositions[index];
+ float y = mDotPositions[index + 1];
+
+ if (mPattern.contains(dotIndex)) {
+ canvas.drawCircle(x, y, DOT_RADIUS + 2, mSelectedDotPaint);
+ } else {
+ canvas.drawCircle(x, y, DOT_RADIUS, mDotPaint);
+ }
+ }
+ }
+ }
+
+ private void drawPattern(Canvas canvas) {
+ if (mPattern.isEmpty()) {
+ return;
+ }
+
+ mPath.reset();
+ int firstIndex = mPattern.get(0);
+ int firstIndex2 = firstIndex * 2;
+ mPath.moveTo(mDotPositions[firstIndex2], mDotPositions[firstIndex2 + 1]);
+
+ for (int i = 1; i < mPattern.size(); i++) {
+ int index = mPattern.get(i);
+ int index2 = index * 2;
+ mPath.lineTo(mDotPositions[index2], mDotPositions[index2 + 1]);
+ }
+
+ if (mIsDrawing) {
+ mPath.lineTo(mLastX, mLastY);
+ }
+
+ canvas.drawPath(mPath, mLinePaint);
+ }
+
+ @Override
+ public boolean onTouchEvent(MotionEvent event) {
+ float x = event.getX();
+ float y = event.getY();
+
+ switch (event.getAction()) {
+ case MotionEvent.ACTION_DOWN:
+ mIsDrawing = true;
+ mPattern.clear();
+ mPath.reset();
+ mLastX = x;
+ mLastY = y;
+ checkDot(x, y);
+ if (mListener != null) {
+ mListener.onPatternStart();
+ }
+ break;
+
+ case MotionEvent.ACTION_MOVE:
+ if (mIsDrawing) {
+ mLastX = x;
+ mLastY = y;
+ checkDot(x, y);
+ }
+ break;
+
+ case MotionEvent.ACTION_UP:
+ if (mIsDrawing) {
+ mIsDrawing = false;
+ if (mListener != null) {
+ mListener.onPatternDetected(new ArrayList<>(mPattern));
+ }
+ }
+ break;
+
+ default:
+ break;
+ }
+
+ invalidate();
+ return true;
+ }
+
+ private void checkDot(float x, float y) {
+ for (int row = 0; row < GRID_SIZE; row++) {
+ for (int col = 0; col < GRID_SIZE; col++) {
+ int dotIndex = row * GRID_SIZE + col;
+ int index2 = dotIndex * 2;
+ float dotX = mDotPositions[index2];
+ float dotY = mDotPositions[index2 + 1];
+
+ float distance = (float) Math.sqrt(Math.pow(x - dotX, 2) + Math.pow(y - dotY, 2));
+
+ if (distance < DOT_RADIUS * 3 && !mPattern.contains(dotIndex)) {
+ mPattern.add(dotIndex);
+ break;
+ }
+ }
+ }
+ }
+
+ public void clearPattern() {
+ mPattern.clear();
+ mPath.reset();
+ mIsDrawing = false;
+ invalidate();
+ if (mListener != null) {
+ mListener.onPatternCleared();
+ }
+ }
+
+ public void setOnPatternListener(OnPatternListener listener) {
+ mListener = listener;
+ }
+
+ public List getPattern() {
+ return new ArrayList<>(mPattern);
+ }
+
+ public boolean isPatternValid() {
+ return LockPasswordUtils.isValidPattern(convertPatternToIntArray());
+ }
+
+ private int[] convertPatternToIntArray() {
+ int[] pattern = new int[mPattern.size()];
+ for (int i = 0; i < mPattern.size(); i++) {
+ pattern[i] = mPattern.get(i);
+ }
+ return pattern;
+ }
+}
diff --git a/src/main/java/net/micode/notes/ui/NoteEditActivity.java b/src/main/java/net/micode/notes/ui/NoteEditActivity.java
index 34e2c8d..0b3d568 100644
--- a/src/main/java/net/micode/notes/ui/NoteEditActivity.java
+++ b/src/main/java/net/micode/notes/ui/NoteEditActivity.java
@@ -58,6 +58,7 @@ import net.micode.notes.data.Notes.TextNote;
import net.micode.notes.model.WorkingNote;
import net.micode.notes.model.WorkingNote.NoteSettingChangedListener;
import net.micode.notes.tool.DataUtils;
+import net.micode.notes.tool.LockPasswordUtils;
import net.micode.notes.tool.ResourceParser;
import net.micode.notes.tool.ResourceParser.TextAppearanceResources;
import net.micode.notes.ui.DateTimePickerDialog.OnDateTimeSetListener;
@@ -78,7 +79,7 @@ import java.util.regex.Pattern;
* 该类负责处理便签的创建、编辑、保存等核心功能,支持普通文本模式和 checklist 模式
* 实现了背景色切换、字体大小调整、提醒设置、分享等功能
*
- *
+ *
* @author MiCode Open Source Community
* @version 1.0
*/
@@ -170,7 +171,7 @@ public class NoteEditActivity extends Activity implements OnClickListener,
*
* 当活动创建时调用,设置布局并初始化活动状态
*
- *
+ *
* @param savedInstanceState 保存的实例状态,用于恢复活动
*/
@Override
@@ -192,7 +193,7 @@ public class NoteEditActivity extends Activity implements OnClickListener,
*
* 当活动因内存不足被杀死后重新加载时,恢复之前的状态
*
- *
+ *
* @param savedInstanceState 保存的实例状态
*/
@Override
@@ -215,13 +216,13 @@ public class NoteEditActivity extends Activity implements OnClickListener,
*
* 根据传入的意图初始化便签编辑活动,处理查看、创建、编辑便签的情况
*
- *
+ *
* @param intent 传入的意图,包含操作类型和数据
* @return 初始化是否成功
*/
private boolean initActivityState(Intent intent) {
mWorkingNote = null;
-
+
// 处理查看便签操作
if (TextUtils.equals(Intent.ACTION_VIEW, intent.getAction())) {
long noteId = intent.getLongExtra(Intent.EXTRA_UID, 0);
@@ -305,7 +306,7 @@ public class NoteEditActivity extends Activity implements OnClickListener,
finish();
return false;
}
-
+
// 设置设置状态变化监听器
mWorkingNote.setOnSettingStatusChangedListener(this);
return true;
@@ -333,7 +334,7 @@ public class NoteEditActivity extends Activity implements OnClickListener,
// 设置编辑器字体大小
mNoteEditor.setTextAppearance(this, TextAppearanceResources
.getTexAppearanceResource(mFontSizeId));
-
+
// 根据便签模式显示内容
if (mWorkingNote.getCheckListMode() == TextNote.MODE_CHECK_LIST) {
// 切换到列表模式
@@ -344,12 +345,12 @@ public class NoteEditActivity extends Activity implements OnClickListener,
// 将光标定位到文本末尾
mNoteEditor.setSelection(mNoteEditor.getText().length());
}
-
+
// 隐藏所有背景选择状态
for (Integer id : sBgSelectorSelectionMap.keySet()) {
findViewById(sBgSelectorSelectionMap.get(id)).setVisibility(View.GONE);
}
-
+
// 设置头部和编辑器背景
mHeadViewPanel.setBackgroundResource(mWorkingNote.getTitleBgResId());
mNoteEditorPanel.setBackgroundResource(mWorkingNote.getBgColorResId());
@@ -396,7 +397,7 @@ public class NoteEditActivity extends Activity implements OnClickListener,
*
* 当活动已经存在且收到新意图时调用,重新初始化活动状态
*
- *
+ *
* @param intent 新的意图
*/
@Override
@@ -410,18 +411,18 @@ public class NoteEditActivity extends Activity implements OnClickListener,
*
* 当活动即将被销毁时调用,保存当前便签状态
*
- *
+ *
* @param outState 用于保存状态的Bundle对象
*/
@Override
protected void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
-
+
// 对于新便签,先保存生成ID
if (!mWorkingNote.existInDatabase()) {
saveNote();
}
-
+
// 保存便签ID
outState.putLong(Intent.EXTRA_UID, mWorkingNote.getNoteId());
Log.d(TAG, "Save working note id: " + mWorkingNote.getNoteId() + " onSaveInstanceState");
@@ -432,7 +433,7 @@ public class NoteEditActivity extends Activity implements OnClickListener,
*
* 处理屏幕触摸事件,点击外部区域关闭背景选择器和字体大小选择器
*
- *
+ *
* @param ev 触摸事件对象
* @return 是否消耗了该事件
*/
@@ -451,14 +452,14 @@ public class NoteEditActivity extends Activity implements OnClickListener,
mFontSizeSelector.setVisibility(View.GONE);
return true;
}
-
+
// 其他情况交给父类处理
return super.dispatchTouchEvent(ev);
}
/**
* 检查触摸事件是否在指定视图范围内
- *
+ *
* @param view 要检查的视图
* @param ev 触摸事件对象
* @return 是否在视图范围内
@@ -468,14 +469,14 @@ public class NoteEditActivity extends Activity implements OnClickListener,
view.getLocationOnScreen(location);
int x = location[0];
int y = location[1];
-
+
// 检查触摸坐标是否在视图范围内
if (ev.getX() < x
|| ev.getX() > (x + view.getWidth())
|| ev.getY() < y
|| ev.getY() > (y + view.getHeight())) {
- return false;
- }
+ return false;
+ }
return true;
}
@@ -494,11 +495,11 @@ public class NoteEditActivity extends Activity implements OnClickListener,
mNoteHeaderHolder.tvAlertDate = (TextView) findViewById(R.id.tv_alert_date);
mNoteHeaderHolder.ibSetBgColor = (ImageView) findViewById(R.id.btn_set_bg_color);
mNoteHeaderHolder.ibSetBgColor.setOnClickListener(this);
-
+
// 初始化编辑器
mNoteEditor = (EditText) findViewById(R.id.note_edit_view);
mNoteEditorPanel = findViewById(R.id.sv_note_edit);
-
+
// 初始化背景颜色选择器
mNoteBgColorSelector = findViewById(R.id.note_bg_color_selector);
for (int id : sBgSelectorBtnsMap.keySet()) {
@@ -512,16 +513,16 @@ public class NoteEditActivity extends Activity implements OnClickListener,
View view = findViewById(id);
view.setOnClickListener(this);
};
-
+
// 初始化共享偏好设置和字体大小
mSharedPrefs = PreferenceManager.getDefaultSharedPreferences(this);
mFontSizeId = mSharedPrefs.getInt(PREFERENCE_FONT_SIZE, ResourceParser.BG_DEFAULT_FONT_SIZE);
-
+
// 修复字体大小ID可能超出范围的问题
if(mFontSizeId >= TextAppearanceResources.getResourcesSize()) {
mFontSizeId = ResourceParser.BG_DEFAULT_FONT_SIZE;
}
-
+
// 初始化编辑列表
mEditTextList = (LinearLayout) findViewById(R.id.note_edit_list);
}
@@ -559,7 +560,7 @@ public class NoteEditActivity extends Activity implements OnClickListener,
}
intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, new int[] {
- mWorkingNote.getWidgetId()
+ mWorkingNote.getWidgetId()
});
sendBroadcast(intent);
@@ -571,7 +572,7 @@ public class NoteEditActivity extends Activity implements OnClickListener,
*
* 处理UI组件的点击事件,包括设置背景色、选择背景色、选择字体大小
*
- *
+ *
* @param v 被点击的视图
*/
public void onClick(View v) {
@@ -622,7 +623,7 @@ public class NoteEditActivity extends Activity implements OnClickListener,
*
* 关闭背景选择器和字体大小选择器
*
- *
+ *
* @return 是否清除了设置状态
*/
private boolean clearSettingState() {
@@ -654,7 +655,7 @@ public class NoteEditActivity extends Activity implements OnClickListener,
*
* 在显示菜单前调用,根据当前便签状态调整菜单项
*
- *
+ *
* @param menu 菜单对象
* @return 是否显示菜单
*/
@@ -663,10 +664,10 @@ public class NoteEditActivity extends Activity implements OnClickListener,
if (isFinishing()) {
return true;
}
-
+
// 清除设置状态
clearSettingState();
-
+
// 根据便签类型加载不同菜单
menu.clear();
if (mWorkingNote.getFolderId() == Notes.ID_CALL_RECORD_FOLDER) {
@@ -674,20 +675,23 @@ public class NoteEditActivity extends Activity implements OnClickListener,
} else {
getMenuInflater().inflate(R.menu.note_edit, menu);
}
-
+
// 根据便签模式调整列表模式菜单项标题
if (mWorkingNote.getCheckListMode() == TextNote.MODE_CHECK_LIST) {
menu.findItem(R.id.menu_list_mode).setTitle(R.string.menu_normal_mode);
} else {
menu.findItem(R.id.menu_list_mode).setTitle(R.string.menu_list_mode);
}
-
+
// 根据是否有提醒调整提醒相关菜单项可见性
if (mWorkingNote.hasClockAlert()) {
menu.findItem(R.id.menu_alert).setVisible(false);
} else {
menu.findItem(R.id.menu_delete_remind).setVisible(false);
}
+
+ menu.findItem(R.id.menu_set_password).setVisible(!mWorkingNote.isLocked());
+ menu.findItem(R.id.menu_remove_lock).setVisible(mWorkingNote.isLocked());
return true;
}
@@ -696,7 +700,7 @@ public class NoteEditActivity extends Activity implements OnClickListener,
*
* 处理菜单选项的点击事件,包括新建便签、删除、字体大小、列表模式、分享等
*
- *
+ *
* @param item 被点击的菜单项
* @return 是否处理了该事件
*/
@@ -735,6 +739,23 @@ public class NoteEditActivity extends Activity implements OnClickListener,
setReminder();
} else if (id == R.id.menu_delete_remind) {
mWorkingNote.setAlertDate(0, false);
+ } else if (id == R.id.menu_set_password) {
+ showPasswordTypeDialog();
+ } else if (id == R.id.menu_remove_lock) {
+ Intent intent;
+ String passwordType = mWorkingNote.getPasswordType();
+ if (LockPasswordUtils.TYPE_GESTURE.equals(passwordType)) {
+ intent = new Intent(this, SetLockActivity.class);
+ intent.putExtra(SetLockActivity.EXTRA_NOTE_ID, mWorkingNote.getNoteId());
+ intent.putExtra(SetLockActivity.EXTRA_MODE, SetLockActivity.MODE_REMOVE);
+ } else if (LockPasswordUtils.TYPE_NUMERIC.equals(passwordType)) {
+ intent = new Intent(this, NumericPasswordActivity.class);
+ intent.putExtra(NumericPasswordActivity.EXTRA_NOTE_ID, mWorkingNote.getNoteId());
+ intent.putExtra(NumericPasswordActivity.EXTRA_MODE, NumericPasswordActivity.MODE_REMOVE);
+ } else {
+ return true;
+ }
+ startActivity(intent);
} else {
// 默认情况,什么也不做
}
@@ -748,13 +769,39 @@ public class NoteEditActivity extends Activity implements OnClickListener,
*
*/
private void setReminder() {
- DateTimePickerDialog d = new DateTimePickerDialog(this, System.currentTimeMillis());
- d.setOnDateTimeSetListener(new OnDateTimeSetListener() {
- public void OnDateTimeSet(AlertDialog dialog, long date) {
- mWorkingNote.setAlertDate(date , true);
- }
- });
- d.show();
+ showPasswordTypeDialog();
+ }
+
+ /**
+ * 显示密码类型选择弹窗
+ *
+ * 弹出对话框让用户选择手势密码或数字密码
+ *
+ */
+ private void showPasswordTypeDialog() {
+ new android.app.AlertDialog.Builder(this)
+ .setTitle(R.string.dialog_set_password_title)
+ .setItems(new String[] {
+ getString(R.string.dialog_set_gesture_password),
+ getString(R.string.dialog_set_numeric_password)
+ }, new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ if (which == 0) {
+ Intent intent = new Intent(NoteEditActivity.this, SetLockActivity.class);
+ intent.putExtra(SetLockActivity.EXTRA_NOTE_ID, mWorkingNote.getNoteId());
+ intent.putExtra(SetLockActivity.EXTRA_MODE, SetLockActivity.MODE_SET);
+ startActivity(intent);
+ } else if (which == 1) {
+ Intent intent = new Intent(NoteEditActivity.this, NumericPasswordActivity.class);
+ intent.putExtra(NumericPasswordActivity.EXTRA_NOTE_ID, mWorkingNote.getNoteId());
+ intent.putExtra(NumericPasswordActivity.EXTRA_MODE, NumericPasswordActivity.MODE_SET);
+ startActivity(intent);
+ }
+ }
+ })
+ .setCancelable(true)
+ .show();
}
/**
@@ -762,7 +809,7 @@ public class NoteEditActivity extends Activity implements OnClickListener,
*
* 将便签内容分享给支持ACTION_SEND和text/plain类型的应用
*
- *
+ *
* @param context 上下文对象
* @param info 要分享的内容
*/
@@ -806,7 +853,7 @@ public class NoteEditActivity extends Activity implements OnClickListener,
} else {
Log.d(TAG, "Wrong note id, should not happen");
}
-
+
// 根据同步模式处理删除
if (!isSyncMode()) {
// 非同步模式,直接删除
@@ -820,7 +867,7 @@ public class NoteEditActivity extends Activity implements OnClickListener,
}
}
}
-
+
// 标记为已删除
mWorkingNote.markDeleted(true);
}
@@ -830,7 +877,7 @@ public class NoteEditActivity extends Activity implements OnClickListener,
*
* 判断当前是否已配置同步账号
*
- *
+ *
* @return 是否为同步模式
*/
private boolean isSyncMode() {
@@ -842,7 +889,7 @@ public class NoteEditActivity extends Activity implements OnClickListener,
*
* 当提醒时间变化时调用,设置或取消系统闹钟
*
- *
+ *
* @param date 提醒日期时间
* @param set 是否设置提醒
*/
@@ -851,17 +898,17 @@ public class NoteEditActivity extends Activity implements OnClickListener,
if (!mWorkingNote.existInDatabase()) {
saveNote();
}
-
+
if (mWorkingNote.getNoteId() > 0) {
// 创建闹钟意图
Intent intent = new Intent(this, AlarmReceiver.class);
intent.setData(ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, mWorkingNote.getNoteId()));
PendingIntent pendingIntent = PendingIntent.getBroadcast(this, 0, intent, 0);
AlarmManager alarmManager = ((AlarmManager) getSystemService(ALARM_SERVICE));
-
+
// 更新提醒头部
showAlertHeader();
-
+
// 设置或取消闹钟
if(!set) {
alarmManager.cancel(pendingIntent);
diff --git a/src/main/java/net/micode/notes/ui/NoteItemData.java b/src/main/java/net/micode/notes/ui/NoteItemData.java
index d576ddb..3f5c1cc 100644
--- a/src/main/java/net/micode/notes/ui/NoteItemData.java
+++ b/src/main/java/net/micode/notes/ui/NoteItemData.java
@@ -48,6 +48,7 @@ public class NoteItemData {
NoteColumns.TYPE,
NoteColumns.WIDGET_ID,
NoteColumns.WIDGET_TYPE,
+ NoteColumns.IS_LOCKED,
};
/**
@@ -106,6 +107,10 @@ public class NoteItemData {
* 小部件类型列索引
*/
private static final int WIDGET_TYPE_COLUMN = 13;
+ /**
+ * 锁定状态列索引
+ */
+ private static final int IS_LOCKED_COLUMN = 14;
/**
* 笔记ID
@@ -163,6 +168,10 @@ public class NoteItemData {
* 置顶优先级
*/
private long mPinPriority;
+ /**
+ * 锁定状态
+ */
+ private boolean mIsLocked;
/**
* 联系人姓名
*/
@@ -216,6 +225,7 @@ public class NoteItemData {
mType = cursor.getInt(TYPE_COLUMN);
mWidgetId = cursor.getInt(WIDGET_ID_COLUMN);
mWidgetType = cursor.getInt(WIDGET_TYPE_COLUMN);
+ mIsLocked = (cursor.getInt(IS_LOCKED_COLUMN) > 0);
mPhoneNumber = "";
// 如果是通话记录文件夹,获取电话号码和联系人姓名
@@ -453,6 +463,14 @@ public class NoteItemData {
return (mParentId == Notes.ID_CALL_RECORD_FOLDER && !TextUtils.isEmpty(mPhoneNumber));
}
+ /**
+ * 是否锁定
+ * @return 是否锁定
+ */
+ public boolean isLocked() {
+ return mIsLocked;
+ }
+
/**
* 获取笔记类型
* @param cursor 游标
diff --git a/src/main/java/net/micode/notes/ui/NotesListActivity.java b/src/main/java/net/micode/notes/ui/NotesListActivity.java
index 68c768b..3f63999 100644
--- a/src/main/java/net/micode/notes/ui/NotesListActivity.java
+++ b/src/main/java/net/micode/notes/ui/NotesListActivity.java
@@ -590,10 +590,16 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt
}
private void openNode(NoteItemData data) {
- Intent intent = new Intent(this, NoteEditActivity.class);
- intent.setAction(Intent.ACTION_VIEW);
- intent.putExtra(Intent.EXTRA_UID, data.getId());
- this.startActivityForResult(intent, REQUEST_CODE_OPEN_NODE);
+ if (data.isLocked()) {
+ Intent unlockIntent = new Intent(this, UnlockActivity.class);
+ unlockIntent.putExtra(UnlockActivity.EXTRA_NOTE_ID, data.getId());
+ startActivity(unlockIntent);
+ } else {
+ Intent intent = new Intent(this, NoteEditActivity.class);
+ intent.setAction(Intent.ACTION_VIEW);
+ intent.putExtra(Intent.EXTRA_UID, data.getId());
+ this.startActivityForResult(intent, REQUEST_CODE_OPEN_NODE);
+ }
}
private void openFolder(NoteItemData data) {
diff --git a/src/main/java/net/micode/notes/ui/NotesListItem.java b/src/main/java/net/micode/notes/ui/NotesListItem.java
index 21abae5..c23f4a2 100644
--- a/src/main/java/net/micode/notes/ui/NotesListItem.java
+++ b/src/main/java/net/micode/notes/ui/NotesListItem.java
@@ -42,6 +42,10 @@ public class NotesListItem extends LinearLayout {
* 置顶图标
*/
private ImageView mPinned;
+ /**
+ * 锁定图标
+ */
+ private ImageView mLocked;
/**
* 标题文本
*/
@@ -74,6 +78,7 @@ public class NotesListItem extends LinearLayout {
// 初始化控件
mAlert = (ImageView) findViewById(R.id.iv_alert_icon);
mPinned = (ImageView) findViewById(R.id.iv_pinned_icon);
+ mLocked = (ImageView) findViewById(R.id.iv_locked_icon);
mTitle = (TextView) findViewById(R.id.tv_title);
mTime = (TextView) findViewById(R.id.tv_time);
mCallName = (TextView) findViewById(R.id.tv_name);
@@ -146,6 +151,12 @@ public class NotesListItem extends LinearLayout {
} else {
mPinned.setVisibility(View.GONE);
}
+ // 设置锁定图标
+ if (data.isLocked()) {
+ mLocked.setVisibility(View.VISIBLE);
+ } else {
+ mLocked.setVisibility(View.GONE);
+ }
}
}
// 设置修改时间
diff --git a/src/main/java/net/micode/notes/ui/NumericPasswordActivity.java b/src/main/java/net/micode/notes/ui/NumericPasswordActivity.java
new file mode 100644
index 0000000..c688118
--- /dev/null
+++ b/src/main/java/net/micode/notes/ui/NumericPasswordActivity.java
@@ -0,0 +1,287 @@
+/*
+ * Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net)
+ *
+ * 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 net.micode.notes.ui;
+
+import android.app.Activity;
+import android.content.Intent;
+import android.os.Bundle;
+import android.os.Vibrator;
+import android.text.TextUtils;
+import android.view.View;
+import android.widget.Button;
+import android.widget.TextView;
+import android.widget.Toast;
+
+import net.micode.notes.R;
+import net.micode.notes.model.WorkingNote;
+import net.micode.notes.tool.LockPasswordUtils;
+
+/**
+ * 数字密码输入界面
+ * 用于设置、修改或删除6位数字密码
+ */
+public class NumericPasswordActivity extends Activity {
+ private static final String TAG = "NumericPasswordActivity";
+
+ public static final String EXTRA_NOTE_ID = "note_id";
+ public static final String EXTRA_MODE = "mode";
+ public static final String MODE_SET = "set";
+ public static final String MODE_CHANGE = "change";
+ public static final String MODE_REMOVE = "remove";
+ public static final String MODE_VERIFY = "verify";
+
+ private TextView mPasswordDisplay;
+ private TextView mHintText;
+ private Button[] mNumberButtons = new Button[10];
+ private Button mDeleteButton;
+ private Button mCancelButton;
+
+ private long mNoteId;
+ private String mMode;
+ private WorkingNote mWorkingNote;
+
+ private String mFirstPassword;
+ private String mSecondPassword;
+ private String mOldPassword;
+ private StringBuilder mCurrentPassword;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_numeric_password);
+
+ mNoteId = getIntent().getLongExtra(EXTRA_NOTE_ID, 0);
+ mMode = getIntent().getStringExtra(EXTRA_MODE);
+
+ if (mNoteId <= 0) {
+ finish();
+ return;
+ }
+
+ mWorkingNote = WorkingNote.load(this, mNoteId);
+ mCurrentPassword = new StringBuilder();
+
+ initViews();
+ setupMode();
+ }
+
+ private void initViews() {
+ mPasswordDisplay = (TextView) findViewById(R.id.tv_password_display);
+ mHintText = (TextView) findViewById(R.id.tv_password_hint);
+ mDeleteButton = (Button) findViewById(R.id.btn_delete);
+ mCancelButton = (Button) findViewById(R.id.btn_cancel);
+
+ mNumberButtons[0] = (Button) findViewById(R.id.btn_0);
+ mNumberButtons[1] = (Button) findViewById(R.id.btn_1);
+ mNumberButtons[2] = (Button) findViewById(R.id.btn_2);
+ mNumberButtons[3] = (Button) findViewById(R.id.btn_3);
+ mNumberButtons[4] = (Button) findViewById(R.id.btn_4);
+ mNumberButtons[5] = (Button) findViewById(R.id.btn_5);
+ mNumberButtons[6] = (Button) findViewById(R.id.btn_6);
+ mNumberButtons[7] = (Button) findViewById(R.id.btn_7);
+ mNumberButtons[8] = (Button) findViewById(R.id.btn_8);
+ mNumberButtons[9] = (Button) findViewById(R.id.btn_9);
+
+ for (int i = 0; i < 10; i++) {
+ final int number = i;
+ mNumberButtons[i].setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ onNumberPressed(number);
+ }
+ });
+ }
+
+ mDeleteButton.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ onDeletePressed();
+ }
+ });
+
+ mCancelButton.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ finish();
+ }
+ });
+ }
+
+ private void setupMode() {
+ if (MODE_SET.equals(mMode)) {
+ mHintText.setText(R.string.numeric_password_hint_set);
+ } else if (MODE_CHANGE.equals(mMode)) {
+ mHintText.setText(R.string.numeric_password_hint_verify);
+ } else if (MODE_REMOVE.equals(mMode)) {
+ mHintText.setText(R.string.numeric_password_hint_remove);
+ } else if (MODE_VERIFY.equals(mMode)) {
+ mHintText.setText(R.string.numeric_password_hint_verify);
+ }
+ }
+
+ private void onNumberPressed(int number) {
+ if (mCurrentPassword.length() < 6) {
+ mCurrentPassword.append(number);
+ updatePasswordDisplay();
+
+ if (mCurrentPassword.length() == 6) {
+ handlePasswordComplete();
+ }
+ }
+ }
+
+ private void onDeletePressed() {
+ if (mCurrentPassword.length() > 0) {
+ mCurrentPassword.deleteCharAt(mCurrentPassword.length() - 1);
+ updatePasswordDisplay();
+ }
+ }
+
+ private void updatePasswordDisplay() {
+ StringBuilder display = new StringBuilder();
+ for (int i = 0; i < mCurrentPassword.length(); i++) {
+ display.append("●");
+ }
+ mPasswordDisplay.setText(display.toString());
+ }
+
+ private void handlePasswordComplete() {
+ String password = mCurrentPassword.toString();
+
+ if (MODE_SET.equals(mMode)) {
+ handleSetMode(password);
+ } else if (MODE_CHANGE.equals(mMode)) {
+ handleChangeMode(password);
+ } else if (MODE_REMOVE.equals(mMode)) {
+ handleRemoveMode(password);
+ } else if (MODE_VERIFY.equals(mMode)) {
+ handleVerifyMode(password);
+ }
+ }
+
+ private void handleSetMode(String password) {
+ if (mFirstPassword == null) {
+ mFirstPassword = password;
+ mHintText.setText(R.string.numeric_password_confirm);
+ mCurrentPassword.setLength(0);
+ updatePasswordDisplay();
+ } else {
+ mSecondPassword = password;
+ if (mFirstPassword.equals(mSecondPassword)) {
+ String encryptedPassword = LockPasswordUtils.encryptPassword(mFirstPassword);
+ mWorkingNote.setLocked(true);
+ mWorkingNote.setPasswordType(LockPasswordUtils.TYPE_NUMERIC);
+ mWorkingNote.setNumericPassword(encryptedPassword);
+ mWorkingNote.setLockPassword("");
+ mWorkingNote.saveNote();
+ Toast.makeText(this, R.string.numeric_password_success, Toast.LENGTH_SHORT).show();
+ finish();
+ } else {
+ mHintText.setText(R.string.numeric_password_confirm_error);
+ mFirstPassword = null;
+ mSecondPassword = null;
+ mCurrentPassword.setLength(0);
+ updatePasswordDisplay();
+ vibrate();
+ }
+ }
+ }
+
+ private void handleChangeMode(String password) {
+ if (mOldPassword == null) {
+ String encryptedInput = LockPasswordUtils.encryptPassword(password);
+ String storedPassword = mWorkingNote.getNumericPassword();
+
+ if (encryptedInput.equals(storedPassword)) {
+ mOldPassword = storedPassword;
+ mHintText.setText(R.string.numeric_password_hint_new);
+ mCurrentPassword.setLength(0);
+ updatePasswordDisplay();
+ } else {
+ mHintText.setText(R.string.numeric_password_error);
+ mCurrentPassword.setLength(0);
+ updatePasswordDisplay();
+ vibrate();
+ }
+ } else if (mFirstPassword == null) {
+ mFirstPassword = password;
+ mHintText.setText(R.string.numeric_password_confirm);
+ mCurrentPassword.setLength(0);
+ updatePasswordDisplay();
+ } else {
+ mSecondPassword = password;
+ if (mFirstPassword.equals(mSecondPassword)) {
+ String encryptedPassword = LockPasswordUtils.encryptPassword(mFirstPassword);
+ mWorkingNote.setNumericPassword(encryptedPassword);
+ mWorkingNote.saveNote();
+ Toast.makeText(this, R.string.numeric_password_success, Toast.LENGTH_SHORT).show();
+ finish();
+ } else {
+ mHintText.setText(R.string.numeric_password_confirm_error);
+ mFirstPassword = null;
+ mSecondPassword = null;
+ mCurrentPassword.setLength(0);
+ updatePasswordDisplay();
+ vibrate();
+ }
+ }
+ }
+
+ private void handleRemoveMode(String password) {
+ String encryptedInput = LockPasswordUtils.encryptPassword(password);
+ String storedPassword = mWorkingNote.getNumericPassword();
+
+ if (encryptedInput.equals(storedPassword)) {
+ mWorkingNote.setLocked(false);
+ mWorkingNote.setNumericPassword("");
+ mWorkingNote.setPasswordType("");
+ mWorkingNote.saveNote();
+ Toast.makeText(this, R.string.numeric_password_removed, Toast.LENGTH_SHORT).show();
+ finish();
+ } else {
+ mHintText.setText(R.string.numeric_password_error);
+ mCurrentPassword.setLength(0);
+ updatePasswordDisplay();
+ vibrate();
+ }
+ }
+
+ private void handleVerifyMode(String password) {
+ String encryptedInput = LockPasswordUtils.encryptPassword(password);
+ String storedPassword = mWorkingNote.getNumericPassword();
+
+ if (encryptedInput.equals(storedPassword)) {
+ Intent intent = new Intent(this, NoteEditActivity.class);
+ intent.setAction(Intent.ACTION_VIEW);
+ intent.putExtra(Intent.EXTRA_UID, mNoteId);
+ startActivity(intent);
+ finish();
+ } else {
+ mHintText.setText(R.string.numeric_password_error);
+ mCurrentPassword.setLength(0);
+ updatePasswordDisplay();
+ vibrate();
+ }
+ }
+
+ private void vibrate() {
+ Vibrator vibrator = (Vibrator) getSystemService(VIBRATOR_SERVICE);
+ if (vibrator != null && vibrator.hasVibrator()) {
+ vibrator.vibrate(200);
+ }
+ }
+}
diff --git a/src/main/java/net/micode/notes/ui/SetLockActivity.java b/src/main/java/net/micode/notes/ui/SetLockActivity.java
new file mode 100644
index 0000000..e7096bc
--- /dev/null
+++ b/src/main/java/net/micode/notes/ui/SetLockActivity.java
@@ -0,0 +1,273 @@
+/*
+ * Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net)
+ *
+ * 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 net.micode.notes.ui;
+
+import android.app.Activity;
+import android.content.Intent;
+import android.os.Bundle;
+import android.text.TextUtils;
+import android.view.View;
+import android.widget.Button;
+import android.widget.TextView;
+import android.widget.Toast;
+
+import net.micode.notes.R;
+import net.micode.notes.data.Notes;
+import net.micode.notes.model.WorkingNote;
+import net.micode.notes.tool.LockPasswordUtils;
+
+import java.util.List;
+
+/**
+ * 设置密码界面
+ * 用于设置、修改或删除便签的手势密码
+ */
+public class SetLockActivity extends Activity {
+ private static final String TAG = "SetLockActivity";
+
+ public static final String EXTRA_NOTE_ID = "note_id";
+ public static final String EXTRA_MODE = "mode";
+ public static final String MODE_SET = "set";
+ public static final String MODE_CHANGE = "change";
+ public static final String MODE_REMOVE = "remove";
+
+ private LockPatternView mLockPatternView;
+ private TextView mTitleText;
+ private TextView mHintText;
+ private Button mConfirmButton;
+ private Button mCancelButton;
+
+ private long mNoteId;
+ private String mMode;
+ private WorkingNote mWorkingNote;
+
+ private List mFirstPattern;
+ private List mSecondPattern;
+ private String mOldPassword;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_set_lock);
+
+ mNoteId = getIntent().getLongExtra(EXTRA_NOTE_ID, 0);
+ mMode = getIntent().getStringExtra(EXTRA_MODE);
+
+ if (mNoteId <= 0) {
+ finish();
+ return;
+ }
+
+ mWorkingNote = WorkingNote.load(this, mNoteId);
+
+ initViews();
+ setupMode();
+ }
+
+ private void initViews() {
+ mLockPatternView = (LockPatternView) findViewById(R.id.lock_pattern_view);
+ mTitleText = (TextView) findViewById(R.id.tv_lock_title);
+ mHintText = (TextView) findViewById(R.id.tv_lock_hint);
+ mConfirmButton = (Button) findViewById(R.id.btn_confirm);
+ mCancelButton = (Button) findViewById(R.id.btn_cancel);
+
+ mLockPatternView.setOnPatternListener(new LockPatternView.OnPatternListener() {
+ @Override
+ public void onPatternStart() {
+ mHintText.setText("");
+ }
+
+ @Override
+ public void onPatternCleared() {
+ }
+
+ @Override
+ public void onPatternDetected(List pattern) {
+ handlePatternDetected(pattern);
+ }
+ });
+
+ mCancelButton.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ finish();
+ }
+ });
+
+ mConfirmButton.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ handleConfirm();
+ }
+ });
+ }
+
+ private void setupMode() {
+ if (MODE_SET.equals(mMode)) {
+ mTitleText.setText(R.string.lock_pattern_title);
+ mHintText.setText(R.string.lock_pattern_hint_set);
+ mConfirmButton.setVisibility(View.GONE);
+ } else if (MODE_CHANGE.equals(mMode)) {
+ mTitleText.setText(R.string.lock_pattern_title_change);
+ mHintText.setText(R.string.lock_pattern_hint_old);
+ mConfirmButton.setVisibility(View.GONE);
+ } else if (MODE_REMOVE.equals(mMode)) {
+ mTitleText.setText(R.string.lock_pattern_title_remove);
+ mHintText.setText(R.string.lock_pattern_hint_remove);
+ mConfirmButton.setVisibility(View.GONE);
+ }
+ }
+
+ private void handlePatternDetected(List pattern) {
+ if (pattern.size() < 4) {
+ mHintText.setText(R.string.lock_pattern_too_short);
+ mLockPatternView.clearPattern();
+ return;
+ }
+
+ if (MODE_SET.equals(mMode)) {
+ handleSetMode(pattern);
+ } else if (MODE_CHANGE.equals(mMode)) {
+ handleChangeMode(pattern);
+ } else if (MODE_REMOVE.equals(mMode)) {
+ handleRemoveMode(pattern);
+ }
+ }
+
+ private void handleSetMode(List pattern) {
+ if (mFirstPattern == null) {
+ mFirstPattern = pattern;
+ mHintText.setText(R.string.lock_pattern_confirm);
+ mLockPatternView.clearPattern();
+ } else {
+ mSecondPattern = pattern;
+ if (patternsMatch(mFirstPattern, mSecondPattern)) {
+ String password = LockPasswordUtils.patternToString(
+ LockPasswordUtils.stringToPattern(
+ LockPasswordUtils.patternToString(convertPatternToIntArray(mFirstPattern))
+ )
+ );
+ String encryptedPassword = LockPasswordUtils.encryptPassword(password);
+ mWorkingNote.setLocked(true);
+ mWorkingNote.setPasswordType(LockPasswordUtils.TYPE_GESTURE);
+ mWorkingNote.setLockPassword(encryptedPassword);
+ mWorkingNote.setNumericPassword("");
+ mWorkingNote.saveNote();
+ Toast.makeText(this, R.string.lock_pattern_success, Toast.LENGTH_SHORT).show();
+ finish();
+ } else {
+ mHintText.setText(R.string.lock_pattern_confirm_error);
+ mFirstPattern = null;
+ mSecondPattern = null;
+ mLockPatternView.clearPattern();
+ }
+ }
+ }
+
+ private void handleChangeMode(List pattern) {
+ if (mOldPassword == null) {
+ String inputPassword = LockPasswordUtils.patternToString(
+ LockPasswordUtils.stringToPattern(
+ LockPasswordUtils.patternToString(convertPatternToIntArray(pattern))
+ )
+ );
+ String encryptedInput = LockPasswordUtils.encryptPassword(inputPassword);
+ String storedPassword = mWorkingNote.getLockPassword();
+
+ if (encryptedInput.equals(storedPassword)) {
+ mOldPassword = storedPassword;
+ mHintText.setText(R.string.lock_pattern_hint_new);
+ mLockPatternView.clearPattern();
+ } else {
+ mHintText.setText(R.string.lock_pattern_error);
+ mLockPatternView.clearPattern();
+ }
+ } else if (mFirstPattern == null) {
+ mFirstPattern = pattern;
+ mHintText.setText(R.string.lock_pattern_confirm);
+ mLockPatternView.clearPattern();
+ } else {
+ mSecondPattern = pattern;
+ if (patternsMatch(mFirstPattern, mSecondPattern)) {
+ String password = LockPasswordUtils.patternToString(
+ LockPasswordUtils.stringToPattern(
+ LockPasswordUtils.patternToString(convertPatternToIntArray(mFirstPattern))
+ )
+ );
+ String encryptedPassword = LockPasswordUtils.encryptPassword(password);
+ mWorkingNote.setLockPassword(encryptedPassword);
+ mWorkingNote.saveNote();
+ Toast.makeText(this, R.string.lock_pattern_success, Toast.LENGTH_SHORT).show();
+ finish();
+ } else {
+ mHintText.setText(R.string.lock_pattern_confirm_error);
+ mFirstPattern = null;
+ mSecondPattern = null;
+ mLockPatternView.clearPattern();
+ }
+ }
+ }
+
+ private void handleRemoveMode(List pattern) {
+ String inputPassword = LockPasswordUtils.patternToString(
+ LockPasswordUtils.stringToPattern(
+ LockPasswordUtils.patternToString(convertPatternToIntArray(pattern))
+ )
+ );
+ String encryptedInput = LockPasswordUtils.encryptPassword(inputPassword);
+ String storedPassword = mWorkingNote.getLockPassword();
+
+ if (encryptedInput.equals(storedPassword)) {
+ mWorkingNote.setLocked(false);
+ mWorkingNote.setLockPassword("");
+ mWorkingNote.setPasswordType("");
+ mWorkingNote.saveNote();
+ Toast.makeText(this, R.string.lock_pattern_removed, Toast.LENGTH_SHORT).show();
+ finish();
+ } else {
+ mHintText.setText(R.string.lock_pattern_error);
+ mLockPatternView.clearPattern();
+ }
+ }
+
+ private void handleConfirm() {
+ finish();
+ }
+
+ private boolean patternsMatch(List pattern1, List pattern2) {
+ if (pattern1 == null || pattern2 == null) {
+ return false;
+ }
+ if (pattern1.size() != pattern2.size()) {
+ return false;
+ }
+ for (int i = 0; i < pattern1.size(); i++) {
+ if (!pattern1.get(i).equals(pattern2.get(i))) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ private int[] convertPatternToIntArray(List pattern) {
+ int[] array = new int[pattern.size()];
+ for (int i = 0; i < pattern.size(); i++) {
+ array[i] = pattern.get(i);
+ }
+ return array;
+ }
+}
diff --git a/src/main/java/net/micode/notes/ui/UnlockActivity.java b/src/main/java/net/micode/notes/ui/UnlockActivity.java
new file mode 100644
index 0000000..7341628
--- /dev/null
+++ b/src/main/java/net/micode/notes/ui/UnlockActivity.java
@@ -0,0 +1,213 @@
+/*
+ * Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net)
+ *
+ * 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 net.micode.notes.ui;
+
+import android.app.Activity;
+import android.content.Intent;
+import android.os.Bundle;
+import android.os.Vibrator;
+import android.text.TextUtils;
+import android.view.View;
+import android.widget.Button;
+import android.widget.TextView;
+import android.widget.Toast;
+
+import net.micode.notes.R;
+import net.micode.notes.data.Notes;
+import net.micode.notes.model.WorkingNote;
+import net.micode.notes.tool.LockPasswordUtils;
+
+import java.util.List;
+
+/**
+ * 解锁界面
+ * 用于验证手势密码或数字密码,解锁便签
+ */
+public class UnlockActivity extends Activity {
+ private static final String TAG = "UnlockActivity";
+
+ public static final String EXTRA_NOTE_ID = "note_id";
+
+ private LockPatternView mLockPatternView;
+ private TextView mTitleText;
+ private TextView mHintText;
+ private Button mCancelButton;
+
+ private long mNoteId;
+ private WorkingNote mWorkingNote;
+ private String mPasswordType;
+ private String mEncryptedPassword;
+ private int mFailedAttempts;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_unlock);
+
+ mNoteId = getIntent().getLongExtra(EXTRA_NOTE_ID, 0);
+
+ if (mNoteId <= 0) {
+ finish();
+ return;
+ }
+
+ mWorkingNote = WorkingNote.load(this, mNoteId);
+ mPasswordType = mWorkingNote.getPasswordType();
+
+ if (!mWorkingNote.isLocked()) {
+ unlockNote();
+ return;
+ }
+
+ if (TextUtils.isEmpty(mPasswordType)) {
+ mPasswordType = LockPasswordUtils.TYPE_GESTURE;
+ }
+
+ if (LockPasswordUtils.TYPE_GESTURE.equals(mPasswordType)) {
+ mEncryptedPassword = mWorkingNote.getLockPassword();
+ } else if (LockPasswordUtils.TYPE_NUMERIC.equals(mPasswordType)) {
+ mEncryptedPassword = mWorkingNote.getNumericPassword();
+ }
+
+ if (TextUtils.isEmpty(mEncryptedPassword)) {
+ unlockNote();
+ return;
+ }
+
+ initViews();
+ }
+
+ private void initViews() {
+ mLockPatternView = (LockPatternView) findViewById(R.id.lock_pattern_view);
+ mTitleText = (TextView) findViewById(R.id.tv_unlock_title);
+ mHintText = (TextView) findViewById(R.id.tv_unlock_hint);
+ mCancelButton = (Button) findViewById(R.id.btn_cancel);
+
+ mTitleText.setText(getString(R.string.lock_note_title, mWorkingNote.getContent().substring(0,
+ Math.min(20, mWorkingNote.getContent().length()))));
+
+ if (LockPasswordUtils.TYPE_GESTURE.equals(mPasswordType)) {
+ setupGestureUnlock();
+ } else if (LockPasswordUtils.TYPE_NUMERIC.equals(mPasswordType)) {
+ setupNumericUnlock();
+ }
+
+ mCancelButton.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ finish();
+ }
+ });
+ }
+
+ private void setupGestureUnlock() {
+ mHintText.setText(R.string.lock_note_hint);
+ mLockPatternView.setVisibility(View.VISIBLE);
+ mLockPatternView.setOnPatternListener(new LockPatternView.OnPatternListener() {
+ @Override
+ public void onPatternStart() {
+ mHintText.setText("");
+ }
+
+ @Override
+ public void onPatternCleared() {
+ }
+
+ @Override
+ public void onPatternDetected(List pattern) {
+ handleGesturePatternDetected(pattern);
+ }
+ });
+ }
+
+ private void setupNumericUnlock() {
+ mHintText.setText(R.string.numeric_password_hint_verify);
+ mLockPatternView.setVisibility(View.GONE);
+ showNumericPasswordActivity();
+ }
+
+ private void showNumericPasswordActivity() {
+ Intent intent = new Intent(this, NumericPasswordActivity.class);
+ intent.putExtra(NumericPasswordActivity.EXTRA_NOTE_ID, mNoteId);
+ intent.putExtra(NumericPasswordActivity.EXTRA_MODE, NumericPasswordActivity.MODE_VERIFY);
+ startActivity(intent);
+ finish();
+ }
+
+ private void handleGesturePatternDetected(List pattern) {
+ if (pattern.size() < 4) {
+ mHintText.setText(R.string.lock_pattern_too_short);
+ mLockPatternView.clearPattern();
+ return;
+ }
+
+ String inputPassword = LockPasswordUtils.patternToString(
+ LockPasswordUtils.stringToPattern(
+ LockPasswordUtils.patternToString(convertPatternToIntArray(pattern))
+ )
+ );
+ String encryptedInput = LockPasswordUtils.encryptPassword(inputPassword);
+
+ if (encryptedInput.equals(mEncryptedPassword)) {
+ unlockNote();
+ } else {
+ mFailedAttempts++;
+ mHintText.setText(R.string.lock_pattern_error);
+ mLockPatternView.clearPattern();
+ vibrate();
+
+ if (mFailedAttempts >= 5) {
+ mHintText.setText(getString(R.string.lock_pattern_too_many_attempts));
+ mCancelButton.setEnabled(false);
+ mLockPatternView.setEnabled(false);
+
+ new android.os.Handler().postDelayed(new Runnable() {
+ @Override
+ public void run() {
+ mFailedAttempts = 0;
+ mHintText.setText(R.string.lock_note_hint);
+ mCancelButton.setEnabled(true);
+ mLockPatternView.setEnabled(true);
+ }
+ }, 30000);
+ }
+ }
+ }
+
+ private void unlockNote() {
+ Intent intent = new Intent(this, NoteEditActivity.class);
+ intent.setAction(Intent.ACTION_VIEW);
+ intent.putExtra(Intent.EXTRA_UID, mNoteId);
+ startActivity(intent);
+ finish();
+ }
+
+ private void vibrate() {
+ Vibrator vibrator = (Vibrator) getSystemService(VIBRATOR_SERVICE);
+ if (vibrator != null && vibrator.hasVibrator()) {
+ vibrator.vibrate(200);
+ }
+ }
+
+ private int[] convertPatternToIntArray(List pattern) {
+ int[] array = new int[pattern.size()];
+ for (int i = 0; i < pattern.size(); i++) {
+ array[i] = pattern.get(i);
+ }
+ return array;
+ }
+}
diff --git a/src/main/res/layout/activity_numeric_password.xml b/src/main/res/layout/activity_numeric_password.xml
new file mode 100644
index 0000000..b4808f7
--- /dev/null
+++ b/src/main/res/layout/activity_numeric_password.xml
@@ -0,0 +1,183 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/main/res/layout/activity_set_lock.xml b/src/main/res/layout/activity_set_lock.xml
new file mode 100644
index 0000000..e1ff9e6
--- /dev/null
+++ b/src/main/res/layout/activity_set_lock.xml
@@ -0,0 +1,82 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/main/res/layout/activity_unlock.xml b/src/main/res/layout/activity_unlock.xml
new file mode 100644
index 0000000..aeaeee5
--- /dev/null
+++ b/src/main/res/layout/activity_unlock.xml
@@ -0,0 +1,45 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/main/res/layout/note_item.xml b/src/main/res/layout/note_item.xml
index 3c7e739..6f671fe 100644
--- a/src/main/res/layout/note_item.xml
+++ b/src/main/res/layout/note_item.xml
@@ -83,4 +83,12 @@
android:layout_gravity="top|left"
android:src="@drawable/ic_pinned"
android:visibility="gone"/>
+
+
diff --git a/src/main/res/menu/note_edit.xml b/src/main/res/menu/note_edit.xml
index 35cacd1..e6a0273 100644
--- a/src/main/res/menu/note_edit.xml
+++ b/src/main/res/menu/note_edit.xml
@@ -1,5 +1,4 @@
-