|
|
/*
|
|
|
* 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<Long>() {{ 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<Void, Void, HashSet<AppWidgetAttribute>>() {
|
|
|
protected HashSet<AppWidgetAttribute> doInBackground(Void... unused) {
|
|
|
HashSet<AppWidgetAttribute> 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<AppWidgetAttribute> 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<Void, Void, HashSet<AppWidgetAttribute>>() {
|
|
|
protected HashSet<AppWidgetAttribute> doInBackground(Void... unused) {
|
|
|
HashSet<AppWidgetAttribute> widgets = mNotesListAdapter.getSelectedWidget();
|
|
|
// 将选中的笔记从回收站恢复
|
|
|
HashSet<Long> 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<AppWidgetAttribute> 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<Void, Void, HashSet<AppWidgetAttribute>>() {
|
|
|
protected HashSet<AppWidgetAttribute> doInBackground(Void... unused) {
|
|
|
HashSet<AppWidgetAttribute> 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<AppWidgetAttribute> 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<Long> items = new HashSet<Long>();
|
|
|
// 将文件夹本身也添加到要恢复的列表中
|
|
|
items.add(folderId);
|
|
|
// 递归获取所有子项
|
|
|
getRecycleBinFolderItems(folderId, items);
|
|
|
|
|
|
// 恢复文件夹及其所有子项
|
|
|
new AsyncTask<Void, Void, HashSet<AppWidgetAttribute>>() {
|
|
|
protected HashSet<AppWidgetAttribute> doInBackground(Void... unused) {
|
|
|
HashSet<AppWidgetAttribute> 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<AppWidgetAttribute> 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<Long> items = new HashSet<>();
|
|
|
|
|
|
// 添加文件夹本身
|
|
|
items.add(folderId);
|
|
|
|
|
|
// 递归获取所有子项
|
|
|
getRecycleBinFolderItems(folderId, items);
|
|
|
|
|
|
// 彻底删除文件夹及其所有子项
|
|
|
new AsyncTask<Void, Void, HashSet<AppWidgetAttribute>>() {
|
|
|
protected HashSet<AppWidgetAttribute> doInBackground(Void... unused) {
|
|
|
// 获取所有相关的小部件
|
|
|
HashSet<AppWidgetAttribute> 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<AppWidgetAttribute> 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<Long> 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<Long> items = DataUtils.getFolderItems(mContentResolver, folderId);
|
|
|
// 将文件夹本身也添加到要删除的列表中
|
|
|
items.add(folderId);
|
|
|
|
|
|
// 保存原始父文件夹ID并将文件夹及其所有子项移动到回收站
|
|
|
// 对于文件夹本身,将其PARENT_ID设置为回收站ID
|
|
|
// 对于文件夹内的便签,将其PARENT_ID设置为回收站ID,ORIGIN_PARENT_ID设置为原始父文件夹ID
|
|
|
// 这样在回收站中打开文件夹时,我们可以通过ORIGIN_PARENT_ID来查询该文件夹下的所有便签和文件夹
|
|
|
|
|
|
HashSet<AppWidgetAttribute> 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<Long> noteIds = new HashSet<Long>();
|
|
|
noteIds.add(mFocusNoteDataItem.getId());
|
|
|
DataUtils.batchDeleteNotes(getContentResolver(), noteIds);
|
|
|
} else {
|
|
|
// 普通文件夹中的便签,移动到回收站
|
|
|
HashSet<Long> noteIds = new HashSet<Long>();
|
|
|
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<Void, Void, Integer>() {
|
|
|
|
|
|
@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();
|
|
|
}
|
|
|
} |