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..78eb3a2 100644 --- a/src/Notes-master/src/net/micode/notes/data/Contact.java +++ b/src/Notes-master/src/net/micode/notes/data/Contact.java @@ -26,8 +26,8 @@ import android.util.Log; import java.util.HashMap; public class Contact { - private static HashMap sContactCache; - private static final String TAG = "Contact"; + private static HashMap sContactCache;//定义sContactCache是一个缓存电话号码和相应联系人名字的哈希表 + private static final String TAG = "Contact";//定义用于日志输出的标识(TAG) private static final String CALLER_ID_SELECTION = "PHONE_NUMBERS_EQUAL(" + Phone.NUMBER + ",?) AND " + Data.MIMETYPE + "='" + Phone.CONTENT_ITEM_TYPE + "'" @@ -35,25 +35,37 @@ public class Contact { + "(SELECT raw_contact_id " + " FROM phone_lookup" + " WHERE min_match = '+')"; - - public static String getContact(Context context, String phoneNumber) { + //用于构建数据库查询条件的字符串常量 + public static String getContact(Context context, String phoneNumber) +//参数:Context对象:用于访问系统服务和应用资源 phoneNumber:需要查询的联系人电话号码 + { if(sContactCache == null) { sContactCache = new HashMap(); + + // 没映射表就建表,有就查缓存中有没有这个联系人 } if(sContactCache.containsKey(phoneNumber)) { return sContactCache.get(phoneNumber); } - + //返回从表里查询到的对应名字 String selection = CALLER_ID_SELECTION.replace("+", PhoneNumberUtils.toCallerIDMinMatch(phoneNumber)); + //构造一个SQL查询条件:CALLER_ID_SELECTION中的"+"被替换为电话号码的最小匹配值 + //然后执行查询语句 Cursor cursor = context.getContentResolver().query( Data.CONTENT_URI, new String [] { Phone.DISPLAY_NAME }, selection, new String[] { phoneNumber }, null); - + //判断查询结果: + //查询结果不为空,且能够移动到第一条记录: + // 那么就尝试从Cursor中获取联系人姓名,并将其存入缓存sContactCache。然后返回联系人姓名。 + // 异常情况:如果在获取字符串时发生数组越界异常,则记录一个错误日志并返回null。 + // 最后都要确保关闭Cursor对象,以避免内存泄漏。 + //如果查询结果为空或者没有记录可以移动到(即没有找到匹配的联系人): + // 则记录一条调试日志并返回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..9d829ab 100644 --- a/src/Notes-master/src/net/micode/notes/data/Notes.java +++ b/src/Notes-master/src/net/micode/notes/data/Notes.java @@ -18,8 +18,11 @@ package net.micode.notes.data; import android.net.Uri; public class Notes { + //用于表示笔记应用中的各种类型、标识符以及Intent的额外数据 public static final String AUTHORITY = "micode_notes"; public static final String TAG = "Notes"; + //对NoteColumns.TYPE的值进行设置时使用: + //即不同种类:笔记、文件夹和系统文件夹 public static final int TYPE_NOTE = 0; public static final int TYPE_FOLDER = 1; public static final int TYPE_SYSTEM = 2; @@ -30,11 +33,18 @@ public class Notes { * {@link Notes#ID_TEMPARAY_FOLDER } is for notes belonging no folder * {@link Notes#ID_CALL_RECORD_FOLDER} is to store call records */ + //以下id是系统文件夹的标识符(即系统文件夹的分类) + //ID_ROOT_FOLDER:默认文件夹 + //ID_TEMPARAY_FOLDER:不属于文件夹的笔记 + //ID_CALL_RECORD_FOLDER:用于存储通话记录,以便返回 + //ID_TRASH_FOLER:垃圾回收站 public static final int ID_ROOT_FOLDER = 0; public static final int ID_TEMPARAY_FOLDER = -1; public static final int ID_CALL_RECORD_FOLDER = -2; public static final int ID_TRASH_FOLER = -3; - + // 额外的数据键,个人理解为就是定义一些布局的ID + // 这部分就是用于设置UI界面的一些布局或小组件的id,给它定义成常量了。 + // (这样的封装性可能比较好?因为如果有部分要修改,则直接来这边修改即可,不用在activity部分一个一个修改。) public static final String INTENT_EXTRA_ALERT_DATE = "net.micode.notes.alert_date"; public static final String INTENT_EXTRA_BACKGROUND_ID = "net.micode.notes.background_color_id"; public static final String INTENT_EXTRA_WIDGET_ID = "net.micode.notes.widget_id"; @@ -45,12 +55,14 @@ public class Notes { public static final int TYPE_WIDGET_INVALIDE = -1; public static final int TYPE_WIDGET_2X = 0; public static final int TYPE_WIDGET_4X = 1; - + // 数据常量:里面定义了两种类型:文本便签和通话记录 public static class DataConstants { public static final String NOTE = TextNote.CONTENT_ITEM_TYPE; public static final String CALL_NOTE = CallNote.CONTENT_ITEM_TYPE; } - + //定义一堆访问笔记和文件的uri + //GPT:Android开发中常见的用于定义内容提供者(Content Provider)URI + //内容提供者是一种Android组件,它允许应用程序共享和存储数据。这里定义了一个URI来查询数据 /** * Uri to query all notes and folders */ @@ -97,6 +109,7 @@ public class Notes { * Folder's name or text content of note *

Type: TEXT

*/ + // 摘要 public static final String SNIPPET = "snippet"; /** @@ -140,6 +153,8 @@ public class Notes { * The last sync id *

Type: INTEGER (long)

*/ + + //在数据同步过程中,这个ID可能用来跟踪和识别每次同步操作的唯一性,确保数据的一致性。 public static final String SYNC_ID = "sync_id"; /** @@ -168,6 +183,9 @@ public class Notes { } public interface DataColumns { + + // DataColumns的接口,这个接口包含了一系列静态常量,这些常量代表了数据库表中用于存储数据的列名。 + // 每个常量都有相应的注释,说明该列的作用和数据类型。 /** * The unique ID for a row *

Type: INTEGER (long)

@@ -178,32 +196,42 @@ public class Notes { * The MIME type of the item represented by this row. *

Type: Text

*/ + //MIME类型是一种标准,用于标识文档、文件或字节流的性质和格式。在数据库中,这个字段可以用来识别不同类型的数据,例如文本、图片、音频或视频等。 public static final String MIME_TYPE = "mime_type"; /** * The reference id to note that this data belongs to *

Type: INTEGER (long)

*/ + + //归属的Note的ID public static final String NOTE_ID = "note_id"; /** * Created data for note or folder *

Type: INTEGER (long)

*/ + //创建日期 public static final String CREATED_DATE = "created_date"; /** * Latest modified date *

Type: INTEGER (long)

*/ + //最近修改日期 public static final String MODIFIED_DATE = "modified_date"; /** * Data's content *

Type: TEXT

*/ + + //数据内容 public static final String CONTENT = "content"; + // 以下5个是通用数据列,它们的具体意义取决于MIME类型(由MIME_TYPE字段指定)。 + // 不同的MIME类型可能需要存储不同类型的数据,这五个字段提供了灵活性,允许根据MIME类型来存储相应的数据。 + // 读后面的代码感觉这部分是在表示内容的不同状态 /** * Generic data column, the meaning is {@link #MIMETYPE} specific, used for @@ -241,39 +269,41 @@ public class Notes { public static final String DATA5 = "data5"; } + //以下是文本便签的定义 public static final class TextNote implements DataColumns { /** * Mode to indicate the text in check list mode or not *

Type: Integer 1:check list mode 0: normal mode

*/ - public static final String MODE = DATA1; + public static final String MODE = DATA1;//模式?这个被存在DATA1列中 - public static final int MODE_CHECK_LIST = 1; + public static final int MODE_CHECK_LIST = 1;//所处检查列表模式 - public static final String CONTENT_TYPE = "vnd.android.cursor.dir/text_note"; + public static final String CONTENT_TYPE = "vnd.android.cursor.dir/text_note";// 定义了MIME类型,用于标识文本标签的目录 - public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/text_note"; + public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/text_note";// 定义了MIME类型,用于标识文本标签的单个项 - public static final Uri CONTENT_URI = Uri.parse("content://" + AUTHORITY + "/text_note"); + public static final Uri CONTENT_URI = Uri.parse("content://" + AUTHORITY + "/text_note");//文本标签内容提供者(Content Provider)的URI,用于访问文本标签数据 } + // 通话记录的定义 public static final class CallNote implements DataColumns { /** * Call date for this record *

Type: INTEGER (long)

*/ - public static final String CALL_DATE = DATA1; + public static final String CALL_DATE = DATA1;//一个字符串常量,表示通话记录的日期 /** * Phone number for this record *

Type: TEXT

*/ - public static final String PHONE_NUMBER = DATA3; + public static final String PHONE_NUMBER = DATA3;//意味着在数据库表中,这个电话号码信息将被存储在DATA3列中 - public static final String CONTENT_TYPE = "vnd.android.cursor.dir/call_note"; + public static final String CONTENT_TYPE = "vnd.android.cursor.dir/call_note";// 同样定义了MIME类型,是用于标识通话记录的目录。 - public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/call_note"; + public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/call_note";// 同样定义了MIME类型,是用于标识通话记录的单个项 - public static final Uri CONTENT_URI = Uri.parse("content://" + AUTHORITY + "/call_note"); + public static final Uri CONTENT_URI = Uri.parse("content://" + AUTHORITY + "/call_note");//定义了通话记录内容提供者的URI,用于访问通话记录数据 } } 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..4a1ba5a 100644 --- a/src/Notes-master/src/net/micode/notes/data/NotesDatabaseHelper.java +++ b/src/Notes-master/src/net/micode/notes/data/NotesDatabaseHelper.java @@ -1,19 +1,3 @@ -/* - * Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net) - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - package net.micode.notes.data; import android.content.ContentValues; @@ -28,188 +12,253 @@ import net.micode.notes.data.Notes.NoteColumns; public class NotesDatabaseHelper extends SQLiteOpenHelper { + // 数据库帮助类,用于管理名为 note.db 的 SQLite 数据库。 +// 它继承自 SQLiteOpenHelper 类,这是 Android提供的一个方便的工具类,用于管理数据库的创建和版本更新. + // 数据库的基本信息;数据库名称和版本信息(在创建实例对象时会用到) private static final String DB_NAME = "note.db"; private static final int DB_VERSION = 4; + //内部接口:个人理解为两个表名,一个note,一个data public interface TABLE { public static final String NOTE = "note"; public static final String DATA = "data"; } + //一个标签,方便日志输出时识别出信息来自哪里 private static final String TAG = "NotesDatabaseHelper"; + //静态所有变量,提供一个全局访问点来获取数据库辅助类的唯一实例,使得在应用的任何地方都可以方便地使用它 private static NotesDatabaseHelper mInstance; + /* 以下都是一些SQL语句,辅助我们来对数据库进行操作 */ + //创建note表的语句,这里的NoteColumns就是我们刚刚在Notes中定义的一个接口,里面定义了一系列静态的数据库表中的列名 private static final String CREATE_NOTE_TABLE_SQL = - "CREATE TABLE " + TABLE.NOTE + "(" + - NoteColumns.ID + " INTEGER PRIMARY KEY," + - NoteColumns.PARENT_ID + " INTEGER NOT NULL DEFAULT 0," + - NoteColumns.ALERTED_DATE + " INTEGER NOT NULL DEFAULT 0," + - NoteColumns.BG_COLOR_ID + " INTEGER NOT NULL DEFAULT 0," + - NoteColumns.CREATED_DATE + " INTEGER NOT NULL DEFAULT (strftime('%s','now') * 1000)," + - NoteColumns.HAS_ATTACHMENT + " INTEGER NOT NULL DEFAULT 0," + - NoteColumns.MODIFIED_DATE + " INTEGER NOT NULL DEFAULT (strftime('%s','now') * 1000)," + - NoteColumns.NOTES_COUNT + " INTEGER NOT NULL DEFAULT 0," + - NoteColumns.SNIPPET + " TEXT NOT NULL DEFAULT ''," + - NoteColumns.TYPE + " INTEGER NOT NULL DEFAULT 0," + - NoteColumns.WIDGET_ID + " INTEGER NOT NULL DEFAULT 0," + - NoteColumns.WIDGET_TYPE + " INTEGER NOT NULL DEFAULT -1," + - NoteColumns.SYNC_ID + " INTEGER NOT NULL DEFAULT 0," + - NoteColumns.LOCAL_MODIFIED + " INTEGER NOT NULL DEFAULT 0," + - NoteColumns.ORIGIN_PARENT_ID + " INTEGER NOT NULL DEFAULT 0," + - NoteColumns.GTASK_ID + " TEXT NOT NULL DEFAULT ''," + - NoteColumns.VERSION + " INTEGER NOT NULL DEFAULT 0" + - ")"; - + "CREATE TABLE " + TABLE.NOTE + "(" + + NoteColumns.ID + " INTEGER PRIMARY KEY," + + NoteColumns.PARENT_ID + " INTEGER NOT NULL DEFAULT 0," + + NoteColumns.ALERTED_DATE + " INTEGER NOT NULL DEFAULT 0," + + NoteColumns.BG_COLOR_ID + " INTEGER NOT NULL DEFAULT 0," + + NoteColumns.CREATED_DATE + " INTEGER NOT NULL DEFAULT (strftime('%s','now') * 1000)," + + NoteColumns.HAS_ATTACHMENT + " INTEGER NOT NULL DEFAULT 0," + + NoteColumns.MODIFIED_DATE + " INTEGER NOT NULL DEFAULT (strftime('%s','now') * 1000)," + + NoteColumns.NOTES_COUNT + " INTEGER NOT NULL DEFAULT 0," + + NoteColumns.SNIPPET + " TEXT NOT NULL DEFAULT ''," + + NoteColumns.TYPE + " INTEGER NOT NULL DEFAULT 0," + + NoteColumns.WIDGET_ID + " INTEGER NOT NULL DEFAULT 0," + + NoteColumns.WIDGET_TYPE + " INTEGER NOT NULL DEFAULT -1," + + NoteColumns.SYNC_ID + " INTEGER NOT NULL DEFAULT 0," + + NoteColumns.LOCAL_MODIFIED + " INTEGER NOT NULL DEFAULT 0," + + NoteColumns.ORIGIN_PARENT_ID + " INTEGER NOT NULL DEFAULT 0," + + NoteColumns.GTASK_ID + " TEXT NOT NULL DEFAULT ''," + + NoteColumns.VERSION + " INTEGER NOT NULL DEFAULT 0" + + ")"; + + //同上,创建data表的语句,这里的DataColumns就是我们刚刚在Notes中定义的一个接口,里面定义了一系列静态的数据库表中的列名 private static final String CREATE_DATA_TABLE_SQL = - "CREATE TABLE " + TABLE.DATA + "(" + - DataColumns.ID + " INTEGER PRIMARY KEY," + - DataColumns.MIME_TYPE + " TEXT NOT NULL," + - DataColumns.NOTE_ID + " INTEGER NOT NULL DEFAULT 0," + - NoteColumns.CREATED_DATE + " INTEGER NOT NULL DEFAULT (strftime('%s','now') * 1000)," + - NoteColumns.MODIFIED_DATE + " INTEGER NOT NULL DEFAULT (strftime('%s','now') * 1000)," + - DataColumns.CONTENT + " TEXT NOT NULL DEFAULT ''," + - DataColumns.DATA1 + " INTEGER," + - DataColumns.DATA2 + " INTEGER," + - DataColumns.DATA3 + " TEXT NOT NULL DEFAULT ''," + - DataColumns.DATA4 + " TEXT NOT NULL DEFAULT ''," + - DataColumns.DATA5 + " TEXT NOT NULL DEFAULT ''" + - ")"; - + "CREATE TABLE " + TABLE.DATA + "(" + + DataColumns.ID + " INTEGER PRIMARY KEY," + + DataColumns.MIME_TYPE + " TEXT NOT NULL," + + DataColumns.NOTE_ID + " INTEGER NOT NULL DEFAULT 0," + + NoteColumns.CREATED_DATE + " INTEGER NOT NULL DEFAULT (strftime('%s','now') * 1000)," + + NoteColumns.MODIFIED_DATE + " INTEGER NOT NULL DEFAULT (strftime('%s','now') * 1000)," + + DataColumns.CONTENT + " TEXT NOT NULL DEFAULT ''," + + DataColumns.DATA1 + " INTEGER," + + DataColumns.DATA2 + " INTEGER," + + DataColumns.DATA3 + " TEXT NOT NULL DEFAULT ''," + + DataColumns.DATA4 + " TEXT NOT NULL DEFAULT ''," + + DataColumns.DATA5 + " TEXT NOT NULL DEFAULT ''" + + ")"; + + // 功能简介: + // 创建一个以note的ID为索引 + // 解读: + // 用于在TABLE.DATA表上创建一个名为note_id_index的索引。 + // 这个索引是基于DataColumns.NOTE_ID列的。IF NOT EXISTS确保了如果索引已经存在,那么就不会尝试重新创建它,避免了可能的错误。 + // 索引通常用于提高查询性能,特别是在对某个字段进行频繁查询时。 private static final String CREATE_DATA_NOTE_ID_INDEX_SQL = - "CREATE INDEX IF NOT EXISTS note_id_index ON " + - TABLE.DATA + "(" + DataColumns.NOTE_ID + ");"; - + "CREATE INDEX IF NOT EXISTS note_id_index ON " + + TABLE.DATA + "(" + DataColumns.NOTE_ID + ");"; + + /* 以下是一些对便签增删改定义的触发器 */ + /* 总结 + * 这些触发器都是用来维护NOTE表和与之相关联的DATA表之间数据一致性的。 + * 当在NOTE表中发生删除或更新操作时,这些触发器会自动执行相应的数据清理或更新操作,确保数据库中的数据保持正确和一致。 + * 特别是在处理文件夹和回收站等逻辑时,这些触发器起到了非常重要的作用,可以自动管理数据的移动和删除。*/ /** * Increase folder's note count when move note to the folder */ + // 功能简介: + // 添加触发器:增加文件夹的便签个数记录(因为我们会移动便签进入文件夹,这时候文件夹的计数要进行更新) + // 解读: + // 定义了一个SQL触发器increase_folder_count_on_update。 + // 触发器是一种特殊的存储过程,它会在指定表上的指定事件(如INSERT、UPDATE、DELETE)发生时自动执行。 + // 这个触发器会在TABLE.NOTE表的NoteColumns.PARENT_ID字段更新后执行。 + // 触发器的逻辑是:当某个笔记的PARENT_ID(即父文件夹ID)被更新时,它会找到对应的文件夹(通过新的PARENT_ID),并将该文件夹的NOTES_COUNT(即笔记数)增加1。 private static final String NOTE_INCREASE_FOLDER_COUNT_ON_UPDATE_TRIGGER = - "CREATE TRIGGER increase_folder_count_on_update "+ - " AFTER UPDATE OF " + NoteColumns.PARENT_ID + " ON " + TABLE.NOTE + - " BEGIN " + - " UPDATE " + TABLE.NOTE + - " SET " + NoteColumns.NOTES_COUNT + "=" + NoteColumns.NOTES_COUNT + " + 1" + - " WHERE " + NoteColumns.ID + "=new." + NoteColumns.PARENT_ID + ";" + - " END"; + "CREATE TRIGGER increase_folder_count_on_update "+ + " AFTER UPDATE OF " + NoteColumns.PARENT_ID + " ON " + TABLE.NOTE + + " BEGIN " + + " UPDATE " + TABLE.NOTE + + " SET " + NoteColumns.NOTES_COUNT + "=" + NoteColumns.NOTES_COUNT + " + 1" + + " WHERE " + NoteColumns.ID + "=new." + NoteColumns.PARENT_ID + ";" + + " END"; /** * Decrease folder's note count when move note from folder */ + // 功能简介:(触发器和上面的 “增加文件夹的便签个数记录” 同理,就不细节解读了) + // 添加触发器:减少文件夹的便签个数记录(因为我们会移动便签移出文件夹,这时候文件夹的计数要进行更新) private static final String NOTE_DECREASE_FOLDER_COUNT_ON_UPDATE_TRIGGER = - "CREATE TRIGGER decrease_folder_count_on_update " + - " AFTER UPDATE OF " + NoteColumns.PARENT_ID + " ON " + TABLE.NOTE + - " BEGIN " + - " UPDATE " + TABLE.NOTE + - " SET " + NoteColumns.NOTES_COUNT + "=" + NoteColumns.NOTES_COUNT + "-1" + - " WHERE " + NoteColumns.ID + "=old." + NoteColumns.PARENT_ID + - " AND " + NoteColumns.NOTES_COUNT + ">0" + ";" + - " END"; + "CREATE TRIGGER decrease_folder_count_on_update " + + " AFTER UPDATE OF " + NoteColumns.PARENT_ID + " ON " + TABLE.NOTE + + " BEGIN " + + " UPDATE " + TABLE.NOTE + + " SET " + NoteColumns.NOTES_COUNT + "=" + NoteColumns.NOTES_COUNT + "-1" + + " WHERE " + NoteColumns.ID + "=old." + NoteColumns.PARENT_ID + + " AND " + NoteColumns.NOTES_COUNT + ">0" + ";" + + " END"; /** * Increase folder's note count when insert new note to the folder */ + // 功能简介:(触发器原理和上面的 “增加文件夹的便签个数记录” 同理,就不细节解读了) + // 添加触发器:当我们在文件夹插入便签时,增加文件夹的便签个数记录 private static final String NOTE_INCREASE_FOLDER_COUNT_ON_INSERT_TRIGGER = - "CREATE TRIGGER increase_folder_count_on_insert " + - " AFTER INSERT ON " + TABLE.NOTE + - " BEGIN " + - " UPDATE " + TABLE.NOTE + - " SET " + NoteColumns.NOTES_COUNT + "=" + NoteColumns.NOTES_COUNT + " + 1" + - " WHERE " + NoteColumns.ID + "=new." + NoteColumns.PARENT_ID + ";" + - " END"; + "CREATE TRIGGER increase_folder_count_on_insert " + + " AFTER INSERT ON " + TABLE.NOTE + + " BEGIN " + + " UPDATE " + TABLE.NOTE + + " SET " + NoteColumns.NOTES_COUNT + "=" + NoteColumns.NOTES_COUNT + " + 1" + + " WHERE " + NoteColumns.ID + "=new." + NoteColumns.PARENT_ID + ";" + + " END"; /** * Decrease folder's note count when delete note from the folder */ + // 功能简介:(触发器原理和上面的 “增加文件夹的便签个数记录” 同理,就不细节解读了) + // 添加触发器:当我们在文件夹删除便签时,减少文件夹的便签个数记录 private static final String NOTE_DECREASE_FOLDER_COUNT_ON_DELETE_TRIGGER = - "CREATE TRIGGER decrease_folder_count_on_delete " + - " AFTER DELETE ON " + TABLE.NOTE + - " BEGIN " + - " UPDATE " + TABLE.NOTE + - " SET " + NoteColumns.NOTES_COUNT + "=" + NoteColumns.NOTES_COUNT + "-1" + - " WHERE " + NoteColumns.ID + "=old." + NoteColumns.PARENT_ID + - " AND " + NoteColumns.NOTES_COUNT + ">0;" + - " END"; + "CREATE TRIGGER decrease_folder_count_on_delete " + + " AFTER DELETE ON " + TABLE.NOTE + + " BEGIN " + + " UPDATE " + TABLE.NOTE + + " SET " + NoteColumns.NOTES_COUNT + "=" + NoteColumns.NOTES_COUNT + "-1" + + " WHERE " + NoteColumns.ID + "=old." + NoteColumns.PARENT_ID + + " AND " + NoteColumns.NOTES_COUNT + ">0;" + + " END"; /** * Update note's content when insert data with type {@link DataConstants#NOTE} */ + // 功能简介: + // 添加触发器:当向DATA表中插入类型为NOTE(便签)的数据时,更新note表对应的笔记内容。 + // 解读: + // 在DATA表上进行INSERT操作后,如果新插入的数据的MIME_TYPE为NOTE,则触发此操作。 + // 它会更新NOTE表,将与新插入数据相关联的标签的SNIPPET(摘要)字段设置为新插入数据的CONTENT字段的值 private static final String DATA_UPDATE_NOTE_CONTENT_ON_INSERT_TRIGGER = - "CREATE TRIGGER update_note_content_on_insert " + - " AFTER INSERT ON " + TABLE.DATA + - " WHEN new." + DataColumns.MIME_TYPE + "='" + DataConstants.NOTE + "'" + - " BEGIN" + - " UPDATE " + TABLE.NOTE + - " SET " + NoteColumns.SNIPPET + "=new." + DataColumns.CONTENT + - " WHERE " + NoteColumns.ID + "=new." + DataColumns.NOTE_ID + ";" + - " END"; + "CREATE TRIGGER update_note_content_on_insert " + + " AFTER INSERT ON " + TABLE.DATA + + " WHEN new." + DataColumns.MIME_TYPE + "='" + DataConstants.NOTE + "'" + + " BEGIN" + + " UPDATE " + TABLE.NOTE + + " SET " + NoteColumns.SNIPPET + "=new." + DataColumns.CONTENT + + " WHERE " + NoteColumns.ID + "=new." + DataColumns.NOTE_ID + ";" + + " END"; /** * Update note's content when data with {@link DataConstants#NOTE} type has changed */ + // 功能简介: + // 添加触发器:当DATA表中,类型为NOTE(便签)的数据更改时,更新note表对应的笔记内容。 + // 解读: + // 在DATA表上进行UPDATE操作后,如果更新前的数据的MIME_TYPE为NOTE,则触发此操作。 + // 它会更新NOTE表,将与更新后的数据相关联的笔记的SNIPPET字段设置为新数据的CONTENT字段的值 private static final String DATA_UPDATE_NOTE_CONTENT_ON_UPDATE_TRIGGER = - "CREATE TRIGGER update_note_content_on_update " + - " AFTER UPDATE ON " + TABLE.DATA + - " WHEN old." + DataColumns.MIME_TYPE + "='" + DataConstants.NOTE + "'" + - " BEGIN" + - " UPDATE " + TABLE.NOTE + - " SET " + NoteColumns.SNIPPET + "=new." + DataColumns.CONTENT + - " WHERE " + NoteColumns.ID + "=new." + DataColumns.NOTE_ID + ";" + - " END"; + "CREATE TRIGGER update_note_content_on_update " + + " AFTER UPDATE ON " + TABLE.DATA + + " WHEN old." + DataColumns.MIME_TYPE + "='" + DataConstants.NOTE + "'" + + " BEGIN" + + " UPDATE " + TABLE.NOTE + + " SET " + NoteColumns.SNIPPET + "=new." + DataColumns.CONTENT + + " WHERE " + NoteColumns.ID + "=new." + DataColumns.NOTE_ID + ";" + + " END"; /** * Update note's content when data with {@link DataConstants#NOTE} type has deleted */ + // 功能简介: + // 添加触发器:当DATA表中,类型为NOTE(便签)的数据删除时,更新note表对应的笔记内容(置空)。 + // 解读: + // 在DATA表上进行DELETE操作后,如果删除的数据的MIME_TYPE为NOTE,则触发此操作。 + // 它会更新NOTE表,将与删除的数据相关联的笔记的SNIPPET字段设置为空字符串。 private static final String DATA_UPDATE_NOTE_CONTENT_ON_DELETE_TRIGGER = - "CREATE TRIGGER update_note_content_on_delete " + - " AFTER delete ON " + TABLE.DATA + - " WHEN old." + DataColumns.MIME_TYPE + "='" + DataConstants.NOTE + "'" + - " BEGIN" + - " UPDATE " + TABLE.NOTE + - " SET " + NoteColumns.SNIPPET + "=''" + - " WHERE " + NoteColumns.ID + "=old." + DataColumns.NOTE_ID + ";" + - " END"; + "CREATE TRIGGER update_note_content_on_delete " + + " AFTER delete ON " + TABLE.DATA + + " WHEN old." + DataColumns.MIME_TYPE + "='" + DataConstants.NOTE + "'" + + " BEGIN" + + " UPDATE " + TABLE.NOTE + + " SET " + NoteColumns.SNIPPET + "=''" + + " WHERE " + NoteColumns.ID + "=old." + DataColumns.NOTE_ID + ";" + + " END"; /** * Delete datas belong to note which has been deleted */ + // 功能简介: + // 添加触发器:当从NOTE表中删除笔记时,删除与该笔记相关联的数据(就是删除data表中为该note的数据) + // 解读: + // 在NOTE表上进行DELETE操作后,此触发器被激活。 + // 它会从DATA表中删除所有与已删除的笔记(由old.ID表示)相关联的数据行(通过比较DATA表中的NOTE_ID字段与已删除笔记的ID来实现) private static final String NOTE_DELETE_DATA_ON_DELETE_TRIGGER = - "CREATE TRIGGER delete_data_on_delete " + - " AFTER DELETE ON " + TABLE.NOTE + - " BEGIN" + - " DELETE FROM " + TABLE.DATA + - " WHERE " + DataColumns.NOTE_ID + "=old." + NoteColumns.ID + ";" + - " END"; + "CREATE TRIGGER delete_data_on_delete " + + " AFTER DELETE ON " + TABLE.NOTE + + " BEGIN" + + " DELETE FROM " + TABLE.DATA + + " WHERE " + DataColumns.NOTE_ID + "=old." + NoteColumns.ID + ";" + + " END"; /** * Delete notes belong to folder which has been deleted */ + // 功能简介: + // 添加触发器:当从NOTE表中删除一个文件夹时,删除该文件夹下的所有笔记。 + // 解读: + // 在NOTE表上进行DELETE操作后,如果删除的是一个文件夹(由old.ID表示) + // 触发器会删除所有以该文件夹为父级(PARENT_ID)的笔记(通过比较NOTE表中的PARENT_ID字段与已删除文件夹的ID来实现) private static final String FOLDER_DELETE_NOTES_ON_DELETE_TRIGGER = - "CREATE TRIGGER folder_delete_notes_on_delete " + - " AFTER DELETE ON " + TABLE.NOTE + - " BEGIN" + - " DELETE FROM " + TABLE.NOTE + - " WHERE " + NoteColumns.PARENT_ID + "=old." + NoteColumns.ID + ";" + - " END"; + "CREATE TRIGGER folder_delete_notes_on_delete " + + " AFTER DELETE ON " + TABLE.NOTE + + " BEGIN" + + " DELETE FROM " + TABLE.NOTE + + " WHERE " + NoteColumns.PARENT_ID + "=old." + NoteColumns.ID + ";" + + " END"; /** * Move notes belong to folder which has been moved to trash folder */ + // 功能简介: + // 添加触发器:当某个文件夹被移动到回收站时,移动该文件夹下的所有笔记到回收站 + // 解读: + // 在NOTE表上进行UPDATE操作后,如果某个文件夹的新PARENT_ID字段值等于回收站的ID(Notes.ID_TRASH_FOLER) + // 触发器会更新所有以该文件夹为父级(PARENT_ID)的笔记,将它们也移动到回收站。 private static final String FOLDER_MOVE_NOTES_ON_TRASH_TRIGGER = - "CREATE TRIGGER folder_move_notes_on_trash " + - " AFTER UPDATE ON " + TABLE.NOTE + - " WHEN new." + NoteColumns.PARENT_ID + "=" + Notes.ID_TRASH_FOLER + - " BEGIN" + - " UPDATE " + TABLE.NOTE + - " SET " + NoteColumns.PARENT_ID + "=" + Notes.ID_TRASH_FOLER + - " WHERE " + NoteColumns.PARENT_ID + "=old." + NoteColumns.ID + ";" + - " END"; - + "CREATE TRIGGER folder_move_notes_on_trash " + + " AFTER UPDATE ON " + TABLE.NOTE + + " WHEN new." + NoteColumns.PARENT_ID + "=" + Notes.ID_TRASH_FOLER + + " BEGIN" + + " UPDATE " + TABLE.NOTE + + " SET " + NoteColumns.PARENT_ID + "=" + Notes.ID_TRASH_FOLER + + " WHERE " + NoteColumns.PARENT_ID + "=old." + NoteColumns.ID + ";" + + " END"; + + // 构造器 public NotesDatabaseHelper(Context context) { super(context, DB_NAME, null, DB_VERSION); } + // 创建note(标签)表 public void createNoteTable(SQLiteDatabase db) { db.execSQL(CREATE_NOTE_TABLE_SQL); reCreateNoteTableTriggers(db); @@ -217,6 +266,9 @@ public class NotesDatabaseHelper extends SQLiteOpenHelper { Log.d(TAG, "note table has been created"); } + // 重新创建或更新与笔记表相关的触发器。 + // 首先,使用DROP TRIGGER IF EXISTS语句删除已存在的触发器。确保在重新创建触发器之前,不存在同名的触发器。 + // 然后,使用db.execSQL()方法执行预定义的SQL语句,这些语句用于创建新的触发器。 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"); @@ -235,6 +287,17 @@ public class NotesDatabaseHelper extends SQLiteOpenHelper { db.execSQL(FOLDER_MOVE_NOTES_ON_TRASH_TRIGGER); } + /* 以下部分是操作SQLite数据库部分 */ + // 功能简介: + // 创建通话记录文件夹、默认文件夹、临时文件夹和回收站,并插入相关数据 + // 具体解读: + // ContentValues是一个用于存储键值对的类,常用于SQLite数据库的插入操作 + // values.put方法可以向ContentValues对象中添加数据。 + // NoteColumns.ID是存储文件夹ID的列名,Notes.ID_CALL_RECORD_FOLDER是通话记录文件夹的ID。 + // NoteColumns.TYPE是存储文件夹类型的列名,Notes.TYPE_SYSTEM表示这是一个系统文件夹。 + // 使用db.insert方法将values中的数据插入到TABLE.NOTE(即标签表)中。 + // 每次插入新数据前,都使用values.clear()方法清除ContentValues对象中的旧数据,确保不会重复插入旧数据。 + // 然后分别创建默认文件夹、临时文件夹和回收站,并以同样的方法插入数据。 private void createSystemFolder(SQLiteDatabase db) { ContentValues values = new ContentValues(); @@ -248,6 +311,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 +320,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 +329,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(数据)表 + //解读: + //这个方法用于创建数据表,以及与之相关的触发器。 + //创建数据表:使用db.execSQL方法执行预定义的SQL语句CREATE_DATA_TABLE_SQL,用于创建数据表。 + //重新创建数据表触发器:调用reCreateDataTableTriggers方法,用于删除并重新创建与数据表相关的触发器。 + //创建索引:使用db.execSQL方法执行CREATE_DATA_NOTE_ID_INDEX_SQL语句,为数据表创建索引。 + //记录日志:使用Log.d方法记录一条调试级别的日志,表示数据表已经创建。 public void createDataTable(SQLiteDatabase db) { db.execSQL(CREATE_DATA_TABLE_SQL); reCreateDataTableTriggers(db); @@ -277,6 +351,10 @@ public class NotesDatabaseHelper extends SQLiteOpenHelper { Log.d(TAG, "data table has been created"); } + //和上面的note表的reCreate...同理 + //重新创建或更新与笔记表相关的触发器。 + //首先,使用DROP TRIGGER IF EXISTS语句删除已存在的触发器。确保在重新创建触发器之前,不存在同名的触发器。 + //然后,使用db.execSQL()方法执行预定义的SQL语句,这些语句用于创建新的触发器。 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"); @@ -287,6 +365,11 @@ public class NotesDatabaseHelper extends SQLiteOpenHelper { db.execSQL(DATA_UPDATE_NOTE_CONTENT_ON_DELETE_TRIGGER); } + //解读: + //synchronized关键字确保在多线程环境下,只有一个线程能够进入这个方法,防止了同时创建多个实例的情况 + //getInstance(Context context)方法使用了单例模式来确保整个应用程序中只有一个NotesDatabaseHelper实例。 + //它首先检查mInstance(类的静态成员变量,没有在代码片段中显示)是否为null。 + //如果是null,则创建一个新的NotesDatabaseHelper实例,并将其赋值给mInstance。最后返回mInstance。 static synchronized NotesDatabaseHelper getInstance(Context context) { if (mInstance == null) { mInstance = new NotesDatabaseHelper(context); @@ -294,12 +377,19 @@ public class NotesDatabaseHelper extends SQLiteOpenHelper { return mInstance; } + //功能简介: + //当数据库首次创建时,onCreate方法会被调用。 + //这里重写onCreate方法,它调用了上述createNoteTable(db)和createDataTable(db)两个方法 + //这样首次创建数据库时就多出了两张表。 @Override public void onCreate(SQLiteDatabase db) { createNoteTable(db); createDataTable(db); } + //功能简介: + //当数据库需要升级时(即数据库的版本号改变),onUpgrade方法会被调用。 + //该方法会根据当前的oldVersion和新的newVersion来执行相应的升级操作 @Override public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { boolean reCreateTriggers = false; @@ -327,12 +417,17 @@ public class NotesDatabaseHelper extends SQLiteOpenHelper { reCreateDataTableTriggers(db); } - if (oldVersion != newVersion) { + if (oldVersion != newVersion) { //数据库升级失败,抛出一个异常,表示数据库升级失败 throw new IllegalStateException("Upgrade notes database to version " + newVersion + "fails"); } } + //功能简介: + // 将数据库从版本1升级到版本2。 + //解读: + // 首先,它删除了已经存在的NOTE和DATA表(如果存在的话)。DROP TABLE IF EXISTS语句确保了即使这些表不存在,也不会抛出错误。 + // 然后,它调用了createNoteTable(db)和createDataTable(db)方法来重新创建这两个表。这意味着在升级到版本2时,这两个表的内容会被完全清除,并重新创建新的空表。 private void upgradeToV2(SQLiteDatabase db) { db.execSQL("DROP TABLE IF EXISTS " + TABLE.NOTE); db.execSQL("DROP TABLE IF EXISTS " + TABLE.DATA); @@ -340,6 +435,12 @@ public class NotesDatabaseHelper extends SQLiteOpenHelper { createDataTable(db); } + //功能简介: + // 将数据库从版本2(或可能是跳过版本2的某个状态)升级到版本3。 + //解读: + // 首先,删除了三个不再使用的触发器(如果存在的话)。触发器是数据库中的一种对象,可以在插入、更新或删除记录时自动执行某些操作。 + // 然后,使用ALTER TABLE语句修改表结构,向NOTE表中添加了一个名为GTASK_ID的新列,并设置默认值为空字符串。 + // 最后,向NOTE表中插入了一条新的系统文件夹记录,表示一个名为“trash folder”的系统文件夹。这可能是用于存储已删除笔记的回收站功能。 private void upgradeToV3(SQLiteDatabase db) { // drop unused triggers db.execSQL("DROP TRIGGER IF EXISTS update_note_modified_date_on_insert"); @@ -355,8 +456,12 @@ public class NotesDatabaseHelper extends SQLiteOpenHelper { db.insert(TABLE.NOTE, null, values); } + //功能简介: + // 这个方法负责将数据库从版本3升级到版本4。 + //解读: + // 它向NOTE表中添加了一个名为VERSION的新列,并设置了默认值为0。这个新列用于记录标签版本信息。 private void upgradeToV4(SQLiteDatabase db) { db.execSQL("ALTER TABLE " + TABLE.NOTE + " ADD COLUMN " + NoteColumns.VERSION + " INTEGER NOT NULL DEFAULT 0"); } -} +} \ No newline at end of file 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..e688aa2 100644 --- a/src/Notes-master/src/net/micode/notes/data/NotesProvider.java +++ b/src/Notes-master/src/net/micode/notes/data/NotesProvider.java @@ -1,19 +1,3 @@ -/* - * Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net) - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - package net.micode.notes.data; @@ -36,12 +20,24 @@ import net.micode.notes.data.NotesDatabaseHelper.TABLE; public class NotesProvider extends ContentProvider { +// Android 应用程序中的一部分:内容提供者(ContentProvider)。 +// 内容提供者是 Android 四大组件之一,它允许应用程序之间共享数据。 + + //概述: + //NotesProvider的主要功能是作为一个内容提供者,为其他应用程序或组件提供对“Notes”数据的访问。 + //它允许其他应用程序查询、插入、更新或删除标签数据。 + //通过URI匹配,NotesProvider能够区分对哪种数据类型的请求(例如,单独的标签、标签的数据、文件夹操作等),并执行相应的操作。 + + //用于匹配不同URI的UriMatcher对象,通常用于解析传入的URI,并确定应该执行哪种操作。 private static final UriMatcher mMatcher; + //NotesDatabaseHelper实类,用来操作SQLite数据库,负责创建、更新和查询数据库。 private NotesDatabaseHelper mHelper; + //标签,输出日志时用来表示是该类发出的消息 private static final String TAG = "NotesProvider"; + //6个URI的匹配码,用于区分不同的URI类型 private static final int URI_NOTE = 1; private static final int URI_NOTE_ITEM = 2; private static final int URI_DATA = 3; @@ -50,13 +46,23 @@ public class NotesProvider extends ContentProvider { private static final int URI_SEARCH = 5; private static final int URI_SEARCH_SUGGEST = 6; + //进一步定义了URI匹配规则和搜索查询的投影 + //功能概述: + //初始化了一个UriMatcher对象mMatcher,并添加了一系列的URI匹配规则。 + //解读: static { + //创建了一个UriMatcher实例,并设置默认匹配码为NO_MATCH,表示如果没有任何URI匹配,则返回这个码。 mMatcher = new UriMatcher(UriMatcher.NO_MATCH); + //添加规则,当URI的authority为Notes.AUTHORITY,路径为note时,返回匹配码URI_NOTE。 mMatcher.addURI(Notes.AUTHORITY, "note", URI_NOTE); + //添加规则,当URI的authority为Notes.AUTHORITY,路径为note/后跟一个数字(#代表数字)时,返回匹配码URI_NOTE_ITEM。 mMatcher.addURI(Notes.AUTHORITY, "note/#", URI_NOTE_ITEM); + //和上面两句同理,但用于匹配数据相关的URI mMatcher.addURI(Notes.AUTHORITY, "data", URI_DATA); mMatcher.addURI(Notes.AUTHORITY, "data/#", URI_DATA_ITEM); + //用于匹配搜索相关的URI mMatcher.addURI(Notes.AUTHORITY, "search", URI_SEARCH); + //这两行用于匹配搜索建议相关的URI mMatcher.addURI(Notes.AUTHORITY, SearchManager.SUGGEST_URI_PATH_QUERY, URI_SEARCH_SUGGEST); mMatcher.addURI(Notes.AUTHORITY, SearchManager.SUGGEST_URI_PATH_QUERY + "/*", URI_SEARCH_SUGGEST); } @@ -65,33 +71,66 @@ 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. */ - private static final String NOTES_SEARCH_PROJECTION = NoteColumns.ID + "," - + NoteColumns.ID + " AS " + SearchManager.SUGGEST_COLUMN_INTENT_EXTRA_DATA + "," - + "TRIM(REPLACE(" + NoteColumns.SNIPPET + ", x'0A','')) AS " + SearchManager.SUGGEST_COLUMN_TEXT_1 + "," - + "TRIM(REPLACE(" + NoteColumns.SNIPPET + ", x'0A','')) AS " + SearchManager.SUGGEST_COLUMN_TEXT_2 + "," - + R.drawable.search_result + " AS " + SearchManager.SUGGEST_COLUMN_ICON_1 + "," - + "'" + Intent.ACTION_VIEW + "' AS " + SearchManager.SUGGEST_COLUMN_INTENT_ACTION + "," - + "'" + Notes.TextNote.CONTENT_TYPE + "' AS " + SearchManager.SUGGEST_COLUMN_INTENT_DATA; + //功能概述: + //一个 SQL 查询的投影部分,用于定义查询返回的结果集中应该包含哪些列。 + //解读:(每行对应) + //返回笔记的 ID。 + //笔记的 ID 也被重命名为 SUGGEST_COLUMN_INTENT_EXTRA_DATA,这通常用于 Android 的搜索建议中,作为传递给相关 Intent 的额外数据。 + //对 SNIPPET 列的处理:首先使用 REPLACE 函数将 x'0A'(即换行符 \n)替换为空字符串,然后使用 TRIM 函数删除前后的空白字符,处理后的结果分别重命名为 SUGGEST_COLUMN_TEXT_1 + //对 SNIPPET 列的处理:首先使用 REPLACE 函数将 x'0A'(即换行符 \n)替换为空字符串,然后使用 TRIM 函数删除前后的空白字符,处理后的结果分别重命名为 SUGGEST_COLUMN_TEXT_2 + //返回一个用于搜索建议图标的资源 ID,并命名为 SUGGEST_COLUMN_ICON_1。 + //返回一个固定的 Intent 动作 ACTION_VIEW,并命名为 SUGGEST_COLUMN_INTENT_ACTION。 + //返回一个内容类型,并命名为 SUGGEST_COLUMN_INTENT_DATA。 + private static final String NOTES_SEARCH_PROJECTION = NoteColumns.ID + "," //返回笔记的 ID + + NoteColumns.ID + " AS " + SearchManager.SUGGEST_COLUMN_INTENT_EXTRA_DATA + "," + + "TRIM(REPLACE(" + NoteColumns.SNIPPET + ", x'0A','')) AS " + SearchManager.SUGGEST_COLUMN_TEXT_1 + "," + + "TRIM(REPLACE(" + NoteColumns.SNIPPET + ", x'0A','')) AS " + SearchManager.SUGGEST_COLUMN_TEXT_2 + "," + + R.drawable.search_result + " AS " + SearchManager.SUGGEST_COLUMN_ICON_1 + "," + + "'" + Intent.ACTION_VIEW + "' AS " + SearchManager.SUGGEST_COLUMN_INTENT_ACTION + "," + + "'" + Notes.TextNote.CONTENT_TYPE + "' AS " + SearchManager.SUGGEST_COLUMN_INTENT_DATA; + //功能概述: + //完整的 SQL 查询语句,用于从 TABLE.NOTE 表中检索信息 + //解读: + // 使用上面定义的投影来选择数据。 + // 并指定从哪个表中选择数据。 + //WHERE子句包含三个条件: + // ①搜索 SNIPPET 列中包含特定模式的行(? 是一个占位符,实际查询时会用具体的值替换)。 + // ②父ID不为回收站的ID:排除那些父 ID 为回收站的行。 + // ③只选择类型为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; + + " FROM " + TABLE.NOTE + + " WHERE " + NoteColumns.SNIPPET + " LIKE ?" + + " AND " + NoteColumns.PARENT_ID + "<>" + Notes.ID_TRASH_FOLER + + " AND " + NoteColumns.TYPE + "=" + Notes.TYPE_NOTE; + //重写onCreate方法: + //getContext() 方法被调用以获取当前组件的上下文(Context),以便 NotesDatabaseHelper 能够访问应用程序的资源和其他功能 + //mHelper用于存储从 NotesDatabaseHelper.getInstance 方法返回的实例。这样,该实例就可以在整个组件的其他方法中被访问和使用。 @Override public boolean onCreate() { mHelper = NotesDatabaseHelper.getInstance(getContext()); return true; } + //功能:查询数据 @Override public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, - String sortOrder) { + String sortOrder) { + //初始化变量: + //Cursor对象 c,用来存储查询结果 + //使用 NotesDatabaseHelper 的实例 mHelper来获取一个可读的数据库实例 + //定义一个字符串id,用来存储从URI中解析出的ID Cursor c = null; SQLiteDatabase db = mHelper.getReadableDatabase(); String id = null; + + //根据匹配不同的URI来进行不同的查询 switch (mMatcher.match(uri)) { + // URI_NOTE:查询整个 NOTE 表。 + // URI_NOTE_ITEM:查询 NOTE 表中的特定项。ID 从 URI 的路径段中获取,并添加到查询条件中。 + // URI_DATA:查询整个 DATA 表。 + // URI_DATA_ITEM:查询 DATA 表中的特定项。ID 的获取和处理方式与 URI_NOTE_ITEM 相同。 case URI_NOTE: c = db.query(TABLE.NOTE, projection, selection, selectionArgs, null, null, sortOrder); @@ -110,6 +149,12 @@ public class NotesProvider extends ContentProvider { c = db.query(TABLE.DATA, projection, DataColumns.ID + "=" + id + parseSelection(selection), selectionArgs, null, null, sortOrder); break; + + //URI_SEARCH 和 URI_SEARCH_SUGGEST:处理搜索查询。 + // 代码首先检查是否提供了不应与搜索查询一起使用的参数(如 sortOrder, selection, selectionArgs, 或 projection)。 + // 如果提供了这些参数,则抛出一个 IllegalArgumentException。 + // 根据 URI 类型,从 URI 的路径段或查询参数中获取搜索字符串 searchString。 + // 如果 searchString 为空或无效,则返回 null,表示没有搜索结果。 case URI_SEARCH: case URI_SEARCH_SUGGEST: if (sortOrder != null || projection != null) { @@ -130,6 +175,8 @@ public class NotesProvider extends ContentProvider { return null; } + //字符串格式化:格式化后的字符串就会是 "%s%",即包含s是任何文本 + //然后执行原始SQL查询 try { searchString = String.format("%%%s%%", searchString); c = db.rawQuery(NOTES_SNIPPET_SEARCH_QUERY, @@ -138,19 +185,31 @@ public class NotesProvider extends ContentProvider { Log.e(TAG, "got exception: " + ex.toString()); } break; + + //未知URI处理: default: throw new IllegalArgumentException("Unknown URI " + uri); } + //如果查询结果不为空(即 Cursor 对象 c 不是 null),则为其设置一个通知 URI。 + //这意味着当与这个 URI 关联的数据发生变化时,任何注册了监听这个 URI 的 ContentObserver 都会被通知。 if (c != null) { c.setNotificationUri(getContext().getContentResolver(), uri); } return c; } + //功能:插入数据 + //参数:Uri 用来标识要插入数据的表,ContentValues对象包含要插入的键值对 @Override public Uri insert(Uri uri, ContentValues values) { + //获取数据库 + //三个长整型变量,分别用来存储数据项ID、便签ID 和插入行的ID SQLiteDatabase db = mHelper.getWritableDatabase(); long dataId = 0, noteId = 0, insertedId = 0; + + //对于 URI_NOTE,将values插入到 TABLE.NOTE 表中,并返回插入行的 ID。 + //对于 URI_DATA,首先检查values是否包含 DataColumns.NOTE_ID,如果包含,则获取其值。如果不包含,记录一条日志信息。然后,将 values 插入到 TABLE.DATA 表中,并返回插入行的 ID。 + //如果 uri 不是已知的 URI 类型,则抛出一个 IllegalArgumentException。 switch (mMatcher.match(uri)) { case URI_NOTE: insertedId = noteId = db.insert(TABLE.NOTE, null, values); @@ -166,6 +225,10 @@ public class NotesProvider extends ContentProvider { default: throw new IllegalArgumentException("Unknown URI " + uri); } + + //功能:通知变化 + //如果noteId 或 dataId 大于 0(即成功插入了数据),则使用 ContentResolver 的 notifyChange 方法通知监听这些 URI 的观察者,告知数据已经改变。 + //ContentUris.withAppendedId 方法用于在基本 URI 后面追加一个 ID,形成完整的 URI。 // Notify the note uri if (noteId > 0) { getContext().getContentResolver().notifyChange( @@ -178,16 +241,28 @@ public class NotesProvider extends ContentProvider { ContentUris.withAppendedId(Notes.CONTENT_DATA_URI, dataId), null); } + //返回包含新插入数据项ID 的 Uri。允许调用者知道新插入的数据项的位置 return ContentUris.withAppendedId(uri, insertedId); } + //功能:删除数据项 + //参数:uri:标识要删除数据的表或数据项。 selection:一个可选的 WHERE 子句,用于指定删除条件。 selectionArgs:一个可选的字符串数组,用于替换 selection 中的占位符 @Override public int delete(Uri uri, String selection, String[] selectionArgs) { + //count:记录被删除的行数。 + //id:用于存储从 URI 中解析出的数据项 ID。 + //db:可写的数据库对象,用于执行删除操作。 + //deleteData:一个布尔值,用于标记是否删除了 DATA 表中的数据。 int count = 0; String id = null; SQLiteDatabase db = mHelper.getWritableDatabase(); boolean deleteData = false; + switch (mMatcher.match(uri)) { + //URI_NOTE: 修改 selection 语句:确保只删除 ID 大于 0 的笔记。然后执行删除操作并返回被删除的行数。 + //URI_NOTE_ITEM: 从 URI 中解析出 ID。检查 ID 是否小于等于 0,如果是,则不执行删除操作;否则执行删除操作并返回被删除的行数 + //URI_DATA: 执行删除操作并返回被删除的行数。设置 deleteData 为 true,表示删除了 DATA 表中的数据。 + //URI_DATA_ITEM: 先从 URI 中解析出 ID,然后执行删除操作并返回被删除的行数,并设置 deleteData 为 true,表示删除了 DATA 表中的数据。 case URI_NOTE: selection = "(" + selection + ") AND " + NoteColumns.ID + ">0 "; count = db.delete(TABLE.NOTE, selection, selectionArgs); @@ -218,22 +293,39 @@ public class NotesProvider extends ContentProvider { default: throw new IllegalArgumentException("Unknown URI " + uri); } + + //如果 count 大于 0,说明有数据被删除。 + //如果 deleteData 为 true,则通知监听 Notes.CONTENT_NOTE_URI 的观察者,数据已改变。 + //通知监听传入 uri 的观察者数据已改变。 if (count > 0) { if (deleteData) { getContext().getContentResolver().notifyChange(Notes.CONTENT_NOTE_URI, null); } getContext().getContentResolver().notifyChange(uri, null); } + return count; } + //功能:更新数据库的数据 + //参数:uri:标识要更新数据的表或数据项。 values:一个包含新值的键值对集合。 + // selection:一个可选的 WHERE 子句,用于指定更新条件。 selectionArgs:一个可选的字符串数组,用于替换 selection 中的占位符。 @Override public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) { + //count:记录被更新的行数。 + //id:用于存储从 URI 中解析出的数据项 ID。 + //db:可写的 SQLite 数据库对象,用于执行更新操作。 + //updateData:用于标记是否更新了 data 表中的数据。 int count = 0; String id = null; SQLiteDatabase db = mHelper.getWritableDatabase(); boolean updateData = false; + switch (mMatcher.match(uri)) { + //URI_NOTE:调用 increaseNoteVersion 方法(用于增加便签版本),然后在note表执行更新操作并返回被更新的行数。 + //URI_NOTE_ITEM:从 URI 中解析出 ID,并调用 increaseNoteVersion 方法,传入解析出的 ID,最后在note表执行更新操作并返回被更新的行数。 + //URI_DATA:在data表执行更新操作并返回被更新的行数。设置 updateData 为 true,表示更新了 DATA 表中的数据。 + //URI_DATA_ITEM:从 URI 中解析出 ID。执行更新操作并返回被更新的行数。置 updateData 为 true,表示更新了 DATA 表中的数据。 case URI_NOTE: increaseNoteVersion(-1, selection, selectionArgs); count = db.update(TABLE.NOTE, values, selection, selectionArgs); @@ -258,6 +350,9 @@ public class NotesProvider extends ContentProvider { throw new IllegalArgumentException("Unknown URI " + uri); } + //如果 count 大于 0,说明有数据被更新。 + //如果 updateData 为 true,则通知监听 Notes.CONTENT_NOTE_URI 的观察者数据已改变。 + //通知监听传入 uri 的观察者数据已改变。 if (count > 0) { if (updateData) { getContext().getContentResolver().notifyChange(Notes.CONTENT_NOTE_URI, null); @@ -267,10 +362,12 @@ public class NotesProvider extends ContentProvider { return count; } + //解析传入的条件语句:一个 SQL WHERE 子句的一部分 private String parseSelection(String selection) { return (!TextUtils.isEmpty(selection) ? " AND (" + selection + ')' : ""); } + //更新note表的version列,将其值增加 1。 private void increaseNoteVersion(long id, String selection, String[] selectionArgs) { StringBuilder sql = new StringBuilder(120); sql.append("UPDATE "); @@ -302,4 +399,4 @@ public class NotesProvider extends ContentProvider { return null; } -} +} \ No newline at end of file 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..2506b02 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 @@ -16,67 +16,124 @@ package net.micode.notes.gtask.data; -import android.database.Cursor; -import android.util.Log; +import android.database.Cursor; // 数据库查询游标:父类方法参数,此类未实际使用 +import android.util.Log; // 日志工具:打印错误/警告日志 -import net.micode.notes.tool.GTaskStringUtils; - -import org.json.JSONException; -import org.json.JSONObject; +import net.micode.notes.tool.GTaskStringUtils; // GTask字符串工具类:存储元数据相关常量(如Gid字段名) +import org.json.JSONException; // JSON解析异常:JSON操作失败时抛出 +import org.json.JSONObject; // JSON对象:存储和解析元数据键值对 +/** + * GTask元数据封装类 + * 通俗说:这个类是GTask的“关联信息管家”,继承自GTask任务类(Task), + * 专门用来存储本地便签和远程GTask任务之间的关联元信息(核心是远程GTask的唯一ID), + * 负责元信息的JSON序列化(转为字符串存储)和反序列化(从字符串解析出关联ID) + */ public class MetaData extends Task { + // 日志标签:使用类名作为标签,方便定位该类的日志信息 private final static String TAG = MetaData.class.getSimpleName(); + // 关联的远程GTask唯一ID:用来绑定本地便签和远程GTask任务(null表示未关联) private String mRelatedGid = null; + /** + * 设置元数据(绑定远程GTask ID并序列化) + * 通俗说:把远程GTask的唯一ID存入元信息JSON对象,再将JSON转为字符串作为任务备注, + * 同时把任务名称设为元数据专用名称,方便GTask识别这是元数据任务 + * @param gid 远程GTask任务的唯一ID(关联标识) + * @param metaInfo 元信息JSON对象(用于存储额外元数据) + */ public void setMeta(String gid, JSONObject metaInfo) { try { + // 将远程GTask ID存入JSON对象,键为元数据头部GTask ID常量 metaInfo.put(GTaskStringUtils.META_HEAD_GTASK_ID, gid); } catch (JSONException e) { + // JSON存入失败,打印错误日志 Log.e(TAG, "failed to put related gid"); } + // 将元信息JSON对象转为字符串,设置为当前任务的备注(存储元数据) setNotes(metaInfo.toString()); + // 将任务名称设为元数据专用名称,标识这是元数据任务 setName(GTaskStringUtils.META_NOTE_NAME); } + /** + * 获取关联的远程GTask唯一ID + * 通俗说:外部调用该方法,获取本地便签绑定的远程GTask任务ID + * @return 远程GTask ID(null表示未关联) + */ public String getRelatedGid() { return mRelatedGid; } + /** + * 重写父类方法:判断元数据是否值得保存 + * 通俗说:只要元数据的备注(JSON字符串)不为空,就表示有有效元信息,值得保存到GTask + * @return true=值得保存,false=无需保存 + */ @Override public boolean isWorthSaving() { return getNotes() != null; } + /** + * 重写父类方法:从远程GTask的JSON数据中解析元信息 + * 通俗说:当从GTask服务器拉取数据时,用该方法解析元数据任务,提取出关联的GTask ID + * @param js 远程GTask返回的JSON对象(包含元数据任务信息) + */ @Override public void setContentByRemoteJSON(JSONObject js) { + // 先调用父类方法,解析通用的GTask任务内容(如名称、备注等) super.setContentByRemoteJSON(js); + + // 如果任务备注(元数据JSON字符串)不为空 if (getNotes() != null) { try { + // 将备注字符串转为JSON对象(去除首尾空白字符,避免解析失败) JSONObject metaInfo = new JSONObject(getNotes().trim()); + // 从JSON对象中提取关联的远程GTask ID mRelatedGid = metaInfo.getString(GTaskStringUtils.META_HEAD_GTASK_ID); } catch (JSONException e) { + // JSON解析失败,打印警告日志,并将关联ID置为null Log.w(TAG, "failed to get related gid"); mRelatedGid = null; } } } + /** + * 重写父类方法:从本地JSON设置内容(禁止调用) + * 通俗说:MetaData只处理远程GTask的元数据,不需要从本地JSON加载内容, + * 一旦调用该方法,直接抛出异常提示非法访问 + */ @Override public void setContentByLocalJSON(JSONObject js) { - // this function should not be called + // 抛出非法访问错误,提示该方法不应该被调用 throw new IllegalAccessError("MetaData:setContentByLocalJSON should not be called"); } + /** + * 重写父类方法:从内容生成本地JSON(禁止调用) + * 通俗说:MetaData不需要生成本地JSON数据,一旦调用该方法,直接抛出异常提示非法访问 + * @return 无返回值(直接抛出异常) + */ @Override public JSONObject getLocalJSONFromContent() { + // 抛出非法访问错误,提示该方法不应该被调用 throw new IllegalAccessError("MetaData:getLocalJSONFromContent should not be called"); } + /** + * 重写父类方法:获取同步动作(禁止调用) + * 通俗说:MetaData不需要处理本地数据库的同步动作判断,一旦调用该方法,直接抛出异常提示非法访问 + * @param c 数据库查询游标(此类未使用) + * @return 无返回值(直接抛出异常) + */ @Override public int getSyncAction(Cursor c) { + // 抛出非法访问错误,提示该方法不应该被调用 throw new IllegalAccessError("MetaData:getSyncAction should not be called"); } -} +} \ No newline at end of file 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..92e99d1 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 @@ -16,86 +16,147 @@ package net.micode.notes.gtask.data; -import android.database.Cursor; +import android.database.Cursor; // 数据库查询游标:用于子类查询本地数据库,判断同步动作 -import org.json.JSONObject; +import org.json.JSONObject; // JSON对象:用于封装GTask接口所需参数,或存储本地数据 +/** + * GTask同步抽象基类 + * 通俗说:这个类是所有GTask同步数据(如任务、元数据、文件夹)的“通用模板”, + * 定义了同步所需的所有动作常量(比如新增、删除、更新)、通用属性(远程ID、名称等), + * 还约定了子类必须实现的同步相关抽象方法,统一了GTask同步的核心流程规范 + */ public abstract class Node { + // 同步动作常量:表示无任何同步操作(本地和远程数据一致) public static final int SYNC_ACTION_NONE = 0; - + // 同步动作常量:往远程GTask服务器添加新数据(本地有新增,远程没有) public static final int SYNC_ACTION_ADD_REMOTE = 1; - + // 同步动作常量:往本地数据库添加新数据(远程有新增,本地没有) public static final int SYNC_ACTION_ADD_LOCAL = 2; - + // 同步动作常量:删除远程GTask服务器上的数据(本地已删除,远程还存在) public static final int SYNC_ACTION_DEL_REMOTE = 3; - + // 同步动作常量:删除本地数据库里的数据(远程已删除,本地还存在) public static final int SYNC_ACTION_DEL_LOCAL = 4; - + // 同步动作常量:更新远程GTask服务器上的数据(本地修改比远程新) 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; + // 远程GTask唯一标识(Gid):每个同步数据在GTask服务器上的唯一ID(null表示未上传到远程) private String mGid; - + // 节点名称:同步数据的名称(比如便签标题、文件夹名称) private String mName; - + // 最后修改时间戳:数据最后一次被修改的时间(用于判断本地和远程哪个更新) private long mLastModified; - + // 删除标记:是否被标记为删除(true=已删除,需要同步删除操作) private boolean mDeleted; + /** + * 构造方法:初始化同步节点的默认属性 + * 通俗说:创建Node对象时,给所有通用属性设置默认值(未关联远程、空名称、无修改、未删除) + */ public Node() { - mGid = null; - mName = ""; - mLastModified = 0; - mDeleted = false; + mGid = null; // 默认未关联远程GTask,无Gid + mName = ""; // 默认名称为空字符串 + mLastModified = 0; // 默认最后修改时间为0(未修改) + mDeleted = false; // 默认未被删除 } + /** + * 抽象方法:获取创建动作的JSON对象 + * 通俗说:子类必须实现该方法,封装往GTask服务器创建数据所需的参数(转为JSON格式), + * 供GTask接口调用(比如创建新便签任务) + * @param actionId 动作ID(GTask接口所需的唯一动作标识) + * @return 创建动作的JSON参数对象 + */ public abstract JSONObject getCreateAction(int actionId); + /** + * 抽象方法:获取更新动作的JSON对象 + * 通俗说:子类必须实现该方法,封装往GTask服务器更新数据所需的参数(转为JSON格式), + * 供GTask接口调用(比如更新已有的便签内容) + * @param actionId 动作ID(GTask接口所需的唯一动作标识) + * @return 更新动作的JSON参数对象 + */ public abstract JSONObject getUpdateAction(int actionId); + /** + * 抽象方法:从远程GTask的JSON数据中设置内容 + * 通俗说:子类必须实现该方法,当从GTask服务器拉取数据后, + * 解析返回的JSON对象,把数据赋值给本地属性(比如解析远程便签内容,存入本地对象) + * @param js 远程GTask返回的JSON数据对象 + */ public abstract void setContentByRemoteJSON(JSONObject js); + /** + * 抽象方法:从本地JSON数据中设置内容 + * 通俗说:子类必须实现该方法,解析本地存储的JSON数据, + * 把数据赋值给当前节点的属性(比如从本地数据库读取JSON格式的便签数据,初始化对象) + * @param js 本地存储的JSON数据对象 + */ public abstract void setContentByLocalJSON(JSONObject js); + /** + * 抽象方法:从节点内容生成本地JSON对象 + * 通俗说:子类必须实现该方法,把当前节点的属性(比如便签内容、修改时间) + * 封装为JSON对象,用于存储到本地数据库(方便后续同步对比) + * @return 包含节点内容的本地JSON对象 + */ public abstract JSONObject getLocalJSONFromContent(); + /** + * 抽象方法:判断当前节点的同步动作类型 + * 通俗说:子类必须实现该方法,通过查询本地数据库(Cursor), + * 对比本地数据和远程数据的状态(是否新增、修改、删除),返回对应的同步动作常量(比如新增远程、更新本地等) + * @param c 本地数据库查询游标(包含本地数据的查询结果) + * @return 同步动作类型(对应上面定义的SYNC_ACTION_XXX常量) + */ public abstract int getSyncAction(Cursor c); + // 以下是通用属性的Setter方法:设置对应的属性值,供子类或外部调用 + /** 设置远程GTask的唯一标识(Gid) */ public void setGid(String gid) { this.mGid = gid; } + /** 设置节点名称(如便签标题、文件夹名称) */ public void setName(String name) { this.mName = name; } + /** 设置最后修改时间戳(用于同步对比新旧) */ public void setLastModified(long lastModified) { this.mLastModified = lastModified; } + /** 设置删除标记(标记是否需要同步删除操作) */ public void setDeleted(boolean deleted) { this.mDeleted = deleted; } + // 以下是通用属性的Getter方法:获取对应的属性值,供子类或外部调用 + /** 获取远程GTask的唯一标识(Gid) */ public String getGid() { return this.mGid; } + /** 获取节点名称(如便签标题、文件夹名称) */ public String getName() { return this.mName; } + /** 获取最后修改时间戳(用于同步对比新旧) */ public long getLastModified() { return this.mLastModified; } + /** 获取删除标记(判断是否需要同步删除操作) */ public boolean getDeleted() { return this.mDeleted; } -} +} \ No newline at end of file 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..7dca5e6 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,155 +35,260 @@ import org.json.JSONException; import org.json.JSONObject; +/** + * 本地便签数据库(数据明细表)操作工具 + * 通俗说:这个类专门用来处理本地便签数据库里“数据明细”表的内容, + * 负责创建新数据、加载已有数据、记录数据变更、把变更保存到数据库 + */ 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 + DataColumns.ID, // 数据唯一ID + DataColumns.MIME_TYPE, // 数据类型(比如普通便签、通话便签) + DataColumns.CONTENT, // 数据内容(比如便签正文) + DataColumns.DATA1, // 扩展字段1(存一些额外信息,比如便签模式) + DataColumns.DATA3 // 扩展字段3(存一些额外信息) }; - public static final int DATA_ID_COLUMN = 0; - - public static final int DATA_MIME_TYPE_COLUMN = 1; - - public static final int DATA_CONTENT_COLUMN = 2; - - public static final int DATA_CONTENT_DATA_1_COLUMN = 3; - - public static final int DATA_CONTENT_DATA_3_COLUMN = 4; + // 上面字段清单对应的索引位置,方便从查询结果里快速取值 + public static final int DATA_ID_COLUMN = 0; // 数据ID在查询结果里的位置 + public static final int DATA_MIME_TYPE_COLUMN = 1; // 数据类型在查询结果里的位置 + public static final int DATA_CONTENT_COLUMN = 2; // 数据内容在查询结果里的位置 + public static final int DATA_CONTENT_DATA_1_COLUMN = 3; // 扩展字段1在查询结果里的位置 + public static final int DATA_CONTENT_DATA_3_COLUMN = 4; // 扩展字段3在查询结果里的位置 + // 数据库操作工具:用来和本地便签数据库打交道(增删改查) private ContentResolver mContentResolver; + // 是否是新数据:true=新创建的(还没存到数据库),false=已存在的(从数据库加载的) private boolean mIsCreate; + // 这条数据的唯一ID private long mDataId; + // 这条数据的类型(比如普通便签、通话便签) private String mDataMimeType; + // 这条数据的内容(比如便签的正文文字) private String mDataContent; + // 扩展字段1的值(存额外信息,比如便签是普通模式还是清单模式) private long mDataContentData1; + // 扩展字段3的值(存额外信息) private String mDataContentData3; + // 数据变更记录:用来存和原来不一样的内容(只更新有变化的部分,提高效率) private ContentValues mDiffDataValues; + /** + * 构造方法:创建一条新的数据(还没存到数据库) + * @param context 上下文(用来获取数据库操作工具) + */ public SqlData(Context context) { + // 获取数据库操作工具 mContentResolver = context.getContentResolver(); + // 标记为新数据(还没存数据库) mIsCreate = true; + // 初始化为无效ID(还没有数据库分配的唯一ID) mDataId = INVALID_ID; + // 初始数据类型为普通便签 mDataMimeType = DataConstants.NOTE; + // 初始内容为空字符串 mDataContent = ""; + // 初始扩展字段1的值为0 mDataContentData1 = 0; + // 初始扩展字段3的值为空字符串 mDataContentData3 = ""; + // 初始化数据变更记录容器 mDiffDataValues = new ContentValues(); } + /** + * 构造方法:加载数据库里已有的数据 + * @param context 上下文(用来获取数据库操作工具) + * @param c 数据库查询结果(里面存着已有的数据信息) + */ public SqlData(Context context, Cursor c) { + // 获取数据库操作工具 mContentResolver = context.getContentResolver(); + // 标记为已有数据(不是新创建的) mIsCreate = false; + // 从查询结果里加载数据到当前对象 loadFromCursor(c); + // 初始化数据变更记录容器 mDiffDataValues = new ContentValues(); } + /** + * 从数据库查询结果里加载数据 + * 通俗说:把查询结果里的各项信息取出来,赋值给当前对象的属性 + * @param c 数据库查询结果 + */ private void loadFromCursor(Cursor c) { + // 从查询结果里取数据ID mDataId = c.getLong(DATA_ID_COLUMN); + // 从查询结果里取数据类型 mDataMimeType = c.getString(DATA_MIME_TYPE_COLUMN); + // 从查询结果里取数据内容 mDataContent = c.getString(DATA_CONTENT_COLUMN); + // 从查询结果里取扩展字段1的值 mDataContentData1 = c.getLong(DATA_CONTENT_DATA_1_COLUMN); + // 从查询结果里取扩展字段3的值 mDataContentData3 = c.getString(DATA_CONTENT_DATA_3_COLUMN); } + /** + * 从JSON数据包里设置数据内容,并记录变更 + * 通俗说:把JSON里的信息解析出来,更新当前对象的属性, + * 只有和原来不一样的内容,才记录到变更容器里(准备后续更新数据库) + * @param js 包含数据信息的JSON数据包 + * @throws JSONException JSON解析失败时抛出异常 + */ public void setContent(JSONObject js) throws JSONException { + // 从JSON里取数据ID,没有的话就用无效ID long dataId = js.has(DataColumns.ID) ? js.getLong(DataColumns.ID) : INVALID_ID; + // 如果是新数据,或者当前ID和JSON里的ID不一样,就记录这个变更 if (mIsCreate || mDataId != dataId) { mDiffDataValues.put(DataColumns.ID, dataId); } + // 更新当前数据ID mDataId = dataId; + // 从JSON里取数据类型,没有的话就默认是普通便签 String dataMimeType = js.has(DataColumns.MIME_TYPE) ? js.getString(DataColumns.MIME_TYPE) : DataConstants.NOTE; + // 如果是新数据,或者当前类型和JSON里的类型不一样,就记录这个变更 if (mIsCreate || !mDataMimeType.equals(dataMimeType)) { mDiffDataValues.put(DataColumns.MIME_TYPE, dataMimeType); } + // 更新当前数据类型 mDataMimeType = dataMimeType; + // 从JSON里取数据内容,没有的话就为空字符串 String dataContent = js.has(DataColumns.CONTENT) ? js.getString(DataColumns.CONTENT) : ""; + // 如果是新数据,或者当前内容和JSON里的内容不一样,就记录这个变更 if (mIsCreate || !mDataContent.equals(dataContent)) { mDiffDataValues.put(DataColumns.CONTENT, dataContent); } + // 更新当前数据内容 mDataContent = dataContent; + // 从JSON里取扩展字段1的值,没有的话就为0 long dataContentData1 = js.has(DataColumns.DATA1) ? js.getLong(DataColumns.DATA1) : 0; + // 如果是新数据,或者当前扩展字段1和JSON里的不一样,就记录这个变更 if (mIsCreate || mDataContentData1 != dataContentData1) { mDiffDataValues.put(DataColumns.DATA1, dataContentData1); } + // 更新当前扩展字段1的值 mDataContentData1 = dataContentData1; + // 从JSON里取扩展字段3的值,没有的话就为空字符串 String dataContentData3 = js.has(DataColumns.DATA3) ? js.getString(DataColumns.DATA3) : ""; + // 如果是新数据,或者当前扩展字段3和JSON里的不一样,就记录这个变更 if (mIsCreate || !mDataContentData3.equals(dataContentData3)) { mDiffDataValues.put(DataColumns.DATA3, dataContentData3); } + // 更新当前扩展字段3的值 mDataContentData3 = dataContentData3; } + /** + * 把当前数据打包成JSON格式 + * 通俗说:把当前对象里的所有属性信息,整理成JSON数据包,方便后续使用 + * @return 包含当前数据信息的JSON数据包 + * @throws JSONException JSON打包失败时抛出异常 + */ public JSONObject getContent() throws JSONException { + // 如果是新数据(还没存到数据库),打印错误日志并返回null if (mIsCreate) { Log.e(TAG, "it seems that we haven't created this in database yet"); return null; } + // 创建空的JSON数据包 JSONObject js = new JSONObject(); + // 把数据ID存入JSON js.put(DataColumns.ID, mDataId); + // 把数据类型存入JSON js.put(DataColumns.MIME_TYPE, mDataMimeType); + // 把数据内容存入JSON js.put(DataColumns.CONTENT, mDataContent); + // 把扩展字段1的值存入JSON js.put(DataColumns.DATA1, mDataContentData1); + // 把扩展字段3的值存入JSON js.put(DataColumns.DATA3, mDataContentData3); + // 返回打包好的JSON return js; } + /** + * 把数据变更提交到数据库 + * 通俗说:新数据就插入到数据库,已有数据就更新变更的部分,最后清空变更记录 + * @param noteId 这条数据所属的便签ID(关联便签主表) + * @param validateVersion 是否验证便签版本(避免同步时多人同时修改导致冲突) + * @param version 要验证的便签版本号 + */ public void commit(long noteId, boolean validateVersion, long version) { - + // 如果是新数据(要插入数据库) if (mIsCreate) { + // 如果数据ID是无效的,就把ID从变更记录里移除(数据库会自动生成唯一ID) if (mDataId == INVALID_ID && mDiffDataValues.containsKey(DataColumns.ID)) { mDiffDataValues.remove(DataColumns.ID); } - + // 把所属的便签ID存入变更记录(关联到便签主表) mDiffDataValues.put(DataColumns.NOTE_ID, noteId); + // 把变更记录里的内容插入到数据明细表,返回新数据的URI Uri uri = mContentResolver.insert(Notes.CONTENT_DATA_URI, mDiffDataValues); try { + // 从返回的URI里提取数据库分配的唯一数据ID mDataId = Long.valueOf(uri.getPathSegments().get(1)); } catch (NumberFormatException e) { + // 提取ID失败,打印错误日志并抛出异常 Log.e(TAG, "Get note id error :" + e.toString()); throw new ActionFailureException("create note failed"); } } else { + // 如果是已有数据(要更新数据库),且有变更内容 if (mDiffDataValues.size() > 0) { int result = 0; + // 如果不需要验证便签版本,直接更新数据 if (!validateVersion) { - result = mContentResolver.update(ContentUris.withAppendedId( - Notes.CONTENT_DATA_URI, mDataId), mDiffDataValues, null, null); + 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, + // 如果需要验证便签版本,只有版本匹配时才更新(避免冲突) + result = mContentResolver.update( + ContentUris.withAppendedId(Notes.CONTENT_DATA_URI, mDataId), + mDiffDataValues, " ? in (SELECT " + NoteColumns.ID + " FROM " + TABLE.NOTE - + " WHERE " + NoteColumns.VERSION + "=?)", new String[] { - String.valueOf(noteId), String.valueOf(version) - }); + + " WHERE " + NoteColumns.VERSION + "=?)", + new String[] { String.valueOf(noteId), String.valueOf(version) }); } + // 如果更新结果为0,说明没有更新成功(可能版本不匹配或数据已被修改) if (result == 0) { Log.w(TAG, "there is no update. maybe user updates note when syncing"); } } } + // 清空变更记录(本次提交完成,下次变更重新记录) mDiffDataValues.clear(); + // 标记为非新数据(就算是刚插入的,现在也已经存到数据库了) mIsCreate = false; } + /** + * 获取这条数据的唯一ID + * @return 数据唯一ID + */ public long getId() { return mDataId; } -} +} \ No newline at end of file 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..ae3864f 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,222 +38,313 @@ import org.json.JSONObject; import java.util.ArrayList; +/** + * 本地便签(主表)操作工具 + * 通俗说:这个类专门管本地便签数据库里“便签主表”的内容, + * 包含便签的基本信息(比如标题、背景色、创建时间),还关联着便签的明细内容(比如正文), + * 负责新便签创建、已有便签加载、内容修改记录、打包和保存到数据库 + */ 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, - NoteColumns.NOTES_COUNT, NoteColumns.PARENT_ID, NoteColumns.SNIPPET, NoteColumns.TYPE, - NoteColumns.WIDGET_ID, NoteColumns.WIDGET_TYPE, NoteColumns.SYNC_ID, - NoteColumns.LOCAL_MODIFIED, NoteColumns.ORIGIN_PARENT_ID, NoteColumns.GTASK_ID, - NoteColumns.VERSION + NoteColumns.ID, // 便签唯一ID + NoteColumns.ALERTED_DATE, // 提醒时间 + NoteColumns.BG_COLOR_ID, // 背景颜色ID + NoteColumns.CREATED_DATE, // 创建时间 + NoteColumns.HAS_ATTACHMENT, // 是否有附件(0=没有,1=有) + NoteColumns.MODIFIED_DATE, // 最后修改时间 + NoteColumns.NOTES_COUNT, // 子便签数量 + NoteColumns.PARENT_ID, // 所属文件夹ID + NoteColumns.SNIPPET, // 便签摘要(标题/部分正文) + NoteColumns.TYPE, // 便签类型(普通便签/文件夹/系统文件夹) + NoteColumns.WIDGET_ID, // 桌面小组件ID + NoteColumns.WIDGET_TYPE, // 桌面小组件类型 + NoteColumns.SYNC_ID, // 同步编号 + NoteColumns.LOCAL_MODIFIED, // 本地修改标记 + NoteColumns.ORIGIN_PARENT_ID, // 原始所属文件夹ID + NoteColumns.GTASK_ID, // 对应云端GTask的ID + NoteColumns.VERSION // 便签版本号(用来避免同步冲突) }; - public static final int ID_COLUMN = 0; - - public static final int ALERTED_DATE_COLUMN = 1; - - 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; - - public static final int PARENT_ID_COLUMN = 7; - - public static final int SNIPPET_COLUMN = 8; - - public static final int TYPE_COLUMN = 9; - - public static final int WIDGET_ID_COLUMN = 10; - - public static final int WIDGET_TYPE_COLUMN = 11; - - public static final int SYNC_ID_COLUMN = 12; - - public static final int LOCAL_MODIFIED_COLUMN = 13; - - public static final int ORIGIN_PARENT_ID_COLUMN = 14; - - public static final int GTASK_ID_COLUMN = 15; - - public static final int VERSION_COLUMN = 16; - + // 上面字段清单对应的索引位置,方便从查询结果里快速取值 + public static final int ID_COLUMN = 0; // 便签ID在查询结果里的位置 + public static final int ALERTED_DATE_COLUMN = 1; // 提醒时间在查询结果里的位置 + public static final int BG_COLOR_ID_COLUMN = 2; // 背景颜色ID在查询结果里的位置 + 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; // 子便签数量在查询结果里的位置 + public static final int PARENT_ID_COLUMN = 7; // 所属文件夹ID在查询结果里的位置 + public static final int SNIPPET_COLUMN = 8; // 便签摘要在查询结果里的位置 + public static final int TYPE_COLUMN = 9; // 便签类型在查询结果里的位置 + public static final int WIDGET_ID_COLUMN = 10; // 桌面小组件ID在查询结果里的位置 + public static final int WIDGET_TYPE_COLUMN = 11; // 桌面小组件类型在查询结果里的位置 + public static final int SYNC_ID_COLUMN = 12; // 同步编号在查询结果里的位置 + public static final int LOCAL_MODIFIED_COLUMN = 13; // 本地修改标记在查询结果里的位置 + public static final int ORIGIN_PARENT_ID_COLUMN = 14; // 原始所属文件夹ID在查询结果里的位置 + public static final int GTASK_ID_COLUMN = 15; // 云端GTaskID在查询结果里的位置 + public static final int VERSION_COLUMN = 16; // 便签版本号在查询结果里的位置 + + // 上下文:用来获取数据库操作工具、默认配置等 private Context mContext; - + // 数据库操作工具:用来和本地便签数据库打交道(增删改查) private ContentResolver mContentResolver; - + // 是否是新便签:true=新创建的(还没存到数据库),false=已存在的(从数据库加载的) private boolean mIsCreate; - + // 便签唯一ID private long mId; - + // 提醒时间(比如设置了早上8点提醒,这里存的是对应的时间戳) private long mAlertDate; - + // 背景颜色ID(对应不同的便签背景色,比如白色、黄色) private int mBgColorId; - + // 创建时间(存的是时间戳,记录便签什么时候被创建) private long mCreatedDate; - + // 是否有附件(0=没有附件,1=有附件) private int mHasAttachment; - + // 最后修改时间(存的是时间戳,记录便签最后一次被修改的时间) private long mModifiedDate; - + // 所属文件夹ID(标记这个便签在哪个文件夹里) private long mParentId; - + // 便签摘要(一般是便签的标题,或者正文的前几句) private String mSnippet; - + // 便签类型(普通便签/文件夹/系统文件夹) private int mType; - + // 桌面小组件ID(如果这个便签添加到桌面,这里存小组件的编号) private int mWidgetId; - + // 桌面小组件类型(标记桌面小组件的样式) private int mWidgetType; - + // 原始所属文件夹ID(记录便签最初在哪个文件夹里) private long mOriginParent; - + // 便签版本号(每次修改都会递增,用来避免同步时多人同时修改导致冲突) private long mVersion; - + // 便签主表的变更记录:只存和原来不一样的内容,后续只更新这些变更,提高效率 private ContentValues mDiffNoteValues; - + // 便签的明细数据列表(比如普通便签的正文内容,用SqlData对象存储) private ArrayList mDataList; + /** + * 构造方法:创建一条新的便签(还没存到数据库) + * @param context 上下文(用来获取数据库操作工具、默认背景色等) + */ public SqlNote(Context context) { mContext = context; + // 获取数据库操作工具 mContentResolver = context.getContentResolver(); + // 标记为新便签(还没存数据库) mIsCreate = true; + // 初始化为无效ID(还没有数据库分配的唯一ID) mId = INVALID_ID; + // 初始提醒时间为0(没有设置提醒) mAlertDate = 0; + // 初始背景色为系统默认背景色 mBgColorId = ResourceParser.getDefaultBgId(context); + // 初始创建时间为当前时间(获取系统当前时间戳) mCreatedDate = System.currentTimeMillis(); + // 初始没有附件 mHasAttachment = 0; + // 初始最后修改时间为当前时间 mModifiedDate = System.currentTimeMillis(); + // 初始所属文件夹ID为0(默认在根目录) mParentId = 0; + // 初始摘要为空字符串 mSnippet = ""; + // 初始类型为普通便签 mType = Notes.TYPE_NOTE; + // 初始桌面小组件ID为无效ID(没有添加到桌面) mWidgetId = AppWidgetManager.INVALID_APPWIDGET_ID; + // 初始桌面小组件类型为无效类型 mWidgetType = Notes.TYPE_WIDGET_INVALIDE; + // 初始原始所属文件夹ID为0 mOriginParent = 0; + // 初始版本号为0 mVersion = 0; + // 初始化便签主表的变更记录容器 mDiffNoteValues = new ContentValues(); + // 初始化明细数据列表 mDataList = new ArrayList(); } + /** + * 构造方法:从数据库查询结果里加载已有便签 + * @param context 上下文(用来获取数据库操作工具) + * @param c 数据库查询结果(里面存着已有便签的基本信息) + */ public SqlNote(Context context, Cursor c) { mContext = context; + // 获取数据库操作工具 mContentResolver = context.getContentResolver(); + // 标记为已有便签(不是新创建的) mIsCreate = false; + // 从查询结果里加载便签基本信息 loadFromCursor(c); + // 初始化明细数据列表 mDataList = new ArrayList(); + // 如果是普通便签,加载对应的明细内容(比如正文) if (mType == Notes.TYPE_NOTE) loadDataContent(); + // 初始化便签主表的变更记录容器 mDiffNoteValues = new ContentValues(); } + /** + * 构造方法:通过便签ID加载数据库里已有的便签 + * @param context 上下文(用来获取数据库操作工具) + * @param id 要加载的便签ID + */ public SqlNote(Context context, long id) { mContext = context; + // 获取数据库操作工具 mContentResolver = context.getContentResolver(); + // 标记为已有便签(不是新创建的) mIsCreate = false; + // 通过便签ID查询数据库,再加载便签基本信息 loadFromCursor(id); + // 初始化明细数据列表 mDataList = new ArrayList(); + // 如果是普通便签,加载对应的明细内容(比如正文) if (mType == Notes.TYPE_NOTE) loadDataContent(); + // 初始化便签主表的变更记录容器 mDiffNoteValues = new ContentValues(); - } + /** + * 通过便签ID查询数据库,再加载便签基本信息 + * @param id 要查询的便签ID + */ private void loadFromCursor(long id) { - Cursor c = null; + Cursor c = null; // 数据库查询结果容器 try { + // 根据便签ID查询便签主表,获取该便签的基本信息 c = mContentResolver.query(Notes.CONTENT_NOTE_URI, PROJECTION_NOTE, "(_id=?)", - new String[] { - String.valueOf(id) - }, null); + new String[] { String.valueOf(id) }, null); if (c != null) { + // 移动到查询结果的第一条(因为ID是唯一的,只有一条结果) c.moveToNext(); + // 从查询结果里加载数据到当前对象 loadFromCursor(c); } else { + // 查询结果为空,打印警告日志 Log.w(TAG, "loadFromCursor: cursor = null"); } } finally { + // 不管查询成功与否,最后都关闭查询结果,避免占用资源 if (c != null) c.close(); } } + /** + * 从数据库查询结果里加载便签基本信息 + * 通俗说:把查询结果里的各项便签信息取出来,赋值给当前对象的属性 + * @param c 数据库查询结果 + */ private void loadFromCursor(Cursor c) { - mId = c.getLong(ID_COLUMN); - mAlertDate = c.getLong(ALERTED_DATE_COLUMN); - mBgColorId = c.getInt(BG_COLOR_ID_COLUMN); - mCreatedDate = c.getLong(CREATED_DATE_COLUMN); - mHasAttachment = c.getInt(HAS_ATTACHMENT_COLUMN); - mModifiedDate = c.getLong(MODIFIED_DATE_COLUMN); - mParentId = c.getLong(PARENT_ID_COLUMN); - mSnippet = c.getString(SNIPPET_COLUMN); - mType = c.getInt(TYPE_COLUMN); - mWidgetId = c.getInt(WIDGET_ID_COLUMN); - mWidgetType = c.getInt(WIDGET_TYPE_COLUMN); - mVersion = c.getLong(VERSION_COLUMN); + mId = c.getLong(ID_COLUMN); // 取便签ID + mAlertDate = c.getLong(ALERTED_DATE_COLUMN); // 取提醒时间 + mBgColorId = c.getInt(BG_COLOR_ID_COLUMN); // 取背景颜色ID + mCreatedDate = c.getLong(CREATED_DATE_COLUMN); // 取创建时间 + mHasAttachment = c.getInt(HAS_ATTACHMENT_COLUMN); // 取是否有附件 + mModifiedDate = c.getLong(MODIFIED_DATE_COLUMN); // 取最后修改时间 + mParentId = c.getLong(PARENT_ID_COLUMN); // 取所属文件夹ID + mSnippet = c.getString(SNIPPET_COLUMN); // 取便签摘要 + mType = c.getInt(TYPE_COLUMN); // 取便签类型 + mWidgetId = c.getInt(WIDGET_ID_COLUMN); // 取桌面小组件ID + mWidgetType = c.getInt(WIDGET_TYPE_COLUMN); // 取桌面小组件类型 + mVersion = c.getLong(VERSION_COLUMN); // 取便签版本号 } + /** + * 加载当前便签关联的明细内容(比如普通便签的正文) + * 通俗说:根据便签ID查询数据明细表,把关联的明细数据加载到mDataList里 + */ private void loadDataContent() { - Cursor c = null; + Cursor c = null; // 数据库查询结果容器 + // 先清空现有的明细数据列表 mDataList.clear(); try { + // 根据便签ID查询数据明细表,获取该便签的明细内容 c = mContentResolver.query(Notes.CONTENT_DATA_URI, SqlData.PROJECTION_DATA, - "(note_id=?)", new String[] { - String.valueOf(mId) - }, null); + "(note_id=?)", new String[] { String.valueOf(mId) }, null); if (c != null) { + // 如果没有查询到明细数据,打印警告日志并返回 if (c.getCount() == 0) { Log.w(TAG, "it seems that the note has not data"); return; } + // 遍历所有明细数据,逐个加载到mDataList while (c.moveToNext()) { - SqlData data = new SqlData(mContext, c); - mDataList.add(data); + SqlData data = new SqlData(mContext, c); // 从查询结果创建明细数据对象 + mDataList.add(data); // 添加到明细数据列表 } } else { + // 查询结果为空,打印警告日志 Log.w(TAG, "loadDataContent: cursor = null"); } } finally { + // 不管查询成功与否,最后都关闭查询结果,避免占用资源 if (c != null) c.close(); } } + /** + * 从JSON数据包里设置便签内容,并记录变更 + * 通俗说:把JSON里的便签信息解析出来,更新当前对象的属性, + * 有变更的内容就记录到变更容器里,同时处理明细数据(正文) + * @param js 包含便签信息的JSON数据包 + * @return true=设置成功,false=设置失败(JSON解析出错) + */ public boolean setContent(JSONObject js) { try { + // 从JSON里取出便签基本信息的数据包 JSONObject note = js.getJSONObject(GTaskStringUtils.META_HEAD_NOTE); + + // 如果是系统文件夹,不允许修改,打印警告日志 if (note.getInt(NoteColumns.TYPE) == Notes.TYPE_SYSTEM) { Log.w(TAG, "cannot set system folder"); - } else if (note.getInt(NoteColumns.TYPE) == Notes.TYPE_FOLDER) { - // for folder we can only update the snnipet and type + } + // 如果是普通文件夹,只能更新摘要和类型 + else if (note.getInt(NoteColumns.TYPE) == Notes.TYPE_FOLDER) { + // 从JSON里取文件夹摘要,没有的话为空字符串 String snippet = note.has(NoteColumns.SNIPPET) ? note .getString(NoteColumns.SNIPPET) : ""; + // 如果是新文件夹,或者摘要有变化,记录变更 if (mIsCreate || !mSnippet.equals(snippet)) { mDiffNoteValues.put(NoteColumns.SNIPPET, snippet); } + // 更新当前文件夹摘要 mSnippet = snippet; + // 从JSON里取文件夹类型,没有的话默认是普通便签 int type = note.has(NoteColumns.TYPE) ? note.getInt(NoteColumns.TYPE) : Notes.TYPE_NOTE; + // 如果是新文件夹,或者类型有变化,记录变更 if (mIsCreate || mType != type) { mDiffNoteValues.put(NoteColumns.TYPE, type); } + // 更新当前文件夹类型 mType = type; - } else if (note.getInt(NoteColumns.TYPE) == Notes.TYPE_NOTE) { + } + // 如果是普通便签,更新所有基本信息和明细内容 + else if (note.getInt(NoteColumns.TYPE) == Notes.TYPE_NOTE) { + // 取出明细数据的JSON数组(比如正文内容) JSONArray dataArray = js.getJSONArray(GTaskStringUtils.META_HEAD_DATA); + + // 处理便签ID long id = note.has(NoteColumns.ID) ? note.getLong(NoteColumns.ID) : INVALID_ID; if (mIsCreate || mId != id) { mDiffNoteValues.put(NoteColumns.ID, id); } mId = id; + // 处理提醒时间 long alertDate = note.has(NoteColumns.ALERTED_DATE) ? note .getLong(NoteColumns.ALERTED_DATE) : 0; if (mIsCreate || mAlertDate != alertDate) { @@ -261,6 +352,7 @@ public class SqlNote { } mAlertDate = alertDate; + // 处理背景颜色ID int bgColorId = note.has(NoteColumns.BG_COLOR_ID) ? note .getInt(NoteColumns.BG_COLOR_ID) : ResourceParser.getDefaultBgId(mContext); if (mIsCreate || mBgColorId != bgColorId) { @@ -268,6 +360,7 @@ public class SqlNote { } mBgColorId = bgColorId; + // 处理创建时间 long createDate = note.has(NoteColumns.CREATED_DATE) ? note .getLong(NoteColumns.CREATED_DATE) : System.currentTimeMillis(); if (mIsCreate || mCreatedDate != createDate) { @@ -275,6 +368,7 @@ public class SqlNote { } mCreatedDate = createDate; + // 处理是否有附件 int hasAttachment = note.has(NoteColumns.HAS_ATTACHMENT) ? note .getInt(NoteColumns.HAS_ATTACHMENT) : 0; if (mIsCreate || mHasAttachment != hasAttachment) { @@ -282,6 +376,7 @@ public class SqlNote { } mHasAttachment = hasAttachment; + // 处理最后修改时间 long modifiedDate = note.has(NoteColumns.MODIFIED_DATE) ? note .getLong(NoteColumns.MODIFIED_DATE) : System.currentTimeMillis(); if (mIsCreate || mModifiedDate != modifiedDate) { @@ -289,6 +384,7 @@ public class SqlNote { } mModifiedDate = modifiedDate; + // 处理所属文件夹ID long parentId = note.has(NoteColumns.PARENT_ID) ? note .getLong(NoteColumns.PARENT_ID) : 0; if (mIsCreate || mParentId != parentId) { @@ -296,6 +392,7 @@ public class SqlNote { } mParentId = parentId; + // 处理便签摘要 String snippet = note.has(NoteColumns.SNIPPET) ? note .getString(NoteColumns.SNIPPET) : ""; if (mIsCreate || !mSnippet.equals(snippet)) { @@ -303,6 +400,7 @@ public class SqlNote { } mSnippet = snippet; + // 处理便签类型 int type = note.has(NoteColumns.TYPE) ? note.getInt(NoteColumns.TYPE) : Notes.TYPE_NOTE; if (mIsCreate || mType != type) { @@ -310,6 +408,7 @@ public class SqlNote { } mType = type; + // 处理桌面小组件ID int widgetId = note.has(NoteColumns.WIDGET_ID) ? note.getInt(NoteColumns.WIDGET_ID) : AppWidgetManager.INVALID_APPWIDGET_ID; if (mIsCreate || mWidgetId != widgetId) { @@ -317,6 +416,7 @@ public class SqlNote { } mWidgetId = widgetId; + // 处理桌面小组件类型 int widgetType = note.has(NoteColumns.WIDGET_TYPE) ? note .getInt(NoteColumns.WIDGET_TYPE) : Notes.TYPE_WIDGET_INVALIDE; if (mIsCreate || mWidgetType != widgetType) { @@ -324,6 +424,7 @@ public class SqlNote { } mWidgetType = widgetType; + // 处理原始所属文件夹ID long originParent = note.has(NoteColumns.ORIGIN_PARENT_ID) ? note .getLong(NoteColumns.ORIGIN_PARENT_ID) : 0; if (mIsCreate || mOriginParent != originParent) { @@ -331,9 +432,12 @@ public class SqlNote { } mOriginParent = originParent; + // 处理明细数据(比如正文内容) for (int i = 0; i < dataArray.length(); i++) { JSONObject data = dataArray.getJSONObject(i); SqlData sqlData = null; + + // 如果明细数据有ID,先在现有列表里找对应的明细对象 if (data.has(DataColumns.ID)) { long dataId = data.getLong(DataColumns.ID); for (SqlData temp : mDataList) { @@ -343,32 +447,46 @@ public class SqlNote { } } + // 如果没找到对应的明细对象,创建新的并加入列表 if (sqlData == null) { sqlData = new SqlData(mContext); mDataList.add(sqlData); } + // 给明细对象设置内容,并记录变更 sqlData.setContent(data); } } } catch (JSONException e) { + // JSON解析出错,打印错误日志并返回false Log.e(TAG, e.toString()); e.printStackTrace(); return false; } + // 设置成功,返回true return true; } + /** + * 把当前便签内容打包成JSON格式 + * 通俗说:把便签的基本信息和明细内容整理成JSON数据包,方便后续同步或存储 + * @return 包含便签信息的JSON数据包(新便签/打包失败返回null) + */ public JSONObject getContent() { try { + // 创建空的JSON数据包 JSONObject js = new JSONObject(); + // 如果是新便签(还没存到数据库),打印错误日志并返回null if (mIsCreate) { Log.e(TAG, "it seems that we haven't created this in database yet"); return null; } + // 创建存储便签基本信息的JSON对象 JSONObject note = new JSONObject(); + + // 如果是普通便签,打包所有基本信息和明细内容 if (mType == Notes.TYPE_NOTE) { note.put(NoteColumns.ID, mId); note.put(NoteColumns.ALERTED_DATE, mAlertDate); @@ -382,111 +500,174 @@ public class SqlNote { note.put(NoteColumns.WIDGET_ID, mWidgetId); note.put(NoteColumns.WIDGET_TYPE, mWidgetType); note.put(NoteColumns.ORIGIN_PARENT_ID, mOriginParent); + // 把便签基本信息存入总JSON js.put(GTaskStringUtils.META_HEAD_NOTE, note); + // 创建存储明细数据的JSON数组 JSONArray dataArray = new JSONArray(); for (SqlData sqlData : mDataList) { + // 把每个明细数据打包成JSON JSONObject data = sqlData.getContent(); if (data != null) { dataArray.put(data); } } + // 把明细数据数组存入总JSON js.put(GTaskStringUtils.META_HEAD_DATA, dataArray); - } else if (mType == Notes.TYPE_FOLDER || mType == Notes.TYPE_SYSTEM) { + } + // 如果是文件夹/系统文件夹,只打包ID、类型和摘要 + else if (mType == Notes.TYPE_FOLDER || mType == Notes.TYPE_SYSTEM) { note.put(NoteColumns.ID, mId); note.put(NoteColumns.TYPE, mType); note.put(NoteColumns.SNIPPET, mSnippet); + // 把文件夹信息存入总JSON js.put(GTaskStringUtils.META_HEAD_NOTE, note); } + // 返回打包好的JSON return js; } catch (JSONException e) { + // JSON打包出错,打印错误日志 Log.e(TAG, e.toString()); e.printStackTrace(); } + // 打包失败,返回null return null; } + /** + * 设置便签所属的文件夹ID,并记录变更 + * @param id 文件夹ID + */ public void setParentId(long id) { mParentId = id; mDiffNoteValues.put(NoteColumns.PARENT_ID, id); } + /** + * 设置便签对应的云端GTask ID,并记录变更 + * @param gid 云端GTask的唯一ID + */ public void setGtaskId(String gid) { mDiffNoteValues.put(NoteColumns.GTASK_ID, gid); } + /** + * 设置便签的同步编号,并记录变更 + * @param syncId 同步编号 + */ public void setSyncId(long syncId) { mDiffNoteValues.put(NoteColumns.SYNC_ID, syncId); } + /** + * 重置本地修改标记(标记为未修改),并记录变更 + */ public void resetLocalModified() { mDiffNoteValues.put(NoteColumns.LOCAL_MODIFIED, 0); } + /** + * 获取便签的唯一ID + * @return 便签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; } + /** + * 把便签的变更提交到数据库 + * 通俗说:新便签就插入到主表,再插入明细数据;已有便签就更新变更的部分, + * 之后刷新便签信息,清空变更记录 + * @param validateVersion 是否验证便签版本(避免同步时多人同时修改导致冲突) + */ public void commit(boolean validateVersion) { + // 如果是新便签(要插入数据库) if (mIsCreate) { + // 如果便签ID是无效的,就把ID从变更记录里移除(数据库会自动生成唯一ID) if (mId == INVALID_ID && mDiffNoteValues.containsKey(NoteColumns.ID)) { mDiffNoteValues.remove(NoteColumns.ID); } + // 把便签基本信息插入到便签主表,返回新便签的URI Uri uri = mContentResolver.insert(Notes.CONTENT_NOTE_URI, mDiffNoteValues); try { + // 从返回的URI里提取数据库分配的唯一便签ID mId = Long.valueOf(uri.getPathSegments().get(1)); } catch (NumberFormatException e) { + // 提取ID失败,打印错误日志并抛出异常 Log.e(TAG, "Get note id error :" + e.toString()); throw new ActionFailureException("create note failed"); } + // 如果提取的ID为0,说明创建失败,抛出异常 if (mId == 0) { throw new IllegalStateException("Create thread id failed"); } + // 如果是普通便签,把明细数据也插入到数据明细表 if (mType == Notes.TYPE_NOTE) { for (SqlData sqlData : mDataList) { sqlData.commit(mId, false, -1); } } - } else { + } + // 如果是已有便签(要更新数据库) + else { + // 验证便签ID是否有效(除了根文件夹和通话记录文件夹,其他ID不能<=0) if (mId <= 0 && mId != Notes.ID_ROOT_FOLDER && mId != Notes.ID_CALL_RECORD_FOLDER) { Log.e(TAG, "No such note"); throw new IllegalStateException("Try to update note with invalid id"); } + + // 如果有变更内容,才执行更新操作 if (mDiffNoteValues.size() > 0) { + // 版本号递增(每次修改都升级版本) mVersion ++; int result = 0; + + // 如果不需要验证版本,直接更新便签主表 if (!validateVersion) { result = mContentResolver.update(Notes.CONTENT_NOTE_URI, mDiffNoteValues, "(" - + NoteColumns.ID + "=?)", new String[] { - String.valueOf(mId) - }); - } else { + + NoteColumns.ID + "=?)", new String[] { String.valueOf(mId) }); + } + // 如果需要验证版本,只有版本匹配时才更新(避免同步冲突) + else { result = mContentResolver.update(Notes.CONTENT_NOTE_URI, mDiffNoteValues, "(" - + NoteColumns.ID + "=?) AND (" + NoteColumns.VERSION + "<=?)", - new String[] { - String.valueOf(mId), String.valueOf(mVersion) - }); + + NoteColumns.ID + "=?) AND (" + NoteColumns.VERSION + "<=?)", + new String[] { String.valueOf(mId), String.valueOf(mVersion) }); } + + // 如果更新结果为0,说明没有更新成功(可能版本不匹配或数据已被修改) if (result == 0) { Log.w(TAG, "there is no update. maybe user updates note when syncing"); } } + // 如果是普通便签,更新对应的明细数据 if (mType == Notes.TYPE_NOTE) { for (SqlData sqlData : mDataList) { sqlData.commit(mId, validateVersion, mVersion); @@ -494,12 +675,14 @@ public class SqlNote { } } - // refresh local info + // 刷新便签信息(重新从数据库加载最新数据) loadFromCursor(mId); if (mType == Notes.TYPE_NOTE) loadDataContent(); + // 清空便签主表的变更记录(本次提交完成,下次变更重新记录) mDiffNoteValues.clear(); + // 标记为非新便签(就算是刚插入的,现在也已经存到数据库了) mIsCreate = false; } -} +} \ No newline at end of file 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..dfc9553 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,142 +32,174 @@ import org.json.JSONException; import org.json.JSONObject; +/** + * GTask云端任务处理类(对应本地普通便签) + * 通俗说:这个类是用来处理和GTask云端交互的“任务”(对应本地的普通便签), + * 继承自同步基础模板Node,负责打包创建/更新云端任务的请求、解析云端/本地数据、 + * 判断同步操作类型,还关联着任务的所属列表和兄弟任务 + */ public class Task extends Node { + // 日志标签:打印这个类的日志时,用来标识日志来自这个类 private static final String TAG = Task.class.getSimpleName(); + // 任务是否完成(true=已完成,false=未完成) private boolean mCompleted; - + // 任务备注(额外的说明信息) private String mNotes; - + // 本地便签的元信息(存储便签的详细配置,是个JSON数据包) private JSONObject mMetaInfo; - + // 上一个兄弟任务(当前任务在列表里的前一个任务,用来确定任务顺序) private Task mPriorSibling; - + // 所属任务列表(对应本地的文件夹,当前任务在哪个列表/文件夹里) private TaskList mParent; + /** + * 构造方法:初始化任务的默认状态 + * 通俗说:创建任务对象时,设置默认值,同时调用父类的初始化方法 + */ public Task() { - super(); - mCompleted = false; - mNotes = null; - mPriorSibling = null; - mParent = null; - mMetaInfo = null; + super(); // 调用父类Node的构造方法,初始化同步相关的默认属性 + mCompleted = false; // 默认任务未完成 + mNotes = null; // 默认没有任务备注 + mPriorSibling = null; // 默认没有上一个兄弟任务 + mParent = null; // 默认没有所属任务列表 + mMetaInfo = null; // 默认没有本地便签元信息 } + /** + * 打包“创建云端任务”的请求参数 + * 通俗说:把本地新便签的信息整理成云端能识别的JSON格式,传给云端用来创建新任务 + * @param actionId 动作编号(云端用来识别这次操作的唯一标记) + * @return 整理好的创建任务请求参数(JSON格式) + */ public JSONObject getCreateAction(int actionId) { - JSONObject js = new JSONObject(); + JSONObject js = new JSONObject(); // 创建空的JSON数据包 try { - // action_type + // 1. 设置动作类型:告诉云端这次是“创建任务” js.put(GTaskStringUtils.GTASK_JSON_ACTION_TYPE, GTaskStringUtils.GTASK_JSON_ACTION_TYPE_CREATE); - // action_id + // 2. 设置动作ID:给这次创建操作一个唯一编号,方便云端识别 js.put(GTaskStringUtils.GTASK_JSON_ACTION_ID, actionId); - // index + // 3. 设置任务位置:告诉云端这个新任务在所属列表里的排序位置 js.put(GTaskStringUtils.GTASK_JSON_INDEX, mParent.getChildTaskIndex(this)); - // entity_delta + // 4. 设置任务核心信息:打包任务的名称、创建者等信息 JSONObject entity = new JSONObject(); - entity.put(GTaskStringUtils.GTASK_JSON_NAME, getName()); - entity.put(GTaskStringUtils.GTASK_JSON_CREATOR_ID, "null"); + entity.put(GTaskStringUtils.GTASK_JSON_NAME, getName()); // 任务名称(对应便签摘要/内容) + entity.put(GTaskStringUtils.GTASK_JSON_CREATOR_ID, "null"); // 创建者ID(这里默认传null) entity.put(GTaskStringUtils.GTASK_JSON_ENTITY_TYPE, - GTaskStringUtils.GTASK_JSON_TYPE_TASK); - if (getNotes() != null) { + GTaskStringUtils.GTASK_JSON_TYPE_TASK); // 实体类型:告诉云端这是一个“任务” + if (getNotes() != null) { // 如果有任务备注,就把备注也传过去 entity.put(GTaskStringUtils.GTASK_JSON_NOTES, getNotes()); } - js.put(GTaskStringUtils.GTASK_JSON_ENTITY_DELTA, entity); + js.put(GTaskStringUtils.GTASK_JSON_ENTITY_DELTA, entity); // 把任务核心信息存入总JSON - // parent_id + // 5. 设置所属列表ID:告诉云端这个任务属于哪个列表(对应本地文件夹) js.put(GTaskStringUtils.GTASK_JSON_PARENT_ID, mParent.getGid()); - // dest_parent_type + // 6. 设置所属列表类型:告诉云端父级是“任务列表”(文件夹类型) js.put(GTaskStringUtils.GTASK_JSON_DEST_PARENT_TYPE, GTaskStringUtils.GTASK_JSON_TYPE_GROUP); - // list_id + // 7. 设置列表ID:和所属列表ID一致,确认任务归属 js.put(GTaskStringUtils.GTASK_JSON_LIST_ID, mParent.getGid()); - // prior_sibling_id + // 8. 设置上一个兄弟任务ID:如果有前一个任务,就传过去,保证任务排序 if (mPriorSibling != null) { js.put(GTaskStringUtils.GTASK_JSON_PRIOR_SIBLING_ID, mPriorSibling.getGid()); } } catch (JSONException e) { + // JSON打包出错,打印错误日志并抛出异常 Log.e(TAG, e.toString()); e.printStackTrace(); throw new ActionFailureException("fail to generate task-create jsonobject"); } - return js; + return js; // 返回打包好的创建请求参数 } + /** + * 打包“更新云端任务”的请求参数 + * 通俗说:把本地修改后的便签信息整理成云端能识别的JSON格式,传给云端用来更新已有任务 + * @param actionId 动作编号(云端用来识别这次操作的唯一标记) + * @return 整理好的更新任务请求参数(JSON格式) + */ public JSONObject getUpdateAction(int actionId) { - JSONObject js = new JSONObject(); + JSONObject js = new JSONObject(); // 创建空的JSON数据包 try { - // action_type + // 1. 设置动作类型:告诉云端这次是“更新任务” js.put(GTaskStringUtils.GTASK_JSON_ACTION_TYPE, GTaskStringUtils.GTASK_JSON_ACTION_TYPE_UPDATE); - // action_id + // 2. 设置动作ID:给这次更新操作一个唯一编号,方便云端识别 js.put(GTaskStringUtils.GTASK_JSON_ACTION_ID, actionId); - // id + // 3. 设置任务ID:告诉云端要更新哪个任务(用云端给的唯一ID) js.put(GTaskStringUtils.GTASK_JSON_ID, getGid()); - // entity_delta + // 4. 设置要更新的任务信息:打包修改后的名称、备注、删除状态 JSONObject entity = new JSONObject(); - entity.put(GTaskStringUtils.GTASK_JSON_NAME, getName()); - if (getNotes() != null) { + entity.put(GTaskStringUtils.GTASK_JSON_NAME, getName()); // 更新后的任务名称 + if (getNotes() != null) { // 如果有备注,就更新备注 entity.put(GTaskStringUtils.GTASK_JSON_NOTES, getNotes()); } - entity.put(GTaskStringUtils.GTASK_JSON_DELETED, getDeleted()); - js.put(GTaskStringUtils.GTASK_JSON_ENTITY_DELTA, entity); + entity.put(GTaskStringUtils.GTASK_JSON_DELETED, getDeleted()); // 任务是否被删除(标记云端是否要删除该任务) + js.put(GTaskStringUtils.GTASK_JSON_ENTITY_DELTA, entity); // 把更新信息存入总JSON } catch (JSONException e) { + // JSON打包出错,打印错误日志并抛出异常 Log.e(TAG, e.toString()); e.printStackTrace(); throw new ActionFailureException("fail to generate task-update jsonobject"); } - return js; + return js; // 返回打包好的更新请求参数 } + /** + * 解析云端返回的任务数据,赋值给当前任务对象 + * 通俗说:从云端获取到任务信息后,把数据拆解开,存到当前对象的属性里(同步到本地) + * @param js 云端返回的任务数据包(JSON格式) + */ public void setContentByRemoteJSON(JSONObject js) { - if (js != null) { + if (js != null) { // 如果云端返回的数据不为空 try { - // id + // 1. 提取任务ID(云端给的唯一编号),设置到当前对象 if (js.has(GTaskStringUtils.GTASK_JSON_ID)) { setGid(js.getString(GTaskStringUtils.GTASK_JSON_ID)); } - // last_modified + // 2. 提取任务最后修改时间,设置到当前对象 if (js.has(GTaskStringUtils.GTASK_JSON_LAST_MODIFIED)) { setLastModified(js.getLong(GTaskStringUtils.GTASK_JSON_LAST_MODIFIED)); } - // name + // 3. 提取任务名称,设置到当前对象 if (js.has(GTaskStringUtils.GTASK_JSON_NAME)) { setName(js.getString(GTaskStringUtils.GTASK_JSON_NAME)); } - // notes + // 4. 提取任务备注,设置到当前对象 if (js.has(GTaskStringUtils.GTASK_JSON_NOTES)) { setNotes(js.getString(GTaskStringUtils.GTASK_JSON_NOTES)); } - // deleted + // 5. 提取任务删除状态,设置到当前对象 if (js.has(GTaskStringUtils.GTASK_JSON_DELETED)) { setDeleted(js.getBoolean(GTaskStringUtils.GTASK_JSON_DELETED)); } - // completed + // 6. 提取任务完成状态,设置到当前对象 if (js.has(GTaskStringUtils.GTASK_JSON_COMPLETED)) { setCompleted(js.getBoolean(GTaskStringUtils.GTASK_JSON_COMPLETED)); } } catch (JSONException e) { + // JSON解析出错,打印错误日志并抛出异常 Log.e(TAG, e.toString()); e.printStackTrace(); throw new ActionFailureException("fail to get task content from jsonobject"); @@ -175,135 +207,180 @@ public class Task extends Node { } } + /** + * 解析本地便签的JSON数据,设置任务内容 + * 通俗说:把本地便签的JSON数据拆解开,提取便签内容作为任务名称(同步到云端任务) + * @param js 本地便签的JSON数据包 + */ public void setContentByLocalJSON(JSONObject js) { + // 如果JSON数据为空,或者缺少便签信息/明细内容,打印警告日志 if (js == null || !js.has(GTaskStringUtils.META_HEAD_NOTE) || !js.has(GTaskStringUtils.META_HEAD_DATA)) { Log.w(TAG, "setContentByLocalJSON: nothing is avaiable"); } try { + // 1. 提取本地便签的基本信息 JSONObject note = js.getJSONObject(GTaskStringUtils.META_HEAD_NOTE); + // 2. 提取本地便签的明细内容(比如正文) JSONArray dataArray = js.getJSONArray(GTaskStringUtils.META_HEAD_DATA); + // 3. 如果不是普通便签类型,打印错误日志并返回 if (note.getInt(NoteColumns.TYPE) != Notes.TYPE_NOTE) { Log.e(TAG, "invalid type"); return; } + // 4. 遍历明细内容,提取普通便签的正文作为任务名称 for (int i = 0; i < dataArray.length(); i++) { JSONObject data = dataArray.getJSONObject(i); + // 判断是否是普通便签的明细内容 if (TextUtils.equals(data.getString(DataColumns.MIME_TYPE), DataConstants.NOTE)) { - setName(data.getString(DataColumns.CONTENT)); - break; + setName(data.getString(DataColumns.CONTENT)); // 把便签正文设为任务名称 + break; // 找到后就停止遍历 } } } catch (JSONException e) { + // JSON解析出错,打印错误日志 Log.e(TAG, e.toString()); e.printStackTrace(); } } + /** + * 把当前任务数据打包成本地便签的JSON格式 + * 通俗说:把云端任务的信息整理成本地便签能识别的JSON格式,方便保存到本地数据库 + * @return 本地便签格式的JSON数据包(打包失败返回null) + */ public JSONObject getLocalJSONFromContent() { - String name = getName(); + String name = getName(); // 获取任务名称(对应便签内容) try { + // 情况1:没有本地元信息(云端新建的任务,要同步到本地) if (mMetaInfo == null) { - // new task created from web + // 如果任务名称为空,打印警告日志并返回null if (name == null) { Log.w(TAG, "the note seems to be an empty one"); return null; } + // 创建本地便签的JSON数据包 JSONObject js = new JSONObject(); - JSONObject note = new JSONObject(); - JSONArray dataArray = new JSONArray(); - JSONObject data = new JSONObject(); - data.put(DataColumns.CONTENT, name); - dataArray.put(data); - js.put(GTaskStringUtils.META_HEAD_DATA, dataArray); - note.put(NoteColumns.TYPE, Notes.TYPE_NOTE); - js.put(GTaskStringUtils.META_HEAD_NOTE, note); - return js; - } else { - // synced task + JSONObject note = new JSONObject(); // 便签基本信息 + JSONArray dataArray = new JSONArray(); // 便签明细内容 + JSONObject data = new JSONObject(); // 明细内容(正文) + data.put(DataColumns.CONTENT, name); // 把任务名称设为便签正文 + dataArray.put(data); // 把正文加入明细数组 + js.put(GTaskStringUtils.META_HEAD_DATA, dataArray); // 存入明细内容 + note.put(NoteColumns.TYPE, Notes.TYPE_NOTE); // 标记为普通便签类型 + js.put(GTaskStringUtils.META_HEAD_NOTE, note); // 存入便签基本信息 + return js; // 返回打包好的本地便签JSON + } + // 情况2:有本地元信息(已同步过的任务,更新本地内容) + else { + // 提取已有的本地便签元信息 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)) { - data.put(DataColumns.CONTENT, getName()); - break; + data.put(DataColumns.CONTENT, getName()); // 更新便签正文 + break; // 找到后停止遍历 } } - note.put(NoteColumns.TYPE, Notes.TYPE_NOTE); - return mMetaInfo; + note.put(NoteColumns.TYPE, Notes.TYPE_NOTE); // 确保是普通便签类型 + return mMetaInfo; // 返回更新后的本地元信息 } } catch (JSONException e) { + // JSON打包出错,打印错误日志 Log.e(TAG, e.toString()); e.printStackTrace(); return null; } } + /** + * 设置本地便签的元信息 + * 通俗说:把MetaData里的本地便签信息,转换成JSON格式存到当前任务对象 + * @param metaData 本地便签的元数据对象 + */ public void setMetaInfo(MetaData metaData) { + // 如果元数据不为空,且包含便签信息 if (metaData != null && metaData.getNotes() != null) { try { + // 把元数据里的便签信息转换成JSON对象,存入mMetaInfo mMetaInfo = new JSONObject(metaData.getNotes()); } catch (JSONException e) { + // 转换失败,打印警告日志,清空元信息 Log.w(TAG, e.toString()); mMetaInfo = null; } } } + /** + * 对比本地和云端数据,判断该执行哪种同步操作 + * 通俗说:查本地数据库,对比便签和云端任务的状态,确定是上传、下载还是冲突 + * @param c 本地数据库查询结果(存着本地便签的信息) + * @return 同步操作类型(对应Node类里的SYNC_ACTION_XXX常量) + */ public int getSyncAction(Cursor c) { try { JSONObject noteInfo = null; + // 提取本地便签的元信息(如果存在) if (mMetaInfo != null && mMetaInfo.has(GTaskStringUtils.META_HEAD_NOTE)) { noteInfo = mMetaInfo.getJSONObject(GTaskStringUtils.META_HEAD_NOTE); } + // 1. 如果本地元信息为空,说明本地便签已删除,返回“更新云端”(让云端也删除) if (noteInfo == null) { Log.w(TAG, "it seems that note meta has been deleted"); return SYNC_ACTION_UPDATE_REMOTE; } + // 2. 如果元信息里没有便签ID,说明本地便签无效,返回“更新本地”(用云端数据覆盖) if (!noteInfo.has(NoteColumns.ID)) { Log.w(TAG, "remote note id seems to be deleted"); return SYNC_ACTION_UPDATE_LOCAL; } - // validate the note id now + // 3. 验证便签ID是否匹配(本地便签ID和元信息里的ID不一致,返回“更新本地”) if (c.getLong(SqlNote.ID_COLUMN) != noteInfo.getLong(NoteColumns.ID)) { Log.w(TAG, "note id doesn't match"); return SYNC_ACTION_UPDATE_LOCAL; } + // 4. 判断本地是否有修改 if (c.getInt(SqlNote.LOCAL_MODIFIED_COLUMN) == 0) { - // there is no local update + // 本地没有修改 if (c.getLong(SqlNote.SYNC_ID_COLUMN) == getLastModified()) { - // no update both side + // 本地和云端都没有修改,返回“无需操作” return SYNC_ACTION_NONE; } else { - // apply remote to local + // 云端有修改,返回“更新本地”(用云端数据覆盖本地) return SYNC_ACTION_UPDATE_LOCAL; } } else { - // validate gtask id + // 本地有修改 + // 验证云端任务ID是否匹配(不匹配返回“同步异常”) if (!c.getString(SqlNote.GTASK_ID_COLUMN).equals(getGid())) { Log.e(TAG, "gtask id doesn't match"); return SYNC_ACTION_ERROR; } if (c.getLong(SqlNote.SYNC_ID_COLUMN) == getLastModified()) { - // local modification only + // 只有本地修改,返回“更新云端”(把本地修改同步到云端) return SYNC_ACTION_UPDATE_REMOTE; } else { + // 本地和云端都有修改,返回“同步冲突”(需要用户手动选择) return SYNC_ACTION_UPDATE_CONFLICT; } } } catch (Exception e) { + // 出现异常,打印错误日志,返回“同步异常” Log.e(TAG, e.toString()); e.printStackTrace(); } @@ -311,41 +388,79 @@ 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); + return mMetaInfo != null // 有本地元信息 + || (getName() != null && getName().trim().length() > 0) // 任务名称不为空 + || (getNotes() != null && getNotes().trim().length() > 0); // 任务备注不为空 } + /** + * 设置任务的完成状态 + * @param completed true=已完成,false=未完成 + */ 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=已完成,false=未完成 + */ public boolean getCompleted() { return this.mCompleted; } + /** + * 获取任务的备注信息 + * @return 任务备注 + */ public String getNotes() { return this.mNotes; } + /** + * 获取任务的上一个兄弟任务 + * @return 上一个兄弟任务(没有则返回null) + */ public Task getPriorSibling() { return this.mPriorSibling; } + /** + * 获取任务的所属任务列表 + * @return 所属任务列表(没有则返回null) + */ public TaskList getParent() { return this.mParent; } -} +} \ No newline at end of file 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..101266d 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,98 +30,130 @@ import org.json.JSONObject; import java.util.ArrayList; +/** + * GTask云端任务列表处理类(对应本地文件夹) + * 通俗说:这个类是用来处理和GTask云端交互的“任务列表”(对应本地的便签文件夹), + * 继承自同步基础模板Node,负责打包创建/更新云端列表的请求、解析云端/本地文件夹数据、 + * 判断同步操作类型,还负责管理列表下的子任务(对应文件夹里的普通便签) + */ public class TaskList extends Node { + // 日志标签:打印这个类的日志时,用来标识日志来自这个类 private static final String TAG = TaskList.class.getSimpleName(); + // 列表排序索引:用来确定这个任务列表在云端的显示顺序 private int mIndex; - + // 子任务列表:存储当前列表下的所有任务(对应本地文件夹里的普通便签) private ArrayList mChildren; + /** + * 构造方法:初始化任务列表的默认状态 + * 通俗说:创建任务列表对象时,设置默认值,同时调用父类的初始化方法 + */ public TaskList() { - super(); - mChildren = new ArrayList(); - mIndex = 1; + super(); // 调用父类Node的构造方法,初始化同步相关的默认属性 + mChildren = new ArrayList(); // 初始化子任务列表(空列表) + mIndex = 1; // 默认排序索引为1(控制列表在云端的显示顺序) } + /** + * 打包“创建云端任务列表”的请求参数 + * 通俗说:把本地新文件夹的信息整理成云端能识别的JSON格式,传给云端用来创建新列表 + * @param actionId 动作编号(云端用来识别这次操作的唯一标记) + * @return 整理好的创建列表请求参数(JSON格式) + */ public JSONObject getCreateAction(int actionId) { - JSONObject js = new JSONObject(); + JSONObject js = new JSONObject(); // 创建空的JSON数据包 try { - // action_type + // 1. 设置动作类型:告诉云端这次是“创建任务列表” js.put(GTaskStringUtils.GTASK_JSON_ACTION_TYPE, GTaskStringUtils.GTASK_JSON_ACTION_TYPE_CREATE); - // action_id + // 2. 设置动作ID:给这次创建操作一个唯一编号,方便云端识别 js.put(GTaskStringUtils.GTASK_JSON_ACTION_ID, actionId); - // index + // 3. 设置列表排序索引:告诉云端这个新列表在云端的显示顺序 js.put(GTaskStringUtils.GTASK_JSON_INDEX, mIndex); - // entity_delta + // 4. 设置列表核心信息:打包列表的名称、创建者等信息 JSONObject entity = new JSONObject(); - entity.put(GTaskStringUtils.GTASK_JSON_NAME, getName()); - entity.put(GTaskStringUtils.GTASK_JSON_CREATOR_ID, "null"); + entity.put(GTaskStringUtils.GTASK_JSON_NAME, getName()); // 列表名称(对应本地文件夹名称) + entity.put(GTaskStringUtils.GTASK_JSON_CREATOR_ID, "null"); // 创建者ID(这里默认传null) entity.put(GTaskStringUtils.GTASK_JSON_ENTITY_TYPE, - GTaskStringUtils.GTASK_JSON_TYPE_GROUP); - js.put(GTaskStringUtils.GTASK_JSON_ENTITY_DELTA, entity); + GTaskStringUtils.GTASK_JSON_TYPE_GROUP); // 实体类型:告诉云端这是一个“任务列表”(文件夹) + js.put(GTaskStringUtils.GTASK_JSON_ENTITY_DELTA, entity); // 把列表核心信息存入总JSON } catch (JSONException e) { + // JSON打包出错,打印错误日志并抛出异常 Log.e(TAG, e.toString()); e.printStackTrace(); throw new ActionFailureException("fail to generate tasklist-create jsonobject"); } - return js; + return js; // 返回打包好的创建请求参数 } + /** + * 打包“更新云端任务列表”的请求参数 + * 通俗说:把本地修改后的文件夹信息整理成云端能识别的JSON格式,传给云端用来更新已有列表 + * @param actionId 动作编号(云端用来识别这次操作的唯一标记) + * @return 整理好的更新列表请求参数(JSON格式) + */ public JSONObject getUpdateAction(int actionId) { - JSONObject js = new JSONObject(); + JSONObject js = new JSONObject(); // 创建空的JSON数据包 try { - // action_type + // 1. 设置动作类型:告诉云端这次是“更新任务列表” js.put(GTaskStringUtils.GTASK_JSON_ACTION_TYPE, GTaskStringUtils.GTASK_JSON_ACTION_TYPE_UPDATE); - // action_id + // 2. 设置动作ID:给这次更新操作一个唯一编号,方便云端识别 js.put(GTaskStringUtils.GTASK_JSON_ACTION_ID, actionId); - // id + // 3. 设置列表ID:告诉云端要更新哪个列表(用云端给的唯一ID) js.put(GTaskStringUtils.GTASK_JSON_ID, getGid()); - // entity_delta + // 4. 设置要更新的列表信息:打包修改后的名称、删除状态 JSONObject entity = new JSONObject(); - entity.put(GTaskStringUtils.GTASK_JSON_NAME, getName()); - entity.put(GTaskStringUtils.GTASK_JSON_DELETED, getDeleted()); - js.put(GTaskStringUtils.GTASK_JSON_ENTITY_DELTA, entity); + entity.put(GTaskStringUtils.GTASK_JSON_NAME, getName()); // 更新后的列表名称 + entity.put(GTaskStringUtils.GTASK_JSON_DELETED, getDeleted()); // 列表是否被删除(标记云端是否要删除该列表) + js.put(GTaskStringUtils.GTASK_JSON_ENTITY_DELTA, entity); // 把更新信息存入总JSON } catch (JSONException e) { + // JSON打包出错,打印错误日志并抛出异常 Log.e(TAG, e.toString()); e.printStackTrace(); throw new ActionFailureException("fail to generate tasklist-update jsonobject"); } - return js; + return js; // 返回打包好的更新请求参数 } + /** + * 解析云端返回的列表数据,赋值给当前列表对象 + * 通俗说:从云端获取到列表信息后,把数据拆解开,存到当前对象的属性里(同步到本地) + * @param js 云端返回的列表数据包(JSON格式) + */ public void setContentByRemoteJSON(JSONObject js) { - if (js != null) { + if (js != null) { // 如果云端返回的数据不为空 try { - // id + // 1. 提取列表ID(云端给的唯一编号),设置到当前对象 if (js.has(GTaskStringUtils.GTASK_JSON_ID)) { setGid(js.getString(GTaskStringUtils.GTASK_JSON_ID)); } - // last_modified + // 2. 提取列表最后修改时间,设置到当前对象 if (js.has(GTaskStringUtils.GTASK_JSON_LAST_MODIFIED)) { setLastModified(js.getLong(GTaskStringUtils.GTASK_JSON_LAST_MODIFIED)); } - // name + // 3. 提取列表名称,设置到当前对象 if (js.has(GTaskStringUtils.GTASK_JSON_NAME)) { setName(js.getString(GTaskStringUtils.GTASK_JSON_NAME)); } } catch (JSONException e) { + // JSON解析出错,打印错误日志并抛出异常 Log.e(TAG, e.toString()); e.printStackTrace(); throw new ActionFailureException("fail to get tasklist content from jsonobject"); @@ -129,86 +161,128 @@ public class TaskList extends Node { } } + /** + * 解析本地文件夹的JSON数据,设置列表内容 + * 通俗说:把本地文件夹的JSON数据拆解开,提取文件夹名称,设置为云端列表名称(加专属前缀) + * @param js 本地文件夹的JSON数据包 + */ public void setContentByLocalJSON(JSONObject js) { + // 如果JSON数据为空,或者缺少文件夹信息,打印警告日志 if (js == null || !js.has(GTaskStringUtils.META_HEAD_NOTE)) { Log.w(TAG, "setContentByLocalJSON: nothing is avaiable"); } try { + // 1. 提取本地文件夹的基本信息 JSONObject folder = js.getJSONObject(GTaskStringUtils.META_HEAD_NOTE); + // 2. 如果是普通文件夹(用户自己创建的文件夹) if (folder.getInt(NoteColumns.TYPE) == Notes.TYPE_FOLDER) { + // 提取文件夹名称,加上MIUI专属前缀(区分云端其他列表) String name = folder.getString(NoteColumns.SNIPPET); setName(GTaskStringUtils.MIUI_FOLDER_PREFFIX + name); - } else if (folder.getInt(NoteColumns.TYPE) == Notes.TYPE_SYSTEM) { + } + // 3. 如果是系统文件夹(自带的根目录/通话记录文件夹) + else if (folder.getInt(NoteColumns.TYPE) == Notes.TYPE_SYSTEM) { + // 根目录文件夹:设置默认名称(加前缀) 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) setName(GTaskStringUtils.MIUI_FOLDER_PREFFIX + GTaskStringUtils.FOLDER_CALL_NOTE); + // 其他系统文件夹:打印错误日志 else Log.e(TAG, "invalid system folder"); - } else { + } + // 4. 无效类型:打印错误日志 + else { Log.e(TAG, "error type"); } } catch (JSONException e) { + // JSON解析出错,打印错误日志 Log.e(TAG, e.toString()); e.printStackTrace(); } } + /** + * 把当前列表数据打包成本地文件夹的JSON格式 + * 通俗说:把云端列表的信息整理成本地文件夹能识别的JSON格式,方便保存到本地数据库 + * @return 本地文件夹格式的JSON数据包(打包失败返回null) + */ public JSONObject getLocalJSONFromContent() { try { + // 创建本地文件夹的JSON数据包 JSONObject js = new JSONObject(); - JSONObject folder = new JSONObject(); + JSONObject folder = new JSONObject(); // 文件夹基本信息 + // 1. 提取云端列表名称,去掉MIUI专属前缀(还原成本地文件夹名称) String folderName = getName(); if (getName().startsWith(GTaskStringUtils.MIUI_FOLDER_PREFFIX)) folderName = folderName.substring(GTaskStringUtils.MIUI_FOLDER_PREFFIX.length(), folderName.length()); + + // 2. 设置文件夹名称(摘要) folder.put(NoteColumns.SNIPPET, folderName); + + // 3. 判断文件夹类型(系统文件夹/普通文件夹) if (folderName.equals(GTaskStringUtils.FOLDER_DEFAULT) - || folderName.equals(GTaskStringUtils.FOLDER_CALL_NOTE)) + || folderName.equals(GTaskStringUtils.FOLDER_CALL_NOTE)) { + // 根目录/通话记录文件夹:标记为系统文件夹 folder.put(NoteColumns.TYPE, Notes.TYPE_SYSTEM); - else + } else { + // 其他文件夹:标记为普通文件夹 folder.put(NoteColumns.TYPE, Notes.TYPE_FOLDER); + } + // 4. 把文件夹信息存入总JSON js.put(GTaskStringUtils.META_HEAD_NOTE, folder); - return js; + return js; // 返回打包好的本地文件夹JSON } catch (JSONException e) { + // JSON打包出错,打印错误日志 Log.e(TAG, e.toString()); e.printStackTrace(); return null; } } + /** + * 对比本地和云端列表数据,判断该执行哪种同步操作 + * 通俗说:查本地数据库,对比文件夹和云端列表的状态,确定是上传、下载还是无需操作 + * @param c 本地数据库查询结果(存着本地文件夹的信息) + * @return 同步操作类型(对应Node类里的SYNC_ACTION_XXX常量) + */ public int getSyncAction(Cursor c) { try { + // 1. 判断本地文件夹是否有修改 if (c.getInt(SqlNote.LOCAL_MODIFIED_COLUMN) == 0) { - // there is no local update + // 本地没有修改 if (c.getLong(SqlNote.SYNC_ID_COLUMN) == getLastModified()) { - // no update both side + // 本地和云端都没有修改,返回“无需操作” return SYNC_ACTION_NONE; } else { - // apply remote to local + // 云端有修改,返回“更新本地”(用云端列表数据覆盖本地文件夹) return SYNC_ACTION_UPDATE_LOCAL; } } else { - // validate gtask id + // 本地有修改 + // 2. 验证云端列表ID是否匹配(不匹配返回“同步异常”) if (!c.getString(SqlNote.GTASK_ID_COLUMN).equals(getGid())) { Log.e(TAG, "gtask id doesn't match"); return SYNC_ACTION_ERROR; } if (c.getLong(SqlNote.SYNC_ID_COLUMN) == getLastModified()) { - // local modification only + // 只有本地修改,返回“更新云端”(把本地文件夹修改同步到云端) return SYNC_ACTION_UPDATE_REMOTE; } else { - // for folder conflicts, just apply local modification + // 本地和云端都有修改,文件夹冲突时优先本地修改,返回“更新云端” return SYNC_ACTION_UPDATE_REMOTE; } } } catch (Exception e) { + // 出现异常,打印错误日志,返回“同步异常” Log.e(TAG, e.toString()); e.printStackTrace(); } @@ -216,62 +290,89 @@ 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)) { - ret = mChildren.add(task); + ret = mChildren.add(task); // 添加任务到列表末尾 if (ret) { - // need to set prior sibling and parent + // 设置任务的上一个兄弟任务(列表最后一个任务的前一个) task.setPriorSibling(mChildren.isEmpty() ? null : mChildren .get(mChildren.size() - 1)); - task.setParent(this); + task.setParent(this); // 设置任务的所属列表(当前文件夹) } } return ret; } + /** + * 给当前列表添加子任务(指定位置添加) + * 通俗说:往文件夹的指定位置插入一个普通便签,更新该任务和前后任务的兄弟关系 + * @param task 要添加的子任务(普通便签) + * @param index 要插入的位置索引 + * @return true=添加成功,false=添加失败(索引无效/任务已存在) + */ public boolean addChildTask(Task task, int index) { + // 索引无效(小于0或大于列表长度),打印错误日志并返回false if (index < 0 || index > mChildren.size()) { Log.e(TAG, "add child task: invalid index"); return false; } + // 查找任务在列表中的位置(-1表示不存在) int pos = mChildren.indexOf(task); if (task != null && pos == -1) { - mChildren.add(index, task); + mChildren.add(index, task); // 在指定位置插入任务 - // update the task list + // 获取该任务的前一个和后一个任务 Task preTask = null; Task afterTask = null; if (index != 0) - preTask = mChildren.get(index - 1); + preTask = mChildren.get(index - 1); // 前一个任务(上一个兄弟) if (index != mChildren.size() - 1) - afterTask = mChildren.get(index + 1); + afterTask = mChildren.get(index + 1); // 后一个任务 - task.setPriorSibling(preTask); + task.setPriorSibling(preTask); // 设置当前任务的上一个兄弟 if (afterTask != null) - afterTask.setPriorSibling(task); + afterTask.setPriorSibling(task); // 更新后一个任务的上一个兄弟为当前任务 } return true; } + /** + * 从当前列表移除子任务 + * 通俗说:从文件夹里删除一个普通便签,重置该便签的父文件夹和兄弟关系,更新后续便签的兄弟关系 + * @param task 要移除的子任务(普通便签) + * @return true=移除成功,false=移除失败(任务不存在) + */ public boolean removeChildTask(Task task) { boolean ret = false; + // 查找任务在列表中的位置 int index = mChildren.indexOf(task); if (index != -1) { - ret = mChildren.remove(task); + ret = mChildren.remove(task); // 移除该任务 if (ret) { - // reset prior sibling and parent - task.setPriorSibling(null); - task.setParent(null); + task.setPriorSibling(null); // 重置该任务的上一个兄弟关系 + task.setParent(null); // 重置该任务的所属列表 - // update the task list + // 如果移除的不是最后一个任务,更新后续任务的兄弟关系 if (index != mChildren.size()) { mChildren.get(index).setPriorSibling( index == 0 ? null : mChildren.get(index - 1)); @@ -281,24 +382,40 @@ public class TaskList extends Node { return ret; } + /** + * 移动子任务在列表中的位置 + * 通俗说:调整文件夹里普通便签的显示顺序,先移除再添加到指定位置 + * @param task 要移动的子任务(普通便签) + * @param index 要移动到的目标位置 + * @return true=移动成功,false=移动失败(索引无效/任务不存在/位置未变) + */ public boolean moveChildTask(Task task, int index) { - + // 索引无效(小于0或大于等于列表长度),打印错误日志并返回false if (index < 0 || index >= mChildren.size()) { Log.e(TAG, "move child task: invalid index"); return false; } + // 查找任务当前位置(-1表示不存在) int pos = mChildren.indexOf(task); if (pos == -1) { Log.e(TAG, "move child task: the task should in the list"); return false; } + // 如果当前位置和目标位置一致,无需移动,返回true if (pos == index) return true; + // 先移除任务,再添加到目标位置,返回操作结果 return (removeChildTask(task) && addChildTask(task, index)); } + /** + * 通过云端ID查找列表下的子任务 + * 通俗说:根据便签对应的云端唯一ID,在文件夹里找到对应的普通便签 + * @param gid 云端唯一ID + * @return 找到的子任务(普通便签),找不到返回null + */ public Task findChildTaskByGid(String gid) { for (int i = 0; i < mChildren.size(); i++) { Task t = mChildren.get(i); @@ -309,11 +426,24 @@ public class TaskList extends Node { return null; } + /** + * 获取子任务在列表中的位置索引 + * 通俗说:查找某个普通便签在文件夹里的排序位置 + * @param task 要查找的子任务(普通便签) + * @return 任务的位置索引(不存在返回-1) + */ public int getChildTaskIndex(Task task) { return mChildren.indexOf(task); } + /** + * 通过位置索引获取子任务 + * 通俗说:根据排序位置,获取文件夹里对应的普通便签 + * @param index 位置索引 + * @return 对应的子任务(普通便签),索引无效返回null + */ public Task getChildTaskByIndex(int index) { + // 索引无效,打印错误日志并返回null if (index < 0 || index >= mChildren.size()) { Log.e(TAG, "getTaskByIndex: invalid index"); return null; @@ -321,6 +451,12 @@ public class TaskList extends Node { return mChildren.get(index); } + /** + * 通过云端ID查找列表下的子任务(和findChildTaskByGid功能一致) + * 通俗说:根据便签对应的云端唯一ID,在文件夹里找到对应的普通便签 + * @param gid 云端唯一ID + * @return 找到的子任务(普通便签),找不到返回null + */ public Task getChilTaskByGid(String gid) { for (Task task : mChildren) { if (task.getGid().equals(gid)) @@ -329,15 +465,30 @@ 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; } -} +} \ No newline at end of file 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..f455c7a 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 @@ -1,28 +1,27 @@ /* - * Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net) - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * Description:支持小米便签运行过程中的运行异常处理。 */ package net.micode.notes.gtask.exception; public class ActionFailureException extends RuntimeException { private static final long serialVersionUID = 4425249765923293627L; + /* + * serialVersionUID相当于java类的身份证。主要用于版本控制。 + * serialVersionUID作用是序列化时保持版本的兼容性,即在版本升级时反序列化仍保持对象的唯一性。 + * Made By Cuican + */ public ActionFailureException() { super(); } - + /* + * 在JAVA类中使用super来引用父类的成分,用this来引用当前对象. + * 如果一个类从另外一个类继承,我们new这个子类的实例对象的时候,这个子类对象里面会有一个父类对象。 + * 怎么去引用里面的父类对象呢?使用super来引用 + * 也就是说,此处super()以及super (paramString)可认为是Exception ()和Exception (paramString) + * Made By Cuican + */ public ActionFailureException(String paramString) { super(paramString); } @@ -30,4 +29,4 @@ public class ActionFailureException extends RuntimeException { public ActionFailureException(String paramString, Throwable paramThrowable) { super(paramString, paramThrowable); } -} +} \ No newline at end of file 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..63a1adb 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 @@ -1,28 +1,28 @@ /* - * Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net) - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * Description:支持小米便签运行过程中的网络异常处理。 */ package net.micode.notes.gtask.exception; public class NetworkFailureException extends Exception { private static final long serialVersionUID = 2107610287180234136L; + /* + * serialVersionUID相当于java类的身份证。主要用于版本控制。 + * serialVersionUID作用是序列化时保持版本的兼容性,即在版本升级时反序列化仍保持对象的唯一性。 + * Made By Cuican + */ public NetworkFailureException() { super(); } + /* + * 在JAVA类中使用super来引用父类的成分,用this来引用当前对象. + * 如果一个类从另外一个类继承,我们new这个子类的实例对象的时候,这个子类对象里面会有一个父类对象。 + * 怎么去引用里面的父类对象呢?使用super来引用 + * 也就是说,此处super()以及super (paramString)可认为是Exception ()和Exception (paramString) + * Made By Cuican + */ public NetworkFailureException(String paramString) { super(paramString); } @@ -30,4 +30,4 @@ public class NetworkFailureException extends Exception { public NetworkFailureException(String paramString, Throwable paramThrowable) { super(paramString, paramThrowable); } -} +} \ No newline at end of file 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..b99f6c2 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 @@ -1,4 +1,3 @@ - /* * Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net) * @@ -17,107 +16,264 @@ package net.micode.notes.gtask.remote; -import android.app.Notification; import android.app.NotificationManager; import android.app.PendingIntent; import android.content.Context; import android.content.Intent; import android.os.AsyncTask; +import android.os.Build; +// 导入弱引用工具:防止因一直持有页面/服务导致的内存浪费(通俗说就是不占无用内存) +import java.lang.ref.WeakReference; + +// 导入兼容版通知工具:让老安卓手机和新安卓手机都能正常显示通知 +import androidx.core.app.NotificationCompat; +// 导入通知权限检查工具:用来判断用户有没有开启这个APP的通知权限 +import androidx.core.app.NotificationManagerCompat; +// 导入APP资源:获取APP里的文字、图标等内容 import net.micode.notes.R; +// 导入便签列表页面:通知点击后跳转到这个页面 import net.micode.notes.ui.NotesListActivity; +// 导入便签设置页面:同步失败时,通知点击后跳转到这个页面 import net.micode.notes.ui.NotesPreferenceActivity; - +/** + * 这个类是用来在后台同步GTask和本地便签的(不会卡住手机界面) + * 注意:类名里的"ASync"写错了,应该是"Async",而且类名要和文件名一模一样 + */ +// 类名和文件名保持一致:GTaskASyncTask(建议改成GTaskAsyncTask更规范) public class GTaskASyncTask extends AsyncTask { - private static int GTASK_SYNC_NOTIFICATION_ID = 5234235; + // 同步通知的唯一编号:用来区分这个同步通知和其他通知,不会弄混 + private static final int GTASK_SYNC_NOTIFICATION_ID = 5234235; + // 通知分类ID(安卓8.0以上必须有):给同步通知单独分个类 + private static final String GTASK_SYNC_CHANNEL_ID = "gtask_sync_channel"; + // 通知分类名称(安卓8.0以上必须有):在手机设置里能看到这个分类的名字 + private static final String GTASK_SYNC_CHANNEL_NAME = "GTask同步通知"; + /** + * 任务完成后的回调接口 + * 通俗说:同步任务不管成功、失败还是取消,都会告诉外部“我做完了” + */ public interface OnCompleteListener { + /** + * 任务完成后会调用这个方法 + */ void onComplete(); } - private Context mContext; - - private NotificationManager mNotifiManager; - - private GTaskManager mTaskManager; - - private OnCompleteListener mOnCompleteListener; + // 用弱引用存上下文(页面/服务信息):页面关闭后,这个引用不会占着内存不放 + private final WeakReference mContextRef; + // 用弱引用存回调监听器:外部页面关闭后,这个监听器不会浪费内存 + private final WeakReference mListenerRef; + // 通知管理器:用来发送、关闭手机通知的工具 + private NotificationManager mNotificationManager; + // GTask管理工具:专门处理GTask同步逻辑的单例(整个APP只有这一个实例) + private final GTaskManager mTaskManager; + /** + * 构造方法:初始化这个同步任务 + * @param context 页面/服务的上下文(用来获取图标、跳转页面等) + * @param listener 任务完成后的回调(告诉外部任务做完了) + */ public GTaskASyncTask(Context context, OnCompleteListener listener) { - mContext = context; - mOnCompleteListener = listener; - mNotifiManager = (NotificationManager) mContext - .getSystemService(Context.NOTIFICATION_SERVICE); + // 把上下文存成弱引用,防止内存浪费 + mContextRef = new WeakReference<>(context); + // 把回调监听器存成弱引用,防止内存浪费 + mListenerRef = new WeakReference<>(listener); + // 先获取上下文(弱引用要先取出来才能用,还要判断不为空) + Context ctx = mContextRef.get(); + if (ctx != null) { + // 获取手机的通知管理服务(用来发通知的) + mNotificationManager = (NotificationManager) ctx.getSystemService(Context.NOTIFICATION_SERVICE); + // 创建通知分类 + createNotificationChannel(); + } + // 获取GTask管理工具的实例 mTaskManager = GTaskManager.getInstance(); } + /** + * 取消同步任务 + * 通俗说:不想同步了,调用这个方法就能停止 + */ public void cancelSync() { mTaskManager.cancelSync(); } - public void publishProgess(String message) { - publishProgress(new String[] { - message - }); + /** + * 发送同步进度信息 + * 通俗说:把后台同步的进度(比如“正在登录账号”)告诉主线程,用来显示通知 + * @param message 进度提示文字(比如“正在同步第2条便签”) + */ + public void publishProgress(String message) { + // 调用系统方法,把进度信息传出去 + publishProgress(new String[]{message}); + } + + /** + * 创建通知分类(安卓8.0以上必须有) + * 通俗说:给同步通知单独建个分类,用户可以在手机设置里单独开关这个分类的通知 + */ + private void createNotificationChannel() { + // 先获取上下文 + Context ctx = mContextRef.get(); + // 上下文为空,或者手机系统低于安卓8.0,就不用创建分类 + if (ctx == null || Build.VERSION.SDK_INT < Build.VERSION_CODES.O) { + return; + } + // 构建通知分类 + android.app.NotificationChannel channel = new android.app.NotificationChannel( + GTASK_SYNC_CHANNEL_ID, // 分类ID + GTASK_SYNC_CHANNEL_NAME, // 分类名称 + NotificationManager.IMPORTANCE_DEFAULT // 通知重要性:默认级别(有提示音,不震动) + ); + // 开启通知指示灯(如果手机有指示灯的话) + channel.enableLights(true); + // 不在手机桌面APP图标上显示角标 + channel.setShowBadge(false); + // 把分类注册到手机系统里 + if (mNotificationManager != null) { + mNotificationManager.createNotificationChannel(channel); + } } + /** + * 显示同步通知 + * 通俗说:根据同步状态(正在同步/成功/失败),在手机通知栏显示对应的提示 + * @param tickerId 通知顶部一闪而过的文字ID(比如“正在同步GTask”) + * @param content 通知正文文字(比如“同步成功:共3条便签”) + */ 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; + // 先获取上下文 + Context ctx = mContextRef.get(); + // 上下文或通知管理器为空,就不显示通知 + if (ctx == null || mNotificationManager == null) { + return; + } + + // 安卓13以上要检查通知权限:用户没开权限,就不显示通知 + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + if (!NotificationManagerCompat.from(ctx).areNotificationsEnabled()) { + // 这里可以提示用户开权限,现在先直接返回 + return; + } + } + + // 构建通知点击后的跳转意图 PendingIntent pendingIntent; + Intent intent; + // 同步成功就跳转到便签列表,失败/取消就跳转到便签设置 if (tickerId != R.string.ticker_success) { - pendingIntent = PendingIntent.getActivity(mContext, 0, new Intent(mContext, - NotesPreferenceActivity.class), 0); - + intent = new Intent(ctx, NotesPreferenceActivity.class); // 跳设置页面 } else { - pendingIntent = PendingIntent.getActivity(mContext, 0, new Intent(mContext, - NotesListActivity.class), 0); + intent = new Intent(ctx, NotesListActivity.class); // 跳便签列表 } - notification.setLatestEventInfo(mContext, mContext.getString(R.string.app_name), content, - pendingIntent); - mNotifiManager.notify(GTASK_SYNC_NOTIFICATION_ID, notification); + + // 配置跳转意图的标志:更新已有意图,防止重复创建 + int flags = PendingIntent.FLAG_UPDATE_CURRENT; + // 安卓6.0以上添加不可变标志,防止报错 + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + flags |= PendingIntent.FLAG_IMMUTABLE; + } + // 创建跳转意图 + pendingIntent = PendingIntent.getActivity(ctx, 0, intent, flags); + + // 构建通知内容 + NotificationCompat.Builder builder = new NotificationCompat.Builder(ctx, GTASK_SYNC_CHANNEL_ID) + .setSmallIcon(R.drawable.notification) // 通知小图标(必须设置,不然不显示) + .setContentTitle(ctx.getString(R.string.app_name)) // 通知标题(显示APP名字) + .setContentText(content) // 通知正文 + .setTicker(ctx.getString(tickerId)) // 通知顶部一闪而过的文字(老手机有效) + .setWhen(System.currentTimeMillis()) // 通知创建时间 + .setDefaults(NotificationCompat.DEFAULT_LIGHTS) // 开启默认指示灯 + .setAutoCancel(true) // 点击通知后,通知自动消失 + .setContentIntent(pendingIntent); // 通知点击后跳转到指定页面 + + // 显示通知 + mNotificationManager.notify(GTASK_SYNC_NOTIFICATION_ID, builder.build()); } + /** + * 后台执行同步任务(运行在子线程,不会卡界面) + * @param unused 没有参数 + * @return 同步结果(成功/网络错误/内部错误/取消) + */ @Override protected Integer doInBackground(Void... unused) { - publishProgess(mContext.getString(R.string.sync_progress_login, NotesPreferenceActivity - .getSyncAccountName(mContext))); - return mTaskManager.sync(mContext, this); + // 获取上下文 + Context ctx = mContextRef.get(); + if (ctx != null) { + // 发送进度:提示正在登录同步账号 + publishProgress(ctx.getString(R.string.sync_progress_login, + NotesPreferenceActivity.getSyncAccountName(ctx))); + } + // 执行同步逻辑,返回同步结果 + return mTaskManager.sync(ctx, this); } + /** + * 进度更新回调(运行在主线程,可以更新界面/显示通知) + * @param progress 进度信息 + */ @Override protected void onProgressUpdate(String... progress) { + // 显示“正在同步”的通知 showNotification(R.string.ticker_syncing, progress[0]); - if (mContext instanceof GTaskSyncService) { - ((GTaskSyncService) mContext).sendBroadcast(progress[0]); + // 获取上下文 + Context ctx = mContextRef.get(); + // 如果上下文是GTask同步服务,就发送广播告诉服务当前进度 + if (ctx instanceof GTaskSyncService) { + ((GTaskSyncService) ctx).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)); + // 获取上下文 + Context ctx = mContextRef.get(); + if (ctx != null) { + // 根据同步结果显示对应通知 + if (result == GTaskManager.STATE_SUCCESS) { + // 同步成功:显示成功通知,记录最后同步时间 + showNotification(R.string.ticker_success, ctx.getString( + R.string.success_sync_account, mTaskManager.getSyncAccount())); + NotesPreferenceActivity.setLastSyncTime(ctx, System.currentTimeMillis()); + } else if (result == GTaskManager.STATE_NETWORK_ERROR) { + // 网络错误:显示网络异常通知 + showNotification(R.string.ticker_fail, ctx.getString(R.string.error_sync_network)); + } else if (result == GTaskManager.STATE_INTERNAL_ERROR) { + // 内部错误:显示内部异常通知 + showNotification(R.string.ticker_fail, ctx.getString(R.string.error_sync_internal)); + } else if (result == GTaskManager.STATE_SYNC_CANCELLED) { + // 同步取消:显示取消通知 + showNotification(R.string.ticker_cancel, ctx.getString(R.string.error_sync_cancelled)); + } + } + + // 获取回调监听器,告诉外部任务完成了 + OnCompleteListener listener = mListenerRef.get(); + if (listener != null) { + listener.onComplete(); } - if (mOnCompleteListener != null) { - new Thread(new Runnable() { + } - public void run() { - mOnCompleteListener.onComplete(); - } - }).start(); + /** + * 任务被取消时的回调 + * 通俗说:调用cancel()方法取消任务后,会执行这个方法 + * @param result 同步结果 + */ + @Override + protected void onCancelled(Integer result) { + super.onCancelled(result); + // 告诉外部任务完成了 + OnCompleteListener listener = mListenerRef.get(); + if (listener != null) { + listener.onComplete(); } } -} +} \ No newline at end of file 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..bb03c97 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 @@ -16,190 +16,248 @@ package net.micode.notes.gtask.remote; -import android.accounts.Account; -import android.accounts.AccountManager; -import android.accounts.AccountManagerFuture; -import android.app.Activity; -import android.os.Bundle; -import android.text.TextUtils; -import android.util.Log; - -import net.micode.notes.gtask.data.Node; -import net.micode.notes.gtask.data.Task; -import net.micode.notes.gtask.data.TaskList; -import net.micode.notes.gtask.exception.ActionFailureException; -import net.micode.notes.gtask.exception.NetworkFailureException; -import net.micode.notes.tool.GTaskStringUtils; -import net.micode.notes.ui.NotesPreferenceActivity; - -import org.apache.http.HttpEntity; -import org.apache.http.HttpResponse; -import org.apache.http.client.ClientProtocolException; -import org.apache.http.client.entity.UrlEncodedFormEntity; -import org.apache.http.client.methods.HttpGet; -import org.apache.http.client.methods.HttpPost; -import org.apache.http.cookie.Cookie; -import org.apache.http.impl.client.BasicCookieStore; -import org.apache.http.impl.client.DefaultHttpClient; -import org.apache.http.message.BasicNameValuePair; -import org.apache.http.params.BasicHttpParams; -import org.apache.http.params.HttpConnectionParams; -import org.apache.http.params.HttpParams; -import org.apache.http.params.HttpProtocolParams; -import org.json.JSONArray; -import org.json.JSONException; -import org.json.JSONObject; - -import java.io.BufferedReader; -import java.io.IOException; -import java.io.InputStream; -import java.io.InputStreamReader; -import java.util.LinkedList; -import java.util.List; -import java.util.zip.GZIPInputStream; -import java.util.zip.Inflater; -import java.util.zip.InflaterInputStream; - - +import android.accounts.Account; // 谷歌账号相关:存储账号名称、类型等信息 +import android.accounts.AccountManager; // 账号管理器:获取手机里的谷歌账号、登录凭证等 +import android.accounts.AccountManagerFuture; // 账号操作结果:异步获取账号登录凭证的返回值 +import android.app.Activity; // 页面相关:用来关联登录的页面 +import android.os.Bundle; // 数据容器:用来存放账号登录凭证等数据 +import android.text.TextUtils; // 文本工具:判断字符串是否为空、去除空格等 +import android.util.Log; // 日志工具:打印调试信息,方便查找问题 + +import net.micode.notes.gtask.data.Node; // 基础数据节点:任务/任务列表的通用数据模型 +import net.micode.notes.gtask.data.Task; // 任务数据模型:存储单个GTask任务的信息 +import net.micode.notes.gtask.data.TaskList; // 任务列表数据模型:存储GTask任务列表的信息 +import net.micode.notes.gtask.exception.ActionFailureException; // 操作失败异常:创建/删除任务等操作失败时抛出 +import net.micode.notes.gtask.exception.NetworkFailureException; // 网络失败异常:网络不通或请求失败时抛出 +import net.micode.notes.tool.GTaskStringUtils; // 字符串工具:存储GTask接口用到的固定字符串(比如接口参数名) +import net.micode.notes.ui.NotesPreferenceActivity; // 便签设置页面:获取用户设置的同步账号名称 + +import org.apache.http.HttpEntity; // 网络请求实体:存储网络请求/响应的内容 +import org.apache.http.HttpResponse; // 网络响应:存储服务器返回的所有数据(状态码、内容等) +import org.apache.http.client.ClientProtocolException; // 客户端协议异常:网络请求格式错误时抛出 +import org.apache.http.client.entity.UrlEncodedFormEntity; // 表单实体:把请求参数封装成表单格式 +import org.apache.http.client.methods.HttpGet; // GET请求:向服务器获取数据的请求方式 +import org.apache.http.client.methods.HttpPost; // POST请求:向服务器提交数据的请求方式 +import org.apache.http.cookie.Cookie; // 登录凭证:用来保持登录状态,不用每次都输账号密码 +import org.apache.http.impl.client.BasicCookieStore; // Cookie容器:存储登录后的Cookie信息 +import org.apache.http.impl.client.DefaultHttpClient; // 网络请求客户端:发送GET/POST请求的工具 +import org.apache.http.message.BasicNameValuePair; // 键值对:存储请求参数(比如"key=value") +import org.apache.http.params.BasicHttpParams; // 网络参数容器:存储网络请求的配置(比如超时时间) +import org.apache.http.params.HttpConnectionParams; // 连接参数:设置网络连接的超时时间 +import org.apache.http.params.HttpParams; // 网络参数:通用的网络配置参数 +import org.apache.http.params.HttpProtocolParams; // 协议参数:设置网络请求的协议配置 +import org.json.JSONArray; // JSON数组:存储一组格式化数据(比如多个任务信息) +import org.json.JSONException; // JSON解析异常:数据格式错误导致解析失败时抛出 +import org.json.JSONObject; // JSON对象:存储键值对格式的数据(比如单个任务信息) + +import java.io.BufferedReader; // 缓冲读取器:高效读取服务器返回的文本内容 +import java.io.IOException; // IO异常:读取/写入数据失败时抛出 +import java.io.InputStream; // 输入流:读取服务器返回的原始数据 +import java.io.InputStreamReader; // 输入流读取器:把原始字节数据转换成文本数据 +import java.util.LinkedList; // 链表:存储请求参数的容器,有序且方便添加 +import java.util.List; // 集合:存储一组数据的通用接口 +import java.util.zip.GZIPInputStream; // GZIP解压流:解压服务器返回的GZIP格式数据 +import java.util.zip.Inflater; // 解压工具:处理DEFLATE格式的解压 +import java.util.zip.InflaterInputStream; // DEFLATE解压流:解压服务器返回的DEFLATE格式数据 + +/** + * GTask客户端工具类 + * 通俗说:这个类专门负责和谷歌任务(GTask)服务器打交道,比如登录、创建任务、获取任务列表等 + * 特点:整个APP只有这一个实例(不会创建多个重复对象),节省内存 + */ public class GTaskClient { + // 日志标签:打印日志时用来标识是这个类的日志,方便查找问题 private static final String TAG = GTaskClient.class.getSimpleName(); + // GTask基础地址:谷歌任务的根地址 private static final String GTASK_URL = "https://mail.google.com/tasks/"; + // GTask GET请求地址:用来从服务器获取数据(比如任务列表) private static final String GTASK_GET_URL = "https://mail.google.com/tasks/ig"; + // GTask POST请求地址:用来向服务器提交数据(比如创建任务、删除任务) private static final String GTASK_POST_URL = "https://mail.google.com/tasks/r/ig"; + // 本类的唯一实例:保证整个APP只有一个GTaskClient对象 private static GTaskClient mInstance = null; + // 网络请求客户端:用来发送GET/POST请求,和服务器通信 private DefaultHttpClient mHttpClient; + // 实际使用的GET请求地址:可能是默认地址,也可能是自定义域名地址 private String mGetUrl; + // 实际使用的POST请求地址:可能是默认地址,也可能是自定义域名地址 private String mPostUrl; + // 客户端版本号:和服务器交互时需要传递的版本信息,用来兼容不同版本 private long mClientVersion; + // 登录状态标记:true=已登录,false=未登录 private boolean mLoggedin; + // 上次登录时间:用来判断登录状态是否过期 private long mLastLoginTime; + // 操作ID:每次和服务器交互的操作都会分配一个唯一ID,自增累加 private int mActionId; + // 同步账号:当前用来同步GTask的谷歌账号 private Account mAccount; + // 更新数据数组:存储待提交的更新操作(比如修改任务、新增任务),批量提交更高效 private JSONArray mUpdateArray; + /** + * 私有构造方法 + * 通俗说:不让外部直接创建这个类的对象,只能通过getInstance()获取唯一实例 + */ private GTaskClient() { - mHttpClient = null; - mGetUrl = GTASK_GET_URL; - mPostUrl = GTASK_POST_URL; - mClientVersion = -1; - mLoggedin = false; - mLastLoginTime = 0; - mActionId = 1; - mAccount = null; - mUpdateArray = null; + mHttpClient = null; // 初始化网络请求客户端为null + mGetUrl = GTASK_GET_URL; // 默认使用GTask默认GET地址 + mPostUrl = GTASK_POST_URL; // 默认使用GTask默认POST地址 + mClientVersion = -1; // 客户端版本号初始化为-1(未获取) + mLoggedin = false; // 初始状态为未登录 + mLastLoginTime = 0; // 上次登录时间初始化为0 + mActionId = 1; // 操作ID从1开始自增 + mAccount = null; // 同步账号初始化为null + mUpdateArray = null; // 更新数据数组初始化为null } + /** + * 获取GTaskClient的唯一实例 + * 通俗说:整个APP只能通过这个方法拿到GTaskClient对象,保证只有一个实例 + * @return GTaskClient唯一实例 + */ public static synchronized GTaskClient getInstance() { + // 如果实例为null,就创建一个新的(懒加载:用到时才创建) if (mInstance == null) { mInstance = new GTaskClient(); } return mInstance; } + /** + * 登录GTask服务器 + * 通俗说:验证谷歌账号,获取登录凭证,保持和服务器的登录状态 + * @param 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 + // 假设登录凭证5分钟过期,过期后需要重新登录 final long interval = 1000 * 60 * 5; if (mLastLoginTime + interval < System.currentTimeMillis()) { - mLoggedin = false; + mLoggedin = false; // 登录过期,标记为未登录 } - // need to re-login after account switch + // 如果已经登录,但账号和设置里的同步账号不一致(用户切换了账号),也需要重新登录 if (mLoggedin && !TextUtils.equals(getSyncAccount().name, NotesPreferenceActivity - .getSyncAccountName(activity))) { + .getSyncAccountName(activity))) { mLoggedin = false; } + // 如果已经登录且未过期、账号未切换,直接返回登录成功 if (mLoggedin) { - Log.d(TAG, "already logged in"); + Log.d(TAG, "already logged in"); // 打印日志:已登录 return true; } + // 记录本次登录时间 mLastLoginTime = System.currentTimeMillis(); + // 获取谷歌账号的登录凭证(令牌) String authToken = loginGoogleAccount(activity, false); if (authToken == null) { - Log.e(TAG, "login google account failed"); + Log.e(TAG, "login google account failed"); // 打印日志:谷歌账号登录失败 return false; } - // login with custom domain if necessary + // 处理非gmail/googlemail域名的账号(自定义域名账号,需要切换请求地址) if (!(mAccount.name.toLowerCase().endsWith("gmail.com") || mAccount.name.toLowerCase() .endsWith("googlemail.com"))) { + // 拼接自定义域名的请求地址 StringBuilder url = new StringBuilder(GTASK_URL).append("a/"); - int index = mAccount.name.indexOf('@') + 1; + int index = mAccount.name.indexOf('@') + 1; // 找到@符号的位置,截取域名后缀 String suffix = mAccount.name.substring(index); url.append(suffix + "/"); - mGetUrl = url.toString() + "ig"; - mPostUrl = url.toString() + "r/ig"; + mGetUrl = url.toString() + "ig"; // 自定义GET地址 + mPostUrl = url.toString() + "r/ig"; // 自定义POST地址 + // 使用自定义地址尝试登录GTask if (tryToLoginGtask(activity, authToken)) { - mLoggedin = true; + mLoggedin = true; // 登录成功,标记为已登录 } } - // try to login with google official url + // 如果自定义地址登录失败,使用默认地址再次尝试登录 if (!mLoggedin) { - mGetUrl = GTASK_GET_URL; - mPostUrl = GTASK_POST_URL; + mGetUrl = GTASK_GET_URL; // 恢复默认GET地址 + mPostUrl = GTASK_POST_URL; // 恢复默认POST地址 + // 默认地址登录失败,返回false if (!tryToLoginGtask(activity, authToken)) { return false; } } + // 所有登录逻辑完成,标记为已登录 mLoggedin = true; return true; } + /** + * 获取谷歌账号的登录凭证(令牌) + * 通俗说:从手机的账号管理器中,获取指定谷歌账号的登录凭证,用来登录GTask + * @param activity 关联页面 + * @param invalidateToken 是否失效旧凭证(旧凭证过期时需要设为true) + * @return 登录凭证(令牌),null=获取失败 + */ private String loginGoogleAccount(Activity activity, boolean invalidateToken) { - String authToken; + String authToken; // 登录凭证 + // 获取手机的账号管理器 AccountManager accountManager = AccountManager.get(activity); + // 获取手机里所有的谷歌账号 Account[] accounts = accountManager.getAccountsByType("com.google"); + // 如果没有谷歌账号,返回null if (accounts.length == 0) { Log.e(TAG, "there is no available google account"); return null; } + // 获取用户在便签设置里指定的同步账号名称 String accountName = NotesPreferenceActivity.getSyncAccountName(activity); Account account = null; + // 遍历所有谷歌账号,找到和设置里一致的账号 for (Account a : accounts) { if (a.name.equals(accountName)) { account = a; break; } } + // 找到匹配的账号,保存到成员变量 if (account != null) { mAccount = account; } else { + // 没找到匹配的账号,打印日志并返回null Log.e(TAG, "unable to get an account with the same name in the settings"); return null; } - // get the token now + // 获取账号的登录凭证 AccountManagerFuture accountManagerFuture = accountManager.getAuthToken(account, "goanna_mobile", null, activity, null, null); try { + // 获取凭证返回结果 Bundle authTokenBundle = accountManagerFuture.getResult(); + // 从结果中提取登录凭证 authToken = authTokenBundle.getString(AccountManager.KEY_AUTHTOKEN); + // 如果需要失效旧凭证,先失效再重新获取 if (invalidateToken) { accountManager.invalidateAuthToken("com.google", authToken); loginGoogleAccount(activity, false); } } catch (Exception e) { + // 获取凭证失败,打印日志并设为null Log.e(TAG, "get auth token failed"); authToken = null; } @@ -207,16 +265,24 @@ public class GTaskClient { return authToken; } + /** + * 尝试登录GTask服务器 + * 通俗说:先用当前凭证登录,失败的话就失效旧凭证,重新获取凭证再登录 + * @param 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 - // token and try again + // 登录失败,说明凭证过期,重新获取凭证 authToken = loginGoogleAccount(activity, true); if (authToken == null) { Log.e(TAG, "login google account failed"); return false; } + // 用新凭证再次尝试登录,还是失败就返回false if (!loginGtask(authToken)) { Log.e(TAG, "login gtask failed"); return false; @@ -225,54 +291,76 @@ public class GTaskClient { return true; } + /** + * 实际执行GTask登录逻辑 + * 通俗说:用登录凭证发送请求,获取登录状态(Cookie)和客户端版本号 + * @param authToken 登录凭证 + * @return true=登录成功,false=登录失败 + */ private boolean loginGtask(String authToken) { + // 设置网络连接超时时间:10秒(连接不上服务器就超时) int timeoutConnection = 10000; + // 设置网络读取超时时间:15秒(服务器没返回数据就超时) int timeoutSocket = 15000; + // 创建网络参数容器 HttpParams httpParameters = new BasicHttpParams(); + // 设置连接超时时间 HttpConnectionParams.setConnectionTimeout(httpParameters, timeoutConnection); + // 设置读取超时时间 HttpConnectionParams.setSoTimeout(httpParameters, timeoutSocket); + // 初始化网络请求客户端,传入超时参数 mHttpClient = new DefaultHttpClient(httpParameters); + // 创建Cookie容器,存储登录后的凭证 BasicCookieStore localBasicCookieStore = new BasicCookieStore(); + // 给网络客户端设置Cookie容器 mHttpClient.setCookieStore(localBasicCookieStore); + // 关闭Expect-Continue协议,避免部分服务器不兼容 HttpProtocolParams.setUseExpectContinue(mHttpClient.getParams(), false); - // login gtask + // 执行GTask登录请求 try { + // 拼接登录请求地址(带登录凭证) String loginUrl = mGetUrl + "?auth=" + authToken; + // 创建GET请求 HttpGet httpGet = new HttpGet(loginUrl); HttpResponse response = null; + // 发送请求,获取服务器响应 response = mHttpClient.execute(httpGet); - // get the cookie now + // 获取登录后的Cookie,判断是否包含GTask的授权Cookie List cookies = mHttpClient.getCookieStore().getCookies(); boolean hasAuthCookie = false; for (Cookie cookie : cookies) { if (cookie.getName().contains("GTL")) { - hasAuthCookie = true; + hasAuthCookie = true; // 包含授权Cookie } } if (!hasAuthCookie) { - Log.w(TAG, "it seems that there is no auth cookie"); + Log.w(TAG, "it seems that there is no auth cookie"); // 打印警告:没有授权Cookie } - // get the client version + // 获取服务器返回的内容,解析出客户端版本号 String resString = getResponseContent(response.getEntity()); String jsBegin = "_setup("; String jsEnd = ")}"; + // 找到JSON数据的起始和结束位置 int begin = resString.indexOf(jsBegin); int end = resString.lastIndexOf(jsEnd); String jsString = null; if (begin != -1 && end != -1 && begin < end) { + // 截取JSON数据字符串 jsString = resString.substring(begin + jsBegin.length(), end); } + // 解析JSON数据,获取客户端版本号 JSONObject js = new JSONObject(jsString); mClientVersion = js.getLong("v"); } catch (JSONException e) { + // JSON解析失败,打印日志并返回false Log.e(TAG, e.toString()); e.printStackTrace(); return false; } catch (Exception e) { - // simply catch all exceptions + // 其他异常(比如网络异常),打印日志并返回false Log.e(TAG, "httpget gtask_url failed"); return false; } @@ -280,152 +368,230 @@ public class GTaskClient { return true; } + /** + * 获取自增的操作ID + * 通俗说:每次和服务器交互,都给操作分配一个唯一ID,用后自增(1→2→3→...) + * @return 唯一操作ID + */ private int getActionId() { return mActionId++; } + /** + * 创建POST请求对象 + * 通俗说:封装POST请求的公共配置(请求头),不用每次创建都重复写 + * @return 配置好的POST请求对象 + */ private HttpPost createHttpPost() { HttpPost httpPost = new HttpPost(mPostUrl); + // 设置请求内容类型:表单格式,编码为UTF-8 httpPost.setHeader("Content-Type", "application/x-www-form-urlencoded;charset=utf-8"); + // 设置AT请求头:固定值1,GTask接口要求 httpPost.setHeader("AT", "1"); return httpPost; } + /** + * 获取网络响应的文本内容 + * 通俗说:把服务器返回的原始数据(字节)转换成文本,还会处理压缩格式(GZIP/DEFLATE) + * @param entity 网络响应实体(存储服务器返回的内容) + * @return 服务器返回的文本内容 + * @throws IOException 读取数据失败时抛出 + */ private String getResponseContent(HttpEntity entity) throws IOException { String contentEncoding = null; + // 获取响应内容的编码格式(判断是否是压缩格式) if (entity.getContentEncoding() != null) { contentEncoding = entity.getContentEncoding().getValue(); Log.d(TAG, "encoding: " + contentEncoding); } + // 获取响应的原始输入流 InputStream input = entity.getContent(); + // 如果是GZIP压缩格式,解压后再读取 if (contentEncoding != null && contentEncoding.equalsIgnoreCase("gzip")) { input = new GZIPInputStream(entity.getContent()); - } else if (contentEncoding != null && contentEncoding.equalsIgnoreCase("deflate")) { + } + // 如果是DEFLATE压缩格式,解压后再读取 + else if (contentEncoding != null && contentEncoding.equalsIgnoreCase("deflate")) { Inflater inflater = new Inflater(true); input = new InflaterInputStream(entity.getContent(), inflater); } try { + // 把字节输入流转换成文本输入流 InputStreamReader isr = new InputStreamReader(input); + // 用缓冲读取器提高读取效率 BufferedReader br = new BufferedReader(isr); - StringBuilder sb = new StringBuilder(); + StringBuilder sb = new StringBuilder(); // 存储读取到的文本内容 + // 循环读取每一行文本,直到读取完毕 while (true) { String buff = br.readLine(); if (buff == null) { - return sb.toString(); + return sb.toString(); // 返回读取到的所有文本 } - sb = sb.append(buff); + sb = sb.append(buff); // 把每行文本添加到字符串中 } } finally { - input.close(); + input.close(); // 关闭输入流,释放资源 } } + /** + * 发送POST请求到GTask服务器 + * 通俗说:把封装好的JSON数据提交给服务器,获取返回结果,处理各种异常 + * @param js 要提交的JSON数据(包含操作类型、参数等) + * @return 服务器返回的JSON结果 + * @throws NetworkFailureException 网络失败时抛出 + */ private JSONObject postRequest(JSONObject js) throws NetworkFailureException { + // 如果未登录,抛出异常 if (!mLoggedin) { Log.e(TAG, "please login first"); throw new ActionFailureException("not logged in"); } + // 创建配置好的POST请求 HttpPost httpPost = createHttpPost(); try { + // 创建请求参数列表(存储"r=JSON字符串"这个参数) LinkedList list = new LinkedList(); list.add(new BasicNameValuePair("r", js.toString())); + // 把参数列表封装成表单实体 UrlEncodedFormEntity entity = new UrlEncodedFormEntity(list, "UTF-8"); + // 给POST请求设置请求实体 httpPost.setEntity(entity); - // execute the post + // 发送POST请求,获取服务器响应 HttpResponse response = mHttpClient.execute(httpPost); + // 获取响应的文本内容 String jsString = getResponseContent(response.getEntity()); + // 把文本内容解析成JSON对象返回 return new JSONObject(jsString); } catch (ClientProtocolException e) { + // 客户端协议异常,打印日志并抛出网络失败异常 Log.e(TAG, e.toString()); e.printStackTrace(); throw new NetworkFailureException("postRequest failed"); } catch (IOException e) { + // IO异常(比如网络断开),打印日志并抛出网络失败异常 Log.e(TAG, e.toString()); e.printStackTrace(); throw new NetworkFailureException("postRequest failed"); } catch (JSONException e) { + // JSON解析异常,打印日志并抛出操作失败异常 Log.e(TAG, e.toString()); e.printStackTrace(); throw new ActionFailureException("unable to convert response content to jsonobject"); } catch (Exception e) { + // 其他异常,打印日志并抛出操作失败异常 Log.e(TAG, e.toString()); e.printStackTrace(); throw new ActionFailureException("error occurs when posting request"); } } + /** + * 创建单个GTask任务 + * 通俗说:把本地任务数据提交到GTask服务器,创建新任务,并获取服务器返回的任务ID + * @param task 要创建的本地任务对象 + * @throws NetworkFailureException 网络失败时抛出 + */ public void createTask(Task task) throws NetworkFailureException { + // 先提交之前待处理的更新操作 commitUpdate(); try { + // 创建POST请求的JSON数据 JSONObject jsPost = new JSONObject(); - JSONArray actionList = new JSONArray(); + JSONArray actionList = new JSONArray(); // 操作列表(存储创建任务的操作) - // action_list + // 把创建任务的操作添加到操作列表 actionList.put(task.getCreateAction(getActionId())); + // 给JSON数据添加操作列表 jsPost.put(GTaskStringUtils.GTASK_JSON_ACTION_LIST, actionList); - // client_version + // 给JSON数据添加客户端版本号 jsPost.put(GTaskStringUtils.GTASK_JSON_CLIENT_VERSION, mClientVersion); - // post + // 发送POST请求,获取服务器响应 JSONObject jsResponse = postRequest(jsPost); + // 从响应中获取创建任务的结果 JSONObject jsResult = (JSONObject) jsResponse.getJSONArray( GTaskStringUtils.GTASK_JSON_RESULTS).get(0); + // 把服务器返回的任务ID设置到本地任务对象中 task.setGid(jsResult.getString(GTaskStringUtils.GTASK_JSON_NEW_ID)); } catch (JSONException e) { + // JSON解析失败,打印日志并抛出操作失败异常 Log.e(TAG, e.toString()); e.printStackTrace(); throw new ActionFailureException("create task: handing jsonobject failed"); } } + /** + * 创建GTask任务列表 + * 通俗说:把本地任务列表数据提交到GTask服务器,创建新任务列表,并获取服务器返回的列表ID + * @param tasklist 要创建的本地任务列表对象 + * @throws NetworkFailureException 网络失败时抛出 + */ public void createTaskList(TaskList tasklist) throws NetworkFailureException { + // 先提交之前待处理的更新操作 commitUpdate(); try { + // 创建POST请求的JSON数据 JSONObject jsPost = new JSONObject(); - JSONArray actionList = new JSONArray(); + JSONArray actionList = new JSONArray(); // 操作列表(存储创建任务列表的操作) - // action_list + // 把创建任务列表的操作添加到操作列表 actionList.put(tasklist.getCreateAction(getActionId())); + // 给JSON数据添加操作列表 jsPost.put(GTaskStringUtils.GTASK_JSON_ACTION_LIST, actionList); - // client version + // 给JSON数据添加客户端版本号 jsPost.put(GTaskStringUtils.GTASK_JSON_CLIENT_VERSION, mClientVersion); - // post + // 发送POST请求,获取服务器响应 JSONObject jsResponse = postRequest(jsPost); + // 从响应中获取创建任务列表的结果 JSONObject jsResult = (JSONObject) jsResponse.getJSONArray( GTaskStringUtils.GTASK_JSON_RESULTS).get(0); + // 把服务器返回的列表ID设置到本地任务列表对象中 tasklist.setGid(jsResult.getString(GTaskStringUtils.GTASK_JSON_NEW_ID)); } catch (JSONException e) { + // JSON解析失败,打印日志并抛出操作失败异常 Log.e(TAG, e.toString()); e.printStackTrace(); throw new ActionFailureException("create tasklist: handing jsonobject failed"); } } + /** + * 提交待处理的更新操作 + * 通俗说:把mUpdateArray里存储的批量更新操作(修改/新增任务)一次性提交给服务器 + * @throws NetworkFailureException 网络失败时抛出 + */ public void commitUpdate() throws NetworkFailureException { + // 如果有未提交的更新操作 if (mUpdateArray != null) { try { + // 创建POST请求的JSON数据 JSONObject jsPost = new JSONObject(); - // action_list + // 给JSON数据添加更新操作列表 jsPost.put(GTaskStringUtils.GTASK_JSON_ACTION_LIST, mUpdateArray); - // client_version + // 给JSON数据添加客户端版本号 jsPost.put(GTaskStringUtils.GTASK_JSON_CLIENT_VERSION, mClientVersion); + // 发送POST请求,提交更新 postRequest(jsPost); - mUpdateArray = null; + mUpdateArray = null; // 提交后清空更新列表 } catch (JSONException e) { + // JSON解析失败,打印日志并抛出操作失败异常 Log.e(TAG, e.toString()); e.printStackTrace(); throw new ActionFailureException("commit update: handing jsonobject failed"); @@ -433,153 +599,230 @@ public class GTaskClient { } } + /** + * 添加待更新的节点(任务/任务列表) + * 通俗说:把单个更新操作添加到批量更新列表,累计到10个就自动提交,避免一次性提交太多失败 + * @param node 要更新的节点(任务/任务列表) + * @throws NetworkFailureException 网络失败时抛出 + */ public void addUpdateNode(Node node) throws NetworkFailureException { if (node != null) { - // too many update items may result in an error - // set max to 10 items + // 如果更新列表不为空,且数量超过10个,先提交一次 if (mUpdateArray != null && mUpdateArray.length() > 10) { commitUpdate(); } + // 如果更新列表为空,初始化一个新的JSON数组 if (mUpdateArray == null) mUpdateArray = new JSONArray(); + // 把节点的更新操作添加到更新列表 mUpdateArray.put(node.getUpdateAction(getActionId())); } } + /** + * 移动GTask任务 + * 通俗说:把任务从一个任务列表移动到另一个,或在同一个列表内调整位置 + * @param task 要移动的任务 + * @param preParent 任务原来的父列表 + * @param curParent 任务要移动到的目标列表 + * @throws NetworkFailureException 网络失败时抛出 + */ public void moveTask(Task task, TaskList preParent, TaskList curParent) throws NetworkFailureException { + // 先提交之前待处理的更新操作 commitUpdate(); try { + // 创建POST请求的JSON数据 JSONObject jsPost = new JSONObject(); - JSONArray actionList = new JSONArray(); - JSONObject action = new JSONObject(); + JSONArray actionList = new JSONArray(); // 操作列表 + JSONObject action = new JSONObject(); // 移动任务的操作 - // action_list + // 设置操作类型:移动 action.put(GTaskStringUtils.GTASK_JSON_ACTION_TYPE, GTaskStringUtils.GTASK_JSON_ACTION_TYPE_MOVE); + // 设置操作ID action.put(GTaskStringUtils.GTASK_JSON_ACTION_ID, getActionId()); + // 设置要移动的任务ID action.put(GTaskStringUtils.GTASK_JSON_ID, task.getGid()); + // 如果在同一个列表内移动,且不是第一个任务,设置上一个任务的ID(用来确定位置) if (preParent == curParent && task.getPriorSibling() != null) { - // put prioring_sibing_id only if moving within the tasklist and - // it is not the first one action.put(GTaskStringUtils.GTASK_JSON_PRIOR_SIBLING_ID, task.getPriorSibling()); } + // 设置任务原来的列表ID action.put(GTaskStringUtils.GTASK_JSON_SOURCE_LIST, preParent.getGid()); + // 设置任务目标列表的父ID action.put(GTaskStringUtils.GTASK_JSON_DEST_PARENT, curParent.getGid()); + // 如果在不同列表间移动,设置目标列表ID if (preParent != curParent) { - // put the dest_list only if moving between tasklists action.put(GTaskStringUtils.GTASK_JSON_DEST_LIST, curParent.getGid()); } + // 把移动操作添加到操作列表 actionList.put(action); + // 给JSON数据添加操作列表 jsPost.put(GTaskStringUtils.GTASK_JSON_ACTION_LIST, actionList); - // client_version + // 给JSON数据添加客户端版本号 jsPost.put(GTaskStringUtils.GTASK_JSON_CLIENT_VERSION, mClientVersion); + // 发送POST请求,执行移动操作 postRequest(jsPost); } catch (JSONException e) { + // JSON解析失败,打印日志并抛出操作失败异常 Log.e(TAG, e.toString()); e.printStackTrace(); throw new ActionFailureException("move task: handing jsonobject failed"); } } + /** + * 删除GTask节点(任务/任务列表) + * 通俗说:把节点标记为已删除,提交到服务器,完成删除操作 + * @param node 要删除的节点(任务/任务列表) + * @throws NetworkFailureException 网络失败时抛出 + */ public void deleteNode(Node node) throws NetworkFailureException { + // 先提交之前待处理的更新操作 commitUpdate(); try { + // 创建POST请求的JSON数据 JSONObject jsPost = new JSONObject(); - JSONArray actionList = new JSONArray(); + JSONArray actionList = new JSONArray(); // 操作列表 - // action_list + // 把节点标记为已删除 node.setDeleted(true); + // 把删除操作添加到操作列表 actionList.put(node.getUpdateAction(getActionId())); + // 给JSON数据添加操作列表 jsPost.put(GTaskStringUtils.GTASK_JSON_ACTION_LIST, actionList); - // client_version + // 给JSON数据添加客户端版本号 jsPost.put(GTaskStringUtils.GTASK_JSON_CLIENT_VERSION, mClientVersion); + // 发送POST请求,执行删除操作 postRequest(jsPost); - mUpdateArray = null; + mUpdateArray = null; // 清空更新列表 } catch (JSONException e) { + // JSON解析失败,打印日志并抛出操作失败异常 Log.e(TAG, e.toString()); e.printStackTrace(); throw new ActionFailureException("delete node: handing jsonobject failed"); } } + /** + * 获取所有GTask任务列表 + * 通俗说:从GTask服务器获取用户的所有任务列表(比如“我的任务”“工作任务”) + * @return 任务列表的JSON数组 + * @throws NetworkFailureException 网络失败时抛出 + */ public JSONArray getTaskLists() throws NetworkFailureException { + // 如果未登录,抛出异常 if (!mLoggedin) { Log.e(TAG, "please login first"); throw new ActionFailureException("not logged in"); } try { + // 创建GET请求 HttpGet httpGet = new HttpGet(mGetUrl); HttpResponse response = null; + // 发送GET请求,获取服务器响应 response = mHttpClient.execute(httpGet); - // get the task list + // 获取响应的文本内容 String resString = getResponseContent(response.getEntity()); String jsBegin = "_setup("; String jsEnd = ")}"; + // 找到JSON数据的起始和结束位置 int begin = resString.indexOf(jsBegin); int end = resString.lastIndexOf(jsEnd); String jsString = null; if (begin != -1 && end != -1 && begin < end) { + // 截取JSON数据字符串 jsString = resString.substring(begin + jsBegin.length(), end); } + // 解析JSON数据,获取任务列表数组 JSONObject js = new JSONObject(jsString); return js.getJSONObject("t").getJSONArray(GTaskStringUtils.GTASK_JSON_LISTS); } catch (ClientProtocolException e) { + // 客户端协议异常,打印日志并抛出网络失败异常 Log.e(TAG, e.toString()); e.printStackTrace(); throw new NetworkFailureException("gettasklists: httpget failed"); } catch (IOException e) { + // IO异常,打印日志并抛出网络失败异常 Log.e(TAG, e.toString()); e.printStackTrace(); throw new NetworkFailureException("gettasklists: httpget failed"); } catch (JSONException e) { + // JSON解析失败,打印日志并抛出操作失败异常 Log.e(TAG, e.toString()); e.printStackTrace(); throw new ActionFailureException("get task lists: handing jasonobject failed"); } } + /** + * 获取指定GTask任务列表的所有任务 + * 通俗说:根据任务列表ID,从服务器获取该列表下的所有任务 + * @param listGid 任务列表ID + * @return 任务的JSON数组 + * @throws NetworkFailureException 网络失败时抛出 + */ public JSONArray getTaskList(String listGid) throws NetworkFailureException { + // 先提交之前待处理的更新操作 commitUpdate(); try { + // 创建POST请求的JSON数据 JSONObject jsPost = new JSONObject(); - JSONArray actionList = new JSONArray(); - JSONObject action = new JSONObject(); + JSONArray actionList = new JSONArray(); // 操作列表 + JSONObject action = new JSONObject(); // 获取任务列表的操作 - // action_list + // 设置操作类型:获取所有任务 action.put(GTaskStringUtils.GTASK_JSON_ACTION_TYPE, GTaskStringUtils.GTASK_JSON_ACTION_TYPE_GETALL); + // 设置操作ID action.put(GTaskStringUtils.GTASK_JSON_ACTION_ID, getActionId()); + // 设置要获取的任务列表ID action.put(GTaskStringUtils.GTASK_JSON_LIST_ID, listGid); + // 设置是否获取已删除的任务:false=不获取 action.put(GTaskStringUtils.GTASK_JSON_GET_DELETED, false); + // 把获取操作添加到操作列表 actionList.put(action); + // 给JSON数据添加操作列表 jsPost.put(GTaskStringUtils.GTASK_JSON_ACTION_LIST, actionList); - // client_version + // 给JSON数据添加客户端版本号 jsPost.put(GTaskStringUtils.GTASK_JSON_CLIENT_VERSION, mClientVersion); + // 发送POST请求,获取服务器响应 JSONObject jsResponse = postRequest(jsPost); + // 从响应中获取任务数组并返回 return jsResponse.getJSONArray(GTaskStringUtils.GTASK_JSON_TASKS); } catch (JSONException e) { + // JSON解析失败,打印日志并抛出操作失败异常 Log.e(TAG, e.toString()); e.printStackTrace(); throw new ActionFailureException("get task list: handing jsonobject failed"); } } + /** + * 获取当前同步的谷歌账号 + * 通俗说:返回当前用来和GTask同步的谷歌账号信息 + * @return 同步账号 + */ public Account getSyncAccount() { return mAccount; } + /** + * 重置更新数据数组 + * 通俗说:清空待提交的更新操作列表,放弃未提交的更新 + */ public void resetUpdateArray() { mUpdateArray = null; } -} +} \ No newline at end of file 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..39546fe 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 @@ -16,110 +16,145 @@ package net.micode.notes.gtask.remote; -import android.app.Activity; -import android.content.ContentResolver; -import android.content.ContentUris; -import android.content.ContentValues; -import android.content.Context; -import android.database.Cursor; -import android.util.Log; - -import net.micode.notes.R; -import net.micode.notes.data.Notes; -import net.micode.notes.data.Notes.DataColumns; -import net.micode.notes.data.Notes.NoteColumns; -import net.micode.notes.gtask.data.MetaData; -import net.micode.notes.gtask.data.Node; -import net.micode.notes.gtask.data.SqlNote; -import net.micode.notes.gtask.data.Task; -import net.micode.notes.gtask.data.TaskList; -import net.micode.notes.gtask.exception.ActionFailureException; -import net.micode.notes.gtask.exception.NetworkFailureException; -import net.micode.notes.tool.DataUtils; -import net.micode.notes.tool.GTaskStringUtils; - -import org.json.JSONArray; -import org.json.JSONException; -import org.json.JSONObject; - -import java.util.HashMap; -import java.util.HashSet; -import java.util.Iterator; -import java.util.Map; - - +import android.app.Activity; // 页面类:用来关联登录需要的页面 +import android.content.ContentResolver; // 本地数据库操作工具:用来读写手机本地的便签数据库 +import android.content.ContentUris; // URI工具:用来拼接本地数据库的访问地址 +import android.content.ContentValues; // 数据容器:用来存储要更新到本地数据库的键值对数据 +import android.content.Context; // 上下文:保存页面/服务信息,获取资源和数据库工具 +import android.database.Cursor; // 数据库查询游标:类似“查询结果的遍历工具”,用来逐条读取数据库查询结果 +import android.util.Log; // 日志工具:打印调试信息,方便查找同步过程中的问题 + +import net.micode.notes.R; // 资源类:获取APP里的文字资源(比如同步进度提示) +import net.micode.notes.data.Notes; // 便签常量类:存储本地便签的类型、文件夹ID等固定值 +import net.micode.notes.data.Notes.DataColumns; // 便签数据列:本地便签数据表里的字段名(比如内容ID) +import net.micode.notes.data.Notes.NoteColumns; // 便签主列:本地便签主表里的字段名(比如便签ID、修改时间) +import net.micode.notes.gtask.data.MetaData; // 元数据类:存储便签的额外信息(比如本地数据库ID映射) +import net.micode.notes.gtask.data.Node; // 基础节点类:GTask任务/任务列表的通用数据模型 +import net.micode.notes.gtask.data.SqlNote; // 本地便签操作类:用来读写本地便签数据库的工具 +import net.micode.notes.gtask.data.Task; // GTask任务类:存储单个GTask任务的信息 +import net.micode.notes.gtask.data.TaskList; // GTask任务列表类:存储GTask任务列表的信息 +import net.micode.notes.gtask.exception.ActionFailureException; // 操作失败异常:本地/远程同步操作失败时抛出 +import net.micode.notes.gtask.exception.NetworkFailureException; // 网络失败异常:网络不通或GTask服务器请求失败时抛出 +import net.micode.notes.tool.DataUtils; // 本地便签工具:提供批量删除、判断便签是否存在等通用方法 +import net.micode.notes.tool.GTaskStringUtils; // 字符串工具:存储GTask同步用到的固定字符串(比如文件夹前缀) + +import org.json.JSONArray; // JSON数组:存储一组格式化数据(比如多个便签信息) +import org.json.JSONException; // JSON解析异常:数据格式错误导致解析失败时抛出 +import org.json.JSONObject; // JSON对象:存储键值对格式的数据(比如单个便签的信息) + +import java.util.HashMap; // 哈希表:用来存储“键-值”映射关系(比如GTaskID对应本地便签ID) +import java.util.HashSet; // 哈希集合:用来存储不重复的数据(比如要删除的本地便签ID) +import java.util.Iterator; // 迭代器:用来遍历哈希表/集合里的所有数据 +import java.util.Map; // 映射接口:哈希表的通用接口 + +/** + * GTask同步总管家类 + * 通俗说:这个类是GTask同步的核心,负责统筹所有同步工作(登录、初始化任务列表、同步文件夹/便签、处理增删改查) + * 特点:整个APP只有一个实例(不会创建多个重复对象),所有同步相关操作都由它统一调度 + */ public class GTaskManager { + // 日志标签:打印日志时用来标识是这个类的日志,方便查找同步问题 private static final String TAG = GTaskManager.class.getSimpleName(); + // 同步状态常量:同步成功 public static final int STATE_SUCCESS = 0; - + // 同步状态常量:网络错误(比如没网、GTask服务器连接失败) public static final int STATE_NETWORK_ERROR = 1; - + // 同步状态常量:内部错误(比如本地数据库查询失败、JSON解析失败) 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; + // 本类的唯一实例:保证整个APP只有一个GTaskManager对象 private static GTaskManager mInstance = null; + // 登录关联页面:用来获取谷歌账号的登录凭证(令牌) private Activity mActivity; - + // 上下文:保存当前页面/服务信息,用来获取数据库工具 private Context mContext; - + // 本地数据库操作工具:用来读写本地便签数据库 private ContentResolver mContentResolver; - + // 同步中标记:true=正在同步,false=未同步 private boolean mSyncing; - + // 同步取消标记:true=用户取消了同步,false=同步正常执行 private boolean mCancelled; + // 远程GTask列表哈希表:存储“GTask列表ID-任务列表对象”的映射,方便快速查找 private HashMap mGTaskListHashMap; - + // 远程GTask节点哈希表:存储“GTask节点ID-节点对象(任务/列表)”的映射,方便快速查找 private HashMap mGTaskHashMap; - + // 元数据哈希表:存储“GTask任务ID-元数据对象”的映射,保存便签额外信息 private HashMap mMetaHashMap; - + // 元数据列表:GTask上专门存储元数据的任务列表 private TaskList mMetaList; - + // 本地待删除便签ID集合:存储需要从本地数据库删除的便签ID(比如远程已删除的便签) private HashSet mLocalDeleteIdMap; - + // GTaskID对应本地便签ID哈希表:存储“GTaskID-本地便签ID”的映射 private HashMap mGidToNid; - + // 本地便签ID对应GTaskID哈希表:存储“本地便签ID-GTaskID”的映射 private HashMap mNidToGid; + /** + * 私有构造方法 + * 通俗说:不让外部直接创建这个类的对象,只能通过getInstance()获取唯一实例 + */ private GTaskManager() { - mSyncing = false; - mCancelled = false; - mGTaskListHashMap = new HashMap(); - mGTaskHashMap = new HashMap(); - mMetaHashMap = new HashMap(); - mMetaList = null; - mLocalDeleteIdMap = new HashSet(); - mGidToNid = new HashMap(); - mNidToGid = new HashMap(); + mSyncing = false; // 初始状态为未同步 + mCancelled = false; // 初始状态为未取消 + mGTaskListHashMap = new HashMap(); // 初始化远程GTask列表哈希表 + mGTaskHashMap = new HashMap(); // 初始化远程GTask节点哈希表 + mMetaHashMap = new HashMap(); // 初始化元数据哈希表 + mMetaList = null; // 元数据列表初始化为null + mLocalDeleteIdMap = new HashSet(); // 初始化本地待删除便签ID集合 + mGidToNid = new HashMap(); // 初始化GTaskID-本地ID映射表 + mNidToGid = new HashMap(); // 初始化本地ID-GTaskID映射表 } + /** + * 获取GTaskManager的唯一实例 + * 通俗说:整个APP只能通过这个方法拿到GTaskManager对象,保证只有一个实例 + * @return GTaskManager唯一实例 + */ public static synchronized GTaskManager getInstance() { + // 如果实例为null,就创建一个新的(懒加载:用到时才创建) if (mInstance == null) { mInstance = new GTaskManager(); } return mInstance; } + /** + * 设置登录关联的页面 + * 通俗说:给同步管理器设置一个页面,用来获取谷歌账号的登录凭证(没有这个页面没法登录GTask) + * @param activity 登录关联的页面 + */ public synchronized void setActivityContext(Activity activity) { - // used for getting authtoken + // 这个页面用来获取谷歌账号的登录令牌 mActivity = activity; } + /** + * 同步主方法:启动GTask和本地便签的同步 + * 通俗说:这是同步的入口方法,负责统筹整个同步流程,返回同步结果状态 + * @param context 上下文(获取数据库工具用) + * @param asyncTask 异步任务对象(用来发送同步进度提示) + * @return 同步状态(成功/网络错误/内部错误/正在同步/已取消) + */ public int sync(Context context, GTaskASyncTask asyncTask) { + // 如果已经在同步中,打印日志并返回“正在同步”状态 if (mSyncing) { Log.d(TAG, "Sync is in progress"); return STATE_SYNC_IN_PROGRESS; } + + // 初始化同步所需的变量 mContext = context; - mContentResolver = mContext.getContentResolver(); - mSyncing = true; - mCancelled = false; + mContentResolver = mContext.getContentResolver(); // 获取本地数据库操作工具 + mSyncing = true; // 标记为正在同步 + mCancelled = false; // 标记为未取消 + // 清空所有存储映射的容器(避免上次同步的数据残留) mGTaskListHashMap.clear(); mGTaskHashMap.clear(); mMetaHashMap.clear(); @@ -128,74 +163,94 @@ public class GTaskManager { mNidToGid.clear(); try { + // 获取GTask客户端实例(用来和GTask服务器通信) GTaskClient client = GTaskClient.getInstance(); - client.resetUpdateArray(); + client.resetUpdateArray(); // 清空GTask客户端的待更新操作列表 - // login google task + // 第一步:登录GTask服务器(如果用户没取消同步) if (!mCancelled) { if (!client.login(mActivity)) { + // 登录失败,抛出网络失败异常 throw new NetworkFailureException("login google task failed"); } } - // get the task list from google - asyncTask.publishProgess(mContext.getString(R.string.sync_progress_init_list)); + // 第二步:初始化远程GTask任务列表(发送“正在初始化列表”的进度提示) + asyncTask.publishProgress(mContext.getString(R.string.sync_progress_init_list)); initGTaskList(); - // do content sync work - asyncTask.publishProgess(mContext.getString(R.string.sync_progress_syncing)); + // 第三步:执行具体的内容同步工作(发送“正在同步”的进度提示) + asyncTask.publishProgress(mContext.getString(R.string.sync_progress_syncing)); syncContent(); + } catch (NetworkFailureException e) { + // 网络错误(比如没网),打印日志并返回网络错误状态 Log.e(TAG, e.toString()); return STATE_NETWORK_ERROR; } catch (ActionFailureException e) { + // 内部操作错误(比如数据库查询失败),打印日志并返回内部错误状态 Log.e(TAG, e.toString()); return STATE_INTERNAL_ERROR; } catch (Exception e) { + // 其他未知错误,打印日志和异常堆栈,返回内部错误状态 Log.e(TAG, e.toString()); e.printStackTrace(); return STATE_INTERNAL_ERROR; } finally { + // 无论同步成功还是失败,都清空所有容器,释放资源 mGTaskListHashMap.clear(); mGTaskHashMap.clear(); mMetaHashMap.clear(); mLocalDeleteIdMap.clear(); mGidToNid.clear(); mNidToGid.clear(); - mSyncing = false; + mSyncing = false; // 标记为未同步 } + // 根据是否取消同步,返回对应状态 return mCancelled ? STATE_SYNC_CANCELLED : STATE_SUCCESS; } + /** + * 初始化远程GTask任务列表 + * 通俗说:从GTask服务器获取所有任务列表和任务,存储到本地容器中,方便后续同步 + * @throws NetworkFailureException 网络失败时抛出 + */ private void initGTaskList() throws NetworkFailureException { + // 如果用户取消了同步,直接返回 if (mCancelled) return; + + // 获取GTask客户端实例 GTaskClient client = GTaskClient.getInstance(); try { + // 从GTask服务器获取所有任务列表的JSON数据 JSONArray jsTaskLists = client.getTaskLists(); - // init meta list first + // 第一步:先初始化元数据列表(存储便签额外信息的列表) mMetaList = null; for (int i = 0; i < jsTaskLists.length(); i++) { JSONObject object = jsTaskLists.getJSONObject(i); - String gid = object.getString(GTaskStringUtils.GTASK_JSON_ID); - String name = object.getString(GTaskStringUtils.GTASK_JSON_NAME); + String gid = object.getString(GTaskStringUtils.GTASK_JSON_ID); // 获取任务列表ID + String name = object.getString(GTaskStringUtils.GTASK_JSON_NAME); // 获取任务列表名称 + // 判断是否是元数据列表(名称以固定前缀+META结尾) if (name .equals(GTaskStringUtils.MIUI_FOLDER_PREFFIX + GTaskStringUtils.FOLDER_META)) { mMetaList = new TaskList(); - mMetaList.setContentByRemoteJSON(object); + mMetaList.setContentByRemoteJSON(object); // 用服务器返回的数据初始化元数据列表 - // load meta data + // 加载元数据列表里的所有元数据 JSONArray jsMetas = client.getTaskList(gid); for (int j = 0; j < jsMetas.length(); j++) { object = (JSONObject) jsMetas.getJSONObject(j); MetaData metaData = new MetaData(); - metaData.setContentByRemoteJSON(object); + metaData.setContentByRemoteJSON(object); // 用服务器数据初始化元数据 + // 如果元数据有保存价值(不是空数据) if (metaData.isWorthSaving()) { - mMetaList.addChildTask(metaData); + mMetaList.addChildTask(metaData); // 添加到元数据列表 if (metaData.getGid() != null) { + // 存储到元数据哈希表(关联GTask任务ID和元数据) mMetaHashMap.put(metaData.getRelatedGid(), metaData); } } @@ -203,358 +258,437 @@ public class GTaskManager { } } - // create meta list if not existed + // 如果元数据列表不存在,就创建一个新的元数据列表并提交到GTask服务器 if (mMetaList == null) { mMetaList = new TaskList(); mMetaList.setName(GTaskStringUtils.MIUI_FOLDER_PREFFIX - + GTaskStringUtils.FOLDER_META); - GTaskClient.getInstance().createTaskList(mMetaList); + + GTaskStringUtils.FOLDER_META); // 设置元数据列表名称 + GTaskClient.getInstance().createTaskList(mMetaList); // 创建远程元数据列表 } - // init task list + // 第二步:初始化普通GTask任务列表(非元数据列表) for (int i = 0; i < jsTaskLists.length(); i++) { JSONObject object = jsTaskLists.getJSONObject(i); - String gid = object.getString(GTaskStringUtils.GTASK_JSON_ID); - String name = object.getString(GTaskStringUtils.GTASK_JSON_NAME); + String gid = object.getString(GTaskStringUtils.GTASK_JSON_ID); // 获取任务列表ID + String name = object.getString(GTaskStringUtils.GTASK_JSON_NAME); // 获取任务列表名称 + // 判断是否是MIUI便签对应的任务列表(以固定前缀开头,且不是元数据列表) if (name.startsWith(GTaskStringUtils.MIUI_FOLDER_PREFFIX) && !name.equals(GTaskStringUtils.MIUI_FOLDER_PREFFIX - + GTaskStringUtils.FOLDER_META)) { + + GTaskStringUtils.FOLDER_META)) { TaskList tasklist = new TaskList(); - tasklist.setContentByRemoteJSON(object); + tasklist.setContentByRemoteJSON(object); // 用服务器数据初始化任务列表 + // 存储到任务列表哈希表和节点哈希表 mGTaskListHashMap.put(gid, tasklist); mGTaskHashMap.put(gid, tasklist); - // load tasks + // 加载该任务列表下的所有任务 JSONArray jsTasks = client.getTaskList(gid); for (int j = 0; j < jsTasks.length(); j++) { object = (JSONObject) jsTasks.getJSONObject(j); - gid = object.getString(GTaskStringUtils.GTASK_JSON_ID); + gid = object.getString(GTaskStringUtils.GTASK_JSON_ID); // 获取任务ID Task task = new Task(); - task.setContentByRemoteJSON(object); + task.setContentByRemoteJSON(object); // 用服务器数据初始化任务 + // 如果任务有保存价值(不是空数据) if (task.isWorthSaving()) { - task.setMetaInfo(mMetaHashMap.get(gid)); - tasklist.addChildTask(task); - mGTaskHashMap.put(gid, task); + task.setMetaInfo(mMetaHashMap.get(gid)); // 设置任务的元数据 + tasklist.addChildTask(task); // 添加到任务列表 + mGTaskHashMap.put(gid, task); // 存储到节点哈希表 } } } } } catch (JSONException e) { + // JSON解析失败,打印日志和堆栈,抛出操作失败异常 Log.e(TAG, e.toString()); e.printStackTrace(); throw new ActionFailureException("initGTaskList: handing JSONObject failed"); } } + /** + * 同步核心方法:处理本地和远程的内容同步(文件夹、便签的增删改查) + * 通俗说:这是同步的核心逻辑,负责对比本地和远程的便签/文件夹,处理各种同步场景(新增、删除、更新) + * @throws NetworkFailureException 网络失败时抛出 + */ private void syncContent() throws NetworkFailureException { - int syncType; - Cursor c = null; - String gid; - Node node; + int syncType; // 同步类型(新增本地/远程、删除本地/远程、更新本地/远程等) + Cursor c = null; // 数据库查询游标 + String gid; // GTask节点ID + Node node; // GTask节点对象(任务/列表) + // 清空本地待删除便签ID集合 mLocalDeleteIdMap.clear(); + // 如果用户取消了同步,直接返回 if (mCancelled) { return; } - // for local deleted note + // 第一步:处理本地回收站里的便签(这些是本地已删除的便签,需要同步删除远程对应任务) try { + // 查询本地回收站里的非系统便签(parent_id=回收站ID,type≠系统类型) c = mContentResolver.query(Notes.CONTENT_NOTE_URI, SqlNote.PROJECTION_NOTE, "(type<>? AND parent_id=?)", new String[] { String.valueOf(Notes.TYPE_SYSTEM), String.valueOf(Notes.ID_TRASH_FOLER) }, null); if (c != null) { + // 逐条读取查询结果 while (c.moveToNext()) { - gid = c.getString(SqlNote.GTASK_ID_COLUMN); - node = mGTaskHashMap.get(gid); + gid = c.getString(SqlNote.GTASK_ID_COLUMN); // 获取便签对应的GTaskID + node = mGTaskHashMap.get(gid); // 根据GTaskID获取远程节点 if (node != null) { + // 如果远程存在对应节点,从节点哈希表移除,并执行“删除远程节点”操作 mGTaskHashMap.remove(gid); doContentSync(Node.SYNC_ACTION_DEL_REMOTE, node, c); } - + // 将该便签ID加入本地待删除集合(后续批量删除本地数据) mLocalDeleteIdMap.add(c.getLong(SqlNote.ID_COLUMN)); } } else { + // 查询失败,打印警告日志 Log.w(TAG, "failed to query trash folder"); } } finally { + // 无论查询成功与否,都关闭游标,释放资源 if (c != null) { c.close(); c = null; } } - // sync folder first + // 第二步:先同步文件夹(文件夹同步优先级高于普通便签) syncFolder(); - // for note existing in database + // 第三步:处理本地数据库中存在的普通便签(非回收站、类型为普通便签) try { + // 查询本地非回收站的普通便签(type=普通便签,parent_id≠回收站ID) c = mContentResolver.query(Notes.CONTENT_NOTE_URI, SqlNote.PROJECTION_NOTE, "(type=? AND parent_id<>?)", new String[] { String.valueOf(Notes.TYPE_NOTE), String.valueOf(Notes.ID_TRASH_FOLER) }, NoteColumns.TYPE + " DESC"); if (c != null) { + // 逐条读取查询结果 while (c.moveToNext()) { - gid = c.getString(SqlNote.GTASK_ID_COLUMN); - node = mGTaskHashMap.get(gid); + gid = c.getString(SqlNote.GTASK_ID_COLUMN); // 获取便签对应的GTaskID + node = mGTaskHashMap.get(gid); // 根据GTaskID获取远程节点 if (node != null) { + // 远程存在对应节点:从节点哈希表移除,建立GTaskID和本地ID的映射 mGTaskHashMap.remove(gid); mGidToNid.put(gid, c.getLong(SqlNote.ID_COLUMN)); mNidToGid.put(c.getLong(SqlNote.ID_COLUMN), gid); + // 获取同步类型(判断是更新本地还是更新远程) syncType = node.getSyncAction(c); } else { + // 远程不存在对应节点 if (c.getString(SqlNote.GTASK_ID_COLUMN).trim().length() == 0) { - // local add + // 本地GTaskID为空,说明是本地新增便签,需要“新增到远程” syncType = Node.SYNC_ACTION_ADD_REMOTE; } else { - // remote delete + // 本地有GTaskID但远程不存在,说明远程已删除,需要“删除本地便签” syncType = Node.SYNC_ACTION_DEL_LOCAL; } } + // 执行具体的同步操作 doContentSync(syncType, node, c); } } else { + // 查询失败,打印警告日志 Log.w(TAG, "failed to query existing note in database"); } } finally { + // 关闭游标,释放资源 if (c != null) { c.close(); c = null; } } - // go through remaining items + // 第四步:处理远程有但本地没有的节点(远程新增的内容,需要同步到本地) Iterator> iter = mGTaskHashMap.entrySet().iterator(); while (iter.hasNext()) { Map.Entry entry = iter.next(); node = entry.getValue(); + // 执行“新增到本地”的同步操作 doContentSync(Node.SYNC_ACTION_ADD_LOCAL, node, null); } - // mCancelled can be set by another thread, so we neet to check one by - // one - // clear local delete table + // 第五步:批量删除本地待删除的便签(如果用户没取消同步) if (!mCancelled) { if (!DataUtils.batchDeleteNotes(mContentResolver, mLocalDeleteIdMap)) { + // 批量删除失败,抛出操作失败异常 throw new ActionFailureException("failed to batch-delete local deleted notes"); } } - // refresh local sync id + // 第六步:提交远程更新并刷新本地同步ID(如果用户没取消同步) if (!mCancelled) { - GTaskClient.getInstance().commitUpdate(); - refreshLocalSyncId(); + GTaskClient.getInstance().commitUpdate(); // 提交所有待更新的远程操作 + refreshLocalSyncId(); // 刷新本地便签的同步ID(记录最后修改时间) } } + /** + * 同步文件夹:处理本地和远程文件夹的增删改查 + * 通俗说:专门同步便签文件夹(根文件夹、通话记录文件夹、自定义文件夹) + * @throws NetworkFailureException 网络失败时抛出 + */ private void syncFolder() throws NetworkFailureException { - Cursor c = null; - String gid; - Node node; - int syncType; + Cursor c = null; // 数据库查询游标 + String gid; // GTask文件夹ID + Node node; // GTask文件夹节点 + int syncType; // 同步类型 + // 如果用户取消了同步,直接返回 if (mCancelled) { return; } - // for root folder + // 第一步:同步根文件夹(本地默认的根文件夹) try { + // 查询本地根文件夹(ID=根文件夹ID) c = mContentResolver.query(ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, Notes.ID_ROOT_FOLDER), SqlNote.PROJECTION_NOTE, null, null, null); if (c != null) { - c.moveToNext(); - gid = c.getString(SqlNote.GTASK_ID_COLUMN); - node = mGTaskHashMap.get(gid); + c.moveToNext(); // 根文件夹只有一个,直接移动到第一条 + gid = c.getString(SqlNote.GTASK_ID_COLUMN); // 获取根文件夹对应的GTaskID + node = mGTaskHashMap.get(gid); // 根据GTaskID获取远程文件夹 if (node != null) { + // 远程存在对应文件夹:从节点哈希表移除,建立GTaskID和本地ID的映射 mGTaskHashMap.remove(gid); mGidToNid.put(gid, (long) Notes.ID_ROOT_FOLDER); mNidToGid.put((long) Notes.ID_ROOT_FOLDER, gid); - // for system folder, only update remote name if necessary + // 系统文件夹只更新名称:如果远程名称和本地默认名称不一致,执行“更新远程名称”操作 if (!node.getName().equals( GTaskStringUtils.MIUI_FOLDER_PREFFIX + GTaskStringUtils.FOLDER_DEFAULT)) doContentSync(Node.SYNC_ACTION_UPDATE_REMOTE, node, c); } else { + // 远程不存在对应文件夹,执行“新增到远程”操作 doContentSync(Node.SYNC_ACTION_ADD_REMOTE, node, c); } } else { + // 查询失败,打印警告日志 Log.w(TAG, "failed to query root folder"); } } finally { + // 关闭游标,释放资源 if (c != null) { c.close(); c = null; } } - // for call-note folder + // 第二步:同步通话记录文件夹 try { + // 查询本地通话记录文件夹(ID=通话记录文件夹ID) c = mContentResolver.query(Notes.CONTENT_NOTE_URI, SqlNote.PROJECTION_NOTE, "(_id=?)", new String[] { - String.valueOf(Notes.ID_CALL_RECORD_FOLDER) + String.valueOf(Notes.ID_CALL_RECORD_FOLDER) }, null); if (c != null) { if (c.moveToNext()) { - gid = c.getString(SqlNote.GTASK_ID_COLUMN); - node = mGTaskHashMap.get(gid); + gid = c.getString(SqlNote.GTASK_ID_COLUMN); // 获取通话记录文件夹对应的GTaskID + node = mGTaskHashMap.get(gid); // 根据GTaskID获取远程文件夹 if (node != null) { + // 远程存在对应文件夹:从节点哈希表移除,建立GTaskID和本地ID的映射 mGTaskHashMap.remove(gid); mGidToNid.put(gid, (long) Notes.ID_CALL_RECORD_FOLDER); mNidToGid.put((long) Notes.ID_CALL_RECORD_FOLDER, gid); - // for system folder, only update remote name if - // necessary + // 系统文件夹只更新名称:如果远程名称和本地默认名称不一致,执行“更新远程名称”操作 if (!node.getName().equals( GTaskStringUtils.MIUI_FOLDER_PREFFIX + GTaskStringUtils.FOLDER_CALL_NOTE)) doContentSync(Node.SYNC_ACTION_UPDATE_REMOTE, node, c); } else { + // 远程不存在对应文件夹,执行“新增到远程”操作 doContentSync(Node.SYNC_ACTION_ADD_REMOTE, node, c); } } } else { + // 查询失败,打印警告日志 Log.w(TAG, "failed to query call note folder"); } } finally { + // 关闭游标,释放资源 if (c != null) { c.close(); c = null; } } - // for local existing folders + // 第三步:同步本地自定义文件夹(非系统文件夹、非回收站) try { + // 查询本地自定义文件夹(type=文件夹,parent_id≠回收站ID) c = mContentResolver.query(Notes.CONTENT_NOTE_URI, SqlNote.PROJECTION_NOTE, "(type=? AND parent_id<>?)", new String[] { String.valueOf(Notes.TYPE_FOLDER), String.valueOf(Notes.ID_TRASH_FOLER) }, NoteColumns.TYPE + " DESC"); if (c != null) { + // 逐条读取查询结果 while (c.moveToNext()) { - gid = c.getString(SqlNote.GTASK_ID_COLUMN); - node = mGTaskHashMap.get(gid); + gid = c.getString(SqlNote.GTASK_ID_COLUMN); // 获取文件夹对应的GTaskID + node = mGTaskHashMap.get(gid); // 根据GTaskID获取远程文件夹 if (node != null) { + // 远程存在对应文件夹:从节点哈希表移除,建立GTaskID和本地ID的映射 mGTaskHashMap.remove(gid); mGidToNid.put(gid, c.getLong(SqlNote.ID_COLUMN)); mNidToGid.put(c.getLong(SqlNote.ID_COLUMN), gid); + // 获取同步类型(判断是更新本地还是更新远程) syncType = node.getSyncAction(c); } else { + // 远程不存在对应文件夹 if (c.getString(SqlNote.GTASK_ID_COLUMN).trim().length() == 0) { - // local add + // 本地GTaskID为空,说明是本地新增文件夹,需要“新增到远程” syncType = Node.SYNC_ACTION_ADD_REMOTE; } else { - // remote delete + // 本地有GTaskID但远程不存在,说明远程已删除,需要“删除本地文件夹” syncType = Node.SYNC_ACTION_DEL_LOCAL; } } + // 执行具体的同步操作 doContentSync(syncType, node, c); } } else { + // 查询失败,打印警告日志 Log.w(TAG, "failed to query existing folder"); } } finally { + // 关闭游标,释放资源 if (c != null) { c.close(); c = null; } } - // for remote add folders + // 第四步:处理远程有但本地没有的文件夹(远程新增的文件夹,需要同步到本地) Iterator> iter = mGTaskListHashMap.entrySet().iterator(); while (iter.hasNext()) { Map.Entry entry = iter.next(); gid = entry.getKey(); node = entry.getValue(); if (mGTaskHashMap.containsKey(gid)) { + // 从节点哈希表移除,执行“新增到本地”的同步操作 mGTaskHashMap.remove(gid); doContentSync(Node.SYNC_ACTION_ADD_LOCAL, node, null); } } + // 如果用户没取消同步,提交文件夹的远程更新 if (!mCancelled) GTaskClient.getInstance().commitUpdate(); } + /** + * 处理具体的同步操作 + * 通俗说:根据不同的同步类型,调用对应的方法(新增、删除、更新本地/远程内容) + * @param syncType 同步类型(新增本地/远程、删除本地/远程等) + * @param node GTask节点对象(任务/文件夹) + * @param c 数据库查询游标(存储本地便签/文件夹数据) + * @throws NetworkFailureException 网络失败时抛出 + */ private void doContentSync(int syncType, Node node, Cursor c) throws NetworkFailureException { + // 如果用户取消了同步,直接返回 if (mCancelled) { return; } - MetaData meta; + MetaData meta; // 元数据对象 + // 根据同步类型执行不同的操作 switch (syncType) { case Node.SYNC_ACTION_ADD_LOCAL: + // 远程新增:同步到本地 addLocalNode(node); break; case Node.SYNC_ACTION_ADD_REMOTE: + // 本地新增:同步到远程 addRemoteNode(node, c); break; case Node.SYNC_ACTION_DEL_LOCAL: + // 远程删除:删除本地对应内容 meta = mMetaHashMap.get(c.getString(SqlNote.GTASK_ID_COLUMN)); if (meta != null) { - GTaskClient.getInstance().deleteNode(meta); + GTaskClient.getInstance().deleteNode(meta); // 删除远程元数据 } - mLocalDeleteIdMap.add(c.getLong(SqlNote.ID_COLUMN)); + mLocalDeleteIdMap.add(c.getLong(SqlNote.ID_COLUMN)); // 加入本地待删除集合 break; case Node.SYNC_ACTION_DEL_REMOTE: + // 本地删除:删除远程对应内容 meta = mMetaHashMap.get(node.getGid()); if (meta != null) { - GTaskClient.getInstance().deleteNode(meta); + GTaskClient.getInstance().deleteNode(meta); // 删除远程元数据 } - GTaskClient.getInstance().deleteNode(node); + GTaskClient.getInstance().deleteNode(node); // 删除远程节点 break; case Node.SYNC_ACTION_UPDATE_LOCAL: + // 远程更新:更新本地对应内容 updateLocalNode(node, c); break; case Node.SYNC_ACTION_UPDATE_REMOTE: + // 本地更新:更新远程对应内容 updateRemoteNode(node, c); break; case Node.SYNC_ACTION_UPDATE_CONFLICT: - // merging both modifications maybe a good idea - // right now just use local update simply + // 同步冲突:目前简单处理为以本地更新为准,更新远程内容 + // (更优方案可以合并本地和远程的修改,这里暂不实现) updateRemoteNode(node, c); break; case Node.SYNC_ACTION_NONE: + // 无需同步:直接跳过 break; case Node.SYNC_ACTION_ERROR: default: + // 未知同步类型:抛出操作失败异常 throw new ActionFailureException("unkown sync action type"); } } + /** + * 新增本地节点:把远程新增的GTask节点(任务/文件夹)同步到本地数据库 + * @param node 远程GTask节点 + * @throws NetworkFailureException 网络失败时抛出 + */ private void addLocalNode(Node node) throws NetworkFailureException { + // 如果用户取消了同步,直接返回 if (mCancelled) { return; } - SqlNote sqlNote; + SqlNote sqlNote; // 本地便签操作对象 if (node instanceof TaskList) { + // 同步的是文件夹 if (node.getName().equals( GTaskStringUtils.MIUI_FOLDER_PREFFIX + GTaskStringUtils.FOLDER_DEFAULT)) { + // 远程是默认文件夹,对应本地根文件夹 sqlNote = new SqlNote(mContext, Notes.ID_ROOT_FOLDER); } else if (node.getName().equals( GTaskStringUtils.MIUI_FOLDER_PREFFIX + GTaskStringUtils.FOLDER_CALL_NOTE)) { + // 远程是通话记录文件夹,对应本地通话记录文件夹 sqlNote = new SqlNote(mContext, Notes.ID_CALL_RECORD_FOLDER); } else { + // 远程是自定义文件夹,创建新的本地文件夹 sqlNote = new SqlNote(mContext); - sqlNote.setContent(node.getLocalJSONFromContent()); - sqlNote.setParentId(Notes.ID_ROOT_FOLDER); + sqlNote.setContent(node.getLocalJSONFromContent()); // 设置文件夹内容 + sqlNote.setParentId(Notes.ID_ROOT_FOLDER); // 父文件夹为根文件夹 } } else { + // 同步的是普通任务(便签) sqlNote = new SqlNote(mContext); - JSONObject js = node.getLocalJSONFromContent(); + JSONObject js = node.getLocalJSONFromContent(); // 获取任务的本地JSON数据 try { + // 处理便签ID:如果ID已存在,移除ID(重新生成新ID) if (js.has(GTaskStringUtils.META_HEAD_NOTE)) { JSONObject note = js.getJSONObject(GTaskStringUtils.META_HEAD_NOTE); if (note.has(NoteColumns.ID)) { long id = note.getLong(NoteColumns.ID); if (DataUtils.existInNoteDatabase(mContentResolver, id)) { - // the id is not available, have to create a new one note.remove(NoteColumns.ID); } } } + // 处理便签数据ID:如果数据ID已存在,移除ID(重新生成新ID) if (js.has(GTaskStringUtils.META_HEAD_DATA)) { JSONArray dataArray = js.getJSONArray(GTaskStringUtils.META_HEAD_DATA); for (int i = 0; i < dataArray.length(); i++) { @@ -562,92 +696,114 @@ public class GTaskManager { if (data.has(DataColumns.ID)) { long dataId = data.getLong(DataColumns.ID); if (DataUtils.existInDataDatabase(mContentResolver, dataId)) { - // the data id is not available, have to create - // a new one data.remove(DataColumns.ID); } } } - } } catch (JSONException e) { + // JSON解析失败,打印警告日志 Log.w(TAG, e.toString()); e.printStackTrace(); } - sqlNote.setContent(js); + sqlNote.setContent(js); // 设置便签内容 + // 获取任务的父文件夹本地ID(通过GTask父文件夹ID映射) Long parentId = mGidToNid.get(((Task) node).getParent().getGid()); if (parentId == null) { + // 找不到父文件夹ID,抛出操作失败异常 Log.e(TAG, "cannot find task's parent id locally"); throw new ActionFailureException("cannot add local node"); } - sqlNote.setParentId(parentId.longValue()); + sqlNote.setParentId(parentId.longValue()); // 设置便签的父文件夹ID } - // create the local node + // 设置便签对应的GTaskID,提交到本地数据库 sqlNote.setGtaskId(node.getGid()); - sqlNote.commit(false); + sqlNote.commit(false); // false=新增数据 - // update gid-nid mapping + // 更新GTaskID和本地ID的映射关系 mGidToNid.put(node.getGid(), sqlNote.getId()); mNidToGid.put(sqlNote.getId(), node.getGid()); - // update meta + // 更新远程元数据(记录本地便签信息) updateRemoteMeta(node.getGid(), sqlNote); } + /** + * 更新本地节点:用远程更新的GTask节点数据,更新本地对应的便签/文件夹 + * @param node 远程更新后的GTask节点 + * @param c 本地数据库查询游标(存储旧的本地数据) + * @throws NetworkFailureException 网络失败时抛出 + */ private void updateLocalNode(Node node, Cursor c) throws NetworkFailureException { + // 如果用户取消了同步,直接返回 if (mCancelled) { return; } - SqlNote sqlNote; - // update the note locally - sqlNote = new SqlNote(mContext, c); + // 用本地旧数据初始化便签操作对象 + SqlNote sqlNote = new SqlNote(mContext, c); + // 用远程更新的数据设置便签内容 sqlNote.setContent(node.getLocalJSONFromContent()); + // 获取父文件夹本地ID(文件夹的父ID默认是根文件夹,任务的父ID通过GTask映射) Long parentId = (node instanceof Task) ? mGidToNid.get(((Task) node).getParent().getGid()) : new Long(Notes.ID_ROOT_FOLDER); if (parentId == null) { + // 找不到父文件夹ID,抛出操作失败异常 Log.e(TAG, "cannot find task's parent id locally"); throw new ActionFailureException("cannot update local node"); } - sqlNote.setParentId(parentId.longValue()); - sqlNote.commit(true); + sqlNote.setParentId(parentId.longValue()); // 设置父文件夹ID + sqlNote.commit(true); // true=更新数据 - // update meta info + // 更新远程元数据(记录本地便签的最新信息) updateRemoteMeta(node.getGid(), sqlNote); } + /** + * 新增远程节点:把本地新增的便签/文件夹,同步到GTask服务器 + * @param node (暂时无用,本地新增节点对应远程无节点,传null) + * @param c 本地数据库查询游标(存储本地新增的便签/文件夹数据) + * @throws NetworkFailureException 网络失败时抛出 + */ private void addRemoteNode(Node node, Cursor c) throws NetworkFailureException { + // 如果用户取消了同步,直接返回 if (mCancelled) { return; } + // 用本地数据初始化便签操作对象 SqlNote sqlNote = new SqlNote(mContext, c); - Node n; + Node n; // 要同步到远程的节点对象 - // update remotely + // 更新到远程 if (sqlNote.isNoteType()) { + // 同步的是普通便签 Task task = new Task(); - task.setContentByLocalJSON(sqlNote.getContent()); + task.setContentByLocalJSON(sqlNote.getContent()); // 用本地数据初始化GTask任务 + // 获取本地父文件夹对应的GTaskID String parentGid = mNidToGid.get(sqlNote.getParentId()); if (parentGid == null) { + // 找不到父文件夹的GTaskID,抛出操作失败异常 Log.e(TAG, "cannot find task's parent tasklist"); throw new ActionFailureException("cannot add remote task"); } + // 将任务添加到对应的远程任务列表 mGTaskListHashMap.get(parentGid).addChildTask(task); - GTaskClient.getInstance().createTask(task); - n = (Node) task; + GTaskClient.getInstance().createTask(task); // 在GTask服务器创建任务 + n = (Node) task; // 赋值给节点对象 - // add meta + // 添加元数据(记录本地便签信息) updateRemoteMeta(task.getGid(), sqlNote); } else { + // 同步的是文件夹 TaskList tasklist = null; - // we need to skip folder if it has already existed + // 拼接文件夹的远程名称(固定前缀+本地文件夹名称) String folderName = GTaskStringUtils.MIUI_FOLDER_PREFFIX; if (sqlNote.getId() == Notes.ID_ROOT_FOLDER) folderName += GTaskStringUtils.FOLDER_DEFAULT; @@ -656,6 +812,7 @@ public class GTaskManager { else folderName += sqlNote.getSnippet(); + // 检查远程是否已存在该文件夹(避免重复创建) Iterator> iter = mGTaskListHashMap.entrySet().iterator(); while (iter.hasNext()) { Map.Entry entry = iter.next(); @@ -663,126 +820,160 @@ public class GTaskManager { TaskList list = entry.getValue(); if (list.getName().equals(folderName)) { - tasklist = list; + tasklist = list; // 找到已存在的文件夹 if (mGTaskHashMap.containsKey(gid)) { - mGTaskHashMap.remove(gid); + mGTaskHashMap.remove(gid); // 从节点哈希表移除 } break; } } - // no match we can add now + // 如果远程不存在该文件夹,创建新的远程文件夹 if (tasklist == null) { tasklist = new TaskList(); - tasklist.setContentByLocalJSON(sqlNote.getContent()); - GTaskClient.getInstance().createTaskList(tasklist); - mGTaskListHashMap.put(tasklist.getGid(), tasklist); + tasklist.setContentByLocalJSON(sqlNote.getContent()); // 用本地数据初始化GTask文件夹 + GTaskClient.getInstance().createTaskList(tasklist); // 在GTask服务器创建文件夹 + mGTaskListHashMap.put(tasklist.getGid(), tasklist); // 加入远程文件夹哈希表 } - n = (Node) tasklist; + n = (Node) tasklist; // 赋值给节点对象 } - // update local note + // 更新本地便签的GTaskID,提交更新 sqlNote.setGtaskId(n.getGid()); - sqlNote.commit(false); - sqlNote.resetLocalModified(); - sqlNote.commit(true); + sqlNote.commit(false); // 先提交GTaskID + sqlNote.resetLocalModified(); // 重置本地修改标记 + sqlNote.commit(true); // 提交修改标记的更新 - // gid-id mapping + // 更新GTaskID和本地ID的映射关系 mGidToNid.put(n.getGid(), sqlNote.getId()); mNidToGid.put(sqlNote.getId(), n.getGid()); } + /** + * 更新远程节点:用本地更新的便签/文件夹数据,更新GTask服务器对应的节点 + * @param node 远程待更新的GTask节点 + * @param c 本地数据库查询游标(存储本地更新后的便签/文件夹数据) + * @throws NetworkFailureException 网络失败时抛出 + */ private void updateRemoteNode(Node node, Cursor c) throws NetworkFailureException { + // 如果用户取消了同步,直接返回 if (mCancelled) { return; } + // 用本地更新后的数据初始化便签操作对象 SqlNote sqlNote = new SqlNote(mContext, c); - // update remotely + // 用本地数据更新远程节点内容 node.setContentByLocalJSON(sqlNote.getContent()); - GTaskClient.getInstance().addUpdateNode(node); + GTaskClient.getInstance().addUpdateNode(node); // 添加到远程待更新列表 - // update meta + // 更新远程元数据(记录本地便签的最新信息) updateRemoteMeta(node.getGid(), sqlNote); - // move task if necessary + // 如果是普通便签,还需要处理任务移动(父文件夹变化) if (sqlNote.isNoteType()) { Task task = (Task) node; - TaskList preParentList = task.getParent(); + TaskList preParentList = task.getParent(); // 任务原来的远程父文件夹 + // 获取本地更新后的父文件夹对应的GTaskID String curParentGid = mNidToGid.get(sqlNote.getParentId()); if (curParentGid == null) { + // 找不到父文件夹的GTaskID,抛出操作失败异常 Log.e(TAG, "cannot find task's parent tasklist"); throw new ActionFailureException("cannot update remote task"); } - TaskList curParentList = mGTaskListHashMap.get(curParentGid); + TaskList curParentList = mGTaskListHashMap.get(curParentGid); // 任务新的远程父文件夹 + // 如果父文件夹发生变化(任务被移动) if (preParentList != curParentList) { - preParentList.removeChildTask(task); - curParentList.addChildTask(task); - GTaskClient.getInstance().moveTask(task, preParentList, curParentList); + preParentList.removeChildTask(task); // 从原来的父文件夹移除任务 + curParentList.addChildTask(task); // 添加到新的父文件夹 + GTaskClient.getInstance().moveTask(task, preParentList, curParentList); // 在GTask服务器移动任务 } } - // clear local modified flag + // 重置本地修改标记,提交更新 sqlNote.resetLocalModified(); sqlNote.commit(true); } + /** + * 更新远程元数据:把本地便签的信息存储到GTask的元数据列表中 + * @param gid 便签对应的GTaskID + * @param sqlNote 本地便签操作对象 + * @throws NetworkFailureException 网络失败时抛出 + */ private void updateRemoteMeta(String gid, SqlNote sqlNote) throws NetworkFailureException { + // 只给普通便签更新元数据 if (sqlNote != null && sqlNote.isNoteType()) { - MetaData metaData = mMetaHashMap.get(gid); + MetaData metaData = mMetaHashMap.get(gid); // 获取已有的元数据 if (metaData != null) { + // 元数据已存在,更新内容并添加到远程待更新列表 metaData.setMeta(gid, sqlNote.getContent()); GTaskClient.getInstance().addUpdateNode(metaData); } else { + // 元数据不存在,创建新的元数据并提交到GTask服务器 metaData = new MetaData(); metaData.setMeta(gid, sqlNote.getContent()); - mMetaList.addChildTask(metaData); - mMetaHashMap.put(gid, metaData); - GTaskClient.getInstance().createTask(metaData); + mMetaList.addChildTask(metaData); // 添加到元数据列表 + mMetaHashMap.put(gid, metaData); // 加入元数据哈希表 + GTaskClient.getInstance().createTask(metaData); // 创建远程元数据 } } } + /** + * 刷新本地同步ID:把GTask节点的最后修改时间,更新到本地便签的同步ID字段 + * 通俗说:记录本地便签和远程GTask的同步状态,方便下次同步判断是否有修改 + * @throws NetworkFailureException 网络失败时抛出 + */ private void refreshLocalSyncId() throws NetworkFailureException { + // 如果用户取消了同步,直接返回 if (mCancelled) { return; } - // get the latest gtask list + // 重新获取最新的远程GTask列表(确保数据是最新的) mGTaskHashMap.clear(); mGTaskListHashMap.clear(); mMetaHashMap.clear(); initGTaskList(); - Cursor c = null; + Cursor c = null; // 数据库查询游标 try { + // 查询本地非系统、非回收站的便签/文件夹 c = mContentResolver.query(Notes.CONTENT_NOTE_URI, SqlNote.PROJECTION_NOTE, "(type<>? AND parent_id<>?)", new String[] { String.valueOf(Notes.TYPE_SYSTEM), String.valueOf(Notes.ID_TRASH_FOLER) }, NoteColumns.TYPE + " DESC"); if (c != null) { + // 逐条读取查询结果 while (c.moveToNext()) { - String gid = c.getString(SqlNote.GTASK_ID_COLUMN); - Node node = mGTaskHashMap.get(gid); + String gid = c.getString(SqlNote.GTASK_ID_COLUMN); // 获取便签对应的GTaskID + Node node = mGTaskHashMap.get(gid); // 获取远程节点 if (node != null) { + // 远程节点存在,从节点哈希表移除 mGTaskHashMap.remove(gid); + // 准备更新的数据:同步ID=远程节点的最后修改时间 ContentValues values = new ContentValues(); values.put(NoteColumns.SYNC_ID, node.getLastModified()); + // 更新本地便签的同步ID mContentResolver.update(ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, c.getLong(SqlNote.ID_COLUMN)), values, null, null); } else { + // 远程节点不存在,说明同步有遗漏,抛出操作失败异常 Log.e(TAG, "something is missed"); throw new ActionFailureException( "some local items don't have gid after sync"); } } } else { + // 查询失败,打印警告日志 Log.w(TAG, "failed to query local note to refresh sync id"); } } finally { + // 关闭游标,释放资源 if (c != null) { c.close(); c = null; @@ -790,11 +981,20 @@ public class GTaskManager { } } + /** + * 获取当前同步的谷歌账号名称 + * 通俗说:返回正在用来同步GTask的谷歌账号(比如xxx@gmail.com) + * @return 谷歌账号名称 + */ public String getSyncAccount() { return GTaskClient.getInstance().getSyncAccount().name; } + /** + * 取消同步:标记同步为已取消,同步过程中会检测该标记并停止 + * 通俗说:用户不想同步了,调用这个方法就能停止正在执行的同步任务 + */ public void cancelSync() { mCancelled = true; } -} +} \ No newline at end of file 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..d9f20e0 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 @@ -16,113 +16,206 @@ package net.micode.notes.gtask.remote; -import android.app.Activity; -import android.app.Service; -import android.content.Context; -import android.content.Intent; -import android.os.Bundle; -import android.os.IBinder; - +import android.app.Activity; // 页面类:外部启动同步时需要关联的页面 +import android.app.Service; // 后台服务类:在后台执行耗时操作(同步),不占用前台页面资源 +import android.content.Context; // 上下文:保存页面/服务信息,用来启动服务 +import android.content.Intent; // 意图:用来传递指令(启动/取消同步)、发送广播 +import android.os.Bundle; // 数据容器:用来存储意图中的额外参数(比如同步操作类型) +import android.os.IBinder; // 绑定接口:服务绑定相关(这里用不到) + +/** + * GTask同步后台服务类 + * 通俗说:这个类是后台运行的服务,专门负责管理GTask同步(启动同步、取消同步), + * 还会发送广播,把同步状态(是否在同步)和进度(比如“正在登录”)通知给前台界面 + * 特点:在后台运行,即使前台页面关闭,同步也能继续执行 + */ public class GTaskSyncService extends Service { + // 意图中存储“同步操作类型”的键名:用来区分是“启动同步”还是“取消同步” public final static String ACTION_STRING_NAME = "sync_action_type"; + // 同步操作类型:启动同步 public final static int ACTION_START_SYNC = 0; - + // 同步操作类型:取消同步 public final static int ACTION_CANCEL_SYNC = 1; - + // 同步操作类型:无效操作(传递错误指令时用) 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"; + // 静态变量:当前正在执行的同步异步任务(整个APP共享,确保只有一个同步任务在运行) private static GTaskASyncTask mSyncTask = null; - + // 静态变量:当前同步进度提示文字(比如“正在初始化列表”“正在同步”) private static String mSyncProgress = ""; + /** + * 启动同步任务 + * 通俗说:创建并执行同步异步任务,确保同一时间只有一个同步任务在运行 + */ private void startSync() { + // 如果当前没有正在执行的同步任务,才创建新任务(避免重复同步) if (mSyncTask == null) { + // 创建GTask同步异步任务,传入服务上下文和任务完成监听器 mSyncTask = new GTaskASyncTask(this, new GTaskASyncTask.OnCompleteListener() { + /** + * 同步任务完成后的回调方法 + * 通俗说:同步不管成功、失败还是被取消,都会执行这个方法 + */ public void onComplete() { - mSyncTask = null; - sendBroadcast(""); - stopSelf(); + mSyncTask = null; // 同步完成,清空当前任务引用 + sendBroadcast(""); // 发送空进度广播,通知界面同步已结束 + stopSelf(); // 停止当前服务(同步完成,不需要后台服务了) } }); - sendBroadcast(""); - mSyncTask.execute(); + sendBroadcast(""); // 发送广播,通知界面同步已开始 + mSyncTask.execute(); // 执行同步异步任务(在后台开始同步) } } + /** + * 取消同步任务 + * 通俗说:如果有正在执行的同步任务,就停止它 + */ private void cancelSync() { + // 如果当前有正在执行的同步任务 if (mSyncTask != null) { + // 调用异步任务的取消方法,终止同步 mSyncTask.cancelSync(); } } + /** + * 服务创建时的初始化方法 + * 通俗说:这个服务第一次被创建时,会执行这里的代码,初始化同步任务为null + */ @Override public void onCreate() { - mSyncTask = null; + mSyncTask = null; // 初始化当前同步任务为null(还没有同步任务执行) } + /** + * 服务启动时的处理逻辑 + * 通俗说:外部通过意图启动服务时,会执行这里的代码,根据意图中的指令执行“启动”或“取消”同步 + * @param intent 外部传递的意图(包含同步操作类型) + * @param flags 服务启动标记(系统使用) + * @param startId 服务启动ID(系统使用) + * @return 服务启动模式(START_STICKY=服务被意外杀死后,系统会尝试重启) + */ @Override public int onStartCommand(Intent intent, int flags, int startId) { + // 获取意图中的额外参数(存储了同步操作类型) Bundle bundle = intent.getExtras(); + // 如果参数不为空,且包含“同步操作类型”的键 if (bundle != null && bundle.containsKey(ACTION_STRING_NAME)) { + // 根据同步操作类型执行对应逻辑 switch (bundle.getInt(ACTION_STRING_NAME, ACTION_INVALID)) { case ACTION_START_SYNC: + // 收到“启动同步”指令,执行启动同步方法 startSync(); break; case ACTION_CANCEL_SYNC: + // 收到“取消同步”指令,执行取消同步方法 cancelSync(); break; default: + // 收到无效指令,不做处理 break; } + // 返回START_STICKY:服务被意外杀死(比如内存不足),系统会尝试重启服务 return START_STICKY; } + // 如果没有有效参数,执行父类默认逻辑 return super.onStartCommand(intent, flags, startId); } + /** + * 手机内存不足时的处理方法 + * 通俗说:当手机内存不够用的时候,系统会调用这个方法,这里选择取消同步来释放内存 + */ @Override public void onLowMemory() { + // 如果有正在执行的同步任务,就取消它,释放内存 if (mSyncTask != null) { mSyncTask.cancelSync(); } } + /** + * 绑定服务的方法 + * 通俗说:这个服务不需要被前台界面绑定,所以直接返回null + * @param intent 绑定服务的意图 + * @return null(表示不支持绑定) + */ public IBinder onBind(Intent intent) { return null; } + /** + * 发送同步状态广播 + * 通俗说:把当前的同步状态(是否在同步)和进度提示,通过广播发送给前台界面,让界面更新显示 + * @param msg 同步进度提示文字(比如“正在登录”“正在同步”) + */ public void sendBroadcast(String msg) { - mSyncProgress = 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); } + /** + * 静态方法:外部启动GTask同步的快捷方法 + * 通俗说:前台界面可以直接调用这个方法,快速启动GTask同步服务 + * @param activity 关联的页面(用来获取谷歌账号登录凭证) + */ public static void startSync(Activity activity) { + // 给GTask管理器设置关联页面(用于登录) GTaskManager.getInstance().setActivityContext(activity); + // 创建启动服务的意图,指定服务类 Intent intent = new Intent(activity, GTaskSyncService.class); + // 存入“启动同步”的操作类型 intent.putExtra(GTaskSyncService.ACTION_STRING_NAME, GTaskSyncService.ACTION_START_SYNC); + // 启动后台服务(开始同步) activity.startService(intent); } + /** + * 静态方法:外部取消GTask同步的快捷方法 + * 通俗说:前台界面可以直接调用这个方法,快速取消正在执行的同步 + * @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 mSyncTask != null; // 有同步任务就是正在同步 } + /** + * 静态方法:获取当前同步进度提示文字 + * 通俗说:前台界面可以调用这个方法,获取同步进度(用来显示在界面上,比如“正在初始化列表”) + * @return 同步进度提示文字 + */ public static String getProgressString() { return mSyncProgress; } -} +} \ No newline at end of file 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..8c569c8 100644 --- a/src/Notes-master/src/net/micode/notes/model/Note.java +++ b/src/Notes-master/src/net/micode/notes/model/Note.java @@ -15,143 +15,239 @@ */ package net.micode.notes.model; -import android.content.ContentProviderOperation; -import android.content.ContentProviderResult; -import android.content.ContentUris; -import android.content.ContentValues; -import android.content.Context; -import android.content.OperationApplicationException; -import android.net.Uri; -import android.os.RemoteException; -import android.util.Log; - -import net.micode.notes.data.Notes; -import net.micode.notes.data.Notes.CallNote; -import net.micode.notes.data.Notes.DataColumns; -import net.micode.notes.data.Notes.NoteColumns; -import net.micode.notes.data.Notes.TextNote; - -import java.util.ArrayList; - +import android.content.ContentProviderOperation; // 数据库批量操作指令:用来批量执行数据库更新操作 +import android.content.ContentProviderResult; // 数据库批量操作结果:存储批量操作的执行结果 +import android.content.ContentUris; // URI拼接工具:用来拼接便签/数据的数据库访问地址 +import android.content.ContentValues; // 数据键值对容器:用来存储要写入数据库的字段和对应值 +import android.content.Context; // 上下文:用来获取本地数据库操作工具 +import android.content.OperationApplicationException; // 批量操作异常:批量执行数据库操作失败时抛出 +import android.net.Uri; // 数据库访问地址:标识数据库中的某类数据(比如便签表、便签数据表) +import android.os.RemoteException; // 远程操作异常:跨进程访问数据库失败时抛出 +import android.util.Log; // 日志工具:打印错误信息,方便排查问题 + +import net.micode.notes.data.Notes; // 便签常量类:存储便签类型、数据库地址等固定值 +import net.micode.notes.data.Notes.CallNote; // 通话便签常量:存储通话便签的MIME类型等 +import net.micode.notes.data.Notes.DataColumns; // 便签数据列:便签数据表的字段名(比如所属便签ID、内容) +import net.micode.notes.data.Notes.NoteColumns; // 便签主列:便签主表的字段名(比如创建时间、修改时间) +import net.micode.notes.data.Notes.TextNote; // 文本便签常量:存储文本便签的MIME类型等 + +import java.util.ArrayList; // 数组列表:用来存储批量操作指令 + +/** + * 本地便签数据模型类 + * 通俗说:这个类是本地便签的“数据管家”,负责创建新便签ID、存储便签的主信息(创建时间、所属文件夹等) + * 和具体数据(文本内容、通话记录内容),还能把便签的修改同步到本地数据库 + */ 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 + // 1. 准备新便签的默认数据(存储到便签主表) ContentValues values = new ContentValues(); - long createdTime = System.currentTimeMillis(); - values.put(NoteColumns.CREATED_DATE, createdTime); - values.put(NoteColumns.MODIFIED_DATE, createdTime); - values.put(NoteColumns.TYPE, Notes.TYPE_NOTE); - values.put(NoteColumns.LOCAL_MODIFIED, 1); - values.put(NoteColumns.PARENT_ID, folderId); + long createdTime = System.currentTimeMillis(); // 获取当前时间作为创建时间 + values.put(NoteColumns.CREATED_DATE, createdTime); // 存入创建时间 + values.put(NoteColumns.MODIFIED_DATE, createdTime); // 存入修改时间(初始和创建时间一致) + values.put(NoteColumns.TYPE, Notes.TYPE_NOTE); // 存入便签类型(普通文本便签) + values.put(NoteColumns.LOCAL_MODIFIED, 1); // 存入本地修改标记(1=已修改,需要同步) + values.put(NoteColumns.PARENT_ID, folderId); // 存入所属文件夹ID + + // 2. 插入新便签到数据库,获取数据库返回的访问地址(包含新便签ID) Uri uri = context.getContentResolver().insert(Notes.CONTENT_NOTE_URI, values); + // 3. 从访问地址中解析出新便签ID long noteId = 0; try { + // 数据库返回的Uri格式是:content://xxx/notes/[noteId],取第2个分段就是便签ID noteId = Long.valueOf(uri.getPathSegments().get(1)); } catch (NumberFormatException e) { + // 解析ID失败,打印错误日志 Log.e(TAG, "Get note id error :" + e.toString()); noteId = 0; } + + // 4. 校验便签ID是否有效(-1表示插入失败) if (noteId == -1) { throw new IllegalStateException("Wrong note id:" + noteId); } return noteId; } + /** + * 构造方法:初始化便签对象 + * 通俗说:创建Note对象时,自动初始化便签主信息容器和具体数据管理对象 + */ public Note() { - mNoteDiffValues = new ContentValues(); - mNoteData = new NoteData(); + mNoteDiffValues = new ContentValues(); // 初始化便签主信息修改容器 + mNoteData = new NoteData(); // 初始化便签具体数据管理对象 } + /** + * 设置便签主信息字段 + * 通俗说:修改便签的主信息(比如标题、所属文件夹),同时标记为“已修改”并更新修改时间 + * @param key 便签主表的字段名(比如NoteColumns.TITLE) + * @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()); + mNoteDiffValues.put(key, value); // 存入要修改的字段和值 + mNoteDiffValues.put(NoteColumns.LOCAL_MODIFIED, 1); // 标记为本地已修改(需要同步到GTask) + mNoteDiffValues.put(NoteColumns.MODIFIED_DATE, System.currentTimeMillis()); // 更新修改时间为当前时间 } + /** + * 设置便签文本数据字段 + * 通俗说:修改便签的文本内容相关字段(比如正文文字) + * @param key 便签数据表的文本字段名(比如TextNote.CONTENT) + * @param value 文本字段对应的值(比如便签正文) + */ public void setTextData(String key, String value) { - mNoteData.setTextData(key, value); + mNoteData.setTextData(key, value); // 委托给NoteData对象处理 } + /** + * 设置文本数据的ID + * 通俗说:给便签的文本数据设置唯一ID(对应数据库里的文本数据记录ID) + * @param id 文本数据ID + */ public void setTextDataId(long id) { - mNoteData.setTextDataId(id); + mNoteData.setTextDataId(id); // 委托给NoteData对象处理 } + /** + * 获取文本数据的ID + * 通俗说:获取便签文本数据对应的数据库记录ID + * @return 文本数据ID + */ public long getTextDataId() { - return mNoteData.mTextDataId; + return mNoteData.mTextDataId; // 从NoteData对象中获取 } + /** + * 设置通话数据的ID + * 通俗说:给便签的通话记录数据设置唯一ID(对应数据库里的通话数据记录ID) + * @param id 通话数据ID + */ public void setCallDataId(long id) { - mNoteData.setCallDataId(id); + mNoteData.setCallDataId(id); // 委托给NoteData对象处理 } + /** + * 设置便签通话数据字段 + * 通俗说:修改便签的通话记录相关字段(比如通话号码、通话时间) + * @param key 便签数据表的通话字段名(比如CallNote.NUMBER) + * @param value 通话字段对应的值(比如10086) + */ public void setCallData(String key, String value) { - mNoteData.setCallData(key, value); + mNoteData.setCallData(key, value); // 委托给NoteData对象处理 } + /** + * 判断便签是否有本地修改 + * 通俗说:检查便签的主信息或具体数据是否有修改,用来判断是否需要同步到数据库/GTask + * @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) { + // 1. 校验便签ID是否有效(必须大于0) if (noteId <= 0) { throw new IllegalArgumentException("Wrong note id:" + noteId); } + // 2. 如果没有本地修改,直接返回成功(无需同步) if (!isLocalModified()) { return true; } /** - * 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 + * 理论上:只要便签数据有修改,就必须更新“本地修改标记”和“修改时间” + * 为了数据安全:即使便签主信息更新失败,也要尝试更新便签具体数据 */ + // 3. 更新便签主信息到数据库 if (context.getContentResolver().update( - ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, noteId), mNoteDiffValues, null, - null) == 0) { + ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, noteId), // 拼接该便签的数据库访问地址 + mNoteDiffValues, // 要更新的主信息数据 + null, null) == 0) { + // 更新返回0表示失败,打印错误日志(理论上不该发生) Log.e(TAG, "Update note error, should not happen"); - // Do not return, fall through + // 不返回,继续尝试更新具体数据 } - mNoteDiffValues.clear(); + mNoteDiffValues.clear(); // 主信息更新完成(无论成败),清空容器 + // 4. 更新便签具体数据到数据库 if (mNoteData.isLocalModified() && (mNoteData.pushIntoContentResolver(context, noteId) == null)) { + // 具体数据有修改 且 更新失败,返回同步失败 return false; } + // 5. 所有修改同步完成,返回成功 return true; } + /** + * 内部类:便签具体数据管理类 + * 通俗说:专门负责管理便签的文本数据和通话记录数据,处理这两类数据的存储和数据库同步 + */ private class NoteData { + // 文本数据ID:对应数据库里文本便签数据的唯一ID private long mTextDataId; - + // 文本数据修改容器:存储文本数据的修改字段和值 private ContentValues mTextDataValues; - + // 通话数据ID:对应数据库里通话便签数据的唯一ID private long mCallDataId; - + // 通话数据修改容器:存储通话数据的修改字段和值 private ContentValues mCallDataValues; - + // 日志标签:打印该内部类的错误日志 private static final String TAG = "NoteData"; + /** + * 构造方法:初始化便签具体数据管理对象 + */ public NoteData() { - mTextDataValues = new ContentValues(); - mCallDataValues = new ContentValues(); - mTextDataId = 0; - mCallDataId = 0; + mTextDataValues = new ContentValues(); // 初始化文本数据容器 + mCallDataValues = new ContentValues(); // 初始化通话数据容器 + mTextDataId = 0; // 初始文本数据ID为0(未关联数据库记录) + mCallDataId = 0; // 初始通话数据ID为0(未关联数据库记录) } + /** + * 判断具体数据是否有本地修改 + * 通俗说:检查文本数据或通话数据是否有修改 + * @return true=有修改,false=无修改 + */ boolean isLocalModified() { + // 文本数据容器有数据 或 通话数据容器有数据,就表示有修改 return mTextDataValues.size() > 0 || mCallDataValues.size() > 0; } + /** + * 设置文本数据ID + * 通俗说:给文本数据绑定数据库记录ID,ID必须大于0 + * @param id 文本数据ID + */ void setTextDataId(long id) { if(id <= 0) { throw new IllegalArgumentException("Text data id should larger than 0"); @@ -159,6 +255,11 @@ public class Note { mTextDataId = id; } + /** + * 设置通话数据ID + * 通俗说:给通话数据绑定数据库记录ID,ID必须大于0 + * @param id 通话数据ID + */ void setCallDataId(long id) { if (id <= 0) { throw new IllegalArgumentException("Call data id should larger than 0"); @@ -166,88 +267,122 @@ public class Note { mCallDataId = id; } + /** + * 设置通话数据字段 + * 通俗说:存储通话数据的修改字段和值,同时标记便签主信息为“已修改” + * @param key 通话数据字段名(比如CallNote.DATE) + * @param value 通话数据字段值(比如通话时间戳) + */ void setCallData(String key, String value) { - mCallDataValues.put(key, value); + mCallDataValues.put(key, value); // 存入通话数据修改 + // 同步更新便签主信息的“本地修改标记”和“修改时间” mNoteDiffValues.put(NoteColumns.LOCAL_MODIFIED, 1); mNoteDiffValues.put(NoteColumns.MODIFIED_DATE, System.currentTimeMillis()); } + /** + * 设置文本数据字段 + * 通俗说:存储文本数据的修改字段和值,同时标记便签主信息为“已修改” + * @param key 文本数据字段名(比如TextNote.CONTENT) + * @param value 文本数据字段值(比如便签正文) + */ void setTextData(String key, String value) { - mTextDataValues.put(key, value); + mTextDataValues.put(key, value); // 存入文本数据修改 + // 同步更新便签主信息的“本地修改标记”和“修改时间” mNoteDiffValues.put(NoteColumns.LOCAL_MODIFIED, 1); mNoteDiffValues.put(NoteColumns.MODIFIED_DATE, System.currentTimeMillis()); } + /** + * 将具体数据同步到本地数据库 + * 通俗说:把文本数据和通话数据的修改,写入本地数据库(新增或更新数据记录) + * @param context 上下文(获取数据库操作工具用) + * @param noteId 所属便签ID + * @return 同步后的便签URI(null表示同步失败) + */ Uri pushIntoContentResolver(Context context, long noteId) { - /** - * Check for safety - */ + // 1. 校验便签ID是否有效 if (noteId <= 0) { throw new IllegalArgumentException("Wrong note id:" + noteId); } + // 2. 准备数据库批量操作指令列表(批量执行更新,效率更高) ArrayList operationList = new ArrayList(); - ContentProviderOperation.Builder builder = null; + ContentProviderOperation.Builder builder = null; // 批量操作指令构建器 + // 3. 处理文本数据同步 if(mTextDataValues.size() > 0) { - mTextDataValues.put(DataColumns.NOTE_ID, noteId); + mTextDataValues.put(DataColumns.NOTE_ID, noteId); // 绑定所属便签ID if (mTextDataId == 0) { - mTextDataValues.put(DataColumns.MIME_TYPE, TextNote.CONTENT_ITEM_TYPE); - Uri uri = context.getContentResolver().insert(Notes.CONTENT_DATA_URI, - mTextDataValues); + // 文本数据ID为0:表示是新增文本数据,插入到数据库 + mTextDataValues.put(DataColumns.MIME_TYPE, TextNote.CONTENT_ITEM_TYPE); // 标记为文本数据类型 + Uri uri = context.getContentResolver().insert(Notes.CONTENT_DATA_URI, mTextDataValues); try { + // 解析插入后返回的ID,绑定到文本数据 setTextDataId(Long.valueOf(uri.getPathSegments().get(1))); } catch (NumberFormatException e) { + // 插入失败,打印日志并清空数据容器 Log.e(TAG, "Insert new text data fail with noteId" + noteId); mTextDataValues.clear(); return null; } } else { + // 文本数据ID不为0:表示是更新已有文本数据,添加批量更新指令 builder = ContentProviderOperation.newUpdate(ContentUris.withAppendedId( - Notes.CONTENT_DATA_URI, mTextDataId)); - builder.withValues(mTextDataValues); - operationList.add(builder.build()); + Notes.CONTENT_DATA_URI, mTextDataId)); // 拼接文本数据的数据库地址 + builder.withValues(mTextDataValues); // 设置要更新的数据 + operationList.add(builder.build()); // 添加到批量操作列表 } - mTextDataValues.clear(); + mTextDataValues.clear(); // 文本数据处理完成,清空容器 } + // 4. 处理通话数据同步(逻辑和文本数据一致) if(mCallDataValues.size() > 0) { - mCallDataValues.put(DataColumns.NOTE_ID, noteId); + mCallDataValues.put(DataColumns.NOTE_ID, noteId); // 绑定所属便签ID if (mCallDataId == 0) { - mCallDataValues.put(DataColumns.MIME_TYPE, CallNote.CONTENT_ITEM_TYPE); - Uri uri = context.getContentResolver().insert(Notes.CONTENT_DATA_URI, - mCallDataValues); + // 通话数据ID为0:新增通话数据,插入到数据库 + mCallDataValues.put(DataColumns.MIME_TYPE, CallNote.CONTENT_ITEM_TYPE); // 标记为通话数据类型 + Uri uri = context.getContentResolver().insert(Notes.CONTENT_DATA_URI, mCallDataValues); try { + // 解析插入后返回的ID,绑定到通话数据 setCallDataId(Long.valueOf(uri.getPathSegments().get(1))); } catch (NumberFormatException e) { + // 插入失败,打印日志并清空数据容器 Log.e(TAG, "Insert new call data fail with noteId" + noteId); mCallDataValues.clear(); return null; } } else { + // 通话数据ID不为0:更新已有通话数据,添加批量更新指令 builder = ContentProviderOperation.newUpdate(ContentUris.withAppendedId( - Notes.CONTENT_DATA_URI, mCallDataId)); - builder.withValues(mCallDataValues); - operationList.add(builder.build()); + Notes.CONTENT_DATA_URI, mCallDataId)); // 拼接通话数据的数据库地址 + builder.withValues(mCallDataValues); // 设置要更新的数据 + operationList.add(builder.build()); // 添加到批量操作列表 } - mCallDataValues.clear(); + mCallDataValues.clear(); // 通话数据处理完成,清空容器 } + // 5. 执行批量更新操作(如果有更新指令) if (operationList.size() > 0) { try { + // 批量执行数据库操作 ContentProviderResult[] results = context.getContentResolver().applyBatch( Notes.AUTHORITY, operationList); + // 判断操作结果是否有效,返回便签的数据库地址 return (results == null || results.length == 0 || results[0] == null) ? null : ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, noteId); } catch (RemoteException e) { + // 跨进程访问失败,打印日志并返回null Log.e(TAG, String.format("%s: %s", e.toString(), e.getMessage())); return null; } catch (OperationApplicationException e) { + // 批量操作执行失败,打印日志并返回null Log.e(TAG, String.format("%s: %s", e.toString(), e.getMessage())); return null; } } + // 没有批量更新指令,返回null(表示无需更新或更新完成) return null; } } -} +} \ No newline at end of file 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..e3fdd05 100644 --- a/src/Notes-master/src/net/micode/notes/model/WorkingNote.java +++ b/src/Notes-master/src/net/micode/notes/model/WorkingNote.java @@ -16,121 +16,131 @@ package net.micode.notes.model; -import android.appwidget.AppWidgetManager; -import android.content.ContentUris; -import android.content.Context; -import android.database.Cursor; -import android.text.TextUtils; -import android.util.Log; - -import net.micode.notes.data.Notes; -import net.micode.notes.data.Notes.CallNote; -import net.micode.notes.data.Notes.DataColumns; -import net.micode.notes.data.Notes.DataConstants; -import net.micode.notes.data.Notes.NoteColumns; -import net.micode.notes.data.Notes.TextNote; -import net.micode.notes.tool.ResourceParser.NoteBgResources; - - +import android.appwidget.AppWidgetManager; // 桌面小组件管理类:用于判断小组件ID是否有效 +import android.content.ContentUris; // URI拼接工具:拼接便签的数据库访问地址 +import android.content.Context; // 上下文:获取数据库操作工具和资源 +import android.database.Cursor; // 数据库查询结果游标:存储数据库查询返回的结果集 +import android.text.TextUtils; // 文本工具类:判断字符串是否为空/空白 +import android.util.Log; // 日志工具:打印错误/调试信息 + +import net.micode.notes.data.Notes; // 便签常量类:存储便签类型、文件夹ID等固定值 +import net.micode.notes.data.Notes.CallNote; // 通话便签常量:存储通话便签的字段名 +import net.micode.notes.data.Notes.DataColumns; // 便签数据表字段:数据表的列名 +import net.micode.notes.data.Notes.DataConstants; // 便签数据类型常量:区分文本/通话便签 +import net.micode.notes.data.Notes.NoteColumns; // 便签主表字段:主表的列名 +import net.micode.notes.data.Notes.TextNote; // 文本便签常量:存储文本便签的字段名 +import net.micode.notes.tool.ResourceParser.NoteBgResources; // 便签背景资源工具:根据颜色ID获取背景资源 + +/** + * 工作便签封装类 + * 通俗说:这个类是便签的“前台操作管家”,封装了便签的所有属性(内容、提醒时间、背景色等) + * 还负责加载已有便签、创建新便签、保存便签修改,以及通知界面便签设置的变化(比如背景色改变) + */ public class WorkingNote { - // Note for the working note + // 关联的Note数据对象:负责和本地数据库交互(存储/同步便签数据) private Note mNote; - // Note Id + // 便签唯一ID:对应数据库里的便签ID(0表示新便签,未存入数据库) private long mNoteId; - // Note content + // 便签内容:文本便签的正文文字 private String mContent; - // Note mode + // 便签模式:区分普通文本模式和清单模式(比如0=普通模式,1=清单模式) private int mMode; - + // 提醒时间戳:便签的提醒日期(0表示无提醒) private long mAlertDate; - + // 修改时间戳:便签最后一次修改的时间 private long mModifiedDate; - + // 背景颜色ID:便签的背景色标识(对应不同的背景样式) private int mBgColorId; - + // 桌面小组件ID:关联的桌面便签小组件ID(无效时为INVALID_APPWIDGET_ID) private int mWidgetId; - + // 桌面小组件类型:关联的桌面小组件类型(无效时为TYPE_WIDGET_INVALIDE) private int mWidgetType; - + // 所属文件夹ID:便签所在的文件夹ID(比如通话记录文件夹、自定义文件夹) private long mFolderId; - + // 上下文:用于获取数据库工具、资源等 private Context mContext; - + // 日志标签:打印该类的日志,方便排查问题 private static final String TAG = "WorkingNote"; - + // 删除标记:是否标记为已删除(true=已删除,无需保存) private boolean mIsDeleted; - + // 便签设置变化监听器:用于通知界面便签设置的改变(比如背景色、提醒时间变化) private NoteSettingChangedListener mNoteSettingStatusListener; + // 便签数据表查询投影:查询便签数据时,要获取的字段列表(相当于查询结果的列清单) public static final String[] DATA_PROJECTION = new String[] { - DataColumns.ID, - DataColumns.CONTENT, - DataColumns.MIME_TYPE, - DataColumns.DATA1, - DataColumns.DATA2, - DataColumns.DATA3, - DataColumns.DATA4, + DataColumns.ID, // 0: 数据记录ID + DataColumns.CONTENT, // 1: 数据内容(文本便签的正文) + DataColumns.MIME_TYPE, // 2: 数据类型(文本/通话便签) + DataColumns.DATA1, // 3: 扩展字段1(存储便签模式) + DataColumns.DATA2, // 4: 扩展字段2 + DataColumns.DATA3, // 5: 扩展字段3 + DataColumns.DATA4, // 6: 扩展字段4 }; + // 便签主表查询投影:查询便签主信息时,要获取的字段列表 public static final String[] NOTE_PROJECTION = new String[] { - NoteColumns.PARENT_ID, - NoteColumns.ALERTED_DATE, - NoteColumns.BG_COLOR_ID, - NoteColumns.WIDGET_ID, - NoteColumns.WIDGET_TYPE, - NoteColumns.MODIFIED_DATE + NoteColumns.PARENT_ID, // 0: 所属文件夹ID + NoteColumns.ALERTED_DATE,// 1: 提醒时间 + NoteColumns.BG_COLOR_ID, // 2: 背景颜色ID + NoteColumns.WIDGET_ID, // 3: 桌面小组件ID + NoteColumns.WIDGET_TYPE, // 4: 桌面小组件类型 + NoteColumns.MODIFIED_DATE// 5: 修改时间 }; - private static final int DATA_ID_COLUMN = 0; - - private static final int DATA_CONTENT_COLUMN = 1; - - private static final int DATA_MIME_TYPE_COLUMN = 2; - - private static final int DATA_MODE_COLUMN = 3; - - private static final int NOTE_PARENT_ID_COLUMN = 0; - - private static final int NOTE_ALERTED_DATE_COLUMN = 1; - - private static final int NOTE_BG_COLOR_ID_COLUMN = 2; - - private static final int NOTE_WIDGET_ID_COLUMN = 3; - - private static final int NOTE_WIDGET_TYPE_COLUMN = 4; - - private static final int NOTE_MODIFIED_DATE_COLUMN = 5; - - // New note construct + // 数据表查询结果的列索引:对应DATA_PROJECTION的字段位置,方便快速取值 + private static final int DATA_ID_COLUMN = 0; // 数据记录ID的索引 + private static final int DATA_CONTENT_COLUMN = 1; // 数据内容的索引 + private static final int DATA_MIME_TYPE_COLUMN = 2; // 数据类型的索引 + private static final int DATA_MODE_COLUMN = 3; // 便签模式的索引 + + // 主表查询结果的列索引:对应NOTE_PROJECTION的字段位置,方便快速取值 + private static final int NOTE_PARENT_ID_COLUMN = 0; // 所属文件夹ID的索引 + private static final int NOTE_ALERTED_DATE_COLUMN = 1; // 提醒时间的索引 + private static final int NOTE_BG_COLOR_ID_COLUMN = 2; // 背景颜色ID的索引 + private static final int NOTE_WIDGET_ID_COLUMN = 3; // 桌面小组件ID的索引 + private static final int NOTE_WIDGET_TYPE_COLUMN = 4; // 桌面小组件类型的索引 + private static final int NOTE_MODIFIED_DATE_COLUMN = 5; // 修改时间的索引 + + // 私有构造方法:创建新便签(未存入数据库) + // 通俗说:初始化一个空白便签,设置默认属性 private WorkingNote(Context context, long folderId) { mContext = context; - mAlertDate = 0; - mModifiedDate = System.currentTimeMillis(); - mFolderId = folderId; - mNote = new Note(); - mNoteId = 0; - mIsDeleted = false; - mMode = 0; - mWidgetType = Notes.TYPE_WIDGET_INVALIDE; + mAlertDate = 0; // 默认无提醒 + mModifiedDate = System.currentTimeMillis(); // 默认修改时间为当前时间 + mFolderId = folderId; // 设置所属文件夹ID + mNote = new Note(); // 创建关联的Note数据对象 + mNoteId = 0; // 新便签ID为0(未存入数据库) + mIsDeleted = false; // 默认未删除 + mMode = 0; // 默认普通文本模式 + mWidgetType = Notes.TYPE_WIDGET_INVALIDE; // 默认无有效桌面小组件 } - // Existing note construct + // 私有构造方法:加载已有便签(从数据库读取) + // 通俗说:根据便签ID,从数据库加载已有便签的所有信息 private WorkingNote(Context context, long noteId, long folderId) { mContext = context; - mNoteId = noteId; - mFolderId = folderId; - mIsDeleted = false; - mNote = new Note(); - loadNote(); + mNoteId = noteId; // 设置已有便签的ID + mFolderId = folderId; // 设置所属文件夹ID + mIsDeleted = false; // 默认未删除 + mNote = new Note(); // 创建关联的Note数据对象 + loadNote(); // 加载便签主信息 } + /** + * 加载便签主信息(从数据库主表读取) + * 通俗说:根据便签ID,从数据库便签主表中读取文件夹ID、背景色、提醒时间等主信息 + */ private void loadNote() { + // 拼接该便签的数据库访问地址,查询主表信息 Cursor cursor = mContext.getContentResolver().query( - ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, mNoteId), NOTE_PROJECTION, null, - null, null); + ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, mNoteId), + NOTE_PROJECTION, // 要查询的字段列表 + null, null, null); if (cursor != null) { + // 如果查询到结果,移动到第一条记录 if (cursor.moveToFirst()) { + // 从查询结果中读取各字段值,赋值给成员变量 mFolderId = cursor.getLong(NOTE_PARENT_ID_COLUMN); mBgColorId = cursor.getInt(NOTE_BG_COLOR_ID_COLUMN); mWidgetId = cursor.getInt(NOTE_WIDGET_ID_COLUMN); @@ -138,84 +148,135 @@ public class WorkingNote { mAlertDate = cursor.getLong(NOTE_ALERTED_DATE_COLUMN); mModifiedDate = cursor.getLong(NOTE_MODIFIED_DATE_COLUMN); } - cursor.close(); + cursor.close(); // 关闭游标,释放资源 } else { + // 未查询到便签,打印错误日志并抛出异常 Log.e(TAG, "No note with id:" + mNoteId); throw new IllegalArgumentException("Unable to find note with id " + mNoteId); } - loadNoteData(); + loadNoteData(); // 主信息加载完成后,加载便签具体数据 } + /** + * 加载便签具体数据(从数据库数据表读取) + * 通俗说:根据便签ID,从数据库便签数据表中读取文本内容、便签模式等具体数据 + */ private void loadNoteData() { - Cursor cursor = mContext.getContentResolver().query(Notes.CONTENT_DATA_URI, DATA_PROJECTION, - DataColumns.NOTE_ID + "=?", new String[] { - String.valueOf(mNoteId) - }, null); + // 查询该便签对应的所有数据记录(条件:NOTE_ID等于当前便签ID) + Cursor cursor = mContext.getContentResolver().query(Notes.CONTENT_DATA_URI, + DATA_PROJECTION, + DataColumns.NOTE_ID + "=?", // 查询条件 + new String[] { String.valueOf(mNoteId) }, // 条件参数 + null); if (cursor != null) { + // 如果查询到结果,移动到第一条记录 if (cursor.moveToFirst()) { + // 循环遍历所有数据记录(一个便签可能对应多条数据,比如文本+通话记录) do { - String type = cursor.getString(DATA_MIME_TYPE_COLUMN); + String type = cursor.getString(DATA_MIME_TYPE_COLUMN); // 获取数据类型 if (DataConstants.NOTE.equals(type)) { + // 文本便签:读取内容、模式,并绑定数据ID 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)) { + // 通话便签:绑定通话数据ID mNote.setCallDataId(cursor.getLong(DATA_ID_COLUMN)); } else { + // 未知数据类型,打印调试日志 Log.d(TAG, "Wrong note type with type:" + type); } - } while (cursor.moveToNext()); + } while (cursor.moveToNext()); // 移动到下一条记录,继续遍历 } - cursor.close(); + cursor.close(); // 关闭游标,释放资源 } else { + // 未查询到便签数据,打印错误日志并抛出异常 Log.e(TAG, "No data with id:" + mNoteId); throw new IllegalArgumentException("Unable to find note's data with id " + mNoteId); } } + /** + * 静态方法:创建空白便签(带默认配置) + * 通俗说:外部调用该方法,创建一个指定文件夹、小组件配置和默认背景色的空白便签 + * @param context 上下文 + * @param folderId 所属文件夹ID + * @param widgetId 桌面小组件ID + * @param widgetType 桌面小组件类型 + * @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); - note.setBgColorId(defaultBgColorId); - note.setWidgetId(widgetId); - note.setWidgetType(widgetType); + int widgetType, int defaultBgColorId) { + WorkingNote note = new WorkingNote(context, folderId); // 创建空白便签 + note.setBgColorId(defaultBgColorId); // 设置默认背景色 + note.setWidgetId(widgetId); // 设置桌面小组件ID + note.setWidgetType(widgetType); // 设置桌面小组件类型 return note; } + /** + * 静态方法:加载已有便签 + * 通俗说:外部调用该方法,根据便签ID加载数据库中的已有便签 + * @param context 上下文 + * @param id 便签ID + * @return 加载完成的WorkingNote对象 + */ public static WorkingNote load(Context context, long id) { return new WorkingNote(context, id, 0); } + /** + * 保存便签到数据库 + * 通俗说:判断便签是否值得保存,若值得则新建/更新数据库记录,并通知小组件更新 + * @return true=保存成功,false=保存失败/无需保存 + */ public synchronized boolean saveNote() { + // 先判断是否值得保存(已删除、空白新便签、无修改的旧便签都无需保存) if (isWorthSaving()) { + // 如果是新便签(未存入数据库),先生成新便签ID if (!existInDatabase()) { - if ((mNoteId = Note.getNewNoteId(mContext, mFolderId)) == 0) { + mNoteId = Note.getNewNoteId(mContext, mFolderId); + if (mNoteId == 0) { + // 生成新ID失败,打印错误日志并返回失败 Log.e(TAG, "Create new note fail with id:" + mNoteId); return false; } } + // 调用Note对象的同步方法,将修改写入数据库 mNote.syncNote(mContext, mNoteId); /** - * Update widget content if there exist any widget of this note + * 如果该便签关联了有效桌面小组件,且设置了监听器,通知小组件更新内容 */ if (mWidgetId != AppWidgetManager.INVALID_APPWIDGET_ID && mWidgetType != Notes.TYPE_WIDGET_INVALIDE && mNoteSettingStatusListener != null) { mNoteSettingStatusListener.onWidgetChanged(); } - return true; + return true; // 保存成功 } else { - return false; + return false; // 无需保存 } } + /** + * 判断便签是否已存入数据库 + * 通俗说:通过便签ID是否大于0,判断是否是已存在的便签 + * @return true=已存入数据库,false=新便签(未存入) + */ public boolean existInDatabase() { return mNoteId > 0; } + /** + * 判断便签是否值得保存 + * 通俗说:满足以下条件之一则无需保存: + * 1. 已标记为删除;2. 新便签且内容为空;3. 旧便签且无任何本地修改 + * @return true=值得保存,false=无需保存 + */ private boolean isWorthSaving() { if (mIsDeleted || (!existInDatabase() && TextUtils.isEmpty(mContent)) || (existInDatabase() && !mNote.isLocalModified())) { @@ -225,144 +286,185 @@ public class WorkingNote { } } + /** + * 设置便签设置变化监听器 + * 通俗说:给当前便签绑定监听器,以便便签设置变化时通知界面更新 + * @param l 监听器对象 + */ public void setOnSettingStatusChangedListener(NoteSettingChangedListener l) { mNoteSettingStatusListener = l; } + /** + * 设置便签提醒时间 + * 通俗说:修改便签的提醒时间,并通知监听器提醒时间已变化 + * @param date 新的提醒时间戳 + * @param set 是否设置提醒(true=设置,false=取消) + */ public void setAlertDate(long date, boolean set) { if (date != mAlertDate) { - mAlertDate = date; + mAlertDate = date; // 更新提醒时间 + // 将提醒时间存入Note对象,准备同步到数据库 mNote.setNoteValue(NoteColumns.ALERTED_DATE, String.valueOf(mAlertDate)); } + // 如果设置了监听器,通知提醒时间变化 if (mNoteSettingStatusListener != null) { mNoteSettingStatusListener.onClockAlertChanged(date, set); } } + /** + * 标记便签为已删除/未删除 + * 通俗说:设置便签的删除标记,若关联了桌面小组件,通知小组件更新 + * @param mark true=标记为已删除,false=取消删除标记 + */ public void markDeleted(boolean mark) { - mIsDeleted = mark; + mIsDeleted = mark; // 更新删除标记 + // 如果关联了有效桌面小组件且设置了监听器,通知小组件变化 if (mWidgetId != AppWidgetManager.INVALID_APPWIDGET_ID && mWidgetType != Notes.TYPE_WIDGET_INVALIDE && mNoteSettingStatusListener != null) { - mNoteSettingStatusListener.onWidgetChanged(); + mNoteSettingStatusListener.onWidgetChanged(); } } + /** + * 设置便签背景颜色ID + * 通俗说:修改便签的背景色,通知监听器背景色已变化,并准备同步到数据库 + * @param id 新的背景颜色ID + */ public void setBgColorId(int id) { if (id != mBgColorId) { - mBgColorId = id; + mBgColorId = id; // 更新背景颜色ID + // 如果设置了监听器,通知背景色变化 if (mNoteSettingStatusListener != null) { mNoteSettingStatusListener.onBackgroundColorChanged(); } + // 将背景颜色ID存入Note对象,准备同步到数据库 mNote.setNoteValue(NoteColumns.BG_COLOR_ID, String.valueOf(id)); } } + /** + * 设置便签模式(普通/清单) + * 通俗说:切换便签的显示模式,通知监听器模式变化,并准备同步到数据库 + * @param mode 新的便签模式(0=普通,1=清单等) + */ public void setCheckListMode(int mode) { if (mMode != mode) { + // 如果设置了监听器,通知模式变化(传入旧模式和新模式) if (mNoteSettingStatusListener != null) { mNoteSettingStatusListener.onCheckListModeChanged(mMode, mode); } - mMode = mode; + mMode = mode; // 更新便签模式 + // 将便签模式存入Note对象,准备同步到数据库 mNote.setTextData(TextNote.MODE, String.valueOf(mMode)); } } + /** + * 设置桌面小组件类型 + * 通俗说:修改便签关联的桌面小组件类型,并准备同步到数据库 + * @param type 新的小组件类型 + */ public void setWidgetType(int type) { if (type != mWidgetType) { - mWidgetType = type; + mWidgetType = type; // 更新小组件类型 + // 将小组件类型存入Note对象,准备同步到数据库 mNote.setNoteValue(NoteColumns.WIDGET_TYPE, String.valueOf(mWidgetType)); } } + /** + * 设置桌面小组件ID + * 通俗说:修改便签关联的桌面小组件ID,并准备同步到数据库 + * @param id 新的小组件ID + */ public void setWidgetId(int id) { if (id != mWidgetId) { - mWidgetId = id; + mWidgetId = id; // 更新小组件ID + // 将小组件ID存入Note对象,准备同步到数据库 mNote.setNoteValue(NoteColumns.WIDGET_ID, String.valueOf(mWidgetId)); } } + /** + * 设置便签文本内容 + * 通俗说:修改便签的正文内容,只有内容变化时才更新,并准备同步到数据库 + * @param text 新的便签内容 + */ public void setWorkingText(String text) { if (!TextUtils.equals(mContent, text)) { - mContent = text; + mContent = text; // 更新便签内容 + // 将新内容存入Note对象,准备同步到数据库 mNote.setTextData(DataColumns.CONTENT, mContent); } } + /** + * 转换为通话便签 + * 通俗说:将当前便签设置为通话便签,存入通话号码和通话时间,并指定到通话记录文件夹 + * @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)); } + /** + * 判断是否设置了提醒 + * 通俗说:通过提醒时间戳是否大于0,判断便签是否有有效提醒 + * @return true=有提醒,false=无提醒 + */ public boolean hasClockAlert() { return (mAlertDate > 0 ? true : false); } - public String getContent() { - return mContent; - } - - public long getAlertDate() { - return mAlertDate; - } - - public long getModifiedDate() { - return mModifiedDate; - } - - public int getBgColorResId() { - return NoteBgResources.getNoteBgResource(mBgColorId); - } - - public int getBgColorId() { - return mBgColorId; - } - - public int getTitleBgResId() { - return NoteBgResources.getNoteTitleBgResource(mBgColorId); - } - - public int getCheckListMode() { - return mMode; - } - - public long getNoteId() { - return mNoteId; - } - - public long getFolderId() { - return mFolderId; - } - - public int getWidgetId() { - return mWidgetId; - } - - public int getWidgetType() { - return mWidgetType; - } - + // 以下都是属性获取方法:返回对应的便签属性值,供外部界面使用 + public String getContent() { return mContent; } + public long getAlertDate() { return mAlertDate; } + public long getModifiedDate() { return mModifiedDate; } + public int getBgColorResId() { return NoteBgResources.getNoteBgResource(mBgColorId); } + public int getBgColorId() { return mBgColorId; } + public int getTitleBgResId() { return NoteBgResources.getNoteTitleBgResource(mBgColorId); } + public int getCheckListMode() { return mMode; } + public long getNoteId() { return mNoteId; } + public long getFolderId() { return mFolderId; } + public int getWidgetId() { return mWidgetId; } + 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 + * 便签提醒时间变化时的回调 + * 通俗说:当设置/取消便签提醒后,会调用该方法,界面可在此更新提醒相关UI */ void onClockAlertChanged(long date, boolean set); /** - * Call when user create note from 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); } -} +} \ No newline at end of file