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 @@ + + + + + + + + + + + + + + +