From 08ab1fc652efcf6882e86265b0c54d8164556b83 Mon Sep 17 00:00:00 2001 From: JTXjtx Date: Tue, 23 Dec 2025 20:37:40 +0800 Subject: [PATCH 1/3] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E5=AF=B9=E4=BA=8E?= =?UTF-8?q?=E5=B0=8F=E7=B1=B3=E4=BE=BF=E7=AD=BE=E5=BC=80=E6=BA=90=E4=BB=A3?= =?UTF-8?q?=E7=A0=81=E7=9A=84=E6=B3=A8=E9=87=8A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/net/micode/notes/data/Contact.java | 58 +++ .../src/net/micode/notes/data/Notes.java | 78 +++- .../notes/data/NotesDatabaseHelper.java | 236 ++++++++++++ .../net/micode/notes/data/NotesProvider.java | 218 ++++++++++- .../net/micode/notes/gtask/data/MetaData.java | 78 ++++ .../src/net/micode/notes/gtask/data/Node.java | 144 +++++++ .../net/micode/notes/gtask/data/SqlData.java | 80 ++++ .../net/micode/notes/gtask/data/SqlNote.java | 165 +++++++- .../src/net/micode/notes/gtask/data/Task.java | 148 ++++++++ .../net/micode/notes/gtask/data/TaskList.java | 167 ++++++++ .../exception/ActionFailureException.java | 22 ++ .../exception/NetworkFailureException.java | 22 ++ .../notes/gtask/remote/GTaskASyncTask.java | 117 +++++- .../notes/gtask/remote/GTaskClient.java | 206 ++++++++++ .../notes/gtask/remote/GTaskManager.java | 59 ++- .../notes/gtask/remote/GTaskSyncService.java | 113 ++++++ .../src/net/micode/notes/model/Note.java | 186 ++++++++- .../net/micode/notes/model/WorkingNote.java | 270 ++++++++++++- .../net/micode/notes/tool/BackupUtils.java | 130 ++++++- .../src/net/micode/notes/tool/DataUtils.java | 146 ++++++- .../micode/notes/tool/GTaskStringUtils.java | 54 ++- .../net/micode/notes/tool/ResourceParser.java | 146 ++++++- .../micode/notes/ui/AlarmAlertActivity.java | 114 +++++- .../micode/notes/ui/AlarmInitReceiver.java | 81 +++- .../net/micode/notes/ui/AlarmReceiver.java | 41 +- .../net/micode/notes/ui/DateTimePicker.java | 264 ++++++++++--- .../micode/notes/ui/DateTimePickerDialog.java | 89 +++++ .../src/net/micode/notes/ui/DropdownMenu.java | 48 +++ .../micode/notes/ui/FoldersListAdapter.java | 77 +++- .../net/micode/notes/ui/NoteEditActivity.java | 323 +++++++++++++++- .../src/net/micode/notes/ui/NoteEditText.java | 143 ++++++- .../src/net/micode/notes/ui/NoteItemData.java | 178 ++++++++- .../micode/notes/ui/NotesListActivity.java | 356 +++++++++++++++++- .../net/micode/notes/ui/NotesListAdapter.java | 107 ++++++ .../net/micode/notes/ui/NotesListItem.java | 27 ++ .../notes/ui/NotesPreferenceActivity.java | 190 ++++++++++ 36 files changed, 4735 insertions(+), 146 deletions(-) diff --git a/src/Notes-master/src/net/micode/notes/data/Contact.java b/src/Notes-master/src/net/micode/notes/data/Contact.java index d97ac5d..da3a693 100644 --- a/src/Notes-master/src/net/micode/notes/data/Contact.java +++ b/src/Notes-master/src/net/micode/notes/data/Contact.java @@ -25,10 +25,52 @@ import android.util.Log; import java.util.HashMap; +/** + * 联系人信息查询工具类 + *

+ * 提供根据电话号码查询联系人姓名的功能,使用缓存机制提高查询效率。 + * 通过Android系统的ContactsContract Provider查询联系人信息。 + *

+ *

+ * 主要功能: + *

+ *

+ *

+ * 使用场景: + * 当笔记中包含电话号码时,使用此类查询对应的联系人姓名并显示。 + *

+ * + * @see ContactsContract + * @see PhoneNumberUtils + */ public class Contact { + /** + * 联系人信息缓存 + *

+ * 使用HashMap存储已查询的电话号码和对应的联系人姓名, + * 避免重复查询系统联系人数据库,提高性能。 + *

+ * Key: 电话号码 + * Value: 联系人姓名 + */ private static HashMap sContactCache; + /** + * 日志标签 + */ private static final String TAG = "Contact"; + /** + * 查询联系人的SQL选择条件 + *

+ * 使用PHONE_NUMBERS_EQUAL函数进行号码匹配,支持国际号码格式。 + * 只查询电话号码类型的数据(Phone.CONTENT_ITEM_TYPE)。 + * 使用min_match='+'进行最小匹配,提高查询效率。 + *

+ */ private static final String CALLER_ID_SELECTION = "PHONE_NUMBERS_EQUAL(" + Phone.NUMBER + ",?) AND " + Data.MIMETYPE + "='" + Phone.CONTENT_ITEM_TYPE + "'" + " AND " + Data.RAW_CONTACT_ID + " IN " @@ -36,15 +78,30 @@ public class Contact { + " FROM phone_lookup" + " WHERE min_match = '+')"; + /** + * 根据电话号码获取联系人姓名 + *

+ * 首先检查缓存中是否已存在该号码对应的联系人姓名, + * 如果存在则直接返回,否则查询系统联系人数据库。 + * 查询结果会被缓存以提高后续查询效率。 + *

+ * + * @param context 应用上下文,用于访问ContentResolver + * @param phoneNumber 要查询的电话号码 + * @return 联系人姓名,如果未找到则返回null + */ public static String getContact(Context context, String phoneNumber) { + // 初始化缓存 if(sContactCache == null) { sContactCache = new HashMap(); } + // 检查缓存中是否已存在 if(sContactCache.containsKey(phoneNumber)) { return sContactCache.get(phoneNumber); } + // 构建查询条件,使用toCallerIDMinMatch进行号码最小匹配 String selection = CALLER_ID_SELECTION.replace("+", PhoneNumberUtils.toCallerIDMinMatch(phoneNumber)); Cursor cursor = context.getContentResolver().query( @@ -54,6 +111,7 @@ public class Contact { new String[] { phoneNumber }, null); + // 处理查询结果 if (cursor != null && cursor.moveToFirst()) { try { String name = cursor.getString(0); diff --git a/src/Notes-master/src/net/micode/notes/data/Notes.java b/src/Notes-master/src/net/micode/notes/data/Notes.java index f240604..8b1f853 100644 --- a/src/Notes-master/src/net/micode/notes/data/Notes.java +++ b/src/Notes-master/src/net/micode/notes/data/Notes.java @@ -17,33 +17,103 @@ package net.micode.notes.data; import android.net.Uri; +/** + * 笔记数据常量定义类 + *

+ * 定义了笔记应用中使用的所有常量、接口和内部类,包括: + *

    + *
  • Content Provider的Authority和URI
  • + *
  • 笔记类型常量(普通笔记、文件夹、系统文件夹)
  • + *
  • 系统文件夹ID常量
  • + *
  • Intent Extra键常量
  • + *
  • Widget类型常量
  • + *
  • 笔记数据列接口(NoteColumns、DataColumns)
  • + *
  • 文本笔记和通话记录笔记内部类
  • + *
+ *

+ *

+ * 该类主要用于定义数据库表结构和Content Provider的契约, + * 提供统一的常量访问接口,方便应用各模块使用。 + *

+ */ public class Notes { + /** + * Content Provider的Authority + */ 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; /** - * Following IDs are system folders' identifiers - * {@link Notes#ID_ROOT_FOLDER } is default folder - * {@link Notes#ID_TEMPARAY_FOLDER } is for notes belonging no folder - * {@link Notes#ID_CALL_RECORD_FOLDER} is to store call records + * 以下ID是系统文件夹的标识符 + * {@link Notes#ID_ROOT_FOLDER } 是默认文件夹 + * {@link Notes#ID_TEMPARAY_FOLDER } 用于不属于任何文件夹的笔记 + * {@link Notes#ID_CALL_RECORD_FOLDER} 用于存储通话记录 */ public static final int ID_ROOT_FOLDER = 0; + /** + * 临时文件夹ID,用于不属于任何文件夹的笔记 + */ public static final int ID_TEMPARAY_FOLDER = -1; + /** + * 通话记录文件夹ID,用于存储通话记录 + */ public static final int ID_CALL_RECORD_FOLDER = -2; + /** + * 回收站文件夹ID,用于存储已删除的笔记 + */ public static final int ID_TRASH_FOLER = -3; + /** + * Intent Extra键:提醒日期 + */ public static final String INTENT_EXTRA_ALERT_DATE = "net.micode.notes.alert_date"; + /** + * Intent Extra键:背景颜色ID + */ public static final String INTENT_EXTRA_BACKGROUND_ID = "net.micode.notes.background_color_id"; + /** + * Intent Extra键:Widget ID + */ public static final String INTENT_EXTRA_WIDGET_ID = "net.micode.notes.widget_id"; + /** + * Intent Extra键:Widget类型 + */ public static final String INTENT_EXTRA_WIDGET_TYPE = "net.micode.notes.widget_type"; + /** + * Intent Extra键:文件夹ID + */ public static final String INTENT_EXTRA_FOLDER_ID = "net.micode.notes.folder_id"; + /** + * Intent Extra键:通话日期 + */ public static final String INTENT_EXTRA_CALL_DATE = "net.micode.notes.call_date"; + /** + * 无效的Widget类型 + */ public static final int TYPE_WIDGET_INVALIDE = -1; + /** + * 2x2 Widget类型 + */ public static final int TYPE_WIDGET_2X = 0; + /** + * 4x4 Widget类型 + */ public static final int TYPE_WIDGET_4X = 1; public static class DataConstants { diff --git a/src/Notes-master/src/net/micode/notes/data/NotesDatabaseHelper.java b/src/Notes-master/src/net/micode/notes/data/NotesDatabaseHelper.java index ffe5d57..558caf7 100644 --- a/src/Notes-master/src/net/micode/notes/data/NotesDatabaseHelper.java +++ b/src/Notes-master/src/net/micode/notes/data/NotesDatabaseHelper.java @@ -27,21 +27,114 @@ import net.micode.notes.data.Notes.DataConstants; import net.micode.notes.data.Notes.NoteColumns; +/** + * 笔记数据库帮助类 + *

+ * 继承自SQLiteOpenHelper,负责笔记应用SQLite数据库的创建、升级和管理。 + * 管理两个主要数据表:note表(存储笔记和文件夹信息)和data表(存储笔记的详细内容)。 + * 使用数据库触发器自动维护笔记计数、内容同步等关联关系。 + *

+ *

+ * 主要功能: + *

    + *
  • 创建和升级数据库表结构
  • + *
  • 创建和管理数据库触发器
  • + *
  • 维护系统文件夹(通话记录、根文件夹、临时文件夹、回收站)
  • + *
  • 支持数据库版本升级(当前版本:4)
  • + *
  • 提供单例模式访问数据库帮助类实例
  • + *
+ *

+ *

+ * 数据库版本历史: + *

    + *
  • V1: 初始版本
  • + *
  • V2: 重构表结构
  • + *
  • V3: 添加GTASK_ID列和回收站文件夹
  • + *
  • V4: 添加VERSION列
  • + *
+ *

+ * + * @see SQLiteOpenHelper + * @see Notes + */ public class NotesDatabaseHelper extends SQLiteOpenHelper { + /** + * 数据库文件名 + */ private static final String DB_NAME = "note.db"; + /** + * 数据库版本号 + *

+ * 当前数据库版本为4,用于跟踪数据库结构变更。 + * 当数据库版本变更时,onUpgrade方法会被调用以执行升级逻辑。 + *

+ */ private static final int DB_VERSION = 4; + /** + * 数据库表名常量接口 + */ public interface TABLE { + /** + * 笔记表名 + *

+ * 存储笔记和文件夹的基本信息,包括ID、父文件夹ID、创建时间、修改时间、 + * 背景颜色、提醒时间、附件状态、笔记数量、摘要、类型、Widget信息、 + * 同步ID、本地修改状态、原始父文件夹ID、GTASK ID、版本等字段。 + *

+ */ public static final String NOTE = "note"; + /** + * 数据表名 + *

+ * 存储笔记的详细内容,支持多种MIME类型(文本、图片、附件等)。 + * 每条数据记录关联到一条笔记,包含MIME类型、内容、以及5个通用数据字段。 + *

+ */ public static final String DATA = "data"; } + /** + * 日志标签 + */ private static final String TAG = "NotesDatabaseHelper"; + /** + * 数据库帮助类单例实例 + *

+ * 使用单例模式确保全局只有一个数据库帮助类实例, + * 避免多个实例同时操作数据库导致的数据不一致问题。 + *

+ */ private static NotesDatabaseHelper mInstance; + /** + * 创建笔记表的SQL语句 + *

+ * 创建note表,包含以下字段: + *

    + *
  • ID: 主键,自增
  • + *
  • PARENT_ID: 父文件夹ID,默认为0
  • + *
  • ALERTED_DATE: 提醒时间,默认为0
  • + *
  • BG_COLOR_ID: 背景颜色ID,默认为0
  • + *
  • CREATED_DATE: 创建时间,默认为当前时间戳
  • + *
  • HAS_ATTACHMENT: 是否有附件,默认为0
  • + *
  • MODIFIED_DATE: 修改时间,默认为当前时间戳
  • + *
  • NOTES_COUNT: 笔记数量,默认为0(仅文件夹有效)
  • + *
  • SNIPPET: 笔记摘要,默认为空字符串
  • + *
  • TYPE: 类型(0=普通笔记,1=文件夹,2=系统),默认为0
  • + *
  • WIDGET_ID: Widget ID,默认为0
  • + *
  • WIDGET_TYPE: Widget类型,默认为-1
  • + *
  • SYNC_ID: 同步ID,默认为0
  • + *
  • LOCAL_MODIFIED: 本地修改标志,默认为0
  • + *
  • ORIGIN_PARENT_ID: 原始父文件夹ID,默认为0
  • + *
  • GTASK_ID: Google Tasks ID,默认为空字符串
  • + *
  • VERSION: 版本号,默认为0
  • + *
+ *

+ */ private static final String CREATE_NOTE_TABLE_SQL = "CREATE TABLE " + TABLE.NOTE + "(" + NoteColumns.ID + " INTEGER PRIMARY KEY," + @@ -63,6 +156,21 @@ public class NotesDatabaseHelper extends SQLiteOpenHelper { NoteColumns.VERSION + " INTEGER NOT NULL DEFAULT 0" + ")"; + /** + * 创建数据表的SQL语句 + *

+ * 创建data表,包含以下字段: + *

    + *
  • ID: 主键,自增
  • + *
  • MIME_TYPE: MIME类型,不能为空
  • + *
  • NOTE_ID: 关联的笔记ID,默认为0
  • + *
  • CREATED_DATE: 创建时间,默认为当前时间戳
  • + *
  • MODIFIED_DATE: 修改时间,默认为当前时间戳
  • + *
  • CONTENT: 内容,默认为空字符串
  • + *
  • DATA1-5: 通用数据字段,用于存储不同类型的数据
  • + *
+ *

+ */ private static final String CREATE_DATA_TABLE_SQL = "CREATE TABLE " + TABLE.DATA + "(" + DataColumns.ID + " INTEGER PRIMARY KEY," + @@ -78,6 +186,12 @@ public class NotesDatabaseHelper extends SQLiteOpenHelper { DataColumns.DATA5 + " TEXT NOT NULL DEFAULT ''" + ")"; + /** + * 创建数据表索引的SQL语句 + *

+ * 在data表的NOTE_ID字段上创建索引,提高按笔记ID查询数据的效率。 + *

+ */ private static final String CREATE_DATA_NOTE_ID_INDEX_SQL = "CREATE INDEX IF NOT EXISTS note_id_index ON " + TABLE.DATA + "(" + DataColumns.NOTE_ID + ");"; @@ -206,10 +320,23 @@ public class NotesDatabaseHelper extends SQLiteOpenHelper { " WHERE " + NoteColumns.PARENT_ID + "=old." + NoteColumns.ID + ";" + " END"; + /** + * 构造器 + * + * @param context 应用上下文 + */ public NotesDatabaseHelper(Context context) { super(context, DB_NAME, null, DB_VERSION); } + /** + * 创建笔记表 + *

+ * 执行创建note表的SQL语句,创建相关触发器,并初始化系统文件夹。 + *

+ * + * @param db SQLiteDatabase实例 + */ public void createNoteTable(SQLiteDatabase db) { db.execSQL(CREATE_NOTE_TABLE_SQL); reCreateNoteTableTriggers(db); @@ -217,7 +344,17 @@ public class NotesDatabaseHelper extends SQLiteOpenHelper { Log.d(TAG, "note table has been created"); } + /** + * 重新创建笔记表触发器 + *

+ * 先删除所有已存在的note表相关触发器,然后重新创建所有触发器。 + * 用于在数据库升级时更新触发器逻辑。 + *

+ * + * @param db SQLiteDatabase实例 + */ 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"); @@ -226,6 +363,7 @@ public class NotesDatabaseHelper extends SQLiteOpenHelper { 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_FOLDER_COUNT_ON_DELETE_TRIGGER); @@ -235,12 +373,27 @@ public class NotesDatabaseHelper extends SQLiteOpenHelper { db.execSQL(FOLDER_MOVE_NOTES_ON_TRASH_TRIGGER); } + /** + * 创建系统文件夹 + *

+ * 在note表中创建四个系统文件夹: + *

    + *
  • 通话记录文件夹(ID_CALL_RECORD_FOLDER)
  • + *
  • 根文件夹(ID_ROOT_FOLDER)
  • + *
  • 临时文件夹(ID_TEMPARAY_FOLDER)
  • + *
  • 回收站文件夹(ID_TRASH_FOLER)
  • + *
+ *

+ * + * @param db SQLiteDatabase实例 + */ private void createSystemFolder(SQLiteDatabase db) { ContentValues values = new ContentValues(); /** * call record foler for call notes */ + // 创建通话记录文件夹 values.put(NoteColumns.ID, Notes.ID_CALL_RECORD_FOLDER); values.put(NoteColumns.TYPE, Notes.TYPE_SYSTEM); db.insert(TABLE.NOTE, null, values); @@ -248,6 +401,7 @@ public class NotesDatabaseHelper extends SQLiteOpenHelper { /** * root folder which is default folder */ + // 创建根文件夹(默认文件夹) values.clear(); values.put(NoteColumns.ID, Notes.ID_ROOT_FOLDER); values.put(NoteColumns.TYPE, Notes.TYPE_SYSTEM); @@ -256,6 +410,7 @@ public class NotesDatabaseHelper extends SQLiteOpenHelper { /** * temporary folder which is used for moving note */ + // 创建临时文件夹(用于移动笔记) values.clear(); values.put(NoteColumns.ID, Notes.ID_TEMPARAY_FOLDER); values.put(NoteColumns.TYPE, Notes.TYPE_SYSTEM); @@ -264,12 +419,21 @@ public class NotesDatabaseHelper extends SQLiteOpenHelper { /** * create trash folder */ + // 创建回收站文件夹 values.clear(); values.put(NoteColumns.ID, Notes.ID_TRASH_FOLER); values.put(NoteColumns.TYPE, Notes.TYPE_SYSTEM); db.insert(TABLE.NOTE, null, values); } + /** + * 创建数据表 + *

+ * 执行创建data表的SQL语句,创建相关触发器,并创建索引。 + *

+ * + * @param db SQLiteDatabase实例 + */ public void createDataTable(SQLiteDatabase db) { db.execSQL(CREATE_DATA_TABLE_SQL); reCreateDataTableTriggers(db); @@ -277,16 +441,36 @@ public class NotesDatabaseHelper extends SQLiteOpenHelper { Log.d(TAG, "data table has been created"); } + /** + * 重新创建数据表触发器 + *

+ * 先删除所有已存在的data表相关触发器,然后重新创建所有触发器。 + * 用于在数据库升级时更新触发器逻辑。 + *

+ * + * @param db SQLiteDatabase实例 + */ 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); } + /** + * 获取数据库帮助类单例实例 + *

+ * 使用双重检查锁定模式确保线程安全的单例实现。 + *

+ * + * @param context 应用上下文 + * @return NotesDatabaseHelper单例实例 + */ static synchronized NotesDatabaseHelper getInstance(Context context) { if (mInstance == null) { mInstance = new NotesDatabaseHelper(context); @@ -294,45 +478,78 @@ public class NotesDatabaseHelper extends SQLiteOpenHelper { return mInstance; } + /** + * 创建数据库 + *

+ * 当数据库文件不存在时调用,创建note表和data表。 + *

+ * + * @param db SQLiteDatabase实例 + */ @Override public void onCreate(SQLiteDatabase db) { createNoteTable(db); createDataTable(db); } + /** + * 升级数据库 + *

+ * 当数据库版本号增加时调用,执行从旧版本到新版本的升级逻辑。 + * 支持增量升级,从当前版本逐步升级到目标版本。 + *

+ * + * @param db SQLiteDatabase实例 + * @param oldVersion 当前数据库版本号 + * @param newVersion 目标数据库版本号 + * @throws IllegalStateException 如果升级失败 + */ @Override public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { boolean reCreateTriggers = false; boolean skipV2 = false; + // 从V1升级到V2(包括V2到V3) if (oldVersion == 1) { upgradeToV2(db); skipV2 = true; // this upgrade including the upgrade from v2 to v3 oldVersion++; } + // 从V2升级到V3 if (oldVersion == 2 && !skipV2) { upgradeToV3(db); reCreateTriggers = true; oldVersion++; } + // 从V3升级到V4 if (oldVersion == 3) { upgradeToV4(db); oldVersion++; } + // 如果需要,重新创建触发器 if (reCreateTriggers) { reCreateNoteTableTriggers(db); reCreateDataTableTriggers(db); } + // 检查升级是否成功 if (oldVersion != newVersion) { throw new IllegalStateException("Upgrade notes database to version " + newVersion + "fails"); } } + /** + * 升级数据库到V2版本 + *

+ * 删除旧表并重新创建note表和data表。 + *

+ * + * @param db SQLiteDatabase实例 + */ private void upgradeToV2(SQLiteDatabase db) { db.execSQL("DROP TABLE IF EXISTS " + TABLE.NOTE); db.execSQL("DROP TABLE IF EXISTS " + TABLE.DATA); @@ -340,21 +557,40 @@ public class NotesDatabaseHelper extends SQLiteOpenHelper { createDataTable(db); } + /** + * 升级数据库到V3版本 + *

+ * 添加GTASK_ID列到note表,并创建回收站系统文件夹。 + *

+ * + * @param db SQLiteDatabase实例 + */ private void upgradeToV3(SQLiteDatabase db) { // drop unused triggers + // 删除未使用的触发器 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"); // add a column for gtask id + // 添加GTASK_ID列 db.execSQL("ALTER TABLE " + TABLE.NOTE + " ADD COLUMN " + NoteColumns.GTASK_ID + " TEXT NOT NULL DEFAULT ''"); // add a trash system folder + // 添加回收站系统文件夹 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); } + /** + * 升级数据库到V4版本 + *

+ * 添加VERSION列到note表,用于跟踪笔记版本。 + *

+ * + * @param db SQLiteDatabase实例 + */ private void upgradeToV4(SQLiteDatabase db) { db.execSQL("ALTER TABLE " + TABLE.NOTE + " ADD COLUMN " + NoteColumns.VERSION + " INTEGER NOT NULL DEFAULT 0"); diff --git a/src/Notes-master/src/net/micode/notes/data/NotesProvider.java b/src/Notes-master/src/net/micode/notes/data/NotesProvider.java index edb0a60..aa2cf34 100644 --- a/src/Notes-master/src/net/micode/notes/data/NotesProvider.java +++ b/src/Notes-master/src/net/micode/notes/data/NotesProvider.java @@ -35,21 +35,97 @@ import net.micode.notes.data.Notes.NoteColumns; import net.micode.notes.data.NotesDatabaseHelper.TABLE; +/** + * 笔记Content Provider + *

+ * 继承自ContentProvider,提供对笔记数据的增删改查(CRUD)操作。 + * 管理note表和data表的数据访问,支持URI匹配、数据查询、插入、更新和删除操作。 + * 同时提供搜索建议功能,支持全局搜索笔记内容。 + *

+ *

+ * 主要功能: + *

    + *
  • URI路由匹配,支持多种URI模式
  • + *
  • 笔记和数据的查询操作
  • + *
  • 笔记和数据的插入操作
  • + *
  • 笔记和数据的更新操作
  • + *
  • 笔记和数据的删除操作
  • + *
  • 全局搜索和搜索建议功能
  • + *
  • 数据变更通知
  • + *
  • 笔记版本号自动递增
  • + *
+ *

+ *

+ * 支持的URI模式: + *

    + *
  • content://micode_notes/note - 查询所有笔记
  • + *
  • content://micode_notes/note/# - 查询指定ID的笔记
  • + *
  • content://micode_notes/data - 查询所有数据
  • + *
  • content://micode_notes/data/# - 查询指定ID的数据
  • + *
  • content://micode_notes/search - 搜索笔记
  • + *
  • content://micode_notes/search_suggest_query - 搜索建议
  • + *
+ *

+ * + * @see ContentProvider + * @see NotesDatabaseHelper + * @see Notes + */ public class NotesProvider extends ContentProvider { + /** + * URI匹配器 + *

+ * 用于匹配不同的URI模式,将请求路由到对应的处理逻辑。 + * 支持笔记、数据、搜索等多种URI模式。 + *

+ */ private static final UriMatcher mMatcher; + /** + * 数据库帮助类实例 + *

+ * 用于获取可读和可写的SQLiteDatabase实例。 + *

+ */ private NotesDatabaseHelper mHelper; + /** + * 日志标签 + */ private static final String TAG = "NotesProvider"; + /** + * 笔记URI匹配码 + */ private static final int URI_NOTE = 1; + /** + * 笔记项URI匹配码 + */ private static final int URI_NOTE_ITEM = 2; + /** + * 数据URI匹配码 + */ private static final int URI_DATA = 3; + /** + * 数据项URI匹配码 + */ private static final int URI_DATA_ITEM = 4; + /** + * 搜索URI匹配码 + */ private static final int URI_SEARCH = 5; + /** + * 搜索建议URI匹配码 + */ private static final int URI_SEARCH_SUGGEST = 6; + /** + * URI匹配器初始化块 + *

+ * 初始化UriMatcher,注册所有支持的URI模式。 + *

+ */ static { mMatcher = new UriMatcher(UriMatcher.NO_MATCH); mMatcher.addURI(Notes.AUTHORITY, "note", URI_NOTE); @@ -62,8 +138,15 @@ public class NotesProvider extends ContentProvider { } /** - * x'0A' represents the '\n' character in sqlite. For title and content in the search result, - * we will trim '\n' and white space in order to show more information. + * 搜索结果投影 + *

+ * 定义搜索建议返回的列,包括笔记ID、文本内容、图标、Intent动作等。 + * 使用TRIM和REPLACE函数去除换行符和空白字符,以便更好地显示搜索结果。 + *

+ *

+ * x'0A'代表SQLite中的换行符'\n'。对于搜索结果中的标题和内容, + * 我们会去除换行符和空白字符,以显示更多信息。 + *

*/ private static final String NOTES_SEARCH_PROJECTION = NoteColumns.ID + "," + NoteColumns.ID + " AS " + SearchManager.SUGGEST_COLUMN_INTENT_EXTRA_DATA + "," @@ -73,18 +156,49 @@ public class NotesProvider extends ContentProvider { + "'" + Intent.ACTION_VIEW + "' AS " + SearchManager.SUGGEST_COLUMN_INTENT_ACTION + "," + "'" + Notes.TextNote.CONTENT_TYPE + "' AS " + SearchManager.SUGGEST_COLUMN_INTENT_DATA; + /** + * 笔记摘要搜索查询SQL语句 + *

+ * 搜索note表中SNIPPET字段包含指定关键词的笔记。 + * 排除回收站中的笔记(PARENT_ID不等于ID_TRASH_FOLER)。 + * 只搜索普通笔记(TYPE等于TYPE_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; + /** + * 创建Content Provider + *

+ * 初始化数据库帮助类实例。 + *

+ * + * @return true表示创建成功 + */ @Override public boolean onCreate() { mHelper = NotesDatabaseHelper.getInstance(getContext()); return true; } + /** + * 查询数据 + *

+ * 根据URI模式查询对应的数据表,支持笔记、数据、搜索等多种查询模式。 + * 对于搜索模式,使用LIKE模糊匹配查询笔记摘要。 + *

+ * + * @param uri 查询的URI + * @param projection 要查询的列数组 + * @param selection 查询条件 + * @param selectionArgs 查询条件参数 + * @param sortOrder 排序方式 + * @return 查询结果的Cursor对象 + * @throws IllegalArgumentException 如果URI模式不支持或搜索时指定了不允许的参数 + */ @Override public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) { @@ -93,25 +207,30 @@ public class NotesProvider extends ContentProvider { String id = null; switch (mMatcher.match(uri)) { case URI_NOTE: + // 查询所有笔记 c = db.query(TABLE.NOTE, projection, selection, selectionArgs, null, null, sortOrder); break; case URI_NOTE_ITEM: + // 查询指定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: + // 查询所有数据 c = db.query(TABLE.DATA, projection, selection, selectionArgs, null, null, sortOrder); break; case URI_DATA_ITEM: + // 查询指定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: + // 搜索笔记或搜索建议 if (sortOrder != null || projection != null) { throw new IllegalArgumentException( "do not specify sortOrder, selection, selectionArgs, or projection" + "with this query"); @@ -119,18 +238,22 @@ public class NotesProvider extends ContentProvider { String searchString = null; if (mMatcher.match(uri) == URI_SEARCH_SUGGEST) { + // 从URI路径中获取搜索关键词 if (uri.getPathSegments().size() > 1) { searchString = uri.getPathSegments().get(1); } } else { + // 从查询参数中获取搜索关键词 searchString = uri.getQueryParameter("pattern"); } + // 搜索关键词为空时返回null if (TextUtils.isEmpty(searchString)) { return null; } try { + // 使用模糊匹配搜索笔记摘要 searchString = String.format("%%%s%%", searchString); c = db.rawQuery(NOTES_SNIPPET_SEARCH_QUERY, new String[] { searchString }); @@ -141,21 +264,36 @@ public class NotesProvider extends ContentProvider { default: throw new IllegalArgumentException("Unknown URI " + uri); } + // 设置通知URI,当数据变更时通知观察者 if (c != null) { c.setNotificationUri(getContext().getContentResolver(), uri); } return c; } + /** + * 插入数据 + *

+ * 根据URI模式向对应的数据表插入数据,支持笔记和数据的插入。 + * 插入成功后通知相关URI的观察者。 + *

+ * + * @param uri 插入数据的URI + * @param values 要插入的数据值 + * @return 插入数据的URI(包含新增记录的ID) + * @throws IllegalArgumentException 如果URI模式不支持 + */ @Override public Uri insert(Uri uri, ContentValues values) { SQLiteDatabase db = mHelper.getWritableDatabase(); long dataId = 0, noteId = 0, insertedId = 0; switch (mMatcher.match(uri)) { case URI_NOTE: + // 插入笔记 insertedId = noteId = db.insert(TABLE.NOTE, null, values); break; case URI_DATA: + // 插入数据 if (values.containsKey(DataColumns.NOTE_ID)) { noteId = values.getAsLong(DataColumns.NOTE_ID); } else { @@ -167,12 +305,14 @@ public class NotesProvider extends ContentProvider { throw new IllegalArgumentException("Unknown URI " + uri); } // Notify the note uri + // 通知笔记URI的观察者 if (noteId > 0) { getContext().getContentResolver().notifyChange( ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, noteId), null); } // Notify the data uri + // 通知数据URI的观察者 if (dataId > 0) { getContext().getContentResolver().notifyChange( ContentUris.withAppendedId(Notes.CONTENT_DATA_URI, dataId), null); @@ -181,6 +321,20 @@ public class NotesProvider extends ContentProvider { return ContentUris.withAppendedId(uri, insertedId); } + /** + * 删除数据 + *

+ * 根据URI模式删除对应的数据表中的数据,支持笔记和数据的删除。 + * 删除笔记时,不允许删除系统文件夹(ID小于等于0)。 + * 删除成功后通知相关URI的观察者。 + *

+ * + * @param uri 删除数据的URI + * @param selection 删除条件 + * @param selectionArgs 删除条件参数 + * @return 删除的记录数 + * @throws IllegalArgumentException 如果URI模式不支持 + */ @Override public int delete(Uri uri, String selection, String[] selectionArgs) { int count = 0; @@ -189,14 +343,17 @@ public class NotesProvider extends ContentProvider { boolean deleteData = false; switch (mMatcher.match(uri)) { case URI_NOTE: + // 删除笔记(排除系统文件夹) selection = "(" + selection + ") AND " + NoteColumns.ID + ">0 "; count = db.delete(TABLE.NOTE, selection, selectionArgs); break; case URI_NOTE_ITEM: + // 删除指定ID的笔记 id = uri.getPathSegments().get(1); /** * ID that smaller than 0 is system folder which is not allowed to * trash + * ID小于等于0的是系统文件夹,不允许删除 */ long noteId = Long.valueOf(id); if (noteId <= 0) { @@ -206,10 +363,12 @@ public class NotesProvider extends ContentProvider { NoteColumns.ID + "=" + id + parseSelection(selection), selectionArgs); break; case URI_DATA: + // 删除数据 count = db.delete(TABLE.DATA, selection, selectionArgs); deleteData = true; break; case URI_DATA_ITEM: + // 删除指定ID的数据 id = uri.getPathSegments().get(1); count = db.delete(TABLE.DATA, DataColumns.ID + "=" + id + parseSelection(selection), selectionArgs); @@ -218,8 +377,10 @@ public class NotesProvider extends ContentProvider { default: throw new IllegalArgumentException("Unknown URI " + uri); } + // 删除成功后通知观察者 if (count > 0) { if (deleteData) { + // 删除数据时通知笔记URI getContext().getContentResolver().notifyChange(Notes.CONTENT_NOTE_URI, null); } getContext().getContentResolver().notifyChange(uri, null); @@ -227,6 +388,21 @@ public class NotesProvider extends ContentProvider { return count; } + /** + * 更新数据 + *

+ * 根据URI模式更新对应的数据表中的数据,支持笔记和数据的更新。 + * 更新笔记时自动递增笔记的版本号。 + * 更新成功后通知相关URI的观察者。 + *

+ * + * @param uri 更新数据的URI + * @param values 要更新的数据值 + * @param selection 更新条件 + * @param selectionArgs 更新条件参数 + * @return 更新的记录数 + * @throws IllegalArgumentException 如果URI模式不支持 + */ @Override public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) { int count = 0; @@ -235,20 +411,24 @@ public class NotesProvider extends ContentProvider { boolean updateData = false; switch (mMatcher.match(uri)) { case URI_NOTE: + // 更新笔记(递增版本号) increaseNoteVersion(-1, selection, selectionArgs); count = db.update(TABLE.NOTE, values, selection, selectionArgs); break; case URI_NOTE_ITEM: + // 更新指定ID的笔记(递增版本号) 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: + // 更新数据 count = db.update(TABLE.DATA, values, selection, selectionArgs); updateData = true; break; case URI_DATA_ITEM: + // 更新指定ID的数据 id = uri.getPathSegments().get(1); count = db.update(TABLE.DATA, values, DataColumns.ID + "=" + id + parseSelection(selection), selectionArgs); @@ -258,8 +438,10 @@ public class NotesProvider extends ContentProvider { throw new IllegalArgumentException("Unknown URI " + uri); } + // 更新成功后通知观察者 if (count > 0) { if (updateData) { + // 更新数据时通知笔记URI getContext().getContentResolver().notifyChange(Notes.CONTENT_NOTE_URI, null); } getContext().getContentResolver().notifyChange(uri, null); @@ -267,10 +449,30 @@ public class NotesProvider extends ContentProvider { return count; } + /** + * 解析查询条件 + *

+ * 将查询条件与ID条件组合,用于构建完整的SQL WHERE子句。 + *

+ * + * @param selection 原始查询条件 + * @return 组合后的查询条件字符串 + */ private String parseSelection(String selection) { return (!TextUtils.isEmpty(selection) ? " AND (" + selection + ')' : ""); } + /** + * 递增笔记版本号 + *

+ * 更新指定笔记的VERSION字段,使其值加1。 + * 用于跟踪笔记的修改历史,支持同步功能。 + *

+ * + * @param id 笔记ID,如果小于等于0则使用selection条件 + * @param selection 查询条件 + * @param selectionArgs 查询条件参数 + */ private void increaseNoteVersion(long id, String selection, String[] selectionArgs) { StringBuilder sql = new StringBuilder(120); sql.append("UPDATE "); @@ -279,6 +481,7 @@ public class NotesProvider extends ContentProvider { sql.append(NoteColumns.VERSION); sql.append("=" + NoteColumns.VERSION + "+1 "); + // 构建WHERE子句 if (id > 0 || !TextUtils.isEmpty(selection)) { sql.append(" WHERE "); } @@ -287,6 +490,7 @@ public class NotesProvider extends ContentProvider { } if (!TextUtils.isEmpty(selection)) { String selectString = id > 0 ? parseSelection(selection) : selection; + // 替换查询条件中的占位符 for (String args : selectionArgs) { selectString = selectString.replaceFirst("\\?", args); } @@ -296,10 +500,18 @@ public class NotesProvider extends ContentProvider { mHelper.getWritableDatabase().execSQL(sql.toString()); } + /** + * 获取数据MIME类型 + *

+ * 返回指定URI对应的数据MIME类型。 + *

+ * + * @param uri 数据URI + * @return MIME类型字符串 + */ @Override public String getType(Uri uri) { // TODO Auto-generated method stub return null; } - } diff --git a/src/Notes-master/src/net/micode/notes/gtask/data/MetaData.java b/src/Notes-master/src/net/micode/notes/gtask/data/MetaData.java index 3a2050b..28f6294 100644 --- a/src/Notes-master/src/net/micode/notes/gtask/data/MetaData.java +++ b/src/Notes-master/src/net/micode/notes/gtask/data/MetaData.java @@ -25,36 +25,86 @@ import org.json.JSONException; import org.json.JSONObject; +/** + * Google Tasks 元数据类 + *

+ * 继承自 Task,用于存储和管理 Google Tasks 同步的元数据信息。 + * 元数据以特殊任务的形式存储在 Google Tasks 中,用于关联本地笔记和远程任务的对应关系。 + * 该类不应通过本地 JSON 或数据库游标进行操作,仅用于远程同步场景。 + *

+ */ public class MetaData extends Task { + /** + * 日志标签 + */ private final static String TAG = MetaData.class.getSimpleName(); + /** + * 关联的 Google Tasks ID + */ private String mRelatedGid = null; + /** + * 设置元数据信息 + *

+ * 将 Google Tasks ID 添加到元信息 JSON 对象中,并设置任务名称为元数据专用名称。 + * 元信息以 JSON 字符串形式存储在任务的 notes 字段中。 + *

+ * + * @param gid 关联的 Google Tasks ID + * @param metaInfo 元信息 JSON 对象 + */ public void setMeta(String gid, JSONObject metaInfo) { try { + // 将关联的 GID 添加到元信息中 metaInfo.put(GTaskStringUtils.META_HEAD_GTASK_ID, gid); } catch (JSONException e) { Log.e(TAG, "failed to put related gid"); } + // 将元信息转换为字符串并设置为任务备注 setNotes(metaInfo.toString()); + // 设置为元数据专用名称 setName(GTaskStringUtils.META_NOTE_NAME); } + /** + * 获取关联的 Google Tasks ID + * + * @return 关联的 Google Tasks ID,如果未设置则返回 null + */ public String getRelatedGid() { return mRelatedGid; } + /** + * 判断是否值得保存 + *

+ * 只有当 notes 字段不为空时才值得保存,因为元数据信息存储在 notes 中。 + *

+ * + * @return 如果 notes 不为 null 返回 true,否则返回 false + */ @Override public boolean isWorthSaving() { return getNotes() != null; } + /** + * 根据远程 JSON 设置内容 + *

+ * 从远程服务器返回的 JSON 对象中解析元数据信息,提取关联的 Google Tasks ID。 + *

+ * + * @param js 远程服务器返回的 JSON 对象 + */ @Override public void setContentByRemoteJSON(JSONObject js) { super.setContentByRemoteJSON(js); if (getNotes() != null) { try { + // 从 notes 字段中解析元信息 JSON JSONObject metaInfo = new JSONObject(getNotes().trim()); + // 提取关联的 GID mRelatedGid = metaInfo.getString(GTaskStringUtils.META_HEAD_GTASK_ID); } catch (JSONException e) { Log.w(TAG, "failed to get related gid"); @@ -63,17 +113,45 @@ public class MetaData extends Task { } } + /** + * 根据本地 JSON 设置内容 + *

+ * 此方法不应被调用,因为元数据不通过本地 JSON 进行操作。 + *

+ * + * @param js 本地 JSON 对象 + * @throws IllegalAccessError 总是抛出此异常,表示不应调用此方法 + */ @Override public void setContentByLocalJSON(JSONObject js) { // this function should not be called throw new IllegalAccessError("MetaData:setContentByLocalJSON should not be called"); } + /** + * 从内容生成本地 JSON 对象 + *

+ * 此方法不应被调用,因为元数据不通过本地 JSON 进行操作。 + *

+ * + * @return 无返回值,总是抛出异常 + * @throws IllegalAccessError 总是抛出此异常,表示不应调用此方法 + */ @Override public JSONObject getLocalJSONFromContent() { throw new IllegalAccessError("MetaData:getLocalJSONFromContent should not be called"); } + /** + * 根据数据库游标获取同步动作 + *

+ * 此方法不应被调用,因为元数据不通过数据库游标进行操作。 + *

+ * + * @param c 数据库游标 + * @return 无返回值,总是抛出异常 + * @throws IllegalAccessError 总是抛出此异常,表示不应调用此方法 + */ @Override public int getSyncAction(Cursor c) { throw new IllegalAccessError("MetaData:getSyncAction should not be called"); diff --git a/src/Notes-master/src/net/micode/notes/gtask/data/Node.java b/src/Notes-master/src/net/micode/notes/gtask/data/Node.java index 63950e0..ad7e431 100644 --- a/src/Notes-master/src/net/micode/notes/gtask/data/Node.java +++ b/src/Notes-master/src/net/micode/notes/gtask/data/Node.java @@ -20,33 +20,86 @@ import android.database.Cursor; import org.json.JSONObject; +/** + * Google Tasks 同步节点抽象基类 + *

+ * 定义所有可同步数据模型(Task、TaskList、MetaData)的公共属性和抽象方法。 + * 负责管理同步状态、Google ID、名称、最后修改时间和删除标记等通用属性。 + * 子类需要实现具体的 JSON 转换和同步动作生成逻辑。 + *

+ */ 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; + /** + * Google Tasks ID,用于唯一标识远程任务 + */ private String mGid; + /** + * 节点名称 + */ private String mName; + /** + * 最后修改时间(时间戳) + */ private long mLastModified; + /** + * 删除标记,true 表示已删除 + */ private boolean mDeleted; + /** + * 构造一个新的节点实例 + *

+ * 初始化所有属性为默认值:GID 为 null,名称为空字符串,最后修改时间为 0,删除标记为 false。 + *

+ */ public Node() { mGid = null; mName = ""; @@ -54,46 +107,137 @@ public abstract class Node { mDeleted = false; } + /** + * 获取创建动作的 JSON 对象 + *

+ * 根据指定的动作 ID 生成用于在远程服务器创建节点的 JSON 请求。 + *

+ * + * @param actionId 动作 ID,标识具体的创建操作类型 + * @return 包含创建动作信息的 JSON 对象 + */ public abstract JSONObject getCreateAction(int actionId); + /** + * 获取更新动作的 JSON 对象 + *

+ * 根据指定的动作 ID 生成用于在远程服务器更新节点的 JSON 请求。 + *

+ * + * @param actionId 动作 ID,标识具体的更新操作类型 + * @return 包含更新动作信息的 JSON 对象 + */ public abstract JSONObject getUpdateAction(int actionId); + /** + * 根据远程 JSON 设置节点内容 + *

+ * 从远程服务器返回的 JSON 对象中解析并设置节点的属性值。 + *

+ * + * @param js 远程服务器返回的 JSON 对象 + */ public abstract void setContentByRemoteJSON(JSONObject js); + /** + * 根据本地 JSON 设置节点内容 + *

+ * 从本地数据库存储的 JSON 对象中解析并设置节点的属性值。 + *

+ * + * @param js 本地数据库存储的 JSON 对象 + */ public abstract void setContentByLocalJSON(JSONObject js); + /** + * 从节点内容生成本地 JSON 对象 + *

+ * 将节点的当前属性值转换为 JSON 对象,用于存储到本地数据库。 + *

+ * + * @return 包含节点内容的 JSON 对象 + */ public abstract JSONObject getLocalJSONFromContent(); + /** + * 根据数据库游标获取同步动作 + *

+ * 比较本地数据库中的数据与当前节点状态,确定需要执行的同步动作类型。 + *

+ * + * @param c 指向本地数据库记录的游标 + * @return 同步动作类型,取值为 SYNC_ACTION_* 常量之一 + */ public abstract int getSyncAction(Cursor c); + /** + * 设置 Google Tasks ID + * + * @param gid Google Tasks ID,用于唯一标识远程任务 + */ public void setGid(String gid) { this.mGid = gid; } + /** + * 设置节点名称 + * + * @param name 节点名称 + */ public void setName(String name) { this.mName = name; } + /** + * 设置最后修改时间 + * + * @param lastModified 最后修改时间(时间戳) + */ public void setLastModified(long lastModified) { this.mLastModified = lastModified; } + /** + * 设置删除标记 + * + * @param deleted 删除标记,true 表示已删除 + */ public void setDeleted(boolean deleted) { this.mDeleted = deleted; } + /** + * 获取 Google Tasks ID + * + * @return Google Tasks ID,如果未设置则返回 null + */ public String getGid() { return this.mGid; } + /** + * 获取节点名称 + * + * @return 节点名称 + */ public String getName() { return this.mName; } + /** + * 获取最后修改时间 + * + * @return 最后修改时间(时间戳) + */ public long getLastModified() { return this.mLastModified; } + /** + * 获取删除标记 + * + * @return 删除标记,true 表示已删除 + */ public boolean getDeleted() { return this.mDeleted; } diff --git a/src/Notes-master/src/net/micode/notes/gtask/data/SqlData.java b/src/Notes-master/src/net/micode/notes/gtask/data/SqlData.java index d3ec3be..174560e 100644 --- a/src/Notes-master/src/net/micode/notes/gtask/data/SqlData.java +++ b/src/Notes-master/src/net/micode/notes/gtask/data/SqlData.java @@ -35,24 +35,39 @@ import org.json.JSONException; import org.json.JSONObject; +/** + * SQLite 数据内容类 + *

+ * 表示笔记的一条数据记录,存储笔记的具体内容信息。 + * 每条数据记录包含 MIME 类型、内容文本和扩展数据字段。 + * 支持从 JSON 对象加载内容或将内容导出为 JSON,用于与 Google Tasks 的数据同步。 + *

+ */ public class SqlData { private static final String TAG = SqlData.class.getSimpleName(); + /** 无效 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 类型字段在投影数组中的索引 */ public static final int DATA_MIME_TYPE_COLUMN = 1; + /** 内容字段在投影数组中的索引 */ public static final int DATA_CONTENT_COLUMN = 2; + /** 扩展数据 1 字段在投影数组中的索引 */ public static final int DATA_CONTENT_DATA_1_COLUMN = 3; + /** 扩展数据 3 字段在投影数组中的索引 */ public static final int DATA_CONTENT_DATA_3_COLUMN = 4; private ContentResolver mContentResolver; @@ -71,6 +86,15 @@ public class SqlData { private ContentValues mDiffDataValues; + /** + * 构造一个新建的数据对象 + *

+ * 创建一个尚未保存到数据库的新数据记录,初始化所有字段为默认值。 + * 标记为创建状态,后续调用 commit 方法时会执行插入操作。 + *

+ * + * @param context 上下文对象,用于获取 ContentResolver + */ public SqlData(Context context) { mContentResolver = context.getContentResolver(); mIsCreate = true; @@ -82,6 +106,16 @@ public class SqlData { mDiffDataValues = new ContentValues(); } + /** + * 从数据库游标构造数据对象 + *

+ * 从游标中读取数据记录并初始化对象。 + * 标记为非创建状态,后续调用 commit 方法时会执行更新操作。 + *

+ * + * @param context 上下文对象 + * @param c 指向数据记录的数据库游标 + */ public SqlData(Context context, Cursor c) { mContentResolver = context.getContentResolver(); mIsCreate = false; @@ -89,6 +123,14 @@ public class SqlData { mDiffDataValues = new ContentValues(); } + /** + * 从数据库游标加载数据内容 + *

+ * 从游标的当前行读取所有数据字段值并初始化对象的成员变量。 + *

+ * + * @param c 指向数据记录的数据库游标 + */ private void loadFromCursor(Cursor c) { mDataId = c.getLong(DATA_ID_COLUMN); mDataMimeType = c.getString(DATA_MIME_TYPE_COLUMN); @@ -97,6 +139,16 @@ public class SqlData { mDataContentData3 = c.getString(DATA_CONTENT_DATA_3_COLUMN); } + /** + * 从 JSON 对象设置数据内容 + *

+ * 解析 JSON 对象中的数据字段,更新当前对象的成员变量。 + * 比较新旧值,将变更记录到差异值集合中。 + *

+ * + * @param js 包含数据信息的 JSON 对象 + * @throws JSONException 如果 JSON 解析失败 + */ public void setContent(JSONObject js) throws JSONException { long dataId = js.has(DataColumns.ID) ? js.getLong(DataColumns.ID) : INVALID_ID; if (mIsCreate || mDataId != dataId) { @@ -130,6 +182,15 @@ public class SqlData { mDataContentData3 = dataContentData3; } + /** + * 获取数据内容的 JSON 对象 + *

+ * 将当前数据的所有字段导出为 JSON 对象格式。 + *

+ * + * @return 包含数据信息的 JSON 对象,如果尚未创建到数据库则返回 null + * @throws JSONException 如果 JSON 生成失败 + */ public JSONObject getContent() throws JSONException { if (mIsCreate) { Log.e(TAG, "it seems that we haven't created this in database yet"); @@ -144,6 +205,19 @@ public class SqlData { return js; } + /** + * 提交数据变更到数据库 + *

+ * 根据当前状态执行插入或更新操作: + * - 如果是新建数据,插入新记录并获取生成的 ID + * - 如果是已存在的数据,更新变更的字段 + *

+ * + * @param noteId 关联的笔记 ID + * @param validateVersion 是否验证版本号,为 true 时仅更新版本号匹配的记录 + * @param version 笔记的版本号,用于版本验证 + * @throws ActionFailureException 如果创建数据失败 + */ public void commit(long noteId, boolean validateVersion, long version) { if (mIsCreate) { @@ -166,6 +240,7 @@ public class SqlData { 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 @@ -183,6 +258,11 @@ public class SqlData { mIsCreate = false; } + /** + * 获取数据 ID + * + * @return 数据在数据库中的 ID + */ public long getId() { return mDataId; } diff --git a/src/Notes-master/src/net/micode/notes/gtask/data/SqlNote.java b/src/Notes-master/src/net/micode/notes/gtask/data/SqlNote.java index 79a4095..3355141 100644 --- a/src/Notes-master/src/net/micode/notes/gtask/data/SqlNote.java +++ b/src/Notes-master/src/net/micode/notes/gtask/data/SqlNote.java @@ -38,11 +38,21 @@ import org.json.JSONObject; import java.util.ArrayList; +/** + * SQLite 笔记数据类 + *

+ * 表示本地数据库中的一条笔记记录,负责笔记数据的增删改查操作。 + * 支持与 Google Tasks 的双向同步,能够从 JSON 对象加载内容或将内容导出为 JSON。 + * 区分普通笔记、文件夹和系统文件夹三种类型,提供版本控制和本地修改标记功能。 + *

+ */ public class SqlNote { private static final String TAG = SqlNote.class.getSimpleName(); + /** 无效 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, @@ -52,38 +62,55 @@ public class SqlNote { 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; + /** Widget ID 字段在投影数组中的索引 */ public static final int WIDGET_ID_COLUMN = 10; + /** Widget 类型字段在投影数组中的索引 */ 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; + /** Google Tasks ID 字段在投影数组中的索引 */ public static final int GTASK_ID_COLUMN = 15; + /** 版本号字段在投影数组中的索引 */ public static final int VERSION_COLUMN = 16; private Context mContext; @@ -122,6 +149,15 @@ public class SqlNote { private ArrayList mDataList; + /** + * 构造一个新建的笔记对象 + *

+ * 创建一个尚未保存到数据库的新笔记,初始化所有字段为默认值。 + * 标记为创建状态,后续调用 commit 方法时会执行插入操作。 + *

+ * + * @param context 上下文对象,用于获取 ContentResolver 和默认资源 + */ public SqlNote(Context context) { mContext = context; mContentResolver = context.getContentResolver(); @@ -143,6 +179,16 @@ public class SqlNote { mDataList = new ArrayList(); } + /** + * 从数据库游标构造笔记对象 + *

+ * 从游标中读取笔记数据并初始化对象,如果笔记类型为普通笔记则加载其数据内容。 + * 标记为非创建状态,后续调用 commit 方法时会执行更新操作。 + *

+ * + * @param context 上下文对象 + * @param c 指向笔记记录的数据库游标 + */ public SqlNote(Context context, Cursor c) { mContext = context; mContentResolver = context.getContentResolver(); @@ -154,6 +200,16 @@ public class SqlNote { mDiffNoteValues = new ContentValues(); } + /** + * 从数据库 ID 构造笔记对象 + *

+ * 根据笔记 ID 从数据库查询记录并初始化对象,如果笔记类型为普通笔记则加载其数据内容。 + * 标记为非创建状态,后续调用 commit 方法时会执行更新操作。 + *

+ * + * @param context 上下文对象 + * @param id 笔记在数据库中的 ID + */ public SqlNote(Context context, long id) { mContext = context; mContentResolver = context.getContentResolver(); @@ -166,6 +222,14 @@ public class SqlNote { } + /** + * 从数据库 ID 加载笔记数据 + *

+ * 根据笔记 ID 查询数据库获取笔记记录,并调用 loadFromCursor(Cursor) 加载数据。 + *

+ * + * @param id 笔记在数据库中的 ID + */ private void loadFromCursor(long id) { Cursor c = null; try { @@ -185,6 +249,14 @@ public class SqlNote { } } + /** + * 从数据库游标加载笔记数据 + *

+ * 从游标的当前行读取所有笔记字段值并初始化对象的成员变量。 + *

+ * + * @param c 指向笔记记录的数据库游标 + */ private void loadFromCursor(Cursor c) { mId = c.getLong(ID_COLUMN); mAlertDate = c.getLong(ALERTED_DATE_COLUMN); @@ -200,6 +272,13 @@ public class SqlNote { mVersion = c.getLong(VERSION_COLUMN); } + /** + * 加载笔记的数据内容 + *

+ * 从数据库查询当前笔记的所有数据记录(Data 表),并创建 SqlData 对象列表。 + * 仅对普通笔记类型有效,文件夹类型没有数据内容。 + *

+ */ private void loadDataContent() { Cursor c = null; mDataList.clear(); @@ -226,6 +305,17 @@ public class SqlNote { } } + /** + * 从 JSON 对象设置笔记内容 + *

+ * 解析 JSON 对象中的笔记信息和数据,更新当前笔记的字段值。 + * 根据笔记类型(系统文件夹、文件夹、普通笔记)执行不同的更新逻辑。 + * 对于普通笔记,会同时更新其数据内容列表。 + *

+ * + * @param js 包含笔记信息的 JSON 对象 + * @return 如果设置成功返回 true,否则返回 false + */ public boolean setContent(JSONObject js) { try { JSONObject note = js.getJSONObject(GTaskStringUtils.META_HEAD_NOTE); @@ -359,6 +449,15 @@ public class SqlNote { return true; } + /** + * 获取笔记内容的 JSON 对象 + *

+ * 将当前笔记的所有字段和数据内容导出为 JSON 对象格式。 + * 根据笔记类型生成不同结构的 JSON,普通笔记包含数据数组。 + *

+ * + * @return 包含笔记信息的 JSON 对象,如果尚未创建到数据库则返回 null + */ public JSONObject getContent() { try { JSONObject js = new JSONObject(); @@ -407,39 +506,102 @@ public class SqlNote { return null; } + /** + * 设置父文件夹 ID + *

+ * 更新笔记的父文件夹 ID,并将变更记录到差异值集合中。 + *

+ * + * @param id 新的父文件夹 ID + */ public void setParentId(long id) { mParentId = id; mDiffNoteValues.put(NoteColumns.PARENT_ID, id); } + /** + * 设置 Google Tasks ID + *

+ * 将 Google Tasks 的任务 ID 关联到当前笔记,用于同步标识。 + *

+ * + * @param gid Google Tasks 任务 ID + */ public void setGtaskId(String gid) { mDiffNoteValues.put(NoteColumns.GTASK_ID, gid); } + /** + * 设置同步 ID + *

+ * 记录最后一次同步的时间戳,用于判断本地和远程数据的同步状态。 + *

+ * + * @param syncId 同步时间戳 + */ public void setSyncId(long syncId) { mDiffNoteValues.put(NoteColumns.SYNC_ID, syncId); } + /** + * 重置本地修改标记 + *

+ * 将本地修改标记设置为 0,表示笔记已同步,无待同步的本地修改。 + *

+ */ public void resetLocalModified() { mDiffNoteValues.put(NoteColumns.LOCAL_MODIFIED, 0); } + /** + * 获取笔记 ID + * + * @return 笔记在数据库中的 ID,如果尚未创建则返回 INVALID_ID + */ public long getId() { return mId; } + /** + * 获取父文件夹 ID + * + * @return 父文件夹在数据库中的 ID + */ public long getParentId() { return mParentId; } + /** + * 获取笔记摘要文本 + * + * @return 笔记的摘要文本 + */ public String getSnippet() { return mSnippet; } + /** + * 判断是否为普通笔记类型 + * + * @return 如果是普通笔记返回 true,否则返回 false + */ public boolean isNoteType() { return mType == Notes.TYPE_NOTE; } + /** + * 提交笔记变更到数据库 + *

+ * 根据当前状态执行插入或更新操作: + * - 如果是新建笔记,插入新记录并获取生成的 ID + * - 如果是已存在的笔记,更新变更的字段 + * - 对于普通笔记,同时提交其数据内容 + *

+ * + * @param validateVersion 是否验证版本号,为 true 时仅更新版本号不大于当前版本的记录 + * @throws ActionFailureException 如果创建笔记失败 + * @throws IllegalStateException 如果尝试更新无效 ID 的笔记 + */ public void commit(boolean validateVersion) { if (mIsCreate) { if (mId == INVALID_ID && mDiffNoteValues.containsKey(NoteColumns.ID)) { @@ -476,6 +638,7 @@ public class SqlNote { String.valueOf(mId) }); } else { + // 仅更新版本号不大于当前版本的记录,防止并发更新冲突 result = mContentResolver.update(Notes.CONTENT_NOTE_URI, mDiffNoteValues, "(" + NoteColumns.ID + "=?) AND (" + NoteColumns.VERSION + "<=?)", new String[] { @@ -494,7 +657,7 @@ public class SqlNote { } } - // refresh local info + // 从数据库重新加载最新数据,确保内存状态与数据库一致 loadFromCursor(mId); if (mType == Notes.TYPE_NOTE) loadDataContent(); diff --git a/src/Notes-master/src/net/micode/notes/gtask/data/Task.java b/src/Notes-master/src/net/micode/notes/gtask/data/Task.java index 6a19454..f9f0ef3 100644 --- a/src/Notes-master/src/net/micode/notes/gtask/data/Task.java +++ b/src/Notes-master/src/net/micode/notes/gtask/data/Task.java @@ -32,19 +32,51 @@ import org.json.JSONException; import org.json.JSONObject; +/** + * Google Tasks 任务类 + *

+ * 继承自 Node,表示 Google Tasks 中的一个任务项。 + * 负责管理任务的完成状态、备注信息、元数据、前驱兄弟节点和父任务列表。 + * 支持与本地笔记的双向同步,能够生成创建和更新动作的 JSON 对象。 + *

+ */ public class Task extends Node { + /** + * 日志标签 + */ private static final String TAG = Task.class.getSimpleName(); + /** + * 完成状态标记,true 表示已完成 + */ private boolean mCompleted; + /** + * 任务备注信息 + */ private String mNotes; + /** + * 元数据 JSON 对象,包含本地笔记的完整信息 + */ private JSONObject mMetaInfo; + /** + * 前驱兄弟任务,用于维护任务在列表中的顺序 + */ private Task mPriorSibling; + /** + * 父任务列表 + */ private TaskList mParent; + /** + * 构造一个新的任务实例 + *

+ * 初始化所有属性为默认值:未完成、备注为 null、无前驱兄弟、无父列表、无元数据。 + *

+ */ public Task() { super(); mCompleted = false; @@ -54,6 +86,16 @@ public class Task extends Node { mMetaInfo = null; } + /** + * 获取创建动作的 JSON 对象 + *

+ * 生成用于在远程服务器创建任务的 JSON 请求,包含任务名称、备注、父列表 ID 等信息。 + *

+ * + * @param actionId 动作 ID,标识具体的创建操作 + * @return 包含创建动作信息的 JSON 对象 + * @throws ActionFailureException 如果生成 JSON 对象失败 + */ public JSONObject getCreateAction(int actionId) { JSONObject js = new JSONObject(); @@ -103,6 +145,16 @@ public class Task extends Node { return js; } + /** + * 获取更新动作的 JSON 对象 + *

+ * 生成用于在远程服务器更新任务的 JSON 请求,包含任务名称、备注、删除状态等信息。 + *

+ * + * @param actionId 动作 ID,标识具体的更新操作 + * @return 包含更新动作信息的 JSON 对象 + * @throws ActionFailureException 如果生成 JSON 对象失败 + */ public JSONObject getUpdateAction(int actionId) { JSONObject js = new JSONObject(); @@ -135,6 +187,15 @@ public class Task extends Node { return js; } + /** + * 根据远程 JSON 设置内容 + *

+ * 从远程服务器返回的 JSON 对象中解析并设置任务的属性值,包括 ID、名称、备注、完成状态等。 + *

+ * + * @param js 远程服务器返回的 JSON 对象 + * @throws ActionFailureException 如果解析 JSON 失败 + */ public void setContentByRemoteJSON(JSONObject js) { if (js != null) { try { @@ -175,6 +236,15 @@ public class Task extends Node { } } + /** + * 根据本地 JSON 设置内容 + *

+ * 从本地数据库存储的 JSON 对象中解析并设置任务的属性值。 + * 从笔记数据中提取内容作为任务名称。 + *

+ * + * @param js 本地数据库存储的 JSON 对象 + */ public void setContentByLocalJSON(JSONObject js) { if (js == null || !js.has(GTaskStringUtils.META_HEAD_NOTE) || !js.has(GTaskStringUtils.META_HEAD_DATA)) { @@ -190,6 +260,7 @@ public class Task extends Node { return; } + // 遍历数据数组,查找笔记内容 for (int i = 0; i < dataArray.length(); i++) { JSONObject data = dataArray.getJSONObject(i); if (TextUtils.equals(data.getString(DataColumns.MIME_TYPE), DataConstants.NOTE)) { @@ -204,6 +275,15 @@ public class Task extends Node { } } + /** + * 从内容生成本地 JSON 对象 + *

+ * 将任务的当前属性值转换为 JSON 对象,用于存储到本地数据库。 + * 如果是新建任务,创建新的 JSON 结构;如果是已同步任务,更新现有元数据。 + *

+ * + * @return 包含任务内容的 JSON 对象,如果生成失败则返回 null + */ public JSONObject getLocalJSONFromContent() { String name = getName(); try { @@ -229,6 +309,7 @@ public class Task extends Node { JSONObject note = mMetaInfo.getJSONObject(GTaskStringUtils.META_HEAD_NOTE); JSONArray dataArray = mMetaInfo.getJSONArray(GTaskStringUtils.META_HEAD_DATA); + // 更新数据数组中的笔记内容 for (int i = 0; i < dataArray.length(); i++) { JSONObject data = dataArray.getJSONObject(i); if (TextUtils.equals(data.getString(DataColumns.MIME_TYPE), DataConstants.NOTE)) { @@ -247,6 +328,15 @@ public class Task extends Node { } } + /** + * 设置元数据信息 + *

+ * 从元数据对象中解析并设置任务的元信息 JSON 对象。 + * 元信息包含本地笔记的完整结构,用于双向同步。 + *

+ * + * @param metaData 元数据对象 + */ public void setMetaInfo(MetaData metaData) { if (metaData != null && metaData.getNotes() != null) { try { @@ -258,6 +348,16 @@ public class Task extends Node { } } + /** + * 根据数据库游标获取同步动作 + *

+ * 比较本地数据库中的数据与当前任务状态,确定需要执行的同步动作类型。 + * 处理各种同步场景:无更新、本地更新、远程更新、冲突、错误等。 + *

+ * + * @param c 指向本地数据库记录的游标 + * @return 同步动作类型,取值为 SYNC_ACTION_* 常量之一 + */ public int getSyncAction(Cursor c) { try { JSONObject noteInfo = null; @@ -311,39 +411,87 @@ public class Task extends Node { return SYNC_ACTION_ERROR; } + /** + * 判断是否值得保存 + *

+ * 只有当任务有元数据、非空名称或非空备注时才值得保存。 + *

+ * + * @return 如果有元数据、非空名称或非空备注返回 true,否则返回 false + */ public boolean isWorthSaving() { return mMetaInfo != null || (getName() != null && getName().trim().length() > 0) || (getNotes() != null && getNotes().trim().length() > 0); } + /** + * 设置完成状态 + * + * @param completed 完成状态,true 表示已完成 + */ public void setCompleted(boolean completed) { this.mCompleted = completed; } + /** + * 设置备注信息 + * + * @param notes 备注信息 + */ public void setNotes(String notes) { this.mNotes = notes; } + /** + * 设置前驱兄弟任务 + * + * @param priorSibling 前驱兄弟任务 + */ public void setPriorSibling(Task priorSibling) { this.mPriorSibling = priorSibling; } + /** + * 设置父任务列表 + * + * @param parent 父任务列表 + */ public void setParent(TaskList parent) { this.mParent = parent; } + /** + * 获取完成状态 + * + * @return 完成状态,true 表示已完成 + */ public boolean getCompleted() { return this.mCompleted; } + /** + * 获取备注信息 + * + * @return 备注信息 + */ public String getNotes() { return this.mNotes; } + /** + * 获取前驱兄弟任务 + * + * @return 前驱兄弟任务 + */ public Task getPriorSibling() { return this.mPriorSibling; } + /** + * 获取父任务列表 + * + * @return 父任务列表 + */ public TaskList getParent() { return this.mParent; } diff --git a/src/Notes-master/src/net/micode/notes/gtask/data/TaskList.java b/src/Notes-master/src/net/micode/notes/gtask/data/TaskList.java index 4ea21c5..d454fe7 100644 --- a/src/Notes-master/src/net/micode/notes/gtask/data/TaskList.java +++ b/src/Notes-master/src/net/micode/notes/gtask/data/TaskList.java @@ -30,19 +30,52 @@ import org.json.JSONObject; import java.util.ArrayList; +/** + * Google Tasks 任务列表类 + *

+ * 继承自 Node,表示 Google Tasks 中的一个任务列表(文件夹)。 + * 负责管理任务列表的子任务集合,提供任务的增删改查操作。 + * 支持与本地笔记文件夹的双向同步,能够生成创建和更新动作的 JSON 对象。 + *

+ */ public class TaskList extends Node { + /** + * 日志标签 + */ private static final String TAG = TaskList.class.getSimpleName(); + /** + * 任务列表索引 + */ private int mIndex; + /** + * 子任务列表 + */ private ArrayList mChildren; + /** + * 构造一个新的任务列表实例 + *

+ * 初始化子任务列表为空,索引设置为 1。 + *

+ */ public TaskList() { super(); mChildren = new ArrayList(); mIndex = 1; } + /** + * 获取创建动作的 JSON 对象 + *

+ * 生成用于在远程服务器创建任务列表的 JSON 请求,包含列表名称等信息。 + *

+ * + * @param actionId 动作 ID,标识具体的创建操作 + * @return 包含创建动作信息的 JSON 对象 + * @throws ActionFailureException 如果生成 JSON 对象失败 + */ public JSONObject getCreateAction(int actionId) { JSONObject js = new JSONObject(); @@ -74,6 +107,16 @@ public class TaskList extends Node { return js; } + /** + * 获取更新动作的 JSON 对象 + *

+ * 生成用于在远程服务器更新任务列表的 JSON 请求,包含列表名称、删除状态等信息。 + *

+ * + * @param actionId 动作 ID,标识具体的更新操作 + * @return 包含更新动作信息的 JSON 对象 + * @throws ActionFailureException 如果生成 JSON 对象失败 + */ public JSONObject getUpdateAction(int actionId) { JSONObject js = new JSONObject(); @@ -103,6 +146,15 @@ public class TaskList extends Node { return js; } + /** + * 根据远程 JSON 设置内容 + *

+ * 从远程服务器返回的 JSON 对象中解析并设置任务列表的属性值。 + *

+ * + * @param js 远程服务器返回的 JSON 对象 + * @throws ActionFailureException 如果解析 JSON 失败 + */ public void setContentByRemoteJSON(JSONObject js) { if (js != null) { try { @@ -129,6 +181,15 @@ public class TaskList extends Node { } } + /** + * 根据本地 JSON 设置内容 + *

+ * 从本地数据库存储的 JSON 对象中解析并设置任务列表的属性值。 + * 根据文件夹类型(普通文件夹或系统文件夹)设置对应的名称。 + *

+ * + * @param js 本地数据库存储的 JSON 对象 + */ public void setContentByLocalJSON(JSONObject js) { if (js == null || !js.has(GTaskStringUtils.META_HEAD_NOTE)) { Log.w(TAG, "setContentByLocalJSON: nothing is avaiable"); @@ -138,9 +199,11 @@ public class TaskList extends Node { JSONObject folder = js.getJSONObject(GTaskStringUtils.META_HEAD_NOTE); if (folder.getInt(NoteColumns.TYPE) == Notes.TYPE_FOLDER) { + // 普通文件夹,使用文件夹名称 String name = folder.getString(NoteColumns.SNIPPET); setName(GTaskStringUtils.MIUI_FOLDER_PREFFIX + name); } else if (folder.getInt(NoteColumns.TYPE) == Notes.TYPE_SYSTEM) { + // 系统文件夹,根据 ID 设置对应的名称 if (folder.getLong(NoteColumns.ID) == Notes.ID_ROOT_FOLDER) setName(GTaskStringUtils.MIUI_FOLDER_PREFFIX + GTaskStringUtils.FOLDER_DEFAULT); else if (folder.getLong(NoteColumns.ID) == Notes.ID_CALL_RECORD_FOLDER) @@ -157,16 +220,27 @@ public class TaskList extends Node { } } + /** + * 从内容生成本地 JSON 对象 + *

+ * 将任务列表的当前属性值转换为 JSON 对象,用于存储到本地数据库。 + * 根据文件夹名称判断是系统文件夹还是普通文件夹。 + *

+ * + * @return 包含任务列表内容的 JSON 对象,如果生成失败则返回 null + */ public JSONObject getLocalJSONFromContent() { try { JSONObject js = new JSONObject(); JSONObject folder = new JSONObject(); + // 去除文件夹名称前缀 String folderName = getName(); if (getName().startsWith(GTaskStringUtils.MIUI_FOLDER_PREFFIX)) folderName = folderName.substring(GTaskStringUtils.MIUI_FOLDER_PREFFIX.length(), folderName.length()); folder.put(NoteColumns.SNIPPET, folderName); + // 根据文件夹名称判断类型 if (folderName.equals(GTaskStringUtils.FOLDER_DEFAULT) || folderName.equals(GTaskStringUtils.FOLDER_CALL_NOTE)) folder.put(NoteColumns.TYPE, Notes.TYPE_SYSTEM); @@ -183,6 +257,16 @@ public class TaskList extends Node { } } + /** + * 根据数据库游标获取同步动作 + *

+ * 比较本地数据库中的数据与当前任务列表状态,确定需要执行的同步动作类型。 + * 对于文件夹冲突,优先应用本地修改。 + *

+ * + * @param c 指向本地数据库记录的游标 + * @return 同步动作类型,取值为 SYNC_ACTION_* 常量之一 + */ public int getSyncAction(Cursor c) { try { if (c.getInt(SqlNote.LOCAL_MODIFIED_COLUMN) == 0) { @@ -216,10 +300,24 @@ public class TaskList extends Node { return SYNC_ACTION_ERROR; } + /** + * 获取子任务数量 + * + * @return 子任务的数量 + */ public int getChildTaskCount() { return mChildren.size(); } + /** + * 添加子任务到列表末尾 + *

+ * 将任务添加到子任务列表的末尾,并设置其前驱兄弟和父列表。 + *

+ * + * @param task 要添加的子任务 + * @return 如果添加成功返回 true,否则返回 false + */ public boolean addChildTask(Task task) { boolean ret = false; if (task != null && !mChildren.contains(task)) { @@ -234,6 +332,16 @@ public class TaskList extends Node { return ret; } + /** + * 在指定位置添加子任务 + *

+ * 将任务插入到子任务列表的指定位置,并更新相关任务的前驱兄弟关系。 + *

+ * + * @param task 要添加的子任务 + * @param index 插入位置索引,必须在 0 到子任务数量之间 + * @return 如果添加成功返回 true,否则返回 false + */ public boolean addChildTask(Task task, int index) { if (index < 0 || index > mChildren.size()) { Log.e(TAG, "add child task: invalid index"); @@ -260,6 +368,16 @@ public class TaskList extends Node { return true; } + /** + * 移除子任务 + *

+ * 从子任务列表中移除指定任务,并重置其前驱兄弟和父列表关系。 + * 同时更新后续任务的前驱兄弟关系。 + *

+ * + * @param task 要移除的子任务 + * @return 如果移除成功返回 true,否则返回 false + */ public boolean removeChildTask(Task task) { boolean ret = false; int index = mChildren.indexOf(task); @@ -281,6 +399,16 @@ public class TaskList extends Node { return ret; } + /** + * 移动子任务到指定位置 + *

+ * 将子任务从当前位置移动到目标位置,通过先移除再添加实现。 + *

+ * + * @param task 要移动的子任务 + * @param index 目标位置索引,必须在 0 到子任务数量减 1 之间 + * @return 如果移动成功返回 true,否则返回 false + */ public boolean moveChildTask(Task task, int index) { if (index < 0 || index >= mChildren.size()) { @@ -299,6 +427,12 @@ public class TaskList extends Node { return (removeChildTask(task) && addChildTask(task, index)); } + /** + * 根据 GID 查找子任务 + * + * @param gid Google Tasks ID + * @return 找到的子任务,如果未找到则返回 null + */ public Task findChildTaskByGid(String gid) { for (int i = 0; i < mChildren.size(); i++) { Task t = mChildren.get(i); @@ -309,10 +443,22 @@ public class TaskList extends Node { return null; } + /** + * 获取子任务的索引位置 + * + * @param task 子任务 + * @return 子任务的索引位置,如果未找到则返回 -1 + */ public int getChildTaskIndex(Task task) { return mChildren.indexOf(task); } + /** + * 根据索引获取子任务 + * + * @param index 索引位置,必须在 0 到子任务数量减 1 之间 + * @return 对应的子任务,如果索引无效则返回 null + */ public Task getChildTaskByIndex(int index) { if (index < 0 || index >= mChildren.size()) { Log.e(TAG, "getTaskByIndex: invalid index"); @@ -321,6 +467,12 @@ public class TaskList extends Node { return mChildren.get(index); } + /** + * 根据 GID 获取子任务 + * + * @param gid Google Tasks ID + * @return 对应的子任务,如果未找到则返回 null + */ public Task getChilTaskByGid(String gid) { for (Task task : mChildren) { if (task.getGid().equals(gid)) @@ -329,14 +481,29 @@ public class TaskList extends Node { return null; } + /** + * 获取子任务列表 + * + * @return 子任务列表的副本 + */ public ArrayList getChildTaskList() { return this.mChildren; } + /** + * 设置任务列表索引 + * + * @param index 任务列表索引 + */ public void setIndex(int index) { this.mIndex = index; } + /** + * 获取任务列表索引 + * + * @return 任务列表索引 + */ public int getIndex() { return this.mIndex; } diff --git a/src/Notes-master/src/net/micode/notes/gtask/exception/ActionFailureException.java b/src/Notes-master/src/net/micode/notes/gtask/exception/ActionFailureException.java index 15504be..12b3bcd 100644 --- a/src/Notes-master/src/net/micode/notes/gtask/exception/ActionFailureException.java +++ b/src/Notes-master/src/net/micode/notes/gtask/exception/ActionFailureException.java @@ -16,17 +16,39 @@ package net.micode.notes.gtask.exception; +/** + * 操作失败异常类 + *

+ * 用于表示 Google Tasks 同步过程中操作执行失败的情况。 + * 当同步操作(如创建、更新、删除任务或任务列表)失败时抛出此异常。 + * 该异常继承自 RuntimeException,属于非受检异常,调用方可以选择性处理。 + *

+ */ public class ActionFailureException extends RuntimeException { private static final long serialVersionUID = 4425249765923293627L; + /** + * 构造一个无详细信息的操作失败异常 + */ public ActionFailureException() { super(); } + /** + * 构造一个带有详细信息的操作失败异常 + * + * @param paramString 异常的详细信息,描述操作失败的具体原因 + */ public ActionFailureException(String paramString) { super(paramString); } + /** + * 构造一个带有详细信息和原因的操作失败异常 + * + * @param paramString 异常的详细信息,描述操作失败的具体原因 + * @param paramThrowable 导致此异常的底层异常或错误 + */ public ActionFailureException(String paramString, Throwable paramThrowable) { super(paramString, paramThrowable); } diff --git a/src/Notes-master/src/net/micode/notes/gtask/exception/NetworkFailureException.java b/src/Notes-master/src/net/micode/notes/gtask/exception/NetworkFailureException.java index b08cfb1..a7aeedf 100644 --- a/src/Notes-master/src/net/micode/notes/gtask/exception/NetworkFailureException.java +++ b/src/Notes-master/src/net/micode/notes/gtask/exception/NetworkFailureException.java @@ -16,17 +16,39 @@ package net.micode.notes.gtask.exception; +/** + * 网络异常类 + *

+ * 用于表示 Google Tasks 同步过程中发生的网络相关错误。 + * 当网络连接失败、超时或无法访问 Google Tasks 服务时抛出此异常。 + * 该异常继承自 Exception,属于受检异常,调用方必须处理或继续抛出。 + *

+ */ public class NetworkFailureException extends Exception { private static final long serialVersionUID = 2107610287180234136L; + /** + * 构造一个无详细信息的网络异常 + */ public NetworkFailureException() { super(); } + /** + * 构造一个带有详细信息的网络异常 + * + * @param paramString 异常的详细信息,描述网络失败的具体原因 + */ public NetworkFailureException(String paramString) { super(paramString); } + /** + * 构造一个带有详细信息和原因的网络异常 + * + * @param paramString 异常的详细信息,描述网络失败的具体原因 + * @param paramThrowable 导致此异常的底层异常或错误 + */ public NetworkFailureException(String paramString, Throwable paramThrowable) { super(paramString, paramThrowable); } diff --git a/src/Notes-master/src/net/micode/notes/gtask/remote/GTaskASyncTask.java b/src/Notes-master/src/net/micode/notes/gtask/remote/GTaskASyncTask.java index b3b61e7..f8ea190 100644 --- a/src/Notes-master/src/net/micode/notes/gtask/remote/GTaskASyncTask.java +++ b/src/Notes-master/src/net/micode/notes/gtask/remote/GTaskASyncTask.java @@ -29,88 +29,187 @@ import net.micode.notes.ui.NotesListActivity; import net.micode.notes.ui.NotesPreferenceActivity; +/** + * Google Tasks 同步异步任务 + *

+ * 继承自 AsyncTask,用于在后台执行 Google Tasks 同步操作。 + * 支持进度更新、通知显示和同步完成回调。 + *

+ */ public class GTaskASyncTask extends AsyncTask { + /** 同步通知的唯一标识符 */ private static int GTASK_SYNC_NOTIFICATION_ID = 5234235; + /** + * 同步完成监听器接口 + *

+ * 定义同步完成时的回调方法,用于通知调用方同步任务已结束。 + *

+ */ public interface OnCompleteListener { + /** + * 同步完成时的回调方法 + */ void onComplete(); } + /** 应用上下文 */ private Context mContext; + /** 通知管理器 */ private NotificationManager mNotifiManager; + /** Google Tasks 管理器实例 */ private GTaskManager mTaskManager; + /** 同步完成监听器 */ private OnCompleteListener mOnCompleteListener; + /** + * 构造函数 + *

+ * 初始化异步任务所需的上下文、监听器、通知管理器和任务管理器。 + *

+ * + * @param context 应用上下文 + * @param listener 同步完成监听器 + */ public GTaskASyncTask(Context context, OnCompleteListener listener) { mContext = context; mOnCompleteListener = listener; + // 获取系统通知服务 mNotifiManager = (NotificationManager) mContext .getSystemService(Context.NOTIFICATION_SERVICE); + // 获取 GTaskManager 单例 mTaskManager = GTaskManager.getInstance(); } + /** + * 取消同步操作 + *

+ * 调用 GTaskManager 的 cancelSync() 方法取消正在进行的同步。 + *

+ */ public void cancelSync() { mTaskManager.cancelSync(); } + /** + * 发布同步进度 + *

+ * 调用 AsyncTask 的 publishProgress() 方法发布进度消息到 UI 线程。 + *

+ * + * @param message 进度消息 + */ public void publishProgess(String message) { publishProgress(new String[] { message }); } + /** + * 显示同步通知 + *

+ * 在状态栏显示同步进度或结果通知。 + * 同步成功时跳转到笔记列表,其他情况跳转到设置页面。 + *

+ * + * @param tickerId 通知标题字符串资源 ID + * @param content 通知内容文本 + */ private void showNotification(int tickerId, String content) { - 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; + // 根据同步结果选择跳转目标 if (tickerId != R.string.ticker_success) { + // 同步失败或取消,跳转到设置页面 pendingIntent = PendingIntent.getActivity(mContext, 0, new Intent(mContext, - NotesPreferenceActivity.class), 0); - + NotesPreferenceActivity.class), PendingIntent.FLAG_IMMUTABLE); } else { + // 同步成功,跳转到笔记列表 pendingIntent = PendingIntent.getActivity(mContext, 0, new Intent(mContext, - NotesListActivity.class), 0); + NotesListActivity.class), PendingIntent.FLAG_IMMUTABLE); } - notification.setLatestEventInfo(mContext, mContext.getString(R.string.app_name), content, - pendingIntent); + // 构建通知 + Notification.Builder builder = new Notification.Builder(mContext) + .setAutoCancel(true) + .setContentTitle(mContext.getString(R.string.app_name)) + .setContentText(content) + .setContentIntent(pendingIntent) + .setWhen(System.currentTimeMillis()) + .setOngoing(true); + Notification notification=builder.getNotification(); + // 显示通知 mNotifiManager.notify(GTASK_SYNC_NOTIFICATION_ID, notification); } + /** + * 后台执行同步操作 + *

+ * 在后台线程执行 Google Tasks 同步,发布登录进度并返回同步结果。 + *

+ * + * @param unused 未使用的参数 + * @return 同步状态码(GTaskManager.STATE_SUCCESS、STATE_NETWORK_ERROR、STATE_INTERNAL_ERROR、STATE_SYNC_IN_PROGRESS 或 STATE_SYNC_CANCELLED) + */ @Override protected Integer doInBackground(Void... unused) { + // 发布登录进度 publishProgess(mContext.getString(R.string.sync_progress_login, NotesPreferenceActivity .getSyncAccountName(mContext))); + // 执行同步并返回结果 return mTaskManager.sync(mContext, this); } + /** + * 进度更新回调 + *

+ * 在 UI 线程更新同步进度,显示通知并发送广播。 + *

+ * + * @param progress 进度消息数组 + */ @Override protected void onProgressUpdate(String... progress) { + // 显示进度通知 showNotification(R.string.ticker_syncing, progress[0]); + // 如果上下文是 GTaskSyncService,发送广播 if (mContext instanceof GTaskSyncService) { ((GTaskSyncService) mContext).sendBroadcast(progress[0]); } } + /** + * 同步完成回调 + *

+ * 根据同步结果显示相应的通知,并调用完成监听器。 + * 更新最后同步时间(仅在同步成功时)。 + *

+ * + * @param result 同步结果状态码 + */ @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() { diff --git a/src/Notes-master/src/net/micode/notes/gtask/remote/GTaskClient.java b/src/Notes-master/src/net/micode/notes/gtask/remote/GTaskClient.java index c67dfdf..76ce522 100644 --- a/src/Notes-master/src/net/micode/notes/gtask/remote/GTaskClient.java +++ b/src/Notes-master/src/net/micode/notes/gtask/remote/GTaskClient.java @@ -61,15 +61,27 @@ import java.util.zip.Inflater; import java.util.zip.InflaterInputStream; +/** + * Google Tasks 客户端类 + *

+ * 单例模式实现的 Google Tasks API 客户端,负责与 Google Tasks 服务器的网络通信。 + * 提供登录认证、任务列表和任务的增删改查、批量更新等功能。 + * 使用 HTTP 协议与 Google Tasks API 交互,支持 Cookie 认证和会话管理。 + *

+ */ public class GTaskClient { private static final String TAG = GTaskClient.class.getSimpleName(); + /** Google Tasks 基础 URL */ private static final String GTASK_URL = "https://mail.google.com/tasks/"; + /** Google Tasks GET 请求 URL */ private static final String GTASK_GET_URL = "https://mail.google.com/tasks/ig"; + /** Google Tasks POST 请求 URL */ private static final String GTASK_POST_URL = "https://mail.google.com/tasks/r/ig"; + /** 单例实例 */ private static GTaskClient mInstance = null; private DefaultHttpClient mHttpClient; @@ -90,6 +102,12 @@ public class GTaskClient { private JSONArray mUpdateArray; + /** + * 私有构造函数 + *

+ * 初始化所有成员变量为默认值,防止外部直接实例化。 + *

+ */ private GTaskClient() { mHttpClient = null; mGetUrl = GTASK_GET_URL; @@ -102,6 +120,14 @@ public class GTaskClient { mUpdateArray = null; } + /** + * 获取 GTaskClient 单例实例 + *

+ * 使用双重检查锁定确保线程安全的单例实现。 + *

+ * + * @return GTaskClient 单例实例 + */ public static synchronized GTaskClient getInstance() { if (mInstance == null) { mInstance = new GTaskClient(); @@ -109,6 +135,17 @@ public class GTaskClient { return mInstance; } + /** + * 登录 Google Tasks + *

+ * 检查登录状态和账户信息,必要时重新登录。 + * Cookie 有效期为 5 分钟,超时后需要重新登录。 + * 支持自定义域名账户和标准 Gmail/Googlemail 账户。 + *

+ * + * @param activity Activity 上下文,用于账户管理 + * @return 如果登录成功返回 true,否则返回 false + */ public boolean login(Activity activity) { // we suppose that the cookie would expire after 5 minutes // then we need to re-login @@ -164,6 +201,26 @@ public class GTaskClient { return true; } + /** + * 获取同步账户 + * + * @return 当前登录的 Google 账户 + */ + private Account getSyncAccount() { + return mAccount; + } + + /** + * 登录 Google 账户获取认证令牌 + *

+ * 从系统账户管理器获取 Google 账户的认证令牌。 + * 如果 invalidateToken 为 true,会先使旧令牌失效再获取新令牌。 + *

+ * + * @param activity Activity 上下文 + * @param invalidateToken 是否使旧令牌失效 + * @return 认证令牌,如果失败则返回 null + */ private String loginGoogleAccount(Activity activity, boolean invalidateToken) { String authToken; AccountManager accountManager = AccountManager.get(activity); @@ -207,6 +264,16 @@ public class GTaskClient { return authToken; } + /** + * 尝试登录 Google Tasks + *

+ * 使用认证令牌尝试登录 Google Tasks,如果失败则使令牌失效并重试。 + *

+ * + * @param activity Activity 上下文 + * @param authToken 认证令牌 + * @return 如果登录成功返回 true,否则返回 false + */ private boolean tryToLoginGtask(Activity activity, String authToken) { if (!loginGtask(authToken)) { // maybe the auth token is out of date, now let's invalidate the @@ -225,6 +292,15 @@ public class GTaskClient { return true; } + /** + * 使用认证令牌登录 Google Tasks + *

+ * 向 Google Tasks 服务器发送 GET 请求进行认证,获取 Cookie 和客户端版本号。 + *

+ * + * @param authToken 认证令牌 + * @return 如果登录成功返回 true,否则返回 false + */ private boolean loginGtask(String authToken) { int timeoutConnection = 10000; int timeoutSocket = 15000; @@ -280,10 +356,26 @@ public class GTaskClient { return true; } + /** + * 获取下一个动作 ID + *

+ * 每次调用返回递增的动作 ID,用于标识不同的操作请求。 + *

+ * + * @return 动作 ID + */ private int getActionId() { return mActionId++; } + /** + * 创建 HTTP POST 请求对象 + *

+ * 配置请求头,设置内容类型为 application/x-www-form-urlencoded。 + *

+ * + * @return 配置好的 HttpPost 对象 + */ private HttpPost createHttpPost() { HttpPost httpPost = new HttpPost(mPostUrl); httpPost.setHeader("Content-Type", "application/x-www-form-urlencoded;charset=utf-8"); @@ -291,6 +383,16 @@ public class GTaskClient { return httpPost; } + /** + * 获取 HTTP 响应内容 + *

+ * 解析 HTTP 实体的内容,支持 gzip 和 deflate 压缩格式。 + *

+ * + * @param entity HTTP 响应实体 + * @return 响应内容的字符串 + * @throws IOException 如果读取响应内容失败 + */ private String getResponseContent(HttpEntity entity) throws IOException { String contentEncoding = null; if (entity.getContentEncoding() != null) { @@ -323,6 +425,17 @@ public class GTaskClient { } } + /** + * 发送 POST 请求到 Google Tasks 服务器 + *

+ * 将 JSON 数据封装为 POST 请求发送到服务器,并解析返回的 JSON 响应。 + *

+ * + * @param js 要发送的 JSON 对象 + * @return 服务器返回的 JSON 对象 + * @throws NetworkFailureException 如果网络请求失败 + * @throws ActionFailureException 如果未登录或 JSON 解析失败 + */ private JSONObject postRequest(JSONObject js) throws NetworkFailureException { if (!mLoggedin) { Log.e(TAG, "please login first"); @@ -360,6 +473,16 @@ public class GTaskClient { } } + /** + * 创建新的任务 + *

+ * 向 Google Tasks 服务器发送创建任务请求,获取服务器分配的任务 ID。 + *

+ * + * @param task 要创建的任务对象 + * @throws NetworkFailureException 如果网络请求失败 + * @throws ActionFailureException 如果 JSON 处理失败 + */ public void createTask(Task task) throws NetworkFailureException { commitUpdate(); try { @@ -386,6 +509,16 @@ public class GTaskClient { } } + /** + * 创建新的任务列表 + *

+ * 向 Google Tasks 服务器发送创建任务列表请求,获取服务器分配的任务列表 ID。 + *

+ * + * @param tasklist 要创建的任务列表对象 + * @throws NetworkFailureException 如果网络请求失败 + * @throws ActionFailureException 如果 JSON 处理失败 + */ public void createTaskList(TaskList tasklist) throws NetworkFailureException { commitUpdate(); try { @@ -412,6 +545,16 @@ public class GTaskClient { } } + /** + * 提交批量更新请求 + *

+ * 将待更新的节点批量发送到 Google Tasks 服务器。 + * 如果没有待更新的节点,则不执行任何操作。 + *

+ * + * @throws NetworkFailureException 如果网络请求失败 + * @throws ActionFailureException 如果 JSON 处理失败 + */ public void commitUpdate() throws NetworkFailureException { if (mUpdateArray != null) { try { @@ -433,6 +576,15 @@ public class GTaskClient { } } + /** + * 添加待更新节点到批量更新队列 + *

+ * 将节点添加到更新队列中,当队列超过 10 个节点时自动提交。 + *

+ * + * @param node 要更新的节点,如果为 null 则不执行任何操作 + * @throws NetworkFailureException 如果提交更新时网络请求失败 + */ public void addUpdateNode(Node node) throws NetworkFailureException { if (node != null) { // too many update items may result in an error @@ -447,6 +599,18 @@ public class GTaskClient { } } + /** + * 移动任务到新的任务列表或新位置 + *

+ * 将任务从一个任务列表移动到另一个任务列表,或在同一任务列表中调整顺序。 + *

+ * + * @param task 要移动的任务 + * @param preParent 任务的原父任务列表 + * @param curParent 任务的新父任务列表 + * @throws NetworkFailureException 如果网络请求失败 + * @throws ActionFailureException 如果 JSON 处理失败 + */ public void moveTask(Task task, TaskList preParent, TaskList curParent) throws NetworkFailureException { commitUpdate(); @@ -486,6 +650,16 @@ public class GTaskClient { } } + /** + * 删除节点 + *

+ * 向 Google Tasks 服务器发送删除节点请求,将节点标记为已删除。 + *

+ * + * @param node 要删除的节点 + * @throws NetworkFailureException 如果网络请求失败 + * @throws ActionFailureException 如果 JSON 处理失败 + */ public void deleteNode(Node node) throws NetworkFailureException { commitUpdate(); try { @@ -509,6 +683,16 @@ public class GTaskClient { } } + /** + * 获取所有任务列表 + *

+ * 从 Google Tasks 服务器获取当前账户的所有任务列表。 + *

+ * + * @return 包含所有任务列表信息的 JSON 数组 + * @throws NetworkFailureException 如果网络请求失败 + * @throws ActionFailureException 如果未登录或 JSON 解析失败 + */ public JSONArray getTaskLists() throws NetworkFailureException { if (!mLoggedin) { Log.e(TAG, "please login first"); @@ -547,6 +731,17 @@ public class GTaskClient { } } + /** + * 获取指定任务列表中的所有任务 + *

+ * 从 Google Tasks 服务器获取指定任务列表中的所有任务。 + *

+ * + * @param listGid 任务列表的 Google ID + * @return 包含该任务列表中所有任务信息的 JSON 数组 + * @throws NetworkFailureException 如果网络请求失败 + * @throws ActionFailureException 如果 JSON 处理失败 + */ public JSONArray getTaskList(String listGid) throws NetworkFailureException { commitUpdate(); try { @@ -575,10 +770,21 @@ public class GTaskClient { } } + /** + * 获取同步账户 + * + * @return 当前登录的 Google 账户 + */ public Account getSyncAccount() { return mAccount; } + /** + * 重置更新数组 + *

+ * 清空待更新的节点队列,取消所有未提交的更新操作。 + *

+ */ public void resetUpdateArray() { mUpdateArray = null; } diff --git a/src/Notes-master/src/net/micode/notes/gtask/remote/GTaskManager.java b/src/Notes-master/src/net/micode/notes/gtask/remote/GTaskManager.java index d2b4082..6beb7a7 100644 --- a/src/Notes-master/src/net/micode/notes/gtask/remote/GTaskManager.java +++ b/src/Notes-master/src/net/micode/notes/gtask/remote/GTaskManager.java @@ -48,17 +48,30 @@ import java.util.Iterator; import java.util.Map; +/** + * Google Tasks 同步管理器 + *

+ * 单例模式实现的同步管理器,负责本地笔记与 Google Tasks 之间的数据同步。 + * 提供完整的双向同步功能,包括文件夹、笔记的增删改查操作。 + * 支持同步状态管理、冲突解决和元数据维护。 + *

+ */ public class GTaskManager { private static final String TAG = GTaskManager.class.getSimpleName(); + /** 同步成功状态码 */ public static final int STATE_SUCCESS = 0; + /** 网络错误状态码 */ public static final int STATE_NETWORK_ERROR = 1; + /** 内部错误状态码 */ public static final int STATE_INTERNAL_ERROR = 2; + /** 同步进行中状态码 */ public static final int STATE_SYNC_IN_PROGRESS = 3; + /** 同步已取消状态码 */ public static final int STATE_SYNC_CANCELLED = 4; private static GTaskManager mInstance = null; @@ -87,6 +100,12 @@ public class GTaskManager { private HashMap mNidToGid; + /** + * 私有构造函数 + *

+ * 初始化所有成员变量,防止外部直接实例化。 + *

+ */ private GTaskManager() { mSyncing = false; mCancelled = false; @@ -99,6 +118,14 @@ public class GTaskManager { mNidToGid = new HashMap(); } + /** + * 获取 GTaskManager 单例实例 + *

+ * 使用双重检查锁定确保线程安全的单例实现。 + *

+ * + * @return GTaskManager 单例实例 + */ public static synchronized GTaskManager getInstance() { if (mInstance == null) { mInstance = new GTaskManager(); @@ -106,11 +133,30 @@ public class GTaskManager { return mInstance; } + /** + * 设置 Activity 上下文 + *

+ * 用于获取 Google 账户的认证令牌。 + *

+ * + * @param activity Activity 上下文 + */ public synchronized void setActivityContext(Activity activity) { // used for getting authtoken mActivity = activity; } + /** + * 执行同步操作 + *

+ * 执行本地笔记与 Google Tasks 之间的双向同步。 + * 包括登录 Google Tasks、初始化任务列表、同步内容等步骤。 + *

+ * + * @param context 应用上下文 + * @param asyncTask 异步任务对象,用于发布进度 + * @return 同步状态码(STATE_SUCCESS、STATE_NETWORK_ERROR、STATE_INTERNAL_ERROR、STATE_SYNC_IN_PROGRESS 或 STATE_SYNC_CANCELLED) + */ public int sync(Context context, GTaskASyncTask asyncTask) { if (mSyncing) { Log.d(TAG, "Sync is in progress"); @@ -790,10 +836,21 @@ public class GTaskManager { } } + /** + * 获取同步账户名称 + * + * @return 当前同步的 Google 账户名称 + */ public String getSyncAccount() { - return GTaskClient.getInstance().getSyncAccount().name; + return mActivity == null ? null : GTaskClient.getInstance().getSyncAccount().name; } + /** + * 取消同步操作 + *

+ * 设置取消标志,停止正在进行的同步操作。 + *

+ */ public void cancelSync() { mCancelled = true; } diff --git a/src/Notes-master/src/net/micode/notes/gtask/remote/GTaskSyncService.java b/src/Notes-master/src/net/micode/notes/gtask/remote/GTaskSyncService.java index cca36f7..d207f97 100644 --- a/src/Notes-master/src/net/micode/notes/gtask/remote/GTaskSyncService.java +++ b/src/Notes-master/src/net/micode/notes/gtask/remote/GTaskSyncService.java @@ -23,54 +23,110 @@ import android.content.Intent; import android.os.Bundle; import android.os.IBinder; +/** + * Google Tasks 同步服务 + *

+ * 负责管理本地笔记与 Google Tasks 之间的后台同步操作。 + * 通过异步任务执行同步,支持同步状态广播和进度更新。 + *

+ */ public class GTaskSyncService extends Service { + /** Intent 附加参数名称,用于指定同步操作类型 */ public final static String ACTION_STRING_NAME = "sync_action_type"; + /** 启动同步操作的 Action 值 */ public final static int ACTION_START_SYNC = 0; + /** 取消同步操作的 Action 值 */ public final static int ACTION_CANCEL_SYNC = 1; + /** 无效的 Action 值 */ public final static int ACTION_INVALID = 2; + /** 同步服务广播名称 */ public final static String GTASK_SERVICE_BROADCAST_NAME = "net.micode.notes.gtask.remote.gtask_sync_service"; + /** 广播附加参数名称,用于标识是否正在同步 */ public final static String GTASK_SERVICE_BROADCAST_IS_SYNCING = "isSyncing"; + /** 广播附加参数名称,用于传递同步进度消息 */ public final static String GTASK_SERVICE_BROADCAST_PROGRESS_MSG = "progressMsg"; + /** 同步异步任务实例 */ private static GTaskASyncTask mSyncTask = null; + /** 同步进度消息 */ private static String mSyncProgress = ""; + /** + * 启动同步操作 + *

+ * 创建并执行 GTaskASyncTask 异步任务,监听同步完成事件。 + * 同步完成后发送广播并停止服务。 + *

+ */ private void startSync() { + // 检查是否已有同步任务在运行 if (mSyncTask == null) { mSyncTask = new GTaskASyncTask(this, new GTaskASyncTask.OnCompleteListener() { public void onComplete() { + // 清空同步任务引用 mSyncTask = null; + // 发送同步完成广播 sendBroadcast(""); + // 停止服务 stopSelf(); } }); + // 发送同步开始广播 sendBroadcast(""); + // 执行异步同步任务 mSyncTask.execute(); } } + /** + * 取消同步操作 + *

+ * 如果存在正在运行的同步任务,则调用其 cancelSync() 方法取消同步。 + *

+ */ private void cancelSync() { if (mSyncTask != null) { + // 取消异步同步任务 mSyncTask.cancelSync(); } } + /** + * 服务创建时的回调 + *

+ * 初始化同步任务为 null。 + *

+ */ @Override public void onCreate() { mSyncTask = null; } + /** + * 服务启动命令的回调 + *

+ * 根据 Intent 中的 Action 类型执行相应的同步操作。 + * 支持 ACTION_START_SYNC 和 ACTION_CANCEL_SYNC 两种操作。 + *

+ * + * @param intent 启动服务的 Intent,包含 Action 类型参数 + * @param flags 启动标志 + * @param startId 启动 ID + * @return START_STICKY 表示服务被杀死后会自动重启 + */ @Override public int onStartCommand(Intent intent, int flags, int startId) { Bundle bundle = intent.getExtras(); + // 检查 Intent 是否包含 Action 参数 if (bundle != null && bundle.containsKey(ACTION_STRING_NAME)) { + // 根据 Action 类型执行相应操作 switch (bundle.getInt(ACTION_STRING_NAME, ACTION_INVALID)) { case ACTION_START_SYNC: startSync(); @@ -86,42 +142,99 @@ public class GTaskSyncService extends Service { return super.onStartCommand(intent, flags, startId); } + /** + * 系统内存不足时的回调 + *

+ * 取消正在进行的同步操作以释放资源。 + *

+ */ @Override public void onLowMemory() { if (mSyncTask != null) { + // 取消同步任务以释放内存 mSyncTask.cancelSync(); } } + /** + * 绑定服务的回调 + *

+ * 本服务不支持绑定,返回 null。 + *

+ * + * @param intent 绑定服务的 Intent + * @return null,表示不支持绑定 + */ public IBinder onBind(Intent intent) { return null; } + /** + * 发送同步状态广播 + *

+ * 向应用发送广播,包含当前同步状态和进度消息。 + *

+ * + * @param msg 同步进度消息 + */ public void sendBroadcast(String msg) { + // 更新同步进度消息 mSyncProgress = msg; Intent intent = new Intent(GTASK_SERVICE_BROADCAST_NAME); + // 添加是否正在同步的标志 intent.putExtra(GTASK_SERVICE_BROADCAST_IS_SYNCING, mSyncTask != null); + // 添加进度消息 intent.putExtra(GTASK_SERVICE_BROADCAST_PROGRESS_MSG, msg); + // 发送广播 sendBroadcast(intent); } + /** + * 启动同步服务 + *

+ * 设置 Activity 上下文到 GTaskManager,然后启动同步服务执行同步操作。 + *

+ * + * @param activity Activity 上下文,用于获取 Google 账户认证信息 + */ public static void startSync(Activity activity) { + // 设置 Activity 上下文用于账户认证 GTaskManager.getInstance().setActivityContext(activity); Intent intent = new Intent(activity, GTaskSyncService.class); intent.putExtra(GTaskSyncService.ACTION_STRING_NAME, GTaskSyncService.ACTION_START_SYNC); + // 启动同步服务 activity.startService(intent); } + /** + * 取消同步服务 + *

+ * 启动同步服务并发送取消同步的命令。 + *

+ * + * @param context 应用上下文 + */ public static void cancelSync(Context context) { Intent intent = new Intent(context, GTaskSyncService.class); intent.putExtra(GTaskSyncService.ACTION_STRING_NAME, GTaskSyncService.ACTION_CANCEL_SYNC); + // 启动服务发送取消命令 context.startService(intent); } + /** + * 检查是否正在同步 + * + * @return 如果正在同步返回 true,否则返回 false + */ public static boolean isSyncing() { return mSyncTask != null; } + /** + * 获取同步进度消息 + * + * @return 当前同步进度消息字符串 + */ public static String getProgressString() { return mSyncProgress; } diff --git a/src/Notes-master/src/net/micode/notes/model/Note.java b/src/Notes-master/src/net/micode/notes/model/Note.java index 6706cf6..4cbd456 100644 --- a/src/Notes-master/src/net/micode/notes/model/Note.java +++ b/src/Notes-master/src/net/micode/notes/model/Note.java @@ -34,15 +34,36 @@ import net.micode.notes.data.Notes.TextNote; import java.util.ArrayList; +/** + * 笔记数据模型类 + *

+ * 负责管理笔记的基本信息和数据内容,支持笔记的创建、修改和同步操作。 + * 包含笔记元数据(如创建时间、修改时间、父文件夹等)和笔记数据(文本、通话记录等)。 + *

+ */ public class Note { + /** 笔记差异值,用于记录需要同步的字段变更 */ private ContentValues mNoteDiffValues; + + /** 笔记数据对象,包含文本数据和通话数据 */ private NoteData mNoteData; + + /** 日志标签 */ private static final String TAG = "Note"; + /** - * Create a new note id for adding a new note to databases + * 创建新笔记 ID + *

+ * 在数据库中创建一条新笔记记录,并返回其 ID。 + * 初始化笔记的创建时间、修改时间、类型和父文件夹 ID。 + *

+ * + * @param context 应用上下文 + * @param folderId 父文件夹 ID + * @return 新创建的笔记 ID,失败时返回 0 */ public static synchronized long getNewNoteId(Context context, long folderId) { - // Create a new note in the database + // 在数据库中创建新笔记 ContentValues values = new ContentValues(); long createdTime = System.currentTimeMillis(); values.put(NoteColumns.CREATED_DATE, createdTime); @@ -54,6 +75,7 @@ public class Note { long noteId = 0; try { + // 从 URI 中提取笔记 ID noteId = Long.valueOf(uri.getPathSegments().get(1)); } catch (NumberFormatException e) { Log.e(TAG, "Get note id error :" + e.toString()); @@ -65,41 +87,114 @@ public class Note { return noteId; } + /** + * 构造函数 + *

+ * 初始化笔记差异值和笔记数据对象。 + *

+ */ public Note() { mNoteDiffValues = new ContentValues(); mNoteData = new NoteData(); } + /** + * 设置笔记属性值 + *

+ * 设置笔记的指定属性值,并标记为本地修改。 + *

+ * + * @param key 属性键名 + * @param value 属性值 + */ public void setNoteValue(String key, String value) { mNoteDiffValues.put(key, value); mNoteDiffValues.put(NoteColumns.LOCAL_MODIFIED, 1); mNoteDiffValues.put(NoteColumns.MODIFIED_DATE, System.currentTimeMillis()); } + /** + * 设置文本数据 + *

+ * 设置笔记的文本数据内容。 + *

+ * + * @param key 数据键名 + * @param value 数据值 + */ public void setTextData(String key, String value) { mNoteData.setTextData(key, value); } + /** + * 设置文本数据 ID + *

+ * 设置笔记文本数据的数据库记录 ID。 + *

+ * + * @param id 文本数据 ID + */ public void setTextDataId(long id) { mNoteData.setTextDataId(id); } + /** + * 获取文本数据 ID + * + * @return 文本数据 ID + */ public long getTextDataId() { return mNoteData.mTextDataId; } + /** + * 设置通话数据 ID + *

+ * 设置笔记通话数据的数据库记录 ID。 + *

+ * + * @param id 通话数据 ID + */ public void setCallDataId(long id) { mNoteData.setCallDataId(id); } + /** + * 设置通话数据 + *

+ * 设置笔记的通话数据内容。 + *

+ * + * @param key 数据键名 + * @param value 数据值 + */ public void setCallData(String key, String value) { mNoteData.setCallData(key, value); } + /** + * 检查是否本地修改 + *

+ * 检查笔记是否有本地未同步的修改。 + *

+ * + * @return 如果有本地修改返回 true,否则返回 false + */ public boolean isLocalModified() { return mNoteDiffValues.size() > 0 || mNoteData.isLocalModified(); } + /** + * 同步笔记到数据库 + *

+ * 将笔记的本地修改同步到数据库。 + * 更新笔记元数据和数据内容。 + *

+ * + * @param context 应用上下文 + * @param noteId 笔记 ID + * @return 如果同步成功返回 true,否则返回 false + */ public boolean syncNote(Context context, long noteId) { if (noteId <= 0) { throw new IllegalArgumentException("Wrong note id:" + noteId); @@ -110,15 +205,14 @@ public class Note { } /** - * In theory, once data changed, the note should be updated on {@link NoteColumns#LOCAL_MODIFIED} and - * {@link NoteColumns#MODIFIED_DATE}. For data safety, though update note fails, we also update the - * note data info + * 理论上,数据变更后应更新 {@link NoteColumns#LOCAL_MODIFIED} 和 + * {@link NoteColumns#MODIFIED_DATE}。为数据安全,即使更新失败也更新笔记数据信息 */ if (context.getContentResolver().update( ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, noteId), mNoteDiffValues, null, null) == 0) { Log.e(TAG, "Update note error, should not happen"); - // Do not return, fall through + // 不返回,继续执行 } mNoteDiffValues.clear(); @@ -130,17 +224,35 @@ public class Note { return true; } + /** + * 笔记数据内部类 + *

+ * 管理笔记的文本数据和通话数据。 + * 支持数据的增删改查和批量同步操作。 + *

+ */ private class NoteData { + /** 文本数据 ID */ private long mTextDataId; + /** 文本数据值 */ private ContentValues mTextDataValues; + /** 通话数据 ID */ private long mCallDataId; + /** 通话数据值 */ private ContentValues mCallDataValues; + /** 日志标签 */ private static final String TAG = "NoteData"; + /** + * 构造函数 + *

+ * 初始化文本数据和通话数据的 ContentValues 对象。 + *

+ */ public NoteData() { mTextDataValues = new ContentValues(); mCallDataValues = new ContentValues(); @@ -148,10 +260,26 @@ public class Note { mCallDataId = 0; } + /** + * 检查是否本地修改 + *

+ * 检查文本数据或通话数据是否有本地未同步的修改。 + *

+ * + * @return 如果有本地修改返回 true,否则返回 false + */ boolean isLocalModified() { return mTextDataValues.size() > 0 || mCallDataValues.size() > 0; } + /** + * 设置文本数据 ID + *

+ * 设置文本数据的数据库记录 ID。 + *

+ * + * @param id 文本数据 ID,必须大于 0 + */ void setTextDataId(long id) { if(id <= 0) { throw new IllegalArgumentException("Text data id should larger than 0"); @@ -159,6 +287,14 @@ public class Note { mTextDataId = id; } + /** + * 设置通话数据 ID + *

+ * 设置通话数据的数据库记录 ID。 + *

+ * + * @param id 通话数据 ID,必须大于 0 + */ void setCallDataId(long id) { if (id <= 0) { throw new IllegalArgumentException("Call data id should larger than 0"); @@ -166,21 +302,50 @@ public class Note { mCallDataId = id; } + /** + * 设置通话数据 + *

+ * 设置笔记的通话数据内容,并标记为本地修改。 + *

+ * + * @param key 数据键名 + * @param value 数据值 + */ void setCallData(String key, String value) { mCallDataValues.put(key, value); mNoteDiffValues.put(NoteColumns.LOCAL_MODIFIED, 1); mNoteDiffValues.put(NoteColumns.MODIFIED_DATE, System.currentTimeMillis()); } + /** + * 设置文本数据 + *

+ * 设置笔记的文本数据内容,并标记为本地修改。 + *

+ * + * @param key 数据键名 + * @param value 数据值 + */ void setTextData(String key, String value) { mTextDataValues.put(key, value); mNoteDiffValues.put(NoteColumns.LOCAL_MODIFIED, 1); mNoteDiffValues.put(NoteColumns.MODIFIED_DATE, System.currentTimeMillis()); } + /** + * 将数据推送到 ContentResolver + *

+ * 将文本数据和通话数据的修改同步到数据库。 + * 支持新增和更新操作。 + *

+ * + * @param context 应用上下文 + * @param noteId 笔记 ID + * @return 笔记 URI,失败时返回 null + */ Uri pushIntoContentResolver(Context context, long noteId) { /** - * Check for safety + * 安全性检查 */ if (noteId <= 0) { throw new IllegalArgumentException("Wrong note id:" + noteId); @@ -189,9 +354,11 @@ public class Note { ArrayList operationList = new ArrayList(); ContentProviderOperation.Builder builder = null; + // 处理文本数据 if(mTextDataValues.size() > 0) { mTextDataValues.put(DataColumns.NOTE_ID, noteId); if (mTextDataId == 0) { + // 新增文本数据 mTextDataValues.put(DataColumns.MIME_TYPE, TextNote.CONTENT_ITEM_TYPE); Uri uri = context.getContentResolver().insert(Notes.CONTENT_DATA_URI, mTextDataValues); @@ -203,6 +370,7 @@ public class Note { return null; } } else { + // 更新现有文本数据 builder = ContentProviderOperation.newUpdate(ContentUris.withAppendedId( Notes.CONTENT_DATA_URI, mTextDataId)); builder.withValues(mTextDataValues); @@ -211,9 +379,11 @@ public class Note { mTextDataValues.clear(); } + // 处理通话数据 if(mCallDataValues.size() > 0) { mCallDataValues.put(DataColumns.NOTE_ID, noteId); if (mCallDataId == 0) { + // 新增通话数据 mCallDataValues.put(DataColumns.MIME_TYPE, CallNote.CONTENT_ITEM_TYPE); Uri uri = context.getContentResolver().insert(Notes.CONTENT_DATA_URI, mCallDataValues); @@ -225,6 +395,7 @@ public class Note { return null; } } else { + // 更新现有通话数据 builder = ContentProviderOperation.newUpdate(ContentUris.withAppendedId( Notes.CONTENT_DATA_URI, mCallDataId)); builder.withValues(mCallDataValues); @@ -233,6 +404,7 @@ public class Note { mCallDataValues.clear(); } + // 批量执行更新操作 if (operationList.size() > 0) { try { ContentProviderResult[] results = context.getContentResolver().applyBatch( diff --git a/src/Notes-master/src/net/micode/notes/model/WorkingNote.java b/src/Notes-master/src/net/micode/notes/model/WorkingNote.java index be081e4..3aa0cd3 100644 --- a/src/Notes-master/src/net/micode/notes/model/WorkingNote.java +++ b/src/Notes-master/src/net/micode/notes/model/WorkingNote.java @@ -32,36 +32,57 @@ import net.micode.notes.data.Notes.TextNote; import net.micode.notes.tool.ResourceParser.NoteBgResources; +/** + * 工作笔记类 + *

+ * 表示正在编辑或查看的笔记对象,提供笔记的加载、保存和修改功能。 + * 支持文本笔记和通话记录笔记两种类型,包含笔记的所有属性和设置监听器。 + *

+ */ public class WorkingNote { - // Note for the working note + /** 底层笔记对象 */ private Note mNote; - // Note Id + + /** 笔记 ID */ private long mNoteId; - // Note content + + /** 笔记内容 */ private String mContent; - // Note mode + + /** 笔记模式 */ private int mMode; + /** 提醒日期 */ private long mAlertDate; + /** 修改日期 */ private long mModifiedDate; + /** 背景颜色 ID */ private int mBgColorId; + /** Widget ID */ private int mWidgetId; + /** Widget 类型 */ private int mWidgetType; + /** 父文件夹 ID */ private long mFolderId; + /** 应用上下文 */ private Context mContext; + /** 日志标签 */ private static final String TAG = "WorkingNote"; + /** 是否已删除 */ private boolean mIsDeleted; + /** 笔记设置变更监听器 */ private NoteSettingChangedListener mNoteSettingStatusListener; + /** 数据查询投影 - 笔记数据 */ public static final String[] DATA_PROJECTION = new String[] { DataColumns.ID, DataColumns.CONTENT, @@ -72,6 +93,7 @@ public class WorkingNote { DataColumns.DATA4, }; + /** 数据查询投影 - 笔记元数据 */ public static final String[] NOTE_PROJECTION = new String[] { NoteColumns.PARENT_ID, NoteColumns.ALERTED_DATE, @@ -81,26 +103,45 @@ public class WorkingNote { NoteColumns.MODIFIED_DATE }; + /** 数据 ID 列索引 */ private static final int DATA_ID_COLUMN = 0; + /** 数据内容列索引 */ private static final int DATA_CONTENT_COLUMN = 1; + /** 数据 MIME 类型列索引 */ private static final int DATA_MIME_TYPE_COLUMN = 2; + /** 数据模式列索引 */ private static final int DATA_MODE_COLUMN = 3; + /** 笔记父 ID 列索引 */ private static final int NOTE_PARENT_ID_COLUMN = 0; + /** 笔记提醒日期列索引 */ private static final int NOTE_ALERTED_DATE_COLUMN = 1; + /** 笔记背景颜色 ID 列索引 */ private static final int NOTE_BG_COLOR_ID_COLUMN = 2; + /** 笔记 Widget ID 列索引 */ private static final int NOTE_WIDGET_ID_COLUMN = 3; + /** 笔记 Widget 类型列索引 */ private static final int NOTE_WIDGET_TYPE_COLUMN = 4; + /** 笔记修改日期列索引 */ private static final int NOTE_MODIFIED_DATE_COLUMN = 5; + /** + * 新建笔记构造函数 + *

+ * 创建一个新的空笔记对象,初始化所有属性为默认值。 + *

+ * + * @param context 应用上下文 + * @param folderId 父文件夹 ID + */ // New note construct private WorkingNote(Context context, long folderId) { mContext = context; @@ -114,6 +155,16 @@ public class WorkingNote { mWidgetType = Notes.TYPE_WIDGET_INVALIDE; } + /** + * 已有笔记构造函数 + *

+ * 从数据库加载现有笔记数据,初始化笔记对象。 + *

+ * + * @param context 应用上下文 + * @param noteId 笔记 ID + * @param folderId 父文件夹 ID + */ // Existing note construct private WorkingNote(Context context, long noteId, long folderId) { mContext = context; @@ -124,6 +175,12 @@ public class WorkingNote { loadNote(); } + /** + * 加载笔记元数据 + *

+ * 从数据库加载笔记的基本信息,包括父文件夹、背景颜色、Widget 信息等。 + *

+ */ private void loadNote() { Cursor cursor = mContext.getContentResolver().query( ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, mNoteId), NOTE_PROJECTION, null, @@ -146,6 +203,12 @@ public class WorkingNote { loadNoteData(); } + /** + * 加载笔记数据内容 + *

+ * 从数据库加载笔记的详细数据,包括文本内容和通话记录。 + *

+ */ private void loadNoteData() { Cursor cursor = mContext.getContentResolver().query(Notes.CONTENT_DATA_URI, DATA_PROJECTION, DataColumns.NOTE_ID + "=?", new String[] { @@ -157,10 +220,12 @@ public class WorkingNote { do { String type = cursor.getString(DATA_MIME_TYPE_COLUMN); if (DataConstants.NOTE.equals(type)) { + // 加载文本笔记数据 mContent = cursor.getString(DATA_CONTENT_COLUMN); mMode = cursor.getInt(DATA_MODE_COLUMN); mNote.setTextDataId(cursor.getLong(DATA_ID_COLUMN)); } else if (DataConstants.CALL_NOTE.equals(type)) { + // 加载通话记录数据 mNote.setCallDataId(cursor.getLong(DATA_ID_COLUMN)); } else { Log.d(TAG, "Wrong note type with type:" + type); @@ -174,6 +239,19 @@ public class WorkingNote { } } + /** + * 创建空笔记 + *

+ * 创建一个新的空笔记对象,并设置默认属性。 + *

+ * + * @param context 应用上下文 + * @param folderId 父文件夹 ID + * @param widgetId Widget ID + * @param widgetType Widget 类型 + * @param defaultBgColorId 默认背景颜色 ID + * @return 新创建的 WorkingNote 对象 + */ public static WorkingNote createEmptyNote(Context context, long folderId, int widgetId, int widgetType, int defaultBgColorId) { WorkingNote note = new WorkingNote(context, folderId); @@ -183,23 +261,45 @@ public class WorkingNote { return note; } + /** + * 加载已有笔记 + *

+ * 从数据库加载指定 ID 的笔记。 + *

+ * + * @param context 应用上下文 + * @param id 笔记 ID + * @return 加载的 WorkingNote 对象 + */ public static WorkingNote load(Context context, long id) { return new WorkingNote(context, id, 0); } + /** + * 保存笔记 + *

+ * 将笔记的修改保存到数据库。 + * 如果笔记不存在则创建新笔记,否则更新现有笔记。 + * 如果有 Widget 则更新 Widget 内容。 + *

+ * + * @return 如果保存成功返回 true,否则返回 false + */ public synchronized boolean saveNote() { if (isWorthSaving()) { if (!existInDatabase()) { + // 创建新笔记 if ((mNoteId = Note.getNewNoteId(mContext, mFolderId)) == 0) { Log.e(TAG, "Create new note fail with id:" + mNoteId); return false; } } + // 同步笔记数据 mNote.syncNote(mContext, mNoteId); /** - * Update widget content if there exist any widget of this note + * 如果存在该笔记的 Widget,则更新 Widget 内容 */ if (mWidgetId != AppWidgetManager.INVALID_APPWIDGET_ID && mWidgetType != Notes.TYPE_WIDGET_INVALIDE @@ -212,10 +312,23 @@ public class WorkingNote { } } + /** + * 检查笔记是否存在于数据库 + * + * @return 如果笔记 ID 大于 0 返回 true,否则返回 false + */ public boolean existInDatabase() { return mNoteId > 0; } + /** + * 检查是否值得保存 + *

+ * 判断笔记是否有需要保存的修改。 + *

+ * + * @return 如果值得保存返回 true,否则返回 false + */ private boolean isWorthSaving() { if (mIsDeleted || (!existInDatabase() && TextUtils.isEmpty(mContent)) || (existInDatabase() && !mNote.isLocalModified())) { @@ -225,10 +338,24 @@ public class WorkingNote { } } + /** + * 设置笔记设置变更监听器 + * + * @param l 监听器对象 + */ public void setOnSettingStatusChangedListener(NoteSettingChangedListener l) { mNoteSettingStatusListener = l; } + /** + * 设置提醒日期 + *

+ * 设置笔记的提醒日期,并通知监听器。 + *

+ * + * @param date 提醒日期(毫秒时间戳) + * @param set 是否设置提醒 + */ public void setAlertDate(long date, boolean set) { if (date != mAlertDate) { mAlertDate = date; @@ -239,6 +366,14 @@ public class WorkingNote { } } + /** + * 标记删除 + *

+ * 标记笔记为删除状态,并更新 Widget。 + *

+ * + * @param mark 是否标记为删除 + */ public void markDeleted(boolean mark) { mIsDeleted = mark; if (mWidgetId != AppWidgetManager.INVALID_APPWIDGET_ID @@ -247,6 +382,14 @@ public class WorkingNote { } } + /** + * 设置背景颜色 ID + *

+ * 设置笔记的背景颜色,并通知监听器。 + *

+ * + * @param id 背景颜色 ID + */ public void setBgColorId(int id) { if (id != mBgColorId) { mBgColorId = id; @@ -257,6 +400,14 @@ public class WorkingNote { } } + /** + * 设置清单模式 + *

+ * 设置笔记的编辑模式(普通模式或清单模式),并通知监听器。 + *

+ * + * @param mode 模式值 + */ public void setCheckListMode(int mode) { if (mMode != mode) { if (mNoteSettingStatusListener != null) { @@ -267,6 +418,11 @@ public class WorkingNote { } } + /** + * 设置 Widget 类型 + * + * @param type Widget 类型 + */ public void setWidgetType(int type) { if (type != mWidgetType) { mWidgetType = type; @@ -274,6 +430,11 @@ public class WorkingNote { } } + /** + * 设置 Widget ID + * + * @param id Widget ID + */ public void setWidgetId(int id) { if (id != mWidgetId) { mWidgetId = id; @@ -281,6 +442,14 @@ public class WorkingNote { } } + /** + * 设置工作文本 + *

+ * 设置笔记的文本内容。 + *

+ * + * @param text 文本内容 + */ public void setWorkingText(String text) { if (!TextUtils.equals(mContent, text)) { mContent = text; @@ -288,80 +457,159 @@ public class WorkingNote { } } + /** + * 转换为通话记录笔记 + *

+ * 将笔记转换为通话记录类型,设置电话号码和通话日期。 + *

+ * + * @param phoneNumber 电话号码 + * @param callDate 通话日期(毫秒时间戳) + */ public void convertToCallNote(String phoneNumber, long callDate) { mNote.setCallData(CallNote.CALL_DATE, String.valueOf(callDate)); mNote.setCallData(CallNote.PHONE_NUMBER, phoneNumber); mNote.setNoteValue(NoteColumns.PARENT_ID, String.valueOf(Notes.ID_CALL_RECORD_FOLDER)); } + /** + * 检查是否有提醒 + * + * @return 如果有提醒返回 true,否则返回 false + */ public boolean hasClockAlert() { return (mAlertDate > 0 ? true : false); } + /** + * 获取笔记内容 + * + * @return 笔记内容字符串 + */ public String getContent() { return mContent; } + /** + * 获取提醒日期 + * + * @return 提醒日期(毫秒时间戳) + */ public long getAlertDate() { return mAlertDate; } + /** + * 获取修改日期 + * + * @return 修改日期(毫秒时间戳) + */ public long getModifiedDate() { return mModifiedDate; } + /** + * 获取背景颜色资源 ID + * + * @return 背景颜色资源 ID + */ public int getBgColorResId() { return NoteBgResources.getNoteBgResource(mBgColorId); } + /** + * 获取背景颜色 ID + * + * @return 背景颜色 ID + */ public int getBgColorId() { return mBgColorId; } + /** + * 获取标题背景资源 ID + * + * @return 标题背景资源 ID + */ public int getTitleBgResId() { return NoteBgResources.getNoteTitleBgResource(mBgColorId); } + /** + * 获取清单模式 + * + * @return 清单模式值 + */ public int getCheckListMode() { return mMode; } + /** + * 获取笔记 ID + * + * @return 笔记 ID + */ public long getNoteId() { return mNoteId; } + /** + * 获取父文件夹 ID + * + * @return 父文件夹 ID + */ public long getFolderId() { return mFolderId; } + /** + * 获取 Widget ID + * + * @return Widget ID + */ public int getWidgetId() { return mWidgetId; } + /** + * 获取 Widget 类型 + * + * @return Widget 类型 + */ public int getWidgetType() { return mWidgetType; } + /** + * 笔记设置变更监听器接口 + *

+ * 定义笔记设置变更时的回调方法,用于通知 UI 更新。 + *

+ */ public interface NoteSettingChangedListener { /** - * Called when the background color of current note has just changed + * 当前笔记背景颜色变更时调用 */ void onBackgroundColorChanged(); /** - * Called when user set clock + * 用户设置闹钟时调用 + * + * @param date 提醒日期 + * @param set 是否设置提醒 */ void onClockAlertChanged(long date, boolean set); /** - * Call when user create note from widget + * 用户从 Widget 创建笔记时调用 */ void onWidgetChanged(); /** - * Call when switch between check list mode and normal mode - * @param oldMode is previous mode before change - * @param newMode is new mode + * 在清单模式和普通模式之间切换时调用 + * + * @param oldMode 变更前的模式 + * @param newMode 变更后的模式 */ void onCheckListModeChanged(int oldMode, int newMode); } diff --git a/src/Notes-master/src/net/micode/notes/tool/BackupUtils.java b/src/Notes-master/src/net/micode/notes/tool/BackupUtils.java index 39f6ec4..10c3994 100644 --- a/src/Notes-master/src/net/micode/notes/tool/BackupUtils.java +++ b/src/Notes-master/src/net/micode/notes/tool/BackupUtils.java @@ -36,11 +36,27 @@ import java.io.IOException; import java.io.PrintStream; +/** + * 备份工具类 + *

+ * 提供笔记数据导出为文本文件的功能。 + * 支持将笔记、文件夹、通话记录等数据导出到 SD 卡中。 + * 使用单例模式确保全局只有一个实例。 + *

+ */ public class BackupUtils { + /** 日志标签 */ private static final String TAG = "BackupUtils"; // Singleton stuff + /** 单例实例 */ private static BackupUtils sInstance; + /** + * 获取备份工具类的单例实例 + * + * @param context 应用上下文 + * @return 备份工具类实例 + */ public static synchronized BackupUtils getInstance(Context context) { if (sInstance == null) { sInstance = new BackupUtils(context); @@ -49,43 +65,84 @@ public class BackupUtils { } /** - * Following states are signs to represents backup or restore - * status + * 备份或恢复的状态常量 + *

+ * 以下状态常量用于表示备份或恢复操作的状态。 + *

*/ // Currently, the sdcard is not mounted + /** SD 卡未挂载 */ public static final int STATE_SD_CARD_UNMOUONTED = 0; // The backup file not exist + /** 备份文件不存在 */ public static final int STATE_BACKUP_FILE_NOT_EXIST = 1; // The data is not well formated, may be changed by other programs + /** 数据格式损坏,可能被其他程序修改 */ public static final int STATE_DATA_DESTROIED = 2; // Some run-time exception which causes restore or backup fails + /** 系统错误,运行时异常导致备份或恢复失败 */ public static final int STATE_SYSTEM_ERROR = 3; // Backup or restore success + /** 备份或恢复成功 */ public static final int STATE_SUCCESS = 4; + /** 文本导出对象 */ private TextExport mTextExport; + /** + * 私有构造函数 + * + * @param context 应用上下文 + */ private BackupUtils(Context context) { mTextExport = new TextExport(context); } + /** + * 检查外部存储是否可用 + * + * @return 如果外部存储已挂载且可读写则返回 true,否则返回 false + */ private static boolean externalStorageAvailable() { return Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState()); } + /** + * 导出笔记数据为文本文件 + * + * @return 导出状态码,可能为 STATE_SD_CARD_UNMOUONTED、STATE_SYSTEM_ERROR 或 STATE_SUCCESS + */ public int exportToText() { return mTextExport.exportToText(); } + /** + * 获取导出的文本文件名 + * + * @return 导出的文本文件名 + */ public String getExportedTextFileName() { return mTextExport.mFileName; } + /** + * 获取导出的文本文件目录 + * + * @return 导出的文本文件目录路径 + */ public String getExportedTextFileDir() { return mTextExport.mFileDirectory; } + /** + * 文本导出内部类 + *

+ * 负责将笔记数据导出为可读的文本文件。 + * 支持导出文件夹、笔记和通话记录等不同类型的数据。 + *

+ */ private static class TextExport { + /** 笔记查询投影字段 */ private static final String[] NOTE_PROJECTION = { NoteColumns.ID, NoteColumns.MODIFIED_DATE, @@ -93,12 +150,16 @@ public class BackupUtils { NoteColumns.TYPE }; + /** 笔记 ID 列索引 */ private static final int NOTE_COLUMN_ID = 0; + /** 笔记修改日期列索引 */ private static final int NOTE_COLUMN_MODIFIED_DATE = 1; + /** 笔记摘要列索引 */ private static final int NOTE_COLUMN_SNIPPET = 2; + /** 数据查询投影字段 */ private static final String[] DATA_PROJECTION = { DataColumns.CONTENT, DataColumns.MIME_TYPE, @@ -108,23 +169,39 @@ public class BackupUtils { DataColumns.DATA4, }; + /** 数据内容列索引 */ private static final int DATA_COLUMN_CONTENT = 0; + /** 数据 MIME 类型列索引 */ private static final int DATA_COLUMN_MIME_TYPE = 1; + /** 通话日期列索引 */ private static final int DATA_COLUMN_CALL_DATE = 2; + /** 电话号码列索引 */ private static final int DATA_COLUMN_PHONE_NUMBER = 4; + /** 导出文本格式数组 */ private final String [] TEXT_FORMAT; + /** 文件夹名称格式索引 */ private static final int FORMAT_FOLDER_NAME = 0; + /** 笔记日期格式索引 */ private static final int FORMAT_NOTE_DATE = 1; + /** 笔记内容格式索引 */ private static final int FORMAT_NOTE_CONTENT = 2; + /** 应用上下文 */ private Context mContext; + /** 导出文件名 */ private String mFileName; + /** 导出文件目录 */ private String mFileDirectory; + /** + * 构造函数 + * + * @param context 应用上下文 + */ public TextExport(Context context) { TEXT_FORMAT = context.getResources().getStringArray(R.array.format_for_exported_note); mContext = context; @@ -132,12 +209,24 @@ public class BackupUtils { mFileDirectory = ""; } + /** + * 获取指定格式的文本 + * + * @param id 格式索引 + * @return 格式化字符串 + */ private String getFormat(int id) { return TEXT_FORMAT[id]; } /** - * Export the folder identified by folder id to text + * 导出指定文件夹及其笔记到文本 + *

+ * 查询属于该文件夹的所有笔记,并将每个笔记的内容导出到输出流中。 + *

+ * + * @param folderId 文件夹 ID + * @param ps 输出流 */ private void exportFolderToText(String folderId, PrintStream ps) { // Query notes belong to this folder @@ -163,7 +252,13 @@ public class BackupUtils { } /** - * Export note identified by id to a print stream + * 导出指定笔记到输出流 + *

+ * 查询笔记的所有数据,根据 MIME 类型分别处理通话记录和普通笔记。 + *

+ * + * @param noteId 笔记 ID + * @param ps 输出流 */ private void exportNoteToText(String noteId, PrintStream ps) { Cursor dataCursor = mContext.getContentResolver().query(Notes.CONTENT_DATA_URI, @@ -216,7 +311,13 @@ public class BackupUtils { } /** - * Note will be exported as text which is user readable + * 导出笔记数据为文本文件 + *

+ * 将所有笔记、文件夹和通话记录导出为用户可读的文本文件。 + * 首先导出文件夹及其笔记,然后导出根目录下的笔记。 + *

+ * + * @return 导出状态码,可能为 STATE_SD_CARD_UNMOUONTED、STATE_SYSTEM_ERROR 或 STATE_SUCCESS */ public int exportToText() { if (!externalStorageAvailable()) { @@ -283,7 +384,12 @@ public class BackupUtils { } /** - * Get a print stream pointed to the file {@generateExportedTextFile} + * 获取导出文本文件的输出流 + *

+ * 在 SD 卡上创建导出文件,并返回对应的 PrintStream。 + *

+ * + * @return PrintStream 对象,如果创建失败则返回 null */ private PrintStream getExportToTextPrintStream() { File file = generateFileMountedOnSDcard(mContext, R.string.file_path, @@ -310,7 +416,15 @@ public class BackupUtils { } /** - * Generate the text file to store imported data + * 在 SD 卡上生成导出文本文件 + *

+ * 在指定的路径下创建导出文件,如果目录不存在则创建目录。 + *

+ * + * @param context 应用上下文 + * @param filePathResId 文件路径资源 ID + * @param fileNameFormatResId 文件名格式资源 ID + * @return 生成的文件对象,如果创建失败则返回 null */ private static File generateFileMountedOnSDcard(Context context, int filePathResId, int fileNameFormatResId) { StringBuilder sb = new StringBuilder(); @@ -325,9 +439,11 @@ public class BackupUtils { try { if (!filedir.exists()) { + // 创建目录 filedir.mkdir(); } if (!file.exists()) { + // 创建文件 file.createNewFile(); } return file; diff --git a/src/Notes-master/src/net/micode/notes/tool/DataUtils.java b/src/Notes-master/src/net/micode/notes/tool/DataUtils.java index 2a14982..d982351 100644 --- a/src/Notes-master/src/net/micode/notes/tool/DataUtils.java +++ b/src/Notes-master/src/net/micode/notes/tool/DataUtils.java @@ -35,8 +35,28 @@ import java.util.ArrayList; import java.util.HashSet; +/** + * 数据工具类 + *

+ * 提供笔记数据的批量操作、查询和统计功能。 + * 支持批量删除、移动笔记,以及各种数据查询操作。 + *

+ */ public class DataUtils { + /** 日志标签 */ public static final String TAG = "DataUtils"; + + /** + * 批量删除笔记 + *

+ * 从数据库中批量删除指定 ID 的笔记。 + * 跳过系统根文件夹,不允许删除系统文件夹。 + *

+ * + * @param resolver ContentResolver 对象 + * @param ids 要删除的笔记 ID 集合 + * @return 如果删除成功返回 true,否则返回 false + */ public static boolean batchDeleteNotes(ContentResolver resolver, HashSet ids) { if (ids == null) { Log.d(TAG, "the ids is null"); @@ -50,6 +70,7 @@ public class DataUtils { ArrayList operationList = new ArrayList(); for (long id : ids) { if(id == Notes.ID_ROOT_FOLDER) { + // 跳过系统根文件夹 Log.e(TAG, "Don't delete system folder root"); continue; } @@ -72,6 +93,17 @@ public class DataUtils { return false; } + /** + * 移动笔记到指定文件夹 + *

+ * 将笔记从源文件夹移动到目标文件夹,并记录原始父文件夹 ID。 + *

+ * + * @param resolver ContentResolver 对象 + * @param id 笔记 ID + * @param srcFolderId 源文件夹 ID + * @param desFolderId 目标文件夹 ID + */ public static void moveNoteToFoler(ContentResolver resolver, long id, long srcFolderId, long desFolderId) { ContentValues values = new ContentValues(); values.put(NoteColumns.PARENT_ID, desFolderId); @@ -80,6 +112,17 @@ public class DataUtils { resolver.update(ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, id), values, null, null); } + /** + * 批量移动笔记到指定文件夹 + *

+ * 将多个笔记批量移动到目标文件夹。 + *

+ * + * @param resolver ContentResolver 对象 + * @param ids 要移动的笔记 ID 集合 + * @param folderId 目标文件夹 ID + * @return 如果移动成功返回 true,否则返回 false + */ public static boolean batchMoveToFolder(ContentResolver resolver, HashSet ids, long folderId) { if (ids == null) { @@ -112,7 +155,14 @@ public class DataUtils { } /** - * Get the all folder count except system folders {@link Notes#TYPE_SYSTEM}} + * 获取用户文件夹数量 + *

+ * 统计除系统文件夹外的所有用户文件夹数量。 + * 排除回收站文件夹。 + *

+ * + * @param resolver ContentResolver 对象 + * @return 用户文件夹数量 */ public static int getUserFolderCount(ContentResolver resolver) { Cursor cursor =resolver.query(Notes.CONTENT_NOTE_URI, @@ -136,6 +186,17 @@ public class DataUtils { return count; } + /** + * 检查笔记是否在数据库中可见 + *

+ * 检查指定 ID 和类型的笔记是否在数据库中存在且可见(不在回收站)。 + *

+ * + * @param resolver ContentResolver 对象 + * @param noteId 笔记 ID + * @param type 笔记类型 + * @return 如果笔记可见返回 true,否则返回 false + */ public static boolean visibleInNoteDatabase(ContentResolver resolver, long noteId, int type) { Cursor cursor = resolver.query(ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, noteId), null, @@ -153,6 +214,16 @@ public class DataUtils { return exist; } + /** + * 检查笔记是否存在于数据库 + *

+ * 检查指定 ID 的笔记是否在数据库中存在。 + *

+ * + * @param resolver ContentResolver 对象 + * @param noteId 笔记 ID + * @return 如果笔记存在返回 true,否则返回 false + */ public static boolean existInNoteDatabase(ContentResolver resolver, long noteId) { Cursor cursor = resolver.query(ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, noteId), null, null, null, null); @@ -167,6 +238,16 @@ public class DataUtils { return exist; } + /** + * 检查数据是否存在于数据库 + *

+ * 检查指定 ID 的笔记数据是否在数据库中存在。 + *

+ * + * @param resolver ContentResolver 对象 + * @param dataId 数据 ID + * @return 如果数据存在返回 true,否则返回 false + */ public static boolean existInDataDatabase(ContentResolver resolver, long dataId) { Cursor cursor = resolver.query(ContentUris.withAppendedId(Notes.CONTENT_DATA_URI, dataId), null, null, null, null); @@ -181,6 +262,16 @@ public class DataUtils { return exist; } + /** + * 检查可见文件夹名称是否存在 + *

+ * 检查指定名称的文件夹是否在可见区域存在(不在回收站)。 + *

+ * + * @param resolver ContentResolver 对象 + * @param name 文件夹名称 + * @return 如果文件夹名称存在返回 true,否则返回 false + */ public static boolean checkVisibleFolderName(ContentResolver resolver, String name) { Cursor cursor = resolver.query(Notes.CONTENT_NOTE_URI, null, NoteColumns.TYPE + "=" + Notes.TYPE_FOLDER + @@ -197,6 +288,16 @@ public class DataUtils { return exist; } + /** + * 获取文件夹中的 Widget 信息 + *

+ * 获取指定文件夹下所有笔记关联的 Widget 信息。 + *

+ * + * @param resolver ContentResolver 对象 + * @param folderId 文件夹 ID + * @return Widget 属性集合,如果没有则返回 null + */ public static HashSet getFolderNoteWidget(ContentResolver resolver, long folderId) { Cursor c = resolver.query(Notes.CONTENT_NOTE_URI, new String[] { NoteColumns.WIDGET_ID, NoteColumns.WIDGET_TYPE }, @@ -224,6 +325,16 @@ public class DataUtils { return set; } + /** + * 根据笔记 ID 获取通话号码 + *

+ * 查询指定笔记 ID 关联的通话记录中的电话号码。 + *

+ * + * @param resolver ContentResolver 对象 + * @param noteId 笔记 ID + * @return 电话号码,如果未找到则返回空字符串 + */ public static String getCallNumberByNoteId(ContentResolver resolver, long noteId) { Cursor cursor = resolver.query(Notes.CONTENT_DATA_URI, new String [] { CallNote.PHONE_NUMBER }, @@ -243,6 +354,17 @@ public class DataUtils { return ""; } + /** + * 根据电话号码和通话日期获取笔记 ID + *

+ * 查询指定电话号码和通话日期对应的笔记 ID。 + *

+ * + * @param resolver ContentResolver 对象 + * @param phoneNumber 电话号码 + * @param callDate 通话日期(毫秒时间戳) + * @return 笔记 ID,如果未找到则返回 0 + */ public static long getNoteIdByPhoneNumberAndCallDate(ContentResolver resolver, String phoneNumber, long callDate) { Cursor cursor = resolver.query(Notes.CONTENT_DATA_URI, new String [] { CallNote.NOTE_ID }, @@ -264,6 +386,17 @@ public class DataUtils { return 0; } + /** + * 根据笔记 ID 获取摘要 + *

+ * 查询指定笔记 ID 的摘要内容。 + *

+ * + * @param resolver ContentResolver 对象 + * @param noteId 笔记 ID + * @return 笔记摘要 + * @throws IllegalArgumentException 如果笔记不存在 + */ public static String getSnippetById(ContentResolver resolver, long noteId) { Cursor cursor = resolver.query(Notes.CONTENT_NOTE_URI, new String [] { NoteColumns.SNIPPET }, @@ -282,9 +415,20 @@ public class DataUtils { throw new IllegalArgumentException("Note is not found with id: " + noteId); } + /** + * 格式化摘要内容 + *

+ * 去除摘要首尾空格,并截取到第一个换行符之前的内容。 + *

+ * + * @param snippet 原始摘要内容 + * @return 格式化后的摘要内容 + */ public static String getFormattedSnippet(String snippet) { if (snippet != null) { + // 去除首尾空格 snippet = snippet.trim(); + // 截取到第一个换行符之前的内容 int index = snippet.indexOf('\n'); if (index != -1) { snippet = snippet.substring(0, index); diff --git a/src/Notes-master/src/net/micode/notes/tool/GTaskStringUtils.java b/src/Notes-master/src/net/micode/notes/tool/GTaskStringUtils.java index 666b729..ce9eb54 100644 --- a/src/Notes-master/src/net/micode/notes/tool/GTaskStringUtils.java +++ b/src/Notes-master/src/net/micode/notes/tool/GTaskStringUtils.java @@ -16,98 +16,150 @@ package net.micode.notes.tool; +/** + * Google Tasks 字符串常量工具类 + *

+ * 定义与 Google Tasks 同步相关的所有 JSON 字段名称和常量。 + * 包括操作类型、实体类型、文件夹名称等常量定义。 + *

+ */ public class GTaskStringUtils { + /** 操作 ID */ public final static String GTASK_JSON_ACTION_ID = "action_id"; + /** 操作列表 */ public final static String GTASK_JSON_ACTION_LIST = "action_list"; + /** 操作类型 */ public final static String GTASK_JSON_ACTION_TYPE = "action_type"; + /** 创建操作类型 */ public final static String GTASK_JSON_ACTION_TYPE_CREATE = "create"; + /** 获取所有操作类型 */ public final static String GTASK_JSON_ACTION_TYPE_GETALL = "get_all"; + /** 移动操作类型 */ public final static String GTASK_JSON_ACTION_TYPE_MOVE = "move"; + /** 更新操作类型 */ public final static String GTASK_JSON_ACTION_TYPE_UPDATE = "update"; + /** 创建者 ID */ public final static String GTASK_JSON_CREATOR_ID = "creator_id"; + /** 子实体 */ public final static String GTASK_JSON_CHILD_ENTITY = "child_entity"; + /** 客户端版本 */ public final static String GTASK_JSON_CLIENT_VERSION = "client_version"; + /** 完成状态 */ public final static String GTASK_JSON_COMPLETED = "completed"; + /** 当前列表 ID */ public final static String GTASK_JSON_CURRENT_LIST_ID = "current_list_id"; + /** 默认列表 ID */ public final static String GTASK_JSON_DEFAULT_LIST_ID = "default_list_id"; + /** 删除标记 */ public final static String GTASK_JSON_DELETED = "deleted"; + /** 目标列表 */ public final static String GTASK_JSON_DEST_LIST = "dest_list"; + /** 目标父节点 */ public final static String GTASK_JSON_DEST_PARENT = "dest_parent"; + /** 目标父节点类型 */ public final static String GTASK_JSON_DEST_PARENT_TYPE = "dest_parent_type"; + /** 实体增量 */ public final static String GTASK_JSON_ENTITY_DELTA = "entity_delta"; + /** 实体类型 */ public final static String GTASK_JSON_ENTITY_TYPE = "entity_type"; + /** 获取已删除标记 */ public final static String GTASK_JSON_GET_DELETED = "get_deleted"; + /** ID */ public final static String GTASK_JSON_ID = "id"; + /** 索引 */ public final static String GTASK_JSON_INDEX = "index"; + /** 最后修改时间 */ public final static String GTASK_JSON_LAST_MODIFIED = "last_modified"; + /** 最新同步点 */ public final static String GTASK_JSON_LATEST_SYNC_POINT = "latest_sync_point"; + /** 列表 ID */ public final static String GTASK_JSON_LIST_ID = "list_id"; + /** 列表集合 */ public final static String GTASK_JSON_LISTS = "lists"; + /** 名称 */ public final static String GTASK_JSON_NAME = "name"; + /** 新 ID */ public final static String GTASK_JSON_NEW_ID = "new_id"; + /** 笔记集合 */ public final static String GTASK_JSON_NOTES = "notes"; + /** 父节点 ID */ public final static String GTASK_JSON_PARENT_ID = "parent_id"; + /** 前一个兄弟节点 ID */ public final static String GTASK_JSON_PRIOR_SIBLING_ID = "prior_sibling_id"; + /** 结果集合 */ public final static String GTASK_JSON_RESULTS = "results"; + /** 源列表 */ public final static String GTASK_JSON_SOURCE_LIST = "source_list"; + /** 任务集合 */ public final static String GTASK_JSON_TASKS = "tasks"; + /** 类型 */ public final static String GTASK_JSON_TYPE = "type"; + /** 分组类型 */ public final static String GTASK_JSON_TYPE_GROUP = "GROUP"; + /** 任务类型 */ public final static String GTASK_JSON_TYPE_TASK = "TASK"; + /** 用户信息 */ public final static String GTASK_JSON_USER = "user"; + /** MIUI 文件夹前缀 */ public final static String MIUI_FOLDER_PREFFIX = "[MIUI_Notes]"; + /** 默认文件夹名称 */ public final static String FOLDER_DEFAULT = "Default"; + /** 通话记录文件夹名称 */ public final static String FOLDER_CALL_NOTE = "Call_Note"; + /** 元数据文件夹名称 */ public final static String FOLDER_META = "METADATA"; + /** 元数据 GTask ID 头 */ public final static String META_HEAD_GTASK_ID = "meta_gid"; + /** 元数据笔记头 */ public final static String META_HEAD_NOTE = "meta_note"; + /** 元数据头 */ public final static String META_HEAD_DATA = "meta_data"; + /** 元数据笔记名称 */ public final static String META_NOTE_NAME = "[META INFO] DON'T UPDATE AND DELETE"; - } diff --git a/src/Notes-master/src/net/micode/notes/tool/ResourceParser.java b/src/Notes-master/src/net/micode/notes/tool/ResourceParser.java index 1ad3ad6..4677289 100644 --- a/src/Notes-master/src/net/micode/notes/tool/ResourceParser.java +++ b/src/Notes-master/src/net/micode/notes/tool/ResourceParser.java @@ -22,24 +22,57 @@ import android.preference.PreferenceManager; import net.micode.notes.R; import net.micode.notes.ui.NotesPreferenceActivity; +/** + * 资源解析工具类 + *

+ * 提供笔记背景颜色、字体大小、Widget 样式等资源的解析和获取功能。 + * 支持多种颜色主题和字体大小的配置。 + *

+ */ public class ResourceParser { + /** 黄色背景 */ public static final int YELLOW = 0; + + /** 蓝色背景 */ public static final int BLUE = 1; + + /** 白色背景 */ public static final int WHITE = 2; + + /** 绿色背景 */ public static final int GREEN = 3; + + /** 红色背景 */ public static final int RED = 4; + /** 默认背景颜色 */ public static final int BG_DEFAULT_COLOR = YELLOW; + /** 小号字体 */ public static final int TEXT_SMALL = 0; + + /** 中号字体 */ public static final int TEXT_MEDIUM = 1; + + /** 大号字体 */ public static final int TEXT_LARGE = 2; + + /** 超大号字体 */ public static final int TEXT_SUPER = 3; + /** 默认字体大小 */ public static final int BG_DEFAULT_FONT_SIZE = TEXT_MEDIUM; + /** + * 笔记背景资源类 + *

+ * 提供笔记编辑页面的背景颜色资源。 + * 包含编辑区域背景和标题栏背景两种资源。 + *

+ */ public static class NoteBgResources { + /** 编辑区域背景资源数组 */ private final static int [] BG_EDIT_RESOURCES = new int [] { R.drawable.edit_yellow, R.drawable.edit_blue, @@ -48,6 +81,7 @@ public class ResourceParser { R.drawable.edit_red }; + /** 标题栏背景资源数组 */ private final static int [] BG_EDIT_TITLE_RESOURCES = new int [] { R.drawable.edit_title_yellow, R.drawable.edit_title_blue, @@ -56,25 +90,56 @@ public class ResourceParser { R.drawable.edit_title_red }; + /** + * 获取笔记编辑区域背景资源 ID + * + * @param id 背景颜色 ID(0-4) + * @return 背景资源 ID + */ public static int getNoteBgResource(int id) { return BG_EDIT_RESOURCES[id]; } + /** + * 获取笔记标题栏背景资源 ID + * + * @param id 背景颜色 ID(0-4) + * @return 标题栏背景资源 ID + */ public static int getNoteTitleBgResource(int id) { return BG_EDIT_TITLE_RESOURCES[id]; } } + /** + * 获取默认背景颜色 ID + *

+ * 根据用户设置返回默认背景颜色。 + * 如果用户启用了随机背景颜色,则随机返回一个颜色 ID。 + *

+ * + * @param context 应用上下文 + * @return 背景颜色 ID(0-4) + */ public static int getDefaultBgId(Context context) { if (PreferenceManager.getDefaultSharedPreferences(context).getBoolean( NotesPreferenceActivity.PREFERENCE_SET_BG_COLOR_KEY, false)) { + // 随机选择背景颜色 return (int) (Math.random() * NoteBgResources.BG_EDIT_RESOURCES.length); } else { return BG_DEFAULT_COLOR; } } + /** + * 笔记列表项背景资源类 + *

+ * 提供笔记列表项的背景颜色资源。 + * 包含首项、中间项、末项和单项四种样式。 + *

+ */ public static class NoteItemBgResources { + /** 首项背景资源数组 */ private final static int [] BG_FIRST_RESOURCES = new int [] { R.drawable.list_yellow_up, R.drawable.list_blue_up, @@ -83,6 +148,7 @@ public class ResourceParser { R.drawable.list_red_up }; + /** 中间项背景资源数组 */ private final static int [] BG_NORMAL_RESOURCES = new int [] { R.drawable.list_yellow_middle, R.drawable.list_blue_middle, @@ -91,6 +157,7 @@ public class ResourceParser { R.drawable.list_red_middle }; + /** 末项背景资源数组 */ private final static int [] BG_LAST_RESOURCES = new int [] { R.drawable.list_yellow_down, R.drawable.list_blue_down, @@ -99,6 +166,7 @@ public class ResourceParser { R.drawable.list_red_down, }; + /** 单项背景资源数组 */ private final static int [] BG_SINGLE_RESOURCES = new int [] { R.drawable.list_yellow_single, R.drawable.list_blue_single, @@ -107,28 +175,65 @@ public class ResourceParser { R.drawable.list_red_single }; + /** + * 获取笔记列表首项背景资源 ID + * + * @param id 背景颜色 ID(0-4) + * @return 首项背景资源 ID + */ public static int getNoteBgFirstRes(int id) { return BG_FIRST_RESOURCES[id]; } + /** + * 获取笔记列表末项背景资源 ID + * + * @param id 背景颜色 ID(0-4) + * @return 末项背景资源 ID + */ public static int getNoteBgLastRes(int id) { return BG_LAST_RESOURCES[id]; } + /** + * 获取笔记列表单项背景资源 ID + * + * @param id 背景颜色 ID(0-4) + * @return 单项背景资源 ID + */ public static int getNoteBgSingleRes(int id) { return BG_SINGLE_RESOURCES[id]; } + /** + * 获取笔记列表中间项背景资源 ID + * + * @param id 背景颜色 ID(0-4) + * @return 中间项背景资源 ID + */ public static int getNoteBgNormalRes(int id) { return BG_NORMAL_RESOURCES[id]; } + /** + * 获取文件夹背景资源 ID + * + * @return 文件夹背景资源 ID + */ public static int getFolderBgRes() { return R.drawable.list_folder; } } + /** + * Widget 背景资源类 + *

+ * 提供桌面 Widget 的背景颜色资源。 + * 支持 2x2 和 4x4 两种尺寸的 Widget。 + *

+ */ public static class WidgetBgResources { + /** 2x2 Widget 背景资源数组 */ private final static int [] BG_2X_RESOURCES = new int [] { R.drawable.widget_2x_yellow, R.drawable.widget_2x_blue, @@ -137,10 +242,17 @@ public class ResourceParser { R.drawable.widget_2x_red, }; + /** + * 获取 2x2 Widget 背景资源 ID + * + * @param id 背景颜色 ID(0-4) + * @return 2x2 Widget 背景资源 ID + */ public static int getWidget2xBgResource(int id) { return BG_2X_RESOURCES[id]; } + /** 4x4 Widget 背景资源数组 */ private final static int [] BG_4X_RESOURCES = new int [] { R.drawable.widget_4x_yellow, R.drawable.widget_4x_blue, @@ -149,12 +261,26 @@ public class ResourceParser { R.drawable.widget_4x_red }; + /** + * 获取 4x4 Widget 背景资源 ID + * + * @param id 背景颜色 ID(0-4) + * @return 4x4 Widget 背景资源 ID + */ public static int getWidget4xBgResource(int id) { return BG_4X_RESOURCES[id]; } } + /** + * 文本外观资源类 + *

+ * 提供笔记文本的字体样式资源。 + * 支持四种字体大小:小、中、大、超大。 + *

+ */ public static class TextAppearanceResources { + /** 文本外观样式资源数组 */ private final static int [] TEXTAPPEARANCE_RESOURCES = new int [] { R.style.TextAppearanceNormal, R.style.TextAppearanceMedium, @@ -162,11 +288,20 @@ public class ResourceParser { R.style.TextAppearanceSuper }; + /** + * 获取文本外观样式资源 ID + *

+ * 如果 ID 超出范围,则返回默认字体大小。 + *

+ * + * @param id 字体大小 ID(0-3) + * @return 文本外观样式资源 ID + */ public static int getTexAppearanceResource(int id) { /** - * 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} + * HACKME: 修复在 SharedPreferences 中存储资源 ID 的 bug。 + * ID 可能大于资源数组的长度,在这种情况下, + * 返回 {@link ResourceParser#BG_DEFAULT_FONT_SIZE} */ if (id >= TEXTAPPEARANCE_RESOURCES.length) { return BG_DEFAULT_FONT_SIZE; @@ -174,6 +309,11 @@ public class ResourceParser { return TEXTAPPEARANCE_RESOURCES[id]; } + /** + * 获取文本外观资源数量 + * + * @return 资源数量 + */ public static int getResourcesSize() { return TEXTAPPEARANCE_RESOURCES.length; } diff --git a/src/Notes-master/src/net/micode/notes/ui/AlarmAlertActivity.java b/src/Notes-master/src/net/micode/notes/ui/AlarmAlertActivity.java index 85723be..09181bf 100644 --- a/src/Notes-master/src/net/micode/notes/ui/AlarmAlertActivity.java +++ b/src/Notes-master/src/net/micode/notes/ui/AlarmAlertActivity.java @@ -39,33 +39,70 @@ import net.micode.notes.tool.DataUtils; import java.io.IOException; - +/** + * 闹钟提醒活动 + * + * 这个类负责显示笔记提醒的闹钟界面,当笔记设置的提醒时间到达时, + * 由AlarmReceiver启动此活动,显示笔记内容摘要并播放闹钟声音。 + * + * 主要功能: + * 1. 在锁屏状态下显示闹钟界面 + * 2. 显示笔记内容摘要 + * 3. 播放系统闹钟声音 + * 4. 提供操作选项(关闭提醒或查看笔记) + * + * @see NoteEditActivity + * @see net.micode.notes.tool.DataUtils + */ public class AlarmAlertActivity extends Activity implements OnClickListener, OnDismissListener { + // 当前提醒的笔记ID private long mNoteId; + // 笔记内容摘要 private String mSnippet; + // 摘要预览最大长度 private static final int SNIPPET_PREW_MAX_LEN = 60; + // 媒体播放器,用于播放闹钟声音 MediaPlayer mPlayer; + /** + * 活动创建时的初始化方法 + * + * 设置窗口属性,获取笔记信息,检查笔记是否存在, + * 如果存在则显示提醒对话框并播放闹钟声音 + * + * @param savedInstanceState 保存的实例状态 + */ @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); + // 请求无标题窗口 requestWindowFeature(Window.FEATURE_NO_TITLE); final Window win = getWindow(); + // 添加FLAG_SHOW_WHEN_LOCKED标志,使活动可以在锁屏界面上显示 win.addFlags(WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED); + // 如果屏幕当前是关闭状态,添加以下标志 if (!isScreenOn()) { + // 保持屏幕常亮 + // 打开屏幕 + // 允许在屏幕亮起时锁定 + // 设置窗口布局包含系统装饰区域 win.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON | WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON | WindowManager.LayoutParams.FLAG_ALLOW_LOCK_WHILE_SCREEN_ON | WindowManager.LayoutParams.FLAG_LAYOUT_INSET_DECOR); } + // 获取启动此活动的Intent Intent intent = getIntent(); try { + // 从Intent中解析出笔记ID mNoteId = Long.valueOf(intent.getData().getPathSegments().get(1)); + // 通过笔记ID获取笔记内容摘要 mSnippet = DataUtils.getSnippetById(this.getContentResolver(), mNoteId); + // 如果摘要超过最大长度,截取并添加省略号 mSnippet = mSnippet.length() > SNIPPET_PREW_MAX_LEN ? mSnippet.substring(0, SNIPPET_PREW_MAX_LEN) + getResources().getString(R.string.notelist_string_info) : mSnippet; @@ -74,85 +111,150 @@ public class AlarmAlertActivity extends Activity implements OnClickListener, OnD return; } + // 初始化媒体播放器 mPlayer = new MediaPlayer(); + // 检查笔记是否在数据库中存在且可见 if (DataUtils.visibleInNoteDatabase(getContentResolver(), mNoteId, Notes.TYPE_NOTE)) { + // 显示操作对话框 showActionDialog(); + // 播放闹钟声音 playAlarmSound(); } else { + // 如果笔记不存在,直接关闭活动 finish(); } } + /** + * 检查屏幕是否处于开启状态 + * + * @return 如果屏幕开启返回true,否则返回false + */ private boolean isScreenOn() { PowerManager pm = (PowerManager) getSystemService(Context.POWER_SERVICE); return pm.isScreenOn(); } + /** + * 播放闹钟声音 + * + * 获取系统默认的闹钟铃声,设置音频流类型, + * 并循环播放闹钟声音 + */ private void playAlarmSound() { + // 获取系统默认的闹钟铃声URI Uri url = RingtoneManager.getActualDefaultRingtoneUri(this, RingtoneManager.TYPE_ALARM); + // 获取受静音模式影响的音频流类型 int silentModeStreams = Settings.System.getInt(getContentResolver(), Settings.System.MODE_RINGER_STREAMS_AFFECTED, 0); + // 如果闹钟音频流受静音模式影响,使用受影响的流类型 if ((silentModeStreams & (1 << AudioManager.STREAM_ALARM)) != 0) { mPlayer.setAudioStreamType(silentModeStreams); } else { + // 否则使用标准闹钟音频流 mPlayer.setAudioStreamType(AudioManager.STREAM_ALARM); } try { + // 设置音频源 mPlayer.setDataSource(this, url); + // 准备播放 mPlayer.prepare(); + // 设置循环播放 mPlayer.setLooping(true); + // 开始播放 mPlayer.start(); } catch (IllegalArgumentException e) { - // TODO Auto-generated catch block e.printStackTrace(); } catch (SecurityException e) { - // TODO Auto-generated catch block e.printStackTrace(); } catch (IllegalStateException e) { - // TODO Auto-generated catch block e.printStackTrace(); } catch (IOException e) { - // TODO Auto-generated catch block e.printStackTrace(); } } + /** + * 显示操作对话框 + * + * 创建一个AlertDialog,显示笔记摘要和操作按钮 + * 当屏幕开启时,显示"查看笔记"按钮 + */ private void showActionDialog() { + // 创建AlertDialog构建器 AlertDialog.Builder dialog = new AlertDialog.Builder(this); + // 设置对话框标题为应用名称 dialog.setTitle(R.string.app_name); + // 设置对话框内容为笔记摘要 dialog.setMessage(mSnippet); + // 添加"确定"按钮,点击事件由当前类处理 dialog.setPositiveButton(R.string.notealert_ok, this); + // 如果屏幕是开启状态,添加"查看笔记"按钮 if (isScreenOn()) { dialog.setNegativeButton(R.string.notealert_enter, this); } + // 显示对话框并设置关闭监听器 dialog.show().setOnDismissListener(this); } + /** + * 对话框按钮点击事件处理 + * + * 处理用户在提醒对话框中的按钮点击操作,根据点击的按钮执行相应的操作 + * + * @param dialog 触发点击事件的对话框对象,不能为 null + * @param which 点击的按钮ID,取值为 DialogInterface.BUTTON_POSITIVE(确定按钮) + * 或 DialogInterface.BUTTON_NEGATIVE(查看笔记按钮) + */ public void onClick(DialogInterface dialog, int which) { switch (which) { + // 如果点击了"查看笔记"按钮(负按钮) case DialogInterface.BUTTON_NEGATIVE: + // 创建跳转到笔记编辑活动的Intent Intent intent = new Intent(this, NoteEditActivity.class); + // 设置动作为查看 intent.setAction(Intent.ACTION_VIEW); + // 传递笔记ID intent.putExtra(Intent.EXTRA_UID, mNoteId); + // 启动笔记编辑活动 startActivity(intent); break; + // 默认情况(点击"确定"按钮) default: break; } } + /** + * 对话框关闭事件处理 + * + * 当对话框被关闭时(无论是点击按钮还是外部点击), + * 停止闹钟声音并关闭当前活动 + * + * @param dialog 被关闭的对话框对象,不能为 null + */ public void onDismiss(DialogInterface dialog) { + // 停止闹钟声音 stopAlarmSound(); + // 关闭当前活动 finish(); } + /** + * 停止闹钟声音 + * + * 停止媒体播放器,释放资源并将播放器对象置空 + */ private void stopAlarmSound() { if (mPlayer != null) { + // 停止播放 mPlayer.stop(); + // 释放资源 mPlayer.release(); + // 将播放器对象置空 mPlayer = null; } } -} +} \ No newline at end of file diff --git a/src/Notes-master/src/net/micode/notes/ui/AlarmInitReceiver.java b/src/Notes-master/src/net/micode/notes/ui/AlarmInitReceiver.java index f221202..c00b5c6 100644 --- a/src/Notes-master/src/net/micode/notes/ui/AlarmInitReceiver.java +++ b/src/Notes-master/src/net/micode/notes/ui/AlarmInitReceiver.java @@ -16,50 +16,91 @@ package net.micode.notes.ui; -import android.app.AlarmManager; -import android.app.PendingIntent; -import android.content.BroadcastReceiver; -import android.content.ContentUris; -import android.content.Context; -import android.content.Intent; -import android.database.Cursor; - -import net.micode.notes.data.Notes; -import net.micode.notes.data.Notes.NoteColumns; +import android.app.AlarmManager; // 系统闹钟管理器,用于设置和管理系统级闹钟 +import android.app.PendingIntent; // 延迟意图,用于在指定时间触发操作 +import android.content.BroadcastReceiver; // 广播接收器基类,用于接收系统广播 +import android.content.ContentUris; // 用于处理内容URI的工具类 +import android.content.Context; // 应用上下文,提供访问应用环境和资源的接口 +import android.content.Intent; // 意图,用于组件间通信 +import android.database.Cursor; // 数据库游标,用于遍历查询结果 +import net.micode.notes.data.Notes; // 笔记数据相关类 +import net.micode.notes.data.Notes.NoteColumns; // 笔记表列定义 +/** + * 闹钟初始化接收器 + * + * 这个类继承自BroadcastReceiver,用于在系统启动或应用需要时重新初始化所有未触发的笔记提醒闹钟。 + * 它会查询数据库中所有设置了提醒时间且未过期的笔记,并为每个笔记设置系统闹钟。 + * + * 主要触发时机: + * 1. 系统启动完成时(接收BOOT_COMPLETED广播) + * 2. 应用安装或更新后可能需要手动触发 + */ public class AlarmInitReceiver extends BroadcastReceiver { + /** + * 数据库查询投影,指定需要从笔记表中获取的列 + * 只需要ID和提醒日期两列,用于设置闹钟 + */ private static final String [] PROJECTION = new String [] { - NoteColumns.ID, - NoteColumns.ALERTED_DATE + NoteColumns.ID, // 笔记ID + NoteColumns.ALERTED_DATE // 提醒日期 }; - private static final int COLUMN_ID = 0; - private static final int COLUMN_ALERTED_DATE = 1; + // 列索引常量,用于从查询结果中获取对应列的数据 + private static final int COLUMN_ID = 0; // ID列在结果集中的索引 + private static final int COLUMN_ALERTED_DATE = 1; // 提醒日期列在结果集中的索引 + /** + * 接收广播后的处理方法 + * + * 当接收到广播(通常是系统启动完成广播)时,此方法会被调用。 + * 它会查询所有未过期的笔记提醒,并为每个笔记设置系统闹钟。 + * + * @param context 应用上下文,用于访问系统服务和资源 + * @param intent 接收到的广播意图 + */ @Override public void onReceive(Context context, Intent intent) { + // 获取当前系统时间,作为查询条件 long currentDate = System.currentTimeMillis(); + + // 查询所有提醒时间晚于当前时间的笔记 + // 查询条件:提醒日期 > 当前时间 AND 笔记类型 = 普通笔记 Cursor c = context.getContentResolver().query(Notes.CONTENT_NOTE_URI, - PROJECTION, - NoteColumns.ALERTED_DATE + ">? AND " + NoteColumns.TYPE + "=" + Notes.TYPE_NOTE, - new String[] { String.valueOf(currentDate) }, - null); + PROJECTION, // 指定查询的列 + NoteColumns.ALERTED_DATE + ">? AND " + NoteColumns.TYPE + "=" + Notes.TYPE_NOTE, // 查询条件 + new String[] { String.valueOf(currentDate) }, // 查询参数 + null); // 排序方式,null表示默认排序 + // 处理查询结果 if (c != null) { + // 如果有查询结果,遍历所有符合条件的笔记 if (c.moveToFirst()) { do { + // 获取笔记的提醒时间 long alertDate = c.getLong(COLUMN_ALERTED_DATE); + + // 创建一个指向AlarmReceiver的Intent,用于在闹钟触发时接收广播 Intent sender = new Intent(context, AlarmReceiver.class); + // 将笔记ID作为URI数据附加到Intent中,这样AlarmReceiver就能知道是哪个笔记的闹钟触发了 sender.setData(ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, c.getLong(COLUMN_ID))); + + // 创建PendingIntent,它封装了上述Intent,可以在指定时间触发 PendingIntent pendingIntent = PendingIntent.getBroadcast(context, 0, sender, 0); + + // 获取系统闹钟服务 AlarmManager alermManager = (AlarmManager) context .getSystemService(Context.ALARM_SERVICE); + + // 设置闹钟 + // 使用RTC_WAKEUP模式,即使设备处于睡眠状态也会唤醒设备并触发广播 alermManager.set(AlarmManager.RTC_WAKEUP, alertDate, pendingIntent); - } while (c.moveToNext()); + } while (c.moveToNext()); // 移动到下一条记录 } + // 关闭游标,释放资源 c.close(); } } -} +} \ No newline at end of file diff --git a/src/Notes-master/src/net/micode/notes/ui/AlarmReceiver.java b/src/Notes-master/src/net/micode/notes/ui/AlarmReceiver.java index 54e503b..1ef8a05 100644 --- a/src/Notes-master/src/net/micode/notes/ui/AlarmReceiver.java +++ b/src/Notes-master/src/net/micode/notes/ui/AlarmReceiver.java @@ -16,15 +16,48 @@ package net.micode.notes.ui; -import android.content.BroadcastReceiver; -import android.content.Context; -import android.content.Intent; +import android.content.BroadcastReceiver; // 广播接收器基类,用于接收系统广播 +import android.content.Context; // 应用上下文,提供访问应用环境和资源的接口 +import android.content.Intent; // 意图,用于组件间通信 +/** + * 闹钟接收器 + * + * 这个类继承自BroadcastReceiver,用于接收由AlarmManager设置的闹钟触发事件。 + * 当笔记的提醒时间到达时,AlarmManager会发送一个广播,这个接收器会接收该广播 + * 并启动闹钟提醒界面(AlarmAlertActivity)来显示提醒信息。 + * + * 工作流程: + * 1. AlarmInitReceiver为每个设置了提醒时间的笔记设置系统闹钟 + * 2. 当提醒时间到达时,系统发送广播 + * 3. AlarmReceiver接收广播并启动AlarmAlertActivity显示提醒 + */ public class AlarmReceiver extends BroadcastReceiver { + + /** + * 接收闹钟广播后的处理方法 + * + * 当闹钟时间到达时,系统会发送广播,此方法会被调用。 + * 它会将接收到的Intent重新定向到AlarmAlertActivity,并添加FLAG_ACTIVITY_NEW_TASK标志 + * 确保即使在非UI上下文中也能启动Activity。 + * + * @param context 应用上下文,用于启动Activity + * @param intent 接收到的闹钟广播Intent,包含触发闹钟的笔记ID等信息 + */ @Override public void onReceive(Context context, Intent intent) { + // 将Intent的目标组件设置为AlarmAlertActivity + // 这样当启动Activity时就会显示闹钟提醒界面 intent.setClass(context, AlarmAlertActivity.class); + + // 添加FLAG_ACTIVITY_NEW_TASK标志 + // 这是必需的,因为从非Activity上下文(如BroadcastReceiver)启动Activity时, + // 必须指定这个标志,表示启动一个新的任务栈 intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + + // 启动AlarmAlertActivity显示闹钟提醒 + // 原始Intent中包含了触发闹钟的笔记ID等信息,AlarmAlertActivity会使用这些信息 + // 来显示相应的笔记内容和提醒信息 context.startActivity(intent); } -} +} \ No newline at end of file diff --git a/src/Notes-master/src/net/micode/notes/ui/DateTimePicker.java b/src/Notes-master/src/net/micode/notes/ui/DateTimePicker.java index 496b0cd..015522e 100644 --- a/src/Notes-master/src/net/micode/notes/ui/DateTimePicker.java +++ b/src/Notes-master/src/net/micode/notes/ui/DateTimePicker.java @@ -28,6 +28,28 @@ import android.view.View; import android.widget.FrameLayout; import android.widget.NumberPicker; +/** + * 日期时间选择器 + *

+ * 继承自FrameLayout,提供日期和时间选择的自定义视图组件。 + * 使用NumberPicker组件实现日期、小时、分钟和上午/下午的选择功能。 + * 支持24小时制和12小时制两种显示模式。 + *

+ *

+ * 主要功能: + *

    + *
  • 显示日期选择器(显示前后3天,共7天)
  • + *
  • 显示小时选择器(24小时制:0-23,12小时制:1-12)
  • + *
  • 显示分钟选择器(0-59)
  • + *
  • 显示上午/下午选择器(仅12小时制)
  • + *
  • 支持设置日期时间变更监听器
  • + *
  • 支持启用/禁用状态切换
  • + *
+ *

+ * + * @see NumberPicker + * @see OnDateTimeChangedListener + */ public class DateTimePicker extends FrameLayout { private static final boolean DEFAULT_ENABLE_STATE = true; @@ -64,36 +86,55 @@ public class DateTimePicker extends FrameLayout { private OnDateTimeChangedListener mOnDateTimeChangedListener; + /** + * 日期变更监听器 + *

+ * 监听日期选择器的值变化,更新内部日期对象并通知外部监听器。 + *

+ */ private NumberPicker.OnValueChangeListener mOnDateChangedListener = new NumberPicker.OnValueChangeListener() { @Override public void onValueChange(NumberPicker picker, int oldVal, int newVal) { + // 根据选择器的变化调整日期 mDate.add(Calendar.DAY_OF_YEAR, newVal - oldVal); updateDateControl(); onDateTimeChanged(); } }; + /** + * 小时变更监听器 + *

+ * 监听小时选择器的值变化,处理跨日情况(如从23点变为0点或从0点变为23点), + * 在12小时制下处理上午/下午的切换。 + *

+ */ private NumberPicker.OnValueChangeListener mOnHourChangedListener = new NumberPicker.OnValueChangeListener() { @Override public void onValueChange(NumberPicker picker, int oldVal, int newVal) { boolean isDateChanged = false; Calendar cal = Calendar.getInstance(); + // 处理12小时制下的跨日情况 if (!mIs24HourView) { + // 从下午11点变为12点,日期加1天 if (!mIsAm && oldVal == HOURS_IN_HALF_DAY - 1 && newVal == HOURS_IN_HALF_DAY) { cal.setTimeInMillis(mDate.getTimeInMillis()); cal.add(Calendar.DAY_OF_YEAR, 1); isDateChanged = true; + // 从12点变为下午11点,日期减1天 } else if (mIsAm && oldVal == HOURS_IN_HALF_DAY && newVal == HOURS_IN_HALF_DAY - 1) { cal.setTimeInMillis(mDate.getTimeInMillis()); cal.add(Calendar.DAY_OF_YEAR, -1); isDateChanged = true; } + // 切换上午/下午 if (oldVal == HOURS_IN_HALF_DAY - 1 && newVal == HOURS_IN_HALF_DAY || oldVal == HOURS_IN_HALF_DAY && newVal == HOURS_IN_HALF_DAY - 1) { mIsAm = !mIsAm; updateAmPmControl(); } } else { + // 处理24小时制下的跨日情况 if (oldVal == HOURS_IN_ALL_DAY - 1 && newVal == 0) { cal.setTimeInMillis(mDate.getTimeInMillis()); cal.add(Calendar.DAY_OF_YEAR, 1); @@ -104,9 +145,11 @@ public class DateTimePicker extends FrameLayout { isDateChanged = true; } } + // 计算新的小时数 int newHour = mHourSpinner.getValue() % HOURS_IN_HALF_DAY + (mIsAm ? 0 : HOURS_IN_HALF_DAY); mDate.set(Calendar.HOUR_OF_DAY, newHour); onDateTimeChanged(); + // 如果日期发生变化,更新年月日 if (isDateChanged) { setCurrentYear(cal.get(Calendar.YEAR)); setCurrentMonth(cal.get(Calendar.MONTH)); @@ -115,21 +158,31 @@ public class DateTimePicker extends FrameLayout { } }; + /** + * 分钟变更监听器 + *

+ * 监听分钟选择器的值变化,处理跨小时情况(如从59分变为0分或从0分变为59分)。 + *

+ */ private NumberPicker.OnValueChangeListener mOnMinuteChangedListener = new NumberPicker.OnValueChangeListener() { @Override public void onValueChange(NumberPicker picker, int oldVal, int newVal) { int minValue = mMinuteSpinner.getMinValue(); int maxValue = mMinuteSpinner.getMaxValue(); int offset = 0; + // 从最大值变为最小值,小时加1 if (oldVal == maxValue && newVal == minValue) { offset += 1; + // 从最小值变为最大值,小时减1 } else if (oldVal == minValue && newVal == maxValue) { offset -= 1; } + // 如果跨小时,更新小时和日期 if (offset != 0) { mDate.add(Calendar.HOUR_OF_DAY, offset); mHourSpinner.setValue(getCurrentHour()); updateDateControl(); + // 更新上午/下午状态 int newHour = getCurrentHourOfDay(); if (newHour >= HOURS_IN_HALF_DAY) { mIsAm = false; @@ -144,10 +197,17 @@ public class DateTimePicker extends FrameLayout { } }; + /** + * 上午/下午变更监听器 + *

+ * 监听上午/下午选择器的值变化,切换上午/下午时调整小时数。 + *

+ */ private NumberPicker.OnValueChangeListener mOnAmPmChangedListener = new NumberPicker.OnValueChangeListener() { @Override public void onValueChange(NumberPicker picker, int oldVal, int newVal) { mIsAm = !mIsAm; + // 切换上午/下午,调整小时数 if (mIsAm) { mDate.add(Calendar.HOUR_OF_DAY, -HOURS_IN_HALF_DAY); } else { @@ -158,39 +218,88 @@ public class DateTimePicker extends FrameLayout { } }; + /** + * 日期时间变更监听器接口 + *

+ * 用于监听日期时间选择器的值变化,当用户修改日期、小时或分钟时回调。 + *

+ */ public interface OnDateTimeChangedListener { + /** + * 当日期时间发生变化时调用 + * + * @param view 日期时间选择器实例 + * @param year 年份 + * @param month 月份(0-11) + * @param dayOfMonth 日(1-31) + * @param hourOfDay 小时(0-23) + * @param minute 分钟(0-59) + */ void onDateTimeChanged(DateTimePicker view, int year, int month, int dayOfMonth, int hourOfDay, int minute); } + /** + * 构造器 + * + * 创建日期时间选择器,使用当前系统时间作为初始值。 + * 根据系统设置自动判断是否使用24小时制显示。 + * + * @param context 应用上下文 + */ public DateTimePicker(Context context) { this(context, System.currentTimeMillis()); } + /** + * 构造器 + * + * 创建日期时间选择器,使用指定的时间作为初始值。 + * 根据系统设置自动判断是否使用24小时制显示。 + * + * @param context 应用上下文 + * @param date 初始日期时间,以毫秒为单位的时间戳 + */ public DateTimePicker(Context context, long date) { this(context, date, DateFormat.is24HourFormat(context)); } + /** + * 构造器 + * + * 创建日期时间选择器,使用指定的时间和显示模式作为初始值。 + * 初始化所有NumberPicker组件并设置监听器。 + * + * @param context 应用上下文 + * @param date 初始日期时间,以毫秒为单位的时间戳 + * @param is24HourView 是否使用24小时制显示,true表示24小时制,false表示12小时制 + */ public DateTimePicker(Context context, long date, boolean is24HourView) { super(context); mDate = Calendar.getInstance(); mInitialising = true; + // 判断当前是否为下午 mIsAm = getCurrentHourOfDay() >= HOURS_IN_HALF_DAY; + // 加载布局 inflate(context, R.layout.datetime_picker, this); + // 初始化日期选择器 mDateSpinner = (NumberPicker) findViewById(R.id.date); mDateSpinner.setMinValue(DATE_SPINNER_MIN_VAL); mDateSpinner.setMaxValue(DATE_SPINNER_MAX_VAL); mDateSpinner.setOnValueChangedListener(mOnDateChangedListener); + // 初始化小时选择器 mHourSpinner = (NumberPicker) findViewById(R.id.hour); mHourSpinner.setOnValueChangedListener(mOnHourChangedListener); + // 初始化分钟选择器 mMinuteSpinner = (NumberPicker) findViewById(R.id.minute); mMinuteSpinner.setMinValue(MINUT_SPINNER_MIN_VAL); mMinuteSpinner.setMaxValue(MINUT_SPINNER_MAX_VAL); mMinuteSpinner.setOnLongPressUpdateInterval(100); mMinuteSpinner.setOnValueChangedListener(mOnMinuteChangedListener); + // 初始化上午/下午选择器 String[] stringsForAmPm = new DateFormatSymbols().getAmPmStrings(); mAmPmSpinner = (NumberPicker) findViewById(R.id.amPm); mAmPmSpinner.setMinValue(AMPM_SPINNER_MIN_VAL); @@ -198,22 +307,31 @@ public class DateTimePicker extends FrameLayout { mAmPmSpinner.setDisplayedValues(stringsForAmPm); mAmPmSpinner.setOnValueChangedListener(mOnAmPmChangedListener); - // update controls to initial state + // 更新控件到初始状态 updateDateControl(); updateHourControl(); updateAmPmControl(); + // 设置24小时制显示模式 set24HourView(is24HourView); - // set to current time + // 设置当前时间 setCurrentDate(date); + // 设置启用状态 setEnabled(isEnabled()); - // set the content descriptions + // 设置内容描述 mInitialising = false; } + /** + * 设置启用状态 + * + * 设置所有NumberPicker组件的启用状态,控制用户是否可以修改日期时间。 + * + * @param enabled true表示启用,false表示禁用 + */ @Override public void setEnabled(boolean enabled) { if (mIsEnabled == enabled) { @@ -227,24 +345,29 @@ public class DateTimePicker extends FrameLayout { mIsEnabled = enabled; } + /** + * 获取启用状态 + * + * @return true表示已启用,false表示已禁用 + */ @Override public boolean isEnabled() { return mIsEnabled; } /** - * Get the current date in millis - * - * @return the current date in millis + * 获取当前日期时间(毫秒) + * + * @return 当前日期时间,以毫秒为单位的时间戳 */ public long getCurrentDateInTimeMillis() { return mDate.getTimeInMillis(); } /** - * Set the current date - * - * @param date The current date in millis + * 设置当前日期时间 + * + * @param date 要设置的日期时间,以毫秒为单位的时间戳 */ public void setCurrentDate(long date) { Calendar cal = Calendar.getInstance(); @@ -254,13 +377,13 @@ public class DateTimePicker extends FrameLayout { } /** - * Set the current date - * - * @param year The current year - * @param month The current month - * @param dayOfMonth The current dayOfMonth - * @param hourOfDay The current hourOfDay - * @param minute The current minute + * 设置当前日期时间 + * + * @param year 年份 + * @param month 月份(0-11) + * @param dayOfMonth 日(1-31) + * @param hourOfDay 小时(0-23) + * @param minute 分钟(0-59) */ public void setCurrentDate(int year, int month, int dayOfMonth, int hourOfDay, int minute) { @@ -272,18 +395,18 @@ public class DateTimePicker extends FrameLayout { } /** - * Get current year - * - * @return The current year + * 获取当前年份 + * + * @return 当前年份 */ public int getCurrentYear() { return mDate.get(Calendar.YEAR); } /** - * Set current year - * - * @param year The current year + * 设置当前年份 + * + * @param year 要设置的年份 */ public void setCurrentYear(int year) { if (!mInitialising && year == getCurrentYear()) { @@ -295,18 +418,18 @@ public class DateTimePicker extends FrameLayout { } /** - * Get current month in the year - * - * @return The current month in the year + * 获取当前月份 + * + * @return 当前月份(0-11) */ public int getCurrentMonth() { return mDate.get(Calendar.MONTH); } /** - * Set current month in the year - * - * @param month The month in the year + * 设置当前月份 + * + * @param month 要设置的月份(0-11) */ public void setCurrentMonth(int month) { if (!mInitialising && month == getCurrentMonth()) { @@ -318,18 +441,18 @@ public class DateTimePicker extends FrameLayout { } /** - * Get current day of the month - * - * @return The day of the month + * 获取当前日 + * + * @return 当前日(1-31) */ public int getCurrentDay() { return mDate.get(Calendar.DAY_OF_MONTH); } /** - * Set current day of the month - * - * @param dayOfMonth The day of the month + * 设置当前日 + * + * @param dayOfMonth 要设置的日(1-31) */ public void setCurrentDay(int dayOfMonth) { if (!mInitialising && dayOfMonth == getCurrentDay()) { @@ -341,13 +464,21 @@ public class DateTimePicker extends FrameLayout { } /** - * Get current hour in 24 hour mode, in the range (0~23) - * @return The current hour in 24 hour mode + * 获取当前小时(24小时制) + * + * @return 当前小时(0-23) */ public int getCurrentHourOfDay() { return mDate.get(Calendar.HOUR_OF_DAY); } + /** + * 获取当前小时(根据显示模式) + * + * 在24小时制下返回0-23,在12小时制下返回1-12 + * + * @return 当前小时 + */ private int getCurrentHour() { if (mIs24HourView){ return getCurrentHourOfDay(); @@ -362,9 +493,9 @@ public class DateTimePicker extends FrameLayout { } /** - * Set current hour in 24 hour mode, in the range (0~23) - * - * @param hourOfDay + * 设置当前小时(24小时制) + * + * @param hourOfDay 要设置的小时(0-23) */ public void setCurrentHour(int hourOfDay) { if (!mInitialising && hourOfDay == getCurrentHourOfDay()) { @@ -372,6 +503,7 @@ public class DateTimePicker extends FrameLayout { } mDate.set(Calendar.HOUR_OF_DAY, hourOfDay); if (!mIs24HourView) { + // 处理12小时制下的上午/下午状态 if (hourOfDay >= HOURS_IN_HALF_DAY) { mIsAm = false; if (hourOfDay > HOURS_IN_HALF_DAY) { @@ -390,16 +522,18 @@ public class DateTimePicker extends FrameLayout { } /** - * Get currentMinute - * - * @return The Current Minute + * 获取当前分钟 + * + * @return 当前分钟(0-59) */ public int getCurrentMinute() { return mDate.get(Calendar.MINUTE); } /** - * Set current minute + * 设置当前分钟 + * + * @param minute 要设置的分钟(0-59) */ public void setCurrentMinute(int minute) { if (!mInitialising && minute == getCurrentMinute()) { @@ -411,22 +545,25 @@ public class DateTimePicker extends FrameLayout { } /** - * @return true if this is in 24 hour view else false. + * 判断是否为24小时制显示 + * + * @return true表示24小时制,false表示12小时制 */ public boolean is24HourView () { return mIs24HourView; } /** - * Set whether in 24 hour or AM/PM mode. - * - * @param is24HourView True for 24 hour mode. False for AM/PM mode. + * 设置显示模式 + * + * @param is24HourView true表示使用24小时制,false表示使用12小时制 */ public void set24HourView(boolean is24HourView) { if (mIs24HourView == is24HourView) { return; } mIs24HourView = is24HourView; + // 根据显示模式显示或隐藏上午/下午选择器 mAmPmSpinner.setVisibility(is24HourView ? View.GONE : View.VISIBLE); int hour = getCurrentHourOfDay(); updateHourControl(); @@ -434,30 +571,53 @@ public class DateTimePicker extends FrameLayout { updateAmPmControl(); } + /** + * 更新日期选择器显示 + * + * 根据当前日期更新日期选择器的显示值,显示前后3天,共7天的日期。 + * 每个日期的格式为"MM.dd EEEE"(月.日 星期)。 + */ private void updateDateControl() { Calendar cal = Calendar.getInstance(); + // 设置为当前日期的前4天 cal.setTimeInMillis(mDate.getTimeInMillis()); cal.add(Calendar.DAY_OF_YEAR, -DAYS_IN_ALL_WEEK / 2 - 1); mDateSpinner.setDisplayedValues(null); + // 生成7天的日期显示值 for (int i = 0; i < DAYS_IN_ALL_WEEK; ++i) { cal.add(Calendar.DAY_OF_YEAR, 1); mDateDisplayValues[i] = (String) DateFormat.format("MM.dd EEEE", cal); } mDateSpinner.setDisplayedValues(mDateDisplayValues); + // 设置当前选中项为中间项 mDateSpinner.setValue(DAYS_IN_ALL_WEEK / 2); mDateSpinner.invalidate(); } + /** + * 更新上午/下午选择器显示 + * + * 根据当前显示模式和上午/下午状态更新上午/下午选择器的可见性和选中值。 + * 在24小时制下隐藏上午/下午选择器,在12小时制下显示并设置当前选中值。 + */ private void updateAmPmControl() { if (mIs24HourView) { + // 24小时制下隐藏上午/下午选择器 mAmPmSpinner.setVisibility(View.GONE); } else { + // 12小时制下显示上午/下午选择器 int index = mIsAm ? Calendar.AM : Calendar.PM; mAmPmSpinner.setValue(index); mAmPmSpinner.setVisibility(View.VISIBLE); } } + /** + * 更新小时选择器范围 + * + * 根据当前显示模式更新小时选择器的最小值和最大值。 + * 24小时制:0-23,12小时制:1-12。 + */ private void updateHourControl() { if (mIs24HourView) { mHourSpinner.setMinValue(HOUR_SPINNER_MIN_VAL_24_HOUR_VIEW); @@ -469,13 +629,19 @@ public class DateTimePicker extends FrameLayout { } /** - * Set the callback that indicates the 'Set' button has been pressed. - * @param callback the callback, if null will do nothing + * 设置日期时间变更监听器 + * + * @param callback 日期时间变更监听器,如果为null则不执行任何操作 */ public void setOnDateTimeChangedListener(OnDateTimeChangedListener callback) { mOnDateTimeChangedListener = callback; } + /** + * 触发日期时间变更事件 + * + * 如果设置了监听器,则通知监听器日期时间已发生变化。 + */ private void onDateTimeChanged() { if (mOnDateTimeChangedListener != null) { mOnDateTimeChangedListener.onDateTimeChanged(this, getCurrentYear(), diff --git a/src/Notes-master/src/net/micode/notes/ui/DateTimePickerDialog.java b/src/Notes-master/src/net/micode/notes/ui/DateTimePickerDialog.java index 2c47ba4..a95bc43 100644 --- a/src/Notes-master/src/net/micode/notes/ui/DateTimePickerDialog.java +++ b/src/Notes-master/src/net/micode/notes/ui/DateTimePickerDialog.java @@ -29,6 +29,25 @@ import android.content.DialogInterface.OnClickListener; import android.text.format.DateFormat; import android.text.format.DateUtils; +/** + * 日期时间选择对话框 + *

+ * 继承自AlertDialog,提供日期和时间选择的对话框界面。 + * 使用DateTimePicker组件作为主视图,支持设置24小时制或12小时制显示。 + *

+ *

+ * 主要功能: + *

    + *
  • 显示日期和时间选择器
  • + *
  • 支持设置监听器获取用户选择的时间
  • + *
  • 动态更新对话框标题显示当前选择的时间
  • + *
  • 支持24小时制和12小时制切换
  • + *
+ *

+ * + * @see DateTimePicker + * @see OnDateTimeSetListener + */ public class DateTimePickerDialog extends AlertDialog implements OnClickListener { private Calendar mDate = Calendar.getInstance(); @@ -36,52 +55,122 @@ public class DateTimePickerDialog extends AlertDialog implements OnClickListener private OnDateTimeSetListener mOnDateTimeSetListener; private DateTimePicker mDateTimePicker; + /** + * 日期时间设置监听器接口 + *

+ * 用于监听用户在对话框中点击确定按钮后的回调,获取用户选择的日期和时间。 + *

+ */ public interface OnDateTimeSetListener { + /** + * 当用户点击确定按钮时调用 + * + * @param dialog 日期时间选择对话框实例 + * @param date 用户选择的日期时间,以毫秒为单位的时间戳 + */ void OnDateTimeSet(AlertDialog dialog, long date); } + /** + * 构造器 + * + * 创建日期时间选择对话框,初始化DateTimePicker组件并设置默认日期时间。 + * 根据系统设置自动判断是否使用24小时制显示。 + * + * @param context 应用上下文 + * @param date 初始日期时间,以毫秒为单位的时间戳 + */ public DateTimePickerDialog(Context context, long date) { super(context); + // 创建日期时间选择器组件 mDateTimePicker = new DateTimePicker(context); setView(mDateTimePicker); + // 设置日期时间变更监听器 mDateTimePicker.setOnDateTimeChangedListener(new OnDateTimeChangedListener() { public void onDateTimeChanged(DateTimePicker view, int year, int month, int dayOfMonth, int hourOfDay, int minute) { + // 更新内部Calendar对象 mDate.set(Calendar.YEAR, year); mDate.set(Calendar.MONTH, month); mDate.set(Calendar.DAY_OF_MONTH, dayOfMonth); mDate.set(Calendar.HOUR_OF_DAY, hourOfDay); mDate.set(Calendar.MINUTE, minute); + // 更新对话框标题 updateTitle(mDate.getTimeInMillis()); } }); + // 设置初始日期时间 mDate.setTimeInMillis(date); + // 将秒数清零 mDate.set(Calendar.SECOND, 0); + // 设置选择器当前日期 mDateTimePicker.setCurrentDate(mDate.getTimeInMillis()); + // 设置确定按钮 setButton(context.getString(R.string.datetime_dialog_ok), this); + // 设置取消按钮 setButton2(context.getString(R.string.datetime_dialog_cancel), (OnClickListener)null); + // 根据系统设置判断是否使用24小时制 set24HourView(DateFormat.is24HourFormat(this.getContext())); + // 更新对话框标题 updateTitle(mDate.getTimeInMillis()); } + /** + * 设置是否使用24小时制显示 + *

+ * 根据系统设置或用户偏好,判断是否使用24小时制显示时间。 + * 如果设置为true,将使用24小时制;如果设置为false,将使用12小时制。 + *

+ * + * @param is24HourView true表示使用24小时制,false表示使用12小时制 + */ public void set24HourView(boolean is24HourView) { mIs24HourView = is24HourView; } + /** + * 设置日期时间设置监听器 + *

+ * 当用户点击对话框的确定按钮时,调用此监听器的OnDateTimeSet方法, + * 并传递用户选择的日期时间作为参数。 + *

+ * + * @param callBack 日期时间设置监听器,当用户点击确定按钮时回调 + */ public void setOnDateTimeSetListener(OnDateTimeSetListener callBack) { mOnDateTimeSetListener = callBack; } + /** + * 更新对话框标题 + * + * 根据指定的日期时间格式化字符串,并设置为对话框标题。 + * 显示格式包含年、月、日和时间,根据mIs24HourView决定是否使用24小时制。 + * + * @param date 要显示的日期时间,以毫秒为单位的时间戳 + */ private void updateTitle(long date) { + // 设置日期时间格式标志 int flag = DateUtils.FORMAT_SHOW_YEAR | DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_SHOW_TIME; + // 根据是否24小时制设置相应的格式标志 flag |= mIs24HourView ? DateUtils.FORMAT_24HOUR : DateUtils.FORMAT_24HOUR; + // 格式化日期时间并设置为对话框标题 setTitle(DateUtils.formatDateTime(this.getContext(), date, flag)); } + /** + * 处理对话框按钮点击事件 + * + * 当用户点击确定按钮时,调用监听器的OnDateTimeSet方法,传递用户选择的日期时间。 + * + * @param arg0 触发事件的对话框 + * @param arg1 被点击的按钮ID + */ public void onClick(DialogInterface arg0, int arg1) { + // 如果设置了监听器,通知监听器用户选择的日期时间 if (mOnDateTimeSetListener != null) { mOnDateTimeSetListener.OnDateTimeSet(this, mDate.getTimeInMillis()); } diff --git a/src/Notes-master/src/net/micode/notes/ui/DropdownMenu.java b/src/Notes-master/src/net/micode/notes/ui/DropdownMenu.java index 613dc74..7276081 100644 --- a/src/Notes-master/src/net/micode/notes/ui/DropdownMenu.java +++ b/src/Notes-master/src/net/micode/notes/ui/DropdownMenu.java @@ -27,17 +27,49 @@ import android.widget.PopupMenu.OnMenuItemClickListener; import net.micode.notes.R; +/** + * 下拉菜单类 + *

+ * 封装了PopupMenu和Button,提供下拉菜单功能。 + * 点击按钮时显示弹出菜单,支持设置菜单项点击监听器和标题。 + *

+ *

+ * 主要功能: + *

    + *
  • 显示下拉菜单
  • + *
  • 设置菜单项点击监听器
  • + *
  • 查找菜单项
  • + *
  • 设置按钮标题
  • + *
+ *

+ */ public class DropdownMenu { + // 下拉按钮 private Button mButton; + // 弹出菜单 private PopupMenu mPopupMenu; + // 菜单对象 private Menu mMenu; + /** + * 构造器 + * + * 初始化下拉菜单,设置按钮背景、创建PopupMenu并加载菜单资源 + * + * @param context 应用上下文 + * @param button 触发下拉菜单的按钮 + * @param menuId 菜单资源ID,用于加载菜单项 + */ public DropdownMenu(Context context, Button button, int menuId) { mButton = button; + // 设置下拉图标背景 mButton.setBackgroundResource(R.drawable.dropdown_icon); + // 创建弹出菜单 mPopupMenu = new PopupMenu(context, mButton); mMenu = mPopupMenu.getMenu(); + // 加载菜单资源 mPopupMenu.getMenuInflater().inflate(menuId, mMenu); + // 设置按钮点击监听器,点击时显示弹出菜单 mButton.setOnClickListener(new OnClickListener() { public void onClick(View v) { mPopupMenu.show(); @@ -45,16 +77,32 @@ public class DropdownMenu { }); } + /** + * 设置菜单项点击监听器 + * + * @param listener 菜单项点击监听器 + */ public void setOnDropdownMenuItemClickListener(OnMenuItemClickListener listener) { if (mPopupMenu != null) { mPopupMenu.setOnMenuItemClickListener(listener); } } + /** + * 查找指定ID的菜单项 + * + * @param id 菜单项ID + * @return 找到的菜单项对象,如果未找到则返回null + */ public MenuItem findItem(int id) { return mMenu.findItem(id); } + /** + * 设置按钮标题 + * + * @param title 要设置的标题文本 + */ public void setTitle(CharSequence title) { mButton.setText(title); } diff --git a/src/Notes-master/src/net/micode/notes/ui/FoldersListAdapter.java b/src/Notes-master/src/net/micode/notes/ui/FoldersListAdapter.java index 96b77da..7176cf9 100644 --- a/src/Notes-master/src/net/micode/notes/ui/FoldersListAdapter.java +++ b/src/Notes-master/src/net/micode/notes/ui/FoldersListAdapter.java @@ -28,50 +28,123 @@ import net.micode.notes.R; import net.micode.notes.data.Notes; import net.micode.notes.data.Notes.NoteColumns; - +/** + * 文件夹列表适配器 + *

+ * 继承自CursorAdapter,用于将数据库中的文件夹数据绑定到ListView中显示。 + * 主要用于笔记移动功能中显示可选择的文件夹列表。 + *

+ *

+ * 主要功能: + *

    + *
  • 显示所有可用文件夹
  • + *
  • 处理根文件夹的特殊显示
  • + *
  • 提供获取文件夹名称的方法
  • + *
+ *

+ * + * @see NotesListActivity + */ public class FoldersListAdapter extends CursorAdapter { + // 数据库查询投影,指定需要从笔记表中获取的列 public static final String [] PROJECTION = { NoteColumns.ID, NoteColumns.SNIPPET }; + // 列索引常量,用于从查询结果中获取对应列的数据 public static final int ID_COLUMN = 0; public static final int NAME_COLUMN = 1; + /** + * 构造器 + * + * 初始化文件夹列表适配器 + * + * @param context 应用上下文 + * @param c 数据库游标,包含文件夹数据 + */ public FoldersListAdapter(Context context, Cursor c) { super(context, c); - // TODO Auto-generated constructor stub } + /** + * 创建新的列表项视图 + * + * 创建一个新的FolderListItem视图对象 + * + * @param context 应用上下文 + * @param cursor 数据库游标,包含当前项的数据 + * @param parent 父视图 + * @return 新创建的FolderListItem视图对象 + */ @Override public View newView(Context context, Cursor cursor, ViewGroup parent) { return new FolderListItem(context); } + /** + * 绑定数据到视图 + * + * 将数据库游标中的数据绑定到已存在的视图上 + * + * @param view 需要绑定数据的视图 + * @param context 应用上下文 + * @param cursor 数据库游标,包含当前项的数据 + */ @Override public void bindView(View view, Context context, Cursor cursor) { if (view instanceof FolderListItem) { + // 如果是根文件夹,显示特殊文本;否则显示文件夹名称 String folderName = (cursor.getLong(ID_COLUMN) == Notes.ID_ROOT_FOLDER) ? context .getString(R.string.menu_move_parent_folder) : cursor.getString(NAME_COLUMN); ((FolderListItem) view).bind(folderName); } } + /** + * 获取指定位置的文件夹名称 + * + * @param context 应用上下文,用于获取根文件夹的显示文本 + * @param position 列表项位置,从0开始 + * @return 文件夹名称,如果是根文件夹则返回特殊显示文本 + */ public String getFolderName(Context context, int position) { Cursor cursor = (Cursor) getItem(position); return (cursor.getLong(ID_COLUMN) == Notes.ID_ROOT_FOLDER) ? context .getString(R.string.menu_move_parent_folder) : cursor.getString(NAME_COLUMN); } + /** + * 文件夹列表项视图 + *

+ * 自定义的LinearLayout,用于显示文件夹列表中的单个文件夹项。 + *

+ */ private class FolderListItem extends LinearLayout { + // 文件夹名称文本视图 private TextView mName; + /** + * 构造器 + * + * 初始化文件夹列表项视图 + * + * @param context 应用上下文 + */ public FolderListItem(Context context) { super(context); + // 加载布局文件 inflate(context, R.layout.folder_list_item, this); + // 获取文件夹名称文本视图 mName = (TextView) findViewById(R.id.tv_folder_name); } + /** + * 绑定文件夹名称到视图 + * + * @param name 要显示的文件夹名称 + */ public void bind(String name) { mName.setText(name); } diff --git a/src/Notes-master/src/net/micode/notes/ui/NoteEditActivity.java b/src/Notes-master/src/net/micode/notes/ui/NoteEditActivity.java index 96a9ff8..9544f6c 100644 --- a/src/Notes-master/src/net/micode/notes/ui/NoteEditActivity.java +++ b/src/Notes-master/src/net/micode/notes/ui/NoteEditActivity.java @@ -74,6 +74,12 @@ import java.util.regex.Pattern; public class NoteEditActivity extends Activity implements OnClickListener, NoteSettingChangedListener, OnTextViewChangeListener { + /** + * 笔记头部视图持有者 + *

+ * 持有笔记编辑界面头部区域的UI组件引用,包括修改时间、提醒图标、提醒日期和背景颜色设置按钮。 + *

+ */ private class HeadViewHolder { public TextView tvModified; @@ -162,8 +168,12 @@ public class NoteEditActivity extends Activity implements OnClickListener, } /** - * Current activity may be killed when the memory is low. Once it is killed, for another time - * user load this activity, we should restore the former state + * 恢复活动状态 + *

+ * 当系统内存不足导致活动被杀死时,重新加载活动需要恢复之前的状态。 + * 从保存的实例状态中恢复笔记ID,并重新初始化活动状态。 + *

+ * @param savedInstanceState 包含之前保存状态的Bundle对象 */ @Override protected void onRestoreInstanceState(Bundle savedInstanceState) { @@ -179,6 +189,18 @@ public class NoteEditActivity extends Activity implements OnClickListener, } } + /** + * 初始化活动状态 + *

+ * 根据传入的Intent初始化活动状态,支持以下操作: + *

    + *
  • ACTION_VIEW: 查看现有笔记,支持从搜索结果打开
  • + *
  • ACTION_INSERT_OR_EDIT: 创建新笔记或编辑笔记,支持通话记录笔记
  • + *
+ *

+ * @param intent 包含操作类型和参数的Intent对象 + * @return 初始化成功返回true,失败返回false + */ private boolean initActivityState(Intent intent) { /** * If the user specified the {@link Intent#ACTION_VIEW} but not provided with id, @@ -268,6 +290,18 @@ public class NoteEditActivity extends Activity implements OnClickListener, initNoteScreen(); } + /** + * 初始化笔记编辑界面 + *

+ * 设置笔记编辑界面的显示内容,包括: + *

    + *
  • 根据字体大小设置文本外观
  • + *
  • 根据模式(普通/清单)显示笔记内容
  • + *
  • 设置背景颜色
  • + *
  • 显示修改时间和提醒信息
  • + *
+ *

+ */ private void initNoteScreen() { mNoteEditor.setTextAppearance(this, TextAppearanceResources .getTexAppearanceResource(mFontSizeId)); @@ -295,6 +329,13 @@ public class NoteEditActivity extends Activity implements OnClickListener, showAlertHeader(); } + /** + * 显示提醒头部信息 + *

+ * 根据笔记是否设置了闹钟提醒,显示或隐藏提醒图标和提醒日期。 + * 如果提醒已过期,显示过期提示;否则显示相对时间。 + *

+ */ private void showAlertHeader() { if (mWorkingNote.hasClockAlert()) { long time = System.currentTimeMillis(); @@ -318,6 +359,14 @@ public class NoteEditActivity extends Activity implements OnClickListener, initActivityState(intent); } + /** + * 保存活动实例状态 + *

+ * 在活动被系统销毁前保存当前笔记的ID,以便后续恢复。 + * 如果是新笔记且尚未保存到数据库,会先保存笔记以生成ID。 + *

+ * @param outState 用于保存状态的Bundle对象 + */ @Override protected void onSaveInstanceState(Bundle outState) { super.onSaveInstanceState(outState); @@ -333,6 +382,15 @@ public class NoteEditActivity extends Activity implements OnClickListener, Log.d(TAG, "Save working note id: " + mWorkingNote.getNoteId() + " onSaveInstanceState"); } + /** + * 分发触摸事件 + *

+ * 处理触摸事件,当用户点击背景颜色选择器或字体大小选择器外部区域时, + * 隐藏相应的选择器面板。 + *

+ * @param ev 触摸事件对象 + * @return 如果事件被处理返回true,否则返回false + */ @Override public boolean dispatchTouchEvent(MotionEvent ev) { if (mNoteBgColorSelector.getVisibility() == View.VISIBLE @@ -349,6 +407,15 @@ public class NoteEditActivity extends Activity implements OnClickListener, return super.dispatchTouchEvent(ev); } + /** + * 检查触摸点是否在视图范围内 + *

+ * 判断给定的触摸事件坐标是否位于指定视图的显示区域内。 + *

+ * @param view 要检查的视图 + * @param ev 触摸事件对象 + * @return 如果触摸点在视图范围内返回true,否则返回false + */ private boolean inRangeOfView(View view, MotionEvent ev) { int []location = new int[2]; view.getLocationOnScreen(location); @@ -363,6 +430,13 @@ public class NoteEditActivity extends Activity implements OnClickListener, return true; } + /** + * 初始化资源 + *

+ * 初始化所有UI组件的引用,设置点击监听器, + * 并从SharedPreferences中读取字体大小设置。 + *

+ */ private void initResources() { mHeadViewPanel = findViewById(R.id.note_title); mNoteHeaderHolder = new HeadViewHolder(); @@ -397,6 +471,12 @@ public class NoteEditActivity extends Activity implements OnClickListener, mEditTextList = (LinearLayout) findViewById(R.id.note_edit_list); } + /** + * 活动暂停时保存笔记 + *

+ * 在活动暂停时自动保存笔记内容,并清除设置状态(如打开的颜色选择器)。 + *

+ */ @Override protected void onPause() { super.onPause(); @@ -406,6 +486,12 @@ public class NoteEditActivity extends Activity implements OnClickListener, clearSettingState(); } + /** + * 更新桌面小部件 + *

+ * 发送广播通知桌面小部件更新,根据笔记的小部件类型(2x或4x)发送相应的更新意图。 + *

+ */ private void updateWidget() { Intent intent = new Intent(AppWidgetManager.ACTION_APPWIDGET_UPDATE); if (mWorkingNote.getWidgetType() == Notes.TYPE_WIDGET_2X) { @@ -425,6 +511,18 @@ public class NoteEditActivity extends Activity implements OnClickListener, setResult(RESULT_OK, intent); } + /** + * 处理点击事件 + *

+ * 处理各种UI组件的点击事件,包括: + *

    + *
  • 背景颜色设置按钮:显示颜色选择器
  • + *
  • 背景颜色选项:设置笔记背景颜色
  • + *
  • 字体大小选项:设置编辑器字体大小
  • + *
+ *

+ * @param v 被点击的视图 + */ public void onClick(View v) { int id = v.getId(); if (id == R.id.btn_set_bg_color) { @@ -452,6 +550,13 @@ public class NoteEditActivity extends Activity implements OnClickListener, } } + /** + * 处理返回键按下事件 + *

+ * 如果当前有打开的设置面板(颜色选择器或字体选择器),先关闭面板; + * 否则保存笔记并退出活动。 + *

+ */ @Override public void onBackPressed() { if(clearSettingState()) { @@ -462,6 +567,13 @@ public class NoteEditActivity extends Activity implements OnClickListener, super.onBackPressed(); } + /** + * 清除设置状态 + *

+ * 检查并关闭所有打开的设置面板(背景颜色选择器和字体大小选择器)。 + *

+ * @return 如果关闭了任何面板返回true,否则返回false + */ private boolean clearSettingState() { if (mNoteBgColorSelector.getVisibility() == View.VISIBLE) { mNoteBgColorSelector.setVisibility(View.GONE); @@ -473,6 +585,17 @@ public class NoteEditActivity extends Activity implements OnClickListener, return false; } + /** + * 背景颜色改变回调 + *

+ * 当笔记背景颜色改变时调用,更新UI显示: + *

    + *
  • 显示选中颜色的指示器
  • + *
  • 更新编辑器面板背景
  • + *
  • 更新头部面板背景
  • + *
+ *

+ */ public void onBackgroundColorChanged() { findViewById(sBgSelectorSelectionMap.get(mWorkingNote.getBgColorId())).setVisibility( View.VISIBLE); @@ -480,6 +603,19 @@ public class NoteEditActivity extends Activity implements OnClickListener, mHeadViewPanel.setBackgroundResource(mWorkingNote.getTitleBgResId()); } + /** + * 准备选项菜单 + *

+ * 根据当前笔记的状态动态设置菜单项: + *

    + *
  • 通话记录笔记使用特殊菜单
  • + *
  • 清单模式下切换菜单项标题
  • + *
  • 根据是否设置提醒显示/隐藏相应菜单项
  • + *
+ *

+ * @param menu 选项菜单对象 + * @return 返回true表示菜单已准备好 + */ @Override public boolean onPrepareOptionsMenu(Menu menu) { if (isFinishing()) { @@ -505,6 +641,24 @@ public class NoteEditActivity extends Activity implements OnClickListener, return true; } + /** + * 处理选项菜单项选择 + *

+ * 处理各种菜单项的点击事件,包括: + *

    + *
  • 新建笔记:创建新笔记
  • + *
  • 删除笔记:显示确认对话框后删除当前笔记
  • + *
  • 字体大小:显示字体大小选择器
  • + *
  • 清单模式:切换普通/清单模式
  • + *
  • 分享:分享笔记内容到其他应用
  • + *
  • 发送到桌面:创建桌面小部件
  • + *
  • 设置提醒:设置闹钟提醒
  • + *
  • 删除提醒:删除已设置的提醒
  • + *
+ *

+ * @param item 被选中的菜单项 + * @return 返回true表示事件已处理 + */ @Override public boolean onOptionsItemSelected(MenuItem item) { switch (item.getItemId()) { @@ -553,6 +707,13 @@ public class NoteEditActivity extends Activity implements OnClickListener, return true; } + /** + * 设置提醒 + *

+ * 显示日期时间选择对话框,让用户选择提醒时间。 + * 选择完成后设置笔记的提醒日期。 + *

+ */ private void setReminder() { DateTimePickerDialog d = new DateTimePickerDialog(this, System.currentTimeMillis()); d.setOnDateTimeSetListener(new OnDateTimeSetListener() { @@ -564,8 +725,12 @@ public class NoteEditActivity extends Activity implements OnClickListener, } /** - * Share note to apps that support {@link Intent#ACTION_SEND} action - * and {@text/plain} type + * 分享笔记到其他应用 + *

+ * 使用ACTION_SEND Intent将笔记内容分享到支持文本分享的应用。 + *

+ * @param context 上下文对象 + * @param info 要分享的文本内容 */ private void sendTo(Context context, String info) { Intent intent = new Intent(Intent.ACTION_SEND); @@ -574,6 +739,13 @@ public class NoteEditActivity extends Activity implements OnClickListener, context.startActivity(intent); } + /** + * 创建新笔记 + *

+ * 先保存当前编辑的笔记,然后启动新的NoteEditActivity创建新笔记。 + * 新笔记将创建在与当前笔记相同的文件夹中。 + *

+ */ private void createNewNote() { // Firstly, save current editing notes saveNote(); @@ -586,6 +758,13 @@ public class NoteEditActivity extends Activity implements OnClickListener, startActivity(intent); } + /** + * 删除当前笔记 + *

+ * 删除当前编辑的笔记。如果处于同步模式,将笔记移动到垃圾箱; + * 否则直接从数据库中删除。 + *

+ */ private void deleteCurrentNote() { if (mWorkingNote.existInDatabase()) { HashSet ids = new HashSet(); @@ -608,10 +787,27 @@ public class NoteEditActivity extends Activity implements OnClickListener, mWorkingNote.markDeleted(true); } + /** + * 检查是否处于同步模式 + *

+ * 检查是否配置了同步账户,如果配置了则处于同步模式。 + *

+ * @return 如果配置了同步账户返回true,否则返回false + */ private boolean isSyncMode() { return NotesPreferenceActivity.getSyncAccountName(this).trim().length() > 0; } + /** + * 闹钟提醒改变回调 + *

+ * 当笔记的闹钟提醒设置改变时调用。 + * 如果笔记尚未保存到数据库,先保存笔记。 + * 然后使用AlarmManager设置或取消闹钟。 + *

+ * @param date 提醒日期时间(毫秒) + * @param set true表示设置提醒,false表示取消提醒 + */ public void onClockAlertChanged(long date, boolean set) { /** * User could set clock to an unsaved note, so before setting the @@ -642,10 +838,25 @@ public class NoteEditActivity extends Activity implements OnClickListener, } } + /** + * 小部件改变回调 + *

+ * 当笔记的小部件设置改变时调用,更新桌面小部件显示。 + *

+ */ public void onWidgetChanged() { updateWidget(); } + /** + * 编辑文本删除回调 + *

+ * 在清单模式下删除某个编辑项时调用。 + * 删除指定位置的编辑项,并更新后续项的索引。 + *

+ * @param index 要删除的编辑项索引 + * @param text 编辑项的文本内容 + */ public void onEditTextDelete(int index, String text) { int childCount = mEditTextList.getChildCount(); if (childCount == 1) { @@ -672,6 +883,15 @@ public class NoteEditActivity extends Activity implements OnClickListener, edit.setSelection(length); } + /** + * 编辑文本回车回调 + *

+ * 在清单模式下,当用户在某个编辑项中按下回车键时调用。 + * 在指定位置插入新的编辑项,并更新后续项的索引。 + *

+ * @param index 回车位置所在的编辑项索引 + * @param text 编辑项的文本内容 + */ public void onEditTextEnter(int index, String text) { /** * Should not happen, check for debug @@ -691,6 +911,14 @@ public class NoteEditActivity extends Activity implements OnClickListener, } } + /** + * 切换到清单模式 + *

+ * 将普通文本编辑器切换到清单模式。 + * 将文本按行分割,每行创建一个清单项,包含复选框和编辑框。 + *

+ * @param text 要转换为清单的文本内容 + */ private void switchToListMode(String text) { mEditTextList.removeAllViews(); String[] items = text.split("\n"); @@ -708,6 +936,16 @@ public class NoteEditActivity extends Activity implements OnClickListener, mEditTextList.setVisibility(View.VISIBLE); } + /** + * 高亮显示搜索结果 + *

+ * 在文本中高亮显示用户搜索的关键词。 + * 使用背景色标记匹配的文本。 + *

+ * @param fullText 完整的文本内容 + * @param userQuery 用户搜索的关键词 + * @return 带有高亮标记的Spannable对象 + */ private Spannable getHighlightQueryResult(String fullText, String userQuery) { SpannableString spannable = new SpannableString(fullText == null ? "" : fullText); if (!TextUtils.isEmpty(userQuery)) { @@ -725,6 +963,16 @@ public class NoteEditActivity extends Activity implements OnClickListener, return spannable; } + /** + * 创建清单列表项视图 + *

+ * 创建清单模式下的单个列表项,包含复选框和编辑框。 + * 根据文本内容设置复选框状态和文本样式。 + *

+ * @param item 列表项的文本内容 + * @param index 列表项的索引 + * @return 列表项视图 + */ 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); @@ -756,6 +1004,15 @@ public class NoteEditActivity extends Activity implements OnClickListener, return view; } + /** + * 文本改变回调 + *

+ * 在清单模式下,当某个编辑项的文本内容改变时调用。 + * 根据是否有文本内容显示或隐藏复选框。 + *

+ * @param index 编辑项的索引 + * @param hasText 是否有文本内容 + */ public void onTextChange(int index, boolean hasText) { if (index >= mEditTextList.getChildCount()) { Log.e(TAG, "Wrong index, should not happen"); @@ -768,6 +1025,16 @@ public class NoteEditActivity extends Activity implements OnClickListener, } } + /** + * 清单模式改变回调 + *

+ * 当笔记的清单模式改变时调用。 + * 切换到清单模式时,将文本转换为清单项; + * 切换到普通模式时,将清单项转换为文本。 + *

+ * @param oldMode 旧的模式 + * @param newMode 新的模式 + */ public void onCheckListModeChanged(int oldMode, int newMode) { if (newMode == TextNote.MODE_CHECK_LIST) { switchToListMode(mNoteEditor.getText().toString()); @@ -782,6 +1049,14 @@ public class NoteEditActivity extends Activity implements OnClickListener, } } + /** + * 获取工作文本 + *

+ * 从当前编辑器中获取文本内容并设置到WorkingNote。 + * 如果是清单模式,将所有清单项合并为文本,并标记已选中项。 + *

+ * @return 如果有已选中的清单项返回true,否则返回false + */ private boolean getWorkingText() { boolean hasChecked = false; if (mWorkingNote.getCheckListMode() == TextNote.MODE_CHECK_LIST) { @@ -805,6 +1080,14 @@ public class NoteEditActivity extends Activity implements OnClickListener, return hasChecked; } + /** + * 保存笔记 + *

+ * 将当前编辑的笔记保存到数据库。 + * 保存成功后设置RESULT_OK结果码,用于标识创建/编辑状态。 + *

+ * @return 保存成功返回true,失败返回false + */ private boolean saveNote() { getWorkingText(); boolean saved = mWorkingNote.saveNote(); @@ -821,6 +1104,14 @@ public class NoteEditActivity extends Activity implements OnClickListener, return saved; } + /** + * 发送到桌面 + *

+ * 将笔记创建为桌面快捷方式。 + * 如果笔记尚未保存到数据库,先保存笔记。 + * 快捷方式使用笔记内容的前10个字符作为标题。 + *

+ */ private void sendToDesktop() { /** * Before send message to home, we should make sure that current @@ -856,6 +1147,15 @@ public class NoteEditActivity extends Activity implements OnClickListener, } } + /** + * 生成快捷方式图标标题 + *

+ * 从笔记内容中提取文本作为快捷方式标题。 + * 移除清单标记,并限制标题长度为10个字符。 + *

+ * @param content 笔记内容 + * @return 快捷方式标题 + */ private String makeShortcutIconTitle(String content) { content = content.replace(TAG_CHECKED, ""); content = content.replace(TAG_UNCHECKED, ""); @@ -863,10 +1163,25 @@ public class NoteEditActivity extends Activity implements OnClickListener, SHORTCUT_ICON_TITLE_MAX_LEN) : content; } + /** + * 显示Toast提示 + *

+ * 显示短时Toast提示消息。 + *

+ * @param resId 字符串资源ID + */ private void showToast(int resId) { showToast(resId, Toast.LENGTH_SHORT); } + /** + * 显示Toast提示 + *

+ * 显示指定时长的Toast提示消息。 + *

+ * @param resId 字符串资源ID + * @param duration 显示时长(Toast.LENGTH_SHORT或Toast.LENGTH_LONG) + */ private void showToast(int resId, int duration) { Toast.makeText(this, resId, duration).show(); } diff --git a/src/Notes-master/src/net/micode/notes/ui/NoteEditText.java b/src/Notes-master/src/net/micode/notes/ui/NoteEditText.java index 2afe2a8..df117b3 100644 --- a/src/Notes-master/src/net/micode/notes/ui/NoteEditText.java +++ b/src/Notes-master/src/net/micode/notes/ui/NoteEditText.java @@ -37,15 +37,40 @@ import net.micode.notes.R; import java.util.HashMap; import java.util.Map; +/** + * 笔记编辑文本框 + *

+ * 自定义的EditText,用于笔记编辑界面,支持多行文本编辑、链接识别和上下文菜单。 + * 提供了与NoteEditActivity的交互接口,用于处理删除和回车事件。 + *

+ *

+ * 主要功能: + *

    + *
  • 支持多行文本编辑,每行是一个独立的EditText
  • + *
  • 识别并处理URL、电话号码、邮件地址等链接
  • + *
  • 处理删除和回车事件,通知监听器
  • + *
  • 支持文本选择和上下文菜单
  • + *
+ *

+ * + * @see NoteEditActivity + */ public class NoteEditText extends EditText { + // 日志标签 private static final String TAG = "NoteEditText"; + // 当前EditText的索引 private int mIndex; + // 删除前的光标位置 private int mSelectionStartBeforeDelete; + // 电话号码URI方案 private static final String SCHEME_TEL = "tel:" ; + // HTTP URI方案 private static final String SCHEME_HTTP = "http:" ; + // 邮件URI方案 private static final String SCHEME_EMAIL = "mailto:" ; + // URI方案与上下文菜单资源ID的映射 private static final Map sSchemaActionResMap = new HashMap(); static { sSchemaActionResMap.put(SCHEME_TEL, R.string.note_link_tel); @@ -54,66 +79,119 @@ public class NoteEditText extends EditText { } /** - * Call by the {@link NoteEditActivity} to delete or add edit text + * 文本视图变更监听器接口 + *

+ * 由NoteEditActivity实现,用于处理EditText的删除、回车和文本变更事件。 + *

+ * + * @see NoteEditActivity */ public interface OnTextViewChangeListener { /** - * Delete current edit text when {@link KeyEvent#KEYCODE_DEL} happens - * and the text is null + * 当按下删除键且文本为空时调用 + * + * @param index 当前EditText的索引 + * @param text 当前EditText中的文本内容 */ void onEditTextDelete(int index, String text); /** - * Add edit text after current edit text when {@link KeyEvent#KEYCODE_ENTER} - * happen + * 当按下回车键时调用 + * + * @param index 当前EditText的索引 + * @param text 当前EditText中的文本内容 */ void onEditTextEnter(int index, String text); /** - * Hide or show item option when text change + * 当文本内容变更时调用 + * + * @param index 当前EditText的索引 + * @param hasText 是否有文本内容 */ void onTextChange(int index, boolean hasText); } + // 文本视图变更监听器 private OnTextViewChangeListener mOnTextViewChangeListener; + /** + * 构造器 + * + * @param context 应用上下文 + */ public NoteEditText(Context context) { super(context, null); mIndex = 0; } + /** + * 设置当前EditText的索引 + * + * @param index EditText的索引值 + */ public void setIndex(int index) { mIndex = index; } + /** + * 设置文本视图变更监听器 + * + * @param listener 文本视图变更监听器对象 + */ public void setOnTextViewChangeListener(OnTextViewChangeListener listener) { mOnTextViewChangeListener = listener; } + /** + * 构造器 + * + * @param context 应用上下文 + * @param attrs XML属性集 + */ public NoteEditText(Context context, AttributeSet attrs) { super(context, attrs, android.R.attr.editTextStyle); } + /** + * 构造器 + * + * @param context 应用上下文 + * @param attrs XML属性集 + * @param defStyle 默认样式 + */ public NoteEditText(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); - // TODO Auto-generated constructor stub } + /** + * 处理触摸事件 + * + * 根据触摸位置设置文本选择光标的位置 + * + * @param event 触摸事件对象 + * @return 如果事件被处理返回true,否则返回false + */ @Override public boolean onTouchEvent(MotionEvent event) { switch (event.getAction()) { case MotionEvent.ACTION_DOWN: - + // 获取触摸坐标 int x = (int) event.getX(); int y = (int) event.getY(); + // 减去内边距,得到内容区域的坐标 x -= getTotalPaddingLeft(); y -= getTotalPaddingTop(); + // 加上滚动偏移量 x += getScrollX(); y += getScrollY(); Layout layout = getLayout(); + // 获取触摸点所在的行号 int line = layout.getLineForVertical(y); + // 获取触摸点在行中的字符偏移量 int off = layout.getOffsetForHorizontal(line, x); + // 设置文本选择光标位置 Selection.setSelection(getText(), off); break; } @@ -121,15 +199,26 @@ public class NoteEditText extends EditText { return super.onTouchEvent(event); } + /** + * 处理按键按下事件 + * + * 处理删除键和回车键的按下事件 + * + * @param keyCode 按键代码 + * @param event 按键事件对象 + * @return 如果事件被处理返回true,否则返回false + */ @Override public boolean onKeyDown(int keyCode, KeyEvent event) { switch (keyCode) { case KeyEvent.KEYCODE_ENTER: + // 如果设置了监听器,返回false让onKeyUp处理 if (mOnTextViewChangeListener != null) { return false; } break; case KeyEvent.KEYCODE_DEL: + // 记录删除前的光标位置 mSelectionStartBeforeDelete = getSelectionStart(); break; default: @@ -138,11 +227,22 @@ public class NoteEditText extends EditText { return super.onKeyDown(keyCode, event); } + /** + * 处理按键抬起事件 + * + * 处理删除键和回车键的抬起事件,通知监听器执行相应操作 + * + * @param keyCode 按键代码 + * @param event 按键事件对象 + * @return 如果事件被处理返回true,否则返回false + */ @Override public boolean onKeyUp(int keyCode, KeyEvent event) { switch(keyCode) { case KeyEvent.KEYCODE_DEL: + // 处理删除键 if (mOnTextViewChangeListener != null) { + // 如果光标在开头且不是第一个EditText,删除当前EditText if (0 == mSelectionStartBeforeDelete && mIndex != 0) { mOnTextViewChangeListener.onEditTextDelete(mIndex, getText().toString()); return true; @@ -152,10 +252,14 @@ public class NoteEditText extends EditText { } break; case KeyEvent.KEYCODE_ENTER: + // 处理回车键 if (mOnTextViewChangeListener != null) { int selectionStart = getSelectionStart(); + // 获取光标后的文本 String text = getText().subSequence(selectionStart, length()).toString(); + // 保留光标前的文本 setText(getText().subSequence(0, selectionStart)); + // 通知监听器创建新的EditText mOnTextViewChangeListener.onEditTextEnter(mIndex + 1, text); } else { Log.d(TAG, "OnTextViewChangeListener was not seted"); @@ -167,10 +271,20 @@ public class NoteEditText extends EditText { return super.onKeyUp(keyCode, event); } + /** + * 焦点变更时的处理 + * + * 当失去焦点且文本为空时,通知监听器 + * + * @param focused 是否获得焦点 + * @param direction 焦点移动方向 + * @param previouslyFocusedRect 之前获得焦点的视图矩形 + */ @Override protected void onFocusChanged(boolean focused, int direction, Rect previouslyFocusedRect) { if (mOnTextViewChangeListener != null) { if (!focused && TextUtils.isEmpty(getText())) { + // 失去焦点且文本为空 mOnTextViewChangeListener.onTextChange(mIndex, false); } else { mOnTextViewChangeListener.onTextChange(mIndex, true); @@ -179,18 +293,28 @@ public class NoteEditText extends EditText { super.onFocusChanged(focused, direction, previouslyFocusedRect); } + /** + * 创建上下文菜单 + * + * 如果选中的文本包含URL链接,添加相应的菜单项 + * + * @param menu 上下文菜单对象 + */ @Override protected void onCreateContextMenu(ContextMenu menu) { if (getText() instanceof Spanned) { int selStart = getSelectionStart(); int selEnd = getSelectionEnd(); + // 获取选区的起始和结束位置 int min = Math.min(selStart, selEnd); int max = Math.max(selStart, selEnd); + // 获取选区内的所有URLSpan final URLSpan[] urls = ((Spanned) getText()).getSpans(min, max, URLSpan.class); if (urls.length == 1) { int defaultResId = 0; + // 根据URL类型确定菜单项文本 for(String schema: sSchemaActionResMap.keySet()) { if(urls[0].getURL().indexOf(schema) >= 0) { defaultResId = sSchemaActionResMap.get(schema); @@ -202,10 +326,11 @@ public class NoteEditText extends EditText { defaultResId = R.string.note_link_other; } + // 添加菜单项 menu.add(0, 0, 0, defaultResId).setOnMenuItemClickListener( new OnMenuItemClickListener() { public boolean onMenuItemClick(MenuItem item) { - // goto a new intent + // 点击菜单项时打开链接 urls[0].onClick(NoteEditText.this); return true; } diff --git a/src/Notes-master/src/net/micode/notes/ui/NoteItemData.java b/src/Notes-master/src/net/micode/notes/ui/NoteItemData.java index 0f5a878..024cb5d 100644 --- a/src/Notes-master/src/net/micode/notes/ui/NoteItemData.java +++ b/src/Notes-master/src/net/micode/notes/ui/NoteItemData.java @@ -25,8 +25,27 @@ import net.micode.notes.data.Notes; import net.micode.notes.data.Notes.NoteColumns; import net.micode.notes.tool.DataUtils; - +/** + * 笔记项数据类 + *

+ * 用于封装笔记列表项的数据信息,从数据库游标中提取笔记的各项属性, + * 并提供便捷的访问方法。该类支持普通笔记、文件夹和通话记录笔记等多种类型。 + *

+ *

+ * 主要功能: + *

    + *
  • 从数据库游标中提取笔记数据
  • + *
  • 判断笔记在列表中的位置状态(首项、末项、唯一项等)
  • + *
  • 判断笔记是否跟随文件夹显示
  • + *
  • 处理通话记录笔记的特殊逻辑
  • + *
+ *

+ * + * @see NotesListItem + * @see NotesListAdapter + */ public class NoteItemData { + // 数据库查询投影,指定需要从笔记表中获取的列 static final String [] PROJECTION = new String [] { NoteColumns.ID, NoteColumns.ALERTED_DATE, @@ -42,6 +61,7 @@ public class NoteItemData { NoteColumns.WIDGET_TYPE, }; + // 列索引常量,用于从查询结果中获取对应列的数据 private static final int ID_COLUMN = 0; private static final int ALERTED_DATE_COLUMN = 1; private static final int BG_COLOR_ID_COLUMN = 2; @@ -55,27 +75,55 @@ public class NoteItemData { private static final int WIDGET_ID_COLUMN = 10; private static final int WIDGET_TYPE_COLUMN = 11; + // 笔记ID private long mId; + // 提醒日期 private long mAlertDate; + // 背景颜色ID private int mBgColorId; + // 创建日期 private long mCreatedDate; + // 是否有附件 private boolean mHasAttachment; + // 修改日期 private long mModifiedDate; + // 笔记数量(用于文件夹) private int mNotesCount; + // 父文件夹ID private long mParentId; + // 笔记摘要 private String mSnippet; + // 笔记类型 private int mType; + // 桌面小部件ID private int mWidgetId; + // 桌面小部件类型 private int mWidgetType; + // 联系人名称(用于通话记录) private String mName; + // 电话号码(用于通话记录) private String mPhoneNumber; + // 是否为列表最后一项 private boolean mIsLastItem; + // 是否为列表第一项 private boolean mIsFirstItem; + // 是否为列表唯一一项 private boolean mIsOnlyOneItem; + // 是否为文件夹后跟随的单个笔记 private boolean mIsOneNoteFollowingFolder; + // 是否为文件夹后跟随的多个笔记之一 private boolean mIsMultiNotesFollowingFolder; + /** + * 构造器 + * + * 从数据库游标中提取笔记数据并初始化各项属性。 + * 对于通话记录笔记,会额外获取联系人信息。 + * + * @param context 应用上下文,用于访问内容提供者和联系人信息 + * @param cursor 数据库游标,包含笔记数据,游标必须包含PROJECTION中指定的所有列 + */ public NoteItemData(Context context, Cursor cursor) { mId = cursor.getLong(ID_COLUMN); mAlertDate = cursor.getLong(ALERTED_DATE_COLUMN); @@ -86,6 +134,7 @@ public class NoteItemData { mNotesCount = cursor.getInt(NOTES_COUNT_COLUMN); mParentId = cursor.getLong(PARENT_ID_COLUMN); mSnippet = cursor.getString(SNIPPET_COLUMN); + // 移除清单项的勾选标记,只保留文本内容 mSnippet = mSnippet.replace(NoteEditActivity.TAG_CHECKED, "").replace( NoteEditActivity.TAG_UNCHECKED, ""); mType = cursor.getInt(TYPE_COLUMN); @@ -93,10 +142,12 @@ public class NoteItemData { mWidgetType = cursor.getInt(WIDGET_TYPE_COLUMN); mPhoneNumber = ""; + // 如果是通话记录笔记,获取电话号码和联系人名称 if (mParentId == Notes.ID_CALL_RECORD_FOLDER) { mPhoneNumber = DataUtils.getCallNumberByNoteId(context.getContentResolver(), mId); if (!TextUtils.isEmpty(mPhoneNumber)) { mName = Contact.getContact(context, mPhoneNumber); + // 如果找不到联系人,使用电话号码作为名称 if (mName == null) { mName = mPhoneNumber; } @@ -106,9 +157,17 @@ public class NoteItemData { if (mName == null) { mName = ""; } + // 检查当前项在列表中的位置状态 checkPostion(cursor); } + /** + * 检查当前项在列表中的位置状态 + * + * 判断当前项是否为首项、末项、唯一项,以及是否跟随文件夹显示。 + * + * @param cursor 数据库游标,用于判断位置状态 + */ private void checkPostion(Cursor cursor) { mIsLastItem = cursor.isLast() ? true : false; mIsFirstItem = cursor.isFirst() ? true : false; @@ -116,17 +175,21 @@ public class NoteItemData { mIsMultiNotesFollowingFolder = false; mIsOneNoteFollowingFolder = false; + // 如果是普通笔记且不是第一项,检查前一项是否为文件夹 if (mType == Notes.TYPE_NOTE && !mIsFirstItem) { int position = cursor.getPosition(); if (cursor.moveToPrevious()) { + // 前一项是文件夹或系统文件夹 if (cursor.getInt(TYPE_COLUMN) == Notes.TYPE_FOLDER || cursor.getInt(TYPE_COLUMN) == Notes.TYPE_SYSTEM) { + // 检查文件夹后是否还有更多笔记 if (cursor.getCount() > (position + 1)) { mIsMultiNotesFollowingFolder = true; } else { mIsOneNoteFollowingFolder = true; } } + // 移动回原位置 if (!cursor.moveToNext()) { throw new IllegalStateException("cursor move to previous but can't move back"); } @@ -134,90 +197,203 @@ public class NoteItemData { } } + /** + * 判断是否为文件夹后跟随的单个笔记 + * + * @return 如果是文件夹后跟随的单个笔记返回true,否则返回false + */ public boolean isOneFollowingFolder() { return mIsOneNoteFollowingFolder; } + /** + * 判断是否为文件夹后跟随的多个笔记之一 + * + * @return 如果是文件夹后跟随的多个笔记之一返回true,否则返回false + */ public boolean isMultiFollowingFolder() { return mIsMultiNotesFollowingFolder; } + /** + * 判断是否为列表最后一项 + * + * @return 如果是最后一项返回true,否则返回false + */ public boolean isLast() { return mIsLastItem; } + /** + * 获取通话记录的联系人名称 + * + * @return 联系人名称,如果不是通话记录或找不到联系人则返回空字符串 + */ public String getCallName() { return mName; } + /** + * 判断是否为列表第一项 + * + * @return 如果是第一项返回true,否则返回false + */ public boolean isFirst() { return mIsFirstItem; } + /** + * 判断是否为列表唯一一项 + * + * @return 如果是唯一一项返回true,否则返回false + */ public boolean isSingle() { return mIsOnlyOneItem; } + /** + * 获取笔记ID + * + * @return 笔记ID + */ public long getId() { return mId; } + /** + * 获取提醒日期 + * + * @return 提醒日期(毫秒时间戳),如果没有设置提醒则返回0 + */ public long getAlertDate() { return mAlertDate; } + /** + * 获取创建日期 + * + * @return 创建日期(毫秒时间戳) + */ public long getCreatedDate() { return mCreatedDate; } + /** + * 判断笔记是否有附件 + * + * @return 如果有附件返回true,否则返回false + */ public boolean hasAttachment() { return mHasAttachment; } + /** + * 获取修改日期 + * + * @return 修改日期(毫秒时间戳) + */ public long getModifiedDate() { return mModifiedDate; } + /** + * 获取背景颜色ID + * + * @return 背景颜色ID + */ public int getBgColorId() { return mBgColorId; } + /** + * 获取父文件夹ID + * + * @return 父文件夹ID + */ public long getParentId() { return mParentId; } + /** + * 获取笔记数量 + * + * @return 笔记数量(主要用于文件夹类型) + */ public int getNotesCount() { return mNotesCount; } + /** + * 获取文件夹ID + * + * @return 文件夹ID(与getParentId相同) + */ public long getFolderId () { return mParentId; } + /** + * 获取笔记类型 + * + * @return 笔记类型,取值为Notes.TYPE_NOTE、Notes.TYPE_FOLDER或Notes.TYPE_SYSTEM + */ public int getType() { return mType; } + /** + * 获取桌面小部件类型 + * + * @return 桌面小部件类型 + */ public int getWidgetType() { return mWidgetType; } + /** + * 获取桌面小部件ID + * + * @return 桌面小部件ID + */ public int getWidgetId() { return mWidgetId; } + /** + * 获取笔记摘要 + * + * @return 笔记摘要文本(已移除清单项标记) + */ public String getSnippet() { return mSnippet; } + /** + * 判断是否设置了提醒 + * + * @return 如果设置了提醒返回true,否则返回false + */ public boolean hasAlert() { return (mAlertDate > 0); } + /** + * 判断是否为通话记录笔记 + * + * @return 如果是通话记录笔记且包含电话号码返回true,否则返回false + */ public boolean isCallRecord() { return (mParentId == Notes.ID_CALL_RECORD_FOLDER && !TextUtils.isEmpty(mPhoneNumber)); } + /** + * 从游标中获取笔记类型 + * + * 静态方法,直接从游标中读取类型列的值,无需创建NoteItemData对象 + * + * @param cursor 数据库游标,必须包含TYPE_COLUMN列 + * @return 笔记类型 + */ public static int getNoteType(Cursor cursor) { return cursor.getInt(TYPE_COLUMN); } diff --git a/src/Notes-master/src/net/micode/notes/ui/NotesListActivity.java b/src/Notes-master/src/net/micode/notes/ui/NotesListActivity.java index e843aec..6ff1b90 100644 --- a/src/Notes-master/src/net/micode/notes/ui/NotesListActivity.java +++ b/src/Notes-master/src/net/micode/notes/ui/NotesListActivity.java @@ -78,63 +78,119 @@ import java.io.InputStream; import java.io.InputStreamReader; import java.util.HashSet; +/** + * 笔记列表活动 + * + * 这个类是应用的主界面,用于显示笔记列表并提供笔记管理功能。 + * 支持创建、编辑、删除笔记,文件夹管理,笔记同步,以及桌面小部件集成。 + * + * 主要功能: + * 1. 显示笔记列表,支持按文件夹分类查看 + * 2. 创建新笔记和文件夹 + * 3. 批量选择和操作笔记(删除、移动) + * 4. 笔记同步到 Google Tasks + * 5. 导出笔记为文本文件 + * 6. 与桌面小部件集成 + * + * @see NoteEditActivity + * @see NotesListAdapter + * @see GTaskSyncService + */ 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; + // 首次使用应用时添加介绍笔记的偏好设置键 private static final String PREFERENCE_ADD_INTRODUCTION = "net.micode.notes.introduction"; + /** + * 列表编辑状态枚举 + * + * 定义笔记列表的三种显示状态 + */ private enum ListEditState { - NOTE_LIST, SUB_FOLDER, CALL_RECORD_FOLDER + NOTE_LIST, // 笔记列表状态 + SUB_FOLDER, // 子文件夹状态 + CALL_RECORD_FOLDER // 通话记录文件夹状态 }; + // 当前列表编辑状态 private ListEditState mState; + // 后台查询处理器 private BackgroundQueryHandler mBackgroundQueryHandler; + // 笔记列表适配器 private NotesListAdapter mNotesListAdapter; + // 笔记列表视图 private ListView mNotesListView; + // 新建笔记按钮 private Button mAddNewNote; + // 是否正在分发触摸事件 private boolean mDispatch; + // 触摸事件的原始Y坐标 private int mOriginY; + // 分发触摸事件的Y坐标 private int mDispatchY; + // 标题栏文本视图 private TextView mTitleBar; + // 当前文件夹ID private long mCurrentFolderId; + // 内容解析器 private ContentResolver mContentResolver; + // 多选模式回调 private ModeCallback mModeCallBack; private static final String TAG = "NotesListActivity"; + // 笔记列表滚动速率 public static final int NOTES_LISTVIEW_SCROLL_RATE = 30; + // 当前聚焦的笔记数据项 private NoteItemData mFocusNoteDataItem; + // 普通选择条件:指定父文件夹ID private static final String NORMAL_SELECTION = NoteColumns.PARENT_ID + "=?"; + // 根文件夹选择条件:显示所有非系统笔记和有内容的通话记录文件夹 private static final String ROOT_FOLDER_SELECTION = "(" + NoteColumns.TYPE + "<>" + Notes.TYPE_SYSTEM + " AND " + NoteColumns.PARENT_ID + "=?)" + " OR (" + NoteColumns.ID + "=" + Notes.ID_CALL_RECORD_FOLDER + " AND " + NoteColumns.NOTES_COUNT + ">0)"; + // 打开笔记的请求码 private final static int REQUEST_CODE_OPEN_NODE = 102; + // 新建笔记的请求码 private final static int REQUEST_CODE_NEW_NODE = 103; + /** + * 活动创建时的初始化方法 + * + * 设置布局,初始化资源,首次使用时添加介绍笔记 + * + * @param savedInstanceState 保存的实例状态 + */ @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); @@ -147,6 +203,15 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt setAppInfoFromRawRes(); } + /** + * 活动结果回调方法 + * + * 当从笔记编辑活动返回时,刷新笔记列表 + * + * @param requestCode 请求码,标识是哪个活动返回 + * @param resultCode 结果码,RESULT_OK表示操作成功 + * @param data 返回的Intent数据 + */ @Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { if (resultCode == RESULT_OK @@ -157,6 +222,11 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt } } + /** + * 从原始资源文件加载并创建介绍笔记 + * + * 首次使用应用时,从res/raw/introduction文件读取内容并创建一条介绍笔记 + */ private void setAppInfoFromRawRes() { SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(this); if (!sp.getBoolean(PREFERENCE_ADD_INTRODUCTION, false)) { @@ -203,12 +273,22 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt } } + /** + * 活动启动时的回调方法 + * + * 启动异步查询笔记列表 + */ @Override protected void onStart() { super.onStart(); startAsyncNotesListQuery(); } + /** + * 初始化资源 + * + * 初始化所有UI组件、适配器和监听器 + */ private void initResources() { mContentResolver = this.getContentResolver(); mBackgroundQueryHandler = new BackgroundQueryHandler(this.getContentResolver()); @@ -231,11 +311,23 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt mModeCallBack = new ModeCallback(); } + /** + * 多选模式回调类 + * + * 实现ListView.MultiChoiceModeListener接口,处理多选模式的创建、销毁和项选中状态变化 + */ private class ModeCallback implements ListView.MultiChoiceModeListener, OnMenuItemClickListener { private DropdownMenu mDropDownMenu; private ActionMode mActionMode; private MenuItem mMoveMenu; + /** + * 创建多选模式的操作栏 + * + * @param mode ActionMode对象 + * @param menu 菜单对象 + * @return true表示成功创建 + */ public boolean onCreateActionMode(ActionMode mode, Menu menu) { getMenuInflater().inflate(R.menu.note_list_options, menu); menu.findItem(R.id.delete).setOnMenuItemClickListener(this); @@ -259,7 +351,13 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt (Button) customView.findViewById(R.id.selection_menu), R.menu.note_list_dropdown); mDropDownMenu.setOnDropdownMenuItemClickListener(new PopupMenu.OnMenuItemClickListener(){ - public boolean onMenuItemClick(MenuItem item) { + /** + * 下拉菜单项点击事件处理 + * + * @param item 被点击的菜单项 + * @return true表示事件已处理 + */ + public boolean onMenuItemClick(MenuItem item) { mNotesListAdapter.selectAll(!mNotesListAdapter.isAllSelected()); updateMenu(); return true; @@ -269,6 +367,11 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt return true; } + /** + * 更新菜单显示 + * + * 根据选中数量更新下拉菜单标题和全选按钮状态 + */ private void updateMenu() { int selectedCount = mNotesListAdapter.getSelectedCount(); // Update dropdown menu @@ -286,26 +389,60 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt } } + /** + * 准备多选模式的操作栏 + * + * @param mode ActionMode对象 + * @param menu 菜单对象 + * @return false + */ public boolean onPrepareActionMode(ActionMode mode, Menu menu) { // TODO Auto-generated method stub return false; } + /** + * 操作栏菜单项点击事件处理 + * + * @param mode ActionMode对象 + * @param item 被点击的菜单项 + * @return false + */ public boolean onActionItemClicked(ActionMode mode, MenuItem item) { // TODO Auto-generated method stub return false; } + /** + * 销毁多选模式的操作栏 + * + * 退出选择模式,恢复列表视图的常规状态 + * + * @param mode ActionMode对象 + */ public void onDestroyActionMode(ActionMode mode) { mNotesListAdapter.setChoiceMode(false); mNotesListView.setLongClickable(true); mAddNewNote.setVisibility(View.VISIBLE); } + /** + * 完成多选模式 + * + * 手动结束ActionMode + */ public void finishActionMode() { mActionMode.finish(); } + /** + * 列表项选中状态变化事件处理 + * + * @param mode ActionMode对象 + * @param position 列表项位置 + * @param id 列表项ID + * @param checked 是否选中 + */ public void onItemCheckedStateChanged(ActionMode mode, int position, long id, boolean checked) { mNotesListAdapter.setCheckedItem(position, checked); @@ -408,6 +545,13 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt }; + /** + * 启动异步笔记列表查询 + *

+ * 根据当前文件夹ID构建查询条件,启动后台查询获取笔记列表数据。 + * 根文件夹使用特殊的查询条件,子文件夹使用普通查询条件。 + *

+ */ private void startAsyncNotesListQuery() { String selection = (mCurrentFolderId == Notes.ID_ROOT_FOLDER) ? ROOT_FOLDER_SELECTION : NORMAL_SELECTION; @@ -417,11 +561,35 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt }, NoteColumns.TYPE + " DESC," + NoteColumns.MODIFIED_DATE + " DESC"); } + /** + * 后台查询处理器 + *

+ * 继承自AsyncQueryHandler,用于在后台线程执行数据库查询, + * 避免阻塞UI线程。 + *

+ */ private final class BackgroundQueryHandler extends AsyncQueryHandler { + /** + * 构造函数 + * @param contentResolver 内容解析器 + */ public BackgroundQueryHandler(ContentResolver contentResolver) { super(contentResolver); } + /** + * 查询完成回调 + *

+ * 根据查询令牌处理不同的查询结果: + *

    + *
  • FOLDER_NOTE_LIST_QUERY_TOKEN: 更新笔记列表适配器
  • + *
  • FOLDER_LIST_QUERY_TOKEN: 显示文件夹选择菜单
  • + *
+ *

+ * @param token 查询令牌 + * @param cookie Cookie对象 + * @param cursor 查询结果游标 + */ @Override protected void onQueryComplete(int token, Object cookie, Cursor cursor) { switch (token) { @@ -441,6 +609,14 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt } } + /** + * 显示文件夹选择菜单 + *

+ * 显示一个对话框,列出所有可用的目标文件夹供用户选择, + * 用于移动选中的笔记到指定文件夹。 + *

+ * @param cursor 包含文件夹列表的游标 + */ private void showFolderListMenu(Cursor cursor) { AlertDialog.Builder builder = new AlertDialog.Builder(NotesListActivity.this); builder.setTitle(R.string.menu_title_select_folder); @@ -462,6 +638,12 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt builder.show(); } + /** + * 创建新笔记 + *

+ * 启动NoteEditActivity创建新笔记,传递当前文件夹ID。 + *

+ */ private void createNewNote() { Intent intent = new Intent(this, NoteEditActivity.class); intent.setAction(Intent.ACTION_INSERT_OR_EDIT); @@ -469,6 +651,14 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt this.startActivityForResult(intent, REQUEST_CODE_NEW_NODE); } + /** + * 批量删除笔记 + *

+ * 在后台线程中删除选中的笔记。 + * 如果处于同步模式,将笔记移动到垃圾箱文件夹; + * 否则直接删除。同时更新相关的小部件。 + *

+ */ private void batchDelete() { new AsyncTask>() { protected HashSet doInBackground(Void... unused) { @@ -506,6 +696,15 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt }.execute(); } + /** + * 删除文件夹 + *

+ * 删除指定的文件夹及其包含的所有笔记。 + * 如果处于同步模式,将文件夹移动到垃圾箱; + * 否则直接删除。同时更新相关的小部件。 + *

+ * @param folderId 要删除的文件夹ID + */ private void deleteFolder(long folderId) { if (folderId == Notes.ID_ROOT_FOLDER) { Log.e(TAG, "Wrong folder id, should not happen " + folderId); @@ -533,6 +732,13 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt } } + /** + * 打开笔记 + *

+ * 启动NoteEditActivity查看和编辑指定的笔记。 + *

+ * @param data 笔记数据项 + */ private void openNode(NoteItemData data) { Intent intent = new Intent(this, NoteEditActivity.class); intent.setAction(Intent.ACTION_VIEW); @@ -540,6 +746,14 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt this.startActivityForResult(intent, REQUEST_CODE_OPEN_NODE); } + /** + * 打开文件夹 + *

+ * 进入指定的文件夹,显示该文件夹中的笔记列表。 + * 更新标题栏显示文件夹名称,并隐藏新建笔记按钮(如果是通话记录文件夹)。 + *

+ * @param data 文件夹数据项 + */ private void openFolder(NoteItemData data) { mCurrentFolderId = data.getId(); startAsyncNotesListQuery(); @@ -567,6 +781,12 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt } } + /** + * 显示软键盘 + *

+ * 强制显示系统软键盘,用于输入文件夹名称。 + *

+ */ private void showSoftInput() { InputMethodManager inputMethodManager = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE); if (inputMethodManager != null) { @@ -574,11 +794,26 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt } } + /** + * 隐藏软键盘 + *

+ * 隐藏指定视图的软键盘。 + *

+ * @param view 要隐藏键盘的视图 + */ private void hideSoftInput(View view) { InputMethodManager inputMethodManager = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE); inputMethodManager.hideSoftInputFromWindow(view.getWindowToken(), 0); } + /** + * 显示创建或修改文件夹对话框 + *

+ * 显示一个对话框,允许用户输入文件夹名称。 + * 根据create参数决定是创建新文件夹还是修改现有文件夹名称。 + *

+ * @param create true表示创建新文件夹,false表示修改文件夹名称 + */ private void showCreateOrModifyFolderDialog(final boolean create) { final AlertDialog.Builder builder = new AlertDialog.Builder(this); View view = LayoutInflater.from(this).inflate(R.layout.dialog_edit_text, null); @@ -664,6 +899,16 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt }); } + /** + * 返回键按下处理 + *

+ * 根据当前列表状态处理返回键事件: + *

    + *
  • 子文件夹或通话记录文件夹:返回根文件夹列表
  • + *
  • 笔记列表:调用父类方法退出Activity
  • + *
+ *

+ */ @Override public void onBackPressed() { switch (mState) { @@ -688,6 +933,14 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt } } + /** + * 更新小部件 + *

+ * 发送广播更新指定的小部件,使其显示最新的笔记内容。 + *

+ * @param appWidgetId 小部件ID + * @param appWidgetType 小部件类型(2x或4x) + */ private void updateWidget(int appWidgetId, int appWidgetType) { Intent intent = new Intent(AppWidgetManager.ACTION_APPWIDGET_UPDATE); if (appWidgetType == Notes.TYPE_WIDGET_2X) { @@ -707,6 +960,12 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt setResult(RESULT_OK, intent); } + /** + * 文件夹上下文菜单创建监听器 + *

+ * 为文件夹项创建上下文菜单,提供查看、删除和重命名选项。 + *

+ */ private final OnCreateContextMenuListener mFolderOnCreateContextMenuListener = new OnCreateContextMenuListener() { public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) { if (mFocusNoteDataItem != null) { @@ -760,6 +1019,19 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt return true; } + /** + * 准备选项菜单 + *

+ * 根据当前列表状态加载不同的菜单资源: + *

    + *
  • 笔记列表:显示同步、设置、新建文件夹、导出、搜索等选项
  • + *
  • 子文件夹:显示新建笔记选项
  • + *
  • 通话记录文件夹:显示新建笔记选项
  • + *
+ *

+ * @param menu 选项菜单对象 + * @return true + */ @Override public boolean onPrepareOptionsMenu(Menu menu) { menu.clear(); @@ -778,6 +1050,22 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt return true; } + /** + * 选项菜单项选择处理 + *

+ * 处理用户点击选项菜单的事件,包括: + *

    + *
  • 新建文件夹
  • + *
  • 导出笔记为文本
  • + *
  • 同步或取消同步
  • + *
  • 打开设置
  • + *
  • 新建笔记
  • + *
  • 搜索
  • + *
+ *

+ * @param item 被点击的菜单项 + * @return true + */ @Override public boolean onOptionsItemSelected(MenuItem item) { switch (item.getItemId()) { @@ -818,12 +1106,26 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt return true; } + /** + * 搜索请求处理 + *

+ * 启动系统搜索界面,允许用户搜索笔记内容。 + *

+ * @return true + */ @Override public boolean onSearchRequested() { startSearch(null, false, null /* appData */, false); return true; } + /** + * 导出笔记为文本文件 + *

+ * 在后台线程中将所有笔记导出为文本文件到SD卡。 + * 根据导出结果显示相应的提示对话框。 + *

+ */ private void exportNoteToText() { final BackupUtils backup = BackupUtils.getInstance(NotesListActivity.this); new AsyncTask() { @@ -866,19 +1168,51 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt }.execute(); } + /** + * 检查是否处于同步模式 + *

+ * 判断是否已设置同步账户,如果已设置则表示处于同步模式。 + *

+ * @return true表示处于同步模式,false表示未同步 + */ private boolean isSyncMode() { return NotesPreferenceActivity.getSyncAccountName(this).trim().length() > 0; } + /** + * 启动设置Activity + *

+ * 启动NotesPreferenceActivity进行应用设置。 + *

+ */ private void startPreferenceActivity() { Activity from = getParent() != null ? getParent() : this; Intent intent = new Intent(from, NotesPreferenceActivity.class); from.startActivityIfNeeded(intent, -1); } + /** + * 列表项点击监听器 + *

+ * 处理笔记列表项的点击事件,根据当前状态和项类型执行相应操作: + *

    + *
  • 多选模式:切换选中状态
  • + *
  • 笔记列表:打开文件夹或笔记
  • + *
  • 子文件夹/通话记录文件夹:打开笔记
  • + *
+ *

+ */ private class OnListItemClickListener implements OnItemClickListener { - public void onItemClick(AdapterView parent, View view, int position, long id) { + /** + * 列表项点击事件处理 + * + * @param parent 父视图 + * @param view 被点击的视图 + * @param position 列表项位置 + * @param id 列表项ID + */ + public void onItemClick(AdapterView parent, View view, int position, long id) { if (view instanceof NotesListItem) { NoteItemData item = ((NotesListItem) view).getItemData(); if (mNotesListAdapter.isInChoiceMode()) { @@ -917,6 +1251,13 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt } + /** + * 启动查询目标文件夹 + *

+ * 查询所有可用的文件夹,用于显示在移动笔记的对话框中。 + * 排除垃圾箱文件夹和当前文件夹。 + *

+ */ private void startQueryDestinationFolders() { String selection = NoteColumns.TYPE + "=? AND " + NoteColumns.PARENT_ID + "<>? AND " + NoteColumns.ID + "<>?"; selection = (mState == ListEditState.NOTE_LIST) ? selection: @@ -935,6 +1276,15 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt NoteColumns.MODIFIED_DATE + " DESC"); } + /** + * 列表项长按事件处理 + * + * @param parent 父视图 + * @param view 被长按的视图 + * @param position 列表项位置 + * @param id 列表项ID + * @return true表示事件已处理 + */ public boolean onItemLongClick(AdapterView parent, View view, int position, long id) { if (view instanceof NotesListItem) { mFocusNoteDataItem = ((NotesListItem) view).getItemData(); diff --git a/src/Notes-master/src/net/micode/notes/ui/NotesListAdapter.java b/src/Notes-master/src/net/micode/notes/ui/NotesListAdapter.java index 51c9cb9..6085bf0 100644 --- a/src/Notes-master/src/net/micode/notes/ui/NotesListAdapter.java +++ b/src/Notes-master/src/net/micode/notes/ui/NotesListAdapter.java @@ -31,18 +31,51 @@ import java.util.HashSet; import java.util.Iterator; +/** + * 笔记列表适配器 + * + * 这个类继承自CursorAdapter,用于将数据库中的笔记数据绑定到ListView中显示。 + * 它支持笔记的选择模式、批量操作以及与桌面小部件的关联。 + * + * 主要功能: + * 1. 将笔记数据绑定到NotesListItem视图 + * 2. 支持多选模式和批量选择操作 + * 3. 获取选中的笔记ID和关联的桌面小部件信息 + * 4. 统计笔记数量和选中数量 + * + * @see NotesListItem + * @see NoteItemData + */ public class NotesListAdapter extends CursorAdapter { private static final String TAG = "NotesListAdapter"; + // 应用上下文 private Context mContext; + // 记录选中状态的Map,key为位置,value为是否选中 private HashMap mSelectedIndex; + // 笔记总数 private int mNotesCount; + // 是否处于选择模式 private boolean mChoiceMode; + /** + * 桌面小部件属性类 + * + * 用于存储桌面小部件的ID和类型信息 + */ public static class AppWidgetAttribute { + // 桌面小部件ID public int widgetId; + // 桌面小部件类型 public int widgetType; }; + /** + * 构造器 + * + * 初始化笔记列表适配器,创建选中状态Map和计数器 + * + * @param context 应用上下文,不能为 null + */ public NotesListAdapter(Context context) { super(context, null); mSelectedIndex = new HashMap(); @@ -50,11 +83,28 @@ public class NotesListAdapter extends CursorAdapter { mNotesCount = 0; } + /** + * 创建新的列表项视图 + * + * @param context 应用上下文 + * @param cursor 数据库游标,包含当前项的数据 + * @param parent 父视图 + * @return 新创建的NotesListItem视图对象 + */ @Override public View newView(Context context, Cursor cursor, ViewGroup parent) { return new NotesListItem(context); } + /** + * 绑定数据到视图 + * + * 将数据库游标中的数据绑定到已存在的视图上 + * + * @param view 需要绑定数据的视图 + * @param context 应用上下文 + * @param cursor 数据库游标,包含当前项的数据 + */ @Override public void bindView(View view, Context context, Cursor cursor) { if (view instanceof NotesListItem) { @@ -64,20 +114,41 @@ public class NotesListAdapter extends CursorAdapter { } } + /** + * 设置指定位置的选中状态 + * + * @param position 列表项位置,从0开始 + * @param checked 是否选中 + */ public void setCheckedItem(final int position, final boolean checked) { mSelectedIndex.put(position, checked); notifyDataSetChanged(); } + /** + * 判断是否处于选择模式 + * + * @return 如果处于选择模式返回true,否则返回false + */ public boolean isInChoiceMode() { return mChoiceMode; } + /** + * 设置选择模式 + * + * @param mode true表示进入选择模式,false表示退出选择模式 + */ public void setChoiceMode(boolean mode) { mSelectedIndex.clear(); mChoiceMode = mode; } + /** + * 全选或取消全选所有笔记 + * + * @param checked true表示全选,false表示取消全选 + */ public void selectAll(boolean checked) { Cursor cursor = getCursor(); for (int i = 0; i < getCount(); i++) { @@ -89,6 +160,11 @@ public class NotesListAdapter extends CursorAdapter { } } + /** + * 获取所有选中项的笔记ID集合 + * + * @return 包含所有选中笔记ID的HashSet集合,如果没有选中项则返回空集合 + */ public HashSet getSelectedItemIds() { HashSet itemSet = new HashSet(); for (Integer position : mSelectedIndex.keySet()) { @@ -105,6 +181,11 @@ public class NotesListAdapter extends CursorAdapter { return itemSet; } + /** + * 获取所有选中项关联的桌面小部件集合 + * + * @return 包含所有选中笔记关联的桌面小部件属性的HashSet集合,如果游标无效则返回null + */ public HashSet getSelectedWidget() { HashSet itemSet = new HashSet(); for (Integer position : mSelectedIndex.keySet()) { @@ -128,6 +209,11 @@ public class NotesListAdapter extends CursorAdapter { return itemSet; } + /** + * 获取选中项的数量 + * + * @return 选中项的数量,如果没有选中项则返回0 + */ public int getSelectedCount() { Collection values = mSelectedIndex.values(); if (null == values) { @@ -143,11 +229,22 @@ public class NotesListAdapter extends CursorAdapter { return count; } + /** + * 判断是否已全选所有笔记 + * + * @return 如果所有笔记都被选中且至少有一个笔记则返回true,否则返回false + */ public boolean isAllSelected() { int checkedCount = getSelectedCount(); return (checkedCount != 0 && checkedCount == mNotesCount); } + /** + * 判断指定位置的项是否被选中 + * + * @param position 列表项位置,从0开始 + * @return 如果该项被选中返回true,否则返回false + */ public boolean isSelectedItem(final int position) { if (null == mSelectedIndex.get(position)) { return false; @@ -155,12 +252,22 @@ public class NotesListAdapter extends CursorAdapter { return mSelectedIndex.get(position); } + /** + * 当内容发生变化时调用 + * + * 重新计算笔记数量 + */ @Override protected void onContentChanged() { super.onContentChanged(); calcNotesCount(); } + /** + * 更换游标 + * + * @param cursor 新的数据库游标 + */ @Override public void changeCursor(Cursor cursor) { super.changeCursor(cursor); diff --git a/src/Notes-master/src/net/micode/notes/ui/NotesListItem.java b/src/Notes-master/src/net/micode/notes/ui/NotesListItem.java index 1221e80..ad89d41 100644 --- a/src/Notes-master/src/net/micode/notes/ui/NotesListItem.java +++ b/src/Notes-master/src/net/micode/notes/ui/NotesListItem.java @@ -30,6 +30,14 @@ import net.micode.notes.tool.DataUtils; import net.micode.notes.tool.ResourceParser.NoteItemBgResources; +/** + * 笔记列表项视图 + *

+ * 自定义的 LinearLayout,表示笔记列表中的单个笔记项。 + * 该视图显示笔记信息,包括标题、时间、通话名称(针对通话记录)和提醒图标。 + * 支持在多选模式下显示复选框。 + *

+ */ public class NotesListItem extends LinearLayout { private ImageView mAlert; private TextView mTitle; @@ -38,6 +46,10 @@ public class NotesListItem extends LinearLayout { private NoteItemData mItemData; private CheckBox mCheckBox; + /** + * 构造函数 + * @param context 用于加载布局的上下文对象 + */ public NotesListItem(Context context) { super(context); inflate(context, R.layout.note_item, this); @@ -48,6 +60,13 @@ public class NotesListItem extends LinearLayout { mCheckBox = (CheckBox) findViewById(android.R.id.checkbox); } + /** + * 绑定笔记数据到视图项 + * @param context 用于访问资源的上下文对象 + * @param data 包含要显示的笔记信息的 NoteItemData 对象 + * @param choiceMode 列表是否处于多选模式(显示复选框) + * @param checked 该项是否被选中(仅在多选模式下有意义) + */ public void bind(Context context, NoteItemData data, boolean choiceMode, boolean checked) { if (choiceMode && data.getType() == Notes.TYPE_NOTE) { mCheckBox.setVisibility(View.VISIBLE); @@ -99,6 +118,10 @@ public class NotesListItem extends LinearLayout { setBackground(data); } + /** + * 根据笔记项的位置和类型设置合适的背景资源 + * @param data 包含笔记背景颜色和位置信息的 NoteItemData 对象 + */ private void setBackground(NoteItemData data) { int id = data.getBgColorId(); if (data.getType() == Notes.TYPE_NOTE) { @@ -116,6 +139,10 @@ public class NotesListItem extends LinearLayout { } } + /** + * 获取绑定到该视图项的笔记数据 + * @return 包含该笔记信息的 NoteItemData 对象 + */ public NoteItemData getItemData() { return mItemData; } diff --git a/src/Notes-master/src/net/micode/notes/ui/NotesPreferenceActivity.java b/src/Notes-master/src/net/micode/notes/ui/NotesPreferenceActivity.java index 07c5f7e..7f9475f 100644 --- a/src/Notes-master/src/net/micode/notes/ui/NotesPreferenceActivity.java +++ b/src/Notes-master/src/net/micode/notes/ui/NotesPreferenceActivity.java @@ -48,27 +48,85 @@ import net.micode.notes.data.Notes.NoteColumns; import net.micode.notes.gtask.remote.GTaskSyncService; +/** + * 设置界面Activity + *

+ * 该Activity用于管理应用的各种设置,主要包括: + *

    + *
  • Google Tasks同步账户的设置和管理
  • + *
  • 同步状态显示和手动同步控制
  • + *
  • 背景颜色随机显示设置
  • + *
+ *

+ *

+ * 该类继承自PreferenceActivity,使用SharedPreferences来持久化设置数据。 + * 通过GTaskReceiver接收同步服务的广播,实时更新同步状态。 + *

+ */ public class NotesPreferenceActivity extends PreferenceActivity { + /** + * SharedPreferences文件名 + */ public static final String PREFERENCE_NAME = "notes_preferences"; + /** + * 同步账户名称的SharedPreferences键 + */ public static final String PREFERENCE_SYNC_ACCOUNT_NAME = "pref_key_account_name"; + /** + * 最后同步时间的SharedPreferences键 + */ public static final String PREFERENCE_LAST_SYNC_TIME = "pref_last_sync_time"; + /** + * 背景颜色随机显示设置的SharedPreferences键 + */ public static final String PREFERENCE_SET_BG_COLOR_KEY = "pref_key_bg_random_appear"; + /** + * 同步账户分类的Preference键 + */ private static final String PREFERENCE_SYNC_ACCOUNT_KEY = "pref_sync_account_key"; + /** + * 账户授权过滤器键,用于添加账户Intent + */ private static final String AUTHORITIES_FILTER_KEY = "authorities"; + /** + * 同步账户分类的PreferenceCategory + */ private PreferenceCategory mAccountCategory; + /** + * 同步服务广播接收器 + */ private GTaskReceiver mReceiver; + /** + * 原始账户数组,用于检测新增账户 + */ private Account[] mOriAccounts; + /** + * 是否添加了新账户的标志 + */ private boolean mHasAddedAccount; + /** + * 创建Activity + *

+ * 初始化设置界面,包括: + *

    + *
  • 启用ActionBar的返回导航
  • + *
  • 加载preferences.xml配置文件
  • + *
  • 初始化账户分类和广播接收器
  • + *
  • 添加设置界面头部视图
  • + *
+ *

+ * @param icicle 保存的实例状态 + */ @Override protected void onCreate(Bundle icicle) { super.onCreate(icicle); @@ -88,6 +146,13 @@ public class NotesPreferenceActivity extends PreferenceActivity { getListView().addHeaderView(header, null, true); } + /** + * Activity恢复时调用 + *

+ * 检查是否有新添加的Google账户,如果有则自动设置为同步账户。 + * 然后刷新UI显示。 + *

+ */ @Override protected void onResume() { super.onResume(); @@ -116,6 +181,12 @@ public class NotesPreferenceActivity extends PreferenceActivity { refreshUI(); } + /** + * Activity销毁时调用 + *

+ * 注销同步服务广播接收器,防止内存泄漏。 + *

+ */ @Override protected void onDestroy() { if (mReceiver != null) { @@ -124,6 +195,18 @@ public class NotesPreferenceActivity extends PreferenceActivity { super.onDestroy(); } + /** + * 加载账户设置选项 + *

+ * 创建并添加账户Preference到账户分类中。 + * 点击该Preference时: + *

    + *
  • 如果未设置账户,显示账户选择对话框
  • + *
  • 如果已设置账户,显示确认更改账户对话框
  • + *
  • 如果正在同步,显示提示消息
  • + *
+ *

+ */ private void loadAccountPreference() { mAccountCategory.removeAll(); @@ -154,6 +237,17 @@ public class NotesPreferenceActivity extends PreferenceActivity { mAccountCategory.addPreference(accountPref); } + /** + * 加载同步按钮和同步状态显示 + *

+ * 根据当前同步状态设置按钮文本和点击事件: + *

    + *
  • 正在同步:显示"取消同步"按钮,点击取消同步
  • + *
  • 未同步:显示"立即同步"按钮,点击开始同步
  • + *
+ * 同时显示最后同步时间或当前同步进度。 + *

+ */ private void loadSyncButton() { Button syncButton = (Button) findViewById(R.id.preference_sync_button); TextView lastSyncTimeView = (TextView) findViewById(R.id.prefenerece_sync_status_textview); @@ -193,11 +287,24 @@ public class NotesPreferenceActivity extends PreferenceActivity { } } + /** + * 刷新UI显示 + *

+ * 重新加载账户设置选项和同步按钮状态。 + *

+ */ private void refreshUI() { loadAccountPreference(); loadSyncButton(); } + /** + * 显示选择账户对话框 + *

+ * 显示一个对话框,列出所有可用的Google账户供用户选择。 + * 同时提供"添加账户"选项,点击后跳转到系统账户添加界面。 + *

+ */ private void showSelectAccountAlertDialog() { AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(this); @@ -254,6 +361,17 @@ public class NotesPreferenceActivity extends PreferenceActivity { }); } + /** + * 显示更改账户确认对话框 + *

+ * 显示一个对话框,提供三个选项: + *

    + *
  • 更改账户:显示账户选择对话框
  • + *
  • 移除账户:删除当前同步账户并清理相关数据
  • + *
  • 取消:关闭对话框
  • + *
+ *

+ */ private void showChangeAccountConfirmAlertDialog() { AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(this); @@ -283,11 +401,29 @@ public class NotesPreferenceActivity extends PreferenceActivity { dialogBuilder.show(); } + /** + * 获取所有Google账户 + *

+ * 从系统AccountManager中获取所有类型为"com.google"的账户。 + *

+ * @return Google账户数组 + */ private Account[] getGoogleAccounts() { AccountManager accountManager = AccountManager.get(this); return accountManager.getAccountsByType("com.google"); } + /** + * 设置同步账户 + *

+ * 保存指定的账户名称到SharedPreferences,并清理相关数据: + *

    + *
  • 清除最后同步时间
  • + *
  • 清除所有笔记的GTASK_ID和SYNC_ID
  • + *
+ *

+ * @param account 要设置的账户名称 + */ private void setSyncAccount(String account) { if (!getSyncAccountName(this).equals(account)) { SharedPreferences settings = getSharedPreferences(PREFERENCE_NAME, Context.MODE_PRIVATE); @@ -318,6 +454,13 @@ public class NotesPreferenceActivity extends PreferenceActivity { } } + /** + * 移除同步账户 + *

+ * 从SharedPreferences中删除同步账户和最后同步时间, + * 并清理所有笔记的GTASK_ID和SYNC_ID。 + *

+ */ private void removeSyncAccount() { SharedPreferences settings = getSharedPreferences(PREFERENCE_NAME, Context.MODE_PRIVATE); SharedPreferences.Editor editor = settings.edit(); @@ -340,12 +483,28 @@ public class NotesPreferenceActivity extends PreferenceActivity { }).start(); } + /** + * 获取同步账户名称 + *

+ * 从SharedPreferences中读取已设置的同步账户名称。 + *

+ * @param context 上下文对象 + * @return 同步账户名称,如果未设置则返回空字符串 + */ public static String getSyncAccountName(Context context) { SharedPreferences settings = context.getSharedPreferences(PREFERENCE_NAME, Context.MODE_PRIVATE); return settings.getString(PREFERENCE_SYNC_ACCOUNT_NAME, ""); } + /** + * 设置最后同步时间 + *

+ * 将指定的同步时间保存到SharedPreferences。 + *

+ * @param context 上下文对象 + * @param time 同步时间戳 + */ public static void setLastSyncTime(Context context, long time) { SharedPreferences settings = context.getSharedPreferences(PREFERENCE_NAME, Context.MODE_PRIVATE); @@ -354,14 +513,36 @@ public class NotesPreferenceActivity extends PreferenceActivity { editor.commit(); } + /** + * 获取最后同步时间 + *

+ * 从SharedPreferences中读取最后同步时间。 + *

+ * @param context 上下文对象 + * @return 最后同步时间戳,如果未同步过则返回0 + */ public static long getLastSyncTime(Context context) { SharedPreferences settings = context.getSharedPreferences(PREFERENCE_NAME, Context.MODE_PRIVATE); return settings.getLong(PREFERENCE_LAST_SYNC_TIME, 0); } + /** + * 同步服务广播接收器 + *

+ * 接收GTaskSyncService发送的广播,实时更新UI显示同步状态和进度。 + *

+ */ private class GTaskReceiver extends BroadcastReceiver { + /** + * 接收广播 + *

+ * 当收到同步服务广播时,刷新UI并更新同步状态显示。 + *

+ * @param context 上下文对象 + * @param intent 广播Intent + */ @Override public void onReceive(Context context, Intent intent) { refreshUI(); @@ -374,6 +555,15 @@ public class NotesPreferenceActivity extends PreferenceActivity { } } + /** + * 处理菜单项选择 + *

+ * 处理ActionBar上的菜单项点击事件。 + * 当点击返回按钮时,返回到笔记列表界面。 + *

+ * @param item 被点击的菜单项 + * @return true表示已处理,false表示未处理 + */ public boolean onOptionsItemSelected(MenuItem item) { switch (item.getItemId()) { case android.R.id.home: -- 2.34.1 From acb6972819388227b0c89f51a2c5fa99f8788bbc Mon Sep 17 00:00:00 2001 From: JTXjtx Date: Tue, 23 Dec 2025 20:37:40 +0800 Subject: [PATCH 2/3] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E5=AF=B9=E4=BA=8E?= =?UTF-8?q?=E5=B0=8F=E7=B1=B3=E4=BE=BF=E7=AD=BE=E5=BC=80=E6=BA=90=E4=BB=A3?= =?UTF-8?q?=E7=A0=81=E7=9A=84=E6=B3=A8=E9=87=8A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/net/micode/notes/data/Contact.java | 58 +++ .../src/net/micode/notes/data/Notes.java | 78 +++- .../notes/data/NotesDatabaseHelper.java | 236 ++++++++++++ .../net/micode/notes/data/NotesProvider.java | 218 ++++++++++- .../net/micode/notes/gtask/data/MetaData.java | 78 ++++ .../src/net/micode/notes/gtask/data/Node.java | 144 +++++++ .../net/micode/notes/gtask/data/SqlData.java | 80 ++++ .../net/micode/notes/gtask/data/SqlNote.java | 165 +++++++- .../src/net/micode/notes/gtask/data/Task.java | 148 ++++++++ .../net/micode/notes/gtask/data/TaskList.java | 167 ++++++++ .../exception/ActionFailureException.java | 22 ++ .../exception/NetworkFailureException.java | 22 ++ .../notes/gtask/remote/GTaskASyncTask.java | 117 +++++- .../notes/gtask/remote/GTaskClient.java | 206 ++++++++++ .../notes/gtask/remote/GTaskManager.java | 59 ++- .../notes/gtask/remote/GTaskSyncService.java | 113 ++++++ .../src/net/micode/notes/model/Note.java | 186 ++++++++- .../net/micode/notes/model/WorkingNote.java | 270 ++++++++++++- .../net/micode/notes/tool/BackupUtils.java | 130 ++++++- .../src/net/micode/notes/tool/DataUtils.java | 146 ++++++- .../micode/notes/tool/GTaskStringUtils.java | 54 ++- .../net/micode/notes/tool/ResourceParser.java | 146 ++++++- .../micode/notes/ui/AlarmAlertActivity.java | 114 +++++- .../micode/notes/ui/AlarmInitReceiver.java | 81 +++- .../net/micode/notes/ui/AlarmReceiver.java | 41 +- .../net/micode/notes/ui/DateTimePicker.java | 264 ++++++++++--- .../micode/notes/ui/DateTimePickerDialog.java | 89 +++++ .../src/net/micode/notes/ui/DropdownMenu.java | 48 +++ .../micode/notes/ui/FoldersListAdapter.java | 77 +++- .../net/micode/notes/ui/NoteEditActivity.java | 323 +++++++++++++++- .../src/net/micode/notes/ui/NoteEditText.java | 143 ++++++- .../src/net/micode/notes/ui/NoteItemData.java | 178 ++++++++- .../micode/notes/ui/NotesListActivity.java | 356 +++++++++++++++++- .../net/micode/notes/ui/NotesListAdapter.java | 107 ++++++ .../net/micode/notes/ui/NotesListItem.java | 27 ++ .../notes/ui/NotesPreferenceActivity.java | 190 ++++++++++ 36 files changed, 4735 insertions(+), 146 deletions(-) diff --git a/src/Notes-master/src/net/micode/notes/data/Contact.java b/src/Notes-master/src/net/micode/notes/data/Contact.java index d97ac5d..da3a693 100644 --- a/src/Notes-master/src/net/micode/notes/data/Contact.java +++ b/src/Notes-master/src/net/micode/notes/data/Contact.java @@ -25,10 +25,52 @@ import android.util.Log; import java.util.HashMap; +/** + * 联系人信息查询工具类 + *

+ * 提供根据电话号码查询联系人姓名的功能,使用缓存机制提高查询效率。 + * 通过Android系统的ContactsContract Provider查询联系人信息。 + *

+ *

+ * 主要功能: + *

    + *
  • 根据电话号码查询联系人姓名
  • + *
  • 使用HashMap缓存已查询的联系人信息,避免重复查询
  • + *
  • 支持国际号码格式的匹配
  • + *
+ *

+ *

+ * 使用场景: + * 当笔记中包含电话号码时,使用此类查询对应的联系人姓名并显示。 + *

+ * + * @see ContactsContract + * @see PhoneNumberUtils + */ public class Contact { + /** + * 联系人信息缓存 + *

+ * 使用HashMap存储已查询的电话号码和对应的联系人姓名, + * 避免重复查询系统联系人数据库,提高性能。 + *

+ * Key: 电话号码 + * Value: 联系人姓名 + */ private static HashMap sContactCache; + /** + * 日志标签 + */ private static final String TAG = "Contact"; + /** + * 查询联系人的SQL选择条件 + *

+ * 使用PHONE_NUMBERS_EQUAL函数进行号码匹配,支持国际号码格式。 + * 只查询电话号码类型的数据(Phone.CONTENT_ITEM_TYPE)。 + * 使用min_match='+'进行最小匹配,提高查询效率。 + *

+ */ private static final String CALLER_ID_SELECTION = "PHONE_NUMBERS_EQUAL(" + Phone.NUMBER + ",?) AND " + Data.MIMETYPE + "='" + Phone.CONTENT_ITEM_TYPE + "'" + " AND " + Data.RAW_CONTACT_ID + " IN " @@ -36,15 +78,30 @@ public class Contact { + " FROM phone_lookup" + " WHERE min_match = '+')"; + /** + * 根据电话号码获取联系人姓名 + *

+ * 首先检查缓存中是否已存在该号码对应的联系人姓名, + * 如果存在则直接返回,否则查询系统联系人数据库。 + * 查询结果会被缓存以提高后续查询效率。 + *

+ * + * @param context 应用上下文,用于访问ContentResolver + * @param phoneNumber 要查询的电话号码 + * @return 联系人姓名,如果未找到则返回null + */ public static String getContact(Context context, String phoneNumber) { + // 初始化缓存 if(sContactCache == null) { sContactCache = new HashMap(); } + // 检查缓存中是否已存在 if(sContactCache.containsKey(phoneNumber)) { return sContactCache.get(phoneNumber); } + // 构建查询条件,使用toCallerIDMinMatch进行号码最小匹配 String selection = CALLER_ID_SELECTION.replace("+", PhoneNumberUtils.toCallerIDMinMatch(phoneNumber)); Cursor cursor = context.getContentResolver().query( @@ -54,6 +111,7 @@ public class Contact { new String[] { phoneNumber }, null); + // 处理查询结果 if (cursor != null && cursor.moveToFirst()) { try { String name = cursor.getString(0); diff --git a/src/Notes-master/src/net/micode/notes/data/Notes.java b/src/Notes-master/src/net/micode/notes/data/Notes.java index f240604..8b1f853 100644 --- a/src/Notes-master/src/net/micode/notes/data/Notes.java +++ b/src/Notes-master/src/net/micode/notes/data/Notes.java @@ -17,33 +17,103 @@ package net.micode.notes.data; import android.net.Uri; +/** + * 笔记数据常量定义类 + *

+ * 定义了笔记应用中使用的所有常量、接口和内部类,包括: + *

    + *
  • Content Provider的Authority和URI
  • + *
  • 笔记类型常量(普通笔记、文件夹、系统文件夹)
  • + *
  • 系统文件夹ID常量
  • + *
  • Intent Extra键常量
  • + *
  • Widget类型常量
  • + *
  • 笔记数据列接口(NoteColumns、DataColumns)
  • + *
  • 文本笔记和通话记录笔记内部类
  • + *
+ *

+ *

+ * 该类主要用于定义数据库表结构和Content Provider的契约, + * 提供统一的常量访问接口,方便应用各模块使用。 + *

+ */ public class Notes { + /** + * Content Provider的Authority + */ 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; /** - * Following IDs are system folders' identifiers - * {@link Notes#ID_ROOT_FOLDER } is default folder - * {@link Notes#ID_TEMPARAY_FOLDER } is for notes belonging no folder - * {@link Notes#ID_CALL_RECORD_FOLDER} is to store call records + * 以下ID是系统文件夹的标识符 + * {@link Notes#ID_ROOT_FOLDER } 是默认文件夹 + * {@link Notes#ID_TEMPARAY_FOLDER } 用于不属于任何文件夹的笔记 + * {@link Notes#ID_CALL_RECORD_FOLDER} 用于存储通话记录 */ public static final int ID_ROOT_FOLDER = 0; + /** + * 临时文件夹ID,用于不属于任何文件夹的笔记 + */ public static final int ID_TEMPARAY_FOLDER = -1; + /** + * 通话记录文件夹ID,用于存储通话记录 + */ public static final int ID_CALL_RECORD_FOLDER = -2; + /** + * 回收站文件夹ID,用于存储已删除的笔记 + */ public static final int ID_TRASH_FOLER = -3; + /** + * Intent Extra键:提醒日期 + */ public static final String INTENT_EXTRA_ALERT_DATE = "net.micode.notes.alert_date"; + /** + * Intent Extra键:背景颜色ID + */ public static final String INTENT_EXTRA_BACKGROUND_ID = "net.micode.notes.background_color_id"; + /** + * Intent Extra键:Widget ID + */ public static final String INTENT_EXTRA_WIDGET_ID = "net.micode.notes.widget_id"; + /** + * Intent Extra键:Widget类型 + */ public static final String INTENT_EXTRA_WIDGET_TYPE = "net.micode.notes.widget_type"; + /** + * Intent Extra键:文件夹ID + */ public static final String INTENT_EXTRA_FOLDER_ID = "net.micode.notes.folder_id"; + /** + * Intent Extra键:通话日期 + */ public static final String INTENT_EXTRA_CALL_DATE = "net.micode.notes.call_date"; + /** + * 无效的Widget类型 + */ public static final int TYPE_WIDGET_INVALIDE = -1; + /** + * 2x2 Widget类型 + */ public static final int TYPE_WIDGET_2X = 0; + /** + * 4x4 Widget类型 + */ public static final int TYPE_WIDGET_4X = 1; public static class DataConstants { diff --git a/src/Notes-master/src/net/micode/notes/data/NotesDatabaseHelper.java b/src/Notes-master/src/net/micode/notes/data/NotesDatabaseHelper.java index ffe5d57..558caf7 100644 --- a/src/Notes-master/src/net/micode/notes/data/NotesDatabaseHelper.java +++ b/src/Notes-master/src/net/micode/notes/data/NotesDatabaseHelper.java @@ -27,21 +27,114 @@ import net.micode.notes.data.Notes.DataConstants; import net.micode.notes.data.Notes.NoteColumns; +/** + * 笔记数据库帮助类 + *

+ * 继承自SQLiteOpenHelper,负责笔记应用SQLite数据库的创建、升级和管理。 + * 管理两个主要数据表:note表(存储笔记和文件夹信息)和data表(存储笔记的详细内容)。 + * 使用数据库触发器自动维护笔记计数、内容同步等关联关系。 + *

+ *

+ * 主要功能: + *

    + *
  • 创建和升级数据库表结构
  • + *
  • 创建和管理数据库触发器
  • + *
  • 维护系统文件夹(通话记录、根文件夹、临时文件夹、回收站)
  • + *
  • 支持数据库版本升级(当前版本:4)
  • + *
  • 提供单例模式访问数据库帮助类实例
  • + *
+ *

+ *

+ * 数据库版本历史: + *

    + *
  • V1: 初始版本
  • + *
  • V2: 重构表结构
  • + *
  • V3: 添加GTASK_ID列和回收站文件夹
  • + *
  • V4: 添加VERSION列
  • + *
+ *

+ * + * @see SQLiteOpenHelper + * @see Notes + */ public class NotesDatabaseHelper extends SQLiteOpenHelper { + /** + * 数据库文件名 + */ private static final String DB_NAME = "note.db"; + /** + * 数据库版本号 + *

+ * 当前数据库版本为4,用于跟踪数据库结构变更。 + * 当数据库版本变更时,onUpgrade方法会被调用以执行升级逻辑。 + *

+ */ private static final int DB_VERSION = 4; + /** + * 数据库表名常量接口 + */ public interface TABLE { + /** + * 笔记表名 + *

+ * 存储笔记和文件夹的基本信息,包括ID、父文件夹ID、创建时间、修改时间、 + * 背景颜色、提醒时间、附件状态、笔记数量、摘要、类型、Widget信息、 + * 同步ID、本地修改状态、原始父文件夹ID、GTASK ID、版本等字段。 + *

+ */ public static final String NOTE = "note"; + /** + * 数据表名 + *

+ * 存储笔记的详细内容,支持多种MIME类型(文本、图片、附件等)。 + * 每条数据记录关联到一条笔记,包含MIME类型、内容、以及5个通用数据字段。 + *

+ */ public static final String DATA = "data"; } + /** + * 日志标签 + */ private static final String TAG = "NotesDatabaseHelper"; + /** + * 数据库帮助类单例实例 + *

+ * 使用单例模式确保全局只有一个数据库帮助类实例, + * 避免多个实例同时操作数据库导致的数据不一致问题。 + *

+ */ private static NotesDatabaseHelper mInstance; + /** + * 创建笔记表的SQL语句 + *

+ * 创建note表,包含以下字段: + *

    + *
  • ID: 主键,自增
  • + *
  • PARENT_ID: 父文件夹ID,默认为0
  • + *
  • ALERTED_DATE: 提醒时间,默认为0
  • + *
  • BG_COLOR_ID: 背景颜色ID,默认为0
  • + *
  • CREATED_DATE: 创建时间,默认为当前时间戳
  • + *
  • HAS_ATTACHMENT: 是否有附件,默认为0
  • + *
  • MODIFIED_DATE: 修改时间,默认为当前时间戳
  • + *
  • NOTES_COUNT: 笔记数量,默认为0(仅文件夹有效)
  • + *
  • SNIPPET: 笔记摘要,默认为空字符串
  • + *
  • TYPE: 类型(0=普通笔记,1=文件夹,2=系统),默认为0
  • + *
  • WIDGET_ID: Widget ID,默认为0
  • + *
  • WIDGET_TYPE: Widget类型,默认为-1
  • + *
  • SYNC_ID: 同步ID,默认为0
  • + *
  • LOCAL_MODIFIED: 本地修改标志,默认为0
  • + *
  • ORIGIN_PARENT_ID: 原始父文件夹ID,默认为0
  • + *
  • GTASK_ID: Google Tasks ID,默认为空字符串
  • + *
  • VERSION: 版本号,默认为0
  • + *
+ *

+ */ private static final String CREATE_NOTE_TABLE_SQL = "CREATE TABLE " + TABLE.NOTE + "(" + NoteColumns.ID + " INTEGER PRIMARY KEY," + @@ -63,6 +156,21 @@ public class NotesDatabaseHelper extends SQLiteOpenHelper { NoteColumns.VERSION + " INTEGER NOT NULL DEFAULT 0" + ")"; + /** + * 创建数据表的SQL语句 + *

+ * 创建data表,包含以下字段: + *

    + *
  • ID: 主键,自增
  • + *
  • MIME_TYPE: MIME类型,不能为空
  • + *
  • NOTE_ID: 关联的笔记ID,默认为0
  • + *
  • CREATED_DATE: 创建时间,默认为当前时间戳
  • + *
  • MODIFIED_DATE: 修改时间,默认为当前时间戳
  • + *
  • CONTENT: 内容,默认为空字符串
  • + *
  • DATA1-5: 通用数据字段,用于存储不同类型的数据
  • + *
+ *

+ */ private static final String CREATE_DATA_TABLE_SQL = "CREATE TABLE " + TABLE.DATA + "(" + DataColumns.ID + " INTEGER PRIMARY KEY," + @@ -78,6 +186,12 @@ public class NotesDatabaseHelper extends SQLiteOpenHelper { DataColumns.DATA5 + " TEXT NOT NULL DEFAULT ''" + ")"; + /** + * 创建数据表索引的SQL语句 + *

+ * 在data表的NOTE_ID字段上创建索引,提高按笔记ID查询数据的效率。 + *

+ */ private static final String CREATE_DATA_NOTE_ID_INDEX_SQL = "CREATE INDEX IF NOT EXISTS note_id_index ON " + TABLE.DATA + "(" + DataColumns.NOTE_ID + ");"; @@ -206,10 +320,23 @@ public class NotesDatabaseHelper extends SQLiteOpenHelper { " WHERE " + NoteColumns.PARENT_ID + "=old." + NoteColumns.ID + ";" + " END"; + /** + * 构造器 + * + * @param context 应用上下文 + */ public NotesDatabaseHelper(Context context) { super(context, DB_NAME, null, DB_VERSION); } + /** + * 创建笔记表 + *

+ * 执行创建note表的SQL语句,创建相关触发器,并初始化系统文件夹。 + *

+ * + * @param db SQLiteDatabase实例 + */ public void createNoteTable(SQLiteDatabase db) { db.execSQL(CREATE_NOTE_TABLE_SQL); reCreateNoteTableTriggers(db); @@ -217,7 +344,17 @@ public class NotesDatabaseHelper extends SQLiteOpenHelper { Log.d(TAG, "note table has been created"); } + /** + * 重新创建笔记表触发器 + *

+ * 先删除所有已存在的note表相关触发器,然后重新创建所有触发器。 + * 用于在数据库升级时更新触发器逻辑。 + *

+ * + * @param db SQLiteDatabase实例 + */ 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"); @@ -226,6 +363,7 @@ public class NotesDatabaseHelper extends SQLiteOpenHelper { 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_FOLDER_COUNT_ON_DELETE_TRIGGER); @@ -235,12 +373,27 @@ public class NotesDatabaseHelper extends SQLiteOpenHelper { db.execSQL(FOLDER_MOVE_NOTES_ON_TRASH_TRIGGER); } + /** + * 创建系统文件夹 + *

+ * 在note表中创建四个系统文件夹: + *

    + *
  • 通话记录文件夹(ID_CALL_RECORD_FOLDER)
  • + *
  • 根文件夹(ID_ROOT_FOLDER)
  • + *
  • 临时文件夹(ID_TEMPARAY_FOLDER)
  • + *
  • 回收站文件夹(ID_TRASH_FOLER)
  • + *
+ *

+ * + * @param db SQLiteDatabase实例 + */ private void createSystemFolder(SQLiteDatabase db) { ContentValues values = new ContentValues(); /** * call record foler for call notes */ + // 创建通话记录文件夹 values.put(NoteColumns.ID, Notes.ID_CALL_RECORD_FOLDER); values.put(NoteColumns.TYPE, Notes.TYPE_SYSTEM); db.insert(TABLE.NOTE, null, values); @@ -248,6 +401,7 @@ public class NotesDatabaseHelper extends SQLiteOpenHelper { /** * root folder which is default folder */ + // 创建根文件夹(默认文件夹) values.clear(); values.put(NoteColumns.ID, Notes.ID_ROOT_FOLDER); values.put(NoteColumns.TYPE, Notes.TYPE_SYSTEM); @@ -256,6 +410,7 @@ public class NotesDatabaseHelper extends SQLiteOpenHelper { /** * temporary folder which is used for moving note */ + // 创建临时文件夹(用于移动笔记) values.clear(); values.put(NoteColumns.ID, Notes.ID_TEMPARAY_FOLDER); values.put(NoteColumns.TYPE, Notes.TYPE_SYSTEM); @@ -264,12 +419,21 @@ public class NotesDatabaseHelper extends SQLiteOpenHelper { /** * create trash folder */ + // 创建回收站文件夹 values.clear(); values.put(NoteColumns.ID, Notes.ID_TRASH_FOLER); values.put(NoteColumns.TYPE, Notes.TYPE_SYSTEM); db.insert(TABLE.NOTE, null, values); } + /** + * 创建数据表 + *

+ * 执行创建data表的SQL语句,创建相关触发器,并创建索引。 + *

+ * + * @param db SQLiteDatabase实例 + */ public void createDataTable(SQLiteDatabase db) { db.execSQL(CREATE_DATA_TABLE_SQL); reCreateDataTableTriggers(db); @@ -277,16 +441,36 @@ public class NotesDatabaseHelper extends SQLiteOpenHelper { Log.d(TAG, "data table has been created"); } + /** + * 重新创建数据表触发器 + *

+ * 先删除所有已存在的data表相关触发器,然后重新创建所有触发器。 + * 用于在数据库升级时更新触发器逻辑。 + *

+ * + * @param db SQLiteDatabase实例 + */ 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); } + /** + * 获取数据库帮助类单例实例 + *

+ * 使用双重检查锁定模式确保线程安全的单例实现。 + *

+ * + * @param context 应用上下文 + * @return NotesDatabaseHelper单例实例 + */ static synchronized NotesDatabaseHelper getInstance(Context context) { if (mInstance == null) { mInstance = new NotesDatabaseHelper(context); @@ -294,45 +478,78 @@ public class NotesDatabaseHelper extends SQLiteOpenHelper { return mInstance; } + /** + * 创建数据库 + *

+ * 当数据库文件不存在时调用,创建note表和data表。 + *

+ * + * @param db SQLiteDatabase实例 + */ @Override public void onCreate(SQLiteDatabase db) { createNoteTable(db); createDataTable(db); } + /** + * 升级数据库 + *

+ * 当数据库版本号增加时调用,执行从旧版本到新版本的升级逻辑。 + * 支持增量升级,从当前版本逐步升级到目标版本。 + *

+ * + * @param db SQLiteDatabase实例 + * @param oldVersion 当前数据库版本号 + * @param newVersion 目标数据库版本号 + * @throws IllegalStateException 如果升级失败 + */ @Override public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { boolean reCreateTriggers = false; boolean skipV2 = false; + // 从V1升级到V2(包括V2到V3) if (oldVersion == 1) { upgradeToV2(db); skipV2 = true; // this upgrade including the upgrade from v2 to v3 oldVersion++; } + // 从V2升级到V3 if (oldVersion == 2 && !skipV2) { upgradeToV3(db); reCreateTriggers = true; oldVersion++; } + // 从V3升级到V4 if (oldVersion == 3) { upgradeToV4(db); oldVersion++; } + // 如果需要,重新创建触发器 if (reCreateTriggers) { reCreateNoteTableTriggers(db); reCreateDataTableTriggers(db); } + // 检查升级是否成功 if (oldVersion != newVersion) { throw new IllegalStateException("Upgrade notes database to version " + newVersion + "fails"); } } + /** + * 升级数据库到V2版本 + *

+ * 删除旧表并重新创建note表和data表。 + *

+ * + * @param db SQLiteDatabase实例 + */ private void upgradeToV2(SQLiteDatabase db) { db.execSQL("DROP TABLE IF EXISTS " + TABLE.NOTE); db.execSQL("DROP TABLE IF EXISTS " + TABLE.DATA); @@ -340,21 +557,40 @@ public class NotesDatabaseHelper extends SQLiteOpenHelper { createDataTable(db); } + /** + * 升级数据库到V3版本 + *

+ * 添加GTASK_ID列到note表,并创建回收站系统文件夹。 + *

+ * + * @param db SQLiteDatabase实例 + */ private void upgradeToV3(SQLiteDatabase db) { // drop unused triggers + // 删除未使用的触发器 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"); // add a column for gtask id + // 添加GTASK_ID列 db.execSQL("ALTER TABLE " + TABLE.NOTE + " ADD COLUMN " + NoteColumns.GTASK_ID + " TEXT NOT NULL DEFAULT ''"); // add a trash system folder + // 添加回收站系统文件夹 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); } + /** + * 升级数据库到V4版本 + *

+ * 添加VERSION列到note表,用于跟踪笔记版本。 + *

+ * + * @param db SQLiteDatabase实例 + */ private void upgradeToV4(SQLiteDatabase db) { db.execSQL("ALTER TABLE " + TABLE.NOTE + " ADD COLUMN " + NoteColumns.VERSION + " INTEGER NOT NULL DEFAULT 0"); diff --git a/src/Notes-master/src/net/micode/notes/data/NotesProvider.java b/src/Notes-master/src/net/micode/notes/data/NotesProvider.java index edb0a60..aa2cf34 100644 --- a/src/Notes-master/src/net/micode/notes/data/NotesProvider.java +++ b/src/Notes-master/src/net/micode/notes/data/NotesProvider.java @@ -35,21 +35,97 @@ import net.micode.notes.data.Notes.NoteColumns; import net.micode.notes.data.NotesDatabaseHelper.TABLE; +/** + * 笔记Content Provider + *

+ * 继承自ContentProvider,提供对笔记数据的增删改查(CRUD)操作。 + * 管理note表和data表的数据访问,支持URI匹配、数据查询、插入、更新和删除操作。 + * 同时提供搜索建议功能,支持全局搜索笔记内容。 + *

+ *

+ * 主要功能: + *

    + *
  • URI路由匹配,支持多种URI模式
  • + *
  • 笔记和数据的查询操作
  • + *
  • 笔记和数据的插入操作
  • + *
  • 笔记和数据的更新操作
  • + *
  • 笔记和数据的删除操作
  • + *
  • 全局搜索和搜索建议功能
  • + *
  • 数据变更通知
  • + *
  • 笔记版本号自动递增
  • + *
+ *

+ *

+ * 支持的URI模式: + *

    + *
  • content://micode_notes/note - 查询所有笔记
  • + *
  • content://micode_notes/note/# - 查询指定ID的笔记
  • + *
  • content://micode_notes/data - 查询所有数据
  • + *
  • content://micode_notes/data/# - 查询指定ID的数据
  • + *
  • content://micode_notes/search - 搜索笔记
  • + *
  • content://micode_notes/search_suggest_query - 搜索建议
  • + *
+ *

+ * + * @see ContentProvider + * @see NotesDatabaseHelper + * @see Notes + */ public class NotesProvider extends ContentProvider { + /** + * URI匹配器 + *

+ * 用于匹配不同的URI模式,将请求路由到对应的处理逻辑。 + * 支持笔记、数据、搜索等多种URI模式。 + *

+ */ private static final UriMatcher mMatcher; + /** + * 数据库帮助类实例 + *

+ * 用于获取可读和可写的SQLiteDatabase实例。 + *

+ */ private NotesDatabaseHelper mHelper; + /** + * 日志标签 + */ private static final String TAG = "NotesProvider"; + /** + * 笔记URI匹配码 + */ private static final int URI_NOTE = 1; + /** + * 笔记项URI匹配码 + */ private static final int URI_NOTE_ITEM = 2; + /** + * 数据URI匹配码 + */ private static final int URI_DATA = 3; + /** + * 数据项URI匹配码 + */ private static final int URI_DATA_ITEM = 4; + /** + * 搜索URI匹配码 + */ private static final int URI_SEARCH = 5; + /** + * 搜索建议URI匹配码 + */ private static final int URI_SEARCH_SUGGEST = 6; + /** + * URI匹配器初始化块 + *

+ * 初始化UriMatcher,注册所有支持的URI模式。 + *

+ */ static { mMatcher = new UriMatcher(UriMatcher.NO_MATCH); mMatcher.addURI(Notes.AUTHORITY, "note", URI_NOTE); @@ -62,8 +138,15 @@ public class NotesProvider extends ContentProvider { } /** - * x'0A' represents the '\n' character in sqlite. For title and content in the search result, - * we will trim '\n' and white space in order to show more information. + * 搜索结果投影 + *

+ * 定义搜索建议返回的列,包括笔记ID、文本内容、图标、Intent动作等。 + * 使用TRIM和REPLACE函数去除换行符和空白字符,以便更好地显示搜索结果。 + *

+ *

+ * x'0A'代表SQLite中的换行符'\n'。对于搜索结果中的标题和内容, + * 我们会去除换行符和空白字符,以显示更多信息。 + *

*/ private static final String NOTES_SEARCH_PROJECTION = NoteColumns.ID + "," + NoteColumns.ID + " AS " + SearchManager.SUGGEST_COLUMN_INTENT_EXTRA_DATA + "," @@ -73,18 +156,49 @@ public class NotesProvider extends ContentProvider { + "'" + Intent.ACTION_VIEW + "' AS " + SearchManager.SUGGEST_COLUMN_INTENT_ACTION + "," + "'" + Notes.TextNote.CONTENT_TYPE + "' AS " + SearchManager.SUGGEST_COLUMN_INTENT_DATA; + /** + * 笔记摘要搜索查询SQL语句 + *

+ * 搜索note表中SNIPPET字段包含指定关键词的笔记。 + * 排除回收站中的笔记(PARENT_ID不等于ID_TRASH_FOLER)。 + * 只搜索普通笔记(TYPE等于TYPE_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; + /** + * 创建Content Provider + *

+ * 初始化数据库帮助类实例。 + *

+ * + * @return true表示创建成功 + */ @Override public boolean onCreate() { mHelper = NotesDatabaseHelper.getInstance(getContext()); return true; } + /** + * 查询数据 + *

+ * 根据URI模式查询对应的数据表,支持笔记、数据、搜索等多种查询模式。 + * 对于搜索模式,使用LIKE模糊匹配查询笔记摘要。 + *

+ * + * @param uri 查询的URI + * @param projection 要查询的列数组 + * @param selection 查询条件 + * @param selectionArgs 查询条件参数 + * @param sortOrder 排序方式 + * @return 查询结果的Cursor对象 + * @throws IllegalArgumentException 如果URI模式不支持或搜索时指定了不允许的参数 + */ @Override public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) { @@ -93,25 +207,30 @@ public class NotesProvider extends ContentProvider { String id = null; switch (mMatcher.match(uri)) { case URI_NOTE: + // 查询所有笔记 c = db.query(TABLE.NOTE, projection, selection, selectionArgs, null, null, sortOrder); break; case URI_NOTE_ITEM: + // 查询指定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: + // 查询所有数据 c = db.query(TABLE.DATA, projection, selection, selectionArgs, null, null, sortOrder); break; case URI_DATA_ITEM: + // 查询指定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: + // 搜索笔记或搜索建议 if (sortOrder != null || projection != null) { throw new IllegalArgumentException( "do not specify sortOrder, selection, selectionArgs, or projection" + "with this query"); @@ -119,18 +238,22 @@ public class NotesProvider extends ContentProvider { String searchString = null; if (mMatcher.match(uri) == URI_SEARCH_SUGGEST) { + // 从URI路径中获取搜索关键词 if (uri.getPathSegments().size() > 1) { searchString = uri.getPathSegments().get(1); } } else { + // 从查询参数中获取搜索关键词 searchString = uri.getQueryParameter("pattern"); } + // 搜索关键词为空时返回null if (TextUtils.isEmpty(searchString)) { return null; } try { + // 使用模糊匹配搜索笔记摘要 searchString = String.format("%%%s%%", searchString); c = db.rawQuery(NOTES_SNIPPET_SEARCH_QUERY, new String[] { searchString }); @@ -141,21 +264,36 @@ public class NotesProvider extends ContentProvider { default: throw new IllegalArgumentException("Unknown URI " + uri); } + // 设置通知URI,当数据变更时通知观察者 if (c != null) { c.setNotificationUri(getContext().getContentResolver(), uri); } return c; } + /** + * 插入数据 + *

+ * 根据URI模式向对应的数据表插入数据,支持笔记和数据的插入。 + * 插入成功后通知相关URI的观察者。 + *

+ * + * @param uri 插入数据的URI + * @param values 要插入的数据值 + * @return 插入数据的URI(包含新增记录的ID) + * @throws IllegalArgumentException 如果URI模式不支持 + */ @Override public Uri insert(Uri uri, ContentValues values) { SQLiteDatabase db = mHelper.getWritableDatabase(); long dataId = 0, noteId = 0, insertedId = 0; switch (mMatcher.match(uri)) { case URI_NOTE: + // 插入笔记 insertedId = noteId = db.insert(TABLE.NOTE, null, values); break; case URI_DATA: + // 插入数据 if (values.containsKey(DataColumns.NOTE_ID)) { noteId = values.getAsLong(DataColumns.NOTE_ID); } else { @@ -167,12 +305,14 @@ public class NotesProvider extends ContentProvider { throw new IllegalArgumentException("Unknown URI " + uri); } // Notify the note uri + // 通知笔记URI的观察者 if (noteId > 0) { getContext().getContentResolver().notifyChange( ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, noteId), null); } // Notify the data uri + // 通知数据URI的观察者 if (dataId > 0) { getContext().getContentResolver().notifyChange( ContentUris.withAppendedId(Notes.CONTENT_DATA_URI, dataId), null); @@ -181,6 +321,20 @@ public class NotesProvider extends ContentProvider { return ContentUris.withAppendedId(uri, insertedId); } + /** + * 删除数据 + *

+ * 根据URI模式删除对应的数据表中的数据,支持笔记和数据的删除。 + * 删除笔记时,不允许删除系统文件夹(ID小于等于0)。 + * 删除成功后通知相关URI的观察者。 + *

+ * + * @param uri 删除数据的URI + * @param selection 删除条件 + * @param selectionArgs 删除条件参数 + * @return 删除的记录数 + * @throws IllegalArgumentException 如果URI模式不支持 + */ @Override public int delete(Uri uri, String selection, String[] selectionArgs) { int count = 0; @@ -189,14 +343,17 @@ public class NotesProvider extends ContentProvider { boolean deleteData = false; switch (mMatcher.match(uri)) { case URI_NOTE: + // 删除笔记(排除系统文件夹) selection = "(" + selection + ") AND " + NoteColumns.ID + ">0 "; count = db.delete(TABLE.NOTE, selection, selectionArgs); break; case URI_NOTE_ITEM: + // 删除指定ID的笔记 id = uri.getPathSegments().get(1); /** * ID that smaller than 0 is system folder which is not allowed to * trash + * ID小于等于0的是系统文件夹,不允许删除 */ long noteId = Long.valueOf(id); if (noteId <= 0) { @@ -206,10 +363,12 @@ public class NotesProvider extends ContentProvider { NoteColumns.ID + "=" + id + parseSelection(selection), selectionArgs); break; case URI_DATA: + // 删除数据 count = db.delete(TABLE.DATA, selection, selectionArgs); deleteData = true; break; case URI_DATA_ITEM: + // 删除指定ID的数据 id = uri.getPathSegments().get(1); count = db.delete(TABLE.DATA, DataColumns.ID + "=" + id + parseSelection(selection), selectionArgs); @@ -218,8 +377,10 @@ public class NotesProvider extends ContentProvider { default: throw new IllegalArgumentException("Unknown URI " + uri); } + // 删除成功后通知观察者 if (count > 0) { if (deleteData) { + // 删除数据时通知笔记URI getContext().getContentResolver().notifyChange(Notes.CONTENT_NOTE_URI, null); } getContext().getContentResolver().notifyChange(uri, null); @@ -227,6 +388,21 @@ public class NotesProvider extends ContentProvider { return count; } + /** + * 更新数据 + *

+ * 根据URI模式更新对应的数据表中的数据,支持笔记和数据的更新。 + * 更新笔记时自动递增笔记的版本号。 + * 更新成功后通知相关URI的观察者。 + *

+ * + * @param uri 更新数据的URI + * @param values 要更新的数据值 + * @param selection 更新条件 + * @param selectionArgs 更新条件参数 + * @return 更新的记录数 + * @throws IllegalArgumentException 如果URI模式不支持 + */ @Override public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) { int count = 0; @@ -235,20 +411,24 @@ public class NotesProvider extends ContentProvider { boolean updateData = false; switch (mMatcher.match(uri)) { case URI_NOTE: + // 更新笔记(递增版本号) increaseNoteVersion(-1, selection, selectionArgs); count = db.update(TABLE.NOTE, values, selection, selectionArgs); break; case URI_NOTE_ITEM: + // 更新指定ID的笔记(递增版本号) 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: + // 更新数据 count = db.update(TABLE.DATA, values, selection, selectionArgs); updateData = true; break; case URI_DATA_ITEM: + // 更新指定ID的数据 id = uri.getPathSegments().get(1); count = db.update(TABLE.DATA, values, DataColumns.ID + "=" + id + parseSelection(selection), selectionArgs); @@ -258,8 +438,10 @@ public class NotesProvider extends ContentProvider { throw new IllegalArgumentException("Unknown URI " + uri); } + // 更新成功后通知观察者 if (count > 0) { if (updateData) { + // 更新数据时通知笔记URI getContext().getContentResolver().notifyChange(Notes.CONTENT_NOTE_URI, null); } getContext().getContentResolver().notifyChange(uri, null); @@ -267,10 +449,30 @@ public class NotesProvider extends ContentProvider { return count; } + /** + * 解析查询条件 + *

+ * 将查询条件与ID条件组合,用于构建完整的SQL WHERE子句。 + *

+ * + * @param selection 原始查询条件 + * @return 组合后的查询条件字符串 + */ private String parseSelection(String selection) { return (!TextUtils.isEmpty(selection) ? " AND (" + selection + ')' : ""); } + /** + * 递增笔记版本号 + *

+ * 更新指定笔记的VERSION字段,使其值加1。 + * 用于跟踪笔记的修改历史,支持同步功能。 + *

+ * + * @param id 笔记ID,如果小于等于0则使用selection条件 + * @param selection 查询条件 + * @param selectionArgs 查询条件参数 + */ private void increaseNoteVersion(long id, String selection, String[] selectionArgs) { StringBuilder sql = new StringBuilder(120); sql.append("UPDATE "); @@ -279,6 +481,7 @@ public class NotesProvider extends ContentProvider { sql.append(NoteColumns.VERSION); sql.append("=" + NoteColumns.VERSION + "+1 "); + // 构建WHERE子句 if (id > 0 || !TextUtils.isEmpty(selection)) { sql.append(" WHERE "); } @@ -287,6 +490,7 @@ public class NotesProvider extends ContentProvider { } if (!TextUtils.isEmpty(selection)) { String selectString = id > 0 ? parseSelection(selection) : selection; + // 替换查询条件中的占位符 for (String args : selectionArgs) { selectString = selectString.replaceFirst("\\?", args); } @@ -296,10 +500,18 @@ public class NotesProvider extends ContentProvider { mHelper.getWritableDatabase().execSQL(sql.toString()); } + /** + * 获取数据MIME类型 + *

+ * 返回指定URI对应的数据MIME类型。 + *

+ * + * @param uri 数据URI + * @return MIME类型字符串 + */ @Override public String getType(Uri uri) { // TODO Auto-generated method stub return null; } - } diff --git a/src/Notes-master/src/net/micode/notes/gtask/data/MetaData.java b/src/Notes-master/src/net/micode/notes/gtask/data/MetaData.java index 3a2050b..28f6294 100644 --- a/src/Notes-master/src/net/micode/notes/gtask/data/MetaData.java +++ b/src/Notes-master/src/net/micode/notes/gtask/data/MetaData.java @@ -25,36 +25,86 @@ import org.json.JSONException; import org.json.JSONObject; +/** + * Google Tasks 元数据类 + *

+ * 继承自 Task,用于存储和管理 Google Tasks 同步的元数据信息。 + * 元数据以特殊任务的形式存储在 Google Tasks 中,用于关联本地笔记和远程任务的对应关系。 + * 该类不应通过本地 JSON 或数据库游标进行操作,仅用于远程同步场景。 + *

+ */ public class MetaData extends Task { + /** + * 日志标签 + */ private final static String TAG = MetaData.class.getSimpleName(); + /** + * 关联的 Google Tasks ID + */ private String mRelatedGid = null; + /** + * 设置元数据信息 + *

+ * 将 Google Tasks ID 添加到元信息 JSON 对象中,并设置任务名称为元数据专用名称。 + * 元信息以 JSON 字符串形式存储在任务的 notes 字段中。 + *

+ * + * @param gid 关联的 Google Tasks ID + * @param metaInfo 元信息 JSON 对象 + */ public void setMeta(String gid, JSONObject metaInfo) { try { + // 将关联的 GID 添加到元信息中 metaInfo.put(GTaskStringUtils.META_HEAD_GTASK_ID, gid); } catch (JSONException e) { Log.e(TAG, "failed to put related gid"); } + // 将元信息转换为字符串并设置为任务备注 setNotes(metaInfo.toString()); + // 设置为元数据专用名称 setName(GTaskStringUtils.META_NOTE_NAME); } + /** + * 获取关联的 Google Tasks ID + * + * @return 关联的 Google Tasks ID,如果未设置则返回 null + */ public String getRelatedGid() { return mRelatedGid; } + /** + * 判断是否值得保存 + *

+ * 只有当 notes 字段不为空时才值得保存,因为元数据信息存储在 notes 中。 + *

+ * + * @return 如果 notes 不为 null 返回 true,否则返回 false + */ @Override public boolean isWorthSaving() { return getNotes() != null; } + /** + * 根据远程 JSON 设置内容 + *

+ * 从远程服务器返回的 JSON 对象中解析元数据信息,提取关联的 Google Tasks ID。 + *

+ * + * @param js 远程服务器返回的 JSON 对象 + */ @Override public void setContentByRemoteJSON(JSONObject js) { super.setContentByRemoteJSON(js); if (getNotes() != null) { try { + // 从 notes 字段中解析元信息 JSON JSONObject metaInfo = new JSONObject(getNotes().trim()); + // 提取关联的 GID mRelatedGid = metaInfo.getString(GTaskStringUtils.META_HEAD_GTASK_ID); } catch (JSONException e) { Log.w(TAG, "failed to get related gid"); @@ -63,17 +113,45 @@ public class MetaData extends Task { } } + /** + * 根据本地 JSON 设置内容 + *

+ * 此方法不应被调用,因为元数据不通过本地 JSON 进行操作。 + *

+ * + * @param js 本地 JSON 对象 + * @throws IllegalAccessError 总是抛出此异常,表示不应调用此方法 + */ @Override public void setContentByLocalJSON(JSONObject js) { // this function should not be called throw new IllegalAccessError("MetaData:setContentByLocalJSON should not be called"); } + /** + * 从内容生成本地 JSON 对象 + *

+ * 此方法不应被调用,因为元数据不通过本地 JSON 进行操作。 + *

+ * + * @return 无返回值,总是抛出异常 + * @throws IllegalAccessError 总是抛出此异常,表示不应调用此方法 + */ @Override public JSONObject getLocalJSONFromContent() { throw new IllegalAccessError("MetaData:getLocalJSONFromContent should not be called"); } + /** + * 根据数据库游标获取同步动作 + *

+ * 此方法不应被调用,因为元数据不通过数据库游标进行操作。 + *

+ * + * @param c 数据库游标 + * @return 无返回值,总是抛出异常 + * @throws IllegalAccessError 总是抛出此异常,表示不应调用此方法 + */ @Override public int getSyncAction(Cursor c) { throw new IllegalAccessError("MetaData:getSyncAction should not be called"); diff --git a/src/Notes-master/src/net/micode/notes/gtask/data/Node.java b/src/Notes-master/src/net/micode/notes/gtask/data/Node.java index 63950e0..ad7e431 100644 --- a/src/Notes-master/src/net/micode/notes/gtask/data/Node.java +++ b/src/Notes-master/src/net/micode/notes/gtask/data/Node.java @@ -20,33 +20,86 @@ import android.database.Cursor; import org.json.JSONObject; +/** + * Google Tasks 同步节点抽象基类 + *

+ * 定义所有可同步数据模型(Task、TaskList、MetaData)的公共属性和抽象方法。 + * 负责管理同步状态、Google ID、名称、最后修改时间和删除标记等通用属性。 + * 子类需要实现具体的 JSON 转换和同步动作生成逻辑。 + *

+ */ 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; + /** + * Google Tasks ID,用于唯一标识远程任务 + */ private String mGid; + /** + * 节点名称 + */ private String mName; + /** + * 最后修改时间(时间戳) + */ private long mLastModified; + /** + * 删除标记,true 表示已删除 + */ private boolean mDeleted; + /** + * 构造一个新的节点实例 + *

+ * 初始化所有属性为默认值:GID 为 null,名称为空字符串,最后修改时间为 0,删除标记为 false。 + *

+ */ public Node() { mGid = null; mName = ""; @@ -54,46 +107,137 @@ public abstract class Node { mDeleted = false; } + /** + * 获取创建动作的 JSON 对象 + *

+ * 根据指定的动作 ID 生成用于在远程服务器创建节点的 JSON 请求。 + *

+ * + * @param actionId 动作 ID,标识具体的创建操作类型 + * @return 包含创建动作信息的 JSON 对象 + */ public abstract JSONObject getCreateAction(int actionId); + /** + * 获取更新动作的 JSON 对象 + *

+ * 根据指定的动作 ID 生成用于在远程服务器更新节点的 JSON 请求。 + *

+ * + * @param actionId 动作 ID,标识具体的更新操作类型 + * @return 包含更新动作信息的 JSON 对象 + */ public abstract JSONObject getUpdateAction(int actionId); + /** + * 根据远程 JSON 设置节点内容 + *

+ * 从远程服务器返回的 JSON 对象中解析并设置节点的属性值。 + *

+ * + * @param js 远程服务器返回的 JSON 对象 + */ public abstract void setContentByRemoteJSON(JSONObject js); + /** + * 根据本地 JSON 设置节点内容 + *

+ * 从本地数据库存储的 JSON 对象中解析并设置节点的属性值。 + *

+ * + * @param js 本地数据库存储的 JSON 对象 + */ public abstract void setContentByLocalJSON(JSONObject js); + /** + * 从节点内容生成本地 JSON 对象 + *

+ * 将节点的当前属性值转换为 JSON 对象,用于存储到本地数据库。 + *

+ * + * @return 包含节点内容的 JSON 对象 + */ public abstract JSONObject getLocalJSONFromContent(); + /** + * 根据数据库游标获取同步动作 + *

+ * 比较本地数据库中的数据与当前节点状态,确定需要执行的同步动作类型。 + *

+ * + * @param c 指向本地数据库记录的游标 + * @return 同步动作类型,取值为 SYNC_ACTION_* 常量之一 + */ public abstract int getSyncAction(Cursor c); + /** + * 设置 Google Tasks ID + * + * @param gid Google Tasks ID,用于唯一标识远程任务 + */ public void setGid(String gid) { this.mGid = gid; } + /** + * 设置节点名称 + * + * @param name 节点名称 + */ public void setName(String name) { this.mName = name; } + /** + * 设置最后修改时间 + * + * @param lastModified 最后修改时间(时间戳) + */ public void setLastModified(long lastModified) { this.mLastModified = lastModified; } + /** + * 设置删除标记 + * + * @param deleted 删除标记,true 表示已删除 + */ public void setDeleted(boolean deleted) { this.mDeleted = deleted; } + /** + * 获取 Google Tasks ID + * + * @return Google Tasks ID,如果未设置则返回 null + */ public String getGid() { return this.mGid; } + /** + * 获取节点名称 + * + * @return 节点名称 + */ public String getName() { return this.mName; } + /** + * 获取最后修改时间 + * + * @return 最后修改时间(时间戳) + */ public long getLastModified() { return this.mLastModified; } + /** + * 获取删除标记 + * + * @return 删除标记,true 表示已删除 + */ public boolean getDeleted() { return this.mDeleted; } diff --git a/src/Notes-master/src/net/micode/notes/gtask/data/SqlData.java b/src/Notes-master/src/net/micode/notes/gtask/data/SqlData.java index d3ec3be..174560e 100644 --- a/src/Notes-master/src/net/micode/notes/gtask/data/SqlData.java +++ b/src/Notes-master/src/net/micode/notes/gtask/data/SqlData.java @@ -35,24 +35,39 @@ import org.json.JSONException; import org.json.JSONObject; +/** + * SQLite 数据内容类 + *

+ * 表示笔记的一条数据记录,存储笔记的具体内容信息。 + * 每条数据记录包含 MIME 类型、内容文本和扩展数据字段。 + * 支持从 JSON 对象加载内容或将内容导出为 JSON,用于与 Google Tasks 的数据同步。 + *

+ */ public class SqlData { private static final String TAG = SqlData.class.getSimpleName(); + /** 无效 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 类型字段在投影数组中的索引 */ public static final int DATA_MIME_TYPE_COLUMN = 1; + /** 内容字段在投影数组中的索引 */ public static final int DATA_CONTENT_COLUMN = 2; + /** 扩展数据 1 字段在投影数组中的索引 */ public static final int DATA_CONTENT_DATA_1_COLUMN = 3; + /** 扩展数据 3 字段在投影数组中的索引 */ public static final int DATA_CONTENT_DATA_3_COLUMN = 4; private ContentResolver mContentResolver; @@ -71,6 +86,15 @@ public class SqlData { private ContentValues mDiffDataValues; + /** + * 构造一个新建的数据对象 + *

+ * 创建一个尚未保存到数据库的新数据记录,初始化所有字段为默认值。 + * 标记为创建状态,后续调用 commit 方法时会执行插入操作。 + *

+ * + * @param context 上下文对象,用于获取 ContentResolver + */ public SqlData(Context context) { mContentResolver = context.getContentResolver(); mIsCreate = true; @@ -82,6 +106,16 @@ public class SqlData { mDiffDataValues = new ContentValues(); } + /** + * 从数据库游标构造数据对象 + *

+ * 从游标中读取数据记录并初始化对象。 + * 标记为非创建状态,后续调用 commit 方法时会执行更新操作。 + *

+ * + * @param context 上下文对象 + * @param c 指向数据记录的数据库游标 + */ public SqlData(Context context, Cursor c) { mContentResolver = context.getContentResolver(); mIsCreate = false; @@ -89,6 +123,14 @@ public class SqlData { mDiffDataValues = new ContentValues(); } + /** + * 从数据库游标加载数据内容 + *

+ * 从游标的当前行读取所有数据字段值并初始化对象的成员变量。 + *

+ * + * @param c 指向数据记录的数据库游标 + */ private void loadFromCursor(Cursor c) { mDataId = c.getLong(DATA_ID_COLUMN); mDataMimeType = c.getString(DATA_MIME_TYPE_COLUMN); @@ -97,6 +139,16 @@ public class SqlData { mDataContentData3 = c.getString(DATA_CONTENT_DATA_3_COLUMN); } + /** + * 从 JSON 对象设置数据内容 + *

+ * 解析 JSON 对象中的数据字段,更新当前对象的成员变量。 + * 比较新旧值,将变更记录到差异值集合中。 + *

+ * + * @param js 包含数据信息的 JSON 对象 + * @throws JSONException 如果 JSON 解析失败 + */ public void setContent(JSONObject js) throws JSONException { long dataId = js.has(DataColumns.ID) ? js.getLong(DataColumns.ID) : INVALID_ID; if (mIsCreate || mDataId != dataId) { @@ -130,6 +182,15 @@ public class SqlData { mDataContentData3 = dataContentData3; } + /** + * 获取数据内容的 JSON 对象 + *

+ * 将当前数据的所有字段导出为 JSON 对象格式。 + *

+ * + * @return 包含数据信息的 JSON 对象,如果尚未创建到数据库则返回 null + * @throws JSONException 如果 JSON 生成失败 + */ public JSONObject getContent() throws JSONException { if (mIsCreate) { Log.e(TAG, "it seems that we haven't created this in database yet"); @@ -144,6 +205,19 @@ public class SqlData { return js; } + /** + * 提交数据变更到数据库 + *

+ * 根据当前状态执行插入或更新操作: + * - 如果是新建数据,插入新记录并获取生成的 ID + * - 如果是已存在的数据,更新变更的字段 + *

+ * + * @param noteId 关联的笔记 ID + * @param validateVersion 是否验证版本号,为 true 时仅更新版本号匹配的记录 + * @param version 笔记的版本号,用于版本验证 + * @throws ActionFailureException 如果创建数据失败 + */ public void commit(long noteId, boolean validateVersion, long version) { if (mIsCreate) { @@ -166,6 +240,7 @@ public class SqlData { 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 @@ -183,6 +258,11 @@ public class SqlData { mIsCreate = false; } + /** + * 获取数据 ID + * + * @return 数据在数据库中的 ID + */ public long getId() { return mDataId; } diff --git a/src/Notes-master/src/net/micode/notes/gtask/data/SqlNote.java b/src/Notes-master/src/net/micode/notes/gtask/data/SqlNote.java index 79a4095..3355141 100644 --- a/src/Notes-master/src/net/micode/notes/gtask/data/SqlNote.java +++ b/src/Notes-master/src/net/micode/notes/gtask/data/SqlNote.java @@ -38,11 +38,21 @@ import org.json.JSONObject; import java.util.ArrayList; +/** + * SQLite 笔记数据类 + *

+ * 表示本地数据库中的一条笔记记录,负责笔记数据的增删改查操作。 + * 支持与 Google Tasks 的双向同步,能够从 JSON 对象加载内容或将内容导出为 JSON。 + * 区分普通笔记、文件夹和系统文件夹三种类型,提供版本控制和本地修改标记功能。 + *

+ */ public class SqlNote { private static final String TAG = SqlNote.class.getSimpleName(); + /** 无效 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, @@ -52,38 +62,55 @@ public class SqlNote { 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; + /** Widget ID 字段在投影数组中的索引 */ public static final int WIDGET_ID_COLUMN = 10; + /** Widget 类型字段在投影数组中的索引 */ 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; + /** Google Tasks ID 字段在投影数组中的索引 */ public static final int GTASK_ID_COLUMN = 15; + /** 版本号字段在投影数组中的索引 */ public static final int VERSION_COLUMN = 16; private Context mContext; @@ -122,6 +149,15 @@ public class SqlNote { private ArrayList mDataList; + /** + * 构造一个新建的笔记对象 + *

+ * 创建一个尚未保存到数据库的新笔记,初始化所有字段为默认值。 + * 标记为创建状态,后续调用 commit 方法时会执行插入操作。 + *

+ * + * @param context 上下文对象,用于获取 ContentResolver 和默认资源 + */ public SqlNote(Context context) { mContext = context; mContentResolver = context.getContentResolver(); @@ -143,6 +179,16 @@ public class SqlNote { mDataList = new ArrayList(); } + /** + * 从数据库游标构造笔记对象 + *

+ * 从游标中读取笔记数据并初始化对象,如果笔记类型为普通笔记则加载其数据内容。 + * 标记为非创建状态,后续调用 commit 方法时会执行更新操作。 + *

+ * + * @param context 上下文对象 + * @param c 指向笔记记录的数据库游标 + */ public SqlNote(Context context, Cursor c) { mContext = context; mContentResolver = context.getContentResolver(); @@ -154,6 +200,16 @@ public class SqlNote { mDiffNoteValues = new ContentValues(); } + /** + * 从数据库 ID 构造笔记对象 + *

+ * 根据笔记 ID 从数据库查询记录并初始化对象,如果笔记类型为普通笔记则加载其数据内容。 + * 标记为非创建状态,后续调用 commit 方法时会执行更新操作。 + *

+ * + * @param context 上下文对象 + * @param id 笔记在数据库中的 ID + */ public SqlNote(Context context, long id) { mContext = context; mContentResolver = context.getContentResolver(); @@ -166,6 +222,14 @@ public class SqlNote { } + /** + * 从数据库 ID 加载笔记数据 + *

+ * 根据笔记 ID 查询数据库获取笔记记录,并调用 loadFromCursor(Cursor) 加载数据。 + *

+ * + * @param id 笔记在数据库中的 ID + */ private void loadFromCursor(long id) { Cursor c = null; try { @@ -185,6 +249,14 @@ public class SqlNote { } } + /** + * 从数据库游标加载笔记数据 + *

+ * 从游标的当前行读取所有笔记字段值并初始化对象的成员变量。 + *

+ * + * @param c 指向笔记记录的数据库游标 + */ private void loadFromCursor(Cursor c) { mId = c.getLong(ID_COLUMN); mAlertDate = c.getLong(ALERTED_DATE_COLUMN); @@ -200,6 +272,13 @@ public class SqlNote { mVersion = c.getLong(VERSION_COLUMN); } + /** + * 加载笔记的数据内容 + *

+ * 从数据库查询当前笔记的所有数据记录(Data 表),并创建 SqlData 对象列表。 + * 仅对普通笔记类型有效,文件夹类型没有数据内容。 + *

+ */ private void loadDataContent() { Cursor c = null; mDataList.clear(); @@ -226,6 +305,17 @@ public class SqlNote { } } + /** + * 从 JSON 对象设置笔记内容 + *

+ * 解析 JSON 对象中的笔记信息和数据,更新当前笔记的字段值。 + * 根据笔记类型(系统文件夹、文件夹、普通笔记)执行不同的更新逻辑。 + * 对于普通笔记,会同时更新其数据内容列表。 + *

+ * + * @param js 包含笔记信息的 JSON 对象 + * @return 如果设置成功返回 true,否则返回 false + */ public boolean setContent(JSONObject js) { try { JSONObject note = js.getJSONObject(GTaskStringUtils.META_HEAD_NOTE); @@ -359,6 +449,15 @@ public class SqlNote { return true; } + /** + * 获取笔记内容的 JSON 对象 + *

+ * 将当前笔记的所有字段和数据内容导出为 JSON 对象格式。 + * 根据笔记类型生成不同结构的 JSON,普通笔记包含数据数组。 + *

+ * + * @return 包含笔记信息的 JSON 对象,如果尚未创建到数据库则返回 null + */ public JSONObject getContent() { try { JSONObject js = new JSONObject(); @@ -407,39 +506,102 @@ public class SqlNote { return null; } + /** + * 设置父文件夹 ID + *

+ * 更新笔记的父文件夹 ID,并将变更记录到差异值集合中。 + *

+ * + * @param id 新的父文件夹 ID + */ public void setParentId(long id) { mParentId = id; mDiffNoteValues.put(NoteColumns.PARENT_ID, id); } + /** + * 设置 Google Tasks ID + *

+ * 将 Google Tasks 的任务 ID 关联到当前笔记,用于同步标识。 + *

+ * + * @param gid Google Tasks 任务 ID + */ public void setGtaskId(String gid) { mDiffNoteValues.put(NoteColumns.GTASK_ID, gid); } + /** + * 设置同步 ID + *

+ * 记录最后一次同步的时间戳,用于判断本地和远程数据的同步状态。 + *

+ * + * @param syncId 同步时间戳 + */ public void setSyncId(long syncId) { mDiffNoteValues.put(NoteColumns.SYNC_ID, syncId); } + /** + * 重置本地修改标记 + *

+ * 将本地修改标记设置为 0,表示笔记已同步,无待同步的本地修改。 + *

+ */ public void resetLocalModified() { mDiffNoteValues.put(NoteColumns.LOCAL_MODIFIED, 0); } + /** + * 获取笔记 ID + * + * @return 笔记在数据库中的 ID,如果尚未创建则返回 INVALID_ID + */ public long getId() { return mId; } + /** + * 获取父文件夹 ID + * + * @return 父文件夹在数据库中的 ID + */ public long getParentId() { return mParentId; } + /** + * 获取笔记摘要文本 + * + * @return 笔记的摘要文本 + */ public String getSnippet() { return mSnippet; } + /** + * 判断是否为普通笔记类型 + * + * @return 如果是普通笔记返回 true,否则返回 false + */ public boolean isNoteType() { return mType == Notes.TYPE_NOTE; } + /** + * 提交笔记变更到数据库 + *

+ * 根据当前状态执行插入或更新操作: + * - 如果是新建笔记,插入新记录并获取生成的 ID + * - 如果是已存在的笔记,更新变更的字段 + * - 对于普通笔记,同时提交其数据内容 + *

+ * + * @param validateVersion 是否验证版本号,为 true 时仅更新版本号不大于当前版本的记录 + * @throws ActionFailureException 如果创建笔记失败 + * @throws IllegalStateException 如果尝试更新无效 ID 的笔记 + */ public void commit(boolean validateVersion) { if (mIsCreate) { if (mId == INVALID_ID && mDiffNoteValues.containsKey(NoteColumns.ID)) { @@ -476,6 +638,7 @@ public class SqlNote { String.valueOf(mId) }); } else { + // 仅更新版本号不大于当前版本的记录,防止并发更新冲突 result = mContentResolver.update(Notes.CONTENT_NOTE_URI, mDiffNoteValues, "(" + NoteColumns.ID + "=?) AND (" + NoteColumns.VERSION + "<=?)", new String[] { @@ -494,7 +657,7 @@ public class SqlNote { } } - // refresh local info + // 从数据库重新加载最新数据,确保内存状态与数据库一致 loadFromCursor(mId); if (mType == Notes.TYPE_NOTE) loadDataContent(); diff --git a/src/Notes-master/src/net/micode/notes/gtask/data/Task.java b/src/Notes-master/src/net/micode/notes/gtask/data/Task.java index 6a19454..f9f0ef3 100644 --- a/src/Notes-master/src/net/micode/notes/gtask/data/Task.java +++ b/src/Notes-master/src/net/micode/notes/gtask/data/Task.java @@ -32,19 +32,51 @@ import org.json.JSONException; import org.json.JSONObject; +/** + * Google Tasks 任务类 + *

+ * 继承自 Node,表示 Google Tasks 中的一个任务项。 + * 负责管理任务的完成状态、备注信息、元数据、前驱兄弟节点和父任务列表。 + * 支持与本地笔记的双向同步,能够生成创建和更新动作的 JSON 对象。 + *

+ */ public class Task extends Node { + /** + * 日志标签 + */ private static final String TAG = Task.class.getSimpleName(); + /** + * 完成状态标记,true 表示已完成 + */ private boolean mCompleted; + /** + * 任务备注信息 + */ private String mNotes; + /** + * 元数据 JSON 对象,包含本地笔记的完整信息 + */ private JSONObject mMetaInfo; + /** + * 前驱兄弟任务,用于维护任务在列表中的顺序 + */ private Task mPriorSibling; + /** + * 父任务列表 + */ private TaskList mParent; + /** + * 构造一个新的任务实例 + *

+ * 初始化所有属性为默认值:未完成、备注为 null、无前驱兄弟、无父列表、无元数据。 + *

+ */ public Task() { super(); mCompleted = false; @@ -54,6 +86,16 @@ public class Task extends Node { mMetaInfo = null; } + /** + * 获取创建动作的 JSON 对象 + *

+ * 生成用于在远程服务器创建任务的 JSON 请求,包含任务名称、备注、父列表 ID 等信息。 + *

+ * + * @param actionId 动作 ID,标识具体的创建操作 + * @return 包含创建动作信息的 JSON 对象 + * @throws ActionFailureException 如果生成 JSON 对象失败 + */ public JSONObject getCreateAction(int actionId) { JSONObject js = new JSONObject(); @@ -103,6 +145,16 @@ public class Task extends Node { return js; } + /** + * 获取更新动作的 JSON 对象 + *

+ * 生成用于在远程服务器更新任务的 JSON 请求,包含任务名称、备注、删除状态等信息。 + *

+ * + * @param actionId 动作 ID,标识具体的更新操作 + * @return 包含更新动作信息的 JSON 对象 + * @throws ActionFailureException 如果生成 JSON 对象失败 + */ public JSONObject getUpdateAction(int actionId) { JSONObject js = new JSONObject(); @@ -135,6 +187,15 @@ public class Task extends Node { return js; } + /** + * 根据远程 JSON 设置内容 + *

+ * 从远程服务器返回的 JSON 对象中解析并设置任务的属性值,包括 ID、名称、备注、完成状态等。 + *

+ * + * @param js 远程服务器返回的 JSON 对象 + * @throws ActionFailureException 如果解析 JSON 失败 + */ public void setContentByRemoteJSON(JSONObject js) { if (js != null) { try { @@ -175,6 +236,15 @@ public class Task extends Node { } } + /** + * 根据本地 JSON 设置内容 + *

+ * 从本地数据库存储的 JSON 对象中解析并设置任务的属性值。 + * 从笔记数据中提取内容作为任务名称。 + *

+ * + * @param js 本地数据库存储的 JSON 对象 + */ public void setContentByLocalJSON(JSONObject js) { if (js == null || !js.has(GTaskStringUtils.META_HEAD_NOTE) || !js.has(GTaskStringUtils.META_HEAD_DATA)) { @@ -190,6 +260,7 @@ public class Task extends Node { return; } + // 遍历数据数组,查找笔记内容 for (int i = 0; i < dataArray.length(); i++) { JSONObject data = dataArray.getJSONObject(i); if (TextUtils.equals(data.getString(DataColumns.MIME_TYPE), DataConstants.NOTE)) { @@ -204,6 +275,15 @@ public class Task extends Node { } } + /** + * 从内容生成本地 JSON 对象 + *

+ * 将任务的当前属性值转换为 JSON 对象,用于存储到本地数据库。 + * 如果是新建任务,创建新的 JSON 结构;如果是已同步任务,更新现有元数据。 + *

+ * + * @return 包含任务内容的 JSON 对象,如果生成失败则返回 null + */ public JSONObject getLocalJSONFromContent() { String name = getName(); try { @@ -229,6 +309,7 @@ public class Task extends Node { JSONObject note = mMetaInfo.getJSONObject(GTaskStringUtils.META_HEAD_NOTE); JSONArray dataArray = mMetaInfo.getJSONArray(GTaskStringUtils.META_HEAD_DATA); + // 更新数据数组中的笔记内容 for (int i = 0; i < dataArray.length(); i++) { JSONObject data = dataArray.getJSONObject(i); if (TextUtils.equals(data.getString(DataColumns.MIME_TYPE), DataConstants.NOTE)) { @@ -247,6 +328,15 @@ public class Task extends Node { } } + /** + * 设置元数据信息 + *

+ * 从元数据对象中解析并设置任务的元信息 JSON 对象。 + * 元信息包含本地笔记的完整结构,用于双向同步。 + *

+ * + * @param metaData 元数据对象 + */ public void setMetaInfo(MetaData metaData) { if (metaData != null && metaData.getNotes() != null) { try { @@ -258,6 +348,16 @@ public class Task extends Node { } } + /** + * 根据数据库游标获取同步动作 + *

+ * 比较本地数据库中的数据与当前任务状态,确定需要执行的同步动作类型。 + * 处理各种同步场景:无更新、本地更新、远程更新、冲突、错误等。 + *

+ * + * @param c 指向本地数据库记录的游标 + * @return 同步动作类型,取值为 SYNC_ACTION_* 常量之一 + */ public int getSyncAction(Cursor c) { try { JSONObject noteInfo = null; @@ -311,39 +411,87 @@ public class Task extends Node { return SYNC_ACTION_ERROR; } + /** + * 判断是否值得保存 + *

+ * 只有当任务有元数据、非空名称或非空备注时才值得保存。 + *

+ * + * @return 如果有元数据、非空名称或非空备注返回 true,否则返回 false + */ public boolean isWorthSaving() { return mMetaInfo != null || (getName() != null && getName().trim().length() > 0) || (getNotes() != null && getNotes().trim().length() > 0); } + /** + * 设置完成状态 + * + * @param completed 完成状态,true 表示已完成 + */ public void setCompleted(boolean completed) { this.mCompleted = completed; } + /** + * 设置备注信息 + * + * @param notes 备注信息 + */ public void setNotes(String notes) { this.mNotes = notes; } + /** + * 设置前驱兄弟任务 + * + * @param priorSibling 前驱兄弟任务 + */ public void setPriorSibling(Task priorSibling) { this.mPriorSibling = priorSibling; } + /** + * 设置父任务列表 + * + * @param parent 父任务列表 + */ public void setParent(TaskList parent) { this.mParent = parent; } + /** + * 获取完成状态 + * + * @return 完成状态,true 表示已完成 + */ public boolean getCompleted() { return this.mCompleted; } + /** + * 获取备注信息 + * + * @return 备注信息 + */ public String getNotes() { return this.mNotes; } + /** + * 获取前驱兄弟任务 + * + * @return 前驱兄弟任务 + */ public Task getPriorSibling() { return this.mPriorSibling; } + /** + * 获取父任务列表 + * + * @return 父任务列表 + */ public TaskList getParent() { return this.mParent; } diff --git a/src/Notes-master/src/net/micode/notes/gtask/data/TaskList.java b/src/Notes-master/src/net/micode/notes/gtask/data/TaskList.java index 4ea21c5..d454fe7 100644 --- a/src/Notes-master/src/net/micode/notes/gtask/data/TaskList.java +++ b/src/Notes-master/src/net/micode/notes/gtask/data/TaskList.java @@ -30,19 +30,52 @@ import org.json.JSONObject; import java.util.ArrayList; +/** + * Google Tasks 任务列表类 + *

+ * 继承自 Node,表示 Google Tasks 中的一个任务列表(文件夹)。 + * 负责管理任务列表的子任务集合,提供任务的增删改查操作。 + * 支持与本地笔记文件夹的双向同步,能够生成创建和更新动作的 JSON 对象。 + *

+ */ public class TaskList extends Node { + /** + * 日志标签 + */ private static final String TAG = TaskList.class.getSimpleName(); + /** + * 任务列表索引 + */ private int mIndex; + /** + * 子任务列表 + */ private ArrayList mChildren; + /** + * 构造一个新的任务列表实例 + *

+ * 初始化子任务列表为空,索引设置为 1。 + *

+ */ public TaskList() { super(); mChildren = new ArrayList(); mIndex = 1; } + /** + * 获取创建动作的 JSON 对象 + *

+ * 生成用于在远程服务器创建任务列表的 JSON 请求,包含列表名称等信息。 + *

+ * + * @param actionId 动作 ID,标识具体的创建操作 + * @return 包含创建动作信息的 JSON 对象 + * @throws ActionFailureException 如果生成 JSON 对象失败 + */ public JSONObject getCreateAction(int actionId) { JSONObject js = new JSONObject(); @@ -74,6 +107,16 @@ public class TaskList extends Node { return js; } + /** + * 获取更新动作的 JSON 对象 + *

+ * 生成用于在远程服务器更新任务列表的 JSON 请求,包含列表名称、删除状态等信息。 + *

+ * + * @param actionId 动作 ID,标识具体的更新操作 + * @return 包含更新动作信息的 JSON 对象 + * @throws ActionFailureException 如果生成 JSON 对象失败 + */ public JSONObject getUpdateAction(int actionId) { JSONObject js = new JSONObject(); @@ -103,6 +146,15 @@ public class TaskList extends Node { return js; } + /** + * 根据远程 JSON 设置内容 + *

+ * 从远程服务器返回的 JSON 对象中解析并设置任务列表的属性值。 + *

+ * + * @param js 远程服务器返回的 JSON 对象 + * @throws ActionFailureException 如果解析 JSON 失败 + */ public void setContentByRemoteJSON(JSONObject js) { if (js != null) { try { @@ -129,6 +181,15 @@ public class TaskList extends Node { } } + /** + * 根据本地 JSON 设置内容 + *

+ * 从本地数据库存储的 JSON 对象中解析并设置任务列表的属性值。 + * 根据文件夹类型(普通文件夹或系统文件夹)设置对应的名称。 + *

+ * + * @param js 本地数据库存储的 JSON 对象 + */ public void setContentByLocalJSON(JSONObject js) { if (js == null || !js.has(GTaskStringUtils.META_HEAD_NOTE)) { Log.w(TAG, "setContentByLocalJSON: nothing is avaiable"); @@ -138,9 +199,11 @@ public class TaskList extends Node { JSONObject folder = js.getJSONObject(GTaskStringUtils.META_HEAD_NOTE); if (folder.getInt(NoteColumns.TYPE) == Notes.TYPE_FOLDER) { + // 普通文件夹,使用文件夹名称 String name = folder.getString(NoteColumns.SNIPPET); setName(GTaskStringUtils.MIUI_FOLDER_PREFFIX + name); } else if (folder.getInt(NoteColumns.TYPE) == Notes.TYPE_SYSTEM) { + // 系统文件夹,根据 ID 设置对应的名称 if (folder.getLong(NoteColumns.ID) == Notes.ID_ROOT_FOLDER) setName(GTaskStringUtils.MIUI_FOLDER_PREFFIX + GTaskStringUtils.FOLDER_DEFAULT); else if (folder.getLong(NoteColumns.ID) == Notes.ID_CALL_RECORD_FOLDER) @@ -157,16 +220,27 @@ public class TaskList extends Node { } } + /** + * 从内容生成本地 JSON 对象 + *

+ * 将任务列表的当前属性值转换为 JSON 对象,用于存储到本地数据库。 + * 根据文件夹名称判断是系统文件夹还是普通文件夹。 + *

+ * + * @return 包含任务列表内容的 JSON 对象,如果生成失败则返回 null + */ public JSONObject getLocalJSONFromContent() { try { JSONObject js = new JSONObject(); JSONObject folder = new JSONObject(); + // 去除文件夹名称前缀 String folderName = getName(); if (getName().startsWith(GTaskStringUtils.MIUI_FOLDER_PREFFIX)) folderName = folderName.substring(GTaskStringUtils.MIUI_FOLDER_PREFFIX.length(), folderName.length()); folder.put(NoteColumns.SNIPPET, folderName); + // 根据文件夹名称判断类型 if (folderName.equals(GTaskStringUtils.FOLDER_DEFAULT) || folderName.equals(GTaskStringUtils.FOLDER_CALL_NOTE)) folder.put(NoteColumns.TYPE, Notes.TYPE_SYSTEM); @@ -183,6 +257,16 @@ public class TaskList extends Node { } } + /** + * 根据数据库游标获取同步动作 + *

+ * 比较本地数据库中的数据与当前任务列表状态,确定需要执行的同步动作类型。 + * 对于文件夹冲突,优先应用本地修改。 + *

+ * + * @param c 指向本地数据库记录的游标 + * @return 同步动作类型,取值为 SYNC_ACTION_* 常量之一 + */ public int getSyncAction(Cursor c) { try { if (c.getInt(SqlNote.LOCAL_MODIFIED_COLUMN) == 0) { @@ -216,10 +300,24 @@ public class TaskList extends Node { return SYNC_ACTION_ERROR; } + /** + * 获取子任务数量 + * + * @return 子任务的数量 + */ public int getChildTaskCount() { return mChildren.size(); } + /** + * 添加子任务到列表末尾 + *

+ * 将任务添加到子任务列表的末尾,并设置其前驱兄弟和父列表。 + *

+ * + * @param task 要添加的子任务 + * @return 如果添加成功返回 true,否则返回 false + */ public boolean addChildTask(Task task) { boolean ret = false; if (task != null && !mChildren.contains(task)) { @@ -234,6 +332,16 @@ public class TaskList extends Node { return ret; } + /** + * 在指定位置添加子任务 + *

+ * 将任务插入到子任务列表的指定位置,并更新相关任务的前驱兄弟关系。 + *

+ * + * @param task 要添加的子任务 + * @param index 插入位置索引,必须在 0 到子任务数量之间 + * @return 如果添加成功返回 true,否则返回 false + */ public boolean addChildTask(Task task, int index) { if (index < 0 || index > mChildren.size()) { Log.e(TAG, "add child task: invalid index"); @@ -260,6 +368,16 @@ public class TaskList extends Node { return true; } + /** + * 移除子任务 + *

+ * 从子任务列表中移除指定任务,并重置其前驱兄弟和父列表关系。 + * 同时更新后续任务的前驱兄弟关系。 + *

+ * + * @param task 要移除的子任务 + * @return 如果移除成功返回 true,否则返回 false + */ public boolean removeChildTask(Task task) { boolean ret = false; int index = mChildren.indexOf(task); @@ -281,6 +399,16 @@ public class TaskList extends Node { return ret; } + /** + * 移动子任务到指定位置 + *

+ * 将子任务从当前位置移动到目标位置,通过先移除再添加实现。 + *

+ * + * @param task 要移动的子任务 + * @param index 目标位置索引,必须在 0 到子任务数量减 1 之间 + * @return 如果移动成功返回 true,否则返回 false + */ public boolean moveChildTask(Task task, int index) { if (index < 0 || index >= mChildren.size()) { @@ -299,6 +427,12 @@ public class TaskList extends Node { return (removeChildTask(task) && addChildTask(task, index)); } + /** + * 根据 GID 查找子任务 + * + * @param gid Google Tasks ID + * @return 找到的子任务,如果未找到则返回 null + */ public Task findChildTaskByGid(String gid) { for (int i = 0; i < mChildren.size(); i++) { Task t = mChildren.get(i); @@ -309,10 +443,22 @@ public class TaskList extends Node { return null; } + /** + * 获取子任务的索引位置 + * + * @param task 子任务 + * @return 子任务的索引位置,如果未找到则返回 -1 + */ public int getChildTaskIndex(Task task) { return mChildren.indexOf(task); } + /** + * 根据索引获取子任务 + * + * @param index 索引位置,必须在 0 到子任务数量减 1 之间 + * @return 对应的子任务,如果索引无效则返回 null + */ public Task getChildTaskByIndex(int index) { if (index < 0 || index >= mChildren.size()) { Log.e(TAG, "getTaskByIndex: invalid index"); @@ -321,6 +467,12 @@ public class TaskList extends Node { return mChildren.get(index); } + /** + * 根据 GID 获取子任务 + * + * @param gid Google Tasks ID + * @return 对应的子任务,如果未找到则返回 null + */ public Task getChilTaskByGid(String gid) { for (Task task : mChildren) { if (task.getGid().equals(gid)) @@ -329,14 +481,29 @@ public class TaskList extends Node { return null; } + /** + * 获取子任务列表 + * + * @return 子任务列表的副本 + */ public ArrayList getChildTaskList() { return this.mChildren; } + /** + * 设置任务列表索引 + * + * @param index 任务列表索引 + */ public void setIndex(int index) { this.mIndex = index; } + /** + * 获取任务列表索引 + * + * @return 任务列表索引 + */ public int getIndex() { return this.mIndex; } diff --git a/src/Notes-master/src/net/micode/notes/gtask/exception/ActionFailureException.java b/src/Notes-master/src/net/micode/notes/gtask/exception/ActionFailureException.java index 15504be..12b3bcd 100644 --- a/src/Notes-master/src/net/micode/notes/gtask/exception/ActionFailureException.java +++ b/src/Notes-master/src/net/micode/notes/gtask/exception/ActionFailureException.java @@ -16,17 +16,39 @@ package net.micode.notes.gtask.exception; +/** + * 操作失败异常类 + *

+ * 用于表示 Google Tasks 同步过程中操作执行失败的情况。 + * 当同步操作(如创建、更新、删除任务或任务列表)失败时抛出此异常。 + * 该异常继承自 RuntimeException,属于非受检异常,调用方可以选择性处理。 + *

+ */ public class ActionFailureException extends RuntimeException { private static final long serialVersionUID = 4425249765923293627L; + /** + * 构造一个无详细信息的操作失败异常 + */ public ActionFailureException() { super(); } + /** + * 构造一个带有详细信息的操作失败异常 + * + * @param paramString 异常的详细信息,描述操作失败的具体原因 + */ public ActionFailureException(String paramString) { super(paramString); } + /** + * 构造一个带有详细信息和原因的操作失败异常 + * + * @param paramString 异常的详细信息,描述操作失败的具体原因 + * @param paramThrowable 导致此异常的底层异常或错误 + */ public ActionFailureException(String paramString, Throwable paramThrowable) { super(paramString, paramThrowable); } diff --git a/src/Notes-master/src/net/micode/notes/gtask/exception/NetworkFailureException.java b/src/Notes-master/src/net/micode/notes/gtask/exception/NetworkFailureException.java index b08cfb1..a7aeedf 100644 --- a/src/Notes-master/src/net/micode/notes/gtask/exception/NetworkFailureException.java +++ b/src/Notes-master/src/net/micode/notes/gtask/exception/NetworkFailureException.java @@ -16,17 +16,39 @@ package net.micode.notes.gtask.exception; +/** + * 网络异常类 + *

+ * 用于表示 Google Tasks 同步过程中发生的网络相关错误。 + * 当网络连接失败、超时或无法访问 Google Tasks 服务时抛出此异常。 + * 该异常继承自 Exception,属于受检异常,调用方必须处理或继续抛出。 + *

+ */ public class NetworkFailureException extends Exception { private static final long serialVersionUID = 2107610287180234136L; + /** + * 构造一个无详细信息的网络异常 + */ public NetworkFailureException() { super(); } + /** + * 构造一个带有详细信息的网络异常 + * + * @param paramString 异常的详细信息,描述网络失败的具体原因 + */ public NetworkFailureException(String paramString) { super(paramString); } + /** + * 构造一个带有详细信息和原因的网络异常 + * + * @param paramString 异常的详细信息,描述网络失败的具体原因 + * @param paramThrowable 导致此异常的底层异常或错误 + */ public NetworkFailureException(String paramString, Throwable paramThrowable) { super(paramString, paramThrowable); } diff --git a/src/Notes-master/src/net/micode/notes/gtask/remote/GTaskASyncTask.java b/src/Notes-master/src/net/micode/notes/gtask/remote/GTaskASyncTask.java index b3b61e7..f8ea190 100644 --- a/src/Notes-master/src/net/micode/notes/gtask/remote/GTaskASyncTask.java +++ b/src/Notes-master/src/net/micode/notes/gtask/remote/GTaskASyncTask.java @@ -29,88 +29,187 @@ import net.micode.notes.ui.NotesListActivity; import net.micode.notes.ui.NotesPreferenceActivity; +/** + * Google Tasks 同步异步任务 + *

+ * 继承自 AsyncTask,用于在后台执行 Google Tasks 同步操作。 + * 支持进度更新、通知显示和同步完成回调。 + *

+ */ public class GTaskASyncTask extends AsyncTask { + /** 同步通知的唯一标识符 */ private static int GTASK_SYNC_NOTIFICATION_ID = 5234235; + /** + * 同步完成监听器接口 + *

+ * 定义同步完成时的回调方法,用于通知调用方同步任务已结束。 + *

+ */ public interface OnCompleteListener { + /** + * 同步完成时的回调方法 + */ void onComplete(); } + /** 应用上下文 */ private Context mContext; + /** 通知管理器 */ private NotificationManager mNotifiManager; + /** Google Tasks 管理器实例 */ private GTaskManager mTaskManager; + /** 同步完成监听器 */ private OnCompleteListener mOnCompleteListener; + /** + * 构造函数 + *

+ * 初始化异步任务所需的上下文、监听器、通知管理器和任务管理器。 + *

+ * + * @param context 应用上下文 + * @param listener 同步完成监听器 + */ public GTaskASyncTask(Context context, OnCompleteListener listener) { mContext = context; mOnCompleteListener = listener; + // 获取系统通知服务 mNotifiManager = (NotificationManager) mContext .getSystemService(Context.NOTIFICATION_SERVICE); + // 获取 GTaskManager 单例 mTaskManager = GTaskManager.getInstance(); } + /** + * 取消同步操作 + *

+ * 调用 GTaskManager 的 cancelSync() 方法取消正在进行的同步。 + *

+ */ public void cancelSync() { mTaskManager.cancelSync(); } + /** + * 发布同步进度 + *

+ * 调用 AsyncTask 的 publishProgress() 方法发布进度消息到 UI 线程。 + *

+ * + * @param message 进度消息 + */ public void publishProgess(String message) { publishProgress(new String[] { message }); } + /** + * 显示同步通知 + *

+ * 在状态栏显示同步进度或结果通知。 + * 同步成功时跳转到笔记列表,其他情况跳转到设置页面。 + *

+ * + * @param tickerId 通知标题字符串资源 ID + * @param content 通知内容文本 + */ private void showNotification(int tickerId, String content) { - 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; + // 根据同步结果选择跳转目标 if (tickerId != R.string.ticker_success) { + // 同步失败或取消,跳转到设置页面 pendingIntent = PendingIntent.getActivity(mContext, 0, new Intent(mContext, - NotesPreferenceActivity.class), 0); - + NotesPreferenceActivity.class), PendingIntent.FLAG_IMMUTABLE); } else { + // 同步成功,跳转到笔记列表 pendingIntent = PendingIntent.getActivity(mContext, 0, new Intent(mContext, - NotesListActivity.class), 0); + NotesListActivity.class), PendingIntent.FLAG_IMMUTABLE); } - notification.setLatestEventInfo(mContext, mContext.getString(R.string.app_name), content, - pendingIntent); + // 构建通知 + Notification.Builder builder = new Notification.Builder(mContext) + .setAutoCancel(true) + .setContentTitle(mContext.getString(R.string.app_name)) + .setContentText(content) + .setContentIntent(pendingIntent) + .setWhen(System.currentTimeMillis()) + .setOngoing(true); + Notification notification=builder.getNotification(); + // 显示通知 mNotifiManager.notify(GTASK_SYNC_NOTIFICATION_ID, notification); } + /** + * 后台执行同步操作 + *

+ * 在后台线程执行 Google Tasks 同步,发布登录进度并返回同步结果。 + *

+ * + * @param unused 未使用的参数 + * @return 同步状态码(GTaskManager.STATE_SUCCESS、STATE_NETWORK_ERROR、STATE_INTERNAL_ERROR、STATE_SYNC_IN_PROGRESS 或 STATE_SYNC_CANCELLED) + */ @Override protected Integer doInBackground(Void... unused) { + // 发布登录进度 publishProgess(mContext.getString(R.string.sync_progress_login, NotesPreferenceActivity .getSyncAccountName(mContext))); + // 执行同步并返回结果 return mTaskManager.sync(mContext, this); } + /** + * 进度更新回调 + *

+ * 在 UI 线程更新同步进度,显示通知并发送广播。 + *

+ * + * @param progress 进度消息数组 + */ @Override protected void onProgressUpdate(String... progress) { + // 显示进度通知 showNotification(R.string.ticker_syncing, progress[0]); + // 如果上下文是 GTaskSyncService,发送广播 if (mContext instanceof GTaskSyncService) { ((GTaskSyncService) mContext).sendBroadcast(progress[0]); } } + /** + * 同步完成回调 + *

+ * 根据同步结果显示相应的通知,并调用完成监听器。 + * 更新最后同步时间(仅在同步成功时)。 + *

+ * + * @param result 同步结果状态码 + */ @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() { diff --git a/src/Notes-master/src/net/micode/notes/gtask/remote/GTaskClient.java b/src/Notes-master/src/net/micode/notes/gtask/remote/GTaskClient.java index c67dfdf..76ce522 100644 --- a/src/Notes-master/src/net/micode/notes/gtask/remote/GTaskClient.java +++ b/src/Notes-master/src/net/micode/notes/gtask/remote/GTaskClient.java @@ -61,15 +61,27 @@ import java.util.zip.Inflater; import java.util.zip.InflaterInputStream; +/** + * Google Tasks 客户端类 + *

+ * 单例模式实现的 Google Tasks API 客户端,负责与 Google Tasks 服务器的网络通信。 + * 提供登录认证、任务列表和任务的增删改查、批量更新等功能。 + * 使用 HTTP 协议与 Google Tasks API 交互,支持 Cookie 认证和会话管理。 + *

+ */ public class GTaskClient { private static final String TAG = GTaskClient.class.getSimpleName(); + /** Google Tasks 基础 URL */ private static final String GTASK_URL = "https://mail.google.com/tasks/"; + /** Google Tasks GET 请求 URL */ private static final String GTASK_GET_URL = "https://mail.google.com/tasks/ig"; + /** Google Tasks POST 请求 URL */ private static final String GTASK_POST_URL = "https://mail.google.com/tasks/r/ig"; + /** 单例实例 */ private static GTaskClient mInstance = null; private DefaultHttpClient mHttpClient; @@ -90,6 +102,12 @@ public class GTaskClient { private JSONArray mUpdateArray; + /** + * 私有构造函数 + *

+ * 初始化所有成员变量为默认值,防止外部直接实例化。 + *

+ */ private GTaskClient() { mHttpClient = null; mGetUrl = GTASK_GET_URL; @@ -102,6 +120,14 @@ public class GTaskClient { mUpdateArray = null; } + /** + * 获取 GTaskClient 单例实例 + *

+ * 使用双重检查锁定确保线程安全的单例实现。 + *

+ * + * @return GTaskClient 单例实例 + */ public static synchronized GTaskClient getInstance() { if (mInstance == null) { mInstance = new GTaskClient(); @@ -109,6 +135,17 @@ public class GTaskClient { return mInstance; } + /** + * 登录 Google Tasks + *

+ * 检查登录状态和账户信息,必要时重新登录。 + * Cookie 有效期为 5 分钟,超时后需要重新登录。 + * 支持自定义域名账户和标准 Gmail/Googlemail 账户。 + *

+ * + * @param activity Activity 上下文,用于账户管理 + * @return 如果登录成功返回 true,否则返回 false + */ public boolean login(Activity activity) { // we suppose that the cookie would expire after 5 minutes // then we need to re-login @@ -164,6 +201,26 @@ public class GTaskClient { return true; } + /** + * 获取同步账户 + * + * @return 当前登录的 Google 账户 + */ + private Account getSyncAccount() { + return mAccount; + } + + /** + * 登录 Google 账户获取认证令牌 + *

+ * 从系统账户管理器获取 Google 账户的认证令牌。 + * 如果 invalidateToken 为 true,会先使旧令牌失效再获取新令牌。 + *

+ * + * @param activity Activity 上下文 + * @param invalidateToken 是否使旧令牌失效 + * @return 认证令牌,如果失败则返回 null + */ private String loginGoogleAccount(Activity activity, boolean invalidateToken) { String authToken; AccountManager accountManager = AccountManager.get(activity); @@ -207,6 +264,16 @@ public class GTaskClient { return authToken; } + /** + * 尝试登录 Google Tasks + *

+ * 使用认证令牌尝试登录 Google Tasks,如果失败则使令牌失效并重试。 + *

+ * + * @param activity Activity 上下文 + * @param authToken 认证令牌 + * @return 如果登录成功返回 true,否则返回 false + */ private boolean tryToLoginGtask(Activity activity, String authToken) { if (!loginGtask(authToken)) { // maybe the auth token is out of date, now let's invalidate the @@ -225,6 +292,15 @@ public class GTaskClient { return true; } + /** + * 使用认证令牌登录 Google Tasks + *

+ * 向 Google Tasks 服务器发送 GET 请求进行认证,获取 Cookie 和客户端版本号。 + *

+ * + * @param authToken 认证令牌 + * @return 如果登录成功返回 true,否则返回 false + */ private boolean loginGtask(String authToken) { int timeoutConnection = 10000; int timeoutSocket = 15000; @@ -280,10 +356,26 @@ public class GTaskClient { return true; } + /** + * 获取下一个动作 ID + *

+ * 每次调用返回递增的动作 ID,用于标识不同的操作请求。 + *

+ * + * @return 动作 ID + */ private int getActionId() { return mActionId++; } + /** + * 创建 HTTP POST 请求对象 + *

+ * 配置请求头,设置内容类型为 application/x-www-form-urlencoded。 + *

+ * + * @return 配置好的 HttpPost 对象 + */ private HttpPost createHttpPost() { HttpPost httpPost = new HttpPost(mPostUrl); httpPost.setHeader("Content-Type", "application/x-www-form-urlencoded;charset=utf-8"); @@ -291,6 +383,16 @@ public class GTaskClient { return httpPost; } + /** + * 获取 HTTP 响应内容 + *

+ * 解析 HTTP 实体的内容,支持 gzip 和 deflate 压缩格式。 + *

+ * + * @param entity HTTP 响应实体 + * @return 响应内容的字符串 + * @throws IOException 如果读取响应内容失败 + */ private String getResponseContent(HttpEntity entity) throws IOException { String contentEncoding = null; if (entity.getContentEncoding() != null) { @@ -323,6 +425,17 @@ public class GTaskClient { } } + /** + * 发送 POST 请求到 Google Tasks 服务器 + *

+ * 将 JSON 数据封装为 POST 请求发送到服务器,并解析返回的 JSON 响应。 + *

+ * + * @param js 要发送的 JSON 对象 + * @return 服务器返回的 JSON 对象 + * @throws NetworkFailureException 如果网络请求失败 + * @throws ActionFailureException 如果未登录或 JSON 解析失败 + */ private JSONObject postRequest(JSONObject js) throws NetworkFailureException { if (!mLoggedin) { Log.e(TAG, "please login first"); @@ -360,6 +473,16 @@ public class GTaskClient { } } + /** + * 创建新的任务 + *

+ * 向 Google Tasks 服务器发送创建任务请求,获取服务器分配的任务 ID。 + *

+ * + * @param task 要创建的任务对象 + * @throws NetworkFailureException 如果网络请求失败 + * @throws ActionFailureException 如果 JSON 处理失败 + */ public void createTask(Task task) throws NetworkFailureException { commitUpdate(); try { @@ -386,6 +509,16 @@ public class GTaskClient { } } + /** + * 创建新的任务列表 + *

+ * 向 Google Tasks 服务器发送创建任务列表请求,获取服务器分配的任务列表 ID。 + *

+ * + * @param tasklist 要创建的任务列表对象 + * @throws NetworkFailureException 如果网络请求失败 + * @throws ActionFailureException 如果 JSON 处理失败 + */ public void createTaskList(TaskList tasklist) throws NetworkFailureException { commitUpdate(); try { @@ -412,6 +545,16 @@ public class GTaskClient { } } + /** + * 提交批量更新请求 + *

+ * 将待更新的节点批量发送到 Google Tasks 服务器。 + * 如果没有待更新的节点,则不执行任何操作。 + *

+ * + * @throws NetworkFailureException 如果网络请求失败 + * @throws ActionFailureException 如果 JSON 处理失败 + */ public void commitUpdate() throws NetworkFailureException { if (mUpdateArray != null) { try { @@ -433,6 +576,15 @@ public class GTaskClient { } } + /** + * 添加待更新节点到批量更新队列 + *

+ * 将节点添加到更新队列中,当队列超过 10 个节点时自动提交。 + *

+ * + * @param node 要更新的节点,如果为 null 则不执行任何操作 + * @throws NetworkFailureException 如果提交更新时网络请求失败 + */ public void addUpdateNode(Node node) throws NetworkFailureException { if (node != null) { // too many update items may result in an error @@ -447,6 +599,18 @@ public class GTaskClient { } } + /** + * 移动任务到新的任务列表或新位置 + *

+ * 将任务从一个任务列表移动到另一个任务列表,或在同一任务列表中调整顺序。 + *

+ * + * @param task 要移动的任务 + * @param preParent 任务的原父任务列表 + * @param curParent 任务的新父任务列表 + * @throws NetworkFailureException 如果网络请求失败 + * @throws ActionFailureException 如果 JSON 处理失败 + */ public void moveTask(Task task, TaskList preParent, TaskList curParent) throws NetworkFailureException { commitUpdate(); @@ -486,6 +650,16 @@ public class GTaskClient { } } + /** + * 删除节点 + *

+ * 向 Google Tasks 服务器发送删除节点请求,将节点标记为已删除。 + *

+ * + * @param node 要删除的节点 + * @throws NetworkFailureException 如果网络请求失败 + * @throws ActionFailureException 如果 JSON 处理失败 + */ public void deleteNode(Node node) throws NetworkFailureException { commitUpdate(); try { @@ -509,6 +683,16 @@ public class GTaskClient { } } + /** + * 获取所有任务列表 + *

+ * 从 Google Tasks 服务器获取当前账户的所有任务列表。 + *

+ * + * @return 包含所有任务列表信息的 JSON 数组 + * @throws NetworkFailureException 如果网络请求失败 + * @throws ActionFailureException 如果未登录或 JSON 解析失败 + */ public JSONArray getTaskLists() throws NetworkFailureException { if (!mLoggedin) { Log.e(TAG, "please login first"); @@ -547,6 +731,17 @@ public class GTaskClient { } } + /** + * 获取指定任务列表中的所有任务 + *

+ * 从 Google Tasks 服务器获取指定任务列表中的所有任务。 + *

+ * + * @param listGid 任务列表的 Google ID + * @return 包含该任务列表中所有任务信息的 JSON 数组 + * @throws NetworkFailureException 如果网络请求失败 + * @throws ActionFailureException 如果 JSON 处理失败 + */ public JSONArray getTaskList(String listGid) throws NetworkFailureException { commitUpdate(); try { @@ -575,10 +770,21 @@ public class GTaskClient { } } + /** + * 获取同步账户 + * + * @return 当前登录的 Google 账户 + */ public Account getSyncAccount() { return mAccount; } + /** + * 重置更新数组 + *

+ * 清空待更新的节点队列,取消所有未提交的更新操作。 + *

+ */ public void resetUpdateArray() { mUpdateArray = null; } diff --git a/src/Notes-master/src/net/micode/notes/gtask/remote/GTaskManager.java b/src/Notes-master/src/net/micode/notes/gtask/remote/GTaskManager.java index d2b4082..6beb7a7 100644 --- a/src/Notes-master/src/net/micode/notes/gtask/remote/GTaskManager.java +++ b/src/Notes-master/src/net/micode/notes/gtask/remote/GTaskManager.java @@ -48,17 +48,30 @@ import java.util.Iterator; import java.util.Map; +/** + * Google Tasks 同步管理器 + *

+ * 单例模式实现的同步管理器,负责本地笔记与 Google Tasks 之间的数据同步。 + * 提供完整的双向同步功能,包括文件夹、笔记的增删改查操作。 + * 支持同步状态管理、冲突解决和元数据维护。 + *

+ */ public class GTaskManager { private static final String TAG = GTaskManager.class.getSimpleName(); + /** 同步成功状态码 */ public static final int STATE_SUCCESS = 0; + /** 网络错误状态码 */ public static final int STATE_NETWORK_ERROR = 1; + /** 内部错误状态码 */ public static final int STATE_INTERNAL_ERROR = 2; + /** 同步进行中状态码 */ public static final int STATE_SYNC_IN_PROGRESS = 3; + /** 同步已取消状态码 */ public static final int STATE_SYNC_CANCELLED = 4; private static GTaskManager mInstance = null; @@ -87,6 +100,12 @@ public class GTaskManager { private HashMap mNidToGid; + /** + * 私有构造函数 + *

+ * 初始化所有成员变量,防止外部直接实例化。 + *

+ */ private GTaskManager() { mSyncing = false; mCancelled = false; @@ -99,6 +118,14 @@ public class GTaskManager { mNidToGid = new HashMap(); } + /** + * 获取 GTaskManager 单例实例 + *

+ * 使用双重检查锁定确保线程安全的单例实现。 + *

+ * + * @return GTaskManager 单例实例 + */ public static synchronized GTaskManager getInstance() { if (mInstance == null) { mInstance = new GTaskManager(); @@ -106,11 +133,30 @@ public class GTaskManager { return mInstance; } + /** + * 设置 Activity 上下文 + *

+ * 用于获取 Google 账户的认证令牌。 + *

+ * + * @param activity Activity 上下文 + */ public synchronized void setActivityContext(Activity activity) { // used for getting authtoken mActivity = activity; } + /** + * 执行同步操作 + *

+ * 执行本地笔记与 Google Tasks 之间的双向同步。 + * 包括登录 Google Tasks、初始化任务列表、同步内容等步骤。 + *

+ * + * @param context 应用上下文 + * @param asyncTask 异步任务对象,用于发布进度 + * @return 同步状态码(STATE_SUCCESS、STATE_NETWORK_ERROR、STATE_INTERNAL_ERROR、STATE_SYNC_IN_PROGRESS 或 STATE_SYNC_CANCELLED) + */ public int sync(Context context, GTaskASyncTask asyncTask) { if (mSyncing) { Log.d(TAG, "Sync is in progress"); @@ -790,10 +836,21 @@ public class GTaskManager { } } + /** + * 获取同步账户名称 + * + * @return 当前同步的 Google 账户名称 + */ public String getSyncAccount() { - return GTaskClient.getInstance().getSyncAccount().name; + return mActivity == null ? null : GTaskClient.getInstance().getSyncAccount().name; } + /** + * 取消同步操作 + *

+ * 设置取消标志,停止正在进行的同步操作。 + *

+ */ public void cancelSync() { mCancelled = true; } diff --git a/src/Notes-master/src/net/micode/notes/gtask/remote/GTaskSyncService.java b/src/Notes-master/src/net/micode/notes/gtask/remote/GTaskSyncService.java index cca36f7..d207f97 100644 --- a/src/Notes-master/src/net/micode/notes/gtask/remote/GTaskSyncService.java +++ b/src/Notes-master/src/net/micode/notes/gtask/remote/GTaskSyncService.java @@ -23,54 +23,110 @@ import android.content.Intent; import android.os.Bundle; import android.os.IBinder; +/** + * Google Tasks 同步服务 + *

+ * 负责管理本地笔记与 Google Tasks 之间的后台同步操作。 + * 通过异步任务执行同步,支持同步状态广播和进度更新。 + *

+ */ public class GTaskSyncService extends Service { + /** Intent 附加参数名称,用于指定同步操作类型 */ public final static String ACTION_STRING_NAME = "sync_action_type"; + /** 启动同步操作的 Action 值 */ public final static int ACTION_START_SYNC = 0; + /** 取消同步操作的 Action 值 */ public final static int ACTION_CANCEL_SYNC = 1; + /** 无效的 Action 值 */ public final static int ACTION_INVALID = 2; + /** 同步服务广播名称 */ public final static String GTASK_SERVICE_BROADCAST_NAME = "net.micode.notes.gtask.remote.gtask_sync_service"; + /** 广播附加参数名称,用于标识是否正在同步 */ public final static String GTASK_SERVICE_BROADCAST_IS_SYNCING = "isSyncing"; + /** 广播附加参数名称,用于传递同步进度消息 */ public final static String GTASK_SERVICE_BROADCAST_PROGRESS_MSG = "progressMsg"; + /** 同步异步任务实例 */ private static GTaskASyncTask mSyncTask = null; + /** 同步进度消息 */ private static String mSyncProgress = ""; + /** + * 启动同步操作 + *

+ * 创建并执行 GTaskASyncTask 异步任务,监听同步完成事件。 + * 同步完成后发送广播并停止服务。 + *

+ */ private void startSync() { + // 检查是否已有同步任务在运行 if (mSyncTask == null) { mSyncTask = new GTaskASyncTask(this, new GTaskASyncTask.OnCompleteListener() { public void onComplete() { + // 清空同步任务引用 mSyncTask = null; + // 发送同步完成广播 sendBroadcast(""); + // 停止服务 stopSelf(); } }); + // 发送同步开始广播 sendBroadcast(""); + // 执行异步同步任务 mSyncTask.execute(); } } + /** + * 取消同步操作 + *

+ * 如果存在正在运行的同步任务,则调用其 cancelSync() 方法取消同步。 + *

+ */ private void cancelSync() { if (mSyncTask != null) { + // 取消异步同步任务 mSyncTask.cancelSync(); } } + /** + * 服务创建时的回调 + *

+ * 初始化同步任务为 null。 + *

+ */ @Override public void onCreate() { mSyncTask = null; } + /** + * 服务启动命令的回调 + *

+ * 根据 Intent 中的 Action 类型执行相应的同步操作。 + * 支持 ACTION_START_SYNC 和 ACTION_CANCEL_SYNC 两种操作。 + *

+ * + * @param intent 启动服务的 Intent,包含 Action 类型参数 + * @param flags 启动标志 + * @param startId 启动 ID + * @return START_STICKY 表示服务被杀死后会自动重启 + */ @Override public int onStartCommand(Intent intent, int flags, int startId) { Bundle bundle = intent.getExtras(); + // 检查 Intent 是否包含 Action 参数 if (bundle != null && bundle.containsKey(ACTION_STRING_NAME)) { + // 根据 Action 类型执行相应操作 switch (bundle.getInt(ACTION_STRING_NAME, ACTION_INVALID)) { case ACTION_START_SYNC: startSync(); @@ -86,42 +142,99 @@ public class GTaskSyncService extends Service { return super.onStartCommand(intent, flags, startId); } + /** + * 系统内存不足时的回调 + *

+ * 取消正在进行的同步操作以释放资源。 + *

+ */ @Override public void onLowMemory() { if (mSyncTask != null) { + // 取消同步任务以释放内存 mSyncTask.cancelSync(); } } + /** + * 绑定服务的回调 + *

+ * 本服务不支持绑定,返回 null。 + *

+ * + * @param intent 绑定服务的 Intent + * @return null,表示不支持绑定 + */ public IBinder onBind(Intent intent) { return null; } + /** + * 发送同步状态广播 + *

+ * 向应用发送广播,包含当前同步状态和进度消息。 + *

+ * + * @param msg 同步进度消息 + */ public void sendBroadcast(String msg) { + // 更新同步进度消息 mSyncProgress = msg; Intent intent = new Intent(GTASK_SERVICE_BROADCAST_NAME); + // 添加是否正在同步的标志 intent.putExtra(GTASK_SERVICE_BROADCAST_IS_SYNCING, mSyncTask != null); + // 添加进度消息 intent.putExtra(GTASK_SERVICE_BROADCAST_PROGRESS_MSG, msg); + // 发送广播 sendBroadcast(intent); } + /** + * 启动同步服务 + *

+ * 设置 Activity 上下文到 GTaskManager,然后启动同步服务执行同步操作。 + *

+ * + * @param activity Activity 上下文,用于获取 Google 账户认证信息 + */ public static void startSync(Activity activity) { + // 设置 Activity 上下文用于账户认证 GTaskManager.getInstance().setActivityContext(activity); Intent intent = new Intent(activity, GTaskSyncService.class); intent.putExtra(GTaskSyncService.ACTION_STRING_NAME, GTaskSyncService.ACTION_START_SYNC); + // 启动同步服务 activity.startService(intent); } + /** + * 取消同步服务 + *

+ * 启动同步服务并发送取消同步的命令。 + *

+ * + * @param context 应用上下文 + */ public static void cancelSync(Context context) { Intent intent = new Intent(context, GTaskSyncService.class); intent.putExtra(GTaskSyncService.ACTION_STRING_NAME, GTaskSyncService.ACTION_CANCEL_SYNC); + // 启动服务发送取消命令 context.startService(intent); } + /** + * 检查是否正在同步 + * + * @return 如果正在同步返回 true,否则返回 false + */ public static boolean isSyncing() { return mSyncTask != null; } + /** + * 获取同步进度消息 + * + * @return 当前同步进度消息字符串 + */ public static String getProgressString() { return mSyncProgress; } diff --git a/src/Notes-master/src/net/micode/notes/model/Note.java b/src/Notes-master/src/net/micode/notes/model/Note.java index 6706cf6..4cbd456 100644 --- a/src/Notes-master/src/net/micode/notes/model/Note.java +++ b/src/Notes-master/src/net/micode/notes/model/Note.java @@ -34,15 +34,36 @@ import net.micode.notes.data.Notes.TextNote; import java.util.ArrayList; +/** + * 笔记数据模型类 + *

+ * 负责管理笔记的基本信息和数据内容,支持笔记的创建、修改和同步操作。 + * 包含笔记元数据(如创建时间、修改时间、父文件夹等)和笔记数据(文本、通话记录等)。 + *

+ */ public class Note { + /** 笔记差异值,用于记录需要同步的字段变更 */ private ContentValues mNoteDiffValues; + + /** 笔记数据对象,包含文本数据和通话数据 */ private NoteData mNoteData; + + /** 日志标签 */ private static final String TAG = "Note"; + /** - * Create a new note id for adding a new note to databases + * 创建新笔记 ID + *

+ * 在数据库中创建一条新笔记记录,并返回其 ID。 + * 初始化笔记的创建时间、修改时间、类型和父文件夹 ID。 + *

+ * + * @param context 应用上下文 + * @param folderId 父文件夹 ID + * @return 新创建的笔记 ID,失败时返回 0 */ public static synchronized long getNewNoteId(Context context, long folderId) { - // Create a new note in the database + // 在数据库中创建新笔记 ContentValues values = new ContentValues(); long createdTime = System.currentTimeMillis(); values.put(NoteColumns.CREATED_DATE, createdTime); @@ -54,6 +75,7 @@ public class Note { long noteId = 0; try { + // 从 URI 中提取笔记 ID noteId = Long.valueOf(uri.getPathSegments().get(1)); } catch (NumberFormatException e) { Log.e(TAG, "Get note id error :" + e.toString()); @@ -65,41 +87,114 @@ public class Note { return noteId; } + /** + * 构造函数 + *

+ * 初始化笔记差异值和笔记数据对象。 + *

+ */ public Note() { mNoteDiffValues = new ContentValues(); mNoteData = new NoteData(); } + /** + * 设置笔记属性值 + *

+ * 设置笔记的指定属性值,并标记为本地修改。 + *

+ * + * @param key 属性键名 + * @param value 属性值 + */ public void setNoteValue(String key, String value) { mNoteDiffValues.put(key, value); mNoteDiffValues.put(NoteColumns.LOCAL_MODIFIED, 1); mNoteDiffValues.put(NoteColumns.MODIFIED_DATE, System.currentTimeMillis()); } + /** + * 设置文本数据 + *

+ * 设置笔记的文本数据内容。 + *

+ * + * @param key 数据键名 + * @param value 数据值 + */ public void setTextData(String key, String value) { mNoteData.setTextData(key, value); } + /** + * 设置文本数据 ID + *

+ * 设置笔记文本数据的数据库记录 ID。 + *

+ * + * @param id 文本数据 ID + */ public void setTextDataId(long id) { mNoteData.setTextDataId(id); } + /** + * 获取文本数据 ID + * + * @return 文本数据 ID + */ public long getTextDataId() { return mNoteData.mTextDataId; } + /** + * 设置通话数据 ID + *

+ * 设置笔记通话数据的数据库记录 ID。 + *

+ * + * @param id 通话数据 ID + */ public void setCallDataId(long id) { mNoteData.setCallDataId(id); } + /** + * 设置通话数据 + *

+ * 设置笔记的通话数据内容。 + *

+ * + * @param key 数据键名 + * @param value 数据值 + */ public void setCallData(String key, String value) { mNoteData.setCallData(key, value); } + /** + * 检查是否本地修改 + *

+ * 检查笔记是否有本地未同步的修改。 + *

+ * + * @return 如果有本地修改返回 true,否则返回 false + */ public boolean isLocalModified() { return mNoteDiffValues.size() > 0 || mNoteData.isLocalModified(); } + /** + * 同步笔记到数据库 + *

+ * 将笔记的本地修改同步到数据库。 + * 更新笔记元数据和数据内容。 + *

+ * + * @param context 应用上下文 + * @param noteId 笔记 ID + * @return 如果同步成功返回 true,否则返回 false + */ public boolean syncNote(Context context, long noteId) { if (noteId <= 0) { throw new IllegalArgumentException("Wrong note id:" + noteId); @@ -110,15 +205,14 @@ public class Note { } /** - * In theory, once data changed, the note should be updated on {@link NoteColumns#LOCAL_MODIFIED} and - * {@link NoteColumns#MODIFIED_DATE}. For data safety, though update note fails, we also update the - * note data info + * 理论上,数据变更后应更新 {@link NoteColumns#LOCAL_MODIFIED} 和 + * {@link NoteColumns#MODIFIED_DATE}。为数据安全,即使更新失败也更新笔记数据信息 */ if (context.getContentResolver().update( ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, noteId), mNoteDiffValues, null, null) == 0) { Log.e(TAG, "Update note error, should not happen"); - // Do not return, fall through + // 不返回,继续执行 } mNoteDiffValues.clear(); @@ -130,17 +224,35 @@ public class Note { return true; } + /** + * 笔记数据内部类 + *

+ * 管理笔记的文本数据和通话数据。 + * 支持数据的增删改查和批量同步操作。 + *

+ */ private class NoteData { + /** 文本数据 ID */ private long mTextDataId; + /** 文本数据值 */ private ContentValues mTextDataValues; + /** 通话数据 ID */ private long mCallDataId; + /** 通话数据值 */ private ContentValues mCallDataValues; + /** 日志标签 */ private static final String TAG = "NoteData"; + /** + * 构造函数 + *

+ * 初始化文本数据和通话数据的 ContentValues 对象。 + *

+ */ public NoteData() { mTextDataValues = new ContentValues(); mCallDataValues = new ContentValues(); @@ -148,10 +260,26 @@ public class Note { mCallDataId = 0; } + /** + * 检查是否本地修改 + *

+ * 检查文本数据或通话数据是否有本地未同步的修改。 + *

+ * + * @return 如果有本地修改返回 true,否则返回 false + */ boolean isLocalModified() { return mTextDataValues.size() > 0 || mCallDataValues.size() > 0; } + /** + * 设置文本数据 ID + *

+ * 设置文本数据的数据库记录 ID。 + *

+ * + * @param id 文本数据 ID,必须大于 0 + */ void setTextDataId(long id) { if(id <= 0) { throw new IllegalArgumentException("Text data id should larger than 0"); @@ -159,6 +287,14 @@ public class Note { mTextDataId = id; } + /** + * 设置通话数据 ID + *

+ * 设置通话数据的数据库记录 ID。 + *

+ * + * @param id 通话数据 ID,必须大于 0 + */ void setCallDataId(long id) { if (id <= 0) { throw new IllegalArgumentException("Call data id should larger than 0"); @@ -166,21 +302,50 @@ public class Note { mCallDataId = id; } + /** + * 设置通话数据 + *

+ * 设置笔记的通话数据内容,并标记为本地修改。 + *

+ * + * @param key 数据键名 + * @param value 数据值 + */ void setCallData(String key, String value) { mCallDataValues.put(key, value); mNoteDiffValues.put(NoteColumns.LOCAL_MODIFIED, 1); mNoteDiffValues.put(NoteColumns.MODIFIED_DATE, System.currentTimeMillis()); } + /** + * 设置文本数据 + *

+ * 设置笔记的文本数据内容,并标记为本地修改。 + *

+ * + * @param key 数据键名 + * @param value 数据值 + */ void setTextData(String key, String value) { mTextDataValues.put(key, value); mNoteDiffValues.put(NoteColumns.LOCAL_MODIFIED, 1); mNoteDiffValues.put(NoteColumns.MODIFIED_DATE, System.currentTimeMillis()); } + /** + * 将数据推送到 ContentResolver + *

+ * 将文本数据和通话数据的修改同步到数据库。 + * 支持新增和更新操作。 + *

+ * + * @param context 应用上下文 + * @param noteId 笔记 ID + * @return 笔记 URI,失败时返回 null + */ Uri pushIntoContentResolver(Context context, long noteId) { /** - * Check for safety + * 安全性检查 */ if (noteId <= 0) { throw new IllegalArgumentException("Wrong note id:" + noteId); @@ -189,9 +354,11 @@ public class Note { ArrayList operationList = new ArrayList(); ContentProviderOperation.Builder builder = null; + // 处理文本数据 if(mTextDataValues.size() > 0) { mTextDataValues.put(DataColumns.NOTE_ID, noteId); if (mTextDataId == 0) { + // 新增文本数据 mTextDataValues.put(DataColumns.MIME_TYPE, TextNote.CONTENT_ITEM_TYPE); Uri uri = context.getContentResolver().insert(Notes.CONTENT_DATA_URI, mTextDataValues); @@ -203,6 +370,7 @@ public class Note { return null; } } else { + // 更新现有文本数据 builder = ContentProviderOperation.newUpdate(ContentUris.withAppendedId( Notes.CONTENT_DATA_URI, mTextDataId)); builder.withValues(mTextDataValues); @@ -211,9 +379,11 @@ public class Note { mTextDataValues.clear(); } + // 处理通话数据 if(mCallDataValues.size() > 0) { mCallDataValues.put(DataColumns.NOTE_ID, noteId); if (mCallDataId == 0) { + // 新增通话数据 mCallDataValues.put(DataColumns.MIME_TYPE, CallNote.CONTENT_ITEM_TYPE); Uri uri = context.getContentResolver().insert(Notes.CONTENT_DATA_URI, mCallDataValues); @@ -225,6 +395,7 @@ public class Note { return null; } } else { + // 更新现有通话数据 builder = ContentProviderOperation.newUpdate(ContentUris.withAppendedId( Notes.CONTENT_DATA_URI, mCallDataId)); builder.withValues(mCallDataValues); @@ -233,6 +404,7 @@ public class Note { mCallDataValues.clear(); } + // 批量执行更新操作 if (operationList.size() > 0) { try { ContentProviderResult[] results = context.getContentResolver().applyBatch( diff --git a/src/Notes-master/src/net/micode/notes/model/WorkingNote.java b/src/Notes-master/src/net/micode/notes/model/WorkingNote.java index be081e4..3aa0cd3 100644 --- a/src/Notes-master/src/net/micode/notes/model/WorkingNote.java +++ b/src/Notes-master/src/net/micode/notes/model/WorkingNote.java @@ -32,36 +32,57 @@ import net.micode.notes.data.Notes.TextNote; import net.micode.notes.tool.ResourceParser.NoteBgResources; +/** + * 工作笔记类 + *

+ * 表示正在编辑或查看的笔记对象,提供笔记的加载、保存和修改功能。 + * 支持文本笔记和通话记录笔记两种类型,包含笔记的所有属性和设置监听器。 + *

+ */ public class WorkingNote { - // Note for the working note + /** 底层笔记对象 */ private Note mNote; - // Note Id + + /** 笔记 ID */ private long mNoteId; - // Note content + + /** 笔记内容 */ private String mContent; - // Note mode + + /** 笔记模式 */ private int mMode; + /** 提醒日期 */ private long mAlertDate; + /** 修改日期 */ private long mModifiedDate; + /** 背景颜色 ID */ private int mBgColorId; + /** Widget ID */ private int mWidgetId; + /** Widget 类型 */ private int mWidgetType; + /** 父文件夹 ID */ private long mFolderId; + /** 应用上下文 */ private Context mContext; + /** 日志标签 */ private static final String TAG = "WorkingNote"; + /** 是否已删除 */ private boolean mIsDeleted; + /** 笔记设置变更监听器 */ private NoteSettingChangedListener mNoteSettingStatusListener; + /** 数据查询投影 - 笔记数据 */ public static final String[] DATA_PROJECTION = new String[] { DataColumns.ID, DataColumns.CONTENT, @@ -72,6 +93,7 @@ public class WorkingNote { DataColumns.DATA4, }; + /** 数据查询投影 - 笔记元数据 */ public static final String[] NOTE_PROJECTION = new String[] { NoteColumns.PARENT_ID, NoteColumns.ALERTED_DATE, @@ -81,26 +103,45 @@ public class WorkingNote { NoteColumns.MODIFIED_DATE }; + /** 数据 ID 列索引 */ private static final int DATA_ID_COLUMN = 0; + /** 数据内容列索引 */ private static final int DATA_CONTENT_COLUMN = 1; + /** 数据 MIME 类型列索引 */ private static final int DATA_MIME_TYPE_COLUMN = 2; + /** 数据模式列索引 */ private static final int DATA_MODE_COLUMN = 3; + /** 笔记父 ID 列索引 */ private static final int NOTE_PARENT_ID_COLUMN = 0; + /** 笔记提醒日期列索引 */ private static final int NOTE_ALERTED_DATE_COLUMN = 1; + /** 笔记背景颜色 ID 列索引 */ private static final int NOTE_BG_COLOR_ID_COLUMN = 2; + /** 笔记 Widget ID 列索引 */ private static final int NOTE_WIDGET_ID_COLUMN = 3; + /** 笔记 Widget 类型列索引 */ private static final int NOTE_WIDGET_TYPE_COLUMN = 4; + /** 笔记修改日期列索引 */ private static final int NOTE_MODIFIED_DATE_COLUMN = 5; + /** + * 新建笔记构造函数 + *

+ * 创建一个新的空笔记对象,初始化所有属性为默认值。 + *

+ * + * @param context 应用上下文 + * @param folderId 父文件夹 ID + */ // New note construct private WorkingNote(Context context, long folderId) { mContext = context; @@ -114,6 +155,16 @@ public class WorkingNote { mWidgetType = Notes.TYPE_WIDGET_INVALIDE; } + /** + * 已有笔记构造函数 + *

+ * 从数据库加载现有笔记数据,初始化笔记对象。 + *

+ * + * @param context 应用上下文 + * @param noteId 笔记 ID + * @param folderId 父文件夹 ID + */ // Existing note construct private WorkingNote(Context context, long noteId, long folderId) { mContext = context; @@ -124,6 +175,12 @@ public class WorkingNote { loadNote(); } + /** + * 加载笔记元数据 + *

+ * 从数据库加载笔记的基本信息,包括父文件夹、背景颜色、Widget 信息等。 + *

+ */ private void loadNote() { Cursor cursor = mContext.getContentResolver().query( ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, mNoteId), NOTE_PROJECTION, null, @@ -146,6 +203,12 @@ public class WorkingNote { loadNoteData(); } + /** + * 加载笔记数据内容 + *

+ * 从数据库加载笔记的详细数据,包括文本内容和通话记录。 + *

+ */ private void loadNoteData() { Cursor cursor = mContext.getContentResolver().query(Notes.CONTENT_DATA_URI, DATA_PROJECTION, DataColumns.NOTE_ID + "=?", new String[] { @@ -157,10 +220,12 @@ public class WorkingNote { do { String type = cursor.getString(DATA_MIME_TYPE_COLUMN); if (DataConstants.NOTE.equals(type)) { + // 加载文本笔记数据 mContent = cursor.getString(DATA_CONTENT_COLUMN); mMode = cursor.getInt(DATA_MODE_COLUMN); mNote.setTextDataId(cursor.getLong(DATA_ID_COLUMN)); } else if (DataConstants.CALL_NOTE.equals(type)) { + // 加载通话记录数据 mNote.setCallDataId(cursor.getLong(DATA_ID_COLUMN)); } else { Log.d(TAG, "Wrong note type with type:" + type); @@ -174,6 +239,19 @@ public class WorkingNote { } } + /** + * 创建空笔记 + *

+ * 创建一个新的空笔记对象,并设置默认属性。 + *

+ * + * @param context 应用上下文 + * @param folderId 父文件夹 ID + * @param widgetId Widget ID + * @param widgetType Widget 类型 + * @param defaultBgColorId 默认背景颜色 ID + * @return 新创建的 WorkingNote 对象 + */ public static WorkingNote createEmptyNote(Context context, long folderId, int widgetId, int widgetType, int defaultBgColorId) { WorkingNote note = new WorkingNote(context, folderId); @@ -183,23 +261,45 @@ public class WorkingNote { return note; } + /** + * 加载已有笔记 + *

+ * 从数据库加载指定 ID 的笔记。 + *

+ * + * @param context 应用上下文 + * @param id 笔记 ID + * @return 加载的 WorkingNote 对象 + */ public static WorkingNote load(Context context, long id) { return new WorkingNote(context, id, 0); } + /** + * 保存笔记 + *

+ * 将笔记的修改保存到数据库。 + * 如果笔记不存在则创建新笔记,否则更新现有笔记。 + * 如果有 Widget 则更新 Widget 内容。 + *

+ * + * @return 如果保存成功返回 true,否则返回 false + */ public synchronized boolean saveNote() { if (isWorthSaving()) { if (!existInDatabase()) { + // 创建新笔记 if ((mNoteId = Note.getNewNoteId(mContext, mFolderId)) == 0) { Log.e(TAG, "Create new note fail with id:" + mNoteId); return false; } } + // 同步笔记数据 mNote.syncNote(mContext, mNoteId); /** - * Update widget content if there exist any widget of this note + * 如果存在该笔记的 Widget,则更新 Widget 内容 */ if (mWidgetId != AppWidgetManager.INVALID_APPWIDGET_ID && mWidgetType != Notes.TYPE_WIDGET_INVALIDE @@ -212,10 +312,23 @@ public class WorkingNote { } } + /** + * 检查笔记是否存在于数据库 + * + * @return 如果笔记 ID 大于 0 返回 true,否则返回 false + */ public boolean existInDatabase() { return mNoteId > 0; } + /** + * 检查是否值得保存 + *

+ * 判断笔记是否有需要保存的修改。 + *

+ * + * @return 如果值得保存返回 true,否则返回 false + */ private boolean isWorthSaving() { if (mIsDeleted || (!existInDatabase() && TextUtils.isEmpty(mContent)) || (existInDatabase() && !mNote.isLocalModified())) { @@ -225,10 +338,24 @@ public class WorkingNote { } } + /** + * 设置笔记设置变更监听器 + * + * @param l 监听器对象 + */ public void setOnSettingStatusChangedListener(NoteSettingChangedListener l) { mNoteSettingStatusListener = l; } + /** + * 设置提醒日期 + *

+ * 设置笔记的提醒日期,并通知监听器。 + *

+ * + * @param date 提醒日期(毫秒时间戳) + * @param set 是否设置提醒 + */ public void setAlertDate(long date, boolean set) { if (date != mAlertDate) { mAlertDate = date; @@ -239,6 +366,14 @@ public class WorkingNote { } } + /** + * 标记删除 + *

+ * 标记笔记为删除状态,并更新 Widget。 + *

+ * + * @param mark 是否标记为删除 + */ public void markDeleted(boolean mark) { mIsDeleted = mark; if (mWidgetId != AppWidgetManager.INVALID_APPWIDGET_ID @@ -247,6 +382,14 @@ public class WorkingNote { } } + /** + * 设置背景颜色 ID + *

+ * 设置笔记的背景颜色,并通知监听器。 + *

+ * + * @param id 背景颜色 ID + */ public void setBgColorId(int id) { if (id != mBgColorId) { mBgColorId = id; @@ -257,6 +400,14 @@ public class WorkingNote { } } + /** + * 设置清单模式 + *

+ * 设置笔记的编辑模式(普通模式或清单模式),并通知监听器。 + *

+ * + * @param mode 模式值 + */ public void setCheckListMode(int mode) { if (mMode != mode) { if (mNoteSettingStatusListener != null) { @@ -267,6 +418,11 @@ public class WorkingNote { } } + /** + * 设置 Widget 类型 + * + * @param type Widget 类型 + */ public void setWidgetType(int type) { if (type != mWidgetType) { mWidgetType = type; @@ -274,6 +430,11 @@ public class WorkingNote { } } + /** + * 设置 Widget ID + * + * @param id Widget ID + */ public void setWidgetId(int id) { if (id != mWidgetId) { mWidgetId = id; @@ -281,6 +442,14 @@ public class WorkingNote { } } + /** + * 设置工作文本 + *

+ * 设置笔记的文本内容。 + *

+ * + * @param text 文本内容 + */ public void setWorkingText(String text) { if (!TextUtils.equals(mContent, text)) { mContent = text; @@ -288,80 +457,159 @@ public class WorkingNote { } } + /** + * 转换为通话记录笔记 + *

+ * 将笔记转换为通话记录类型,设置电话号码和通话日期。 + *

+ * + * @param phoneNumber 电话号码 + * @param callDate 通话日期(毫秒时间戳) + */ public void convertToCallNote(String phoneNumber, long callDate) { mNote.setCallData(CallNote.CALL_DATE, String.valueOf(callDate)); mNote.setCallData(CallNote.PHONE_NUMBER, phoneNumber); mNote.setNoteValue(NoteColumns.PARENT_ID, String.valueOf(Notes.ID_CALL_RECORD_FOLDER)); } + /** + * 检查是否有提醒 + * + * @return 如果有提醒返回 true,否则返回 false + */ public boolean hasClockAlert() { return (mAlertDate > 0 ? true : false); } + /** + * 获取笔记内容 + * + * @return 笔记内容字符串 + */ public String getContent() { return mContent; } + /** + * 获取提醒日期 + * + * @return 提醒日期(毫秒时间戳) + */ public long getAlertDate() { return mAlertDate; } + /** + * 获取修改日期 + * + * @return 修改日期(毫秒时间戳) + */ public long getModifiedDate() { return mModifiedDate; } + /** + * 获取背景颜色资源 ID + * + * @return 背景颜色资源 ID + */ public int getBgColorResId() { return NoteBgResources.getNoteBgResource(mBgColorId); } + /** + * 获取背景颜色 ID + * + * @return 背景颜色 ID + */ public int getBgColorId() { return mBgColorId; } + /** + * 获取标题背景资源 ID + * + * @return 标题背景资源 ID + */ public int getTitleBgResId() { return NoteBgResources.getNoteTitleBgResource(mBgColorId); } + /** + * 获取清单模式 + * + * @return 清单模式值 + */ public int getCheckListMode() { return mMode; } + /** + * 获取笔记 ID + * + * @return 笔记 ID + */ public long getNoteId() { return mNoteId; } + /** + * 获取父文件夹 ID + * + * @return 父文件夹 ID + */ public long getFolderId() { return mFolderId; } + /** + * 获取 Widget ID + * + * @return Widget ID + */ public int getWidgetId() { return mWidgetId; } + /** + * 获取 Widget 类型 + * + * @return Widget 类型 + */ public int getWidgetType() { return mWidgetType; } + /** + * 笔记设置变更监听器接口 + *

+ * 定义笔记设置变更时的回调方法,用于通知 UI 更新。 + *

+ */ public interface NoteSettingChangedListener { /** - * Called when the background color of current note has just changed + * 当前笔记背景颜色变更时调用 */ void onBackgroundColorChanged(); /** - * Called when user set clock + * 用户设置闹钟时调用 + * + * @param date 提醒日期 + * @param set 是否设置提醒 */ void onClockAlertChanged(long date, boolean set); /** - * Call when user create note from widget + * 用户从 Widget 创建笔记时调用 */ void onWidgetChanged(); /** - * Call when switch between check list mode and normal mode - * @param oldMode is previous mode before change - * @param newMode is new mode + * 在清单模式和普通模式之间切换时调用 + * + * @param oldMode 变更前的模式 + * @param newMode 变更后的模式 */ void onCheckListModeChanged(int oldMode, int newMode); } diff --git a/src/Notes-master/src/net/micode/notes/tool/BackupUtils.java b/src/Notes-master/src/net/micode/notes/tool/BackupUtils.java index 39f6ec4..10c3994 100644 --- a/src/Notes-master/src/net/micode/notes/tool/BackupUtils.java +++ b/src/Notes-master/src/net/micode/notes/tool/BackupUtils.java @@ -36,11 +36,27 @@ import java.io.IOException; import java.io.PrintStream; +/** + * 备份工具类 + *

+ * 提供笔记数据导出为文本文件的功能。 + * 支持将笔记、文件夹、通话记录等数据导出到 SD 卡中。 + * 使用单例模式确保全局只有一个实例。 + *

+ */ public class BackupUtils { + /** 日志标签 */ private static final String TAG = "BackupUtils"; // Singleton stuff + /** 单例实例 */ private static BackupUtils sInstance; + /** + * 获取备份工具类的单例实例 + * + * @param context 应用上下文 + * @return 备份工具类实例 + */ public static synchronized BackupUtils getInstance(Context context) { if (sInstance == null) { sInstance = new BackupUtils(context); @@ -49,43 +65,84 @@ public class BackupUtils { } /** - * Following states are signs to represents backup or restore - * status + * 备份或恢复的状态常量 + *

+ * 以下状态常量用于表示备份或恢复操作的状态。 + *

*/ // Currently, the sdcard is not mounted + /** SD 卡未挂载 */ public static final int STATE_SD_CARD_UNMOUONTED = 0; // The backup file not exist + /** 备份文件不存在 */ public static final int STATE_BACKUP_FILE_NOT_EXIST = 1; // The data is not well formated, may be changed by other programs + /** 数据格式损坏,可能被其他程序修改 */ public static final int STATE_DATA_DESTROIED = 2; // Some run-time exception which causes restore or backup fails + /** 系统错误,运行时异常导致备份或恢复失败 */ public static final int STATE_SYSTEM_ERROR = 3; // Backup or restore success + /** 备份或恢复成功 */ public static final int STATE_SUCCESS = 4; + /** 文本导出对象 */ private TextExport mTextExport; + /** + * 私有构造函数 + * + * @param context 应用上下文 + */ private BackupUtils(Context context) { mTextExport = new TextExport(context); } + /** + * 检查外部存储是否可用 + * + * @return 如果外部存储已挂载且可读写则返回 true,否则返回 false + */ private static boolean externalStorageAvailable() { return Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState()); } + /** + * 导出笔记数据为文本文件 + * + * @return 导出状态码,可能为 STATE_SD_CARD_UNMOUONTED、STATE_SYSTEM_ERROR 或 STATE_SUCCESS + */ public int exportToText() { return mTextExport.exportToText(); } + /** + * 获取导出的文本文件名 + * + * @return 导出的文本文件名 + */ public String getExportedTextFileName() { return mTextExport.mFileName; } + /** + * 获取导出的文本文件目录 + * + * @return 导出的文本文件目录路径 + */ public String getExportedTextFileDir() { return mTextExport.mFileDirectory; } + /** + * 文本导出内部类 + *

+ * 负责将笔记数据导出为可读的文本文件。 + * 支持导出文件夹、笔记和通话记录等不同类型的数据。 + *

+ */ private static class TextExport { + /** 笔记查询投影字段 */ private static final String[] NOTE_PROJECTION = { NoteColumns.ID, NoteColumns.MODIFIED_DATE, @@ -93,12 +150,16 @@ public class BackupUtils { NoteColumns.TYPE }; + /** 笔记 ID 列索引 */ private static final int NOTE_COLUMN_ID = 0; + /** 笔记修改日期列索引 */ private static final int NOTE_COLUMN_MODIFIED_DATE = 1; + /** 笔记摘要列索引 */ private static final int NOTE_COLUMN_SNIPPET = 2; + /** 数据查询投影字段 */ private static final String[] DATA_PROJECTION = { DataColumns.CONTENT, DataColumns.MIME_TYPE, @@ -108,23 +169,39 @@ public class BackupUtils { DataColumns.DATA4, }; + /** 数据内容列索引 */ private static final int DATA_COLUMN_CONTENT = 0; + /** 数据 MIME 类型列索引 */ private static final int DATA_COLUMN_MIME_TYPE = 1; + /** 通话日期列索引 */ private static final int DATA_COLUMN_CALL_DATE = 2; + /** 电话号码列索引 */ private static final int DATA_COLUMN_PHONE_NUMBER = 4; + /** 导出文本格式数组 */ private final String [] TEXT_FORMAT; + /** 文件夹名称格式索引 */ private static final int FORMAT_FOLDER_NAME = 0; + /** 笔记日期格式索引 */ private static final int FORMAT_NOTE_DATE = 1; + /** 笔记内容格式索引 */ private static final int FORMAT_NOTE_CONTENT = 2; + /** 应用上下文 */ private Context mContext; + /** 导出文件名 */ private String mFileName; + /** 导出文件目录 */ private String mFileDirectory; + /** + * 构造函数 + * + * @param context 应用上下文 + */ public TextExport(Context context) { TEXT_FORMAT = context.getResources().getStringArray(R.array.format_for_exported_note); mContext = context; @@ -132,12 +209,24 @@ public class BackupUtils { mFileDirectory = ""; } + /** + * 获取指定格式的文本 + * + * @param id 格式索引 + * @return 格式化字符串 + */ private String getFormat(int id) { return TEXT_FORMAT[id]; } /** - * Export the folder identified by folder id to text + * 导出指定文件夹及其笔记到文本 + *

+ * 查询属于该文件夹的所有笔记,并将每个笔记的内容导出到输出流中。 + *

+ * + * @param folderId 文件夹 ID + * @param ps 输出流 */ private void exportFolderToText(String folderId, PrintStream ps) { // Query notes belong to this folder @@ -163,7 +252,13 @@ public class BackupUtils { } /** - * Export note identified by id to a print stream + * 导出指定笔记到输出流 + *

+ * 查询笔记的所有数据,根据 MIME 类型分别处理通话记录和普通笔记。 + *

+ * + * @param noteId 笔记 ID + * @param ps 输出流 */ private void exportNoteToText(String noteId, PrintStream ps) { Cursor dataCursor = mContext.getContentResolver().query(Notes.CONTENT_DATA_URI, @@ -216,7 +311,13 @@ public class BackupUtils { } /** - * Note will be exported as text which is user readable + * 导出笔记数据为文本文件 + *

+ * 将所有笔记、文件夹和通话记录导出为用户可读的文本文件。 + * 首先导出文件夹及其笔记,然后导出根目录下的笔记。 + *

+ * + * @return 导出状态码,可能为 STATE_SD_CARD_UNMOUONTED、STATE_SYSTEM_ERROR 或 STATE_SUCCESS */ public int exportToText() { if (!externalStorageAvailable()) { @@ -283,7 +384,12 @@ public class BackupUtils { } /** - * Get a print stream pointed to the file {@generateExportedTextFile} + * 获取导出文本文件的输出流 + *

+ * 在 SD 卡上创建导出文件,并返回对应的 PrintStream。 + *

+ * + * @return PrintStream 对象,如果创建失败则返回 null */ private PrintStream getExportToTextPrintStream() { File file = generateFileMountedOnSDcard(mContext, R.string.file_path, @@ -310,7 +416,15 @@ public class BackupUtils { } /** - * Generate the text file to store imported data + * 在 SD 卡上生成导出文本文件 + *

+ * 在指定的路径下创建导出文件,如果目录不存在则创建目录。 + *

+ * + * @param context 应用上下文 + * @param filePathResId 文件路径资源 ID + * @param fileNameFormatResId 文件名格式资源 ID + * @return 生成的文件对象,如果创建失败则返回 null */ private static File generateFileMountedOnSDcard(Context context, int filePathResId, int fileNameFormatResId) { StringBuilder sb = new StringBuilder(); @@ -325,9 +439,11 @@ public class BackupUtils { try { if (!filedir.exists()) { + // 创建目录 filedir.mkdir(); } if (!file.exists()) { + // 创建文件 file.createNewFile(); } return file; diff --git a/src/Notes-master/src/net/micode/notes/tool/DataUtils.java b/src/Notes-master/src/net/micode/notes/tool/DataUtils.java index 2a14982..d982351 100644 --- a/src/Notes-master/src/net/micode/notes/tool/DataUtils.java +++ b/src/Notes-master/src/net/micode/notes/tool/DataUtils.java @@ -35,8 +35,28 @@ import java.util.ArrayList; import java.util.HashSet; +/** + * 数据工具类 + *

+ * 提供笔记数据的批量操作、查询和统计功能。 + * 支持批量删除、移动笔记,以及各种数据查询操作。 + *

+ */ public class DataUtils { + /** 日志标签 */ public static final String TAG = "DataUtils"; + + /** + * 批量删除笔记 + *

+ * 从数据库中批量删除指定 ID 的笔记。 + * 跳过系统根文件夹,不允许删除系统文件夹。 + *

+ * + * @param resolver ContentResolver 对象 + * @param ids 要删除的笔记 ID 集合 + * @return 如果删除成功返回 true,否则返回 false + */ public static boolean batchDeleteNotes(ContentResolver resolver, HashSet ids) { if (ids == null) { Log.d(TAG, "the ids is null"); @@ -50,6 +70,7 @@ public class DataUtils { ArrayList operationList = new ArrayList(); for (long id : ids) { if(id == Notes.ID_ROOT_FOLDER) { + // 跳过系统根文件夹 Log.e(TAG, "Don't delete system folder root"); continue; } @@ -72,6 +93,17 @@ public class DataUtils { return false; } + /** + * 移动笔记到指定文件夹 + *

+ * 将笔记从源文件夹移动到目标文件夹,并记录原始父文件夹 ID。 + *

+ * + * @param resolver ContentResolver 对象 + * @param id 笔记 ID + * @param srcFolderId 源文件夹 ID + * @param desFolderId 目标文件夹 ID + */ public static void moveNoteToFoler(ContentResolver resolver, long id, long srcFolderId, long desFolderId) { ContentValues values = new ContentValues(); values.put(NoteColumns.PARENT_ID, desFolderId); @@ -80,6 +112,17 @@ public class DataUtils { resolver.update(ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, id), values, null, null); } + /** + * 批量移动笔记到指定文件夹 + *

+ * 将多个笔记批量移动到目标文件夹。 + *

+ * + * @param resolver ContentResolver 对象 + * @param ids 要移动的笔记 ID 集合 + * @param folderId 目标文件夹 ID + * @return 如果移动成功返回 true,否则返回 false + */ public static boolean batchMoveToFolder(ContentResolver resolver, HashSet ids, long folderId) { if (ids == null) { @@ -112,7 +155,14 @@ public class DataUtils { } /** - * Get the all folder count except system folders {@link Notes#TYPE_SYSTEM}} + * 获取用户文件夹数量 + *

+ * 统计除系统文件夹外的所有用户文件夹数量。 + * 排除回收站文件夹。 + *

+ * + * @param resolver ContentResolver 对象 + * @return 用户文件夹数量 */ public static int getUserFolderCount(ContentResolver resolver) { Cursor cursor =resolver.query(Notes.CONTENT_NOTE_URI, @@ -136,6 +186,17 @@ public class DataUtils { return count; } + /** + * 检查笔记是否在数据库中可见 + *

+ * 检查指定 ID 和类型的笔记是否在数据库中存在且可见(不在回收站)。 + *

+ * + * @param resolver ContentResolver 对象 + * @param noteId 笔记 ID + * @param type 笔记类型 + * @return 如果笔记可见返回 true,否则返回 false + */ public static boolean visibleInNoteDatabase(ContentResolver resolver, long noteId, int type) { Cursor cursor = resolver.query(ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, noteId), null, @@ -153,6 +214,16 @@ public class DataUtils { return exist; } + /** + * 检查笔记是否存在于数据库 + *

+ * 检查指定 ID 的笔记是否在数据库中存在。 + *

+ * + * @param resolver ContentResolver 对象 + * @param noteId 笔记 ID + * @return 如果笔记存在返回 true,否则返回 false + */ public static boolean existInNoteDatabase(ContentResolver resolver, long noteId) { Cursor cursor = resolver.query(ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, noteId), null, null, null, null); @@ -167,6 +238,16 @@ public class DataUtils { return exist; } + /** + * 检查数据是否存在于数据库 + *

+ * 检查指定 ID 的笔记数据是否在数据库中存在。 + *

+ * + * @param resolver ContentResolver 对象 + * @param dataId 数据 ID + * @return 如果数据存在返回 true,否则返回 false + */ public static boolean existInDataDatabase(ContentResolver resolver, long dataId) { Cursor cursor = resolver.query(ContentUris.withAppendedId(Notes.CONTENT_DATA_URI, dataId), null, null, null, null); @@ -181,6 +262,16 @@ public class DataUtils { return exist; } + /** + * 检查可见文件夹名称是否存在 + *

+ * 检查指定名称的文件夹是否在可见区域存在(不在回收站)。 + *

+ * + * @param resolver ContentResolver 对象 + * @param name 文件夹名称 + * @return 如果文件夹名称存在返回 true,否则返回 false + */ public static boolean checkVisibleFolderName(ContentResolver resolver, String name) { Cursor cursor = resolver.query(Notes.CONTENT_NOTE_URI, null, NoteColumns.TYPE + "=" + Notes.TYPE_FOLDER + @@ -197,6 +288,16 @@ public class DataUtils { return exist; } + /** + * 获取文件夹中的 Widget 信息 + *

+ * 获取指定文件夹下所有笔记关联的 Widget 信息。 + *

+ * + * @param resolver ContentResolver 对象 + * @param folderId 文件夹 ID + * @return Widget 属性集合,如果没有则返回 null + */ public static HashSet getFolderNoteWidget(ContentResolver resolver, long folderId) { Cursor c = resolver.query(Notes.CONTENT_NOTE_URI, new String[] { NoteColumns.WIDGET_ID, NoteColumns.WIDGET_TYPE }, @@ -224,6 +325,16 @@ public class DataUtils { return set; } + /** + * 根据笔记 ID 获取通话号码 + *

+ * 查询指定笔记 ID 关联的通话记录中的电话号码。 + *

+ * + * @param resolver ContentResolver 对象 + * @param noteId 笔记 ID + * @return 电话号码,如果未找到则返回空字符串 + */ public static String getCallNumberByNoteId(ContentResolver resolver, long noteId) { Cursor cursor = resolver.query(Notes.CONTENT_DATA_URI, new String [] { CallNote.PHONE_NUMBER }, @@ -243,6 +354,17 @@ public class DataUtils { return ""; } + /** + * 根据电话号码和通话日期获取笔记 ID + *

+ * 查询指定电话号码和通话日期对应的笔记 ID。 + *

+ * + * @param resolver ContentResolver 对象 + * @param phoneNumber 电话号码 + * @param callDate 通话日期(毫秒时间戳) + * @return 笔记 ID,如果未找到则返回 0 + */ public static long getNoteIdByPhoneNumberAndCallDate(ContentResolver resolver, String phoneNumber, long callDate) { Cursor cursor = resolver.query(Notes.CONTENT_DATA_URI, new String [] { CallNote.NOTE_ID }, @@ -264,6 +386,17 @@ public class DataUtils { return 0; } + /** + * 根据笔记 ID 获取摘要 + *

+ * 查询指定笔记 ID 的摘要内容。 + *

+ * + * @param resolver ContentResolver 对象 + * @param noteId 笔记 ID + * @return 笔记摘要 + * @throws IllegalArgumentException 如果笔记不存在 + */ public static String getSnippetById(ContentResolver resolver, long noteId) { Cursor cursor = resolver.query(Notes.CONTENT_NOTE_URI, new String [] { NoteColumns.SNIPPET }, @@ -282,9 +415,20 @@ public class DataUtils { throw new IllegalArgumentException("Note is not found with id: " + noteId); } + /** + * 格式化摘要内容 + *

+ * 去除摘要首尾空格,并截取到第一个换行符之前的内容。 + *

+ * + * @param snippet 原始摘要内容 + * @return 格式化后的摘要内容 + */ public static String getFormattedSnippet(String snippet) { if (snippet != null) { + // 去除首尾空格 snippet = snippet.trim(); + // 截取到第一个换行符之前的内容 int index = snippet.indexOf('\n'); if (index != -1) { snippet = snippet.substring(0, index); diff --git a/src/Notes-master/src/net/micode/notes/tool/GTaskStringUtils.java b/src/Notes-master/src/net/micode/notes/tool/GTaskStringUtils.java index 666b729..ce9eb54 100644 --- a/src/Notes-master/src/net/micode/notes/tool/GTaskStringUtils.java +++ b/src/Notes-master/src/net/micode/notes/tool/GTaskStringUtils.java @@ -16,98 +16,150 @@ package net.micode.notes.tool; +/** + * Google Tasks 字符串常量工具类 + *

+ * 定义与 Google Tasks 同步相关的所有 JSON 字段名称和常量。 + * 包括操作类型、实体类型、文件夹名称等常量定义。 + *

+ */ public class GTaskStringUtils { + /** 操作 ID */ public final static String GTASK_JSON_ACTION_ID = "action_id"; + /** 操作列表 */ public final static String GTASK_JSON_ACTION_LIST = "action_list"; + /** 操作类型 */ public final static String GTASK_JSON_ACTION_TYPE = "action_type"; + /** 创建操作类型 */ public final static String GTASK_JSON_ACTION_TYPE_CREATE = "create"; + /** 获取所有操作类型 */ public final static String GTASK_JSON_ACTION_TYPE_GETALL = "get_all"; + /** 移动操作类型 */ public final static String GTASK_JSON_ACTION_TYPE_MOVE = "move"; + /** 更新操作类型 */ public final static String GTASK_JSON_ACTION_TYPE_UPDATE = "update"; + /** 创建者 ID */ public final static String GTASK_JSON_CREATOR_ID = "creator_id"; + /** 子实体 */ public final static String GTASK_JSON_CHILD_ENTITY = "child_entity"; + /** 客户端版本 */ public final static String GTASK_JSON_CLIENT_VERSION = "client_version"; + /** 完成状态 */ public final static String GTASK_JSON_COMPLETED = "completed"; + /** 当前列表 ID */ public final static String GTASK_JSON_CURRENT_LIST_ID = "current_list_id"; + /** 默认列表 ID */ public final static String GTASK_JSON_DEFAULT_LIST_ID = "default_list_id"; + /** 删除标记 */ public final static String GTASK_JSON_DELETED = "deleted"; + /** 目标列表 */ public final static String GTASK_JSON_DEST_LIST = "dest_list"; + /** 目标父节点 */ public final static String GTASK_JSON_DEST_PARENT = "dest_parent"; + /** 目标父节点类型 */ public final static String GTASK_JSON_DEST_PARENT_TYPE = "dest_parent_type"; + /** 实体增量 */ public final static String GTASK_JSON_ENTITY_DELTA = "entity_delta"; + /** 实体类型 */ public final static String GTASK_JSON_ENTITY_TYPE = "entity_type"; + /** 获取已删除标记 */ public final static String GTASK_JSON_GET_DELETED = "get_deleted"; + /** ID */ public final static String GTASK_JSON_ID = "id"; + /** 索引 */ public final static String GTASK_JSON_INDEX = "index"; + /** 最后修改时间 */ public final static String GTASK_JSON_LAST_MODIFIED = "last_modified"; + /** 最新同步点 */ public final static String GTASK_JSON_LATEST_SYNC_POINT = "latest_sync_point"; + /** 列表 ID */ public final static String GTASK_JSON_LIST_ID = "list_id"; + /** 列表集合 */ public final static String GTASK_JSON_LISTS = "lists"; + /** 名称 */ public final static String GTASK_JSON_NAME = "name"; + /** 新 ID */ public final static String GTASK_JSON_NEW_ID = "new_id"; + /** 笔记集合 */ public final static String GTASK_JSON_NOTES = "notes"; + /** 父节点 ID */ public final static String GTASK_JSON_PARENT_ID = "parent_id"; + /** 前一个兄弟节点 ID */ public final static String GTASK_JSON_PRIOR_SIBLING_ID = "prior_sibling_id"; + /** 结果集合 */ public final static String GTASK_JSON_RESULTS = "results"; + /** 源列表 */ public final static String GTASK_JSON_SOURCE_LIST = "source_list"; + /** 任务集合 */ public final static String GTASK_JSON_TASKS = "tasks"; + /** 类型 */ public final static String GTASK_JSON_TYPE = "type"; + /** 分组类型 */ public final static String GTASK_JSON_TYPE_GROUP = "GROUP"; + /** 任务类型 */ public final static String GTASK_JSON_TYPE_TASK = "TASK"; + /** 用户信息 */ public final static String GTASK_JSON_USER = "user"; + /** MIUI 文件夹前缀 */ public final static String MIUI_FOLDER_PREFFIX = "[MIUI_Notes]"; + /** 默认文件夹名称 */ public final static String FOLDER_DEFAULT = "Default"; + /** 通话记录文件夹名称 */ public final static String FOLDER_CALL_NOTE = "Call_Note"; + /** 元数据文件夹名称 */ public final static String FOLDER_META = "METADATA"; + /** 元数据 GTask ID 头 */ public final static String META_HEAD_GTASK_ID = "meta_gid"; + /** 元数据笔记头 */ public final static String META_HEAD_NOTE = "meta_note"; + /** 元数据头 */ public final static String META_HEAD_DATA = "meta_data"; + /** 元数据笔记名称 */ public final static String META_NOTE_NAME = "[META INFO] DON'T UPDATE AND DELETE"; - } diff --git a/src/Notes-master/src/net/micode/notes/tool/ResourceParser.java b/src/Notes-master/src/net/micode/notes/tool/ResourceParser.java index 1ad3ad6..4677289 100644 --- a/src/Notes-master/src/net/micode/notes/tool/ResourceParser.java +++ b/src/Notes-master/src/net/micode/notes/tool/ResourceParser.java @@ -22,24 +22,57 @@ import android.preference.PreferenceManager; import net.micode.notes.R; import net.micode.notes.ui.NotesPreferenceActivity; +/** + * 资源解析工具类 + *

+ * 提供笔记背景颜色、字体大小、Widget 样式等资源的解析和获取功能。 + * 支持多种颜色主题和字体大小的配置。 + *

+ */ public class ResourceParser { + /** 黄色背景 */ public static final int YELLOW = 0; + + /** 蓝色背景 */ public static final int BLUE = 1; + + /** 白色背景 */ public static final int WHITE = 2; + + /** 绿色背景 */ public static final int GREEN = 3; + + /** 红色背景 */ public static final int RED = 4; + /** 默认背景颜色 */ public static final int BG_DEFAULT_COLOR = YELLOW; + /** 小号字体 */ public static final int TEXT_SMALL = 0; + + /** 中号字体 */ public static final int TEXT_MEDIUM = 1; + + /** 大号字体 */ public static final int TEXT_LARGE = 2; + + /** 超大号字体 */ public static final int TEXT_SUPER = 3; + /** 默认字体大小 */ public static final int BG_DEFAULT_FONT_SIZE = TEXT_MEDIUM; + /** + * 笔记背景资源类 + *

+ * 提供笔记编辑页面的背景颜色资源。 + * 包含编辑区域背景和标题栏背景两种资源。 + *

+ */ public static class NoteBgResources { + /** 编辑区域背景资源数组 */ private final static int [] BG_EDIT_RESOURCES = new int [] { R.drawable.edit_yellow, R.drawable.edit_blue, @@ -48,6 +81,7 @@ public class ResourceParser { R.drawable.edit_red }; + /** 标题栏背景资源数组 */ private final static int [] BG_EDIT_TITLE_RESOURCES = new int [] { R.drawable.edit_title_yellow, R.drawable.edit_title_blue, @@ -56,25 +90,56 @@ public class ResourceParser { R.drawable.edit_title_red }; + /** + * 获取笔记编辑区域背景资源 ID + * + * @param id 背景颜色 ID(0-4) + * @return 背景资源 ID + */ public static int getNoteBgResource(int id) { return BG_EDIT_RESOURCES[id]; } + /** + * 获取笔记标题栏背景资源 ID + * + * @param id 背景颜色 ID(0-4) + * @return 标题栏背景资源 ID + */ public static int getNoteTitleBgResource(int id) { return BG_EDIT_TITLE_RESOURCES[id]; } } + /** + * 获取默认背景颜色 ID + *

+ * 根据用户设置返回默认背景颜色。 + * 如果用户启用了随机背景颜色,则随机返回一个颜色 ID。 + *

+ * + * @param context 应用上下文 + * @return 背景颜色 ID(0-4) + */ public static int getDefaultBgId(Context context) { if (PreferenceManager.getDefaultSharedPreferences(context).getBoolean( NotesPreferenceActivity.PREFERENCE_SET_BG_COLOR_KEY, false)) { + // 随机选择背景颜色 return (int) (Math.random() * NoteBgResources.BG_EDIT_RESOURCES.length); } else { return BG_DEFAULT_COLOR; } } + /** + * 笔记列表项背景资源类 + *

+ * 提供笔记列表项的背景颜色资源。 + * 包含首项、中间项、末项和单项四种样式。 + *

+ */ public static class NoteItemBgResources { + /** 首项背景资源数组 */ private final static int [] BG_FIRST_RESOURCES = new int [] { R.drawable.list_yellow_up, R.drawable.list_blue_up, @@ -83,6 +148,7 @@ public class ResourceParser { R.drawable.list_red_up }; + /** 中间项背景资源数组 */ private final static int [] BG_NORMAL_RESOURCES = new int [] { R.drawable.list_yellow_middle, R.drawable.list_blue_middle, @@ -91,6 +157,7 @@ public class ResourceParser { R.drawable.list_red_middle }; + /** 末项背景资源数组 */ private final static int [] BG_LAST_RESOURCES = new int [] { R.drawable.list_yellow_down, R.drawable.list_blue_down, @@ -99,6 +166,7 @@ public class ResourceParser { R.drawable.list_red_down, }; + /** 单项背景资源数组 */ private final static int [] BG_SINGLE_RESOURCES = new int [] { R.drawable.list_yellow_single, R.drawable.list_blue_single, @@ -107,28 +175,65 @@ public class ResourceParser { R.drawable.list_red_single }; + /** + * 获取笔记列表首项背景资源 ID + * + * @param id 背景颜色 ID(0-4) + * @return 首项背景资源 ID + */ public static int getNoteBgFirstRes(int id) { return BG_FIRST_RESOURCES[id]; } + /** + * 获取笔记列表末项背景资源 ID + * + * @param id 背景颜色 ID(0-4) + * @return 末项背景资源 ID + */ public static int getNoteBgLastRes(int id) { return BG_LAST_RESOURCES[id]; } + /** + * 获取笔记列表单项背景资源 ID + * + * @param id 背景颜色 ID(0-4) + * @return 单项背景资源 ID + */ public static int getNoteBgSingleRes(int id) { return BG_SINGLE_RESOURCES[id]; } + /** + * 获取笔记列表中间项背景资源 ID + * + * @param id 背景颜色 ID(0-4) + * @return 中间项背景资源 ID + */ public static int getNoteBgNormalRes(int id) { return BG_NORMAL_RESOURCES[id]; } + /** + * 获取文件夹背景资源 ID + * + * @return 文件夹背景资源 ID + */ public static int getFolderBgRes() { return R.drawable.list_folder; } } + /** + * Widget 背景资源类 + *

+ * 提供桌面 Widget 的背景颜色资源。 + * 支持 2x2 和 4x4 两种尺寸的 Widget。 + *

+ */ public static class WidgetBgResources { + /** 2x2 Widget 背景资源数组 */ private final static int [] BG_2X_RESOURCES = new int [] { R.drawable.widget_2x_yellow, R.drawable.widget_2x_blue, @@ -137,10 +242,17 @@ public class ResourceParser { R.drawable.widget_2x_red, }; + /** + * 获取 2x2 Widget 背景资源 ID + * + * @param id 背景颜色 ID(0-4) + * @return 2x2 Widget 背景资源 ID + */ public static int getWidget2xBgResource(int id) { return BG_2X_RESOURCES[id]; } + /** 4x4 Widget 背景资源数组 */ private final static int [] BG_4X_RESOURCES = new int [] { R.drawable.widget_4x_yellow, R.drawable.widget_4x_blue, @@ -149,12 +261,26 @@ public class ResourceParser { R.drawable.widget_4x_red }; + /** + * 获取 4x4 Widget 背景资源 ID + * + * @param id 背景颜色 ID(0-4) + * @return 4x4 Widget 背景资源 ID + */ public static int getWidget4xBgResource(int id) { return BG_4X_RESOURCES[id]; } } + /** + * 文本外观资源类 + *

+ * 提供笔记文本的字体样式资源。 + * 支持四种字体大小:小、中、大、超大。 + *

+ */ public static class TextAppearanceResources { + /** 文本外观样式资源数组 */ private final static int [] TEXTAPPEARANCE_RESOURCES = new int [] { R.style.TextAppearanceNormal, R.style.TextAppearanceMedium, @@ -162,11 +288,20 @@ public class ResourceParser { R.style.TextAppearanceSuper }; + /** + * 获取文本外观样式资源 ID + *

+ * 如果 ID 超出范围,则返回默认字体大小。 + *

+ * + * @param id 字体大小 ID(0-3) + * @return 文本外观样式资源 ID + */ public static int getTexAppearanceResource(int id) { /** - * 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} + * HACKME: 修复在 SharedPreferences 中存储资源 ID 的 bug。 + * ID 可能大于资源数组的长度,在这种情况下, + * 返回 {@link ResourceParser#BG_DEFAULT_FONT_SIZE} */ if (id >= TEXTAPPEARANCE_RESOURCES.length) { return BG_DEFAULT_FONT_SIZE; @@ -174,6 +309,11 @@ public class ResourceParser { return TEXTAPPEARANCE_RESOURCES[id]; } + /** + * 获取文本外观资源数量 + * + * @return 资源数量 + */ public static int getResourcesSize() { return TEXTAPPEARANCE_RESOURCES.length; } diff --git a/src/Notes-master/src/net/micode/notes/ui/AlarmAlertActivity.java b/src/Notes-master/src/net/micode/notes/ui/AlarmAlertActivity.java index 85723be..09181bf 100644 --- a/src/Notes-master/src/net/micode/notes/ui/AlarmAlertActivity.java +++ b/src/Notes-master/src/net/micode/notes/ui/AlarmAlertActivity.java @@ -39,33 +39,70 @@ import net.micode.notes.tool.DataUtils; import java.io.IOException; - +/** + * 闹钟提醒活动 + * + * 这个类负责显示笔记提醒的闹钟界面,当笔记设置的提醒时间到达时, + * 由AlarmReceiver启动此活动,显示笔记内容摘要并播放闹钟声音。 + * + * 主要功能: + * 1. 在锁屏状态下显示闹钟界面 + * 2. 显示笔记内容摘要 + * 3. 播放系统闹钟声音 + * 4. 提供操作选项(关闭提醒或查看笔记) + * + * @see NoteEditActivity + * @see net.micode.notes.tool.DataUtils + */ public class AlarmAlertActivity extends Activity implements OnClickListener, OnDismissListener { + // 当前提醒的笔记ID private long mNoteId; + // 笔记内容摘要 private String mSnippet; + // 摘要预览最大长度 private static final int SNIPPET_PREW_MAX_LEN = 60; + // 媒体播放器,用于播放闹钟声音 MediaPlayer mPlayer; + /** + * 活动创建时的初始化方法 + * + * 设置窗口属性,获取笔记信息,检查笔记是否存在, + * 如果存在则显示提醒对话框并播放闹钟声音 + * + * @param savedInstanceState 保存的实例状态 + */ @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); + // 请求无标题窗口 requestWindowFeature(Window.FEATURE_NO_TITLE); final Window win = getWindow(); + // 添加FLAG_SHOW_WHEN_LOCKED标志,使活动可以在锁屏界面上显示 win.addFlags(WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED); + // 如果屏幕当前是关闭状态,添加以下标志 if (!isScreenOn()) { + // 保持屏幕常亮 + // 打开屏幕 + // 允许在屏幕亮起时锁定 + // 设置窗口布局包含系统装饰区域 win.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON | WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON | WindowManager.LayoutParams.FLAG_ALLOW_LOCK_WHILE_SCREEN_ON | WindowManager.LayoutParams.FLAG_LAYOUT_INSET_DECOR); } + // 获取启动此活动的Intent Intent intent = getIntent(); try { + // 从Intent中解析出笔记ID mNoteId = Long.valueOf(intent.getData().getPathSegments().get(1)); + // 通过笔记ID获取笔记内容摘要 mSnippet = DataUtils.getSnippetById(this.getContentResolver(), mNoteId); + // 如果摘要超过最大长度,截取并添加省略号 mSnippet = mSnippet.length() > SNIPPET_PREW_MAX_LEN ? mSnippet.substring(0, SNIPPET_PREW_MAX_LEN) + getResources().getString(R.string.notelist_string_info) : mSnippet; @@ -74,85 +111,150 @@ public class AlarmAlertActivity extends Activity implements OnClickListener, OnD return; } + // 初始化媒体播放器 mPlayer = new MediaPlayer(); + // 检查笔记是否在数据库中存在且可见 if (DataUtils.visibleInNoteDatabase(getContentResolver(), mNoteId, Notes.TYPE_NOTE)) { + // 显示操作对话框 showActionDialog(); + // 播放闹钟声音 playAlarmSound(); } else { + // 如果笔记不存在,直接关闭活动 finish(); } } + /** + * 检查屏幕是否处于开启状态 + * + * @return 如果屏幕开启返回true,否则返回false + */ private boolean isScreenOn() { PowerManager pm = (PowerManager) getSystemService(Context.POWER_SERVICE); return pm.isScreenOn(); } + /** + * 播放闹钟声音 + * + * 获取系统默认的闹钟铃声,设置音频流类型, + * 并循环播放闹钟声音 + */ private void playAlarmSound() { + // 获取系统默认的闹钟铃声URI Uri url = RingtoneManager.getActualDefaultRingtoneUri(this, RingtoneManager.TYPE_ALARM); + // 获取受静音模式影响的音频流类型 int silentModeStreams = Settings.System.getInt(getContentResolver(), Settings.System.MODE_RINGER_STREAMS_AFFECTED, 0); + // 如果闹钟音频流受静音模式影响,使用受影响的流类型 if ((silentModeStreams & (1 << AudioManager.STREAM_ALARM)) != 0) { mPlayer.setAudioStreamType(silentModeStreams); } else { + // 否则使用标准闹钟音频流 mPlayer.setAudioStreamType(AudioManager.STREAM_ALARM); } try { + // 设置音频源 mPlayer.setDataSource(this, url); + // 准备播放 mPlayer.prepare(); + // 设置循环播放 mPlayer.setLooping(true); + // 开始播放 mPlayer.start(); } catch (IllegalArgumentException e) { - // TODO Auto-generated catch block e.printStackTrace(); } catch (SecurityException e) { - // TODO Auto-generated catch block e.printStackTrace(); } catch (IllegalStateException e) { - // TODO Auto-generated catch block e.printStackTrace(); } catch (IOException e) { - // TODO Auto-generated catch block e.printStackTrace(); } } + /** + * 显示操作对话框 + * + * 创建一个AlertDialog,显示笔记摘要和操作按钮 + * 当屏幕开启时,显示"查看笔记"按钮 + */ private void showActionDialog() { + // 创建AlertDialog构建器 AlertDialog.Builder dialog = new AlertDialog.Builder(this); + // 设置对话框标题为应用名称 dialog.setTitle(R.string.app_name); + // 设置对话框内容为笔记摘要 dialog.setMessage(mSnippet); + // 添加"确定"按钮,点击事件由当前类处理 dialog.setPositiveButton(R.string.notealert_ok, this); + // 如果屏幕是开启状态,添加"查看笔记"按钮 if (isScreenOn()) { dialog.setNegativeButton(R.string.notealert_enter, this); } + // 显示对话框并设置关闭监听器 dialog.show().setOnDismissListener(this); } + /** + * 对话框按钮点击事件处理 + * + * 处理用户在提醒对话框中的按钮点击操作,根据点击的按钮执行相应的操作 + * + * @param dialog 触发点击事件的对话框对象,不能为 null + * @param which 点击的按钮ID,取值为 DialogInterface.BUTTON_POSITIVE(确定按钮) + * 或 DialogInterface.BUTTON_NEGATIVE(查看笔记按钮) + */ public void onClick(DialogInterface dialog, int which) { switch (which) { + // 如果点击了"查看笔记"按钮(负按钮) case DialogInterface.BUTTON_NEGATIVE: + // 创建跳转到笔记编辑活动的Intent Intent intent = new Intent(this, NoteEditActivity.class); + // 设置动作为查看 intent.setAction(Intent.ACTION_VIEW); + // 传递笔记ID intent.putExtra(Intent.EXTRA_UID, mNoteId); + // 启动笔记编辑活动 startActivity(intent); break; + // 默认情况(点击"确定"按钮) default: break; } } + /** + * 对话框关闭事件处理 + * + * 当对话框被关闭时(无论是点击按钮还是外部点击), + * 停止闹钟声音并关闭当前活动 + * + * @param dialog 被关闭的对话框对象,不能为 null + */ public void onDismiss(DialogInterface dialog) { + // 停止闹钟声音 stopAlarmSound(); + // 关闭当前活动 finish(); } + /** + * 停止闹钟声音 + * + * 停止媒体播放器,释放资源并将播放器对象置空 + */ private void stopAlarmSound() { if (mPlayer != null) { + // 停止播放 mPlayer.stop(); + // 释放资源 mPlayer.release(); + // 将播放器对象置空 mPlayer = null; } } -} +} \ No newline at end of file diff --git a/src/Notes-master/src/net/micode/notes/ui/AlarmInitReceiver.java b/src/Notes-master/src/net/micode/notes/ui/AlarmInitReceiver.java index f221202..c00b5c6 100644 --- a/src/Notes-master/src/net/micode/notes/ui/AlarmInitReceiver.java +++ b/src/Notes-master/src/net/micode/notes/ui/AlarmInitReceiver.java @@ -16,50 +16,91 @@ package net.micode.notes.ui; -import android.app.AlarmManager; -import android.app.PendingIntent; -import android.content.BroadcastReceiver; -import android.content.ContentUris; -import android.content.Context; -import android.content.Intent; -import android.database.Cursor; - -import net.micode.notes.data.Notes; -import net.micode.notes.data.Notes.NoteColumns; +import android.app.AlarmManager; // 系统闹钟管理器,用于设置和管理系统级闹钟 +import android.app.PendingIntent; // 延迟意图,用于在指定时间触发操作 +import android.content.BroadcastReceiver; // 广播接收器基类,用于接收系统广播 +import android.content.ContentUris; // 用于处理内容URI的工具类 +import android.content.Context; // 应用上下文,提供访问应用环境和资源的接口 +import android.content.Intent; // 意图,用于组件间通信 +import android.database.Cursor; // 数据库游标,用于遍历查询结果 +import net.micode.notes.data.Notes; // 笔记数据相关类 +import net.micode.notes.data.Notes.NoteColumns; // 笔记表列定义 +/** + * 闹钟初始化接收器 + * + * 这个类继承自BroadcastReceiver,用于在系统启动或应用需要时重新初始化所有未触发的笔记提醒闹钟。 + * 它会查询数据库中所有设置了提醒时间且未过期的笔记,并为每个笔记设置系统闹钟。 + * + * 主要触发时机: + * 1. 系统启动完成时(接收BOOT_COMPLETED广播) + * 2. 应用安装或更新后可能需要手动触发 + */ public class AlarmInitReceiver extends BroadcastReceiver { + /** + * 数据库查询投影,指定需要从笔记表中获取的列 + * 只需要ID和提醒日期两列,用于设置闹钟 + */ private static final String [] PROJECTION = new String [] { - NoteColumns.ID, - NoteColumns.ALERTED_DATE + NoteColumns.ID, // 笔记ID + NoteColumns.ALERTED_DATE // 提醒日期 }; - private static final int COLUMN_ID = 0; - private static final int COLUMN_ALERTED_DATE = 1; + // 列索引常量,用于从查询结果中获取对应列的数据 + private static final int COLUMN_ID = 0; // ID列在结果集中的索引 + private static final int COLUMN_ALERTED_DATE = 1; // 提醒日期列在结果集中的索引 + /** + * 接收广播后的处理方法 + * + * 当接收到广播(通常是系统启动完成广播)时,此方法会被调用。 + * 它会查询所有未过期的笔记提醒,并为每个笔记设置系统闹钟。 + * + * @param context 应用上下文,用于访问系统服务和资源 + * @param intent 接收到的广播意图 + */ @Override public void onReceive(Context context, Intent intent) { + // 获取当前系统时间,作为查询条件 long currentDate = System.currentTimeMillis(); + + // 查询所有提醒时间晚于当前时间的笔记 + // 查询条件:提醒日期 > 当前时间 AND 笔记类型 = 普通笔记 Cursor c = context.getContentResolver().query(Notes.CONTENT_NOTE_URI, - PROJECTION, - NoteColumns.ALERTED_DATE + ">? AND " + NoteColumns.TYPE + "=" + Notes.TYPE_NOTE, - new String[] { String.valueOf(currentDate) }, - null); + PROJECTION, // 指定查询的列 + NoteColumns.ALERTED_DATE + ">? AND " + NoteColumns.TYPE + "=" + Notes.TYPE_NOTE, // 查询条件 + new String[] { String.valueOf(currentDate) }, // 查询参数 + null); // 排序方式,null表示默认排序 + // 处理查询结果 if (c != null) { + // 如果有查询结果,遍历所有符合条件的笔记 if (c.moveToFirst()) { do { + // 获取笔记的提醒时间 long alertDate = c.getLong(COLUMN_ALERTED_DATE); + + // 创建一个指向AlarmReceiver的Intent,用于在闹钟触发时接收广播 Intent sender = new Intent(context, AlarmReceiver.class); + // 将笔记ID作为URI数据附加到Intent中,这样AlarmReceiver就能知道是哪个笔记的闹钟触发了 sender.setData(ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, c.getLong(COLUMN_ID))); + + // 创建PendingIntent,它封装了上述Intent,可以在指定时间触发 PendingIntent pendingIntent = PendingIntent.getBroadcast(context, 0, sender, 0); + + // 获取系统闹钟服务 AlarmManager alermManager = (AlarmManager) context .getSystemService(Context.ALARM_SERVICE); + + // 设置闹钟 + // 使用RTC_WAKEUP模式,即使设备处于睡眠状态也会唤醒设备并触发广播 alermManager.set(AlarmManager.RTC_WAKEUP, alertDate, pendingIntent); - } while (c.moveToNext()); + } while (c.moveToNext()); // 移动到下一条记录 } + // 关闭游标,释放资源 c.close(); } } -} +} \ No newline at end of file diff --git a/src/Notes-master/src/net/micode/notes/ui/AlarmReceiver.java b/src/Notes-master/src/net/micode/notes/ui/AlarmReceiver.java index 54e503b..1ef8a05 100644 --- a/src/Notes-master/src/net/micode/notes/ui/AlarmReceiver.java +++ b/src/Notes-master/src/net/micode/notes/ui/AlarmReceiver.java @@ -16,15 +16,48 @@ package net.micode.notes.ui; -import android.content.BroadcastReceiver; -import android.content.Context; -import android.content.Intent; +import android.content.BroadcastReceiver; // 广播接收器基类,用于接收系统广播 +import android.content.Context; // 应用上下文,提供访问应用环境和资源的接口 +import android.content.Intent; // 意图,用于组件间通信 +/** + * 闹钟接收器 + * + * 这个类继承自BroadcastReceiver,用于接收由AlarmManager设置的闹钟触发事件。 + * 当笔记的提醒时间到达时,AlarmManager会发送一个广播,这个接收器会接收该广播 + * 并启动闹钟提醒界面(AlarmAlertActivity)来显示提醒信息。 + * + * 工作流程: + * 1. AlarmInitReceiver为每个设置了提醒时间的笔记设置系统闹钟 + * 2. 当提醒时间到达时,系统发送广播 + * 3. AlarmReceiver接收广播并启动AlarmAlertActivity显示提醒 + */ public class AlarmReceiver extends BroadcastReceiver { + + /** + * 接收闹钟广播后的处理方法 + * + * 当闹钟时间到达时,系统会发送广播,此方法会被调用。 + * 它会将接收到的Intent重新定向到AlarmAlertActivity,并添加FLAG_ACTIVITY_NEW_TASK标志 + * 确保即使在非UI上下文中也能启动Activity。 + * + * @param context 应用上下文,用于启动Activity + * @param intent 接收到的闹钟广播Intent,包含触发闹钟的笔记ID等信息 + */ @Override public void onReceive(Context context, Intent intent) { + // 将Intent的目标组件设置为AlarmAlertActivity + // 这样当启动Activity时就会显示闹钟提醒界面 intent.setClass(context, AlarmAlertActivity.class); + + // 添加FLAG_ACTIVITY_NEW_TASK标志 + // 这是必需的,因为从非Activity上下文(如BroadcastReceiver)启动Activity时, + // 必须指定这个标志,表示启动一个新的任务栈 intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + + // 启动AlarmAlertActivity显示闹钟提醒 + // 原始Intent中包含了触发闹钟的笔记ID等信息,AlarmAlertActivity会使用这些信息 + // 来显示相应的笔记内容和提醒信息 context.startActivity(intent); } -} +} \ No newline at end of file diff --git a/src/Notes-master/src/net/micode/notes/ui/DateTimePicker.java b/src/Notes-master/src/net/micode/notes/ui/DateTimePicker.java index 496b0cd..015522e 100644 --- a/src/Notes-master/src/net/micode/notes/ui/DateTimePicker.java +++ b/src/Notes-master/src/net/micode/notes/ui/DateTimePicker.java @@ -28,6 +28,28 @@ import android.view.View; import android.widget.FrameLayout; import android.widget.NumberPicker; +/** + * 日期时间选择器 + *

+ * 继承自FrameLayout,提供日期和时间选择的自定义视图组件。 + * 使用NumberPicker组件实现日期、小时、分钟和上午/下午的选择功能。 + * 支持24小时制和12小时制两种显示模式。 + *

+ *

+ * 主要功能: + *

    + *
  • 显示日期选择器(显示前后3天,共7天)
  • + *
  • 显示小时选择器(24小时制:0-23,12小时制:1-12)
  • + *
  • 显示分钟选择器(0-59)
  • + *
  • 显示上午/下午选择器(仅12小时制)
  • + *
  • 支持设置日期时间变更监听器
  • + *
  • 支持启用/禁用状态切换
  • + *
+ *

+ * + * @see NumberPicker + * @see OnDateTimeChangedListener + */ public class DateTimePicker extends FrameLayout { private static final boolean DEFAULT_ENABLE_STATE = true; @@ -64,36 +86,55 @@ public class DateTimePicker extends FrameLayout { private OnDateTimeChangedListener mOnDateTimeChangedListener; + /** + * 日期变更监听器 + *

+ * 监听日期选择器的值变化,更新内部日期对象并通知外部监听器。 + *

+ */ private NumberPicker.OnValueChangeListener mOnDateChangedListener = new NumberPicker.OnValueChangeListener() { @Override public void onValueChange(NumberPicker picker, int oldVal, int newVal) { + // 根据选择器的变化调整日期 mDate.add(Calendar.DAY_OF_YEAR, newVal - oldVal); updateDateControl(); onDateTimeChanged(); } }; + /** + * 小时变更监听器 + *

+ * 监听小时选择器的值变化,处理跨日情况(如从23点变为0点或从0点变为23点), + * 在12小时制下处理上午/下午的切换。 + *

+ */ private NumberPicker.OnValueChangeListener mOnHourChangedListener = new NumberPicker.OnValueChangeListener() { @Override public void onValueChange(NumberPicker picker, int oldVal, int newVal) { boolean isDateChanged = false; Calendar cal = Calendar.getInstance(); + // 处理12小时制下的跨日情况 if (!mIs24HourView) { + // 从下午11点变为12点,日期加1天 if (!mIsAm && oldVal == HOURS_IN_HALF_DAY - 1 && newVal == HOURS_IN_HALF_DAY) { cal.setTimeInMillis(mDate.getTimeInMillis()); cal.add(Calendar.DAY_OF_YEAR, 1); isDateChanged = true; + // 从12点变为下午11点,日期减1天 } else if (mIsAm && oldVal == HOURS_IN_HALF_DAY && newVal == HOURS_IN_HALF_DAY - 1) { cal.setTimeInMillis(mDate.getTimeInMillis()); cal.add(Calendar.DAY_OF_YEAR, -1); isDateChanged = true; } + // 切换上午/下午 if (oldVal == HOURS_IN_HALF_DAY - 1 && newVal == HOURS_IN_HALF_DAY || oldVal == HOURS_IN_HALF_DAY && newVal == HOURS_IN_HALF_DAY - 1) { mIsAm = !mIsAm; updateAmPmControl(); } } else { + // 处理24小时制下的跨日情况 if (oldVal == HOURS_IN_ALL_DAY - 1 && newVal == 0) { cal.setTimeInMillis(mDate.getTimeInMillis()); cal.add(Calendar.DAY_OF_YEAR, 1); @@ -104,9 +145,11 @@ public class DateTimePicker extends FrameLayout { isDateChanged = true; } } + // 计算新的小时数 int newHour = mHourSpinner.getValue() % HOURS_IN_HALF_DAY + (mIsAm ? 0 : HOURS_IN_HALF_DAY); mDate.set(Calendar.HOUR_OF_DAY, newHour); onDateTimeChanged(); + // 如果日期发生变化,更新年月日 if (isDateChanged) { setCurrentYear(cal.get(Calendar.YEAR)); setCurrentMonth(cal.get(Calendar.MONTH)); @@ -115,21 +158,31 @@ public class DateTimePicker extends FrameLayout { } }; + /** + * 分钟变更监听器 + *

+ * 监听分钟选择器的值变化,处理跨小时情况(如从59分变为0分或从0分变为59分)。 + *

+ */ private NumberPicker.OnValueChangeListener mOnMinuteChangedListener = new NumberPicker.OnValueChangeListener() { @Override public void onValueChange(NumberPicker picker, int oldVal, int newVal) { int minValue = mMinuteSpinner.getMinValue(); int maxValue = mMinuteSpinner.getMaxValue(); int offset = 0; + // 从最大值变为最小值,小时加1 if (oldVal == maxValue && newVal == minValue) { offset += 1; + // 从最小值变为最大值,小时减1 } else if (oldVal == minValue && newVal == maxValue) { offset -= 1; } + // 如果跨小时,更新小时和日期 if (offset != 0) { mDate.add(Calendar.HOUR_OF_DAY, offset); mHourSpinner.setValue(getCurrentHour()); updateDateControl(); + // 更新上午/下午状态 int newHour = getCurrentHourOfDay(); if (newHour >= HOURS_IN_HALF_DAY) { mIsAm = false; @@ -144,10 +197,17 @@ public class DateTimePicker extends FrameLayout { } }; + /** + * 上午/下午变更监听器 + *

+ * 监听上午/下午选择器的值变化,切换上午/下午时调整小时数。 + *

+ */ private NumberPicker.OnValueChangeListener mOnAmPmChangedListener = new NumberPicker.OnValueChangeListener() { @Override public void onValueChange(NumberPicker picker, int oldVal, int newVal) { mIsAm = !mIsAm; + // 切换上午/下午,调整小时数 if (mIsAm) { mDate.add(Calendar.HOUR_OF_DAY, -HOURS_IN_HALF_DAY); } else { @@ -158,39 +218,88 @@ public class DateTimePicker extends FrameLayout { } }; + /** + * 日期时间变更监听器接口 + *

+ * 用于监听日期时间选择器的值变化,当用户修改日期、小时或分钟时回调。 + *

+ */ public interface OnDateTimeChangedListener { + /** + * 当日期时间发生变化时调用 + * + * @param view 日期时间选择器实例 + * @param year 年份 + * @param month 月份(0-11) + * @param dayOfMonth 日(1-31) + * @param hourOfDay 小时(0-23) + * @param minute 分钟(0-59) + */ void onDateTimeChanged(DateTimePicker view, int year, int month, int dayOfMonth, int hourOfDay, int minute); } + /** + * 构造器 + * + * 创建日期时间选择器,使用当前系统时间作为初始值。 + * 根据系统设置自动判断是否使用24小时制显示。 + * + * @param context 应用上下文 + */ public DateTimePicker(Context context) { this(context, System.currentTimeMillis()); } + /** + * 构造器 + * + * 创建日期时间选择器,使用指定的时间作为初始值。 + * 根据系统设置自动判断是否使用24小时制显示。 + * + * @param context 应用上下文 + * @param date 初始日期时间,以毫秒为单位的时间戳 + */ public DateTimePicker(Context context, long date) { this(context, date, DateFormat.is24HourFormat(context)); } + /** + * 构造器 + * + * 创建日期时间选择器,使用指定的时间和显示模式作为初始值。 + * 初始化所有NumberPicker组件并设置监听器。 + * + * @param context 应用上下文 + * @param date 初始日期时间,以毫秒为单位的时间戳 + * @param is24HourView 是否使用24小时制显示,true表示24小时制,false表示12小时制 + */ public DateTimePicker(Context context, long date, boolean is24HourView) { super(context); mDate = Calendar.getInstance(); mInitialising = true; + // 判断当前是否为下午 mIsAm = getCurrentHourOfDay() >= HOURS_IN_HALF_DAY; + // 加载布局 inflate(context, R.layout.datetime_picker, this); + // 初始化日期选择器 mDateSpinner = (NumberPicker) findViewById(R.id.date); mDateSpinner.setMinValue(DATE_SPINNER_MIN_VAL); mDateSpinner.setMaxValue(DATE_SPINNER_MAX_VAL); mDateSpinner.setOnValueChangedListener(mOnDateChangedListener); + // 初始化小时选择器 mHourSpinner = (NumberPicker) findViewById(R.id.hour); mHourSpinner.setOnValueChangedListener(mOnHourChangedListener); + // 初始化分钟选择器 mMinuteSpinner = (NumberPicker) findViewById(R.id.minute); mMinuteSpinner.setMinValue(MINUT_SPINNER_MIN_VAL); mMinuteSpinner.setMaxValue(MINUT_SPINNER_MAX_VAL); mMinuteSpinner.setOnLongPressUpdateInterval(100); mMinuteSpinner.setOnValueChangedListener(mOnMinuteChangedListener); + // 初始化上午/下午选择器 String[] stringsForAmPm = new DateFormatSymbols().getAmPmStrings(); mAmPmSpinner = (NumberPicker) findViewById(R.id.amPm); mAmPmSpinner.setMinValue(AMPM_SPINNER_MIN_VAL); @@ -198,22 +307,31 @@ public class DateTimePicker extends FrameLayout { mAmPmSpinner.setDisplayedValues(stringsForAmPm); mAmPmSpinner.setOnValueChangedListener(mOnAmPmChangedListener); - // update controls to initial state + // 更新控件到初始状态 updateDateControl(); updateHourControl(); updateAmPmControl(); + // 设置24小时制显示模式 set24HourView(is24HourView); - // set to current time + // 设置当前时间 setCurrentDate(date); + // 设置启用状态 setEnabled(isEnabled()); - // set the content descriptions + // 设置内容描述 mInitialising = false; } + /** + * 设置启用状态 + * + * 设置所有NumberPicker组件的启用状态,控制用户是否可以修改日期时间。 + * + * @param enabled true表示启用,false表示禁用 + */ @Override public void setEnabled(boolean enabled) { if (mIsEnabled == enabled) { @@ -227,24 +345,29 @@ public class DateTimePicker extends FrameLayout { mIsEnabled = enabled; } + /** + * 获取启用状态 + * + * @return true表示已启用,false表示已禁用 + */ @Override public boolean isEnabled() { return mIsEnabled; } /** - * Get the current date in millis - * - * @return the current date in millis + * 获取当前日期时间(毫秒) + * + * @return 当前日期时间,以毫秒为单位的时间戳 */ public long getCurrentDateInTimeMillis() { return mDate.getTimeInMillis(); } /** - * Set the current date - * - * @param date The current date in millis + * 设置当前日期时间 + * + * @param date 要设置的日期时间,以毫秒为单位的时间戳 */ public void setCurrentDate(long date) { Calendar cal = Calendar.getInstance(); @@ -254,13 +377,13 @@ public class DateTimePicker extends FrameLayout { } /** - * Set the current date - * - * @param year The current year - * @param month The current month - * @param dayOfMonth The current dayOfMonth - * @param hourOfDay The current hourOfDay - * @param minute The current minute + * 设置当前日期时间 + * + * @param year 年份 + * @param month 月份(0-11) + * @param dayOfMonth 日(1-31) + * @param hourOfDay 小时(0-23) + * @param minute 分钟(0-59) */ public void setCurrentDate(int year, int month, int dayOfMonth, int hourOfDay, int minute) { @@ -272,18 +395,18 @@ public class DateTimePicker extends FrameLayout { } /** - * Get current year - * - * @return The current year + * 获取当前年份 + * + * @return 当前年份 */ public int getCurrentYear() { return mDate.get(Calendar.YEAR); } /** - * Set current year - * - * @param year The current year + * 设置当前年份 + * + * @param year 要设置的年份 */ public void setCurrentYear(int year) { if (!mInitialising && year == getCurrentYear()) { @@ -295,18 +418,18 @@ public class DateTimePicker extends FrameLayout { } /** - * Get current month in the year - * - * @return The current month in the year + * 获取当前月份 + * + * @return 当前月份(0-11) */ public int getCurrentMonth() { return mDate.get(Calendar.MONTH); } /** - * Set current month in the year - * - * @param month The month in the year + * 设置当前月份 + * + * @param month 要设置的月份(0-11) */ public void setCurrentMonth(int month) { if (!mInitialising && month == getCurrentMonth()) { @@ -318,18 +441,18 @@ public class DateTimePicker extends FrameLayout { } /** - * Get current day of the month - * - * @return The day of the month + * 获取当前日 + * + * @return 当前日(1-31) */ public int getCurrentDay() { return mDate.get(Calendar.DAY_OF_MONTH); } /** - * Set current day of the month - * - * @param dayOfMonth The day of the month + * 设置当前日 + * + * @param dayOfMonth 要设置的日(1-31) */ public void setCurrentDay(int dayOfMonth) { if (!mInitialising && dayOfMonth == getCurrentDay()) { @@ -341,13 +464,21 @@ public class DateTimePicker extends FrameLayout { } /** - * Get current hour in 24 hour mode, in the range (0~23) - * @return The current hour in 24 hour mode + * 获取当前小时(24小时制) + * + * @return 当前小时(0-23) */ public int getCurrentHourOfDay() { return mDate.get(Calendar.HOUR_OF_DAY); } + /** + * 获取当前小时(根据显示模式) + * + * 在24小时制下返回0-23,在12小时制下返回1-12 + * + * @return 当前小时 + */ private int getCurrentHour() { if (mIs24HourView){ return getCurrentHourOfDay(); @@ -362,9 +493,9 @@ public class DateTimePicker extends FrameLayout { } /** - * Set current hour in 24 hour mode, in the range (0~23) - * - * @param hourOfDay + * 设置当前小时(24小时制) + * + * @param hourOfDay 要设置的小时(0-23) */ public void setCurrentHour(int hourOfDay) { if (!mInitialising && hourOfDay == getCurrentHourOfDay()) { @@ -372,6 +503,7 @@ public class DateTimePicker extends FrameLayout { } mDate.set(Calendar.HOUR_OF_DAY, hourOfDay); if (!mIs24HourView) { + // 处理12小时制下的上午/下午状态 if (hourOfDay >= HOURS_IN_HALF_DAY) { mIsAm = false; if (hourOfDay > HOURS_IN_HALF_DAY) { @@ -390,16 +522,18 @@ public class DateTimePicker extends FrameLayout { } /** - * Get currentMinute - * - * @return The Current Minute + * 获取当前分钟 + * + * @return 当前分钟(0-59) */ public int getCurrentMinute() { return mDate.get(Calendar.MINUTE); } /** - * Set current minute + * 设置当前分钟 + * + * @param minute 要设置的分钟(0-59) */ public void setCurrentMinute(int minute) { if (!mInitialising && minute == getCurrentMinute()) { @@ -411,22 +545,25 @@ public class DateTimePicker extends FrameLayout { } /** - * @return true if this is in 24 hour view else false. + * 判断是否为24小时制显示 + * + * @return true表示24小时制,false表示12小时制 */ public boolean is24HourView () { return mIs24HourView; } /** - * Set whether in 24 hour or AM/PM mode. - * - * @param is24HourView True for 24 hour mode. False for AM/PM mode. + * 设置显示模式 + * + * @param is24HourView true表示使用24小时制,false表示使用12小时制 */ public void set24HourView(boolean is24HourView) { if (mIs24HourView == is24HourView) { return; } mIs24HourView = is24HourView; + // 根据显示模式显示或隐藏上午/下午选择器 mAmPmSpinner.setVisibility(is24HourView ? View.GONE : View.VISIBLE); int hour = getCurrentHourOfDay(); updateHourControl(); @@ -434,30 +571,53 @@ public class DateTimePicker extends FrameLayout { updateAmPmControl(); } + /** + * 更新日期选择器显示 + * + * 根据当前日期更新日期选择器的显示值,显示前后3天,共7天的日期。 + * 每个日期的格式为"MM.dd EEEE"(月.日 星期)。 + */ private void updateDateControl() { Calendar cal = Calendar.getInstance(); + // 设置为当前日期的前4天 cal.setTimeInMillis(mDate.getTimeInMillis()); cal.add(Calendar.DAY_OF_YEAR, -DAYS_IN_ALL_WEEK / 2 - 1); mDateSpinner.setDisplayedValues(null); + // 生成7天的日期显示值 for (int i = 0; i < DAYS_IN_ALL_WEEK; ++i) { cal.add(Calendar.DAY_OF_YEAR, 1); mDateDisplayValues[i] = (String) DateFormat.format("MM.dd EEEE", cal); } mDateSpinner.setDisplayedValues(mDateDisplayValues); + // 设置当前选中项为中间项 mDateSpinner.setValue(DAYS_IN_ALL_WEEK / 2); mDateSpinner.invalidate(); } + /** + * 更新上午/下午选择器显示 + * + * 根据当前显示模式和上午/下午状态更新上午/下午选择器的可见性和选中值。 + * 在24小时制下隐藏上午/下午选择器,在12小时制下显示并设置当前选中值。 + */ private void updateAmPmControl() { if (mIs24HourView) { + // 24小时制下隐藏上午/下午选择器 mAmPmSpinner.setVisibility(View.GONE); } else { + // 12小时制下显示上午/下午选择器 int index = mIsAm ? Calendar.AM : Calendar.PM; mAmPmSpinner.setValue(index); mAmPmSpinner.setVisibility(View.VISIBLE); } } + /** + * 更新小时选择器范围 + * + * 根据当前显示模式更新小时选择器的最小值和最大值。 + * 24小时制:0-23,12小时制:1-12。 + */ private void updateHourControl() { if (mIs24HourView) { mHourSpinner.setMinValue(HOUR_SPINNER_MIN_VAL_24_HOUR_VIEW); @@ -469,13 +629,19 @@ public class DateTimePicker extends FrameLayout { } /** - * Set the callback that indicates the 'Set' button has been pressed. - * @param callback the callback, if null will do nothing + * 设置日期时间变更监听器 + * + * @param callback 日期时间变更监听器,如果为null则不执行任何操作 */ public void setOnDateTimeChangedListener(OnDateTimeChangedListener callback) { mOnDateTimeChangedListener = callback; } + /** + * 触发日期时间变更事件 + * + * 如果设置了监听器,则通知监听器日期时间已发生变化。 + */ private void onDateTimeChanged() { if (mOnDateTimeChangedListener != null) { mOnDateTimeChangedListener.onDateTimeChanged(this, getCurrentYear(), diff --git a/src/Notes-master/src/net/micode/notes/ui/DateTimePickerDialog.java b/src/Notes-master/src/net/micode/notes/ui/DateTimePickerDialog.java index 2c47ba4..a95bc43 100644 --- a/src/Notes-master/src/net/micode/notes/ui/DateTimePickerDialog.java +++ b/src/Notes-master/src/net/micode/notes/ui/DateTimePickerDialog.java @@ -29,6 +29,25 @@ import android.content.DialogInterface.OnClickListener; import android.text.format.DateFormat; import android.text.format.DateUtils; +/** + * 日期时间选择对话框 + *

+ * 继承自AlertDialog,提供日期和时间选择的对话框界面。 + * 使用DateTimePicker组件作为主视图,支持设置24小时制或12小时制显示。 + *

+ *

+ * 主要功能: + *

    + *
  • 显示日期和时间选择器
  • + *
  • 支持设置监听器获取用户选择的时间
  • + *
  • 动态更新对话框标题显示当前选择的时间
  • + *
  • 支持24小时制和12小时制切换
  • + *
+ *

+ * + * @see DateTimePicker + * @see OnDateTimeSetListener + */ public class DateTimePickerDialog extends AlertDialog implements OnClickListener { private Calendar mDate = Calendar.getInstance(); @@ -36,52 +55,122 @@ public class DateTimePickerDialog extends AlertDialog implements OnClickListener private OnDateTimeSetListener mOnDateTimeSetListener; private DateTimePicker mDateTimePicker; + /** + * 日期时间设置监听器接口 + *

+ * 用于监听用户在对话框中点击确定按钮后的回调,获取用户选择的日期和时间。 + *

+ */ public interface OnDateTimeSetListener { + /** + * 当用户点击确定按钮时调用 + * + * @param dialog 日期时间选择对话框实例 + * @param date 用户选择的日期时间,以毫秒为单位的时间戳 + */ void OnDateTimeSet(AlertDialog dialog, long date); } + /** + * 构造器 + * + * 创建日期时间选择对话框,初始化DateTimePicker组件并设置默认日期时间。 + * 根据系统设置自动判断是否使用24小时制显示。 + * + * @param context 应用上下文 + * @param date 初始日期时间,以毫秒为单位的时间戳 + */ public DateTimePickerDialog(Context context, long date) { super(context); + // 创建日期时间选择器组件 mDateTimePicker = new DateTimePicker(context); setView(mDateTimePicker); + // 设置日期时间变更监听器 mDateTimePicker.setOnDateTimeChangedListener(new OnDateTimeChangedListener() { public void onDateTimeChanged(DateTimePicker view, int year, int month, int dayOfMonth, int hourOfDay, int minute) { + // 更新内部Calendar对象 mDate.set(Calendar.YEAR, year); mDate.set(Calendar.MONTH, month); mDate.set(Calendar.DAY_OF_MONTH, dayOfMonth); mDate.set(Calendar.HOUR_OF_DAY, hourOfDay); mDate.set(Calendar.MINUTE, minute); + // 更新对话框标题 updateTitle(mDate.getTimeInMillis()); } }); + // 设置初始日期时间 mDate.setTimeInMillis(date); + // 将秒数清零 mDate.set(Calendar.SECOND, 0); + // 设置选择器当前日期 mDateTimePicker.setCurrentDate(mDate.getTimeInMillis()); + // 设置确定按钮 setButton(context.getString(R.string.datetime_dialog_ok), this); + // 设置取消按钮 setButton2(context.getString(R.string.datetime_dialog_cancel), (OnClickListener)null); + // 根据系统设置判断是否使用24小时制 set24HourView(DateFormat.is24HourFormat(this.getContext())); + // 更新对话框标题 updateTitle(mDate.getTimeInMillis()); } + /** + * 设置是否使用24小时制显示 + *

+ * 根据系统设置或用户偏好,判断是否使用24小时制显示时间。 + * 如果设置为true,将使用24小时制;如果设置为false,将使用12小时制。 + *

+ * + * @param is24HourView true表示使用24小时制,false表示使用12小时制 + */ public void set24HourView(boolean is24HourView) { mIs24HourView = is24HourView; } + /** + * 设置日期时间设置监听器 + *

+ * 当用户点击对话框的确定按钮时,调用此监听器的OnDateTimeSet方法, + * 并传递用户选择的日期时间作为参数。 + *

+ * + * @param callBack 日期时间设置监听器,当用户点击确定按钮时回调 + */ public void setOnDateTimeSetListener(OnDateTimeSetListener callBack) { mOnDateTimeSetListener = callBack; } + /** + * 更新对话框标题 + * + * 根据指定的日期时间格式化字符串,并设置为对话框标题。 + * 显示格式包含年、月、日和时间,根据mIs24HourView决定是否使用24小时制。 + * + * @param date 要显示的日期时间,以毫秒为单位的时间戳 + */ private void updateTitle(long date) { + // 设置日期时间格式标志 int flag = DateUtils.FORMAT_SHOW_YEAR | DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_SHOW_TIME; + // 根据是否24小时制设置相应的格式标志 flag |= mIs24HourView ? DateUtils.FORMAT_24HOUR : DateUtils.FORMAT_24HOUR; + // 格式化日期时间并设置为对话框标题 setTitle(DateUtils.formatDateTime(this.getContext(), date, flag)); } + /** + * 处理对话框按钮点击事件 + * + * 当用户点击确定按钮时,调用监听器的OnDateTimeSet方法,传递用户选择的日期时间。 + * + * @param arg0 触发事件的对话框 + * @param arg1 被点击的按钮ID + */ public void onClick(DialogInterface arg0, int arg1) { + // 如果设置了监听器,通知监听器用户选择的日期时间 if (mOnDateTimeSetListener != null) { mOnDateTimeSetListener.OnDateTimeSet(this, mDate.getTimeInMillis()); } diff --git a/src/Notes-master/src/net/micode/notes/ui/DropdownMenu.java b/src/Notes-master/src/net/micode/notes/ui/DropdownMenu.java index 613dc74..7276081 100644 --- a/src/Notes-master/src/net/micode/notes/ui/DropdownMenu.java +++ b/src/Notes-master/src/net/micode/notes/ui/DropdownMenu.java @@ -27,17 +27,49 @@ import android.widget.PopupMenu.OnMenuItemClickListener; import net.micode.notes.R; +/** + * 下拉菜单类 + *

+ * 封装了PopupMenu和Button,提供下拉菜单功能。 + * 点击按钮时显示弹出菜单,支持设置菜单项点击监听器和标题。 + *

+ *

+ * 主要功能: + *

    + *
  • 显示下拉菜单
  • + *
  • 设置菜单项点击监听器
  • + *
  • 查找菜单项
  • + *
  • 设置按钮标题
  • + *
+ *

+ */ public class DropdownMenu { + // 下拉按钮 private Button mButton; + // 弹出菜单 private PopupMenu mPopupMenu; + // 菜单对象 private Menu mMenu; + /** + * 构造器 + * + * 初始化下拉菜单,设置按钮背景、创建PopupMenu并加载菜单资源 + * + * @param context 应用上下文 + * @param button 触发下拉菜单的按钮 + * @param menuId 菜单资源ID,用于加载菜单项 + */ public DropdownMenu(Context context, Button button, int menuId) { mButton = button; + // 设置下拉图标背景 mButton.setBackgroundResource(R.drawable.dropdown_icon); + // 创建弹出菜单 mPopupMenu = new PopupMenu(context, mButton); mMenu = mPopupMenu.getMenu(); + // 加载菜单资源 mPopupMenu.getMenuInflater().inflate(menuId, mMenu); + // 设置按钮点击监听器,点击时显示弹出菜单 mButton.setOnClickListener(new OnClickListener() { public void onClick(View v) { mPopupMenu.show(); @@ -45,16 +77,32 @@ public class DropdownMenu { }); } + /** + * 设置菜单项点击监听器 + * + * @param listener 菜单项点击监听器 + */ public void setOnDropdownMenuItemClickListener(OnMenuItemClickListener listener) { if (mPopupMenu != null) { mPopupMenu.setOnMenuItemClickListener(listener); } } + /** + * 查找指定ID的菜单项 + * + * @param id 菜单项ID + * @return 找到的菜单项对象,如果未找到则返回null + */ public MenuItem findItem(int id) { return mMenu.findItem(id); } + /** + * 设置按钮标题 + * + * @param title 要设置的标题文本 + */ public void setTitle(CharSequence title) { mButton.setText(title); } diff --git a/src/Notes-master/src/net/micode/notes/ui/FoldersListAdapter.java b/src/Notes-master/src/net/micode/notes/ui/FoldersListAdapter.java index 96b77da..7176cf9 100644 --- a/src/Notes-master/src/net/micode/notes/ui/FoldersListAdapter.java +++ b/src/Notes-master/src/net/micode/notes/ui/FoldersListAdapter.java @@ -28,50 +28,123 @@ import net.micode.notes.R; import net.micode.notes.data.Notes; import net.micode.notes.data.Notes.NoteColumns; - +/** + * 文件夹列表适配器 + *

+ * 继承自CursorAdapter,用于将数据库中的文件夹数据绑定到ListView中显示。 + * 主要用于笔记移动功能中显示可选择的文件夹列表。 + *

+ *

+ * 主要功能: + *

    + *
  • 显示所有可用文件夹
  • + *
  • 处理根文件夹的特殊显示
  • + *
  • 提供获取文件夹名称的方法
  • + *
+ *

+ * + * @see NotesListActivity + */ public class FoldersListAdapter extends CursorAdapter { + // 数据库查询投影,指定需要从笔记表中获取的列 public static final String [] PROJECTION = { NoteColumns.ID, NoteColumns.SNIPPET }; + // 列索引常量,用于从查询结果中获取对应列的数据 public static final int ID_COLUMN = 0; public static final int NAME_COLUMN = 1; + /** + * 构造器 + * + * 初始化文件夹列表适配器 + * + * @param context 应用上下文 + * @param c 数据库游标,包含文件夹数据 + */ public FoldersListAdapter(Context context, Cursor c) { super(context, c); - // TODO Auto-generated constructor stub } + /** + * 创建新的列表项视图 + * + * 创建一个新的FolderListItem视图对象 + * + * @param context 应用上下文 + * @param cursor 数据库游标,包含当前项的数据 + * @param parent 父视图 + * @return 新创建的FolderListItem视图对象 + */ @Override public View newView(Context context, Cursor cursor, ViewGroup parent) { return new FolderListItem(context); } + /** + * 绑定数据到视图 + * + * 将数据库游标中的数据绑定到已存在的视图上 + * + * @param view 需要绑定数据的视图 + * @param context 应用上下文 + * @param cursor 数据库游标,包含当前项的数据 + */ @Override public void bindView(View view, Context context, Cursor cursor) { if (view instanceof FolderListItem) { + // 如果是根文件夹,显示特殊文本;否则显示文件夹名称 String folderName = (cursor.getLong(ID_COLUMN) == Notes.ID_ROOT_FOLDER) ? context .getString(R.string.menu_move_parent_folder) : cursor.getString(NAME_COLUMN); ((FolderListItem) view).bind(folderName); } } + /** + * 获取指定位置的文件夹名称 + * + * @param context 应用上下文,用于获取根文件夹的显示文本 + * @param position 列表项位置,从0开始 + * @return 文件夹名称,如果是根文件夹则返回特殊显示文本 + */ public String getFolderName(Context context, int position) { Cursor cursor = (Cursor) getItem(position); return (cursor.getLong(ID_COLUMN) == Notes.ID_ROOT_FOLDER) ? context .getString(R.string.menu_move_parent_folder) : cursor.getString(NAME_COLUMN); } + /** + * 文件夹列表项视图 + *

+ * 自定义的LinearLayout,用于显示文件夹列表中的单个文件夹项。 + *

+ */ private class FolderListItem extends LinearLayout { + // 文件夹名称文本视图 private TextView mName; + /** + * 构造器 + * + * 初始化文件夹列表项视图 + * + * @param context 应用上下文 + */ public FolderListItem(Context context) { super(context); + // 加载布局文件 inflate(context, R.layout.folder_list_item, this); + // 获取文件夹名称文本视图 mName = (TextView) findViewById(R.id.tv_folder_name); } + /** + * 绑定文件夹名称到视图 + * + * @param name 要显示的文件夹名称 + */ public void bind(String name) { mName.setText(name); } diff --git a/src/Notes-master/src/net/micode/notes/ui/NoteEditActivity.java b/src/Notes-master/src/net/micode/notes/ui/NoteEditActivity.java index 96a9ff8..9544f6c 100644 --- a/src/Notes-master/src/net/micode/notes/ui/NoteEditActivity.java +++ b/src/Notes-master/src/net/micode/notes/ui/NoteEditActivity.java @@ -74,6 +74,12 @@ import java.util.regex.Pattern; public class NoteEditActivity extends Activity implements OnClickListener, NoteSettingChangedListener, OnTextViewChangeListener { + /** + * 笔记头部视图持有者 + *

+ * 持有笔记编辑界面头部区域的UI组件引用,包括修改时间、提醒图标、提醒日期和背景颜色设置按钮。 + *

+ */ private class HeadViewHolder { public TextView tvModified; @@ -162,8 +168,12 @@ public class NoteEditActivity extends Activity implements OnClickListener, } /** - * Current activity may be killed when the memory is low. Once it is killed, for another time - * user load this activity, we should restore the former state + * 恢复活动状态 + *

+ * 当系统内存不足导致活动被杀死时,重新加载活动需要恢复之前的状态。 + * 从保存的实例状态中恢复笔记ID,并重新初始化活动状态。 + *

+ * @param savedInstanceState 包含之前保存状态的Bundle对象 */ @Override protected void onRestoreInstanceState(Bundle savedInstanceState) { @@ -179,6 +189,18 @@ public class NoteEditActivity extends Activity implements OnClickListener, } } + /** + * 初始化活动状态 + *

+ * 根据传入的Intent初始化活动状态,支持以下操作: + *

    + *
  • ACTION_VIEW: 查看现有笔记,支持从搜索结果打开
  • + *
  • ACTION_INSERT_OR_EDIT: 创建新笔记或编辑笔记,支持通话记录笔记
  • + *
+ *

+ * @param intent 包含操作类型和参数的Intent对象 + * @return 初始化成功返回true,失败返回false + */ private boolean initActivityState(Intent intent) { /** * If the user specified the {@link Intent#ACTION_VIEW} but not provided with id, @@ -268,6 +290,18 @@ public class NoteEditActivity extends Activity implements OnClickListener, initNoteScreen(); } + /** + * 初始化笔记编辑界面 + *

+ * 设置笔记编辑界面的显示内容,包括: + *

    + *
  • 根据字体大小设置文本外观
  • + *
  • 根据模式(普通/清单)显示笔记内容
  • + *
  • 设置背景颜色
  • + *
  • 显示修改时间和提醒信息
  • + *
+ *

+ */ private void initNoteScreen() { mNoteEditor.setTextAppearance(this, TextAppearanceResources .getTexAppearanceResource(mFontSizeId)); @@ -295,6 +329,13 @@ public class NoteEditActivity extends Activity implements OnClickListener, showAlertHeader(); } + /** + * 显示提醒头部信息 + *

+ * 根据笔记是否设置了闹钟提醒,显示或隐藏提醒图标和提醒日期。 + * 如果提醒已过期,显示过期提示;否则显示相对时间。 + *

+ */ private void showAlertHeader() { if (mWorkingNote.hasClockAlert()) { long time = System.currentTimeMillis(); @@ -318,6 +359,14 @@ public class NoteEditActivity extends Activity implements OnClickListener, initActivityState(intent); } + /** + * 保存活动实例状态 + *

+ * 在活动被系统销毁前保存当前笔记的ID,以便后续恢复。 + * 如果是新笔记且尚未保存到数据库,会先保存笔记以生成ID。 + *

+ * @param outState 用于保存状态的Bundle对象 + */ @Override protected void onSaveInstanceState(Bundle outState) { super.onSaveInstanceState(outState); @@ -333,6 +382,15 @@ public class NoteEditActivity extends Activity implements OnClickListener, Log.d(TAG, "Save working note id: " + mWorkingNote.getNoteId() + " onSaveInstanceState"); } + /** + * 分发触摸事件 + *

+ * 处理触摸事件,当用户点击背景颜色选择器或字体大小选择器外部区域时, + * 隐藏相应的选择器面板。 + *

+ * @param ev 触摸事件对象 + * @return 如果事件被处理返回true,否则返回false + */ @Override public boolean dispatchTouchEvent(MotionEvent ev) { if (mNoteBgColorSelector.getVisibility() == View.VISIBLE @@ -349,6 +407,15 @@ public class NoteEditActivity extends Activity implements OnClickListener, return super.dispatchTouchEvent(ev); } + /** + * 检查触摸点是否在视图范围内 + *

+ * 判断给定的触摸事件坐标是否位于指定视图的显示区域内。 + *

+ * @param view 要检查的视图 + * @param ev 触摸事件对象 + * @return 如果触摸点在视图范围内返回true,否则返回false + */ private boolean inRangeOfView(View view, MotionEvent ev) { int []location = new int[2]; view.getLocationOnScreen(location); @@ -363,6 +430,13 @@ public class NoteEditActivity extends Activity implements OnClickListener, return true; } + /** + * 初始化资源 + *

+ * 初始化所有UI组件的引用,设置点击监听器, + * 并从SharedPreferences中读取字体大小设置。 + *

+ */ private void initResources() { mHeadViewPanel = findViewById(R.id.note_title); mNoteHeaderHolder = new HeadViewHolder(); @@ -397,6 +471,12 @@ public class NoteEditActivity extends Activity implements OnClickListener, mEditTextList = (LinearLayout) findViewById(R.id.note_edit_list); } + /** + * 活动暂停时保存笔记 + *

+ * 在活动暂停时自动保存笔记内容,并清除设置状态(如打开的颜色选择器)。 + *

+ */ @Override protected void onPause() { super.onPause(); @@ -406,6 +486,12 @@ public class NoteEditActivity extends Activity implements OnClickListener, clearSettingState(); } + /** + * 更新桌面小部件 + *

+ * 发送广播通知桌面小部件更新,根据笔记的小部件类型(2x或4x)发送相应的更新意图。 + *

+ */ private void updateWidget() { Intent intent = new Intent(AppWidgetManager.ACTION_APPWIDGET_UPDATE); if (mWorkingNote.getWidgetType() == Notes.TYPE_WIDGET_2X) { @@ -425,6 +511,18 @@ public class NoteEditActivity extends Activity implements OnClickListener, setResult(RESULT_OK, intent); } + /** + * 处理点击事件 + *

+ * 处理各种UI组件的点击事件,包括: + *

    + *
  • 背景颜色设置按钮:显示颜色选择器
  • + *
  • 背景颜色选项:设置笔记背景颜色
  • + *
  • 字体大小选项:设置编辑器字体大小
  • + *
+ *

+ * @param v 被点击的视图 + */ public void onClick(View v) { int id = v.getId(); if (id == R.id.btn_set_bg_color) { @@ -452,6 +550,13 @@ public class NoteEditActivity extends Activity implements OnClickListener, } } + /** + * 处理返回键按下事件 + *

+ * 如果当前有打开的设置面板(颜色选择器或字体选择器),先关闭面板; + * 否则保存笔记并退出活动。 + *

+ */ @Override public void onBackPressed() { if(clearSettingState()) { @@ -462,6 +567,13 @@ public class NoteEditActivity extends Activity implements OnClickListener, super.onBackPressed(); } + /** + * 清除设置状态 + *

+ * 检查并关闭所有打开的设置面板(背景颜色选择器和字体大小选择器)。 + *

+ * @return 如果关闭了任何面板返回true,否则返回false + */ private boolean clearSettingState() { if (mNoteBgColorSelector.getVisibility() == View.VISIBLE) { mNoteBgColorSelector.setVisibility(View.GONE); @@ -473,6 +585,17 @@ public class NoteEditActivity extends Activity implements OnClickListener, return false; } + /** + * 背景颜色改变回调 + *

+ * 当笔记背景颜色改变时调用,更新UI显示: + *

    + *
  • 显示选中颜色的指示器
  • + *
  • 更新编辑器面板背景
  • + *
  • 更新头部面板背景
  • + *
+ *

+ */ public void onBackgroundColorChanged() { findViewById(sBgSelectorSelectionMap.get(mWorkingNote.getBgColorId())).setVisibility( View.VISIBLE); @@ -480,6 +603,19 @@ public class NoteEditActivity extends Activity implements OnClickListener, mHeadViewPanel.setBackgroundResource(mWorkingNote.getTitleBgResId()); } + /** + * 准备选项菜单 + *

+ * 根据当前笔记的状态动态设置菜单项: + *

    + *
  • 通话记录笔记使用特殊菜单
  • + *
  • 清单模式下切换菜单项标题
  • + *
  • 根据是否设置提醒显示/隐藏相应菜单项
  • + *
+ *

+ * @param menu 选项菜单对象 + * @return 返回true表示菜单已准备好 + */ @Override public boolean onPrepareOptionsMenu(Menu menu) { if (isFinishing()) { @@ -505,6 +641,24 @@ public class NoteEditActivity extends Activity implements OnClickListener, return true; } + /** + * 处理选项菜单项选择 + *

+ * 处理各种菜单项的点击事件,包括: + *

    + *
  • 新建笔记:创建新笔记
  • + *
  • 删除笔记:显示确认对话框后删除当前笔记
  • + *
  • 字体大小:显示字体大小选择器
  • + *
  • 清单模式:切换普通/清单模式
  • + *
  • 分享:分享笔记内容到其他应用
  • + *
  • 发送到桌面:创建桌面小部件
  • + *
  • 设置提醒:设置闹钟提醒
  • + *
  • 删除提醒:删除已设置的提醒
  • + *
+ *

+ * @param item 被选中的菜单项 + * @return 返回true表示事件已处理 + */ @Override public boolean onOptionsItemSelected(MenuItem item) { switch (item.getItemId()) { @@ -553,6 +707,13 @@ public class NoteEditActivity extends Activity implements OnClickListener, return true; } + /** + * 设置提醒 + *

+ * 显示日期时间选择对话框,让用户选择提醒时间。 + * 选择完成后设置笔记的提醒日期。 + *

+ */ private void setReminder() { DateTimePickerDialog d = new DateTimePickerDialog(this, System.currentTimeMillis()); d.setOnDateTimeSetListener(new OnDateTimeSetListener() { @@ -564,8 +725,12 @@ public class NoteEditActivity extends Activity implements OnClickListener, } /** - * Share note to apps that support {@link Intent#ACTION_SEND} action - * and {@text/plain} type + * 分享笔记到其他应用 + *

+ * 使用ACTION_SEND Intent将笔记内容分享到支持文本分享的应用。 + *

+ * @param context 上下文对象 + * @param info 要分享的文本内容 */ private void sendTo(Context context, String info) { Intent intent = new Intent(Intent.ACTION_SEND); @@ -574,6 +739,13 @@ public class NoteEditActivity extends Activity implements OnClickListener, context.startActivity(intent); } + /** + * 创建新笔记 + *

+ * 先保存当前编辑的笔记,然后启动新的NoteEditActivity创建新笔记。 + * 新笔记将创建在与当前笔记相同的文件夹中。 + *

+ */ private void createNewNote() { // Firstly, save current editing notes saveNote(); @@ -586,6 +758,13 @@ public class NoteEditActivity extends Activity implements OnClickListener, startActivity(intent); } + /** + * 删除当前笔记 + *

+ * 删除当前编辑的笔记。如果处于同步模式,将笔记移动到垃圾箱; + * 否则直接从数据库中删除。 + *

+ */ private void deleteCurrentNote() { if (mWorkingNote.existInDatabase()) { HashSet ids = new HashSet(); @@ -608,10 +787,27 @@ public class NoteEditActivity extends Activity implements OnClickListener, mWorkingNote.markDeleted(true); } + /** + * 检查是否处于同步模式 + *

+ * 检查是否配置了同步账户,如果配置了则处于同步模式。 + *

+ * @return 如果配置了同步账户返回true,否则返回false + */ private boolean isSyncMode() { return NotesPreferenceActivity.getSyncAccountName(this).trim().length() > 0; } + /** + * 闹钟提醒改变回调 + *

+ * 当笔记的闹钟提醒设置改变时调用。 + * 如果笔记尚未保存到数据库,先保存笔记。 + * 然后使用AlarmManager设置或取消闹钟。 + *

+ * @param date 提醒日期时间(毫秒) + * @param set true表示设置提醒,false表示取消提醒 + */ public void onClockAlertChanged(long date, boolean set) { /** * User could set clock to an unsaved note, so before setting the @@ -642,10 +838,25 @@ public class NoteEditActivity extends Activity implements OnClickListener, } } + /** + * 小部件改变回调 + *

+ * 当笔记的小部件设置改变时调用,更新桌面小部件显示。 + *

+ */ public void onWidgetChanged() { updateWidget(); } + /** + * 编辑文本删除回调 + *

+ * 在清单模式下删除某个编辑项时调用。 + * 删除指定位置的编辑项,并更新后续项的索引。 + *

+ * @param index 要删除的编辑项索引 + * @param text 编辑项的文本内容 + */ public void onEditTextDelete(int index, String text) { int childCount = mEditTextList.getChildCount(); if (childCount == 1) { @@ -672,6 +883,15 @@ public class NoteEditActivity extends Activity implements OnClickListener, edit.setSelection(length); } + /** + * 编辑文本回车回调 + *

+ * 在清单模式下,当用户在某个编辑项中按下回车键时调用。 + * 在指定位置插入新的编辑项,并更新后续项的索引。 + *

+ * @param index 回车位置所在的编辑项索引 + * @param text 编辑项的文本内容 + */ public void onEditTextEnter(int index, String text) { /** * Should not happen, check for debug @@ -691,6 +911,14 @@ public class NoteEditActivity extends Activity implements OnClickListener, } } + /** + * 切换到清单模式 + *

+ * 将普通文本编辑器切换到清单模式。 + * 将文本按行分割,每行创建一个清单项,包含复选框和编辑框。 + *

+ * @param text 要转换为清单的文本内容 + */ private void switchToListMode(String text) { mEditTextList.removeAllViews(); String[] items = text.split("\n"); @@ -708,6 +936,16 @@ public class NoteEditActivity extends Activity implements OnClickListener, mEditTextList.setVisibility(View.VISIBLE); } + /** + * 高亮显示搜索结果 + *

+ * 在文本中高亮显示用户搜索的关键词。 + * 使用背景色标记匹配的文本。 + *

+ * @param fullText 完整的文本内容 + * @param userQuery 用户搜索的关键词 + * @return 带有高亮标记的Spannable对象 + */ private Spannable getHighlightQueryResult(String fullText, String userQuery) { SpannableString spannable = new SpannableString(fullText == null ? "" : fullText); if (!TextUtils.isEmpty(userQuery)) { @@ -725,6 +963,16 @@ public class NoteEditActivity extends Activity implements OnClickListener, return spannable; } + /** + * 创建清单列表项视图 + *

+ * 创建清单模式下的单个列表项,包含复选框和编辑框。 + * 根据文本内容设置复选框状态和文本样式。 + *

+ * @param item 列表项的文本内容 + * @param index 列表项的索引 + * @return 列表项视图 + */ 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); @@ -756,6 +1004,15 @@ public class NoteEditActivity extends Activity implements OnClickListener, return view; } + /** + * 文本改变回调 + *

+ * 在清单模式下,当某个编辑项的文本内容改变时调用。 + * 根据是否有文本内容显示或隐藏复选框。 + *

+ * @param index 编辑项的索引 + * @param hasText 是否有文本内容 + */ public void onTextChange(int index, boolean hasText) { if (index >= mEditTextList.getChildCount()) { Log.e(TAG, "Wrong index, should not happen"); @@ -768,6 +1025,16 @@ public class NoteEditActivity extends Activity implements OnClickListener, } } + /** + * 清单模式改变回调 + *

+ * 当笔记的清单模式改变时调用。 + * 切换到清单模式时,将文本转换为清单项; + * 切换到普通模式时,将清单项转换为文本。 + *

+ * @param oldMode 旧的模式 + * @param newMode 新的模式 + */ public void onCheckListModeChanged(int oldMode, int newMode) { if (newMode == TextNote.MODE_CHECK_LIST) { switchToListMode(mNoteEditor.getText().toString()); @@ -782,6 +1049,14 @@ public class NoteEditActivity extends Activity implements OnClickListener, } } + /** + * 获取工作文本 + *

+ * 从当前编辑器中获取文本内容并设置到WorkingNote。 + * 如果是清单模式,将所有清单项合并为文本,并标记已选中项。 + *

+ * @return 如果有已选中的清单项返回true,否则返回false + */ private boolean getWorkingText() { boolean hasChecked = false; if (mWorkingNote.getCheckListMode() == TextNote.MODE_CHECK_LIST) { @@ -805,6 +1080,14 @@ public class NoteEditActivity extends Activity implements OnClickListener, return hasChecked; } + /** + * 保存笔记 + *

+ * 将当前编辑的笔记保存到数据库。 + * 保存成功后设置RESULT_OK结果码,用于标识创建/编辑状态。 + *

+ * @return 保存成功返回true,失败返回false + */ private boolean saveNote() { getWorkingText(); boolean saved = mWorkingNote.saveNote(); @@ -821,6 +1104,14 @@ public class NoteEditActivity extends Activity implements OnClickListener, return saved; } + /** + * 发送到桌面 + *

+ * 将笔记创建为桌面快捷方式。 + * 如果笔记尚未保存到数据库,先保存笔记。 + * 快捷方式使用笔记内容的前10个字符作为标题。 + *

+ */ private void sendToDesktop() { /** * Before send message to home, we should make sure that current @@ -856,6 +1147,15 @@ public class NoteEditActivity extends Activity implements OnClickListener, } } + /** + * 生成快捷方式图标标题 + *

+ * 从笔记内容中提取文本作为快捷方式标题。 + * 移除清单标记,并限制标题长度为10个字符。 + *

+ * @param content 笔记内容 + * @return 快捷方式标题 + */ private String makeShortcutIconTitle(String content) { content = content.replace(TAG_CHECKED, ""); content = content.replace(TAG_UNCHECKED, ""); @@ -863,10 +1163,25 @@ public class NoteEditActivity extends Activity implements OnClickListener, SHORTCUT_ICON_TITLE_MAX_LEN) : content; } + /** + * 显示Toast提示 + *

+ * 显示短时Toast提示消息。 + *

+ * @param resId 字符串资源ID + */ private void showToast(int resId) { showToast(resId, Toast.LENGTH_SHORT); } + /** + * 显示Toast提示 + *

+ * 显示指定时长的Toast提示消息。 + *

+ * @param resId 字符串资源ID + * @param duration 显示时长(Toast.LENGTH_SHORT或Toast.LENGTH_LONG) + */ private void showToast(int resId, int duration) { Toast.makeText(this, resId, duration).show(); } diff --git a/src/Notes-master/src/net/micode/notes/ui/NoteEditText.java b/src/Notes-master/src/net/micode/notes/ui/NoteEditText.java index 2afe2a8..df117b3 100644 --- a/src/Notes-master/src/net/micode/notes/ui/NoteEditText.java +++ b/src/Notes-master/src/net/micode/notes/ui/NoteEditText.java @@ -37,15 +37,40 @@ import net.micode.notes.R; import java.util.HashMap; import java.util.Map; +/** + * 笔记编辑文本框 + *

+ * 自定义的EditText,用于笔记编辑界面,支持多行文本编辑、链接识别和上下文菜单。 + * 提供了与NoteEditActivity的交互接口,用于处理删除和回车事件。 + *

+ *

+ * 主要功能: + *

    + *
  • 支持多行文本编辑,每行是一个独立的EditText
  • + *
  • 识别并处理URL、电话号码、邮件地址等链接
  • + *
  • 处理删除和回车事件,通知监听器
  • + *
  • 支持文本选择和上下文菜单
  • + *
+ *

+ * + * @see NoteEditActivity + */ public class NoteEditText extends EditText { + // 日志标签 private static final String TAG = "NoteEditText"; + // 当前EditText的索引 private int mIndex; + // 删除前的光标位置 private int mSelectionStartBeforeDelete; + // 电话号码URI方案 private static final String SCHEME_TEL = "tel:" ; + // HTTP URI方案 private static final String SCHEME_HTTP = "http:" ; + // 邮件URI方案 private static final String SCHEME_EMAIL = "mailto:" ; + // URI方案与上下文菜单资源ID的映射 private static final Map sSchemaActionResMap = new HashMap(); static { sSchemaActionResMap.put(SCHEME_TEL, R.string.note_link_tel); @@ -54,66 +79,119 @@ public class NoteEditText extends EditText { } /** - * Call by the {@link NoteEditActivity} to delete or add edit text + * 文本视图变更监听器接口 + *

+ * 由NoteEditActivity实现,用于处理EditText的删除、回车和文本变更事件。 + *

+ * + * @see NoteEditActivity */ public interface OnTextViewChangeListener { /** - * Delete current edit text when {@link KeyEvent#KEYCODE_DEL} happens - * and the text is null + * 当按下删除键且文本为空时调用 + * + * @param index 当前EditText的索引 + * @param text 当前EditText中的文本内容 */ void onEditTextDelete(int index, String text); /** - * Add edit text after current edit text when {@link KeyEvent#KEYCODE_ENTER} - * happen + * 当按下回车键时调用 + * + * @param index 当前EditText的索引 + * @param text 当前EditText中的文本内容 */ void onEditTextEnter(int index, String text); /** - * Hide or show item option when text change + * 当文本内容变更时调用 + * + * @param index 当前EditText的索引 + * @param hasText 是否有文本内容 */ void onTextChange(int index, boolean hasText); } + // 文本视图变更监听器 private OnTextViewChangeListener mOnTextViewChangeListener; + /** + * 构造器 + * + * @param context 应用上下文 + */ public NoteEditText(Context context) { super(context, null); mIndex = 0; } + /** + * 设置当前EditText的索引 + * + * @param index EditText的索引值 + */ public void setIndex(int index) { mIndex = index; } + /** + * 设置文本视图变更监听器 + * + * @param listener 文本视图变更监听器对象 + */ public void setOnTextViewChangeListener(OnTextViewChangeListener listener) { mOnTextViewChangeListener = listener; } + /** + * 构造器 + * + * @param context 应用上下文 + * @param attrs XML属性集 + */ public NoteEditText(Context context, AttributeSet attrs) { super(context, attrs, android.R.attr.editTextStyle); } + /** + * 构造器 + * + * @param context 应用上下文 + * @param attrs XML属性集 + * @param defStyle 默认样式 + */ public NoteEditText(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); - // TODO Auto-generated constructor stub } + /** + * 处理触摸事件 + * + * 根据触摸位置设置文本选择光标的位置 + * + * @param event 触摸事件对象 + * @return 如果事件被处理返回true,否则返回false + */ @Override public boolean onTouchEvent(MotionEvent event) { switch (event.getAction()) { case MotionEvent.ACTION_DOWN: - + // 获取触摸坐标 int x = (int) event.getX(); int y = (int) event.getY(); + // 减去内边距,得到内容区域的坐标 x -= getTotalPaddingLeft(); y -= getTotalPaddingTop(); + // 加上滚动偏移量 x += getScrollX(); y += getScrollY(); Layout layout = getLayout(); + // 获取触摸点所在的行号 int line = layout.getLineForVertical(y); + // 获取触摸点在行中的字符偏移量 int off = layout.getOffsetForHorizontal(line, x); + // 设置文本选择光标位置 Selection.setSelection(getText(), off); break; } @@ -121,15 +199,26 @@ public class NoteEditText extends EditText { return super.onTouchEvent(event); } + /** + * 处理按键按下事件 + * + * 处理删除键和回车键的按下事件 + * + * @param keyCode 按键代码 + * @param event 按键事件对象 + * @return 如果事件被处理返回true,否则返回false + */ @Override public boolean onKeyDown(int keyCode, KeyEvent event) { switch (keyCode) { case KeyEvent.KEYCODE_ENTER: + // 如果设置了监听器,返回false让onKeyUp处理 if (mOnTextViewChangeListener != null) { return false; } break; case KeyEvent.KEYCODE_DEL: + // 记录删除前的光标位置 mSelectionStartBeforeDelete = getSelectionStart(); break; default: @@ -138,11 +227,22 @@ public class NoteEditText extends EditText { return super.onKeyDown(keyCode, event); } + /** + * 处理按键抬起事件 + * + * 处理删除键和回车键的抬起事件,通知监听器执行相应操作 + * + * @param keyCode 按键代码 + * @param event 按键事件对象 + * @return 如果事件被处理返回true,否则返回false + */ @Override public boolean onKeyUp(int keyCode, KeyEvent event) { switch(keyCode) { case KeyEvent.KEYCODE_DEL: + // 处理删除键 if (mOnTextViewChangeListener != null) { + // 如果光标在开头且不是第一个EditText,删除当前EditText if (0 == mSelectionStartBeforeDelete && mIndex != 0) { mOnTextViewChangeListener.onEditTextDelete(mIndex, getText().toString()); return true; @@ -152,10 +252,14 @@ public class NoteEditText extends EditText { } break; case KeyEvent.KEYCODE_ENTER: + // 处理回车键 if (mOnTextViewChangeListener != null) { int selectionStart = getSelectionStart(); + // 获取光标后的文本 String text = getText().subSequence(selectionStart, length()).toString(); + // 保留光标前的文本 setText(getText().subSequence(0, selectionStart)); + // 通知监听器创建新的EditText mOnTextViewChangeListener.onEditTextEnter(mIndex + 1, text); } else { Log.d(TAG, "OnTextViewChangeListener was not seted"); @@ -167,10 +271,20 @@ public class NoteEditText extends EditText { return super.onKeyUp(keyCode, event); } + /** + * 焦点变更时的处理 + * + * 当失去焦点且文本为空时,通知监听器 + * + * @param focused 是否获得焦点 + * @param direction 焦点移动方向 + * @param previouslyFocusedRect 之前获得焦点的视图矩形 + */ @Override protected void onFocusChanged(boolean focused, int direction, Rect previouslyFocusedRect) { if (mOnTextViewChangeListener != null) { if (!focused && TextUtils.isEmpty(getText())) { + // 失去焦点且文本为空 mOnTextViewChangeListener.onTextChange(mIndex, false); } else { mOnTextViewChangeListener.onTextChange(mIndex, true); @@ -179,18 +293,28 @@ public class NoteEditText extends EditText { super.onFocusChanged(focused, direction, previouslyFocusedRect); } + /** + * 创建上下文菜单 + * + * 如果选中的文本包含URL链接,添加相应的菜单项 + * + * @param menu 上下文菜单对象 + */ @Override protected void onCreateContextMenu(ContextMenu menu) { if (getText() instanceof Spanned) { int selStart = getSelectionStart(); int selEnd = getSelectionEnd(); + // 获取选区的起始和结束位置 int min = Math.min(selStart, selEnd); int max = Math.max(selStart, selEnd); + // 获取选区内的所有URLSpan final URLSpan[] urls = ((Spanned) getText()).getSpans(min, max, URLSpan.class); if (urls.length == 1) { int defaultResId = 0; + // 根据URL类型确定菜单项文本 for(String schema: sSchemaActionResMap.keySet()) { if(urls[0].getURL().indexOf(schema) >= 0) { defaultResId = sSchemaActionResMap.get(schema); @@ -202,10 +326,11 @@ public class NoteEditText extends EditText { defaultResId = R.string.note_link_other; } + // 添加菜单项 menu.add(0, 0, 0, defaultResId).setOnMenuItemClickListener( new OnMenuItemClickListener() { public boolean onMenuItemClick(MenuItem item) { - // goto a new intent + // 点击菜单项时打开链接 urls[0].onClick(NoteEditText.this); return true; } diff --git a/src/Notes-master/src/net/micode/notes/ui/NoteItemData.java b/src/Notes-master/src/net/micode/notes/ui/NoteItemData.java index 0f5a878..024cb5d 100644 --- a/src/Notes-master/src/net/micode/notes/ui/NoteItemData.java +++ b/src/Notes-master/src/net/micode/notes/ui/NoteItemData.java @@ -25,8 +25,27 @@ import net.micode.notes.data.Notes; import net.micode.notes.data.Notes.NoteColumns; import net.micode.notes.tool.DataUtils; - +/** + * 笔记项数据类 + *

+ * 用于封装笔记列表项的数据信息,从数据库游标中提取笔记的各项属性, + * 并提供便捷的访问方法。该类支持普通笔记、文件夹和通话记录笔记等多种类型。 + *

+ *

+ * 主要功能: + *

    + *
  • 从数据库游标中提取笔记数据
  • + *
  • 判断笔记在列表中的位置状态(首项、末项、唯一项等)
  • + *
  • 判断笔记是否跟随文件夹显示
  • + *
  • 处理通话记录笔记的特殊逻辑
  • + *
+ *

+ * + * @see NotesListItem + * @see NotesListAdapter + */ public class NoteItemData { + // 数据库查询投影,指定需要从笔记表中获取的列 static final String [] PROJECTION = new String [] { NoteColumns.ID, NoteColumns.ALERTED_DATE, @@ -42,6 +61,7 @@ public class NoteItemData { NoteColumns.WIDGET_TYPE, }; + // 列索引常量,用于从查询结果中获取对应列的数据 private static final int ID_COLUMN = 0; private static final int ALERTED_DATE_COLUMN = 1; private static final int BG_COLOR_ID_COLUMN = 2; @@ -55,27 +75,55 @@ public class NoteItemData { private static final int WIDGET_ID_COLUMN = 10; private static final int WIDGET_TYPE_COLUMN = 11; + // 笔记ID private long mId; + // 提醒日期 private long mAlertDate; + // 背景颜色ID private int mBgColorId; + // 创建日期 private long mCreatedDate; + // 是否有附件 private boolean mHasAttachment; + // 修改日期 private long mModifiedDate; + // 笔记数量(用于文件夹) private int mNotesCount; + // 父文件夹ID private long mParentId; + // 笔记摘要 private String mSnippet; + // 笔记类型 private int mType; + // 桌面小部件ID private int mWidgetId; + // 桌面小部件类型 private int mWidgetType; + // 联系人名称(用于通话记录) private String mName; + // 电话号码(用于通话记录) private String mPhoneNumber; + // 是否为列表最后一项 private boolean mIsLastItem; + // 是否为列表第一项 private boolean mIsFirstItem; + // 是否为列表唯一一项 private boolean mIsOnlyOneItem; + // 是否为文件夹后跟随的单个笔记 private boolean mIsOneNoteFollowingFolder; + // 是否为文件夹后跟随的多个笔记之一 private boolean mIsMultiNotesFollowingFolder; + /** + * 构造器 + * + * 从数据库游标中提取笔记数据并初始化各项属性。 + * 对于通话记录笔记,会额外获取联系人信息。 + * + * @param context 应用上下文,用于访问内容提供者和联系人信息 + * @param cursor 数据库游标,包含笔记数据,游标必须包含PROJECTION中指定的所有列 + */ public NoteItemData(Context context, Cursor cursor) { mId = cursor.getLong(ID_COLUMN); mAlertDate = cursor.getLong(ALERTED_DATE_COLUMN); @@ -86,6 +134,7 @@ public class NoteItemData { mNotesCount = cursor.getInt(NOTES_COUNT_COLUMN); mParentId = cursor.getLong(PARENT_ID_COLUMN); mSnippet = cursor.getString(SNIPPET_COLUMN); + // 移除清单项的勾选标记,只保留文本内容 mSnippet = mSnippet.replace(NoteEditActivity.TAG_CHECKED, "").replace( NoteEditActivity.TAG_UNCHECKED, ""); mType = cursor.getInt(TYPE_COLUMN); @@ -93,10 +142,12 @@ public class NoteItemData { mWidgetType = cursor.getInt(WIDGET_TYPE_COLUMN); mPhoneNumber = ""; + // 如果是通话记录笔记,获取电话号码和联系人名称 if (mParentId == Notes.ID_CALL_RECORD_FOLDER) { mPhoneNumber = DataUtils.getCallNumberByNoteId(context.getContentResolver(), mId); if (!TextUtils.isEmpty(mPhoneNumber)) { mName = Contact.getContact(context, mPhoneNumber); + // 如果找不到联系人,使用电话号码作为名称 if (mName == null) { mName = mPhoneNumber; } @@ -106,9 +157,17 @@ public class NoteItemData { if (mName == null) { mName = ""; } + // 检查当前项在列表中的位置状态 checkPostion(cursor); } + /** + * 检查当前项在列表中的位置状态 + * + * 判断当前项是否为首项、末项、唯一项,以及是否跟随文件夹显示。 + * + * @param cursor 数据库游标,用于判断位置状态 + */ private void checkPostion(Cursor cursor) { mIsLastItem = cursor.isLast() ? true : false; mIsFirstItem = cursor.isFirst() ? true : false; @@ -116,17 +175,21 @@ public class NoteItemData { mIsMultiNotesFollowingFolder = false; mIsOneNoteFollowingFolder = false; + // 如果是普通笔记且不是第一项,检查前一项是否为文件夹 if (mType == Notes.TYPE_NOTE && !mIsFirstItem) { int position = cursor.getPosition(); if (cursor.moveToPrevious()) { + // 前一项是文件夹或系统文件夹 if (cursor.getInt(TYPE_COLUMN) == Notes.TYPE_FOLDER || cursor.getInt(TYPE_COLUMN) == Notes.TYPE_SYSTEM) { + // 检查文件夹后是否还有更多笔记 if (cursor.getCount() > (position + 1)) { mIsMultiNotesFollowingFolder = true; } else { mIsOneNoteFollowingFolder = true; } } + // 移动回原位置 if (!cursor.moveToNext()) { throw new IllegalStateException("cursor move to previous but can't move back"); } @@ -134,90 +197,203 @@ public class NoteItemData { } } + /** + * 判断是否为文件夹后跟随的单个笔记 + * + * @return 如果是文件夹后跟随的单个笔记返回true,否则返回false + */ public boolean isOneFollowingFolder() { return mIsOneNoteFollowingFolder; } + /** + * 判断是否为文件夹后跟随的多个笔记之一 + * + * @return 如果是文件夹后跟随的多个笔记之一返回true,否则返回false + */ public boolean isMultiFollowingFolder() { return mIsMultiNotesFollowingFolder; } + /** + * 判断是否为列表最后一项 + * + * @return 如果是最后一项返回true,否则返回false + */ public boolean isLast() { return mIsLastItem; } + /** + * 获取通话记录的联系人名称 + * + * @return 联系人名称,如果不是通话记录或找不到联系人则返回空字符串 + */ public String getCallName() { return mName; } + /** + * 判断是否为列表第一项 + * + * @return 如果是第一项返回true,否则返回false + */ public boolean isFirst() { return mIsFirstItem; } + /** + * 判断是否为列表唯一一项 + * + * @return 如果是唯一一项返回true,否则返回false + */ public boolean isSingle() { return mIsOnlyOneItem; } + /** + * 获取笔记ID + * + * @return 笔记ID + */ public long getId() { return mId; } + /** + * 获取提醒日期 + * + * @return 提醒日期(毫秒时间戳),如果没有设置提醒则返回0 + */ public long getAlertDate() { return mAlertDate; } + /** + * 获取创建日期 + * + * @return 创建日期(毫秒时间戳) + */ public long getCreatedDate() { return mCreatedDate; } + /** + * 判断笔记是否有附件 + * + * @return 如果有附件返回true,否则返回false + */ public boolean hasAttachment() { return mHasAttachment; } + /** + * 获取修改日期 + * + * @return 修改日期(毫秒时间戳) + */ public long getModifiedDate() { return mModifiedDate; } + /** + * 获取背景颜色ID + * + * @return 背景颜色ID + */ public int getBgColorId() { return mBgColorId; } + /** + * 获取父文件夹ID + * + * @return 父文件夹ID + */ public long getParentId() { return mParentId; } + /** + * 获取笔记数量 + * + * @return 笔记数量(主要用于文件夹类型) + */ public int getNotesCount() { return mNotesCount; } + /** + * 获取文件夹ID + * + * @return 文件夹ID(与getParentId相同) + */ public long getFolderId () { return mParentId; } + /** + * 获取笔记类型 + * + * @return 笔记类型,取值为Notes.TYPE_NOTE、Notes.TYPE_FOLDER或Notes.TYPE_SYSTEM + */ public int getType() { return mType; } + /** + * 获取桌面小部件类型 + * + * @return 桌面小部件类型 + */ public int getWidgetType() { return mWidgetType; } + /** + * 获取桌面小部件ID + * + * @return 桌面小部件ID + */ public int getWidgetId() { return mWidgetId; } + /** + * 获取笔记摘要 + * + * @return 笔记摘要文本(已移除清单项标记) + */ public String getSnippet() { return mSnippet; } + /** + * 判断是否设置了提醒 + * + * @return 如果设置了提醒返回true,否则返回false + */ public boolean hasAlert() { return (mAlertDate > 0); } + /** + * 判断是否为通话记录笔记 + * + * @return 如果是通话记录笔记且包含电话号码返回true,否则返回false + */ public boolean isCallRecord() { return (mParentId == Notes.ID_CALL_RECORD_FOLDER && !TextUtils.isEmpty(mPhoneNumber)); } + /** + * 从游标中获取笔记类型 + * + * 静态方法,直接从游标中读取类型列的值,无需创建NoteItemData对象 + * + * @param cursor 数据库游标,必须包含TYPE_COLUMN列 + * @return 笔记类型 + */ public static int getNoteType(Cursor cursor) { return cursor.getInt(TYPE_COLUMN); } diff --git a/src/Notes-master/src/net/micode/notes/ui/NotesListActivity.java b/src/Notes-master/src/net/micode/notes/ui/NotesListActivity.java index e843aec..6ff1b90 100644 --- a/src/Notes-master/src/net/micode/notes/ui/NotesListActivity.java +++ b/src/Notes-master/src/net/micode/notes/ui/NotesListActivity.java @@ -78,63 +78,119 @@ import java.io.InputStream; import java.io.InputStreamReader; import java.util.HashSet; +/** + * 笔记列表活动 + * + * 这个类是应用的主界面,用于显示笔记列表并提供笔记管理功能。 + * 支持创建、编辑、删除笔记,文件夹管理,笔记同步,以及桌面小部件集成。 + * + * 主要功能: + * 1. 显示笔记列表,支持按文件夹分类查看 + * 2. 创建新笔记和文件夹 + * 3. 批量选择和操作笔记(删除、移动) + * 4. 笔记同步到 Google Tasks + * 5. 导出笔记为文本文件 + * 6. 与桌面小部件集成 + * + * @see NoteEditActivity + * @see NotesListAdapter + * @see GTaskSyncService + */ 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; + // 首次使用应用时添加介绍笔记的偏好设置键 private static final String PREFERENCE_ADD_INTRODUCTION = "net.micode.notes.introduction"; + /** + * 列表编辑状态枚举 + * + * 定义笔记列表的三种显示状态 + */ private enum ListEditState { - NOTE_LIST, SUB_FOLDER, CALL_RECORD_FOLDER + NOTE_LIST, // 笔记列表状态 + SUB_FOLDER, // 子文件夹状态 + CALL_RECORD_FOLDER // 通话记录文件夹状态 }; + // 当前列表编辑状态 private ListEditState mState; + // 后台查询处理器 private BackgroundQueryHandler mBackgroundQueryHandler; + // 笔记列表适配器 private NotesListAdapter mNotesListAdapter; + // 笔记列表视图 private ListView mNotesListView; + // 新建笔记按钮 private Button mAddNewNote; + // 是否正在分发触摸事件 private boolean mDispatch; + // 触摸事件的原始Y坐标 private int mOriginY; + // 分发触摸事件的Y坐标 private int mDispatchY; + // 标题栏文本视图 private TextView mTitleBar; + // 当前文件夹ID private long mCurrentFolderId; + // 内容解析器 private ContentResolver mContentResolver; + // 多选模式回调 private ModeCallback mModeCallBack; private static final String TAG = "NotesListActivity"; + // 笔记列表滚动速率 public static final int NOTES_LISTVIEW_SCROLL_RATE = 30; + // 当前聚焦的笔记数据项 private NoteItemData mFocusNoteDataItem; + // 普通选择条件:指定父文件夹ID private static final String NORMAL_SELECTION = NoteColumns.PARENT_ID + "=?"; + // 根文件夹选择条件:显示所有非系统笔记和有内容的通话记录文件夹 private static final String ROOT_FOLDER_SELECTION = "(" + NoteColumns.TYPE + "<>" + Notes.TYPE_SYSTEM + " AND " + NoteColumns.PARENT_ID + "=?)" + " OR (" + NoteColumns.ID + "=" + Notes.ID_CALL_RECORD_FOLDER + " AND " + NoteColumns.NOTES_COUNT + ">0)"; + // 打开笔记的请求码 private final static int REQUEST_CODE_OPEN_NODE = 102; + // 新建笔记的请求码 private final static int REQUEST_CODE_NEW_NODE = 103; + /** + * 活动创建时的初始化方法 + * + * 设置布局,初始化资源,首次使用时添加介绍笔记 + * + * @param savedInstanceState 保存的实例状态 + */ @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); @@ -147,6 +203,15 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt setAppInfoFromRawRes(); } + /** + * 活动结果回调方法 + * + * 当从笔记编辑活动返回时,刷新笔记列表 + * + * @param requestCode 请求码,标识是哪个活动返回 + * @param resultCode 结果码,RESULT_OK表示操作成功 + * @param data 返回的Intent数据 + */ @Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { if (resultCode == RESULT_OK @@ -157,6 +222,11 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt } } + /** + * 从原始资源文件加载并创建介绍笔记 + * + * 首次使用应用时,从res/raw/introduction文件读取内容并创建一条介绍笔记 + */ private void setAppInfoFromRawRes() { SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(this); if (!sp.getBoolean(PREFERENCE_ADD_INTRODUCTION, false)) { @@ -203,12 +273,22 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt } } + /** + * 活动启动时的回调方法 + * + * 启动异步查询笔记列表 + */ @Override protected void onStart() { super.onStart(); startAsyncNotesListQuery(); } + /** + * 初始化资源 + * + * 初始化所有UI组件、适配器和监听器 + */ private void initResources() { mContentResolver = this.getContentResolver(); mBackgroundQueryHandler = new BackgroundQueryHandler(this.getContentResolver()); @@ -231,11 +311,23 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt mModeCallBack = new ModeCallback(); } + /** + * 多选模式回调类 + * + * 实现ListView.MultiChoiceModeListener接口,处理多选模式的创建、销毁和项选中状态变化 + */ private class ModeCallback implements ListView.MultiChoiceModeListener, OnMenuItemClickListener { private DropdownMenu mDropDownMenu; private ActionMode mActionMode; private MenuItem mMoveMenu; + /** + * 创建多选模式的操作栏 + * + * @param mode ActionMode对象 + * @param menu 菜单对象 + * @return true表示成功创建 + */ public boolean onCreateActionMode(ActionMode mode, Menu menu) { getMenuInflater().inflate(R.menu.note_list_options, menu); menu.findItem(R.id.delete).setOnMenuItemClickListener(this); @@ -259,7 +351,13 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt (Button) customView.findViewById(R.id.selection_menu), R.menu.note_list_dropdown); mDropDownMenu.setOnDropdownMenuItemClickListener(new PopupMenu.OnMenuItemClickListener(){ - public boolean onMenuItemClick(MenuItem item) { + /** + * 下拉菜单项点击事件处理 + * + * @param item 被点击的菜单项 + * @return true表示事件已处理 + */ + public boolean onMenuItemClick(MenuItem item) { mNotesListAdapter.selectAll(!mNotesListAdapter.isAllSelected()); updateMenu(); return true; @@ -269,6 +367,11 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt return true; } + /** + * 更新菜单显示 + * + * 根据选中数量更新下拉菜单标题和全选按钮状态 + */ private void updateMenu() { int selectedCount = mNotesListAdapter.getSelectedCount(); // Update dropdown menu @@ -286,26 +389,60 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt } } + /** + * 准备多选模式的操作栏 + * + * @param mode ActionMode对象 + * @param menu 菜单对象 + * @return false + */ public boolean onPrepareActionMode(ActionMode mode, Menu menu) { // TODO Auto-generated method stub return false; } + /** + * 操作栏菜单项点击事件处理 + * + * @param mode ActionMode对象 + * @param item 被点击的菜单项 + * @return false + */ public boolean onActionItemClicked(ActionMode mode, MenuItem item) { // TODO Auto-generated method stub return false; } + /** + * 销毁多选模式的操作栏 + * + * 退出选择模式,恢复列表视图的常规状态 + * + * @param mode ActionMode对象 + */ public void onDestroyActionMode(ActionMode mode) { mNotesListAdapter.setChoiceMode(false); mNotesListView.setLongClickable(true); mAddNewNote.setVisibility(View.VISIBLE); } + /** + * 完成多选模式 + * + * 手动结束ActionMode + */ public void finishActionMode() { mActionMode.finish(); } + /** + * 列表项选中状态变化事件处理 + * + * @param mode ActionMode对象 + * @param position 列表项位置 + * @param id 列表项ID + * @param checked 是否选中 + */ public void onItemCheckedStateChanged(ActionMode mode, int position, long id, boolean checked) { mNotesListAdapter.setCheckedItem(position, checked); @@ -408,6 +545,13 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt }; + /** + * 启动异步笔记列表查询 + *

+ * 根据当前文件夹ID构建查询条件,启动后台查询获取笔记列表数据。 + * 根文件夹使用特殊的查询条件,子文件夹使用普通查询条件。 + *

+ */ private void startAsyncNotesListQuery() { String selection = (mCurrentFolderId == Notes.ID_ROOT_FOLDER) ? ROOT_FOLDER_SELECTION : NORMAL_SELECTION; @@ -417,11 +561,35 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt }, NoteColumns.TYPE + " DESC," + NoteColumns.MODIFIED_DATE + " DESC"); } + /** + * 后台查询处理器 + *

+ * 继承自AsyncQueryHandler,用于在后台线程执行数据库查询, + * 避免阻塞UI线程。 + *

+ */ private final class BackgroundQueryHandler extends AsyncQueryHandler { + /** + * 构造函数 + * @param contentResolver 内容解析器 + */ public BackgroundQueryHandler(ContentResolver contentResolver) { super(contentResolver); } + /** + * 查询完成回调 + *

+ * 根据查询令牌处理不同的查询结果: + *

    + *
  • FOLDER_NOTE_LIST_QUERY_TOKEN: 更新笔记列表适配器
  • + *
  • FOLDER_LIST_QUERY_TOKEN: 显示文件夹选择菜单
  • + *
+ *

+ * @param token 查询令牌 + * @param cookie Cookie对象 + * @param cursor 查询结果游标 + */ @Override protected void onQueryComplete(int token, Object cookie, Cursor cursor) { switch (token) { @@ -441,6 +609,14 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt } } + /** + * 显示文件夹选择菜单 + *

+ * 显示一个对话框,列出所有可用的目标文件夹供用户选择, + * 用于移动选中的笔记到指定文件夹。 + *

+ * @param cursor 包含文件夹列表的游标 + */ private void showFolderListMenu(Cursor cursor) { AlertDialog.Builder builder = new AlertDialog.Builder(NotesListActivity.this); builder.setTitle(R.string.menu_title_select_folder); @@ -462,6 +638,12 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt builder.show(); } + /** + * 创建新笔记 + *

+ * 启动NoteEditActivity创建新笔记,传递当前文件夹ID。 + *

+ */ private void createNewNote() { Intent intent = new Intent(this, NoteEditActivity.class); intent.setAction(Intent.ACTION_INSERT_OR_EDIT); @@ -469,6 +651,14 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt this.startActivityForResult(intent, REQUEST_CODE_NEW_NODE); } + /** + * 批量删除笔记 + *

+ * 在后台线程中删除选中的笔记。 + * 如果处于同步模式,将笔记移动到垃圾箱文件夹; + * 否则直接删除。同时更新相关的小部件。 + *

+ */ private void batchDelete() { new AsyncTask>() { protected HashSet doInBackground(Void... unused) { @@ -506,6 +696,15 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt }.execute(); } + /** + * 删除文件夹 + *

+ * 删除指定的文件夹及其包含的所有笔记。 + * 如果处于同步模式,将文件夹移动到垃圾箱; + * 否则直接删除。同时更新相关的小部件。 + *

+ * @param folderId 要删除的文件夹ID + */ private void deleteFolder(long folderId) { if (folderId == Notes.ID_ROOT_FOLDER) { Log.e(TAG, "Wrong folder id, should not happen " + folderId); @@ -533,6 +732,13 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt } } + /** + * 打开笔记 + *

+ * 启动NoteEditActivity查看和编辑指定的笔记。 + *

+ * @param data 笔记数据项 + */ private void openNode(NoteItemData data) { Intent intent = new Intent(this, NoteEditActivity.class); intent.setAction(Intent.ACTION_VIEW); @@ -540,6 +746,14 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt this.startActivityForResult(intent, REQUEST_CODE_OPEN_NODE); } + /** + * 打开文件夹 + *

+ * 进入指定的文件夹,显示该文件夹中的笔记列表。 + * 更新标题栏显示文件夹名称,并隐藏新建笔记按钮(如果是通话记录文件夹)。 + *

+ * @param data 文件夹数据项 + */ private void openFolder(NoteItemData data) { mCurrentFolderId = data.getId(); startAsyncNotesListQuery(); @@ -567,6 +781,12 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt } } + /** + * 显示软键盘 + *

+ * 强制显示系统软键盘,用于输入文件夹名称。 + *

+ */ private void showSoftInput() { InputMethodManager inputMethodManager = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE); if (inputMethodManager != null) { @@ -574,11 +794,26 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt } } + /** + * 隐藏软键盘 + *

+ * 隐藏指定视图的软键盘。 + *

+ * @param view 要隐藏键盘的视图 + */ private void hideSoftInput(View view) { InputMethodManager inputMethodManager = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE); inputMethodManager.hideSoftInputFromWindow(view.getWindowToken(), 0); } + /** + * 显示创建或修改文件夹对话框 + *

+ * 显示一个对话框,允许用户输入文件夹名称。 + * 根据create参数决定是创建新文件夹还是修改现有文件夹名称。 + *

+ * @param create true表示创建新文件夹,false表示修改文件夹名称 + */ private void showCreateOrModifyFolderDialog(final boolean create) { final AlertDialog.Builder builder = new AlertDialog.Builder(this); View view = LayoutInflater.from(this).inflate(R.layout.dialog_edit_text, null); @@ -664,6 +899,16 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt }); } + /** + * 返回键按下处理 + *

+ * 根据当前列表状态处理返回键事件: + *

    + *
  • 子文件夹或通话记录文件夹:返回根文件夹列表
  • + *
  • 笔记列表:调用父类方法退出Activity
  • + *
+ *

+ */ @Override public void onBackPressed() { switch (mState) { @@ -688,6 +933,14 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt } } + /** + * 更新小部件 + *

+ * 发送广播更新指定的小部件,使其显示最新的笔记内容。 + *

+ * @param appWidgetId 小部件ID + * @param appWidgetType 小部件类型(2x或4x) + */ private void updateWidget(int appWidgetId, int appWidgetType) { Intent intent = new Intent(AppWidgetManager.ACTION_APPWIDGET_UPDATE); if (appWidgetType == Notes.TYPE_WIDGET_2X) { @@ -707,6 +960,12 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt setResult(RESULT_OK, intent); } + /** + * 文件夹上下文菜单创建监听器 + *

+ * 为文件夹项创建上下文菜单,提供查看、删除和重命名选项。 + *

+ */ private final OnCreateContextMenuListener mFolderOnCreateContextMenuListener = new OnCreateContextMenuListener() { public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) { if (mFocusNoteDataItem != null) { @@ -760,6 +1019,19 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt return true; } + /** + * 准备选项菜单 + *

+ * 根据当前列表状态加载不同的菜单资源: + *

    + *
  • 笔记列表:显示同步、设置、新建文件夹、导出、搜索等选项
  • + *
  • 子文件夹:显示新建笔记选项
  • + *
  • 通话记录文件夹:显示新建笔记选项
  • + *
+ *

+ * @param menu 选项菜单对象 + * @return true + */ @Override public boolean onPrepareOptionsMenu(Menu menu) { menu.clear(); @@ -778,6 +1050,22 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt return true; } + /** + * 选项菜单项选择处理 + *

+ * 处理用户点击选项菜单的事件,包括: + *

    + *
  • 新建文件夹
  • + *
  • 导出笔记为文本
  • + *
  • 同步或取消同步
  • + *
  • 打开设置
  • + *
  • 新建笔记
  • + *
  • 搜索
  • + *
+ *

+ * @param item 被点击的菜单项 + * @return true + */ @Override public boolean onOptionsItemSelected(MenuItem item) { switch (item.getItemId()) { @@ -818,12 +1106,26 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt return true; } + /** + * 搜索请求处理 + *

+ * 启动系统搜索界面,允许用户搜索笔记内容。 + *

+ * @return true + */ @Override public boolean onSearchRequested() { startSearch(null, false, null /* appData */, false); return true; } + /** + * 导出笔记为文本文件 + *

+ * 在后台线程中将所有笔记导出为文本文件到SD卡。 + * 根据导出结果显示相应的提示对话框。 + *

+ */ private void exportNoteToText() { final BackupUtils backup = BackupUtils.getInstance(NotesListActivity.this); new AsyncTask() { @@ -866,19 +1168,51 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt }.execute(); } + /** + * 检查是否处于同步模式 + *

+ * 判断是否已设置同步账户,如果已设置则表示处于同步模式。 + *

+ * @return true表示处于同步模式,false表示未同步 + */ private boolean isSyncMode() { return NotesPreferenceActivity.getSyncAccountName(this).trim().length() > 0; } + /** + * 启动设置Activity + *

+ * 启动NotesPreferenceActivity进行应用设置。 + *

+ */ private void startPreferenceActivity() { Activity from = getParent() != null ? getParent() : this; Intent intent = new Intent(from, NotesPreferenceActivity.class); from.startActivityIfNeeded(intent, -1); } + /** + * 列表项点击监听器 + *

+ * 处理笔记列表项的点击事件,根据当前状态和项类型执行相应操作: + *

    + *
  • 多选模式:切换选中状态
  • + *
  • 笔记列表:打开文件夹或笔记
  • + *
  • 子文件夹/通话记录文件夹:打开笔记
  • + *
+ *

+ */ private class OnListItemClickListener implements OnItemClickListener { - public void onItemClick(AdapterView parent, View view, int position, long id) { + /** + * 列表项点击事件处理 + * + * @param parent 父视图 + * @param view 被点击的视图 + * @param position 列表项位置 + * @param id 列表项ID + */ + public void onItemClick(AdapterView parent, View view, int position, long id) { if (view instanceof NotesListItem) { NoteItemData item = ((NotesListItem) view).getItemData(); if (mNotesListAdapter.isInChoiceMode()) { @@ -917,6 +1251,13 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt } + /** + * 启动查询目标文件夹 + *

+ * 查询所有可用的文件夹,用于显示在移动笔记的对话框中。 + * 排除垃圾箱文件夹和当前文件夹。 + *

+ */ private void startQueryDestinationFolders() { String selection = NoteColumns.TYPE + "=? AND " + NoteColumns.PARENT_ID + "<>? AND " + NoteColumns.ID + "<>?"; selection = (mState == ListEditState.NOTE_LIST) ? selection: @@ -935,6 +1276,15 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt NoteColumns.MODIFIED_DATE + " DESC"); } + /** + * 列表项长按事件处理 + * + * @param parent 父视图 + * @param view 被长按的视图 + * @param position 列表项位置 + * @param id 列表项ID + * @return true表示事件已处理 + */ public boolean onItemLongClick(AdapterView parent, View view, int position, long id) { if (view instanceof NotesListItem) { mFocusNoteDataItem = ((NotesListItem) view).getItemData(); diff --git a/src/Notes-master/src/net/micode/notes/ui/NotesListAdapter.java b/src/Notes-master/src/net/micode/notes/ui/NotesListAdapter.java index 51c9cb9..6085bf0 100644 --- a/src/Notes-master/src/net/micode/notes/ui/NotesListAdapter.java +++ b/src/Notes-master/src/net/micode/notes/ui/NotesListAdapter.java @@ -31,18 +31,51 @@ import java.util.HashSet; import java.util.Iterator; +/** + * 笔记列表适配器 + * + * 这个类继承自CursorAdapter,用于将数据库中的笔记数据绑定到ListView中显示。 + * 它支持笔记的选择模式、批量操作以及与桌面小部件的关联。 + * + * 主要功能: + * 1. 将笔记数据绑定到NotesListItem视图 + * 2. 支持多选模式和批量选择操作 + * 3. 获取选中的笔记ID和关联的桌面小部件信息 + * 4. 统计笔记数量和选中数量 + * + * @see NotesListItem + * @see NoteItemData + */ public class NotesListAdapter extends CursorAdapter { private static final String TAG = "NotesListAdapter"; + // 应用上下文 private Context mContext; + // 记录选中状态的Map,key为位置,value为是否选中 private HashMap mSelectedIndex; + // 笔记总数 private int mNotesCount; + // 是否处于选择模式 private boolean mChoiceMode; + /** + * 桌面小部件属性类 + * + * 用于存储桌面小部件的ID和类型信息 + */ public static class AppWidgetAttribute { + // 桌面小部件ID public int widgetId; + // 桌面小部件类型 public int widgetType; }; + /** + * 构造器 + * + * 初始化笔记列表适配器,创建选中状态Map和计数器 + * + * @param context 应用上下文,不能为 null + */ public NotesListAdapter(Context context) { super(context, null); mSelectedIndex = new HashMap(); @@ -50,11 +83,28 @@ public class NotesListAdapter extends CursorAdapter { mNotesCount = 0; } + /** + * 创建新的列表项视图 + * + * @param context 应用上下文 + * @param cursor 数据库游标,包含当前项的数据 + * @param parent 父视图 + * @return 新创建的NotesListItem视图对象 + */ @Override public View newView(Context context, Cursor cursor, ViewGroup parent) { return new NotesListItem(context); } + /** + * 绑定数据到视图 + * + * 将数据库游标中的数据绑定到已存在的视图上 + * + * @param view 需要绑定数据的视图 + * @param context 应用上下文 + * @param cursor 数据库游标,包含当前项的数据 + */ @Override public void bindView(View view, Context context, Cursor cursor) { if (view instanceof NotesListItem) { @@ -64,20 +114,41 @@ public class NotesListAdapter extends CursorAdapter { } } + /** + * 设置指定位置的选中状态 + * + * @param position 列表项位置,从0开始 + * @param checked 是否选中 + */ public void setCheckedItem(final int position, final boolean checked) { mSelectedIndex.put(position, checked); notifyDataSetChanged(); } + /** + * 判断是否处于选择模式 + * + * @return 如果处于选择模式返回true,否则返回false + */ public boolean isInChoiceMode() { return mChoiceMode; } + /** + * 设置选择模式 + * + * @param mode true表示进入选择模式,false表示退出选择模式 + */ public void setChoiceMode(boolean mode) { mSelectedIndex.clear(); mChoiceMode = mode; } + /** + * 全选或取消全选所有笔记 + * + * @param checked true表示全选,false表示取消全选 + */ public void selectAll(boolean checked) { Cursor cursor = getCursor(); for (int i = 0; i < getCount(); i++) { @@ -89,6 +160,11 @@ public class NotesListAdapter extends CursorAdapter { } } + /** + * 获取所有选中项的笔记ID集合 + * + * @return 包含所有选中笔记ID的HashSet集合,如果没有选中项则返回空集合 + */ public HashSet getSelectedItemIds() { HashSet itemSet = new HashSet(); for (Integer position : mSelectedIndex.keySet()) { @@ -105,6 +181,11 @@ public class NotesListAdapter extends CursorAdapter { return itemSet; } + /** + * 获取所有选中项关联的桌面小部件集合 + * + * @return 包含所有选中笔记关联的桌面小部件属性的HashSet集合,如果游标无效则返回null + */ public HashSet getSelectedWidget() { HashSet itemSet = new HashSet(); for (Integer position : mSelectedIndex.keySet()) { @@ -128,6 +209,11 @@ public class NotesListAdapter extends CursorAdapter { return itemSet; } + /** + * 获取选中项的数量 + * + * @return 选中项的数量,如果没有选中项则返回0 + */ public int getSelectedCount() { Collection values = mSelectedIndex.values(); if (null == values) { @@ -143,11 +229,22 @@ public class NotesListAdapter extends CursorAdapter { return count; } + /** + * 判断是否已全选所有笔记 + * + * @return 如果所有笔记都被选中且至少有一个笔记则返回true,否则返回false + */ public boolean isAllSelected() { int checkedCount = getSelectedCount(); return (checkedCount != 0 && checkedCount == mNotesCount); } + /** + * 判断指定位置的项是否被选中 + * + * @param position 列表项位置,从0开始 + * @return 如果该项被选中返回true,否则返回false + */ public boolean isSelectedItem(final int position) { if (null == mSelectedIndex.get(position)) { return false; @@ -155,12 +252,22 @@ public class NotesListAdapter extends CursorAdapter { return mSelectedIndex.get(position); } + /** + * 当内容发生变化时调用 + * + * 重新计算笔记数量 + */ @Override protected void onContentChanged() { super.onContentChanged(); calcNotesCount(); } + /** + * 更换游标 + * + * @param cursor 新的数据库游标 + */ @Override public void changeCursor(Cursor cursor) { super.changeCursor(cursor); diff --git a/src/Notes-master/src/net/micode/notes/ui/NotesListItem.java b/src/Notes-master/src/net/micode/notes/ui/NotesListItem.java index 1221e80..ad89d41 100644 --- a/src/Notes-master/src/net/micode/notes/ui/NotesListItem.java +++ b/src/Notes-master/src/net/micode/notes/ui/NotesListItem.java @@ -30,6 +30,14 @@ import net.micode.notes.tool.DataUtils; import net.micode.notes.tool.ResourceParser.NoteItemBgResources; +/** + * 笔记列表项视图 + *

+ * 自定义的 LinearLayout,表示笔记列表中的单个笔记项。 + * 该视图显示笔记信息,包括标题、时间、通话名称(针对通话记录)和提醒图标。 + * 支持在多选模式下显示复选框。 + *

+ */ public class NotesListItem extends LinearLayout { private ImageView mAlert; private TextView mTitle; @@ -38,6 +46,10 @@ public class NotesListItem extends LinearLayout { private NoteItemData mItemData; private CheckBox mCheckBox; + /** + * 构造函数 + * @param context 用于加载布局的上下文对象 + */ public NotesListItem(Context context) { super(context); inflate(context, R.layout.note_item, this); @@ -48,6 +60,13 @@ public class NotesListItem extends LinearLayout { mCheckBox = (CheckBox) findViewById(android.R.id.checkbox); } + /** + * 绑定笔记数据到视图项 + * @param context 用于访问资源的上下文对象 + * @param data 包含要显示的笔记信息的 NoteItemData 对象 + * @param choiceMode 列表是否处于多选模式(显示复选框) + * @param checked 该项是否被选中(仅在多选模式下有意义) + */ public void bind(Context context, NoteItemData data, boolean choiceMode, boolean checked) { if (choiceMode && data.getType() == Notes.TYPE_NOTE) { mCheckBox.setVisibility(View.VISIBLE); @@ -99,6 +118,10 @@ public class NotesListItem extends LinearLayout { setBackground(data); } + /** + * 根据笔记项的位置和类型设置合适的背景资源 + * @param data 包含笔记背景颜色和位置信息的 NoteItemData 对象 + */ private void setBackground(NoteItemData data) { int id = data.getBgColorId(); if (data.getType() == Notes.TYPE_NOTE) { @@ -116,6 +139,10 @@ public class NotesListItem extends LinearLayout { } } + /** + * 获取绑定到该视图项的笔记数据 + * @return 包含该笔记信息的 NoteItemData 对象 + */ public NoteItemData getItemData() { return mItemData; } diff --git a/src/Notes-master/src/net/micode/notes/ui/NotesPreferenceActivity.java b/src/Notes-master/src/net/micode/notes/ui/NotesPreferenceActivity.java index 07c5f7e..7f9475f 100644 --- a/src/Notes-master/src/net/micode/notes/ui/NotesPreferenceActivity.java +++ b/src/Notes-master/src/net/micode/notes/ui/NotesPreferenceActivity.java @@ -48,27 +48,85 @@ import net.micode.notes.data.Notes.NoteColumns; import net.micode.notes.gtask.remote.GTaskSyncService; +/** + * 设置界面Activity + *

+ * 该Activity用于管理应用的各种设置,主要包括: + *

    + *
  • Google Tasks同步账户的设置和管理
  • + *
  • 同步状态显示和手动同步控制
  • + *
  • 背景颜色随机显示设置
  • + *
+ *

+ *

+ * 该类继承自PreferenceActivity,使用SharedPreferences来持久化设置数据。 + * 通过GTaskReceiver接收同步服务的广播,实时更新同步状态。 + *

+ */ public class NotesPreferenceActivity extends PreferenceActivity { + /** + * SharedPreferences文件名 + */ public static final String PREFERENCE_NAME = "notes_preferences"; + /** + * 同步账户名称的SharedPreferences键 + */ public static final String PREFERENCE_SYNC_ACCOUNT_NAME = "pref_key_account_name"; + /** + * 最后同步时间的SharedPreferences键 + */ public static final String PREFERENCE_LAST_SYNC_TIME = "pref_last_sync_time"; + /** + * 背景颜色随机显示设置的SharedPreferences键 + */ public static final String PREFERENCE_SET_BG_COLOR_KEY = "pref_key_bg_random_appear"; + /** + * 同步账户分类的Preference键 + */ private static final String PREFERENCE_SYNC_ACCOUNT_KEY = "pref_sync_account_key"; + /** + * 账户授权过滤器键,用于添加账户Intent + */ private static final String AUTHORITIES_FILTER_KEY = "authorities"; + /** + * 同步账户分类的PreferenceCategory + */ private PreferenceCategory mAccountCategory; + /** + * 同步服务广播接收器 + */ private GTaskReceiver mReceiver; + /** + * 原始账户数组,用于检测新增账户 + */ private Account[] mOriAccounts; + /** + * 是否添加了新账户的标志 + */ private boolean mHasAddedAccount; + /** + * 创建Activity + *

+ * 初始化设置界面,包括: + *

    + *
  • 启用ActionBar的返回导航
  • + *
  • 加载preferences.xml配置文件
  • + *
  • 初始化账户分类和广播接收器
  • + *
  • 添加设置界面头部视图
  • + *
+ *

+ * @param icicle 保存的实例状态 + */ @Override protected void onCreate(Bundle icicle) { super.onCreate(icicle); @@ -88,6 +146,13 @@ public class NotesPreferenceActivity extends PreferenceActivity { getListView().addHeaderView(header, null, true); } + /** + * Activity恢复时调用 + *

+ * 检查是否有新添加的Google账户,如果有则自动设置为同步账户。 + * 然后刷新UI显示。 + *

+ */ @Override protected void onResume() { super.onResume(); @@ -116,6 +181,12 @@ public class NotesPreferenceActivity extends PreferenceActivity { refreshUI(); } + /** + * Activity销毁时调用 + *

+ * 注销同步服务广播接收器,防止内存泄漏。 + *

+ */ @Override protected void onDestroy() { if (mReceiver != null) { @@ -124,6 +195,18 @@ public class NotesPreferenceActivity extends PreferenceActivity { super.onDestroy(); } + /** + * 加载账户设置选项 + *

+ * 创建并添加账户Preference到账户分类中。 + * 点击该Preference时: + *

    + *
  • 如果未设置账户,显示账户选择对话框
  • + *
  • 如果已设置账户,显示确认更改账户对话框
  • + *
  • 如果正在同步,显示提示消息
  • + *
+ *

+ */ private void loadAccountPreference() { mAccountCategory.removeAll(); @@ -154,6 +237,17 @@ public class NotesPreferenceActivity extends PreferenceActivity { mAccountCategory.addPreference(accountPref); } + /** + * 加载同步按钮和同步状态显示 + *

+ * 根据当前同步状态设置按钮文本和点击事件: + *

    + *
  • 正在同步:显示"取消同步"按钮,点击取消同步
  • + *
  • 未同步:显示"立即同步"按钮,点击开始同步
  • + *
+ * 同时显示最后同步时间或当前同步进度。 + *

+ */ private void loadSyncButton() { Button syncButton = (Button) findViewById(R.id.preference_sync_button); TextView lastSyncTimeView = (TextView) findViewById(R.id.prefenerece_sync_status_textview); @@ -193,11 +287,24 @@ public class NotesPreferenceActivity extends PreferenceActivity { } } + /** + * 刷新UI显示 + *

+ * 重新加载账户设置选项和同步按钮状态。 + *

+ */ private void refreshUI() { loadAccountPreference(); loadSyncButton(); } + /** + * 显示选择账户对话框 + *

+ * 显示一个对话框,列出所有可用的Google账户供用户选择。 + * 同时提供"添加账户"选项,点击后跳转到系统账户添加界面。 + *

+ */ private void showSelectAccountAlertDialog() { AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(this); @@ -254,6 +361,17 @@ public class NotesPreferenceActivity extends PreferenceActivity { }); } + /** + * 显示更改账户确认对话框 + *

+ * 显示一个对话框,提供三个选项: + *

    + *
  • 更改账户:显示账户选择对话框
  • + *
  • 移除账户:删除当前同步账户并清理相关数据
  • + *
  • 取消:关闭对话框
  • + *
+ *

+ */ private void showChangeAccountConfirmAlertDialog() { AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(this); @@ -283,11 +401,29 @@ public class NotesPreferenceActivity extends PreferenceActivity { dialogBuilder.show(); } + /** + * 获取所有Google账户 + *

+ * 从系统AccountManager中获取所有类型为"com.google"的账户。 + *

+ * @return Google账户数组 + */ private Account[] getGoogleAccounts() { AccountManager accountManager = AccountManager.get(this); return accountManager.getAccountsByType("com.google"); } + /** + * 设置同步账户 + *

+ * 保存指定的账户名称到SharedPreferences,并清理相关数据: + *

    + *
  • 清除最后同步时间
  • + *
  • 清除所有笔记的GTASK_ID和SYNC_ID
  • + *
+ *

+ * @param account 要设置的账户名称 + */ private void setSyncAccount(String account) { if (!getSyncAccountName(this).equals(account)) { SharedPreferences settings = getSharedPreferences(PREFERENCE_NAME, Context.MODE_PRIVATE); @@ -318,6 +454,13 @@ public class NotesPreferenceActivity extends PreferenceActivity { } } + /** + * 移除同步账户 + *

+ * 从SharedPreferences中删除同步账户和最后同步时间, + * 并清理所有笔记的GTASK_ID和SYNC_ID。 + *

+ */ private void removeSyncAccount() { SharedPreferences settings = getSharedPreferences(PREFERENCE_NAME, Context.MODE_PRIVATE); SharedPreferences.Editor editor = settings.edit(); @@ -340,12 +483,28 @@ public class NotesPreferenceActivity extends PreferenceActivity { }).start(); } + /** + * 获取同步账户名称 + *

+ * 从SharedPreferences中读取已设置的同步账户名称。 + *

+ * @param context 上下文对象 + * @return 同步账户名称,如果未设置则返回空字符串 + */ public static String getSyncAccountName(Context context) { SharedPreferences settings = context.getSharedPreferences(PREFERENCE_NAME, Context.MODE_PRIVATE); return settings.getString(PREFERENCE_SYNC_ACCOUNT_NAME, ""); } + /** + * 设置最后同步时间 + *

+ * 将指定的同步时间保存到SharedPreferences。 + *

+ * @param context 上下文对象 + * @param time 同步时间戳 + */ public static void setLastSyncTime(Context context, long time) { SharedPreferences settings = context.getSharedPreferences(PREFERENCE_NAME, Context.MODE_PRIVATE); @@ -354,14 +513,36 @@ public class NotesPreferenceActivity extends PreferenceActivity { editor.commit(); } + /** + * 获取最后同步时间 + *

+ * 从SharedPreferences中读取最后同步时间。 + *

+ * @param context 上下文对象 + * @return 最后同步时间戳,如果未同步过则返回0 + */ public static long getLastSyncTime(Context context) { SharedPreferences settings = context.getSharedPreferences(PREFERENCE_NAME, Context.MODE_PRIVATE); return settings.getLong(PREFERENCE_LAST_SYNC_TIME, 0); } + /** + * 同步服务广播接收器 + *

+ * 接收GTaskSyncService发送的广播,实时更新UI显示同步状态和进度。 + *

+ */ private class GTaskReceiver extends BroadcastReceiver { + /** + * 接收广播 + *

+ * 当收到同步服务广播时,刷新UI并更新同步状态显示。 + *

+ * @param context 上下文对象 + * @param intent 广播Intent + */ @Override public void onReceive(Context context, Intent intent) { refreshUI(); @@ -374,6 +555,15 @@ public class NotesPreferenceActivity extends PreferenceActivity { } } + /** + * 处理菜单项选择 + *

+ * 处理ActionBar上的菜单项点击事件。 + * 当点击返回按钮时,返回到笔记列表界面。 + *

+ * @param item 被点击的菜单项 + * @return true表示已处理,false表示未处理 + */ public boolean onOptionsItemSelected(MenuItem item) { switch (item.getItemId()) { case android.R.id.home: -- 2.34.1 From b4d68f5c9ea03b7c04d84520d19c8a744ad887c3 Mon Sep 17 00:00:00 2001 From: JTXjtx Date: Tue, 23 Dec 2025 21:49:53 +0800 Subject: [PATCH 3/3] =?UTF-8?q?=E4=BF=AE=E6=94=B9=E6=8A=A5=E5=91=8A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- doc/小米便签泛读报告.docx | Bin 206027 -> 210756 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/doc/小米便签泛读报告.docx b/doc/小米便签泛读报告.docx index 5a7866a14e76138dacf3508ac9788786e6d23d2c..d47e0659e7a1cb4c5064ee70167b162d845c0e88 100644 GIT binary patch delta 24656 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 F{{fYYp5Xuh delta 19861 zcmV)WK(4>U?hMPx43ISmn?klTYXJZNe33pGf5A@UFciG6#6Lv2CwAPV6!dCT0-O+A zsevAm<-9f)iEY^i(Z9!`t8BP(doyE?#~#DY=g=n~=)5v|ofbJw6V%eQN*~whbMug` z(!>X$ThSYh>-2;^z2O&sVC#%U7Zmy=T4=vcyAbR(AzpSk2%kq;N9ADLAVP%hNX+4& ze@6aWAK2mpsp;y|PA%<)+Plh*+u ze-wq^1^+|Idz++nTMcaoDtmG;a5C{lLUOki+q5KcUH$c@T~{ZHkC$`qchAjDGC!7u zcK~hjN~MSf1bLuDC3Ce)(erxY$H+65E6IyWL5f;1Xr5hPC8B1c(y-E14cg{lJV#Q7 zi8@7lYiowF5ql`P37kVYaa-wj6!bKBN#ry=eoTZ)r7x{oFwjZM=8P1MJ5`6+%` zK0fuU`nl@fN&w0dDf)3tXy6v4=K$<9?yPL0`-k-cWg!WPPi}oWThkjBO&KAtRS7=3 zhW-wCVpTe8i_gw5*4;4*ZtT+CZJUF5Wd~*({1FB?WhlWGmeWq*@^;5^XuYP`+>#Lcp!1sPBM`*DbE`uK^Dh6R3v3Z`Ssf*MN5g>*{PY@CsAnhf$pyx&E_w^el7B| zM_)HtRbEc$D>6Cr<)T_<<^AR4^Q}D(CTC6S%B9Pz(qB&YzM1^``rrThWjAen+rqZy z3`CU8v{+nD)@{3)UR*SbwJ%)rs(;$}5{Rp+E?f(L>-&qs)eqav`JyT|uFd9Ip0)c6 zMu?gm#8j7)ZCy?eBF>9!QCCg1YCD!`wOVBhfA|?W>c66Q;7qE;w(w=!YrLp^4mMO} zv(7fnD6ROr(%^{eQOM)Z4)Ivz6ypCa0R}0@X5PzT|AC6nN ztb7tf#WO)qu6_lsz8G|0bfSQrlK$yvd6PfmN1{DM@j9E=t{%b+064K?)5P1Rt%}68 z?n&Nmw|lkQG_Mxr!HAQnQGNlWBh%!pSWK_(%c^$s9N@X5V)7cW`K_vov)y##>ji`x zfKBM+q9e@VJb?D3dSA7(ZGT-?+j9BVJ6MD5j}A^nUfyV{s(rR`dBD`i8ix3_LSYpczuPOyW*W$ktltv7YH{N2}Wws84u;})TFBe!^Ha@VX)SCx9=HovW1*1K$2=y#`IH&DA~>ulA2@+}mWey>~p39!4) zO8?ep_v`kmgrMXnY=2*Bx4HFS+b>zW?hkR)|GDw0Yg*G}u8eB$9zJbz-wY-+-lo@X zUv1m||4`&+Lzx8Ml&%1X4y%XKcT+9>1W4ODd&2)%Jzvbnj-iJtm8Wj~QBeg*05JLf zY_oQsgTKpmmVNW4T)qPbWKc}|ApZjkerA9#JFk9#OW=0D@qgAk2mo9kfB(2YQ`U#u z=Go>ZgRHKv$|dC9|4mu8TKO6jWUlo$kknaS?Rq~Cwd9}bz1U3$m~^QE1?G2GH^;|Q zC>0f4&8>Tagx|lb{&V*PH^pyX-+%d(-xW9e_bg?1w-55}o7ulF?!JC_bGN&> zy&-=jcjAvXpMSH}ZY=o6GZDI!fX>% z5_Y(?zmI|tN+{F^B47c*DT#NX zDy20?rGF_+pjcpjQ=wpZ4N-K#yG0)LIgG>mB^P^-h36Ffd)LN!Ib6I!7a znvEs^RH1r|kx7cy9TV$h?EJs4AtvQKMswR(Z3G(ahzWyZCrSjtvjiw*1P6DvXSTQgRgz93BtL2zk_rXgDS;#67uq_L$+qDV{tqYTxP zaFeeYFbMYe&d4B-h@#S$G8O%WbS3}I$`i#yG2 z8ibUjnBO1`I3OOu0W{E1DPrV2H0^3ULE=MGlTwaFfm$u44v^fYHlf1Ch_9)&7D$K% zj6z65%x?@@4V7ZjnAEX=g8dx5OdH4_tgC@Q8xdnSE%+I_8mv!bM#g_6WJs2<6dF5= z-6bp)(A7|+Uc==>AWIM*@b1P65+vuiU|OUpMuE19S&c-2E3PE5h}B%fwLnq?Z#;mq zX9A280^SMMAMh{%9)anJBxM2i*2bYI<9#Aed89*y+?pKFF2Vq#*f1a|<^w=S%#=cG z6HyRpMv$5)Vjc-7%g=vNCDMRWZV>NCcrzq1_VyuJoTxBFZghBKql961qg0{M46D5o z`V$dixh+E(C=nuAFO9G=#on_dyte{I<@1xugr_W2h&08dea426p6yncwXa-`2a zOt<|hWwxD<#PfgifzT8#&n=9JM?!x!7SnEco%mHRHotN8{ZZ~=M~$w+`25|ItZu6B z>o+jI-wfJzFtHtGexo`H-QPgVN|^N)<9f55&ql5iK1-aC;9J0l$GT&>IJ##yZQ*NH zKRdm4$6>rL&p*$)v70|kgD)rF*5~p2jvJQ~do*ihcr@_CD*8g#f?y#`?@X&1-9%0wv;kd@fHGW)!Pd&pvaa@z*nmn$l zaScAk?WSv(+17bhKEPmleC*a&Ri0NnzkE9`UQV7Z4NmAsvRCCI-!44>1m=p(RXJ2lt)1!z4z6(e6;B z_gu%%SNz|9Iyy{=WFXQV2uCC0xsDrRqT^rA|Ih#Nx53WtXmFrkj3iCCMI_NV7z>{3 z=u0MJooCM`f_-9tAn{#)C>W0>qCLs)g3CbRecK zCl;#iZWkg~)q4@ju3%+m+mTmGRCI$Qw12rT5EIqq#1fu?L^9g%4I~5VeFp~zzZ;At zz6(Zjj3kfxILlf5ba+vRsXy3xp*IqZ2fD(4c(*}LFb#566Csd}^Wa@w(e4}M-xv-H zov}bXaH0EL2k+v&4$fzzr^GJC=^2-kH^QQ6u=83Vj59jUl7s&zs2m7_0g7>UG|sMw z`=gO$0w?r^BFS?d-C|E*Ae`*Lfj}Y{0@S4amCW91c3~vDbB`SEb43y*4vT?A(v=8* zVFwB>pbH2GBE8BI#K^ZlUnVO!JJ?B=fPZs~r*BYtS<2l{dRwiJOs{v3^N?;*ipzFzXO#4ivnNELKIveTPWXOlSZGW zwzs8wPyX7SFW6V04B6JN;E3kO4t7@h{$zof`*)-_|41Lq{rAwYG&Lfv&7}^sfZyzJ z**S~7fL@OUe z@FJzbXyaWTfp- z;8*$Kr_|nqeCn2dyP4X5yo1xEkqvfQc5z5vzloE{{rB(4%P*y8cW|1tu-_V3Qj*bs zg>^R!YRD5q={MW|J#@1GR;Agt+3K)*t->+t7z|RXrySwXCHZq9ckW`E*)gh7{u%Jt zF-Hy9XRI?6Awt8{9ZDpxoa?~D$S1e@=?C@GCGv@0B3@4tcVF;-`ne9l>ENt(YlkU# z<6MUwZ)xVO^gUvoVo#44O#0ZlFqv^{Si$%ks-e@0(@b4Z3prd&-aAIzh!^9g5X^6z zgTqaE4yQx{#0UdDq$kEqR0YJD!Jn>u9}mR(LP38VzY}2)=Yrh)iT_s-W}AQlQH2jb9< z{)SJT_&>KSd{X@=+P9!H9(;SXt)E}wcLdPvLXhf(tBKZ`dRX|;Hh2V zP|P0+hskD<&!%{%*xw}rb>bJg35^9hfulbp;5DG1*e?EmU*6#|o1MJ-TaVT3`BpI7 zec!sAg8f^&*=HBbf`jvLp8rR#hAU_wA!3Drus0S`K&sW%n4>P{+GeOkJp+_x&(Z|} zj}DiJkK`rDS`*2*m<;xjk3D$5OTasVb9q0|%{Y92vNZ{+4h1@|_r&|jmGD`bK$@U5 z&?J2*+l1qP=oODpE{{Ps{9cUqo5%<3DAquCG;j?Y%2uPCf~6fUfPTbRy51Z$5){t= zj?XH~A)gO95s|s{NGa3BWwi))zXi)sb-zN>MF;aFe41c!X?|_9`#n7GEHLXUP_vJ9 zKD2ddJsvfJAWZFtr?9ObVK)lh%c>)(Vf3T#2yLlqoG|cAz zIkc!{`&nqBF>t5#3{CZo6#$fpjQlyY)FQy#9g??anQ?5iH_jJOs!3lgsY+`bg3sgN zd9(Et6&%42&DZ7k=0CCm1?PyBD)5CSX+ujV0tKc^Kmk<_!t|3kXu1^b4zeMNfiQojEc!F6r_hU1|J9_URjgk}lzY zKVC5`CPlnS6On25;ek9gA$|ELBNvdAN+{{u8`A7uP?&t@AB0RP_&{5X<_IONjl!Fs z+PTXv$u5khHb11_?EuR7mBb8Io~7@u$%_;6!*TV_>6Mp=8E7Oj-siE}Ed_DgwxG^W ztZGC#Fm3^j01u=|1a?S5Q;}$3ILCf}0ykDMj9qdR!`N|Yd`+GjlUC+|Nvco)!U2;q z2Tvg>AUm={&vR^_J{RZ z=!xv|)aGPr_XXhu^qJCAO1>9 zKIh4|8H@OjJi7?!sDc>en%RE-7k~KSj`yP2H>IsCT zsU`q=W_L^PJdmF)5=OH*B0pUN7m%OqBa;F8gJPLZ;z2M@abf0QglH5i^QpaQBs?Hv zC3Wyp9$wYP9XPYkpebp44ys{hWk0>QVo>goOd>rw z(Omq20ff;h`^<=!{SBUfkY0GgJV>Bl>d6^qKcx?LGOPFCL1JGsFPG(8BZSNu?XS;B zTMyagI486EA^ZFzV>Rj}=|6sxW{6`7k4S%*V5WgKKEOMjyv4(}z0Jn)HkWqPLdCTc z)q(1y(uc<+#qrHMI65l`mNtsza)t!ZO^ML2hIcw=)^3q>Q!3_vbIx2P0X0Y76h_QG z0k8;3e|Qc1A0GfdLhu_S_yEXiqi2pYyDT=hzpeEfCVEtso*jUCE)K9cB27PJ5db67 zvyYTunox2cylM&~i`DS4q_t5-)0w?Lb%yK!8eefb%u;FvuTV~zM9ut+p916opEpvb87P3z!OrdU&KPumNTa=qKH%Am_L?9kwIqUYwGXrD zj{w?ph)tveq_!8-BdM)B*~g!Vz#RFAb6Tmyls7U|D=9Ixo9$-7>mcQCYI#|L|Fvg>{wlA1wE5a%7a-TWNZFvMEU7PL0%&f#EHLgX}1>? zz)qHb@gTq0ln!o5>o;LO$RFO|2l>e=ai)Tw(g+xsCOZ>`49>)9(#kCsW=(C6R-kkn z7im23lcV7KVmx{+)C~{YnV*oTolw3ul$Y(q@d@4)B+DVEdJ;Ipj8jNlB+aOJBE5P> z-0I^mh?jkipC$gm z6F;&^JgOA*kR(v`XqGrl3*3GH^pIw!DIy3ksfff#IdOk=r! z4KHwBm)WPcC&AH@y|#j{8qh_FWX7eS-X!vlTa1RF%x4Z3)5~{QIyDJkFFl5?#l$d@ zw6*VtVF6#-&N1aBqbDk`RTMYp$(2@~xT0QqLs%_LP6jK<_xT1Lu!Z50$#wC92{i#o zccmKID|!4&`u%DJn`l^EI)CPSQoZ1R35Q_ywU&8T8f%Ce_U1MUr*aI2`V6u|JE)fh zZm?=T6k@axZu}UC1bW4IYas`<{aK|AO6_LvN;7Nn#5`XLc&aA)GzE8$g$iZ8FiN>#@oQe3>wZCyQh+M?uSO*(c(gN8KODbVPIUW`?=MevFkSh>zs3d_GOn} zzLj4MYm@92!DjJ$`P$XQwB57K=blYQqb(@d;YLF?t@H&|>0fe!ou+kvXTB^mXKjj( zSOCvPn;B{HA8KhSh9V$W4Scs1*e!{e|C|hk6WU+4Ddamv1sVa}LM%*;n#(k+A2@+R zX8Gg87D0kbVj?;a4~iEthNm^tCNN4xem1LWVyws*nwWN~Xlq=D6AO{&sC&fGiN|Qp zXFlIXzuy)iZFeAu8O1Gs$l)RTQ`>iQoe)|2%j&K)wylc!0@5z+H{-k(ugxjcZSGlJ z>TGJB0<5V3$GGfsAXI=!$0pDh57Urg8$Ll}FHW37ccq)?ZCrXHI9J%kS6@OzkC z-GLYyC=je!xy5p4q}h86zohktKq{u2vo<2yx68QvgeEfofKCz$>S%xICSWgR$cUhd z_I+pu;4`|={?1JRd!#xLzc(D+^4vyF#K01#CV6Yozpp#gn&eMVfJHnQ{7JK0S`n4n zev{h1564UzZA4Ii*Me@8M@w5uYw>0?Z+BtlW4lIC=`wL-Rz|x2 ziB=cR9X{6BZ8CfydigJuODILCt%e%3luUZQtz<)x`azO^3ZXC$NM{ywBdxw3t+5O> zNOKG;Ax$Ark4EQ`;F8qlG?Ln+wQXs26IZEs&=&OOygtssTk7VPtN-{&#idqS;j#Gz zuUT+4MNwHy~M^n|$1;c8(J) zXv{=U!Myf=+*dRN!8rEAq3u@QhIY(2jSOwakYn@>Va=S5AKK3Gc3gv@?VOpjDHj}0 zZRd-owc~rYm(SG;&0L%d3@QkWgwVERmdAn{!^ zCPuFJhkK&&{y-96D z3oZX$~oy|Df^N=@8EClzwn;vCWkokr^ouH{V&$Vi!gkQ+IKAh#x?+-$bG zd8buxQku3NtmU-o1}o009CJ=lWk1e;pg);fwaT}HxbWC`4GT&xU`S#pRsq)|`$Z=- z6pRtFU>q9nw(2z`OXJ|lOS+TUb%mnu*{BnXq6H)Cn@=P` zXZzrrP7TOociPP!pIPrXy*BdD8R{pacbqJR5agjnDrz1aLnT!An2M%0Ao&Y_m0|J| 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