/* * Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net) * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package net.micode.notes.ui; import android.app.Activity; import android.app.AlertDialog; 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; import android.content.Intent; import android.content.SharedPreferences; import android.database.Cursor; import android.net.Uri; import android.os.AsyncTask; import android.os.Bundle; import android.preference.PreferenceManager; import android.text.Editable; import android.text.TextUtils; import android.text.TextWatcher; import android.util.Log; import android.view.ActionMode; import android.view.ContextMenu; import android.view.ContextMenu.ContextMenuInfo; import android.view.Display; import android.view.HapticFeedbackConstants; import android.view.LayoutInflater; import android.view.Menu; import android.view.MenuItem; import android.view.MenuItem.OnMenuItemClickListener; import android.view.MotionEvent; import android.view.View; import android.view.View.OnClickListener; import android.view.View.OnCreateContextMenuListener; import android.view.View.OnTouchListener; import android.view.Window; import android.view.WindowManager; import android.view.Gravity; import android.view.inputmethod.InputMethodManager; import android.widget.AdapterView; import android.widget.AdapterView.OnItemClickListener; import android.widget.AdapterView.OnItemLongClickListener; import android.widget.Button; import android.widget.EditText; import android.widget.LinearLayout; import android.widget.ListView; import android.widget.PopupMenu; import android.widget.RadioGroup; import android.widget.TextView; import android.widget.Toast; import android.graphics.Color; import android.graphics.drawable.ColorDrawable; 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; import net.micode.notes.tool.DataUtils; import net.micode.notes.tool.ResourceParser; import net.micode.notes.ui.NotesListAdapter.AppWidgetAttribute; import net.micode.notes.widget.NoteWidgetProvider_2x; import net.micode.notes.widget.NoteWidgetProvider_4x; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.util.HashSet; /** * 笔记列表活动 * 笔记应用的主界面,显示笔记和文件夹列表,提供笔记的浏览、管理、搜索和设置等功能 * 支持多选操作、文件夹管理、导入导出等高级功能 */ public class NotesListActivity extends Activity implements OnClickListener, OnItemLongClickListener { // 异步查询标记 private static final int FOLDER_NOTE_LIST_QUERY_TOKEN = 0; // 文件夹笔记列表查询标记 private static final int FOLDER_LIST_QUERY_TOKEN = 1; // 文件夹列表查询标记 // 上下文菜单项ID 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 int MENU_NOTE_DELETE = 102; // 删除便签 // 首选项键名 private static final String PREFERENCE_ADD_INTRODUCTION = "net.micode.notes.introduction"; /** * 列表编辑状态枚举 */ private enum ListEditState { NOTE_LIST, // 普通笔记列表 SUB_FOLDER, // 子文件夹视图 CALL_RECORD_FOLDER, // 通话记录文件夹 TRASH_FOLDER // 回收站文件夹 }; private ListEditState mState; // 当前列表状态 private BackgroundQueryHandler mBackgroundQueryHandler; // 后台查询处理器 private NotesListAdapter mNotesListAdapter; // 笔记列表适配器 private ListView mNotesListView; // 笔记列表视图 private Button mAddNewNote; // 新建笔记按钮 private boolean mDispatch; // 是否分发触摸事件标志 private int mOriginY; // 原始Y坐标 private int mDispatchY; // 分发Y坐标 private LinearLayout mTitleBarLayout; // 标题栏布局 private TextView mTitleBar; // 标题栏文本 private Button mStudyTimerButton; // 学习计时器按钮 private long mCurrentFolderId; // 当前文件夹ID private ContentResolver mContentResolver; // 内容解析器 private ModeCallback mModeCallBack; // 多选模式回调 private static final String TAG = "NotesListActivity"; // 日志标签 public static final int NOTES_LISTVIEW_SCROLL_RATE = 30; // 列表滚动速率 private NoteItemData mFocusNoteDataItem; // 当前焦点笔记项数据 // 查询条件 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 (" + 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; // 打开笔记 private final static int REQUEST_CODE_NEW_NODE = 103; // 新建笔记 @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.note_list); // 设置列表布局 // 初始化资源和视图 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 protected void onActivityResult(int requestCode, int resultCode, Intent data) { if (resultCode == RESULT_OK && (requestCode == REQUEST_CODE_OPEN_NODE || requestCode == REQUEST_CODE_NEW_NODE)) { // 笔记操作后刷新列表 mNotesListAdapter.changeCursor(null); // 接收并处理从 NoteEditActivity 返回的计时器状态 if (data != null) { boolean isTimerRunning = data.getBooleanExtra("is_timer_running", false); long timerCurrentTime = data.getLongExtra("timer_current_time", 0); boolean isCountdownMode = data.getBooleanExtra("is_countdown_mode", false); long timerDuration = data.getLongExtra("timer_duration", 0); // 更新计时器状态 mIsTimerRunning = isTimerRunning; mTimerCurrentTime = timerCurrentTime; mIsCountdownMode = isCountdownMode; mTimerDuration = timerDuration; // 如果计时器已经停止,关闭对话框并刷新列表 if (!mIsTimerRunning) { // 关闭对话框 if (mTimerDialog != null && mTimerDialog.isShowing()) { mTimerDialog.dismiss(); mTimerDialog = null; } // 刷新文件夹显示,更新专注时长 startAsyncNotesListQuery(); } } } else { super.onActivityResult(requestCode, resultCode, data); } } /** * 从资源文件读取应用介绍信息并插入到笔记中 */ private void setAppInfoFromRawRes() { SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(this); // 检查是否已经添加过介绍 if (!sp.getBoolean(PREFERENCE_ADD_INTRODUCTION, false)) { StringBuilder sb = new StringBuilder(); InputStream in = null; try { in = getResources().openRawResource(R.raw.introduction); if (in != null) { InputStreamReader isr = new InputStreamReader(in); BufferedReader br = new BufferedReader(isr); char [] buf = new char[1024]; int len = 0; while ((len = br.read(buf)) > 0) { sb.append(buf, 0, len); } } else { Log.e(TAG, "Read introduction file error"); return; } } catch (IOException e) { e.printStackTrace(); return; } finally { if(in != null) { try { in.close(); } catch (IOException e) { e.printStackTrace(); } } } // 创建介绍笔记 WorkingNote note = WorkingNote.createEmptyNote(this, Notes.ID_ROOT_FOLDER, AppWidgetManager.INVALID_APPWIDGET_ID, Notes.TYPE_WIDGET_INVALIDE, ResourceParser.RED); note.setWorkingText(sb.toString()); if (note.saveNote()) { sp.edit().putBoolean(PREFERENCE_ADD_INTRODUCTION, true).commit(); } else { Log.e(TAG, "Save introduction note error"); return; } } } @Override protected void onStart() { super.onStart(); startAsyncNotesListQuery(); // 启动异步笔记列表查询 } /** * 初始化资源和视图 */ private void initResources() { mContentResolver = this.getContentResolver(); mBackgroundQueryHandler = new BackgroundQueryHandler(this.getContentResolver()); mCurrentFolderId = Notes.ID_ROOT_FOLDER; // 默认根文件夹 mNotesListView = (ListView) findViewById(R.id.notes_list); // 添加列表脚部视图 mNotesListView.addFooterView(LayoutInflater.from(this).inflate(R.layout.note_list_footer, null), null, false); mNotesListView.setOnItemClickListener(new OnListItemClickListener()); // 设置列表项点击监听 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()); // 设置新建笔记按钮触摸监听 mDispatch = false; mDispatchY = 0; mOriginY = 0; mTitleBarLayout = (LinearLayout) findViewById(R.id.title_bar); mTitleBar = (TextView) findViewById(R.id.tv_title_bar); mStudyTimerButton = (Button) findViewById(R.id.btn_study_timer); mStudyTimerButton.setOnClickListener(this); // 设置时钟按钮点击监听器 mState = ListEditState.NOTE_LIST; // 初始状态为普通笔记列表 mModeCallBack = new ModeCallback(); // 创建多选模式回调 } /** * 多选模式回调类 * 实现列表的多选操作功能 */ private class ModeCallback implements ListView.MultiChoiceModeListener, OnMenuItemClickListener { 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) { 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 { // 普通文件夹中显示菜单 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); // 启用选择模式 // 注意:不再禁用长按,以便在多选模式下也能通过长按显示上下文菜单 mAddNewNote.setVisibility(View.GONE); // 隐藏新建笔记按钮 // 设置自定义视图(下拉菜单) View customView = LayoutInflater.from(NotesListActivity.this).inflate( R.layout.note_list_dropdown_menu, null); mode.setCustomView(customView); mDropDownMenu = new DropdownMenu(NotesListActivity.this, (Button) customView.findViewById(R.id.selection_menu), R.menu.note_list_dropdown); mDropDownMenu.setOnDropdownMenuItemClickListener(new PopupMenu.OnMenuItemClickListener(){ public boolean onMenuItemClick(MenuItem item) { // 全选/取消全选 mNotesListAdapter.selectAll(!mNotesListAdapter.isAllSelected()); updateMenu(); // 更新菜单 return true; } }); return true; } /** * 更新菜单状态 */ private void updateMenu() { int selectedCount = mNotesListAdapter.getSelectedCount(); // 更新下拉菜单标题 String format = getResources().getString(R.string.menu_select_title, selectedCount); mDropDownMenu.setTitle(format); MenuItem item = mDropDownMenu.findItem(R.id.action_select_all); if (item != null) { if (mNotesListAdapter.isAllSelected()) { item.setChecked(true); item.setTitle(R.string.menu_deselect_all); // 全选时显示"取消全选" } else { item.setChecked(false); 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) { // 根据选中的便签数量动态显示/隐藏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) { // TODO Auto-generated method stub return false; } public void onDestroyActionMode(ActionMode mode) { // 退出多选模式时的清理工作 mNotesListAdapter.setChoiceMode(false); // 禁用选择模式 mNotesListView.setLongClickable(true); // 启用长按 mAddNewNote.setVisibility(View.VISIBLE); // 显示新建笔记按钮 mActionMode = null; mNotesListView.clearChoices(); } public void finishActionMode() { if (mActionMode != null) { mActionMode.finish(); // 结束操作模式,防止空指针异常 } } 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) { if (mNotesListAdapter.getSelectedCount() == 0) { Toast.makeText(NotesListActivity.this, getString(R.string.menu_select_none), Toast.LENGTH_SHORT).show(); // 未选择任何项时提示 return true; } switch (item.getItemId()) { case R.id.delete: // 批量删除确认对话框 AlertDialog.Builder builder = new AlertDialog.Builder(NotesListActivity.this); builder.setTitle(getString(R.string.alert_title_delete)); builder.setIcon(android.R.drawable.ic_dialog_alert); builder.setMessage(getString(R.string.alert_message_delete_notes, mNotesListAdapter.getSelectedCount())); builder.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int which) { batchDelete(); // 执行批量删除 } }); builder.setNegativeButton(android.R.string.cancel, null); 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; } return true; } } /** * 新建笔记按钮的触摸监听器 * 处理透明区域的事件分发 */ private class NewNoteOnTouchListener implements OnTouchListener { public boolean onTouch(View v, MotionEvent event) { switch (event.getAction()) { case MotionEvent.ACTION_DOWN: { Display display = getWindowManager().getDefaultDisplay(); int screenHeight = display.getHeight(); int newNoteViewHeight = mAddNewNote.getHeight(); int start = screenHeight - newNoteViewHeight; int eventY = start + (int) event.getY(); /** * 减去标题栏的高度 */ if (mState == ListEditState.SUB_FOLDER) { eventY -= mTitleBar.getHeight(); start -= mTitleBar.getHeight(); } /** * HACKME:当点击"新建笔记"按钮的透明部分时,将事件分发给列表视图。 * 透明部分的公式为 y=-0.12x+94(单位:像素)和按钮的顶部线条。 * 坐标基于"新建笔记"按钮的左侧。 * 94表示透明部分的最大高度。 * 注意:如果按钮背景改变,公式也应改变。 * 这只是为了满足UI设计师的强烈要求。 */ if (event.getY() < (event.getX() * (-0.12) + 94)) { View view = mNotesListView.getChildAt(mNotesListView.getChildCount() - 1 - mNotesListView.getFooterViewsCount()); if (view != null && view.getBottom() > start && (view.getTop() < (start + 94))) { mOriginY = (int) event.getY(); mDispatchY = eventY; event.setLocation(event.getX(), mDispatchY); mDispatch = true; return mNotesListView.dispatchTouchEvent(event); // 分发触摸事件 } } break; } case MotionEvent.ACTION_MOVE: { if (mDispatch) { mDispatchY += (int) event.getY() - mOriginY; event.setLocation(event.getX(), mDispatchY); return mNotesListView.dispatchTouchEvent(event); } break; } default: { if (mDispatch) { event.setLocation(event.getX(), mDispatchY); mDispatch = false; return mNotesListView.dispatchTouchEvent(event); } break; } } return false; } }; /** * 启动异步笔记列表查询 */ private void startAsyncNotesListQuery() { 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"); } /** * 后台查询处理器 */ private final class BackgroundQueryHandler extends AsyncQueryHandler { public BackgroundQueryHandler(ContentResolver contentResolver) { super(contentResolver); } @Override protected void onQueryComplete(int token, Object cookie, Cursor cursor) { switch (token) { case FOLDER_NOTE_LIST_QUERY_TOKEN: mNotesListAdapter.changeCursor(cursor); // 更新笔记列表适配器 break; case FOLDER_LIST_QUERY_TOKEN: if (cursor != null && cursor.getCount() > 0) { showFolderListMenu(cursor); // 显示文件夹列表菜单 } else { Log.e(TAG, "Query folder failed"); } break; default: return; } } } /** * 显示文件夹列表菜单(用于移动笔记) */ 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) { // 获取目标文件夹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(); } /** * 创建新笔记 */ private void createNewNote() { Intent intent = new Intent(this, NoteEditActivity.class); intent.setAction(Intent.ACTION_INSERT_OR_EDIT); intent.putExtra(Notes.INTENT_EXTRA_FOLDER_ID, mCurrentFolderId); // 传递计时器状态 intent.putExtra("is_timer_running", mIsTimerRunning); intent.putExtra("timer_current_time", mTimerCurrentTime); intent.putExtra("is_countdown_mode", mIsCountdownMode); intent.putExtra("timer_duration", mTimerDuration); this.startActivityForResult(intent, REQUEST_CODE_NEW_NODE); } /** * 批量删除笔记 */ 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(); // 将选中的笔记从回收站恢复 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 { // 在回收站根目录恢复 // 查询笔记的原始父文件夹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); } } 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; } @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(); } /** * 获取笔记的原始父文件夹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)) { // 由于NoteItemData中已经处理过HTML标签,这里不需要再处理 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(); } } /** * 删除文件夹 */ private void deleteFolder(long folderId) { if (folderId == Notes.ID_ROOT_FOLDER) { Log.e(TAG, "Wrong folder id, should not happen " + folderId); return; } // 获取文件夹下的所有子项(包括便签和子文件夹) 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 && widget.widgetType != Notes.TYPE_WIDGET_INVALIDE) { updateWidget(widget.widgetId, widget.widgetType); // 更新小部件 } } } } /** * 打开笔记(进入编辑界面) */ private void openNode(NoteItemData data) { Intent intent = new Intent(this, NoteEditActivity.class); intent.setAction(Intent.ACTION_VIEW); intent.putExtra(Intent.EXTRA_UID, data.getId()); // 传递计时器状态 intent.putExtra("is_timer_running", mIsTimerRunning); intent.putExtra("timer_current_time", mTimerCurrentTime); intent.putExtra("is_countdown_mode", mIsCountdownMode); intent.putExtra("timer_duration", mTimerDuration); this.startActivityForResult(intent, REQUEST_CODE_OPEN_NODE); } /** * 打开文件夹(进入文件夹视图) */ private void openFolder(NoteItemData data) { // 检查是否为隐私文件夹 if (data.isPrivate()) { // 显示密码验证对话框 showPasswordVerifyDialog(data); } else { // 普通文件夹,直接打开 openFolderInternal(data); } } /** * 打开文件夹的内部方法,不包含密码验证 */ private void openFolderInternal(NoteItemData data) { mCurrentFolderId = data.getId(); startAsyncNotesListQuery(); // 查询文件夹内容 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; } if (data.getId() == Notes.ID_CALL_RECORD_FOLDER) { mTitleBar.setText(R.string.call_record_folder_name); mStudyTimerButton.setVisibility(View.GONE); } else if (data.isStudy()) { // 学习文件夹,显示文件夹名称和专注时长 String folderName = data.getSnippet(); String focusDuration = data.getFormattedFocusDuration(); mTitleBar.setText(folderName + " (专注时间: " + focusDuration + ")"); mStudyTimerButton.setVisibility(View.VISIBLE); } else { mTitleBar.setText(data.getSnippet()); // 显示文件夹名称 mStudyTimerButton.setVisibility(View.GONE); } mTitleBarLayout.setVisibility(View.VISIBLE); // 显示标题栏布局 } /** * 显示密码验证对话框 */ private void showPasswordVerifyDialog(final NoteItemData folderData) { final AlertDialog.Builder builder = new AlertDialog.Builder(this); View view = LayoutInflater.from(this).inflate(R.layout.dialog_password_input, null); final EditText etPassword = (EditText) view.findViewById(R.id.et_password); builder.setTitle(getString(R.string.menu_enter_password)); builder.setMessage(getString(R.string.enter_password_to_open)); showSoftInput(); // 显示软键盘 builder.setPositiveButton(android.R.string.ok, null); builder.setNegativeButton(android.R.string.cancel, new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int which) { hideSoftInput(etPassword); // 隐藏软键盘 } }); final Dialog dialog = builder.setView(view).show(); final Button positive = (Button) dialog.findViewById(android.R.id.button1); positive.setOnClickListener(new OnClickListener() { public void onClick(View v) { hideSoftInput(etPassword); String password = etPassword.getText().toString(); // 获取存储的密码 SharedPreferences preferences = getSharedPreferences("notes_private_folders", MODE_PRIVATE); String storedPassword = preferences.getString("password_" + folderData.getId(), null); // 验证密码 if (storedPassword != null && storedPassword.equals(password)) { // 密码正确,打开文件夹 dialog.dismiss(); openFolderInternal(folderData); } else { // 密码错误 Toast.makeText(NotesListActivity.this, getString(R.string.password_incorrect), Toast.LENGTH_LONG).show(); etPassword.setSelection(0, etPassword.length()); } } }); // 初始时如果密码为空,禁用确定按钮 if (TextUtils.isEmpty(etPassword.getText())) { positive.setEnabled(false); } // 监听密码输入变化 etPassword.addTextChangedListener(new TextWatcher() { public void beforeTextChanged(CharSequence s, int start, int count, int after) {} public void onTextChanged(CharSequence s, int start, int before, int count) { // 只有当密码为6位数字时,才启用确定按钮 String password = s.toString(); positive.setEnabled(password.length() == 6 && password.matches("\\d{6}")); } public void afterTextChanged(Editable s) {} }); } /** * 显示删除隐私文件夹的密码验证对话框 */ private void showPasswordVerifyDialogForDelete(final NoteItemData folderData) { final AlertDialog.Builder builder = new AlertDialog.Builder(this); View view = LayoutInflater.from(this).inflate(R.layout.dialog_password_input, null); final EditText etPassword = (EditText) view.findViewById(R.id.et_password); builder.setTitle(getString(R.string.menu_enter_password)); builder.setMessage(getString(R.string.enter_password_to_delete)); showSoftInput(); // 显示软键盘 builder.setPositiveButton(android.R.string.ok, null); builder.setNegativeButton(android.R.string.cancel, new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int which) { hideSoftInput(etPassword); // 隐藏软键盘 } }); final Dialog dialog = builder.setView(view).show(); final Button positive = (Button) dialog.findViewById(android.R.id.button1); positive.setOnClickListener(new OnClickListener() { public void onClick(View v) { hideSoftInput(etPassword); String password = etPassword.getText().toString(); // 获取存储的密码 SharedPreferences preferences = getSharedPreferences("notes_private_folders", MODE_PRIVATE); String storedPassword = preferences.getString("password_" + folderData.getId(), null); // 验证密码 if (storedPassword != null && storedPassword.equals(password)) { // 密码正确,显示删除确认对话框 dialog.dismiss(); AlertDialog.Builder deleteBuilder = new AlertDialog.Builder(NotesListActivity.this); deleteBuilder.setTitle(getString(R.string.alert_title_delete)); deleteBuilder.setIcon(android.R.drawable.ic_dialog_alert); deleteBuilder.setMessage(getString(R.string.alert_message_delete_folder)); deleteBuilder.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() { public void onClick(DialogInterface deleteDialog, int which) { // 彻底删除隐私文件夹及其内容 permanentDeleteFolder(folderData.getId()); // 删除密码存储 SharedPreferences.Editor editor = preferences.edit(); editor.remove("password_" + folderData.getId()); editor.apply(); } }); deleteBuilder.setNegativeButton(android.R.string.cancel, null); deleteBuilder.show(); } else { // 密码错误 Toast.makeText(NotesListActivity.this, getString(R.string.password_incorrect), Toast.LENGTH_LONG).show(); etPassword.setSelection(0, etPassword.length()); } } }); // 初始时如果密码为空,禁用确定按钮 if (TextUtils.isEmpty(etPassword.getText())) { positive.setEnabled(false); } // 监听密码输入变化 etPassword.addTextChangedListener(new TextWatcher() { public void beforeTextChanged(CharSequence s, int start, int count, int after) {} public void onTextChanged(CharSequence s, int start, int before, int count) { // 只有当密码为6位数字时,才启用确定按钮 String password = s.toString(); positive.setEnabled(password.length() == 6 && password.matches("\\d{6}")); } public void afterTextChanged(Editable s) {} }); } public void onClick(View v) { switch (v.getId()) { case R.id.btn_new_note: createNewNote(); // 创建新笔记 break; case R.id.btn_study_timer: // 处理学习计时器按钮点击 if (mIsTimerRunning) { // 如果计时器正在运行,切换计时控制界面显示/隐藏 if (mTimerDialog != null && mTimerDialog.isShowing()) { // 如果对话框已显示,则隐藏 mTimerDialog.dismiss(); } else { // 如果对话框未显示,则显示 showTimerControlDialog(); } } else { // 如果计时器未运行,显示设置对话框 showStudyTimerDialog(); } break; default: break; } } /** * 显示软键盘 */ private void showSoftInput() { InputMethodManager inputMethodManager = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE); if (inputMethodManager != null) { inputMethodManager.toggleSoftInput(InputMethodManager.SHOW_FORCED, 0); } } /** * 隐藏软键盘 */ private void hideSoftInput(View view) { InputMethodManager inputMethodManager = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE); inputMethodManager.hideSoftInputFromWindow(view.getWindowToken(), 0); } /** * 显示创建或修改文件夹对话框 */ private void showCreateOrModifyFolderDialog(final boolean create, final boolean isPrivate, final boolean isStudy) { final AlertDialog.Builder builder = new AlertDialog.Builder(this); View view = LayoutInflater.from(this).inflate(R.layout.dialog_edit_text, null); final EditText etName = (EditText) view.findViewById(R.id.et_foler_name); showSoftInput(); // 显示软键盘 if (!create) { // 修改文件夹名称 if (mFocusNoteDataItem != null) { etName.setText(mFocusNoteDataItem.getSnippet()); builder.setTitle(getString(R.string.menu_folder_change_name)); } else { Log.e(TAG, "The long click data item is null"); return; } } else { // 创建新文件夹 etName.setText(""); if (isStudy) { builder.setTitle(R.string.menu_create_study_folder); } else { builder.setTitle(isPrivate ? getString(R.string.menu_create_private_folder) : getString(R.string.menu_create_folder)); } } builder.setPositiveButton(android.R.string.ok, null); builder.setNegativeButton(android.R.string.cancel, new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int which) { hideSoftInput(etName); // 隐藏软键盘 } }); final Dialog dialog = builder.setView(view).show(); final Button positive = (Button)dialog.findViewById(android.R.id.button1); positive.setOnClickListener(new OnClickListener() { public void onClick(View v) { hideSoftInput(etName); String name = etName.getText().toString(); // 检查文件夹名称是否已存在 if (DataUtils.checkVisibleFolderName(mContentResolver, name)) { Toast.makeText(NotesListActivity.this, getString(R.string.folder_exist, name), Toast.LENGTH_LONG).show(); etName.setSelection(0, etName.length()); // 选中所有文本 return; } if (!create) { // 修改文件夹名称 if (!TextUtils.isEmpty(name)) { ContentValues values = new ContentValues(); values.put(NoteColumns.SNIPPET, name); values.put(NoteColumns.TYPE, Notes.TYPE_FOLDER); values.put(NoteColumns.LOCAL_MODIFIED, 1); mContentResolver.update(Notes.CONTENT_NOTE_URI, values, NoteColumns.ID + "=?", new String[] { String.valueOf(mFocusNoteDataItem.getId()) }); } } else if (!TextUtils.isEmpty(name)) { // 创建新文件夹 ContentValues values = new ContentValues(); values.put(NoteColumns.SNIPPET, name); values.put(NoteColumns.TYPE, Notes.TYPE_FOLDER); values.put(NoteColumns.PRIVATE, isPrivate ? 1 : 0); values.put(NoteColumns.IS_STUDY, isStudy ? 1 : 0); values.put(NoteColumns.PINNED, isPrivate ? 1 : 0); // 隐私文件夹永远置顶 Uri uri = mContentResolver.insert(Notes.CONTENT_NOTE_URI, values); if (isPrivate && uri != null) { // 对于隐私文件夹,需要存储密码 long folderId = ContentUris.parseId(uri); showPasswordInputDialog(folderId); } } dialog.dismiss(); } }); // 初始时如果名称为空,禁用确定按钮 if (TextUtils.isEmpty(etName.getText())) { positive.setEnabled(false); } /** * 当名称编辑框为空时,禁用确定按钮 */ etName.addTextChangedListener(new TextWatcher() { public void beforeTextChanged(CharSequence s, int start, int count, int after) { // TODO Auto-generated method stub } public void onTextChanged(CharSequence s, int start, int before, int count) { // 根据输入内容启用/禁用确定按钮 if (TextUtils.isEmpty(etName.getText())) { positive.setEnabled(false); } else { positive.setEnabled(true); } } public void afterTextChanged(Editable s) { // TODO Auto-generated method stub } }); } /** * 显示密码输入对话框 * @param folderId 隐私文件夹ID */ private void showPasswordInputDialog(final long folderId) { final AlertDialog.Builder builder = new AlertDialog.Builder(this); View view = LayoutInflater.from(this).inflate(R.layout.dialog_password_input, null); final EditText etPassword = (EditText) view.findViewById(R.id.et_password); builder.setTitle(getString(R.string.menu_enter_password)); builder.setMessage(getString(R.string.password_hint)); showSoftInput(); // 显示软键盘 builder.setPositiveButton(android.R.string.ok, null); builder.setNegativeButton(android.R.string.cancel, new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int which) { hideSoftInput(etPassword); // 隐藏软键盘 // 如果取消密码设置,删除刚刚创建的隐私文件夹 mContentResolver.delete(Notes.CONTENT_NOTE_URI, NoteColumns.ID + "=?", new String[] { String.valueOf(folderId) }); startAsyncNotesListQuery(); } }); final Dialog dialog = builder.setView(view).show(); final Button positive = (Button) dialog.findViewById(android.R.id.button1); positive.setOnClickListener(new OnClickListener() { public void onClick(View v) { hideSoftInput(etPassword); String password = etPassword.getText().toString(); // 验证密码是否为6位数字 if (password.length() != 6 || !password.matches("\\d{6}")) { Toast.makeText(NotesListActivity.this, getString(R.string.password_invalid), Toast.LENGTH_LONG).show(); etPassword.setSelection(0, etPassword.length()); return; } // 存储密码(这里简单使用SharedPreferences,实际应用中应该加密存储) SharedPreferences preferences = getSharedPreferences("notes_private_folders", MODE_PRIVATE); SharedPreferences.Editor editor = preferences.edit(); editor.putString("password_" + folderId, password); editor.apply(); dialog.dismiss(); startAsyncNotesListQuery(); } }); // 初始时如果密码为空,禁用确定按钮 if (TextUtils.isEmpty(etPassword.getText())) { positive.setEnabled(false); } // 监听密码输入变化 etPassword.addTextChangedListener(new TextWatcher() { public void beforeTextChanged(CharSequence s, int start, int count, int after) {} public void onTextChanged(CharSequence s, int start, int before, int count) { // 只有当密码为6位数字时,才启用确定按钮 String password = s.toString(); positive.setEnabled(password.length() == 6 && password.matches("\\d{6}")); } public void afterTextChanged(Editable s) {} }); } /** * 处理返回键按下事件 * 统一处理不同状态下的返回键逻辑,适用于传统onBackPressed和Android 13+的OnBackInvokedDispatcher */ private void handleBackPress() { switch (mState) { case SUB_FOLDER: // 从普通子文件夹返回,回到根目录 mCurrentFolderId = Notes.ID_ROOT_FOLDER; mState = ListEditState.NOTE_LIST; mAddNewNote.setVisibility(View.VISIBLE); // 显示新建笔记按钮 mTitleBarLayout.setVisibility(View.GONE); // 隐藏标题栏布局 mStudyTimerButton.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); // 显示新建笔记按钮 mTitleBarLayout.setVisibility(View.GONE); // 隐藏标题栏布局 mStudyTimerButton.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); // 回收站中不允许新建笔记 mStudyTimerButton.setVisibility(View.GONE); // 隐藏学习计时器按钮 startAsyncNotesListQuery(); } break; case CALL_RECORD_FOLDER: // 返回根文件夹列表 mCurrentFolderId = Notes.ID_ROOT_FOLDER; mState = ListEditState.NOTE_LIST; mAddNewNote.setVisibility(View.VISIBLE); // 显示新建笔记按钮 mTitleBarLayout.setVisibility(View.GONE); // 隐藏标题栏布局 mStudyTimerButton.setVisibility(View.GONE); // 隐藏学习计时器按钮 startAsyncNotesListQuery(); break; case NOTE_LIST: // 在NOTE_LIST状态下,调用父类的onBackPressed()退出应用 NotesListActivity.super.onBackPressed(); break; default: break; } } @Override public void onBackPressed() { handleBackPress(); } @Override protected void onResume() { super.onResume(); } /** * 更新小部件 */ private void updateWidget(int appWidgetId, int appWidgetType) { Intent intent = new Intent(AppWidgetManager.ACTION_APPWIDGET_UPDATE); // 根据小部件类型设置目标类 if (appWidgetType == Notes.TYPE_WIDGET_2X) { intent.setClass(this, NoteWidgetProvider_2x.class); } else if (appWidgetType == Notes.TYPE_WIDGET_4X) { intent.setClass(this, NoteWidgetProvider_4x.class); } else { Log.e(TAG, "Unspported widget type"); return; } intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, new int[] { appWidgetId }); sendBroadcast(intent); 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_DELETE, 0, R.string.menu_delete); // 删除便签 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); // 置顶 } } } } }; /** * 文件夹上下文菜单创建监听器 */ private final OnCreateContextMenuListener mFolderOnCreateContextMenuListener = new OnCreateContextMenuListener() { public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) { if (mFocusNoteDataItem != null) { menu.setHeaderTitle(mFocusNoteDataItem.getSnippet()); // 设置菜单标题为文件夹名称 // 检查当前是否在回收站中 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) { // 不要移除上下文菜单监听器,否则后续长按将无法显示上下文菜单 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) { Log.e(TAG, "The long click data item is null"); return false; } switch (item.getItemId()) { case MENU_FOLDER_VIEW: openFolder(mFocusNoteDataItem); // 打开文件夹 break; case MENU_FOLDER_DELETE: // 删除文件夹确认对话框 if (mFocusNoteDataItem.isPrivate()) { // 隐私文件夹,需要密码验证 showPasswordVerifyDialogForDelete(mFocusNoteDataItem); } else { // 普通文件夹,直接显示删除确认对话框 AlertDialog.Builder builder = new AlertDialog.Builder(this); builder.setTitle(getString(R.string.alert_title_delete)); builder.setIcon(android.R.drawable.ic_dialog_alert); builder.setMessage(getString(R.string.alert_message_delete_folder)); builder.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int which) { deleteFolder(mFocusNoteDataItem.getId()); // 执行删除 } }); builder.setNegativeButton(android.R.string.cancel, null); builder.show(); } break; case MENU_FOLDER_CHANGE_NAME: showCreateOrModifyFolderDialog(false, false, false); // 修改文件夹名称 break; case MENU_FOLDER_PIN: // 置顶文件夹 setFolderPinnedStatus(mFocusNoteDataItem.getId(), true); break; case MENU_NOTE_DELETE: // 删除便签确认对话框 AlertDialog.Builder deleteNoteBuilder = new AlertDialog.Builder(this); deleteNoteBuilder.setTitle(getString(R.string.alert_title_delete)); deleteNoteBuilder.setIcon(android.R.drawable.ic_dialog_alert); deleteNoteBuilder.setMessage(getString(R.string.alert_message_delete_note)); deleteNoteBuilder.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int which) { // 检查便签的父文件夹是否为隐私文件夹 long parentFolderId = mFocusNoteDataItem.getParentId(); boolean isPrivateFolder = false; // 查询父文件夹信息 Cursor cursor = mContentResolver.query(Notes.CONTENT_NOTE_URI, new String[]{NoteColumns.PRIVATE}, NoteColumns.ID + "=? AND " + NoteColumns.TYPE + "=" + Notes.TYPE_FOLDER, new String[]{String.valueOf(parentFolderId)}, null); if (cursor != null) { if (cursor.moveToFirst()) { isPrivateFolder = (cursor.getInt(0) > 0); } cursor.close(); } // 根据父文件夹类型决定删除方式 if (isPrivateFolder) { // 隐私文件夹中的便签,直接彻底删除 HashSet noteIds = new HashSet(); noteIds.add(mFocusNoteDataItem.getId()); DataUtils.batchDeleteNotes(getContentResolver(), noteIds); } else { // 普通文件夹中的便签,移动到回收站 HashSet noteIds = new HashSet(); noteIds.add(mFocusNoteDataItem.getId()); DataUtils.batchMoveToFolder(getContentResolver(), noteIds, Notes.ID_TRASH_FOLER); } // 刷新列表 startAsyncNotesListQuery(); } }); deleteNoteBuilder.setNegativeButton(android.R.string.cancel, null); deleteNoteBuilder.show(); 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; } return true; } @Override public boolean onPrepareOptionsMenu(Menu menu) { menu.clear(); // 清空菜单 // 根据当前状态加载不同菜单 if (mState == ListEditState.NOTE_LIST) { getMenuInflater().inflate(R.menu.note_list, menu); // 根据同步状态设置菜单项标题 menu.findItem(R.id.menu_sync).setTitle( GTaskSyncService.isSyncing() ? R.string.menu_sync_cancel : R.string.menu_sync); } 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); } else { Log.e(TAG, "Wrong state:" + mState); } return true; } @Override public boolean onOptionsItemSelected(MenuItem item) { switch (item.getItemId()) { case R.id.menu_new_folder: { showCreateOrModifyFolderDialog(true, false, false); // 创建新文件夹 break; } case R.id.menu_new_private_folder: { showCreateOrModifyFolderDialog(true, true, false); // 创建新隐私文件夹 break; } case R.id.menu_new_study_folder: { showCreateOrModifyFolderDialog(true, false, true); // 创建新学习文件夹 break; } case R.id.menu_export_text: { exportNoteToText(); // 导出笔记为文本 break; } case R.id.menu_sync: { if (isSyncMode()) { // 同步模式下,启动或取消同步 if (TextUtils.equals(item.getTitle(), getString(R.string.menu_sync))) { GTaskSyncService.startSync(this); // 开始同步 } else { GTaskSyncService.cancelSync(this); // 取消同步 } } else { startPreferenceActivity(); // 非同步模式下打开设置页面 } break; } case R.id.menu_setting: { startPreferenceActivity(); // 打开设置页面 break; } case R.id.menu_new_note: { createNewNote(); // 新建笔记 break; } case R.id.menu_search: onSearchRequested(); // 触发搜索 break; case R.id.menu_rubbish_bin: openRubbishBin(); // 打开回收站 break; default: break; } return true; } @Override public boolean onSearchRequested() { startSearch(null, false, null /* appData */, false); // 启动搜索 return true; } /** * 将笔记导出为文本文件 */ private void exportNoteToText() { final BackupUtils backup = BackupUtils.getInstance(NotesListActivity.this); new AsyncTask() { @Override protected Integer doInBackground(Void... unused) { return backup.exportToText(); // 执行导出 } @Override protected void onPostExecute(Integer result) { // 根据导出结果显示相应对话框 if (result == BackupUtils.STATE_SD_CARD_UNMOUONTED) { AlertDialog.Builder builder = new AlertDialog.Builder(NotesListActivity.this); builder.setTitle(NotesListActivity.this .getString(R.string.failed_sdcard_export)); builder.setMessage(NotesListActivity.this .getString(R.string.error_sdcard_unmounted)); builder.setPositiveButton(android.R.string.ok, null); builder.show(); } else if (result == BackupUtils.STATE_SUCCESS) { AlertDialog.Builder builder = new AlertDialog.Builder(NotesListActivity.this); builder.setTitle(NotesListActivity.this .getString(R.string.success_sdcard_export)); builder.setMessage(NotesListActivity.this.getString( R.string.format_exported_file_location, backup .getExportedTextFileName(), backup.getExportedTextFileDir())); builder.setPositiveButton(android.R.string.ok, null); builder.show(); } else if (result == BackupUtils.STATE_SYSTEM_ERROR) { AlertDialog.Builder builder = new AlertDialog.Builder(NotesListActivity.this); builder.setTitle(NotesListActivity.this .getString(R.string.failed_sdcard_export)); builder.setMessage(NotesListActivity.this .getString(R.string.error_sdcard_export)); builder.setPositiveButton(android.R.string.ok, null); builder.show(); } } }.execute(); } /** * 检查是否处于同步模式 */ private boolean isSyncMode() { 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(); // 启动异步查询,显示回收站内容 } /** * 启动设置页面 */ private void startPreferenceActivity() { Activity from = getParent() != null ? getParent() : this; Intent intent = new Intent(from, NotesPreferenceActivity.class); from.startActivityIfNeeded(intent, -1); } /** * 列表项点击监听器 */ private class OnListItemClickListener implements OnItemClickListener { public void onItemClick(AdapterView parent, View view, int position, long id) { if (view instanceof NotesListItem) { NoteItemData item = ((NotesListItem) view).getItemData(); if (mNotesListAdapter.isInChoiceMode()) { // 多选模式下,处理笔记选择 if (item.getType() == Notes.TYPE_NOTE) { position = position - mNotesListView.getHeaderViewsCount(); mModeCallBack.onItemCheckedStateChanged(null, position, id, !mNotesListAdapter.isSelectedItem(position)); } return; } // 根据当前状态处理点击事件 switch (mState) { case NOTE_LIST: if (item.getType() == Notes.TYPE_FOLDER || item.getType() == Notes.TYPE_SYSTEM) { openFolder(item); // 打开文件夹 } else if (item.getType() == Notes.TYPE_NOTE) { openNode(item); // 打开笔记 } else { Log.e(TAG, "Wrong note type in NOTE_LIST"); } break; case SUB_FOLDER: case CALL_RECORD_FOLDER: if (item.getType() == Notes.TYPE_NOTE) { openNode(item); // 打开笔记 } else { 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; } } } } /** * 查询目标文件夹(用于移动笔记) */ private void startQueryDestinationFolders() { String selection = NoteColumns.TYPE + "=? AND " + NoteColumns.PARENT_ID + "<>? AND " + NoteColumns.ID + "<>?"; // 根据当前状态调整查询条件 selection = (mState == ListEditState.NOTE_LIST) ? selection: "(" + selection + ") OR (" + NoteColumns.ID + "=" + Notes.ID_ROOT_FOLDER + ")"; mBackgroundQueryHandler.startQuery(FOLDER_LIST_QUERY_TOKEN, null, Notes.CONTENT_NOTE_URI, FoldersListAdapter.PROJECTION, selection, new String[] { String.valueOf(Notes.TYPE_FOLDER), String.valueOf(Notes.ID_TRASH_FOLER), String.valueOf(mCurrentFolderId) }, NoteColumns.MODIFIED_DATE + " DESC"); } public boolean onItemLongClick(AdapterView parent, View view, int position, long id) { if (view instanceof NotesListItem) { NoteItemData itemData = ((NotesListItem) view).getItemData(); if (itemData != null) { // 无论是文件夹还是便签,长按都显示上下文菜单 mFocusNoteDataItem = itemData; // 确保退出多选模式 if (mModeCallBack != null && mModeCallBack.mActionMode != null) { mModeCallBack.mActionMode.finish(); } // 重置列表的选择模式,确保长按总是触发上下文菜单 mNotesListView.setChoiceMode(ListView.CHOICE_MODE_NONE); mNotesListAdapter.setChoiceMode(false); mNotesListView.clearChoices(); mNotesListView.setMultiChoiceModeListener(null); mNotesListAdapter.notifyDataSetChanged(); return false; } } return false; } // 学习计时器相关变量 private boolean mIsTimerRunning = false; // 计时器是否正在运行 private boolean mIsCountdownMode = false; // 是否为逆向计时模式 private long mTimerStartTime = 0; // 计时器开始时间 private long mTimerCurrentTime = 0; // 当前计时时间(毫秒) private long mTimerDuration = 0; // 逆向计时时长(毫秒) private android.os.Handler mTimerHandler = new android.os.Handler(); private Runnable mTimerRunnable = new Runnable() { @Override public void run() { if (mIsTimerRunning) { if (mIsCountdownMode) { // 逆向计时 long elapsed = System.currentTimeMillis() - mTimerStartTime; mTimerCurrentTime = mTimerDuration - elapsed; if (mTimerCurrentTime <= 0) { // 计时结束 mTimerCurrentTime = mTimerDuration; // 设置为完整时长,确保计入专注时间 mIsTimerRunning = false; updateTimerDisplay(); Toast.makeText(NotesListActivity.this, "学习时间结束!", Toast.LENGTH_LONG).show(); // 停止计时器并处理 stopStudyTimerWithDisplay(); return; } } else { // 正向计时 mTimerCurrentTime = System.currentTimeMillis() - mTimerStartTime; } updateTimerDisplay(); mTimerHandler.postDelayed(this, 1000); // 每秒更新一次 } } }; // 计时器对话框相关变量 private Dialog mTimerDialog = null; private TextView mTimerDisplayView = null; private Button mPauseResumeButton = null; private android.os.Handler mDisplayHandler = new android.os.Handler(); // 悬浮窗相关变量 private Dialog mFloatingTimerDialog = null; private TextView mFloatingTimerDisplayView = null; private boolean mIsFloatingMode = false; /** * 显示学习计时器对话框 */ private void showStudyTimerDialog() { final AlertDialog.Builder builder = new AlertDialog.Builder(this); View view = LayoutInflater.from(this).inflate(R.layout.dialog_study_timer, null); final RadioGroup timerModeGroup = (RadioGroup) view.findViewById(R.id.timer_mode_group); final EditText hoursEdit = (EditText) view.findViewById(R.id.edit_hours); final EditText minutesEdit = (EditText) view.findViewById(R.id.edit_minutes); final EditText secondsEdit = (EditText) view.findViewById(R.id.edit_seconds); builder.setTitle("设置学习计时器"); builder.setView(view); builder.setPositiveButton("开始", new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { // 获取计时模式 int selectedModeId = timerModeGroup.getCheckedRadioButtonId(); mIsCountdownMode = (selectedModeId == R.id.radio_countdown); // 获取时间设置 int hours = Integer.parseInt(!TextUtils.isEmpty(hoursEdit.getText()) ? hoursEdit.getText().toString() : "0"); int minutes = Integer.parseInt(!TextUtils.isEmpty(minutesEdit.getText()) ? minutesEdit.getText().toString() : "0"); int seconds = Integer.parseInt(!TextUtils.isEmpty(secondsEdit.getText()) ? secondsEdit.getText().toString() : "0"); // 计算总时长(毫秒) mTimerDuration = (hours * 3600 + minutes * 60 + seconds) * 1000; // 验证时间设置 if (mIsCountdownMode && mTimerDuration < 60000) { // 逆向计时时长小于1分钟 Toast.makeText(NotesListActivity.this, "逆向计时时长必须大于等于1分钟", Toast.LENGTH_SHORT).show(); return; } // 开始计时 startStudyTimer(); dialog.dismiss(); } }); builder.setNegativeButton("取消", null); builder.show(); } /** * 开始学习计时器 */ private void startStudyTimer() { mTimerStartTime = System.currentTimeMillis(); mIsTimerRunning = true; mTimerHandler.post(mTimerRunnable); // 显示计时控制对话框 showTimerControlDialog(); } /** * 暂停学习计时器 */ private void pauseStudyTimer() { mIsTimerRunning = false; mTimerHandler.removeCallbacks(mTimerRunnable); // 停止显示更新 mDisplayHandler.removeCallbacksAndMessages(null); } /** * 恢复学习计时器 */ private void resumeStudyTimer() { if (!mIsTimerRunning) { if (mIsCountdownMode) { // 倒计时模式:计算已经过去的时间 long elapsedTime = mTimerDuration - mTimerCurrentTime; mTimerStartTime = System.currentTimeMillis() - elapsedTime; } else { // 正向计时模式:使用当前时间减去已计时时间 mTimerStartTime = System.currentTimeMillis() - mTimerCurrentTime; } mIsTimerRunning = true; mTimerHandler.post(mTimerRunnable); // 重启显示更新 startTimerDisplayUpdate(); } } /** * 停止学习计时器 */ // 旧的停止方法,已被stopStudyTimerWithDisplay替代 // private void stopStudyTimer() { // mIsTimerRunning = false; // mTimerHandler.removeCallbacks(mTimerRunnable); // // // 验证学习有效性 // if (mTimerCurrentTime >= 60000) { // // 学习时间超过1分钟,视为有效学习 // updateFocusDuration(mTimerCurrentTime); // } // // // 重置计时器变量 // mTimerCurrentTime = 0; // mTimerDuration = 0; // } /** * 更新计时器显示 */ private void updateTimerDisplay() { long totalSeconds = mTimerCurrentTime / 1000; int hours = (int) (totalSeconds / 3600); int minutes = (int) ((totalSeconds % 3600) / 60); int seconds = (int) (totalSeconds % 60); String timeStr = String.format("%02d:%02d:%02d", hours, minutes, seconds); if (mTimerDisplayView != null) { mTimerDisplayView.setText(timeStr); } if (mFloatingTimerDisplayView != null) { mFloatingTimerDisplayView.setText(timeStr); } } /** * 启动计时器显示更新 */ private void startTimerDisplayUpdate() { mDisplayHandler.removeCallbacksAndMessages(null); final Runnable updateRunnable = new Runnable() { @Override public void run() { if (mIsTimerRunning) { updateTimerDisplay(); mDisplayHandler.postDelayed(this, 1000); } } }; mDisplayHandler.postDelayed(updateRunnable, 1000); } /** * 显示计时器控制对话框 */ private void showTimerControlDialog() { // 创建一个悬浮的非模态对话框 final Dialog timerDialog = new Dialog(this, android.R.style.Theme_Dialog); timerDialog.requestWindowFeature(Window.FEATURE_NO_TITLE); timerDialog.setCancelable(true); timerDialog.setCanceledOnTouchOutside(true); // 设置对话框大小和位置 Window window = timerDialog.getWindow(); if (window != null) { WindowManager.LayoutParams params = window.getAttributes(); params.width = WindowManager.LayoutParams.MATCH_PARENT; params.height = WindowManager.LayoutParams.WRAP_CONTENT; params.gravity = Gravity.BOTTOM; params.flags &= ~WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE; params.flags &= ~WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL; window.setAttributes(params); window.setBackgroundDrawable(new ColorDrawable(Color.TRANSPARENT)); } View view = LayoutInflater.from(this).inflate(R.layout.dialog_timer_control, null); mTimerDisplayView = (TextView) view.findViewById(R.id.timer_display); mPauseResumeButton = (Button) view.findViewById(R.id.btn_pause_resume); final Button btnStop = (Button) view.findViewById(R.id.btn_stop); // 更新计时器显示 updateTimerDisplay(); // 设置按钮文本 mPauseResumeButton.setText(mIsTimerRunning ? "暂停" : "继续"); mPauseResumeButton.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { if (mIsTimerRunning) { pauseStudyTimer(); mPauseResumeButton.setText("继续"); } else { resumeStudyTimer(); mPauseResumeButton.setText("暂停"); } } }); btnStop.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { stopStudyTimerWithDisplay(); timerDialog.dismiss(); } }); timerDialog.setContentView(view); timerDialog.show(); mTimerDialog = timerDialog; // 启动计时器显示更新 startTimerDisplayUpdate(); } /** * 显示悬浮窗计时器(固定按钮形式) */ private void showFloatingTimer() { // 关闭之前的对话框 if (mTimerDialog != null && mTimerDialog.isShowing()) { mTimerDialog.dismiss(); mTimerDialog = null; } // 如果悬浮窗已经显示,直接返回 if (mFloatingTimerDialog != null && mFloatingTimerDialog.isShowing()) { return; } // 创建固定的悬浮按钮对话框 mFloatingTimerDialog = new Dialog(this, android.R.style.Theme_Dialog); mFloatingTimerDialog.requestWindowFeature(Window.FEATURE_NO_TITLE); mFloatingTimerDialog.setCancelable(false); // 设置为不可取消,确保按钮一直存在 // 设置对话框大小和位置 Window window = mFloatingTimerDialog.getWindow(); if (window != null) { WindowManager.LayoutParams params = window.getAttributes(); params.width = WindowManager.LayoutParams.WRAP_CONTENT; params.height = WindowManager.LayoutParams.WRAP_CONTENT; params.gravity = Gravity.TOP | Gravity.RIGHT; params.flags |= WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE; params.flags |= WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL; params.flags |= WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH; window.setAttributes(params); window.setBackgroundDrawable(new ColorDrawable(Color.TRANSPARENT)); } // 使用闹钟图标布局 View view = LayoutInflater.from(this).inflate(R.layout.floating_timer_icon, null); // 点击按钮时显示完整的计时器对话框 view.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { if (mFloatingTimerDialog != null && mFloatingTimerDialog.isShowing()) { mFloatingTimerDialog.dismiss(); mFloatingTimerDialog = null; showTimerControlDialog(); } } }); mFloatingTimerDialog.setContentView(view); mFloatingTimerDialog.show(); mIsFloatingMode = true; } /** * 停止学习计时器(带显示处理) */ private void stopStudyTimerWithDisplay() { mIsTimerRunning = false; mTimerHandler.removeCallbacks(mTimerRunnable); mDisplayHandler.removeCallbacksAndMessages(null); // 验证学习有效性 if (mTimerCurrentTime >= 60000) { // 学习时间超过1分钟,视为有效学习 updateFocusDuration(mTimerCurrentTime); } // 重置计时器变量 mTimerCurrentTime = 0; mTimerDuration = 0; // 关闭计时器对话框 if (mTimerDialog != null && mTimerDialog.isShowing()) { mTimerDialog.dismiss(); mTimerDialog = null; mTimerDisplayView = null; mPauseResumeButton = null; } } /** * 更新计时器显示视图 */ private void updateTimerDisplayView(TextView display) { long totalSeconds = mTimerCurrentTime / 1000; int hours = (int) (totalSeconds / 3600); int minutes = (int) ((totalSeconds % 3600) / 60); int seconds = (int) (totalSeconds % 60); String timeStr = String.format("%02d:%02d:%02d", hours, minutes, seconds); display.setText(timeStr); } /** * 更新专注时长 */ private void updateFocusDuration(long duration) { // 查询当前文件夹的专注时长 Cursor cursor = getContentResolver().query( Notes.CONTENT_NOTE_URI, new String[]{NoteColumns.FOCUS_DURATION}, NoteColumns.ID + "=? AND " + NoteColumns.TYPE + "=?", new String[]{String.valueOf(mCurrentFolderId), String.valueOf(Notes.TYPE_FOLDER)}, null ); long currentDuration = 0; if (cursor != null && cursor.moveToFirst()) { currentDuration = cursor.getLong(0); cursor.close(); } // 更新专注时长 long newDuration = currentDuration + duration; ContentValues values = new ContentValues(); values.put(NoteColumns.FOCUS_DURATION, newDuration); values.put(NoteColumns.LOCAL_MODIFIED, 1); getContentResolver().update( Notes.CONTENT_NOTE_URI, values, NoteColumns.ID + "=? AND " + NoteColumns.TYPE + "=?", new String[]{String.valueOf(mCurrentFolderId), String.valueOf(Notes.TYPE_FOLDER)} ); // 刷新文件夹显示 startAsyncNotesListQuery(); } }