/* * Copyright (c) 2010 - 2011, The MiCode Open Source Community (www.micode.net) * * 以下是版权相关声明,说明该代码遵循Apache License 2.0许可证进行开源使用。 * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * 意味着只有在符合许可证规定的情况下才能使用本文件。 * 你可以通过以下网址获取许可证副本: * http://www.apache.org/licenses/LICENSE-2.0 * * 除非适用法律要求或者书面同意,否则依据本许可证分发的软件按“原样”提供, * 不附带任何明示或暗示的保证或条件,关于具体的权限和限制,可查看许可证内容。 * 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. */ // 包声明,表明该类所属的包名,这里属于net.micode.notes.ui包,通常意味着该类与应用的用户界面相关功能有关 package net.micode.notes.ui; // 导入各种Android系统相关的类,用于实现Activity的基本功能、对话框、小部件管理、异步查询、内容提供器操作等功能 import android.app.Activity; import android.app.AlertDialog; import android.app.Dialog; import android.appwidget.AppWidgetManager; import android.content.AsyncQueryHandler; import android.content.ContentResolver; import android.content.ContentValues; import android.content.Context; import android.content.DialogInterface; import android.content.Intent; import android.content.SharedPreferences; import android.database.Cursor; import android.os.AsyncTask; import android.os.Bundle; import android.preference.PreferenceManager; import android.text.Editable; import android.text.TextUtils; import android.text.TextWatcher; import android.util.Log; import android.view.ActionMode; import android.view.ContextMenu; import android.view.ContextMenu.ContextMenuInfo; import android.view.Display; import android.view.HapticFeedbackConstants; import android.view.LayoutInflater; import android.view.Menu; import android.view.MenuItem; import android.view.MenuItem.OnMenuItemClickListener; import android.view.MotionEvent; import android.view.View; import android.view.View.OnClickListener; import android.view.View.OnCreateContextMenuListener; import android.view.View.OnTouchListener; import android.view.inputmethod.InputMethodManager; import android.widget.AdapterView; import android.widget.AdapterView.OnItemClickListener; import android.widget.AdapterView.OnItemLongClickListener; import android.widget.Button; import android.widget.EditText; import android.widget.ListView; import android.widget.PopupMenu; import android.widget.TextView; import android.widget.Toast; // 导入项目自定义的各类资源、数据模型、工具类等,用于处理笔记相关的数据、界面展示以及各种业务逻辑 import net.micode.notes.R; import net.micode.notes.data.Notes; import net.micode.notes.data.Notes.NoteColumns; import net.micode.notes.gtask.remote.GTaskSyncService; import net.micode.notes.model.WorkingNote; import net.micode.notes.tool.BackupUtils; import net.micode.notes.tool.DataUtils; import net.micode.notes.tool.ResourceParser; import net.micode.notes.ui.NotesListAdapter.AppWidgetAttribute; import net.micode.notes.widget.NoteWidgetProvider_2x; import net.micode.notes.widget.NoteWidgetProvider_4x; // 导入Java标准库中用于处理输入流读取的相关类,可能用于读取文件等操作 import java.io.BufferedReader; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.util.HashSet; // NotesListActivity类继承自Activity,是Android应用中一个用于展示笔记列表的界面类,同时实现了点击和长按列表项的监听器接口 public class NotesListActivity extends Activity implements OnClickListener, OnItemLongClickListener { // 定义一个常量,用于标识文件夹内笔记列表查询任务的令牌(token),在异步查询机制中作为任务的唯一标识,方便区分不同类型的查询操作 private static final int FOLDER_NOTE_LIST_QUERY_TOKEN = 0; // 定义一个常量,用于标识文件夹列表查询任务的令牌(token),同样用于异步查询时区分不同的查询任务 private static final int FOLDER_LIST_QUERY_TOKEN = 1; // 定义一个常量,作为菜单中“删除文件夹”菜单项的唯一ID,用于在菜单操作中识别该菜单项 private static final int MENU_FOLDER_DELETE = 0; // 定义一个常量,作为菜单中“查看文件夹”菜单项的唯一ID,用于在菜单操作中识别该菜单项 private static final int MENU_FOLDER_VIEW = 1; // 定义一个常量,作为菜单中“修改文件夹名称”菜单项的唯一ID,用于在菜单操作中识别该菜单项 private static final int MENU_FOLDER_CHANGE_NAME = 2; // 定义一个字符串常量,作为存储是否添加应用介绍相关偏好设置的键名,用于在SharedPreferences中进行相应设置的读写操作 private static final String PREFERENCE_ADD_INTRODUCTION = "net.micode.notes.introduction"; // 定义一个枚举类型,用于表示列表编辑的不同状态,包含笔记列表、子文件夹、通话记录文件夹三种情况,方便根据不同状态执行不同的业务逻辑 private enum ListEditState { NOTE_LIST, SUB_FOLDER, CALL_RECORD_FOLDER }; // 定义一个变量,用于记录当前列表的编辑状态,初始化为NOTE_LIST状态,表示默认处于笔记列表编辑状态 private ListEditState mState = ListEditState.NOTE_LIST; // 定义一个变量,用于处理后台数据库查询操作的异步查询处理器,通过它可以在后台线程中执行数据库相关的查询任务,避免阻塞主线程 private BackgroundQueryHandler mBackgroundQueryHandler; // 定义一个变量,作为笔记列表的适配器,负责将笔记数据适配到ListView上进行展示,管理数据与视图之间的绑定关系 private NotesListAdapter mNotesListAdapter; // 定义一个变量,代表用于展示笔记列表的ListView控件,用户可以在界面上看到笔记列表并与之交互 private ListView mNotesListView; // 定义一个变量,代表“新建笔记”按钮,用户点击该按钮可触发新建笔记的操作 private Button mAddNewNote; // 定义一个布尔变量,用于标记是否进行事件分发操作,初始值为false,在触摸事件处理逻辑中根据具体情况进行设置和判断 private boolean mDispatch = false; // 定义一个整型变量,用于记录触摸事件起始的Y坐标,在处理触摸相关的操作(如滑动等)时,用于计算位置变化等情况 private int mOriginY = 0; // 定义一个整型变量,用于记录在分发触摸事件时的Y坐标,同样在触摸事件处理流程中发挥作用,辅助传递准确的触摸位置信息 private int mDispatchY = 0; // 定义一个变量,代表标题栏的TextView控件,通常用于显示页面的标题信息,也可能包含其他相关的文本提示等内容 private TextView mTitleBar; // 定义一个长整型变量,用于记录当前所在文件夹的ID,通过该ID可以确定当前操作的笔记所属的文件夹,方便进行数据筛选和展示等操作 private long mCurrentFolderId; // 定义一个变量,用于获取应用的ContentResolver实例,通过它可以与应用的内容提供器进行交互,实现数据的增删改查等操作 private ContentResolver mContentResolver; // 定义一个变量,作为实现了ListView的多选模式监听器和菜单项点击监听器接口的回调类实例,用于处理ListView在多选模式下以及菜单项点击时的相关业务逻辑 private ModeCallback mModeCallBack; // 定义一个字符串常量,作为日志输出的标签,方便在调试时通过该标签在Logcat中筛选出该类相关的日志信息,便于排查问题和查看运行状态 private static final String TAG = "NotesListActivity"; // 定义一个公共静态常量,用于表示笔记列表视图滚动的速率,具体的单位和实际使用场景需要结合滚动相关的具体逻辑来确定,可能是像素/单位时间等,用于控制列表滚动的速度效果 public static final int NOTES_LISTVIEW_SCROLL_RATE = 30; // 定义一个变量,用于记录当前获取焦点的笔记数据项,通过该对象可以获取和操作当前用户关注的笔记相关的各种属性信息,比如笔记的内容、ID等 private NoteItemData mFocusNoteDataItem; // 定义一个静态字符串常量,作为查询普通笔记的选择条件语句,通过匹配笔记的父级ID来筛选相应的笔记,其中的问号部分在实际查询时会传入具体的文件夹ID值进行查询 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)"; // 定义一个静态常量,作为请求码,用于标识打开笔记节点的操作,在Activity的onActivityResult方法中,通过该请求码来判断是哪个操作返回的结果,以便进行相应的后续处理 private final static int REQUEST_CODE_OPEN_NODE = 102; // 定义一个静态常量,作为请求码,用于标识新建笔记节点的操作,同样在onActivityResult方法中用于区分不同来源的返回结果,执行对应的逻辑 private final static int REQUEST_CODE_NEW_NODE = 103; // 重写Activity的onCreate方法,该方法在Activity创建时被调用,用于进行各种初始化操作 @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); // 设置该Activity对应的布局文件,这里通过R.layout.note_list找到对应的XML布局资源,将界面布局加载到Activity中 setContentView(R.layout.note_list); // 调用initResources方法,初始化Activity中需要使用的各种资源,如视图控件、数据适配器等 initResources(); /** * 在用户首次使用该应用时插入一个应用介绍,可能是弹出一个引导界面或者添加一条示例笔记等形式, * 具体实现逻辑在setAppInfoFromRawRes方法中,目的是帮助用户快速了解和使用应用。 */ setAppInfoFromRawRes(); } // 重写Activity的onActivityResult方法,该方法用于接收来自其他Activity返回的结果 @Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { // 首先判断返回的结果码是否为RESULT_OK,表示操作成功完成,并且请求码是打开笔记节点(REQUEST_CODE_OPEN_NODE)或者新建笔记节点(REQUEST_CODE_NEW_NODE)的请求码 if (resultCode == RESULT_OK && (requestCode == REQUEST_CODE_OPEN_NODE || requestCode == REQUEST_CODE_NEW_NODE)) { // 如果满足上述条件,调用笔记列表适配器(mNotesListAdapter)的changeCursor方法,并传入null参数。 // 这可能是为了通知适配器数据发生了变化(比如新的笔记创建或者已有笔记被打开等情况),使其刷新显示内容, // 传入null可能是让适配器重新获取数据的一种触发方式,具体取决于适配器内部的实现逻辑。 mNotesListAdapter.changeCursor(null); } else { // 如果不满足上述判断条件,即返回结果不符合预期或者不是来自打开或新建笔记节点的操作, // 则调用父类(Activity)的onActivityResult方法,继续执行默认的结果处理逻辑,确保不会遗漏其他可能的情况。 super.onActivityResult(requestCode, resultCode, data); } } // 私有方法,用于从原始资源文件中设置应用信息,可能是创建一个初始的引导笔记之类的功能 private void setAppInfoFromRawRes() { // 获取默认的共享偏好设置实例,通过PreferenceManager来获取应用的默认SharedPreferences对象, // 可以用于读取和存储一些简单的键值对形式的配置信息,比如应用的各种设置选项、首次使用标记等。 SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(this); // 判断在共享偏好设置中,是否不存在名为PREFERENCE_ADD_INTRODUCTION(其值为"net.micode.notes.introduction")的键对应的布尔值为true的情况, // 即判断是否还没有添加过应用介绍信息,如果是首次使用或者还未执行过此操作,则进入以下逻辑。 if (!sp.getBoolean(PREFERENCE_ADD_INTRODUCTION, false)) { // 创建一个可变的字符串构建器对象,用于逐步构建要设置的应用介绍信息内容,后续会从资源文件中读取相关文本并添加到这里。 StringBuilder sb = new StringBuilder(); InputStream in = null; try { // 通过当前上下文(this)获取名为introduction的原始资源文件对应的输入流, // 这个资源文件应该是存储在项目的res/raw目录下的文本文件之类的资源,用于提供应用介绍的内容。 in = getResources().openRawResource(R.raw.introduction); if (in!= null) { // 创建一个基于输入流(in)的InputStreamReader对象,它将字节流转换为字符流,方便按字符读取文件内容, // 为后续使用BufferedReader进行高效字符读取做准备。 InputStreamReader isr = new InputStreamReader(in); // 创建一个BufferedReader对象,它带有缓冲功能,可以更高效地读取字符流,减少每次读取的系统开销, // 并提高读取大文件时的性能。 BufferedReader br = new BufferedReader(isr); // 创建一个长度为1024的字符数组,用于每次从文件中读取一定量的字符,作为缓冲区, // 可以根据实际文件大小和性能需求适当调整这个缓冲区大小。 char[] buf = new char[1024]; int len = 0; // 通过循环不断从文件中读取字符到缓冲区buf中,只要读取的字符长度大于0,就表示还有内容可读, // 然后将读取到的字符添加到字符串构建器sb中,逐步构建完整的文件内容字符串。 while ((len = br.read(buf)) > 0) { sb.append(buf, 0, len); } } else { // 如果无法成功打开资源文件获取输入流,在日志中输出错误信息,标记为读取介绍文件出错, // 并直接返回,不再继续后续操作,因为没有有效的文件内容可供使用了。 Log.e(TAG, "Read introduction file error"); return; } } catch (IOException e) { // 如果在读取文件过程中出现IO异常(比如文件不存在、权限问题等),打印异常的堆栈信息,方便排查问题, // 然后直接返回,不再继续后续操作,因为出现了异常情况导致无法正常读取文件内容。 e.printStackTrace(); return; } finally { // 在无论是否出现异常的情况下,都要尝试关闭输入流,释放相关的系统资源,避免资源泄漏。 // 如果输入流不为null,表示之前成功打开了输入流,就执行关闭操作, // 如果关闭过程中出现异常,会再次打印异常的堆栈信息,不过不会影响程序的正常流程继续向下执行。 if (in!= null) { try { in.close(); } catch (IOException e) { e.printStackTrace(); } } } // 创建一个空的工作笔记(WorkingNote)实例,通过WorkingNote类的createEmptyNote静态方法来创建, // 传入当前上下文(this)、根文件夹的ID(Notes.ID_ROOT_FOLDER,其具体值应该在Notes类中定义,用于标识根文件夹)、 // 无效的部件(AppWidget)ID(AppWidgetManager.INVALID_APPWIDGET_ID,用于表示不是与某个有效小部件关联的情况)、 // 无效的部件类型(Notes.TYPE_WIDGET_INVALIDE,同样在Notes类中定义的表示无效部件类型的常量)以及一个颜色相关的值(ResourceParser.RED,具体颜色含义需看ResourceParser类定义), // 这个工作笔记实例后续会用于存储和操作要添加的应用介绍信息内容。 WorkingNote note = WorkingNote.createEmptyNote(this, Notes.ID_ROOT_FOLDER, AppWidgetManager.INVALID_APPWIDGET_ID, Notes.TYPE_WIDGET_INVALIDE, ResourceParser.RED); // 将从资源文件中读取到的内容(存储在字符串构建器sb中,通过toString方法转换为字符串)设置为工作笔记的文本内容, // 这样就把应用介绍信息填充到了笔记对象中,准备进行保存操作。 note.setWorkingText(sb.toString()); // 调用工作笔记(note)的saveNote方法尝试保存这个包含应用介绍信息的笔记, // 如果保存操作成功(saveNote方法返回true),则表示成功将应用介绍信息以笔记的形式保存到了相应的数据存储中(可能是数据库等), // 此时将共享偏好设置中名为PREFERENCE_ADD_INTRODUCTION的键对应的布尔值设置为true,表示已经添加过应用介绍信息了, // 并通过commit方法提交这个设置更改,使其生效。 if (note.saveNote()) { sp.edit().putBoolean(PREFERENCE_ADD_INTRODUCTION, true).commit(); } else { // 如果保存笔记失败,在日志中输出错误信息,标记为保存介绍笔记出错,然后直接返回,不再继续后续操作, // 因为未能成功保存应用介绍信息到相应的数据存储中。 Log.e(TAG, "Save introduction note error"); return; } } } // 重写Activity的onStart方法,该方法在Activity开始可见时被调用,通常用于执行一些需要在界面可见时启动的操作 @Override protected void onStart() { super.onStart(); // 调用startAsyncNotesListQuery方法,该方法应该是用于启动一个异步的笔记列表查询操作, // 目的是在Activity启动并可见后,尽快获取笔记数据并展示在界面上,避免阻塞主线程,提升用户体验。 // 具体的查询逻辑和实现细节应该在startAsyncNotesListQuery方法内部定义(此处未展示完整代码)。 startAsyncNotesListQuery(); } // 私有方法,用于初始化Activity中需要使用的各种资源,比如获取ContentResolver、设置ListView的相关属性和监听器等 private void initResources() { // 获取当前上下文(this)对应的ContentResolver实例,ContentResolver用于与应用的内容提供器进行交互, // 通过它可以实现对应用内部数据(如数据库中的笔记数据等)的查询、插入、更新、删除等操作,是进行数据操作的重要接口。 mContentResolver = this.getContentResolver(); // 创建一个BackgroundQueryHandler实例,传入刚刚获取的ContentResolver对象, // BackgroundQueryHandler通常用于在后台线程中处理数据库查询相关的操作,避免在主线程执行耗时的查询任务导致界面卡顿, // 具体的查询处理逻辑应该在BackgroundQueryHandler类内部定义(此处未展示完整代码)。 mBackgroundQueryHandler = new BackgroundQueryHandler(this.getContentResolver()); // 将当前所在文件夹的ID设置为根文件夹的ID(Notes.ID_ROOT_FOLDER,其具体值在Notes类中定义,用于标识根文件夹), // 表示初始状态下操作的是根文件夹下的笔记数据,后续可能会根据用户操作等情况进行更改。 mCurrentFolderId = Notes.ID_ROOT_FOLDER; // 通过findViewById方法,根据布局文件中定义的ID(R.id.notes_list)找到对应的ListView控件, // 这个ListView就是用于展示笔记列表的视图组件,后续会将笔记数据通过适配器绑定到这个ListView上进行展示。 mNotesListView = (ListView) findViewById(R.id.notes_list); // 为ListView添加一个页脚视图,通过LayoutInflater从当前上下文(this)加载名为note_list_footer.xml的布局文件并实例化为视图对象, // 添加页脚视图可以用于展示一些额外的信息或者操作按钮等,比如加载更多、提示信息等, // 第二个参数传入null表示不需要传递额外的数据给页脚视图,第三个参数false表示这个页脚视图不是可选择的(如果是列表项则可以设置为true)。 mNotesListView.addFooterView(LayoutInflater.from(this).inflate(R.layout.note_list_footer, null), null, false); // 设置ListView的点击监听器,当用户点击列表中的某个笔记项时,会触发OnListItemClickListener类中定义的逻辑, // 具体的点击处理逻辑在OnListItemClickListener类内部实现(此处未展示完整代码)。 mNotesListView.setOnItemClickListener(new OnListItemClickListener()); // 设置ListView的长按监听器为当前类(因为当前类实现了OnItemLongClickListener接口), // 这样当用户长按列表中的笔记项时,会触发当前类中重写的onItemLongClick方法,用于处理长按相关的操作,比如弹出上下文菜单等。 mNotesListView.setOnItemLongClickListener(this); // 创建一个NotesListAdapter实例,传入当前上下文(this),这个适配器用于将笔记数据适配到ListView上进行展示, // 它会管理数据与视图之间的绑定关系,比如将笔记的各个属性显示在ListView的每一项对应的视图中, // 具体的适配逻辑在NotesListAdapter类内部定义(此处未展示完整代码)。 mNotesListAdapter = new NotesListAdapter(this); // 将创建好的笔记列表适配器(mNotesListAdapter)设置给ListView,使得ListView能够通过这个适配器获取数据并展示笔记列表, // 这样就建立了数据与视图之间的关联,让用户可以在界面上看到笔记信息。 mNotesListView.setAdapter(mNotesListAdapter); // 通过findViewById方法,根据布局文件中定义的ID(R.id.btn_new_note)找到对应的“新建笔记”按钮控件, // 这个按钮用于用户点击后触发新建笔记的操作,后续会设置相应的点击监听器来处理点击事件。 mAddNewNote = (Button) findViewById(R.id.btn_new_note); // 设置“新建笔记”按钮的点击监听器为当前类(因为当前类实现了OnClickListener接口), // 这样当用户点击这个按钮时,会触发当前类中重写的onClick方法,用于处理新建笔记相关的操作逻辑, // 具体的点击处理逻辑在当前类重写的onClick方法中实现(此处未展示完整代码)。 mAddNewNote.setOnClickListener(this); // 设置“新建笔记”按钮的触摸监听器,通过传入一个新创建的NewNoteOnTouchListener实例来处理触摸按钮相关的操作逻辑, // 比如触摸滑动、按下抬起等事件的处理,具体的触摸事件处理逻辑在NewNoteOnTouchListener类内部定义(此处未展示完整代码)。 mAddNewNote.setOnTouchListener(new NewNoteOnTouchListener()); // 初始化用于标记是否进行事件分发的布尔变量mDispatch为false,表示初始状态下不进行事件分发操作, // 这个变量会在触摸事件处理逻辑中根据具体情况进行更新和判断,用于控制触摸事件的流向等情况。 mDispatch = false; // 初始化用于记录在分发触摸事件时的Y坐标的变量mDispatchY为0,用于在触摸事件处理过程中记录和传递准确的触摸位置信息, // 特别是在涉及触摸滑动等操作时,会根据这个坐标的变化来进行相应的逻辑处理。 mDispatchY = 0; // 初始化用于记录触摸事件起始的Y坐标的变量mOriginY为0,同样在触摸事件处理中起到记录起始位置的作用, // 与mDispatchY配合使用,用于计算触摸操作过程中的位置变化等情况。 mOriginY = 0; // 通过findViewById方法,根据布局文件中定义的ID(R.id.tv_title_bar)找到对应的标题栏TextView控件, // 这个TextView通常用于显示页面的标题信息,也可能包含其他相关的文本提示等内容,具体显示内容会根据应用的逻辑进行设置。 mTitleBar = (TextView) findViewById(R.id.tv_title_bar); // 设置当前列表的编辑状态为笔记列表状态(ListEditState.NOTE_LIST),表示初始情况下处于普通的笔记列表编辑模式, // 后续可能会根据用户操作等情况更改为子文件夹、通话记录文件夹等其他编辑状态,不同状态下会有不同的操作逻辑和界面展示效果。 mState = ListEditState.NOTE_LIST; // 创建一个ModeCallback实例,这个实例实现了ListView的多选模式监听器和菜单项点击监听器接口, // 用于处理ListView在多选模式下以及菜单项点击时的相关业务逻辑,比如显示多选操作菜单、处理菜单点击事件等, // 具体的逻辑在ModeCallback类内部实现(此处未展示完整代码)。 mModeCallBack = new ModeCallback(); } // 定义一个私有内部类ModeCallback,它实现了ListView.MultiChoiceModeListener(用于处理ListView的多选模式相关事件) // 和OnMenuItemClickListener(用于处理菜单项点击事件)两个接口,用于处理笔记列表在多选模式下的各种交互逻辑 private class ModeCallback implements ListView.MultiChoiceModeListener, OnMenuItemClickListener { // 定义一个成员变量,用于存储下拉菜单实例,这个下拉菜单可能用于展示一些额外的操作选项或者提供快捷操作入口等功能 private DropdownMenu mDropDownMenu; // 定义一个成员变量,用于保存当前的ActionMode实例,ActionMode用于管理多选模式下的操作界面以及相关状态等 private ActionMode mActionMode; // 定义一个成员变量,用于指向菜单中的“移动”菜单项,方便后续对该菜单项进行显示隐藏以及点击事件处理等操作 private MenuItem mMoveMenu; // 当ListView进入多选模式时,会调用此方法来创建多选模式的操作界面,进行一些初始化设置 public boolean onCreateActionMode(ActionMode mode, Menu menu) { // 通过菜单填充器(getMenuInflater)将名为note_list_options.xml的菜单布局文件加载到传入的菜单(menu)中, // 这样就为多选模式创建了对应的操作菜单,菜单中的各个菜单项可以在这个布局文件中定义,包含了如删除、移动等操作选项。 getMenuInflater().inflate(R.menu.note_list_options, menu); // 找到菜单中ID为R.id.delete的菜单项(通常是“删除”操作菜单项),并设置它的点击监听器为当前类(this), // 意味着点击这个“删除”菜单项时,会触发当前类中实现的onMenuItemClick方法来处理相应逻辑。 menu.findItem(R.id.delete).setOnMenuItemClickListener(this); // 获取菜单中ID为R.id.move的菜单项(即“移动”菜单项),并赋值给mMoveMenu变量,方便后续操作。 mMoveMenu = menu.findItem(R.id.move); // 判断当前获取焦点的笔记数据项(mFocusNoteDataItem)的父级ID是否等于通话记录文件夹的ID(Notes.ID_CALL_RECORD_FOLDER), // 或者通过DataUtils工具类获取的用户创建的文件夹数量是否为0,如果满足这两个条件之一,说明当前情况可能不适合进行笔记移动操作。 if (mFocusNoteDataItem.getParentId() == Notes.ID_CALL_RECORD_FOLDER || DataUtils.getUserFolderCount(mContentResolver) == 0) { // 如果不适合移动操作,将“移动”菜单项设置为不可见,避免用户误操作或者操作无效的情况。 mMoveMenu.setVisible(false); } else { // 如果适合移动操作,将“移动”菜单项设置为可见,并设置它的点击监听器为当前类(this), // 这样点击“移动”菜单项时同样会触发当前类中实现的onMenuItemClick方法来处理移动相关的逻辑。 mMoveMenu.setVisible(true); mMoveMenu.setOnMenuItemClickListener(this); } // 将传入的ActionMode实例保存到成员变量mActionMode中,方便后续在其他方法中对这个多选模式进行操作,比如结束多选模式等。 mActionMode = mode; // 调用笔记列表适配器(mNotesListAdapter)的setChoiceMode方法,传入true参数,将笔记列表设置为多选模式, // 使得ListView可以支持多选操作,用户能够选择多个笔记项进行批量操作。 mNotesListAdapter.setChoiceMode(true); // 将ListView的长按可点击属性设置为false,因为进入多选模式后,长按操作通常由多选模式的逻辑来处理, // 避免长按操作出现重复响应或者冲突的情况。 mNotesListView.setLongClickable(false); // 将“新建笔记”按钮(mAddNewNote)的可见性设置为不可见(View.GONE),在多选模式下可能不需要显示该按钮, // 避免干扰用户对多选操作的注意力以及操作界面的简洁性。 mAddNewNote.setVisibility(View.GONE); // 通过LayoutInflater从NotesListActivity的上下文(this)加载名为note_list_dropdown_menu.xml的布局文件, // 并实例化为一个视图对象(customView),这个视图将作为自定义的下拉菜单视图,用于展示一些额外的操作选项等。 View customView = LayoutInflater.from(NotesListActivity.this).inflate( R.layout.note_list_dropdown_menu, null); // 将刚刚创建的自定义视图(customView)设置为当前ActionMode的自定义视图,这样就可以在多选模式的界面上展示这个自定义的下拉菜单视图了。 mode.setCustomView(customView); // 创建一个DropdownMenu实例,传入NotesListActivity的上下文(this)、自定义视图中ID为R.id.selection_menu的按钮(通常作为触发下拉菜单展开的按钮), // 以及名为note_list_dropdown的菜单资源(用于定义下拉菜单中的具体菜单项内容),用于管理和操作这个下拉菜单相关的逻辑。 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(), // 这个操作的目的是切换笔记列表的全选状态,如果当前不是全选则全选所有笔记项,如果当前是全选则取消全选,实现一个快捷的全选/取消全选功能。 mNotesListAdapter.selectAll(!mNotesListAdapter.isAllSelected()); // 调用updateMenu方法,用于更新下拉菜单的显示内容,比如根据当前选中的笔记数量更新菜单标题以及相关菜单项的文本和选中状态等。 updateMenu(); return true; } }); return true; } // 用于更新下拉菜单的显示内容的私有方法,根据当前选中的笔记数量等情况来调整菜单的标题、菜单项的选中状态和文本等信息 private void updateMenu() { // 获取笔记列表适配器(mNotesListAdapter)中当前被选中的笔记数量,通过调用getSelectedCount方法来获取。 int selectedCount = mNotesListAdapter.getSelectedCount(); // 通过当前上下文(getResources)获取一个格式化字符串资源,这个字符串资源的格式定义在strings.xml文件中, // 名为menu_select_title,它可能包含一个占位符用于显示选中的笔记数量,这里将选中数量作为参数传入, // 得到一个格式化后的字符串,用于设置下拉菜单的标题,直观地展示当前选中的笔记数量情况。 String format = getResources().getString(R.string.menu_select_title, selectedCount); mDropDownMenu.setTitle(format); // 在下拉菜单(mDropDownMenu)中查找ID为R.id.action_select_all的菜单项(通常是用于全选/取消全选的菜单项), // 如果找到了这个菜单项(item不为null),则进行以下操作来更新它的选中状态和文本显示内容。 MenuItem item = mDropDownMenu.findItem(R.id.action_select_all); if (item!= null) { // 如果笔记列表适配器(mNotesListAdapter)当前处于全选状态(通过调用isAllSelected方法判断), // 则将这个全选菜单项设置为选中状态(setChecked(true)),并将它的标题文本设置为取消全选的提示文本(R.string.menu_deselect_all), // 这样用户可以直观地看到当前的全选状态以及点击该菜单项后会执行的操作。 if (mNotesListAdapter.isAllSelected()) { item.setChecked(true); item.setTitle(R.string.menu_deselect_all); } else { // 如果笔记列表适配器当前不是全选状态,则将这个全选菜单项设置为未选中状态(setChecked(false)), // 并将它的标题文本设置为全选的提示文本(R.string.menu_select_all),同样方便用户了解操作功能和当前状态。 item.setChecked(false); item.setTitle(R.string.menu_select_all); } } } // 当多选模式的操作菜单需要准备显示时(例如在菜单显示前可能需要根据某些条件动态调整菜单项的可用性等情况),会调用此方法, // 目前此方法体中没有具体实现逻辑(只是一个占位符,返回false),如果后续有相关需求,可以在这里添加相应的代码来处理准备菜单显示的操作。 public boolean onPrepareActionMode(ActionMode mode, Menu menu) { // TODO Auto-generated method stub return false; } // 当用户点击多选模式操作菜单中的菜单项时,会调用此方法来处理相应的点击事件,目前此方法体中没有具体实现逻辑(只是一个占位符,返回false), // 如果要处理具体的菜单项点击操作逻辑,比如根据不同菜单项执行不同的数据操作、界面更新等,需要在这里添加相应的代码来实现功能。 public boolean onActionItemClicked(ActionMode mode, MenuItem item) { // TODO Auto-generated method stub return false; } // 当多选模式结束时(例如用户点击了返回键或者通过代码主动结束多选模式等情况),会调用此方法来进行一些清理和界面恢复操作 public void onDestroyActionMode(ActionMode mode) { // 调用笔记列表适配器(mNotesListAdapter)的setChoiceMode方法,传入false参数,将笔记列表从多选模式恢复为普通模式, // 使得ListView不再支持多选操作,恢复到正常的单选项点击等交互方式。 mNotesListAdapter.setChoiceMode(false); // 将ListView的长按可点击属性设置为true,恢复长按操作的响应能力,因为多选模式已经结束,长按操作可以按照常规逻辑进行处理了。 mNotesListView.setLongClickable(true); // 将“新建笔记”按钮(mAddNewNote)的可见性设置为可见(View.VISIBLE),在多选模式结束后重新显示该按钮,方便用户继续进行新建笔记等操作。 mAddNewNote.setVisibility(View.VISIBLE); } // 用于结束当前的多选模式,通过调用保存的ActionMode实例(mActionMode)的finish方法来实现, // 这会触发onDestroyActionMode等相关方法来进行多选模式结束后的清理和界面恢复操作。 public void finishActionMode() { mActionMode.finish(); } // 当ListView中的某个笔记项的选中状态发生改变时(例如用户点击选择或者取消选择某个笔记项),会调用此方法来处理相应的逻辑 public void onItemCheckedStateChanged(ActionMode mode, int position, long id, boolean checked) { // 调用笔记列表适配器(mNotesListAdapter)的setCheckedItem方法,传入笔记项的位置(position)和新的选中状态(checked)参数, // 用于更新笔记列表适配器中对应笔记项的选中状态记录,确保数据和界面显示的选中状态保持一致。 mNotesListAdapter.setCheckedItem(position, checked); // 调用updateMenu方法,更新下拉菜单的显示内容,因为笔记项的选中状态改变了,可能需要相应地更新菜单标题、全选菜单项状态等信息, // 保持下拉菜单展示的信息与当前选中情况相符。 updateMenu(); } // 当用户点击菜单项时(无论是多选模式操作菜单还是下拉菜单中的菜单项),会调用此方法来处理相应的点击事件逻辑 public boolean onMenuItemClick(MenuItem item) { // 首先判断笔记列表适配器(mNotesListAdapter)中当前被选中的笔记数量是否为0,如果是0,表示没有选中任何笔记项, // 在这种情况下,弹出一个Toast提示信息,告知用户没有选中任何笔记,显示的文本内容通过getString方法获取strings.xml文件中定义的 // menu_select_none字符串资源,显示时长为短暂(Toast.LENGTH_SHORT),然后直接返回true,表示已经处理了这个点击事件,避免继续执行其他不必要的逻辑。 if (mNotesListAdapter.getSelectedCount() == 0) { Toast.makeText(NotesListActivity.this, getString(R.string.menu_select_none), Toast.LENGTH_SHORT).show(); return true; } // 根据点击的菜单项的ID(item.getItemId())来判断具体是哪个菜单项被点击了,然后执行相应的业务逻辑。 switch (item.getItemId()) { case R.id.delete: // 如果点击的是“删除”菜单项(ID为R.id.delete),创建一个AlertDialog.Builder实例,用于构建一个确认删除的对话框, // 传入NotesListActivity的上下文(this),以便对话框能够正确显示在当前界面上。 AlertDialog.Builder builder = new AlertDialog.Builder(NotesListActivity.this); // 设置对话框的标题文本,通过getString方法获取strings.xml文件中定义的alert_title_delete字符串资源作为标题内容, // 通常用于提示用户当前操作是删除相关的操作。 builder.setTitle(getString(R.string.alert_title_delete)); // 设置对话框的图标,使用Android系统自带的警告图标(android.R.drawable.ic_dialog_alert),增强提示效果,让用户直观地知道这是一个重要操作的提示对话框。 builder.setIcon(android.R.drawable.ic_dialog_alert); // 设置对话框的消息内容,通过getString方法获取strings.xml文件中定义的alert_message_delete_notes字符串资源, // 并传入笔记列表适配器中当前被选中的笔记数量(mNotesListAdapter.getSelectedCount())作为参数进行格式化, // 这样消息内容可以准确地提示用户将要删除的笔记数量,让用户明确操作的影响范围。 builder.setMessage(getString(R.string.alert_message_delete_notes, mNotesListAdapter.getSelectedCount())); // 设置对话框的“确定”按钮(PositiveButton),传入Android系统自带的确认文本(android.R.string.ok)作为按钮文本, // 并设置点击监听器,当用户点击“确定”按钮时,会触发这里定义的逻辑,即调用batchDelete方法来执行批量删除选中笔记的操作, // 具体的批量删除逻辑应该在batchDelete方法中定义(此处未展示完整代码)。 builder.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int which) { batchDelete(); } }); // 设置对话框的“取消”按钮(NegativeButton),传入Android系统自带的取消文本(android.R.string.cancel)作为按钮文本, // 并设置点击监听器为null,表示点击“取消”按钮时不执行额外的操作,只是关闭对话框,取消当前的删除操作。 builder.setNegativeButton(android.R.string.cancel, null); // 最后调用show方法显示这个构建好的确认删除对话框,让用户进行确认操作,确保重要的删除操作是经过用户明确同意的。 builder.show(); break; case R.id.move: // 如果点击的是“移动”菜单项(ID为R.id.move),调用startQueryDestinationFolders方法, // 这个方法应该是用于启动查询目标文件夹的操作,比如弹出一个界面让用户选择要将选中的笔记移动到哪个文件夹, // 具体的查询目标文件夹逻辑应该在startQueryDestinationFolders方法中定义(此处未展示完整代码)。 startQueryDestinationFolders(); break; default: // 如果点击的菜单项不是上述处理的“删除”或“移动”菜单项,返回false,表示当前类没有处理这个菜单项的点击事件, // 可能会按照默认的菜单项点击逻辑或者由其他相关的监听器来继续处理这个点击事件。 return false; } return true; } } // 定义一个私有内部类NewNoteOnTouchListener,它实现了OnTouchListener接口,用于处理“新建笔记”按钮(mAddNewNote)的触摸事件 private class NewNoteOnTouchListener implements OnTouchListener { // 重写OnTouchListener接口中的onTouch方法,该方法会在“新建笔记”按钮被触摸时触发,根据触摸事件的不同动作(如按下、移动、抬起等)执行相应的逻辑 public boolean onTouch(View v, MotionEvent event) { // 根据触摸事件(event)的动作类型(通过event.getAction()获取)进行不同的处理,这里使用switch语句来区分不同的动作情况 switch (event.getAction()) { // 当触摸事件的动作为ACTION_DOWN,即手指按下按钮时的情况 case MotionEvent.ACTION_DOWN: { // 获取当前窗口的默认显示对象,用于获取屏幕相关的信息,比如屏幕的尺寸等 Display display = getWindowManager().getDefaultDisplay(); // 通过显示对象获取屏幕的高度,单位通常是像素,用于后续计算触摸位置等相关操作 int screenHeight = display.getHeight(); // 获取“新建笔记”按钮(mAddNewNote)的高度,单位同样是像素,用于确定按钮在屏幕中的位置以及触摸区域的判断等 int newNoteViewHeight = mAddNewNote.getHeight(); // 计算一个起始位置坐标,这里是屏幕高度减去“新建笔记”按钮的高度,大致表示按钮底部在屏幕中的垂直位置坐标, // 后续可以基于这个坐标来判断触摸点是否在按钮的有效区域或者透明区域等范围内。 int start = screenHeight - newNoteViewHeight; // 计算触摸事件的Y坐标,在起始位置坐标的基础上加上触摸点相对按钮左上角的Y坐标偏移量(通过event.getY()获取), // 这样得到的eventY就是触摸点在整个屏幕坐标系下相对按钮底部位置的Y坐标值,方便后续的位置判断和逻辑处理。 int eventY = start + (int) event.getY(); /** * 如果当前列表的编辑状态(mState)是子文件夹状态(ListEditState.SUB_FOLDER), * 需要减去标题栏(mTitleBar)的高度,因为在这种情况下,触摸位置的计算可能需要考虑标题栏占据的空间, * 使得触摸坐标的计算更加准确,符合实际的界面布局和交互逻辑。同时也对起始位置start做同样的高度减去操作,保持计算的一致性。 */ if (mState == ListEditState.SUB_FOLDER) { eventY -= mTitleBar.getHeight(); start -= mTitleBar.getHeight(); } /** * HACKME注释部分:这里描述了一种针对“新建笔记”按钮透明部分触摸事件处理的特殊逻辑,这种处理方式不太理想,只是为了满足UI设计师的特定要求。 * 当点击“新建笔记”按钮的透明部分时,需要将触摸事件分发给按钮后面的列表视图(ListView,即mNotesListView)进行处理。 * “新建笔记”按钮的透明部分可以用公式 y = -0.12x + 94(单位:像素)以及按钮的上边界来表示,这里的坐标是基于“新建笔记”按钮的左侧边缘为原点的坐标系。 * 其中的94表示透明部分的最大高度,需要注意的是,如果按钮的背景发生变化,这个公式也需要相应地改变,因为透明部分的形状和范围可能会随之改变。 */ if (event.getY() < (event.getX() * (-0.12) + 94)) { // 获取ListView中的最后一个可见子视图(不包含页脚视图),通过getChildAt方法来获取, // 传入的参数是ListView的子视图总数减去1(获取最后一个子视图)再减去ListView的页脚视图数量, // 这样得到的就是列表中实际显示数据的最后一个子视图,用于后续判断触摸点是否在这个子视图的相关范围内。 View view = mNotesListView.getChildAt(mNotesListView.getChildCount() - 1 - mNotesListView.getFooterViewsCount()); // 判断获取到的这个子视图不为空,并且子视图的底部坐标大于前面计算的起始位置start, // 同时子视图的顶部坐标小于起始位置加上透明部分最大高度(start + 94),这意味着触摸点在这个子视图与“新建笔记”按钮透明部分重叠的有效范围内。 if (view!= null && view.getBottom() > start && (view.getTop() < (start + 94))) { // 记录触摸事件起始的Y坐标(相对按钮左上角),通过event.getY()获取并赋值给mOriginY变量, // 这个坐标用于后续在触摸移动等操作中计算位置变化量。 mOriginY = (int) event.getY(); // 将前面计算的基于按钮底部位置的触摸事件Y坐标(已经考虑了各种情况调整后的坐标)赋值给mDispatchY变量, // 用于在后续触摸事件分发等操作中传递准确的触摸位置信息。 mDispatchY = eventY; // 通过event.setLocation方法重新设置触摸事件的坐标位置,将X坐标保持不变(event.getX()), // Y坐标设置为前面计算的mDispatchY,这样就调整了触摸事件的坐标,使其符合后续分发到ListView的坐标要求。 event.setLocation(event.getX(), mDispatchY); // 将mDispatch标记变量设置为true,表示当前需要进行触摸事件的分发操作,后续在触摸移动和抬起等操作中会根据这个标记来执行相应的逻辑。 mDispatch = true; // 调用ListView(mNotesListView)的dispatchTouchEvent方法,将调整后的触摸事件(event)分发给ListView进行处理, // 这样就实现了将“新建笔记”按钮透明部分的触摸事件传递给列表视图,让列表视图能够响应用户在这个特殊区域的触摸操作, // 并返回ListView对这个触摸事件处理的结果(true表示处理了该事件,false表示未处理)。 return mNotesListView.dispatchTouchEvent(event); } } break; } // 当触摸事件的动作为ACTION_MOVE,即手指在按钮上滑动时的情况 case MotionEvent.ACTION_MOVE: { // 判断mDispatch标记变量是否为true,即前面在ACTION_DOWN阶段是否已经确定需要进行触摸事件的分发操作, // 如果是,则进行以下触摸事件坐标调整和分发逻辑。 if (mDispatch) { // 根据当前触摸点的Y坐标(event.getY())与触摸起始的Y坐标(mOriginY)的差值,更新mDispatchY变量, // 这样mDispatchY就始终保持着触摸事件在分发过程中的准确Y坐标位置,反映了手指在屏幕上滑动过程中的位置变化。 mDispatchY += (int) event.getY() - mOriginY; // 通过event.setLocation方法再次重新设置触摸事件的坐标位置,X坐标保持不变(event.getX()), // Y坐标设置为更新后的mDispatchY,确保传递给ListView的触摸事件坐标是准确的,符合实际的触摸位置变化情况。 event.setLocation(event.getX(), mDispatchY); // 调用ListView(mNotesListView)的dispatchTouchEvent方法,将更新坐标后的触摸事件(event)继续分发给ListView进行处理, // 使得ListView能够持续响应手指在按钮透明区域滑动过程中的触摸操作,并返回处理结果(true或false)。 return mNotesListView.dispatchTouchEvent(event); } break; } // 当触摸事件的动作为默认情况(除了ACTION_DOWN和ACTION_MOVE之外的其他动作,比如手指抬起等情况) default: { // 同样先判断mDispatch标记变量是否为true,即之前是否进行了触摸事件的分发操作,如果是,则进行以下收尾操作。 if (mDispatch) { // 再次通过event.setLocation方法设置触摸事件的坐标位置,将当前的mDispatchY坐标赋值给触摸事件的Y坐标, // 确保最后一次传递给ListView的触摸事件坐标是准确的,虽然此时触摸动作已经结束,但可能ListView还需要根据这个最终坐标来处理一些收尾逻辑。 event.setLocation(event.getX(), mDispatchY); // 将mDispatch标记变量设置为false,表示触摸事件分发操作结束,后续如果再有触摸操作,需要重新根据ACTION_DOWN阶段的逻辑来判断是否进行分发。 mDispatch = false; // 调用ListView(mNotesListView)的dispatchTouchEvent方法,将最终调整后的触摸事件(event)分发给ListView进行处理, // 让ListView能够处理触摸动作结束时的相关逻辑,比如判断是否触发了点击、滑动结束等操作,并返回处理结果(true或false)。 return mNotesListView.dispatchTouchEvent(event); } break; } } // 如果触摸事件不属于上述处理的情况(比如触摸点不在按钮透明区域等),返回false,表示当前类没有处理这个触摸事件, // 可能会按照默认的触摸事件处理逻辑或者由其他相关的监听器来继续处理这个触摸事件。 return false; } }; // 启动异步笔记列表查询的方法,用于在后台获取笔记列表数据并更新界面显示 private void startAsyncNotesListQuery() { // 根据当前所在文件夹的ID(mCurrentFolderId)来确定查询的选择条件语句(selection)。 // 如果当前所在文件夹是根文件夹(mCurrentFolderId == Notes.ID_ROOT_FOLDER),则使用根文件夹的查询条件(ROOT_FOLDER_SELECTION), // 否则使用普通的通过父级ID查询笔记的条件(NORMAL_SELECTION)。这样可以根据不同的文件夹情况准确筛选出要展示的笔记数据。 String selection = (mCurrentFolderId == Notes.ID_ROOT_FOLDER)? ROOT_FOLDER_SELECTION : NORMAL_SELECTION; // 调用BackgroundQueryHandler(继承自AsyncQueryHandler,用于在后台处理数据库查询操作)的startQuery方法,启动一个异步查询任务。 // 参数含义如下: // FOLDER_NOTE_LIST_QUERY_TOKEN:是此次查询任务的令牌(token),用于在查询完成后区分不同类型的查询任务,这里表示是文件夹内笔记列表的查询任务。 // null:一般用于传递额外的上下文相关信息(cookie),这里暂时传入null,表示没有额外信息需要传递。 // Notes.CONTENT_NOTE_URI:代表笔记数据的内容URI,用于指定从哪里获取笔记数据,类似于数据库表的地址,指明了要查询的数据来源。 // NoteItemData.PROJECTION:是一个字符串数组,用于指定要从查询结果中返回的列名,即确定查询出来的数据具体包含哪些字段信息,类似于数据库查询语句中的SELECT子句指定要查询的列。 // selection:前面根据文件夹情况确定的查询条件语句,用于筛选符合条件的笔记数据,类似数据库查询语句中的WHERE子句。 // new String[] { String.valueOf(mCurrentFolderId) }:是查询条件语句中占位符(如果有的话)对应的具体值,这里将当前文件夹的ID转换为字符串后作为参数传入,用于替换查询条件中的相应占位符,确保查询的准确性。 // NoteColumns.TYPE + " DESC," + NoteColumns.MODIFIED_DATE + " DESC":是指定查询结果的排序方式,按照笔记的类型(NoteColumns.TYPE)降序排列,若类型相同再按照修改日期(NoteColumns.MODIFIED_DATE)降序排列,确保查询结果按照特定顺序返回,方便界面展示等操作。 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"); } // 定义一个私有内部类BackgroundQueryHandler,继承自AsyncQueryHandler,用于在后台线程处理数据库查询相关的操作,并在查询完成后进行相应的后续处理 private final class BackgroundQueryHandler extends AsyncQueryHandler { // 构造方法,接收一个ContentResolver对象,调用父类(AsyncQueryHandler)的构造方法,将ContentResolver传递给父类, // 使得该查询处理器能够通过ContentResolver与内容提供器进行交互,实现对数据库等数据源的查询操作。 public BackgroundQueryHandler(ContentResolver contentResolver) { super(contentResolver); } // 重写AsyncQueryHandler的onQueryComplete方法,该方法会在异步查询任务完成后被调用,根据不同的查询任务令牌(token)来执行相应的后续处理逻辑 @Override protected void onQueryComplete(int token, Object cookie, Cursor cursor) { // 根据查询任务令牌(token)进行判断,区分不同类型的查询任务并执行对应的操作。 switch (token) { // 如果令牌是FOLDER_NOTE_LIST_QUERY_TOKEN,表示是文件夹内笔记列表的查询任务完成了,执行以下操作。 case FOLDER_NOTE_LIST_QUERY_TOKEN: // 调用笔记列表适配器(mNotesListAdapter)的changeCursor方法,传入查询得到的游标(cursor)对象, // 这样适配器就可以根据新的游标数据更新笔记列表的显示内容,将从数据库中查询到的最新笔记信息展示在界面上。 mNotesListAdapter.changeCursor(cursor); break; // 如果令牌是FOLDER_LIST_QUERY_TOKEN,表示是文件夹列表查询任务完成了,执行以下操作。 case FOLDER_LIST_QUERY_TOKEN: // 判断游标(cursor)不为空并且游标中的数据行数大于0,即查询到了有效的文件夹数据,才执行展示文件夹列表菜单的操作(showFolderListMenu方法)。 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实例,用于构建一个对话框,传入NotesListActivity的上下文(this),以便对话框能够正确显示在当前界面上。 AlertDialog.Builder builder = new AlertDialog.Builder(NotesListActivity.this); // 设置对话框的标题文本,通过R.string.menu_title_select_folder获取在strings.xml文件中定义的对应字符串资源作为标题内容, // 一般用于提示用户当前对话框是用于选择文件夹的操作。 builder.setTitle(R.string.menu_title_select_folder); // 创建一个FoldersListAdapter实例,传入当前上下文(this)和查询得到的游标(cursor),这个适配器用于将文件夹数据适配到对话框的列表展示中, // 使得对话框能够以列表形式展示各个文件夹选项,方便用户选择,具体的适配逻辑在FoldersListAdapter类内部定义(此处未展示完整代码)。 final FoldersListAdapter adapter = new FoldersListAdapter(this, cursor); // 设置对话框的列表适配器,将前面创建的文件夹列表适配器(adapter)设置给对话框,这样对话框就会展示文件夹列表内容, // 并设置点击监听器,当用户点击列表中的某个文件夹选项时,会触发这里定义的逻辑,即调用onClick方法进行相应的操作处理。 builder.setAdapter(adapter, new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int which) { // 调用DataUtils工具类的batchMoveToFolder方法,传入ContentResolver(用于操作数据)、笔记列表适配器中当前选中的笔记项的ID数组(mNotesListAdapter.getSelectedItemIds()), // 以及用户点击选择的文件夹的ID(通过adapter.getItemId(which)获取,which是点击的文件夹在列表中的位置索引), // 目的是将选中的笔记批量移动到用户选择的文件夹中,具体的移动逻辑在DataUtils类的batchMoveToFolder方法中定义(此处未展示完整代码)。 DataUtils.batchMoveToFolder(mContentResolver, mNotesListAdapter.getSelectedItemIds(), adapter.getItemId(which)); // 弹出一个Toast提示信息,告知用户笔记移动操作的结果,通过getString方法获取strings.xml文件中定义的format_move_notes_to_folder字符串资源, // 并传入两个参数:笔记列表适配器中当前选中的笔记数量(mNotesListAdapter.getSelectedCount())以及用户选择的文件夹名称(通过adapter.getFolderName方法获取), // 这样可以显示一个包含具体移动笔记数量和目标文件夹名称的提示信息,让用户清楚操作的结果,显示时长为短暂(Toast.LENGTH_SHORT)。 Toast.makeText( NotesListActivity.this, getString(R.string.format_move_notes_to_folder, mNotesListAdapter.getSelectedCount(), adapter.getFolderName(NotesListActivity.this, which)), Toast.LENGTH_SHORT).show(); // 调用ModeCallback实例(mModeCallBack)的finishActionMode方法,结束当前可能处于的多选模式(比如在多选笔记后进行移动操作的情况), // 该方法会进行一些界面恢复和相关状态清理的操作,具体逻辑在ModeCallback类的finishActionMode方法中定义(此处未展示完整代码)。 mModeCallBack.finishActionMode(); } }); // 最后调用show方法显示这个构建好的包含文件夹列表的对话框,让用户能够进行文件夹选择操作。 builder.show(); } // 创建新笔记的方法,用于启动一个新的Activity,进入笔记编辑界面,让用户可以创建新的笔记内容 private void createNewNote() { // 创建一个Intent对象,用于启动一个新的Activity,指定要启动的Activity类为NoteEditActivity.class, // 这意味着点击“新建笔记”按钮等操作触发此方法后,会跳转到NoteEditActivity这个用于编辑笔记的界面。 Intent intent = new Intent(this, NoteEditActivity.class); // 设置Intent的动作(Action)为Intent.ACTION_INSERT_OR_EDIT,这个动作通常表示要进行插入或编辑数据的操作, // 在这里明确了进入NoteEditActivity界面是为了创建新笔记(插入操作)或者编辑已有笔记(虽然此处主要是新建,但可能复用该界面逻辑用于编辑)。 intent.setAction(Intent.ACTION_INSERT_OR_EDIT); // 通过Intent的putExtra方法,向即将启动的Activity传递额外的数据,这里传递的是当前所在文件夹的ID(mCurrentFolderId), // 键名为Notes.INTENT_EXTRA_FOLDER_ID,在NoteEditActivity中可以通过获取这个额外数据来确定新笔记所属的文件夹等相关信息,方便后续操作。 intent.putExtra(Notes.INTENT_EXTRA_FOLDER_ID, mCurrentFolderId); // 调用当前上下文(this)的startActivityForResult方法,传入创建好的Intent和请求码(REQUEST_CODE_NEW_NODE), // 启动NoteEditActivity,并期望在该Activity操作完成后返回结果,通过请求码可以在onActivityResult方法中区分不同来源的返回结果,进行相应的后续处理。 this.startActivityForResult(intent, REQUEST_CODE_NEW_NODE); } // 批量删除笔记的方法,通过异步任务(AsyncTask)在后台执行删除操作,同时根据是否处于同步模式等情况进行不同的处理逻辑 private void batchDelete() { // 创建一个继承自AsyncTask的匿名内部类实例,用于在后台线程执行批量删除笔记的操作,并在操作完成后在主线程进行相应的界面更新等后续处理。 new AsyncTask>() { // 重写AsyncTask的doInBackground方法,这个方法在后台线程中执行,用于执行耗时的批量删除笔记操作以及相关的数据处理逻辑,返回一个HashSet类型的结果,用于在后续操作中传递一些与小部件(AppWidget)相关的属性信息。 protected HashSet doInBackground(Void... unused) { // 获取笔记列表适配器(mNotesListAdapter)中当前选中的小部件相关属性信息集合(HashSet), // 这些属性可能与笔记关联的小部件显示等情况有关,用于后续在必要时更新小部件的显示等操作。 HashSet widgets = mNotesListAdapter.getSelectedWidget(); // 判断是否不处于同步模式(通过isSyncMode方法判断,此处未展示该方法具体实现,但应该是返回一个布尔值表示是否同步状态), if (!isSyncMode()) { // 如果不是同步模式,直接删除选中的笔记,调用DataUtils工具类的batchDeleteNotes方法,传入ContentResolver(用于操作数据)和笔记列表适配器中当前选中的笔记项的ID数组(mNotesListAdapter.getSelectedItemIds()), // 尝试执行批量删除操作,如果删除成功则继续后续逻辑,如果删除失败,在日志中输出错误信息,标记为删除笔记出错,这里理论上不应该出现错误,方便排查问题。 if (DataUtils.batchDeleteNotes(mContentResolver, mNotesListAdapter .getSelectedItemIds())) { } else { Log.e(TAG, "Delete notes error, should not happens"); } } else { // 如果处于同步模式,将选中的笔记移动到回收站文件夹(Notes.ID_TRASH_FOLER,其具体值在Notes类中定义,用于标识回收站文件夹), // 调用DataUtils工具类的batchMoveToFolder方法,传入ContentResolver、选中的笔记项的ID数组以及回收站文件夹的ID, // 尝试执行批量移动操作,如果移动失败,在日志中输出错误信息,标记为移动笔记到回收站文件夹出错,同样这里理论上不应该出现错误,方便排查问题。 if (!DataUtils.batchMoveToFolder(mContentResolver, mNotesListAdapter .getSelectedItemIds(), Notes.ID_TRASH_FOLER)) { Log.e(TAG, "Move notes to trash folder error, should not happens"); } } // 将获取到的小部件相关属性信息集合(widgets)作为结果返回,以便在后续的onPostExecute方法中使用这些信息进行相关操作。 return widgets; } // 重写AsyncTask的onPostExecute方法,这个方法在后台的doInBackground方法执行完成后,在主线程中被调用,用于根据后台操作的结果进行界面更新等后续处理。 @Override protected void onPostExecute(HashSet widgets) { // 判断返回的小部件相关属性信息集合(widgets)不为空,即存在与笔记关联的小部件相关属性信息,才进行以下操作。 if (widgets!= null) { // 遍历小部件相关属性信息集合中的每个AppWidgetAttribute对象(widget),进行相应的小部件更新操作。 for (AppWidgetAttribute widget : widgets) { // 判断小部件的ID(widget.widgetId)不是无效的小部件ID(AppWidgetManager.INVALID_APPWIDGET_ID),并且小部件的类型(widget.widgetType)也不是无效的小部件类型(Notes.TYPE_WIDGET_INVALIDE), // 只有满足这两个条件的有效小部件才执行更新操作,避免对无效的小部件进行不必要的操作。 if (widget.widgetId!= AppWidgetManager.INVALID_APPWIDGET_ID && widget.widgetType!= Notes.TYPE_WIDGET_INVALIDE) { // 调用updateWidget方法(此处未展示该方法具体实现,但应该是用于更新小部件显示等相关操作的方法),传入小部件的ID(widget.widgetId)和小部件的类型(widget.widgetType), // 对符合条件的小部件进行更新,确保小部件的显示内容等与笔记数据的变化保持一致,比如笔记被删除或移动后,小部件显示相应更新。 updateWidget(widget.widgetId, widget.widgetType); } } } // 调用ModeCallback实例(mModeCallBack)的finishActionMode方法,结束当前可能处于的多选模式(比如在多选笔记后进行删除操作的情况), // 进行一些界面恢复和相关状态清理的操作,如恢复ListView的长按可点击属性、重新显示“新建笔记”按钮等,具体逻辑在ModeCallback类的finishActionMode方法中定义(此处未展示完整代码)。 mModeCallBack.finishActionMode(); } }.execute(); } private void deleteFolder(long folderId) { // 这段代码可能是用于处理文件夹相关操作的逻辑,比如删除文件夹等情况,根据文件夹是否处于同步模式来执行不同的处理方式,并对相关的小部件进行更新 if (folderId == Notes.ID_ROOT_FOLDER) { // 如果传入的文件夹ID(folderId)等于根文件夹的ID(Notes.ID_ROOT_FOLDER),则在日志中输出错误信息, // 提示不应该出现这种情况,因为根文件夹通常不应该被当作普通文件夹进行某些操作(比如删除等),然后直接返回,不再继续后续操作。 Log.e(TAG, "Wrong folder id, should not happen " + folderId); return; } // 创建一个HashSet集合,用于存储要操作的文件夹ID,这里先将传入的folderId添加到集合中,方便后续批量操作时使用这个集合传递参数。 HashSet ids = new HashSet(); ids.add(folderId); // 通过DataUtils工具类的getFolderNoteWidget方法,获取与指定文件夹(folderId)相关联的小部件属性信息集合(HashSet), // 这些小部件属性信息可能包含小部件的ID、类型等内容,用于后续在文件夹操作后对相关小部件进行相应的更新操作,确保小部件显示与文件夹数据的变化保持一致。 HashSet widgets = DataUtils.getFolderNoteWidget(mContentResolver, folderId); // 判断是否不处于同步模式(通过isSyncMode方法判断,此处未展示该方法具体实现,但应该是返回一个布尔值表示是否同步状态) if (!isSyncMode()) { // 如果不是同步模式,直接删除文件夹对应的笔记,调用DataUtils工具类的batchDeleteNotes方法,传入ContentResolver(用于操作数据)和包含文件夹ID的集合(ids), // 执行批量删除操作,这里可能意味着将该文件夹下的所有笔记都删除,具体的删除逻辑在DataUtils类的batchDeleteNotes方法中定义(此处未展示完整代码)。 DataUtils.batchDeleteNotes(mContentResolver, ids); } else { // 如果处于同步模式,将该文件夹下的笔记移动到回收站文件夹(Notes.ID_TRASH_FOLER,其具体值在Notes类中定义,用于标识回收站文件夹), // 调用DataUtils工具类的batchMoveToFolder方法,传入ContentResolver、包含文件夹ID的集合(ids)以及回收站文件夹的ID, // 执行批量移动操作,将相关笔记移动到回收站,具体的移动逻辑在DataUtils类的batchMoveToFolder方法中定义(此处未展示完整代码)。 DataUtils.batchMoveToFolder(mContentResolver, ids, Notes.ID_TRASH_FOLER); } // 判断获取到的与文件夹相关的小部件属性信息集合(widgets)不为空,即存在与该文件夹关联的小部件相关属性信息,才进行以下操作,对这些小部件进行更新。 if (widgets!= null) { // 遍历小部件属性信息集合中的每个AppWidgetAttribute对象(widget),进行相应的小部件更新操作。 for (AppWidgetAttribute widget : widgets) { // 判断小部件的ID(widget.widgetId)不是无效的小部件ID(AppWidgetManager.INVALID_APPWIDGET_ID),并且小部件的类型(widget.widgetType)也不是无效的小部件类型(Notes.TYPE_WIDGET_INVALIDE), // 只有满足这两个条件的有效小部件才执行更新操作,避免对无效的小部件进行不必要的操作,确保只更新与实际相关且有效的小部件显示等内容。 if (widget.widgetId!= AppWidgetManager.INVALID_APPWIDGET_ID && widget.widgetType!= Notes.TYPE_WIDGET_INVALIDE) { // 调用updateWidget方法(此处未展示该方法具体实现,但应该是用于更新小部件显示等相关操作的方法),传入小部件的ID(widget.widgetId)和小部件的类型(widget.widgetType), // 对符合条件的小部件进行更新,使其显示内容等与文件夹及笔记数据的变化保持一致,比如文件夹被删除或移动后,小部件显示相应更新。 updateWidget(widget.widgetId, widget.widgetType); } } } // 用于打开一个笔记节点的方法,通常是跳转到笔记编辑界面(NoteEditActivity)并以查看(VIEW)模式打开指定的笔记数据,方便用户查看笔记详情 private void openNode(NoteItemData data) { // 创建一个Intent对象,用于启动一个新的Activity,指定要启动的Activity类为NoteEditActivity.class, // 意味着此操作会跳转到NoteEditActivity这个用于编辑笔记的界面,在这里主要用于查看笔记内容。 Intent intent = new Intent(this, NoteEditActivity.class); // 设置Intent的动作(Action)为Intent.ACTION_VIEW,明确表示此次进入NoteEditActivity界面是为了查看笔记,而不是进行编辑等其他操作。 intent.setAction(Intent.ACTION_VIEW); // 通过Intent的putExtra方法,向即将启动的Activity传递额外的数据,这里传递的是笔记的唯一标识符(data.getId()), // 键名为Intent.EXTRA_UID,在NoteEditActivity中可以通过获取这个额外数据来确定要查看的具体笔记,方便加载相应的笔记内容进行展示。 intent.putExtra(Intent.EXTRA_UID, data.getId()); // 调用当前上下文(this)的startActivityForResult方法,传入创建好的Intent和请求码(REQUEST_CODE_OPEN_NODE), // 启动NoteEditActivity,并期望在该Activity操作完成后返回结果,通过请求码可以在onActivityResult方法中区分不同来源的返回结果,进行相应的后续处理。 this.startActivityForResult(intent, REQUEST_CODE_OPEN_NODE); } // 用于打开一个文件夹的方法,根据传入的笔记数据(NoteItemData类型的数据,可能包含文件夹相关信息)来更新当前界面显示的文件夹相关内容,比如切换显示的笔记列表、更新标题栏等 private void openFolder(NoteItemData data) { // 将当前所在文件夹的ID(mCurrentFolderId)设置为传入的笔记数据对应的ID(data.getId()), // 这样后续操作(如查询笔记列表等)就会基于新打开的这个文件夹来进行,确保获取和展示该文件夹下的相关笔记数据。 mCurrentFolderId = data.getId(); // 调用startAsyncNotesListQuery方法,启动一个异步的笔记列表查询操作,目的是获取并展示新打开的文件夹下的笔记列表信息, // 使得界面能够及时更新显示新文件夹对应的笔记内容,具体的查询逻辑在startAsyncNotesListQuery方法中定义(此处未展示完整代码)。 startAsyncNotesListQuery(); // 判断如果打开的文件夹ID(data.getId())等于通话记录文件夹的ID(Notes.ID_CALL_RECORD_FOLDER),则将当前列表的编辑状态(mState)设置为通话记录文件夹状态(ListEditState.CALL_RECORD_FOLDER), // 并且将“新建笔记”按钮(mAddNewNote)的可见性设置为不可见(View.GONE),因为在通话记录文件夹下可能不允许新建笔记等操作,具体根据业务逻辑设定。 if (data.getId() == Notes.ID_CALL_RECORD_FOLDER) { mState = ListEditState.CALL_RECORD_FOLDER; mAddNewNote.setVisibility(View.GONE); } else { // 如果不是通话记录文件夹,则将当前列表的编辑状态设置为子文件夹状态(ListEditState.SUB_FOLDER),表示处于普通的子文件夹操作模式。 mState = ListEditState.SUB_FOLDER; } // 再次判断如果打开的文件夹ID等于通话记录文件夹的ID,将标题栏(mTitleBar)的文本内容设置为通话记录文件夹的名称(通过R.string.call_record_folder_name获取在strings.xml文件中定义的对应字符串资源), // 用于在界面上准确显示当前打开的文件夹名称,方便用户识别。 if (data.getId() == Notes.ID_CALL_RECORD_FOLDER) { mTitleBar.setText(R.string.call_record_folder_name); } else { // 如果不是通话记录文件夹,将标题栏的文本内容设置为传入的笔记数据中的摘要信息(data.getSnippet()),同样用于展示当前打开文件夹的相关提示信息,可能是文件夹的简短描述等内容。 mTitleBar.setText(data.getSnippet()); } // 将标题栏(mTitleBar)的可见性设置为可见(View.VISIBLE),确保标题栏在界面上显示出来,展示相应的文件夹名称等信息。 mTitleBar.setVisibility(View.VISIBLE); } // 实现了OnClickListener接口的onClick方法,用于处理各种点击事件,根据点击的视图(View)的ID来判断具体是哪个控件被点击了,并执行相应的操作逻辑 public void onClick(View v) { switch (v.getId()) { // 如果点击的视图的ID是“新建笔记”按钮的ID(R.id.btn_new_note),则调用createNewNote方法, // 该方法用于启动一个新的Activity进入笔记编辑界面,让用户可以创建新的笔记内容,具体逻辑在createNewNote方法中定义(此处未展示完整代码)。 case R.id.btn_new_note: createNewNote(); break; default: break; } } // 用于显示软键盘的方法,通过获取系统的输入法服务(InputMethodManager)来强制显示软键盘 private void showSoftInput() { // 获取系统的输入法服务,通过getSystemService方法传入Context.INPUT_METHOD_SERVICE参数来获取InputMethodManager实例, // 这个实例用于管理输入法相关的操作,比如显示、隐藏软键盘等。 InputMethodManager inputMethodManager = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE); // 判断获取到的输入法管理器(inputMethodManager)不为空,即成功获取到输入法服务后,才执行显示软键盘的操作。 if (inputMethodManager!= null) { // 调用输入法管理器的toggleSoftInput方法,传入InputMethodManager.SHOW_FORCED(表示强制显示软键盘)和0(表示无额外的标志位参数), // 触发软键盘显示在屏幕上,方便用户输入内容,比如在需要用户输入文本的编辑框获取焦点时可以调用此方法来显示键盘。 inputMethodManager.toggleSoftInput(InputMethodManager.SHOW_FORCED, 0); } } // 用于隐藏软键盘的方法,通过获取系统的输入法服务(InputMethodManager)并根据指定的视图(View)来隐藏软键盘 private void hideSoftInput(View view) { // 获取系统的输入法服务,与前面显示软键盘的获取方式相同,通过getSystemService方法传入Context.INPUT_METHOD_SERVICE参数来获取InputMethodManager实例。 InputMethodManager inputMethodManager = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE); // 调用输入法管理器的hideSoftInputFromWindow方法,传入视图的窗口令牌(view.getWindowToken(),用于标识该视图所在的窗口,输入法管理器通过这个令牌来确定要隐藏键盘的相关窗口)和0(表示无额外的标志位参数), // 触发隐藏软键盘的操作,使得软键盘从屏幕上消失,通常在不需要用户输入内容或者编辑操作完成后调用此方法来隐藏键盘,提升界面整洁性和用户体验。 inputMethodManager.hideSoftInputFromWindow(view.getWindowToken(), 0); } // 用于显示创建或修改文件夹对话框的方法,根据传入的布尔值(create)来确定是创建文件夹还是修改文件夹的操作模式,并进行相应的对话框初始化设置 private void showCreateOrModifyFolderDialog(final boolean create) { // 创建一个AlertDialog.Builder实例,用于构建一个对话框,传入当前上下文(this),以便对话框能够正确显示在当前界面上。 final AlertDialog.Builder builder = new AlertDialog.Builder(this); // 通过LayoutInflater从当前上下文(this)加载名为dialog_edit_text.xml的布局文件,并实例化为一个视图对象(view), // 这个布局文件应该包含了对话框中用于输入文件夹名称等相关的控件内容,比如可能有一个EditText用于输入文件夹名字。 View view = LayoutInflater.from(this).inflate(R.layout.dialog_edit_text, null); // 从加载的视图(view)中通过findViewById方法找到ID为R.id.et_foler_name的EditText控件,用于后续获取用户输入的文件夹名称等操作, // 这个EditText就是对话框中供用户输入文件夹名称的文本编辑框。 final EditText etName = (EditText) view.findViewById(R.id.et_foler_name); // 调用showSoftInput方法,显示软键盘,方便用户在弹出的对话框中直接输入文件夹名称,提升操作便捷性。 showSoftInput(); // 根据传入的布尔值(create)来判断是创建文件夹还是修改文件夹的操作,如果create为false,表示是修改文件夹操作,执行以下逻辑。 if (!create) { // 判断当前获取焦点的笔记数据项(mFocusNoteDataItem)不为空,因为修改文件夹名称可能需要基于已有的某个笔记数据项相关信息来进行, // 如果为空则无法进行修改操作,在日志中输出错误信息提示数据项为空,然后直接返回,不再继续后续操作。 if (mFocusNoteDataItem!= null) { // 如果有有效的笔记数据项,将EditText(etName)的文本内容设置为当前获取焦点的笔记数据项中的摘要信息(mFocusNoteDataItem.getSnippet()), // 这样可以将原文件夹名称等相关信息预先填充到编辑框中,方便用户修改,具体填充内容根据业务逻辑和数据项中的信息而定。 etName.setText(mFocusNoteDataItem.getSnippet()); // 设置对话框的标题文本,通过getString方法获取strings.xml文件中定义的menu_folder_change_name字符串资源作为标题内容, // 用于提示用户当前对话框是用于修改文件夹名称的操作。 builder.setTitle(getString(R.string.menu_folder_change_name)); } else { Log.e(TAG, "The long click data item is null"); return; } } else { // 如果create为true,表示是创建文件夹操作,将EditText(etName)的文本内容设置为空字符串,准备让用户输入新的文件夹名称。 etName.setText(""); // 设置对话框的标题文本,通过this.getString方法获取strings.xml文件中定义的menu_create_folder字符串资源作为标题内容, // 用于提示用户当前对话框是用于创建文件夹的操作。 builder.setTitle(this.getString(R.string.menu_create_folder)); } // 设置对话框的“确定”按钮(PositiveButton),传入Android系统自带的确认文本(android.R.string.ok)作为按钮文本,这里暂时将点击监听器设置为null, // 可能后续还需要根据具体业务逻辑来添加点击“确定”按钮后的具体操作处理代码,比如验证文件夹名称合法性、创建或修改文件夹等操作。 builder.setPositiveButton(android.R.string.ok, null); // 设置对话框的“取消”按钮(NegativeButton),传入Android系统自带的取消文本(android.R.string.cancel)作为按钮文本, // 并设置点击监听器,当用户点击“取消”按钮时,会触发这里定义的逻辑,即调用hideSoftInput方法隐藏软键盘(传入当前的EditText控件etName),避免软键盘在对话框关闭后仍然显示,影响界面美观和后续操作。 builder.setNegativeButton(android.R.string.cancel, new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int which) { hideSoftInput(etName); } }); } // 通过之前创建的AlertDialog.Builder对象(builder)设置要显示的视图(view),然后调用show方法显示对话框,并将显示后的对话框实例赋值给dialog变量, // 这样后续就可以通过这个dialog变量来操作对话框内的各种控件以及处理相关逻辑。 final Dialog dialog = builder.setView(view).show(); // 从显示的对话框(dialog)中通过findViewById方法查找ID为android.R.id.button1的按钮,在Android系统默认的对话框布局中, // 这个ID通常对应的是“确定”按钮(PositiveButton),不同的主题或系统版本可能会遵循此约定来标识该按钮,方便后续为其添加点击事件处理逻辑。 final Button positive = (Button) dialog.findViewById(android.R.id.button1); // 为查找到的“确定”按钮(positive)设置点击监听器,当用户点击该按钮时,会触发以下定义的onClick方法内的逻辑。 positive.setOnClickListener(new OnClickListener() { public void onClick(View v) { // 调用hideSoftInput方法隐藏软键盘,传入当前用于输入文件夹名称的EditText控件(etName),这样做是为了在用户完成对话框操作后, // 关闭软键盘,避免其继续显示在屏幕上影响界面美观以及后续可能的操作交互,提升用户体验。 hideSoftInput(etName); // 获取EditText(etName)中的文本内容,并转换为字符串形式,该字符串将作为文件夹的名称用于后续的创建或修改文件夹操作判断等逻辑中。 String name = etName.getText().toString(); // 调用DataUtils工具类的checkVisibleFolderName方法,传入ContentResolver(用于与应用的数据提供器交互,实现数据操作)和获取到的文件夹名称(name), // 该方法用于检查给定的文件夹名称是否已经存在(具体的检查逻辑在DataUtils类的checkVisibleFolderName方法内部实现,此处未展示完整代码), // 如果检查发现文件夹名称已存在,执行以下操作。 if (DataUtils.checkVisibleFolderName(mContentResolver, name)) { // 弹出一个Toast提示信息,告知用户文件夹已存在,通过getString方法获取strings.xml文件中定义的folder_exist字符串资源, // 并将当前的文件夹名称(name)作为参数传入进行格式化,使得提示信息能够明确指出哪个文件夹名称已存在, // 显示时长设置为较长(Toast.LENGTH_LONG),以便用户能清晰地看到提示内容,知晓当前操作存在的问题。 Toast.makeText(NotesListActivity.this, getString(R.string.folder_exist, name), Toast.LENGTH_LONG).show(); // 将EditText(etName)的光标定位到文本开头(0位置),并选中整个文本内容(etName.length()),这样做方便用户直接修改已存在的文件夹名称, // 用户无需手动重新定位光标或全选文本,提升了操作的便捷性,使其可以快速对重复的名称进行修改后再次尝试操作。 etName.setSelection(0, etName.length()); return; } // 判断当前是否是修改文件夹操作(create为false表示修改操作),如果是修改操作,执行以下逻辑。 if (!create) { // 进一步判断获取到的文件夹名称不为空字符串(通过TextUtils.isEmpty方法进行判断,若返回false则表示不为空), // 只有名称不为空时才进行实际的文件夹信息更新操作,避免更新空名称导致数据异常等情况。 if (!TextUtils.isEmpty(name)) { // 创建一个ContentValues对象,它用于存储要更新的数据,以键值对的形式表示,类似于数据库中的插入或更新操作时的数据封装形式, // 键对应数据库表中的列名,值对应要插入或更新的具体数据内容。 ContentValues values = new ContentValues(); // 将文件夹的摘要信息(通常可以理解为文件夹名称等相关描述信息,对应数据库中的NoteColumns.SNIPPET列)设置为新获取的名称(name), // 通过put方法将列名和对应的值添加到ContentValues对象中,以便后续更新操作使用。 values.put(NoteColumns.SNIPPET, name); // 设置文件夹的类型为普通文件夹类型(Notes.TYPE_FOLDER,其具体值在Notes类中定义,用于明确标识该数据为文件夹类型), // 同样使用put方法将NoteColumns.TYPE列名与对应的文件夹类型值添加到ContentValues对象中,确保数据的类型信息准确更新。 values.put(NoteColumns.TYPE, Notes.TYPE_FOLDER); // 设置文件夹的本地修改标志为1,表示该文件夹有本地修改操作发生,这个标志可能用于后续的数据同步等相关逻辑判断, // 通过put方法将NoteColumns.LOCAL_MODIFIED列名与值1添加到ContentValues对象中,记录文件夹的修改状态信息。 values.put(NoteColumns.LOCAL_MODIFIED, 1); // 通过ContentResolver的update方法来执行实际的文件夹信息更新操作,传入以下参数: // Notes.CONTENT_NOTE_URI:代表笔记数据的内容URI,它指明了要更新的数据所在的数据源位置,类似于数据库表的地址, // 告知系统从哪里获取和更新相应的数据,这里明确了是针对笔记相关的数据进行操作。 // values:前面创建的包含要更新的文件夹相关信息的ContentValues对象,其中封装了要更新的列名及对应的值, // 系统会根据这些信息来更新相应的数据记录。 // NoteColumns.ID + "=?":是更新操作的条件语句,用于指定要更新的具体文件夹记录,通过文件夹的ID来进行筛选, // 这里的问号是占位符,后续需要传入具体的ID值来确定要更新的是哪条文件夹记录。 // new String[] { String.valueOf(mFocusNoteDataItem.getId()) }:是前面条件语句中占位符对应的具体值, // 将当前获取焦点的笔记数据项(mFocusNoteDataItem)的ID转换为字符串后作为参数传入,这样就能准确更新对应的文件夹信息了。 mContentResolver.update(Notes.CONTENT_NOTE_URI, values, NoteColumns.ID + "=?", new String[] { String.valueOf(mFocusNoteDataItem.getId()) }); } } else if (!TextUtils.isEmpty(name)) { // 如果当前是创建文件夹操作(create为true),并且获取到的文件夹名称也不为空字符串,执行以下创建文件夹的逻辑。 ContentValues values = new ContentValues(); // 将文件夹的摘要信息(对应NoteColumns.SNIPPET列)设置为新获取的名称(name),通过put方法添加到ContentValues对象中, // 这就确定了新创建文件夹的名称信息。 values.put(NoteColumns.SNIPPET, name); // 设置文件夹的类型为普通文件夹类型(Notes.TYPE_FOLDER),同样使用put方法添加到ContentValues对象中,明确创建的是文件夹类型的数据记录。 values.put(NoteColumns.TYPE, Notes.TYPE_FOLDER); // 通过ContentResolver的insert方法执行实际的文件夹创建操作,传入以下参数: // Notes.CONTENT_NOTE_URI:代表笔记数据的内容URI,指明了要在哪个数据源位置插入新的文件夹数据记录, // 告知系统在哪里创建新的文件夹相关信息。 // values:前面创建的包含要插入的文件夹相关信息的ContentValues对象,其中封装了新文件夹的名称和类型等信息, // 系统会根据这些信息在指定数据源中插入一条新的文件夹记录。 mContentResolver.insert(Notes.CONTENT_NOTE_URI, values); } // 无论当前是创建还是修改文件夹操作完成后,都调用dialog的dismiss方法关闭对话框,使得界面恢复到正常状态, // 避免对话框一直显示在屏幕上影响用户后续的操作和界面的整洁性。 dialog.dismiss(); } }); // 判断EditText(etName)中的文本内容是否为空字符串(通过TextUtils.isEmpty方法判断),如果为空, // 则将“确定”按钮(positive)设置为不可用状态(通过setEnabled(false)方法实现),这样可以防止用户在没有输入文件夹名称的情况下点击“确定”按钮, // 避免无效的操作发生,提升用户操作的准确性和逻辑性。 if (TextUtils.isEmpty(etName.getText())) { positive.setEnabled(false); } // 以下是为EditText(etName)添加文本变化监听器(TextWatcher)的逻辑,用于实时监听文本内容的变化情况,并根据文本是否为空来动态设置“确定”按钮的可用状态。 etName.addTextChangedListener(new TextWatcher() { // 在文本内容即将发生变化前会调用此方法,目前此方法体中没有具体实现逻辑(只是一个占位符,通常如果有需要在文本变化前进行的操作可以在这里添加代码)。 public void beforeTextChanged(CharSequence s, int start, int count, int after) { // TODO Auto-generated method stub } // 当文本内容发生变化时会调用此方法,在这里根据EditText(etName)中的文本是否为空来动态设置“确定”按钮(positive)的可用状态。 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 } }); // 重写Activity的onBackPressed方法,用于处理用户按下手机返回键时的操作逻辑,根据当前列表的编辑状态(mState)来执行不同的操作。 @Override public void onBackPressed() { switch (mState) { // 如果当前列表编辑状态是子文件夹状态(SUB_FOLDER),执行以下逻辑。 case SUB_FOLDER: // 将当前所在文件夹的ID(mCurrentFolderId)设置为根文件夹的ID(Notes.ID_ROOT_FOLDER),意味着从当前子文件夹返回,回到根文件夹层级。 mCurrentFolderId = Notes.ID_ROOT_FOLDER; // 将列表编辑状态(mState)设置为笔记列表状态(ListEditState.NOTE_LIST),表示回到普通的笔记列表展示模式。 mState = ListEditState.NOTE_LIST; // 调用startAsyncNotesListQuery方法,启动一个异步的笔记列表查询操作,目的是获取并展示根文件夹下的笔记列表信息, // 使得界面能够及时更新显示回到根文件夹后的笔记内容,具体的查询逻辑在startAsyncNotesListQuery方法中定义(此处未展示完整代码)。 startAsyncNotesListQuery(); // 将标题栏(mTitleBar)的可见性设置为不可见(View.GONE),因为回到根文件夹后可能不需要显示之前子文件夹对应的标题栏内容了, // 这样可以保持界面的简洁性,符合不同层级下的界面展示需求。 mTitleBar.setVisibility(View.GONE); break; // 如果当前列表编辑状态是通话记录文件夹状态(CALL_RECORD_FOLDER),执行以下逻辑。 case CALL_RECORD_FOLDER: // 将当前所在文件夹的ID(mCurrentFolderId)设置为根文件夹的ID(Notes.ID_ROOT_FOLDER),同样是从通话记录文件夹返回,回到根文件夹层级。 mCurrentFolderId = Notes.ID_ROOT_FOLDER; // 将列表编辑状态(mState)设置为笔记列表状态(ListEditState.NOTE_LIST),切换回普通的笔记列表展示模式。 mState = ListEditState.NOTE_LIST; // 将“新建笔记”按钮(mAddNewNote)的可见性设置为可见(View.VISIBLE),因为回到根文件夹后通常需要显示该按钮,方便用户继续进行新建笔记等操作, // 而在通话记录文件夹状态下可能之前将其隐藏了(根据业务逻辑设定)。 mAddNewNote.setVisibility(View.VISIBLE); // 将标题栏(mTitleBar)的可见性设置为不可见(View.GONE),与前面子文件夹返回时类似,回到根文件夹后不需要显示通话记录文件夹对应的标题栏内容了。 mTitleBar.setVisibility(View.GONE); // 调用startAsyncNotesListQuery方法,启动异步笔记列表查询操作,获取并展示根文件夹下的笔记列表信息,更新界面显示。 startAsyncNotesListQuery(); break; // 如果当前列表编辑状态是笔记列表状态(NOTE_LIST),直接调用父类(Activity)的onBackPressed方法, // 这意味着按照系统默认的返回键处理逻辑进行操作,比如可能会关闭当前Activity或者执行其他默认的返回相关行为,具体取决于Activity的任务栈等相关机制。 case NOTE_LIST: super.onBackPressed(); break; default: break; } } // 此方法用于更新桌面小部件(Widget) private void updateWidget(int appWidgetId, int appWidgetType) { // 创建一个意图(Intent),指定动作为更新桌面小部件的动作 Intent intent = new Intent(AppWidgetManager.ACTION_APPWIDGET_UPDATE); // 根据传入的小部件类型(appWidgetType)来设置不同的小部件提供类(Widget Provider Class) 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添加到意图的额外数据中,这里是以数组形式传递单个小部件ID intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, new int[] { appWidgetId }); // 发送广播,触发对应的小部件更新操作 sendBroadcast(intent); // 设置结果为操作成功(RESULT_OK),并关联对应的意图 setResult(RESULT_OK, intent); } // 定义一个上下文菜单创建监听器(OnCreateContextMenuListener),用于处理特定视图的上下文菜单创建事件 private final OnCreateContextMenuListener mFolderOnCreateContextMenuListener = new OnCreateContextMenuListener() { public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) { // 如果当前有焦点的笔记数据项(mFocusNoteDataItem)不为空 if (mFocusNoteDataItem!= null) { // 设置上下文菜单的标题为焦点笔记数据项的摘要信息(片段内容) menu.setHeaderTitle(mFocusNoteDataItem.getSnippet()); // 向菜单中添加一个菜单项,用于查看文件夹,菜单项ID为MENU_FOLDER_VIEW,显示文本通过资源字符串获取 menu.add(0, MENU_FOLDER_VIEW, 0, R.string.menu_folder_view); // 向菜单中添加一个菜单项,用于删除文件夹,菜单项ID为MENU_FOLDER_DELETE,显示文本通过资源字符串获取 menu.add(0, MENU_FOLDER_DELETE, 0, R.string.menu_folder_delete); // 向菜单中添加一个菜单项,用于更改文件夹名称,菜单项ID为MENU_FOLDER_CHANGE_NAME,显示文本通过资源字符串获取 menu.add(0, MENU_FOLDER_CHANGE_NAME, 0, R.string.menu_folder_change_name); } } }; // 当上下文菜单关闭时触发的回调方法 @Override public void onContextMenuClosed(Menu menu) { // 如果笔记列表视图(mNotesListView)不为空,则移除其上下文菜单创建监听器 if (mNotesListView!= null) { mNotesListView.setOnCreateContextMenuListener(null); } // 调用父类的onContextMenuClosed方法,执行默认的或父类中定义的关闭后逻辑 super.onContextMenuClosed(menu); } // 当上下文菜单中的某个菜单项被选中时触发的回调方法 @Override public boolean onContextItemSelected(MenuItem item) { // 如果当前有焦点的笔记数据项(mFocusNoteDataItem)为空,则记录错误日志并返回false,表示不处理该事件 if (mFocusNoteDataItem == null) { Log.e(TAG, "The long click data item is null"); return false; } // 根据选中菜单项的ID进行不同的操作处理 switch (item.getItemId()) { case MENU_FOLDER_VIEW: // 调用openFolder方法打开对应的文件夹(具体功能由openFolder方法实现) openFolder(mFocusNoteDataItem); break; case MENU_FOLDER_DELETE: // 创建一个警告对话框构建器(AlertDialog.Builder) 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)); // 设置对话框的确认按钮(正按钮),按钮文本为系统默认的"确定",点击时调用deleteFolder方法删除文件夹(传入对应文件夹的ID) builder.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int which) { deleteFolder(mFocusNoteDataItem.getId()); } }); // 设置对话框的取消按钮(负按钮),点击时不做额外操作(传入null) builder.setNegativeButton(android.R.string.cancel, null); // 显示构建好的警告对话框 builder.show(); break; case MENU_FOLDER_CHANGE_NAME: // 调用showCreateOrModifyFolderDialog方法,传入false参数,用于显示创建或修改文件夹名称的对话框(具体功能由该方法实现) showCreateOrModifyFolderDialog(false); break; default: break; } // 返回true表示已处理该菜单项选择事件 return true; } // 在准备选项菜单(Options Menu)时触发的回调方法,用于动态设置菜单内容 @Override public boolean onPrepareOptionsMenu(Menu menu) { // 先清空菜单中的所有菜单项 menu.clear(); // 根据当前所处的编辑状态(mState)来动态加载不同的菜单布局资源 if (mState == ListEditState.NOTE_LIST) { getMenuInflater().inflate(R.menu.note_list, menu); // 根据同步服务(GTaskSyncService)是否正在同步来设置"同步"菜单项的标题文本(显示同步或取消同步) menu.findItem(R.id.menu_sync).setTitle( GTaskSyncService.isSyncing()? R.string.menu_sync_cancel : R.string.menu_sync); } else if (mState == ListEditState.SUB_FOLDER) { getMenuInflater().inflate(R.menu.sub_folder, menu); } else if (mState == ListEditState.CALL_RECORD_FOLDER) { getMenuInflater().inflate(R.menu.call_record_folder, menu); } else { // 如果处于不识别的状态,则记录错误日志 Log.e(TAG, "Wrong state:" + mState); } return true; } // 当选项菜单中的某个菜单项被选中时触发的回调方法,用于处理不同菜单项对应的操作逻辑 @Override public boolean onOptionsItemSelected(MenuItem item) { switch (item.getItemId()) { case R.id.menu_new_folder: { // 调用showCreateOrModifyFolderDialog方法,传入true参数,用于显示创建或修改文件夹的对话框(具体功能由该方法实现) showCreateOrModifyFolderDialog(true); break; } case R.id.menu_export_text: { // 调用exportNoteToText方法,用于将笔记导出为文本文件(具体功能由该方法实现) exportNoteToText(); break; } case R.id.menu_sync: { // 判断是否处于同步模式(isSyncMode方法的具体逻辑未在代码中体现) if (isSyncMode()) { // 如果当前菜单项标题为"同步",则调用startSync方法启动同步服务(GTaskSyncService) if (TextUtils.equals(item.getTitle(), getString(R.string.menu_sync))) { GTaskSyncService.startSync(this); } else { // 如果当前菜单项标题为"取消同步",则调用cancelSync方法取消同步服务 GTaskSyncService.cancelSync(this); } } else { // 如果不处于同步模式,则调用startPreferenceActivity方法启动偏好设置相关的活动(具体功能由该方法实现) startPreferenceActivity(); } break; } case R.id.menu_setting: { // 调用startPreferenceActivity方法启动偏好设置相关的活动(具体功能由该方法实现) startPreferenceActivity(); break; } case R.id.menu_new_note: { // 调用createNewNote方法创建新的笔记(具体功能由该方法实现) createNewNote(); break; } case R.id.menu_search: // 调用onSearchRequested方法,触发搜索相关的操作(具体功能由该方法实现) onSearchRequested(); break; default: break; } return true; } // 当触发搜索请求时调用的方法 @Override public boolean onSearchRequested() { // 调用startSearch方法来启动搜索功能,传入相关参数,这里传入的参数分别表示:搜索关键词为null、不启用语音搜索(false)、应用数据为null、不限制搜索范围(false) startSearch(null, false, null /* appData */, false); // 返回true表示已处理搜索请求 return true; } // 用于将笔记导出为文本文件的方法 private void exportNoteToText() { // 获取BackupUtils的单例实例,传入当前的NotesListActivity作为上下文 final BackupUtils backup = BackupUtils.getInstance(NotesListActivity.this); // 创建一个异步任务(AsyncTask),用于在后台执行导出操作,其参数类型依次为输入参数(这里不需要,所以是Void)、进度参数(这里不需要,所以是Void)、结果参数(返回的是一个整数,用于表示导出操作的状态) new AsyncTask() { // 在后台线程中执行的方法,用于执行实际的导出到文本的操作,返回导出操作的状态码(由BackupUtils的exportToText方法决定) @Override protected Integer doInBackground(Void... unused) { return backup.exportToText(); } // 在后台操作完成后,在主线程中执行的方法,用于根据返回的结果状态码来进行相应的提示处理 @Override protected void onPostExecute(Integer result) { // 如果结果状态码表示SD卡未挂载 if (result == BackupUtils.STATE_SD_CARD_UNMOUONTED) { // 创建一个警告对话框构建器(AlertDialog.Builder),传入当前的NotesListActivity作为上下文 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)); // 设置对话框的确认按钮(正按钮),按钮文本为系统默认的"确定",点击时不做额外操作(传入null) builder.setPositiveButton(android.R.string.ok, null); // 显示构建好的警告对话框 builder.show(); } else if (result == BackupUtils.STATE_SUCCESS) { // 创建一个警告对话框构建器(AlertDialog.Builder),传入当前的NotesListActivity作为上下文 AlertDialog.Builder builder = new AlertDialog.Builder(NotesListActivity.this); // 设置对话框的标题,通过资源字符串获取对应的文本 builder.setTitle(NotesListActivity.this.getString(R.string.success_sdcard_export)); // 设置对话框的提示信息,通过资源字符串获取对应的格式化文本,其中格式化参数通过BackupUtils实例获取导出的文本文件名和文件目录 builder.setMessage(NotesListActivity.this.getString( R.string.format_exported_file_location, backup.getExportedTextFileName(), backup.getExportedTextFileDir())); // 设置对话框的确认按钮(正按钮),按钮文本为系统默认的"确定",点击时不做额外操作(传入null) builder.setPositiveButton(android.R.string.ok, null); // 显示构建好的警告对话框 builder.show(); } else if (result == BackupUtils.STATE_SYSTEM_ERROR) { // 创建一个警告对话框构建器(AlertDialog.Builder),传入当前的NotesListActivity作为上下文 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)); // 设置对话框的确认按钮(正按钮),按钮文本为系统默认的"确定",点击时不做额外操作(传入null) builder.setPositiveButton(android.R.string.ok, null); // 显示构建好的警告对话框 builder.show(); } } }.execute(); } // 判断是否处于同步模式的方法,通过获取同步账户名称(从NotesPreferenceActivity中获取)并检查其去除空格后的长度是否大于0来判断 private boolean isSyncMode() { return NotesPreferenceActivity.getSyncAccountName(this).trim().length() > 0; } // 用于启动偏好设置相关活动(NotesPreferenceActivity)的方法 private void startPreferenceActivity() { // 获取启动该活动的源Activity,如果当前Activity有父Activity,则使用父Activity,否则使用当前Activity本身 Activity from = getParent()!= null? getParent() : this; // 创建一个意图(Intent),指定要启动的目标Activity为NotesPreferenceActivity.class Intent intent = new Intent(from, NotesPreferenceActivity.class); // 根据意图启动活动,如果活动已经存在则复用(根据传入的请求码 -1 来决定具体行为,这里暂不详细分析请求码相关细节) from.startActivityIfNeeded(intent, -1); } // 实现了OnItemClickListener接口的内部类,用于处理列表项点击事件 private class OnListItemClickListener implements OnItemClickListener { // 当列表项被点击时触发的方法 public void onItemClick(AdapterView parent, View view, int position, long id) { // 如果被点击的视图是NotesListItem类型的实例 if (view instanceof NotesListItem) { // 获取该视图对应的笔记数据项(NoteItemData) NoteItemData item = ((NotesListItem) view).getItemData(); // 如果笔记列表适配器(mNotesListAdapter)处于选择模式(例如多选模式等) if (mNotesListAdapter.isInChoiceMode()) { // 如果数据项类型是笔记(Notes.TYPE_NOTE) if (item.getType() == Notes.TYPE_NOTE) { // 调整位置参数,减去列表头部视图的数量,以获取在实际数据列表中的正确位置 position = position - mNotesListView.getHeaderViewsCount(); // 调用回调接口(mModeCallBack)的方法,通知该项的选中状态发生改变,传入相关参数,这里传入的参数表示:视图为null(可能在某些情况下不需要具体视图引用)、调整后的位置、数据项ID、取反当前该项是否已被选中的状态(即切换选中状态) mModeCallBack.onItemCheckedStateChanged(null, position, id, !mNotesListAdapter.isSelectedItem(position)); } return; } // 根据当前所处的状态(mState)来进行不同的操作处理 switch (mState) { case NOTE_LIST: // 如果数据项类型是文件夹(Notes.TYPE_FOLDER)或者系统类型(Notes.TYPE_SYSTEM) if (item.getType() == Notes.TYPE_FOLDER || item.getType() == Notes.TYPE_SYSTEM) { // 调用openFolder方法打开对应的文件夹(具体功能由openFolder方法实现) openFolder(item); } else if (item.getType() == Notes.TYPE_NOTE) { // 调用openNode方法打开对应的笔记节点(具体功能由openNode方法实现) openNode(item); } else { // 如果数据项类型不符合预期,则记录错误日志 Log.e(TAG, "Wrong note type in NOTE_LIST"); } break; case SUB_FOLDER: case CALL_RECORD_FOLDER: // 如果数据项类型是笔记(Notes.TYPE_NOTE) if (item.getType() == Notes.TYPE_NOTE) { // 调用openNode方法打开对应的笔记节点(具体功能由openNode方法实现) openNode(item); } else { // 如果数据项类型不符合预期,则记录错误日志 Log.e(TAG, "Wrong note type in SUB_FOLDER"); } break; default: break; } } } } // 用于启动查询目标文件夹的方法 private void startQueryDestinationFolders() { // 构建查询条件字符串(SQL语句中的WHERE子句部分),用于筛选符合特定条件的文件夹记录 String selection = NoteColumns.TYPE + "=? AND " + NoteColumns.PARENT_ID + "<>? AND " + NoteColumns.ID + "<>?"; // 根据当前所处的编辑状态(mState)来进一步调整查询条件,如果处于NOTE_LIST状态,则使用原查询条件,否则添加额外的条件(与根文件夹相关的条件) selection = (mState == ListEditState.NOTE_LIST)? selection : "(" + selection + ") OR (" + NoteColumns.ID + "=" + Notes.ID_ROOT_FOLDER + ")"; // 通过后台查询处理器(mBackgroundQueryHandler)启动一个查询操作,传入相关参数,包括查询令牌(用于标识查询任务)、查询结果回调(这里传入null,可能后续有其他处理方式)、查询的内容URI(指向笔记相关的内容提供器的URI)、查询投影(指定要查询返回的列信息)、构建好的查询条件字符串、查询条件的参数值数组(对应查询条件中?占位符的值)以及排序方式(按照修改日期降序排列) 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) { // 获取该视图对应的笔记数据项(NoteItemData),并赋值给mFocusNoteDataItem,用于后续操作(比如上下文菜单等操作可能会用到) mFocusNoteDataItem = ((NotesListItem) view).getItemData(); // 如果数据项类型是笔记(Notes.TYPE_NOTE)并且笔记列表适配器(mNotesListAdapter)不处于选择模式 if (mFocusNoteDataItem.getType() == Notes.TYPE_NOTE &&!mNotesListAdapter.isInChoiceMode()) { // 尝试启动一个操作模式(例如上下文操作模式等),如果启动成功(返回不为null) if (mNotesListView.startActionMode(mModeCallBack)!= null) { // 调用回调接口(mModeCallBack)的方法,通知该项的选中状态变为已选中(传入相关参数,这里传入的参数表示:视图为null(可能在某些情况下不需要具体视图引用)、位置、数据项ID、选中状态为true) mModeCallBack.onItemCheckedStateChanged(null, position, id, true); // 触发触觉反馈(长按震动等反馈),使用系统默认的长按震动反馈常量 mNotesListView.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS); } else { // 如果启动操作模式失败,则记录错误日志 Log.e(TAG, "startActionMode fails"); } } else if (mFocusNoteDataItem.getType() == Notes.TYPE_FOLDER) { // 如果数据项类型是文件夹,则为笔记列表视图(mNotesListView)设置上下文菜单创建监听器为之前定义的mFolderOnCreateContextMenuListener,这样长按文件夹时可以弹出相应的上下文菜单 mNotesListView.setOnCreateContextMenuListener(mFolderOnCreateContextMenuListener); } } // 返回false,可能表示长按操作未完全消费该事件,具体行为可能取决于调用该方法的上层逻辑 return false; } }