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.
MiNote/ui/NotesListActivity.java

397 lines
18 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-2011The MiCode 开源社区 (www.micode.net)
*
* 本软件根据 Apache 许可证 2.0 版("许可证")发布;
* 除非符合许可证,否则不得使用此文件。
* 您可以在以下网址获取许可证副本:
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* 除非法律要求或书面同意,软件
* 根据许可证分发的内容按"原样"提供,
* 不附带任何明示或暗示的保证或条件。
* 请参阅许可证,了解有关权限和限制的具体语言。
*/
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.ContentValues;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.SharedPreferences;
import android.database.Cursor;
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.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.PopupMenu;
import android.widget.TextView;
import android.widget.Toast;
import net.micode.notes.R;
import net.micode.notes.data.Notes;
import net.micode.notes.data.Notes.NoteColumns;
import net.micode.notes.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;
/**
* 笔记列表主活动
* 功能:
* 1. 展示笔记、文件夹、通话记录的列表界面
* 2. 支持多选模式下的批量操作(删除、移动)
* 3. 处理文件夹的创建、重命名、删除
* 4. 集成同步服务GTaskSyncService和桌面小部件更新
* 5. 实现数据备份与恢复(导出为文本)
* 6. 管理不同层级的列表状态(根目录、子文件夹、通话记录文件夹)
*/
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; // 目标文件夹列表查询令牌
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, mDispatchY; // 触摸坐标记录
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; // 长按选中的笔记/文件夹数据
// 查询条件:普通列表(子文件夹下的项)
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);
if (in != null) {
BufferedReader br = new BufferedReader(new InputStreamReader(in));
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 {
DataUtils.closeQuietly(in); // 关闭流工具方法
}
// 创建引导笔记
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(); // 标记已创建引导笔记
}
}
}
@Override
protected void onStart() {
super.onStart();
startAsyncNotesListQuery(); // 启动异步查询加载列表数据
}
/**
* 初始化界面元素和适配器
*/
private void initResources() {
mContentResolver = getContentResolver();
mBackgroundQueryHandler = new BackgroundQueryHandler(mContentResolver);
mCurrentFolderId = Notes.ID_ROOT_FOLDER; // 初始化为根目录
// 初始化列表视图
mNotesListView = (ListView) findViewById(R.id.notes_list);
mNotesListView.addFooterView(LayoutInflater.from(this).inflate(R.layout.note_list_footer, null));
mNotesListView.setOnItemClickListener(new OnListItemClickListener());
mNotesListView.setOnItemLongClickListener(this);
// 设置适配器
mNotesListAdapter = new NotesListAdapter(this);
mNotesListView.setAdapter(mNotesListAdapter);
// 新建笔记按钮
mAddNewNote = (Button) findViewById(R.id.btn_new_note);
mAddNewNote.setOnClickListener(this);
mAddNewNote.setOnTouchListener(new NewNoteOnTouchListener()); // 自定义触摸事件处理
// 标题栏和状态初始化
mTitleBar = (TextView) findViewById(R.id.tv_title_bar);
mState = ListEditState.NOTE_LIST;
mModeCallBack = new ModeCallback(); // 多选模式回调实例
}
/**
* 多选模式回调类处理ActionMode相关逻辑
*/
private class ModeCallback implements ListView.MultiChoiceModeListener, OnMenuItemClickListener {
private DropdownMenu mDropDownMenu; // 下拉菜单
private ActionMode mActionMode; // 操作模式
// 创建ActionMode时初始化菜单
public boolean onCreateActionMode(ActionMode mode, Menu menu) {
getMenuInflater().inflate(R.menu.note_list_options, menu);
menu.findItem(R.id.delete).setOnMenuItemClickListener(this); // 删除菜单项监听
// 移动菜单项可见性控制(通话记录文件夹或无用户文件夹时隐藏)
mMoveMenu = menu.findItem(R.id.move);
boolean isCallRecordFolder = mFocusNoteDataItem.getParentId() == Notes.ID_CALL_RECORD_FOLDER;
boolean hasUserFolders = DataUtils.getUserFolderCount(mContentResolver) > 0;
mMoveMenu.setVisible(!isCallRecordFolder && hasUserFolders);
if (mMoveMenu.isVisible()) {
mMoveMenu.setOnMenuItemClickListener(this); // 移动菜单项监听
}
// 设置自定义ActionMode视图包含下拉菜单
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(item -> {
mNotesListAdapter.selectAll(!mNotesListAdapter.isAllSelected()); // 切换全选状态
updateMenu(); // 更新菜单显示
return true;
});
mActionMode = mode;
mNotesListAdapter.setChoiceMode(true); // 启用多选模式
mNotesListView.setLongClickable(false); // 禁用长按事件
mAddNewNote.setVisibility(View.GONE); // 隐藏新建按钮
return true;
}
// 更新菜单显示(选中数量和全选状态)
private void updateMenu() {
int selectedCount = mNotesListAdapter.getSelectedCount();
mDropDownMenu.setTitle(getString(R.string.menu_select_title, selectedCount));
MenuItem selectAllItem = mDropDownMenu.findItem(R.id.action_select_all);
if (selectAllItem != null) {
boolean isAllSelected = mNotesListAdapter.isAllSelected();
selectAllItem.setChecked(isAllSelected);
selectAllItem.setTitle(isAllSelected ? R.string.menu_deselect_all : R.string.menu_select_all);
}
}
// 处理菜单项点击(删除、移动)
public boolean onMenuItemClick(MenuItem item) {
if (mNotesListAdapter.getSelectedCount() == 0) {
Toast.makeText(this, R.string.menu_select_none, Toast.LENGTH_SHORT).show();
return true;
}
switch (item.getItemId()) {
case R.id.delete: // 删除选中项
showDeleteConfirmationDialog();
break;
case R.id.move: // 移动选中项到目标文件夹
startQueryDestinationFolders();
break;
}
return true;
}
// 省略其他MultiChoiceModeListener接口方法见完整代码
}
/**
* 新建笔记按钮触摸事件处理器(处理透明区域的事件分发)
*/
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 buttonHeight = mAddNewNote.getHeight();
int startY = screenHeight - buttonHeight;
int eventY = startY + (int) event.getY();
// 调整子文件夹状态下的坐标(减去标题栏高度)
if (mState == ListEditState.SUB_FOLDER) {
eventY -= mTitleBar.getHeight();
startY -= mTitleBar.getHeight();
}
// 透明区域判断公式根据UI设计硬编码需与按钮背景匹配
if (event.getY() < (-0.12 * event.getX() + 94)) {
View lastItem = mNotesListView.getChildAt(mNotesListView.getChildCount() - 1
- mNotesListView.getFooterViewsCount());
if (lastItem != null && lastItem.getBottom() > startY && lastItem.getTop() < (startY + 94)) {
mOriginY = (int) event.getY();
mDispatchY = eventY;
event.setLocation(event.getX(), mDispatchY);
mDispatch = true;
return mNotesListView.dispatchTouchEvent(event); // 分发事件到列表视图
}
}
break;
}
// 省略其他触摸事件处理(见完整代码)
}
return false;
}
}
/**
* 启动异步查询加载列表数据
*/
private void startAsyncNotesListQuery() {
String selection = (mCurrentFolderId == Notes.ID_ROOT_FOLDER) ? ROOT_FOLDER_SELECTION : NORMAL_SELECTION;
mBackgroundQueryHandler.startQuery(FOLDER_NOTE_LIST_QUERY_TOKEN, null,
Notes.CONTENT_NOTE_URI, NoteItemData.PROJECTION, selection,
new String[]{String.valueOf(mCurrentFolderId)},
NoteColumns.TYPE + " DESC," + NoteColumns.MODIFIED_DATE + " DESC"); // 按类型降序(文件夹优先)、修改时间降序排序
}
/**
* 后台查询处理器处理Cursor加载回调
*/
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); // 显示文件夹选择菜单
}
break;
}
}
}
/**
* 显示文件夹选择菜单(用于移动操作)
*/
private void showFolderListMenu(Cursor cursor) {
AlertDialog.Builder builder = new AlertDialog.Builder(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(), adapter.getItemId(which));
Toast.makeText(this, getString(R.string.format_move_notes_to_folder,
mNotesListAdapter.getSelectedCount(), adapter.getFolderName(this, which)),
Toast.LENGTH_SHORT).show();
mModeCallBack.finishActionMode(); // 结束多选模式
});
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);
startActivityForResult(intent, REQUEST_CODE_NEW_NODE);
}
/**
* 批量删除选中项(异步处理,支持同步模式下移动到回收站)
*/
private void batch