From 9c06b0dac5198881518bf0f7e978ea3782207f19 Mon Sep 17 00:00:00 2001 From: ranhao <2352406715@qq.com> Date: Tue, 27 Jan 2026 16:00:11 +0800 Subject: [PATCH] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E5=AF=8C=E6=96=87=E6=9C=AC?= =?UTF-8?q?=E7=BC=96=E8=BE=91=E5=8A=9F=E8=83=BD=EF=BC=8C=E4=BC=98=E5=8C=96?= =?UTF-8?q?=E6=90=9C=E7=B4=A2=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/notes/data/NotesProvider.java | 242 +++++++++++++++++++++- src/notes/tool/ElderModeUtils.java | 109 ++++++++++ src/notes/ui/NoteEditActivity.java | 188 ++++++++++++++++- src/notes/ui/NoteItemData.java | 5 + src/notes/ui/NotesPreferenceActivity.java | 61 +++++- 5 files changed, 590 insertions(+), 15 deletions(-) create mode 100644 src/notes/tool/ElderModeUtils.java diff --git a/src/notes/data/NotesProvider.java b/src/notes/data/NotesProvider.java index 8810f7a..77446df 100644 --- a/src/notes/data/NotesProvider.java +++ b/src/notes/data/NotesProvider.java @@ -26,6 +26,8 @@ import android.content.UriMatcher; import android.database.Cursor; import android.database.sqlite.SQLiteDatabase; import android.net.Uri; +import android.os.Bundle; +import android.text.Html; import android.text.TextUtils; import android.util.Log; @@ -188,8 +190,12 @@ public class NotesProvider extends ContentProvider { try { searchString = String.format("%%%s%%", searchString); - c = db.rawQuery(NOTES_SNIPPET_SEARCH_QUERY, + Cursor rawCursor = db.rawQuery(NOTES_SNIPPET_SEARCH_QUERY, new String[] { searchString }); + // 包装Cursor以处理HTML内容 + if (rawCursor != null) { + c = new HtmlCursorWrapper(rawCursor); + } } catch (IllegalStateException ex) { Log.e(TAG, "got exception: " + ex.toString()); } @@ -203,6 +209,240 @@ public class NotesProvider extends ContentProvider { return c; } + /** + * Cursor包装类,用于处理搜索结果中的HTML内容 + * 将HTML格式转换为普通文本,解决富文本编辑后搜索结果显示乱码的问题 + */ + private class HtmlCursorWrapper implements Cursor { + private Cursor mCursor; + + public HtmlCursorWrapper(Cursor cursor) { + mCursor = cursor; + } + + @Override + public int getCount() { + return mCursor.getCount(); + } + + @Override + public int getPosition() { + return mCursor.getPosition(); + } + + @Override + public boolean move(int offset) { + return mCursor.move(offset); + } + + @Override + public boolean moveToPosition(int position) { + return mCursor.moveToPosition(position); + } + + @Override + public boolean moveToFirst() { + return mCursor.moveToFirst(); + } + + @Override + public boolean moveToLast() { + return mCursor.moveToLast(); + } + + @Override + public boolean moveToNext() { + return mCursor.moveToNext(); + } + + @Override + public boolean moveToPrevious() { + return mCursor.moveToPrevious(); + } + + @Override + public boolean isFirst() { + return mCursor.isFirst(); + } + + @Override + public boolean isLast() { + return mCursor.isLast(); + } + + @Override + public boolean isBeforeFirst() { + return mCursor.isBeforeFirst(); + } + + @Override + public boolean isAfterLast() { + return mCursor.isAfterLast(); + } + + @Override + public int getColumnIndex(String columnName) { + return mCursor.getColumnIndex(columnName); + } + + @Override + public int getColumnIndexOrThrow(String columnName) throws IllegalArgumentException { + return mCursor.getColumnIndexOrThrow(columnName); + } + + @Override + public String getColumnName(int columnIndex) { + return mCursor.getColumnName(columnIndex); + } + + @Override + public String[] getColumnNames() { + return mCursor.getColumnNames(); + } + + @Override + public int getColumnCount() { + return mCursor.getColumnCount(); + } + + @Override + public byte[] getBlob(int columnIndex) { + return mCursor.getBlob(columnIndex); + } + + @Override + public String getString(int columnIndex) { + String value = mCursor.getString(columnIndex); + // 处理搜索结果中的HTML内容 + if (value != null && ( + columnIndex == mCursor.getColumnIndex(SearchManager.SUGGEST_COLUMN_TEXT_1) || + columnIndex == mCursor.getColumnIndex(SearchManager.SUGGEST_COLUMN_TEXT_2)) + ) { + // 将HTML转换为普通文本 + return Html.fromHtml(value).toString(); + } + return value; + } + + @Override + public short getShort(int columnIndex) { + return mCursor.getShort(columnIndex); + } + + @Override + public int getInt(int columnIndex) { + return mCursor.getInt(columnIndex); + } + + @Override + public long getLong(int columnIndex) { + return mCursor.getLong(columnIndex); + } + + @Override + public float getFloat(int columnIndex) { + return mCursor.getFloat(columnIndex); + } + + @Override + public double getDouble(int columnIndex) { + return mCursor.getDouble(columnIndex); + } + + @Override + public int getType(int columnIndex) { + return mCursor.getType(columnIndex); + } + + @Override + public boolean isNull(int columnIndex) { + return mCursor.isNull(columnIndex); + } + + @Override + public void deactivate() { + mCursor.deactivate(); + } + + @Override + public boolean requery() { + return mCursor.requery(); + } + + @Override + public void close() { + mCursor.close(); + } + + @Override + public boolean isClosed() { + return mCursor.isClosed(); + } + + @Override + public void registerContentObserver(android.database.ContentObserver observer) { + mCursor.registerContentObserver(observer); + } + + @Override + public void unregisterContentObserver(android.database.ContentObserver observer) { + mCursor.unregisterContentObserver(observer); + } + + @Override + public void registerDataSetObserver(android.database.DataSetObserver observer) { + mCursor.registerDataSetObserver(observer); + } + + @Override + public void unregisterDataSetObserver(android.database.DataSetObserver observer) { + mCursor.unregisterDataSetObserver(observer); + } + + @Override + public void setNotificationUri(android.content.ContentResolver cr, Uri uri) { + mCursor.setNotificationUri(cr, uri); + } + + @Override + public Uri getNotificationUri() { + return mCursor.getNotificationUri(); + } + + @Override + public boolean getWantsAllOnMoveCalls() { + return mCursor.getWantsAllOnMoveCalls(); + } + + @Override + public void setExtras(Bundle extras) { + mCursor.setExtras(extras); + } + + @Override + public Bundle getExtras() { + return mCursor.getExtras(); + } + + @Override + public Bundle respond(Bundle extras) { + return mCursor.respond(extras); + } + + @Override + public void copyStringToBuffer(int columnIndex, android.database.CharArrayBuffer buffer) { + String value = getString(columnIndex); + if (value != null) { + char[] data = value.toCharArray(); + if (buffer.data == null || buffer.data.length < data.length) { + buffer.data = new char[data.length]; + } + System.arraycopy(data, 0, buffer.data, 0, data.length); + buffer.sizeCopied = data.length; + } + } + } + /** * 插入数据 * @param uri 插入的URI diff --git a/src/notes/tool/ElderModeUtils.java b/src/notes/tool/ElderModeUtils.java new file mode 100644 index 0000000..11f4132 --- /dev/null +++ b/src/notes/tool/ElderModeUtils.java @@ -0,0 +1,109 @@ +/* + * Copyright (c) 2026, 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.content.Context; +import android.content.SharedPreferences; +import android.preference.PreferenceManager; +import android.view.View; +import android.view.ViewGroup; +import android.widget.TextView; + +import net.micode.notes.R; +import net.micode.notes.ui.NotesPreferenceActivity; + +/** + * 老年人模式工具类,用于管理老年人模式的设置和应用 + */ +public class ElderModeUtils { + /** + * 老年人模式的字体缩放比例 + */ + private static final float ELDER_MODE_FONT_SCALE = 1.1f; + + /** + * 检查是否启用了老年人模式 + * + * @param context 上下文 + * @return 是否启用了老年人模式 + */ + public static boolean isElderModeEnabled(Context context) { + // 使用默认的SharedPreferences,因为CheckBoxPreference会自动保存到这里 + SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(context); + return preferences.getBoolean(NotesPreferenceActivity.PREFERENCE_ELDER_MODE_KEY, false); + } + + /** + * 应用老年人模式到指定的View + * + * @param context 上下文 + * @param view 要应用老年人模式的View + */ + public static void applyElderMode(Context context, View view) { + if (!isElderModeEnabled(context)) { + return; + } + + // 只对用户输入的内容应用字体增大,系统信息保持不变 + if (view instanceof TextView) { + TextView textView = (TextView) view; + int id = textView.getId(); + + // 只增大用户输入内容的字体 + if (id == R.id.tv_title || // 笔记列表中的标题(用户输入内容) + id == R.id.note_edit_view) { // 编辑界面中的内容(用户输入) + textView.setTextSize(textView.getTextSize() * ELDER_MODE_FONT_SCALE); + } + } else if (view instanceof ViewGroup) { + // 递归处理ViewGroup中的所有子View + ViewGroup viewGroup = (ViewGroup) view; + for (int i = 0; i < viewGroup.getChildCount(); i++) { + applyElderMode(context, viewGroup.getChildAt(i)); + } + } + } + + /** + * 清除老年人模式的应用 + * + * @param context 上下文 + * @param view 要清除老年人模式的View + */ + public static void clearElderMode(Context context, View view) { + if (!isElderModeEnabled(context)) { + return; + } + + // 只对用户输入的内容应用字体恢复,系统信息保持不变 + if (view instanceof TextView) { + TextView textView = (TextView) view; + int id = textView.getId(); + + // 只恢复用户输入内容的字体 + if (id == R.id.tv_title || // 笔记列表中的标题(用户输入内容) + id == R.id.note_edit_view) { // 编辑界面中的内容(用户输入) + textView.setTextSize(textView.getTextSize() / ELDER_MODE_FONT_SCALE); + } + } else if (view instanceof ViewGroup) { + // 递归处理ViewGroup中的所有子View + ViewGroup viewGroup = (ViewGroup) view; + for (int i = 0; i < viewGroup.getChildCount(); i++) { + clearElderMode(context, viewGroup.getChildAt(i)); + } + } + } +} \ No newline at end of file diff --git a/src/notes/ui/NoteEditActivity.java b/src/notes/ui/NoteEditActivity.java index afd6110..7507430 100644 --- a/src/notes/ui/NoteEditActivity.java +++ b/src/notes/ui/NoteEditActivity.java @@ -30,11 +30,16 @@ 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.Spanned; import android.text.TextUtils; import android.text.format.DateUtils; import android.text.style.BackgroundColorSpan; +import android.text.style.StrikethroughSpan; +import android.text.style.StyleSpan; +import android.text.style.UnderlineSpan; import android.util.Log; import android.view.LayoutInflater; import android.view.Menu; @@ -298,7 +303,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)); + // 加载富文本内容 + String content = mWorkingNote.getContent(); + CharSequence htmlContent = Html.fromHtml(content != null ? content : ""); + mNoteEditor.setText(getHighlightQueryResult(htmlContent, mUserQuery)); mNoteEditor.setSelection(mNoteEditor.getText().length()); // 光标移到最后 } @@ -643,6 +651,18 @@ public class NoteEditActivity extends Activity implements OnClickListener, case R.id.menu_delete_remind: mWorkingNote.setAlertDate(0, false); // 删除提醒 break; + case R.id.menu_bold: + applyRichTextStyle(android.graphics.Typeface.BOLD); + break; + case R.id.menu_italic: + applyRichTextStyle(android.graphics.Typeface.ITALIC); + break; + case R.id.menu_underline: + applyUnderlineStyle(); + break; + case R.id.menu_strikethrough: + applyStrikethroughStyle(); + break; default: break; } @@ -846,9 +866,9 @@ public class NoteEditActivity extends Activity implements OnClickListener, /** * 获取高亮搜索结果的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)) { + if (!TextUtils.isEmpty(userQuery) && !TextUtils.isEmpty(fullText)) { mPattern = Pattern.compile(userQuery); Matcher m = mPattern.matcher(fullText); int start = 0; @@ -900,7 +920,8 @@ public class NoteEditActivity extends Activity implements OnClickListener, // 设置编辑文本变化监听器 edit.setOnTextViewChangeListener(this); edit.setIndex(index); // 设置索引 - edit.setText(getHighlightQueryResult(item, mUserQuery)); // 设置文本(支持高亮) + // 列表模式下不使用富文本,直接设置文本 + edit.setText(item); // 设置文本 return view; } @@ -954,17 +975,22 @@ public class NoteEditActivity extends Activity implements OnClickListener, if (!TextUtils.isEmpty(edit.getText())) { // 根据复选框状态添加不同的标记 if (((CheckBox) view.findViewById(R.id.cb_edit_item)).isChecked()) { - sb.append(TAG_CHECKED).append(" ").append(edit.getText()).append("\n"); + sb.append(TAG_CHECKED).append(" " ).append(edit.getText()).append("\n" ); hasChecked = true; } else { - sb.append(TAG_UNCHECKED).append(" ").append(edit.getText()).append("\n"); + sb.append(TAG_UNCHECKED).append(" " ).append(edit.getText()).append("\n" ); } } } mWorkingNote.setWorkingText(sb.toString()); } else { - // 普通模式:直接获取编辑框文本 - mWorkingNote.setWorkingText(mNoteEditor.getText().toString()); + // 普通模式:将富文本转换为HTML格式保存 + if (mNoteEditor.getText() instanceof Spanned) { + String htmlContent = Html.toHtml((Spanned) mNoteEditor.getText()); + mWorkingNote.setWorkingText(htmlContent); + } else { + mWorkingNote.setWorkingText(mNoteEditor.getText().toString()); + } } return hasChecked; } @@ -986,6 +1012,152 @@ public class NoteEditActivity extends Activity implements OnClickListener, return saved; } + /** + * 应用富文本样式(粗体或斜体) + */ + private void applyRichTextStyle(int style) { + if (mWorkingNote.getCheckListMode() == TextNote.MODE_CHECK_LIST) { + // 列表模式下,获取当前焦点的编辑框 + for (int i = 0; i < mEditTextList.getChildCount(); i++) { + NoteEditText editText = (NoteEditText) mEditTextList.getChildAt(i).findViewById(R.id.et_edit_text); + if (editText.hasFocus()) { + applyStyleToEditText(editText, style); + break; + } + } + } else { + // 普通模式下,直接应用到主编辑框 + applyStyleToEditText(mNoteEditor, style); + } + } + + /** + * 应用下划线样式 + */ + private void applyUnderlineStyle() { + if (mWorkingNote.getCheckListMode() == TextNote.MODE_CHECK_LIST) { + // 列表模式下,获取当前焦点的编辑框 + for (int i = 0; i < mEditTextList.getChildCount(); i++) { + NoteEditText editText = (NoteEditText) mEditTextList.getChildAt(i).findViewById(R.id.et_edit_text); + if (editText.hasFocus()) { + applyUnderlineToEditText(editText); + break; + } + } + } else { + // 普通模式下,直接应用到主编辑框 + applyUnderlineToEditText(mNoteEditor); + } + } + + /** + * 应用删除线样式 + */ + private void applyStrikethroughStyle() { + if (mWorkingNote.getCheckListMode() == TextNote.MODE_CHECK_LIST) { + // 列表模式下,获取当前焦点的编辑框 + for (int i = 0; i < mEditTextList.getChildCount(); i++) { + NoteEditText editText = (NoteEditText) mEditTextList.getChildAt(i).findViewById(R.id.et_edit_text); + if (editText.hasFocus()) { + applyStrikethroughToEditText(editText); + break; + } + } + } else { + // 普通模式下,直接应用到主编辑框 + applyStrikethroughToEditText(mNoteEditor); + } + } + + /** + * 向EditText应用样式(粗体或斜体) + */ + private void applyStyleToEditText(EditText editText, int style) { + int start = editText.getSelectionStart(); + int end = editText.getSelectionEnd(); + + if (start != end) { + SpannableString spannable = new SpannableString(editText.getText()); + StyleSpan[] existingSpans = spannable.getSpans(start, end, StyleSpan.class); + + // 检查是否已经应用了相同的样式 + boolean hasStyle = false; + for (StyleSpan span : existingSpans) { + if (span.getStyle() == style) { + hasStyle = true; + break; + } + } + + if (hasStyle) { + // 如果已经应用了相同的样式,则移除 + for (StyleSpan span : existingSpans) { + if (span.getStyle() == style) { + spannable.removeSpan(span); + } + } + } else { + // 如果没有应用相同的样式,则添加 + spannable.setSpan(new StyleSpan(style), start, end, Spannable.SPAN_EXCLUSIVE_INCLUSIVE); + } + + editText.setText(spannable); + editText.setSelection(end); + } + } + + /** + * 向EditText应用下划线样式 + */ + private void applyUnderlineToEditText(EditText editText) { + int start = editText.getSelectionStart(); + int end = editText.getSelectionEnd(); + + if (start != end) { + SpannableString spannable = new SpannableString(editText.getText()); + UnderlineSpan[] existingSpans = spannable.getSpans(start, end, UnderlineSpan.class); + + if (existingSpans.length > 0) { + // 如果已经应用了下划线,则移除 + for (UnderlineSpan span : existingSpans) { + spannable.removeSpan(span); + } + } else { + // 如果没有应用下划线,则添加 + spannable.setSpan(new UnderlineSpan(), start, end, Spannable.SPAN_EXCLUSIVE_INCLUSIVE); + } + + editText.setText(spannable); + editText.setSelection(end); + } + } + + /** + * 向EditText应用删除线样式 + */ + private void applyStrikethroughToEditText(EditText editText) { + int start = editText.getSelectionStart(); + int end = editText.getSelectionEnd(); + + if (start != end) { + SpannableString spannable = new SpannableString(editText.getText()); + StrikethroughSpan[] existingSpans = spannable.getSpans(start, end, StrikethroughSpan.class); + + if (existingSpans.length > 0) { + // 如果已经应用了删除线,则移除 + for (StrikethroughSpan span : existingSpans) { + spannable.removeSpan(span); + } + } else { + // 如果没有应用删除线,则添加 + spannable.setSpan(new StrikethroughSpan(), start, end, Spannable.SPAN_EXCLUSIVE_INCLUSIVE); + } + + editText.setText(spannable); + editText.setSelection(end); + } + } + /** * 发送到桌面(创建快捷方式) */ diff --git a/src/notes/ui/NoteItemData.java b/src/notes/ui/NoteItemData.java index 41a2b4b..25173b8 100644 --- a/src/notes/ui/NoteItemData.java +++ b/src/notes/ui/NoteItemData.java @@ -18,6 +18,7 @@ package net.micode.notes.ui; import android.content.Context; import android.database.Cursor; +import android.text.Html; import android.text.TextUtils; import net.micode.notes.data.Contact; @@ -102,6 +103,10 @@ public class NoteItemData { mNotesCount = cursor.getInt(NOTES_COUNT_COLUMN); mParentId = cursor.getLong(PARENT_ID_COLUMN); mSnippet = cursor.getString(SNIPPET_COLUMN); + // 将HTML格式转换为普通文本,解决富文本编辑后显示乱码的问题 + if (!TextUtils.isEmpty(mSnippet)) { + mSnippet = Html.fromHtml(mSnippet).toString(); + } // 移除摘要中的复选框标记(已勾选和未勾选) mSnippet = mSnippet.replace(NoteEditActivity.TAG_CHECKED, "").replace( NoteEditActivity.TAG_UNCHECKED, ""); diff --git a/src/notes/ui/NotesPreferenceActivity.java b/src/notes/ui/NotesPreferenceActivity.java index 82a0547..c1d0294 100644 --- a/src/notes/ui/NotesPreferenceActivity.java +++ b/src/notes/ui/NotesPreferenceActivity.java @@ -81,12 +81,9 @@ public class NotesPreferenceActivity extends PreferenceActivity { /* 使用应用图标作为导航按钮 */ getActionBar().setDisplayHomeAsUpEnabled(true); - // 检查并设置密保问题 + // 检查并设置密保问题(只在首次设置时显示) if (!hasSecurityQuestionsSet()) { showSetSecurityQuestionsDialog(); - } else { - // 验证密保问题才能进入设置 - showVerifySecurityQuestionsDialog(); } // 从XML文件加载偏好设置 @@ -355,8 +352,8 @@ public class NotesPreferenceActivity extends PreferenceActivity { securityPref.setOnPreferenceClickListener(new OnPreferenceClickListener() { @Override public boolean onPreferenceClick(Preference preference) { - // 显示修改密保问题的对话框 - showChangeSecurityQuestionsDialog(); + // 验证当前密保才能修改 + showVerifySecurityQuestionsForModificationDialog(); return true; } }); @@ -420,6 +417,58 @@ public class NotesPreferenceActivity extends PreferenceActivity { // 显示对话框 builder.show(); } + + /** + * 显示用于修改密保的验证对话框 + */ + private void showVerifySecurityQuestionsForModificationDialog() { + AlertDialog.Builder builder = new AlertDialog.Builder(this); + builder.setTitle("验证身份"); + builder.setIcon(android.R.drawable.ic_lock_idle_lock); + builder.setMessage("请回答您的密保问题以验证身份,验证成功后才能修改密保"); + + // 创建布局 + LinearLayout layout = new LinearLayout(this); + layout.setOrientation(LinearLayout.VERTICAL); + layout.setPadding(40, 20, 40, 20); + + // 创建姓名输入框 + final EditText nameInput = new EditText(this); + nameInput.setHint("请输入姓名"); + layout.addView(nameInput); + + // 创建生日输入框 + final EditText birthdayInput = new EditText(this); + birthdayInput.setHint("请输入生日 (YYYY-MM-DD)"); + layout.addView(birthdayInput); + + builder.setView(layout); + + // 设置确定按钮 + builder.setPositiveButton("确定", new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + String name = nameInput.getText().toString(); + String birthday = birthdayInput.getText().toString(); + if (verifySecurityQuestions(name, birthday)) { + // 验证成功,显示修改密保对话框 + Toast.makeText(NotesPreferenceActivity.this, "验证成功", Toast.LENGTH_SHORT).show(); + showChangeSecurityQuestionsDialog(); + } else { + // 验证失败,显示提示 + Toast.makeText(NotesPreferenceActivity.this, "验证失败,请重新输入", Toast.LENGTH_SHORT).show(); + // 重新显示验证对话框 + showVerifySecurityQuestionsForModificationDialog(); + } + } + }); + + // 设置取消按钮 + builder.setNegativeButton("取消", null); + + // 显示对话框 + builder.show(); + } /** * 加载同步按钮和同步状态显示