From b69f9d196603f838f5bda99640368b283fe94e45 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=8E=8B=E5=B2=9A=E9=A6=A8?= <13450313+xiaolanxiner@user.noreply.giitee.com> Date: Sun, 29 Oct 2023 12:11:27 +0800 Subject: [PATCH] 10.29 --- .../micode/notes/ui/NotesListActivity.java | 1037 +++++++++++++++++ 1 file changed, 1037 insertions(+) create mode 100644 src/xiaomi/app/src/main/java/com/example/notes_master/src/net/micode/notes/ui/NotesListActivity.java diff --git a/src/xiaomi/app/src/main/java/com/example/notes_master/src/net/micode/notes/ui/NotesListActivity.java b/src/xiaomi/app/src/main/java/com/example/notes_master/src/net/micode/notes/ui/NotesListActivity.java new file mode 100644 index 0000000..54086c0 --- /dev/null +++ b/src/xiaomi/app/src/main/java/com/example/notes_master/src/net/micode/notes/ui/NotesListActivity.java @@ -0,0 +1,1037 @@ +/* + * 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 com.example.notes_master.src.net.micode.notes.ui; +import android.app.Activity; +import android.app.AlertDialog; +import android.app.Dialog; +import android.appwidget.AppWidgetManager; +import android.content.AsyncQueryHandler; +import android.content.ContentResolver; +import android.content.ContentValues; +import android.content.Context; +import android.content.DialogInterface; +import android.content.Intent; +import android.content.SharedPreferences; +import android.database.Cursor; +import android.os.AsyncTask; +import android.os.Bundle; +import android.preference.PreferenceManager; +import android.text.Editable; +import android.text.TextUtils; +import android.text.TextWatcher; +import android.util.Log; +import android.view.ActionMode; +import android.view.ContextMenu; +import android.view.ContextMenu.ContextMenuInfo; +import android.view.Display; +import android.view.HapticFeedbackConstants; +import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuItem; +import android.view.MenuItem.OnMenuItemClickListener; +import android.view.MotionEvent; +import android.view.View; +import android.view.View.OnClickListener; +import android.view.View.OnCreateContextMenuListener; +import android.view.View.OnTouchListener; +import android.view.inputmethod.InputMethodManager; +import android.widget.AdapterView; +import android.widget.AdapterView.OnItemClickListener; +import android.widget.AdapterView.OnItemLongClickListener; +import android.widget.Button; +import android.widget.EditText; +import android.widget.ListView; +import android.widget.PopupMenu; +import android.widget.TextView; +import android.widget.Toast; + +import androidx.appcompat.app.AppCompatActivity; + +import com.example.notes_master.src.net.micode.notes.data.Notes; +import com.example.notes_master.src.net.micode.notes.data.Notes.NoteColumns; +import com.example.notes_master.src.net.micode.notes.gtask.remote.GTaskSyncService; +import com.example.notes_master.src.net.micode.notes.model.WorkingNote; +import com.example.notes_master.src.net.micode.notes.tool.BackupUtils; +import com.example.notes_master.src.net.micode.notes.tool.DataUtils; +import com.example.notes_master.src.net.micode.notes.tool.ResourceParser; +import com.example.notes_master.src.net.micode.notes.ui.NotesListAdapter.AppWidgetAttribute; +import com.example.notes_master.src.net.micode.notes.widget.NoteWidgetProvider_2x; +import com.example.notes_master.src.net.micode.notes.widget.NoteWidgetProvider_4x; + +import net.micode.notes.R; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.util.HashSet; +//主界面 +public class NotesListActivity extends AppCompatActivity implements OnClickListener, OnItemLongClickListener { + private static final int FOLDER_NOTE_LIST_QUERY_TOKEN = 0;//查询文件夹中的便签列表 + private static final int FOLDER_LIST_QUERY_TOKEN = 1;//查询文件夹列表 + private static final int MENU_FOLDER_DELETE = 0;//删除文件夹 + private static final int MENU_FOLDER_VIEW = 1;//查看文件夹 + private static final int MENU_FOLDER_CHANGE_NAME = 2;//修改文件夹名字 + private static final String PREFERENCE_ADD_INTRODUCTION = "net.micode.notes.introduction"; + private enum ListEditState { + NOTE_LIST, SUB_FOLDER, CALL_RECORD_FOLDER + }; + private ListEditState mState; + private BackgroundQueryHandler mBackgroundQueryHandler; + private NotesListAdapter mNotesListAdapter; + private ListView mNotesListView; + private Button mAddNewNote; + private boolean mDispatch; + private int mOriginY; + private int mDispatchY; + private TextView mTitleBar; + private long mCurrentFolderId; + private ContentResolver mContentResolver; + private ModeCallback mModeCallBack; + private static final String TAG = "NotesListActivity";//打印日志时作标签 + public static final int NOTES_LISTVIEW_SCROLL_RATE = 30; + private NoteItemData mFocusNoteDataItem; + private static final String NORMAL_SELECTION = NoteColumns.PARENT_ID + "=?"; + private static final String ROOT_FOLDER_SELECTION = "(" + NoteColumns.TYPE + "<>" + + Notes.TYPE_SYSTEM + " AND " + NoteColumns.PARENT_ID + "=?)" + " OR (" + + NoteColumns.ID + "=" + Notes.ID_CALL_RECORD_FOLDER + " AND " + + NoteColumns.NOTES_COUNT + ">0)"; + private final static int REQUEST_CODE_OPEN_NODE = 102; + private final static int REQUEST_CODE_NEW_NODE = 103; + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.note_list); + initResources(); + /** + * Insert an introduction when user firstly use this application + */ + setAppInfoFromRawRes(); + } + @Override + protected void onActivityResult(int requestCode, int resultCode, Intent data) { + if (resultCode == RESULT_OK + && (requestCode == REQUEST_CODE_OPEN_NODE || requestCode == REQUEST_CODE_NEW_NODE)) { + mNotesListAdapter.changeCursor(null); + } else { + super.onActivityResult(requestCode, resultCode, data); + } + } + //当用户第一次使用小米便签时,把默认的欢迎便签保存到界面上 + private void setAppInfoFromRawRes() { + SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(this); + if (!sp.getBoolean(PREFERENCE_ADD_INTRODUCTION, false)) {//"net.micode.notes.introduction" + StringBuilder sb = new StringBuilder(); + InputStream in = null; + // 把资源文件放到应用程序的/raw/raw下,那么就可以在应用中使用getResources获取资源后, + // 以openRawResource方法(不带后缀的资源文件名)打开这个文件。 + try { + in = getResources().openRawResource(R.raw.introduction);//该文件内容:Welcome to use MIUI notes! + 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) { + // TODO Auto-generated catch block + e.printStackTrace(); + } + } + } + // 创建空的WorkingNote + 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()) { + // 更新保存note的信息 + 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;//0 + mNotesListView = (ListView) findViewById(R.id.notes_list);// 绑定XML中的ListView,作为Item的容器 + mNotesListView.addFooterView(LayoutInflater.from(this).inflate(R.layout.note_list_footer, null), + null, false); + mNotesListView.setOnItemClickListener(new OnListItemClickListener());//单击监听器 + mNotesListView.setOnItemLongClickListener(this);//长按监听器 + mNotesListAdapter = new NotesListAdapter(this); + mNotesListView.setAdapter(mNotesListAdapter); + mAddNewNote = (Button) findViewById(R.id.btn_new_note); + mAddNewNote.setOnClickListener(this);//“添加便签”按钮的监听器 + mAddNewNote.setOnTouchListener(new NewNoteOnTouchListener());//给按钮绑定自定义的触摸监听器 + mDispatch = false; + mDispatchY = 0; + mOriginY = 0; + mTitleBar = (TextView) findViewById(R.id.tv_title_bar); + mState = ListEditState.NOTE_LIST; + mModeCallBack = new ModeCallback(); + } + //内部类 + /** + * 私有类`ModeCallback`实现了`ListView.MultiChoiceModeListener`和`OnMenuItemClickListener`接口, + * 它用于处理多选模式下的操作和菜单项点击事件。 + */ + private class ModeCallback implements ListView.MultiChoiceModeListener, OnMenuItemClickListener { + private DropdownMenu mDropDownMenu; // 下拉菜单对象 + private ActionMode mActionMode; // 操作模式对象 + private MenuItem mMoveMenu; // 移动菜单项对象 + /** + * 当操作模式创建时调用。 + * @param mode 提供操作模式的ActionMode对象 + * @param menu 操作模式的菜单 + * @return 返回true表示操作模式已创建 + */ + public boolean onCreateActionMode(ActionMode mode, Menu menu) { + //通过调用 getMenuInflater().inflate() 方法加载菜单布局文件 R.menu.note_list_options, + // 并将菜单项添加到 menu 中。 + getMenuInflater().inflate(R.menu.note_list_options, menu); // 加载菜单布局文件 + menu.findItem(R.id.delete).setOnMenuItemClickListener(this); // 设置删除菜单项的点击监听器 + mMoveMenu = menu.findItem(R.id.move); // 获取移动菜单项 + // 根据条件设置移动菜单项的可见性和点击监听器 + if (mFocusNoteDataItem.getParentId() == Notes.ID_CALL_RECORD_FOLDER + || DataUtils.getUserFolderCount(mContentResolver) == 0) { + mMoveMenu.setVisible(false); + } else { + mMoveMenu.setVisible(true); + mMoveMenu.setOnMenuItemClickListener(this); + } + mActionMode = mode; + mNotesListAdapter.setChoiceMode(true); // 设置列表适配器的选择模式为多选模式 + mNotesListView.setLongClickable(false); // 禁用列表视图的长按点击功能 + mAddNewNote.setVisibility(View.GONE); // 设置添加新笔记按钮不可见 + // 创建自定义视图并设置给操作模式 + View customView = LayoutInflater.from(NotesListActivity.this) + .inflate(R.layout.note_list_dropdown_menu, null); + mode.setCustomView(customView); + // 创建下拉菜单对象并设置下拉菜单项的点击监听器 + mDropDownMenu = new DropdownMenu(NotesListActivity.this, + (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);//全选 + } + } + } + /** + * 当操作模式准备就绪时调用。 + * @param mode 提供操作模式的ActionMode对象 + * @param menu 操作模式的菜单 + * @return 返回true表示操作模式已准备就绪 + */ + public boolean onPrepareActionMode(ActionMode mode, Menu menu) { + // TODO: 添加准备操作模式的逻辑(如果有的话) + return false; + } + /** + * 当操作模式的菜单项被点击时调用。 + * @param mode 提供操作模式的ActionMode对象 + * @param item 被点击的菜单项 + * @return 返回true表示菜单项点击事件已处理 + */ + public boolean onActionItemClicked(ActionMode mode, MenuItem item) { + // TODO: 处理菜单项的点击事件 + return false; + } + /** + * 当操作模式结束时调用的方法。 + * @param mode 提供操作模式的ActionMode对象 + */ + public void onDestroyActionMode(ActionMode mode) { + mNotesListAdapter.setChoiceMode(false); // 取消多选模式 + mNotesListView.setLongClickable(true); // 启用列表视图的长按点击功能 + mAddNewNote.setVisibility(View.VISIBLE); // 设置添加新笔记按钮可见 + } + /** + * 结束操作模式,退出多选模式。 + */ + public void finishActionMode() { + mActionMode.finish(); + } + /** + * 当列表项的选择状态改变时调用,更新HashMap中相应项的值。 + * @param mode 提供选择模式的ActionMode对象 + * @param position 被选中或取消选中的项的适配器位置 + * @param id 被选中或取消选中的项的适配器ID + * @param checked true表示项现在被选中,false表示项现在取消选中 + */ + public void onItemCheckedStateChanged(ActionMode mode, int position, long id, boolean checked) { + mNotesListAdapter.setCheckedItem(position, checked); + updateMenu(); + } + /** + * 当菜单项被点击时调用。 + * @param item 被点击的菜单项 + * @return 返回true表示菜单项点击事件已处理 + */ + 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; + 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(); + /** + * Minus TitleBar's height + */ + // 如果当前状态是ListEditState.SUB_FOLDER,则减去标题栏的高度 + if (mState == ListEditState.SUB_FOLDER) { + eventY -= mTitleBar.getHeight(); + start -= mTitleBar.getHeight(); + } + /** + * HACKME:When click the transparent part of "New Note" button, dispatch + * the event to the list view behind this button. The transparent part of + * "New Note" button could be expressed by formula y=-0.12x+94(Unit:pixel) + * and the line top of the button. The coordinate based on left of the "New + * Note" button. The 94 represents maximum height of the transparent part. + * Notice that, if the background of the button changes, the formula should + * also change. This is very bad, just for the UI designer's strong requirement. + */ + // 如果点击的是"New Note"按钮的透明部分(根据特定的公式判断),则将事件分发给按钮后面的列表视图 + 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() { + // 检查当前文件夹ID是否为根文件夹ID,确定查询的条件 + String selection = (mCurrentFolderId == Notes.ID_ROOT_FOLDER) ? ROOT_FOLDER_SELECTION : NORMAL_SELECTION; + // 使用BackgroundQueryHandler开始异步查询 + // 参数1:查询令牌,用于标识查询的唯一性 + // 参数2:对象,用于传递额外的数据给查询结果的回调方法 + // 参数3:查询的URI,指定要查询的数据表的内容URI + // 参数4:查询的投影,指定要查询的列 + // 参数5:查询的选择条件 + // 参数6:选择条件中的占位符的值 + // 参数7:查询的排序方式 + mBackgroundQueryHandler.startQuery(FOLDER_NOTE_LIST_QUERY_TOKEN, null, Notes.CONTENT_NOTE_URI, + NoteItemData.PROJECTION, selection, new String[]{String.valueOf(mCurrentFolderId)}, + NoteColumns.TYPE + " DESC," + NoteColumns.MODIFIED_DATE + " DESC"); + } + /* + 自定义异步处理查询类,用于在后台执行查询操作并处理查询结果 + 这个类是为了避免在主线程中执行耗时的数据库查询或与内容提供者交互的操作,以确保应用程序的界面响应性能。 + */ + private final class BackgroundQueryHandler extends AsyncQueryHandler { + public BackgroundQueryHandler(ContentResolver contentResolver) { + super(contentResolver); + } + /** + * token 参数表示查询操作的标识符,用于区分不同的查询操作。 + * cookie 参数是一个可选的对象,用于在发起查询时传递额外的数据,在查询完成后原样返回,可以用于标识查询操作的上下文或其他需要的信息。 + * cursor 参数是查询返回的结果集,它是一个游标对象(Cursor),用于遍历查询结果的数据。 + * @param token the token to identify the query, passed in from + * {@link #startQuery}. + * @param cookie the cookie object passed in from {@link #startQuery}. + * @param cursor The cursor holding the results from the query. + */ + @Override + protected void onQueryComplete(int token, Object cookie, Cursor cursor) { + switch (token) { + case FOLDER_NOTE_LIST_QUERY_TOKEN://0 对文件夹内的笔记列表进行查询操作 + mNotesListAdapter.changeCursor(cursor); + break; + case FOLDER_LIST_QUERY_TOKEN://1 对文件夹列表进行查询操作 + if (cursor != null && cursor.getCount() > 0) {//有文件夹 + showFolderListMenu(cursor); + } else { + Log.e(TAG, "Query folder failed"); + } + break; + default: + return; + } + } + } + private void showFolderListMenu(Cursor cursor) { + AlertDialog.Builder builder = new AlertDialog.Builder(NotesListActivity.this); + builder.setTitle(R.string.menu_title_select_folder);//选择文件夹 + final FoldersListAdapter adapter = new FoldersListAdapter(this, cursor); + builder.setAdapter(adapter, new DialogInterface.OnClickListener() { + + public void onClick(DialogInterface dialog, int which) { + DataUtils.batchMoveToFolder(mContentResolver, + mNotesListAdapter.getSelectedItemIds(), adapter.getItemId(which)); + Toast.makeText( + NotesListActivity.this, + getString(R.string.format_move_notes_to_folder, + mNotesListAdapter.getSelectedCount(),//选中的要移到的便签数量 + adapter.getFolderName(NotesListActivity.this, which)), + Toast.LENGTH_SHORT).show();//当把文件夹中的便签移到文件夹外时,显示Toast消息“”已将所选+n+条便签移到上一级文件夹 + mModeCallBack.finishActionMode(); + } + }); + builder.show(); + } + private void createNewNote() { + // 创建一个Intent对象,指定目标为NoteEditActivity,并设置操作为ACTION_INSERT_OR_EDIT + Intent intent = new Intent(this, NoteEditActivity.class); + intent.setAction(Intent.ACTION_INSERT_OR_EDIT); + // 将当前文件夹ID作为额外的INTENT_EXTRA_FOLDER_ID数据传递给Intent + intent.putExtra(Notes.INTENT_EXTRA_FOLDER_ID, mCurrentFolderId); + // 启动NoteEditActivity,并获取结果 + this.startActivityForResult(intent, REQUEST_CODE_NEW_NODE); + } + private void batchDelete() { + // 创建一个异步任务 + new AsyncTask>() { + protected HashSet doInBackground(Void... unused) { + // 获取选中笔记项关联的小部件信息 + HashSet widgets = mNotesListAdapter.getSelectedWidget(); + if (!isSyncMode()) { + // 如果不处于同步模式,直接删除选中的笔记 + if (DataUtils.batchDeleteNotes(mContentResolver, mNotesListAdapter.getSelectedItemIds())) { + // 删除成功 + } else { + Log.e(TAG, "Delete notes error, should not happens"); + } + } else { + // 如果处于同步模式,将选中的笔记移动到回收站文件夹 + if (!DataUtils.batchMoveToFolder(mContentResolver, mNotesListAdapter.getSelectedItemIds(), Notes.ID_TRASH_FOLER)) { + Log.e(TAG, "Move notes to trash folder error, should not happens"); + } + } + return widgets; + } + @Override + protected void onPostExecute(HashSet widgets) { + // 更新与选中笔记关联的小部件 + if (widgets != null) { + for (AppWidgetAttribute widget : widgets) { + if (widget.widgetId != AppWidgetManager.INVALID_APPWIDGET_ID + && widget.widgetType != Notes.TYPE_WIDGET_INVALIDE) { + updateWidget(widget.widgetId, widget.widgetType); + } + } + } + // 完成操作模式,退出多选模式 + mModeCallBack.finishActionMode(); + } + }.execute(); + } + private void deleteFolder(long folderId) { + // 检查文件夹ID是否为根文件夹的ID,如果是,则记录错误日志并返回 + if (folderId == Notes.ID_ROOT_FOLDER) { + Log.e(TAG, "Wrong folder id, should not happen " + folderId); + return; + } + // 创建HashSet来存储要删除的文件夹ID + HashSet ids = new HashSet(); + ids.add(folderId); + // 获取与文件夹关联的小部件信息 + HashSet widgets = DataUtils.getFolderNoteWidget(mContentResolver, folderId); + // 如果不处于同步模式,直接删除文件夹 + if (!isSyncMode()) { + DataUtils.batchDeleteNotes(mContentResolver, ids); + } else { + // 在同步模式下,将被删除的文件夹移动到回收站文件夹 + DataUtils.batchMoveToFolder(mContentResolver, ids, Notes.ID_TRASH_FOLER); + } + // 更新与该文件夹关联的小部件 + 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对象,指定目标为NoteEditActivity,并设置操作为ACTION_VIEW + Intent intent = new Intent(this, NoteEditActivity.class); + intent.setAction(Intent.ACTION_VIEW); + // 将笔记项的ID作为额外的UID数据传递给Intent + intent.putExtra(Intent.EXTRA_UID, data.getId()); + // 启动NoteEditActivity,并获取结果 + this.startActivityForResult(intent, REQUEST_CODE_OPEN_NODE); + } + private void openFolder(NoteItemData data) { + // 将当前文件夹ID设置为给定数据项的ID + mCurrentFolderId = data.getId(); + // 启动异步查询以获取当前文件夹下的笔记列表 + startAsyncNotesListQuery(); + // 根据文件夹ID设置状态和标题栏的文本 + if (data.getId() == Notes.ID_CALL_RECORD_FOLDER) { + mState = ListEditState.CALL_RECORD_FOLDER; + mAddNewNote.setVisibility(View.GONE); + mTitleBar.setText(R.string.call_record_folder_name); + } else { + mState = ListEditState.SUB_FOLDER; + mTitleBar.setText(data.getSnippet()); + } + // 显示标题栏 + mTitleBar.setVisibility(View.VISIBLE); + } + public void onClick(View v) { + switch (v.getId()) { + case R.id.btn_new_note: + // 点击“新建笔记”按钮时,调用createNewNote()方法 + createNewNote(); + break; + default: + break; + } + } + private void showSoftInput() { + // 获取输入法管理器实例 + InputMethodManager inputMethodManager = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE); + // 检查输入法管理器是否为空 + if (inputMethodManager != null) { + // 切换显示软键盘 + // 参数1:显示软键盘的标志,使用SHOW_FORCED表示强制显示 + // 参数2:指定额外的标志,此处为0 + 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) { + // 创建AlertDialog.Builder实例 + final AlertDialog.Builder builder = new AlertDialog.Builder(this); + // 加载布局文件dialog_edit_text.xml作为对话框的内容视图 + View view = LayoutInflater.from(this).inflate(R.layout.dialog_edit_text, null); + // 获取布局文件中的EditText控件 + final EditText etName = (EditText) view.findViewById(R.id.et_foler_name); + // 显示软键盘 + showSoftInput(); + // 根据create参数确定对话框的标题和EditText的初始文本 + if (!create) { + if (mFocusNoteDataItem != null) { + etName.setText(mFocusNoteDataItem.getSnippet()); + builder.setTitle(getString(R.string.menu_folder_change_name)); + } else { + // 如果长按的数据项mFocusNoteDataItem为null,记录错误日志并返回 + Log.e(TAG, "The long click data item is null"); + return; + } + } else { + etName.setText(""); + builder.setTitle(this.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; + } + // 根据create参数确定是修改文件夹还是创建新文件夹 + if (!create) { + // 修改文件夹 + if (!TextUtils.isEmpty(name)) { + ContentValues values = new ContentValues(); + values.put(NoteColumns.SNIPPET, name); + values.put(NoteColumns.TYPE, Notes.TYPE_FOLDER); + values.put(NoteColumns.LOCAL_MODIFIED, 1); + mContentResolver.update(Notes.CONTENT_NOTE_URI, values, NoteColumns.ID + + "=?", new String[] { + String.valueOf(mFocusNoteDataItem.getId()) + }); + } + } else if (!TextUtils.isEmpty(name)) { + // 创建新文件夹 + ContentValues values = new ContentValues(); + values.put(NoteColumns.SNIPPET, name); + values.put(NoteColumns.TYPE, Notes.TYPE_FOLDER); + mContentResolver.insert(Notes.CONTENT_NOTE_URI, values); + } + // 关闭对话框 + dialog.dismiss(); + } + }); + // 如果EditText的文本为空,禁用确定按钮 + if (TextUtils.isEmpty(etName.getText())) { + positive.setEnabled(false); + } + // EditText文本改变时,根据文本是否为空来启用或禁用确定按钮 + 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 + } + }); + } + /** + * 按返回键时根据情况更改类中的数据 + */ + @Override + public void onBackPressed() { + // 根据当前的状态进行处理 + switch (mState) { + case SUB_FOLDER: + // 如果当前状态是SUB_FOLDER,则回退到根文件夹 + mCurrentFolderId = Notes.ID_ROOT_FOLDER; + mState = ListEditState.NOTE_LIST; + startAsyncNotesListQuery(); + mTitleBar.setVisibility(View.GONE); + break; + case CALL_RECORD_FOLDER: + // 如果当前状态是CALL_RECORD_FOLDER,则回退到根文件夹 + mCurrentFolderId = Notes.ID_ROOT_FOLDER; + mState = ListEditState.NOTE_LIST; + mAddNewNote.setVisibility(View.VISIBLE); + mTitleBar.setVisibility(View.GONE); + startAsyncNotesListQuery(); + break; + case NOTE_LIST: + // 如果当前状态是NOTE_LIST,则调用父类的onBackPressed方法 + super.onBackPressed(); + break; + default: + break; + } + } + 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; + } + // 将小部件ID作为额外数据添加到意图中 + intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, new int[] { + appWidgetId + }); + // 发送广播意图,通知小部件进行更新 + sendBroadcast(intent); + // 设置结果为RESULT_OK,表示更新小部件成功 + setResult(RESULT_OK, intent); + } + private final OnCreateContextMenuListener mFolderOnCreateContextMenuListener = new OnCreateContextMenuListener() { + public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) { + // 检查当前焦点的笔记数据项是否为null + if (mFocusNoteDataItem != null) { + // 设置上下文菜单的标题为焦点笔记数据项的摘要 + menu.setHeaderTitle(mFocusNoteDataItem.getSnippet()); + // 添加菜单项到上下文菜单 + // 参数解释:(groupId, itemId, order, title) + menu.add(0, MENU_FOLDER_VIEW, 0, R.string.menu_folder_view); + menu.add(0, MENU_FOLDER_DELETE, 0, R.string.menu_folder_delete); + menu.add(0, MENU_FOLDER_CHANGE_NAME, 0, R.string.menu_folder_change_name); + } + } + }; + @Override + public void onContextMenuClosed(Menu menu) { + // 当上下文菜单关闭时调用该方法 + if (mNotesListView != null) { + // 将mNotesListView的OnCreateContextMenuListener设置为null,以取消菜单监听器 + mNotesListView.setOnCreateContextMenuListener(null); + } + super.onContextMenuClosed(menu); + } + @Override + public boolean onContextItemSelected(MenuItem item) { + // 当上下文菜单项被选中时调用该方法 + if (mFocusNoteDataItem == null) { + // 检查焦点笔记数据项是否为null + Log.e(TAG, "The long click data item is null"); + return false; + } + switch (item.getItemId()) { + case MENU_FOLDER_VIEW: + // 如果选中的菜单项是MENU_FOLDER_VIEW,则打开文件夹 + openFolder(mFocusNoteDataItem); + break; + case MENU_FOLDER_DELETE: + // 如果选中的菜单项是MENU_FOLDER_DELETE,则显示删除文件夹的对话框 + AlertDialog.Builder builder = new AlertDialog.Builder(this); + builder.setTitle(getString(R.string.alert_title_delete)); + builder.setIcon(android.R.drawable.ic_dialog_alert); + builder.setMessage(getString(R.string.alert_message_delete_folder)); + builder.setPositiveButton(android.R.string.ok, + 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: + // 如果选中的菜单项是MENU_FOLDER_CHANGE_NAME,则显示创建或修改文件夹的对话框 + showCreateOrModifyFolderDialog(false); + break; + default: + break; + } + return true; + } + + @Override + public boolean onPrepareOptionsMenu(Menu menu) { + // 当准备显示选项菜单时调用该方法 + menu.clear(); // 清空菜单项 + // 根据当前状态(mState)选择要显示的菜单项 + if (mState == ListEditState.NOTE_LIST) { + // 如果状态为NOTE_LIST,则从资源文件中填充note_list菜单项 + getMenuInflater().inflate(R.menu.note_list, menu); + // 根据同步状态设置菜单项的标题为“sync”或“sync_cancel” + menu.findItem(R.id.menu_sync).setTitle( + GTaskSyncService.isSyncing() ? R.string.menu_sync_cancel : R.string.menu_sync); + } else if (mState == ListEditState.SUB_FOLDER) { + // 如果状态为SUB_FOLDER,则从资源文件中填充sub_folder菜单项 + getMenuInflater().inflate(R.menu.sub_folder, menu); + } else if (mState == ListEditState.CALL_RECORD_FOLDER) { + // 如果状态为CALL_RECORD_FOLDER,则从资源文件中填充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: { + // 如果选中的菜单项是menu_new_folder,则显示创建或修改文件夹的对话框 + showCreateOrModifyFolderDialog(true); + break; + } + case R.id.menu_export_text: { + // 如果选中的菜单项是menu_export_text,则导出笔记为文本文件 + exportNoteToText(); + break; + } + case R.id.menu_sync: { + // 如果选中的菜单项是menu_sync,则根据同步模式进行相应操作 + if (isSyncMode()) { + if (TextUtils.equals(item.getTitle(), getString(R.string.menu_sync))) { + // 如果菜单项标题为"menu_sync",则启动同步服务 + GTaskSyncService.startSync(this); + } else { + // 如果菜单项标题为"menu_sync_cancel",则取消同步服务 + GTaskSyncService.cancelSync(this); + } + } else { + // 如果不是同步模式,则启动首选项活动 + startPreferenceActivity(); + } + break; + } + case R.id.menu_setting: { + // 如果选中的菜单项是menu_setting,则启动首选项活动 + startPreferenceActivity(); + break; + } + case R.id.menu_new_note: { + // 如果选中的菜单项是menu_new_note,则创建新的笔记 + createNewNote(); + break; + } + case R.id.menu_search: + // 如果选中的菜单项是menu_search,则启动搜索 + onSearchRequested(); + break; + default: + break; + } + return true; + } + @Override + public boolean onSearchRequested() { + // 当搜索请求时调用该方法 + startSearch(null, false, null /* appData */, false); // 启动搜索 + return true; + } + private void exportNoteToText() { + // 获取BackupUtils的实例 + final BackupUtils backup = BackupUtils.getInstance(NotesListActivity.this); + // 创建一个异步任务 + new AsyncTask() { + @Override + protected Integer doInBackground(Void... unused) { + // 在后台线程中执行导出笔记为文本的操作 + return backup.exportToText(); + } + @Override + protected void onPostExecute(Integer result) { + // 在后台线程执行完毕后,在主线程中根据导出结果显示相应的对话框 + if (result == BackupUtils.STATE_SD_CARD_UNMOUONTED) { + // 如果导出失败,SD卡未挂载,则显示失败对话框 + 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 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; + default: + break; + } + } + } + } + + private void startQueryDestinationFolders() { + // 构建查询条件 + String selection = NoteColumns.TYPE + "=? AND " + NoteColumns.PARENT_ID + "<>? AND " + NoteColumns.ID + "<>?"; + // 根据当前的列表编辑状态(mState)确定最终的查询条件 + 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) { + // 检查长按的视图是否为NotesListItem + if (view instanceof NotesListItem) { + // 获取长按的笔记项数据 + mFocusNoteDataItem = ((NotesListItem) view).getItemData(); + // 检查笔记项类型是否为Note类型且不在选择模式下 + if (mFocusNoteDataItem.getType() == Notes.TYPE_NOTE && !mNotesListAdapter.isInChoiceMode()) { + // 启动操作模式(ActionMode)以支持多选操作 + if (mNotesListView.startActionMode(mModeCallBack) != null) { + // 更新选中项的状态,并执行长按反馈(震动) + mModeCallBack.onItemCheckedStateChanged(null, position, id, true); + mNotesListView.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS); + } else { + // 启动操作模式失败 + Log.e(TAG, "startActionMode fails"); + } + } else if (mFocusNoteDataItem.getType() == Notes.TYPE_FOLDER) { + // 如果笔记项类型为Folder类型,则设置上下文菜单监听器 + mNotesListView.setOnCreateContextMenuListener(mFolderOnCreateContextMenuListener); + } + } + return false; + } +}