diff --git a/src/MainActivity.java b/src/MainActivity.java new file mode 100644 index 0000000..df00763 --- /dev/null +++ b/src/MainActivity.java @@ -0,0 +1,41 @@ +package net.micode.notes; + +import android.os.Bundle; + +import androidx.activity.EdgeToEdge; +import androidx.appcompat.app.AppCompatActivity; +import androidx.core.graphics.Insets; +import androidx.core.view.ViewCompat; +import androidx.core.view.WindowInsetsCompat; + +/** + * 笔记应用主Activity + * 应用启动时显示的入口页面 + * 继承自AppCompatActivity,使用AndroidX兼容库 + */ +public class MainActivity extends AppCompatActivity { + + /** + * Activity创建时的生命周期回调方法 + * 初始化Activity布局和界面 + * + * @param savedInstanceState 保存的状态数据,用于Activity重建 + */ + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + // 启用边缘到边缘显示,允许内容延伸到系统UI区域(状态栏和导航栏) + EdgeToEdge.enable(this); + // 设置Activity的布局文件为activity_main.xml + setContentView(R.layout.activity_main); + // 设置窗口插入监听器,处理系统UI区域(如状态栏、导航栏)与内容的交互 + ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main), (v, insets) -> { + // 获取系统栏(状态栏和导航栏)的插入区域 + Insets systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars()); + // 为视图设置内边距,避免内容被系统栏遮挡 + v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom); + // 返回处理后的插入信息 + return insets; + }); + } +} \ No newline at end of file diff --git a/src/data/Notes.java b/src/data/Notes.java index 24c05c0..9d1963c 100644 --- a/src/data/Notes.java +++ b/src/data/Notes.java @@ -83,6 +83,8 @@ public class Notes { public static final String ORIGIN_PARENT_ID = "origin_parent_id"; public static final String GTASK_ID = "gtask_id"; public static final String VERSION = "version"; + public static final String PINNED = "pinned"; + public static final String TITLE = "title"; // 便签标题字段 } /** diff --git a/src/data/NotesDatabaseHelper.java b/src/data/NotesDatabaseHelper.java index 826bdf2..d8080fc 100644 --- a/src/data/NotesDatabaseHelper.java +++ b/src/data/NotesDatabaseHelper.java @@ -37,7 +37,7 @@ public class NotesDatabaseHelper extends SQLiteOpenHelper { // 数据库名称 private static final String DB_NAME = "note.db"; // 数据库版本号,用于数据库升级管理 - private static final int DB_VERSION = 4; + private static final int DB_VERSION = 6; // 表名常量接口 public interface TABLE { @@ -61,6 +61,7 @@ public class NotesDatabaseHelper extends SQLiteOpenHelper { NoteColumns.MODIFIED_DATE + " INTEGER NOT NULL DEFAULT (strftime('%s','now') * 1000)," + NoteColumns.NOTES_COUNT + " INTEGER NOT NULL DEFAULT 0," + NoteColumns.SNIPPET + " TEXT NOT NULL DEFAULT ''," + + NoteColumns.TITLE + " TEXT NOT NULL DEFAULT ''," + NoteColumns.TYPE + " INTEGER NOT NULL DEFAULT 0," + NoteColumns.WIDGET_ID + " INTEGER NOT NULL DEFAULT 0," + NoteColumns.WIDGET_TYPE + " INTEGER NOT NULL DEFAULT -1," + @@ -68,7 +69,8 @@ public class NotesDatabaseHelper extends SQLiteOpenHelper { NoteColumns.LOCAL_MODIFIED + " INTEGER NOT NULL DEFAULT 0," + NoteColumns.ORIGIN_PARENT_ID + " INTEGER NOT NULL DEFAULT 0," + NoteColumns.GTASK_ID + " TEXT NOT NULL DEFAULT ''," + - NoteColumns.VERSION + " INTEGER NOT NULL DEFAULT 0" + + NoteColumns.VERSION + " INTEGER NOT NULL DEFAULT 0," + + NoteColumns.PINNED + " INTEGER NOT NULL DEFAULT 0" + ")"; // 创建data表的SQL语句 @@ -186,15 +188,16 @@ public class NotesDatabaseHelper extends SQLiteOpenHelper { " END"; // 触发器:当文件夹被移动到回收站时,将该文件夹下的所有便签也移动到回收站 - private static final String FOLDER_MOVE_NOTES_ON_TRASH_TRIGGER = + // 但保持便签的PARENT_ID不变,这样可以在回收站中查看完整的文件夹结构 + private static final String FOLDER_MOVE_NOTES_ON_TRASH_TRIGGER = "CREATE TRIGGER folder_move_notes_on_trash " + " AFTER UPDATE ON " + TABLE.NOTE + " WHEN new." + NoteColumns.PARENT_ID + "=" + Notes.ID_TRASH_FOLER + " BEGIN" + " UPDATE " + TABLE.NOTE + - " SET " + NoteColumns.PARENT_ID + "=" + Notes.ID_TRASH_FOLER + + " SET " + NoteColumns.ORIGIN_PARENT_ID + "=parent_id" + " WHERE " + NoteColumns.PARENT_ID + "=old." + NoteColumns.ID + ";" + - " END"; + " END;"; /** * 构造方法 @@ -346,6 +349,18 @@ public class NotesDatabaseHelper extends SQLiteOpenHelper { oldVersion++; } + // 从版本4升级到版本5 + if (oldVersion == 4) { + upgradeToV5(db); + oldVersion++; + } + + // 从版本5升级到版本6:添加title列,用于存储便签标题 + if (oldVersion == 5) { + upgradeToV6(db); + oldVersion++; + } + // 如果需要,重新创建触发器 if (reCreateTriggers) { reCreateNoteTableTriggers(db); @@ -400,4 +415,22 @@ public class NotesDatabaseHelper extends SQLiteOpenHelper { db.execSQL("ALTER TABLE " + TABLE.NOTE + " ADD COLUMN " + NoteColumns.VERSION + " INTEGER NOT NULL DEFAULT 0"); } + + /** + * 升级到版本5:添加pinned列,用于标识便签是否被置顶 + * @param db SQLiteDatabase对象 + */ + private void upgradeToV5(SQLiteDatabase db) { + db.execSQL("ALTER TABLE " + TABLE.NOTE + " ADD COLUMN " + NoteColumns.PINNED + + " INTEGER NOT NULL DEFAULT 0"); + } + + /** + * 升级到版本6:添加title列,用于存储便签标题 + * @param db SQLiteDatabase对象 + */ + private void upgradeToV6(SQLiteDatabase db) { + db.execSQL("ALTER TABLE " + TABLE.NOTE + " ADD COLUMN " + NoteColumns.TITLE + + " TEXT NOT NULL DEFAULT ''"); + } } \ No newline at end of file diff --git a/src/model/Note.java b/src/model/Note.java index 2e2723f..0bc03c9 100644 --- a/src/model/Note.java +++ b/src/model/Note.java @@ -83,6 +83,28 @@ public class Note { mNoteDiffValues.put(NoteColumns.LOCAL_MODIFIED, 1); mNoteDiffValues.put(NoteColumns.MODIFIED_DATE, System.currentTimeMillis()); } + + public void setNoteValue(String key, int value) { + mNoteDiffValues.put(key, value); + mNoteDiffValues.put(NoteColumns.LOCAL_MODIFIED, 1); + mNoteDiffValues.put(NoteColumns.MODIFIED_DATE, System.currentTimeMillis()); + } + + /** + * 设置便签的置顶状态 + * @param isPinned 是否置顶,true表示置顶,false表示取消置顶 + */ + public void setPinned(boolean isPinned) { + setNoteValue(NoteColumns.PINNED, isPinned ? 1 : 0); + } + + /** + * 获取便签的置顶状态 + * @return 是否置顶,1表示置顶,0表示取消置顶 + */ + public Integer getPinned() { + return mNoteDiffValues.getAsInteger(NoteColumns.PINNED); + } public void setTextData(String key, String value) { mNoteData.setTextData(key, value); diff --git a/src/model/WorkingNote.java b/src/model/WorkingNote.java index 3f69a29..07d8319 100644 --- a/src/model/WorkingNote.java +++ b/src/model/WorkingNote.java @@ -39,6 +39,7 @@ public class WorkingNote { private Note mNote; private long mNoteId; private String mContent; + private String mTitle; // 便签标题 private int mMode; private long mAlertDate; private long mModifiedDate; @@ -49,6 +50,7 @@ public class WorkingNote { private Context mContext; private static final String TAG = "WorkingNote"; private boolean mIsDeleted; + private boolean mIsPinned; private NoteSettingChangedListener mNoteSettingStatusListener; // 数据表查询投影列 @@ -69,7 +71,9 @@ public class WorkingNote { NoteColumns.BG_COLOR_ID, NoteColumns.WIDGET_ID, NoteColumns.WIDGET_TYPE, - NoteColumns.MODIFIED_DATE + NoteColumns.MODIFIED_DATE, + NoteColumns.PINNED, + NoteColumns.TITLE }; // 数据表投影列索引 @@ -84,6 +88,8 @@ public class WorkingNote { private static final int NOTE_WIDGET_ID_COLUMN = 3; private static final int NOTE_WIDGET_TYPE_COLUMN = 4; private static final int NOTE_MODIFIED_DATE_COLUMN = 5; + private static final int NOTE_PINNED_COLUMN = 6; + private static final int NOTE_TITLE_COLUMN = 7; private WorkingNote(Context context, long folderId) { mContext = context; @@ -93,8 +99,10 @@ public class WorkingNote { mNote = new Note(); mNoteId = 0; mIsDeleted = false; + mIsPinned = false; mMode = 0; mWidgetType = Notes.TYPE_WIDGET_INVALIDE; + mTitle = ""; // 初始化标题为空字符串 } private WorkingNote(Context context, long noteId, long folderId) { @@ -122,6 +130,11 @@ public class WorkingNote { mWidgetType = cursor.getInt(NOTE_WIDGET_TYPE_COLUMN); mAlertDate = cursor.getLong(NOTE_ALERTED_DATE_COLUMN); mModifiedDate = cursor.getLong(NOTE_MODIFIED_DATE_COLUMN); + mIsPinned = cursor.getInt(NOTE_PINNED_COLUMN) == 1; + // 读取便签标题 + if (cursor.getColumnCount() > NOTE_TITLE_COLUMN) { + mTitle = cursor.getString(NOTE_TITLE_COLUMN); + } } cursor.close(); } else { @@ -319,6 +332,21 @@ public class WorkingNote { public boolean hasClockAlert() { return (mAlertDate > 0 ? true : false); } + + /** + * 检查便签是否被置顶 + */ + public boolean isPinned() { + return mIsPinned; + } + + /** + * 设置便签的置顶状态 + */ + public void setPinned(boolean isPinned) { + mIsPinned = isPinned; + mNote.setPinned(isPinned); + } public String getContent() { return mContent; @@ -364,6 +392,23 @@ public class WorkingNote { return mWidgetType; } + /** + * 获取便签标题 + */ + public String getTitle() { + return mTitle; + } + + /** + * 设置便签标题 + */ + public void setTitle(String title) { + if (!TextUtils.equals(mTitle, title)) { + mTitle = title; + mNote.setNoteValue(NoteColumns.TITLE, title); + } + } + /** * 便签设置变更监听器接口 */ diff --git a/src/tool/DataUtils.java b/src/tool/DataUtils.java index 7483c6e..c3ff0fb 100644 --- a/src/tool/DataUtils.java +++ b/src/tool/DataUtils.java @@ -24,6 +24,7 @@ import android.content.ContentValues; import android.content.OperationApplicationException; import android.database.Cursor; import android.os.RemoteException; +import android.text.TextUtils; import android.util.Log; import net.micode.notes.data.Notes; @@ -33,6 +34,8 @@ import net.micode.notes.ui.NotesListAdapter.AppWidgetAttribute; import java.util.ArrayList; import java.util.HashSet; +import java.util.regex.Matcher; +import java.util.regex.Pattern; /** * 数据操作工具类 @@ -97,6 +100,82 @@ public class DataUtils { } return false; // 异常情况 } + + /** + * 字数统计相关方法 + * 用于统计文本中的中文字数、数字和英文字符 + * 统计规则: + * 1. 中文字符:每个汉字算1个字数 + * 2. 数字:每个数字算1个字数 + * 3. 英文字符:每个英文字母算1个字数 + * 4. 不统计标点符号与空格 + */ + + // 中文字符正则表达式 + private static final Pattern CHINESE_CHAR_PATTERN = Pattern.compile("[\\u4e00-\\u9fa5]"); + // 数字正则表达式 + private static final Pattern NUMBER_PATTERN = Pattern.compile("[0-9]"); + // 英文字符正则表达式 + private static final Pattern ENGLISH_CHAR_PATTERN = Pattern.compile("[a-zA-Z]"); + + /** + * 统计文本总字数 + * @param text 要统计的文本 + * @return 总字数 + */ + public static int getTotalWordCount(String text) { + if (TextUtils.isEmpty(text)) { + return 0; + } + + int chineseCount = countChineseCharacters(text); + int numberCount = countNumbers(text); + int englishCount = countEnglishCharacters(text); + + return chineseCount + numberCount + englishCount; + } + + /** + * 统计中文字符数 + * @param text 要统计的文本 + * @return 中文字符数 + */ + private static int countChineseCharacters(String text) { + Matcher matcher = CHINESE_CHAR_PATTERN.matcher(text); + int count = 0; + while (matcher.find()) { + count++; + } + return count; + } + + /** + * 统计数字字符数 + * @param text 要统计的文本 + * @return 数字字符数 + */ + private static int countNumbers(String text) { + Matcher matcher = NUMBER_PATTERN.matcher(text); + int count = 0; + while (matcher.find()) { + count++; + } + return count; + } + + /** + * 统计英文字符数 + * @param text 要统计的文本 + * @return 英文字符数 + */ + private static int countEnglishCharacters(String text) { + Matcher matcher = ENGLISH_CHAR_PATTERN.matcher(text); + int count = 0; + while (matcher.find()) { + count++; + } + return count; + } /** * 移动单个便签到指定文件夹 @@ -111,7 +190,13 @@ public class DataUtils { public static void moveNoteToFoler(ContentResolver resolver, long id, long srcFolderId, long desFolderId) { ContentValues values = new ContentValues(); values.put(NoteColumns.PARENT_ID, desFolderId); // 设置新父文件夹 - values.put(NoteColumns.ORIGIN_PARENT_ID, srcFolderId); // 记录原始父文件夹,用于撤销 + + // 只有当移动到回收站时,才记录原始父文件夹ID + // 从回收站恢复时,保留原始的ORIGIN_PARENT_ID,以便递归恢复子项 + if (desFolderId == Notes.ID_TRASH_FOLER) { + values.put(NoteColumns.ORIGIN_PARENT_ID, srcFolderId); // 记录原始父文件夹,用于撤销 + } + values.put(NoteColumns.LOCAL_MODIFIED, 1); // 标记为本地已修改,需要同步 resolver.update(ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, id), values, null, null); } @@ -123,6 +208,7 @@ public class DataUtils { * 1. 支持空集合的检查 * 2. 使用事务保证数据一致性 * 3. 自动标记为本地修改,触发同步 + * 4. 移动到回收站时记录原始父文件夹ID * * @param resolver 内容解析器 * @param ids 要移动的便签ID集合 @@ -140,6 +226,26 @@ public class DataUtils { for (long id : ids) { ContentProviderOperation.Builder builder = ContentProviderOperation .newUpdate(ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, id)); + + // 如果是移动到回收站,记录原始父文件夹ID + if (folderId == Notes.ID_TRASH_FOLER) { + // 首先查询当前父文件夹ID + Cursor cursor = resolver.query( + ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, id), + new String[]{NoteColumns.PARENT_ID}, + null, + null, + null); + if (cursor != null) { + if (cursor.moveToFirst()) { + long currentParentId = cursor.getLong(0); + // 保存原始父文件夹ID + builder.withValue(NoteColumns.ORIGIN_PARENT_ID, currentParentId); + } + cursor.close(); + } + } + builder.withValue(NoteColumns.PARENT_ID, folderId); // 设置新父文件夹 builder.withValue(NoteColumns.LOCAL_MODIFIED, 1); // 标记为本地修改 operationList.add(builder.build()); @@ -159,6 +265,34 @@ public class DataUtils { } return false; } + + /** + * 获取指定文件夹下的所有直接子项(包括便签和子文件夹) + * + * @param resolver 内容解析器 + * @param folderId 文件夹ID + * @return 子项ID集合,没有子项时返回空集合 + */ + public static HashSet getFolderItems(ContentResolver resolver, long folderId) { + HashSet items = new HashSet(); + Cursor cursor = resolver.query(Notes.CONTENT_NOTE_URI, + new String[]{NoteColumns.ID}, + NoteColumns.PARENT_ID + "=?", + new String[]{String.valueOf(folderId)}, + null); + + if (cursor != null) { + if (cursor.moveToFirst()) { + do { + long id = cursor.getLong(0); + items.add(id); + } while (cursor.moveToNext()); + } + cursor.close(); + } + + return items; + } /** * 获取用户文件夹数量(排除系统文件夹) @@ -201,6 +335,11 @@ public class DataUtils { * @return true表示便签存在且不在回收站中,false表示不存在或在回收站中 */ public static boolean visibleInNoteDatabase(ContentResolver resolver, long noteId, int type) { + // 根文件夹永远可见 + if (noteId == Notes.ID_ROOT_FOLDER) { + return true; + } + Cursor cursor = resolver.query(ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, noteId), null, NoteColumns.TYPE + "=? AND " + NoteColumns.PARENT_ID + "<>" + Notes.ID_TRASH_FOLER, diff --git a/src/ui/NoteEditActivity.java b/src/ui/NoteEditActivity.java index ca47486..a124941 100644 --- a/src/ui/NoteEditActivity.java +++ b/src/ui/NoteEditActivity.java @@ -43,9 +43,13 @@ import android.view.MotionEvent; import android.view.View; import android.view.View.OnClickListener; import android.view.WindowManager; +import android.text.Editable; +import android.text.TextWatcher; import android.widget.CheckBox; import android.widget.CompoundButton; import android.widget.CompoundButton.OnCheckedChangeListener; +import android.widget.Button; +import android.app.Dialog; import android.widget.EditText; import android.widget.ImageView; import android.widget.LinearLayout; @@ -150,6 +154,7 @@ public class NoteEditActivity extends Activity implements OnClickListener, private LinearLayout mEditTextList; // 清单模式下的编辑列表容器 private String mUserQuery; // 搜索查询词(用于高亮显示) private Pattern mPattern; // 搜索高亮正则表达式模式 + private TextView mWordCountView; // 字数统计显示控件 @Override protected void onCreate(Bundle savedInstanceState) { @@ -192,32 +197,33 @@ public class NoteEditActivity extends Activity implements OnClickListener, mWorkingNote = null; // 处理查看笔记的意图 - if (TextUtils.equals(Intent.ACTION_VIEW, intent.getAction())) { - long noteId = intent.getLongExtra(Intent.EXTRA_UID, 0); - mUserQuery = ""; - - // 处理从搜索结果打开的情况 - if (intent.hasExtra(SearchManager.EXTRA_DATA_KEY)) { - noteId = Long.parseLong(intent.getStringExtra(SearchManager.EXTRA_DATA_KEY)); - mUserQuery = intent.getStringExtra(SearchManager.USER_QUERY); - } + if (TextUtils.equals(Intent.ACTION_VIEW, intent.getAction())) { + long noteId = intent.getLongExtra(Intent.EXTRA_UID, 0); + mUserQuery = ""; + + // 处理从搜索结果打开的情况 + if (intent.hasExtra(SearchManager.EXTRA_DATA_KEY)) { + noteId = Long.parseLong(intent.getStringExtra(SearchManager.EXTRA_DATA_KEY)); + mUserQuery = intent.getStringExtra(SearchManager.USER_QUERY); + } - // 检查笔记是否存在 - if (!DataUtils.visibleInNoteDatabase(getContentResolver(), noteId, Notes.TYPE_NOTE)) { - Intent jump = new Intent(this, NotesListActivity.class); - startActivity(jump); - showToast(R.string.error_note_not_exist); - finish(); - return false; - } else { - // 加载笔记 - mWorkingNote = WorkingNote.load(this, noteId); - if (mWorkingNote == null) { - Log.e(TAG, "load note failed with note id" + noteId); + // 检查笔记是否存在 + // 对于回收站中的便签,使用existInNoteDatabase而不是visibleInNoteDatabase + if (!DataUtils.existInNoteDatabase(getContentResolver(), noteId)) { + Intent jump = new Intent(this, NotesListActivity.class); + startActivity(jump); + showToast(R.string.error_note_not_exist); finish(); return false; + } else { + // 加载笔记 + mWorkingNote = WorkingNote.load(this, noteId); + if (mWorkingNote == null) { + Log.e(TAG, "load note failed with note id" + noteId); + finish(); + return false; + } } - } // 隐藏软键盘 getWindow().setSoftInputMode( WindowManager.LayoutParams.SOFT_INPUT_STATE_HIDDEN @@ -299,6 +305,8 @@ public class NoteEditActivity extends Activity implements OnClickListener, // 普通模式,显示内容并高亮搜索词 mNoteEditor.setText(getHighlightQueryResult(mWorkingNote.getContent(), mUserQuery)); mNoteEditor.setSelection(mNoteEditor.getText().length()); + // 更新字数统计 + updateWordCount(); } // 隐藏所有背景选择指示器 @@ -318,6 +326,7 @@ public class NoteEditActivity extends Activity implements OnClickListener, // 显示提醒信息 showAlertHeader(); + } /** @@ -438,6 +447,26 @@ public class NoteEditActivity extends Activity implements OnClickListener, } mEditTextList = (LinearLayout) findViewById(R.id.note_edit_list); + // 初始化字数统计控件 + mWordCountView = (TextView) findViewById(R.id.tv_word_count); + // 设置文本变化监听器 + mNoteEditor.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) { + // 文本变化时更新字数统计 + updateWordCount(); + } + + @Override + public void afterTextChanged(Editable s) { + // 文本变化后的处理,暂不需要 + } + }); } @Override @@ -450,6 +479,47 @@ public class NoteEditActivity extends Activity implements OnClickListener, clearSettingState(); // 清除设置状态 } + /** + * 更新字数统计显示 + */ + private void updateWordCount() { + if (mWordCountView != null) { + String content = getCurrentNoteContent(); + int wordCount = DataUtils.getTotalWordCount(content); + mWordCountView.setText(getString(R.string.note_word_count, wordCount)); + } + } + + /** + * 获取当前便签的内容,根据不同模式选择不同的获取方式 + * @return 当前便签的内容 + */ + private String getCurrentNoteContent() { + StringBuilder content = new StringBuilder(); + + // 检查当前模式 + if (mNoteEditor.getVisibility() == View.VISIBLE) { + // 普通模式,直接获取编辑框内容 + content.append(mNoteEditor.getText().toString()); + } else if (mEditTextList.getVisibility() == View.VISIBLE) { + // 清单模式,收集所有列表项的内容 + for (int i = 0; i < mEditTextList.getChildCount(); i++) { + View itemView = mEditTextList.getChildAt(i); + EditText editText = (EditText) itemView.findViewById(R.id.et_edit_text); + if (editText != null && !TextUtils.isEmpty(editText.getText())) { + // 添加当前列表项的内容 + content.append(editText.getText().toString()); + // 添加换行符,保持原有格式 + if (i < mEditTextList.getChildCount() - 1) { + content.append("\n"); + } + } + } + } + + return content.toString(); + } + /** * 更新桌面小部件 */ @@ -573,6 +643,16 @@ public class NoteEditActivity extends Activity implements OnClickListener, } else { menu.findItem(R.id.menu_delete_remind).setVisible(false); } + + // 根据便签的置顶状态设置菜单项标题 + MenuItem pinItem = menu.findItem(R.id.menu_pin_note); + if (pinItem != null) { + if (mWorkingNote.isPinned()) { + pinItem.setTitle(R.string.menu_unpin_note); + } else { + pinItem.setTitle(R.string.menu_pin_note); + } + } return true; } @@ -597,15 +677,15 @@ public class NoteEditActivity extends Activity implements OnClickListener, getWorkingText(); sendTo(this, mWorkingNote.getContent()); break; - case R.id.menu_send_to_desktop: // 发送到桌面 - sendToDesktop(); - break; case R.id.menu_alert: // 设置提醒 setReminder(); break; case R.id.menu_delete_remind: // 删除提醒 mWorkingNote.setAlertDate(0, false); break; + case R.id.menu_pin_note: // 置顶/取消置顶 + mWorkingNote.setPinned(!mWorkingNote.isPinned()); + break; default: break; } @@ -681,15 +761,9 @@ public class NoteEditActivity extends Activity implements OnClickListener, } else { Log.d(TAG, "Wrong note id, should not happen"); } - // 根据同步模式决定删除方式 - if (!isSyncMode()) { - if (!DataUtils.batchDeleteNotes(getContentResolver(), ids)) { - Log.e(TAG, "Delete Note error"); - } - } else { - if (!DataUtils.batchMoveToFolder(getContentResolver(), ids, Notes.ID_TRASH_FOLER)) { - Log.e(TAG, "Move notes to trash folder error, should not happens"); - } + // 所有模式下,都将删除的笔记移动到回收站 + if (!DataUtils.batchMoveToFolder(getContentResolver(), ids, Notes.ID_TRASH_FOLER)) { + Log.e(TAG, "Move notes to trash folder error, should not happens"); } } mWorkingNote.markDeleted(true); @@ -773,6 +847,7 @@ public class NoteEditActivity extends Activity implements OnClickListener, edit.append(text); edit.requestFocus(); edit.setSelection(length); + } /** @@ -798,6 +873,8 @@ public class NoteEditActivity extends Activity implements OnClickListener, ((NoteEditText) mEditTextList.getChildAt(i).findViewById(R.id.et_edit_text)) .setIndex(i); } + + } /** @@ -822,6 +899,8 @@ public class NoteEditActivity extends Activity implements OnClickListener, mNoteEditor.setVisibility(View.GONE); // 隐藏普通编辑框 mEditTextList.setVisibility(View.VISIBLE); // 显示清单列表 + + } /** @@ -877,6 +956,7 @@ public class NoteEditActivity extends Activity implements OnClickListener, item = item.substring(TAG_UNCHECKED.length(), item.length()).trim(); } + edit.setOnTextViewChangeListener(this); // 设置文本变化监听 edit.setIndex(index); // 设置项目索引 edit.setText(getHighlightQueryResult(item, mUserQuery)); // 设置文本(支持高亮) @@ -897,6 +977,8 @@ public class NoteEditActivity extends Activity implements OnClickListener, } else { mEditTextList.getChildAt(index).findViewById(R.id.cb_edit_item).setVisibility(View.GONE); } + // 更新字数统计 + updateWordCount(); } /** @@ -917,6 +999,9 @@ public class NoteEditActivity extends Activity implements OnClickListener, mEditTextList.setVisibility(View.GONE); mNoteEditor.setVisibility(View.VISIBLE); } + + // 更新字数统计 + updateWordCount(); } /** @@ -1003,6 +1088,7 @@ public class NoteEditActivity extends Activity implements OnClickListener, } } + /** * 生成快捷方式图标标题 */ diff --git a/src/ui/NoteItemData.java b/src/ui/NoteItemData.java index f3c33bd..a8f8715 100644 --- a/src/ui/NoteItemData.java +++ b/src/ui/NoteItemData.java @@ -33,18 +33,20 @@ import net.micode.notes.tool.DataUtils; public class NoteItemData { // 数据库查询列投影 static final String [] PROJECTION = new String [] { - NoteColumns.ID, // 笔记ID - NoteColumns.ALERTED_DATE, // 提醒时间 - NoteColumns.BG_COLOR_ID, // 背景颜色ID - NoteColumns.CREATED_DATE, // 创建时间 - NoteColumns.HAS_ATTACHMENT, // 是否有附件 - NoteColumns.MODIFIED_DATE, // 修改时间 - NoteColumns.NOTES_COUNT, // 笔记数量(针对文件夹) - NoteColumns.PARENT_ID, // 父文件夹ID - NoteColumns.SNIPPET, // 内容摘要 - NoteColumns.TYPE, // 类型(笔记/文件夹/系统文件夹) - NoteColumns.WIDGET_ID, // 小部件ID - NoteColumns.WIDGET_TYPE, // 小部件类型 + NoteColumns.ID, // 0: 笔记ID + NoteColumns.ALERTED_DATE, // 1: 提醒时间 + NoteColumns.BG_COLOR_ID, // 2: 背景颜色ID + NoteColumns.CREATED_DATE, // 3: 创建时间 + NoteColumns.HAS_ATTACHMENT, // 4: 是否有附件 + NoteColumns.MODIFIED_DATE, // 5: 修改时间 + NoteColumns.NOTES_COUNT, // 6: 笔记数量(针对文件夹) + NoteColumns.PARENT_ID, // 7: 父文件夹ID + NoteColumns.SNIPPET, // 8: 内容摘要 + NoteColumns.TITLE, // 9: 笔记标题 + NoteColumns.TYPE, // 10: 类型(笔记/文件夹/系统文件夹) + NoteColumns.WIDGET_ID, // 11: 小部件ID + NoteColumns.WIDGET_TYPE, // 12: 小部件类型 + NoteColumns.PINNED, // 13: 是否置顶 }; // 列索引常量 @@ -57,9 +59,11 @@ public class NoteItemData { private static final int NOTES_COUNT_COLUMN = 6; private static final int PARENT_ID_COLUMN = 7; private static final int SNIPPET_COLUMN = 8; - private static final int TYPE_COLUMN = 9; - private static final int WIDGET_ID_COLUMN = 10; - private static final int WIDGET_TYPE_COLUMN = 11; + private static final int TITLE_COLUMN = 9; + private static final int TYPE_COLUMN = 10; + private static final int WIDGET_ID_COLUMN = 11; + private static final int WIDGET_TYPE_COLUMN = 12; + private static final int PINNED_COLUMN = 13; // 笔记数据字段 private long mId; // 笔记ID @@ -71,9 +75,11 @@ public class NoteItemData { private int mNotesCount; // 笔记数量(针对文件夹) private long mParentId; // 父文件夹ID private String mSnippet; // 内容摘要 + private String mTitle; // 笔记标题 private int mType; // 类型 private int mWidgetId; // 小部件ID private int mWidgetType; // 小部件类型 + private boolean mIsPinned; // 是否置顶 private String mName; // 联系人姓名(针对通话记录) private String mPhoneNumber; // 电话号码(针对通话记录) @@ -97,15 +103,40 @@ public class NoteItemData { mCreatedDate = cursor.getLong(CREATED_DATE_COLUMN); mHasAttachment = (cursor.getInt(HAS_ATTACHMENT_COLUMN) > 0) ? true : false; mModifiedDate = cursor.getLong(MODIFIED_DATE_COLUMN); - mNotesCount = cursor.getInt(NOTES_COUNT_COLUMN); mParentId = cursor.getLong(PARENT_ID_COLUMN); mSnippet = cursor.getString(SNIPPET_COLUMN); // 移除清单标记 mSnippet = mSnippet.replace(NoteEditActivity.TAG_CHECKED, "").replace( NoteEditActivity.TAG_UNCHECKED, ""); + mTitle = cursor.getString(TITLE_COLUMN); // 读取标题字段,修复索引偏移问题 mType = cursor.getInt(TYPE_COLUMN); mWidgetId = cursor.getInt(WIDGET_ID_COLUMN); mWidgetType = cursor.getInt(WIDGET_TYPE_COLUMN); + mIsPinned = (cursor.getInt(PINNED_COLUMN) > 0) ? true : false; + + // 动态计算文件夹的便签数量 + if (mType == Notes.TYPE_FOLDER && mParentId == Notes.ID_TRASH_FOLER) { + // 只对回收站中的文件夹进行动态计算 + // 查询ORIGIN_PARENT_ID等于当前文件夹ID的便签数量 + // 这样可以在回收站中显示文件夹内的便签数量 + Cursor countCursor = context.getContentResolver().query( + Notes.CONTENT_NOTE_URI, + new String[]{"COUNT(*)"}, + Notes.NoteColumns.ORIGIN_PARENT_ID + "=?", + new String[]{String.valueOf(mId)}, + null); + if (countCursor != null) { + if (countCursor.moveToFirst()) { + mNotesCount = countCursor.getInt(0); + } + countCursor.close(); + } else { + mNotesCount = 0; + } + } else { + // 对于其他文件夹,使用原来的NOTES_COUNT字段 + mNotesCount = cursor.getInt(NOTES_COUNT_COLUMN); + } // 初始化通话记录相关字段 mPhoneNumber = ""; @@ -292,6 +323,32 @@ public class NoteItemData { public String getSnippet() { return mSnippet; } + + /** + * 获取标题 + */ + public String getTitle() { + return mTitle; + } + + /** + * 获取显示标题:优先使用标题字段,无标题时使用内容摘要的第一行 + */ + public String getDisplayTitle() { + if (!TextUtils.isEmpty(mTitle)) { + return mTitle; + } + // 没有标题时,返回内容摘要的第一行 + if (!TextUtils.isEmpty(mSnippet)) { + int newlineIndex = mSnippet.indexOf('\n'); + if (newlineIndex > 0) { + return mSnippet.substring(0, newlineIndex); + } else { + return mSnippet; + } + } + return ""; + } /** * 检查是否有提醒 @@ -315,4 +372,12 @@ public class NoteItemData { public static int getNoteType(Cursor cursor) { return cursor.getInt(TYPE_COLUMN); } + + /** + * 检查笔记是否置顶 + * @return 是否置顶 + */ + public boolean isPinned() { + return mIsPinned; + } } \ No newline at end of file diff --git a/src/ui/NotesListActivity.java b/src/ui/NotesListActivity.java index ae77f31..9285a82 100644 --- a/src/ui/NotesListActivity.java +++ b/src/ui/NotesListActivity.java @@ -22,6 +22,7 @@ import android.app.Dialog; import android.appwidget.AppWidgetManager; import android.content.AsyncQueryHandler; import android.content.ContentResolver; +import android.content.ContentUris; import android.content.ContentValues; import android.content.Context; import android.content.DialogInterface; @@ -63,6 +64,8 @@ import android.widget.Toast; import net.micode.notes.R; import net.micode.notes.data.Notes; import net.micode.notes.data.Notes.NoteColumns; +import net.micode.notes.data.Notes.DataConstants; +import net.micode.notes.data.Notes.DataColumns; import net.micode.notes.gtask.remote.GTaskSyncService; import net.micode.notes.model.WorkingNote; import net.micode.notes.tool.BackupUtils; @@ -92,6 +95,13 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt private static final int MENU_FOLDER_DELETE = 0; // 删除文件夹 private static final int MENU_FOLDER_VIEW = 1; // 查看文件夹 private static final int MENU_FOLDER_CHANGE_NAME = 2; // 修改文件夹名称 + private static final int MENU_FOLDER_PIN = 3; // 置顶文件夹 + private static final int MENU_FOLDER_UNPIN = 4; // 取消置顶文件夹 + private static final int MENU_NOTE_EDIT_TOPIC = 5; // 编辑便签标题 + private static final int MENU_NOTE_PIN = 6; // 置顶便签 + private static final int MENU_NOTE_UNPIN = 7; // 取消置顶便签 + private static final int MENU_RESTORE = 100; // 恢复 + private static final int MENU_PERMANENT_DELETE = 101; // 彻底删除 // 首选项键名 private static final String PREFERENCE_ADD_INTRODUCTION = "net.micode.notes.introduction"; @@ -102,7 +112,8 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt private enum ListEditState { NOTE_LIST, // 普通笔记列表 SUB_FOLDER, // 子文件夹视图 - CALL_RECORD_FOLDER // 通话记录文件夹 + CALL_RECORD_FOLDER, // 通话记录文件夹 + TRASH_FOLDER // 回收站文件夹 }; private ListEditState mState; // 当前列表状态 @@ -124,9 +135,11 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt // 查询条件 private static final String NORMAL_SELECTION = NoteColumns.PARENT_ID + "=?"; private static final String ROOT_FOLDER_SELECTION = "(" + NoteColumns.TYPE + "<>" - + Notes.TYPE_SYSTEM + " AND " + NoteColumns.PARENT_ID + "=?)" + " OR (" + + Notes.TYPE_SYSTEM + " AND " + NoteColumns.PARENT_ID + "=?) OR (" + NoteColumns.ID + "=" + Notes.ID_CALL_RECORD_FOLDER + " AND " + NoteColumns.NOTES_COUNT + ">0)"; + private static final String TRASH_SELECTION = "(" + NoteColumns.PARENT_ID + "=?" + " AND " + + NoteColumns.TYPE + "=" + Notes.TYPE_FOLDER + ")"; // 请求码 private final static int REQUEST_CODE_OPEN_NODE = 102; // 打开笔记 @@ -136,12 +149,26 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.note_list); // 设置列表布局 - initResources(); // 初始化资源 + + // 初始化资源和视图 + initResources(); /** * 首次使用时插入介绍说明 */ setAppInfoFromRawRes(); + // 注册Android 13+的返回键回调 + if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.TIRAMISU) { + getOnBackInvokedDispatcher().registerOnBackInvokedCallback( + android.window.OnBackInvokedDispatcher.PRIORITY_DEFAULT, + new android.window.OnBackInvokedCallback() { + @Override + public void onBackInvoked() { + handleBackPress(); + } + } + ); + } } @Override @@ -226,6 +253,8 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt mNotesListView.setOnItemLongClickListener(this); // 设置列表项长按监听 mNotesListAdapter = new NotesListAdapter(this); // 创建列表适配器 mNotesListView.setAdapter(mNotesListAdapter); + // 注册上下文菜单,用于文件夹长按操作 + registerForContextMenu(mNotesListView); mAddNewNote = (Button) findViewById(R.id.btn_new_note); mAddNewNote.setOnClickListener(this); // 设置新建笔记按钮点击监听 mAddNewNote.setOnTouchListener(new NewNoteOnTouchListener()); // 设置新建笔记按钮触摸监听 @@ -236,7 +265,6 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt mState = ListEditState.NOTE_LIST; // 初始状态为普通笔记列表 mModeCallBack = new ModeCallback(); // 创建多选模式回调 } - /** * 多选模式回调类 * 实现列表的多选操作功能 @@ -245,22 +273,51 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt private DropdownMenu mDropDownMenu; // 下拉菜单 private ActionMode mActionMode; // 操作模式 private MenuItem mMoveMenu; // 移动菜单项 + private MenuItem mDeleteMenu; // 删除菜单项 + private MenuItem mRestoreMenu; // 恢复菜单项 + + // 定义菜单项ID常量 + private static final int MENU_RESTORE = 100; + private static final int MENU_PERMANENT_DELETE = 101; public boolean onCreateActionMode(ActionMode mode, Menu menu) { - getMenuInflater().inflate(R.menu.note_list_options, menu); // 加载多选菜单 - menu.findItem(R.id.delete).setOnMenuItemClickListener(this); // 删除菜单项 - mMoveMenu = menu.findItem(R.id.move); // 移动菜单项 - // 根据条件设置移动菜单项可见性 - if (mFocusNoteDataItem.getParentId() == Notes.ID_CALL_RECORD_FOLDER - || DataUtils.getUserFolderCount(mContentResolver) == 0) { - mMoveMenu.setVisible(false); + menu.clear(); // 清空菜单 + + if (mCurrentFolderId == Notes.ID_TRASH_FOLER) { + // 回收站中显示恢复和彻底删除菜单 + menu.add(0, MENU_RESTORE, 0, R.string.menu_restore) + .setOnMenuItemClickListener(this); + menu.add(0, MENU_PERMANENT_DELETE, 1, R.string.menu_permanent_delete) + .setOnMenuItemClickListener(this); } else { - mMoveMenu.setVisible(true); - mMoveMenu.setOnMenuItemClickListener(this); + // 普通文件夹中显示菜单 + getMenuInflater().inflate(R.menu.note_list_options, menu); // 加载多选菜单 + menu.findItem(R.id.delete).setOnMenuItemClickListener(this); // 删除菜单项 + mMoveMenu = menu.findItem(R.id.move); // 移动菜单项 + // 根据条件设置移动菜单项可见性 + boolean hideMoveMenu = DataUtils.getUserFolderCount(mContentResolver) == 0; + // 如果有焦点便签且它来自通话记录文件夹,也隐藏移动菜单 + if (mFocusNoteDataItem != null) { + hideMoveMenu = hideMoveMenu || (mFocusNoteDataItem.getParentId() == Notes.ID_CALL_RECORD_FOLDER); + } + if (hideMoveMenu) { + mMoveMenu.setVisible(false); + } else { + mMoveMenu.setVisible(true); + mMoveMenu.setOnMenuItemClickListener(this); + } + + // 添加Edit topic选项,初始状态隐藏 + MenuItem editTopicMenu = menu.add(0, MENU_NOTE_EDIT_TOPIC, 0, R.string.menu_edit_topic); + editTopicMenu.setOnMenuItemClickListener(this); + editTopicMenu.setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS); + // 初始状态隐藏,在onPrepareActionMode中根据选中数量动态显示 + editTopicMenu.setVisible(false); } + mActionMode = mode; mNotesListAdapter.setChoiceMode(true); // 启用选择模式 - mNotesListView.setLongClickable(false); // 禁用长按 + // 注意:不再禁用长按,以便在多选模式下也能通过长按显示上下文菜单 mAddNewNote.setVisibility(View.GONE); // 隐藏新建笔记按钮 // 设置自定义视图(下拉菜单) @@ -300,11 +357,24 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt item.setTitle(R.string.menu_select_all); // 未全选时显示"全选" } } + // 如果有编辑标题菜单,根据选中数量动态显示/隐藏 + if (mActionMode != null) { + Menu menu = mActionMode.getMenu(); + MenuItem editTopicMenu = menu.findItem(MENU_NOTE_EDIT_TOPIC); + if (editTopicMenu != null) { + editTopicMenu.setVisible(selectedCount == 1); + } + } } public boolean onPrepareActionMode(ActionMode mode, Menu menu) { - // TODO Auto-generated method stub - return false; + // 根据选中的便签数量动态显示/隐藏Edit topic选项 + MenuItem editTopicMenu = menu.findItem(MENU_NOTE_EDIT_TOPIC); + if (editTopicMenu != null) { + int selectedCount = mNotesListAdapter.getSelectedCount(); + editTopicMenu.setVisible(selectedCount == 1); + } + return true; } public boolean onActionItemClicked(ActionMode mode, MenuItem item) { @@ -317,16 +387,32 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt mNotesListAdapter.setChoiceMode(false); // 禁用选择模式 mNotesListView.setLongClickable(true); // 启用长按 mAddNewNote.setVisibility(View.VISIBLE); // 显示新建笔记按钮 + mActionMode = null; + mNotesListView.clearChoices(); } public void finishActionMode() { - mActionMode.finish(); // 结束操作模式 + if (mActionMode != null) { + mActionMode.finish(); // 结束操作模式,防止空指针异常 + } } - public void onItemCheckedStateChanged(ActionMode mode, int position, long id, - boolean checked) { + public void onItemCheckedStateChanged(ActionMode mode, int position, long id, boolean checked) { mNotesListAdapter.setCheckedItem(position, checked); // 设置选中状态 + + // 当只选择一个便签时,保存为焦点项,用于Edit topic + if (checked && mNotesListAdapter.getSelectedCount() == 1) { + Cursor cursor = mNotesListAdapter.getCursor(); + if (cursor != null && cursor.moveToPosition(position)) { + mFocusNoteDataItem = new NoteItemData(NotesListActivity.this, cursor); + } + } else if (!checked && mNotesListAdapter.getSelectedCount() == 0) { + mFocusNoteDataItem = null; + } + updateMenu(); // 更新菜单 + // 注意:不要在这里调用mode.finish(),否则会导致系统销毁意外的ActionMode实例 + // 选择模式应该由用户自己点击返回键或点击空白区域退出 } public boolean onMenuItemClick(MenuItem item) { @@ -355,8 +441,59 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt builder.show(); break; case R.id.move: + // 保存当前选择模式的状态,防止在异步查询过程中被意外结束 startQueryDestinationFolders(); // 查询目标文件夹 break; + case MENU_RESTORE: + // 批量恢复确认对话框 + AlertDialog.Builder restoreBuilder = new AlertDialog.Builder(NotesListActivity.this); + restoreBuilder.setTitle(getString(R.string.menu_restore_from_trash)); + restoreBuilder.setIcon(android.R.drawable.ic_dialog_alert); + restoreBuilder.setMessage(getString(R.string.alert_message_restore_notes, + mNotesListAdapter.getSelectedCount())); + restoreBuilder.setPositiveButton(android.R.string.ok, + new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, + int which) { + batchRestoreFromTrash(); // 执行批量恢复 + } + }); + restoreBuilder.setNegativeButton(android.R.string.cancel, null); + restoreBuilder.show(); + break; + case MENU_PERMANENT_DELETE: + // 批量彻底删除确认对话框 + AlertDialog.Builder permanentDeleteBuilder = new AlertDialog.Builder(NotesListActivity.this); + permanentDeleteBuilder.setTitle(getString(R.string.menu_permanent_delete)); + permanentDeleteBuilder.setIcon(android.R.drawable.ic_dialog_alert); + permanentDeleteBuilder.setMessage(getString(R.string.alert_message_permanent_delete, + mNotesListAdapter.getSelectedCount())); + permanentDeleteBuilder.setPositiveButton(android.R.string.ok, + new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, + int which) { + batchPermanentDelete(); // 执行批量彻底删除 + } + }); + permanentDeleteBuilder.setNegativeButton(android.R.string.cancel, null); + permanentDeleteBuilder.show(); + break; + case MENU_NOTE_EDIT_TOPIC: + // 编辑便签标题 + if (mNotesListAdapter.getSelectedCount() == 1) { + // 在结束选择模式前保存当前焦点便签,防止mFocusNoteDataItem被重置 + NoteItemData tempNoteItem = mFocusNoteDataItem; + mActionMode.finish(); // 结束选择模式 + // 使用保存的便签项显示编辑对话框 + mFocusNoteDataItem = tempNoteItem; + if (mFocusNoteDataItem != null) { + showEditNoteTopicDialog(); + } + } else { + Toast.makeText(NotesListActivity.this, getString(R.string.menu_select_single_note), + Toast.LENGTH_SHORT).show(); + } + break; default: return false; } @@ -433,12 +570,56 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt * 启动异步笔记列表查询 */ private void startAsyncNotesListQuery() { - String selection = (mCurrentFolderId == Notes.ID_ROOT_FOLDER) ? ROOT_FOLDER_SELECTION - : NORMAL_SELECTION; // 根据当前文件夹选择查询条件 - mBackgroundQueryHandler.startQuery(FOLDER_NOTE_LIST_QUERY_TOKEN, null, - Notes.CONTENT_NOTE_URI, NoteItemData.PROJECTION, selection, new String[] { - String.valueOf(mCurrentFolderId) - }, NoteColumns.TYPE + " DESC," + NoteColumns.MODIFIED_DATE + " DESC"); + String selection; + String[] selectionArgs; + + if (mCurrentFolderId == Notes.ID_ROOT_FOLDER) { + // 根文件夹查询条件 + selection = ROOT_FOLDER_SELECTION; + selectionArgs = new String[] { String.valueOf(mCurrentFolderId) }; + } else if (mCurrentFolderId == Notes.ID_TRASH_FOLER) { + // 回收站根目录查询条件 + // 显示所有直接位于回收站根目录的项目,包括单独删除的便签和文件夹 + // 对于文件夹,只显示文件夹本身,不显示文件夹内的便签 + // 对于单独删除的便签,直接显示在回收站根目录 + // 对于文件夹内的便签,不显示在回收站根目录,只显示在文件夹内 + // 显示规则: + // 1. 显示所有类型为文件夹的项目(被删除的文件夹) + // 2. 显示类型为便签的项目,并且它们是单独删除的 + // 单独删除的判断:该便签的ORIGIN_PARENT_ID对应的文件夹不存在于回收站中 + selection = NORMAL_SELECTION + " AND (" + + NoteColumns.TYPE + "=" + Notes.TYPE_FOLDER + " OR " + + "(" + NoteColumns.TYPE + "=" + Notes.TYPE_NOTE + " AND " + + "(" + + // 条件:该便签的ORIGIN_PARENT_ID对应的文件夹不存在于回收站中 + // 即该便签是单独删除的,而不是某个被删除文件夹的内容 + NoteColumns.ORIGIN_PARENT_ID + " NOT IN (" + + "SELECT " + NoteColumns.ID + " FROM " + + net.micode.notes.data.NotesDatabaseHelper.TABLE.NOTE + " WHERE " + + NoteColumns.TYPE + "=" + Notes.TYPE_FOLDER + " AND " + + NoteColumns.PARENT_ID + "=" + Notes.ID_TRASH_FOLER + + ")" + + ")" + + "))"; + selectionArgs = new String[] { String.valueOf(mCurrentFolderId) }; + } else if (mState == ListEditState.TRASH_FOLDER) { + // 在回收站中打开文件夹时,查询ORIGIN_PARENT_ID等于当前文件夹ID的所有便签和文件夹 + // 因为当文件夹被移动到回收站时,文件夹内的便签的PARENT_ID会被设置为回收站ID + // 但它们的ORIGIN_PARENT_ID会被保存为原来的文件夹ID + selection = NoteColumns.ORIGIN_PARENT_ID + "=?"; + selectionArgs = new String[] { String.valueOf(mCurrentFolderId) }; + } else { + // 普通文件夹查询条件 + selection = NORMAL_SELECTION; + selectionArgs = new String[] { String.valueOf(mCurrentFolderId) }; + } + + // 排序条件:先按类型排序(文件夹在前,便签在后),再按置顶状态排序,最后按修改时间排序 + mBackgroundQueryHandler.startQuery(FOLDER_NOTE_LIST_QUERY_TOKEN, + null, + Notes.CONTENT_NOTE_URI, + NoteItemData.PROJECTION, selection, selectionArgs, + NoteColumns.TYPE + " DESC," + NoteColumns.PINNED + " DESC," + NoteColumns.MODIFIED_DATE + " DESC"); } /** @@ -472,21 +653,53 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt * 显示文件夹列表菜单(用于移动笔记) */ private void showFolderListMenu(Cursor cursor) { + // 保存当前的ActionMode引用,以便在操作完成后检查它是否仍然有效 + final ActionMode currentActionMode = mModeCallBack.mActionMode; + AlertDialog.Builder builder = new AlertDialog.Builder(NotesListActivity.this); builder.setTitle(R.string.menu_title_select_folder); final FoldersListAdapter adapter = new FoldersListAdapter(this, cursor); builder.setAdapter(adapter, new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int which) { - DataUtils.batchMoveToFolder(mContentResolver, - mNotesListAdapter.getSelectedItemIds(), adapter.getItemId(which)); - Toast.makeText( - NotesListActivity.this, - getString(R.string.format_move_notes_to_folder, - mNotesListAdapter.getSelectedCount(), - adapter.getFolderName(NotesListActivity.this, which)), - Toast.LENGTH_SHORT).show(); // 显示移动成功提示 - mModeCallBack.finishActionMode(); // 结束多选模式 + // 获取目标文件夹ID + long targetFolderId = adapter.getItemId(which); + // 获取要移动的便签数量 + int moveCount; + + if (mNotesListAdapter.isInChoiceMode()) { + // 多选模式下,移动所有选中的便签 + DataUtils.batchMoveToFolder(mContentResolver, + mNotesListAdapter.getSelectedItemIds(), targetFolderId); + moveCount = mNotesListAdapter.getSelectedCount(); + } else { + // 单选模式下,移动当前焦点便签 + if (mFocusNoteDataItem != null) { + DataUtils.batchMoveToFolder(mContentResolver, + new HashSet() {{ add(mFocusNoteDataItem.getId()); }}, targetFolderId); + moveCount = 1; + } else { + moveCount = 0; + } + } + + if (moveCount > 0) { + // 显示移动成功提示 + Toast.makeText( + NotesListActivity.this, + getString(R.string.format_move_notes_to_folder, + moveCount, + adapter.getFolderName(NotesListActivity.this, which)), + Toast.LENGTH_SHORT).show(); + + // 刷新列表,显示更新后的结果 + startAsyncNotesListQuery(); + } + + // 安全地结束选择模式,只有当当前ActionMode仍然有效时才结束 + if (currentActionMode != null && currentActionMode == mModeCallBack.mActionMode) { + currentActionMode.finish(); + } } }); builder.show(); @@ -506,23 +719,121 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt * 批量删除笔记 */ private void batchDelete() { + // 保存当前的ActionMode引用,以便在异步任务完成后检查它是否仍然有效 + final ActionMode currentActionMode = mModeCallBack.mActionMode; + + new AsyncTask>() { + protected HashSet doInBackground(Void... unused) { + HashSet widgets = mNotesListAdapter.getSelectedWidget(); + // 所有模式下,都将删除的笔记移动到回收站 + if (!DataUtils.batchMoveToFolder(mContentResolver, mNotesListAdapter + .getSelectedItemIds(), Notes.ID_TRASH_FOLER)) { + Log.e(TAG, "Move notes to trash folder error, should not happens"); + } + return widgets; + } + + @Override + protected void onPostExecute(HashSet widgets) { + if (widgets != null) { + for (AppWidgetAttribute widget : widgets) { + if (widget.widgetId != AppWidgetManager.INVALID_APPWIDGET_ID + && widget.widgetType != Notes.TYPE_WIDGET_INVALIDE) { + updateWidget(widget.widgetId, widget.widgetType); // 更新小部件 + } + } + } + + // 刷新列表,显示更新后的结果 + startAsyncNotesListQuery(); + + // 确保 mFocusNoteDataItem 被重置,避免指向已被恢复的数据 + mFocusNoteDataItem = null; + + // 安全地结束选择模式,只有当当前ActionMode仍然有效时才结束 + if (currentActionMode != null && currentActionMode == mModeCallBack.mActionMode) { + currentActionMode.finish(); + } + } + }.execute(); + } + + /** + * 从回收站批量恢复笔记 + */ + private void batchRestoreFromTrash() { + // 保存当前的ActionMode引用,以便在异步任务完成后检查它是否仍然有效 + final ActionMode currentActionMode = mModeCallBack.mActionMode; + new AsyncTask>() { protected HashSet doInBackground(Void... unused) { HashSet widgets = mNotesListAdapter.getSelectedWidget(); - if (!isSyncMode()) { - // 非同步模式下,直接删除笔记 - if (DataUtils.batchDeleteNotes(mContentResolver, mNotesListAdapter - .getSelectedItemIds())) { + // 将选中的笔记从回收站恢复 + HashSet ids = mNotesListAdapter.getSelectedItemIds(); + // 遍历每个选中的笔记 + for (long id : ids) { + // 检查当前是否在回收站中的文件夹内 + if (mCurrentFolderId != Notes.ID_TRASH_FOLER) { + // 在文件夹内恢复单个便签,恢复到根文件夹 + DataUtils.moveNoteToFoler(mContentResolver, id, Notes.ID_TRASH_FOLER, Notes.ID_ROOT_FOLDER); } else { - Log.e(TAG, "Delete notes error, should not happens"); + // 在回收站根目录恢复 + // 查询笔记的原始父文件夹ID + long originParentId = getOriginParentId(id); + // 检查原始父文件夹是否存在且不在回收站中 + boolean parentFolderExists; + if (originParentId == Notes.ID_ROOT_FOLDER) { + // 根文件夹永远存在 + parentFolderExists = true; + } else { + // 检查普通文件夹是否存在且不在回收站中 + parentFolderExists = DataUtils.visibleInNoteDatabase(mContentResolver, originParentId, Notes.TYPE_FOLDER); + } + // 如果父文件夹不存在或在回收站中,恢复到根文件夹 + long targetFolderId = parentFolderExists ? originParentId : Notes.ID_ROOT_FOLDER; + // 将笔记移动到目标文件夹 + DataUtils.moveNoteToFoler(mContentResolver, id, Notes.ID_TRASH_FOLER, targetFolderId); } - } else { - // 同步模式下,将删除的笔记移动到垃圾箱文件夹 - if (!DataUtils.batchMoveToFolder(mContentResolver, mNotesListAdapter - .getSelectedItemIds(), Notes.ID_TRASH_FOLER)) { - Log.e(TAG, "Move notes to trash folder error, should not happens"); + } + return widgets; + } + + @Override + protected void onPostExecute(HashSet widgets) { + if (widgets != null) { + for (AppWidgetAttribute widget : widgets) { + if (widget.widgetId != AppWidgetManager.INVALID_APPWIDGET_ID + && widget.widgetType != Notes.TYPE_WIDGET_INVALIDE) { + updateWidget(widget.widgetId, widget.widgetType); // 更新小部件 + } } } + + // 刷新列表,显示更新后的结果 + startAsyncNotesListQuery(); + + // 安全地结束选择模式,只有当当前ActionMode仍然有效时才结束 + if (currentActionMode != null && currentActionMode == mModeCallBack.mActionMode) { + currentActionMode.finish(); + } + } + }.execute(); + } + + /** + * 批量彻底删除笔记 + */ + private void batchPermanentDelete() { + // 保存当前的ActionMode引用,以便在异步任务完成后检查它是否仍然有效 + final ActionMode currentActionMode = mModeCallBack.mActionMode; + + new AsyncTask>() { + protected HashSet doInBackground(Void... unused) { + HashSet widgets = mNotesListAdapter.getSelectedWidget(); + // 彻底删除选中的笔记 + if (!DataUtils.batchDeleteNotes(mContentResolver, mNotesListAdapter.getSelectedItemIds())) { + Log.e(TAG, "Permanent delete notes error, should not happens"); + } return widgets; } @@ -536,9 +847,324 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt } } } - mModeCallBack.finishActionMode(); // 结束多选模式 + + // 刷新列表,显示更新后的结果 + startAsyncNotesListQuery(); + + // 安全地结束选择模式,只有当当前ActionMode仍然有效时才结束 + if (currentActionMode != null && currentActionMode == mModeCallBack.mActionMode) { + currentActionMode.finish(); + } + } + }.execute(); + } + + /** + * 获取笔记的原始父文件夹ID + */ + private long getOriginParentId(long noteId) { + // 查询笔记的原始父文件夹ID + long originParentId = Notes.ID_ROOT_FOLDER; // 默认根文件夹 + String[] projection = {Notes.NoteColumns.ORIGIN_PARENT_ID}; + Cursor cursor = mContentResolver.query( + ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, noteId), + projection, + null, + null, + null); + if (cursor != null) { + if (cursor.moveToFirst()) { + originParentId = cursor.getLong(0); + // 如果原始父文件夹ID无效,使用根文件夹 + if (originParentId <= 0) { + originParentId = Notes.ID_ROOT_FOLDER; + } + } + cursor.close(); + } + return originParentId; + } + + /** + * 设置文件夹的置顶状态 + * @param folderId 文件夹ID + * @param pinned 是否置顶 + */ + private void setFolderPinnedStatus(long folderId, boolean pinned) { + ContentValues values = new ContentValues(); + values.put(NoteColumns.PINNED, pinned ? 1 : 0); + values.put(NoteColumns.LOCAL_MODIFIED, 1); + + // 更新文件夹的置顶状态 + mContentResolver.update(Notes.CONTENT_NOTE_URI, values, + NoteColumns.ID + "=? AND " + NoteColumns.TYPE + "=?", + new String[] {String.valueOf(folderId), String.valueOf(Notes.TYPE_FOLDER)}); + + // 刷新列表,让置顶状态生效 + startAsyncNotesListQuery(); + } + + /** + * 设置便签的置顶状态 + * @param noteId 便签ID + * @param pinned 是否置顶 + */ + private void setNotePinnedStatus(long noteId, boolean pinned) { + ContentValues values = new ContentValues(); + values.put(NoteColumns.PINNED, pinned ? 1 : 0); + values.put(NoteColumns.LOCAL_MODIFIED, 1); + + // 更新便签的置顶状态 + mContentResolver.update(Notes.CONTENT_NOTE_URI, values, + NoteColumns.ID + "=? AND " + NoteColumns.TYPE + "=?", + new String[] {String.valueOf(noteId), String.valueOf(Notes.TYPE_NOTE)}); + + // 刷新列表,让置顶状态生效 + startAsyncNotesListQuery(); + } + + /** + * 显示编辑便签标题对话框 + */ + private void showEditNoteTopicDialog() { + // 检查mFocusNoteDataItem是否为null + if (mFocusNoteDataItem == null) { + Log.e(TAG, "showEditNoteTopicDialog: mFocusNoteDataItem is null"); + return; + } + + // 创建对话框视图 + View view = LayoutInflater.from(this).inflate(R.layout.dialog_edit_topic, null); + final EditText etTopic = (EditText) view.findViewById(R.id.et_topic); + + // 设置初始标题为便签的标题字段,如果标题字段为空,则使用内容摘要的第一行 + String currentTopic = mFocusNoteDataItem.getDisplayTitle(); + if (!TextUtils.isEmpty(currentTopic)) { + etTopic.setText(currentTopic); + etTopic.setSelection(currentTopic.length()); // 选中所有文本 + } + + // 创建对话框 + AlertDialog.Builder builder = new AlertDialog.Builder(this); + builder.setTitle(R.string.menu_edit_topic); + // 使用系统默认编辑图标 + builder.setIcon(android.R.drawable.ic_menu_edit); + builder.setView(view); + + builder.setPositiveButton(android.R.string.ok, null); + builder.setNegativeButton(android.R.string.cancel, new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int which) { + // 取消编辑,不做任何操作 + } + }); + + final Dialog dialog = builder.show(); + final Button positive = (Button) dialog.findViewById(android.R.id.button1); + positive.setOnClickListener(new OnClickListener() { + public void onClick(View v) { + String topic = etTopic.getText().toString().trim(); + if (!TextUtils.isEmpty(topic)) { + // 更新便签标题 + updateNoteTopic(topic); + dialog.dismiss(); + } else { + // 标题不能为空,显示提示 + Toast.makeText(NotesListActivity.this, getString(R.string.topic_cannot_be_empty), + Toast.LENGTH_LONG).show(); + } + } + }); + } + + /** + * 更新便签标题 + * @param topic 新标题 + */ + private void updateNoteTopic(String topic) { + // 检查mFocusNoteDataItem是否为null + if (mFocusNoteDataItem == null) { + Log.e(TAG, "updateNoteTopic: mFocusNoteDataItem is null"); + return; + } + + // 更新便签的标题字段,而不是修改内容 + ContentValues values = new ContentValues(); + values.put(NoteColumns.TITLE, topic); + values.put(NoteColumns.LOCAL_MODIFIED, 1); + + mContentResolver.update( + Notes.CONTENT_NOTE_URI, + values, + NoteColumns.ID + "=? AND " + NoteColumns.TYPE + "=?", + new String[]{String.valueOf(mFocusNoteDataItem.getId()), String.valueOf(Notes.TYPE_NOTE)}); + + // 刷新列表,显示更新后的结果 + startAsyncNotesListQuery(); + } + + /** + * 恢复文件夹 + * @param folderId 文件夹ID + */ + private void restoreFolder(long folderId) { + if (folderId == Notes.ID_ROOT_FOLDER) { + Log.e(TAG, "Wrong folder id, should not happen " + folderId); + return; + } + + // 获取文件夹下的所有子项(包括便签和子文件夹) + // 使用递归方式获取所有层级的子项 + HashSet items = new HashSet(); + // 将文件夹本身也添加到要恢复的列表中 + items.add(folderId); + // 递归获取所有子项 + getRecycleBinFolderItems(folderId, items); + + // 恢复文件夹及其所有子项 + new AsyncTask>() { + protected HashSet doInBackground(Void... unused) { + HashSet widgets = DataUtils.getFolderNoteWidget(mContentResolver, folderId); + // 遍历每个要恢复的项目 + for (long id : items) { + // 查询项目的原始父文件夹ID + long originParentId = getOriginParentId(id); + // 将项目恢复到原始位置 + DataUtils.moveNoteToFoler(mContentResolver, id, Notes.ID_TRASH_FOLER, originParentId); + } + return widgets; + } + + @Override + protected void onPostExecute(HashSet widgets) { + if (widgets != null) { + for (AppWidgetAttribute widget : widgets) { + if (widget.widgetId != AppWidgetManager.INVALID_APPWIDGET_ID + && widget.widgetType != Notes.TYPE_WIDGET_INVALIDE) { + updateWidget(widget.widgetId, widget.widgetType); // 更新小部件 + } + } + } + + // 确保 mFocusNoteDataItem 被重置,避免指向已被恢复的数据 + mFocusNoteDataItem = null; + } + }.execute(); + + // 刷新列表,显示恢复后的结果 + startAsyncNotesListQuery(); + } + + /** + * 恢复单个便签 + * @param noteId 便签ID + */ + private void restoreNote(long noteId) { + // 查询便签的原始父文件夹ID + long originParentId = getOriginParentId(noteId); + // 检查原始父文件夹是否存在且不在回收站中 + boolean parentFolderExists; + if (originParentId == Notes.ID_ROOT_FOLDER) { + // 根文件夹永远存在 + parentFolderExists = true; + } else { + // 检查普通文件夹是否存在且不在回收站中 + parentFolderExists = DataUtils.visibleInNoteDatabase(mContentResolver, originParentId, Notes.TYPE_FOLDER); + } + // 如果父文件夹不存在或在回收站中,恢复到根文件夹 + long targetFolderId = parentFolderExists ? originParentId : Notes.ID_ROOT_FOLDER; + // 将便签移动到目标文件夹 + DataUtils.moveNoteToFoler(mContentResolver, noteId, Notes.ID_TRASH_FOLER, targetFolderId); + // 刷新列表,显示恢复后的结果 + startAsyncNotesListQuery(); + + // 确保 mFocusNoteDataItem 被重置,避免指向已被恢复的数据 + mFocusNoteDataItem = null; + } + + /** + * 彻底删除文件夹 + * @param folderId 文件夹ID + */ + private void permanentDeleteFolder(long folderId) { + if (folderId == Notes.ID_ROOT_FOLDER) { + Log.e(TAG, "Wrong folder id, should not happen " + folderId); + return; + } + + // 获取文件夹下的所有子项(包括便签和子文件夹) + // 在回收站中,需要根据ORIGIN_PARENT_ID来查询文件夹内容 + HashSet items = new HashSet<>(); + + // 添加文件夹本身 + items.add(folderId); + + // 递归获取所有子项 + getRecycleBinFolderItems(folderId, items); + + // 彻底删除文件夹及其所有子项 + new AsyncTask>() { + protected HashSet doInBackground(Void... unused) { + // 获取所有相关的小部件 + HashSet widgets = new HashSet<>(); + for (long itemId : items) { + // 只获取便签的小部件 + if (DataUtils.visibleInNoteDatabase(mContentResolver, itemId, Notes.TYPE_NOTE)) { + widgets.addAll(DataUtils.getFolderNoteWidget(mContentResolver, itemId)); + } + } + // 彻底删除所有项目 + DataUtils.batchDeleteNotes(mContentResolver, items); + return widgets; + } + + @Override + protected void onPostExecute(HashSet widgets) { + if (widgets != null) { + for (AppWidgetAttribute widget : widgets) { + if (widget.widgetId != AppWidgetManager.INVALID_APPWIDGET_ID + && widget.widgetType != Notes.TYPE_WIDGET_INVALIDE) { + updateWidget(widget.widgetId, widget.widgetType); // 更新小部件 + } + } + } } }.execute(); + + // 刷新列表,显示删除后的结果 + startAsyncNotesListQuery(); + } + + /** + * 递归获取回收站文件夹下的所有子项 + * @param folderId 文件夹ID + * @param items 用于存储子项ID的集合 + */ + private void getRecycleBinFolderItems(long folderId, HashSet items) { + // 查询该文件夹下的所有子项(根据ORIGIN_PARENT_ID和当前PARENT_ID是回收站ID) + Cursor cursor = mContentResolver.query( + Notes.CONTENT_NOTE_URI, + new String[]{Notes.NoteColumns.ID, Notes.NoteColumns.TYPE}, + Notes.NoteColumns.ORIGIN_PARENT_ID + "=? AND " + Notes.NoteColumns.PARENT_ID + "=?", + new String[]{String.valueOf(folderId), String.valueOf(Notes.ID_TRASH_FOLER)}, + null); + + if (cursor != null) { + if (cursor.moveToFirst()) { + do { + long itemId = cursor.getLong(0); + int itemType = cursor.getInt(1); + + // 添加到要恢复的列表中 + items.add(itemId); + + // 如果是文件夹,递归获取其下的子项 + if (itemType == Notes.TYPE_FOLDER) { + getRecycleBinFolderItems(itemId, items); + } + } while (cursor.moveToNext()); + } + cursor.close(); + } } /** @@ -550,17 +1176,22 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt return; } - HashSet ids = new HashSet(); - ids.add(folderId); - HashSet widgets = DataUtils.getFolderNoteWidget(mContentResolver, - folderId); - if (!isSyncMode()) { - // 非同步模式下,直接删除文件夹 - DataUtils.batchDeleteNotes(mContentResolver, ids); - } else { - // 同步模式下,将删除的文件夹移动到垃圾箱文件夹 - DataUtils.batchMoveToFolder(mContentResolver, ids, Notes.ID_TRASH_FOLER); - } + // 获取文件夹下的所有子项(包括便签和子文件夹) + HashSet items = DataUtils.getFolderItems(mContentResolver, folderId); + // 将文件夹本身也添加到要删除的列表中 + items.add(folderId); + + // 保存原始父文件夹ID并将文件夹及其所有子项移动到回收站 + // 对于文件夹本身,将其PARENT_ID设置为回收站ID + // 对于文件夹内的便签,将其PARENT_ID设置为回收站ID,ORIGIN_PARENT_ID设置为原始父文件夹ID + // 这样在回收站中打开文件夹时,我们可以通过ORIGIN_PARENT_ID来查询该文件夹下的所有便签和文件夹 + + HashSet widgets = DataUtils.getFolderNoteWidget(mContentResolver, folderId); + + // 将文件夹及其所有子项一起移动到回收站 + // 这样所有项目的PARENT_ID都会被设置为回收站ID,ORIGIN_PARENT_ID会被设置为原始父文件夹ID + DataUtils.batchMoveToFolder(mContentResolver, items, Notes.ID_TRASH_FOLER); + if (widgets != null) { for (AppWidgetAttribute widget : widgets) { if (widget.widgetId != AppWidgetManager.INVALID_APPWIDGET_ID @@ -590,6 +1221,10 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt if (data.getId() == Notes.ID_CALL_RECORD_FOLDER) { mState = ListEditState.CALL_RECORD_FOLDER; mAddNewNote.setVisibility(View.GONE); // 通话记录文件夹不允许新建笔记 + } else if (mCurrentFolderId == Notes.ID_TRASH_FOLER || data.getParentId() == Notes.ID_TRASH_FOLER) { + // 如果是回收站中的文件夹,设置状态为TRASH_FOLDER + mState = ListEditState.TRASH_FOLDER; + mAddNewNote.setVisibility(View.GONE); // 回收站中不允许新建笔记 } else { mState = ListEditState.SUB_FOLDER; } @@ -724,15 +1359,37 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt }); } - @Override - public void onBackPressed() { + /** + * 处理返回键按下事件 + * 统一处理不同状态下的返回键逻辑,适用于传统onBackPressed和Android 13+的OnBackInvokedDispatcher + */ + private void handleBackPress() { switch (mState) { case SUB_FOLDER: - // 返回根文件夹列表 + // 从普通子文件夹返回,回到根目录 mCurrentFolderId = Notes.ID_ROOT_FOLDER; mState = ListEditState.NOTE_LIST; - startAsyncNotesListQuery(); + mAddNewNote.setVisibility(View.VISIBLE); // 显示新建笔记按钮 mTitleBar.setVisibility(View.GONE); // 隐藏标题栏 + startAsyncNotesListQuery(); + break; + case TRASH_FOLDER: + // 检查当前是否已经在回收站根目录 + if (mCurrentFolderId == Notes.ID_TRASH_FOLER) { + // 从回收站根目录返回,回到主界面 + mCurrentFolderId = Notes.ID_ROOT_FOLDER; + mState = ListEditState.NOTE_LIST; + mAddNewNote.setVisibility(View.VISIBLE); // 显示新建笔记按钮 + mTitleBar.setVisibility(View.GONE); // 隐藏标题栏 + startAsyncNotesListQuery(); + } else { + // 从回收站中的文件夹返回,回到回收站根目录 + mCurrentFolderId = Notes.ID_TRASH_FOLER; + mState = ListEditState.TRASH_FOLDER; + mTitleBar.setText(R.string.menu_rubbish_bin); // 设置标题栏为回收站 + mAddNewNote.setVisibility(View.GONE); // 回收站中不允许新建笔记 + startAsyncNotesListQuery(); + } break; case CALL_RECORD_FOLDER: // 返回根文件夹列表 @@ -743,13 +1400,17 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt startAsyncNotesListQuery(); break; case NOTE_LIST: - super.onBackPressed(); // 退出应用 + // 在NOTE_LIST状态下,调用父类的onBackPressed()退出应用 + NotesListActivity.super.onBackPressed(); break; default: break; } } - + @Override + public void onBackPressed() { + handleBackPress(); + } /** * 更新小部件 */ @@ -773,6 +1434,41 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt setResult(RESULT_OK, intent); } + // 上下文菜单项ID - 便签移动 + private static final int MENU_NOTE_MOVE = 8; + // 上下文菜单项ID - 便签多选 + private static final int MENU_NOTE_MULTI_SELECT = 9; + + /** + * 便签上下文菜单创建监听器 + */ + private final OnCreateContextMenuListener mNoteOnCreateContextMenuListener = new OnCreateContextMenuListener() { + public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) { + if (mFocusNoteDataItem != null) { + menu.setHeaderTitle(mFocusNoteDataItem.getSnippet()); // 设置菜单标题为便签内容 + + // 检查当前是否在回收站中 + if (mCurrentFolderId == Notes.ID_TRASH_FOLER) { + // 在回收站中,显示恢复、彻底删除和多选选项 + menu.add(0, MENU_RESTORE, 0, R.string.menu_restore); // 恢复笔记 + menu.add(0, MENU_PERMANENT_DELETE, 0, R.string.menu_permanent_delete); // 彻底删除 + menu.add(0, MENU_NOTE_MULTI_SELECT, 0, R.string.menu_multi_select); // 多选 + } else { + // 不在回收站中,显示普通便签操作选项 + menu.add(0, MENU_NOTE_EDIT_TOPIC, 0, R.string.menu_edit_topic); // 编辑标题 + menu.add(0, MENU_NOTE_MOVE, 0, R.string.menu_move); // 移动到文件夹 + menu.add(0, MENU_NOTE_MULTI_SELECT, 0, R.string.menu_multi_select); // 多选 + // 根据便签是否已置顶添加相应的菜单选项 + if (mFocusNoteDataItem.isPinned()) { + menu.add(0, MENU_NOTE_UNPIN, 0, R.string.menu_unpin_note); // 取消置顶 + } else { + menu.add(0, MENU_NOTE_PIN, 0, R.string.menu_pin_note); // 置顶 + } + } + } + } + }; + /** * 文件夹上下文菜单创建监听器 */ @@ -780,21 +1476,51 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) { if (mFocusNoteDataItem != null) { menu.setHeaderTitle(mFocusNoteDataItem.getSnippet()); // 设置菜单标题为文件夹名称 - menu.add(0, MENU_FOLDER_VIEW, 0, R.string.menu_folder_view); // 查看文件夹 - menu.add(0, MENU_FOLDER_DELETE, 0, R.string.menu_folder_delete); // 删除文件夹 - menu.add(0, MENU_FOLDER_CHANGE_NAME, 0, R.string.menu_folder_change_name); // 修改文件夹名称 + + // 检查当前是否在回收站中 + if (mCurrentFolderId == Notes.ID_TRASH_FOLER) { + // 在回收站中,显示恢复和彻底删除选项 + if (mFocusNoteDataItem.getType() == Notes.TYPE_FOLDER) { + menu.add(0, MENU_RESTORE, 0, R.string.menu_restore_folder); // 恢复文件夹 + } else { + menu.add(0, MENU_RESTORE, 0, R.string.menu_restore); // 恢复笔记 + } + menu.add(0, MENU_PERMANENT_DELETE, 0, R.string.menu_permanent_delete); // 彻底删除 + } else { + // 不在回收站中,显示普通文件夹操作选项 + menu.add(0, MENU_FOLDER_VIEW, 0, R.string.menu_folder_view); // 查看文件夹 + menu.add(0, MENU_FOLDER_DELETE, 0, R.string.menu_folder_delete); // 删除文件夹 + menu.add(0, MENU_FOLDER_CHANGE_NAME, 0, R.string.menu_folder_change_name); // 修改文件夹名称 + // 根据文件夹是否已置顶添加相应的菜单选项 + if (mFocusNoteDataItem.isPinned()) { + menu.add(0, MENU_FOLDER_UNPIN, 0, R.string.menu_unpin_note); // 取消置顶 + } else { + menu.add(0, MENU_FOLDER_PIN, 0, R.string.menu_pin_note); // 置顶 + } + } } } }; @Override public void onContextMenuClosed(Menu menu) { - if (mNotesListView != null) { - mNotesListView.setOnCreateContextMenuListener(null); // 清除上下文菜单监听器 - } + // 不要移除上下文菜单监听器,否则后续长按将无法显示上下文菜单 super.onContextMenuClosed(menu); } + @Override + public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) { + super.onCreateContextMenu(menu, v, menuInfo); + // 根据当前焦点项目类型设置相应的上下文菜单 + if (mFocusNoteDataItem != null) { + if (mFocusNoteDataItem.getType() == Notes.TYPE_NOTE) { + mNoteOnCreateContextMenuListener.onCreateContextMenu(menu, v, menuInfo); + } else if (mFocusNoteDataItem.getType() == Notes.TYPE_FOLDER) { + mFolderOnCreateContextMenuListener.onCreateContextMenu(menu, v, menuInfo); + } + } + } + @Override public boolean onContextItemSelected(MenuItem item) { if (mFocusNoteDataItem == null) { @@ -823,6 +1549,119 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt case MENU_FOLDER_CHANGE_NAME: showCreateOrModifyFolderDialog(false); // 修改文件夹名称 break; + case MENU_FOLDER_PIN: + // 置顶文件夹 + setFolderPinnedStatus(mFocusNoteDataItem.getId(), true); + break; + case MENU_FOLDER_UNPIN: + // 取消置顶文件夹 + setFolderPinnedStatus(mFocusNoteDataItem.getId(), false); + break; + case MENU_NOTE_EDIT_TOPIC: + // 编辑便签标题 + showEditNoteTopicDialog(); + break; + case MENU_NOTE_MOVE: + // 移动便签到其他文件夹 + // 查询目标文件夹列表 + mBackgroundQueryHandler.startQuery(FOLDER_LIST_QUERY_TOKEN, + null, + Notes.CONTENT_NOTE_URI, + FoldersListAdapter.PROJECTION, + "(" + NoteColumns.TYPE + "=? AND " + NoteColumns.PARENT_ID + "<>? AND " + NoteColumns.ID + "<>?) OR (" + NoteColumns.ID + "=" + Notes.ID_ROOT_FOLDER + ")", + new String[] { + String.valueOf(Notes.TYPE_FOLDER), + String.valueOf(Notes.ID_TRASH_FOLER), + String.valueOf(mCurrentFolderId) + }, + NoteColumns.MODIFIED_DATE + " DESC"); + break; + case MENU_NOTE_MULTI_SELECT: + // 进入多选模式 + if (mFocusNoteDataItem != null) { + // 1. 先确保 mModeCallBack 不为 null + if (mModeCallBack == null) { + mModeCallBack = new ModeCallback(); + } + + // 2. 先设置监听器,再设置选择模式 + mNotesListView.setMultiChoiceModeListener(mModeCallBack); + + // 3. 进入选择模式 + mNotesListAdapter.setChoiceMode(true); + + // 4. 查找当前便签在列表中的位置 + Cursor cursor = mNotesListAdapter.getCursor(); + if (cursor != null) { + long currentNoteId = mFocusNoteDataItem.getId(); + int position = 0; + while (cursor.moveToNext()) { + long noteId = cursor.getLong(cursor.getColumnIndex(NoteColumns.ID)); + if (noteId == currentNoteId) { + // 5. 设置当前便签为选中状态 + mNotesListView.setItemChecked(position, true); + break; + } + position++; + } + } + + // 6. 手动启动 ActionMode,而不是通过长按触发 + mNotesListView.startActionMode(mModeCallBack); + } + break; + case MENU_NOTE_PIN: + // 置顶便签 + setNotePinnedStatus(mFocusNoteDataItem.getId(), true); + break; + case MENU_NOTE_UNPIN: + // 取消置顶便签 + setNotePinnedStatus(mFocusNoteDataItem.getId(), false); + break; + case MENU_RESTORE: + // 根据类型显示不同的恢复对话框 + AlertDialog.Builder restoreBuilder = new AlertDialog.Builder(this); + restoreBuilder.setTitle(getString(R.string.menu_restore_from_trash)); + restoreBuilder.setIcon(android.R.drawable.ic_dialog_alert); + + // 根据当前选中的是文件夹还是便签,显示不同的对话框消息和执行不同的恢复操作 + if (mFocusNoteDataItem.getType() == Notes.TYPE_FOLDER) { + // 恢复文件夹 + restoreBuilder.setMessage(getString(R.string.alert_message_restore_folder)); + restoreBuilder.setPositiveButton(android.R.string.ok, + new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int which) { + restoreFolder(mFocusNoteDataItem.getId()); // 执行文件夹恢复 + } + }); + } else { + // 恢复便签 + restoreBuilder.setMessage(getString(R.string.alert_message_restore_notes, 1)); + restoreBuilder.setPositiveButton(android.R.string.ok, + new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int which) { + restoreNote(mFocusNoteDataItem.getId()); // 执行便签恢复 + } + }); + } + restoreBuilder.setNegativeButton(android.R.string.cancel, null); + restoreBuilder.show(); + break; + case MENU_PERMANENT_DELETE: + // 彻底删除文件夹确认对话框 + AlertDialog.Builder permanentDeleteBuilder = new AlertDialog.Builder(this); + permanentDeleteBuilder.setTitle(getString(R.string.menu_permanent_delete)); + permanentDeleteBuilder.setIcon(android.R.drawable.ic_dialog_alert); + permanentDeleteBuilder.setMessage(getString(R.string.alert_message_permanent_delete_folder)); + permanentDeleteBuilder.setPositiveButton(android.R.string.ok, + new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int which) { + permanentDeleteFolder(mFocusNoteDataItem.getId()); // 执行彻底删除 + } + }); + permanentDeleteBuilder.setNegativeButton(android.R.string.cancel, null); + permanentDeleteBuilder.show(); + break; default: break; } @@ -839,7 +1678,7 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt // 根据同步状态设置菜单项标题 menu.findItem(R.id.menu_sync).setTitle( GTaskSyncService.isSyncing() ? R.string.menu_sync_cancel : R.string.menu_sync); - } else if (mState == ListEditState.SUB_FOLDER) { + } else if (mState == ListEditState.SUB_FOLDER || mState == ListEditState.TRASH_FOLDER) { getMenuInflater().inflate(R.menu.sub_folder, menu); } else if (mState == ListEditState.CALL_RECORD_FOLDER) { getMenuInflater().inflate(R.menu.call_record_folder, menu); @@ -884,6 +1723,9 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt case R.id.menu_search: onSearchRequested(); // 触发搜索 break; + case R.id.menu_rubbish_bin: + openRubbishBin(); // 打开回收站 + break; default: break; } @@ -949,6 +1791,18 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt return NotesPreferenceActivity.getSyncAccountName(this).trim().length() > 0; } + /** + * 打开回收站 + */ + private void openRubbishBin() { + mCurrentFolderId = Notes.ID_TRASH_FOLER; // 设置当前文件夹为回收站 + mState = ListEditState.TRASH_FOLDER; // 设置状态为回收站文件夹 + mTitleBar.setText(R.string.menu_rubbish_bin); // 设置标题栏为回收站 + mTitleBar.setVisibility(View.VISIBLE); // 显示标题栏 + mAddNewNote.setVisibility(View.GONE); // 回收站中不允许新建笔记 + startAsyncNotesListQuery(); // 启动异步查询,显示回收站内容 + } + /** * 启动设置页面 */ @@ -996,6 +1850,16 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt Log.e(TAG, "Wrong note type in SUB_FOLDER"); } break; + case TRASH_FOLDER: + // 回收站中,处理便签和文件夹点击 + if (item.getType() == Notes.TYPE_NOTE) { + openNode(item); // 打开笔记 + } else if (item.getType() == Notes.TYPE_FOLDER) { + openFolder(item); // 打开文件夹 + } else { + Log.e(TAG, "Wrong note type in TRASH_FOLDER"); + } + break; default: break; } @@ -1028,18 +1892,24 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt public boolean onItemLongClick(AdapterView parent, View view, int position, long id) { if (view instanceof NotesListItem) { - mFocusNoteDataItem = ((NotesListItem) view).getItemData(); - if (mFocusNoteDataItem.getType() == Notes.TYPE_NOTE && !mNotesListAdapter.isInChoiceMode()) { - // 长按笔记项进入多选模式 - if (mNotesListView.startActionMode(mModeCallBack) != null) { - mModeCallBack.onItemCheckedStateChanged(null, position, id, true); - mNotesListView.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS); // 触觉反馈 - } else { - Log.e(TAG, "startActionMode fails"); + NoteItemData itemData = ((NotesListItem) view).getItemData(); + if (itemData != null) { + // 无论是文件夹还是便签,长按都显示上下文菜单 + mFocusNoteDataItem = itemData; + + // 确保退出多选模式 + if (mModeCallBack != null && mModeCallBack.mActionMode != null) { + mModeCallBack.mActionMode.finish(); } - } else if (mFocusNoteDataItem.getType() == Notes.TYPE_FOLDER) { - // 长按文件夹项显示上下文菜单 - mNotesListView.setOnCreateContextMenuListener(mFolderOnCreateContextMenuListener); + + // 重置列表的选择模式,确保长按总是触发上下文菜单 + mNotesListView.setChoiceMode(ListView.CHOICE_MODE_NONE); + mNotesListAdapter.setChoiceMode(false); + mNotesListView.clearChoices(); + mNotesListView.setMultiChoiceModeListener(null); + mNotesListAdapter.notifyDataSetChanged(); + + return false; } } return false; diff --git a/src/ui/NotesListAdapter.java b/src/ui/NotesListAdapter.java index 74cbece..6bf02f7 100644 --- a/src/ui/NotesListAdapter.java +++ b/src/ui/NotesListAdapter.java @@ -113,6 +113,7 @@ public class NotesListAdapter extends CursorAdapter { public void setChoiceMode(boolean mode) { mSelectedIndex.clear(); // 清除之前的选中状态 mChoiceMode = mode; + notifyDataSetChanged(); // 通知数据集变化,清除对勾标记 } /** @@ -151,6 +152,14 @@ public class NotesListAdapter extends CursorAdapter { return itemSet; } + + /** + * 获取选中位置的映射表 + * @return 选中位置的映射表 + */ + public HashMap getSelectedIndex() { + return mSelectedIndex; + } /** * 获取所有选中的小部件属性集合 diff --git a/src/ui/NotesListItem.java b/src/ui/NotesListItem.java index 18a6a54..28f29e4 100644 --- a/src/ui/NotesListItem.java +++ b/src/ui/NotesListItem.java @@ -108,17 +108,26 @@ public class NotesListItem extends LinearLayout { mTitle.setText(data.getSnippet() // 设置文件夹名称 + context.getString(R.string.format_folder_files_count, data.getNotesCount())); // 添加笔记数量 - mAlert.setVisibility(View.GONE); // 文件夹不显示提醒图标 - } else { - // 普通笔记类型 - mTitle.setText(DataUtils.getFormattedSnippet(data.getSnippet())); // 设置笔记内容片段 - // 根据是否有提醒设置提醒图标 - if (data.hasAlert()) { - mAlert.setImageResource(R.drawable.clock); // 有时钟图标 + // 根据是否置顶设置置顶图标 + if (data.isPinned()) { + mAlert.setImageResource(R.drawable.arrow_up); // 置顶标记 mAlert.setVisibility(View.VISIBLE); } else { mAlert.setVisibility(View.GONE); } + } else { + // 普通笔记类型 + mTitle.setText(DataUtils.getFormattedSnippet(data.getDisplayTitle())); // 设置笔记标题,优先使用标题字段 + // 根据是否置顶或有提醒设置图标 + if (data.isPinned()) { + mAlert.setImageResource(R.drawable.arrow_up); // 置顶标记 + mAlert.setVisibility(View.VISIBLE); + } else if (data.hasAlert()) { + mAlert.setImageResource(R.drawable.clock); // 有时钟图标 + mAlert.setVisibility(View.VISIBLE); + } else { + mAlert.setVisibility(View.GONE); + } } } // 设置相对时间(如"5分钟前"、"昨天"等格式)