You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
xiaomi/src/ui/NotesListActivity.java

1246 lines
54 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

/*
* 版权所有 (c) 2010-2011MiCode 开源社区 (www.micode.net)
* 根据 Apache 许可证 2.0 版本("许可证")授权;
* 除非符合许可证的规定,否则不得使用本文件。
* 您可以从以下网址获取许可证副本:
* http://www.apache.org/licenses/LICENSE-2.0
* 除非适用法律要求或书面同意,本软件按"原样"分发,
* 没有任何明示或暗示的保证或条件。
* 详见许可证中规定的权限和限制。
* 这是一份标准的Apache许可证2.0版本的开源声明)
*/
// 定义当前包名为net.micode.notes.ui
package net.micode.notes.ui;
/* 导入Android基础包 */
import android.annotation.SuppressLint;
import android.app.Activity; // 基础Activity类
import android.app.AlertDialog; // 警告对话框类
import android.app.Dialog; // 对话框基类
import android.appwidget.AppWidgetManager; // 小部件管理类
import android.content.AsyncQueryHandler; // 异步查询处理器
import android.content.ContentResolver; // 内容解析器
import android.content.ContentValues; // 内容值对象
import android.content.Context; // 上下文对象
import android.content.Intent; // Intent类
import android.content.SharedPreferences; // 共享偏好设置
import android.database.Cursor; // 数据库游标
import android.os.AsyncTask; // 异步任务类
import android.os.Bundle; // 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.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.ListView; // 列表视图
import android.widget.TextView; // 文本视图
import android.widget.Toast; // 提示信息控件
/* 导入AndroidX支持库 */
import androidx.annotation.NonNull; // 非空注解
/* 导入项目资源 */
import net.micode.notes.R; // 自动生成的R资源类
/* 导入项目数据相关类 */
import net.micode.notes.data.Notes; // 笔记数据库常量
import net.micode.notes.data.Notes.NoteColumns; // 笔记列名定义
/* 导入Google任务同步服务 */
import net.micode.notes.gtask.remote.GTaskSyncService; // Google任务同步服务
/* 导入项目模型类 */
import net.micode.notes.model.WorkingNote; // 工作笔记模型
/* 导入项目工具类 */
import net.micode.notes.tool.BackupUtils; // 备份工具
import net.micode.notes.tool.DataUtils; // 数据工具
import net.micode.notes.tool.ResourceParser; // 资源解析器
/* 导入项目UI适配器 */
import net.micode.notes.ui.NotesListAdapter.AppWidgetAttribute; // 小部件属性定义
/* 导入项目小部件相关 */
import net.micode.notes.widget.NoteWidgetProvider_2x; // 2x尺寸小部件
import net.micode.notes.widget.NoteWidgetProvider_4x; // 4x尺寸小部件
/* 导入Java IO类 */
import java.io.BufferedReader; // 缓冲读取器
import java.io.IOException; // IO异常
import java.io.InputStream; // 输入流
import java.io.InputStreamReader; // 输入流读取器
/* 导入Java集合类 */
import java.util.HashSet; // 哈希集合
// 定义笔记列表Activity实现点击和长按监听接口
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; // 删除文件夹菜单ID
private static final int MENU_FOLDER_VIEW = 1; // 查看文件夹菜单ID
private static final int MENU_FOLDER_CHANGE_NAME = 2; // 重命名文件夹菜单ID
// 偏好设置键(是否已添加引导说明)
private static final String PREFERENCE_ADD_INTRODUCTION = "net.micode.notes.introduction";
// 列表编辑状态枚举
private enum ListEditState {
NOTE_LIST, // 普通笔记列表模式
SUB_FOLDER, // 子文件夹模式
CALL_RECORD_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 TextView mTitleBar; // 标题栏文本
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; // 当前焦点笔记数据
// SQL查询条件
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 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();
}
@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); // 清空当前游标触发刷新
} else {
super.onActivityResult(requestCode, resultCode, data);
}
}
// 从raw资源读取并设置应用引导信息
private void setAppInfoFromRawRes() {
SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(this);
// 检查是否已经添加过引导说明
if (!sp.getBoolean(PREFERENCE_ADD_INTRODUCTION, false)) {
StringBuilder sb = new StringBuilder();
InputStream in = null;
try {
// 从raw资源读取引导文本
in = getResources().openRawResource(R.raw.introduction);
InputStreamReader isr = new InputStreamReader(in);
BufferedReader br = new BufferedReader(isr);
char[] buf = new char[1024];
int len;
while ((len = br.read(buf)) > 0) {
sb.append(buf, 0, len);
}
} 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).apply();
} else {
Log.e(TAG, "Save introduction note error");
}
}
}
@Override
protected void onStart() {
super.onStart();
startAsyncNotesListQuery(); // 启动异步笔记列表查询
}
// 初始化视图资源和变量
@SuppressLint("ClickableViewAccessibility")
private void initResources() {
mContentResolver = this.getContentResolver();
mBackgroundQueryHandler = new BackgroundQueryHandler(this.getContentResolver());
mCurrentFolderId = Notes.ID_ROOT_FOLDER; // 默认设为根文件夹
// 初始化列表视图
mNotesListView = 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);
// 初始化新建笔记按钮
mAddNewNote = findViewById(R.id.btn_new_note);
mAddNewNote.setOnClickListener(this);
mAddNewNote.setOnTouchListener(new NewNoteOnTouchListener());
// 初始化触摸相关变量
mDispatch = false;
mDispatchY = 0;
mOriginY = 0;
// 初始化标题栏
mTitleBar = findViewById(R.id.tv_title_bar);
mState = ListEditState.NOTE_LIST; // 初始状态为普通笔记列表
// 初始化多选模式回调
mModeCallBack = new ModeCallback();
}
// 多选模式回调实现类
private class ModeCallback implements ListView.MultiChoiceModeListener, OnMenuItemClickListener {
private DropdownMenu mDropDownMenu; // 下拉菜单
private ActionMode mActionMode; // 操作模式
@Override
public boolean onCreateActionMode(ActionMode mode, Menu menu) {
// 加载多选操作菜单
getMenuInflater().inflate(R.menu.note_list_options, menu);
menu.findItem(R.id.delete).setOnMenuItemClickListener(this);
// 初始化移动菜单
// 移动菜单项
MenuItem mMoveMenu = menu.findItem(R.id.move);
if (mFocusNoteDataItem.getParentId() == Notes.ID_CALL_RECORD_FOLDER
|| DataUtils.getUserFolderCount(mContentResolver) == 0) {
mMoveMenu.setVisible(false); // 通话记录或无文件夹时隐藏移动选项
} else {
mMoveMenu.setVisible(true);
mMoveMenu.setOnMenuItemClickListener(this);
}
// 设置多选模式状态
mActionMode = mode;
mNotesListAdapter.setChoiceMode(true);
mNotesListView.setLongClickable(false);
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,
customView.findViewById(R.id.selection_menu),
R.menu.note_list_dropdown);
mDropDownMenu.setOnDropdownMenuItemClickListener(item -> {
// 全选/反选处理
mNotesListAdapter.selectAll(!mNotesListAdapter.isAllSelected());
updateMenu();
return true;
});
return true;
}
/**
* 更新多选操作菜单的状态显示
* 1. 刷新选中项数量显示
* 2. 更新全选/反选按钮状态
*/
private void updateMenu() {
// 获取当前选中的笔记项数量(从列表适配器中获取)
int selectedCount = mNotesListAdapter.getSelectedCount();
/**
* 设置下拉菜单标题文本
* 使用字符串资源 R.string.menu_select_title 作为模板
* 将选中数量 selectedCount 作为参数插入字符串
* 示例当选中3项时显示"已选3项"
*/
String format = getResources().getString(R.string.menu_select_title, selectedCount);
mDropDownMenu.setTitle(format); // 将格式化后的字符串设置为菜单标题
// 从下拉菜单中获取全选功能菜单项通过资源ID R.id.action_select_all
MenuItem item = mDropDownMenu.findItem(R.id.action_select_all);
// 确保菜单项存在(防御性编程)
if (item != null) {
/**
* 检查是否所有笔记项都被选中
* 根据状态更新菜单项:
* 1. 已全选:显示选中状态和"取消全选"文本
* 2. 未全选:显示未选中状态和"全选"文本
*/
if (mNotesListAdapter.isAllSelected()) {
item.setChecked(true); // 显示复选框选中状态
item.setTitle(R.string.menu_deselect_all); // 设置文本为"取消全选"
} else {
item.setChecked(false); // 显示复选框未选中状态
item.setTitle(R.string.menu_select_all); // 设置文本为"全选"
}
}
}
/**
* ActionMode准备时的回调方法
* @param mode 当前的ActionMode对象
* @param menu 要显示的菜单
* @return boolean 返回false表示不需要重新创建菜单
*/
public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
// TODO: 需要在此处添加菜单项的动态更新逻辑
return false; // 默认返回false
}
/**
* ActionMode菜单项点击事件处理
* @param mode 当前的ActionMode对象
* @param item 被点击的菜单项
* @return boolean 返回false表示事件未处理会继续传递
*/
public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
// TODO: 需要添加其他菜单项的点击处理逻辑
return false; // 默认返回false
}
/**
* 销毁ActionMode时的清理操作
* @param mode 要销毁的ActionMode对象
*/
public void onDestroyActionMode(ActionMode mode) {
mNotesListAdapter.setChoiceMode(false); // 禁用适配器的多选模式
mNotesListView.setLongClickable(true); // 恢复列表视图的长按功能
mAddNewNote.setVisibility(View.VISIBLE); // 显示新建笔记按钮
}
/**
* 主动结束ActionMode
*/
public void finishActionMode() {
mActionMode.finish(); // 调用当前ActionMode的finish方法结束多选模式
}
/**
* 列表项选中状态变化回调
* @param mode 当前的ActionMode对象
* @param position 发生变化的项位置
* @param id 项ID
* @param checked 新的选中状态
*/
public void onItemCheckedStateChanged(ActionMode mode, int position, long id,
boolean checked) {
mNotesListAdapter.setCheckedItem(position, checked); // 更新适配器中对应项的选中状态
updateMenu(); // 刷新菜单显示
}
/**
* 处理上下文菜单项点击事件
* @param item 被点击的菜单项
* @return boolean 返回true表示事件已处理
*/
public boolean onMenuItemClick(@NonNull MenuItem item) {
// 检查当前是否有选中的笔记
if (mNotesListAdapter.getSelectedCount() == 0) {
// 显示"未选中任何项"的提示
Toast.makeText(NotesListActivity.this,
getString(R.string.menu_select_none),
Toast.LENGTH_SHORT).show();
return true; // 事件已处理
}
// 根据点击的菜单项ID执行不同操作
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,
(dialog, which) -> batchDelete());
// 设置取消按钮,点击后不执行任何操作
builder.setNegativeButton(android.R.string.cancel, null);
builder.show(); // 显示对话框
break;
case R.id.move: // 处理移动操作
startQueryDestinationFolders(); // 启动目标文件夹查询
break;
default: // 其他菜单项
return false; // 返回false表示不处理
}
return true; // 返回true表示事件已处理
}
}
/**
* 新建笔记按钮的触摸事件监听器
* 处理透明区域的特殊点击事件分发
*/
private class NewNoteOnTouchListener implements OnTouchListener {
/**
* 处理触摸事件
* @param v 被触摸的视图(新建笔记按钮)
* @param event 触摸事件对象
* @return boolean 返回true表示事件已处理false继续传递
*/
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(); // 按钮高度
// 计算按钮在屏幕中的起始Y坐标屏幕底部向上偏移按钮高度
int start = screenHeight - newNoteViewHeight;
// 计算触摸事件的绝对Y坐标相对于屏幕
int eventY = start + (int) event.getY();
// 如果是子文件夹模式,需要减去标题栏高度
if (mState == ListEditState.SUB_FOLDER) {
eventY -= mTitleBar.getHeight();
start -= mTitleBar.getHeight();
}
/**
* HACK: 处理按钮透明区域的点击事件分发
* 透明区域定义为y < -0.12x + 94基于按钮局部坐标系
* 94表示透明区域的最大高度像素
* 注意:如果按钮背景改变,这个公式需要相应调整
*/
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(); // 记录原始Y坐标局部
mDispatchY = eventY; // 记录分发Y坐标绝对
event.setLocation(event.getX(), mDispatchY); // 修改事件坐标
mDispatch = true; // 设置分发标志
return mNotesListView.dispatchTouchEvent(event); // 分发事件给列表
}
}
break;
}
case MotionEvent.ACTION_MOVE: { // 移动事件
if (mDispatch) {
// 计算Y坐标偏移量并更新分发坐标
mDispatchY += (int) event.getY() - mOriginY;
event.setLocation(event.getX(), mDispatchY);
return mNotesListView.dispatchTouchEvent(event); // 继续分发移动事件
}
break;
}
default: { // 处理ACTION_UP和ACTION_CANCEL
if (mDispatch) {
event.setLocation(event.getX(), mDispatchY); // 保持坐标一致性
mDispatch = false; // 重置分发标志
return mNotesListView.dispatchTouchEvent(event); // 分发抬起/取消事件
}
break;
}
}
return false; // 不处理事件,继续传递
}
}
/**
* 启动异步查询笔记列表
* 根据当前文件夹ID决定查询条件
*/
private void startAsyncNotesListQuery() {
// 选择查询条件:如果是根文件夹使用特殊查询,否则使用普通查询
String selection = (mCurrentFolderId == Notes.ID_ROOT_FOLDER) ? ROOT_FOLDER_SELECTION
: NORMAL_SELECTION;
// 执行异步查询
mBackgroundQueryHandler.startQuery(
FOLDER_NOTE_LIST_QUERY_TOKEN, // 查询标识
null, // cookie对象
Notes.CONTENT_NOTE_URI, // 内容URI
NoteItemData.PROJECTION, // 查询列
selection, // WHERE条件
new String[] { String.valueOf(mCurrentFolderId) }, // WHERE参数
NoteColumns.TYPE + " DESC," + NoteColumns.MODIFIED_DATE + " DESC" // 排序
);
}
/**
* 后台查询处理器
* 继承AsyncQueryHandler实现异步数据库操作
*/
private final class BackgroundQueryHandler extends AsyncQueryHandler {
/**
* 构造函数
* @param contentResolver 内容解析器
*/
public BackgroundQueryHandler(ContentResolver contentResolver) {
super(contentResolver); // 调用父类构造函数
}
/**
* 查询完成回调
* @param token 查询标识
* @param cookie 附加数据
* @param cursor 查询结果游标
*/
@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:
// 其他查询类型不处理
}
}
}
/**
* 显示文件夹选择菜单
* @param cursor 包含文件夹数据的游标
*/
private void showFolderListMenu(Cursor cursor) {
// 创建对话框构建器
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, (dialog, which) -> {
// 执行批量移动操作
DataUtils.batchMoveToFolder(
mContentResolver,
mNotesListAdapter.getSelectedItemIds(), // 选中笔记ID集合
adapter.getItemId(which) // 目标文件夹ID
);
// 显示操作结果提示
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(); // 结束多选模式
});
builder.show(); // 显示对话框
}
/**
* 创建新笔记
* 启动笔记编辑Activity并传递当前文件夹ID
*/
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); // 传递当前文件夹ID
// 启动Activity并等待结果
this.startActivityForResult(intent, REQUEST_CODE_NEW_NODE);
}
/**
* 批量删除选中的笔记项
* 根据同步模式决定直接删除还是移动到回收站
*/
private void batchDelete() {
new AsyncTask<Void, Void, HashSet<AppWidgetAttribute>>() {
@Override
protected HashSet<AppWidgetAttribute> doInBackground(Void... unused) {
// 1. 获取选中笔记关联的小部件集合
HashSet<AppWidgetAttribute> widgets = mNotesListAdapter.getSelectedWidget();
// 2. 根据同步模式选择删除策略
if (!isSyncMode()) {
// 非同步模式:直接删除
if (!DataUtils.batchDeleteNotes(mContentResolver,
mNotesListAdapter.getSelectedItemIds())) {
Log.e(TAG, "Delete notes error, should not happen"); // 错误日志
}
} else {
// 同步模式:移动到回收站
if (!DataUtils.batchMoveToFolder(mContentResolver,
mNotesListAdapter.getSelectedItemIds(), Notes.ID_TRASH_FOLER)) {
Log.e(TAG, "Move notes to trash folder error, should not happen");
}
}
return widgets; // 返回需要更新的小部件集合
}
@Override
protected void onPostExecute(HashSet<AppWidgetAttribute> widgets) {
// 3. 删除完成后更新关联的小部件
if (widgets != null) {
for (AppWidgetAttribute widget : widgets) {
// 检查小部件ID和类型是否有效
if (widget.widgetId != AppWidgetManager.INVALID_APPWIDGET_ID &&
widget.widgetType.get() != Notes.TYPE_WIDGET_INVALIDE) {
updateWidget(widget.widgetId, widget.widgetType.get()); // 更新小部件显示
}
}
}
mModeCallBack.finishActionMode(); // 结束多选模式
}
}.execute(); // 启动异步任务
}
/**
* 删除指定文件夹
* @param folderId 要删除的文件夹ID
*/
private void deleteFolder(long folderId) {
// 检查是否是根文件夹(不允许删除)
if (folderId == Notes.ID_ROOT_FOLDER) {
Log.e(TAG, "Wrong folder id, should not happen " + folderId);
return;
}
// 1. 准备要删除的文件夹ID集合
HashSet<Long> ids = new HashSet<>();
ids.add(folderId);
// 2. 获取文件夹关联的小部件
HashSet<AppWidgetAttribute> widgets = DataUtils.getFolderNoteWidget(mContentResolver, folderId);
// 3. 根据同步模式选择删除策略
if (!isSyncMode()) {
DataUtils.batchDeleteNotes(mContentResolver, ids); // 直接删除
} else {
DataUtils.batchMoveToFolder(mContentResolver, ids, Notes.ID_TRASH_FOLER); // 移动到回收站
}
// 4. 更新关联的小部件
if (widgets != null) {
for (AppWidgetAttribute widget : widgets) {
if (widget.widgetId != AppWidgetManager.INVALID_APPWIDGET_ID &&
widget.widgetType.get() != Notes.TYPE_WIDGET_INVALIDE) {
updateWidget(widget.widgetId, widget.widgetType.get());
}
}
}
}
/**
* 打开笔记查看/编辑界面
* @param data 要打开的笔记数据
*/
private void openNode(NoteItemData data) {
Intent intent = new Intent(this, NoteEditActivity.class);
intent.setAction(Intent.ACTION_VIEW); // 设置为查看模式
intent.putExtra(Intent.EXTRA_UID, data.getId()); // 传递笔记ID
startActivityForResult(intent, REQUEST_CODE_OPEN_NODE); // 启动编辑界面
}
/**
* 打开文件夹显示其内容
* @param data 要打开的文件夹数据
*/
private void openFolder(NoteItemData data) {
// 1. 更新当前文件夹ID并查询内容
mCurrentFolderId = data.getId();
startAsyncNotesListQuery();
// 2. 根据文件夹类型设置状态
if (data.getId() == Notes.ID_CALL_RECORD_FOLDER) {
mState = ListEditState.CALL_RECORD_FOLDER;
mAddNewNote.setVisibility(View.GONE); // 通话记录文件夹隐藏新建按钮
} else {
mState = ListEditState.SUB_FOLDER;
}
// 3. 更新标题栏显示
if (data.getId() == Notes.ID_CALL_RECORD_FOLDER) {
mTitleBar.setText(R.string.call_record_folder_name); // 固定标题
} else {
mTitleBar.setText(data.getSnippet()); // 使用文件夹名作为标题
}
mTitleBar.setVisibility(View.VISIBLE); // 显示标题栏
}
// 处理新建笔记按钮点击
public void onClick(View v) {
if (v.getId() == R.id.btn_new_note) {
createNewNote(); // 调用新建笔记方法
}
}
/**
* 强制显示软键盘
*/
private void showSoftInput() {
InputMethodManager imm = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
if (imm != null) {
imm.toggleSoftInput(InputMethodManager.SHOW_FORCED, 0); // 强制显示
}
}
/**
* 隐藏软键盘
* @param view 当前焦点视图
*/
private void hideSoftInput(View view) {
InputMethodManager imm = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
imm.hideSoftInputFromWindow(view.getWindowToken(), 0); // 根据窗口token隐藏
}
/**
* 显示创建/修改文件夹对话框
* @param create true表示创建false表示修改
*/
private void showCreateOrModifyFolderDialog(final boolean create) {
// 1. 初始化对话框
final AlertDialog.Builder builder = new AlertDialog.Builder(this);
View view = LayoutInflater.from(this).inflate(R.layout.dialog_edit_text, null);
final EditText etName = view.findViewById(R.id.et_foler_name);
// 2. 自动弹出软键盘
showSoftInput();
// 3. 根据模式设置初始文本
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(""); // 创建模式:清空输入框
builder.setTitle(this.getString(R.string.menu_create_folder));
}
// 4. 设置对话框按钮
builder.setPositiveButton(android.R.string.ok, null); // 先设为null后自定义点击
builder.setNegativeButton(android.R.string.cancel, (dialog, which) -> hideSoftInput(etName));
// 5. 显示对话框
final Dialog dialog = builder.setView(view).show();
final Button positive = dialog.findViewById(android.R.id.button1);
// 6. 自定义确认按钮点击逻辑
positive.setOnClickListener(v -> {
hideSoftInput(etName);
String name = etName.getText().toString();
// 检查文件夹名是否已存在
if (DataUtils.checkVisibleFolderName(mContentResolver, name)) {
Toast.makeText(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);
mContentResolver.insert(Notes.CONTENT_NOTE_URI, values);
}
dialog.dismiss(); // 关闭对话框
});
// 7. 初始禁用确认按钮(当名称为空时)
if (TextUtils.isEmpty(etName.getText())) {
positive.setEnabled(false);
}
// 8. 添加文本变化监听
etName.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) {
// 文本变化时启用/禁用确认按钮
positive.setEnabled(!TextUtils.isEmpty(etName.getText()));
}
@Override
public void afterTextChanged(Editable s) {
// 文本变化后(无需实现)
}
});
}
// 处理返回键按下事件
@Override
public void onBackPressed() {
switch (mState) {
case SUB_FOLDER:
// 从子文件夹返回根目录
mCurrentFolderId = Notes.ID_ROOT_FOLDER;
mState = ListEditState.NOTE_LIST;
startAsyncNotesListQuery(); // 重新加载根目录列表
mTitleBar.setVisibility(View.GONE); // 隐藏标题栏
break;
case CALL_RECORD_FOLDER:
// 从通话记录文件夹返回根目录
mCurrentFolderId = Notes.ID_ROOT_FOLDER;
mState = ListEditState.NOTE_LIST;
mAddNewNote.setVisibility(View.VISIBLE); // 显示新建笔记按钮
mTitleBar.setVisibility(View.GONE); // 隐藏标题栏
startAsyncNotesListQuery(); // 重新加载根目录列表
break;
case NOTE_LIST:
super.onBackPressed(); // 默认返回行为
break;
default:
break;
}
}
// 更新指定的小部件
private void updateWidget(int appWidgetId, int appWidgetType) {
// 创建小部件更新Intent
Intent intent = new Intent(AppWidgetManager.ACTION_APPWIDGET_UPDATE);
// 根据小部件类型设置对应的Provider类
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, "Unsupported widget type");
return;
}
// 设置要更新的小部件ID数组
intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, new int[] {appWidgetId});
sendBroadcast(intent); // 发送广播更新小部件
setResult(RESULT_OK, intent); // 设置操作结果为成功
}
// 文件夹长按菜单创建监听器
private final OnCreateContextMenuListener mFolderOnCreateContextMenuListener =
new OnCreateContextMenuListener() {
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); // 添加重命名菜单项
}
}
};
// 上下文菜单关闭时调用
@Override
public void onContextMenuClosed(@NonNull Menu menu) {
if (mNotesListView != null) {
mNotesListView.setOnCreateContextMenuListener(null); // 移除菜单监听器
}
super.onContextMenuClosed(menu);
}
// 处理上下文菜单项选择
@Override
public boolean onContextItemSelected(@NonNull 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: // 删除文件夹
// 显示删除确认对话框
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,
(dialog, which) -> deleteFolder(mFocusNoteDataItem.getId()));
builder.setNegativeButton(android.R.string.cancel, null);
builder.show();
break;
case MENU_FOLDER_CHANGE_NAME: // 重命名文件夹
showCreateOrModifyFolderDialog(false);
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) {
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);
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;
default:
break;
}
return true;
}
// 处理搜索请求
@Override
public boolean onSearchRequested() {
startSearch(null, false, null /* appData */, false);
return true;
}
/**
* 将笔记导出为文本文件的后台操作方法
* 1. 使用异步任务在后台执行导出操作
* 2. 根据导出结果显示相应的提示对话框
*/
private void exportNoteToText() {
// 获取BackupUtils单例实例用于执行导出操作
final BackupUtils backup = BackupUtils.getInstance(NotesListActivity.this);
// 创建异步任务执行导出操作参数Void表示不需要进度更新
new AsyncTask<Void, Void, Integer>() {
/**
* 后台执行方法
* @param unused 可变参数(此处不需要)
* @return 导出操作的结果状态码
*/
@Override
protected Integer doInBackground(Void... unused) {
// 调用BackupUtils执行实际导出操作返回操作结果状态码
return backup.exportToText();
}
/**
* 后台任务完成后在主线程执行
* @param result 导出操作结果状态码
*/
@Override
protected void onPostExecute(Integer result) {
// 处理SD卡未挂载情况
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));
// 设置错误消息SD卡未挂载
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(); // 立即执行异步任务
}
/**
* 检查当前是否处于同步模式
* @return true表示已设置同步账户(同步模式)false表示未设置(本地模式)
*/
private boolean isSyncMode() {
// 获取同步账户名并检查是否为空(去除前后空格)
return !NotesPreferenceActivity.getSyncAccountName(this).trim().isEmpty();
}
/**
* 启动设置Activity
* 优先使用父Activity启动如果存在否则使用当前Activity
*/
private void startPreferenceActivity() {
// 获取父ActivityFragmentActivity情况下不存在则使用当前Activity
Activity from = getParent() != null ? getParent() : this;
// 创建跳转到设置页面的Intent
Intent intent = new Intent(from, NotesPreferenceActivity.class);
// 启动ActivitystartActivityIfNeeded保证单例
from.startActivityIfNeeded(intent, -1);
}
/**
* 列表项点击事件监听器
* 处理笔记/文件夹的点击逻辑
*/
private class OnListItemClickListener implements OnItemClickListener {
@Override
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) {
// 调整位置考虑HeaderViews的影响
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;
default:
break;
}
}
}
}
/**
* 查询可用目标文件夹(用于移动笔记操作)
* 查询条件:
* 1. 文件夹类型
* 2. 非回收站文件夹
* 3. 非当前所在文件夹
* 特殊处理:在根目录状态下包含根文件夹选项
*/
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, // 内容URI
FoldersListAdapter.PROJECTION, // 查询列
selection, // 查询条件
new String[] { // 查询参数值
String.valueOf(Notes.TYPE_FOLDER), // 文件夹类型
String.valueOf(Notes.ID_TRASH_FOLER), // 排除回收站
String.valueOf(mCurrentFolderId) // 排除当前文件夹
},
NoteColumns.MODIFIED_DATE + " DESC" // 按修改时间降序
);
}
/**
* 列表项长按事件处理
* @return true表示已处理事件false继续传递事件
*/
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");
}
}
// 文件夹类型的长按处理
else if (mFocusNoteDataItem.getType() == Notes.TYPE_FOLDER) {
// 设置上下文菜单监听器
mNotesListView.setOnCreateContextMenuListener(mFolderOnCreateContextMenuListener);
}
}
return false; // 允许事件继续传递
}
}