diff --git a/src/main/java/net/micode/notes/data/Notes.java b/src/main/java/net/micode/notes/data/Notes.java index 4b82197..11b031a 100644 --- a/src/main/java/net/micode/notes/data/Notes.java +++ b/src/main/java/net/micode/notes/data/Notes.java @@ -99,6 +99,16 @@ public class Notes { * 组件类型:4x4大小的便签组件 */ public static final int TYPE_WIDGET_4X = 1; + + /** + * 锁类型:便签锁 + */ + public static final int LOCK_TYPE_NOTE = 0; + + /** + * 锁类型:文件夹锁 + */ + public static final int LOCK_TYPE_FOLDER = 1; /** * 数据类型常量类,定义了不同类型便签的数据项类型 @@ -234,6 +244,26 @@ public class Notes { *

0表示未置顶,1表示置顶

*/ public static final String PINNED = "pinned"; + + /** + * 是否上锁 + *

类型: INTEGER

+ *

0表示未上锁,1表示上锁

+ */ + public static final String IS_LOCKED = "is_locked"; + + /** + * 加密后的密码 + *

类型: TEXT

+ */ + public static final String LOCK_PASSWORD = "lock_password"; + + /** + * 锁类型 + *

类型: INTEGER

+ *

0表示便签锁,1表示文件夹锁

+ */ + public static final String LOCK_TYPE = "lock_type"; } public interface DataColumns { diff --git a/src/main/java/net/micode/notes/data/NotesDatabaseHelper.java b/src/main/java/net/micode/notes/data/NotesDatabaseHelper.java index 5eccfec..ce29b9f 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 = 5; + private static final int DB_VERSION = 6; /** * 数据库表名常量定义 @@ -75,7 +75,10 @@ public class NotesDatabaseHelper extends SQLiteOpenHelper { NoteColumns.ORIGIN_PARENT_ID + " INTEGER NOT NULL DEFAULT 0," + NoteColumns.GTASK_ID + " TEXT NOT NULL DEFAULT ''," + NoteColumns.VERSION + " INTEGER NOT NULL DEFAULT 0," + - NoteColumns.PINNED + " INTEGER NOT NULL DEFAULT 0" + + NoteColumns.PINNED + " INTEGER NOT NULL DEFAULT 0," + + NoteColumns.IS_LOCKED + " INTEGER NOT NULL DEFAULT 0," + + NoteColumns.LOCK_PASSWORD + " TEXT NOT NULL DEFAULT ''," + + NoteColumns.LOCK_TYPE + " INTEGER NOT NULL DEFAULT 0" + ")"; /** @@ -385,6 +388,12 @@ public class NotesDatabaseHelper extends SQLiteOpenHelper { oldVersion++; } + // 版本5升级到版本6:添加锁相关字段 + if (oldVersion == 5) { + upgradeToV6(db); + oldVersion++; + } + // 如果需要,重新创建触发器 if (reCreateTriggers) { reCreateNoteTableTriggers(db); @@ -445,4 +454,17 @@ public class NotesDatabaseHelper extends SQLiteOpenHelper { db.execSQL("ALTER TABLE " + TABLE.NOTE + " ADD COLUMN " + NoteColumns.PINNED + " INTEGER NOT NULL DEFAULT 0"); } + + /** + * 升级到版本6:添加锁相关字段,用于便签和文件夹上锁功能 + * @param db 数据库实例 + */ + 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 ''"); + db.execSQL("ALTER TABLE " + TABLE.NOTE + " ADD COLUMN " + NoteColumns.LOCK_TYPE + + " INTEGER NOT NULL DEFAULT 0"); + } } \ No newline at end of file diff --git a/src/main/java/net/micode/notes/model/WorkingNote.java b/src/main/java/net/micode/notes/model/WorkingNote.java index 36003cc..e3b2398 100644 --- a/src/main/java/net/micode/notes/model/WorkingNote.java +++ b/src/main/java/net/micode/notes/model/WorkingNote.java @@ -504,19 +504,45 @@ public class WorkingNote { } } + /** + * 设置笔记的基本属性值 + *

+ * 直接调用内部Note对象的setNoteValue方法,用于更新笔记的基本属性。 + *

+ * + * @param key 属性键名,对应NoteColumns中的字段名 + * @param value 属性值 + */ + public void setNoteValue(String key, String value) { + mNote.setNoteValue(key, value); + } + /** * 设置笔记内容 *

+ * 设置笔记的文本内容,支持富文本格式。 + *

+ * + * @param text 笔记内容,支持Spannable等富文本格式 + */ + public void setWorkingText(CharSequence text) { + String textStr = text.toString(); + if (!TextUtils.equals(mContent, textStr)) { + mContent = textStr; + mNote.setTextData(DataColumns.CONTENT, mContent); + } + } + + /** + * 设置笔记内容(字符串重载) + *

* 设置笔记的文本内容。 *

* * @param text 笔记内容 */ public void setWorkingText(String text) { - if (!TextUtils.equals(mContent, text)) { - mContent = text; - mNote.setTextData(DataColumns.CONTENT, mContent); - } + setWorkingText((CharSequence) text); } /** diff --git a/src/main/java/net/micode/notes/ui/NoteEditActivity.java b/src/main/java/net/micode/notes/ui/NoteEditActivity.java index 088dac7..184fe3b 100644 --- a/src/main/java/net/micode/notes/ui/NoteEditActivity.java +++ b/src/main/java/net/micode/notes/ui/NoteEditActivity.java @@ -30,6 +30,7 @@ import android.content.SharedPreferences; import android.graphics.Paint; import android.os.Bundle; import android.preference.PreferenceManager; +import android.text.Html; import android.text.Spannable; import android.text.SpannableString; import android.text.TextUtils; @@ -43,11 +44,17 @@ import android.view.MotionEvent; import android.view.View; import android.view.View.OnClickListener; import android.view.WindowManager; +import android.widget.Button; import android.widget.CheckBox; import android.widget.CompoundButton; import android.widget.CompoundButton.OnCheckedChangeListener; import android.widget.EditText; +import android.widget.ImageButton; import android.widget.ImageView; +import android.database.Cursor; +import android.content.ContentResolver; +import net.micode.notes.data.Notes; +import net.micode.notes.data.Notes.NoteColumns; import android.widget.LinearLayout; import android.widget.TextView; import android.widget.Toast; @@ -91,7 +98,7 @@ public class NoteEditActivity extends Activity implements OnClickListener, public TextView tvAlertDate; - public ImageView ibSetBgColor; + public ImageButton ibSetBgColor; } /** 背景颜色选择按钮映射表,将按钮ID映射到颜色ID */ @@ -141,14 +148,25 @@ public class NoteEditActivity extends Activity implements OnClickListener, /** 头部视图面板 */ private View mHeadViewPanel; + /** 粗体按钮 */ + private Button mBtnBold; + /** 斜体按钮 */ + private Button mBtnItalic; + /** 下划线按钮 */ + private Button mBtnUnderline; + /** 字体颜色按钮 */ + private Button mBtnTextColor; + /** 背景颜色选择器视图 */ private View mNoteBgColorSelector; + /** 字体颜色选择器视图 */ + private View mNoteTextColorSelector; /** 字体大小选择器视图 */ private View mFontSizeSelector; /** 笔记编辑文本框 */ - private EditText mNoteEditor; + private NoteEditText mNoteEditor; /** 笔记编辑面板 */ private View mNoteEditorPanel; @@ -322,7 +340,10 @@ public class NoteEditActivity extends Activity implements OnClickListener, if (mWorkingNote.getCheckListMode() == TextNote.MODE_CHECK_LIST) { switchToListMode(mWorkingNote.getContent()); } else { - mNoteEditor.setText(getHighlightQueryResult(mWorkingNote.getContent(), mUserQuery)); + // 解析HTML格式的富文本内容,添加null检查防止闪退 + String content = mWorkingNote.getContent(); + CharSequence htmlContent = Html.fromHtml(content == null ? "" : content); + mNoteEditor.setText(getHighlightQueryResult(htmlContent, mUserQuery)); mNoteEditor.setSelection(mNoteEditor.getText().length()); } for (Integer id : sBgSelectorSelectionMap.keySet()) { @@ -408,6 +429,12 @@ public class NoteEditActivity extends Activity implements OnClickListener, return true; } + if (mNoteTextColorSelector.getVisibility() == View.VISIBLE + && !inRangeOfView(mNoteTextColorSelector, ev)) { + mNoteTextColorSelector.setVisibility(View.GONE); + return true; + } + if (mFontSizeSelector.getVisibility() == View.VISIBLE && !inRangeOfView(mFontSizeSelector, ev)) { mFontSizeSelector.setVisibility(View.GONE); @@ -446,9 +473,9 @@ public class NoteEditActivity extends Activity implements OnClickListener, mNoteHeaderHolder.tvModified = (TextView) findViewById(R.id.tv_modified_date); mNoteHeaderHolder.ivAlertIcon = (ImageView) findViewById(R.id.iv_alert_icon); mNoteHeaderHolder.tvAlertDate = (TextView) findViewById(R.id.tv_alert_date); - mNoteHeaderHolder.ibSetBgColor = (ImageView) findViewById(R.id.btn_set_bg_color); + mNoteHeaderHolder.ibSetBgColor = (ImageButton) findViewById(R.id.btn_set_bg_color); mNoteHeaderHolder.ibSetBgColor.setOnClickListener(this); - mNoteEditor = (EditText) findViewById(R.id.note_edit_view); + mNoteEditor = (NoteEditText) 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()) { @@ -456,7 +483,28 @@ public class NoteEditActivity extends Activity implements OnClickListener, iv.setOnClickListener(this); } + // 初始化富文本按钮 + mBtnBold = findViewById(R.id.btn_bold); + mBtnItalic = findViewById(R.id.btn_italic); + mBtnUnderline = findViewById(R.id.btn_underline); + mBtnTextColor = findViewById(R.id.btn_set_text_color); + mBtnBold.setOnClickListener(this); + mBtnItalic.setOnClickListener(this); + mBtnUnderline.setOnClickListener(this); + mBtnTextColor.setOnClickListener(this); + mFontSizeSelector = findViewById(R.id.font_size_selector); + mNoteTextColorSelector = findViewById(R.id.note_text_color_selector); + + // 初始化字体颜色选择器的颜色选项 + ImageView ivTextBlack = findViewById(R.id.iv_text_black); + ImageView ivTextWhite = findViewById(R.id.iv_text_white); + ImageView ivTextRed = findViewById(R.id.iv_text_red); + ImageView ivTextBlue = findViewById(R.id.iv_text_blue); + ivTextBlack.setOnClickListener(this); + ivTextWhite.setOnClickListener(this); + ivTextRed.setOnClickListener(this); + ivTextBlue.setOnClickListener(this); for (int id : sFontSizeBtnsMap.keySet()) { View view = findViewById(id); view.setOnClickListener(this); @@ -472,6 +520,9 @@ public class NoteEditActivity extends Activity implements OnClickListener, mFontSizeId = ResourceParser.BG_DEFAULT_FONT_SIZE; } mEditTextList = (LinearLayout) findViewById(R.id.note_edit_list); + + // 将光标改为文本选中光标 + mNoteEditor.setTextIsSelectable(true); } /** @@ -516,15 +567,39 @@ public class NoteEditActivity extends Activity implements OnClickListener, public void onClick(View v) { int id = v.getId(); if (id == R.id.btn_set_bg_color) { + // 显示背景颜色选择器,隐藏其他选择器 mNoteBgColorSelector.setVisibility(View.VISIBLE); + mNoteTextColorSelector.setVisibility(View.GONE); findViewById(sBgSelectorSelectionMap.get(mWorkingNote.getBgColorId())).setVisibility( - - View.VISIBLE); + View.VISIBLE); + } else if (id == R.id.btn_set_text_color) { + // 显示字体颜色选择器,隐藏其他选择器 + mNoteTextColorSelector.setVisibility(View.VISIBLE); + mNoteBgColorSelector.setVisibility(View.GONE); } else if (sBgSelectorBtnsMap.containsKey(id)) { + // 处理背景颜色选择 findViewById(sBgSelectorSelectionMap.get(mWorkingNote.getBgColorId())).setVisibility( View.GONE); mWorkingNote.setBgColorId(sBgSelectorBtnsMap.get(id)); mNoteBgColorSelector.setVisibility(View.GONE); + } else if (id == R.id.iv_text_black) { + // 应用黑色字体 + mNoteEditor.applyTextColor(0xFF000000); + mNoteTextColorSelector.setVisibility(View.GONE); + } else if (id == R.id.iv_text_white) { + // 应用白色字体 + mNoteEditor.applyTextColor(0xFFFFFFFF); + mNoteTextColorSelector.setVisibility(View.GONE); + } else if (id == R.id.iv_text_red) { + // 应用红色字体 + mNoteEditor.applyTextColor(0xFFFF0000); + mNoteTextColorSelector.setVisibility(View.GONE); + } else if (id == R.id.iv_text_blue) { + // 应用蓝色字体 + mNoteEditor.applyTextColor(0xFF0000FF); + mNoteTextColorSelector.setVisibility(View.GONE); } else if (sFontSizeBtnsMap.containsKey(id)) { + // 处理字体大小选择 findViewById(sFontSelectorSelectionMap.get(mFontSizeId)).setVisibility(View.GONE); mFontSizeId = sFontSizeBtnsMap.get(id); mSharedPrefs.edit().putInt(PREFERENCE_FONT_SIZE, mFontSizeId).commit(); @@ -537,6 +612,15 @@ public class NoteEditActivity extends Activity implements OnClickListener, TextAppearanceResources.getTexAppearanceResource(mFontSizeId)); } mFontSizeSelector.setVisibility(View.GONE); + } else if (id == R.id.btn_bold) { + // 应用粗体 + mNoteEditor.applyBold(); + } else if (id == R.id.btn_italic) { + // 应用斜体 + mNoteEditor.applyItalic(); + } else if (id == R.id.btn_underline) { + // 应用下划线 + mNoteEditor.applyUnderline(); } } @@ -565,6 +649,9 @@ public class NoteEditActivity extends Activity implements OnClickListener, if (mNoteBgColorSelector.getVisibility() == View.VISIBLE) { mNoteBgColorSelector.setVisibility(View.GONE); return true; + } else if (mNoteTextColorSelector.getVisibility() == View.VISIBLE) { + mNoteTextColorSelector.setVisibility(View.GONE); + return true; } else if (mFontSizeSelector.getVisibility() == View.VISIBLE) { mFontSizeSelector.setVisibility(View.GONE); return true; @@ -671,6 +758,12 @@ public class NoteEditActivity extends Activity implements OnClickListener, case R.id.menu_delete_remind: mWorkingNote.setAlertDate(0, false); break; + case R.id.menu_lock: + showPasswordDialogForLock(); + break; + case R.id.menu_unlock: + showPasswordDialogForUnlock(); + break; default: break; } @@ -709,6 +802,139 @@ public class NoteEditActivity extends Activity implements OnClickListener, context.startActivity(intent); } + /** + * 显示密码输入对话框用于锁定笔记 + *

+ * 该方法用于显示一个密码输入对话框,让用户输入密码来锁定当前编辑的笔记 + *

+ */ + private void showPasswordDialogForLock() { + 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); + + new AlertDialog.Builder(this) + .setTitle(R.string.dialog_enter_password) + .setView(passwordEditText) + .setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + String password = passwordEditText.getText().toString(); + if (!TextUtils.isEmpty(password)) { + lockCurrentNote(password); + } + } + }) + .setNegativeButton(android.R.string.cancel, null) + .show(); + } + + /** + * 显示密码输入对话框用于解锁笔记 + *

+ * 该方法用于显示一个密码输入对话框,让用户输入密码来解锁当前编辑的笔记 + *

+ */ + private void showPasswordDialogForUnlock() { + 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); + + new AlertDialog.Builder(this) + .setTitle(R.string.dialog_enter_password) + .setView(passwordEditText) + .setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + String password = passwordEditText.getText().toString(); + if (!TextUtils.isEmpty(password)) { + unlockCurrentNote(password); + } + } + }) + .setNegativeButton(android.R.string.cancel, null) + .show(); + } + + /** + * 锁定当前笔记 + *

+ * 该方法用于锁定当前编辑的笔记,更新其锁定状态、密码和锁定类型 + *

+ * @param password 用于锁定的密码 + */ + private void lockCurrentNote(String password) { + // 设置锁定状态为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)); + // 保存笔记 + saveNote(); + // 显示提示信息 + Toast.makeText(this, getString(R.string.message_note_locked), Toast.LENGTH_SHORT).show(); + } + + /** + * 解锁当前笔记 + *

+ * 该方法用于解锁当前编辑的笔记,更新其锁定状态和密码 + *

+ * @param password 用于解锁的密码 + */ + private void unlockCurrentNote(String password) { + // 使用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(password))) { + // 密码正确,设置锁定状态为0(未锁定) + mWorkingNote.setNoteValue(NoteColumns.IS_LOCKED, "0"); + // 清空密码 + mWorkingNote.setNoteValue(NoteColumns.LOCK_PASSWORD, ""); + // 保存笔记 + saveNote(); + // 显示提示信息 + Toast.makeText(this, getString(R.string.message_note_unlocked), Toast.LENGTH_SHORT).show(); + } else { + // 密码错误,显示错误信息 + Toast.makeText(this, getString(R.string.error_wrong_password), Toast.LENGTH_SHORT).show(); + } + } + + /** + * 加密密码 + *

+ * 该方法使用Base64和ANDROID_ID对密码进行加密 + *

+ * @param password 原始密码 + * @return 加密后的密码 + */ + private String encryptPassword(String password) { + try { + // 获取设备的ANDROID_ID作为加密密钥 + String androidId = android.provider.Settings.Secure.getString(getContentResolver(), + android.provider.Settings.Secure.ANDROID_ID); + // 使用简单的加密算法:将密码与ANDROID_ID拼接后进行Base64编码 + String combined = password + androidId; + return android.util.Base64.encodeToString(combined.getBytes(), android.util.Base64.DEFAULT); + } catch (Exception e) { + Log.e(TAG, "Password encryption failed", e); + return password; // 加密失败时返回原始密码 + } + } + /** * 创建新笔记 *

@@ -910,11 +1136,11 @@ public class NoteEditActivity extends Activity implements OnClickListener, *

* 该方法在完整文本中查找用户查询的内容,并为匹配项添加背景高亮效果 *

- * @param fullText 完整的文本内容 + * @param fullText 完整的文本内容,支持CharSequence类型 * @param userQuery 用户查询的内容 * @return 带有高亮查询结果的Spannable对象 */ - private Spannable getHighlightQueryResult(String fullText, String userQuery) { + private Spannable getHighlightQueryResult(CharSequence fullText, String userQuery) { SpannableString spannable = new SpannableString(fullText == null ? "" : fullText); if (!TextUtils.isEmpty(userQuery)) { mPattern = Pattern.compile(userQuery); @@ -1048,7 +1274,8 @@ public class NoteEditActivity extends Activity implements OnClickListener, } mWorkingNote.setWorkingText(sb.toString()); } else { - mWorkingNote.setWorkingText(mNoteEditor.getText().toString()); + // 保留富文本格式,将 Spanned 转换为 HTML 字符串保存 + mWorkingNote.setWorkingText(Html.toHtml(mNoteEditor.getText())); } return hasChecked; } diff --git a/src/main/java/net/micode/notes/ui/NoteEditText.java b/src/main/java/net/micode/notes/ui/NoteEditText.java index ae5b78b..5a6842e 100644 --- a/src/main/java/net/micode/notes/ui/NoteEditText.java +++ b/src/main/java/net/micode/notes/ui/NoteEditText.java @@ -20,9 +20,15 @@ import android.content.Context; import android.graphics.Rect; import android.text.Layout; import android.text.Selection; +import android.text.Spannable; +import android.text.SpannableStringBuilder; import android.text.Spanned; import android.text.TextUtils; +import android.text.style.StyleSpan; import android.text.style.URLSpan; +import android.text.style.UnderlineSpan; +import android.text.style.ForegroundColorSpan; +import android.graphics.Typeface; import android.util.AttributeSet; import android.util.Log; import android.view.ContextMenu; @@ -316,4 +322,108 @@ public class NoteEditText extends EditText { } super.onCreateContextMenu(menu); } + + /** + * 应用粗体格式到选中文本 + */ + public void applyBold() { + applyStyle(Typeface.BOLD); + } + + /** + * 应用斜体格式到选中文本 + */ + public void applyItalic() { + applyStyle(Typeface.ITALIC); + } + + /** + * 应用下划线格式到选中文本 + */ + public void applyUnderline() { + SpannableStringBuilder ssb = new SpannableStringBuilder(getText()); + int start = getSelectionStart(); + int end = getSelectionEnd(); + + if (start != end) { + // 检查是否已存在下划线样式 + UnderlineSpan[] existingUnderlines = ssb.getSpans(start, end, UnderlineSpan.class); + if (existingUnderlines.length > 0) { + // 移除下划线 + for (UnderlineSpan span : existingUnderlines) { + ssb.removeSpan(span); + } + } else { + // 添加下划线 + ssb.setSpan(new UnderlineSpan(), start, end, Spannable.SPAN_EXCLUSIVE_INCLUSIVE); + } + setText(ssb); + setSelection(start, end); + } + } + + /** + * 应用指定颜色到选中文本 + * @param color 要应用的颜色值 + */ + public void applyTextColor(int color) { + SpannableStringBuilder ssb = new SpannableStringBuilder(getText()); + int start = getSelectionStart(); + int end = getSelectionEnd(); + + if (start != end) { + // 移除已有的文字颜色 + ForegroundColorSpan[] existingSpans = ssb.getSpans(start, end, ForegroundColorSpan.class); + for (ForegroundColorSpan span : existingSpans) { + ssb.removeSpan(span); + } + + // 添加新的文字颜色 + ssb.setSpan(new ForegroundColorSpan(color), start, end, Spannable.SPAN_EXCLUSIVE_INCLUSIVE); + + setText(ssb); + setSelection(start, end); + } + } + + /** + * 应用字体样式到选中文本 + */ + private void applyStyle(int style) { + SpannableStringBuilder ssb = new SpannableStringBuilder(getText()); + int start = getSelectionStart(); + int end = getSelectionEnd(); + + if (start != end) { + // 获取当前选中文本的所有样式 + StyleSpan[] existingSpans = ssb.getSpans(start, end, StyleSpan.class); + + // 计算当前选中文本的总样式 + int currentStyle = Typeface.NORMAL; + if (existingSpans.length > 0) { + // 如果有样式,取第一个样式(假设所有样式一致) + currentStyle = existingSpans[0].getStyle(); + } + + // 计算新的字体样式 + int newStyle; + if ((currentStyle & style) != 0) { + // 移除该样式 + newStyle = currentStyle & ~style; + } else { + // 添加该样式 + newStyle = currentStyle | style; + } + + // 移除旧的样式 + for (StyleSpan span : existingSpans) { + ssb.removeSpan(span); + } + + // 添加新的样式 + ssb.setSpan(new StyleSpan(newStyle), start, end, Spannable.SPAN_EXCLUSIVE_INCLUSIVE); + setText(ssb); + setSelection(start, end); + } + } } diff --git a/src/main/java/net/micode/notes/ui/NoteItemData.java b/src/main/java/net/micode/notes/ui/NoteItemData.java index 44825ee..e2dc7c6 100644 --- a/src/main/java/net/micode/notes/ui/NoteItemData.java +++ b/src/main/java/net/micode/notes/ui/NoteItemData.java @@ -54,6 +54,9 @@ public class NoteItemData { NoteColumns.WIDGET_ID, NoteColumns.WIDGET_TYPE, NoteColumns.PINNED, + NoteColumns.IS_LOCKED, + NoteColumns.LOCK_PASSWORD, + NoteColumns.LOCK_TYPE, }; /** @@ -120,6 +123,21 @@ public class NoteItemData { * 查询结果中置顶状态列的索引 */ private static final int PINNED_COLUMN = 12; + + /** + * 查询结果中锁定状态列的索引 + */ + private static final int IS_LOCKED_COLUMN = 13; + + /** + * 查询结果中锁定密码列的索引 + */ + private static final int LOCK_PASSWORD_COLUMN = 14; + + /** + * 查询结果中锁定类型列的索引 + */ + private static final int LOCK_TYPE_COLUMN = 15; /** * 笔记ID @@ -131,6 +149,21 @@ public class NoteItemData { */ private boolean mPinned; + /** + * 锁定状态 + */ + private boolean mIsLocked; + + /** + * 锁定密码 + */ + private String mLockPassword; + + /** + * 锁定类型 + */ + private int mLockType; + /** * 提醒日期 */ @@ -236,12 +269,16 @@ public class NoteItemData { mNotesCount = cursor.getInt(NOTES_COUNT_COLUMN); mParentId = cursor.getLong(PARENT_ID_COLUMN); mSnippet = cursor.getString(SNIPPET_COLUMN); + // 先替换标签,保留HTML格式以便在列表中显示富文本 mSnippet = mSnippet.replace(NoteEditActivity.TAG_CHECKED, "").replace( NoteEditActivity.TAG_UNCHECKED, ""); mType = cursor.getInt(TYPE_COLUMN); mWidgetId = cursor.getInt(WIDGET_ID_COLUMN); mWidgetType = cursor.getInt(WIDGET_TYPE_COLUMN); mPinned = (cursor.getInt(PINNED_COLUMN) > 0) ? true : false; + mIsLocked = (cursor.getInt(IS_LOCKED_COLUMN) > 0) ? true : false; + mLockPassword = cursor.getString(LOCK_PASSWORD_COLUMN); + mLockType = cursor.getInt(LOCK_TYPE_COLUMN); mPhoneNumber = ""; if (mParentId == Notes.ID_CALL_RECORD_FOLDER) { @@ -475,6 +512,54 @@ public class NoteItemData { public void setPinned(boolean pinned) { mPinned = pinned; } + + /** + * 获取锁定状态 + * @return 锁定状态,true表示已锁定,false表示未锁定 + */ + public boolean isLocked() { + return mIsLocked; + } + + /** + * 设置锁定状态 + * @param locked 锁定状态,true表示锁定,false表示解锁 + */ + public void setLocked(boolean locked) { + mIsLocked = locked; + } + + /** + * 获取锁定密码 + * @return 锁定密码 + */ + public String getLockPassword() { + return mLockPassword; + } + + /** + * 设置锁定密码 + * @param password 锁定密码 + */ + public void setLockPassword(String password) { + mLockPassword = password; + } + + /** + * 获取锁定类型 + * @return 锁定类型,0表示笔记,1表示文件夹 + */ + public int getLockType() { + return mLockType; + } + + /** + * 设置锁定类型 + * @param type 锁定类型,0表示笔记,1表示文件夹 + */ + public void setLockType(int type) { + mLockType = type; + } /** * 获取笔记类型(静态方法) diff --git a/src/main/java/net/micode/notes/ui/NotesListActivity.java b/src/main/java/net/micode/notes/ui/NotesListActivity.java index 78aa3f9..a32e94c 100644 --- a/src/main/java/net/micode/notes/ui/NotesListActivity.java +++ b/src/main/java/net/micode/notes/ui/NotesListActivity.java @@ -115,6 +115,127 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt */ private enum ListEditState { NOTE_LIST, SUB_FOLDER, CALL_RECORD_FOLDER + } + + /** + * 显示密码输入对话框 + *

+ * 该方法用于显示一个密码输入对话框,让用户输入密码来锁定选中的笔记或文件夹 + *

+ */ + private void showPasswordDialog() { + 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); + + new AlertDialog.Builder(this) + .setTitle(R.string.dialog_enter_password) + .setView(passwordEditText) + .setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + String password = passwordEditText.getText().toString(); + if (!TextUtils.isEmpty(password)) { + toggleLockedStatus(password); + } + } + }) + .setNegativeButton(android.R.string.cancel, null) + .show(); + } + + /** + * 切换锁定状态 + *

+ * 该方法用于切换选中笔记或文件夹的锁定状态,使用异步任务处理数据库操作 + *

+ * @param password 用于锁定的密码 + */ + private void toggleLockedStatus(final String password) { + final HashSet selectedIds = mNotesListAdapter.getSelectedItemIds(); + final int selectedCount = mNotesListAdapter.getSelectedCount(); + + new AsyncTask() { + protected Integer doInBackground(Void... unused) { + int finalLockedStatus = -1; // 默认为-1,表示未处理 + for (Long noteId : selectedIds) { + // 查询当前便签的锁定状态 + Cursor cursor = mContentResolver.query(Notes.CONTENT_NOTE_URI, + new String[]{NoteColumns.IS_LOCKED}, + NoteColumns.ID + "=?", + new String[]{String.valueOf(noteId)}, + null); + + if (cursor != null && cursor.moveToFirst()) { + int currentLocked = cursor.getInt(0); + // 切换锁定状态 + int newLocked = currentLocked == 1 ? 0 : 1; + finalLockedStatus = newLocked; // 保存最终锁定状态 + + ContentValues values = new ContentValues(); + values.put(NoteColumns.IS_LOCKED, newLocked); + + if (newLocked == 1) { + // 如果是锁定操作,设置密码和锁定类型 + values.put(NoteColumns.LOCK_PASSWORD, encryptPassword(password)); + // 判断是笔记还是文件夹 + Cursor typeCursor = mContentResolver.query(Notes.CONTENT_NOTE_URI, + new String[]{NoteColumns.TYPE}, + NoteColumns.ID + "=?", + new String[]{String.valueOf(noteId)}, + null); + if (typeCursor != null && typeCursor.moveToFirst()) { + int type = typeCursor.getInt(0); + values.put(NoteColumns.LOCK_TYPE, type == Notes.TYPE_FOLDER ? Notes.LOCK_TYPE_FOLDER : Notes.LOCK_TYPE_NOTE); + typeCursor.close(); + } + } else { + // 如果是解锁操作,清空密码 + values.put(NoteColumns.LOCK_PASSWORD, ""); + } + + mContentResolver.update(Notes.CONTENT_NOTE_URI, + values, + NoteColumns.ID + "=?", + new String[]{String.valueOf(noteId)}); + + cursor.close(); + } + } + return finalLockedStatus; + } + + @Override + protected void onPostExecute(Integer finalLockedStatus) { + String message = finalLockedStatus == 1 ? getString(R.string.message_note_locked) : getString(R.string.message_note_unlocked); + Toast.makeText(NotesListActivity.this, message, Toast.LENGTH_SHORT).show(); + // 重新查询数据,更新列表 + startAsyncNotesListQuery(); + mModeCallBack.finishActionMode(); + } + }.execute(); + } + + /** + * 加密密码 + *

+ * 该方法使用Base64和ANDROID_ID对密码进行加密 + *

+ * @param password 原始密码 + * @return 加密后的密码 + */ + private String encryptPassword(String password) { + try { + // 获取设备的ANDROID_ID作为加密密钥 + String androidId = android.provider.Settings.Secure.getString(getContentResolver(), + android.provider.Settings.Secure.ANDROID_ID); + // 使用简单的加密算法:将密码与ANDROID_ID拼接后进行Base64编码 + String combined = password + androidId; + return android.util.Base64.encodeToString(combined.getBytes(), android.util.Base64.DEFAULT); + } catch (Exception e) { + Log.e(TAG, "Password encryption failed", e); + return password; // 加密失败时返回原始密码 + } }; /** 当前列表编辑状态 */ @@ -195,6 +316,52 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.note_list); + + // 上移底部系统栏,解决写便签按钮与底部系统栏之间的多余间距 + View decorView = getWindow().getDecorView(); + if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.R) { + // Android 11+ 使用新的WindowInsetsController API + android.view.WindowInsetsController insetsController = decorView.getWindowInsetsController(); + if (insetsController != null) { + // 隐藏底部导航栏 + insetsController.hide(android.view.WindowInsets.Type.navigationBars()); + // 设置系统栏行为,允许滑动显示 + insetsController.setSystemBarsBehavior( + android.view.WindowInsetsController.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE); + } + } else if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.JELLY_BEAN) { + // Android 4.1-10 使用旧的系统UI可见性API + int uiOptions = View.SYSTEM_UI_FLAG_HIDE_NAVIGATION + | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION + | View.SYSTEM_UI_FLAG_LAYOUT_STABLE; + decorView.setSystemUiVisibility(uiOptions); + } + + // 添加系统UI可见性变化监听器,确保系统栏保持上移状态 + decorView.setOnSystemUiVisibilityChangeListener( + new View.OnSystemUiVisibilityChangeListener() { + @Override + public void onSystemUiVisibilityChange(int visibility) { + // 当系统UI可见性变化时,重新设置系统UI可见性 + if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.R) { + // Android 11+ 使用新API + android.view.WindowInsetsController insetsController = decorView.getWindowInsetsController(); + if (insetsController != null) { + insetsController.hide(android.view.WindowInsets.Type.navigationBars()); + } + } else if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.JELLY_BEAN) { + // Android 4.1-10 使用旧API + if ((visibility & View.SYSTEM_UI_FLAG_HIDE_NAVIGATION) == 0) { + int uiOptions = View.SYSTEM_UI_FLAG_HIDE_NAVIGATION + | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION + | View.SYSTEM_UI_FLAG_LAYOUT_STABLE; + decorView.setSystemUiVisibility(uiOptions); + } + } + } + } + ); + initResources(); /** @@ -379,6 +546,11 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt if (pinMenu != null) { pinMenu.setOnMenuItemClickListener(this); } + // 添加锁定菜单初始化 + MenuItem lockMenu = menu.findItem(R.id.lock); + if (lockMenu != null) { + lockMenu.setOnMenuItemClickListener(this); + } mActionMode = mode; mNotesListAdapter.setChoiceMode(true); mNotesListView.setLongClickable(false); @@ -541,6 +713,9 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt case R.id.pin: togglePinnedStatus(); break; + case R.id.lock: + showPasswordDialog(); + break; default: return false; } @@ -875,11 +1050,72 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt *

* @param data 要打开的笔记数据项 */ - 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); + private void openNode(final NoteItemData data) { + // 检查笔记是否被锁定 + if (data.isLocked()) { + 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); + + new AlertDialog.Builder(this) + .setTitle(R.string.dialog_enter_password) + .setView(passwordEditText) + .setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + String password = passwordEditText.getText().toString(); + if (!TextUtils.isEmpty(password)) { + verifyPasswordAndOpenNote(data.getId(), password); + } + } + }) + .setNegativeButton(android.R.string.cancel, null) + .show(); + } else { + // 未锁定,直接打开笔记 + Intent intent = new Intent(this, NoteEditActivity.class); + intent.setAction(Intent.ACTION_VIEW); + intent.putExtra(Intent.EXTRA_UID, data.getId()); + startActivityForResult(intent, REQUEST_CODE_OPEN_NODE); + } + } + + /** + * 验证密码并打开笔记 + *

+ * 该方法完成以下工作: + * 1. 查询笔记的加密密码 + * 2. 验证密码是否正确 + * 3. 如果正确,打开笔记;否则显示错误信息 + *

+ * @param noteId 笔记ID + * @param password 用户输入的密码 + */ + private void verifyPasswordAndOpenNote(final long noteId, String password) { + // 查询笔记的加密密码 + String[] projection = {NoteColumns.LOCK_PASSWORD}; + String selection = NoteColumns.ID + "=?"; + String[] selectionArgs = {String.valueOf(noteId)}; + + 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(password))) { + // 密码正确,打开笔记 + Intent intent = new Intent(this, NoteEditActivity.class); + intent.setAction(Intent.ACTION_VIEW); + intent.putExtra(Intent.EXTRA_UID, noteId); + startActivityForResult(intent, REQUEST_CODE_OPEN_NODE); + } else { + // 密码错误,显示错误信息 + Toast.makeText(this, getString(R.string.error_wrong_password), Toast.LENGTH_SHORT).show(); + } } /** diff --git a/src/main/java/net/micode/notes/ui/NotesListAdapter.java b/src/main/java/net/micode/notes/ui/NotesListAdapter.java index e9f5a8a..138e3e7 100644 --- a/src/main/java/net/micode/notes/ui/NotesListAdapter.java +++ b/src/main/java/net/micode/notes/ui/NotesListAdapter.java @@ -289,15 +289,29 @@ public class NotesListAdapter extends CursorAdapter { */ private void calcNotesCount() { mNotesCount = 0; - for (int i = 0; i < getCount(); i++) { - Cursor c = (Cursor) getItem(i); - if (c != null) { - if (NoteItemData.getNoteType(c) == Notes.TYPE_NOTE) { - mNotesCount++; + Cursor cursor = getCursor(); + if (cursor != null && !cursor.isClosed()) { + // 使用 try-finally 块确保 cursor 位置正确恢复 + int originalPosition = cursor.getPosition(); + try { + if (cursor.moveToFirst()) { + do { + try { + if (NoteItemData.getNoteType(cursor) == Notes.TYPE_NOTE) { + mNotesCount++; + } + } catch (Exception e) { + // 捕获可能的异常,避免崩溃 + Log.e(TAG, "Error calculating note count: " + e.getMessage()); + break; + } + } while (cursor.moveToNext()); + } + } finally { + // 恢复 cursor 到原始位置 + if (!cursor.isClosed()) { + cursor.moveToPosition(originalPosition); } - } else { - Log.e(TAG, "Invalid cursor"); - return; } } } diff --git a/src/main/java/net/micode/notes/ui/NotesListItem.java b/src/main/java/net/micode/notes/ui/NotesListItem.java index b6222fd..c08425c 100644 --- a/src/main/java/net/micode/notes/ui/NotesListItem.java +++ b/src/main/java/net/micode/notes/ui/NotesListItem.java @@ -17,6 +17,7 @@ package net.micode.notes.ui; import android.content.Context; +import android.text.Html; import android.text.format.DateUtils; import android.view.View; import android.widget.CheckBox; @@ -103,7 +104,7 @@ public class NotesListItem extends LinearLayout { mCallName.setVisibility(View.VISIBLE); mCallName.setText(data.getCallName()); mTitle.setTextAppearance(context,R.style.TextAppearanceSecondaryItem); - mTitle.setText(DataUtils.getFormattedSnippet(data.getSnippet())); + mTitle.setText(Html.fromHtml(DataUtils.getFormattedSnippet(data.getSnippet()))); if (data.hasAlert()) { mAlert.setImageResource(R.drawable.clock); mAlert.setVisibility(View.VISIBLE); @@ -120,7 +121,7 @@ public class NotesListItem extends LinearLayout { data.getNotesCount())); mAlert.setVisibility(View.GONE); } else { - mTitle.setText(DataUtils.getFormattedSnippet(data.getSnippet())); + mTitle.setText(Html.fromHtml(DataUtils.getFormattedSnippet(data.getSnippet()))); if (data.hasAlert()) { mAlert.setImageResource(R.drawable.clock); mAlert.setVisibility(View.VISIBLE); diff --git a/src/main/res/layout/note_edit.xml b/src/main/res/layout/note_edit.xml index e0cf16b..3344dcc 100644 --- a/src/main/res/layout/note_edit.xml +++ b/src/main/res/layout/note_edit.xml @@ -57,11 +57,81 @@ android:layout_marginRight="8dip" android:textAppearance="@style/TextAppearanceSecondaryItem" /> + +