diff --git a/src/main/java/net/micode/notes/data/Notes.java b/src/main/java/net/micode/notes/data/Notes.java index 297b7dc..33f8d3a 100644 --- a/src/main/java/net/micode/notes/data/Notes.java +++ b/src/main/java/net/micode/notes/data/Notes.java @@ -265,6 +265,13 @@ public class Notes { */ public static final String LOCK_TYPE = "lock_type"; + /** + * 密码提示 + *
类型: TEXT
+ *用于帮助用户记忆密码的提示信息
+ */ + public static final String LOCK_HINT = "lock_hint"; + /** * 便签或文件夹被删除的时间 *类型: INTEGER (long)
diff --git a/src/main/java/net/micode/notes/data/NotesDatabaseHelper.java b/src/main/java/net/micode/notes/data/NotesDatabaseHelper.java index 4edf21d..d31c1c5 100644 --- a/src/main/java/net/micode/notes/data/NotesDatabaseHelper.java +++ b/src/main/java/net/micode/notes/data/NotesDatabaseHelper.java @@ -35,7 +35,7 @@ public class NotesDatabaseHelper extends SQLiteOpenHelper { // 数据库文件名 private static final String DB_NAME = "note.db"; // 数据库版本号,用于升级控制 - private static final int DB_VERSION = 7; + private static final int DB_VERSION = 8; /** * 数据库表名常量定义 @@ -79,6 +79,7 @@ public class NotesDatabaseHelper extends SQLiteOpenHelper { NoteColumns.IS_LOCKED + " INTEGER NOT NULL DEFAULT 0," + NoteColumns.LOCK_PASSWORD + " TEXT NOT NULL DEFAULT ''," + NoteColumns.LOCK_TYPE + " INTEGER NOT NULL DEFAULT 0," + + NoteColumns.LOCK_HINT + " TEXT NOT NULL DEFAULT ''," + NoteColumns.DELETED_DATE + " INTEGER NOT NULL DEFAULT 0" + ")"; @@ -425,6 +426,12 @@ public class NotesDatabaseHelper extends SQLiteOpenHelper { oldVersion++; } + // 版本7升级到版本8:添加密码提示字段 + if (oldVersion == 7) { + upgradeToV8(db); + oldVersion++; + } + // 如果需要,重新创建触发器 if (reCreateTriggers) { reCreateNoteTableTriggers(db); @@ -530,4 +537,14 @@ public class NotesDatabaseHelper extends SQLiteOpenHelper { " WHERE " + NoteColumns.ID + "=new." + NoteColumns.ID + ";" + " END"); } + + /** + * 升级到版本8:添加密码提示字段,用于密码记忆辅助 + * @param db 数据库实例 + */ + private void upgradeToV8(SQLiteDatabase db) { + // 添加LOCK_HINT字段,用于存储密码提示 + db.execSQL("ALTER TABLE " + TABLE.NOTE + " ADD COLUMN " + NoteColumns.LOCK_HINT + + " TEXT NOT NULL DEFAULT ''"); + } } \ No newline at end of file diff --git a/src/main/java/net/micode/notes/ui/NoteEditActivity.java b/src/main/java/net/micode/notes/ui/NoteEditActivity.java index c80949f..df2a73b 100644 --- a/src/main/java/net/micode/notes/ui/NoteEditActivity.java +++ b/src/main/java/net/micode/notes/ui/NoteEditActivity.java @@ -46,6 +46,9 @@ import android.view.View.OnClickListener; import android.view.WindowManager; import android.widget.Button; import android.widget.CheckBox; +import android.widget.LinearLayout; +import android.widget.TextView; +import android.util.TypedValue; import android.widget.CompoundButton; import android.widget.CompoundButton.OnCheckedChangeListener; import android.widget.EditText; @@ -172,6 +175,9 @@ public class NoteEditActivity extends Activity implements OnClickListener, /** 笔记编辑面板 */ private View mNoteEditorPanel; + /** 字符统计显示文本框 */ + private TextView mTvCharCount; + /** 工作笔记对象,用于处理笔记数据 */ private WorkingNote mWorkingNote; @@ -350,6 +356,8 @@ public class NoteEditActivity extends Activity implements OnClickListener, mNoteEditor.setText(getHighlightQueryResult(htmlContent, mUserQuery)); mNoteEditor.setSelection(mNoteEditor.getText().length()); } + // 初始化字符统计显示 + updateCharCount(); for (Integer id : sBgSelectorSelectionMap.keySet()) { findViewById(sBgSelectorSelectionMap.get(id)).setVisibility(View.GONE); } @@ -480,7 +488,9 @@ public class NoteEditActivity extends Activity implements OnClickListener, mNoteHeaderHolder.ibSetBgColor = (ImageButton) findViewById(R.id.btn_set_bg_color); mNoteHeaderHolder.ibSetBgColor.setOnClickListener(this); mNoteEditor = (NoteEditText) findViewById(R.id.note_edit_view); + mNoteEditor.setOnTextViewChangeListener(this); mNoteEditorPanel = findViewById(R.id.sv_note_edit); + mTvCharCount = (TextView) findViewById(R.id.tv_char_count); mNoteBgColorSelector = findViewById(R.id.note_bg_color_selector); for (int id : sBgSelectorBtnsMap.keySet()) { ImageView iv = (ImageView) findViewById(id); @@ -768,6 +778,9 @@ public class NoteEditActivity extends Activity implements OnClickListener, case R.id.menu_unlock: showPasswordDialogForUnlock(); break; + case R.id.menu_change_password: + showChangePasswordDialog(); + break; case R.id.menu_note_template: openTemplateSelector(); break; @@ -812,23 +825,41 @@ public class NoteEditActivity extends Activity implements OnClickListener, /** * 显示密码输入对话框用于锁定笔记 *- * 该方法用于显示一个密码输入对话框,让用户输入密码来锁定当前编辑的笔记 + * 该方法用于显示一个密码输入对话框,让用户输入密码和密码提示来锁定当前编辑的笔记 *
*/ private void showPasswordDialogForLock() { + // 创建布局并添加密码输入框 + LinearLayout layout = new LinearLayout(this); + layout.setOrientation(LinearLayout.VERTICAL); + layout.setPadding(60, 40, 60, 40); + layout.setPadding(60, 40, 60, 40); + // 设置子视图间距 + LinearLayout.LayoutParams params = new LinearLayout.LayoutParams( + LinearLayout.LayoutParams.MATCH_PARENT, + LinearLayout.LayoutParams.WRAP_CONTENT); + params.setMargins(0, 0, 0, 20); // 底部外边距作为间距 + final EditText passwordEditText = new EditText(this); passwordEditText.setHint(R.string.hint_enter_password); passwordEditText.setInputType(android.text.InputType.TYPE_CLASS_TEXT | android.text.InputType.TYPE_TEXT_VARIATION_PASSWORD); + layout.addView(passwordEditText); + + // 添加密码提示输入框 + final EditText hintEditText = new EditText(this); + hintEditText.setHint(R.string.hint_enter_password_hint); + layout.addView(hintEditText); new AlertDialog.Builder(this) .setTitle(R.string.dialog_enter_password) - .setView(passwordEditText) + .setView(layout) .setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { String password = passwordEditText.getText().toString(); + String hint = hintEditText.getText().toString(); if (!TextUtils.isEmpty(password)) { - lockCurrentNote(password); + lockCurrentNote(password, hint); } } }) @@ -843,13 +874,50 @@ public class NoteEditActivity extends Activity implements OnClickListener, * */ private void showPasswordDialogForUnlock() { + // 查询当前笔记的密码提示 + String[] projection = {NoteColumns.LOCK_HINT}; + String selection = NoteColumns.ID + "=?"; + String[] selectionArgs = {String.valueOf(mWorkingNote.getNoteId())}; + + Cursor cursor = getContentResolver().query(Notes.CONTENT_NOTE_URI, projection, selection, selectionArgs, null); + String lockHint = null; + if (cursor != null && cursor.moveToFirst()) { + lockHint = cursor.getString(0); + cursor.close(); + } + + // 创建布局并添加密码输入框 + LinearLayout layout = new LinearLayout(this); + layout.setOrientation(LinearLayout.VERTICAL); + layout.setPadding(60, 40, 60, 40); + + // 创建布局参数,用于设置子视图间距 + LinearLayout.LayoutParams params = new LinearLayout.LayoutParams( + LinearLayout.LayoutParams.MATCH_PARENT, + LinearLayout.LayoutParams.WRAP_CONTENT); + params.setMargins(0, 0, 0, 20); // 底部外边距作为间距 + final EditText passwordEditText = new EditText(this); passwordEditText.setHint(R.string.hint_enter_password); passwordEditText.setInputType(android.text.InputType.TYPE_CLASS_TEXT | android.text.InputType.TYPE_TEXT_VARIATION_PASSWORD); + layout.addView(passwordEditText, params); + + // 如果有密码提示,显示提示信息 + if (!TextUtils.isEmpty(lockHint)) { + TextView hintTextView = new TextView(this); + hintTextView.setText(getString(R.string.password_hint_prefix) + lockHint); + hintTextView.setTextColor(getResources().getColor(R.color.secondary_text_dark)); + hintTextView.setTextSize(TypedValue.COMPLEX_UNIT_SP, 14); + // 提示文本不需要底部间距 + LinearLayout.LayoutParams hintParams = new LinearLayout.LayoutParams( + LinearLayout.LayoutParams.MATCH_PARENT, + LinearLayout.LayoutParams.WRAP_CONTENT); + layout.addView(hintTextView, hintParams); + } new AlertDialog.Builder(this) .setTitle(R.string.dialog_enter_password) - .setView(passwordEditText) + .setView(layout) .setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { @@ -866,17 +934,20 @@ public class NoteEditActivity extends Activity implements OnClickListener, /** * 锁定当前笔记 *- * 该方法用于锁定当前编辑的笔记,更新其锁定状态、密码和锁定类型 + * 该方法用于锁定当前编辑的笔记,更新其锁定状态、密码、密码提示和锁定类型 *
* @param password 用于锁定的密码 + * @param hint 用于帮助记忆密码的提示 */ - private void lockCurrentNote(String password) { + private void lockCurrentNote(String password, String hint) { // 设置锁定状态为1(已锁定) mWorkingNote.setNoteValue(NoteColumns.IS_LOCKED, "1"); // 设置加密后的密码 mWorkingNote.setNoteValue(NoteColumns.LOCK_PASSWORD, encryptPassword(password)); // 设置锁定类型为笔记类型 mWorkingNote.setNoteValue(NoteColumns.LOCK_TYPE, String.valueOf(Notes.LOCK_TYPE_NOTE)); + // 设置密码提示 + mWorkingNote.setNoteValue(NoteColumns.LOCK_HINT, hint); // 保存笔记 saveNote(); // 显示提示信息 @@ -941,6 +1012,97 @@ public class NoteEditActivity extends Activity implements OnClickListener, return password; // 加密失败时返回原始密码 } } + + /** + * 显示修改密码对话框 + *+ * 该方法用于显示一个修改密码对话框,让用户输入旧密码、新密码和新的密码提示 + *
+ */ + private void showChangePasswordDialog() { + // 创建布局并添加输入框 + LinearLayout layout = new LinearLayout(this); + layout.setOrientation(LinearLayout.VERTICAL); + layout.setPadding(60, 40, 60, 40); + + // 创建布局参数,用于设置子视图间距 + LinearLayout.LayoutParams params = new LinearLayout.LayoutParams( + LinearLayout.LayoutParams.MATCH_PARENT, + LinearLayout.LayoutParams.WRAP_CONTENT); + params.setMargins(0, 0, 0, 20); // 底部外边距作为间距 + + // 旧密码输入框 + final EditText oldPasswordEditText = new EditText(this); + oldPasswordEditText.setHint(R.string.hint_enter_password); + oldPasswordEditText.setInputType(android.text.InputType.TYPE_CLASS_TEXT | android.text.InputType.TYPE_TEXT_VARIATION_PASSWORD); + layout.addView(oldPasswordEditText, params); + + // 新密码输入框 + final EditText newPasswordEditText = new EditText(this); + newPasswordEditText.setHint(R.string.hint_enter_password); + newPasswordEditText.setInputType(android.text.InputType.TYPE_CLASS_TEXT | android.text.InputType.TYPE_TEXT_VARIATION_PASSWORD); + layout.addView(newPasswordEditText, params); + + // 新密码提示输入框 + final EditText newHintEditText = new EditText(this); + newHintEditText.setHint(R.string.hint_enter_password_hint); + layout.addView(newHintEditText); + + new AlertDialog.Builder(this) + .setTitle(R.string.menu_change_password) + .setView(layout) + .setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + String oldPassword = oldPasswordEditText.getText().toString(); + String newPassword = newPasswordEditText.getText().toString(); + String newHint = newHintEditText.getText().toString(); + if (!TextUtils.isEmpty(oldPassword) && !TextUtils.isEmpty(newPassword)) { + changePassword(oldPassword, newPassword, newHint); + } + } + }) + .setNegativeButton(android.R.string.cancel, null) + .show(); + } + + /** + * 修改密码 + *+ * 该方法用于修改当前笔记的密码,需要验证旧密码,验证通过后更新新密码和密码提示 + *
+ * @param oldPassword 旧密码 + * @param newPassword 新密码 + * @param newHint 新的密码提示 + */ + private void changePassword(String oldPassword, String newPassword, String newHint) { + // 使用ContentResolver直接查询数据库获取加密密码 + String[] projection = {NoteColumns.LOCK_PASSWORD}; + String selection = NoteColumns.ID + "=?"; + String[] selectionArgs = {String.valueOf(mWorkingNote.getNoteId())}; + + Cursor cursor = getContentResolver().query(Notes.CONTENT_NOTE_URI, projection, selection, selectionArgs, null); + String encryptedPassword = null; + + if (cursor != null && cursor.moveToFirst()) { + encryptedPassword = cursor.getString(0); + cursor.close(); + } + + // 验证旧密码是否正确 + if (encryptedPassword != null && encryptedPassword.equals(encryptPassword(oldPassword))) { + // 旧密码正确,更新新密码和密码提示 + mWorkingNote.setNoteValue(NoteColumns.LOCK_PASSWORD, encryptPassword(newPassword)); + mWorkingNote.setNoteValue(NoteColumns.LOCK_HINT, newHint); + // 保存笔记 + saveNote(); + // 显示提示信息 + Toast.makeText(this, getString(R.string.message_password_changed), Toast.LENGTH_SHORT).show(); + } else { + // 旧密码错误,显示错误信息 + Toast.makeText(this, getString(R.string.error_wrong_password), Toast.LENGTH_SHORT).show(); + } + } /** * 创建新笔记 @@ -1229,6 +1391,15 @@ public class NoteEditActivity extends Activity implements OnClickListener, } else { mEditTextList.getChildAt(index).findViewById(R.id.cb_edit_item).setVisibility(View.GONE); } + // 更新字符统计 + updateCharCount(); + } + + /** + * 当文本内容变化时,更新字符统计 + */ + public void onContentChange() { + updateCharCount(); } /** @@ -1382,6 +1553,45 @@ public class NoteEditActivity extends Activity implements OnClickListener, showToast(resId, Toast.LENGTH_SHORT); } + /** + * 计算字符串中的有效字符数(不包括空格、换行符等空白字符) + * @param text 要计算的字符串 + * @return 有效字符数 + */ + private int countValidCharacters(String text) { + if (TextUtils.isEmpty(text)) { + return 0; + } + // 去除所有空白字符后计算长度 + return text.replaceAll("\\s", "").length(); + } + + /** + * 更新字符统计显示 + */ + private void updateCharCount() { + int charCount = 0; + if (mWorkingNote.getCheckListMode() == TextNote.MODE_CHECK_LIST) { + // 列表模式:遍历所有列表项 + for (int i = 0; i < mEditTextList.getChildCount(); i++) { + View view = mEditTextList.getChildAt(i); + NoteEditText edit = (NoteEditText) view.findViewById(R.id.et_edit_text); + if (!TextUtils.isEmpty(edit.getText())) { + charCount += countValidCharacters(edit.getText().toString()); + } + } + } else { + // 普通文本模式:直接计算编辑框内容 + if (!TextUtils.isEmpty(mNoteEditor.getText())) { + charCount = countValidCharacters(mNoteEditor.getText().toString()); + } + } + // 更新显示 + if (mTvCharCount != null) { + mTvCharCount.setText(charCount + " 字符"); + } + } + /** * 打开模板选择页面 *diff --git a/src/main/java/net/micode/notes/ui/NoteEditText.java b/src/main/java/net/micode/notes/ui/NoteEditText.java index 5a6842e..cd75b4c 100644 --- a/src/main/java/net/micode/notes/ui/NoteEditText.java +++ b/src/main/java/net/micode/notes/ui/NoteEditText.java @@ -37,6 +37,7 @@ import android.view.MenuItem; import android.view.MenuItem.OnMenuItemClickListener; import android.view.MotionEvent; import android.widget.EditText; +import android.text.TextWatcher; import net.micode.notes.R; @@ -112,6 +113,11 @@ public class NoteEditText extends EditText { * 当文本变化时,隐藏或显示项目选项 */ void onTextChange(int index, boolean hasText); + + /** + * 当文本内容变化时,更新字符统计 + */ + void onContentChange(); } /** @@ -151,6 +157,8 @@ public class NoteEditText extends EditText { */ public NoteEditText(Context context, AttributeSet attrs) { super(context, attrs, android.R.attr.editTextStyle); + mIndex = 0; + initTextWatcher(); } /** @@ -161,7 +169,30 @@ public class NoteEditText extends EditText { */ public NoteEditText(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); - // TODO Auto-generated constructor stub + mIndex = 0; + initTextWatcher(); + } + + /** + * 初始化文本变化监听器 + */ + private void initTextWatcher() { + addTextChangedListener(new TextWatcher() { + @Override + public void beforeTextChanged(CharSequence s, int start, int count, int after) { + } + + @Override + public void onTextChanged(CharSequence s, int start, int before, int count) { + } + + @Override + public void afterTextChanged(android.text.Editable s) { + if (mOnTextViewChangeListener != null) { + mOnTextViewChangeListener.onContentChange(); + } + } + }); } /** @@ -207,7 +238,9 @@ public class NoteEditText extends EditText { public boolean onKeyDown(int keyCode, KeyEvent event) { switch (keyCode) { case KeyEvent.KEYCODE_ENTER: - if (mOnTextViewChangeListener != null) { + // 只有在列表模式下(mIndex > 0)才阻止默认行为 + // 普通文本模式下让系统默认处理回车键 + if (mOnTextViewChangeListener != null && mIndex > 0) { return false; } break; @@ -244,10 +277,14 @@ public class NoteEditText extends EditText { break; case KeyEvent.KEYCODE_ENTER: if (mOnTextViewChangeListener != null) { - int selectionStart = getSelectionStart(); - String text = getText().subSequence(selectionStart, length()).toString(); - setText(getText().subSequence(0, selectionStart)); - mOnTextViewChangeListener.onEditTextEnter(mIndex + 1, text); + // 只有在列表模式下(mIndex > 0)才执行特殊处理 + // 普通文本模式下,让系统默认处理回车键 + if (mIndex > 0) { + int selectionStart = getSelectionStart(); + String text = getText().subSequence(selectionStart, length()).toString(); + setText(getText().subSequence(0, selectionStart)); + mOnTextViewChangeListener.onEditTextEnter(mIndex + 1, text); + } } else { Log.d(TAG, "OnTextViewChangeListener was not seted"); } diff --git a/src/main/java/net/micode/notes/ui/NoteItemData.java b/src/main/java/net/micode/notes/ui/NoteItemData.java index 2dcdedf..9a51882 100644 --- a/src/main/java/net/micode/notes/ui/NoteItemData.java +++ b/src/main/java/net/micode/notes/ui/NoteItemData.java @@ -57,6 +57,7 @@ public class NoteItemData { NoteColumns.IS_LOCKED, NoteColumns.LOCK_PASSWORD, NoteColumns.LOCK_TYPE, + NoteColumns.LOCK_HINT, NoteColumns.DELETED_DATE, }; @@ -140,10 +141,15 @@ public class NoteItemData { */ private static final int LOCK_TYPE_COLUMN = 15; + /** + * 查询结果中密码提示列的索引 + */ + private static final int LOCK_HINT_COLUMN = 16; + /** * 查询结果中删除日期列的索引 */ - private static final int DELETED_DATE_COLUMN = 16; + private static final int DELETED_DATE_COLUMN = 17; /** * 笔记ID @@ -170,6 +176,11 @@ public class NoteItemData { */ private int mLockType; + /** + * 密码提示 + */ + private String mLockHint; + /** * 提醒日期 */ @@ -290,6 +301,7 @@ public class NoteItemData { mIsLocked = (cursor.getInt(IS_LOCKED_COLUMN) > 0) ? true : false; mLockPassword = cursor.getString(LOCK_PASSWORD_COLUMN); mLockType = cursor.getInt(LOCK_TYPE_COLUMN); + mLockHint = cursor.getString(LOCK_HINT_COLUMN); mDeletedDate = cursor.getLong(DELETED_DATE_COLUMN); mPhoneNumber = ""; @@ -565,6 +577,22 @@ public class NoteItemData { return mLockType; } + /** + * 获取密码提示 + * @return 密码提示字符串 + */ + public String getLockHint() { + return mLockHint; + } + + /** + * 设置密码提示 + * @param hint 密码提示字符串 + */ + public void setLockHint(String hint) { + mLockHint = hint; + } + /** * 设置锁定类型 * @param type 锁定类型,0表示笔记,1表示文件夹 diff --git a/src/main/java/net/micode/notes/ui/NotesListActivity.java b/src/main/java/net/micode/notes/ui/NotesListActivity.java index ae6ab3f..7eb7a8a 100644 --- a/src/main/java/net/micode/notes/ui/NotesListActivity.java +++ b/src/main/java/net/micode/notes/ui/NotesListActivity.java @@ -57,8 +57,10 @@ import android.widget.Button; import android.widget.EditText; import android.widget.ListView; import android.widget.PopupMenu; +import android.widget.LinearLayout; import android.widget.TextView; import android.widget.Toast; +import android.util.TypedValue; import net.micode.notes.R; import net.micode.notes.data.Notes; @@ -124,19 +126,37 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt *
*/ private void showPasswordDialog() { + // 创建布局并添加输入框 + LinearLayout layout = new LinearLayout(this); + layout.setOrientation(LinearLayout.VERTICAL); + layout.setPadding(60, 40, 60, 40); + + // 创建布局参数,用于设置子视图间距 + LinearLayout.LayoutParams params = new LinearLayout.LayoutParams( + LinearLayout.LayoutParams.MATCH_PARENT, + LinearLayout.LayoutParams.WRAP_CONTENT); + params.setMargins(0, 0, 0, 20); // 底部外边距作为间距 + final EditText passwordEditText = new EditText(this); passwordEditText.setHint(R.string.hint_enter_password); passwordEditText.setInputType(android.text.InputType.TYPE_CLASS_TEXT | android.text.InputType.TYPE_TEXT_VARIATION_PASSWORD); + layout.addView(passwordEditText, params); + + // 添加密码提示输入框 + final EditText hintEditText = new EditText(this); + hintEditText.setHint(R.string.hint_enter_password_hint); + layout.addView(hintEditText); new AlertDialog.Builder(this) .setTitle(R.string.dialog_enter_password) - .setView(passwordEditText) + .setView(layout) .setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { String password = passwordEditText.getText().toString(); + String hint = hintEditText.getText().toString(); if (!TextUtils.isEmpty(password)) { - toggleLockedStatus(password); + toggleLockedStatus(password, hint); } } }) @@ -150,8 +170,9 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt * 该方法用于切换选中笔记或文件夹的锁定状态,使用异步任务处理数据库操作 * * @param password 用于锁定的密码 + * @param hint 用于帮助记忆密码的提示 */ - private void toggleLockedStatus(final String password) { + private void toggleLockedStatus(final String password, final String hint) { final HashSet