diff --git a/lcx(2).txt b/lcx(2).txt new file mode 100644 index 0000000..486ca62 --- /dev/null +++ b/lcx(2).txt @@ -0,0 +1,5636 @@ +一、Contact.java + + + +// 定义一个名为Contact的公共类,用于处理与联系人相关的操作 +public class Contact { + + // 用于缓存联系人信息的静态哈希表,键为电话号码,值为对应的联系人姓名 + private static HashMap sContactCache; + + // 用于日志记录的标签,方便在日志中识别与该类相关的输出 + private static final String TAG = "Contact"; + + // 定义一个用于查询联系人的条件字符串 + // 该条件用于在联系人数据中查找与指定电话号码匹配的记录 + // 它涉及到多个联系人相关的表和字段的条件判断,例如匹配电话号码、数据类型等 + private static final String CALLER_ID_SELECTION = "PHONE_NUMBERS_EQUAL(" + ContactsContract.CommonDataKinds.Phone.NUMBER + + ",?) AND " + ContactsContract.Data.MIMETYPE + "='" + ContactsContract.CommonDataKinds.Phone.CONTENT_ITEM_TYPE + "'" + + " AND " + ContactsContract.Data.RAW_CONTACT_ID + " IN " + + "(SELECT raw_contact_id " + + " FROM contactscontract.phone_lookup" + + " WHERE min_match = '+')"; + + // 静态方法,用于根据给定的上下文和电话号码获取对应的联系人姓名 + public static String getContact(Context context, String phoneNumber) { + // 如果联系人缓存为空,则创建一个新的哈希表用于缓存 + if (sContactCache == null) { + sContactCache = new HashMap(); + } + + // 首先检查缓存中是否已经存在该电话号码对应的联系人姓名 + if (sContactCache.containsKey(phoneNumber)) { + // 如果存在,直接从缓存中获取并返回联系人姓名 + return sContactCache.get(phoneNumber); + } + + // 根据给定的电话号码,替换查询条件中的占位符 "+" + // 使用PhoneNumberUtils工具类将电话号码转换为适合查询的格式 + String selection = CALLER_ID_SELECTION.replace("+", + PhoneNumberUtils.toCallerIDMinMatch(phoneNumber)); + + // 使用上下文的内容解析器来执行查询操作 + // 查询的是联系人数据的内容URI + // 指定要获取的列是联系人的显示名称 + // 查询条件为前面生成的selection,参数是要查询的电话号码 + Cursor cursor = context.getContentResolver().query( + ContactsContract.Data.CONTENT_URI, + new String[]{ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME}, + selection, + new String[]{phoneNumber}, + null); + + // 如果查询结果游标不为空且能够移动到第一条记录(表示找到了匹配的联系人) + if (cursor!= null && cursor.moveToFirst()) { + try { + // 从游标中获取联系人的显示名称 + String name = cursor.getString(0); + // 将获取到的联系人姓名存入缓存,以便后续查询相同电话号码时直接使用 + sContactCache.put(phoneNumber, name); + // 返回获取到的联系人姓名 + return name; + } catch (IndexOutOfBoundsException e) { + // 如果在获取字符串时发生索引越界异常,记录错误日志 + Log.e(TAG, " Cursor get string error " + e.toString()); + return null; + } finally { + // 无论是否发生异常,都要关闭游标,释放资源 + cursor.close(); + } + } else { + // 如果没有找到匹配的联系人,记录一条日志信息 + Log.d(TAG, "No contact matched with number:" + phoneNumber); + return null; + } + } +} + + +二、Notes.java + + +package net.micode.notes.data; + +import android.net.Uri; + +// Notes类,用于定义与笔记相关的各种常量和数据结构 +public class Notes { + + // 定义内容提供者的授权标识,用于在Android系统中唯一标识该应用的内容提供者 + public static final String AUTHORITY = "micode_notes"; + + // 用于日志记录等场景的标签,方便在输出日志时识别与该类相关的信息 + public static final String TAG = "Notes"; + + // 定义笔记的类型常量,用于区分不同类型的笔记或文件夹 + public static final int TYPE_NOTE = 0; + public static final int TYPE_FOLDER = 1; + public static final int TYPE_SYSTEM = 2; + + /** + * 以下是系统文件夹的标识符说明: + * {@link Notes#ID_ROOT_FOLDER} 是默认文件夹的标识符 + * {@link Notes#ID_TEMPARAY_FOLDER} 用于存放不属于任何文件夹的笔记 + * {@link Notes#ID_CALL_RECORD_FOLDER} 用于存储通话记录相关的内容 + */ + public static final int ID_ROOT_FOLDER = 0; + public static final int ID_TEMPARAY_FOLDER = -1; + public static final int ID_CALL_RECORD_FOLDER = -2; + public static final int ID_TRASH_FOLER = -3; + + // 定义用于在Intent中传递提醒日期信息的额外键值 + public static final String INTENT_EXTRA_ALERT_DATE = "net.micode.notes.alert_date"; + + // 定义用于在Intent中传递背景颜色ID信息的额外键值 + public static final String INTENT_EXTRA_BACKGROUND_ID = "net.micode.notes.background_color_id"; + + // 定义用于在Intent中传递小部件ID信息的额外键值 + public static final String INTENT_EXTRA_WIDGET_ID = "net.micode.notes.widget_id"; + + // 定义用于在Intent中传递小部件类型信息的额外键值 + public static final String INTENT_EXTRA_WIDGET_TYPE = "net.micode.notes.widget_type"; + + // 定义用于在Intent中传递文件夹ID信息的额外键值 + public static final String INTENT_EXTRA_FOLDER_ID = "net.micode.notes.folder_id"; + + // 定义用于在Intent中传递通话日期信息的额外键值 + public static final String INTENT_EXTRA_CALL_DATE = "net.micode.notes.call_date"; + + // 定义无效小部件类型的常量 + public static final int TYPE_WIDGET_INVALIDE = -1; + + // 定义一种小部件类型的常量(可能是2x尺寸之类的含义,具体取决于应用场景) + public static final int TYPE_WIDGET_2X = 0; + + // 定义另一种小部件类型的常量(可能是4x尺寸之类的含义,具体取决于应用场景) + public static final int TYPE_WIDGET_4X = 1; + + // 内部静态类,用于定义与笔记数据相关的常量 + public static class DataConstants { + // 定义普通笔记的内容项类型常量,可能对应着某种数据结构或格式的标识 + public static final String NOTE = TextNote.CONTENT_ITEM_TYPE; + + // 定义通话笔记的内容项类型常量,同样可能对应着特定的数据结构或格式标识 + public static final String CALL_NOTE = CallNote.CONTENT_ITEM_TYPE; + } + + /** + * 用于查询所有笔记和文件夹的Uri,通过指定的内容提供者授权标识和路径构建而成 + */ + public static final Uri CONTENT_NOTE_URI = Uri.parse("content://" + AUTHORITY + "/note"); + + /** + * 用于查询数据的Uri,同样基于内容提供者授权标识和特定路径构建 + */ + public static final Uri CONTENT_DATA_URI = Uri.parse("content://" + AUTHORITY + "/data"); + + // 定义一个接口,用于规范笔记相关列的名称和类型等信息 + public interface NoteColumns { + /** + * 行的唯一ID,用于在数据库等存储结构中唯一标识每一行记录 + *

类型:INTEGER(长整型)

+ */ + public static final String ID = "_id"; + + /** + * 笔记或文件夹的父级ID,用于表示层级关系 + *

类型:INTEGER(长整型)

+ */ + public static final String PARENT_ID = "parent_id"; + + /** + * 笔记或文件夹的创建日期 + *

类型:INTEGER(长整型)

+ */ + public static final String CREATED_DATE = "created_date"; + + /** + * 笔记或文件夹的最新修改日期 + *

类型:INTEGER(长整型)

+ */ + public static final String MODIFIED_DATE = "modified_date"; + + + /** + * 提醒日期 + *

类型:INTEGER(长整型)

+ */// 定义一个公共的静态常量字符串,表示提醒日期的字段名 +// 该字段可能用于存储与某个事项(如笔记等)相关的提醒日期信息 +public static final String ALERTED_DATE = "alert_date"; + + /** + * 定义一个公共的静态常量字符串,表示文件夹的名称或者笔记的文本内容字段名 + *

类型:TEXT,即文本类型,用于存储字符串数据

+ */ + public static final String SNIPPET = "snippet"; + + /** + * 定义一个公共的静态常量字符串,表示笔记对应的小部件ID的字段名 + *

类型:INTEGER(长整型),用于存储整数值,可能是用于唯一标识与该笔记相关的小部件

+ */ + public static final String WIDGET_ID = "widget_id"; + + /** + * 定义一个公共的静态常量字符串,表示笔记对应的小部件类型的字段名 + *

类型:INTEGER(长整型),用于存储整数值,可能用于区分不同类型的小部件(如不同尺寸等)

+ */ + public static final String WIDGET_TYPE = "widget_id"; + + /** + * 定义一个公共的静态常量字符串,表示笔记的背景颜色ID的字段名 + *

类型:INTEGER(长整型),用于存储整数值,可能是用于唯一标识某种背景颜色设置

+ */ + public static final String BG_COLOR_ID = "bg_color_id"; + + /** + * 定义一个公共的静态常量字符串,表示是否有附件的字段名 + * 对于文本笔记,通常没有附件;而对于多媒体笔记,则至少有一个附件 + *

类型:INTEGER,用于存储整数值,可能用特定值表示有或无附件(如0表示无,1表示有等)

+ */ + public static final String HAS_ATTACHMENT = "has_attachment"; + + /** + * 定义一个公共的静态常量字符串,表示文件夹中笔记数量的字段名 + *

类型:INTEGER(长整型),用于存储整数值,用于记录该文件夹下包含的笔记的数量

+ */ + public static final String NOTES_COUNT = "notes_count"; + + /** + * 定义一个公共的静态常量字符串,表示文件类型(是文件夹还是笔记)的字段名 + *

类型:INTEGER,用于存储整数值,可能用特定值区分文件夹和笔记(如0表示笔记,1表示文件夹等)

+ */ + public static final String TYPE = "type"; + + /** + * 定义一个公共的静态常量字符串,表示最后同步ID的字段名 + *

类型:INTEGER(长整型),用于存储整数值,可能用于在数据同步过程中标识某次同步操作的唯一性

+ */ + public static final String SYNC_ID = "sync_id"; + + /** + * 定义一个公共的静态常量字符串,表示本地是否修改过的标志字段名 + *

类型:INTEGER,用于存储整数值,可能用特定值表示是否有本地修改(如0表示未修改,1表示已修改等)

+ */ + public static final String LOCAL_MODIFIED = "local_modified"; + + /** + * 定义一个公共的静态常量字符串,表示在移动到临时文件夹之前的原始父级ID的字段名 + *

类型:INTEGER,用于存储整数值,用于记录在进行特定移动操作之前,该文件(笔记或文件夹)原本的父级ID

+ */ + public static final String ORIGIN_PARENT_ID = "origin_parent_id"; + + /** + * 定义一个公共的静态常量字符串,表示GTask ID的字段名 + *

类型:TEXT,即文本类型,用于存储字符串数据,可能是与某种任务管理相关的特定ID标识

+ */ +// 定义一个公共的静态常量字符串,表示GTask ID的字段名 +// GTask可能是与某种任务管理相关的概念,该字段用于存储与之对应的唯一标识ID +// 类型为文本类型,用于存储字符串数据 +public static final String GTASK_ID = "gtask_id"; + + /** + * 定义一个公共的静态常量字符串,表示版本号的字段名 + *

类型:INTEGER(长整型),用于存储整数值,可能用于标识数据结构、应用程序或相关对象的版本信息

+ */ + public static final String VERSION = "version"; + } + + // 定义一个接口,用于规范与数据相关列的名称和类型等信息 + public interface DataColumns { + /** + * 定义一个公共的静态常量字符串,表示行的唯一ID的字段名 + * 用于在数据库等存储结构中唯一标识每一行记录 + *

类型:INTEGER(长整型),用于存储整数值

+ */ + public static final String ID = "_id"; + + /** + * 定义一个公共的静态常量字符串,表示此行所代表的项目的MIME类型的字段名 + * MIME类型用于标识数据的格式和用途等信息 + *

类型:Text,即文本类型,用于存储字符串数据

+ */ + public static final String MIME_TYPE = "mime_type"; + + /** + * 定义一个公共的静态常量字符串,表示该数据所属笔记的参考ID的字段名 + * 用于建立数据与对应笔记之间的关联 + *

类型:INTEGER(长整型),用于存储整数值

+ */ + public static final String NOTE_ID = "note_id"; + + /** + * 定义一个公共的静态常量字符串,表示笔记或文件夹的创建日期的字段名 + *

类型:INTEGER(长整型),用于存储整数值

+ */ + public static final String CREATED_DATE = "created_date"; + + /** + * 定义一个公共的静态常量字符串,表示笔记或文件夹的最新修改日期的字段名 + *

类型:INTEGER(长整型),用于存储整数值

+ */ + public static final String MODIFIED_DATE = "modified_date"; + + /** + * 定义一个公共的静态常量字符串,表示数据内容的字段名 + *

类型:TEXT,即文本类型,用于存储字符串数据

+ */ + public static final String CONTENT = "content"; + + + /** + * 定义一个公共的静态常量字符串,表示通用数据列的字段名,其含义取决于{@link #MIMETYPE} + * 用于存储整数数据类型的数据 + *

类型:INTEGER,用于存储整数值

+ */ + public static final String DATA1 = "data1"; + + /** + * 定义一个公共的静态常量字符串,表示通用数据列的字段名,其含义取决于{@link #MIMETYPE} + * 用于存储整数数据类型的数据 + *

类型:INTEGER,用于存储整数值

+ */ + public static final String DATA2 = "data2"; + + /** + * 定义一个公共的静态常量字符串,表示通用数据列的字段名,其含义取决于{@link #MIMETYPE} + * 用于存储文本数据类型的数据 + *

类型:TEXT,即文本类型,用于存储字符串数据

+ */ + public static final String DATA3 = "data3"; + + /** + * 定义一个公共的静态常量字符串,表示通用数据列的字段名,其含义取决于{@link #MIMETYPE} + * 用于存储文本数据类型的数据 + *

类型:TEXT,即文本类型,用于存储字符串数据

+ */ + public static final String DATA4 = "data4"; + + /** + * 定义一个公共的静态常量字符串,表示通用数据列的字段名,其含义取决于{@link #MIMETYPE} + * 用于存储文本数据类型的数据 + *

类型:TEXT,即文本类型,用于存储字符串数据

+ */ + public static final String DATA5 = "data5"; + } + + // 定义一个公共的静态内部类TextNote,它实现了DataColumns接口 + // 用于表示文本笔记相关的数据结构和属性 + public static final class TextNote implements DataColumns { + // 定义一个公共的静态常量字符串,表示用于指示文本是否处于检查列表模式的字段名 + // 类型为整数,1表示处于检查列表模式,0表示正常模式 + public static final String MODE = DATA1; + + // 定义一个公共的常量,表示检查列表模式的整数值 + public static final int MODE_CHECK_LIST = 1; + + // 定义一个公共的静态常量字符串,表示文本笔记的内容类型(可能用于在内容提供者等场景中标识数据类型) + public static final String CONTENT_TYPE = "vnd.android.cursor.dir/text_note"; + + // 定义一个公共的静态常量字符串,表示文本笔记的内容项类型(可能用于在内容提供者等场景中更精细地标识数据类型) + public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/text_note"; + + // 定义一个公共的静态常量字符串,表示用于查询文本笔记数据的Uri + // 通过指定的内容提供者授权标识和路径构建而成 + public static final Uri CONTENT_URI = Uri.parse("content://" + AUTHORITY + "/text_note"); + } + + // 定义一个公共的静态内部类CallNote,它实现了DataColumns接口 + // 用于表示通话笔记相关的数据结构和属性 + public static final class CallNote implements DataColumns { + // 定义一个公共的静态常量字符串,表示该通话记录的通话日期的字段名 + // 类型为整数(长整型),用于存储整数值 + public static final String CALL_DATE = DATA1; + + // 定义一个公共的静态常量字符串,表示该通话记录的电话号码的字段名 + // 类型为文本类型,用于存储字符串数据 + public static final String PHONE_NUMBER = DATA3; + + // 定义一个公共的静态常量字符串,表示通话笔记的内容类型(可能用于在内容提供者等场景中标识数据类型) + public static final String CONTENT_TYPE = "vnd.android.cursor.dir/call_note"; + + // 定义一个公共的 static常量字符串,表示通话笔记的内容项类型(可能用于在内容提供者等场景中更精细地标识数据类型) + public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/call_note"; + + // 定义一个公共的静态常量字符串,表示用于查询通话笔记数据的Uri + // 通过指定的内容提供者授权标识和路径构建而成 + public static final Uri CONTENT_URI = Uri.parse("content://" + AUTHORITY + "/call_note"); + } +} + + + +三、NotesDatabaseHelper.java + + +以下是对上述代码添加详细注释后的版本: + +```java +package net.micode.notes.data; + +import android.content.ContentValues; +import android.content.Context; +import android.database.sqlite.SQLiteDatabase; +import android.database.sqlite.SQLiteOpenHelper; +import android.util.Log; + +import net.micode.notes.data.Notes.DataColumns; +import net.micode.notes.data.Notes.DataConstants; +import net.micode.notes.data.Notes.NoteColumns; + +// NotesDatabaseHelper类继承自SQLiteOpenHelper,用于管理与笔记相关的数据库的创建、升级等操作 +public class NotesDatabaseHelper extends SQLiteOpenHelper { + + // 数据库的名称,这里指定为"note.db",用于在设备上存储笔记相关的数据 + private static final String DB_NAME = "note.db"; + + // 数据库的版本号,用于在数据库结构发生变化时进行版本控制,这里初始版本为4 + private static final int DB_VERSION = 4; + + // 定义一个内部接口TABLE,用于规范数据库中两张主要表的名称 + public interface TABLE { + // 表示存储笔记基本信息的表名 + public static final String NOTE = "note"; + + // 表示存储与笔记相关的数据(如内容、附件等相关信息)的表名 + public static final String DATA = "data"; + } + + // 用于日志记录的标签,方便在日志输出中识别与该数据库助手类相关的信息 + private static final String TAG = "NotesDatabaseHelper"; + + // 采用单例模式,用于保存NotesDatabaseHelper类的唯一实例,方便在整个应用中统一管理数据库操作 + private static NotesDatabaseHelper mInstance; + + // 创建NOTE表的SQL语句 + // 该表用于存储笔记的各种属性信息,如ID、父级ID、提醒日期、背景颜色ID等 + private static final String CREATE_NOTE_TABLE_SQL = + "CREATE TABLE " + TABLE.NOTE + "(" + + NoteColumns.ID + " INTEGER PRIMARY KEY," + + // 笔记的父级ID,用于表示笔记的层级关系,默认为0 + NoteColumns.PARENT_ID + " INTEGER NOT NULL DEFAULT 0," + + // 笔记的提醒日期,默认为0,具体含义可能根据应用需求确定,比如可能是某个时间戳 + NoteColumns.ALERTED_DATE + " INTEGER NOT NULL DEFAULT 0," + + // 笔记的背景颜色ID,默认为0,可能用于在应用中根据ID设置笔记的背景颜色 + NoteColumns.BG_COLOR_ID + " INTEGER NOT NULL DEFAULT 0," + + // 笔记的创建日期,使用SQLite的strftime函数获取当前时间的时间戳并乘以1000,默认为当前时间 + NoteColumns.CREATED_DATE + " INTEGER NOT NULL DEFAULT (strftime('%s','now') * 1000)," + + // 是否有附件的标识,默认为0表示没有附件,可能在有附件时设置为其他值 + NoteColumns.HAS_ATTACHMENT + " INTEGER NOT NULL DEFAULT 0," + + // 笔记的最新修改日期,同样使用strftime函数获取当前时间的时间戳并乘以1000,默认为当前时间 + NoteColumns.MODIFIED_DATE + " INTEGER NOT NULL DEFAULT (strftime('%s','now') * 1000)," + + // 文件夹中笔记的数量,默认为0,可能用于统计某个文件夹下包含的笔记数量 + NoteColumns.NOTES_COUNT + " INTEGER NOT NULL DEFAULT 0," + + // 笔记的摘要或片段内容,默认为空字符串,可能用于在列表展示等场景显示部分笔记内容 + NoteColumns.SNIPPET + " TEXT NOT NULL DEFAULT ''," + + // 笔记的类型,默认为0,具体类型含义可能根据应用内部分类确定,比如文本笔记、图片笔记等 + NoteColumns.TYPE + " INTEGER NOT NULL DEFAULT 0," + + // 笔记对应的小部件ID,默认为0,可能用于与应用中的小部件功能相关联,标识该笔记在小部件上的显示等 + NoteColumns.WIDGET_ID + " INTEGER NOT NULL DEFAULT 0," + + // 笔记对应的小部件类型,默认为 -1,可能用于区分不同类型的小部件,具体含义由应用定义 + NoteColumns.WIDGET_TYPE + " INTEGER NOT NULL DEFAULT -1," + + // 笔记的同步ID,默认为0,可能用于在数据同步操作中标识该笔记的同步状态等 + NoteColumns.SYNC_ID + " INTEGER NOT NULL DEFAULT 0," + + // 本地是否修改的标识,默认为0,表示未修改,当本地对笔记进行修改后可能设置为1 + NoteColumns.LOCAL_MODIFIED + " INTEGER NOT NULL DEFAULT 0," + + // 笔记在移动到临时文件夹之前的原始父级ID,默认为0,用于记录笔记的原始层级关系 + NoteColumns.ORIGIN_PARENT_ID + " INTEGER NOT NULL DEFAULT 0," + + // 笔记的GTask ID,默认为空字符串,可能与某种任务管理系统相关联的标识 + NoteColumns.GTASK_ID + " TEXT NOT NULL DEFAULT ''," + + // 笔记的版本号,默认为0,可能用于在笔记数据结构发生变化等情况下进行版本管理 + NoteColumns.VERSION + " INTEGER NOT NULL DEFAULT 0" + + ")"; + + // 创建DATA表的SQL语句 + // 该表用于存储与笔记相关的数据信息,如MIME类型、所属笔记ID、内容等 + private static final String CREATE_DATA_TABLE_SQL = + "CREATE TABLE " + TABLE.DATA + "(" + + DataColumns.ID + " INTEGER PRIMARY KEY," + + // 数据的MIME类型,用于标识数据的格式等信息,不能为空,具体类型根据数据内容确定 + DataColumns.MIME_TYPE + " TEXT NOT NULL," + + // 该数据所属笔记的ID,用于建立数据与笔记的关联,默认为0 + DataColumns.NOTE_ID + " INTEGER NOT NULL DEFAULT 0," + + // 数据的创建日期,同样使用strftime函数获取当前时间的时间戳并乘以1000,默认为当前时间 + NoteColumns.CREATED_DATE + " INTEGER NOT NOT NULL DEFAULT (strftime('%s','now') * 1000)," + + // 数据的最新修改日期,也使用strftime函数获取当前时间的时间化瘀间戳并乘以1000,默认为当前时间 + NoteColumns.MODIFIED_DATE + " INTEGER NOT NOT NULL DEFAULT (strftime('%s','now') * 1000)," + + // 数据的内容,默认为空字符串,用于存储具体的数据内容,如笔记的文本内容等 + DataColumns.CONTENT + " TEXT NOT NOT NULL DEFAULT ''," + + // 通用数据列DATA1,用于存储整数类型的数据,具体含义根据MIME_TYPE确定 + DataColumns.DATA1 + " INTEGER," + + // 通用数据列DATA2,用于存储整数类型的数据,具体含义根据MIME_TYPE确定 + DataColumns.DATA2 + " INTEGER," + + // 通用数据列DATA3,用于存储文本类型的数据,具体含义根据MIME_TYPE确定,默认为空字符串 + DataColumns.DATA3 + " TEXT NOT NOT NULL DEFAULT ''," + + // 通用数据列DATA4,用于存储文本类型的数据,具体含义根据MIME_TYPE确定,默为空字符串 + DataColumns.DATA4 + " TEXT NOT NOT NULL DEFAULT ''," + + // 通用数据列DATA5,用于存储文本类型的数据,具体含义根据MIME_TYPE确定,默为空字符串 + DataColumns.DATA5 + " TEXT NOT NOT NULL DEFAULT ''" + + ")"; + + // 创建基于DATA表中NOTE_ID字段的索引的SQL语句 + // 目的是为了提高根据NOTE_ID查询DATA表数据的效率,若索引不存在则创建 + private static final String CREATE_DATA_NOTE_ID_INDEX_SQL = + "CREATE INDEX IF NOT EXISTS note_id_index ON " + + TABLE.DATA + "(" + DataColumns.NOTE_ID + ");"; + + /** + * 以下是一系列触发器相关的SQL语句,用于在特定的数据库操作(如插入、更新、删除等)发生时,自动执行相应的逻辑,以维护数据的一致性和完整性。 + + * 当将笔记移动到文件夹时,增加文件夹的笔记数量的触发器SQL语句。 + * 该触发器在NOTE表的PARENT_ID字段更新后触发,会根据新的父级ID找到对应的文件夹记录,并将其NOTES_COUNT字段值加1。 + */ + private static final String NOTE_INCREASE_FOLDER_COUNT_ON_UPDATE_TRIGGER = + "CREATE TRIGGER increase_folder_count_on_update "+ + " AFTER UPDATE OF " + NoteColumns.PARENT_ID + " ON " + TABLE.NONE; + " BEGIN " + + " UPDATE " + TABLE.NOTE + + " SET " + NoteColumns.NOTES_COUNT + "=" + NoteColumns.NOTES_COUNT + " + 1" + + " WHERE " + NoteColumns.ID + "=new." + NoteColumns.PARENT_ID + ";" + + " END"; + + /** + * 当从文件夹中移动笔记时,减少文件夹的笔记数量的触发器SQL语句。 + * 该触发器在NOTE表的PARENT_ID字段更新后触发,会根据旧的父级ID找到对应的文件夹记录,并在其NOTES_COUNT字段值大于0时,将该值减1。 + */ + private static final String NOTE_DECREASE_FOLDER_COUNT_ON_UPDATE_TRIGGER = + "CREATE TRIGGER decrease_folder_count_on_update " + + " AFTER UPDATE OF " + NoteColumns.PARENT_ID + " ON " + TABLE.NOTE + + " BEGIN " + + " UPDATE " + TABLE.NOTE + + " SET " + NoteColumns.NOTES_COUNT + "=" + NoteColumns.NOTES_COUNT + "-1" + + " WHERE " + NoteColumns.ID + "=old." + NoteColumns.PARENT_ID + + " AND " + NoteColumns.NOTES_COUNT + ">0" + ";" + + " END"; + + /** + * 当向文件夹中插入新笔记时,增加文件夹的笔记数量的触发器SQL语句。 + * 该触发器在NOTE表插入新记录后触发,会根据新插入记录的父级ID找到对应的文件夹记录,并将其NOTES_COUNT字段值加1。 + */ + private static final String NOTE_INCREASE_FOLDER_COUNT_ON_INSERT_TRIGGER = + "CREATE TRIGGER increase_folder_count_on_insert " + + " AFTER INSERT ON " + TABLE.NOTE + + " BEGIN " + + " UPDATE " + TABLE.NOTE + + " SET " + NoteColumns.NOTES_COUNT + "=" + NoteColumns.NOTES_COUNT + " + 1" + + " WHERE " + NoteColumns.ID + "=new." + NoteColumns.PARENT_ID + ";" + + " END"; + + /** + * 当从文件夹中删除笔记时,减少文件夹的笔记数量的触发器SQL语句。 + * 该触发器在NOTE表删除记录后触发,会根据被删除记录的旧父级ID找到对应的文件夹记录,并在其NOTES_COUNT字段值大于0时,将该值减1。 + */ + private static final String NOTE_DECREASE_FOLDER_COUNT_ON_DELETE_TRIGGER = + "CREATE TRIGGER decrease_folder_count_on_delete " + + " AFTER DELETE ON " + TABLE.NOTE + + " BEGIN " + + " UPDATE " + TABLE.NOTE + + " SET " + NoteColumns.NOTES_COUNT + "=" + NoteColumns.NOTES_COUNT + "-1" + + " WHERE " + NoteColumns.ID + "=old." + NoteColumns.PARENT_ID + + " AND " + NoteColumns.NOTES_COUNT + ">0;" + + " END"; + + /** + * 当插入类型为{@link DataConstants#NOTE}的数据时,更新笔记内容的触发器SQL语句。 + * 该触发器在DATA表插入新记录后触发,当插入记录的MIME_TYPE字段值等于DataConstants.NOTE时,会根据新插入记录的NOTE_ID找到对应的笔记记录,并将其SNIPPET字段值更新为新插入记录的CONTENT字段值。 + */ + private static final String DATA_UPDATE_NOTE_CONTENT_ON_INSERT_TRIGGER = + "CREATE TRIGGER update_note_content_on_insert " + + " AFTER INSERT ON " + TABLE.DATA + + " WHEN new." + DataColumns.MIME_TYPE + "='" + DataConstants.NOTE + "'" + + " BEGIN" + + " UPDATE " + TABLE.NOTE + + " SET " + NoteColumns.SNIPPET + "=new." + DataColumns.CONTENT + + " WHERE " + NoteColumns.ID + "=new." + DataColumns.NOTE_ID + ";" + + " END"; + + /** + * 当类型为{@link DataConstants#NOTE}的数据发生更新时,更新笔记内容的触发器SQL语句。 + * 该触发器在DATA表更新记录后触发,当更新记录的旧MIME_TYPE字段值等于DataConstants.NOTE时,会根据更新记录的NOTE_ID找到对应的笔记记录,并将其SNIPPET字段值更新为新插入记录的CONTENT字段值。 + */ + private static final String DATA_UPDATE_NOTE_CONTENT_ON_UPDATE_TRIGGER = + "CREATE TRIGGER update_note_content_on_update " + + " AFTER UPDATE ON " + TABLE.DATA + + " WHEN old." + DataColumns.MIME_TYPE + "='" + DataConstants.NOTE + "'" + + " BEGIN" + + " UPDATE " + TABLE.NOTE + + " SET " + NoteColumns.SNIPPET + "=new." + DataColumns.CONTENT + + " WHERE " + NoteColumns.ID + "=new." + DataColumns.NOTE_ID + ";" + + " END"; + + /** + * 当类型为{@link DataConstants#NOTE}的数据发生删除时,更新笔记内容的触发器SQL语句。 + * 该触发器在DATA表删除记录后触发,当删除记录的旧MIME_TYPE字段值等于DataConstants.NOTE时,会根据删除记录的NOTE_ID找到对应的笔记记录,并将其SNIPPET字段值设置为空字符串。 + */ + private static final String DATA_UPDATE_NOTE_CONTENT_ON_DELETE_TRIGGER = + "CREATE TRIGGER update_note_content_on_delete " + + " AFTER delete ON " + TABLE.DATA + + " WHEN old." + DataColumns.MIME_TYPE + "='" + DataConstants.NOTE + "'" + + " BEGIN" + + " UPDATE " + TABLE.NOTE + + " SET " + NoteColumns.SNIPPET + "=''" + + " WHERE " + NoteColumns.ID + "=old." + DataColumns.NOTE_ID + ";" + + " END"; + + // 以下是SQLiteOpenHelper要求实现的抽象方法,用于数据库的创建和升级操作 + + // 构造函数,接受上下文作为参数,用于初始化SQLiteOpenHelper + public NotesDatabaseHelper(Context context) { + super(context, DB_NAME, null, DB_VERSION); + } + + // 获取NotesDatabaseHelper的单例实例的静态方法 + public static synchronized NotesDatabaseHelper getInstance(Context context) { + if (mInstance == null) { + mInstance = new NotesDatabaseHelper(context); + } + return mInstance; + } + + // 创建数据库的抽象方法,在其中执行创建表、索引和触发器等操作 + @Override + public void onCreate(SQLiteDatabase db) { + // 创建NOTE表 + db.execSQL(CREATE_NOTE_TABLE_SQL); + // 创建DATA表 + db.execSQL(CREATE_DATA_TABLE_SQL); + // 创建基于DATA表中NOTE_ID字段的索引 + db.execSQL(CREATE_DATA_NOTE_ID_INDEX_SQL); + // 创建增加文件夹笔记数量的更新触发器 + db.execSQL(NOTE_INCREASE_FOLDER_COUNT_ON_UPDATE_TRIGGER); + // 创建减少文件夹笔记数量的更新触发器 + db.execSQL(NOTE_DECREASE_FOLDER_COUNT_ON_UPDATE_TRIGGER); + // 创建增加文件夹笔记数量的插入触发器 + db.execSQL(NOTE_INCREASE_FOLDER_COUNT_ON_INSERT_TRIGGER); + // 创建减少文件夹笔记数量的删除触发器 + db.execSQL(NOTE_DECREASE_FOLDER_COUNT_ON_DELETE_TRIGGER); + // 创建插入数据时更新笔记内容的触发器 + db.execSQL(DATA_UPDATE_NOTE_CONTENT_ON_INSERT_TRIGGER); + // 创建更新数据时更新笔记内容的触发器 + db.execSQL(DATA_UPDATE_NOTE_CONTENT_ON_UPDATE_TRIGGER); + // 创建删除数据时更新笔记内容的触发器 + db.execSQL(DATA_UPDATE_NOTE_CONTENT_ON_DELETE_TRIGGER); + } + + // 升级数据库的抽象方法,根据不同的版本号进行相应的数据库结构更新 + @Override + public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { + // 这里可以根据不同的版本差异添加具体的升级逻辑,例如添加新列、修改表结构等 + if (newVersion > oldVersion) { + // 示例:如果从版本1升级到版本2,可能需要添加新列 + if (oldVersion == 1 && newVersion == 2) { + // 添加新列的SQL语句示例 + String addColumnSQL = "ALTER TABLE " + TABLE.NOTE + " ADD COLUMN new_column TEXT"; + db.execSQL(addColumnSQL); + } + } + } +} +/ NotesDatabaseHelper类继承自SQLiteOpenHelper,用于管理与笔记相关的数据库操作,如创建表、触发器、升级数据库等 +public class NotesDatabaseHelper extends SQLiteOpenHelper { + + // 以下是一系列用于定义数据库操作相关的SQL语句和方法,以维护笔记数据的完整性和一致性 + + // 定义一个触发器的SQL语句,用于在删除NOTE表中的记录时,同时删除与之关联的DATA表中的相关数据 + private static final String NOTE_DELETE_DATA_ON_DELETE_TRIGGER = + "CREATE TRIGGER delete_data_on_delete " + + " AFTER DELETE ON " + TABLE.NOTE + + " BEGIN" + + " DELETE FROM " + TABLE.DATA + + " WHERE " + DataColumns.NOTE_ID + "=old." + NoteColumns.ID + ";" + + " END"; + + /** + * 定义一个触发器的SQL语句,用于在删除NOTE表中的某个文件夹记录时,同时删除该文件夹下的所有笔记记录。 + * 即当删除一个文件夹时,将属于该文件夹的所有笔记也一并删除。 + */ + private static final String FOLDER_DELETE_NOTES_ON_DELETE_TRIGGER = + "CREATE TRIGGER folder_delete_notes_on_delete " + + " AFTER DELETE ON " + TABLE.NOTE + + " BEGIN" + + " DELETE FROM " + TABLE.NOTE + + " WHERE " + NoteColumns.PARENT_ID + "=old." + NoteColumns.ID + ";" + + " END"; + + /** + * 定义一个触发器的SQL语句,用于在将某个文件夹移动到回收站文件夹(通过更新NOTE表中的PARENT_ID字段实现)时, + * 将该文件夹下的所有笔记也一并移动到回收站文件夹。 + */ + private static final String FOLDER_MOVE_NOTES_ON_TRASH_TRIGGER = + "CREATE TRIGGER folder_move_notes_on_trash " + + " AFTER UPDATE ON " + TABLE.NOTE + + " WHEN new." + NoteColumns.PARENT_ID + "=" + Notes.ID_TRASH_FOLER + + " BEGIN" + + " UPDATE " + TABLE.NOTE + + " WHERE " + NoteColumns.PAREUSED_ID + "=old." + NoteColumns.ID + ";" + + " END"; + + // 构造函数,接受上下文作为参数,用于初始化SQLiteOpenHelper,传递数据库名称和版本号等信息 + public NotesDatabaseHelper(Context context) { + super(context, DB_NAME, null, DB_VERSION); + } + + // 方法用于在给定的SQLiteDatabase对象上创建NOTE表,并重新创建与NOTE表相关的触发器,以及创建系统文件夹记录 + public void createNoteTable(SQLiteDatabase db) { + // 执行创建NOTE表的SQL语句 + db.execSQL(CREATE_NOTE_TABLE_SQL); + // 重新创建NOTE表相关的触发器 + reCreateNoteTableTriggers(db); + // 创建系统文件夹记录 + createSystemFolder(db); + // 在日志中记录NOTE表已创建的信息 + Log.d(TAG, "note table has been created"); + } + + // 方法用于在给定的SQLiteDatabase对象上重新创建与NOTE表相关的触发器 + // 先删除已存在的相关触发器(如果有的话),然后再重新创建它们 + private void reCreateNoteTableTriggers(SQLiteDatabase db) { + // 删除增加文件夹笔记数量的更新触发器(如果存在) + db.execSQL("DROP TRIGGER IF EXISTS increase_folder_count_on_update"); + // 删除减少文件夹笔记数量的更新触发器(如果存在) + db.execSQL("DROP TRIGGER IF EXISTS decrease_folder_count_on_update"); + // 删除减少文件夹笔记数量的删除触发器(如果存在) + db.execSQL("DROP TRIGGER IF EXISTS decrease_folder_count_on_delete"); + // 删除删除NOTE表记录时同时删除关联DATA表数据的触发器(如果存在) + db.execSQL("DROP TRIGGER IF EXISTS delete_data_on_delete"); + // 删除增加文件夹笔记数量的插入触发器(如果存在) + db.execSQL("DROP TRIGGER IF EXISTS increase_folder_count_on_insert"); + // 删除删除文件夹时同时删除该文件夹下所有笔记的触发器(如果存在) + db.execSQL("DROP TRIGGER IF EXISTS folder_delete_notes_on_delete"); + // 删除将文件夹移动到回收站时同时移动该文件夹下所有笔记的触发器(如果存在) + db.execSQL("DROP TRIGGER IF EXISTS folder_move_notes_on_trash"); + + // 重新创建增加文件夹笔记数量的更新触发器 + db.execSQL(NOTE_INCREASE_FOLDER_COUNT_ON_UPDATE_TRIGGER); + // 重新创建减少文件夹笔记数量的更新触发器 + db.execSQL(NOTE_DECREASE_FOLDER_COUNT_ON_UPDATE_TRIGGER); + // 重新创建减少文件夹笔记数量的删除触发器 + db.execSQL(NOTE_DECREASE_FOLDERS_COUNT_ON_DELETE_TRIGGER); + // 重新创建删除NOTE表记录时同时删除关联DATA表数据的触发器 + db.execSQL(NOTE_DELETE_DATA_ON_DELETE_TRIGGER); + // 重新创建增加文件夹笔记数量的插入触发器 + db.execSQL(NOTE_INCREASE_FOLDER_COUNT_ON_INSERT_TRIGGER); + // 重新创建删除文件夹时同时删除该文件夹下所有笔记的触发器 + db.execSQL(FOLDER_DELETE_NOTES_ON_DELETE_TRIGGER); + // 重新创建将文件夹移动到回收站时同时移动该文件夹下所有笔记的触发器 + db.execSQL(FOLDER_MOVE_NOTES_ON_TRASH_TRIGGER); + } + + // 方法用于在给定的SQLiteDatabase对象上创建系统文件夹记录 + // 分别创建通话记录文件夹、根文件夹、临时文件夹和回收站文件夹的记录 + private void createSystemFolder(SQLiteDatabase db) { + ContentValues values = new ContentValues(); + + /** + * 创建通话记录文件夹记录,用于存储通话笔记 + */ + values.put(NoteColumns.ID, Notes.ID_CALL_RECORD_FOLDER); + values.put(NoteColumns.TYPE, Notes.TYPE_SYSTEM); + db.insert(TABLE.NOTE, null, values); + + /** + * 创建根文件夹记录,它是默认的文件夹 + */ + values.clear(); + values.put(NoteColumns.ID, Notes.ID_ROOT_FOLDER); + values.put(NoteColumns.TYPE, Notes.TYPE_SYSTEM); + db.insert(TABLE.NOTE, null, values); + + /** + * 创建临时文件夹记录,用于在移动笔记时暂存笔记 + */ + values.clear(); + values.put(NoteColumns.ID, Notes.ID_TEMPARAY_FOLDER); + values.put(NoteColumns.TYPE, Notes.TYPE_SYSTEM); + db.insert(TABLE.NOTE, null, values); + + /** + * 创建回收站文件夹记录 + */ + values.clear(); + values.put(NoteColumns.ID, Notes.ID_TRASH_FOLER); + values.put(NoteColumns.TYPE, Notes.TYPE_SYSTEM); + db.insert(TABLE.NOTE, null, values); + } + + // 方法用于在给定的SQLiteDatabase对象上创建DATA表,并重新创建与DATA表相关的触发器,以及创建基于NOTE_ID的索引 + public void createDataTable(SQLiteDatabase db) { + // 执行创建DATA表的SQL语句 + db.execSQL(CREATE_DATA_TABLE_SQL); + // 重新创建与DATA表相关的触发器 + reCreateDataTableTriggers(db); + // 执行创建基于NOTE_ID的索引的SQL语句 + db.execSQL(CREATE_DATA_NOTE_ID_INDEX_SQL); + // 在日志中记录DATA表已创建的信息 + Log.d(TAG, "data table has been created"); + } + + // 方法用于在给定的SQLiteDatabase对象上重新创建与DATA表相关的触发器 + // 先删除已存在的相关触发器(如果有的话),然后再重新创建它们 + private void reCreateDataTableTriggers(SQLiteDatabase db) { + // 删除插入数据时更新笔记内容的触发器(如果存在) + db.execSQL("DROP TRIGGER IF EXISTS update_note_content_on_insert"); + // 删除更新数据时更新笔记内容的触发器(如果存在) + db.execSQL("DROP TRIGGER IF EXISTS update_note_content_on_update"); + // 删除删除数据时更新笔记内容的触发器(如果存在) + db.execSQL("DROP TRIGGER IF EXISTS update_note_content_on_delete"); + + // 重新创建插入数据时更新笔记内容的触发器 + db.execSQL(DATA_UPDATE_NOTE_CONTENT_ON_INSERT_TRIGGER); + // 重新创建更新数据时更新笔记内容的触发器 + db.execSQL(DATA_UPDATE_NOTE_CONTENT_ON_UPDATE_TRIGGER); + // 重新创建删除数据时更新笔记内容的触发器 + db.execSQL(DATA_UPDATE_NOTE_CONTENT_ON_DELETE_TRIGGER); + } + + // 静态同步方法,用于获取NotesDatabaseHelper的单例实例 + // 如果实例不存在,则创建一个新的实例并返回 + static synchronized NotesDatabaseHelper getInstance(Context context) { + if (mInstance == null) { + mInstance = new NotesDatabaseHelper(context); + } + return mInstance; + } + + // 重写SQLiteOpenHelper的onCreate方法,用于在数据库首次创建时执行相关操作 + // 在这里会调用创建NOTE表和DATA表的方法 + @Override + public void onCreate(SQLiteDatabase db) { + createNoteTable(db); + createDataTable(db); + } + + // 重写SQLiteOpenHelper的onUpgrade方法,用于在数据库版本升级时执行相关操作 + // 根据不同的旧版本号和新版本号执行相应的升级逻辑,可能包括修改表结构、添加列、重新创建触发器等 + @Override + public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { + boolean reCreateTriggers = false; + boolean skipV2 = false; + + if (oldVersion == 1) { + // 从版本1升级到更高版本的逻辑 + upgradeToV2(db); + skipV2 = true; // 表示此次升级包含了从v2到v3的升级,跳过后续单独针对v2到v3的升级逻辑 + oldVersion++; + } + + if (oldVersion == 2 &&!skipV2) { + // 单独针对从版本2升级到版本3的逻辑,如果之前已经执行过包含此升级的操作,则跳过 + upgradeToV3(db); + reCreateTriggers = true; + oldVersion++; + } + + if (oldVersion == 3) { + // 从版本3升级到版本4的逻辑 + upgradeToV4(db); + oldVersion++; + } + + if (reCreateTriggers) { + // 如果需要重新创建触发器,则调用相应方法重新创建NOTE表和DATA表的触发器 + reCreateNoteTableTriggers(db); + reCreateDataTableTriggers(db); + } + + if (oldVersion!= newVersion) { + // 如果升级后旧版本号和新版本号不一致,抛出异常,表示升级失败 + throw new IllegalStateException("Upgrade notes database to version " + newVersion + + "fails"); + } + } + + // 方法用于将数据库从版本1升级到版本2的具体操作 + // 包括删除原有的NOTE表和DATA表,然后重新创建它们 + private void upgradeToV2(SQLiteDatabase db) { + db.execSQL("DROP TABLE IF EXISTS " + TABLE.NOTE); + db.execSQL("DROP TABLE IF EXISTS " + TABLE.DATA); + createNoteTable(db); + createDataTable(db); + } + + // 方法用于将数据库从版本2升级到版本3的具体操作 + // 包括删除一些不再使用的触发器,添加一个新列(gtask id)到NOTE表,以及创建回收站系统文件夹 + private void upgradeToV3(db) { + // 删除更新笔记修改日期的插入触发器(如果存在) + db.execSQL("DROP TRIGGER IF EXISTS update_note_modified_date_on_insert"); + // 删除更新笔记修改日期的删除触发器(如果存在) + db.execSQL("DROP TRIGGER IF EXISTS update_note_modified_date_on_delete"); + // 删除更新笔记修改日期的更新触发器(如果存在) + db.execSQL("DROP TRIGGER IF EXISTS update_note_modified_date_on_update"); + // 添加一个新列(gtask id)到NOTE表 + db.execSQL("ALTER TABLE " + TABLE.NOTE + " ADD COLUMN " + NoteColumns.GTASK_ID + + " TEXT NOT NULL DEFAULT ''"); + // 创建一个回收站系统文件夹记录 + ContentValues values = new ContentValues(); + values.put(NoteColumns.ID, Notes.ID_TRASH_FOLER); + values.put(NoteColumns.TYPE, Notes.TYPE_SYSTEM); + db.insert(TABLE.NOTE, null, values); + } + + // 方法用于将数据库从版本3升级到版本4的具体操作 + // 包括添加一个新列(版本号)到NOTE表 + private void upgradeToV4(db) { + db.execSQL("ALTER TABLE " + TABLE.NOTE + " ADD COLUMN " + NoteColumns.VERSION + + " INTEGER NOT NULL DEFAULT 0"); + } +} + + + +四、NotesProvider.java + + + +package net.micode.notes.data; + +import android.app.SearchManager; +import android.content.ContentProvider; +import android.content.ContentUris; +import android.content.ContentValues; +import android.content.Intent; +import android.content.UriMatcher; +import android.database.Cursor; +import android.database.sqlite.SQLiteDatabase; +import android.net.Uri; +import android.text.TextUtils; +import android.util.Log; + +import net.micode.notes.R; +import net.micode.notes.data.Notes.DataColumns; +import net.micode.notes.data.Notes.NoteColumns; +import net.micode.notes.data.NotesDatabaseHelper.TABLE; + +// NotesProvider类继承自ContentProvider,用于作为内容提供者,为应用提供数据访问接口,处理与笔记相关的数据查询、插入、删除和更新等操作 +public class NotesProvider extends ContentProvider { + + // UriMatcher用于匹配传入的Uri,以便确定要执行的操作类型 + private static final UriMatcher mMatcher; + + // 用于辅助数据库操作的NotesDatabaseHelper实例 + private NotesDatabaseHelper mHelper; + + // 用于日志记录的标签,方便在日志输出中识别与该内容提供者相关的信息 + private static final String TAG = "NotesProvider"; + + // 定义不同的常量,用于标识不同类型的Uri,以便在后续的操作中根据Uri类型执行相应的逻辑 + private static final int URI_NOTE = 1; + private static final int URI_NOTE_ITEM = 2; + private static final int URI_DATA = 3; + private static final int URI_DATA_ITEM = 4; + private static final int URI_SEARCH = 5; + private static final int URI_SEARCH_SUGGEST = 6; + + // 静态初始化块,用于配置UriMatcher,将不同的Uri模式与对应的常量进行匹配 + static { + mMatcher = new UriMatcher(UriMatcher.NO_MATCH); + mMatcher.addURI(Notes.AUTHORITY, "note", URI_NOTE); + mMatcher.addURI(Notes.AUTHORITY, "note/#", URI_NOTE_ITEM); + mMatcher.addURI(Notes.AUTHORITY, "data", URI_DATA); + mMatcher.addURI(Notes.AUTHORITY, "data/#", URI_DATA_ITEM); + mMatcher.addURI(Notes.AUTHORITY, "search", URI_SEARCH); + mMatcher.addURI(Notes.AUTHORITY, SearchManager.SUGGEST_URI_PATH_QUERY, URI_SEARCH_SUGGEST); + mMatcher.addURI(Notes.AUTHORITY, SearchManager.SUGGEST_URI_PATH_QUERY + "/*", URI_SEARCH_SUGGEST); + } + + /** + * 在SQLite中,x'0A' 表示换行符 '\n'。对于搜索结果中的标题和内容, + * 我们将去除换行符和空白字符,以便展示更多信息。 + */ + private static final String NOTES_SEARCH_PROJECTION = NoteColumns.ID + "," + + NoteColumns.ID + " AS " + SearchManager.SUGGEST_COLUMN_INTENT_EXTRA_DATA + "," + + "TRIM(REPLACE(" + NoteColumns.SNIPPET + ", x'0A','')) AS " + SearchManager.SUGGEST_COLUMN_TEXT_1 + "," + + "TRIM(REPLACE(" + NoteColumns.SNIPPET + ", x'0A','')) AS " + SearchManager.SUGGEST_COLUMN_TEXT_2 + "," + + R.drawable.search_result + " AS " + SearchManager.SUGGEST_COLUMN_ICON_1 + "," + + "'" + Intent.ACTION_VIEW + "' AS " + SearchManager.SUGGEST_COLUMN_INTENT_ACTION + "," + + "'" + Notes.TextNote.CONTENT_TYPE + "' AS " + SearchManager.SUGGEST_COLUMN_INTENT_DATA; + + // 定义用于搜索笔记片段(SNIPPET)的查询语句模板,用于根据搜索字符串在NOTE表中查找匹配的笔记记录 + private static String NOTES_SNIPPET_SEARCH_QUERY = "SELECT " + NOTES_SEARCH_PROJECTION + + " FROM " + TABLE.NOTE + + " WHERE " + NoteColumns.SNIPPET + " LIKE?" + + " AND " + NoteColumns.PARENT_ID + "<>" + Notes.ID_TRASH_FOLER + + " AND " + NoteColumns.TYPE + "=" + Notes.TYPE_NOTE; + + // 当ContentProvider被创建时调用的方法,用于初始化相关资源,这里主要是获取NotesDatabaseHelper的实例 + @Override + public boolean onCreate() { + mHelper = NotesDatabaseHelper.getInstance(getContext()); + return true; + } + + // 用于处理数据查询操作的方法,根据传入的Uri、投影(要查询的列)、选择条件等参数执行相应的数据库查询操作,并返回查询结果游标 + @Override + public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, + String sortOrder) { + Cursor c = null; + SQLiteDatabase db = mHelper.getReadableDatabase(); + String id = null; + + // 根据UriMatcher匹配传入的Uri,确定要执行的查询逻辑 + switch (mMatcher.match(uri)) { + case URI_NOTE: + // 如果是查询所有笔记(URI_NOTE类型的Uri),直接使用提供的投影、选择条件和排序顺序在NOTE表中进行查询 + c = db.query(TABLE.NOTE, projection, selection, selectionArgs, null, null, + sortOrder); + break; + case URI_NOTE_ITEM: + // 如果是查询特定笔记(URI_NOTE_ITEM类型的Uri),先从Uri中获取笔记的ID,然后在查询条件中添加该ID的匹配条件,再进行查询 + id = uri.getPathSegments().get(1); + c = db.query(TABLE.NOTE, projection, NoteColumns.ID + "=" + id + + parseSelection(selection), selectionArgs, null, null, sortOrder); + break; + case URI_DATA: + // 如果是查询所有数据(URI_DATA类型的Uri),在DATA表中按照提供的投影、选择条件和排序顺序进行查询 + c = db.query(TABLE.DATA, projection, selection, selectionArgs, null, null, + sortOrder); + break; + case URI_DATA_ITEM: + // 如果是查询特定数据(URI_DATA_ITEM类型的Uri),先从Uri中获取数据的ID,然后在查询条件中添加该ID的匹配条件,再进行查询 + id = uri.getPathSegments().get(1); + c = db.query(TABLE.DATA, projection, DataColumns.ID + "=" + id + + parseSelection(selection), selectionArgs, null, null, sortOrder); + break; + case URI_SEARCH: + case URI_SEARCH_SUGGEST: + // 如果是搜索相关的Uri(URI_SEARCH或URI_SEARCH_SUGGEST类型) + if (sortOrder!= null || projection!= null) { + // 如果指定了排序顺序或投影,抛出异常,因为搜索操作有特定的查询逻辑,不应该指定这些参数 + throw new IllegalArgumentException( + "do not specify sortOrder, selection, selectionArgs, or projection" + "with this query"); + } + + String searchString = null; + if (mMatcher.match(uri) == URI_SEARCH_SUGGEST) { + // 如果是URI_SEARCH_SUGGEST类型的Uri,且Uri路径段数量大于1,从路径段中获取搜索字符串 + if (uri.getPathSegments().size() > 1) { + searchString = uri.getPathSegments().get(1); + } + } else { + // 如果是URI_SEARCH类型的Uri,从Uri的查询参数中获取搜索字符串(通过"pattern"参数) + searchString = uri.getQueryParameter("pattern"); + } + + if (TextUtils.isEmpty(searchString)) { + // 如果搜索字符串为空,直接返回空游标,因为没有要查询的内容 + return null; + } + + try { + // 对搜索字符串进行格式化,添加通配符,以便在数据库中进行模糊匹配查询 + searchString = String.format("%%%s%%", searchString); + c = db.rawQuery(NOTES_SNIPPET_SEARCH_QUERY, + new String[] { searchString }); + } catch (IllegalStateException ex) { + // 如果在执行原始查询时出现异常,记录错误日志 + Log.e(TAG, "got exception: " + ex.toString()); + } + break; + default: + // 如果传入的Uri不匹配任何已知的类型,抛出异常,表示未知的Uri + throw new IllegalArgumentException("Unknown URI " + uri); + } + + if (c!= null) { + // 如果查询结果游标不为空,设置通知Uri,以便在数据发生变化时能够接收到通知 + c.setNotificationUri(getContext().getContentResolver(), uri); + } + + return c; + } + + // 用于处理数据插入操作的方法,根据传入的Uri和要插入的ContentValues,将数据插入到相应的数据库表中,并返回插入后数据的Uri + @Override + public Uri insert(Uri uri, ContentValues values) { + SQLiteDatabase db = mHelper.getWritableDatabase(); + long dataId = 0, noteId = 0, insertedId = 0; + + // 根据UriMatcher匹配传入的Uri,确定要执行的插入逻辑 + switch (mMatcher.match(uri)) { + case URI_NOTE: + // 如果是插入笔记(URI_NOTE类型的Uri),直接将数据插入到NOTE表中,并获取插入后的笔记ID + insertedId = noteId = db.insert(TABLE.NOTE, null, values); + break; + case URI_DATA: + // 如果是插入数据(URI_DATA类型的Uri),先检查是否包含所属笔记的ID,如果不包含则记录错误日志 + if (values.containsKey(DataColumns.NOTE_ID)) { + noteId = values.getAsLong(DataColumns.NOTE_ID); + } else { + Log.d(TAG, "Wrong data format without note id:" + values.toString()); + } + // 将数据插入到DATA表中,并获取插入后的 数据ID + insertedId = dataId = db.insert(TABLE.DATA, null, values); + break; + default: + // 如果传入的Uri不匹配任何已知的类型,抛出异常,表示未知的Uri + throw new IllegalArgumentException("Unknown URI " + uri); + } + + // 如果插入的笔记ID大于0,通知与笔记相关的Uri发生了变化,以便相关观察者能够更新数据显示等操作 + if (noteId > 0) { + getContext().getContentResolver().notifyChange( + ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, noteId), null); + } + + // 如果插入的数据ID大于0,通知与数据相关的Uri发生了变化,以便相关观察者能够更新数据显示等操作 + if (dataId > 0) { + getContext().getContentResolver().notifyChange( + ContentUris.withAppendedId(Notes.CONTENT_DATA_URI, dataId), null); + } + + // 返回插入后数据的Uri,通过在原始Uri上附加插入后的ID来构建 + return ContentUris.withAppendedId(uri, insertedId); + } + + // 用于处理数据删除操作的方法,根据传入的Uri、选择条件和选择条件参数,从相应的数据库表中删除匹配的数据,并返回删除的行数 + @Override + public int delete(Uri uri, String selection, String[] selectionArgs) { + int count = 0; + String id = null; + SQLiteDatabase db = mHelper.getWritableDatabase(); + boolean deleteData = false; + + // 根据UriMatcher匹配传入的Uri,确定要执行的删除逻辑 + switch (mMatcher.match(uri)) { + case URI_NOTE: + // 如果是删除所有笔记(URI_NOTE类型的Uri),在选择条件中添加笔记ID大于0的条件(可能是为了避免删除系统文件夹等特殊记录),然后执行删除操作并获取删除的行数 + selection = "(" + selection + ") AND " + NoteColumns.ID + ">0 "; + count = db.delete(TABLE.NOTE, selection, selectionArgs); + break; + case URI_NOTE_ITEM: + // 如果是删除特定笔记(URI_NOTE_ITEM类型的Uri),先从Uri中获取笔记的ID,然后判断如果ID小于等于0(可能是系统文件夹等特殊记录)则不执行删除操作,否则执行删除操作并获取删除的行数 + id = uri.getPathSegments().get(1); + /** + * ID that smaller than 0 is system folder which is not allowed to + * trash + */ + long noteId = Long.valueOf(id); + if (noteId <= 0) { + break; + } + count = db.delete(TABLE.NOTE, + NoteColumns.ID + "=" + id + parseSelection(selection), selectionArgs); + break; + case URI_DATA: + // 如果是删除所有数据(URI_DATA类型的Uri),执行删除操作并设置标记表示正在删除数据,然后获取删除的行数 + count = db.delete(TABLE.DATA, selection, selectionArgs); + deleteData = true; + break; + case URI_DATA_ITEM: + // 如果是删除特定数据(URI_DATA_ITEM类型的Uri),先从Uri中获取数据的ID,然后执行删除操作并设置标记表示正在删除数据,最后获取删除的行数 + id = uri.getPathSegments().get(1); + count = db.delete(TABLE.DATA, + DataColumns.ID + "=" + id + parseSelection(selection), selectionArgs); + deleteData = true; + break; + default: + // 如果传入的Uri不匹配任何已知的类型,抛出异常,表示未知的Uri + throw new IllegalArgumentException("Unknown URI " + uri); + } + + if (count > 0) { + // 如果删除的行数大于0 + if (deleteData) { + // 如果正在删除数据,通知与笔记相关的Uri发生了变化,以便相关观察者能够更新数据显示等操作 + getContext().getContentResolver().notifyChange(Notes.CONTENT_NOTE_URI, null); + } + // 通知与当前操作的Uri发生了变化,以便相关观察者能够更新数据显示等操作 + getContext(). contentTypeResolver.notifyChange(uri, null); + } + + return count; + } + + // 用于处理数据更新操作的方法,根据传入的Uri、要更新的ContentValues、选择条件和选择条件参数,对相应的数据库表中的数据进行更新,并返回更新的行数 + @Override + public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) { + int count = 0; + String id = null; + SQLiteDatabase db = mHelper.getWritableDatabase(); + boolean updateData = false; + + // 根据UriMatcher匹配传入的Uri,确定要执行的更新逻辑 + switch (mMatcher.match(uri)) { + case URI_NOTE: + // 如果是更新所有笔记(URI_NOTE类型的Uri),先调用increaseNoteVersion方法增加笔记的版本号(可能用于数据一致性控制等),然后执行更新操作并获取更新的行数 + increaseNoteVersion(-1, selection, selectionArgs); + count = db.update(TABLE.NOTE, values, selection, selectionArgs); + break; + case URI_NOTE_ITEM: + // 如果是更新特定笔记(URI_NOTE_ITEM类型的Uri),先从Uri中获取笔记的ID,然后调用increaseNoteVersion方法增加指定笔记的版本号,最后执行更新操作并获取更新的行数 + id = uri.getPathSegments().get(1); + increaseNoteVersion(Long.valueOf(id), selection, selectionArgs); + count = db.update(TABLE.NOTE, values, NoteColumns.ID + "=" + id + + parseSelection(selection), selectionArgs); + break; + case URI_DATA: + // 如果是更新所有数据(URI_DATA类型的Uri),执行更新操作并设置标记表示正在更新数据,然后获取更新的行数 + count = db.update(TABLE.DATA, values, selection, selectionArgs); + updateData = true; + break; + case URI_DATA_ITEM: + // 如果是更新特定数据(URI_DATA_ITEM类型的Uri),先从Uri中获取数据的ID,然后执行更新操作并设置标记表示正在更新数据,最后获取更新的行数 + id = uri.getPathSegments().get(1); + count = db.update(TABLE.DATA, values, DataColumns.ID + "=" + id + + parseCountryId = uri.getPathSegments().get(1); + count = db.update(TABLE.DATA, values, DataColumns.ID + "=" + id + + parseSelection(selection), selectionArgs); + updateData = true; + break; + default: + // 如果传入的Uri不匹配任何已知的类型,抛出异常,表示未知的Uri + throw new IllegalArgumentException("Unknown URI " + uri); + } + + if (count > 0) { + // 如果更新的行数大于0 + if (updateData) { + // 如果正在更新数据,通知与笔记相关的Uri发生了变化,以便相关观察者能够更新数据显示等操作 + getContext().getContentResolver().notifyChange(Notes.CONTENT_NOTE_URI, null); + } + // 通知与当前操作的Uri发生了变化,以便相关观察者能够更新数据显示等操作 + getContext().getContentResolver().notifyChange(uri, null); + } + + return count; + } + +/** + * 该方法用于解析选择条件字符串。 + * 如果传入的选择条件字符串不为空,就在其前后添加特定的连接字符,使其符合在数据库操作(如查询、更新、删除)中作为条件使用的格式。 + * 具体来说,如果选择条件不为空,会返回 " AND (" + 选择条件 + ")" 的格式,以便在后续数据库操作的SQL语句中正确拼接条件部分; + * 如果选择条件为空,则返回空字符串。 + * + * @param selection 要解析的选择条件字符串 + * @return 解析后的选择条件字符串,符合数据库操作中条件使用的格式或为空字符串 + */ + private String parseSelection(String selection) { + return (!TextUtils.isEmpty(selection)? " AND (" + selection + ')' : ""); + } + + /** + * 该方法用于增加指定笔记的版本号。 + * 它会构建一个SQL语句来更新笔记表(TABLE.NOTE)中的版本号字段(NoteColumns.VERSION),使其值增加1。 + * 如果传入的笔记ID大于0或者选择条件字符串不为空,会在SQL语句中添加WHERE子句来指定更新的条件。 + * 如果仅传入了笔记ID大于0,就在WHERE子句中添加根据笔记ID进行匹配的条件; + * 如果仅传入了不为空的选择条件字符串,会先调用parseSelection方法对其进行解析,然后将解析后的选择条件字符串添加到WHERE子句中; + * 如果同时传入了笔记ID大于0和不为空的选择条件字符串,会先对选择条件字符串进行解析,然后将解析后的选择条件字符串与笔记ID匹配条件一起添加到WHERE子句中。 + * 在将选择条件字符串添加到WHERE子句之前,如果有占位符("?"),会用传入的选择条件参数数组(selectionArgs)中的值依次替换占位符。 + * 最后,通过获取可写数据库实例(mHelper.getWritableDatabase())并执行构建好的SQL语句来实现更新笔记版本号的操作。 + * + * @param id 要增加版本号的笔记的ID,如果为 -1 可能表示对所有符合条件的笔记进行操作(具体根据业务逻辑确定) + * @param selection 用于进一步筛选要更新笔记的选择条件字符串,可能包含占位符("?") + * @param selectionArgs 与选择条件字符串中的占位符对应的参数数组,用于替换占位符的值 + */ + private void increaseNoteVersion(long id, String selection, String[] selectionArgs) { + StringBuilder sql = new StringBuilder(120); + // 开始构建SQL语句,设置要执行的操作是更新(UPDATE) + sql.append("UPDATE "); + // 指定要更新的表为笔记表(TABLE.NOTE) + sql.append(TABLE.NOTE); + // 设置要更新的字段为版本号字段(NoteColumns.VERSION),并将其值增加1 + sql.append(" SET "); + sql.append(NoteColumns.VERSION); + sql.append("=" + NoteColumns.VERSION + "+1 "); + + // 如果传入的笔记ID大于0或者选择条件字符串不为空,需要添加WHERE子句来指定更新的条件 + if (id > 0 ||!TextUtils.isEmpty(selection)) { + sql.append(" WHERE "); + } + + // 如果传入的笔记ID大于0,在WHERE子句中添加根据笔记ID进行匹配的条件 + if (id > 0) { + sql.append(NoteColumns.ID + "=" + String.valueOf(id)); + } + + // 如果传入的选择条件字符串不为空 + if (!TextUtils.isEmpty(selection)) { + // 如果笔记ID大于0,先调用parseSelection方法对选择条件字符串进行解析,以便在后续正确添加到WHERE子句中 + String selectString = id > 0? parseSelection(selection) : selection; + + // 遍历选择条件参数数组,将选择条件字符串中的占位符("?")依次用参数数组中的值进行替换 + for (String args : selectionArgs) { + selectString = selectString.replaceFirst("\\?", args); + } + + // 将处理好的选择条件字符串添加到SQL语句的WHERE子句中 + sql.append(selectString); + } + + // 获取可写数据库实例,并执行构建好的SQL语句来更新笔记的版本号 + mHelper.getWritableDatabase().execSQL(sql.toString()); + } + + /** + * 该方法是ContentProvider要求重写的方法,用于获取给定Uri对应的MIME类型。 + * 目前该方法只是一个占位符,返回了null,实际应用中应该根据传入的Uri来确定并返回正确的MIME类型。 + * MIME类型用于标识数据的格式和用途等信息,在内容提供者与其他组件(如内容解析器)交互时起到重要作用, + * 例如,当其他组件通过内容解析器查询数据时,可能会根据返回的MIME类型来正确处理获取到的数据。 + * + * @param uri 要获取MIME类型的Uri + * @return 目前返回null,实际应根据Uri返回对应的MIME类型 + */ + @Override + public String getType(Uri uri) { + // TODO Auto-generated method stub + return null; + } + +} + + + +五、MetaData.java + + +package net.micode.notes.gtask.data; +// 导入相关的类,用于数据库游标操作、日志记录以及处理JSON相关操作等 +import android.database.Cursor; +import android.util.Log; +import net.micode.notes.tool.GTaskStringUtils; +import org.json.JSONException; +import org.json.JSONObject; + +// MetaData类继承自Task类,可能用于处理与任务相关的元数据信息 +public class MetaData extends Task { + // 用于日志输出的标签,取当前类的简单名称,方便在日志中识别来源 + private final static String TAG = MetaData.class.getSimpleName(); + // 存储相关的GID(可能是任务的全局唯一标识符之类的标识信息),初始化为null + private String mRelatedGid = null; + + // 设置元数据的方法,接收一个GID和一个JSONObject类型的元数据信息对象 + public void setMeta(String gid, JSONObject metaInfo) { + try { + // 将给定的GID放入元数据信息对象中,对应的键由GTaskStringUtils.META_HEAD_GTASK_ID指定 + metaInfo.put(GTaskStringUtils.META_HEAD_GTASK_ID, gid); + } catch (JSONException e) { + // 如果在放入GID到JSON对象过程中出现异常,记录错误日志 + Log.e(TAG, "failed to put related gid"); + } + // 将处理后的元数据信息对象转换为字符串,并设置为当前对象的笔记内容(可能用于存储等用途) + setNotes(metaInfo.toString()); + // 设置名称为特定的字符串,该字符串由GTaskStringUtils.META_NOTE_NAME指定,可能用于标识这个元数据的类型等 + setName(GTaskStringUtils.META_NOTE_NAME); + } + + // 获取相关GID的方法,外部可通过调用此方法获取之前设置的mRelatedGid值 + public String getRelatedGid() { + return mRelatedGid; + } + + // 判断当前元数据是否值得保存,通过检查笔记内容是否为null来确定 + // 如果笔记内容不为null,意味着有有效的元数据需要保存,返回true;否则返回false + @Override + public boolean isWorthSaving() { + return getNotes()!= null; + } + + // 根据远程JSON数据设置内容的方法,先调用父类的相应方法进行通用的设置操作 + // 然后尝试从已设置的笔记内容(假设是JSON字符串形式)中解析出相关的GID并赋值给mRelatedGid + @Override + public void setContentByRemoteJSON(JSONObject js) { + super.setContentByRemoteJSON(js); + if (getNotes()!= null) { + try { + JSONObject metaInfo = new JSONObject(getNotes().trim()); + mRelatedGid = metaInfo.getString(GTaskStringUtils.META_HEAD_GTASK_ID); + } catch (JSONException e) { + // 如果解析过程出现异常,记录警告日志,并将mRelatedGid设置为null + Log.w(TAG, "failed to get related gid"); + mRelatedGid = null; + } + } + } + + // 此方法明确表示不应该被调用,如果被调用则会抛出非法访问错误 + // 可能是因为元数据的内容设置不应该通过本地JSON这种方式进行(具体需结合业务逻辑确定) + @Override + public void setContentByLocalJSON(JSONObject js) { + // this function should not be called + throw new IllegalAccessError("MetaData:setContentByLocalJSON should not be called"); + } + + // 此方法同样明确表示不应该被调用,若调用会抛出非法访问错误 + // 可能它在设计上就不是用于从内容获取本地JSON的(根据具体业务场景决定) + @Override + public JSONObject getLocalJSONFromContent() { + throw new IllegalAccessError("MetaData:getLocalJSONFromContent should not be called"); + } + + // 此方法也明确表示不应该被调用,若调用会抛出非法访问错误 + // 或许获取同步操作相关信息的逻辑不应该在MetaData类中以这种方式处理(结合整体架构理解) + @Override + public int getSyncAction(Cursor c) { + throw new IllegalAccessError("MetaData:getSyncAction should not be called"); + } +} + + +六、Node.java + + +package net.micode.notes.gtask.data; +// 导入用于数据库游标操作以及处理JSON对象的相关类 +import android.database.Cursor; +import org.json.JSONObject; + +// 定义抽象类Node,通常作为一种基础的节点类,可能被其他具体类继承来实现特定业务逻辑下节点相关的操作 +public abstract class Node { + + // 定义一系列表示同步操作类型的常量,方便代码中通过这些整数值清晰地表示不同的同步动作 + // 表示无同步操作 + public static final int SYNC_ACTION_NONE = 0; + // 表示向远程添加(可能是向远程服务器等添加数据的操作) + public static final int SYNC_ACTION_ADD_REMOTE = 1; + // 表示在本地添加(比如在本地数据库等添加数据) + public static final int SYNC_ACTION_ADD_LOCAL = 2; + // 表示从远程删除(从远程服务器等删除对应的数据) + public static final int SYNC_ACTION_DEL_REMOTE = 3; + // 表示从本地删除(在本地环境删除相关数据) + public static final int SYNC_ACTION_DEL_LOCAL = 4; + // 表示更新远程数据(对远程存储的数据进行更新操作) + public static final int SYNC_ACTION_UPDATE_REMOTE = 5; + // 表示更新本地数据(在本地进行数据更新) + public static final int SYNC_ACTION_UPDATE_LOCAL = 6; + // 表示更新操作出现冲突(比如本地和远程数据修改不一致等导致更新冲突情况) + public static final int SYNC_ACTION_UPDATE_CONFLICT = 7; + // 表示同步操作出现错误(各种可能导致同步失败的错误情况) + public static final int SYNC_ACTION_ERROR = 8; + + // 存储节点的全局唯一标识符(可能用于唯一标识该节点在整个系统中的身份),初始化为null + private String mGid; + // 存储节点的名称,初始化为空字符串 + private String mName; + // 记录节点最后一次被修改的时间戳,初始化为0 + private long mLastModified; + // 表示节点是否已被删除的标记,初始化为false + private boolean mDeleted; + + // 无参构造函数,用于初始化节点对象的各个属性为默认值 + public Node() { + mGid = null; + mName = ""; + mLastModified = 0; + mDeleted = false; + } + + // 抽象方法,用于获取创建操作对应的JSON对象,具体创建操作逻辑由继承该抽象类的具体类去实现, + // 参数actionId可能用于指定具体是哪种创建操作(比如根据不同场景创建不同的初始数据等) + public abstract JSONObject getCreateAction(int actionId); + + // 抽象方法,用于获取更新操作对应的JSON对象,具体更新操作逻辑由继承类实现, + // actionId用于区分不同类型的更新操作情况 + public abstract JSONObject getUpdateAction(int actionId); + + // 抽象方法,根据远程的JSON数据来设置节点的内容,具体如何解析并设置由继承类确定 + public abstract void setContentByRemoteJSON(JSONObject js); + + // 抽象方法,根据本地的JSON数据来设置节点的内容,同样具体设置逻辑留给继承类实现 + public abstract void setContentByLocalJSON(JSONObject js); + + // 抽象方法,从节点的内容中获取本地JSON对象,具体如何从节点内部存储的数据转换为JSON对象由继承类负责 + public abstract JSONObject getLocalJSONFromContent(); + + // 抽象方法,通过数据库游标获取该节点对应的同步操作类型,具体的判断逻辑由继承类根据业务需求实现 + public abstract int getSyncAction(Cursor c); + + // 设置节点的全局唯一标识符的方法 + public void setGid(String gid) { + this.mGid = gid; + } + + // 设置节点名称的方法 + public void setName(String name) { + this.mName = name; + } + + // 设置节点最后修改时间戳的方法 + public void setLastModified(long lastModified) { + this.mLastModified = lastModified; + } + + // 设置节点是否被删除的标记的方法 + public void setDeleted(boolean deleted) { + this.mDeleted = deleted; + } + + // 获取节点的全局唯一标识符的方法 + public String getGid() { + return this.mGid; + } + + // 获取节点名称的方法 + public String getName() { + return this.mName; + } + + // 获取节点最后修改时间戳的方法 + public long getLastModified() { + return this.mLastModified; + } + + // 获取节点是否被删除的标记的方法 + public boolean getDeleted() { + return this.mDeleted; + } +} + + +七、SqlData.java + + +package net.micode.notes.gtask.data; + +import android.content.ContentResolver; +import android.content.ContentUris; +import android.content.ContentValues; +import android.content.Context; +import android.database.Cursor; +import android.net.Uri; +import android.util.Log; + +import net.micode.notes.data.Notes; +import net.micode.notes.data.Notes.DataColumns; +import net.micode.notes.data.Notes.DataConstants; +import net.micode.notes.data.Notes.NoteColumns; +import net.micode.notes.data.NotesDatabaseHelper.TABLE; +import net.micode.notes.gtask.exception.ActionFailureException; + +import org.json.JSONException; +import org.json.JSONObject; + +// SqlData类,可能用于处理与数据库相关的数据操作,比如数据的读取、更新以及和JSON数据之间的转换等 +public class SqlData { + // 用于日志输出的标签,取当前类的简单名称,方便在日志中识别来源 + private static final String TAG = SqlData.class.getSimpleName(); + // 定义一个表示无效ID的值,用于初始化或者标识无效的记录ID情况 + private static final int INVALID_ID = -99999; + + // 定义一个字符串数组,用于指定从数据库查询数据时要获取的列名,这些列名对应了数据相关的各个属性 + public static final String[] PROJECTION_DATA = new String[] { + DataColumns.ID, DataColumns.MIME_TYPE, DataColumns.CONTENT, DataColumns.DATA1, + DataColumns.DATA3 + }; + // 定义常量,表示在查询结果游标中对应数据ID列的索引位置,方便后续从游标中获取该列数据 + public static final int DATA_ID_COLUMN = 0; + // 表示在查询结果游标中对应数据MIME_TYPE列的索引位置 + public static final int DATA_MIME_TYPE_COLUMN = 1; + // 表示在查询结果游标中对应数据CONTENT列的索引位置 + public static final int DATA_CONTENT_COLUMN = 2; + // 表示在查询结果游标中对应数据CONTENT_DATA_1列的索引位置 + public static final int DATA_CONTENT_DATA_1_COLUMN = 3; + // 表示在查询结果游标中对应数据CONTENT_DATA_3列的索引位置 + public static final int DATA_CONTENT_DATA_3_COLUMN = 4; + + // 用于与内容提供器进行交互,可用于执行数据库相关的查询、插入、更新等操作 + private ContentResolver mContentResolver; + // 标记当前操作是否是创建新数据的操作,初始化为true + private boolean mIsCreate; + // 存储数据记录的ID,初始化为无效ID值 + private long mDataId; + // 存储数据的MIME类型,初始化为默认的 NOTE 类型(可能是自定义的一种数据类型标识) + private String mDataMimeType; + // 存储数据的具体内容,初始化为空字符串 + private String mDataContent; + // 存储数据内容相关的一个长整型数据(具体含义根据业务而定),初始化为0 + private long mDataContentData1; + // 存储数据内容相关的另一个字符串数据(具体含义由业务决定),初始化为空字符串 + private String mDataContentData3; + // 用于存储数据变更的值,可用于后续更新数据库操作,初始化为一个新的ContentValues对象 + private ContentValues mDiffDataValues; + + // 构造函数,用于创建一个新的SqlData对象,通常用于创建新数据的场景 + // 参数context用于获取内容提供器,以便后续与数据库进行交互 + public SqlData(Context context) { + // 获取上下文对应的内容提供器 + mContentResolver = context.getContentResolver(); + mIsCreate = true; + mDataId = INVALID_ID; + mDataMimeType = DataConstants.NOTE; + mDataContent = ""; + mDataContentData1 = 0; + mDataContentData3 = ""; + mDiffDataValues = new ContentValues(); + } + + // 另一个构造函数,用于根据已有的数据库游标来创建SqlData对象,通常用于从数据库读取现有数据的场景 + // 参数context用于获取内容提供器,参数c是数据库查询返回的游标,包含了要读取的数据记录信息 + public SqlData(Context context, Cursor c) { + mContentResolver = context.getContentResolver(); + mIsCreate = false; + // 调用方法从游标中加载数据到当前对象的各个属性中 + loadFromCursor(c); + mDiffDataValues = new ContentValues(); + } + + // 私有方法,从给定的数据库游标中加载数据到当前对象的各个属性,根据之前定义的列索引位置获取对应的值 + private void loadFromCursor(Cursor c) { + mDataId = c.getLong(DATA_ID_COLUMN); + mDataMimeType = c.getString(DATA_MIME_TYPE_COLUMN); + mDataContent = c.getString(DATA_CONTENT_COLUMN); + mDataContentData1 = c.getLong(DATA_CONTENT_DATA_1_COLUMN); + mDataContentData3 = c.getString(DATA_CONTENT_DATA_3_COLUMN); + } + + // 设置数据内容的方法,根据传入的JSON对象来更新当前对象的数据属性,并将变更记录到mDiffDataValues中 + // 参数js是包含数据信息的JSON对象,可能来自外部数据源或者其他业务逻辑传递过来的数据表示形式 + public void setContent(JSONObject js) throws JSONException { + // 尝试从JSON对象中获取数据ID,如果不存在则使用无效ID值 + long dataId = js.has(DataColumns.ID)? js.getLong(DataColumns.ID) : INVALID_ID; + // 如果当前是创建操作或者数据ID发生了变化,则将新的数据ID放入变更值集合中 + if (mIsCreate || mDataId!= dataId) { + mDiffDataValues.put(DataColumns.ID, dataId); + } + mDataId = dataId; + + // 尝试从JSON对象中获取MIME_TYPE,如果不存在则使用默认的 NOTE 类型 + String dataMimeType = js.has(DataColumns.MIME_TYPE)? js.getString(DataColumns.MIME_TYPE) + : DataConstants.NOTE; + // 如果当前是创建操作或者MIME类型与当前对象的不一致,则更新变更值集合中的MIME_TYPE + if (mIsCreate ||!mDataMimeType.equals(dataMimeType)) { + mDiffDataValues.put(DataColumns.MIME_TYPE, dataMimeType); + } + mDataMimeType = dataMimeType; + + // 尝试从JSON对象中获取数据内容,如果不存在则使用空字符串 + String dataContent = js.has(DataColumns.CONTENT)? js.getString(DataColumns.CONTENT) : ""; + // 如果当前是创建操作或者数据内容与当前对象的不一致,则更新变更值集合中的数据内容 + if (mIsCreate ||!mDataContent.equals(dataContent)) { + mDiffDataValues.put(DataColumns.CONTENT, dataContent); + } + mDataContent = dataContent; + + // 尝试从JSON对象中获取数据内容相关的长整型数据,如果不存在则使用0 + long dataContentData1 = js.has(DataColumns.DATA1)? js.getLong(DataColumns.DATA1) : 0; + // 如果当前是创建操作或者该长整型数据与当前对象的不一致,则更新变更值集合中的对应值 + if (mIsCreate || mDataContentData1!= dataContentData1) { + mDiffDataValues.put(DataColumns.DATA1, dataContentData1); + } + mDataContentData1 = dataContentData1; + + // 尝试从JSON对象中获取数据内容相关的字符串数据,如果不存在则使用空字符串 + String dataContentData3 = js.has(DataColumns.DATA3)? js.getString(DataColumns.DATA3) : ""; + // 如果当前是创建操作或者该字符串数据与当前对象的不一致,则更新变更值集合中的对应值 + if (mIsCreate ||!mDataContentData3.equals(dataContentData3)) { + mDiffDataValues.put(DataColumns.DATA3, dataContentData3); + } + mDataContentData3 = dataContentData3; + } + + // 获取当前对象数据内容并转换为JSON对象的方法,如果当前是创建操作(意味着数据还未存入数据库),则记录错误日志并返回null + public JSONObject getContent() throws JSONException { + if (mIsCreate) { + Log.e(TAG, "it seems that we haven't created this in database yet"); + return null; + } + JSONObject js = new JSONObject(); + js.put(DataColumns.ID, mDataId); + js.put(DataColumns.MIME_TYPE, mDataMimeType); + js.put(DataColumns.CONTENT, mDataContent); + js.put(DataColumns.DATA1, mDataContentData1); + js.put(DataColumns.DATA3, mDataContentData3); + return js; + } + + // 将数据变更提交到数据库的方法,根据当前是创建操作还是更新操作执行不同的数据库操作逻辑 + // 参数noteId可能是与该数据相关联的笔记ID(具体取决于业务逻辑) + // 参数validateVersion用于决定是否验证版本(可能用于并发控制等情况) + // 参数version可能是数据的版本号,用于版本验证等操作 + public void commit(long noteId, boolean validateVersion, long version) { + + if (mIsCreate) { + // 如果是创建操作,且数据ID为无效ID同时变更值集合中包含了数据ID键(可能是多余的情况),则移除该键 + if (mDataId == INVALID_ID && mDiffDataValues.containsKey(DataColumns.ID)) { + mDiffDataValues.remove(DataColumns.ID); + } + + // 将关联的笔记ID放入变更值集合中 + mDiffDataValues.put(DataColumns.NOTE_ID, noteId); + // 通过内容提供器插入数据到数据库,使用指定的内容数据URI,并传入变更值集合作为要插入的数据 + Uri uri = mContentResolver.insert(Notes.CONTENT_DATA_URI, mDiffDataValues); + try { + // 尝试从插入操作返回的URI中获取新插入数据的ID,如果转换失败则记录错误日志并抛出异常 + mDataId = Long.valueOf(uri.getPathSegments().get(1)); + } catch (NumberFormatException e) { + Log.e(TAG, "Get note id error :" + e.toString()); + throw new ActionFailureException("create note failed"); + } + } else { + // 如果是更新操作,且变更值集合中有数据需要更新 + if (mDiffDataValues.size() > 0) { + int result = 0; + // 如果不需要验证版本 + if (!validateVersion) { + // 直接通过内容提供器更新数据库中对应的数据记录,根据数据ID定位要更新的记录,传入变更值集合作为更新内容 + result = mContentResolver.update(ContentUris.withAppendedId( + Notes.CONTENT_DATA_URI, mDataId), mDiffDataValues, null, null); + } else { + // 如果需要验证版本,则通过更复杂的条件语句来更新数据库记录,验证版本是否匹配后再进行更新 + result = mContentResolver.update(ContentUris.withAppendedId( + Notes.CONTENT_DATA_URI, mDataId), mDiffDataValues, + "? in (SELECT " + NoteColumns.ID + " FROM " + TABLE.NOTE + + " WHERE " + NoteColumns.VERSION + "=?)", new String[] { + String.valueOf(noteId), String.valueOf(version) + }); + } + // 如果更新操作影响的行数为0(意味着可能没有实际更新发生),记录警告日志提示可能用户在同步时更新了笔记 + if (result == 0) { + Log.w(TAG, "there is no update. maybe user updates note when syncing"); + } + } + } + + // 清除变更值集合,准备下一次的数据变更操作 + mDiffDataValues.clear(); + mIsCreate = false; + } + + // 获取当前数据记录ID的方法 + public long getId() { + return mDataId; + } +} + + + +八、SqlNote.java + + +package net.micode.notes.gtask.data; + +import android.appwidget.AppWidgetManager; +import android.content.ContentResolver; +import android.content.ContentValues; +import android.content.Context; +import android.database.Cursor; +import android.net.Uri; +import android.util.Log; + +import net.micode.notes.data.Notes; +import net.micode.notes.data.Notes.DataColumns; +import net.micode.notes.data.Notes.NoteColumns; +import net.micode.notes.gtask.exception.ActionFailureException; +import net.micode.notes.tool.GTaskStringUtils; +import net.micode.notes.tool.ResourceParser; + +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +import java.util.ArrayList; + +// SqlNote类,可能用于处理与笔记相关的数据操作,涉及从数据库读取、更新笔记信息,以及和JSON数据之间的转换等功能 +public class SqlNote { + // 用于日志输出的标签,取当前类的简单名称,方便在日志中识别来源 + private static final String TAG = SqlNote.class.getSimpleName(); + // 定义一个表示无效ID的值,用于初始化或者标识无效的笔记记录ID情况 + private static final int INVALID_ID = -99999; + + // 定义一个字符串数组,用于指定从数据库查询笔记数据时要获取的列名,涵盖了笔记相关的各个属性 + public static final String[] PROJECTION_NOTE = new String[] { + NoteColumns.ID, NoteColumns.ALERTED_DATE, NoteColumns.BG_COLOR_ID, + NoteColumns.CREATED_DATE, NoteColumns.HAS_ATTACHMENT, NoteColumns.MODIFIED_DATE, + NoteColumns.NOTES_COUNT, NoteColumns.PARENT_ID, NoteColumns.SNIPPET, NoteColumns.TYPE, + NoteColumns.WIDGET_ID, NoteColumns.WIDGET_TYPE, NoteColumns.SYNC_ID, + NoteColumns.LOCAL_MODIFIED, NoteColumns.ORIGIN_PARENT_ID, NoteColumns.GTASK_ID, + NoteColumns.VERSION + }; + // 定义常量,表示在查询结果游标中对应笔记ID列的索引位置,方便后续从游标中获取该列数据 + public static final int ID_COLUMN = 0; + // 表示在查询结果游标中对应提醒日期列的索引位置 + public static final int ALERTED_DATE_COLUMN = 1; + // 表示在查询结果游标中对应背景颜色ID列的索引位置 + public static final int BG_COLOR_ID_COLUMN = 2; + // 表示在查询结果游标中对应创建日期列的索引位置 + public static final int CREATED_DATE_COLUMN = 3; + // 表示在查询结果游标中对应是否有附件列的索引位置 + public static final int HAS_ATTACHMENT_COLUMN = 4; + // 表示在查询结果游标中对应修改日期列的索引位置 + public static final int MODIFIED_DATE_COLUMN = 5; + // 表示在查询结果游标中对应笔记数量列的索引位置 + public static final int NOTES_COUNT_COLUMN = 6; + // 表示在查询结果游标中对应父ID列的索引位置 + public static final int PARENT_ID_COLUMN = 7; + // 表示在查询结果游标中对应摘要列的索引位置 + public static final int SNIPPET_COLUMN = 8; + // 表示在查询结果游标中对应类型列的索引位置 + public static final int TYPE_COLUMN = 9; + // 表示在查询结果游标中对应小部件ID列的索引位置 + public static final int WIDGET_ID_COLUMN = 10; + // 表示在查询结果游标中对应小部件类型列的索引位置 + public static final int WIDGET_TYPE_COLUMN = 11; + // 表示在查询结果游标中对应同步ID列的索引位置 + public static final int SYNC_ID_COLUMN = 12; + // 表示在查询结果游标中对应本地修改标记列的索引位置 + public static final int LOCAL_MODIFIED_COLUMN = 13; + // 表示在查询结果游标中对应原始父ID列的索引位置 + public static final int ORIGIN_PARENT_ID_COLUMN = 14; + // 表示在查询结果游标中对应GTask ID列的索引位置 + public static final int GTASK_ID_COLUMN = 15; + // 表示在查询结果游标中对应版本列的索引位置 + public static final int VERSION_COLUMN = 16; + + // 上下文对象,用于获取各种系统服务以及资源等,比如获取内容提供器等操作 + private Context mContext; + // 用于与内容提供器进行交互,可用于执行数据库相关的查询、插入、更新等操作 + private ContentResolver mContentResolver; + // 标记当前操作是否是创建新笔记的操作,初始化为true + private boolean mIsCreate; + // 存储笔记记录的ID,初始化为无效ID值 + private long mId; + // 存储笔记的提醒日期,初始化为0 + private long mAlertDate; + // 存储笔记的背景颜色ID,初始化为通过ResourceParser获取的默认背景颜色ID + private int mBgColorId; + // 存储笔记的创建日期,初始化为当前系统时间 + private long mCreatedDate; + // 存储笔记是否有附件的标识,初始化为0(表示无附件) + private int mHasAttachment; + // 存储笔记的修改日期,初始化为当前系统时间 + private long mModifiedDate; + // 存储笔记的父ID,初始化为0 + private long mParentId; + // 存储笔记的摘要内容,初始化为空字符串 + private String mSnippet; + // 存储笔记的类型,初始化为Notes.TYPE_NOTE(自定义的笔记类型常量) + private int mType; + // 存储笔记关联的小部件ID,初始化为无效的小部件ID值 + private int mWidgetId; + // 存储笔记关联的小部件类型,初始化为无效的小部件类型值 + private int mWidgetType; + // 存储笔记的原始父ID,初始化为0 + private long mOriginParent; + // 存储笔记的版本号,初始化为0 + private long mVersion; + // 用于存储笔记数据变更的值,可用于后续更新数据库操作,初始化为一个新的ContentValues对象 + private ContentValues mDiffNoteValues; + // 存储与该笔记相关的数据列表(可能是笔记包含的具体内容数据等,通过SqlData对象表示) + private ArrayList mDataList; + + // 构造函数,用于创建一个新的SqlNote对象,通常用于创建新笔记的场景 + // 参数context用于获取内容提供器等操作所需的上下文信息 + public SqlNote(Context context) { + mContext = context; + mContentResolver = context.getContentResolver(); + mIsCreate = true; + mId = INVALID_ID; + mAlertDate = 0; + mBgColorId = ResourceParser.getDefaultBgId(context); + mCreatedDate = System.currentTimeMillis(); + mHasAttachment = 0; + mModifiedDate = System.currentTimeMillis(); + mParentId = 0; + mSnippet = ""; + mType = Notes.TYPE_NOTE; + mWidgetId = AppWidgetManager.INVALID_APPWIDGET_ID; + mWidgetType = Notes.TYPE_WIDGET_INVALIDE; + mOriginParent = 0; + mVersion = 0; + mDiffNoteValues = new ContentValues(); + mDataList = new ArrayList(); + } + + // 构造函数,用于根据已有的数据库游标来创建SqlNote对象,通常用于从数据库读取现有笔记数据的场景 + // 参数context用于获取内容提供器,参数c是数据库查询返回的游标,包含了要读取的笔记记录信息 + public SqlNote(Context context, Cursor c) { + mContext = context; + mContentResolver = context.getContentResolver(); + mIsCreate = false; + loadFromCursor(c); + mDataList = new ArrayList(); + if (mType == Notes.TYPE_NOTE) + loadDataContent(); + mDiffNoteValues = new ContentValues(); + } + + // 构造函数,根据给定的笔记ID从数据库加载笔记信息来创建SqlNote对象 + // 参数context用于获取内容提供器,参数id是要加载的笔记的唯一标识符 + public SqlNote(Context context, long id) { + mContext = context; + mContentResolver = context.getContentResolver(); + mIsCreate = false; + loadFromCursor(id); + mDataList = new ArrayList(); + if (mType == Notes.TYPE_NOTE) + loadDataContent(); + mDiffNoteValues = new ContentValues(); + } + + // 私有方法,根据给定的笔记ID从数据库查询并加载笔记信息到当前对象的各个属性中 + // 通过内容提供器查询数据库,获取对应笔记记录的游标,然后调用另一个loadFromCursor方法进行实际的数据加载 + private void loadFromCursor(long id) { + Cursor c = null; + try { + c = mContentResolver.query(Notes.CONTENT_NOTE_URI, PROJECTION_NOTE, "(_id=?)", + new String[] { + String.valueOf(id) + }, null); + if (c!= null) { + c.moveToNext(); + loadFromCursor(c); + } else { + Log.w(TAG, "loadFromCursor: cursor = null"); + } + } finally { + if (c!= null) + c.close(); + } + } + + // 私有方法,从给定的数据库游标中加载笔记数据到当前对象的各个属性,根据之前定义的列索引位置获取对应的值 + private void loadFromCursor(Cursor c) { + mId = c.getLong(ID_COLUMN); + mAlertDate = c.getLong(ALERTED_DATE_COLUMN); + mBgColorId = c.getInt(BG_COLOR_ID_COLUMN); + mCreatedDate = c.getLong(CREATED_DATE_COLUMN); + mHasAttachment = c.getInt(HAS_ATTACHMENT_COLUMN); + mModifiedDate = c.getLong(MODIFIED_DATE_COLUMN); + mParentId = c.getLong(PARENT_ID_COLUMN); + mSnippet = c.getString(SNIPPET_COLUMN); + mType = c.getInt(TYPE_COLUMN); + mWidgetId = c.getInt(WIDGET_ID_COLUMN); + mWidgetType = c.getInt(WIDGET_TYPE_COLUMN); + mVersion = c.getLong(VERSION_COLUMN); + } + + // 私有方法,用于加载与当前笔记相关的数据内容,通过查询数据库获取对应的数据记录, + // 并将每条数据记录转换为SqlData对象后添加到mDataList中 + private void loadDataContent() { + Cursor c = null; + mDataList.clear(); + try { + c = mContentResolver.query(Notes.CONTENT_DATA_URI, SqlData.PROJECTION_DATA, + "(note_id=?)", new String[] { + String.valueOf(mId) + }, null); + if (c!= null) { + if (c.getCount() == 0) { + Log.w(TAG, "it seems that the note has not data"); + return; + } + while (c.moveToNext()) { + SqlData data = new SqlData(mContext, c); + mDataList.add(data); + } + } else { + Log.w(TAG, "loadDataContent: cursor = null"); + } + } finally { + if (c!= null) + c.close(); + } + } + + // 根据传入的JSON对象来设置笔记的内容信息,并将变更记录到mDiffNoteValues中,同时处理与笔记相关的数据列表 + // 如果操作过程中出现JSON解析异常,则记录错误日志并返回false表示设置失败,否则返回true表示设置成功 + public boolean setContent(JSONObject js) { + try { + // 从JSON对象中获取表示笔记头部信息的JSON对象(具体结构由业务定义) + JSONObject note = js.getJSONObject(GTaskStringUtils.META_HEAD_NOTE); + // 如果笔记类型是系统类型,则记录警告日志,因为可能不允许设置系统文件夹相关内容 + if (note.getInt(NoteColumns.TYPE) == Notes.TYPE_SYSTEM) { + Log.w(TAG, "cannot set system folder"); + } else if (note.getInt(NoteColumns.TYPE) == Notes.TYPE_FOLDER) { + // 如果是文件夹类型,只能更新摘要和类型信息 + String snippet = note.has(NoteColumns.SNIPPET)? note.getString(NoteColumns.SNIPPET) : ""; + if (mIsCreate ||!mSnippet.equals(snippet)) { + mDiffNoteValues.put(NoteColumns.SNIPPET, snippet); + } + mSnippet = snippet; + + int type = note.has(NoteColumns.TYPE)? note.getInt(NoteColumns.TYPE) : Notes.TYPE_NOTE; + if (mIsCreate || mType!= type) { + mDiffNoteValues.put(NoteColumns.TYPE, type); + } + mType = type; + } else if (note.getInt(NoteColumns.TYPE) == Notes.TYPE_NOTE) { + // 如果是普通笔记类型 + // 获取笔记数据内容的JSON数组(可能包含多条具体数据记录) + JSONArray dataArray = js.getJSONArray(GTaskStringUtils.META_HEAD_DATA); + long id = note.has(NoteColumns.ID)? note.getLong(NoteColumns.ID) : INVALID_ID; + if (mIsCreate || mId!= id) { + mDiffNoteValues.put(NoteColumns.ID, id); + } + mId = id; + + long alertDate = note.has(NoteColumns.ALERTED_DATE)? note.getLong(NoteColumns.ALERTED_DATE) : 0; + if (mIsCreate || mAlertDate!= alertDate) { + mDiffNoteValues.put(NoteColumns.ALERTED_DATE, alertDate); + } + mAlertDate = alertDate; + + int bgColorId = note.has(NoteColumns.BG_COLOR_ID)? note.getInt(NoteColumns.BG_COLOR_ID) : ResourceParser.getDefaultBgId(mContext); + if (mIsCreate || mBgColorId!= bgColorId) { + mDiffNoteValues.put(NoteColumns.BG_COLOR_ID, bgColorId); + } + mBgColorId = bgColorId; + + long createDate = note.has(NoteColumns.CREATED_DATE)? note.getLong(NoteColumns.CREATED_DATE) : System.currentTimeMillis(); + if (mIsCreate || mCreatedDate!= createDate) { + mDiffNoteValues.put(NoteColumns.CREATED_DATE, createDate); + } + mCreatedDate = createDate; + + int hasAttachment = note.has(NoteColumns.HAS_ATTACHMENT)? note.getInt(NoteColumns.HAS_ATTACHMENT) : 0; + if (mIsCreate || mHasAttachment!= hasAttachment) { + mDiffNoteValues.put(NoteColumns.HAS_ATTACHMENT, hasAttachment); + } + mHasAttachment = hasAttachment; + + long modifiedDate = note.has(NoteColumns.MODIFIED_DATE)? note.getLong(NoteColumns.MODIFIED_DATE) : System.currentTimeMillis(); + if (mIsCreate || mModifiedDate!= modifiedDate) { + mDiffNoteValues.put(NoteColumns.MODIFIED_DATE, modifiedDate); + } + mModifiedDate = modifiedDate; + + long parentId = note.has(NoteColumns.PARENT_ID)? note.getLong(NoteColumns.PARENT_ID) : 0; + if (mIsCreate || mParentId!= parentId) { + mDiffNoteValues.put(NoteColumns.PARENT_ID, parentId); + } + mParentId = parentId; + + String snippet = note.has(NoteColumns.SNIPPET)? note.getString(NoteColumns.SNIPPET) : ""; + if (mIsCreate ||!mSnippet.equals(snippet)) { + mDiffNoteValues.put(NoteColumns.SNIPPET, snippet); + } + mSnippet = snippet; + + int type = note.has(NoteColumns.TYPE)? note.getInt(NoteColumns.TYPE) : Notes.TYPE_NOTE; + if (mIsCreate || mType!= type) { + mDiffNoteValues.put(NoteColumns.TYPE, type); + } + mType = type; + + int widgetId = note.has(NoteColumns.WIDGET_ID)? note.getInt(NoteColumns.WIDGET_ID) : AppWidgetManager.INVALID_APPWIDGET_ID; + if (mIsCreate || mWidgetId!= widgetId) { + mDiffNoteValues.put(NoteColumns.WIDGET_ID, widgetId); + } + mWidgetId = widgetId; + + int widgetType = note.has(NoteColumns.WIDGET_TYPE)? note.getInt(NoteColumns.WIDGET_TYPE) : Notes.TYPE_WIDGET_INVALIDE; + if (mIsCreate || mWidgetType!= widgetType) { + mDiffNoteValues.put(NoteColumns.WIDGET_TYPE, widgetType); + } + mWidgetType = widgetType; + + long originParent = note.has(NoteColumns.ORIGIN_PARENT_ID)? note.getLong(NoteColumns.ORIGIN_PARENT_ID) : 0; + if (mIsCreate || mOriginParent!= originParent) { + mDiffNoteValues.put(NoteColumns.ORIGIN_PARENT_ID, originParent); + } + mOriginParent = originParent; + + for (int i = 0; i < dataArray.length(); i++) { + JSONObject data = dataArray.getJSONObject(i); + SqlData sqlData = null; + if (data.has(DataColumns.ID)) { + long dataId = data.getLong(DataColumns.ID); + for (SqlData temp : mDataList) { + if (dataId == temp.getId()) { + sqlData = temp; + } + } + } + + if (sqlData == null) { + sqlData = new SqlData(mContext); + mDataList.add(sqlData); + } + + sqlData.setContent(data); + } + } + } catch (JSONException e) { + Log.e(TAG, e.toString()); + e.printStackTrace(); + return false; + } + return true; + } + + public JSONObject getContent() { + try { + JSONObject js = new JSONObject(); + + if (mIsCreate) { + Log.e(TAG, "it seems that we haven't created this in database yet"); + return null; + } + + JSONObject note = new JSONObject(); + if (mType == Notes.TYPE_NOTE) { + note.put(NoteColumns.ID, mId); + note.put(NoteColumns.ALERTED_DATE, mAlertDate); + note.put(NoteColumns.BG_COLOR_ID, mBgColorId); + note.put(NoteColumns.CREATED_DATE, mCreatedDate); + note.put(NoteColumns.HAS_ATTACHMENT, mHasAttachment); + note.put(NoteColumns.MODIFIED_DATE, mModifiedDate); + note.put(NoteColumns.PARENT_ID, mParentId); + note.put(NoteColumns.SNIPPET, mSnippet); + note.put(NoteColumns.TYPE, mType); + note.put(NoteColumns.WIDGET_ID, mWidgetId); + note.put(NoteColumns.WIDGET_TYPE, mWidgetType); + note.put(NoteColumns.ORIGIN_PARENT_ID, mOriginParent); + js.put(GTaskStringUtils.META_HEAD_NOTE, note); + + JSONArray dataArray = new JSONArray(); + for (SqlData sqlData : mDataList) { + JSONObject data = sqlData.getContent(); + if (data != null) { + dataArray.put(data); + } + } + js.put(GTaskStringUtils.META_HEAD_DATA, dataArray); + } else if (mType == Notes.TYPE_FOLDER || mType == Notes.TYPE_SYSTEM) { + note.put(NoteColumns.ID, mId); + note.put(NoteColumns.TYPE, mType); + note.put(NoteColumns.SNIPPET, mSnippet); + js.put(GTaskStringUtils.META_HEAD_NOTE, note); + } + + return js; + } catch (JSONException e) { + Log.e(TAG, e.toString()); + e.printStackTrace(); + } + return null; + } + + public void setParentId(long id) { + mParentId = id; + mDiffNoteValues.put(NoteColumns.PARENT_ID, id); + } + + public void setGtaskId(String gid) { + mDiffNoteValues.put(NoteColumns.GTASK_ID, gid); + } + + public void setSyncId(long syncId) { + mDiffNoteValues.put(NoteColumns.SYNC_ID, syncId); + } + + public void resetLocalModified() { + mDiffNoteValues.put(NoteColumns.LOCAL_MODIFIED, 0); + } + + public long getId() { + return mId; + } + + public long getParentId() { + return mParentId; + } + + public String getSnippet() { + return mSnippet; + } + + public boolean isNoteType() { + return mType == Notes.TYPE_NOTE; + } + + public void commit(boolean validateVersion) { + if (mIsCreate) { + if (mId == INVALID_ID && mDiffNoteValues.containsKey(NoteColumns.ID)) { + mDiffNoteValues.remove(NoteColumns.ID); + } + + Uri uri = mContentResolver.insert(Notes.CONTENT_NOTE_URI, mDiffNoteValues); + try { + mId = Long.valueOf(uri.getPathSegments().get(1)); + } catch (NumberFormatException e) { + Log.e(TAG, "Get note id error :" + e.toString()); + throw new ActionFailureException("create note failed"); + } + if (mId == 0) { + throw new IllegalStateException("Create thread id failed"); + } + + if (mType == Notes.TYPE_NOTE) { + for (SqlData sqlData : mDataList) { + sqlData.commit(mId, false, -1); + } + } + } else { + if (mId <= 0 && mId != Notes.ID_ROOT_FOLDER && mId != Notes.ID_CALL_RECORD_FOLDER) { + Log.e(TAG, "No such note"); + throw new IllegalStateException("Try to update note with invalid id"); + } + if (mDiffNoteValues.size() > 0) { + mVersion ++; + int result = 0; + if (!validateVersion) { + result = mContentResolver.update(Notes.CONTENT_NOTE_URI, mDiffNoteValues, "(" + + NoteColumns.ID + "=?)", new String[] { + String.valueOf(mId) + }); + } else { + result = mContentResolver.update(Notes.CONTENT_NOTE_URI, mDiffNoteValues, "(" + + NoteColumns.ID + "=?) AND (" + NoteColumns.VERSION + "<=?)", + new String[] { + String.valueOf(mId), String.valueOf(mVersion) + }); + } + if (result == 0) { + Log.w(TAG, "there is no update. maybe user updates note when syncing"); + } + } + + if (mType == Notes.TYPE_NOTE) { + for (SqlData sqlData : mDataList) { + sqlData.commit(mId, validateVersion, mVersion); + } + } + } + + // refresh local info + loadFromCursor(mId); + if (mType == Notes.TYPE_NOTE) + loadDataContent(); + + mDiffNoteValues.clear(); + mIsCreate = false; + } +} + + + +九、Task.java + + +package net.micode.notes.gtask.data; + +import android.database.Cursor; +import android.text.TextUtils; +import android.util.Log; + +import net.micode.notes.data.Notes; +import net.micode.notes.data.Notes.DataColumns; +import net.micode.notes.data.Notes.DataConstants; +import net.micode.notes.data.Notes.NoteColumns; +import net.micode.notes.gtask.exception.ActionFailureException; +import net.micode.notes.tool.GTaskStringUtils; + +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +// Task类继承自Node类,可能代表一个具体的任务对象,用于处理任务相关的数据、操作以及与JSON数据之间的转换等功能 +public class Task extends Node { + // 用于日志输出的标签,取当前类的简单名称,方便在日志中识别来源 + private static final String TAG = Task.class.getSimpleName(); + // 表示任务是否已完成的标记,初始化为false + private boolean mCompleted; + // 存储任务相关的备注信息(可能是对任务的描述等内容),初始化为null + private String mNotes; + // 存储任务的元数据信息,以JSONObject形式表示,初始化为null + private JSONObject mMetaInfo; + // 指向当前任务的前一个兄弟任务(可能用于任务列表中任务顺序相关的逻辑),初始化为null + private Task mPriorSibling; + // 指向当前任务所属的任务列表(表示任务的层级关系),初始化为null + private TaskList mParent; + + // 构造函数,用于创建一个新的Task对象,先调用父类(Node)的构造函数进行初始化,再初始化Task类特有的属性 + public Task() { + super(); + mCompleted = false; + mNotes = null; + mPriorSibling = null; + mParent = null; + mMetaInfo = null; + } + + // 获取创建任务操作对应的JSON对象的方法,用于构建符合特定格式要求的JSON数据,以便执行创建任务相关的业务逻辑 + public JSONObject getCreateAction(int actionId) { + JSONObject js = new JSONObject(); + + try { + // 设置JSON对象中的"action_type"字段,表示操作类型为创建任务,使用预定义的字符串常量(由GTaskStringUtils定义) + js.put(GTaskStringUtils.GTASK_JSON_ACTION_TYPE, + GTaskStringUtils.GTASK_JSON_ACTION_TYPE_CREATE); + + // 设置JSON对象中的"action_id"字段,传入给定的actionId参数,可能用于唯一标识这个创建操作(具体用途依赖业务逻辑) + js.put(GTaskStringUtils.GTASK_JSON_ACTION_ID, actionId); + + // 设置JSON对象中的"index"字段,通过调用父任务列表(mParent)的getChildTaskIndex方法获取当前任务在任务列表中的索引位置, + // 用于确定任务的顺序等相关信息 + js.put(GTaskStringUtils.GTASK_JSON_INDEX, mParent.getChildTaskIndex(this)); + + // 创建一个新的JSONObject用于表示任务实体相关的信息("entity_delta"部分) + JSONObject entity = new JSONObject(); + // 设置任务实体的名称,通过调用getName方法获取(该方法可能继承自父类Node或者在Task类中有具体实现) + entity.put(GTaskStringUtils.GTASK_JSON_NAME, getName()); + // 设置任务创建者的ID为"null"(可能表示未指定或者默认值,具体含义依赖业务逻辑) + entity.put(GTaskStringUtils.GTASK_JSON_CREATOR_ID, "null"); + // 设置任务实体的类型为任务类型,使用预定义的字符串常量(由GTaskStringUtils定义) + entity.put(GTaskStringUtils.GTASK_JSON_ENTITY_TYPE, + GTaskStringUtils.GTASK_JSON_TYPE_TASK); + // 如果任务有备注信息(getNotes方法返回不为null),将备注信息添加到任务实体的JSON对象中 + if (getNotes()!= null) { + entity.put(GTaskStringUtils.GTASK_JSON_NOTES, getNotes()); + } + // 将包含任务实体信息的JSON对象添加到外层的js对象中,对应"entity_delta"字段 + js.put(GTaskStringUtils.GTASK_JSON_ENTITY_DELTA, entity); + + // 设置JSON对象中的"parent_id"字段,通过调用父任务列表(mParent)的getGid方法获取父任务列表的唯一标识符, + // 表示当前任务所属的父级 + js.put(GTaskStringUtils.GTASK_JSON_PARENT_ID, mParent.getGid()); + + // 设置JSON对象中的"dest_parent_type"字段,设置为任务组类型,使用预定义的字符串常量(由GTaskStringUtils定义), + // 可能用于指定任务在目标位置(比如添加到某个任务组等情况)的父级类型相关信息 + js.put(GTaskStringUtils.GTASK_JSON_DEST_PARENT_TYPE, + GTaskStringUtils.GTASK_JSON_TYPE_GROUP); + + // 设置JSON对象中的"list_id"字段,同样通过调用父任务列表(mParent)的getGid方法获取父任务列表的唯一标识符, + // 可能与任务所在的列表标识等相关逻辑有关 + js.put(GTaskStringUtils.GTASK_JSON_LIST_ID, mParent.getGid()); + + // 如果当前任务存在前一个兄弟任务(mPriorSibling不为null),设置JSON对象中的"prior_sibling_id"字段, + // 通过调用前一个兄弟任务的getGid方法获取其唯一标识符,用于确定任务顺序等相关逻辑 + if (mPriorSibling!= null) { + js.put(GTaskStringUtils.GTASK_JSON_PRIOR_SIBLING_ID, mPriorSibling.getGid()); + } + + } catch (JSONException e) { + // 如果在构建JSON对象过程中出现异常,记录错误日志,打印异常堆栈信息,并抛出创建任务失败的异常, + // 方便开发人员排查问题以及在调用该方法的上层业务逻辑中进行相应的错误处理 + Log.e(TAG, e.toString()); + e.printStackTrace(); + throw new ActionFailureException("fail to generate task-create jsonobject"); + } + + return js; + } + + // 获取更新任务操作对应的JSON对象的方法,构建符合更新任务要求的JSON数据结构,用于执行任务更新相关的业务逻辑 + public JSONObject getUpdateAction(int actionId) { + JSONObject js = new JSONObject(); + + try { + // 设置JSON对象中的"action_type"字段,表示操作类型为更新任务,使用预定义的字符串常量(由GTaskStringUtils定义) + js.put(GTaskStringUtils.GTASK_JSON_ACTION_TYPE, + GTaskStringUtils.GTASK_JSON_ACTION_TYPE_UPDATE); + + // 设置JSON对象中的"action_id"字段,传入给定的actionId参数,用于唯一标识这个更新操作(具体用途依赖业务逻辑) + js.put(GTaskStringUtils.GTASK_JSON_ACTION_ID, actionId); + + // 设置JSON对象中的"id"字段,通过调用自身的getGid方法获取任务的唯一标识符,用于明确要更新的是哪个任务 + js.put(GTaskStringUtils.GTASK_JSON_ID, getGid()); + + // 创建一个新的JSONObject用于表示任务实体相关的更新信息("entity_delta"部分) + JSONObject entity = new JSONObject(); + // 设置任务实体的名称,通过调用getName方法获取 + entity.put(GTaskStringUtils.GTASK_JSON_NAME, getName()); + // 如果任务有备注信息(getNotes方法返回不为null),将备注信息添加到任务实体的更新JSON对象中 + if (getNotes()!= null) { + entity.put(GTaskStringUtils.GTASK_JSON_NOTES, getNotes()); + } + // 设置任务实体的删除标记,通过调用getDeleted方法获取(该方法可能继承自父类Node或者在Task类中有具体实现), + // 用于表示任务是否已被标记为删除等相关状态更新信息 + entity.put(GTaskStringUtils.GTASK_JSON_DELETED, getDeleted()); + // 将包含任务实体更新信息的JSON对象添加到外层的js对象中,对应"entity_delta"字段 + js.put(GTaskStringUtils.GTASK_JSON_ENTITY_DELTA, entity); + + } catch (JSONException e) { + // 如果在构建JSON对象过程中出现异常,记录错误日志,打印异常堆栈信息,并抛出更新任务失败的异常, + // 以便在调用该方法的上层业务逻辑中进行错误处理和问题排查 + Log.e(TAG, e.toString()); + e.printStackTrace(); + throw new ActionFailureException("fail to generate task-update jsonobject"); + } + + return js; + } + + // 根据远程的JSON数据来设置任务的内容信息,从传入的JSON对象中解析出各个任务相关的属性值,并进行相应的设置 + public void setContentByRemoteJSON(JSONObject js) { + if (js!= null) { + try { + // 如果JSON对象中包含任务的唯一标识符字段(由GTaskStringUtils定义的键),则获取并设置任务的唯一标识符(Gid) + if (js.has(GTaskStringUtils.GTASK_JSON_ID)) { + setGid(js.getString(GTaskStringUtils.GTASK_JSON_ID)); + } + + // 如果JSON对象中包含任务最后修改时间字段,获取并设置任务的最后修改时间 + if (js.has(GTaskStringUtils.GTASK_JSON_LAST_MODIFIED)) { + setLastModified(js.getLong(GTaskStringUtils.GTASK_JSON_LAST_MODIFIED)); + } + + // 如果JSON对象中包含任务名称字段,获取并设置任务的名称 + if (js.has(GTaskStringUtils.GTASK_JSON_NAME)) { + setName(js.getString(GTaskStringUtils.GTASK_JSON_NAME)); + } + + // 如果JSON对象中包含任务备注信息字段,获取并设置任务的备注信息 + if (js.has(GTaskStringUtils.GTASK_JSON_NOTES)) { + setNotes(js.getString(GTaskStringUtils.GTASK_JSON_NOTES)); + } + + // 如果JSON对象中包含任务删除标记字段,获取并设置任务的删除标记(是否已删除的状态) + if (js.has(GTaskStringUtils.GTASK_JSON_DELETED)) { + setDeleted(js.getBoolean(GTaskStringUtils.GTASK_JSON_DELETED)); + } + + // 如果JSON对象中包含任务完成标记字段,获取并设置任务是否已完成的状态 + if (js.has(GTaskStringUtils.GTASK_JSON_COMPLETED)) { + setCompleted(js.getBoolean(GTaskStringUtils.GTASK_JSON_COMPLETED)); + } + } catch (JSONException e) { + // 如果在解析JSON数据并设置任务属性过程中出现异常,记录错误日志,打印异常堆栈信息, + // 并抛出获取任务内容失败的异常,方便上层业务逻辑进行相应处理 + Log.e(TAG, e.toString()); + e.printStackTrace(); + throw new ActionFailureException("fail to get task content from jsonobject"); + } + } + } + + // 根据本地的JSON数据来设置任务的内容信息,先进行一些必要的条件判断,如果数据不符合要求则记录警告日志, + // 若数据有效则尝试从JSON对象中解析出任务名称并进行设置 + public void setContentByLocalJSON(JSONObject js) { + if (js == null ||!js.has(GTaskStringUtils.META_HEAD_NOTE) + ||!js.has(GTaskStringUtils.META_HEAD_DATA)) { + Log.w(TAG, "setContentByLocalJSON: nothing is avaiable"); + } + + try { + // 从JSON对象中获取表示笔记头部信息的JSON对象(具体结构由业务定义) + JSONObject note = js.getJSONObject(GTaskStringUtils.META_HEAD_NOTE); + // 从JSON对象中获取表示数据内容的JSON数组(可能包含多条具体数据记录,具体结构由业务定义) + JSONArray dataArray = js.getJSONArray(GTaskStringUtils.META_HEAD_DATA); + + // 如果笔记类型不是普通笔记类型(Notes.TYPE_NOTE),记录错误日志并直接返回,不进行后续设置操作, + // 因为可能不符合该方法处理任务内容的预期类型要求 + if (note.getInt(NoteColumns.TYPE)!= Notes.TYPE_NOTE) { + Log.e(TAG, "invalid type"); + return; + } + + // 遍历数据内容的JSON数组,查找MIME类型为特定类型(DataConstants.NOTE,可能表示文本类型等,由业务定义)的数据记录, + // 一旦找到则将其内容设置为任务的名称,并结束循环(只取第一个符合条件的数据作为任务名称,具体逻辑由业务决定) + for (int i = 0; i < dataArray.length(); i++) { + JSONObject data = dataArray.getJSONObject(i); + if (TextUtils.equals(data.getString(DataColumns.MIME_TYPE), DataConstants.NOTE)) { + setName(data.getString(DataColumns.CONTENT)); + break; + } + } + + } catch (JSONException e) { + // 如果在解析JSON数据过程中出现异常,记录错误日志并打印异常堆栈信息,方便排查问题 + Log.e(TAG, e.toString()); + e.printStackTrace(); + } + } + + // 从任务的内容中获取本地JSON对象的方法,根据任务的不同情况(如是否是新创建的任务、是否已同步等)构建相应的JSON结构, + // 返回表示任务本地信息的JSON对象,可用于本地存储、数据传递等业务场景 + public JSONObject getLocalJSONFromContent() { + String name = getName(); + try { + // 如果任务的元数据信息(mMetaInfo)为null,可能表示这是一个新从网络创建的任务 + if (mMetaInfo == null) { + // 如果任务名称也为null,记录警告日志并返回null,因为可能没有足够信息构建有效的JSON对象, + // 意味着这个任务看起来像是一个空任务(没有关键信息) + if (name == null) { + Log.w(TAG, "the note seems to be an empty one"); + return null; + } + + JSONObject js = new JSONObject(); + JSONObject note = new JSONObject(); + JSONArray dataArray = new JSONArray(); + JSONObject data = new JSONObject(); + // 将任务名称设置为数据记录的内容(具体结构符合业务要求,用于后续解析等操作) + data.put(DataColumns.CONTENT, name); + // 将包含任务名称的数据记录添加到数据数组中 + dataArray.put(data); + // 将数据数组添加到外层的js对象中,对应特定的键(由GTaskStringUtils定义),表示数据内容部分 + js.put(GTaskStringUtils.META_HEAD_DATA, dataArray); + // 设置笔记类型为普通笔记类型(Notes.TYPE_NOTE),添加到表示笔记头部信息的JSON对象中 + note.put(NoteColumns.TYPE, Notes.TYPE_NOTE); + // 将包含笔记类型等信息的头部JSON对象添加到外层的js对象中,对应特定的键(由GTaskStringUtils定义) + js.put(GTaskStringUtils.META_HEAD_NOTE, note); + return js; + } else { + // 如果任务有元数据信息,可能表示这是一个已同步过的任务 + JSONObject note = mMetaInfo.getJSONObject(GTaskStringUtils.META_HEAD_NOTE); + JSONArray dataArray = mMetaInfo.getJSONArray(GTaskStringUtils.META_HEAD_DATA); + + // 遍历数据内容的JSON数组,查找MIME类型为特定类型(DataConstants.NOTE)的数据记录, + // 一旦找到则将其内容更新为当前任务的名称(可能是同步后更新本地数据的一种操作,确保数据一致性),并结束循环 + for (int i = 0; i < dataArray.length(); i++) { + JSONObject data = dataArray.getJSONObject(i); + if (TextUtils.equals(data.getString(DataColumns.MIME_TYPE), DataConstants.NOTE)) { + data.put(DataColumns.CONTENT, getName()); + break; + } + } + + // 设置笔记类型为普通笔记类型(Notes.TYPE_NOTE),并返回包含完整任务本地信息的元数据JSON对象 + note.put(NoteColumns.TYPE, Notes.TYPE_NOTE); + return mMetaInfo; + } + } catch (JSONException e) { + // 如果在构建JSON对象过程中出现异常,记录错误日志并打印异常堆栈信息,返回null表示获取本地JSON失败 + Log.e(TAG, e.toString()); + e.printStackTrace(); + return null; + } + } + + // 设置任务的元数据信息的方法,根据传入的MetaData对象(可能包含了任务的一些额外描述、属性等信息)来更新任务的元数据, + // 如果传入的MetaData对象及其备注信息不为null,则尝试将备注信息转换为JSONObject并赋值给任务的元数据属性(mMetaInfo), + // 若转换过程中出现异常则记录警告日志并将mMetaInfo设置为null + public void setMetaInfo(MetaData metaData) { + if (metaData!= null && metaData.getNotes()!= null) { + try { + mMetaInfo = new JSONObject(metaData.getNotes()); + } catch (JSONException e) { + Log.w(TAG, e.toString()); + mMetaInfo = null; + } + } + } + + // 通过数据库游标获取该任务对应的同步操作类型的方法,根据任务的元数据、数据库中记录的信息以及任务自身的一些属性进行判断, + // 返回对应的同步操作类型常量(如无操作、更新远程、更新本地、冲突等,这些常量在父类Node中定义), + // 用于在同步相关的业务逻辑中确定如何处理该任务的数据一致性等问题 +// 通过数据库游标获取该任务对应的同步操作类型的方法 +public int getSyncAction(Cursor c) { + try { + // 用于存储从任务元数据中获取的笔记信息相关的JSON对象,初始化为null + JSONObject noteInfo = null; + // 如果任务的元数据信息(mMetaInfo)不为null,并且其包含了特定的笔记头部信息(由GTaskStringUtils.META_HEAD_NOTE标识,业务自定义结构) + if (mMetaInfo!= null && mMetaInfo.has(GTaskStringUtils.META_HEAD_NOTE)) { + // 从任务元数据的JSON对象中获取笔记头部信息的JSON对象 + noteInfo = mMetaInfo.getJSONObject(GTaskStringUtils.META_HEAD_NOTE); + } + + // 如果获取到的笔记信息JSON对象为null,说明可能笔记的元数据已被删除 + if (noteInfo == null) { + Log.w(TAG, "it seems that note meta has been deleted"); + // 这种情况下,返回表示更新远程的同步操作类型(意味着需要将远程数据更新为最新状态,具体含义由业务定义的常量决定) + return SYNC_ACTION_UPDATE_REMOTE; + } + + // 检查笔记信息的JSON对象中是否包含笔记的ID字段(NoteColumns.ID,业务自定义字段标识) + if (!noteInfo.has(NoteColumns.ID)) { + Log.w(TAG, "remote note id seems to be deleted"); + // 如果不包含,说明远程笔记的ID似乎被删除了,返回表示更新本地的同步操作类型(可能需要根据本地已有数据进行修复等操作,具体依业务而定) + return SYNC_ACTION_UPDATE_LOCAL; + } + + // 验证笔记的ID是否匹配,将数据库游标中获取的笔记ID(通过SqlNote.ID_COLUMN定位,SqlNote类中定义的列索引) + // 和从笔记信息JSON对象中获取的笔记ID进行对比 + if (c.getLong(SqlNote.ID_COLUMN)!= noteInfo.getLong(NoteColumns.ID)) { + Log.w(TAG, "note id doesn't match"); + // 如果两者不匹配,返回表示更新本地的同步操作类型,可能意味着本地数据的笔记ID出现不一致情况,需要进行本地更新操作 + return SYNC_ACTION_UPDATE_LOCAL; + } + + // 检查数据库游标中表示本地修改标记的列(SqlNote.LOCAL_MODIFIED_COLUMN)的值是否为0,即判断本地是否有更新 + if (c.getInt(SqlNote.LOCAL_MODIFIED_COLUMN) == 0) { + // 如果本地没有更新 + // 再对比数据库游标中获取的同步ID(通过SqlNote.SYNC_ID_COLUMN定位)和任务自身的最后修改时间(通过getLastModified方法获取,可能继承自父类等) + if (c.getLong(SqlNote.SYNC_ID_COLUMN) == getLastModified()) { + // 如果两者相等,说明两边(本地和远程)都没有更新,返回表示无同步操作的类型 + return SYNC_ACTION_NONE; + } else { + // 如果不相等,说明远程有更新而本地没有,需要将远程数据应用到本地,返回表示更新本地的同步操作类型 + return SYNC_ACTION_UPDATE_LOCAL; + } + } else { + // 如果本地有更新 + // 验证任务的Gtask ID是否匹配,将数据库游标中获取的Gtask ID(通过SqlNote.GTASK_ID_COLUMN定位)和任务自身的Gid(通过getGid方法获取,可能继承自父类等)进行对比 + if (!c.getString(SqlNote.GTASK_ID_COLUMN).equals(getGid())) { + Log.e(TAG, "gtask id doesn't match"); + // 如果不匹配,记录严重错误日志,并返回表示同步操作出现错误的类型,说明数据一致性出现严重问题,无法正常同步 + return SYNC_ACTION_ERROR; + } + // 再次对比数据库游标中的同步ID和任务自身的最后修改时间 + if (c.getLong(SqlNote.SYNC_ID_COLUMN) == getLastModified()) { + // 如果两者相等,说明只有本地进行了修改,返回表示更新远程的同步操作类型(可能需要将本地修改推送到远程等操作,依业务而定) + return SYNC_ACTION_UPDATE_REMOTE; + } else { + // 如果不相等,说明本地和远程都有修改,产生了同步冲突,返回表示更新冲突的同步操作类型,后续需要进行冲突处理逻辑 + return SYNC_ACTION_UPDATE_CONFLICT; + } + } + } catch (Exception e) { + // 如果在上述判断过程中出现任何异常,记录错误日志并打印异常堆栈信息,方便排查问题 + Log.e(TAG, e.toString()); + e.printStackTrace(); + } + // 如果出现异常或者前面的逻辑没有正确返回同步操作类型,默认返回表示同步操作出现错误的类型 + return SYNC_ACTION_ERROR; +} + + +十、TaskList.java + + +package net.micode.notes.gtask.data; + +import android.database.Cursor; +import android.util.Log; + +import net.micode.notes.data.Notes; +import net.micode.notes.data.Notes.NoteColumns; +import net.micode.notes.gtask.exception.ActionFailureException; +import net.micode.notes.tool.GTaskStringUtils; + +import org.json.JSONException; +import org.json.JSONObject; + +import java.util.ArrayList; + +// TaskList类继承自Node类,可能代表一个任务列表对象,用于管理一组任务(Task),包含任务列表相关的数据操作、与JSON数据的转换以及任务列表内任务的增删改查等功能 +public class TaskList extends Node { + // 用于日志输出的标签,取当前类的简单名称,方便在日志中识别来源 + private static final String TAG = TaskList.class.getSimpleName(); + // 存储任务列表在某个特定顺序中的索引位置,初始化为1(具体含义依赖业务逻辑中对任务列表顺序的定义) + private int mIndex; + // 存储该任务列表包含的所有任务对象,使用ArrayList来管理任务列表中的多个任务,初始化为一个空的ArrayList + private ArrayList mChildren; + + // 构造函数,用于创建一个新的TaskList对象,先调用父类(Node)的构造函数进行初始化,然后初始化TaskList类特有的属性 + public TaskList() { + super(); + mChildren = new ArrayList(); + mIndex = 1; + } + + // 获取创建任务列表操作对应的JSON对象的方法,用于构建符合特定格式要求的JSON数据,以便执行创建任务列表相关的业务逻辑 + public JSONObject getCreateAction(int actionId) { + JSONObject js = new JSONObject(); + + try { + // 设置JSON对象中的"action_type"字段,表示操作类型为创建任务列表,使用预定义的字符串常量(由GTaskStringUtils定义) + js.put(GTaskStringUtils.GTASK_JSON_ACTION_TYPE, + GTaskStringUtils.GTASK_JSON_ACTION_TYPE_CREATE); + + // 设置JSON对象中的"action_id"字段,传入给定的actionId参数,可能用于唯一标识这个创建操作(具体用途依赖业务逻辑) + js.put(GTaskStringUtils.GTASK_JSON_ACTION_ID, actionId); + + // 设置JSON对象中的"index"字段,将任务列表当前的索引位置(mIndex属性)添加进去,用于确定任务列表在相关顺序中的位置等信息 + js.put(GTaskStringUtils.GTASK_JSON_INDEX, mIndex); + + // 创建一个新的JSONObject用于表示任务列表实体相关的信息("entity_delta"部分) + JSONObject entity = new JSONObject(); + // 设置任务列表实体的名称,通过调用getName方法获取(该方法可能继承自父类Node或者在TaskList类中有具体实现) + entity.put(GTaskStringUtils.GTASK_JSON_NAME, getName()); + // 设置任务列表创建者的ID为"null"(可能表示未指定或者默认值,具体含义依赖业务逻辑) + entity.put(GTaskStringUtils.GTASK_JSON_CREATOR_ID, "null"); + // 设置任务列表实体的类型为任务组类型,使用预定义的字符串常量(由GTaskStringUtils定义) + entity.put(GTaskStringUtils.GTASK_JSON_ENTITY_TYPE, + GTaskStringUtils.GTASK_JSON_TYPE_GROUP); + // 将包含任务列表实体信息的JSON对象添加到外层的js对象中,对应"entity_delta"字段 + js.put(GTaskStringUtils.GTASK_JSON_ENTITY_DELTA, entity); + + } catch (JSONException e) { + // 如果在构建JSON对象过程中出现异常,记录错误日志,打印异常堆栈信息,并抛出创建任务列表失败的异常, + // 方便开发人员排查问题以及在调用该方法的上层业务逻辑中进行相应的错误处理 + Log.e(TAG, e.toString()); + e.printStackTrace(); + throw new ActionFailureException("fail to generate tasklist-create jsonobject"); + } + + return js; + } + + // 获取更新任务列表操作对应的JSON对象的方法,构建符合更新任务列表要求的JSON数据结构,用于执行任务列表更新相关的业务逻辑 + public JSONObject getUpdateAction(int actionId) { + JSONObject js = new JSONObject(); + + try { + // 设置JSON对象中的"action_type"字段,表示操作类型为更新任务列表,使用预定义的字符串常量(由GTaskStringUtils定义) + js.put(GTaskStringUtils.GTASK_JSON_ACTION_TYPE, + GTaskStringUtils.GTASK_JSON_ACTION_TYPE_UPDATE); + + // 设置JSON对象中的"action_id"字段,传入给定的actionId参数,用于唯一标识这个更新操作(具体用途依赖业务逻辑) + js.put(GTaskStringUtils.GTASK_JSON_ACTION_ID, actionId); + + // 设置JSON对象中的"id"字段,通过调用自身的getGid方法获取任务列表的唯一标识符,用于明确要更新的是哪个任务列表 + js.put(GTaskStringUtils.GTASK_JSON_ID, getGid()); + + // 创建一个新的JSONObject用于表示任务列表实体相关的更新信息("entity_delta"部分) + JSONObject entity = new JSONObject(); + // 设置任务列表实体的名称,通过调用getName方法获取 + entity.put(GTaskStringUtils.GTASK_JSON_NAME, getName()); + // 设置任务列表实体的删除标记,通过调用getDeleted方法获取(该方法可能继承自父类Node或者在TaskList类中有具体实现), + // 用于表示任务列表是否已被标记为删除等相关状态更新信息 + entity.put(GTaskStringUtils.GTASK_JSON_DELETED, getDeleted()); + // 将包含任务列表实体更新信息的JSON对象添加到外层的js对象中,对应"entity_delta"字段 + js.put(GTaskStringUtils.GTASK_JSON_ENTITY_DELTA, entity); + + } catch (JSONException e) { + // 如果在构建JSON对象过程中出现异常,记录错误日志,打印异常堆栈信息,并抛出更新任务列表失败的异常, + // 以便在调用该方法的上层业务逻辑中进行错误处理和问题排查 + Log.e(TAG, e.toString()); + e.printStackTrace(); + throw new ActionFailureException("fail to generate tasklist-update jsonobject"); + } + + return js; + } + + // 根据远程的JSON数据来设置任务列表的内容信息,从传入的JSON对象中解析出各个任务列表相关的属性值,并进行相应的设置 + public void setContentByRemoteJSON(JSONObject js) { + if (js!= null) { + try { + // 如果JSON对象中包含任务列表的唯一标识符字段(由GTaskStringUtils定义的键),则获取并设置任务列表的唯一标识符(Gid) + if (js.has(GTaskStringUtils.GTASK_JSON_ID)) { + setGid(js.getString(GTaskStringUtils.GTASK_JSON_ID)); + } + + // 如果JSON对象中包含任务列表最后修改时间字段,获取并设置任务列表的最后修改时间 + if (js.has(GTaskStringUtils.GTASK_JSON_LAST_MODIFIED)) { + setLastModified(js.getLong(GTaskStringUtils.GTASK_JSON_LAST_MODIFIED)); + } + + // 如果JSON对象中包含任务列表名称字段,获取并设置任务列表的名称 + if (js.has(GTaskStringUtils.GTASK_JSON_NAME)) { + setName(js.getString(GTaskStringUtils.GTASK_JSON_NAME)); + } + + } catch (JSONException e) { + // 如果在解析JSON数据并设置任务列表属性过程中出现异常,记录错误日志,打印异常堆栈信息, + // 并抛出获取任务列表内容失败的异常,方便上层业务逻辑进行相应处理 + Log.e(TAG, e.toString()); + e.printStackTrace(); + throw new ActionFailureException("fail to get tasklist content from jsonobject"); + } + } + } + + // 根据本地的JSON数据来设置任务列表的内容信息,先进行一些必要的条件判断,如果数据不符合要求则记录警告日志, + // 若数据有效则根据不同的文件夹类型(普通文件夹、系统文件夹等)从JSON对象中解析并设置任务列表的名称 + public void setContentByLocalJSON(JSONObject js) { + if (js == null ||!js.has(GTaskStringUtils.META_HEAD_NOTE)) { + Log.w(TAG, "setContentByLocalJSON: nothing is avaiable"); + } + + try { + // 从JSON对象中获取表示文件夹信息的JSON对象(具体结构由业务定义,此处用于获取任务列表相关信息) + JSONObject folder = js.getJSONObject(GTaskStringUtils.META_HEAD_NOTE); + + // 如果文件夹类型是普通文件夹类型(Notes.TYPE_FOLDER) + if (folder.getInt(NoteColumns.TYPE) == Notes.TYPE_FOLDER) { + // 获取文件夹的摘要信息(NoteColumns.SNIPPET)作为任务列表的名称,并添加特定前缀(GTaskStringUtils.MIUI_FOLDER_PREFFIX,业务定义) + String name = folder.getString(NoteColumns.SNIPPET); + setName(GTaskStringUtils.MIUI_FOLDER_PREFFIX + name); + } else if (folder.getInt(NoteColumns.TYPE) == Notes.TYPE_SYSTEM) { + // 如果文件夹类型是系统文件夹类型 + // 判断是否是根文件夹(通过文件夹ID与特定的根文件夹ID比较,Notes.ID_ROOT_FOLDER,业务定义) + if (folder.getLong(NoteColumns.ID) == Notes.ID_ROOT_FOLDER) + // 如果是根文件夹,设置任务列表名称为特定的默认名称,并添加前缀(GTaskStringUtils.MIUI_FOLDER_PREFFIX) + setName(GTaskStringUtils.MIUI_FOLDER_PREFFIX + GTaskStringUtils.FOLDER_DEFAULT); + else if (folder.getLong(NoteColumns.ID) == Notes.ID_CALL_RECORD_FOLDER) + // 判断是否是通话记录文件夹,若是则设置相应的特定名称,并添加前缀 + setName(GTaskStringUtils.MIUI_FOLDER_PREFFIX + + GTaskStringUtils.FOLDER_CALL_NOTE); + else + // 如果不是已知的系统文件夹类型,记录错误日志 + Log.e(TAG, "invalid system folder"); + } else { + // 如果不是普通文件夹也不是系统文件夹类型,记录错误日志表示类型错误 + Log.e(TAG, "error type"); + } + } catch (JSONException e) { + // 如果在解析JSON数据过程中出现异常,记录错误日志并打印异常堆栈信息,方便排查问题 + Log.e(TAG, e.toString()); + e.printStackTrace(); + } + } + + // 从任务列表的内容中获取本地JSON对象的方法,根据任务列表的相关属性构建符合本地存储要求的JSON结构, + // 返回表示任务列表本地信息的JSON对象,可用于本地存储、数据传递等业务场景 + public JSONObject getLocalJSONFromContent() { + try { + JSONObject js = new JSONObject(); + JSONObject folder = new JSONObject(); + + String folderName = getName(); + // 如果任务列表名称以特定前缀开头(GTaskStringUtils.MIUI_FOLDER_PREFFIX),去除该前缀获取实际的文件夹名称 + if (getName().startsWith(GTaskStringUtils.MIUI_FOLDER_PREFFIX)) + folderName = folderName.substring(GTaskStringUtils.MIUI_FOLDER_PREFFIX.length(), + folderName.length()); + // 将实际的文件夹名称设置到表示文件夹信息的JSON对象中(对应NoteColumns.SNIPPET字段,业务定义) + folder.put(NoteColumns.SNIPPET, folderName); + // 根据文件夹名称判断是否是默认文件夹或者通话记录文件夹,若是则设置文件夹类型为系统文件夹类型(Notes.TYPE_SYSTEM),否则为普通文件夹类型(Notes.TYPE_FOLDER) + if (folderName.equals(GTaskStringUtils.FOLDER_DEFAULT) + || folderName.equals(GTaskStringUtils.FOLDER_CALL_NOTE)) + folder.put(NoteColumns.TYPE, Notes.TYPE_SYSTEM); + else + folder.put(NoteColumns.TYPE, Notes.TYPE_FOLDER); + + // 将包含文件夹信息的JSON对象添加到外层的js对象中,对应特定的键(由GTaskStringUtils定义),表示文件夹头部信息部分 + js.put(GTaskStringUtils.META_HEAD_NOTE, folder); + + return js; + } catch (JSONException e) { + // 如果在构建JSON对象过程中出现异常,记录错误日志并打印异常堆栈信息,返回null表示获取本地JSON失败 + Log.e(TAG, e.toString()); + e.printStackTrace(); + return null; + } + } + + // 通过数据库游标获取该任务列表对应的同步操作类型的方法,根据任务列表的本地修改情况、同步ID以及自身的Gid等信息进行判断, + // 返回对应的同步操作类型常量(如无操作、更新远程、更新本地、错误等,这些常量可能在父类Node或相关业务逻辑中定义), + // 用于在同步相关的业务逻辑中确定如何处理该任务列表的数据一致性等问题 + public int getSyncAction(Cursor c) { + try { + // 检查数据库游标中表示本地修改标记的列(SqlNote.LOCAL_MODIFIED_COLUMN)的值是否为0,即判断本地是否有更新 + if (c.getInt(SqlNote.LOCAL_MODIFIED_COLUMN) == 0) { + // 如果本地没有更新 + // 再对比数据库游标中获取的同步ID(通过SqlNote.SYNC_ID_COLUMN定位)和任务列表自身的最后修改时间(通过getLastModified方法获取,可能继承自父类等) + if (c.getLong(SqlNote.SYNC_ID_COLUMN) == getLastModified()) { + // 如果两者相等,说明两边(本地和远程)都没有更新,返回表示无同步操作的类型 + return SYNC_ACTION_NONE; + } else { + // 如果不相等,说明远程有更新而本地没有,需要将远程数据应用到本地,返回表示更新本地的同步操作类型 + return SYNC_ACTION_UPDATE_LOCAL; + } + } else { + // 如果本地有更新 + // 验证任务列表的Gtask ID是否匹配,将数据库游标中获取的Gtask ID(通过SqlNote.GTASK_ID_COLUMN定位)和任务列表自身的Gid(通过getGid方法获取,可能继承自父类等)进行对比 + if (!c.getString(SqlNote.GTASK_ID_COLUMN).equals(getGid())) { + Log.e(TAG, "gtask id doesn't match"); + // 如果不匹配,记录严重错误日志,并返回表示同步操作出现错误的类型,说明数据一致性出现严重问题,无法正常同步 + return SYNC_ACTION_ERROR; + } + // 再次对比数据库游标中的同步ID和任务列表自身的最后修改时间 + if (c.getLong(SqlNote.SYNC_ID_COLUMN) == getLastModified()) { + // 如果两者相等,说明只有本地进行了修改,返回表示更新远程的同步操作类型(可能需要将本地修改推送到远程等操作,依业务而定) + return SYNC_ACTION_UPDATE_REMOTE; + } else { + // 对于文件夹冲突的情况(此处针对任务列表类似文件夹的处理逻辑),直接应用本地修改,返回表示更新远程的同步操作类型 + return SYNC_ACTION_UPDATE_REMOTE; + } + } + } catch (Exception e) { + // 如果在上述判断过程中出现任何异常,记录错误日志并打印异常堆栈信息,方便排查问题 + Log.e(TAG, e.toString()); + e.printStackTrace(); + } + + return SYNC_ACTION_ERROR; + } + + // 获取任务列表中包含的任务数量的方法,直接返回存储任务的ArrayList的大小,即任务的个数 + public int getChildTaskCount() { + return mChildren.size(); + } + + // 向任务列表中添加一个任务的方法,将给定的任务对象添加到任务列表中,并设置任务的前一个兄弟任务以及父任务列表相关属性 + public boolean addChildTask(Task task) { + boolean ret = false; + // 如果任务对象不为null且任务列表中不包含该任务(避免重复添加) + if (task!= null &&!mChildren.contains(task)) { + // 尝试将任务添加到任务列表的ArrayList中,并获取添加操作的结果(是否添加成功) + ret = mChildren.add(task); + if (ret) { + // 如果添加成功,需要设置任务的前一个兄弟任务和父任务列表相关属性 + // 如果任务列表为空,说明当前添加的任务是第一个任务,其前一个兄弟任务设置为null,否则设置为列表中最后一个任务(即当前任务的前一个兄弟任务) + task.setPriorSibling(mChildren.isEmpty()? null : mChildren.get(mChildren.size() - 1)); + // 设置任务的父任务列表为当前的TaskList对象 + task.setParent(this); + } + } + return ret; + } + + // 向任务列表中指定索引位置添加一个任务的方法,先进行索引合法性判断,若索引合法且任务不存在于列表中,则将任务添加到指定位置, + // 同时更新任务列表中相关任务的前一个兄弟任务关系,确保任务顺序和关联关系的正确性 + public boolean addChildTask(Task task, int index) { + // 判断索引是否合法,若索引小于0或者大于任务列表中任务的数量,则记录错误日志并返回false,表示添加失败 + if (index < 0 || index > mChildren.size()) { + Log.e(TAG, "add child task: invalid index"); + return false; + } + + // 获取任务在任务列表中的位置索引(如果不存在则返回 -1) + int pos = mChildren.indexOf(task); + if (task!= null && pos == -1) { + // 将任务添加到指定的索引位置 + mChildren.add(index, task); + + // 更新任务列表中相关任务的前一个兄弟任务关系 + Task preTask = null; + Task afterTask = null; + if (index!= 0) + // 向任务列表中指定索引位置添加一个任务的方法,需要保证索引合法以及任务不存在于列表中才能添加成功,并会相应地更新任务之间的关联关系。 +public boolean addChildTask(Task task, int index) { + // 判断传入的索引是否合法,若索引小于0或者大于任务列表中当前已有的任务数量,说明索引超出范围,不符合添加要求。 + // 此时记录错误日志并返回false,表示添加任务操作失败。 + if (index < 0 || index > mChildren.size()) { + Log.e(TAG, "add child task: invalid index"); + return false; + } + + // 获取任务在当前任务列表中的位置索引,如果任务不存在于列表中则返回 -1。 + int pos = mChildren.indexOf(task); + if (task!= null && pos == -1) { + // 将给定的任务添加到任务列表的指定索引位置。 + mChildren.add(index, task); + + // 更新任务列表中相关任务的前一个兄弟任务关系,以维护任务顺序和关联逻辑。 + + // 初始化前一个任务为null,后续根据索引情况判断是否需要重新赋值。 + Task preTask = null; + // 初始化后一个任务为null,后续根据索引情况判断是否需要重新赋值。 + Task afterTask = null; + + // 如果添加任务的索引位置不是0,说明前面有其他任务,获取当前索引位置前一个位置的任务作为前一个兄弟任务。 + if (index!= 0) + preTask = mChildren.get(index - 1); + + // 如果添加任务的索引位置不是任务列表中最后一个位置(即后面还有其他任务),获取当前索引位置后一个位置的任务作为后一个兄弟任务。 + if (index!= mChildren.size() - 1) + afterTask = mChildren.get(index + 1); + + // 设置新添加任务的前一个兄弟任务为前面获取到的preTask(可能为null,如果是第一个任务)。 + task.setPriorSibling(preTask); + + // 如果存在后一个任务(afterTask不为null),则设置后一个任务的前一个兄弟任务为当前新添加的任务,建立正确的任务顺序关联。 + if (afterTask!= null) + afterTask.setPriorSibling(task); + } + + // 表示任务添加操作是否成功,若任务成功添加到指定位置并完成关联关系更新则返回true,否则返回false(比如任务已存在等情况)。 + return true; +} + + + +十一、ActionFailureException.java + + + +package net.micode.notes.gtask.exception; + +// ActionFailureException类继承自RuntimeException,属于运行时异常的一种。 +// 通常用于表示在执行某个操作(可能是与任务相关的业务操作,具体取决于所在项目的业务逻辑)时出现了失败的情况,方便在代码中抛出并传递错误信息。 +public class ActionFailureException extends RuntimeException { + // 用于序列化的版本号,在Java中实现序列化时,这个版本号有助于确保序列化和反序列化过程的兼容性,保证不同版本的类实例在传输或存储后能正确恢复。 + // 这里给定了一个固定的长整型数值作为序列化版本号,是按照Java序列化机制的要求定义的。 + private static final long serialVersionUID = 4425249765923293627L; + + // 无参构造函数,调用父类(RuntimeException)的无参构造函数,创建一个默认的ActionFailureException实例, + // 通常在不需要传递具体错误信息时使用,可能后续通过其他方式(比如调用set方法等,若有定义的话)来设置相关的错误详情。 + public ActionFailureException() { + super(); + } + + // 带有一个字符串参数的构造函数,用于创建一个包含具体错误消息的ActionFailureException实例。 + // 该参数paramString会传递给父类(RuntimeException)的构造函数,以便在抛出异常时能携带相应的错误描述信息,方便开发人员排查问题、了解操作失败的原因。 + public ActionFailureException(String paramString) { + super(paramString); + } + + // 带有一个字符串参数和一个Throwable参数的构造函数,用于创建一个包含具体错误消息以及关联异常(可能是导致当前操作失败的底层异常)的ActionFailureException实例。 + // 其中,paramString作为错误消息传递给父类构造函数,paramThrowable表示关联的异常也传递给父类构造函数,这样在异常堆栈信息等展示中可以完整呈现操作失败的详细情况, + // 有助于更深入地追踪和解决问题,比如当一个操作失败是由另一个更底层的异常引发时,可以通过这个构造函数将底层异常一并传递出去。 + public ActionFailureException(String paramString, Throwable paramThrowable) { + super(paramString, paramThrowable); + } +} + + + +十二、NetworkFailureException.java + + + +package net.micode.notes.gtask.exception; + +// NetworkFailureException类继承自Exception类,属于受检异常(Checked Exception),意味着在使用该异常的地方通常需要显式地进行处理(使用try-catch语句块或者在方法声明中抛出)。 +// 这个类主要用于表示在网络相关操作(比如网络请求、数据传输等与网络交互的场景,具体取决于所在项目的业务逻辑)中出现失败情况时抛出的异常。 +public class NetworkFailureException extends Exception { + // 用于序列化的版本号,这是Java序列化机制要求的一个属性。 + // 当类的实例需要进行序列化(比如保存到文件、在网络间传输对象等情况)和反序列化时,这个版本号可以确保不同版本的类之间的兼容性, + // 这里给定了一个固定的长整型数值作为序列化版本号,用于唯一标识该类的序列化格式。 + private static final long serialVersionUID = 2107610287180234136L; + + // 无参构造函数,调用父类(Exception)的无参构造函数来创建一个默认的NetworkFailureException实例。 + // 一般在不需要传递具体错误信息,只是想表明发生了网络相关操作失败的情况下使用,后续可能通过其他方式(比如设置额外属性等,若有定义的话)来补充错误详情。 + public NetworkFailureException() { + super(); + } + + // 带有一个字符串参数的构造函数,用于创建一个携带具体错误消息的NetworkFailureException实例。 + // 参数paramString会传递给父类(Exception)的构造函数,使得在抛出该异常时能附带相应的错误描述信息,方便开发人员了解网络操作具体是因为什么原因而失败,便于排查问题。 + public NetworkFailureException(String paramString) { + super(paramString); + } + + // 带有一个字符串参数和一个Throwable参数的构造函数,用于创建一个既包含具体错误消息又关联了其他异常(可能是导致网络操作失败的底层异常)的NetworkFailureException实例。 + // 其中,paramString作为错误消息传递给父类构造函数,paramThrowable表示关联的异常也传递给父类构造函数,这样在异常堆栈信息展示等方面可以完整呈现网络操作失败的详细情况, + // 例如,当网络操作失败是由某个底层的IOException等异常引发时,可以通过这个构造函数将底层异常一并传递出去,有助于更深入地追踪和解决网络相关的问题。 + public NetworkFailureException(String paramString, Throwable paramThrowable) { + super(paramString, paramThrowable); + } +} + + + +十三、GTaskASyncTask.java + + + +package net.micode.notes.gtask.remote; + +import android.app.Notification; +import android.app.NotificationManager; +import android.app.PendingIntent; +import android.content.Context; +import android.content.Intent; +import android.os.AsyncTask; + +import net.micode.notes.R; +import net.micode.notes.ui.NotesListActivity; +import net.micode.notes.ui.NotesPreferenceActivity; + +// GTaskASyncTask类继承自AsyncTask,用于在后台线程执行任务,并能在任务执行过程中更新进度、在任务完成后处理结果等, +// 这里主要用于处理与GTask相关的同步任务(推测是与任务数据同步相关的操作,具体取决于所在项目的业务逻辑),并通过通知展示同步状态等信息给用户。 +public class GTaskASyncTask extends AsyncTask { + + // 用于标识GTask同步相关通知的唯一ID,固定为5234235,在整个应用中用于区分不同的通知,方便管理通知的显示、更新和取消等操作。 + private static int GTASK_SYNC_NOTIFICATION_ID = 5234235; + + // 定义一个接口,用于在同步任务完成时通知外部监听器,外部类实现该接口来定义具体的完成后要执行的操作。 + public interface OnCompleteListener { + void onComplete(); + } + + // 存储上下文对象,用于获取系统服务、资源以及启动相关的Activity等操作,与所在的Android应用环境相关。 + private Context mContext; + // 用于管理通知的显示、更新和取消等操作,通过获取系统的通知服务来实例化该对象。 + private NotificationManager mNotifiManager; + // 用于管理GTask相关的操作(可能包括任务数据的获取、同步逻辑等,具体功能由GTaskManager类定义),通过单例模式获取其实例。 + private GTaskManager mTaskManager; + // 存储实现了OnCompleteListener接口的对象,用于在同步任务完成后回调相应的方法,执行外部定义的完成操作。 + private OnCompleteListener mOnCompleteListener; + + // 构造函数,用于创建一个GTaskASyncTask实例,传入应用上下文和完成监听器对象,初始化相关的成员变量。 + public GTaskASyncTask(Context context, OnCompleteListener listener) { + mContext = context; + mOnCompleteListener = listener; + // 获取系统的通知服务,用于后续创建和管理同步任务相关的通知,将其赋值给mNotifiManager成员变量。 + mNotifiManager = (NotificationManager) mContext + .getSystemService(Context.NOTIFICATION_SERVICE); + // 通过单例模式获取GTaskManager的实例,用于执行具体的GTask相关操作,比如同步任务等,将其赋值给mTaskManager成员变量。 + mTaskManager = GTaskManager.getInstance(); + } + + // 用于取消正在进行的同步任务,通过调用GTaskManager实例的cancelSync方法来实现具体的取消逻辑,外部可以在合适的时机调用该方法来停止同步操作。 + public void cancelSync() { + mTaskManager.cancelSync(); + } + + // 对外提供一个方法来发布同步任务的进度信息,实际上是调用AsyncTask的publishProgress方法, + // 将传入的消息字符串包装成字符串数组后传递,触发onProgressUpdate方法的调用(遵循AsyncTask的机制)。 + public void publishProgess(String message) { + publishProgress(new String[] { + message + }); + } + + // 私有方法,用于显示通知,根据传入的提示文本ID(tickerId)和通知内容(content)来创建并显示一个通知,展示同步任务的相关状态信息给用户。 + private void showNotification(int tickerId, String content) { + // 创建一个新的Notification对象,传入应用图标资源ID(R.drawable.notification)、提示文本(通过上下文获取对应资源ID的字符串)以及当前时间戳作为基本信息。 + Notification notification = new Notification(R.drawable.notification, mContext + .getString(tickerId), System.currentTimeMillis()); + // 设置通知的默认属性为显示默认灯光效果(可以根据系统设置来决定具体的灯光表现形式),用于提醒用户有新通知。 + notification.defaults = Notification.DEFAULT_LIGHTS; + // 设置通知的标志为自动取消,意味着当用户点击通知后,该通知会自动从通知栏消失,提供良好的用户交互体验。 + notification.flags = Notification.FLAG_AUTO_CANCEL; + + PendingIntent pendingIntent; + // 根据提示文本ID判断,如果不是表示成功的文本ID(R.string.ticker_success),则创建一个PendingIntent用于启动NotesPreferenceActivity, + // 通常可能是在同步出现非成功情况时,引导用户到相关的设置页面查看或处理问题。 + if (tickerId!= R.string.ticker_success) { + pendingIntent = PendingIntent.getActivity(mContext, 0, new Intent(mContext, + NotesPreferenceActivity.class), 0); + + } else { + // 如果是表示成功的文本ID,则创建一个PendingIntent用于启动NotesListActivity,可能是在同步成功后引导用户查看同步后的任务列表等相关内容。 + pendingIntent = PendingIntent.getActivity(mContext, 0, new Intent(mContext, + NotesListActivity.class), 0); + } + + // 设置通知的详细信息,包括标题(应用名称,通过上下文获取对应资源ID的字符串)、内容(传入的content参数)以及点击通知后要触发的PendingIntent,完善通知的展示和交互功能。 + notification.setLatestEventInfo(mContext, mContext.getString(R.string.app_name), content, + pendingIntent); + + // 通过NotificationManager来显示通知,使用固定的通知ID(GTASK_SYNC_NOTIFICATION_ID)来标识该通知,确保同一个同步任务相关的通知能正确更新或取消。 + mNotifiManager.notify(GTASK_SYNC_NOTIFICATION_ID, notification); + } + + // 在后台线程执行的方法,重写AsyncTask的doInBackground方法,在这里执行具体的同步任务逻辑, + // 首先发布一条登录相关的同步进度消息(包含同步账号名称信息),然后调用GTaskManager的sync方法执行同步操作,并返回同步结果。 + @Override + protected Integer doInBackground(Void... unused) { + publishProgess(mContext.getString(R.string.sync_progress_login, NotesPreferenceActivity + .getSyncAccountName(mContext))); + return mTaskManager.sync(mContext, this); + } + + // 在主线程中执行的方法,用于更新任务进度相关的UI操作,重写AsyncTask的onProgressUpdate方法, + // 在这里调用showNotification方法显示同步进度通知,并根据上下文情况(如果是GTaskSyncService类型)发送广播传递进度信息(可能用于其他组件监听进度情况)。 + @Override + protected void onProgressUpdate(String... progress) { + showNotification(R.string.ticker_syncing, progress[0]); + if (mContext instanceof GTaskSyncService) { + ((GTaskSyncService) mContext).sendBroadcast(progress[0]); + } + } + + // 在主线程中执行的方法,用于处理任务完成后的相关操作,重写AsyncTask的onPostExecute方法, + // 根据同步结果(不同的状态码,由GTaskManager定义)显示相应的通知,告知用户同步成功、网络错误、内部错误或任务取消等不同情况, + // 如果存在完成监听器(mOnCompleteListener不为null),则在新线程中回调其onComplete方法,执行外部定义的完成操作。 + @Override + protected void onPostExecute(Integer result) { + if (result == GTaskManager.STATE_SUCCESS) { + showNotification(R.string.ticker_success, mContext.getString( + R.string.success_sync_account, mTaskManager.getSyncAccount())); + NotesPreferenceActivity.setLastSyncTime(mContext, System.currentTimeMillis()); + } else if (result == GTaskManager.STATE_NETWORK_ERROR) { + showNotification(R.string.ticker_fail, mContext.getString(R.string.error_sync_network)); + } else if (result == GTaskManager.STATE_INTERNAL_ERROR) { + showNotification(R.string.ticker_fail, mContext.getString(R.string.error_sync_internal)); + } else if (result == GTaskManager.STATE_SYNC_CANCELLED) { + showNotification(R.string.ticker_cancel, mContext + .getString(R.string.error_sync_cancelled)); + } + if (mOnCompleteListener!= null) { + new Thread(new Runnable() { + + public void run() { + mOnCompleteListener.onComplete(); + } + }).start(); + } + } +} + + + +十四、NoteEditActivity.java + + + +package net.micode.notes.ui; + +import android.app.Activity; +import android.app.AlarmManager; +import android.app.AlertDialog; +import android.app.PendingIntent; +import android.app.SearchManager; +import android.appwidget.AppWidgetManager; +import android.content.ContentUris; +import android.content.Context; +import android.content.DialogInterface; +import android.content.Intent; +import android.content.SharedPreferences; +import android.graphics.Paint; +import android.os.Bundle; +import android.preference.PreferenceManager; +import android.text.Spannable; +import android.text.SpannableString; +import android.text.TextUtils; +import android.text.format.DateUtils; +import android.text.style.BackgroundColorSpan; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuItem; +import android.view.MotionEvent; +import android.view.View; +import android.view.View.OnClickListener; +import android.view.WindowManager; +import android.widget.CheckBox; +import android.widget.CompoundButton; +import android.widget.CompoundButton.OnCheckedChangeListener; +import android.widget.EditText; +import android.widget.ImageView; +import android.widget.LinearLayout; +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.TextNote; +import net.micode.notes.model.WorkingNote; +import net.micode.notes.model.WorkingNote.NoteSettingChangedListener; +import net.micode.notes.tool.DataUtils; +import net.micode.notes.tool.ResourceParser; +import net.micode.notes.tool.ResourceParser.TextAppearanceResources; +import net.micode.notes.ui.DateTimePickerDialog.OnDateTimeSetListener; +import net.micode.notes.ui.NoteEditText.OnTextViewChangeListener; +import net.micode.notes.widget.NoteWidgetProvider_2x; +import net.micode.notes.widget.NoteWidgetProvider_4x; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +// NoteEditActivity类继承自Activity,是一个用于编辑笔记的Activity,实现了多个接口来处理用户交互、笔记设置变化以及文本变化等相关逻辑, +// 涵盖了笔记的加载、显示、编辑、保存、提醒设置、分享、发送到桌面等多种功能操作,并与相关的工具类、数据类以及其他组件(如Widget)进行交互协作。 +public class NoteEditActivity extends Activity implements OnClickListener, + NoteSettingChangedListener, OnTextViewChangeListener { + + // 用于存储笔记头部视图相关控件的内部类,方便对头部视图中各个控件进行统一管理和操作。 + private class HeadViewHolder { + public TextView tvModified; + public ImageView ivAlertIcon; + public TextView tvAlertDate; + public ImageView ibSetBgColor; + } + + // 用于将背景颜色选择按钮的ID与对应的颜色资源ID进行映射的静态Map,方便根据按钮ID获取对应的颜色值, + // 例如,R.id.iv_bg_yellow按钮对应的颜色资源ID是ResourceParser.YELLOW。 + private static final Map sBgSelectorBtnsMap = new HashMap(); + static { + sBgSelectorBtnsMap.put(R.id.iv_bg_yellow, ResourceParser.YELLOW); + sBgSelectorBtnsMap.put(R.id.iv_bg_red, ResourceParser.RED); + sBgSelectorBtnsMap.put(R.id.iv_bg_blue, ResourceParser.BLUE); + sBgSelectorBtnsMap.put(R.id.iv_bg_green, ResourceParser.GREEN); + sBgSelectorBtnsMap.put(R.id.iv_bg_white, ResourceParser.WHITE); + } + + // 用于将背景颜色资源ID与对应的选中状态图标按钮ID进行映射的静态Map,用于在选择背景颜色后显示相应的选中标识, + // 例如,ResourceParser.YELLOW颜色对应的选中图标按钮ID是R.id.iv_bg_yellow_select。 + private static final Map sBgSelectorSelectionMap = new HashMap(); + static { + sBgSelectorSelectionMap.put(ResourceParser.YELLOW, R.id.iv_bg_yellow_select); + sBgSelectorSelectionMap.put(ResourceParser.RED, R.id.iv_bg_red_select); + sBgSelectorSelectionMap.put(ResourceParser.BLUE, R.id.iv_bg_blue_select); + sBgSelectorSelectionMap.put(ResourceParser.GREEN, R.id.iv_bg_green_select); + sBgSelectorSelectionMap.put(ResourceParser.WHITE, R.id.iv_bg_white_select); + } + + // 用于将字体大小选择按钮所在布局的ID与对应的字体大小资源ID进行映射的静态Map,方便根据按钮所在布局ID获取对应的字体大小值, + // 例如,R.id.ll_font_large布局对应的字体大小资源ID是ResourceParser.TEXT_LARGE。 + private static final Map sFontSizeBtnsMap = new HashMap(); + static { + sFontSizeBtnsMap.put(R.id.ll_font_large, ResourceParser.TEXT_LARGE); + sFontSizeBtnsMap.put(R.id.ll_font_small, ResourceParser.TEXT_SMALL); + sFontSizeBtnsMap.put(R.id.ll_font_normal, ResourceParser.TEXT_MEDIUM); + sFontSizeBtnsMap.put(R.id.ll_font_super, ResourceParser.TEXT_SUPER); + } + + // 用于将字体大小资源ID与对应的字体大小选中状态图标按钮ID进行映射的静态Map,用于在选择字体大小后显示相应的选中标识, + // 例如,ResourceParser.TEXT_LARGE字体大小对应的选中图标按钮ID是R.id.iv_large_select。 + private static final Map sFontSelectorSelectionMap = new HashMap(); + static { + sFontSelectorSelectionMap.put(ResourceParser.TEXT_LARGE, R.id.iv_large_select); + sFontSelectorSelectionMap.put(ResourceParser.TEXT_SMALL, R.id.iv_small_select); + sFontSelectorSelectionMap.put(ResourceParser.TEXT_MEDIUM, R.id.iv_medium_select); + sFontSelectorSelectionMap.put(ResourceParser.TEXT_SUPER, R.id.iv_super_select); + } + + // 用于日志输出的标签,方便在日志中识别该Activity相关的操作记录,取类的简单名称。 + private static final String TAG = "NoteEditActivity"; + + // 存储笔记头部视图相关控件的实例,通过HeadViewHolder类进行管理,方便后续设置和获取这些控件的属性、响应事件等操作。 + private HeadViewHolder mNoteHeaderHolder; + // 存储笔记标题头部视图的整体布局对象,方便对其进行背景设置等操作,通过findViewById方法获取对应的布局视图。 + private View mHeadViewPanel; + // 存储背景颜色选择器的视图对象,用于显示和选择笔记的背景颜色,通过findViewById方法获取对应的视图,点击相关按钮可切换背景颜色。 + private View mNoteBgColorSelector; + // 存储字体大小选择器的视图对象,用于显示和选择笔记编辑文本的字体大小,通过findViewById方法获取对应的视图,点击相关按钮可切换字体大小。 + private View mFontSizeSelector; + // 存储笔记编辑文本的EditText控件,用户可在该控件中输入和编辑笔记的具体内容,通过findViewById方法获取对应的视图。 + private EditText mNoteEditor; + // 存储笔记编辑区域的整体布局对象,方便对其进行背景设置等操作,通过findViewById方法获取对应的布局视图。 + private View mNoteEditorPanel; + // 存储当前正在编辑的笔记对象,包含笔记的各种属性(如内容、修改日期、提醒设置等)以及相关的操作方法(如保存、加载等),通过WorkingNote类进行管理。 + private WorkingNote mWorkingNote; + // 存储应用的共享偏好设置对象,用于获取和保存应用级别的一些配置信息,例如字体大小偏好设置等,通过PreferenceManager获取默认的共享偏好设置实例。 + private SharedPreferences mSharedPrefs; + // 存储当前选择的字体大小资源ID,初始值从共享偏好设置中获取,如果不存在则使用默认字体大小资源ID(ResourceParser.BG_DEFAULT_FONT_SIZE)。 + private int mFontSizeId; + + // 用于存储字体大小偏好设置的键值,在共享偏好设置中通过该键来保存和获取用户选择的字体大小配置信息。 + private static final String PREFERENCE_FONT_SIZE = "pref_font_size"; + + // 定义快捷方式图标标题的最大长度,用于在创建笔记快捷方式时,截取合适长度的笔记内容作为快捷方式图标标题,避免标题过长显示不全。 + private static final int SHORTCUT_ICON_TITLE_MAX_LEN = 10; + + // 定义表示复选框选中状态的字符常量,用于在笔记内容以列表模式展示时,标记已选中的列表项,通过特定字符来体现选中状态,方便解析和展示。 + public static final String TAG_CHECKED = String.valueOf('\u221A'); + // 定义表示复选框未选中状态的字符常量,用于在笔记内容以列表模式展示时,标记未选中的列表项,通过特定字符来体现未选中状态,方便解析和展示。 + public static final String TAG_UNCHECKED = String.valueOf('\u25A1'); + + // 存储用于展示笔记内容为列表模式时的线性布局对象,在列表模式下,每个列表项会添加到该布局中进行展示,通过findViewById方法获取对应的视图。 + private LinearLayout mEditTextList; + + // 存储用户的搜索查询内容,用于在加载笔记时判断是否从搜索结果中打开笔记,以及在笔记编辑过程中对查询内容进行高亮显示等相关操作。 + private String mUserQuery; + // 用于编译用户查询内容的正则表达式对象,用于在笔记内容中匹配查询内容,以便进行高亮显示等操作,通过Pattern类进行编译和后续的匹配操作。 + private Pattern mPattern; + + // Activity创建时调用的方法,用于初始化Activity的基本状态,设置Activity的布局内容, + // 并根据传入的Intent意图判断是否能正确初始化Activity的状态,如果不能则结束Activity,保证数据的完整性和操作的合理性。 + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + // 设置Activity的布局内容,通过加载R.layout.note_edit布局文件来展示笔记编辑的界面视图,包含编辑文本区域、头部信息、各种设置按钮等相关UI组件。 + this.setContentView(R.layout.note_edit); + + // 如果savedInstanceState为null(表示Activity首次创建,不是由于系统回收后恢复)并且无法通过传入的Intent初始化Activity状态, + // 则结束当前Activity,避免出现异常情况或数据不一致问题,直接返回不再执行后续初始化操作。 + if (savedInstanceState == null &&!initActivityState(getIntent())) { + finish(); + return; + } + // 初始化Activity相关的资源,如各种视图控件、共享偏好设置等,方便后续操作使用。 + initResources(); + } + + /** + * 当系统内存不足导致Activity被回收后,再次启动该Activity时会调用此方法,用于从之前保存的状态(savedInstanceState)中恢复Activity的相关信息, + * 如果能从保存的状态中获取到关键信息(如笔记的唯一标识符UID),则尝试重新初始化Activity状态,若初始化失败则结束Activity。 + */ + @Override + protected void onRestoreInstanceState(Bundle savedInstanceState) { + super.onRestoreInstanceState(savedInstanceState); + if (savedInstanceState!= null && savedInstanceState.containsKey(Intent.EXTRA_UID)) { + // 创建一个新的Intent意图,设置动作为Intent.ACTION_VIEW,用于模拟查看笔记的操作意图,后续根据该意图来重新加载笔记信息。 + Intent intent = new Intent(Intent.ACTION_VIEW); + // 从保存的状态中获取笔记的唯一标识符UID,并设置到新的Intent意图中,以便准确找到对应的笔记进行加载。 + intent.putExtra(Intent.EXTRA_UID, savedInstanceState.getLong(Intent.EXTRA_UID)); + if (!initActivityState(intent)) { + finish(); + return; + } + Log.d(TAG, "Restoring from killed activity"); + } + } + + // 根据传入的Intent意图来初始化Activity的状态,包括判断意图的动作(如查看、新建或编辑笔记等), + // 根据不同情况加载相应的笔记数据,如果笔记不存在或者加载失败则结束Activity,同时根据操作类型设置软键盘的显示模式等相关属性。 + private boolean initActivityState(Intent intent) { + // 初始化当前正在编辑的笔记对象为null,后续根据不同的意图动作来加载对应的笔记实例。 + mWorkingNote = null; + // 判断传入的Intent意图的动作是否为Intent.ACTION_VIEW(表示查看笔记操作)。 + if (TextUtils.equals(Intent.ACTION_VIEW, intent.getAction())) { + // 从Intent意图中获取笔记的唯一标识符(noteId),如果没有提供则默认为0,用于后续查找对应的笔记数据。 + long noteId = intent.getLongExtra(Intent.EXTRA_UID, 0); + mUserQuery = ""; + + // 判断Intent意图中是否包含搜索相关的额外数据键(SearchManager.EXTRA_DATA_KEY),如果包含则表示从搜索结果中打开笔记, + // 需要重新解析获取笔记的唯一标识符(noteId),并获取用户的搜索查询内容(mUserQuery),用于后续在笔记中高亮显示查询结果等操作。 + if (intent.hasExtra(SearchManager.EXTRA_DATA_KEY)) { + noteId = Long.parseLong(intent.getStringExtra(SearchManager.EXTRA_DATA_KEY)); + mUserQuery = intent.getStringExtra(SearchManager.USER_QUERY); + } + + // 通过DataUtils工具类判断指定的笔记是否在笔记数据库中可见(即是否存在且满足一定的可见性条件), + // 如果笔记不存在,则跳转到NotesListActivity(笔记列表Activity),显示提示信息告知用户笔记不存在,然后结束当前Activity,并返回false表示初始化失败。 + if (!DataUtils.visibleInNoteDatabase(getContentResolver(), noteId, Notes.TYPE_NOTE)) { + Intent jump = new Intent(this, NotesListActivity.class); + startActivity(jump); + showToast(R.string.error_note_not_exist); + finish(); + return false; + } else { + // 如果笔记存在,则通过WorkingNote类的加载方法,根据笔记的唯一标识符(noteId)加载对应的笔记对象, + // 如果加载失败,则记录错误日志,结束当前Activity,并返回false表示初始化失败。 + mWorkingNote = WorkingNote.load(this, noteId); + if (mWorkingNote == null) { + Log.e(TAG, "load note failed with note id" + noteId); + finish(); + return false; + } + } + // 设置软键盘的显示模式为隐藏状态,并且当软键盘弹出时,Activity的布局会根据软键盘的高度进行自适应调整,提供更好的用户界面展示效果。 + getWindow().setSoftInputMode( + WindowManager.LayoutParams.SOFT_INPUT_STATE_HIDDEN + | WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE); + } else if (TextUtils.equals(Intent.ACTION_INSERT_OR_EDIT, intent.getAction())) { + // 如果Intent意图的动作是Intent.ACTION_INSERT_OR_EDIT(表示新建或编辑笔记操作),则进行新建笔记相关的初始化操作。 + + // 从Intent意图中获取笔记所属文件夹的唯一标识符(folderId),如果没有提供则默认为0,用于确定笔记的所属位置等相关信息。 + long folderId = intent.getLongExtra(Notes.INTENT_EXTRA_FOLDER_ID, 0); + // 从Intent意图中获取与笔记关联的Widget的唯一标识符(widgetId),如果没有提供则使用AppWidgetManager.INVALID_APPWIDGET_ID表示无效的Widget ID。 + int widgetId = intent.getIntExtra(Notes.INTENT_EXTRA_WIDGET_ID, + AppWidgetManager.INVALID_APPWIDGET_ID); + // 从Intent意图中获取与笔记关联的Widget的类型(widgetType),如果没有提供则使用Notes.TYPE_WIDGET_INVALIDE表示无效的Widget类型。 + int widgetType = intent.getIntExtra(Notes.INTENT_EXTRA_WIDGET_TYPE, + Notes.TYPE_WIDGET_INVALIDE); + // 从Intent意图中获取笔记的背景资源ID(bgResId),如果没有提供则通过ResourceParser.getDefaultBgId方法获取默认的背景资源ID。 + int bgResId = intent.getIntExtra(Notes.INTENT_EXTRA_BACKGROUND_ID, + ResourceParser.getDefaultBgId(this)); + + // 尝试解析是否为通话记录笔记,从Intent意图中获取电话号码(phoneNumber)和通话日期(callDate)信息, + // 如果通话日期不为0且电话号码不为null,则表示可能是通话记录笔记,需要进一步处理。 + String phoneNumber = intent.getStringExtra(Intent.EXTRA_PHONE_NUMBER); + long callDate = intent.getLongExtra(Notes.INTENT_EXTRA_CALL_DATE, 0); + if (callDate!= 0 && phoneNumber!= null) { + if (TextUtils.isEmpty(phoneNumber)) { + Log.w(TAG, "The call record number is null"); + } + long noteId = 0; + // 通过DataUtils工具类根据电话号码和通话日期尝试获取对应的笔记唯一标识符(noteId), + // 如果获取到的noteId大于0,则表示存在对应的通话记录笔记,通过WorkingNote类的加载方法加载该笔记对象, + // 如果加载失败,则记录错误日志,结束当前Activity,并返回false表示初始化失败。 + // 通过DataUtils工具类根据电话号码(phoneNumber)和通话日期(callDate)尝试获取对应的笔记唯一标识符(noteId), + // 如果获取到的noteId大于0,则表示存在对应的通话记录笔记,通过WorkingNote类的加载方法加载该笔记对象, + // 如果加载失败,则记录错误日志,结束当前Activity,并返回false表示初始化失败。 + if ((noteId = DataUtils.getNoteIdByPhoneNumberAndCallDate(getContentResolver(), + phoneNumber, callDate)) > 0) { + mWorkingNote = WorkingNote.load(this, noteId); + if (mWorkingNote == null) { + Log.e(TAG, "load call note failed with note id" + noteId); + finish(); + return false; + } + } else { + // 如果根据电话号码和通话日期没有获取到对应的笔记(noteId <= 0),则创建一个空的笔记对象, + // 传入当前Activity上下文(this)、文件夹ID(folderId)、Widget ID(widgetId)、Widget类型(widgetType)以及背景资源ID(bgResId)等参数来初始化笔记的相关属性。 + mWorkingNote = WorkingNote.createEmptyNote(this, folderId, widgetId, + widgetType, bgResId); + // 将新创建的空笔记对象转换为通话记录笔记,设置对应的电话号码和通话日期信息,使其具备通话记录笔记的相关属性和格式。 + mWorkingNote.convertToCallNote(phoneNumber, callDate); + } + } else { + // 如果Intent意图既不是查看笔记(Intent.ACTION_VIEW)也不是新建或编辑笔记(Intent.ACTION_INSERT_OR_EDIT)操作, + // 说明传入的Intent意图不符合预期,记录错误日志,结束当前Activity,并返回false表示不支持该意图操作。 + Log.e(TAG, "Intent not specified action, should not support"); + finish(); + return false; + } + // 设置当前正在编辑的笔记对象(mWorkingNote)的设置状态变化监听器为当前Activity(this), + // 以便当笔记的相关设置(如背景颜色、提醒等设置)发生变化时,能回调Activity中相应的方法进行处理,保持界面显示等与笔记数据的一致性。 + mWorkingNote.setOnSettingStatusChangedListener(this); + return true; +} + +// 当Activity从暂停状态恢复到前台运行时(例如从其他Activity返回)会调用此方法,在这里用于初始化笔记界面的显示内容, +// 根据笔记的不同属性(如字体大小、是否是列表模式、是否有提醒等)来更新界面各个控件的显示状态和内容。 +@Override +protected void onResume() { + super.onResume(); + initNoteScreen(); +} + +// 用于初始化笔记界面的显示内容,包括设置笔记编辑文本的字体外观、根据笔记是否处于列表模式来显示相应的内容、隐藏颜色选择器中所有的选中标识、 +// 设置笔记编辑区域和标题区域的背景颜色、显示笔记的修改日期以及处理提醒相关的头部显示逻辑等操作,以保证界面准确展示笔记的最新状态。 +private void initNoteScreen() { + // 根据当前选择的字体大小资源ID(mFontSizeId)来设置笔记编辑文本(mNoteEditor)的字体外观,调用TextAppearanceResources类的方法获取对应的字体样式资源并应用到EditText上。 + mNoteEditor.setTextAppearance(this, TextAppearanceResources + .getTexAppearanceResource(mFontSizeId)); + // 判断当前笔记的列表模式状态,如果是列表模式(mWorkingNote.getCheckListMode() == TextNote.MODE_CHECK_LIST),则调用switchToListMode方法切换到列表模式的显示界面,展示笔记内容。 + if (mWorkingNote.getCheckListMode() == TextNote.MODE_CHECK_LIST) { + switchToListMode(mWorkingNote.getContent()); + } else { + // 如果不是列表模式,则获取带有高亮显示查询结果的笔记内容(通过调用getHighlightQueryResult方法,传入笔记内容和用户查询内容进行处理), + // 并设置到笔记编辑文本(mNoteEditor)中显示,同时将光标定位到文本末尾,方便用户继续编辑。 + mNoteEditor.setText(getHighlightQueryResult(mWorkingNote.getContent(), mUserQuery)); + mNoteEditor.setSelection(mNoteEditor.getText().length()); + } + // 遍历背景颜色选择器中所有的选中标识对应的按钮ID(通过sBgSelectorSelectionMap获取键集合),将这些选中标识按钮的可见性设置为GONE(隐藏), + // 确保每次进入界面时初始状态下不显示选中标识,后续根据实际选择的颜色来显示对应的选中标识。 + for (Integer id : sBgSelectorSelectionMap.keySet()) { + findViewById(sBgSelectorSelectionMap.get(id)).setVisibility(View.GONE); + } + // 根据笔记对象中存储的标题背景资源ID(mWorkingNote.getTitleBgResId())来设置笔记标题头部视图(mHeadViewPanel)的背景资源,使其显示对应的背景样式。 + mHeadViewPanel.setBackgroundResource(mWorkingNote.getTitleBgResId()); + // 根据笔记对象中存储的背景颜色资源ID(mWorkingNote.getBgColorResId())来设置笔记编辑区域(mNoteEditorPanel)的背景资源,使其显示对应的背景颜色。 + mNoteEditorPanel.setBackgroundResource(mWorkingNote.getBgColorResId()); + + // 使用DateUtils工具类将笔记的修改日期(mWorkingNote.getModifiedDate())格式化为合适的日期时间字符串, + // 并设置到笔记头部视图中用于显示修改日期的TextView(mNoteHeaderHolder.tvModified)上,方便用户查看笔记的修改情况。 + mNoteHeaderHolder.tvModified.setText(DateUtils.formatDateTime(this, + mWorkingNote.getModifiedDate(), DateUtils.FORMAT_SHOW_DATE + | DateUtils.FORMAT_NUMERIC_DATE | DateUtils.FORMAT_SHOW_TIME + | DateUtils.FORMAT_SHOW_YEAR)); + + /** + * TODO: Add the menu for setting alert. Currently disable it because the DateTimePicker + * is not ready + */ + // 调用showAlertHeader方法来处理提醒相关的头部显示逻辑,根据笔记是否设置了提醒以及提醒时间是否过期等情况,显示或隐藏提醒图标和提醒日期文本等相关UI元素。 + showAlertHeader(); +} + +// 根据笔记是否设置了时钟提醒(mWorkingNote.hasClockAlert())来显示或隐藏提醒相关的UI元素(如提醒日期文本和提醒图标), +// 如果设置了提醒且提醒时间未过期,则显示相对时间的提醒文本(如还有多久提醒),如果提醒时间已过期,则显示固定的过期提示文本, +// 用于在笔记头部视图中直观展示提醒的相关状态信息给用户。 +private void showAlertHeader() { + if (mWorkingNote.hasClockAlert()) { + long time = System.currentTimeMillis(); + if (time > mWorkingNote.getAlertDate()) { + mNoteHeaderHolder.tvAlertDate.setText(R.string.note_alert_expired); + } else { + // 使用DateUtils工具类获取相对时间的字符串表示,根据当前时间(time)和笔记的提醒时间(mWorkingNote.getAlertDate())计算还有多久提醒, + // 并设置到用于显示提醒日期的TextView(mNoteHeaderHolder.tvAlertDate)上,方便用户直观了解提醒剩余时间情况。 + mNoteHeaderHolder.tvAlertDate.setText(DateUtils.getRelativeTimeSpanString( + mWorkingNote.getAlertDate(), time, DateUtils.MINUTE_IN_MILLIS)); + } + mNoteHeaderHolder.tvAlertDate.setVisibility(View.VISIBLE); + mNoteHeaderHolder.ivAlertIcon.setVisibility(View.VISIBLE); + } else { + mNoteHeaderHolder.tvAlertDate.setVisibility(View.GONE); + mNoteHeaderHolder.ivAlertIcon.setVisibility(View.GONE); + }; +} + +// 当Activity接收到新的Intent意图时(例如通过启动模式的设置,同一个Activity实例可以接收多个不同的Intent启动)会调用此方法, +// 在这里重新调用initActivityState方法根据新的Intent意图来初始化Activity的状态,保证界面和数据能根据新的意图进行相应更新和处理。 +@Override +protected void onNewIntent(Intent intent) { + super.onNewIntent(intent); + initActivityState(intent); +} + +// 在Activity可能被系统销毁(例如内存不足等情况)前,用于保存Activity的相关状态信息到Bundle对象(outState)中, +// 这里如果正在编辑的笔记还不存在于数据库中(没有保存过,无noteId),则先调用saveNote方法保存笔记,然后将笔记的唯一标识符(mWorkingNote.getNoteId())保存到Bundle中, +// 方便后续恢复Activity时能根据该标识符重新加载笔记数据,同时记录日志便于调试查看保存的笔记ID信息。 +@Override +protected void onSaveInstanceState(Bundle outState) { + super.onSaveInstanceState(outState); + /** + * For new note without note id, we should firstly save it to + * generate a id. If the editing note is not worth saving, there + * is no id which is equivalent to create new note + */ + if (!mWorkingNote.existInDatabase()) { + saveNote(); + } + outState.putLong(Intent.EXTRA_UID, mWorkingNote.getNoteId()); + Log.d(TAG, "Save working note id: " + mWorkingNote.getNoteId() + " onSaveInstanceState"); +} + +// 用于分发触摸事件(如点击、滑动等触摸操作),在该方法中处理了背景颜色选择器和字体大小选择器的显示隐藏逻辑, +// 如果颜色选择器或字体大小选择器处于可见状态,并且触摸事件发生在其范围之外,则将对应的选择器隐藏,避免其长时间显示影响界面操作, +// 同时返回true表示事件已被处理,不再继续传递给其他父类的触摸事件处理方法;如果选择器不在可见状态或者触摸事件在其范围内,则继续调用父类的触摸事件分发方法进行处理。 +@Override +public boolean dispatchTouchEvent(MotionEvent ev) { + if (mNoteBgColorSelector.getVisibility() == View.VISIBLE + &&!inRangeOfView(mNoteBgColorSelector, ev)) { + mNoteBgColorSelector.setVisibility(View.GONE); + return true; + } + + if (mFontSizeSelector.getVisibility() == View.VISIBLE + &&!inRangeOfView(mFontSizeSelector, ev)) { + mFontSizeSelector.setVisibility(View.GONE); + return true; + } + return super.dispatchTouchEvent(ev); +} + +// 用于判断给定的触摸事件(ev)是否发生在指定的视图(view)范围内,通过获取视图在屏幕上的坐标位置(location数组存储x和y坐标), +// 然后比较触摸事件的坐标(ev.getX()和ev.getY())与视图的坐标范围(x、y以及视图的宽度和高度确定的范围),如果触摸事件坐标在视图坐标范围内则返回true,表示在视图内,否则返回false,表示在视图外。 +private boolean inRangeOfView(View view, MotionEvent ev) { + int []location = new int[2]; + view.getLocationOnScreen(location); + int x = location[0]; + int y = location[1]; + if (ev.getX() < x + || ev.getX() > (x + view.getWidth()) + || ev.getY() < y + || ev.getY() > (y + view.getHeight())) { + return false; + } + return true; +} + +// 用于初始化Activity相关的各种资源,包括获取笔记标题头部视图、笔记编辑文本、背景颜色选择器、字体大小选择器等各个视图控件的实例, +// 设置背景颜色选择按钮和字体大小选择按钮的点击事件监听器为当前Activity(实现了OnClickListener接口), +// 从共享偏好设置中获取存储的字体大小资源ID(如果不存在则使用默认字体大小资源ID),并获取用于展示列表模式笔记内容的线性布局对象,完成资源的初始化准备工作,方便后续操作使用。 +private void initResources() { + mHeadViewPanel = findViewById(R.id.note_title); + mNoteHeaderHolder = new HeadViewHolder(); + mNoteHeaderHolder.tvModified = (TextView) findViewById(R.id.tv_modified_date); + mNoteHeaderHolder.ivAlertIcon = (ImageView) findViewById(R.id.iv_alert_icon); + mNoteHeaderHolder.tvAlertDate = (TextView) findViewById(R.id.tv_alert_date); + mNoteHeaderHolder.ibSetBgColor = (ImageView) findViewById(R.id.btn_set_bgColor); + mNoteHeaderHolder.ibSetBgColor.setOnClickListener(this); + mNoteEditor = (EditText) findViewById(R.id.note_edit_view); + mNoteEditorPanel = findViewById(R.id.sv_note_edit); + mNoteBgColorSelector = findViewById(R.id.note_bg_color_selector); + for (int id : sBgSelectorBtnsMap.keySet()) { + ImageView iv = (ImageView) findViewById(id); + iv.setOnClickListener(this); + } + + mFontSizeSelector = findViewById(R.id.font_size_selector); + for (int id : sFontSizeBtnsMap.keySet()) { + View view = findViewById(id); + view.setOnClickListener(this); + }; + mSharedPrefs = PreferenceManager.getDefaultSharedPreferences(this); + mFontSizeId = mSharedPrefs.getInt(PREFERENCE_FONT_SIZE, ResourceParser.BG_DEFAULT_FONT_SIZE); + /** + * HACKME: Fix bug of store the resource id in shared preference. + * The id may larger than the length of resources, in this case, + * return the {@link ResourceParser#BG_DEFAULT_FONT_SIZE} + */ + if (mFontSizeId >= TextAppearanceResources.getResourcesSize()) { + mFontSizeId = ResourceParser.BG_DEFAULT_FONT_SIZE; + } + mEditTextList = (LinearLayout) findViewById(R.id.note_edit_list); +} + +// 当Activity从运行状态切换到暂停状态时(例如按下Home键、切换到其他应用等情况)会调用此方法, +// 在这里先调用saveNote方法保存正在编辑的笔记数据,如果保存成功则记录日志显示保存的笔记内容长度信息,然后调用clearSettingState方法清除一些临时的设置状态(如隐藏选择器等), +// 保证Activity暂停时数据的及时保存和界面状态的清理。 +@Override +protected void onPause() { + super.onPause(); + if (saveNote()) { + Log.d(TAG, "Note data was saved with length:" + mWorkingNote.getContent().length()); + } + clearSettingState(); +} + +// 用于更新与笔记关联的Widget显示内容,根据笔记的Widget类型(mWorkingNote.getWidgetType())创建相应的广播Intent意图(用于通知Widget更新), +// 并设置对应的Widget类(NoteWidgetProvider_2x或NoteWidgetProvider_4x),将笔记关联的Widget ID添加到Intent意图中作为额外信息, +// 然后发送广播通知Widget进行更新,同时设置Activity的返回结果为RESULT_OK,并将Intent意图作为结果数据返回,用于告知调用者(如果有)Widget更新操作已执行。 +private void updateWidget() { + Intent intent = new Intent(AppWidgetManager.ACTION_APPWIDGET_UPDATE); + if (mWorkingNote.getWidgetType() == Notes.TYPE_WIDGET_2X) { + intent.setClass(this, NoteWidgetProvider_2x.class); + } else if (mWorkingNote.getWidgetType() == Notes.TYPE_WIDGET_4X) { + intent.setClass(this, NoteWidgetProvider_4x.class); + } else { + Log.e(TAG, "Unspported widget type"); + return; + } + + intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, new int[] { + mWorkingNote.getWidgetId() + }); + + sendBroadcast(intent); + setResult(RESULT_OK, intent); +} + +// 处理各个视图控件的点击事件,根据点击的视图ID(id)来执行相应的操作,例如点击背景颜色设置按钮(R.id.btn_set_bg_color)则显示背景颜色选择器并显示当前选择颜色的选中标识, +// 点击背景颜色选择按钮(在sBgSelectorBtnsMap中存在对应的ID)则切换背景颜色并隐藏之前颜色的选中标识和颜色选择器,点击字体大小选择按钮(在sFontSizeBtnsMap中存在对应的ID)则切换字体大小, +// 更新共享偏好设置中的字体大小配置,显示新字体大小的选中标识并根据笔记是否处于列表模式来更新界面显示等相关操作,实现了对不同按钮点击事件的具体响应逻辑。 +public void onClick(View v) { + int id = v.getId(); + if (id == R.id.btn_set_bg_color) { + mNoteBgColorSelector.setVisibility(View.VISIBLE); + findViewById(sBgSelectorSelectionMap.get(mWorkingNote.getBgColorId())).setVisibility( + - View.VISIBLE); + } else if (sBgSelectorBtnsMap.containsKey(id)) { + findViewById(sBgSelectorSelectionMap.get(mWorkingNote.getBgColorId())).setVisibility( + View.GONE); + mWorkingNote.setBgColorId(sBgSelectorBtnsMap.get(id)); + mNoteBgColorSelector.setVisibility(View.GONE); + } else if (sFontSizeBtnsMap.containsKey(id)) { + findViewById(sFontSelectorSelectionMap.get(mFontSizeId)).setVisibility(View.GONE); + mFontSizeId = sFontSizeBtnsMap.get(id); + mSharedPrefs.edit().putInt(PREFERENCE_FONT_SIZE, mFontSizeId).commit(); + findViewById(sFontSelectorSelectionMap.get(mFontSizeId)).setVisibility(View.VISIBLE); + if (mWorkingNote.getCheckListMode() == TextNote.MODE_CHECK_LIST) { + getWorkingText(); + switchToListMode(mWorkingNote.getContent()); + // 如果点击的是字体大小选择按钮(在sFontSizeBtnsMap中存在对应的ID),先隐藏之前字体大小对应的选中标识, + // 然后更新当前选择的字体大小资源ID(mFontSizeId)为点击按钮对应的字体大小资源ID, + // 将新的字体大小资源ID保存到共享偏好设置(mSharedPrefs)中,再显示新字体大小对应的选中标识, + // 如果笔记当前处于列表模式(mWorkingNote.getCheckListMode() == TextNote.MODE_CHECK_LIST),则先获取工作文本(调用getWorkingText方法), + // 然后切换到列表模式显示(调用switchToListMode方法);如果不是列表模式,则根据新的字体大小资源ID设置笔记编辑文本(mNoteEditor)的字体外观, + // 最后隐藏字体大小选择器,完成字体大小切换及相关界面更新操作。 + } else if (sFontSizeBtnsMap.containsKey(id)) { + findViewById(sFontSelectorSelectionMap.get(mFontSizeId)).setVisibility(View.GONE); + mFontSizeId = sFontSizeBtnsMap.get(id); + mSharedPrefs.edit().putInt(PREFERENCE_FONT_SIZE, mFontSizeId).commit(); + findViewById(sFontSelectorSelectionMap.get(mFontSizeId)).setVisibility(View.VISIBLE); + if (mWorkingNote.getCheckListMode() == TextNote.MODE_CHECK_LIST) { + getWorkingText(); + switchToListMode(mWorkingNote.getContent()); + } else { + mNoteEditor.setTextAppearance(this, + TextAppearanceResources.getTexAppearanceResource(mFontSizeId)); + } + mFontSizeSelector.setVisibility(View.GONE); + } + } + + // 当用户按下返回键时调用此方法,先调用clearSettingState方法尝试清除一些临时的设置状态(如隐藏背景颜色选择器、字体大小选择器等), + // 如果清除成功(返回true,表示有相关设置状态需要清除且已完成清除操作),则直接返回,不执行后续保存笔记等操作; + // 如果清除状态操作返回false(表示没有需要清除的设置状态或者清除操作未执行),则调用saveNote方法保存正在编辑的笔记数据, + // 然后再调用父类的onBackPressed方法执行默认的返回键操作(例如关闭当前Activity等),保证在返回前数据的保存和界面状态的正确处理。 + @Override + public void onBackPressed() { + if (clearSettingState()) { + return; + } + + saveNote(); + super.onBackPressed(); + } + + // 用于清除一些临时的设置状态,主要是检查背景颜色选择器(mNoteBgColorSelector)和字体大小选择器(mFontSizeSelector)是否处于可见状态, + // 如果背景颜色选择器可见,则将其隐藏,并返回true表示有状态被清除;如果字体大小选择器可见,则将其隐藏,并返回true表示有状态被清除; + // 如果两个选择器都不可见,则返回false,表示没有需要清除的设置状态,方便在其他方法(如onBackPressed方法)中根据返回值判断是否执行了相关清除操作。 + private boolean clearSettingState() { + if (mNoteBgColorSelector.getVisibility() == View.VISIBLE) { + mNoteBgColorSelector.setVisibility(View.GONE); + return true; + } else if (mFontSizeSelector.getVisibility() == View.VISIBLE) { + mFontSizeSelector.setVisibility(View.GONE); + return true; + } + return false; + } + + // 当笔记的背景颜色发生变化时调用此方法,用于更新界面显示以反映背景颜色的变化, + // 显示当前背景颜色对应的选中标识(通过sBgSelectorSelectionMap根据当前背景颜色ID获取对应的选中标识按钮ID,并设置其可见性为View.VISIBLE), + // 同时根据笔记对象中存储的背景颜色资源ID(mWorkingNote.getBgColorResId())来设置笔记编辑区域(mNoteEditorPanel)的背景资源, + // 以及根据笔记对象中存储的标题背景资源ID(mWorkingNote.getTitleBgResId())来设置笔记标题头部视图(mHeadViewPanel)的背景资源, + // 保证界面各个相关部分的背景颜色显示与笔记数据的一致性。 + public void onBackgroundColorChanged() { + findViewById(sBgSelectorSelectionMap.get(mWorkingNote.getBgColorId())).setVisibility( + View.VISIBLE); + mNoteEditorPanel.setBackgroundResource(mWorkingNote.getBgColorResId()); + mHeadViewPanel.setBackgroundResource(mWorkingNote.getTitleBgResId()); + } + + // 在准备显示选项菜单(Options Menu)时调用此方法,用于初始化和配置菜单内容, + // 如果当前Activity正在结束(isFinishing()返回true),则直接返回true,表示不需要进行菜单相关操作; + // 先调用clearSettingState方法清除可能存在的临时设置状态(如隐藏选择器等),然后清空菜单原有内容(menu.clear()), + // 根据笔记所属的文件夹ID(mWorkingNote.getFolderId())判断是否是通话记录文件夹(Notes.ID_CALL_RECORD_FOLDER), + // 如果是,则通过菜单填充器(getMenuInflater)加载通话记录笔记编辑对应的菜单布局资源(R.menu.call_note_edit)到菜单中; + // 如果不是通话记录文件夹,则加载普通笔记编辑对应的菜单布局资源(R.menu.note_edit)到菜单中, + // 接着根据笔记的列表模式状态(mWorkingNote.getCheckListMode())来设置“列表模式”菜单项(R.id.menu_list_mode)的标题文本(在列表模式和非列表模式显示不同的文本), + // 最后根据笔记是否设置了时钟提醒(mWorkingNote.hasClockAlert())来设置“提醒”菜单项(R.id.menu_alert)和“删除提醒”菜单项(R.id.menu_delete_remind)的可见性, + // 完成菜单的初始化和配置工作,并返回true表示菜单已成功准备好。 + @Override + public boolean onPrepareOptionsMenu(Menu menu) { + if (isFinishing()) { + return true; + } + clearSettingState(); + menu.clear(); + if (mWorkingNote.getFolderId() == Notes.ID_CALL_RECORD_FOLDER) { + getMenuInflater().inflate(R.menu.call_note_edit, menu); + } else { + getMenuInflater().inflate(R.menu.note_edit, menu); + } + if (mWorkingNote.getCheckListMode() == TextNote.MODE_CHECK_LIST) { + menu.findItem(R.id.menu_list_mode).setTitle(R.string.menu_normal_mode); + } else { + menu.findItem(R.id.menu_list_mode).setTitle(R.string.menu_list_mode); + } + if (mWorkingNote.hasClockAlert()) { + menu.findItem(R.id.menu_alert).setVisible(false); + } else { + menu.findItem(R.id.menu_delete_remind).setVisible(false); + } + return true; + } + + // 处理选项菜单(Options Menu)中各个菜单项的点击事件,根据点击的菜单项ID(item.getItemId())来执行相应的操作, + // 例如点击“新建笔记”菜单项(R.id.menu_new_note)则调用createNewNote方法创建新的笔记;点击“删除”菜单项(R.id.menu_delete)则弹出确认删除的对话框, + // 点击“字体大小”菜单项(R.id.menu_font_size)则显示字体大小选择器及当前字体大小的选中标识;点击“列表模式”菜单项(R.id.menu_list_mode)则切换笔记的列表模式状态; + // 点击“分享”菜单项(R.id.menu_share)则获取工作文本(调用getWorkingText方法)并分享笔记内容(调用sendTo方法);点击“发送到桌面”菜单项(R.id.menu_send_to_desktop)则调用sendToDesktop方法将笔记快捷方式发送到桌面; + // 点击“提醒”菜单项(R.id.menu_alert)则调用setReminder方法设置笔记的提醒;点击“删除提醒”菜单项(R.id.menu_delete_remind)则清除笔记的提醒设置, + // 处理完相应操作后返回true,表示点击事件已被处理,避免事件继续向上传递,实现了对菜单点击操作的具体响应逻辑。 + @Override + public boolean onOptionsItemSelected(MenuItem item) { + switch (item.getItemId()) { + case R.id.menu_new_note: + createNewNote(); + break; + case R.id.menu_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_note)); + builder.setPositiveButton(android.R.string.ok, + new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int which) { + deleteCurrentNote(); + finish(); + } + }); + builder.setNegativeButton(android.R.string.cancel, null); + builder.show(); + break; + case R.id.menu_font_size: + mFontSizeSelector.setVisibility(View.VISIBLE); + findViewById(sFontSelectorSelectionMap.get(mFontSizeId)).setVisibility(View.VISIBLE); + break; + case R.id.menu_list_mode: + mWorkingNote.setCheckListMode(mWorkingNote.getCheckListMode() == 0? + TextNote.MODE_CHECK_LIST : 0); + break; + case R.id.menu_share: + getWorkingText(); + sendTo(this, mWorkingNote.getContent()); + break; + case R.id.menu_send_to_desktop: + sendToDesktop(); + break; + case R.id.menu_alert: + setReminder(); + break; + case R.id.menu_delete_remind: + mWorkingNote.setAlertDate(0, false); + break; + default: + break; + } + return true; + } + + // 用于设置笔记的提醒功能,创建一个DateTimePickerDialog(日期时间选择对话框)实例,传入当前Activity上下文(this)和当前系统时间(System.currentTimeMillis())作为初始参数, + // 为该对话框设置日期时间设置监听器(OnDateTimeSetListener),当用户在对话框中选择好日期时间并点击确定后,会回调监听器中的OnDateTimeSet方法, + // 在该方法中调用笔记对象(mWorkingNote)的setAlertDate方法,将用户选择的日期时间(date参数)设置为笔记的提醒时间,并设置提醒标志为true,表示启用该提醒设置, + // 最后显示日期时间选择对话框,供用户选择提醒的具体时间,实现提醒时间的设置功能。 + private void setReminder() { + DateTimePickerDialog d = new DateTimePickerDialog(this, System.currentTimeMillis()); + d.setOnDateTimeSetListener(new OnDateTimeSetListener() { + public void OnDateTimeSet(AlertDialog dialog, long date) { + mWorkingNote.setAlertDate(date, true); + } + }); + d.show(); + } + + /** + * Share note to apps that support {@link Intent#ACTION_SEND} action + * and {@text/plain} type + */ + // 用于将笔记内容分享到支持Intent.ACTION_SEND操作且能处理text/plain类型数据的其他应用中, + // 创建一个Intent意图,设置动作为Intent.ACTION_SEND,表示分享操作,将笔记内容(info参数)作为额外文本信息添加到Intent中(通过Intent.EXTRA_TEXT键值对添加), + // 设置Intent的数据类型为text/plain,然后通过传入的上下文(context)启动该Intent,触发系统分享功能,让用户选择要分享到的目标应用,实现笔记内容的分享功能。 + private void sendTo(Context context, String info) { + Intent intent = new Intent(Intent.ACTION_SEND); + intent.putExtra(Intent.EXTRA_TEXT, info); + intent.setType("text/plain"); + context.startActivity(intent); + } + + // 用于创建新的笔记,首先调用saveNote方法保存当前正在编辑的笔记(确保当前编辑内容的保存), + // 然后结束当前的NoteEditActivity(finish()方法),再创建一个新的启动NoteEditActivity的Intent意图,设置动作为Intent.ACTION_INSERT_OR_EDIT(表示新建或编辑笔记操作), + // 将当前笔记所属的文件夹ID(mWorkingNote.getFolderId())作为额外信息添加到Intent意图中,最后通过startActivity方法启动新的NoteEditActivity,进入新建笔记的流程, + // 这种先保存当前笔记再启动新Activity的方式可以保证数据的完整性以及新建笔记操作的正常进行。 + private void createNewNote() { + // Firstly, save current editing notes + saveNote(); + + // For safety, start a new NoteEditActivity + finish(); + Intent intent = new Intent(this, NoteEditActivity.class); + intent.setAction(Intent.ACTION_INSERT_OR_EDIT); + intent.putExtra(Notes.INTENT_EXTRA_FOLDER_ID, mWorkingNote.getFolderId()); + startActivity(intent); + } + + // 用于删除当前正在编辑的笔记,首先判断笔记是否已经存在于数据库中(mWorkingNote.existInDatabase()), + // 如果存在,则创建一个HashSet集合(ids)用于存储要删除的笔记的唯一标识符(noteId),获取当前笔记的唯一标识符(mWorkingNote.getNoteId()), + // 如果笔记的唯一标识符不等于Notes.ID_ROOT_FOLDER(根文件夹ID,这里可能是一种特殊判断,避免误删根文件夹相关的笔记),则将该笔记的唯一标识符添加到HashSet集合中, + // 接着判断是否处于同步模式(通过调用isSyncMode方法判断),如果不是同步模式,则调用DataUtils工具类的batchDeleteNotes方法,尝试批量删除HashSet集合中包含的笔记; + // 如果是同步模式,则调用DataUtils工具类的batchMoveToFolder方法,尝试将HashSet集合中包含的笔记批量移动到回收站文件夹(Notes.ID_TRASH_FOLER), + // 如果删除或移动操作失败,则记录相应的错误日志,最后无论操作是否成功,都将笔记对象标记为已删除(mWorkingNote.markDeleted(true)),完成笔记删除相关的操作逻辑。 + private void deleteCurrentNote() { + if (mWorkingNote.existInDatabase()) { + HashSet ids = new HashSet(); + long id = mWorkingNote.getNoteId(); + if (id!= Notes.ID_ROOT_FOLDER) { + ids.add(id); + } else { + Log.d(TAG, "Wrong note id, should not happen"); + } + if (!isSyncMode()) { + if (!DataUtils.batchDeleteNotes(getContentResolver(), ids)) { + Log.e(TAG, "Delete Note error"); + } + } else { + if (!DataUtils.batchMoveToFolder(getContentResolver(), ids, Notes.ID_TRASH_FOLER)) { + Log.e(TAG, "Move notes to trash folder error, should not happens"); + } + } + } + mWorkingNote.markDeleted(true); + } + + // 用于判断是否处于同步模式,通过调用NotesPreferenceActivity类的静态方法getSyncAccountName获取同步账户名称, + // 判断获取到的同步账户名称去除首尾空白字符后的长度是否大于0,如果大于0则表示存在同步账户,即处于同步模式,返回true;否则表示不存在同步账户,即不处于同步模式,返回false, + // 该方法的返回结果通常用于决定笔记的删除、移动等操作是直接删除还是移动到回收站等不同的处理逻辑。 + private boolean isSyncMode() { + return NotesPreferenceActivity.getSyncAccountName(this).trim().length() > 0; + } + + // 当笔记的时钟提醒相关设置发生变化(如设置提醒时间、取消提醒等)时调用此方法, + // 首先判断笔记是否已经存在于数据库中(mWorkingNote.existInDatabase()),如果不存在,则先调用saveNote方法保存笔记, + // 然后判断笔记的唯一标识符(mWorkingNote.getNoteId())是否大于0,如果大于0,表示笔记已保存且有有效的唯一标识符, + // 则创建一个Intent意图,设置其动作为启动AlarmReceiver(可能是用于接收闹钟提醒广播的组件),并将笔记的内容URI(通过ContentUris.withAppendedId方法根据笔记的唯一标识符构建)设置为Intent的数据, + // 创建一个PendingIntent用于广播发送,获取系统的AlarmManager服务(用于设置闹钟提醒相关操作),调用showAlertHeader方法更新提醒相关的头部显示信息, + // 根据传入的参数set的值来决定是取消提醒(set为false时,调用alarmManager.cancel方法取消之前设置的PendingIntent对应的提醒)还是设置提醒(set为true时, + // 调用alarmManager.set方法根据传入的日期时间(date参数)设置新的提醒,通过PendingIntent触发相应的提醒操作), + // 如果笔记的唯一标识符不大于0(表示可能是新创建还未保存或者不值得保存的笔记,没有有效的唯一标识符),则记录错误日志,并弹出提示Toast告知用户笔记内容不能为空才能设置提醒, + // 实现了笔记时钟提醒设置变化时的相关处理逻辑,保证提醒功能与笔记数据的一致性以及正确的提醒操作执行。 + public void onClockAlertChanged(long date, boolean set) { + /** + * User could set clock to an unsaved note, so before setting the + * alert clock, we should save the note first + */ + if (!mWorkingNote.existInDatabase()) { + saveNote(); + } + if (mWorkingNote.getNoteId() > 0) { + Intent intent = new Intent(this, AlarmReceiver.class); + // 设置用于触发闹钟提醒的Intent的数据部分,通过ContentUris工具类的withAppendedId方法, + // 将笔记的基础内容URI(Notes.CONTENT_NOTE_URI)与当前笔记的唯一标识符(mWorkingNote.getNoteId())进行拼接, + // 构建出一个指向特定笔记的完整内容URI,并设置为Intent的数据,这样后续的闹钟提醒广播在触发时就能准确关联到对应的笔记, + // 方便根据笔记的具体设置来执行提醒相关的操作(比如显示提醒内容等)。 + intent.setData(ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, mWorkingNote.getNoteId())); + // 创建一个PendingIntent对象,用于后续通过广播的方式发送意图,这里传入当前Activity上下文(this)、请求码(0,一般用于区分不同的PendingIntent请求,此处简单设为0)、 + // 前面构建好的intent(包含了指向特定笔记的信息等)以及标志位(0,表示默认的创建方式), + // 这个PendingIntent会在闹钟提醒时间到达时被系统自动触发广播,进而启动相应的处理逻辑(比如启动相关的Activity来显示提醒内容等)。 + PendingIntent pendingIntent = PendingIntent.getBroadcast(this, 0, intent, 0); + // 获取系统的AlarmManager服务实例,AlarmManager用于管理和调度各种闹钟提醒、定时任务等相关操作, + // 通过调用getSystemService方法并传入ALARM_SERVICE参数来获取该服务,后续可以利用它来设置、取消闹钟提醒等操作。 + AlarmManager alarmManager = ((AlarmManager) getSystemService(ALARM_SERVICE)); + // 调用showAlertHeader方法来更新界面上与提醒相关的头部显示信息,比如根据当前笔记的提醒设置情况(是否设置了提醒、提醒时间是否已过期等), + // 来显示或隐藏提醒图标、更新提醒日期文本等,保证界面显示与提醒设置的一致性,让用户能直观看到提醒的相关状态。 + showAlertHeader(); + // 根据传入的set参数值来判断是取消闹钟提醒还是设置闹钟提醒, + // 如果set为false,表示要取消提醒,调用alarmManager的cancel方法,并传入前面创建的PendingIntent对象, + // 这样系统就会取消对应的闹钟提醒设置,不再触发与之相关的广播和后续操作。 + if (!set) { + alarmManager.cancel(pendingIntent); + } else { + // 如果set为true,表示要设置闹钟提醒,调用alarmManager的set方法来设置一个基于实时时钟(RTC_WAKEUP,表示在设备休眠时也能唤醒设备触发提醒)的闹钟提醒, + // 传入提醒的触发时间(date参数,即用户设置的具体提醒时间)以及前面创建的PendingIntent对象, + // 当到达指定的date时间时,系统就会触发该PendingIntent对应的广播,进而执行相关的提醒操作(如弹出提醒对话框、启动相应Activity等)。 + alarmManager.set(AlarmManager.RTC_WAKEUP, date, pendingIntent); + } + } else { + /** + * There is the condition that user has input nothing (the note is + * not worthy saving), we have no note id, remind the user that he + * should input something + */ + // 如果笔记没有有效的唯一标识符(即mWorkingNote.getNoteId() <= 0),说明可能用户还没有输入任何有效内容,笔记不值得保存或者是新创建还未保存的情况, + // 此时记录错误日志,提示“Clock alert setting error”(时钟提醒设置错误),并通过showToast方法弹出一个Toast提示框, + // 显示固定的错误提示文本(R.string.error_note_empty_for_clock),告知用户需要输入一些内容才能设置时钟提醒,避免无效的提醒设置操作。 + Log.e(TAG, "Clock alert setting error"); + showToast(R.string.error_note_empty_for_clock); + } +} + +// 当与笔记关联的Widget发生变化时调用此方法,这里直接调用updateWidget方法, +// 该方法内部会根据笔记的Widget类型(如2x、4x等不同尺寸类型)创建相应的广播Intent意图, +// 并将笔记关联的Widget ID等信息添加到Intent中,然后发送广播通知对应的Widget进行更新,实现Widget显示内容与笔记数据的同步更新。 +public void onWidgetChanged() { + updateWidget(); +} + +// 处理在列表模式下删除笔记中某一行文本的操作,首先获取笔记编辑列表(mEditTextList)中当前的子视图数量(即文本行数,通过getChildCount方法获取), +// 如果只有一行文本(childCount == 1),则直接返回,不做删除操作,因为至少需要保留一行内容。 +// 接着通过循环遍历,从要删除行的下一行(index + 1)开始到最后一行,将每一行的NoteEditText控件的索引值减1(通过调用setIndex方法), +// 以更新后续行的索引顺序,保证索引的连续性,然后从笔记编辑列表中移除指定索引(index)的视图(即删除对应的那一行文本), +// 再根据删除的行索引位置获取对应的NoteEditText控件(如果删除的是第一行则获取列表中的第一个NoteEditText控件,否则获取删除行的前一行对应的NoteEditText控件), +// 获取该控件当前文本的长度(length),将传入的待删除文本(text参数)追加到该控件的文本末尾,让删除行的内容合并到前一行(或第一行)中, +// 最后让该NoteEditText控件获取焦点,并将光标定位到追加文本后的末尾位置(通过setSelection方法),方便用户继续编辑,完成文本行删除及相关的界面更新操作。 +public void onEditTextDelete(int index, String text) { + int childCount = mEditTextList.getChildCount(); + if (childCount == 1) { + return; + } + + for (int i = index + 1; i < childCount; i++) { + ((NoteEditText) mEditTextList.getChildAt(i).findViewById(R.id.et_edit_text)) + .setIndex(i - 1); + } + + mEditTextList.removeViewAt(index); + NoteEditText edit = null; + if (index == 0) { + edit = (NoteEditText) mEditTextList.getChildAt(0).findViewById( + R.id.et_edit_text); + } else { + edit = (NoteEditText) mEditTextList.getChildAt(index - 1).findViewById( + R.id.et_edit_text); + } + int length = edit.length(); + edit.append(text); + edit.requestFocus(); + edit.setSelection(length); +} + +// 处理在列表模式下在指定索引位置插入新的文本行的操作,首先进行一个边界检查, +// 如果传入的插入索引(index)大于笔记编辑列表(mEditTextList)的子视图数量(即超出了现有文本行的范围),则记录错误日志,提示“Index out of mEditTextList boundrary, should not happen”(索引超出列表边界,不应该发生这种情况), +// 因为正常情况下插入索引应该在现有行数范围内。 +// 如果索引合法,调用getListItem方法根据传入的文本(text参数)和索引(index)创建一个新的视图(包含NoteEditText等相关控件的列表项视图), +// 将该视图添加到笔记编辑列表的指定索引位置(通过addView方法),获取添加后的新视图中的NoteEditText控件,让其获取焦点,并将光标定位到文本开头(通过setSelection方法设置选择位置为0), +// 然后通过循环遍历从插入行的下一行(index + 1)开始到最后一行,将每一行的NoteEditText控件的索引值更新为正确的顺序(依次递增),保证整个列表中文本行的索引一致性,完成新文本行插入及相关的界面更新操作。 +public void onEditTextEnter(int index, String text) { + /** + * Should not happen, check for debug + */ + if (index > mEditTextList.getChildCount()) { + Log.e(TAG, "Index out of mEditTextList boundrary, should not happen"); + } + + View view = getListItem(text, index); + mEditTextList.addView(view, index); + NoteEditText edit = (NoteEditText) view.findViewById(R.id.et_edit_text); + edit.requestFocus(); + edit.setSelection(0); + for (int i = index + 1; i < mEditTextList.getChildCount(); i++) { + ((NoteEditText) mEditTextList.getChildAt(i).findViewById(R.id.et_edit_text)) + .setIndex(i); + } +} + +// 用于将笔记的显示模式切换到列表模式,首先清空笔记编辑列表(mEditTextList)中的所有现有视图(通过removeAllViews方法), +// 然后将传入的文本内容(text参数)按换行符(\n)进行分割,得到一个字符串数组(items),每个元素代表列表中的一行文本内容, +// 初始化一个索引变量(index)为0,用于记录当前处理到的行索引,接着遍历字符串数组,对于非空的文本行(通过TextUtils.isEmpty方法判断), +// 调用getListItem方法创建对应的列表项视图,并添加到笔记编辑列表中,同时更新索引值(index++),处理完所有非空文本行后, +// 再添加一个空的列表项视图(通过调用getListItem方法传入空字符串和当前索引),用于方便用户后续继续添加新的文本行, +// 最后获取最后添加的空列表项视图中的NoteEditText控件,让其获取焦点,同时将笔记编辑文本(mNoteEditor)的可见性设置为GONE(隐藏),将笔记编辑列表(mEditTextList)的可见性设置为VISIBLE(显示), +// 完成从非列表模式到列表模式的切换以及界面显示的更新,让笔记内容以列表形式展示给用户。 +private void switchToListMode(String text) { + mEditTextList.removeAllViews(); + String[] items = text.split("\n"); + int index = 0; + for (String item : items) { + if (!TextUtils.isEmpty(item)) { + mEditTextList.addView(getListItem(item, index)); + index++; + } + } + mEditTextList.addView(getListItem("", index)); + mEditTextList.getChildAt(index).findViewById(R.id.et_edit_text).requestFocus(); + + mNoteEditor.setVisibility(View.GONE); + mEditTextList.setVisibility(View.VISIBLE); +} + +// 用于根据用户输入的查询内容(userQuery)对笔记的完整文本内容(fullText)进行高亮显示处理,返回一个处理后的SpannableString对象, +// 首先创建一个SpannableString对象,将传入的完整文本内容(如果fullText为null则初始化为空字符串)作为初始值, +// 如果用户查询内容不为空(!TextUtils.isEmpty(userQuery)),则使用Pattern.compile方法根据用户查询内容编译一个正则表达式模式(mPattern), +// 创建一个Matcher对象,通过传入完整文本内容来进行匹配操作,初始化一个起始位置变量(start)为0,然后通过循环,在文本中查找与查询内容匹配的子串(通过m.find(start)方法查找), +// 每当找到一个匹配的子串,就利用SpannableString的setSpan方法为该子串设置一个背景颜色样式(通过BackgroundColorSpan设置,颜色资源通过R.color.user_query_highlight获取), +// 设置的范围是匹配子串的起始位置(m.start())到结束位置(m.end()),设置的标志位为Spannable.SPAN_INCLUSIVE_EXCLUSIVE,表示包含起始位置但不包含结束位置的样式应用范围, +// 每次查找完一个匹配后,更新起始位置为上一次匹配的结束位置(start = m.end()),以便继续查找后续可能的匹配子串,最后返回处理好的带有高亮显示的SpannableString对象, +// 可用于在界面上展示带有高亮查询结果的笔记内容,方便用户查看匹配的文本部分。 +private Spannable getHighlightQueryResult(String fullText, String userQuery) { + SpannableString spannable = new SpannableString(fullText == null? "" : fullText); + if (!TextUtils.isEmpty(userQuery)) { + mPattern = Pattern.compile(userQuery); + Matcher m = mPattern.matcher(fullText); + int start = 0; + while (m.find(start)) { + spannable.setSpan( + new BackgroundColorSpan(this.getResources().getColor( + R.color.user_query_highlight)), m.start(), m.end(), + Spannable.SPAN_INCLUSIVE_EXCLUSIVE); + start = m.end(); + } + } + return spannable; +} + +// 用于创建一个列表模式下的笔记编辑列表项视图,首先通过LayoutInflater从当前Activity上下文(this)中加载指定布局资源(R.layout.note_edit_list_item)创建一个视图对象(view), +// 获取该视图中的NoteEditText控件(用于显示和编辑文本内容),根据当前选择的字体大小资源ID(mFontSizeId)设置该NoteEditText控件的字体外观(通过setTextAppearance方法), +// 获取该视图中的CheckBox控件(用于标记该行文本是否已完成等状态),为其设置一个选中状态变化监听器(OnCheckedChangeListener), +// 在监听器的回调方法onCheckedChanged中,根据CheckBox的选中状态(isChecked参数)来设置NoteEditText控件的文本绘制标志位, +// 如果选中(isChecked为true),则在文本上添加删除线样式(通过设置Paint.STRIKE_THRU_TEXT_FLAG标志位),表示该项已完成等相关状态;如果未选中(isChecked为false), +// 则设置为正常的文本绘制标志位(Paint.ANTI_ALIAS_FLAG | Paint.DEV_KERN_TEXT_FLAG,表示抗锯齿和字距调整等正常绘制效果), +// 接着根据传入的文本内容(item参数)判断是否以特定的已选中标记(TAG_CHECKED)或未选中标记(TAG_UNCHECKED)开头,如果以TAG_CHECKED开头,则将CheckBox设置为选中状态, +// 并在NoteEditText控件上添加删除线样式,同时去除文本开头的标记部分(通过substring方法截取剩余文本);如果以TAG_UNCHECKED开头,则将CheckBox设置为未选中状态, +// 设置正常的文本绘制标志位,并去除文本开头的标记部分, +// 然后设置NoteEditText控件的文本变化监听器为当前Activity(实现了相关接口),设置该列表项的索引值(index参数), +// 调用getHighlightQueryResult方法获取带有高亮显示(如果有用户查询内容)的处理后的文本内容,并设置到NoteEditText控件中显示,最后返回创建好的包含各种控件和相应设置的列表项视图, +// 用于在笔记编辑列表中展示每一行的文本内容及相关状态和操作功能。 +private View getListItem(String item, int index) { + View view = LayoutInflater.from(this).inflate(R.layout.note_edit_list_item, null); + final NoteEditText edit = (NoteEditText) view.findViewById(R.id.et_edit_text); + edit.setTextAppearance(this, TextAppearanceResources.getTexAppearanceResource(mFontSizeId)); + CheckBox cb = ((CheckBox) view.findViewById(R.id.cb_edit_item)); + cb.setOnCheckedChangeListener(new OnCheckedChangeListener() { + public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { + if (isChecked) { + edit.setPaintFlags(edit.getPaintFlags() | Paint.STRIKE_THRU_TEXT_FLAG); + } else { + edit.setPaintFlags(Paint.ANTI_ALIAS_FLAG | Paint.DEV_KERN_TEXT_FLAG); + } + } + }); + + if (item.startsWith(TAG_CHECKED)) { + cb.setChecked(true); + edit.setPaintFlags(edit.getPaintFlags() | Paint.STRIKE_THRU_TEXT_FLAG); + item = item.substring(TAG_CHECKED.length(), item.length()).trim(); + } else if (item.startsWith(TAG_UNCHECKED)) { + cb.setChecked(false); + edit.setPaintFlags(Paint.ANTI_ALIAS_FLAG | Paint.DEV_KERN_TEXT_FLAG); + item = item.substring(TAG_UNCHECKED.length(), item.length()).trim(); + } + + edit.setOnTextViewChangeListener(this); + edit.setIndex(index); + edit.setText(getHighlightQueryResult(item, mUserQuery)); + return view; +} + +// 当笔记编辑列表中某一行文本的内容发生变化时调用此方法,首先进行边界检查,如果传入的索引(index)大于等于笔记编辑列表(mEditTextList)的子视图数量, +// 则记录错误日志,提示“Wrong index, should not happen”(错误的索引,不应该发生这种情况),因为索引应该在有效范围内(小于子视图数量), +// 如果索引合法,根据该行文本是否有内容(hasText参数)来设置对应的CheckBox控件(用于标记该行文本状态)的可见性, +// 如果有内容(hasText为true),则将CheckBox的可见性设置为VISIBLE(显示),方便用户标记该行文本的相关状态(如已完成等);如果没有内容(hasText为false), +// 则将CheckBox的可见性设置为GONE(隐藏),完成根据文本内容变化来更新对应状态显示控件可见性的操作。 +public void onTextChange(int index, boolean hasText) { + if (index >= mEditTextList.getChildCount()) { + Log.e(TAG, "Wrong index, should not happen"); + return; + } + if (hasText) { + mEditTextList.getChildAt(index).findViewById(R.id.cb_edit_item).setVisibility(View.VISIBLE); + } else { + mEditTextList.getChildAt(index).findViewById(R.id.cb_edit_item).setVisibility(View.GONE); + } +} + // 当笔记的列表模式(如是否为勾选列表形式)发生改变时会调用此方法,传入旧的模式(oldMode)和新的模式(newMode)参数, + // 用于根据模式变化来更新界面显示以及处理相关数据。 + public void onCheckListModeChanged(int oldMode, int newMode) { + // 如果新的模式是列表模式(TextNote.MODE_CHECK_LIST,可能是定义的表示勾选列表的常量), + // 则调用switchToListMode方法将笔记内容切换到列表模式进行显示,这里传入笔记编辑文本(mNoteEditor)中的文本内容(通过mNoteEditor.getText().toString()获取), + // 以便按照列表模式的规则进行分割和展示,例如每行文本作为列表中的一项等操作,完成界面从非列表模式到列表模式的切换显示。 + if (newMode == TextNote.MODE_CHECK_LIST) { + switchToListMode(mNoteEditor.getText().toString()); + } else { + // 如果新的模式不是列表模式,说明要从列表模式切换回普通编辑模式。 + // 首先调用getWorkingText方法获取当前有效的工作文本内容,该方法会根据笔记当前是否处于列表模式来整理文本内容(例如去除列表模式相关的标记等), + // 如果获取的工作文本为空(即!getWorkingText()返回true),则对笔记对象(mWorkingNote)的文本内容进行处理, + // 这里是将文本中以TAG_UNCHECKED + " "开头的部分替换为空字符串,可能是去除列表模式下未勾选项的特定标记,以符合普通编辑模式下文本的格式要求。 + if (!getWorkingText()) { + mWorkingNote.setWorkingText(mWorkingNote.getContent().replace(TAG_UNCHECKED + " ", + "")); + } + // 获取带有高亮显示查询结果的笔记内容(通过调用getHighlightQueryResult方法,传入笔记的内容(mWorkingNote.getContent())和用户查询内容(mUserQuery)进行处理), + // 并将其设置到笔记编辑文本(mNoteEditor)中显示,用于在普通编辑模式下展示给用户查看和编辑,同时将笔记编辑列表(mEditTextList)的可见性设置为GONE(隐藏), + // 因为切换到普通编辑模式不需要显示列表形式的界面了,再将笔记编辑文本(mNoteEditor)的可见性设置为VISIBLE(显示),完成从列表模式到普通编辑模式的切换以及界面显示的更新操作。 + mNoteEditor.setText(getHighlightQueryResult(mWorkingNote.getContent(), mUserQuery)); + mEditTextList.setVisibility(View.GONE); + mNoteEditor.setVisibility(View.VISIBLE); + } + } + + // 用于获取当前笔记的工作文本内容,同时返回一个布尔值表示是否存在已勾选的项(用于一些相关逻辑判断)。 + // 如果笔记当前处于列表模式(mWorkingNote.getCheckListMode() == TextNote.MODE_CHECK_LIST),则创建一个StringBuilder对象用于拼接文本内容, + // 通过循环遍历笔记编辑列表(mEditTextList)中的每一个子视图(即每一行文本对应的视图),获取其中的NoteEditText控件(用于显示和编辑文本的控件), + // 判断该控件中的文本是否为空(通过TextUtils.isEmpty方法判断),如果不为空,再获取该行对应的CheckBox控件(用于标记是否勾选状态), + // 如果CheckBox被勾选(即isChecked为true),则按照特定格式(先添加TAG_CHECKED标记,再添加文本内容和换行符)将文本添加到StringBuilder中,并将hasChecked标志设为true,表示存在已勾选项; + // 如果CheckBox未勾选(即isChecked为false),则按照另一种特定格式(先添加TAG_UNCHECKED标记,再添加文本内容和换行符)将文本添加到StringBuilder中, + // 循环结束后,将StringBuilder中拼接好的内容设置为笔记对象(mWorkingNote)的工作文本内容(通过mWorkingNote.setWorkingText方法),并返回hasChecked标志值,用于告知调用者是否存在已勾选项。 + // 如果笔记不是处于列表模式,则直接将笔记编辑文本(mNoteEditor)中的内容设置为笔记对象的工作文本内容(通过mWorkingNote.setWorkingText方法),并返回false,表示不存在列表模式下的勾选情况, + // 完成根据笔记不同模式获取相应工作文本内容以及判断是否有勾选项的操作。 + private boolean getWorkingText() { + boolean hasChecked = false; + if (mWorkingNote.getCheckListMode() == TextNote.MODE_CHECK_LIST) { + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < mEditTextList.getChildCount(); i++) { + View view = mEditTextList.getChildAt(i); + NoteEditText edit = (NoteEditText) view.findViewById(R.id.et_edit_text); + if (!TextUtils.isEmpty(edit.getText())) { + if (((CheckBox) view.findViewById(R.id.cb_edit_item)).isChecked()) { + sb.append(TAG_CHECKED).append(" ").append(edit.getText()).append("\n"); + hasChecked = true; + } else { + sb.append(TAG_UNCHECKED).append(" ").append(edit.getText()).append("\n"); + } + } + } + mWorkingNote.setWorkingText(sb.toString()); + } else { + mWorkingNote.setWorkingText(mNoteEditor.getText().toString()); + } + return hasChecked; + } + + // 用于保存当前正在编辑的笔记,首先调用getWorkingText方法获取当前笔记的有效工作文本内容(可能涉及对列表模式下文本的整理等操作), + // 然后调用笔记对象(mWorkingNote)的saveNote方法来实际执行保存笔记的操作,该方法内部可能涉及将笔记数据持久化到数据库等相关逻辑, + // 如果保存操作成功(即saved为true),则设置当前Activity的结果码为RESULT_OK,这里的RESULT_OK(可能是定义的一个表示成功的常量)主要用于区分不同的操作结果状态, + // 例如从列表视图进入编辑视图,打开已有笔记和新建/编辑笔记这两种情况,通过这个结果码方便后续(比如在调用startActivityForResult启动的Activity返回时)判断操作是属于哪种情况, + // 最后返回保存操作的结果(true表示保存成功,false表示保存失败),完成笔记保存以及相关结果设置的操作流程。 + private boolean saveNote() { + getWorkingText(); + boolean saved = mWorkingNote.saveNote(); + if (saved) { + /** + * There are two modes from List view to edit view, open one note, + * create/edit a node. Opening node requires to the original + * position in the list when back from edit view, while creating a + * new node requires to the top of the list. This code + * {@link #RESULT_OK} is used to identify the create/edit state + */ + setResult(RESULT_OK); + } + return saved; + } + + // 用于将当前正在编辑的笔记发送到桌面(可能是创建一个快捷方式到桌面以便快速访问笔记), + // 首先进行一个前置判断,检查笔记是否已经存在于数据库中(mWorkingNote.existInDatabase()),如果不存在(即新创建还未保存的笔记),则先调用saveNote方法保存笔记, + // 确保有对应的数据库记录后才能进行后续发送到桌面的操作。 + // 如果笔记的唯一标识符(mWorkingNote.getNoteId())大于0,表示笔记已保存且有有效的唯一标识,接下来创建一个Intent对象(sender)用于发送广播, + // 再创建一个Intent对象(shortcutIntent)用于指定点击快捷方式后要启动的Activity及相关操作,这里设置动作为Intent.ACTION_VIEW,表示查看操作, + // 并将笔记的唯一标识符(通过Intent.EXTRA_UID键值对添加)作为额外信息传递,以便在启动NoteEditActivity时能准确加载对应的笔记内容, + // 然后将shortcutIntent作为额外信息添加到sender Intent中(通过Intent.EXTRA_SHORTCUT_INTENT键值对添加), + // 接着通过调用makeShortcutIconTitle方法根据笔记内容生成快捷方式在桌面显示的图标标题(去除一些不必要的标记等),并将其添加到sender Intent中(通过Intent.EXTRA_SHORTCUT_NAME键值对添加), + // 再设置快捷方式的图标资源(通过Intent.EXTRA_SHORTCUT_ICON_RESOURCE键值对添加,这里从当前Activity上下文获取R.drawable.icon_app作为图标资源), + // 设置一个表示允许重复创建快捷方式的标志(duplicate为true),设置sender Intent的动作为"com.android.launcher.action.INSTALL_SHORTCUT",表示安装快捷方式到桌面的操作, + // 最后通过showToast方法弹出一个提示信息(显示固定的提示文本R.string.info_note_enter_desktop,表示笔记快捷方式已发送到桌面的提示信息),并发送广播(通过sendBroadcast方法)来触发系统创建快捷方式到桌面的操作, + // 如果笔记的唯一标识符不大于0(表示可能是新创建还未保存或者不值得保存的笔记,没有有效的唯一标识符),则记录错误日志,提示“Send to desktop error”(发送到桌面错误), + // 并通过showToast方法弹出一个Toast提示框,显示固定的错误提示文本(R.string.error_note_empty_for_send_to_desktop),告知用户需要输入一些内容才能发送到桌面,避免无效的操作。 + private void sendToDesktop() { + /** + * Before send message to home, we should make sure that current + * editing note is exists in databases. So, for new note, firstly + * save it + */ + if (!mWorkingNote.existInDatabase()) { + saveNote(); + } + + if (mWorkingNote.getNoteId() > 0) { + Intent sender = new Intent(); + Intent shortcutIntent = new Intent(this, NoteEditActivity.class); + shortcutIntent.setAction(Intent.ACTION_VIEW); + shortcutIntent.putExtra(Intent.EXTRA_UID, mWorkingNote.getNoteId()); + sender.putExtra(Intent.EXTRA_SHORTCUT_INTENT, shortcutIntent); + sender.putExtra(Intent.EXTRA_SHORTCUT_NAME, + makeShortcutIconTitle(mWorkingNote.getContent())); + sender.putExtra(Intent.EXTRA_SHORTCUT_ICON_RESOURCE, + Intent.ShortcutIconResource.fromContext(this, R.drawable.icon_app)); + sender.putExtra("duplicate", true); + sender.setAction("com.android.launcher.action.INSTALL_SHORTCUT"); + showToast(R.string.info_note_enter_desktop); + sendBroadcast(sender); + } else { + /** + * There is the condition that user has input nothing (the note is + * not worthy saving), we have no note id, remind the user that he + * should input something + */ + Log.e(TAG, "Send to desktop error"); + showToast(R.string.error_note_empty_for_send_to_desktop); + } + } + + // 用于生成快捷方式在桌面显示的图标标题,首先对传入的笔记内容(content)进行处理,去除其中的已勾选标记(TAG_CHECKED)和未勾选标记(TAG_UNCHECKED), + // 然后判断处理后的内容长度是否大于预设的最大图标标题长度(SHORTCUT_ICON_TITLE_MAX_LEN),如果大于,则截取内容的前SHORTCUT_ICON_TITLE_MAX_LEN个字符作为图标标题返回; + // 如果不大于,则直接返回处理后的内容作为图标标题,通过这种方式生成一个合适长度且去除不必要标记的图标标题,用于在桌面快捷方式上显示,让图标标题更简洁明了。 + private String makeShortcutIconTitle(String content) { + content = content.replace(TAG_CHECKED, ""); + content = content.replace(TAG_UNCHECKED, ""); + return content.length() > SHORTCUT_ICON_TITLE_MAX_LEN? content.substring(0, + SHORTCUT_ICON_TITLE_MAX_LEN) : content; + } + + // 用于显示一个简单的Toast提示信息,默认显示时长为短时间(Toast.LENGTH_SHORT),传入要显示的提示信息资源ID(resId), + // 通过调用Toast.makeText方法创建一个Toast对象,并显示出来,方便给用户提供一些简短的提示信息,例如操作成功、失败等提示内容。 + private void showToast(int resId) { + showToast(resId, Toast.LENGTH_SHORT); + } + + // 用于显示一个指定时长的Toast提示信息,传入要显示的提示信息资源ID(resId)和显示时长(duration,单位为毫秒,可通过Toast类中定义的常量如Toast.LENGTH_SHORT、Toast.LENGTH_LONG等来选择常用时长), + // 通过调用Toast.makeText方法创建一个Toast对象,并设置显示时长,然后显示出来,用于更灵活地给用户提供不同时长的提示信息,满足不同场景下的提示需求。 + private void showToast(int resId, int duration) { + Toast.makeText(this, resId, duration).show(); + } +} + + + +十五、NotesListActivity.java + + +package net.micode.notes.ui; + +import android.app.Activity; +import android.app.AlertDialog; +import android.app.Dialog; +import android.appwidget.AppWidgetManager; +import android.content.AsyncQueryHandler; +import android.content.ContentResolver; +import android.content.ContentValues; +import android.content.Context; +import android.content.DialogInterface; +import android.content.Intent; +import android.content.SharedPreferences; +import android.database.Cursor; +import android.os.AsyncTask; +import android.os.Bundle; +import android.preference.PreferenceManager; +import android.text.Editable; +import android.text.TextUtils; +import android.text.TextWatcher; +import android.util.Log; +import android.view.ActionMode; +import android.view.ContextMenu; +import android.view.ContextMenu.ContextMenuInfo; +import android.view.Display; +import android.view.HapticFeedbackConstants; +import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuItem; +import android.view.MenuItem.OnMenuItemClickListener; +import android.view.MotionEvent; +import android.view.View; +import android.view.View.OnClickListener; +import android.view.View.OnCreateContextMenuListener; +import android.view.View.OnTouchListener; +import android.view.inputmethod.InputMethodManager; +import android.widget.AdapterView; +import android.widget.AdapterView.OnItemClickListener; +import android.widget.AdapterView.OnItemLongClickListener; +import android.widget.Button; +import android.widget.EditText; +import android.widget.ListView; +import android.widget.PopupMenu; +import android.widget.TextView; +import android.widget.Toast; + +import net.micode.notes.R; +import net.micode.notes.data.Notes; +import net.micode.notes.data.Notes.NoteColumns; +import net.micode.notes.gtask.remote.GTaskSyncService; +import net.micode.notes.model.WorkingNote; +import net.micode.notes.tool.BackupUtils; +import net.micode.notes.tool.DataUtils; +import net.micode.notes.tool.ResourceParser; +import net.micode.notes.ui.NotesListAdapter.AppWidgetAttribute; +import net.micode.notes.widget.NoteWidgetProvider_2x; +import net.micode.notes.widget.NoteWidgetProvider_4x; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.util.HashSet; + +// 继承自Activity,表示这是一个安卓的Activity类,实现了OnClickListener和OnItemLongClickListener接口, +// 用于处理点击事件和长按事件。 +public class NotesListActivity extends Activity implements OnClickListener, OnItemLongClickListener { + // 用于标识查询文件夹下笔记列表的请求令牌,可能在异步查询操作中用于区分不同类型的查询任务。 + private static final int FOLDER_NOTE_LIST_QUERY_TOKEN = 0; + // 用于标识查询文件夹列表的请求令牌,同样用于异步查询中区分不同的查询操作。 + private static final int FOLDER_LIST_QUERY_TOKEN = 1; + // 菜单中删除文件夹选项对应的ID,用于在菜单操作中识别用户选择的是删除文件夹功能。 + private static final int MENU_FOLDER_DELETE = 0; + // 菜单中查看文件夹选项对应的ID,用于识别查看文件夹的操作选择。 + private static final int MENU_FOLDER_VIEW = 1; + // 菜单中修改文件夹名称选项对应的ID,用于识别修改文件夹名称的操作选择。 + private static final int MENU_FOLDER_CHANGE_NAME = 2; + // 用于存储是否添加应用介绍的偏好设置的键值,通过这个键可以在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; + // 用于处理后台异步查询操作的类实例,通过传入ContentResolver来进行数据库相关的异步查询工作,比如查询笔记数据等。 + private BackgroundQueryHandler mBackgroundQueryHandler; + // 笔记列表的适配器,用于将数据适配到ListView上显示,管理笔记列表的数据展示和交互逻辑等。 + private NotesListAdapter mNotesListAdapter; + // 显示笔记列表的ListView控件,用于展示笔记条目的列表界面。 + private ListView mNotesListView; + // “新建笔记”按钮,用户点击它可以创建新的笔记。 + private Button mAddNewNote; + // 用于标记是否分发触摸事件的布尔变量,在处理触摸相关逻辑时使用,判断是否需要将触摸事件传递给其他控件等情况。 + private boolean mDispatch; + // 记录触摸事件起始的Y坐标,用于在触摸事件处理过程中计算位置变化等情况。 + private int mOriginY; + // 记录分发触摸事件时的Y坐标,配合mOriginY来处理触摸事件的位置调整等操作。 + private int mDispatchY; + // 顶部标题栏的TextView控件,用于显示当前所在文件夹等相关的标题信息。 + private TextView mTitleBar; + // 当前所在文件夹的ID,用于标识和操作对应的文件夹数据,初始化为根文件夹的ID(Notes.ID_ROOT_FOLDER)。 + private long mCurrentFolderId; + // 用于操作内容提供器(Content Provider)的实例,通过它可以与安卓系统中的数据存储(如数据库)进行交互,比如查询、插入、更新等操作。 + private ContentResolver mContentResolver; + // 实现了ListView.MultiChoiceModeListener和OnMenuItemClickListener接口的实例,用于处理列表的多选模式相关操作和菜单项点击事件。 + private ModeCallback mModeCallBack; + + // 用于日志记录的标签字符串,方便在Log输出中识别是这个类中输出的日志信息。 + private static final String TAG = "NotesListActivity"; + // 定义笔记列表视图滚动的速率,可能用于控制列表滚动的速度等相关逻辑(具体看代码中何处使用该常量)。 + public static final int NOTES_LISTVIEW_SCROLL_RATE = 30; + // 用于存储长按笔记条目时对应的笔记数据对象,方便后续根据这个数据进行相关操作(比如打开笔记、获取笔记详情等)。 + private NoteItemData mFocusNoteDataItem; + + // 定义一个普通的查询条件字符串,用于查询指定父ID的笔记,通过占位符“?”来动态传入具体的父ID值,方便构建数据库查询语句。 + private static final String NORMAL_SELECTION = NoteColumns.PARENT_ID + "=?"; + // 定义查询根文件夹下笔记的条件字符串,比较复杂,包括了排除系统类型笔记以及筛选通话记录文件夹(如果有笔记数量大于0等条件),同样使用占位符来动态传入相关参数构建查询语句。 + 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)"; + + // 用于标识打开已有笔记的请求码,在startActivityForResult启动相关Activity后,通过这个请求码来识别返回结果对应的操作是打开笔记。 + private final static int REQUEST_CODE_OPEN_NODE = 102; + // 用于标识新建笔记的请求码,同样用于在startActivityForResult后识别返回结果对应的是新建笔记操作。 + private final static int REQUEST_CODE_NEW_NODE = 103; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + // 设置Activity的布局文件,这里加载的是R.layout.note_list布局,用于展示笔记列表相关的界面元素。 + setContentView(R.layout.note_list); + // 初始化各种资源,比如获取ContentResolver、创建后台查询处理器、设置ListView的相关监听器、初始化适配器等操作,将在下面的initResources方法中具体实现。 + initResources(); + + /** + * Insert an introduction when user firstly use this application + */ + // 当用户首次使用应用时插入应用介绍信息,具体逻辑在setAppInfoFromRawRes方法中实现。 + setAppInfoFromRawRes(); + } + + @Override + protected void onActivityResult(int requestCode, int resultCode, Intent data) { + // 如果返回结果码是RESULT_OK,并且请求码是打开笔记(REQUEST_CODE_OPEN_NODE)或者新建笔记(REQUEST_CODE_NEW_NODE), + // 则调用笔记列表适配器的changeCursor方法传入null,可能用于重置适配器的数据(比如重新加载数据等,具体看适配器的实现逻辑)。 + if (resultCode == RESULT_OK + && (requestCode == REQUEST_CODE_OPEN_NODE || requestCode == REQUEST_CODE_NEW_NODE)) { + mNotesListAdapter.changeCursor(null); + } else { + // 如果不满足上述条件,则调用父类的onActivityResult方法,按照默认的Activity返回结果处理逻辑进行处理。 + super.onActivityResult(requestCode, resultCode, data); + } + } + + // 用于从原始资源文件中读取应用介绍信息,并创建一个笔记保存起来,同时设置对应的偏好设置表示已添加过介绍,方便下次启动应用不再重复添加。 + private void setAppInfoFromRawRes() { + // 获取默认的SharedPreferences实例,用于读取和存储应用的偏好设置数据。 + SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(this); + // 如果偏好设置中获取的是否添加介绍的布尔值为false,表示还未添加过介绍信息,执行下面的添加操作。 + if (!sp.getBoolean(PREFERENCE_ADD_INTRODUCTION, false)) { + StringBuilder sb = new StringBuilder(); + InputStream in = null; + try { + // 从应用资源中打开名为R.raw.introduction的原始资源文件,该文件可能包含应用介绍的文本内容。 + in = getResources().openRawResource(R.raw.introduction); + if (in!= null) { + // 创建一个InputStreamReader,用于将字节流转换为字符流,方便按字符读取文件内容。 + InputStreamReader isr = new InputStreamReader(in); + // 创建一个BufferedReader,用于更高效地读取字符流,提供缓冲功能。 + BufferedReader br = new BufferedReader(isr); + char[] buf = new char[1024]; + int len = 0; + // 循环读取文件内容,每次读取buf.length个字符到buf数组中,直到读取的长度小于等于0表示文件读取完毕, + // 将读取到的字符添加到StringBuilder中拼接起来。 + 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 { + // 在无论是否发生异常的情况下,都尝试关闭输入流,释放资源,避免内存泄漏等问题。 + if (in!= null) { + try { + in.close(); + } catch (IOException e) { + e.printStackTrace(); + } + } + } + + // 创建一个空的WorkingNote对象,传入当前Activity上下文、根文件夹ID、无效的AppWidget ID、无效的Widget类型以及一个颜色资源(可能用于笔记显示相关的颜色设置,具体看WorkingNote类实现)。 + WorkingNote note = WorkingNote.createEmptyNote(this, Notes.ID_ROOT_FOLDER, + AppWidgetManager.INVALID_APPWIDGET_ID, Notes.TYPE_WIDGET_INVALIDE, + ResourceParser.RED); + // 将前面拼接好的介绍文本内容设置为笔记的工作文本内容。 + note.setWorkingText(sb.toString()); + // 调用笔记的saveNote方法保存笔记,如果保存成功,则在SharedPreferences中编辑并设置添加介绍的布尔值为true,表示已添加过介绍,然后提交修改。 + if (note.saveNote()) { + sp.edit().putBoolean(PREFERENCE_ADD_INTRODUCTION, true).commit(); + } else { + // 如果保存笔记失败,记录错误日志并返回,结束添加介绍笔记的操作。 + Log.e(TAG, "Save introduction note error"); + return; + } + } + } + + @Override + protected void onStart() { + super.onStart(); + // 启动异步查询笔记列表的操作,具体查询条件等逻辑在startAsyncNotesListQuery方法中实现,用于在Activity启动时加载笔记数据显示在列表中。 + startAsyncNotesListQuery(); + } + + // 初始化各种资源,包括获取ContentResolver、创建后台查询处理器、设置ListView的相关监听器、初始化适配器、设置按钮的点击和触摸监听器等操作。 + private void initResources() { + // 获取当前Activity的ContentResolver实例,用于后续与内容提供器交互操作,比如查询数据库中的笔记数据等。 + mContentResolver = this.getContentResolver(); + // 创建BackgroundQueryHandler实例,传入ContentResolver,用于处理后台的异步查询任务,比如从数据库获取笔记列表数据等操作。 + mBackgroundQueryHandler = new BackgroundQueryHandler(this.getContentResolver()); + // 将当前所在文件夹ID初始化为根文件夹的ID,通常表示从根文件夹开始展示笔记列表等操作。 + mCurrentFolderId = Notes.ID_ROOT_FOLDER; + // 通过findViewById方法找到布局文件中ID为notes_list的ListView控件,用于展示笔记列表。 + mNotesListView = (ListView) findViewById(R.id.notes_list); + // 给ListView添加一个底部视图,通过LayoutInflater从当前Activity上下文加载R.layout.note_list_footer布局文件创建视图, + // 第三个参数false表示这个底部视图不是可选择的(具体根据ListView添加底部视图的逻辑来理解)。 + mNotesListView.addFooterView(LayoutInflater.from(this).inflate(R.layout.note_list_footer, null), + null, false); + // 设置ListView的点击事件监听器为OnListItemClickListener实例,用于处理列表项被点击时的操作逻辑,具体在OnListItemClickListener类中实现。 + mNotesListView.setOnItemClickListener(new OnListItemClickListener()); + // 设置ListView的长按事件监听器为当前Activity(因为当前Activity实现了OnItemLongClickListener接口),用于处理列表项被长按的操作逻辑,比如弹出菜单等操作。 + mNotesListView.setOnItemLongClickListener(this); + // 创建NotesListAdapter实例,传入当前Activity上下文,用于将笔记数据适配到ListView上显示,管理数据展示和交互等逻辑。 + mNotesListAdapter = new NotesListAdapter(this); + // 将创建好的笔记列表适配器设置给ListView,使得ListView能够根据适配器提供的数据进行展示。 + mNotesListView.setAdapter(mNotesListAdapter); + // 通过findViewById方法找到布局文件中ID为btn_new_note的按钮,即“新建笔记”按钮,用于用户点击创建新笔记。 + mAddNewNote = (Button) findViewById(R.id.btn_new_note); + // 设置“新建笔记”按钮的点击事件监听器为当前Activity(因为实现了OnClickListener接口),用于处理按钮被点击时的操作逻辑,比如跳转到新建笔记的界面等,具体在onClick方法中实现。 + mAddNewNote.setOnClickListener(this); + // 设置“新建笔记”按钮的触摸事件监听器为NewNoteOnTouchListener实例,用于处理按钮的触摸相关操作逻辑,比如根据触摸位置判断是否分发触摸事件等,具体在NewNoteOnTouchListener类中实现。 + mAddNewNote.setOnTouchListener(new NewNoteOnTouchListener()); + // 将触摸事件分发标记初始化为false,表示默认不进行触摸事件分发操作。 + mDispatch = false; + // 将分发触摸事件的Y坐标初始化为0。 + mDispatchY = 0; + // 将触摸事件起始的Y坐标初始化为0。 + mOriginY = 0; + // 通过findViewById方法找到布局文件中ID为tv_title_bar的TextView控件,即顶部标题栏,用于显示当前所在文件夹等相关标题信息。 + mTitleBar = (TextView) findViewById(R.id.tv_title_bar); + // 将列表的编辑状态初始化为NOTE_LIST,表示默认处于普通笔记列表状态。 + mState = ListEditState.NOTE_LIST; + // 创建ModeCallback实例,用于处理列表的多选模式相关操作和菜单项点击事件,具体在ModeCallback类中实现相关逻辑。 + mModeCallBack = new ModeCallback(); + } + + // 内部类,实现了ListView.MultiChoiceModeListener和OnMenuItemClickListener接口,用于处理列表的多选模式相关操作(如创建多选模式、处理菜单项点击等)以及自定义的菜单点击逻辑。 + private class ModeCallback implements ListView.MultiChoiceModeListener, OnMenuItemClickListener { + // 用于显示下拉菜单的实例,可能用于在多选模式下展示一些额外的操作选项(比如全选、反选等)。 + private DropdownMenu mDropDownMenu; + // 当前的ActionMode实例,代表多选模式的操作状态,通过它可以进行一些与多选模式相关的设置和操作,比如设置自定义视图等。 + private ActionMode mActionMode; + // 菜单中的“移动”菜单项,用于在多选模式下将选中的笔记移动到其他文件夹等操作,根据不同条件可能设置为可见或不可见状态。 + private MenuItem mMoveMenu; + + // 当多选模式被创建时会调用此方法,用于初始化多选模式下的相关界面、设置菜单项点击监听器以及进行一些界面元素的显示隐藏等操作。 + public boolean onCreateActionMode(ActionMode mode, Menu menu) { + // 通过菜单填充器(MenuInflater)加载名为R.menu.note_list_options的菜单布局文件到传入的菜单(menu)对象中, + // 这样就将预定义的菜单布局展示在多选模式的操作菜单里了,这个布局文件中定义了如删除、移动等菜单项。 + getMenuInflater().inflate(R.menu.note_list_options, menu); + // 在加载好的菜单中找到ID为R.id.delete的菜单项,并设置它的点击监听器为当前的ModeCallback实例(因为实现了OnMenuItemClickListener接口), + // 以便后续点击删除菜单项时执行相应的删除操作逻辑(具体逻辑在onMenuItemClick方法中处理)。 + menu.findItem(R.id.delete).setOnMenuItemClickListener(this); + // 获取菜单中ID为R.id.move的菜单项,用于后续根据条件判断其是否可见以及设置点击监听器等操作,这个菜单项可能用于移动选中的笔记到其他文件夹等功能。 + mMoveMenu = menu.findItem(R.id.move); + // 判断长按选中的笔记数据对应的父文件夹ID是否为通话记录文件夹(Notes.ID_CALL_RECORD_FOLDER)或者用户创建的文件夹数量是否为0, + // 如果满足这两个条件之一,表示可能不具备移动笔记的条件(比如没有可移动的目标文件夹等情况),则将“移动”菜单项设置为不可见。 + if (mFocusNoteDataItem.getParentId() == Notes.ID_CALL_RECORD_FOLDER + || DataUtils.getUserFolderCount(mContentResolver) == 0) { + mMoveMenu.setVisible(false); + } else { + // 如果不满足上述不可见的条件,说明可以进行移动操作,则将“移动”菜单项设置为可见,并同样设置它的点击监听器为当前的ModeCallback实例, + // 以便点击时执行相应的移动操作逻辑(具体在onMenuItemClick方法中处理移动相关逻辑)。 + mMoveMenu.setVisible(true); + mMoveMenu.setOnMenuItemClickListener(this); + } + // 将传入的ActionMode对象赋值给成员变量mActionMode,方便后续在其他方法中使用这个代表多选模式状态的对象进行相关操作,比如结束多选模式等。 + mActionMode = mode; + // 调用笔记列表适配器(mNotesListAdapter)的setChoiceMode方法传入true,告知适配器进入多选模式,适配器内部会根据这个状态来处理列表项的选中状态显示等相关逻辑。 + mNotesListAdapter.setChoiceMode(true); + // 将ListView的长按功能设置为不可用,因为已经进入了多选模式,长按操作可能会和多选模式的操作逻辑冲突或者重复,所以在这里禁用掉长按默认的操作逻辑。 + mNotesListView.setLongClickable(false); + // 将“新建笔记”按钮(mAddNewNote)的可见性设置为GONE(即隐藏),在多选模式下通常不需要显示新建笔记按钮,避免用户在多选操作过程中误点击新建操作。 + + mAddNewNote.setVisibility(View.GONE); + + // 通过LayoutInflater从当前的NotesListActivity上下文加载名为R.layout.note_list_dropdown_menu的布局文件创建一个自定义视图(customView), + // 这个视图可能用于在多选模式下展示一些额外的操作选项或者信息,比如全选、反选等功能对应的界面元素。 + View customView = LayoutInflater.from(NotesListActivity.this).inflate( + R.layout.note_list_dropdown_menu, null); + // 将创建好的自定义视图设置给当前的ActionMode,作为多选模式下的自定义显示内容,这样就可以展示自定义的操作界面给用户了。 + mode.setCustomView(customView); + // 创建一个DropdownMenu实例,传入当前Activity上下文、自定义视图中ID为R.id.selection_menu的按钮(可能作为触发下拉菜单显示的按钮)以及名为R.menu.note_list_dropdown的菜单资源, + // 用于创建一个下拉菜单对象,方便后续处理下拉菜单相关的操作逻辑,比如点击菜单项后的操作等。 + mDropDownMenu = new DropdownMenu(NotesListActivity.this, + (Button) customView.findViewById(R.id.selection_menu), + R.menu.note_list_dropdown); + // 设置下拉菜单的菜单项点击监听器,当点击下拉菜单中的菜单项时会触发这个匿名内部类实现的onMenuItemClick方法, + // 这里的逻辑是根据当前是否全选来切换全选/反选状态,然后调用updateMenu方法更新菜单显示状态(比如更新全选按钮的文字、勾选状态等)。 + mDropDownMenu.setOnDropdownMenuItemClickListener(new PopupMenu.OnMenuItemClickListener(){ + public boolean onMenuItemClick(MenuItem item) { + mNotesListAdapter.selectAll(!mNotesListAdapter.isAllSelected()); + updateMenu(); + return true; + } + + }); + // 返回true表示成功创建了多选模式的操作界面和相关设置,如果返回false则可能会导致多选模式创建失败或者不正常显示等情况。 + return true; + } + + // 用于更新下拉菜单的显示状态,根据当前选中的笔记数量以及全选状态等信息来调整菜单的标题、菜单项的勾选状态和文字显示等内容。 + private void updateMenu() { + // 获取笔记列表适配器中当前被选中的笔记数量,通过这个数量来动态调整菜单的相关显示信息,比如在标题中显示选中的数量等。 + int selectedCount = mNotesListAdapter.getSelectedCount(); + // 从资源文件中获取一个格式化字符串,这个字符串用于作为下拉菜单的标题,其中包含一个占位符,会将selectedCount的值替换进去, + // 用于向用户展示当前选中了多少个笔记,格式可能类似"已选中:X个"这样的文本信息(具体看资源文件中定义的格式)。 + String format = getResources().getString(R.string.menu_select_title, selectedCount); + // 将获取到的格式化后的标题字符串设置给下拉菜单(mDropDownMenu),使得菜单标题能实时反映当前选中笔记的数量情况。 + mDropDownMenu.setTitle(format); + // 在下拉菜单中查找ID为R.id.action_select_all的菜单项,这个菜单项可能就是用于全选/反选操作的按钮,用于后续根据全选状态来调整它的勾选状态和文字显示内容。 + MenuItem item = mDropDownMenu.findItem(R.id.action_select_all); + if (item!= null) { + // 判断笔记列表适配器中的所有笔记是否都已被选中,如果是,则将这个全选菜单项设置为勾选状态,并将其文字显示设置为反选相关的文字(比如"取消全选",具体看资源文件中定义的R.string.menu_deselect_all内容), + // 用于提示用户当前操作可以取消全选了。 + if (mNotesListAdapter.isAllSelected()) { + item.setChecked(true); + item.setTitle(R.string.menu_deselect_all); + } else { + // 如果不是全选状态,则将全选菜单项的勾选状态取消,并将其文字显示设置为全选相关的文字(比如"全选",具体看资源文件中定义的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,告知适配器退出多选模式,适配器内部会相应地更新列表项的显示状态等,恢复到非多选模式的外观。 + mNotesListAdapter.setChoiceMode(false); + // 将ListView的长按功能重新设置为可用,因为多选模式已经结束了,需要恢复长按操作的默认功能,以便用户后续可以再次通过长按进行相关操作(比如弹出菜单等)。 + mNotesListView.setLongClickable(true); + // 将“新建笔记”按钮(mAddNewNote)的可见性设置为VISIBLE(即显示),恢复到正常界面下按钮可见的状态,方便用户继续点击新建笔记。 + mAddNewNote.setVisibility(View.VISIBLE); + } + // 用于手动结束当前的多选模式,它内部通过调用mActionMode的finish方法来实现,通常可以在一些特定的业务逻辑执行完后,主动调用这个方法来关闭多选模式界面。 + public void finishActionMode() { + mActionMode.finish(); + } + + // 当在多选模式下列表项的选中状态发生改变时(比如用户点击列表项进行选中或取消选中操作)会调用此方法, + // 用于更新笔记列表适配器中对应项的选中状态,并调用updateMenu方法来更新下拉菜单的显示状态,以实时反映选中情况的变化。 + public void onItemCheckedStateChanged(ActionMode mode, int position, long id, + boolean checked) { + mNotesListAdapter.setCheckedItem(position, checked); + updateMenu(); + } + // 处理菜单项点击事件的方法,根据点击的菜单项ID来执行不同的业务逻辑,比如点击删除菜单项会弹出确认删除对话框,点击移动菜单项会进行查询目标文件夹等操作。 + public boolean onMenuItemClick(MenuItem item) { + // 如果笔记列表适配器中当前选中的笔记数量为0,说明用户没有选中任何笔记却点击了菜单项,这种情况下弹出一个Toast提示用户需要先选中笔记, + // 然后返回true表示已经处理了这个点击事件(虽然是提示用户错误操作,但也算一种事件处理结果),避免后续其他默认的未处理逻辑执行。 + if (mNotesListAdapter.getSelectedCount() == 0) { + Toast.makeText(NotesListActivity.this, getString(R.string.menu_select_none), + Toast.LENGTH_SHORT).show(); + return true; + } + + // 根据点击的菜单项的ID进行不同的操作逻辑分支处理。 + switch (item.getItemId()) { + case R.id.delete: + // 创建一个AlertDialog的构建器(AlertDialog.Builder)实例,用于构建一个确认删除的对话框,这个对话框会提示用户要删除的笔记数量等信息,让用户确认是否删除。 + AlertDialog.Builder builder = new AlertDialog.Builder(NotesListActivity.this); + // 设置对话框的标题,从资源文件中获取对应的字符串作为标题文本,这里的标题可能是类似"确认删除"这样提示用户操作意图的文字(具体看资源文件中定义的R.string.alert_title_delete内容)。 + builder.setTitle(getString(R.string.alert_title_delete)); + // 设置对话框的图标为安卓系统自带的警告图标(android.R.drawable.ic_dialog_alert),用于直观地提示用户这是一个重要的操作(删除操作),可能会有数据丢失风险等情况。 + builder.setIcon(android.R.drawable.ic_dialog_alert); + // 设置对话框的消息内容,从资源文件中获取对应的字符串,并通过占位符将当前选中的笔记数量替换进去,向用户展示具体要删除多少个笔记的提示信息, + // 格式可能类似"确定要删除X个笔记吗?"(具体看资源文件中定义的R.string.alert_message_delete_notes内容以及如何使用占位符)。 + builder.setMessage(getString(R.string.alert_message_delete_notes, + mNotesListAdapter.getSelectedCount())); + // 设置对话框的确认按钮(通常是"确定"或"是"之类的按钮),点击这个按钮会执行传入的DialogInterface.OnClickListener匿名内部类中的onClick方法, + // 这里的逻辑是调用batchDelete方法来执行批量删除选中笔记的操作,实现真正的删除功能。 + builder.setPositiveButton(android.R.string.ok, + new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, + int which) { + batchDelete(); + } + }); + // 设置对话框的取消按钮(通常是"取消"或"否"之类的按钮),点击这个按钮不执行任何额外操作(传入null表示没有具体的点击逻辑),只是关闭对话框,取消删除操作。 + builder.setNegativeButton(android.R.string.cancel, null); + // 通过构建器创建并显示对话框,让用户看到确认删除的提示信息并进行操作选择。 + builder.show(); + break; + case R.id.move: + // 如果点击的是“移动”菜单项,则调用startQueryDestinationFolders方法,这个方法可能会查询可以移动笔记到的目标文件夹信息, + // 比如弹出一个文件夹列表供用户选择等操作,具体逻辑在startQueryDestinationFolders方法中实现。 + startQueryDestinationFolders(); + break; + default: + // 如果点击的是其他未处理的菜单项ID,则返回false,表示这个点击事件没有被正确处理,可能会触发默认的未处理逻辑(如果有的话), + // 一般情况下在实际应用中应该尽量覆盖所有可能点击的菜单项ID并添加对应的处理逻辑,避免返回false的情况出现。 + return false; + } + // 如果成功处理了点击的菜单项(比如删除、移动等操作对应的菜单项点击),则返回true,表示事件已处理完成。 + return true; + } + // 内部类,实现了OnTouchListener接口,用于处理“新建笔记”按钮(mAddNewNote)的触摸事件,根据触摸动作(按下、移动、抬起等)执行不同逻辑, + // 比如判断触摸位置是否在按钮透明区域并进行相应的事件分发等操作。 + private class NewNoteOnTouchListener implements OnTouchListener { + + // 当触摸事件发生在关联的视图(即“新建笔记”按钮)上时,此方法会被调用,根据传入的触摸事件(MotionEvent)的不同动作类型来执行相应的逻辑处理。 + public boolean onTouch(View v, MotionEvent event) { + switch (event.getAction()) { + // 当触摸动作是按下(ACTION_DOWN)时执行以下逻辑,主要用于初始化一些触摸相关的参数,并判断是否需要将触摸事件分发给ListView。 + case MotionEvent.ACTION_DOWN: { + // 获取当前窗口的默认显示对象,用于获取屏幕相关的尺寸信息,比如屏幕的高度等,后续可基于这些信息来确定触摸位置在屏幕中的相对位置。 + Display display = getWindowManager().getDefaultDisplay(); + // 通过获取到的显示对象获取屏幕的高度,单位通常是像素,该值将用于后续计算触摸点相对于屏幕以及按钮等的位置坐标。 + int screenHeight = display.getHeight(); + // 获取“新建笔记”按钮自身的高度,同样单位为像素,用于计算触摸位置与按钮边界等的相对关系,例如判断触摸是否在按钮的特定区域(如透明区域)内。 + int newNoteViewHeight = mAddNewNote.getHeight(); + // 计算一个起始坐标位置,这里是用屏幕高度减去按钮高度,得到的是按钮在屏幕底部时,触摸起始位置相对于屏幕顶部的坐标值, + // 例如屏幕高度为1000像素,按钮高度为100像素,那start的值就是900像素,表示从屏幕底部往上900像素处开始衡量触摸的Y坐标位置。 + int start = screenHeight - newNoteViewHeight; + // 根据触摸事件的Y坐标(这个坐标是相对于按钮左上角的相对坐标),加上前面计算出的起始位置坐标start,得到触摸点在整个屏幕坐标系下的Y坐标值(eventY), + // 如此就将按钮内部的触摸坐标转换为了屏幕坐标系下的坐标,便于后续与其他控件(如ListView)的位置进行比较和判断操作。 + int eventY = start + (int) event.getY(); + + /** + * Minus TitleBar's height + */ + // 如果当前列表的编辑状态是SUB_FOLDER(表示处于子文件夹状态),意味着可能存在标题栏遮挡的情况, + // 此时需要减去标题栏的高度,对触摸位置的坐标进行调整,使得坐标计算更贴合实际显示情况,准确判断触摸位置是否在期望的区域内, + // 同时也对起始位置坐标start进行同样的减去标题栏高度的操作,保证坐标体系的一致性,方便后续准确判断触摸位置与其他控件的相对关系。 + 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. + */ + // 判断触摸点的Y坐标是否小于根据特定公式(y = -0.12x + 94,这里x是触摸点的X坐标,单位均为像素,94表示按钮透明部分的最大高度)计算出的值, + // 如果小于,意味着触摸点可能在“新建笔记”按钮的透明区域内,接下来需要进一步判断是否要将触摸事件分发给ListView。 + if (event.getY() < (event.getX() * (-0.12) + 94)) { + // 从ListView中获取最后一个可见的子视图(除去底部视图的数量),这个子视图可能是列表中的某个笔记条目等,用于后续判断触摸位置与它的位置关系。 + View view = mNotesListView.getChildAt(mNotesListView.getChildCount() - 1 + - mNotesListView.getFooterViewsCount()); + // 判断获取到的视图是否不为空,并且该视图的底部坐标大于前面计算出的起始位置start,同时视图的顶部坐标小于(start + 94), + // 这一系列条件是在进一步判断触摸位置是否在按钮透明区域且与ListView的某个可见子视图有交互位置关系(比如触摸透明区域且对应到了ListView的某个条目上), + // 如果满足这些条件,则进行触摸事件分发相关的操作。 + if (view!= null && view.getBottom() > start + && (view.getTop() < (start + 94))) { + // 记录触摸事件起始的Y坐标(相对于按钮左上角的坐标),用于后续在触摸移动等操作中计算坐标变化量等情况。 + mOriginY = (int) event.getY(); + // 将前面计算出的在屏幕坐标系下的触摸点Y坐标(已经考虑了各种位置调整后的坐标)赋值给mDispatchY,用于后续触摸事件分发时设置事件的位置坐标。 + mDispatchY = eventY; + // 通过event.setLocation方法重新设置触摸事件的位置坐标,将Y坐标设置为mDispatchY,这样后续分发出去的触摸事件的位置就是经过调整后的坐标了, + // 使得ListView能基于这个准确的位置坐标来处理触摸事件,就好像触摸事件直接发生在它上面一样。 + event.setLocation(event.getX(), mDispatchY); + // 将表示是否分发触摸事件的标记mDispatch设置为true,用于后续在触摸移动、抬起等动作处理中判断是否需要继续分发触摸事件。 + mDispatch = true; + // 调用ListView的dispatchTouchEvent方法将触摸事件分发给ListView,让ListView按照自己的触摸处理逻辑来响应这个触摸事件, + // 并返回ListView对触摸事件处理的结果(如果ListView处理了该事件,返回true,否则返回false)。 + return mNotesListView.dispatchTouchEvent(event); + } + } + break; + } + // 当触摸动作是移动(ACTION_MOVE)时执行以下逻辑,主要用于在触摸移动过程中更新触摸事件的位置坐标,并继续分发给ListView进行处理(如果之前已经开始分发了)。 + case MotionEvent.ACTION_MOVE: { + // 判断是否已经开始分发触摸事件(通过mDispatch标记判断,如果为true表示之前在ACTION_DOWN阶段已经确定要分发触摸事件了), + // 如果是,则进行触摸事件位置坐标的更新以及继续分发给ListView的操作。 + if (mDispatch) { + // 根据当前触摸事件的Y坐标与起始触摸Y坐标(mOriginY)的差值,更新mDispatchY的值,用于实时调整触摸事件在ListView中的位置坐标, + // 以模拟触摸点在ListView上移动的效果,保证ListView能正确响应触摸位置变化的情况。 + mDispatchY += (int) event.getY() - mOriginY; + // 通过event.setLocation方法重新设置触摸事件的位置坐标,将Y坐标更新为mDispatchY,使得后续ListView能基于新的坐标来处理触摸移动事件。 + event.setLocation(event.getX(), mDispatchY); + // 再次调用ListView的dispatchTouchEvent方法将更新位置后的触摸事件分发给ListView,让它继续处理触摸移动的情况, + // 并返回ListView对触摸移动事件处理的结果(同样,如果处理了返回true,否则返回false)。 + return mNotesListView.dispatchTouchEvent(event); + } + break; + } + // 当触摸动作是默认情况(通常是抬起,以及其他未明确处理的动作类型)时执行以下逻辑,主要用于处理触摸结束后的相关操作,比如结束触摸事件分发等情况。 + default: { + // 判断是否之前已经在分发触摸事件(通过mDispatch标记判断),如果是,则进行触摸事件位置的最终设置以及将分发标记设置为false,结束触摸事件分发流程, + // 然后将触摸事件分发给ListView进行最后的处理(比如触发ListView中对应条目的点击等相关操作,具体看ListView自身的触摸处理逻辑)。 + if (mDispatch) { + event.setLocation(event.getX(), mDispatchY); + mDispatch = false; + return mNotesListView.dispatchTouchEvent(event); + } + break; + } + } + // 如果没有满足上述任何触摸动作的处理逻辑(比如触摸动作不在处理范围内,或者不满足触摸事件分发的条件等情况),则返回false,表示没有处理该触摸事件, + // 可以由系统或者父容器等继续按照默认的触摸处理逻辑来处理该触摸事件(如果有默认逻辑的话)。 + return false; + } + + }; + // 用于启动异步查询笔记列表的操作,根据当前所在文件夹的ID(mCurrentFolderId)来确定查询的条件,然后通过后台查询处理器(mBackgroundQueryHandler)执行查询任务。 + private void startAsyncNotesListQuery() { + // 根据当前所在文件夹的ID是否为根文件夹(Notes.ID_ROOT_FOLDER)来选择不同的查询条件字符串, + // 如果是根文件夹,则使用ROOT_FOLDER_SELECTION作为查询条件,这个条件相对复杂,可能涉及筛选不同类型的笔记等逻辑(具体看ROOT_FOLDER_SELECTION的定义); + // 如果不是根文件夹,则使用NORMAL_SELECTION作为查询条件,它相对简单,可能是按照指定的父文件夹ID来查询笔记(具体看NORMAL_SELECTION的定义)。 + String selection = (mCurrentFolderId == Notes.ID_ROOT_FOLDER)? ROOT_FOLDER_SELECTION + : NORMAL_SELECTION; + // 调用后台查询处理器(mBackgroundQueryHandler)的startQuery方法启动一个异步查询任务,传入以下参数: + // FOLDER_NOTE_LIST_QUERY_TOKEN:用于标识这个查询是查询文件夹下笔记列表的任务,方便在查询完成后的回调中区分不同类型的查询结果; + // null:这里可以传入一个自定义的对象(通常称为cookie),在查询完成的回调中可以获取到这个对象,用于传递一些额外的上下文信息等,这里传入null表示不需要传递额外信息; + // Notes.CONTENT_NOTE_URI:指定查询的内容提供器的URI,表明要从哪个数据源(可能是数据库中存储笔记的表对应的内容提供器)查询笔记数据; + // NoteItemData.PROJECTION:定义了查询要返回的列信息,即指定要查询笔记数据中的哪些字段,例如笔记的ID、标题、内容等具体字段(具体看NoteItemData.PROJECTION的定义); + // selection:前面根据文件夹ID确定的查询条件字符串,用于筛选符合条件的笔记数据; + // new String[]{ String.valueOf(mCurrentFolderId) }:为查询条件中的占位符(如果有的话)提供具体的值,这里将当前所在文件夹的ID转换为字符串后传入,用于替换查询条件中的占位符,使得查询条件具体可执行; + // NoteColumns.TYPE + " DESC," + NoteColumns.MODIFIED_DATE + " DESC":指定查询结果的排序方式,按照笔记的类型降序以及修改日期降序排列,这样可以将特定类型的笔记优先显示,并且最新修改的笔记排在前面等(具体排序效果看这两个字段在实际数据中的含义和应用场景)。 + 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"); + } // 内部类,继承自AsyncQueryHandler,用于在后台异步处理数据库查询相关的操作,通过重写onQueryComplete方法来处理不同查询任务完成后的回调逻辑, + // 比如更新笔记列表适配器的数据等操作。 + private final class BackgroundQueryHandler extends AsyncQueryHandler { + + // 构造函数,接收一个ContentResolver对象并调用父类的构造函数进行初始化,ContentResolver用于与安卓系统的内容提供器进行交互, + // 进而实现对数据库等数据源的查询、插入、更新、删除等操作,这里传入它以便在异步查询过程中能够访问相应的数据。 + public BackgroundQueryHandler(ContentResolver contentResolver) { + super(contentResolver); + } + + // 重写父类的onQueryComplete方法,该方法在异步查询任务完成后会被自动调用,根据不同的查询令牌(token)来处理相应的查询结果, + // 比如将查询到的笔记数据游标(Cursor)设置给笔记列表适配器等操作,以更新界面显示的数据内容。 + @Override + protected void onQueryComplete(int token, Object cookie, Cursor cursor) { + switch (token) { + // 当查询令牌是FOLDER_NOTE_LIST_QUERY_TOKEN时,表示是查询文件夹下笔记列表的任务完成了,此时调用笔记列表适配器(mNotesListAdapter)的changeCursor方法, + // 将查询到的游标(cursor)对象传入,适配器会根据游标中的数据更新笔记列表的显示内容,实现界面上笔记列表数据的刷新显示。 + case FOLDER_NOTE_LIST_QUERY_TOKEN: + mNotesListAdapter.changeCursor(cursor); + break; + // 当查询令牌是FOLDER_LIST_QUERY_TOKEN时,表示是查询文件夹列表的任务完成了,如果查询到的游标不为空且游标中的数据数量大于0(即有查询到文件夹数据), + // 则调用showFolderListMenu方法,传入游标对象,可能用于弹出一个文件夹列表菜单供用户选择等操作(具体看showFolderListMenu方法的实现逻辑); + // 如果游标为空或者没有查询到数据,则记录一条错误日志,表示查询文件夹失败,方便后续排查问题。 + case FOLDER_LIST_QUERY_TOKEN: + if (cursor!= null && cursor.getCount() > 0) { + showFolderListMenu(cursor); + } else { + Log.e(TAG, "Query folder failed"); + } + break; + // 如果查询令牌是其他未处理的情况,则直接返回,不进行任何操作,因为这里只关注特定的几种查询任务完成后的处理逻辑。 + default: + return; + } + } + } + + // 此方法用于展示一个包含文件夹列表的对话框,用户可以从中选择一个文件夹,然后将选中的笔记移动到所选文件夹中,同时显示相应的提示信息并结束多选模式(如果处于多选模式的话)。 + private void showFolderListMenu(Cursor cursor) { + // 创建一个AlertDialog的构建器(AlertDialog.Builder)实例,用于构建一个对话框,传入当前的NotesListActivity实例作为上下文,以便对话框能正确显示和与当前Activity交互。 + AlertDialog.Builder builder = new AlertDialog.Builder(NotesListActivity.this); + // 设置对话框的标题,从资源文件中获取对应的字符串作为标题文本,这里获取的字符串对应的文本应该是类似“选择文件夹”这样提示用户操作意图的文字(具体看资源文件中定义的R.string.menu_title_select_folder内容)。 + builder.setTitle(R.string.menu_title_select_folder); + // 创建一个FoldersListAdapter实例,传入当前Activity上下文和传入的游标(cursor)对象,这个适配器用于将游标中的文件夹数据适配到对话框的列表视图中展示给用户,方便用户选择文件夹。 + final FoldersListAdapter adapter = new FoldersListAdapter(this, cursor); + // 通过构建器设置对话框的列表适配器为前面创建的FoldersListAdapter,同时设置列表项点击监听器为一个匿名内部类实现的OnClickListener接口, + // 当用户点击列表中的某个文件夹时会触发这个监听器的onClick方法,执行相应的移动笔记到所选文件夹等操作逻辑。 + builder.setAdapter(adapter, new DialogInterface.OnClickListener() { + + public void onClick(DialogInterface dialog, int which) { + // 调用DataUtils类的batchMoveToFolder方法,通过传入ContentResolver、笔记列表适配器中当前选中的笔记的ID数组以及用户点击的文件夹的ID, + // 实现将选中的笔记批量移动到所选文件夹的功能,这个方法内部会处理与数据库等相关的操作来完成实际的移动逻辑。 + DataUtils.batchMoveToFolder(mContentResolver, + mNotesListAdapter.getSelectedItemIds(), adapter.getItemId(which)); + // 弹出一个Toast提示信息,告知用户移动笔记的操作结果,通过从资源文件中获取格式化字符串,并传入选中的笔记数量以及所选文件夹的名称(通过调用adapter的getFolderName方法获取)作为参数, + // 生成类似“已将X个笔记移动到[文件夹名称]文件夹”这样的提示信息,展示给用户知晓操作情况,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(); + // 调用mModeCallBack的finishActionMode方法,结束当前的多选模式(如果处于多选模式下),可能会进行一些界面清理、状态恢复等相关操作,具体在mModeCallBack的finishActionMode方法中实现。 + mModeCallBack.finishActionMode(); + } + }); + // 通过构建器创建并显示对话框,使得用户可以看到文件夹列表并进行选择操作。 + builder.show(); + } // 用于创建一个新笔记的方法,通过创建一个意图(Intent)跳转到NoteEditActivity(可能是用于编辑笔记的Activity),并传递相应的参数,启动新的Activity用于用户创建笔记。 + private void createNewNote() { + // 创建一个意图(Intent),指定目标Activity为NoteEditActivity.class,表明要跳转到这个Activity进行笔记的创建或编辑操作。 + Intent intent = new Intent(this, NoteEditActivity.class); + // 设置意图的动作(Action)为Intent.ACTION_INSERT_OR_EDIT,这个动作通常表示可以进行插入(创建新的)或者编辑已有的数据(这里就是笔记)的操作,告知目标Activity启动的意图用途。 + intent.setAction(Intent.ACTION_INSERT_OR_EDIT); + // 向意图中添加额外的数据,通过键值对的方式,这里的键是Notes.INTENT_EXTRA_FOLDER_ID,值是当前所在文件夹的ID(mCurrentFolderId), + // 用于告知NoteEditActivity新笔记要创建在哪个文件夹下,方便在编辑笔记界面进行相应的文件夹关联等逻辑处理(比如笔记保存时知道存到哪个文件夹对应的位置等)。 + intent.putExtra(Notes.INTENT_EXTRA_FOLDER_ID, mCurrentFolderId); + // 通过当前Activity启动这个意图对应的Activity,并传入请求码REQUEST_CODE_NEW_NODE,用于在新Activity返回结果时通过这个请求码来识别是新建笔记操作的返回结果, + // 方便在onActivityResult方法中根据不同的请求码进行相应的后续处理操作。 + this.startActivityForResult(intent, REQUEST_CODE_NEW_NODE); + } // 用于批量删除笔记的方法,在后台线程(通过AsyncTask实现)中根据同步状态执行不同的删除逻辑,比如直接删除或者移动到回收站文件夹,完成后更新相关的小部件(Widget)显示等操作,并结束多选模式。 + private void batchDelete() { + // 创建一个AsyncTask实例,它在后台线程执行耗时操作(这里就是批量删除笔记相关操作),并能在主线程更新UI等操作,传入三个泛型参数>, + // 分别表示传入的参数类型、后台任务执行过程中的进度参数类型(这里不需要,所以都是Void)以及后台任务执行完成后的返回结果类型(这里是存储AppWidgetAttribute的HashSet集合,用于后续更新小部件相关操作)。 + new AsyncTask>() { + // 在后台线程中执行的方法,用于进行实际的批量删除笔记操作,并根据同步状态选择不同的删除逻辑,最后返回与被删除笔记相关的小部件属性集合。 + protected HashSet doInBackground(Void... unused) { + // 通过笔记列表适配器获取当前选中笔记对应的小部件属性集合,这些属性可能包含小部件的ID、类型等信息,用于后续判断是否需要更新相关小部件的显示等操作。 + HashSet widgets = mNotesListAdapter.getSelectedWidget(); + // 判断是否处于同步模式(通过调用isSyncMode方法),如果不是同步模式,则执行直接删除笔记的逻辑。 + if (!isSyncMode()) { + // 如果不处于同步模式,调用DataUtils类的batchDeleteNotes方法,传入ContentResolver和笔记列表适配器中当前选中笔记的ID数组, + // 尝试直接从数据库等数据源中删除这些选中的笔记,如果删除成功则继续后续操作,如果删除失败则记录一条错误日志,表示不应该出现这种删除笔记错误的情况(理论上非同步模式下直接删除应该正常执行)。 + if (DataUtils.batchDeleteNotes(mContentResolver, mNotesListAdapter + .getSelectedItemIds())) { + } else { + Log.e(TAG, "Delete notes error, should not happens"); + } + } else { + // 如果处于同步模式,则执行将选中的笔记移动到回收站文件夹的逻辑,而不是直接删除。 + // 调用DataUtils类的batchMoveToFolder方法,传入ContentResolver、笔记列表适配器中当前选中笔记的ID数组以及回收站文件夹的ID(Notes.ID_TRASH_FOLER), + // 尝试将选中的笔记移动到回收站文件夹,如果移动失败则记录一条错误日志,表示不应该出现这种移动笔记到回收站文件夹错误的情况(理论上同步模式下移动操作应该正常执行)。 + if (!DataUtils.batchMoveToFolder(mContentResolver, mNotesListAdapter + .getSelectedItemIds(), Notes.ID_TRASH_FOLER)) { + Log.e(TAG, "Move notes to trash folder error, should not happens"); + } + } + // 返回与选中笔记相关的小部件属性集合,以便后续在onPostExecute方法中根据这些属性来更新相应的小部件显示等操作。 + return widgets; + } + + // 在后台任务执行完成后,在主线程中执行的方法,用于根据返回的小部件属性集合来更新相关小部件的显示,并结束多选模式(如果处于多选模式下)。 + @Override + protected void onPostExecute(HashSet widgets) { + if (widgets!= null) { + // 遍历返回的小部件属性集合中的每个AppWidgetAttribute对象,进行小部件更新相关操作。 + for (AppWidgetAttribute widget : widgets) { + // 判断小部件的ID是否不是无效的ID(AppWidgetManager.INVALID_APPWIDGET_ID表示无效的小部件ID)并且小部件的类型也不是无效类型(Notes.TYPE_WIDGET_INVALIDE表示无效的小部件类型), + // 如果满足这两个条件,说明这个小部件是有效的,需要进行更新显示操作,调用updateWidget方法传入小部件的ID和类型来更新小部件的显示内容等。 + if (widget.widgetId!= AppWidgetManager.INVALID_APPWIDGET_ID + && widget.widgetType!= Notes.TYPE_WIDGET_INVALIDE) { + updateWidget(widget.widgetId, widget.widgetType); + } + } + } + // 调用mModeCallBack的finishActionMode方法结束当前的多选模式(如果处于多选模式下),进行界面清理、状态恢复等相关操作,具体在mModeCallBack的finishActionMode方法中实现。 + mModeCallBack.finishActionMode(); + } + }.execute(); + } // 用于删除指定文件夹的方法,根据同步状态执行不同的删除逻辑,比如直接删除或者移动到回收站文件夹,同时更新与该文件夹相关的小部件显示等操作。 + private void deleteFolder(long folderId) { + // 判断传入的文件夹ID是否为根文件夹的ID(Notes.ID_ROOT_FOLDER),如果是则记录一条错误日志,表示不应该出现删除根文件夹这种错误操作情况,然后直接返回,不执行后续的删除逻辑。 + if (folderId == Notes.ID_ROOT_FOLDER) { + Log.e(TAG, "Wrong folder id, should not happen " + folderId); + return; + } + + // 创建一个HashSet集合,用于存储要删除的文件夹的ID,这里将传入的文件夹ID添加到集合中,表示要操作的目标文件夹。 + HashSet ids = new HashSet(); + ids.add(folderId); + // 通过DataUtils类的getFolderNoteWidget方法,传入ContentResolver和要删除的文件夹ID,获取与该文件夹相关的笔记对应的小部件属性集合, + // 这些属性可用于后续判断是否需要更新相关小部件的显示等操作,因为删除文件夹可能会影响到与之关联的小部件显示内容。 + HashSet widgets = DataUtils.getFolderNoteWidget(mContentResolver, + folderId); + // 判断是否处于同步模式(通过调用isSyncMode方法),如果不是同步模式,则执行直接删除文件夹的逻辑。 + if (!isSyncMode()) { + // 如果不处于同步模式,调用DataUtils类的batchDeleteNotes方法,传入ContentResolver和存储文件夹ID的集合(这里只包含一个要删除的文件夹ID), + // 尝试直接从数据库等数据源中删除这个文件夹,如果删除成功则继续后续操作,如果删除失败并没有在这里记录错误日志,可能是因为后续还有其他相关操作需要执行(比如更新小部件显示等),会在相关操作中进一步处理错误情况。 + DataUtils.batchDeleteNotes(mContentResolver, ids); + } else { + // 如果处于同步模式,则执行将文件夹移动到回收站文件夹的逻辑,而不是直接删除。 + // 调用DataUtils类的batchMoveToFolder方法,传入ContentResolver、存储文件夹ID的集合(这里只包含一个要删除的文件夹ID)以及回收站文件夹的ID(Notes.ID_TRASH_FOLER), + // 尝试将该文件夹移动到回收站文件夹,如果移动失败则记录一条错误日志,表示不应该出现这种移动文件夹到回收站文件夹错误的情况(理论上同步模式下移动操作应该正常执行)。 + DataUtils.batchMoveToFolder(mContentResolver, ids, Notes.ID_TRASH_FOLER); + } + // 判断获取到的与文件夹相关的小部件属性集合是否不为空,如果不为空,则遍历集合中的每个小部件属性对象,进行小部件更新相关操作。 + if (widgets!= null) { + for (AppWidgetAttribute widget : widgets) { + // 判断小部件的ID是否不是无效的ID(AppWidgetManager.INVALID_APPWIDGET_ID表示无效的小部件ID)并且小部件的类型也不是无效类型(Notes.TYPE_WIDGET_INVALIDE表示无效的小部件类型), + // 如果满足这两个条件,说明这个小部件是有效的,需要进行更新显示操作,调用updateWidget方法传入小部件的ID和类型来更新小部件的显示内容等。 + if (widget.widgetId!= AppWidgetManager.INVALID_APPWIDGET_ID + && widget.widgetType!= Notes.TYPE_WIDGET_INVALIDE) { + updateWidget(widget.widgetId, widget.widgetType); + } + } + } + } + // 用于打开一个笔记节点(通常是跳转到笔记编辑或查看界面)的方法,通过创建一个意图(Intent)跳转到NoteEditActivity,并传递笔记的唯一标识符(ID)作为参数,启动新的Activity用于用户查看或编辑笔记内容。 + private void openNode(NoteItemData data) { + // 创建一个意图(Intent),指定目标Activity为NoteEditActivity.class,表明要跳转到这个Activity进行笔记的相关操作(查看或编辑等,具体看NoteEditActivity中的逻辑)。 + Intent intent = new Intent(this, NoteEditActivity.class); + // 设置意图的动作(Action)为Intent.ACTION_VIEW,这个动作通常表示查看已有的数据(这里就是笔记)的操作,告知目标Activity启动的意图是查看笔记内容,而不是创建或编辑等其他操作。 + intent.setAction(Intent.ACTION_VIEW); + // 向意图中添加额外的数据,通过键值对的方式,这里的键是Intent.EXTRA_UID,值是传入的NoteItemData对象中获取的笔记的ID(data.getId()), + // 用于告知NoteEditActivity要查看或编辑的是哪个具体的笔记,方便在目标Activity中根据这个ID从数据库等数据源获取对应的笔记内容进行展示或编辑操作。 + intent.putExtra(Intent.EXTRA_UID, data.getId()); + // 通过当前Activity启动这个意图对应的Activity,并传入请求码REQUEST_CODE_OPEN_NODE,用于在新Activity返回结果时通过这个请求码来识别是打开笔记操作的返回结果, + // 方便在onActivityResult方法中根据不同的请求码进行相应的后续处理操作。 + this.startActivityForResult(intent, REQUEST_CODE_OPEN_NODE); + } // 用于打开一个文件夹的方法,根据传入的文件夹数据(NoteItemData)更新当前所在文件夹的ID、重新查询笔记列表、设置列表编辑状态以及更新标题栏显示内容等操作,实现进入文件夹并展示其下笔记列表的功能。 + private void openFolder(NoteItemData data) { + // 将当前所在文件夹的ID更新为传入的文件夹数据对象的ID(data.getId()),表示进入了这个文件夹,后续的操作(比如查询笔记列表等)都将基于这个新的文件夹进行。 + mCurrentFolderId = data.getId(); + // 调用startAsyncNotesListQuery方法启动异步查询笔记列表的操作,根据新的当前所在文件夹ID来查询并获取该文件夹下的笔记数据,然后更新界面上笔记列表的显示内容,展示该文件夹下的笔记情况。 + startAsyncNotesListQuery(); + // 判断传入的文件夹数据对象的ID是否为通话记录文件夹的ID(Notes.ID_CALL_RECORD_FOLDER),如果是,则将列表的编辑状态设置为CALL_RECORD_FOLDER,表示处于通话记录文件夹状态, + // 同时将“新建笔记”按钮(mAddNewNote)设置为不可见,因为通话记录文件夹可能有特殊的操作逻辑,不允许直接新建笔记等情况(具体看业务需求)。 + if (data.getId() == Notes.ID_CALL_RECORD_FOLDER) { + mState = ListEditState.CALL_RECORD_FOLDER; + mAddNewNote.setVisibility(View.GONE); + } else { + // 如果不是通话记录文件夹的ID,则将列表的编辑状态设置为SUB_FOLDER,表示处于普通子文件夹状态。 + mState = ListEditState.SUB_FOLDER; + } + // 再次判断传入的文件夹数据对象的ID是否为通话记录文件夹的ID(Notes.ID_CALL_RECORD_FOLDER),如果是,则将顶部标题栏(mTitleBar)的文本内容设置为通话记录文件夹对应的名称(从资源文件中获取,具体看R.string.call_record_folder_name定义的内容), + // 用于在界面上显示当前所在的是通话记录文件夹;如果不是通话记录文件夹的ID,则将标题栏的文本内容设置为传入的文件夹数据对象的摘要信息(data.getSnippet()),可能是文件夹的名称或其他相关描述信息,展示当前所在文件夹的相关信息给用户。 + if (data.getId() == Notes.ID_CALL_RECORD_FOLDER) { + mTitleBar.setText(R.string.call_record_folder_name); + } else { + mTitleBar.setText(data.getSnippet()); + } + // 将顶部标题栏的可见性设置为可见(View.VISIBLE),使得用户能看到当前所在文件夹的标题信息,方便知晓当前操作所处的文件夹位置。 + mTitleBar.setVisibility(View.VISIBLE); + } + // 处理视图点击事件的方法,根据点击的视图的ID来执行相应的操作逻辑,目前主要是针对“新建笔记”按钮(R.id.btn_new_note)的点击进行处理。 + public void onClick(View v) { + switch (v.getId()) { + // 当点击的视图ID是R.id.btn_new_note时,表示点击了“新建笔记”按钮,此时调用createNewNote方法来创建一个新的笔记, + // 比如跳转到笔记编辑页面等操作,具体的创建逻辑在createNewNote方法中实现。 + case R.id.btn_new_note: + createNewNote(); + break; + // 如果点击的视图ID不是上述处理的特定ID,则不执行任何额外操作,直接跳出switch语句,等待下一次点击事件触发处理逻辑。 + default: + break; + } + } // 用于强制显示软键盘的方法,通过获取系统的输入方法管理器(InputMethodManager)来控制软键盘的显示操作。 + private void showSoftInput() { + // 从系统服务中获取输入方法管理器(InputMethodManager)实例,这个管理器用于管理与输入相关的操作,比如显示、隐藏软键盘等功能。 + InputMethodManager inputMethodManager = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE); + // 判断获取到的输入方法管理器是否不为空,只有不为空时才能进行后续操作,避免空指针异常,若不为空则调用toggleSoftInput方法来显示软键盘。 + // 其中InputMethodManager.SHOW_FORCED参数表示强制显示软键盘,0表示不提供额外的标志或选项,该方法会尝试将软键盘显示出来,方便用户进行文本输入等操作。 + if (inputMethodManager!= null) { + inputMethodManager.toggleSoftInput(InputMethodManager.SHOW_FORCED, 0); + } + } + // 用于隐藏软键盘的方法,通过输入方法管理器(InputMethodManager)针对指定的视图(View)对应的窗口来隐藏软键盘。 + private void hideSoftInput(View view) { + // 从系统服务中获取输入方法管理器(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的构建器(AlertDialog.Builder)实例,用于构建对话框,传入当前Activity实例(this)作为上下文,以便对话框能正确显示和与当前Activity交互。 + final AlertDialog.Builder builder = new AlertDialog.Builder(this); + // 通过LayoutInflater从当前Activity的布局资源中加载名为R.layout.dialog_edit_text的布局文件创建一个视图(View), + // 这个布局文件可能包含了用于输入文件夹名称的文本编辑框等相关UI元素,用于在对话框中展示给用户进行操作。 + View view = LayoutInflater.from(this).inflate(R.layout.dialog_edit_text, null); + // 从加载的视图中找到ID为R.id.et_foler_name的EditText组件,这个组件就是用于用户输入文件夹名称的文本框,后续会对其文本内容进行获取和相关逻辑处理。 + final EditText etName = (EditText) view.findViewById(R.id.et_foler_name); + + // 调用showSoftInput方法,强制显示软键盘,方便用户直接在对话框弹出后就能输入文件夹名称,提升操作便捷性。 + showSoftInput(); + + // 根据传入的create布尔值判断操作模式,如果是false,表示是修改文件夹操作。 + if (!create) { + // 判断长按选中的笔记数据项(mFocusNoteDataItem)是否不为空,只有不为空才能进行后续的获取名称等操作,若为空则记录错误日志并直接返回,不进行修改文件夹相关操作。 + if (mFocusNoteDataItem!= null) { + // 将文本编辑框(etName)的文本内容设置为长按选中的笔记数据项中的摘要信息(可能是文件夹原名称等,通过mFocusNoteDataItem.getSnippet()获取), + // 这样用户在修改时能看到原始的文件夹名称,便于进行修改操作。 + etName.setText(mFocusNoteDataItem.getSnippet()); + // 设置对话框的标题,从资源文件中获取对应的字符串作为标题文本,这里获取的字符串对应的文本应该是类似“修改文件夹名称”这样提示用户操作意图的文字(具体看资源文件中定义的R.string.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,表示是创建文件夹操作,将文本编辑框(etName)的文本内容清空,方便用户输入新的文件夹名称。 + etName.setText(""); + // 设置对话框的标题,从资源文件中获取对应的字符串作为标题文本,这里获取的字符串对应的文本应该是类似“创建文件夹”这样提示用户操作意图的文字(具体看资源文件中定义的this.getString(R.string.menu_create_folder)内容)。 + builder.setTitle(this.getString(R.string.menu_create_folder)); + } + + // 设置对话框的确认按钮(通常是“确定”“OK”之类的按钮),这里传入null表示暂时不设置点击监听器,后续会单独为这个按钮设置点击监听器来处理创建或修改文件夹的具体逻辑。 + builder.setPositiveButton(android.R.string.ok, null); + // 设置对话框的取消按钮(通常是“取消”“Cancel”之类的按钮),并设置点击监听器为一个匿名内部类实现的OnClickListener接口, + // 当用户点击取消按钮时会触发这个监听器的onClick方法,在方法内调用hideSoftInput方法隐藏软键盘,避免软键盘在对话框关闭后还显示在界面上影响用户体验。 + builder.setNegativeButton(android.R.string.cancel, new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int which) { + hideSoftInput(etName); + } + }); + + // 通过构建器设置对话框的视图为前面加载的包含文本编辑框的视图(view),然后创建并显示对话框,使得用户可以看到对话框中的文本编辑框等元素并进行操作。 + final Dialog dialog = builder.setView(view).show(); + + // 从显示的对话框中找到ID为android.R.id.button1的按钮(通常就是前面设置的确认按钮),用于后续为其设置点击监听器来处理创建或修改文件夹的核心逻辑。 + final Button positive = (Button)dialog.findViewById(android.R.id.button1); + positive.setOnClickListener(new OnClickListener() { + public void onClick(View v) { + // 首先调用hideSoftInput方法隐藏软键盘,避免在处理完操作逻辑后软键盘还显示在界面上,影响后续操作或界面美观。 + hideSoftInput(etName); + // 获取文本编辑框(etName)中的文本内容,将其转换为字符串,用于后续判断文件夹名称是否合法以及进行创建或修改文件夹的数据库操作等。 + String name = etName.getText().toString(); + // 调用DataUtils类的checkVisibleFolderName方法,传入ContentResolver和文件夹名称(name),用于检查文件夹名称是否已经存在或者是否符合其他合法性规则(具体看checkVisibleFolderName方法的实现逻辑), + // 如果返回true表示名称已存在或不符合规则,此时弹出一个Toast提示信息告知用户文件夹已存在(通过从资源文件中获取格式化字符串,并传入文件夹名称作为参数,生成类似“文件夹[名称]已存在”这样的提示信息), + // 然后将文本编辑框的光标定位到开头位置(通过etName.setSelection(0, etName.length())设置),方便用户修改名称,最后直接返回,不进行后续的创建或修改文件夹操作。 + 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为true)且文件夹名称不为空(通过!TextUtils.isEmpty(name)判断),则进行修改文件夹的数据库操作。 + if (!create) { + if (!TextUtils.isEmpty(name)) { + // 创建一个ContentValues对象,用于存储要更新的数据,即文件夹的新名称等相关信息,通过put方法分别设置要更新的列名和对应的值。 + ContentValues values = new ContentValues(); + values.put(NoteColumns.SNIPPET, name); + values.put(NoteColumns.TYPE, Notes.TYPE_FOLDER); + values.put(NoteColumns.LOCAL_MODIFIED, 1); + // 通过ContentResolver调用update方法,传入要更新的内容提供器的URI(Notes.CONTENT_NOTE_URI)、存储更新数据的ContentValues对象、 + // 更新的条件(通过NoteColumns.ID + "=?"表示按照文件夹的ID进行匹配更新,后面的new String[]{ String.valueOf(mFocusNoteDataItem.getId()) }提供具体的文件夹ID值), + // 这样就可以根据长按选中的文件夹的ID来更新其名称等相关信息到数据库中,实现文件夹名称的修改功能。 + mContentResolver.update(Notes.CONTENT_NOTE_URI, values, NoteColumns.ID + + "=?", new String[] { + String.valueOf(mFocusNoteDataItem.getId()) + }); + } + } else if (!TextUtils.isEmpty(name)) { + // 如果是创建文件夹操作(create为true)且文件夹名称不为空,同样创建一个ContentValues对象来存储要插入的数据,设置文件夹的名称、类型等相关信息。 + ContentValues values = new ContentValues(); + values.put(NoteColumns.SNIPPET, name); + values.put(NoteColumns.TYPE, Notes.TYPE_FOLDER); + // 通过ContentResolver调用insert方法,传入要插入数据的内容提供器的URI(Notes.CONTENT_NOTE_URI)和存储插入数据的ContentValues对象, + // 这样就可以将新的文件夹信息插入到数据库中,实现创建文件夹的功能。 + mContentResolver.insert(Notes.CONTENT_NOTE_URI, values); + } + // 完成创建或修改文件夹操作后,关闭对话框,使其从界面上消失,结束此次操作流程。 + dialog.dismiss(); + } + }); + + // 判断文本编辑框(etName)中的文本内容是否为空,如果为空则将确认按钮(positive)设置为不可用状态,避免用户在没有输入文件夹名称的情况下点击确认按钮进行无效操作, + // 只有当用户输入了文件夹名称后,按钮才会变为可用状态(在文本编辑框的文本改变监听器中会根据文本内容是否为空来动态更新按钮的可用状态,具体看下面的文本改变监听器代码逻辑)。 + if (TextUtils.isEmpty(etName.getText())) { + positive.setEnabled(false); + } + + /** + * When the name edit text is null, disable the positive button + */ + // 为文本编辑框(etName)添加一个文本改变监听器(TextWatcher),用于监听文本内容的变化情况,根据文本是否为空来动态更新确认按钮(positive)的可用状态。 + 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 + + } + }); + } + // 重写的系统返回键按下时的处理方法,根据当前列表的编辑状态(mState)执行不同的操作,比如返回上一级文件夹、恢复界面状态、重新查询笔记列表等操作。 + @Override + public void onBackPressed() { + switch (mState) { + // 当列表编辑状态为SUB_FOLDER(表示处于子文件夹状态)时,执行以下操作。 + case SUB_FOLDER: + // 将当前所在文件夹的ID设置为根文件夹的ID(Notes.ID_ROOT_FOLDER),表示返回上一级,回到根文件夹层级。 + mCurrentFolderId = Notes.ID_ROOT_FOLDER; + // 将列表编辑状态更新为NOTE_LIST,表示回到普通的笔记列表状态,不再处于子文件夹操作状态了。 + mState = ListEditState.NOTE_LIST; + // 调用startAsyncNotesListQuery方法启动异步查询笔记列表的操作,根据新的当前所在文件夹ID(根文件夹)来查询并获取该文件夹下的笔记数据,然后更新界面上笔记列表的显示内容,展示根文件夹下的笔记情况。 + startAsyncNotesListQuery(); + // 将顶部标题栏(mTitleBar)的可见性设置为不可见(View.GONE),因为回到了根文件夹,可能不需要显示之前子文件夹对应的标题栏内容了,优化界面显示。 + mTitleBar.setVisibility(View.GONE); + break; + // 当列表编辑状态为CALL_RECORD_FOLDER(表示处于通话记录文件夹状态)时,执行以下操作。 + case CALL_RECORD_FOLDER: + // 将当前所在文件夹的ID设置为根文件夹的ID(Notes.ID_ROOT_FOLDER),同样表示返回上一级,回到根文件夹层级。 + mCurrentFolderId = Notes.ID_ROOT_FOLDER; + // 将列表编辑状态更新为NOTE_LIST,表示回到普通的笔记列表状态,不再处于通话记录文件夹操作状态了。 + mState = ListEditState.NOTE_LIST; + // 将“新建笔记”按钮(mAddNewNote)的可见性设置为可见(View.VISIBLE),因为回到了普通笔记列表状态,通常允许用户新建笔记,所以显示该按钮。 + mAddNewNote.setVisibility(View.VISIBLE); + // 将顶部标题栏(mTitleBar)的可见性设置为不可见(View.GONE),回到根文件夹后可能不需要显示通话记录文件夹对应的标题栏内容了,优化界面显示。 + mTitleBar.setVisibility(View.GONE); + // 调用startAsyncNotesListQuery方法启动异步查询笔记列表的操作,根据新的当前所在文件夹ID(根文件夹)来查询并获取该文件夹下的笔记数据,然后更新界面上笔记列表的显示内容,展示根文件夹下的笔记情况。 + startAsyncNotesListQuery(); + break; + // 当列表编辑状态为NOTE_LIST(表示处于普通笔记列表状态)时,直接调用父类的onBackPressed方法,按照系统默认的返回键处理逻辑进行操作, + // 比如可能会退出当前Activity或者执行其他默认的返回行为(具体取决于Activity的继承关系和系统默认设置等情况)。 + case NOTE_LIST: + super.onBackPressed(); + break; + // 如果列表编辑状态是其他未处理的情况,则不执行任何额外操作,直接跳出switch语句。 + default: + break; + } + } + // 此方法用于更新指定的桌面小部件(Widget),根据传入的小部件类型来设置对应的广播意图,向相关组件发送更新通知,同时设置当前Activity的结果并返回。 + private void updateWidget(int appWidgetId, int appWidgetType) { + // 创建一个意图(Intent),并设置其动作为AppWidgetManager.ACTION_APPWIDGET_UPDATE,该动作是用于通知系统或相关的小部件接收者进行小部件更新操作的标准动作。 + Intent intent = new Intent(AppWidgetManager.ACTION_APPWIDGET_UPDATE); + // 根据传入的小部件类型(appWidgetType)进行不同的处理,判断是否为特定类型的小部件,以设置正确的广播接收者类。 + if (appWidgetType == Notes.TYPE_WIDGET_2X) { + // 如果小部件类型是Notes.TYPE_WIDGET_2X,表示是一种特定尺寸(可能是2倍大小)的小部件,通过intent.setClass方法将意图的目标类设置为NoteWidgetProvider_2x.class, + // 这样当发送广播时,系统会将这个广播发送给对应的NoteWidgetProvider_2x类来处理,该类会根据自身逻辑更新相应的2倍大小的小部件显示内容。 + intent.setClass(this, NoteWidgetProvider_2x.class); + } else if (appWidgetType == Notes.TYPE_WIDGET_4X) { + // 如果小部件类型是Notes.TYPE_WIDGET_4X,表示是另一种特定尺寸(可能是4倍大小)的小部件,将意图的目标类设置为NoteWidgetProvider_4x.class, + // 使得广播能被对应的NoteWidgetProvider_4x类接收并处理,以更新这种4倍大小的小部件的显示内容。 + intent.setClass(this, NoteWidgetProvider_4x.class); + } else { + // 如果传入的小部件类型不是上述支持的类型,记录一条错误日志,提示不支持的小部件类型,然后直接返回,不进行后续的广播发送等操作,因为不知道如何处理这种未知类型的小部件更新。 + Log.e(TAG, "Unspported widget type"); + return; + } + + // 向意图中添加额外的数据,通过putExtra方法,这里将小部件的ID数组添加进去,键为AppWidgetManager.EXTRA_APPWIDGET_IDS,值是一个只包含传入的appWidgetId的数组, + // 用于明确告知接收广播的小部件接收者要更新哪个(或哪些,这里实际只有一个)小部件,方便接收者准确找到对应的小部件进行更新操作。 + intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, new int[] { + appWidgetId + }); + + // 通过当前Activity发送广播,广播内容就是前面配置好的intent,这样系统会将这个广播发送给符合条件的广播接收者(根据意图设置的动作和目标类等信息),触发小部件的更新逻辑。 + sendBroadcast(intent); + // 设置当前Activity的结果码为RESULT_OK,表示操作成功(这里主要是小部件更新操作相关的结果反馈),并将前面创建的意图(intent)作为结果返回, + // 方便其他相关的组件(比如启动这个Activity的父Activity等)获取这个结果以及其中包含的信息(比如小部件更新相关的意图内容等),进行后续的处理或判断操作。 + setResult(RESULT_OK, intent); + } + // 这是一个实现了OnCreateContextMenuListener接口的内部成员变量,用于创建与文件夹相关的上下文菜单(Context Menu),当长按与文件夹相关的视图时会触发显示该菜单,菜单中包含查看、删除、修改名称等操作选项。 + private final OnCreateContextMenuListener mFolderOnCreateContextMenuListener = new OnCreateContextMenuListener() { + public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) { + // 首先判断长按选中的笔记数据项(mFocusNoteDataItem)是否不为空,只有不为空才能创建包含有效信息的上下文菜单,若为空则无法获取相关数据来填充菜单内容,不进行菜单创建操作。 + if (mFocusNoteDataItem!= null) { + // 设置上下文菜单的标题,将标题设置为长按选中的笔记数据项中的摘要信息(可能是文件夹名称等,通过mFocusNoteDataItem.getSnippet()获取), + // 这样在菜单显示时,用户可以直观看到当前操作的文件夹相关信息,作为菜单的一个标识提示。 + menu.setHeaderTitle(mFocusNoteDataItem.getSnippet()); + // 向上下文菜单中添加一个菜单项,菜单项的ID为MENU_FOLDER_VIEW,分组ID为0(通常用于菜单分组管理,这里暂不涉及复杂分组逻辑,所以用0),顺序为0(表示添加顺序,这里先添加这个查看菜单项), + // 菜单项的文本从资源文件中获取,对应的文本应该是类似“查看文件夹”这样提示用户操作意图的文字(具体看资源文件中定义的R.string.menu_folder_view内容),点击这个菜单项后会执行对应的查看文件夹操作(具体操作在onContextItemSelected方法中处理)。 + menu.add(0, MENU_FOLDER_VIEW, 0, R.string.menu_folder_view); + // 向上下文菜单中再添加一个菜单项,菜单项的ID为MENU_FOLDER_DELETE,分组ID为0,顺序为0(按照添加顺序依次添加菜单项), + // 菜单项的文本从资源文件中获取,对应的文本应该是类似“删除文件夹”这样提示用户操作意图的文字(具体看资源文件中定义的R.string.menu_folder_delete内容),点击这个菜单项后会执行对应的删除文件夹操作(具体操作在onContextItemSelected方法中处理)。 + menu.add(0, MENU_FOLDER_DELETE, 0, R.string.menu_folder_delete); + // 继续向上下文菜单中添加一个菜单项,菜单项的ID为MENU_FOLDER_CHANGE_NAME,分组ID为0,顺序为0, + // 菜单项的文本从资源文件中获取,对应的文本应该是类似“修改文件夹名称”这样提示用户操作意图的文字(具体看资源文件中定义的R.string.menu_folder_change_name内容),点击这个菜单项后会执行对应的修改文件夹名称操作(具体操作在onContextItemSelected方法中处理)。 + menu.add(0, MENU_FOLDER_CHANGE_NAME, 0, R.string.menu_folder_change_name); + } + } + } + // 重写的上下文菜单关闭时的回调方法,用于在上下文菜单关闭后进行一些清理操作,比如移除之前为ListView设置的上下文菜单监听器,同时调用父类的同名方法完成默认的关闭后处理逻辑。 + @Override + public void onContextMenuClosed(Menu menu) { + // 判断笔记列表视图(mNotesListView)是否不为空,如果不为空,则将其上下文菜单创建监听器设置为null, + // 这意味着移除了之前为ListView设置的用于创建上下文菜单的监听器(比如长按文件夹时创建菜单的监听器),避免在菜单关闭后还可能因为误触发等情况再次显示菜单或者产生其他异常行为。 + if (mNotesListView!= null) { + mNotesListView.setOnCreateContextMenuListener(null); + } + // 调用父类的onContextMenuClosed方法,执行系统默认的上下文菜单关闭后的处理逻辑,可能包括一些资源释放、状态恢复等操作,具体取决于父类的实现情况。 + super.onContextMenuClosed(menu); + } + // 重写的上下文菜单项被选中时的处理方法,根据选中的菜单项的ID来执行相应的操作逻辑,比如查看文件夹、删除文件夹、修改文件夹名称等操作,前提是长按选中的笔记数据项不为空。 + @Override + public boolean onContextItemSelected(MenuItem item) { + // 首先判断长按选中的笔记数据项(mFocusNoteDataItem)是否为空,如果为空则记录一条错误日志,提示长按数据项为空,然后返回false,表示没有成功处理该菜单项选择事件, + // 因为没有有效的数据项就无法进行后续与该数据项相关的操作,比如不知道要操作哪个文件夹等情况。 + if (mFocusNoteDataItem == null) { + Log.e(TAG, "The long click data item is null"); + return false; + } + switch (item.getItemId()) { + // 当选中的菜单项的ID是MENU_FOLDER_VIEW时,表示用户点击了“查看文件夹”菜单项,此时调用openFolder方法并传入长按选中的笔记数据项(mFocusNoteDataItem), + // 执行进入并查看该文件夹相关内容的操作,比如展示文件夹下的笔记列表等,具体的查看逻辑在openFolder方法中实现。 + case MENU_FOLDER_VIEW: + openFolder(mFocusNoteDataItem); + break; + // 当选中的菜单项的ID是MENU_FOLDER_DELETE时,表示用户点击了“删除文件夹”菜单项,此时创建一个AlertDialog的构建器(AlertDialog.Builder)实例,用于构建一个提示删除确认的对话框。 + case MENU_FOLDER_DELETE: + AlertDialog.Builder builder = new AlertDialog.Builder(this); + // 设置对话框的标题,从资源文件中获取对应的字符串作为标题文本,这里获取的字符串对应的文本应该是类似“确认删除”这样提示用户操作意图的文字(具体看资源文件中定义的getString(R.string.alert_title_delete)内容)。 + builder.setTitle(getString(R.string.alert_title_delete)); + // 设置对话框的图标,使用安卓系统自带的表示警告的图标(android.R.drawable.ic_dialog_alert),用于直观提示用户此操作具有一定危险性(删除操作不可逆等情况)。 + builder.setIcon(android.R.drawable.ic_dialog_alert); + // 设置对话框的消息内容,从资源文件中获取对应的字符串作为消息文本,这里获取的字符串对应的文本应该是类似“确认要删除此文件夹吗?”这样提示用户删除操作相关信息的文字(具体看资源文件中定义的getString(R.string.alert_message_delete_folder)内容)。 + builder.setMessage(getString(R.string.alert_message_delete_folder)); + // 设置对话框的确认按钮(通常是“确定”“OK”之类的按钮),并设置点击监听器为一个匿名内部类实现的OnClickListener接口, + // 当用户点击确认按钮时会触发这个监听器的onClick方法,在方法内调用deleteFolder方法并传入长按选中的笔记数据项的ID(mFocusNoteDataItem.getId()),执行删除文件夹的实际操作,具体的删除逻辑在deleteFolder方法中实现。 + builder.setPositiveButton(android.R.string.ok, + new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int which) { + deleteFolder(mFocusNoteDataItem.getId()); + } + }); + // 设置对话框的取消按钮(通常是“取消”“Cancel”之类的按钮),传入null表示不设置额外的点击监听器逻辑,点击取消按钮则直接关闭对话框,不执行删除操作。 + builder.setNegativeButton(android.R.string.cancel, null); + // 通过构建器创建并显示对话框,提示用户确认是否要执行删除文件夹的操作,让用户进行二次确认,避免误操作。 + builder.show(); + break; + // 当选中的菜单项的ID是MENU_FOLDER_CHANGE_NAME时,表示用户点击了“修改文件夹名称”菜单项,此时调用showCreateOrModifyFolderDialog方法并传入false, + // 表示进入修改文件夹名称的操作模式,会弹出一个对话框供用户输入新的文件夹名称等操作,具体的修改文件夹名称逻辑在showCreateOrModifyFolderDialog方法中实现。 + case MENU_FOLDER_CHANGE_NAME: + showCreateOrModifyFolderDialog(false); + break; + // 如果选中的菜单项的ID不是上述处理的特定ID,则不执行任何额外操作,直接跳出switch语句,等待下一次菜单项选择事件触发处理逻辑。 + default: + break; + } + + // 返回true表示已经成功处理了该菜单项选择事件(无论具体是执行了哪种操作逻辑,只要按照预期处理了就返回true),这样系统就知道该事件已经被处理,不会再进行其他默认的处理操作了。 + return true; + } + // 重写的用于准备选项菜单(Options Menu)的方法,根据当前列表的编辑状态(mState)来清空菜单原有内容,并加载不同的菜单资源文件,同时设置特定菜单项(如同步菜单项)的标题显示,以展示符合当前状态的菜单选项。 + @Override + public boolean onPrepareOptionsMenu(Menu menu) { + // 首先清空菜单(menu)中的所有原有菜单项,确保每次准备菜单时都是从一个空白状态开始添加符合当前状态的菜单项,避免出现重复或者不符合当前情况的菜单项显示。 + menu.clear(); + // 根据当前列表的编辑状态(mState)进行不同的操作,判断处于哪种状态来加载对应的菜单资源文件并进行相关设置。 + if (mState == ListEditState.NOTE_LIST) { + // 如果处于NOTE_LIST状态(表示普通的笔记列表状态),通过菜单填充器(getMenuInflater)将名为R.menu.note_list的菜单资源文件填充到传入的菜单(menu)对象中, + // 这样就会在菜单中显示该资源文件中定义的菜单项,例如可能包含新建文件夹、导出文本、同步等相关菜单项(具体看R.menu.note_list资源文件中的定义内容)。 + getMenuInflater().inflate(R.menu.note_list, menu); + // 找到菜单中ID为R.id.menu_sync的菜单项,根据GTaskSyncService.isSyncing()方法的返回值(表示是否正在同步)来设置该菜单项的标题显示内容, + // 如果正在同步,则将标题设置为从资源文件中获取的表示取消同步的字符串(R.string.menu_sync_cancel),如果没有同步,则设置为表示进行同步的字符串(R.string.menu_sync), + // 这样用户可以通过这个菜单项直观地进行同步操作的启动或取消操作,根据当前同步状态来切换功能显示。 + 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状态(表示处于子文件夹状态),通过菜单填充器将名为R.menu.sub_folder的菜单资源文件填充到菜单(menu)对象中, + // 展示符合子文件夹状态下的相关菜单项,例如可能包含一些与子文件夹操作相关的特定功能菜单项(具体看R.menu.sub_folder资源文件中的定义内容)。 + getMenuInflater().inflate(R.menu.sub_folder, menu); + } else if (mState == ListEditState.CALL_RECORD_FOLDER) { + // 如果处于CALL_RECORD_FOLDER状态(表示处于通话记录文件夹状态),通过菜单填充器将名为R.menu.call_record_folder的菜单资源文件填充到菜单(menu)对象中, + // 显示适合通话记录文件夹操作的相关菜单项,例如可能针对通话记录有特定的查看、管理等功能菜单项(具体看R.menu.call_record_folder资源文件中的定义内容)。 + getMenuInflater().inflate(R.menu.call_record_folder, menu); + } else { + // 如果当前列表的编辑状态是其他未处理的情况,则记录一条错误日志,提示出现了错误的状态值,便于后续排查问题,因为理论上应该处于上述几种已知的状态之一。 + Log.e(TAG, "Wrong state:" + mState); + } + // 返回true表示菜单准备工作已完成,系统可以根据这个返回值来决定是否继续后续的菜单显示等相关操作,一般返回true表示准备成功,可以正常显示菜单。 + return true; + } + // 重写的用于处理选项菜单项被选中事件的方法,根据选中菜单项的不同ID执行相应的业务逻辑,例如创建文件夹、导出笔记文本、同步操作、进入设置页面、新建笔记、发起搜索等操作。 + public boolean onOptionsItemSelected(MenuItem item) { + switch (item.getItemId()) { + // 当点击的菜单项ID为R.id.menu_new_folder时,意味着用户选择了“新建文件夹”操作。 + case R.id.menu_new_folder: { + // 调用showCreateOrModifyFolderDialog方法,并传入参数true,表示进入创建文件夹的流程,会弹出相应对话框让用户输入文件夹相关信息来创建新文件夹, + // 具体创建逻辑在showCreateOrModifyFolderDialog方法内实现。 + showCreateOrModifyFolderDialog(true); + break; + } + // 当点击的菜单项ID为R.id.menu_export_text时,代表用户触发了“导出文本”操作,也就是要将笔记内容导出为文本格式。 + case R.id.menu_export_text: { + // 调用exportNoteToText方法来执行具体的导出笔记为文本的操作,该方法内部会处理诸如文件创建、笔记数据写入等相关逻辑,实现导出功能。 + exportNoteToText(); + break; + } + // 当点击的菜单项ID为R.id.menu_sync时,对应“同步”相关操作,这里需要先判断当前是否处于同步模式,再根据菜单项标题进一步确定具体的同步相关行为。 + case R.id.menu_sync: { + // 通过调用isSyncMode方法来判断当前是否处于同步模式,如果处于同步模式才会继续下面根据菜单项标题的不同操作逻辑判断。 + if (isSyncMode()) { + // 判断菜单项的标题是否与从资源文件中获取的表示“同步”的字符串内容相等,以此来区分用户是想开始同步还是取消同步操作。 + if (TextUtils.equals(item.getTitle(), getString(R.string.menu_sync))) { + // 如果相等,说明用户想要开始同步操作,调用GTaskSyncService.startSync方法,并传入当前Activity实例(this)作为参数,启动同步相关的服务或任务来执行数据同步逻辑。 + GTaskSyncService.startSync(this); + } else { + // 如果不相等,意味着用户想要取消同步操作,调用GTaskSyncService.cancelSync方法,并传入当前Activity实例,执行取消正在进行的同步任务等相关逻辑。 + GTaskSyncService.cancelSync(this); + } + } else { + // 如果当前不处于同步模式,调用startPreferenceActivity方法,跳转到与偏好设置相关的Activity(可能是用于配置同步账号等同步相关设置的界面),方便用户进行同步相关的前置设置。 + startPreferenceActivity(); + } + break; + } + // 当点击的菜单项ID为R.id.menu_setting时,表明用户点击了“设置”菜单项,要进入应用的设置页面。 + case R.id.menu_setting: { + // 调用startPreferenceActivity方法,跳转到相关的设置Activity(NotesPreferenceActivity),该Activity可能包含各种应用相关的设置选项,供用户进行个性化配置等操作。 + startPreferenceActivity(); + break; + } + // 当点击的菜单项ID为R.id.menu_new_note时,对应“新建笔记”操作,用户希望创建一个新的笔记。 + case R.id.menu_new_note: { + // 调用createNewNote方法来创建新笔记,比如会跳转到笔记编辑页面等操作,具体创建笔记的详细逻辑在createNewNote方法中实现。 + createNewNote(); + break; + } + // 当点击的菜单项ID为R.id.menu_search时,意味着用户发起了搜索操作,希望在笔记中查找特定内容等。 + case R.id.menu_search: + // 调用onSearchRequested方法来处理具体的搜索请求逻辑,该方法内部可能会启动搜索界面、传递搜索参数等操作,以实现笔记内容的搜索功能。 + onSearchRequested(); + break; + // 如果点击的菜单项ID不属于上述已处理的任何情况,则不执行任何额外操作,直接跳出switch语句,结束本次菜单项选择事件的处理。 + default: + break; + } + // 返回true表示已经成功处理了该菜单项选择事件,告知系统此事件已被正确响应,不需要再进行默认的其他处理逻辑了。 + return true; + } + // 重写的用于处理搜索请求的方法,调用startSearch方法来启动搜索相关的操作,传递相应的参数以实现笔记内容搜索功能,并返回true表示搜索请求已被处理。 + @Override + public boolean onSearchRequested() { + // 调用startSearch方法,传入多个参数来配置搜索行为。这里传入null表示不使用特定的初始查询字符串(即搜索框为空开始搜索);false表示不限制搜索范围为全局(可能只在当前显示的笔记内容等范围内搜索,具体看startSearch方法内部实现); + // 第三个参数传入null表示不使用额外的应用数据来辅助搜索;最后一个false表示不执行其他特定的搜索相关配置(同样具体看startSearch方法内部实现),通过这样调用启动搜索操作流程。 + startSearch(null, false, null /* appData */, false); + // 返回true告知系统搜索请求已被成功处理,后续系统可以根据此返回值进行相应的界面更新等操作,例如显示搜索结果界面等。 + return true; + } + // 用于将笔记内容导出为文本文件的方法,通过异步任务(AsyncTask)在后台执行导出操作,并根据导出结果在主线程中展示不同的提示信息给用户,告知导出是否成功以及相关情况。 + private void exportNoteToText() { + // 获取BackupUtils的单例实例,传入当前的NotesListActivity实例作为上下文参数,BackupUtils类可能封装了与备份、导出笔记相关的各种实用方法,用于后续的导出文本操作。 + final BackupUtils backup = BackupUtils.getInstance(NotesListActivity.this); + // 创建一个异步任务(AsyncTask)实例,用于在后台线程执行导出笔记到文本的耗时操作,泛型参数分别表示传入参数类型(这里不需要传入参数,所以是Void)、后台任务执行进度参数类型(同样不需要,为Void)以及后台任务执行完成后的返回结果类型(这里返回一个整数类型的结果,用于表示不同的导出状态)。 + new AsyncTask() { + + // 在后台线程中执行的方法,用于实际调用BackupUtils的exportToText方法来执行笔记导出为文本的核心操作,并返回导出操作的结果状态码,该状态码会在后续的onPostExecute方法中用于判断并展示相应的提示信息给用户。 + @Override + protected Integer doInBackground(Void... unused) { + return backup.exportToText(); + } + + // 在后台任务执行完成后,在主线程中执行的方法,根据后台任务返回的结果状态码(result)来展示不同的提示信息给用户,告知导出操作的最终情况,比如成功、失败及失败原因等。 + @Override + protected void onPostExecute(Integer result) { + // 如果返回的结果状态码等于BackupUtils.STATE_SD_CARD_UNMOUONTED,表示SD卡处于未挂载状态,导致导出操作无法正常进行。 + if (result == BackupUtils.STATE_SD_CARD_UNMOUONTED) { + // 创建一个AlertDialog的构建器(AlertDialog.Builder)实例,用于构建一个提示对话框,传入当前的NotesListActivity实例作为上下文,以便对话框能正确显示和与当前Activity交互。 + AlertDialog.Builder builder = new AlertDialog.Builder(NotesListActivity.this); + // 设置对话框的标题,从当前Activity的资源文件中获取对应的字符串作为标题文本,这里获取的字符串对应的文本应该是类似“SD卡导出失败”这样提示用户导出操作失败原因的文字(具体看资源文件中定义的getString(R.string.failed_sdcard_export)内容)。 + builder.setTitle(NotesListActivity.this.getString(R.string.failed_sdcard_export)); + // 设置对话框的消息内容,同样从资源文件中获取对应的字符串作为消息文本,这里获取的字符串对应的文本应该是类似“SD卡未挂载,请挂载后再试”这样进一步解释失败原因的文字(具体看资源文件中定义的getString(R.string.error_sdcard_unmounted)内容)。 + builder.setMessage(NotesListActivity.this.getString(R.string.error_sdcard_unmounted)); + // 设置对话框的确认按钮(通常是“确定”“OK”之类的按钮),传入null表示不设置额外的点击监听器逻辑,点击确认按钮则直接关闭对话框,仅起到提示用户的作用。 + builder.setPositiveButton(android.R.string.ok, null); + // 通过构建器创建并显示对话框,将导出失败原因等信息展示给用户知晓。 + builder.show(); + } else if (result == BackupUtils.STATE_SUCCESS) { + // 如果返回的结果状态码等于BackupUtils.STATE_SUCCESS,表示笔记导出为文本文件操作成功。 + AlertDialog.Builder builder = new AlertDialog.Builder(NotesListActivity.this); + builder.setTitle(NotesListActivity.this.getString(R.string.success_sdcard_export)); + // 设置对话框的消息内容,从资源文件中获取对应的格式化字符串作为消息文本,并传入备份工具(backup)中获取的导出文本文件名(backup.getExportedTextFileName())和导出文本文件所在目录(backup.getExportedTextFileDir())作为参数, + // 生成类似“导出文件已成功保存到[文件目录]/[文件名]”这样告知用户导出文件位置的提示信息,方便用户查找导出的文件。 + 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) { + // 如果返回的结果状态码等于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(); + } + // 用于判断当前是否处于同步模式的方法,通过获取同步账号名称并检查其长度是否大于0来确定是否处于同步模式,如果有同步账号名称(长度大于0)则认为处于同步模式,否则不是。 + private boolean isSyncMode() { + // 调用NotesPreferenceActivity的getSyncAccountName方法,传入当前Activity实例(this)作为参数,获取同步账号名称,该方法可能是从应用的偏好设置等地方读取配置的同步账号相关信息。 + return NotesPreferenceActivity.getSyncAccountName(this).trim().length() > 0; + } + // 用于启动与偏好设置相关的Activity(NotesPreferenceActivity)的方法,先确定启动的Activity所在的上下文(可能是当前Activity或者其父Activity),然后创建意图(Intent)并启动对应的Activity,方便用户进行各种应用设置操作。 + private void startPreferenceActivity() { + // 判断当前Activity是否有父Activity(getParent()方法返回不为null),如果有则将Activity from设置为父Activity,否则设置为当前Activity本身,这样确定了启动目标Activity的上下文。 + Activity from = getParent()!= null? getParent() : this; + // 创建一个意图(Intent),指定目标Activity为NotesPreferenceActivity.class,表明要跳转到这个用于偏好设置的Activity,用户可以在其中进行诸如同步账号配置、应用其他个性化设置等操作。 + Intent intent = new Intent(from, NotesPreferenceActivity.class); + // 通过确定好的上下文(from)启动这个意图对应的Activity,如果该Activity已经存在且处于合适的状态(比如在栈顶等情况),则会根据需要进行相应的复用或重新启动等操作,传入的请求码-1表示不使用特定的请求码(通常在需要接收返回结果时会使用特定请求码来区分不同的启动请求,这里不需要接收返回结果所以用-1)。 + from.startActivityIfNeeded(intent, -1); + } +// 这是一个实现了 `OnItemClickListener` 接口的内部类,用于处理列表项(如 `ListView` 中的各项)被点击时的相关逻辑, +// 根据列表项对应的笔记数据类型以及当前列表所处的编辑状态等条件,来决定具体要执行的操作,比如切换笔记选中状态、打开文件夹或者打开笔记详情页面等操作。 +private class OnListItemClickListener implements OnItemClickListener { + + // 实现 `OnItemClickListener` 接口中的方法,当列表项被点击时会触发该方法执行,用于处理点击事件的具体逻辑。 + public void onItemClick(AdapterView parent, View view, int position, long id) { + // 首先判断被点击的视图(view)是否属于 `NotesListItem` 类型,只有是这种类型才能从中获取到对应的笔记数据信息进行后续合理的操作判断,如果不是该类型则直接忽略此次点击操作,不做进一步处理。 + if (view instanceof NotesListItem) { + // 从 `NotesListItem` 类型的视图中获取对应的笔记数据对象(`NoteItemData`),这个对象包含了笔记相关的各类属性信息,例如笔记的类型(是文件夹、普通笔记还是其他特殊类型等)、笔记的唯一标识(ID)以及可能的摘要等内容,后续的操作逻辑会依据这些属性来决定具体行为。 + NoteItemData item = ((NotesListItem) view).getItemData(); + // 判断笔记列表适配器(`mNotesListAdapter`)是否处于选择模式(比如多选模式等情况,通过 `isInChoiceMode` 方法来判断),如果处于选择模式,则进一步判断点击的笔记数据项类型是否为普通笔记类型(`Notes.TYPE_NOTE`),若是普通笔记类型则执行以下操作。 + if (mNotesListAdapter.isInChoiceMode()) { + if (item.getType() == Notes.TYPE_NOTE) { + // 由于列表视图(`mNotesListView`)可能存在头部视图(比如一些用于展示标题、提示信息等的固定视图部分),这些头部视图不属于实际的笔记数据项, + // 所以需要对位置参数(`position`)进行调整,减去头部视图的数量(通过 `mNotesListView.getHeaderViewsCount` 方法获取),从而得到在实际笔记数据列表中的准确位置索引,方便后续进行正确的选中状态等相关操作处理。 + position = position - mNotesListView.getHeaderViewsCount(); + // 调用 `mModeCallBack` 的 `onItemCheckedStateChanged` 方法,传入相关参数来更新当前笔记项的选中状态。这里传入的参数表示将当前笔记项的选中状态设置为与原来相反的状态(通过 `!mNotesListAdapter.isSelectedItem(position)` 取反操作实现), + // 以此来实现多选模式下点击笔记项切换其选中与否状态的功能,具体的选中状态更新逻辑在 `mModeCallBack` 的 `onItemCheckedStateChanged` 方法内部实现。 + mModeCallBack.onItemCheckedStateChanged(null, position, id, + !mNotesListAdapter.isSelectedItem(position)); + } + // 如果处于选择模式且点击的是笔记项,在处理完选中状态切换等相关操作后,直接返回,不再执行后续根据列表编辑状态进行判断的其他操作逻辑了,因为在选择模式下主要就是处理选中相关的操作行为。 + return; + } + + // 根据当前列表的编辑状态(`mState`)来执行不同的操作逻辑判断,以决定针对点击的笔记数据项具体要执行何种操作,例如打开文件夹、打开笔记详情页面等操作。 + switch (mState) { + // 当列表编辑状态为 `NOTE_LIST`(表示当前处于普通笔记列表状态,通常展示所有笔记的列表情况)时,进行以下操作判断。 + case NOTE_LIST: + // 判断笔记数据项的类型,如果是 `Notes.TYPE_FOLDER`(表示该项对应的是一个文件夹,可能点击后会展示文件夹内包含的笔记等相关内容)或者 `Notes.TYPE_SYSTEM`(可能代表系统相关的特殊类型文件夹等情况,点击后有对应的系统相关操作逻辑), + // 则调用 `openFolder` 方法并传入该笔记数据项(`item`),执行打开对应的文件夹操作,具体打开文件夹后展示什么内容、如何展示等详细逻辑在 `openFolder` 方法中实现。 + if (item.getType() == Notes.TYPE_FOLDER + || item.getType() == Notes.TYPE_SYSTEM) { + openFolder(item); + } else if (item.getType() == Notes.TYPE_NOTE) { + // 如果笔记数据项的类型是 `Notes.TYPE_NOTE`(表示该项对应的是一个普通笔记,点击后通常会进入笔记详情页面查看或编辑笔记内容等),则调用 `openNode` 方法并传入该笔记数据项,执行打开对应的笔记节点操作,具体的打开笔记节点后展示笔记详情、支持哪些编辑操作等逻辑在 `openNode` 方法中实现。 + openNode(item); + } else { + // 如果笔记数据项的类型不属于上述已知的几种类型(即既不是文件夹类型也不是普通笔记类型等正常预期的类型),则记录一条错误日志,提示在普通笔记列表状态下出现了错误的笔记类型,方便后续排查问题, + // 因为理论上在这个状态下列表中出现的数据项应该是常见的文件夹或者笔记类型的数据项。 + Log.e(TAG, "Wrong note type in NOTE_LIST"); + } + break; + // 当列表编辑状态为 `SUB_FOLDER`(表示当前处于子文件夹状态,比如在某个文件夹下的子文件夹层级中)或者 `CALL_RECORD_FOLDER`(表示处于通话记录文件夹状态,可能是专门存放通话记录相关笔记的文件夹层级)时,进行以下操作判断。 + case SUB_FOLDER: + case CALL_RECORD_FOLDER: + // 在这两种状态下,如果笔记数据项的类型是 `Notes.TYPE_NOTE`(表示是普通笔记类型),则调用 `openNode` 方法并传入该笔记数据项,执行打开对应的笔记节点操作, + // 因为在子文件夹或者通话记录文件夹状态下点击普通笔记,通常就是查看或编辑该笔记的具体内容,具体的打开笔记节点逻辑在 `openNode` 方法中实现。 + if (item.getType() == Notes.TYPE_NOTE) { + openNode(item); + } else { + // 如果笔记数据项的类型不是普通笔记类型,记录一条错误日志,提示在子文件夹或者通话记录文件夹状态下出现了错误的笔记类型,便于后续排查问题, + // 因为在这两种状态下点击的通常应该是笔记类型的数据项才符合预期操作逻辑。 + Log.e(TAG, "Wrong note type in SUB_FOLDER"); + } + break; + // 如果当前列表编辑状态是其他未处理的情况(即不属于上述列举的 `NOTE_LIST`、`SUB_FOLDER`、`CALL_RECORD_FOLDER` 等状态),则不执行任何额外操作,直接跳出 `switch` 语句,结束本次列表项点击事件的处理逻辑。 + default: + break; + } + } + } +}// 该方法用于启动对目标文件夹的查询操作,通过构建合适的查询条件(`selection`),并利用 `mBackgroundQueryHandler` 来发起查询请求,以获取符合条件的文件夹相关数据信息。 +private void startQueryDestinationFolders() { + // 首先构建一个基础的查询条件字符串(`selection`),用于筛选出特定类型、满足一定父级文件夹 ID 和自身 ID 条件的文件夹数据。 + // 这里的查询条件表示筛选出类型等于某个指定值(通过占位符 `?` 表示,后续会填充具体值)、父级文件夹 ID 不等于某个指定值、自身 ID 也不等于某个指定值的文件夹记录, + // 具体的这些占位符对应的实际值会在后面根据不同情况进行设置,以此来精确查询出期望的文件夹数据集合。 + String selection = NoteColumns.TYPE + "=? AND " + NoteColumns.PARENT_ID + "<>? AND " + NoteColumns.ID + "<>?"; + // 根据当前列表的编辑状态(`mState`)来进一步调整查询条件(`selection`)。如果当前处于 `ListEditState.NOTE_LIST`(普通笔记列表状态),则直接使用前面构建的基础查询条件即可; + // 如果不是普通笔记列表状态,则需要在基础查询条件的基础上添加额外的逻辑,即添加一个 `OR` 条件,表示还需要包含自身 ID 等于根文件夹 ID(`Notes.ID_ROOT_FOLDER`)的文件夹记录, + // 这样可以确保在不同的列表编辑状态下都能查询到合适的目标文件夹数据,满足相应的业务需求。 + selection = (mState == ListEditState.NOTE_LIST)? selection : + "(" + selection + ") OR (" + NoteColumns.ID + "=" + Notes.ID_ROOT_FOLDER + ")"; + + // 使用 `mBackgroundQueryHandler` 来启动查询操作,它可能是一个用于在后台线程执行查询任务的处理器对象,避免查询操作阻塞主线程影响界面响应等。 + // 传入的 `FOLDER_LIST_QUERY_TOKEN` 可能是一个用于标识此次查询任务的唯一令牌(token),方便后续对该查询任务进行区分、跟踪或者取消等操作; + // `null` 参数可能表示不需要传递额外的查询相关的参数对象(具体看 `mBackgroundQueryHandler` 的实现要求); + // `Notes.CONTENT_NOTE_URI` 是查询操作要针对的内容提供器(Content Provider)的统一资源标识符(URI),表明从哪里获取文件夹相关的数据信息,这里可能是与笔记应用相关的数据库中存储文件夹数据的位置; + // `FoldersListAdapter.PROJECTION` 是一个定义了要查询返回的列的数组,指定了查询结果中具体包含哪些文件夹相关的字段信息(比如文件夹名称、创建时间等); + // 接着传入前面构建好的查询条件(`selection`)以及对应查询条件占位符的值数组,这里分别传入文件夹类型值(`Notes.TYPE_FOLDER`)、回收站文件夹 ID(`Notes.ID_TRASH_FOLER`)以及当前所在文件夹的 ID(`mCurrentFolderId`)对应的字符串表示, + // 最后传入 `NoteColumns.MODIFIED_DATE + " DESC"` 表示按照文件夹的修改日期字段进行降序排序,使得查询结果中最新修改的文件夹会排在前面,方便展示最新的文件夹信息等。 + 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) { + // 首先判断被长按的视图(`view`)是否属于 `NotesListItem` 类型,只有是这种类型才能从中获取到对应的笔记数据信息进行后续合理的操作判断,如果不是该类型则直接忽略此次长按操作,不做进一步处理。 + if (view instanceof NotesListItem) { + // 从 `NotesListItem` 类型的视图中获取对应的笔记数据对象(`mFocusNoteDataItem`),后续会根据这个笔记数据对象的类型等属性来决定具体要执行的操作逻辑。 + mFocusNoteDataItem = ((NotesListItem) view).getItemData(); + // 判断获取到的笔记数据对象的类型是否为普通笔记类型(`Notes.TYPE_NOTE`),并且判断笔记列表适配器(`mNotesListAdapter`)是否不处于选择模式(即不是多选等选择状态),如果满足这两个条件,则执行以下操作。 + if (mFocusNoteDataItem.getType() == Notes.TYPE_NOTE &&!mNotesListAdapter.isInChoiceMode()) { + // 尝试通过 `mNotesListView` 启动一个操作模式(`startActionMode`),并传入 `mModeCallBack` 对象作为参数,这个操作模式可能是用于进入多选模式等相关操作场景的, + // 如果启动操作模式成功(即 `startActionMode` 方法返回值不为 `null`),则调用 `mModeCallBack` 的 `onItemCheckedStateChanged` 方法,传入相关参数来将当前长按的笔记项设置为选中状态(传入 `true` 表示选中), + // 同时让列表视图(`mNotesListView`)执行触觉反馈(通过 `performHapticFeedback` 方法传入 `HapticFeedbackConstants.LONG_PRESS` 表示长按的触觉反馈类型),给用户一个长按操作被响应的震动等触觉提示,增强交互体验。 + if (mNotesListView.startActionMode(mModeCallBack)!= null) { + mModeCallBack.onItemCheckedStateChanged(null, position, id, true); + mNotesListView.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS); + } else { + // 如果启动操作模式失败(`startActionMode` 方法返回 `null`),则记录一条错误日志,提示启动操作模式失败,方便后续排查问题,看是哪里出现了异常导致无法进入期望的操作模式。 + Log.e(TAG, "startActionMode fails"); + } + } else if (mFocusNoteDataItem.getType() == Notes.TYPE_FOLDER) { + // 如果笔记数据对象的类型是 `Notes.TYPE_FOLDER`(表示长按的是一个文件夹),则将列表视图(`mNotesListView`)的上下文菜单创建监听器(`OnCreateContextMenuListener`)设置为 `mFolderOnCreateContextMenuListener`, + // 这样当长按文件夹时,就会触发创建并显示与文件夹相关的上下文菜单(比如包含查看、删除、重命名文件夹等操作选项的菜单),具体的上下文菜单创建及显示逻辑在 `mFolderOnCreateContextMenuListener` 对应的代码中实现。 + mNotesListView.setOnCreateContextMenuListener(mFolderOnCreateContextMenuListener); + } + } + // 返回 `false` 表示此次长按事件的默认行为(比如是否继续传播长按事件等情况,具体看调用该方法的父类或相关框架的处理逻辑)不需要执行,即不进行默认的长按后续处理了,因为已经在该方法内按照业务需求处理了长按相关的操作逻辑。 + return false; +} \ No newline at end of file