diff --git a/src/src/net/micode/notes/data/Contact.java b/src/src/net/micode/notes/data/Contact.java
index d97ac5d..a3fb54b 100644
--- a/src/src/net/micode/notes/data/Contact.java
+++ b/src/src/net/micode/notes/data/Contact.java
@@ -25,48 +25,70 @@ import android.util.Log;
import java.util.HashMap;
+/**
+ * 联系人查询工具类
+ *
+ * 主要负责根据电话号码从Android数据库中检索联系人姓名。
+ *
+ *
+ * 核心功能:
+ *
+ * - 基于电话号码查找姓名。
+ * - 维护 {@code HashMap} 缓存,减少 IPC 调用次数。
+ * - 处理国际化号码格式匹配问题。
+ *
+ *
+ * @author 林迪文
+ * @version 1.0
+ * @see android.provider.ContactsContract
+ */
public class Contact {
+ //键值对,用于缓存<姓名,文本>,提高查找速度。
private static HashMap sContactCache;
+ //fianl意思是常量,TAG为Contact的“别名”。
private static final String TAG = "Contact";
+ //这是个SQL语句,CALLER_ID_SELECTION是个字符串,作用是构建匹配模式,用and连接了3个部分:电话匹配、数据类型需是电话、号码必须存在于快速查找表中
private static final String CALLER_ID_SELECTION = "PHONE_NUMBERS_EQUAL(" + Phone.NUMBER
+ ",?) AND " + Data.MIMETYPE + "='" + Phone.CONTENT_ITEM_TYPE + "'"
- + " AND " + Data.RAW_CONTACT_ID + " IN "
+ + " AND " + Data.RAW_CONTACT_ID + " IN " //Data哪来的
+ "(SELECT raw_contact_id "
+ " FROM phone_lookup"
+ " WHERE min_match = '+')";
+ //主函数,输入电话,根据context,返回人名
public static String getContact(Context context, String phoneNumber) {
if(sContactCache == null) {
sContactCache = new HashMap();
- }
+ } //初次调用getContact时,创建缓存
if(sContactCache.containsKey(phoneNumber)) {
return sContactCache.get(phoneNumber);
- }
+ } //缓存命中
String selection = CALLER_ID_SELECTION.replace("+",
- PhoneNumberUtils.toCallerIDMinMatch(phoneNumber));
+ PhoneNumberUtils.toCallerIDMinMatch(phoneNumber)); //用处理过的电话号码,替代CALLER_ID_SELECTION的+。
Cursor cursor = context.getContentResolver().query(
- Data.CONTENT_URI,
- new String [] { Phone.DISPLAY_NAME },
- selection,
- new String[] { phoneNumber },
- null);
+ Data.CONTENT_URI, //查找对象
+ new String [] { Phone.DISPLAY_NAME }, //需要返回的是名字这一列
+ selection, //查找条件
+ new String[] { phoneNumber }, //query可以将phoneNumber填到selection的'?'中
+ null); //不需排序
- if (cursor != null && cursor.moveToFirst()) {
+ if (cursor != null && cursor.moveToFirst()) { //cursor.moveToFirst()为true代表至少有一条记录
try {
String name = cursor.getString(0);
sContactCache.put(phoneNumber, name);
return name;
- } catch (IndexOutOfBoundsException e) {
+ } catch (IndexOutOfBoundsException e) { //越界报错
Log.e(TAG, " Cursor get string error " + e.toString());
return null;
} finally {
- cursor.close();
+ cursor.close(); //关闭进程,防止内存泄露
}
- } else {
- Log.d(TAG, "No contact matched with number:" + phoneNumber);
+ }
+ else {
+ Log.d(TAG, "No contact matched with number:" + phoneNumber); //没找到
return null;
}
}
diff --git a/src/src/net/micode/notes/data/Notes.java b/src/src/net/micode/notes/data/Notes.java
index f240604..7e3769f 100644
--- a/src/src/net/micode/notes/data/Notes.java
+++ b/src/src/net/micode/notes/data/Notes.java
@@ -17,7 +17,16 @@
package net.micode.notes.data;
import android.net.Uri;
+
+/**
+ * Notes类定义了许多常量,是整个项目的常量定义规范集
+ *
+ * @Author: 林迪文
+ * @Updator: 林迪文
+ * @Date 2025/12/11 11:03
+ */
public class Notes {
+ //定义项目名、TAG、文件类型
public static final String AUTHORITY = "micode_notes";
public static final String TAG = "Notes";
public static final int TYPE_NOTE = 0;
@@ -26,15 +35,16 @@ public class Notes {
/**
* Following IDs are system folders' identifiers
- * {@link Notes#ID_ROOT_FOLDER } is default folder
- * {@link Notes#ID_TEMPARAY_FOLDER } is for notes belonging no folder
- * {@link Notes#ID_CALL_RECORD_FOLDER} is to store call records
+ * {@link Notes#ID_ROOT_FOLDER } is default folder,根目录
+ * {@link Notes#ID_TEMPARAY_FOLDER } is for notes belonging no folder,临时文件夹
+ * {@link Notes#ID_CALL_RECORD_FOLDER} is to store call records,通话记录文件夹
*/
- public static final int ID_ROOT_FOLDER = 0;
+ public static final int ID_ROOT_FOLDER = 0; //也是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;
+ public static final int ID_TRASH_FOLER = -3;//垃圾文件夹
+ //这些可以作为intent.putExtra("键名", 值)的键名
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";
@@ -46,21 +56,24 @@ public class Notes {
public static final int TYPE_WIDGET_2X = 0;
public static final int TYPE_WIDGET_4X = 1;
+ //DataConstants存储两个类型常量:note和call_note,但是这个量不好
+ //改名成MimeTypes或者DataTypes更好
public static class DataConstants {
public static final String NOTE = TextNote.CONTENT_ITEM_TYPE;
public static final String CALL_NOTE = CallNote.CONTENT_ITEM_TYPE;
}
/**
- * Uri to query all notes and folders
+ * Uri to query all notes and folders,访问 NOTE 表的 Uri
*/
public static final Uri CONTENT_NOTE_URI = Uri.parse("content://" + AUTHORITY + "/note");
/**
- * Uri to query data
+ * Uri to query data,访问 DATA 表的 Uri
*/
public static final Uri CONTENT_DATA_URI = Uri.parse("content://" + AUTHORITY + "/data");
+ //与便签有关的基本信息
public interface NoteColumns {
/**
* The unique ID for a row
@@ -167,6 +180,7 @@ public class Notes {
public static final String VERSION = "version";
}
+ //便签的详细数据信息,Data1-Data5为通用数据栏,由mimetype决定意义
public interface DataColumns {
/**
* The unique ID for a row
@@ -176,6 +190,7 @@ public class Notes {
/**
* The MIME type of the item represented by this row.
+ * 这个值拿来区分text_notes和call_notes
* Type: Text
*/
public static final String MIME_TYPE = "mime_type";
@@ -241,6 +256,7 @@ public class Notes {
public static final String DATA5 = "data5";
}
+ //TextNote和CallNote为DataColumns的两个实现
public static final class TextNote implements DataColumns {
/**
* Mode to indicate the text in check list mode or not
@@ -248,6 +264,7 @@ public class Notes {
*/
public static final String MODE = DATA1;
+ //清单模式
public static final int MODE_CHECK_LIST = 1;
public static final String CONTENT_TYPE = "vnd.android.cursor.dir/text_note";
@@ -270,8 +287,10 @@ public class Notes {
*/
public static final String PHONE_NUMBER = DATA3;
+ //列表dir类型,用于通过Uri查找类型时返回
public static final String CONTENT_TYPE = "vnd.android.cursor.dir/call_note";
+ //单条数据item类型,用于通过Uri查找类型时返回
public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/call_note";
public static final Uri CONTENT_URI = Uri.parse("content://" + AUTHORITY + "/call_note");
diff --git a/src/src/net/micode/notes/data/NotesDatabaseHelper.java b/src/src/net/micode/notes/data/NotesDatabaseHelper.java
index ffe5d57..be0f476 100644
--- a/src/src/net/micode/notes/data/NotesDatabaseHelper.java
+++ b/src/src/net/micode/notes/data/NotesDatabaseHelper.java
@@ -26,185 +26,216 @@ import net.micode.notes.data.Notes.DataColumns;
import net.micode.notes.data.Notes.DataConstants;
import net.micode.notes.data.Notes.NoteColumns;
-
+/**
+ * 数据库操作核心类
+ *
+ * 负责 SQLite 数据库的创建、表结构初始化以及版本升级逻辑。
+ * 这个类大量使用了 SQLite 的 Trigger(触发器) 特性,
+ * 将“更新文件夹内笔记数量”、“同步笔记摘要”等业务逻辑下沉到数据库层自动处理,
+ * 避免了上层 Java 代码频繁手动同步数据,提高了数据一致性。
+ *
+ *
+ * @Author: 林迪文
+ * @Updator: 林迪文
+ * @Date 2025/12/10 15:10
+ */
public class NotesDatabaseHelper extends SQLiteOpenHelper {
+ // 数据库文件名,位于系统内部存储
private static final String DB_NAME = "note.db";
+ // 数据库版本号,升级数据库结构时需修改此值
private static final int DB_VERSION = 4;
+ // 定义表名常量
public interface TABLE {
- public static final String NOTE = "note";
+ public static final String NOTE = "note"; // 存储笔记的元数据(如ID、创建时间、父文件夹)
- public static final String DATA = "data";
+ public static final String DATA = "data"; // 存储笔记的具体内容(文本、关联数据)
}
private static final String TAG = "NotesDatabaseHelper";
private static NotesDatabaseHelper mInstance;
+ // 创建 NOTE 表的 SQL 语句
+ // strftime('%s','now') * 1000 用于获取当前的毫秒级时间戳,默认填充创建时间和修改时间
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 表的 SQL 语句
+ // data 表通过 note_id 关联到 note 表
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 创建索引,加速根据笔记 ID 查询内容的效率
private static final String CREATE_DATA_NOTE_ID_INDEX_SQL =
- "CREATE INDEX IF NOT EXISTS note_id_index ON " +
- TABLE.DATA + "(" + DataColumns.NOTE_ID + ");";
+ "CREATE INDEX IF NOT EXISTS note_id_index ON " +
+ TABLE.DATA + "(" + DataColumns.NOTE_ID + ");";
/**
- * Increase folder's note count when move note to the folder
+ * 触发器逻辑:当笔记被移动到新文件夹时(Update操作),
+ * 自动将新文件夹(new.parent_id)内的笔记数量 +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
+ * 触发器逻辑:当笔记从旧文件夹移出时(Update操作),
+ * 自动将旧文件夹(old.parent_id)内的笔记数量 -1。
+ * 增加了 >0 的判断,防止数据异常导致计数器变成负数。
*/
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
+ * 触发器逻辑:当在文件夹下新建笔记时(Insert操作),
+ * 自动将该文件夹的笔记数量 +1。
*/
private static final String NOTE_INCREASE_FOLDER_COUNT_ON_INSERT_TRIGGER =
- "CREATE TRIGGER increase_folder_count_on_insert " +
- " AFTER INSERT ON " + TABLE.NOTE +
- " BEGIN " +
- " UPDATE " + TABLE.NOTE +
- " SET " + NoteColumns.NOTES_COUNT + "=" + NoteColumns.NOTES_COUNT + " + 1" +
- " WHERE " + NoteColumns.ID + "=new." + NoteColumns.PARENT_ID + ";" +
- " END";
+ "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
+ * 触发器逻辑:当删除笔记时(Delete操作),
+ * 自动将所在文件夹的笔记数量 -1。
*/
private static final String NOTE_DECREASE_FOLDER_COUNT_ON_DELETE_TRIGGER =
- "CREATE TRIGGER decrease_folder_count_on_delete " +
- " AFTER DELETE ON " + TABLE.NOTE +
- " BEGIN " +
- " UPDATE " + TABLE.NOTE +
- " SET " + NoteColumns.NOTES_COUNT + "=" + NoteColumns.NOTES_COUNT + "-1" +
- " WHERE " + NoteColumns.ID + "=old." + NoteColumns.PARENT_ID +
- " AND " + NoteColumns.NOTES_COUNT + ">0;" +
- " END";
+ "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 表的 snippet(摘要)字段。
+ * 这样列表页可以直接读取 NOTE 表显示预览,无需联表查询。
*/
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 表的摘要。
*/
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 表对应的摘要。
*/
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 表中对应的详细内容,防止产生脏数据。
*/
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
+ * 触发器逻辑:级联删除文件夹内容。
+ * 当一个文件夹被物理删除时,自动删除该文件夹下的所有笔记。
*/
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
+ * 触发器逻辑:级联移动到废纸篓。
+ * 当文件夹被移入废纸篓(ID_TRASH_FOLER)时,
+ * 自动将该文件夹下的所有笔记也修改为废纸篓状态。
*/
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);
@@ -212,11 +243,12 @@ public class NotesDatabaseHelper extends SQLiteOpenHelper {
public void createNoteTable(SQLiteDatabase db) {
db.execSQL(CREATE_NOTE_TABLE_SQL);
- reCreateNoteTableTriggers(db);
- createSystemFolder(db);
+ reCreateNoteTableTriggers(db); // 创建表后立即初始化相关的触发器
+ createSystemFolder(db); // 初始化系统默认文件夹
Log.d(TAG, "note table has been created");
}
+ // 重置 Note 表的所有触发器,先删除旧的再重新创建,确保逻辑最新
private void reCreateNoteTableTriggers(SQLiteDatabase db) {
db.execSQL("DROP TRIGGER IF EXISTS increase_folder_count_on_update");
db.execSQL("DROP TRIGGER IF EXISTS decrease_folder_count_on_update");
@@ -235,12 +267,14 @@ public class NotesDatabaseHelper extends SQLiteOpenHelper {
db.execSQL(FOLDER_MOVE_NOTES_ON_TRASH_TRIGGER);
}
+ // 在数据库中插入默认的系统文件夹
private void createSystemFolder(SQLiteDatabase db) {
ContentValues values = new ContentValues();
/**
* call record foler for call notes
*/
+ // 插入通话记录文件夹
values.put(NoteColumns.ID, Notes.ID_CALL_RECORD_FOLDER);
values.put(NoteColumns.TYPE, Notes.TYPE_SYSTEM);
db.insert(TABLE.NOTE, null, values);
@@ -248,6 +282,7 @@ public class NotesDatabaseHelper extends SQLiteOpenHelper {
/**
* root folder which is default folder
*/
+ // 插入默认根目录,复用 values 对象前先清空
values.clear();
values.put(NoteColumns.ID, Notes.ID_ROOT_FOLDER);
values.put(NoteColumns.TYPE, Notes.TYPE_SYSTEM);
@@ -256,6 +291,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,6 +300,7 @@ 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);
@@ -273,7 +310,7 @@ public class NotesDatabaseHelper extends SQLiteOpenHelper {
public void createDataTable(SQLiteDatabase db) {
db.execSQL(CREATE_DATA_TABLE_SQL);
reCreateDataTableTriggers(db);
- db.execSQL(CREATE_DATA_NOTE_ID_INDEX_SQL);
+ db.execSQL(CREATE_DATA_NOTE_ID_INDEX_SQL); // 建表后创建索引
Log.d(TAG, "data table has been created");
}
@@ -287,6 +324,7 @@ public class NotesDatabaseHelper extends SQLiteOpenHelper {
db.execSQL(DATA_UPDATE_NOTE_CONTENT_ON_DELETE_TRIGGER);
}
+ // 单例模式获取实例,使用 synchronized 保证多线程安全
static synchronized NotesDatabaseHelper getInstance(Context context) {
if (mInstance == null) {
mInstance = new NotesDatabaseHelper(context);
@@ -300,39 +338,46 @@ public class NotesDatabaseHelper extends SQLiteOpenHelper {
createDataTable(db);
}
+ // 处理数据库版本升级逻辑
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
boolean reCreateTriggers = false;
boolean skipV2 = false;
+ // 升级逻辑:v1 -> v2
if (oldVersion == 1) {
upgradeToV2(db);
- skipV2 = true; // this upgrade including the upgrade from v2 to v3
+ skipV2 = true; // 标记跳过 v2 的独立判断逻辑
oldVersion++;
}
+ // 升级逻辑:v2 -> v3 (或从 v1 升上来后继续执行)
if (oldVersion == 2 && !skipV2) {
upgradeToV3(db);
- reCreateTriggers = true;
+ reCreateTriggers = true; // v3 变更需要重置触发器
oldVersion++;
}
+ // 升级逻辑:v3 -> v4
if (oldVersion == 3) {
upgradeToV4(db);
oldVersion++;
}
+ // 如果升级过程中涉及触发器逻辑变更,统一重新创建
if (reCreateTriggers) {
reCreateNoteTableTriggers(db);
reCreateDataTableTriggers(db);
}
+ // 升级后校验版本号,不匹配则抛出异常
if (oldVersion != newVersion) {
throw new IllegalStateException("Upgrade notes database to version " + newVersion
+ "fails");
}
}
+ // v1 升 v2:删除旧表并重建,重置整个数据库结构
private void upgradeToV2(SQLiteDatabase db) {
db.execSQL("DROP TABLE IF EXISTS " + TABLE.NOTE);
db.execSQL("DROP TABLE IF EXISTS " + TABLE.DATA);
@@ -340,12 +385,14 @@ public class NotesDatabaseHelper extends SQLiteOpenHelper {
createDataTable(db);
}
+ // v2 升 v3:增加 GTask ID 字段和废纸篓文件夹,移除旧的触发器
private void upgradeToV3(SQLiteDatabase db) {
// drop unused triggers
db.execSQL("DROP TRIGGER IF EXISTS update_note_modified_date_on_insert");
db.execSQL("DROP TRIGGER IF EXISTS update_note_modified_date_on_delete");
db.execSQL("DROP TRIGGER IF EXISTS update_note_modified_date_on_update");
// add a column for gtask id
+ // 使用 ALTER TABLE 追加列,SQLite 不支持直接删除列
db.execSQL("ALTER TABLE " + TABLE.NOTE + " ADD COLUMN " + NoteColumns.GTASK_ID
+ " TEXT NOT NULL DEFAULT ''");
// add a trash system folder
@@ -355,8 +402,9 @@ public class NotesDatabaseHelper extends SQLiteOpenHelper {
db.insert(TABLE.NOTE, null, values);
}
+ // v3 升 v4:增加版本号字段
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/src/net/micode/notes/data/NotesProvider.java b/src/src/net/micode/notes/data/NotesProvider.java
index edb0a60..97c73bd 100644
--- a/src/src/net/micode/notes/data/NotesProvider.java
+++ b/src/src/net/micode/notes/data/NotesProvider.java
@@ -32,27 +32,45 @@ import android.util.Log;
import net.micode.notes.R;
import net.micode.notes.data.Notes.DataColumns;
import net.micode.notes.data.Notes.NoteColumns;
+import net.micode.notes.data.Notes.TextNote;
import net.micode.notes.data.NotesDatabaseHelper.TABLE;
-
+/**
+ * 内容提供者核心类
+ *
+ * 这是 Android 四大组件之一 {@code ContentProvider} 的实现。
+ * 它的作用就像是一个“管家”,把底层的 SQLite 数据库包装起来,
+ * 外部(比如 UI 界面或者桌面搜索栏)想要查数据、改数据,都得通过这个管家。
+ * 这样既能保证数据的安全(比如防止误删系统文件夹),也能统一管理数据变化的通知。
+ *
+ *
+ * @Author: 林迪文
+ * @Updator: 林迪文
+ * @Date 2025/12/23 16:20
+ */
public class NotesProvider extends ContentProvider {
+ // URI匹配器,用来判断外边传进来的 URI 具体是想操作哪张表,还是想做搜索
private static final UriMatcher mMatcher;
private NotesDatabaseHelper mHelper;
private static final String TAG = "NotesProvider";
- private static final int URI_NOTE = 1;
- private static final int URI_NOTE_ITEM = 2;
- private static final int URI_DATA = 3;
- private static final int URI_DATA_ITEM = 4;
+ // 定义 URI 对应的匹配码,方便 switch-case 使用
+ private static final int URI_NOTE = 1; // 操作整个 Note 表
+ private static final int URI_NOTE_ITEM = 2; // 操作 Note 表里的单条记录
+ private static final int URI_DATA = 3; // 操作 Data 表
+ private static final int URI_DATA_ITEM = 4; // 操作 Data 表里的单条记录
- private static final int URI_SEARCH = 5;
- private static final int URI_SEARCH_SUGGEST = 6;
+ private static final int URI_SEARCH = 5; // 搜索
+ private static final int URI_SEARCH_SUGGEST = 6; // 搜索建议(给搜索框用的)
+ // 静态代码块,类加载时先把规则定好
static {
mMatcher = new UriMatcher(UriMatcher.NO_MATCH);
+ // 比如 content://net.micode.notes/note 对应 URI_NOTE
mMatcher.addURI(Notes.AUTHORITY, "note", URI_NOTE);
+ // # 代表数字通配符,匹配具体 ID
mMatcher.addURI(Notes.AUTHORITY, "note/#", URI_NOTE_ITEM);
mMatcher.addURI(Notes.AUTHORITY, "data", URI_DATA);
mMatcher.addURI(Notes.AUTHORITY, "data/#", URI_DATA_ITEM);
@@ -64,39 +82,49 @@ 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.
+ *
+ * 这个 SQL 投影比较复杂,主要是为了配合 Android 的全局搜索(SearchManager)。
+ * 它把数据库里的换行符(x'0A')全都去掉了,不然搜索结果列表里显示会乱。
+ * 还配置了 ICON 和点击后的 Intent 动作,这样用户点搜索建议就能直接跳进笔记里。
+ *
*/
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;
+ + 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 + ","
+ + "'" + TextNote.CONTENT_TYPE + "' AS " + SearchManager.SUGGEST_COLUMN_INTENT_DATA;
+ // 预定义的搜索 SQL:查 Note 表,匹配 snippet 内容,排除垃圾桶里的和非笔记类型的
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;
@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 = null;
SQLiteDatabase db = mHelper.getReadableDatabase();
String id = null;
switch (mMatcher.match(uri)) {
case URI_NOTE:
+ // 查整个 Note 表
c = db.query(TABLE.NOTE, projection, selection, selectionArgs, null, null,
sortOrder);
break;
case URI_NOTE_ITEM:
+ // 查单个 Note,解析出 ID 拼到查询条件里
id = uri.getPathSegments().get(1);
c = db.query(TABLE.NOTE, projection, NoteColumns.ID + "=" + id
+ parseSelection(selection), selectionArgs, null, null, sortOrder);
@@ -112,12 +140,14 @@ public class NotesProvider extends ContentProvider {
break;
case URI_SEARCH:
case URI_SEARCH_SUGGEST:
+ // 搜索建议模式,不支持自定义排序和筛选
if (sortOrder != null || projection != null) {
throw new IllegalArgumentException(
"do not specify sortOrder, selection, selectionArgs, or projection" + "with this query");
}
String searchString = null;
+ // 解析搜索关键词,有两种传递方式:路径里或者参数里
if (mMatcher.match(uri) == URI_SEARCH_SUGGEST) {
if (uri.getPathSegments().size() > 1) {
searchString = uri.getPathSegments().get(1);
@@ -131,6 +161,7 @@ public class NotesProvider extends ContentProvider {
}
try {
+ // 加上 % 拼成模糊查询
searchString = String.format("%%%s%%", searchString);
c = db.rawQuery(NOTES_SNIPPET_SEARCH_QUERY,
new String[] { searchString });
@@ -142,11 +173,13 @@ public class NotesProvider extends ContentProvider {
throw new IllegalArgumentException("Unknown URI " + uri);
}
if (c != null) {
+ // 绑定通知 URI。这一步很重要,如果数据变了,监听这个 URI 的界面(比如列表)就会自动刷新。
c.setNotificationUri(getContext().getContentResolver(), uri);
}
return c;
}
+ // 增数据接口
@Override
public Uri insert(Uri uri, ContentValues values) {
SQLiteDatabase db = mHelper.getWritableDatabase();
@@ -156,6 +189,7 @@ public class NotesProvider extends ContentProvider {
insertedId = noteId = db.insert(TABLE.NOTE, null, values);
break;
case URI_DATA:
+ // 插入 Data 表必须要有对应的 Note ID
if (values.containsKey(DataColumns.NOTE_ID)) {
noteId = values.getAsLong(DataColumns.NOTE_ID);
} else {
@@ -166,21 +200,23 @@ public class NotesProvider extends ContentProvider {
default:
throw new IllegalArgumentException("Unknown URI " + uri);
}
- // Notify the note uri
+ // 通知 Note 表有变化,UI 该刷新了
if (noteId > 0) {
getContext().getContentResolver().notifyChange(
ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, noteId), null);
}
- // Notify the data uri
+ // 通知 Data 表有变化
if (dataId > 0) {
getContext().getContentResolver().notifyChange(
ContentUris.withAppendedId(Notes.CONTENT_DATA_URI, dataId), null);
}
+ // 算出刚刚插入的数据 ID,拼成新的 URI 返回给调用者
return ContentUris.withAppendedId(uri, insertedId);
}
+ // 删数据接口
@Override
public int delete(Uri uri, String selection, String[] selectionArgs) {
int count = 0;
@@ -189,6 +225,7 @@ public class NotesProvider extends ContentProvider {
boolean deleteData = false;
switch (mMatcher.match(uri)) {
case URI_NOTE:
+ // 只能删 ID > 0 的,防止误删系统文件夹
selection = "(" + selection + ") AND " + NoteColumns.ID + ">0 ";
count = db.delete(TABLE.NOTE, selection, selectionArgs);
break;
@@ -198,6 +235,7 @@ public class NotesProvider extends ContentProvider {
* ID that smaller than 0 is system folder which is not allowed to
* trash
*/
+ // 防御性代码:系统文件夹(ID <= 0,如根目录)绝对不能删
long noteId = Long.valueOf(id);
if (noteId <= 0) {
break;
@@ -218,6 +256,7 @@ public class NotesProvider extends ContentProvider {
default:
throw new IllegalArgumentException("Unknown URI " + uri);
}
+ // 如果删除了数据,记得发通知刷新界面
if (count > 0) {
if (deleteData) {
getContext().getContentResolver().notifyChange(Notes.CONTENT_NOTE_URI, null);
@@ -227,6 +266,7 @@ public class NotesProvider extends ContentProvider {
return count;
}
+ // 改数据接口
@Override
public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
int count = 0;
@@ -235,11 +275,13 @@ public class NotesProvider extends ContentProvider {
boolean updateData = false;
switch (mMatcher.match(uri)) {
case URI_NOTE:
+ // 批量更新 Note,先把涉及到的 Note 版本号 +1
increaseNoteVersion(-1, selection, selectionArgs);
count = db.update(TABLE.NOTE, values, selection, selectionArgs);
break;
case URI_NOTE_ITEM:
id = uri.getPathSegments().get(1);
+ // 更新单个 Note,先更新版本号
increaseNoteVersion(Long.valueOf(id), selection, selectionArgs);
count = db.update(TABLE.NOTE, values, NoteColumns.ID + "=" + id
+ parseSelection(selection), selectionArgs);
@@ -267,10 +309,12 @@ public class NotesProvider extends ContentProvider {
return count;
}
+ // 辅助方法:拼接 selection 查询条件,防止 SQL 注入
private String parseSelection(String selection) {
return (!TextUtils.isEmpty(selection) ? " AND (" + selection + ')' : "");
}
+ // 增加笔记的版本号,这应该是为了配合云同步功能,判断本地数据是否是最新的
private void increaseNoteVersion(long id, String selection, String[] selectionArgs) {
StringBuilder sql = new StringBuilder(120);
sql.append("UPDATE ");
@@ -279,6 +323,7 @@ public class NotesProvider extends ContentProvider {
sql.append(NoteColumns.VERSION);
sql.append("=" + NoteColumns.VERSION + "+1 ");
+ // 拼接 WHERE 条件
if (id > 0 || !TextUtils.isEmpty(selection)) {
sql.append(" WHERE ");
}
@@ -287,6 +332,8 @@ public class NotesProvider extends ContentProvider {
}
if (!TextUtils.isEmpty(selection)) {
String selectString = id > 0 ? parseSelection(selection) : selection;
+ // 因为 rawQuery 不能自动处理 update 语句里的 ? 占位符
+ // 所以这里要手动把参数(selectionArgs)填进去,替换掉 ?
for (String args : selectionArgs) {
selectString = selectString.replaceFirst("\\?", args);
}
@@ -302,4 +349,4 @@ public class NotesProvider extends ContentProvider {
return null;
}
-}
+}
\ No newline at end of file
diff --git a/src/src/net/micode/notes/gtask/data/MetaData.java b/src/src/net/micode/notes/gtask/data/MetaData.java
index 3a2050b..b04ac70 100644
--- a/src/src/net/micode/notes/gtask/data/MetaData.java
+++ b/src/src/net/micode/notes/gtask/data/MetaData.java
@@ -24,37 +24,62 @@ import net.micode.notes.tool.GTaskStringUtils;
import org.json.JSONException;
import org.json.JSONObject;
-
+/**
+ * 元数据类,属于 Google Task 同步模块的一部分。
+ *
+ * 虽然它继承了 {@code Task},但它不存用户的实际笔记。
+ * 它的主要作用是把本地的关联 ID 包装成 JSON,藏在 GTask 的“备注(Notes)”字段里传到服务器,
+ * 或者从服务器拉取这些信息。
+ *
+ *
+ * @Author: 林迪文
+ * @Updator: 林迪文
+ * @Date 2025/12/16 16:35
+ */
public class MetaData extends Task {
+ // 获取类名做 TAG,方便打 Log
private final static String TAG = MetaData.class.getSimpleName();
+ // 关联的 Google Task ID
private String mRelatedGid = null;
+ // 设置元数据信息:把 GID 和其他信息打包塞进 notes 字段
public void setMeta(String gid, JSONObject metaInfo) {
try {
+ // 往 json 里塞一个键值对:KEY是关联ID的头,VALUE是具体的 gid
metaInfo.put(GTaskStringUtils.META_HEAD_GTASK_ID, gid);
} catch (JSONException e) {
Log.e(TAG, "failed to put related gid");
}
+ // 关键操作:把 JSON 转成字符串,存到 Task 的 notes 属性里
+ // 在 Google Tasks 网页版上,这会显示在任务的“备注”栏里
setNotes(metaInfo.toString());
+ // 给这个特殊的 Task 起个固定的名字,方便识别
setName(GTaskStringUtils.META_NOTE_NAME);
}
+ // 取出关联 ID
public String getRelatedGid() {
return mRelatedGid;
}
+ // 如果 notes 里有东西,才值得保存
@Override
public boolean isWorthSaving() {
return getNotes() != null;
}
+ // 从服务器下发的 JSON 数据里恢复内容
@Override
public void setContentByRemoteJSON(JSONObject js) {
+ // 先让父类干完常规的初始化
super.setContentByRemoteJSON(js);
+ // 如果备注不为空,说明里面可能藏着我们要的 GID
if (getNotes() != null) {
try {
+ // 把字符串形式的备注还原成 JSON 对象
JSONObject metaInfo = new JSONObject(getNotes().trim());
+ // 提取出关联的 GID
mRelatedGid = metaInfo.getString(GTaskStringUtils.META_HEAD_GTASK_ID);
} catch (JSONException e) {
Log.w(TAG, "failed to get related gid");
@@ -63,6 +88,9 @@ public class MetaData extends Task {
}
}
+ // 下面这三个方法直接抛 Error,说明 MetaData 这个类很特殊
+ // 它不需要走本地数据库(Local JSON/Cursor)那一套流程
+
@Override
public void setContentByLocalJSON(JSONObject js) {
// this function should not be called
@@ -79,4 +107,4 @@ public class MetaData extends Task {
throw new IllegalAccessError("MetaData:getSyncAction should not be called");
}
-}
+}
\ No newline at end of file
diff --git a/src/src/net/micode/notes/gtask/data/Node.java b/src/src/net/micode/notes/gtask/data/Node.java
index 63950e0..01f6fbb 100644
--- a/src/src/net/micode/notes/gtask/data/Node.java
+++ b/src/src/net/micode/notes/gtask/data/Node.java
@@ -3,6 +3,7 @@
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with 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
@@ -20,52 +21,103 @@ import android.database.Cursor;
import org.json.JSONObject;
+/**
+ * @Author: 林迪文
+ * @Updator: 林迪文
+ * @Date 2025/12/23 16:20
+ *
+ * 同步节点类的基类
+ *
+ * 我觉得这个 Node 类就是个抽象模板,管你是便签还是文件夹,
+ * 只要想跟 Google Tasks 同步,都得继承它。
+ * 它把同步对象共有的属性和行为都抽出来了。
+ */
public abstract class Node {
+ /*
+ * 定义了本地和云端数据对比后的几种同步状态。
+ * 后面 getSyncAction 方法就是根据两边数据的差异,返回这些状态码。
+ *
+ * 0: 无需操作 (数据一致)
+ * 1: 云端新增 (本地有,云端没有)
+ * 2: 本地新增 (云端有,本地没有)
+ * 3: 云端删除 (本地已删,云端还有)
+ * 4: 本地删除 (云端已删,本地还有)
+ * 5: 更新云端 (本地版本新)
+ * 6: 更新本地 (云端版本新)
+ * 7: 冲突 (两边都改了)
+ * 8: 出错
+ */
public static final int SYNC_ACTION_NONE = 0;
-
public static final int SYNC_ACTION_ADD_REMOTE = 1;
-
public static final int SYNC_ACTION_ADD_LOCAL = 2;
-
public static final int SYNC_ACTION_DEL_REMOTE = 3;
-
public static final int SYNC_ACTION_DEL_LOCAL = 4;
-
public static final int SYNC_ACTION_UPDATE_REMOTE = 5;
-
public static final int SYNC_ACTION_UPDATE_LOCAL = 6;
-
public static final int SYNC_ACTION_UPDATE_CONFLICT = 7;
-
public static final int SYNC_ACTION_ERROR = 8;
- private String mGid;
+ private String mGid; // Google那边给的唯一ID
- private String mName;
+ private String mName; // 笔记标题或者文件夹名
- private long mLastModified;
+ private long mLastModified; // 最后修改时间,同步的时候就靠它比对新旧了
- private boolean mDeleted;
+ private boolean mDeleted; // 删除标记,本地删了就标一下
public Node() {
+ // 构造函数,创建一个空的Node,把所有属性都初始化一下
mGid = null;
mName = "";
mLastModified = 0;
mDeleted = false;
}
+ // 下面这几个是抽象方法,也就是说,这个类只是个架子
+ // 具体的便签(Task)和文件夹(TaskList)要自己去实现这些方法
+ // 告诉程序到底怎么处理JSON数据,怎么判断同步状态
+
+ /**
+ * 把本地数据打包成JSON,用来在云端创建新条目
+ * @param actionId 操作ID
+ * @return 打包好的JSON对象
+ */
public abstract JSONObject getCreateAction(int actionId);
+ /**
+ * 把本地数据打包成JSON,用来更新云端已有条目
+ * @param actionId 操作ID
+ * @return 打包好的JSON对象
+ */
public abstract JSONObject getUpdateAction(int actionId);
+ /**
+ * 从Google服务器拉下来的JSON,解析完更新到这个Node对象里
+ * @param js 从云端接收的JSON对象
+ */
public abstract void setContentByRemoteJSON(JSONObject js);
+ /**
+ * 从本地数据库(通过JSON格式)恢复数据到这个Node对象里
+ * @param js 从本地数据库读取的JSON对象
+ */
public abstract void setContentByLocalJSON(JSONObject js);
+ /**
+ * 把Node对象里的内容,转换成JSON格式,方便存到本地数据库
+ * @return 代表当前内容的JSON对象
+ */
public abstract JSONObject getLocalJSONFromContent();
+ /**
+ * 核心方法,比较当前Node对象和数据库里的Cursor
+ * 看看谁新谁旧,然后返回一个上面定义好的同步状态码
+ * @param c 数据库查询结果的游标
+ * @return 同步状态码 (SYNC_ACTION_*)
+ */
public abstract int getSyncAction(Cursor c);
+ // 下面这一堆就是常规的 getter 和 setter,用来读写私有变量
public void setGid(String gid) {
this.mGid = gid;
}
@@ -97,5 +149,4 @@ public abstract class Node {
public boolean getDeleted() {
return this.mDeleted;
}
-
-}
+}
\ No newline at end of file
diff --git a/src/src/net/micode/notes/gtask/data/SqlData.java b/src/src/net/micode/notes/gtask/data/SqlData.java
index d3ec3be..04b128e 100644
--- a/src/src/net/micode/notes/gtask/data/SqlData.java
+++ b/src/src/net/micode/notes/gtask/data/SqlData.java
@@ -9,6 +9,7 @@
*
* 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.
@@ -34,61 +35,70 @@ import net.micode.notes.gtask.exception.ActionFailureException;
import org.json.JSONException;
import org.json.JSONObject;
-
+/**
+ * @Author: 林迪文
+ * @Updator: 林迪文
+ * @Date 2025/12/19 16:45
+ *
+ * 封装了 data 表的一行数据
+ *
+ * 这个类的作用,就是把数据库里的一行 data 数据,或者一个 JSON 对象,
+ * 包装成一个 SqlData 对象。方便在同步的时候,进行比较和提交。
+ * 它还记录了数据的变更,最后统一 commit 到数据库。
+ */
public class SqlData {
- private static final String TAG = SqlData.class.getSimpleName();
+ private static final String TAG = SqlData.class.getSimpleName(); // TAG,打日志用
- private static final int INVALID_ID = -99999;
+ private static final int INVALID_ID = -99999; // 无效ID,-99999 这个值选得有点随意啊?
+ // PROJECTION_DATA,查询投影,就是指定我们从 data 表里只要这几列数据
public static final String[] PROJECTION_DATA = new String[] {
DataColumns.ID, DataColumns.MIME_TYPE, DataColumns.CONTENT, DataColumns.DATA1,
DataColumns.DATA3
};
+ // 下面这几个是上面投影列对应的索引,方便从 Cursor 里取值
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;
- private ContentResolver mContentResolver;
+ private ContentResolver mContentResolver; // 内容解析器,用来跟数据库打交道
- private boolean mIsCreate;
+ private boolean mIsCreate; // 是不是新建模式的标志
+ // 这几个字段,就是 data 表里一行数据的内容
private long mDataId;
-
private String mDataMimeType;
-
private String mDataContent;
-
private long mDataContentData1;
-
private String mDataContentData3;
+ // ContentValues,就是个键值对集合,专门用来存放有变化的数据,等会儿一次性提交
private ContentValues mDiffDataValues;
+ // 构造函数1: 创建一个全新的、空的 SqlData 对象
public SqlData(Context context) {
- mContentResolver = context.getContentResolver();
- mIsCreate = true;
+ mContentResolver = context.getContentResolver(); // 从 context 里拿到 ContentResolver
+ mIsCreate = true; // 标记为“新建”
mDataId = INVALID_ID;
- mDataMimeType = DataConstants.NOTE;
+ mDataMimeType = DataConstants.NOTE; // 默认是普通笔记类型
mDataContent = "";
mDataContentData1 = 0;
mDataContentData3 = "";
mDiffDataValues = new ContentValues();
}
+ // 构造函数2: 从数据库游标 Cursor 里加载数据,创建一个已存在的 SqlData 对象
public SqlData(Context context, Cursor c) {
mContentResolver = context.getContentResolver();
- mIsCreate = false;
- loadFromCursor(c);
+ mIsCreate = false; // 标记为“已存在”
+ loadFromCursor(c); // 用游标里的数据,把这个对象的各个字段都填上
mDiffDataValues = new ContentValues();
}
+ // 从游标 Cursor 里读取数据,初始化对象的各个字段
private void loadFromCursor(Cursor c) {
mDataId = c.getLong(DATA_ID_COLUMN);
mDataMimeType = c.getString(DATA_MIME_TYPE_COLUMN);
@@ -97,8 +107,14 @@ public class SqlData {
mDataContentData3 = c.getString(DATA_CONTENT_DATA_3_COLUMN);
}
+ /**
+ * 用 JSON 对象来更新 SqlData 的内容。
+ * 它会比较 JSON 里的值和当前对象里的值,把不一样的地方记在 mDiffDataValues 里。
+ */
public void setContent(JSONObject js) throws JSONException {
+ // 先用 has 判断一下 JSON 里有没有这个字段,免得直接 get 报错
long dataId = js.has(DataColumns.ID) ? js.getLong(DataColumns.ID) : INVALID_ID;
+ // 如果是新建模式,或者ID变了,就把新ID加到差异集合里
if (mIsCreate || mDataId != dataId) {
mDiffDataValues.put(DataColumns.ID, dataId);
}
@@ -130,8 +146,12 @@ public class SqlData {
mDataContentData3 = dataContentData3;
}
+ /**
+ * 把当前 SqlData 对象的内容,打包成一个 JSON 对象返回。
+ */
public JSONObject getContent() throws JSONException {
if (mIsCreate) {
+ // 如果还是新建状态,说明还没往数据库里存,这时候转成JSON没啥意义
Log.e(TAG, "it seems that we haven't created this in database yet");
return null;
}
@@ -144,41 +164,52 @@ public class SqlData {
return js;
}
+ /**
+ * 把之前记录的所有变更(mDiffDataValues),一次性提交到数据库。
+ */
public void commit(long noteId, boolean validateVersion, long version) {
if (mIsCreate) {
+ // 新建模式,就执行 insert
if (mDataId == INVALID_ID && mDiffDataValues.containsKey(DataColumns.ID)) {
+ // 如果ID是无效的,就把它从要插入的数据里去掉,让数据库自己生成
mDiffDataValues.remove(DataColumns.ID);
}
- mDiffDataValues.put(DataColumns.NOTE_ID, noteId);
+ mDiffDataValues.put(DataColumns.NOTE_ID, noteId); // 别忘了把所属的 noteId 加上
Uri uri = mContentResolver.insert(Notes.CONTENT_DATA_URI, mDiffDataValues);
try {
+ // 从返回的uri里,解析出新生成的ID
mDataId = Long.valueOf(uri.getPathSegments().get(1));
} catch (NumberFormatException e) {
+ // 万一出错了,记个日志,然后抛个自定义的异常出去,告诉上层同步失败了
Log.e(TAG, "Get note id error :" + e.toString());
throw new ActionFailureException("create note failed");
}
} else {
+ // 非新建模式,就执行 update
if (mDiffDataValues.size() > 0) {
int result = 0;
if (!validateVersion) {
+ // 不需要版本验证,直接更新
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,
+ Notes.CONTENT_DATA_URI, mDataId), mDiffDataValues,
" ? in (SELECT " + NoteColumns.ID + " FROM " + TABLE.NOTE
+ " WHERE " + NoteColumns.VERSION + "=?)", new String[] {
String.valueOf(noteId), String.valueOf(version)
});
}
if (result == 0) {
+ // 没更新成功,可能是版本冲突了
Log.w(TAG, "there is no update. maybe user updates note when syncing");
}
}
}
-
+ // 提交完了,就把差异集合清空,再把状态改成“已存在”
mDiffDataValues.clear();
mIsCreate = false;
}
@@ -186,4 +217,4 @@ public class SqlData {
public long getId() {
return mDataId;
}
-}
+}
\ No newline at end of file
diff --git a/src/src/net/micode/notes/gtask/data/SqlNote.java b/src/src/net/micode/notes/gtask/data/SqlNote.java
index 79a4095..bf22f49 100644
--- a/src/src/net/micode/notes/gtask/data/SqlNote.java
+++ b/src/src/net/micode/notes/gtask/data/SqlNote.java
@@ -3,7 +3,7 @@
*
* 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
+ * You may not use a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
@@ -38,11 +38,20 @@ import org.json.JSONObject;
import java.util.ArrayList;
+/**
+ * @Author: 林迪文
+ * @Updator: 林迪文
+ * @Date: 2025/12/16 16:30
+ * @Description: 这个类是笔记在本地数据库的体现,负责把数据库里的数据读出来变成一个对象,或者把对象的改动写回数据库。
+ */
public class SqlNote {
+ // 这个TAG是用来打日志的,方便在Logcat里筛选特定类的输出
private static final String TAG = SqlNote.class.getSimpleName();
+ // 定义一个特殊的负数值来表示无效ID,避免和数据库自增的ID(从0或1开始)混淆
private static final int INVALID_ID = -99999;
+ // 把所有笔记相关的字段预先定义成一个数组,查询数据库时直接用它,就不用每次都手写一遍了
public static final String[] PROJECTION_NOTE = new String[] {
NoteColumns.ID, NoteColumns.ALERTED_DATE, NoteColumns.BG_COLOR_ID,
NoteColumns.CREATED_DATE, NoteColumns.HAS_ATTACHMENT, NoteColumns.MODIFIED_DATE,
@@ -52,83 +61,63 @@ public class SqlNote {
NoteColumns.VERSION
};
+ // 下面这一堆常量,是上面 PROJECTION_NOTE 数组里每个字段对应的下标。
+ // 这样从Cursor里取数据的时候,直接用 c.getLong(ID_COLUMN) 就行,比用 c.getColumnIndex("id") 效率高。
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;
private Context mContext;
-
- private ContentResolver mContentResolver;
-
- private boolean mIsCreate;
-
- private long mId;
-
- private long mAlertDate;
-
- private int mBgColorId;
-
- private long mCreatedDate;
-
- private int mHasAttachment;
-
- private long mModifiedDate;
-
- private long mParentId;
-
- private String mSnippet;
-
- private int mType;
-
- private int mWidgetId;
-
- private int mWidgetType;
-
- private long mOriginParent;
-
- private long mVersion;
-
+ private ContentResolver mContentResolver; // 安卓系统里专门用来跟ContentProvider打交道的东西,增删改查都靠它
+
+ private boolean mIsCreate; // 一个标志位,用来区分这个对象是新创建的,还是从数据库里读出来的
+
+ // 底下这些就是笔记的各种属性,跟数据库表的字段一一对应
+ private long mId; // ID
+ private long mAlertDate; // 提醒日期
+ private int mBgColorId; // 背景颜色ID
+ private long mCreatedDate; // 创建日期
+ private int mHasAttachment; // 是否有附件
+ private long mModifiedDate; // 修改日期
+ private long mParentId; // 父文件夹ID
+ private String mSnippet; // 摘要
+ private int mType; // 类型
+ private int mWidgetId; // 桌面小部件ID
+ private int mWidgetType; // 桌面小部件类型
+ private long mOriginParent; // 原始父文件夹ID
+ private long mVersion; // 版本号
+
+ // 这个ContentValues专门存放发生变化的字段,commit的时候直接把它丢给数据库更新,很方便
private ContentValues mDiffNoteValues;
-
- private ArrayList mDataList;
-
+ private ArrayList mDataList; // 一条笔记可能包含多条数据(比如文字、录音),用list存起来
+
+ /**
+ * @Author: 林迪文
+ * @Updator: 林迪文
+ * @Date: 2025/12/16 17:30
+ * @Description: 这是给新建笔记用的构造函数。
+ */
public SqlNote(Context context) {
mContext = context;
mContentResolver = context.getContentResolver();
- mIsCreate = true;
+ mIsCreate = true; // 明确这是个新笔记
mId = INVALID_ID;
mAlertDate = 0;
- mBgColorId = ResourceParser.getDefaultBgId(context);
+ mBgColorId = ResourceParser.getDefaultBgId(context); // 连颜色都有默认的
mCreatedDate = System.currentTimeMillis();
mHasAttachment = 0;
mModifiedDate = System.currentTimeMillis();
@@ -143,49 +132,63 @@ public class SqlNote {
mDataList = new ArrayList();
}
+ /**
+ * @Author: 林迪文
+ * @Updator: 林迪文
+ * @Date: 2025/12/16 16:46
+ * @Description: 从一个已经查好的数据库Cursor里直接加载数据,这样就不用再查一次数据库了,效率高。
+ */
public SqlNote(Context context, Cursor c) {
mContext = context;
mContentResolver = context.getContentResolver();
- mIsCreate = false;
- loadFromCursor(c);
+ mIsCreate = false; // 从数据库来的,不是新建
+ loadFromCursor(c); // 用传进来的cursor填充数据
mDataList = new ArrayList();
if (mType == Notes.TYPE_NOTE)
- loadDataContent();
+ loadDataContent(); // 如果是笔记类型,还得把它的具体内容也加载进来
mDiffNoteValues = new ContentValues();
}
+ /**
+ * @Author: 林迪文
+ * @Updator: 林迪文
+ * @Date: 2025/12/16 16:54
+ * @Description: 根据一个笔记ID来构造对象,会触发一次数据库查询。
+ */
public SqlNote(Context context, long id) {
mContext = context;
mContentResolver = context.getContentResolver();
mIsCreate = false;
- loadFromCursor(id);
+ loadFromCursor(id); // 这个会去查数据库
mDataList = new ArrayList();
if (mType == Notes.TYPE_NOTE)
loadDataContent();
mDiffNoteValues = new ContentValues();
}
-
private void loadFromCursor(long id) {
Cursor c = null;
try {
+ // 用ContentResolver去查询指定ID的笔记
c = mContentResolver.query(Notes.CONTENT_NOTE_URI, PROJECTION_NOTE, "(_id=?)",
new String[] {
- String.valueOf(id)
+ String.valueOf(id)
}, null);
if (c != null) {
c.moveToNext();
- loadFromCursor(c);
+ loadFromCursor(c); // 把查到的cursor交给另一个重载方法处理
} else {
Log.w(TAG, "loadFromCursor: cursor = null");
}
} finally {
+ // 这个很重要,不管前面有没有出错,只要cursor不是null,就必须关掉,防止内存泄漏
if (c != null)
c.close();
}
}
private void loadFromCursor(Cursor c) {
+ // 把cursor当前行的数据,一个个取出来,塞到这个对象的成员变量里
mId = c.getLong(ID_COLUMN);
mAlertDate = c.getLong(ALERTED_DATE_COLUMN);
mBgColorId = c.getInt(BG_COLOR_ID_COLUMN);
@@ -202,18 +205,20 @@ public class SqlNote {
private void loadDataContent() {
Cursor c = null;
- mDataList.clear();
+ mDataList.clear(); // 先清空,再加载
try {
+ // 查的是另一个URI,说明笔记内容是存在另一张表里的
c = mContentResolver.query(Notes.CONTENT_DATA_URI, SqlData.PROJECTION_DATA,
"(note_id=?)", new String[] {
- String.valueOf(mId)
+ String.valueOf(mId) // 用当前笔记的ID作为查询条件
}, null);
if (c != null) {
if (c.getCount() == 0) {
- Log.w(TAG, "it seems that the note has not data");
+ // 笔记没有内容数据,也正常
return;
}
while (c.moveToNext()) {
+ // 把每一行数据都包装成一个SqlData对象,然后加到List里
SqlData data = new SqlData(mContext, c);
mDataList.add(data);
}
@@ -221,18 +226,25 @@ public class SqlNote {
Log.w(TAG, "loadDataContent: cursor = null");
}
} finally {
+ // 同样,用完cursor要及时关掉
if (c != null)
c.close();
}
}
+ /**
+ * @Author: 林迪文
+ * @Updator: 林迪文
+ * @Date: 2025/12/16 16:44
+ * @Description: 把从云端同步下来的JSON数据解析,然后更新到这个笔记对象里。
+ */
public boolean setContent(JSONObject js) {
try {
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
+ // 文件夹只更新摘要和类型
String snippet = note.has(NoteColumns.SNIPPET) ? note
.getString(NoteColumns.SNIPPET) : "";
if (mIsCreate || !mSnippet.equals(snippet)) {
@@ -247,108 +259,103 @@ public class SqlNote {
}
mType = type;
} else if (note.getInt(NoteColumns.TYPE) == Notes.TYPE_NOTE) {
+ // 如果是笔记类型,就把所有字段都更新一遍
JSONArray dataArray = js.getJSONArray(GTaskStringUtils.META_HEAD_DATA);
+
+ // note.has() 是个好习惯,先判断有没有这个key,防止JSONException
long id = note.has(NoteColumns.ID) ? note.getLong(NoteColumns.ID) : INVALID_ID;
- if (mIsCreate || mId != id) {
- mDiffNoteValues.put(NoteColumns.ID, id);
+ if (mIsCreate || mId != id) { // 如果是新数据,或者数据有变化
+ mDiffNoteValues.put(NoteColumns.ID, id); // 就把这个变化记录到mDiffNoteValues里
}
- mId = id;
+ mId = id; // 同时更新内存里的值
- long alertDate = note.has(NoteColumns.ALERTED_DATE) ? note
- .getLong(NoteColumns.ALERTED_DATE) : 0;
+ long alertDate = note.has(NoteColumns.ALERTED_DATE) ? note.getLong(NoteColumns.ALERTED_DATE) : 0;
if (mIsCreate || mAlertDate != alertDate) {
mDiffNoteValues.put(NoteColumns.ALERTED_DATE, alertDate);
}
mAlertDate = alertDate;
- int bgColorId = note.has(NoteColumns.BG_COLOR_ID) ? note
- .getInt(NoteColumns.BG_COLOR_ID) : ResourceParser.getDefaultBgId(mContext);
+ int bgColorId = note.has(NoteColumns.BG_COLOR_ID) ? note.getInt(NoteColumns.BG_COLOR_ID) : ResourceParser.getDefaultBgId(mContext);
if (mIsCreate || mBgColorId != bgColorId) {
mDiffNoteValues.put(NoteColumns.BG_COLOR_ID, bgColorId);
}
mBgColorId = bgColorId;
- long createDate = note.has(NoteColumns.CREATED_DATE) ? note
- .getLong(NoteColumns.CREATED_DATE) : System.currentTimeMillis();
+ long createDate = note.has(NoteColumns.CREATED_DATE) ? note.getLong(NoteColumns.CREATED_DATE) : System.currentTimeMillis();
if (mIsCreate || mCreatedDate != createDate) {
mDiffNoteValues.put(NoteColumns.CREATED_DATE, createDate);
}
mCreatedDate = createDate;
- int hasAttachment = note.has(NoteColumns.HAS_ATTACHMENT) ? note
- .getInt(NoteColumns.HAS_ATTACHMENT) : 0;
+ int hasAttachment = note.has(NoteColumns.HAS_ATTACHMENT) ? note.getInt(NoteColumns.HAS_ATTACHMENT) : 0;
if (mIsCreate || mHasAttachment != hasAttachment) {
mDiffNoteValues.put(NoteColumns.HAS_ATTACHMENT, hasAttachment);
}
mHasAttachment = hasAttachment;
- long modifiedDate = note.has(NoteColumns.MODIFIED_DATE) ? note
- .getLong(NoteColumns.MODIFIED_DATE) : System.currentTimeMillis();
+ long modifiedDate = note.has(NoteColumns.MODIFIED_DATE) ? note.getLong(NoteColumns.MODIFIED_DATE) : System.currentTimeMillis();
if (mIsCreate || mModifiedDate != modifiedDate) {
mDiffNoteValues.put(NoteColumns.MODIFIED_DATE, modifiedDate);
}
mModifiedDate = modifiedDate;
- long parentId = note.has(NoteColumns.PARENT_ID) ? note
- .getLong(NoteColumns.PARENT_ID) : 0;
+ long parentId = note.has(NoteColumns.PARENT_ID) ? note.getLong(NoteColumns.PARENT_ID) : 0;
if (mIsCreate || mParentId != parentId) {
mDiffNoteValues.put(NoteColumns.PARENT_ID, parentId);
}
mParentId = parentId;
- String snippet = note.has(NoteColumns.SNIPPET) ? note
- .getString(NoteColumns.SNIPPET) : "";
+ String snippet = note.has(NoteColumns.SNIPPET) ? note.getString(NoteColumns.SNIPPET) : "";
if (mIsCreate || !mSnippet.equals(snippet)) {
mDiffNoteValues.put(NoteColumns.SNIPPET, snippet);
}
mSnippet = snippet;
- int type = note.has(NoteColumns.TYPE) ? note.getInt(NoteColumns.TYPE)
- : Notes.TYPE_NOTE;
+ int type = note.has(NoteColumns.TYPE) ? note.getInt(NoteColumns.TYPE) : Notes.TYPE_NOTE;
if (mIsCreate || mType != type) {
mDiffNoteValues.put(NoteColumns.TYPE, type);
}
mType = type;
- int widgetId = note.has(NoteColumns.WIDGET_ID) ? note.getInt(NoteColumns.WIDGET_ID)
- : AppWidgetManager.INVALID_APPWIDGET_ID;
+ int widgetId = note.has(NoteColumns.WIDGET_ID) ? note.getInt(NoteColumns.WIDGET_ID) : AppWidgetManager.INVALID_APPWIDGET_ID;
if (mIsCreate || mWidgetId != widgetId) {
mDiffNoteValues.put(NoteColumns.WIDGET_ID, widgetId);
}
mWidgetId = widgetId;
- int widgetType = note.has(NoteColumns.WIDGET_TYPE) ? note
- .getInt(NoteColumns.WIDGET_TYPE) : Notes.TYPE_WIDGET_INVALIDE;
+ int widgetType = note.has(NoteColumns.WIDGET_TYPE) ? note.getInt(NoteColumns.WIDGET_TYPE) : Notes.TYPE_WIDGET_INVALIDE;
if (mIsCreate || mWidgetType != widgetType) {
mDiffNoteValues.put(NoteColumns.WIDGET_TYPE, widgetType);
}
mWidgetType = widgetType;
- long originParent = note.has(NoteColumns.ORIGIN_PARENT_ID) ? note
- .getLong(NoteColumns.ORIGIN_PARENT_ID) : 0;
+ long originParent = note.has(NoteColumns.ORIGIN_PARENT_ID) ? note.getLong(NoteColumns.ORIGIN_PARENT_ID) : 0;
if (mIsCreate || mOriginParent != originParent) {
mDiffNoteValues.put(NoteColumns.ORIGIN_PARENT_ID, originParent);
}
mOriginParent = originParent;
+ // 处理笔记的具体内容数据
for (int i = 0; i < dataArray.length(); i++) {
JSONObject data = dataArray.getJSONObject(i);
SqlData sqlData = null;
if (data.has(DataColumns.ID)) {
long dataId = data.getLong(DataColumns.ID);
+ // 看看本地这条笔记是否已经有这个ID的内容了
for (SqlData temp : mDataList) {
if (dataId == temp.getId()) {
- sqlData = temp;
+ sqlData = temp; // 找到了就用它,准备更新
}
}
}
if (sqlData == null) {
+ // 没找到,说明是新增的内容
sqlData = new SqlData(mContext);
mDataList.add(sqlData);
}
- sqlData.setContent(data);
+ sqlData.setContent(data); // 把JSON数据设置到SqlData对象里
}
}
} catch (JSONException e) {
@@ -359,17 +366,25 @@ public class SqlNote {
return true;
}
+ /**
+ * @Author: 林迪文
+ * @Updator: 林迪文
+ * @Date: 2025/12/16 16:04
+ * @Description: 把这个笔记对象里的数据,打包成一个JSONObject,方便上传到云端。
+ */
public JSONObject getContent() {
try {
JSONObject js = new JSONObject();
if (mIsCreate) {
+ // 如果还没在数据库里创建,就不能生成内容
Log.e(TAG, "it seems that we haven't created this in database yet");
return null;
}
JSONObject note = new JSONObject();
if (mType == Notes.TYPE_NOTE) {
+ // 把对象的各个属性,一个个put到JSONObject里
note.put(NoteColumns.ID, mId);
note.put(NoteColumns.ALERTED_DATE, mAlertDate);
note.put(NoteColumns.BG_COLOR_ID, mBgColorId);
@@ -384,15 +399,17 @@ public class SqlNote {
note.put(NoteColumns.ORIGIN_PARENT_ID, mOriginParent);
js.put(GTaskStringUtils.META_HEAD_NOTE, note);
+ // 把笔记的具体内容也打包
JSONArray dataArray = new JSONArray();
for (SqlData sqlData : mDataList) {
- JSONObject data = sqlData.getContent();
+ JSONObject data = sqlData.getContent(); // 让SqlData自己去打包
if (data != null) {
dataArray.put(data);
}
}
js.put(GTaskStringUtils.META_HEAD_DATA, dataArray);
} else if (mType == Notes.TYPE_FOLDER || mType == Notes.TYPE_SYSTEM) {
+ // 文件夹类型的数据就简单多了
note.put(NoteColumns.ID, mId);
note.put(NoteColumns.TYPE, mType);
note.put(NoteColumns.SNIPPET, mSnippet);
@@ -406,20 +423,23 @@ public class SqlNote {
}
return null;
}
-
+ // 更新父文件夹ID,同时记录到mDiffNoteValues里
public void setParentId(long id) {
mParentId = id;
mDiffNoteValues.put(NoteColumns.PARENT_ID, id);
}
+ // 更新Google Task的ID
public void setGtaskId(String gid) {
mDiffNoteValues.put(NoteColumns.GTASK_ID, gid);
}
+ // 更新同步ID
public void setSyncId(long syncId) {
mDiffNoteValues.put(NoteColumns.SYNC_ID, syncId);
}
+ // 重置本地修改标志位,通常在同步成功后调用
public void resetLocalModified() {
mDiffNoteValues.put(NoteColumns.LOCAL_MODIFIED, 0);
}
@@ -436,18 +456,28 @@ public class SqlNote {
return mSnippet;
}
+ // 判断当前对象是不是一个笔记(而不是文件夹)
public boolean isNoteType() {
return mType == Notes.TYPE_NOTE;
}
+ /**
+ * @Author: 林迪文
+ * @Updator: 林迪文
+ * @Date: 2025/12/17 8:05
+ * @Description: 提交所有修改。这个是核心方法,负责把内存里的数据变动写入数据库。
+ */
public void commit(boolean validateVersion) {
if (mIsCreate) {
+ // 如果是新笔记,就执行插入操作
if (mId == INVALID_ID && mDiffNoteValues.containsKey(NoteColumns.ID)) {
+ // 如果ID是自己设的无效ID,就从待插入的数据里移除,让数据库自己生成
mDiffNoteValues.remove(NoteColumns.ID);
}
Uri uri = mContentResolver.insert(Notes.CONTENT_NOTE_URI, mDiffNoteValues);
try {
+ // 从返回的uri里解析出新生成的笔记ID,这是ContentProvider的标准操作
mId = Long.valueOf(uri.getPathSegments().get(1));
} catch (NumberFormatException e) {
Log.e(TAG, "Get note id error :" + e.toString());
@@ -457,27 +487,33 @@ public class SqlNote {
throw new IllegalStateException("Create thread id failed");
}
+ // 把笔记的详细内容也跟着存进去
if (mType == Notes.TYPE_NOTE) {
for (SqlData sqlData : mDataList) {
sqlData.commit(mId, false, -1);
}
}
} else {
+ // 如果是已存在的笔记,就执行更新操作
if (mId <= 0 && mId != Notes.ID_ROOT_FOLDER && mId != Notes.ID_CALL_RECORD_FOLDER) {
+ // 防御性编程,更新一个ID不合法的笔记肯定有问题
Log.e(TAG, "No such note");
throw new IllegalStateException("Try to update note with invalid id");
}
if (mDiffNoteValues.size() > 0) {
- mVersion ++;
+ mVersion ++; // 每次更新,版本号加1
int result = 0;
if (!validateVersion) {
+ // 普通更新,直接拿ID去更新
result = mContentResolver.update(Notes.CONTENT_NOTE_URI, mDiffNoteValues, "("
+ NoteColumns.ID + "=?)", new String[] {
- String.valueOf(mId)
+ String.valueOf(mId)
});
} else {
+ // 带版本校验的更新,只有当数据库里的版本号小于等于当前版本号时才更新成功
+ // 这是为了防止本地的旧数据覆盖掉服务器上已经更新的数据,处理同步冲突用的
result = mContentResolver.update(Notes.CONTENT_NOTE_URI, mDiffNoteValues, "("
- + NoteColumns.ID + "=?) AND (" + NoteColumns.VERSION + "<=?)",
+ + NoteColumns.ID + "=?) AND (" + NoteColumns.VERSION + "<=?)",
new String[] {
String.valueOf(mId), String.valueOf(mVersion)
});
@@ -494,12 +530,12 @@ public class SqlNote {
}
}
- // refresh local info
+ // 提交完之后,重新从数据库加载一次数据,保证内存里的对象和数据库完全同步
loadFromCursor(mId);
if (mType == Notes.TYPE_NOTE)
loadDataContent();
- mDiffNoteValues.clear();
- mIsCreate = false;
+ mDiffNoteValues.clear(); // 清空已变更的记录,为下次修改做准备
+ mIsCreate = false; // 不管之前是不是新创建的,现在都已经存在于数据库里了
}
-}
+}
\ No newline at end of file
diff --git a/src/src/net/micode/notes/gtask/data/Task.java b/src/src/net/micode/notes/gtask/data/Task.java
index 6a19454..da1f06a 100644
--- a/src/src/net/micode/notes/gtask/data/Task.java
+++ b/src/src/net/micode/notes/gtask/data/Task.java
@@ -9,6 +9,7 @@
*
* 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.
@@ -32,21 +33,23 @@ import org.json.JSONException;
import org.json.JSONObject;
+/**
+ * @Author: 林迪文
+ * @Updator: 林迪文
+ * @Date: 2025/12/20 16:45
+ * @Description: 这个类代表Google Tasks里的一个具体任务。它负责把任务信息打包成JSON发给服务器,或者解析服务器返回的JSON。
+ */
public class Task extends Node {
private static final String TAG = Task.class.getSimpleName();
- private boolean mCompleted;
-
- private String mNotes;
-
- private JSONObject mMetaInfo;
-
- private Task mPriorSibling;
-
- private TaskList mParent;
+ private boolean mCompleted; // 任务是否完成
+ private String mNotes; // 任务的备注信息
+ private JSONObject mMetaInfo; // 从本地数据库读出来的元数据,JSON格式,同步时很有用
+ private Task mPriorSibling; // 指向上一个任务,Google Tasks用这个来排序
+ private TaskList mParent; // 这个任务属于哪个TaskList
public Task() {
- super();
+ super(); // 用父类的构造函数完成一些基础初始化
mCompleted = false;
mNotes = null;
mPriorSibling = null;
@@ -54,42 +57,44 @@ public class Task extends Node {
mMetaInfo = null;
}
+ /**
+ 当要创建一个新任务时,用这个方法来生成发给服务器的JSON数据包。
+ */
public JSONObject getCreateAction(int actionId) {
JSONObject js = new JSONObject();
try {
- // action_type
+ // action_type: 告诉服务器,咱们这次是要“创建”
js.put(GTaskStringUtils.GTASK_JSON_ACTION_TYPE,
GTaskStringUtils.GTASK_JSON_ACTION_TYPE_CREATE);
- // action_id
+ // action_id: 本次操作的唯一ID
js.put(GTaskStringUtils.GTASK_JSON_ACTION_ID, actionId);
- // index
+ // index: 这个新任务在列表里的位置
js.put(GTaskStringUtils.GTASK_JSON_INDEX, mParent.getChildTaskIndex(this));
- // entity_delta
+ // entity_delta: 这里面放的是任务本身的核心数据
JSONObject entity = new JSONObject();
- entity.put(GTaskStringUtils.GTASK_JSON_NAME, getName());
+ entity.put(GTaskStringUtils.GTASK_JSON_NAME, getName()); // 任务名
entity.put(GTaskStringUtils.GTASK_JSON_CREATOR_ID, "null");
entity.put(GTaskStringUtils.GTASK_JSON_ENTITY_TYPE,
GTaskStringUtils.GTASK_JSON_TYPE_TASK);
if (getNotes() != null) {
- entity.put(GTaskStringUtils.GTASK_JSON_NOTES, getNotes());
+ entity.put(GTaskStringUtils.GTASK_JSON_NOTES, getNotes()); // 任务备注
}
js.put(GTaskStringUtils.GTASK_JSON_ENTITY_DELTA, entity);
- // parent_id
+ // parent_id: 告诉服务器,这个任务要创建在哪个任务列表下面
js.put(GTaskStringUtils.GTASK_JSON_PARENT_ID, mParent.getGid());
- // dest_parent_type
js.put(GTaskStringUtils.GTASK_JSON_DEST_PARENT_TYPE,
GTaskStringUtils.GTASK_JSON_TYPE_GROUP);
- // list_id
+ // list_id: 任务列表的ID
js.put(GTaskStringUtils.GTASK_JSON_LIST_ID, mParent.getGid());
- // prior_sibling_id
+ // prior_sibling_id: 它的前一个任务是谁,服务器靠这个来排序
if (mPriorSibling != null) {
js.put(GTaskStringUtils.GTASK_JSON_PRIOR_SIBLING_ID, mPriorSibling.getGid());
}
@@ -103,21 +108,24 @@ public class Task extends Node {
return js;
}
+ /**
+ 和上面那个对应,这个是用来生成“更新”任务的JSON数据包。
+ */
public JSONObject getUpdateAction(int actionId) {
JSONObject js = new JSONObject();
try {
- // action_type
+ // 这次是“更新”操作
js.put(GTaskStringUtils.GTASK_JSON_ACTION_TYPE,
GTaskStringUtils.GTASK_JSON_ACTION_TYPE_UPDATE);
- // action_id
+ // 本次操作的ID
js.put(GTaskStringUtils.GTASK_JSON_ACTION_ID, actionId);
- // id
+ // id: 必须告诉服务器要更新的是哪个任务
js.put(GTaskStringUtils.GTASK_JSON_ID, getGid());
- // entity_delta
+ // entity_delta: 存放变化了的数据
JSONObject entity = new JSONObject();
entity.put(GTaskStringUtils.GTASK_JSON_NAME, getName());
if (getNotes() != null) {
@@ -134,36 +142,38 @@ public class Task extends Node {
return js;
}
-
+ /**
+ 用从Google服务器返回的JSON数据,来设置Task对象的属性。
+ */
public void setContentByRemoteJSON(JSONObject js) {
if (js != null) {
try {
- // id
+ // id: 服务器分配的唯一ID
if (js.has(GTaskStringUtils.GTASK_JSON_ID)) {
setGid(js.getString(GTaskStringUtils.GTASK_JSON_ID));
}
- // last_modified
+ // last_modified: 最后修改时间,这个是同步的关键
if (js.has(GTaskStringUtils.GTASK_JSON_LAST_MODIFIED)) {
setLastModified(js.getLong(GTaskStringUtils.GTASK_JSON_LAST_MODIFIED));
}
- // name
+ // 任务名
if (js.has(GTaskStringUtils.GTASK_JSON_NAME)) {
setName(js.getString(GTaskStringUtils.GTASK_JSON_NAME));
}
- // notes
+ // 任务备注
if (js.has(GTaskStringUtils.GTASK_JSON_NOTES)) {
setNotes(js.getString(GTaskStringUtils.GTASK_JSON_NOTES));
}
- // deleted
+ // 是否被删除
if (js.has(GTaskStringUtils.GTASK_JSON_DELETED)) {
setDeleted(js.getBoolean(GTaskStringUtils.GTASK_JSON_DELETED));
}
- // completed
+ // 是否已完成
if (js.has(GTaskStringUtils.GTASK_JSON_COMPLETED)) {
setCompleted(js.getBoolean(GTaskStringUtils.GTASK_JSON_COMPLETED));
}
@@ -175,13 +185,18 @@ public class Task extends Node {
}
}
+ /**
+ 这个是把我们自己本地数据库存的JSON,解析了来设置Task的属性。
+ */
public void setContentByLocalJSON(JSONObject js) {
if (js == null || !js.has(GTaskStringUtils.META_HEAD_NOTE)
|| !js.has(GTaskStringUtils.META_HEAD_DATA)) {
Log.w(TAG, "setContentByLocalJSON: nothing is avaiable");
+ return;
}
try {
+ // 本地JSON的结构和服务器返回的不一样,有note头和data头
JSONObject note = js.getJSONObject(GTaskStringUtils.META_HEAD_NOTE);
JSONArray dataArray = js.getJSONArray(GTaskStringUtils.META_HEAD_DATA);
@@ -190,11 +205,12 @@ public class Task extends Node {
return;
}
+ // 遍历data数组,找到真正存笔记内容的那一项,把它的内容作为任务名
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;
+ break; // 找到了就不用再找了
}
}
@@ -203,34 +219,41 @@ public class Task extends Node {
e.printStackTrace();
}
}
-
+ /**
+ * @Author: 林迪文
+ * @Updator: 林迪文
+ * @Date: 2025/12/23 16:45
+ * @Description: 把Task对象里的数据,转换成要存到本地数据库的那种JSON格式。
+ */
public JSONObject getLocalJSONFromContent() {
String name = getName();
try {
if (mMetaInfo == null) {
- // new task created from web
+ // 如果mMetaInfo是空的,说明这是一个从服务器上新拉下来的任务,本地还没有它的记录
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);
+ data.put(DataColumns.CONTENT, name); // 把任务名存到content字段
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
+ // 如果mMetaInfo不是空,说明这个任务之前就在本地存过,我们直接在旧数据上修改就行
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;
@@ -247,6 +270,7 @@ public class Task extends Node {
}
}
+ // 把从数据库查出来的原始JSON存到mMetaInfo里
public void setMetaInfo(MetaData metaData) {
if (metaData != null && metaData.getNotes() != null) {
try {
@@ -258,48 +282,61 @@ public class Task extends Node {
}
}
+ /**
+ 这是同步的核心!比较本地数据和远程数据,决定下一步该干啥。
+ */
public int getSyncAction(Cursor c) {
try {
JSONObject noteInfo = null;
+ // mMetaInfo 是从本地数据库的笔记内容里解析出来的JSON
if (mMetaInfo != null && mMetaInfo.has(GTaskStringUtils.META_HEAD_NOTE)) {
noteInfo = mMetaInfo.getJSONObject(GTaskStringUtils.META_HEAD_NOTE);
}
-
+ // 第一道检查:看元数据还在不在
if (noteInfo == null) {
Log.w(TAG, "it seems that note meta has been deleted");
+ // 如果元数据没了,说明本地记录可能不完整了,干脆把当前任务的信息更新到服务器上,防止数据丢失。
return SYNC_ACTION_UPDATE_REMOTE;
}
+ // 第二道检查:看元数据里有没有ID
if (!noteInfo.has(NoteColumns.ID)) {
Log.w(TAG, "remote note id seems to be deleted");
+ // 元数据里连ID都没有,这数据肯定坏了。那就用服务器上的版本把它覆盖掉,修复一下。
return SYNC_ACTION_UPDATE_LOCAL;
}
- // validate the note id now
+ // 第三道检查:核对ID是否一致
+ // 确认一下元数据里的ID和数据库游标(Cursor)里的ID是不是同一个。
if (c.getLong(SqlNote.ID_COLUMN) != noteInfo.getLong(NoteColumns.ID)) {
Log.w(TAG, "note id doesn't match");
+ // 如果对不上,说明数据乱了,本地记录不可信,也得用服务器的来覆盖。
return SYNC_ACTION_UPDATE_LOCAL;
}
+ // --- 核心逻辑开始 ---
+ // 如果上面的检查都通过了,说明本地数据是基本健康的,可以开始比较时间戳了
if (c.getInt(SqlNote.LOCAL_MODIFIED_COLUMN) == 0) {
- // there is no local update
+ // Case 1: 本地数据从上次同步以来,没被修改过
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
+ // Case 2: 本地数据被修改了
+ // 在这里再确认下gtask 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;
}
}
@@ -308,14 +345,16 @@ public class Task extends Node {
e.printStackTrace();
}
+ // 如果中间出了任何岔子,就返回错误状态
return SYNC_ACTION_ERROR;
}
-
+ // 判断这个任务有没有实际内容,免得存一堆空任务
public boolean isWorthSaving() {
return mMetaInfo != null || (getName() != null && getName().trim().length() > 0)
|| (getNotes() != null && getNotes().trim().length() > 0);
}
+ // 下面都是些简单的get和set方法
public void setCompleted(boolean completed) {
this.mCompleted = completed;
}
@@ -347,5 +386,4 @@ public class Task extends Node {
public TaskList getParent() {
return this.mParent;
}
-
-}
+}
\ No newline at end of file
diff --git a/src/src/net/micode/notes/gtask/data/TaskList.java b/src/src/net/micode/notes/gtask/data/TaskList.java
index 4ea21c5..5c11a72 100644
--- a/src/src/net/micode/notes/gtask/data/TaskList.java
+++ b/src/src/net/micode/notes/gtask/data/TaskList.java
@@ -30,37 +30,46 @@ import org.json.JSONObject;
import java.util.ArrayList;
+/**
+ * @Author: 林迪文
+ * @Updator: 林迪文
+ * @Date: 2025/12/23 17:15
+ * @Description: 这个类代表Google Tasks里的一个“任务列表”,在咱们App里对应的就是“文件夹”。它管理着一堆Task对象。
+ */
public class TaskList extends Node {
+ // 日志TAG
private static final String TAG = TaskList.class.getSimpleName();
- private int mIndex;
+ private int mIndex; // 任务列表的索引位置
- private ArrayList mChildren;
+ private ArrayList mChildren; // 用一个List来存放这个列表下的所有任务
public TaskList() {
- super();
- mChildren = new ArrayList();
+ super(); // 调用父类的构造函数
+ mChildren = new ArrayList(); // new一个list出来,准备装东西
mIndex = 1;
}
+ // 生成'创建'任务列表的JSON数据包,发给服务器用
public JSONObject getCreateAction(int actionId) {
JSONObject js = new JSONObject();
try {
- // action_type
+ // action_type: 告诉服务器,操作类型是“创建”
js.put(GTaskStringUtils.GTASK_JSON_ACTION_TYPE,
GTaskStringUtils.GTASK_JSON_ACTION_TYPE_CREATE);
- // action_id
+ // action_id: 本次操作的唯一ID
js.put(GTaskStringUtils.GTASK_JSON_ACTION_ID, actionId);
- // index
+ // index: 任务列表的位置
js.put(GTaskStringUtils.GTASK_JSON_INDEX, mIndex);
- // entity_delta
+ // entity_delta: 存放这个任务列表的核心信息
JSONObject entity = new JSONObject();
entity.put(GTaskStringUtils.GTASK_JSON_NAME, getName());
entity.put(GTaskStringUtils.GTASK_JSON_CREATOR_ID, "null");
+ // 注意,这里的类型是GROUP,和Task的TASK不一样
entity.put(GTaskStringUtils.GTASK_JSON_ENTITY_TYPE,
GTaskStringUtils.GTASK_JSON_TYPE_GROUP);
js.put(GTaskStringUtils.GTASK_JSON_ENTITY_DELTA, entity);
@@ -74,21 +83,22 @@ public class TaskList extends Node {
return js;
}
+ // 生成'更新'任务列表的JSON数据包
public JSONObject getUpdateAction(int actionId) {
JSONObject js = new JSONObject();
try {
- // action_type
+ // action_type: 这次是“更新”
js.put(GTaskStringUtils.GTASK_JSON_ACTION_TYPE,
GTaskStringUtils.GTASK_JSON_ACTION_TYPE_UPDATE);
- // action_id
+ // action_id: 本次操作的ID
js.put(GTaskStringUtils.GTASK_JSON_ACTION_ID, actionId);
- // id
+ // id: 告诉服务器要更新的是哪一个
js.put(GTaskStringUtils.GTASK_JSON_ID, getGid());
- // entity_delta
+ // entity_delta: 存放变化了的数据,比如名字改了,或者被删了
JSONObject entity = new JSONObject();
entity.put(GTaskStringUtils.GTASK_JSON_NAME, getName());
entity.put(GTaskStringUtils.GTASK_JSON_DELETED, getDeleted());
@@ -102,21 +112,21 @@ public class TaskList extends Node {
return js;
}
-
+ // 用服务器返回的JSON数据,填充任务列表的属性
public void setContentByRemoteJSON(JSONObject js) {
if (js != null) {
try {
- // id
+ // id: 服务器分配的唯一ID
if (js.has(GTaskStringUtils.GTASK_JSON_ID)) {
setGid(js.getString(GTaskStringUtils.GTASK_JSON_ID));
}
- // last_modified
+ // last_modified: 最后修改时间,同步的关键
if (js.has(GTaskStringUtils.GTASK_JSON_LAST_MODIFIED)) {
setLastModified(js.getLong(GTaskStringUtils.GTASK_JSON_LAST_MODIFIED));
}
- // name
+ // name: 任务列表的名字
if (js.has(GTaskStringUtils.GTASK_JSON_NAME)) {
setName(js.getString(GTaskStringUtils.GTASK_JSON_NAME));
}
@@ -129,7 +139,9 @@ public class TaskList extends Node {
}
}
+ // 用本地数据库里的JSON数据,填充任务列表的属性
public void setContentByLocalJSON(JSONObject js) {
+ // 先做个防御性编程,检查传入的js是否有效
if (js == null || !js.has(GTaskStringUtils.META_HEAD_NOTE)) {
Log.w(TAG, "setContentByLocalJSON: nothing is avaiable");
}
@@ -137,10 +149,12 @@ public class TaskList extends Node {
try {
JSONObject folder = js.getJSONObject(GTaskStringUtils.META_HEAD_NOTE);
+ // 根据本地文件夹的类型来设置任务列表的名字,这里有个特殊的"MIUI_"前缀,应该是为了区分
if (folder.getInt(NoteColumns.TYPE) == Notes.TYPE_FOLDER) {
String name = folder.getString(NoteColumns.SNIPPET);
setName(GTaskStringUtils.MIUI_FOLDER_PREFFIX + name);
} else if (folder.getInt(NoteColumns.TYPE) == Notes.TYPE_SYSTEM) {
+ // 系统文件夹要做特殊判断,根据ID来区分是哪个
if (folder.getLong(NoteColumns.ID) == Notes.ID_ROOT_FOLDER)
setName(GTaskStringUtils.MIUI_FOLDER_PREFFIX + GTaskStringUtils.FOLDER_DEFAULT);
else if (folder.getLong(NoteColumns.ID) == Notes.ID_CALL_RECORD_FOLDER)
@@ -157,16 +171,19 @@ public class TaskList extends Node {
}
}
+ // 反过来,把任务列表对象转换成本地数据库要存的JSON格式
public JSONObject getLocalJSONFromContent() {
try {
JSONObject js = new JSONObject();
JSONObject folder = new JSONObject();
String folderName = getName();
+ // 如果名字里有约定的前缀,就把它去掉再存,保证本地数据是干净的
if (getName().startsWith(GTaskStringUtils.MIUI_FOLDER_PREFFIX))
folderName = folderName.substring(GTaskStringUtils.MIUI_FOLDER_PREFFIX.length(),
folderName.length());
folder.put(NoteColumns.SNIPPET, folderName);
+ // 根据名字判断是系统文件夹还是普通文件夹,然后设置正确的类型
if (folderName.equals(GTaskStringUtils.FOLDER_DEFAULT)
|| folderName.equals(GTaskStringUtils.FOLDER_CALL_NOTE))
folder.put(NoteColumns.TYPE, Notes.TYPE_SYSTEM);
@@ -182,29 +199,30 @@ public class TaskList extends Node {
return null;
}
}
-
+ // 比较本地和远程数据,决定同步策略
public int getSyncAction(Cursor c) {
try {
if (c.getInt(SqlNote.LOCAL_MODIFIED_COLUMN) == 0) {
- // there is no local update
+ // Case 1: 本地数据从上次同步以来,没被修改过
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
+ // Case 2: 本地数据被修改了
+ // 先做个安全检查,看gid对不对得上
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;
}
}
@@ -220,12 +238,13 @@ public class TaskList extends Node {
return mChildren.size();
}
+ // 添加一个子任务到列表末尾
public boolean addChildTask(Task task) {
boolean ret = false;
- if (task != null && !mChildren.contains(task)) {
+ if (task != null && !mChildren.contains(task)) { // 避免空指针和重复添加
ret = mChildren.add(task);
if (ret) {
- // need to set prior sibling and parent
+ // 加到列表里只是第一步,关键是要维护好任务之间的前后关系(priorSibling)和父子关系(parent)
task.setPriorSibling(mChildren.isEmpty() ? null : mChildren
.get(mChildren.size() - 1));
task.setParent(this);
@@ -234,6 +253,7 @@ public class TaskList extends Node {
return ret;
}
+ // 在指定位置插入一个子任务
public boolean addChildTask(Task task, int index) {
if (index < 0 || index > mChildren.size()) {
Log.e(TAG, "add child task: invalid index");
@@ -241,10 +261,10 @@ public class TaskList extends Node {
}
int pos = mChildren.indexOf(task);
- if (task != null && pos == -1) {
+ if (task != null && pos == -1) { // 同样,任务不能为null,也不能已存在
mChildren.add(index, task);
- // update the task list
+ // 插入后,需要更新新任务和它后面任务的前后关系,把链条接好
Task preTask = null;
Task afterTask = null;
if (index != 0)
@@ -260,18 +280,19 @@ public class TaskList extends Node {
return true;
}
+ // 移除一个子任务
public boolean removeChildTask(Task task) {
boolean ret = false;
int index = mChildren.indexOf(task);
- if (index != -1) {
+ if (index != -1) { // 必须先找到这个任务
ret = mChildren.remove(task);
if (ret) {
- // reset prior sibling and parent
+ // 移除后,也要把它的父子关系和前后关系都断开,让它变成一个“孤儿”
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 +302,28 @@ public class TaskList extends Node {
return ret;
}
+ // 移动一个子任务到新的位置
public boolean moveChildTask(Task task, int index) {
-
+ // 边界条件检查,防止数组越界
if (index < 0 || index >= mChildren.size()) {
Log.e(TAG, "move child task: invalid index");
return false;
}
int pos = mChildren.indexOf(task);
- if (pos == -1) {
+ if (pos == -1) { // 要移动的任务必须得在列表里
Log.e(TAG, "move child task: the task should in the list");
return false;
}
+ // 如果位置没变,就不用折腾了
if (pos == index)
return true;
+ // 这个实现很巧妙,移动操作 = 先移除 + 再插入,代码复用得很好
return (removeChildTask(task) && addChildTask(task, index));
}
+ // 通过Gid找到一个子任务
public Task findChildTaskByGid(String gid) {
for (int i = 0; i < mChildren.size(); i++) {
Task t = mChildren.get(i);
@@ -321,6 +346,7 @@ public class TaskList extends Node {
return mChildren.get(index);
}
+ // 这个命名不太规范,Chil应该是Child,建议改成findChildTaskByGid,和上面那个方法统一
public Task getChilTaskByGid(String gid) {
for (Task task : mChildren) {
if (task.getGid().equals(gid))
@@ -340,4 +366,4 @@ public class TaskList extends Node {
public int getIndex() {
return this.mIndex;
}
-}
+}
\ No newline at end of file
diff --git a/src/src/net/micode/notes/gtask/exception/ActionFailureException.java b/src/src/net/micode/notes/gtask/exception/ActionFailureException.java
index 15504be..d3aed1b 100644
--- a/src/src/net/micode/notes/gtask/exception/ActionFailureException.java
+++ b/src/src/net/micode/notes/gtask/exception/ActionFailureException.java
@@ -16,18 +16,27 @@
package net.micode.notes.gtask.exception;
+/**
+ * @Author: 林迪文
+ * @Updator: 林迪文
+ * @Date: 2025/12/20 16:10
+ * @Description: 定义一个自定义的运行时异常,用来表示某个操作执行失败的情况。
+ * 尤其是在处理Google Task相关的逻辑时,如果某个操作没有成功,就可以抛出这个异常。
+ */
public class ActionFailureException extends RuntimeException {
+ // 这个序列化ID主要是为了在序列化和反序列化时,能够保持类版本的兼容性。
+ // 虽然是运行时异常,但加了也更规范,预防以后万一需要序列化这个异常对象。
private static final long serialVersionUID = 4425249765923293627L;
public ActionFailureException() {
- super();
+ super(); // 调用父类RuntimeException的无参构造方法
}
public ActionFailureException(String paramString) {
- super(paramString);
+ super(paramString); // 调用父类RuntimeException的带消息参数的构造方法
}
public ActionFailureException(String paramString, Throwable paramThrowable) {
- super(paramString, paramThrowable);
+ super(paramString, paramThrowable); // 与上面类似
}
-}
+}
\ No newline at end of file
diff --git a/src/src/net/micode/notes/gtask/exception/NetworkFailureException.java b/src/src/net/micode/notes/gtask/exception/NetworkFailureException.java
index b08cfb1..7ba8ac6 100644
--- a/src/src/net/micode/notes/gtask/exception/NetworkFailureException.java
+++ b/src/src/net/micode/notes/gtask/exception/NetworkFailureException.java
@@ -3,6 +3,7 @@
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with 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
@@ -16,18 +17,28 @@
package net.micode.notes.gtask.exception;
+/**
+ * @Author: 林迪文
+ * @Updator: 林迪文
+ * @Date: 2025/12/19 20:10
+ * @Description: 定义一个用于表示网络失败的异常。
+ */
public class NetworkFailureException extends Exception {
+ // 序列化版本ID
private static final long serialVersionUID = 2107610287180234136L;
+ // 下面是几个重载的构造方法,它们最终都是调用父类 Exception 的构造方法来完成初始化
public NetworkFailureException() {
super();
}
+ // 只允许传入一个描述性的错误信息字符串
public NetworkFailureException(String paramString) {
- super(paramString);
+ super(paramString); // 把错误信息传给父类
}
+ // 不仅能提供错误信息,还能把原始异常也包装进来。
public NetworkFailureException(String paramString, Throwable paramThrowable) {
super(paramString, paramThrowable);
}
-}
+}
\ No newline at end of file
diff --git a/src/src/net/micode/notes/gtask/remote/GTaskASyncTask.java b/src/src/net/micode/notes/gtask/remote/GTaskASyncTask.java
index b3b61e7..19f6904 100644
--- a/src/src/net/micode/notes/gtask/remote/GTaskASyncTask.java
+++ b/src/src/net/micode/notes/gtask/remote/GTaskASyncTask.java
@@ -1,10 +1,10 @@
-
/*
* 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
+ * You may
+ * obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
@@ -28,70 +28,98 @@ import net.micode.notes.R;
import net.micode.notes.ui.NotesListActivity;
import net.micode.notes.ui.NotesPreferenceActivity;
-
+/**
+ * @Author: 林迪文
+ * @Updator: 林迪文
+ * @Date: 2025/12/21 16:10
+ * @Description: 这是一个专门用来和 Google Tasks 进行同步的后台任务类。
+ * 继承了 AsyncTask,专门用来处理耗时操作。
+ * 把网络请求放到一个单独的线程里去做,这样就不会卡住主界面(UI线程)。
+ */
public class GTaskASyncTask extends AsyncTask {
+ // 给同步时弹出的通知栏消息一个唯一的ID,这样后面可以根据这个ID来更新或者取消它
private static int GTASK_SYNC_NOTIFICATION_ID = 5234235;
+ // 定义一个回调接口,当整个同步任务完成时,会通过这个接口通知调用者
public interface OnCompleteListener {
void onComplete();
}
- private Context mContext;
-
- private NotificationManager mNotifiManager;
-
- private GTaskManager mTaskManager;
-
- private OnCompleteListener mOnCompleteListener;
+ private Context mContext; // 保存一个上下文引用,用来访问系统资源
+ private NotificationManager mNotifiManager; // 用来发通知栏消息
+ private GTaskManager mTaskManager; // 负责执行同步逻辑的管理类
+ private OnCompleteListener mOnCompleteListener; // 任务完成后的回调监听器
public GTaskASyncTask(Context context, OnCompleteListener listener) {
mContext = context;
mOnCompleteListener = listener;
+ // 从系统服务里拿到 NotificationManager 的实例
mNotifiManager = (NotificationManager) mContext
.getSystemService(Context.NOTIFICATION_SERVICE);
+ // 保证整个App里只有一个实例在工作
mTaskManager = GTaskManager.getInstance();
}
+ // 提供一个公开的方法,让外部可以取消正在进行的同步任务
public void cancelSync() {
mTaskManager.cancelSync();
}
+ // 这是一个自定义的工具方法,方便在后台任务里调用,用来发布进度
public void publishProgess(String message) {
+ // 调用 AsyncTask 自带的 publishProgress 方法,它会触发 onProgressUpdate
publishProgress(new String[] {
- message
+ message
});
}
+ /**
+ * @Description: 封装了创建和显示通知栏消息的逻辑。
+ * @param tickerId 状态栏上滚动的提示文字的资源ID
+ * @param content 通知栏里显示的详细内容
+ */
private void showNotification(int tickerId, String content) {
- Notification notification = new Notification(R.drawable.notification, mContext
- .getString(tickerId), System.currentTimeMillis());
- notification.defaults = Notification.DEFAULT_LIGHTS;
- notification.flags = Notification.FLAG_AUTO_CANCEL;
PendingIntent pendingIntent;
+ // 这里有个逻辑判断:如果同步成功,点击通知就跳到笔记列表;如果失败了,就跳到设置页面,方便用户检查账户设置
if (tickerId != R.string.ticker_success) {
pendingIntent = PendingIntent.getActivity(mContext, 0, new Intent(mContext,
- NotesPreferenceActivity.class), 0);
-
+ NotesPreferenceActivity.class), PendingIntent.FLAG_IMMUTABLE);
} else {
pendingIntent = PendingIntent.getActivity(mContext, 0, new Intent(mContext,
- NotesListActivity.class), 0);
+ NotesListActivity.class), PendingIntent.FLAG_IMMUTABLE);
}
- notification.setLatestEventInfo(mContext, mContext.getString(R.string.app_name), content,
- pendingIntent);
- mNotifiManager.notify(GTASK_SYNC_NOTIFICATION_ID, notification);
+
+ // 使用 Builder 模式来创建 Notification 对象
+ Notification.Builder builder = new Notification.Builder(mContext)
+ .setSmallIcon(R.drawable.notification) // 设置小图标
+ .setTicker(mContext.getString(tickerId)) // 设置滚动提示
+ .setContentTitle(mContext.getString(R.string.app_name)) // 设置标题
+ .setContentText(content) // 设置内容
+ .setContentIntent(pendingIntent) // 设置点击事件
+ .setDefaults(Notification.DEFAULT_LIGHTS)
+ .setAutoCancel(true); // 点击后自动消失
+
+ // 调用 notify 方法把通知发出去
+ mNotifiManager.notify(GTASK_SYNC_NOTIFICATION_ID, builder.build());
}
@Override
protected Integer doInBackground(Void... unused) {
+ // 这是 AsyncTask 的核心,这个方法里的代码会在后台线程执行
+ // 先发布一个进度,告诉用户正在登录
publishProgess(mContext.getString(R.string.sync_progress_login, NotesPreferenceActivity
.getSyncAccountName(mContext)));
+ // 调用 GTaskManager 的 sync 方法来执行真正的同步操作,并把结果返回
return mTaskManager.sync(mContext, this);
}
@Override
protected void onProgressUpdate(String... progress) {
+ // 这个方法在UI线程执行,用来响应后台的 publishProgress 调用
+ // 1. 用传过来的进度信息更新通知栏
showNotification(R.string.ticker_syncing, progress[0]);
+ // 2. 如果任务是从 GTaskSyncService 启动的,就发个广播出去,让其他地方也能收到进度更新
if (mContext instanceof GTaskSyncService) {
((GTaskSyncService) mContext).sendBroadcast(progress[0]);
}
@@ -99,9 +127,12 @@ public class GTaskASyncTask extends AsyncTask {
@Override
protected void onPostExecute(Integer result) {
+ // 当 doInBackground 执行完毕后,这个方法会在UI线程被调用
+ // 根据返回的结果码,显示不同的通知,告诉用户同步是成功了还是失败了
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));
@@ -111,13 +142,15 @@ public class GTaskASyncTask extends AsyncTask {
showNotification(R.string.ticker_cancel, mContext
.getString(R.string.error_sync_cancelled));
}
+
+ // 不管同步结果如何,只要任务结束了,就调用onComplete 方法
if (mOnCompleteListener != null) {
+ // 开了一个新线程去执行 onComplete
new Thread(new Runnable() {
-
public void run() {
mOnCompleteListener.onComplete();
}
}).start();
}
}
-}
+}
\ No newline at end of file
diff --git a/src/src/net/micode/notes/gtask/remote/GTaskClient.java b/src/src/net/micode/notes/gtask/remote/GTaskClient.java
index c67dfdf..f843e10 100644
--- a/src/src/net/micode/notes/gtask/remote/GTaskClient.java
+++ b/src/src/net/micode/notes/gtask/remote/GTaskClient.java
@@ -61,145 +61,193 @@ import java.util.zip.Inflater;
import java.util.zip.InflaterInputStream;
+/**
+ * @Author: 林迪文
+ * @Updator: 林迪文
+ * @Date 2025/12/21 19:20
+ * @Description 这个类是专门跟Google Tasks服务器打交道的。登录、获取、修改便签数据,都靠它了。可以理解成一个封装好的网络请求工具。
+ */
public class GTaskClient {
+ // 日志的标签,方便在Logcat里按这个名字过滤,只看这个类相关的日志
private static final String TAG = GTaskClient.class.getSimpleName();
+ // Google Tasks的基础网址
private static final String GTASK_URL = "https://mail.google.com/tasks/";
+ // 获取数据的GET请求地址
private static final String GTASK_GET_URL = "https://mail.google.com/tasks/ig";
+ // 提交数据的POST请求地址
private static final String GTASK_POST_URL = "https://mail.google.com/tasks/r/ig";
+ // 单例模式,保证整个App里只有一个GTaskClient实例
private static GTaskClient mInstance = null;
+ // 用来发HTTP请求的客户端,是Apache提供的库
private DefaultHttpClient mHttpClient;
+ // 实际使用的GET地址,可能会根据邮箱域名变化
private String mGetUrl;
+ // 实际使用的POST地址
private String mPostUrl;
+ // 客户端版本号。发请求时得带上,服务器要根据这个判断
private long mClientVersion;
+ // 登录状态的标记
private boolean mLoggedin;
+ // 上次登录成功的时间戳,用来判断登录状态是否过期
private long mLastLoginTime;
+ // 操作ID,每次操作递增,防止请求重复
private int mActionId;
+ // 当前登录的Google账户信息
private Account mAccount;
+ // 用来批量提交更新操作的JSON数组
private JSONArray mUpdateArray;
+ // 私有的构造方法,不让外面直接new,配合getInstance()实现单例
private GTaskClient() {
mHttpClient = null;
- mGetUrl = GTASK_GET_URL;
+ mGetUrl = GTASK_GET_URL; // 默认用官方地址
mPostUrl = GTASK_POST_URL;
- mClientVersion = -1;
- mLoggedin = false;
+ mClientVersion = -1; // 默认-1,表示还没从服务器获取
+ mLoggedin = false; // 默认未登录
mLastLoginTime = 0;
- mActionId = 1;
+ mActionId = 1; // 从1开始
mAccount = null;
mUpdateArray = null;
}
+ /**
+ * 获取GTaskClient的唯一实例
+ */
public static synchronized GTaskClient getInstance() {
+ // synchronized关键字是用来防止多线程下重复创建实例的
if (mInstance == null) {
mInstance = new GTaskClient();
}
return mInstance;
}
+ /**
+ * 核心的登录逻辑
+ * @param activity 需要一个Activity作为上下文,给AccountManager用
+ * @return 登录成功还是失败
+ */
public boolean login(Activity activity) {
- // we suppose that the cookie would expire after 5 minutes
- // then we need to re-login
- final long interval = 1000 * 60 * 5;
+ // 假设登录状态5分钟过期,超时了就需要重新登录,避免token失效
+ final long interval = 1000 * 60 * 5; // 5分钟的毫秒数
if (mLastLoginTime + interval < System.currentTimeMillis()) {
- mLoggedin = false;
+ mLoggedin = false; // 标记为未登录
}
- // need to re-login after account switch
+ // 如果用户在设置里切换了同步的Google账户,那之前的登录状态就作废了,也得重新登录
if (mLoggedin
&& !TextUtils.equals(getSyncAccount().name, NotesPreferenceActivity
- .getSyncAccountName(activity))) {
+ .getSyncAccountName(activity))) {
mLoggedin = false;
}
+ // 如果已经是登录状态,就没必要再走一遍登录流程了,直接返回成功
if (mLoggedin) {
Log.d(TAG, "already logged in");
return true;
}
- mLastLoginTime = System.currentTimeMillis();
+ mLastLoginTime = System.currentTimeMillis(); // 更新一下最后登录时间
+ // 调用下面的方法去获取Google账户的认证令牌(token),这是第一步
String authToken = loginGoogleAccount(activity, false);
if (authToken == null) {
- Log.e(TAG, "login google account failed");
+ Log.e(TAG, "login google account failed"); // 拿不到token,直接失败
return false;
}
- // login with custom domain if necessary
+ // 如果登录的邮箱不是标准的gmail.com或者googlemail.com,需要拼接一个特殊的URL
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;
- String suffix = mAccount.name.substring(index);
+ String suffix = mAccount.name.substring(index); // 截取@后面的域名
url.append(suffix + "/");
- mGetUrl = url.toString() + "ig";
+ mGetUrl = url.toString() + "ig"; // 拼出新的请求地址
mPostUrl = url.toString() + "r/ig";
+ // 用这个定制的URL去尝试登录
if (tryToLoginGtask(activity, authToken)) {
mLoggedin = true;
}
}
- // try to login with google official url
+ // 如果上面用定制URL没登录成功,或者压根就不是定制邮箱,就用官方标准URL再试一次
if (!mLoggedin) {
mGetUrl = GTASK_GET_URL;
mPostUrl = GTASK_POST_URL;
if (!tryToLoginGtask(activity, authToken)) {
- return false;
+ return false; // 标准地址也失败了,那就是真的失败了
}
}
- mLoggedin = true;
+ mLoggedin = true; // 登录成功,打上标记
return true;
}
+ /**
+ * 和Android系统的账户系统交互,获取指定Google账户的AuthToken
+ * @param activity 上下文
+ * @param invalidateToken 是否需要让旧的token失效
+ * @return 获取到的AuthToken字符串,失败则为null
+ */
private String loginGoogleAccount(Activity activity, boolean invalidateToken) {
String authToken;
+ // 这是Android系统里专门管理各种账户的工具
AccountManager accountManager = AccountManager.get(activity);
+ // 按"com.google"这个类型,拿到手机上所有登录过的Google账户
Account[] accounts = accountManager.getAccountsByType("com.google");
if (accounts.length == 0) {
- Log.e(TAG, "there is no available google account");
+ Log.e(TAG, "there is no available google account"); // 手机上一个Google账户都没有
return null;
}
+ // 从SharedPreferences里读取用户在设置页面选好的账户名
String accountName = NotesPreferenceActivity.getSyncAccountName(activity);
Account account = null;
+ // 遍历手机里的所有Google账户
for (Account a : accounts) {
+ // 找到跟我们设置里存的账户名一样的那个
if (a.name.equals(accountName)) {
account = a;
break;
}
}
if (account != null) {
- mAccount = account;
+ mAccount = account; // 找到了就存到成员变量里
} else {
+ // 如果系统账户里找不到设置里存的那个名字,说明可能账户被移除了
Log.e(TAG, "unable to get an account with the same name in the settings");
return null;
}
- // get the token now
+ // 开始获取token,这是一个异步操作
AccountManagerFuture accountManagerFuture = accountManager.getAuthToken(account,
- "goanna_mobile", null, activity, null, null);
+ "goanna_mobile", null, activity, null, null); // "goanna_mobile"是Google Tasks服务的类型名
try {
+ // getResult()会阻塞当前线程,直到异步操作有结果返回
Bundle authTokenBundle = accountManagerFuture.getResult();
authToken = authTokenBundle.getString(AccountManager.KEY_AUTHTOKEN);
if (invalidateToken) {
+ // 如果需要,就让刚才拿到的这个token失效,然后递归调用自己,重新走一遍流程获取一个新的。
+ // 这一般是在token过期认证失败后才需要做的。
accountManager.invalidateAuthToken("com.google", authToken);
loginGoogleAccount(activity, false);
}
} catch (Exception e) {
+ // 获取token的过程中可能出现各种异常
Log.e(TAG, "get auth token failed");
authToken = null;
}
@@ -207,72 +255,92 @@ public class GTaskClient {
return authToken;
}
+ /**
+ * 用上一步拿到的token尝试登录Gtask。这里面有个重试机制,如果第一次失败了,会认为token过期了,然后让它失效再重新获取一次,再试。
+ * @param activity 上下文
+ * @param authToken 认证令牌
+ * @return 登录成功返回true
+ */
private boolean tryToLoginGtask(Activity activity, String authToken) {
+ // 先直接用传进来的token试一次
if (!loginGtask(authToken)) {
- // maybe the auth token is out of date, now let's invalidate the
- // token and try again
+ // 如果失败了,很可能是token过期了。
+ // 重新调用loginGoogleAccount,并且第二个参数传true,意思是让旧的token失效
authToken = loginGoogleAccount(activity, true);
if (authToken == null) {
- Log.e(TAG, "login google account failed");
+ Log.e(TAG, "login google account failed"); // 重新获取token都失败了,那肯定不行
return false;
}
+ // 用新拿到的token再试最后一次
if (!loginGtask(authToken)) {
- Log.e(TAG, "login gtask failed");
+ Log.e(TAG, "login gtask failed"); // 换了新token还不行,那就是网络或者其他问题了
return false;
}
}
return true;
}
+ /**
+ * 这是真正执行网络操作的地方。它会配置好HttpClient,发一个GET请求到Google服务器,拿到登录后的Cookie和后面操作要用的clientVersion。
+ * @param authToken 认证令牌
+ * @return 登录成功返回true
+ */
private boolean loginGtask(String authToken) {
- int timeoutConnection = 10000;
- int timeoutSocket = 15000;
+ int timeoutConnection = 10000; // 连接超时时间,10秒
+ int timeoutSocket = 15000; // socket通信超时时间,15秒
HttpParams httpParameters = new BasicHttpParams();
+ // 把超时设置放进参数里
HttpConnectionParams.setConnectionTimeout(httpParameters, timeoutConnection);
HttpConnectionParams.setSoTimeout(httpParameters, timeoutSocket);
+ // DefaultHttpClient是发HTTP请求的主力军,把超时参数给它
mHttpClient = new DefaultHttpClient(httpParameters);
+ // 建一个存Cookie的地方。登录成功后服务器会给我们发Cookie,后面带着它访问就不用再登录了
BasicCookieStore localBasicCookieStore = new BasicCookieStore();
mHttpClient.setCookieStore(localBasicCookieStore);
+ // 这个ExpectContinue握手协议关掉,能提高点效率
HttpProtocolParams.setUseExpectContinue(mHttpClient.getParams(), false);
- // login gtask
+ // 开始正式登录
try {
+ // 把token拼接到URL后面作为认证参数
String loginUrl = mGetUrl + "?auth=" + authToken;
HttpGet httpGet = new HttpGet(loginUrl);
HttpResponse response = null;
- response = mHttpClient.execute(httpGet);
+ response = mHttpClient.execute(httpGet); // 发送请求
- // get the cookie now
+ // 从HttpClient里把服务器返回的Cookie拿出来
List cookies = mHttpClient.getCookieStore().getCookies();
boolean hasAuthCookie = false;
+ // 遍历所有Cookie,看看有没有名字里带"GTL"的
for (Cookie cookie : cookies) {
if (cookie.getName().contains("GTL")) {
- hasAuthCookie = true;
+ hasAuthCookie = true; // "GTL"是Google Tasks登录凭证的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
+ // 把响应内容读出来转成字符串,具体实现在下面的getResponseContent方法
String resString = getResponseContent(response.getEntity());
+ // 返回的不是纯JSON,是一段网页代码,我们需要的数据在_setup()这个js函数里,得手动截取出来
String jsBegin = "_setup(";
String jsEnd = ")}";
int begin = resString.indexOf(jsBegin);
int end = resString.lastIndexOf(jsEnd);
String jsString = null;
if (begin != -1 && end != -1 && begin < end) {
- jsString = resString.substring(begin + jsBegin.length(), end);
+ jsString = resString.substring(begin + jsBegin.length(), end); // 截取中间的JSON部分
}
- JSONObject js = new JSONObject(jsString);
- mClientVersion = js.getLong("v");
+ JSONObject js = new JSONObject(jsString); // 把截出来的字符串转成JSON对象
+ mClientVersion = js.getLong("v"); // 从JSON里把"v"这个key对应的值拿出来,这就是客户端版本号,后面发请求都要带上
} catch (JSONException e) {
Log.e(TAG, e.toString());
e.printStackTrace();
return false;
} catch (Exception e) {
- // simply catch all exceptions
+ // 网络异常
Log.e(TAG, "httpget gtask_url failed");
return false;
}
@@ -280,103 +348,132 @@ public class GTaskClient {
return true;
}
+ /**
+ * 获取一个自增的操作ID
+ */
private int getActionId() {
- return mActionId++;
+ return mActionId++; // 每次调用都加1,保证每个操作的ID不一样
}
+ /**
+ * 创建一个已经配置好请求头的HttpPost对象,方便复用
+ */
private HttpPost createHttpPost() {
HttpPost httpPost = new HttpPost(mPostUrl);
+ // 设置请求头,告诉服务器我们发的是表单数据,编码是UTF-8
httpPost.setHeader("Content-Type", "application/x-www-form-urlencoded;charset=utf-8");
+ // AT: 1 这个头应该是Google Tasks API的特定要求
httpPost.setHeader("AT", "1");
return httpPost;
}
+ /**
+ * @Description 从HTTP响应实体(HttpEntity)里把返回的内容读成一个字符串。这个方法比较关键,因为它还处理了服务器可能返回的gzip压缩数据。
+ * @param entity http响应实体
+ * @return 响应内容的字符串
+ * @throws IOException
+ */
private String getResponseContent(HttpEntity entity) throws IOException {
String contentEncoding = null;
if (entity.getContentEncoding() != null) {
- contentEncoding = entity.getContentEncoding().getValue();
+ contentEncoding = entity.getContentEncoding().getValue(); // 先看看服务器返回的数据有没有被压缩
Log.d(TAG, "encoding: " + contentEncoding);
}
- InputStream input = entity.getContent();
+ InputStream input = entity.getContent(); // 拿到原始的输入流(字节流)
+ // 如果内容是gzip或者deflate压缩的,就要用对应的流来解压,不然读出来是乱码
if (contentEncoding != null && contentEncoding.equalsIgnoreCase("gzip")) {
- input = new GZIPInputStream(entity.getContent());
+ input = new GZIPInputStream(entity.getContent()); // GZIPInputStream是专门解压gzip格式的
} 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();
+ InputStreamReader isr = new InputStreamReader(input); // 字节流转字符流,做编码翻译
+ BufferedReader br = new BufferedReader(isr); // 加个缓冲,读起来快一点
+ StringBuilder sb = new StringBuilder(); // 用StringBuilder来拼接字符串,效率高
while (true) {
- String buff = br.readLine();
+ String buff = br.readLine(); // 一行一行地读
if (buff == null) {
- return sb.toString();
+ return sb.toString(); // 读完了,把结果转成String返回
}
- sb = sb.append(buff);
+ sb = sb.append(buff); // 把读到的内容拼到sb里
}
} finally {
- input.close();
+ // finally代码块里的内容,不管try里面有没有出异常,都一定会执行
+ input.close(); // 确保输入流一定会被关闭,这是为了防止资源泄漏,好习惯
}
}
+ /**
+ * 封装一个通用的POST请求方法。所有需要向服务器提交数据的操作(比如新建、修改)都走这里。
+ * @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");
}
- HttpPost httpPost = createHttpPost();
+ HttpPost httpPost = createHttpPost(); // 复用前面写好的方法,把请求头都设好
try {
+ // 建个list,用来放要POST的参数
LinkedList list = new LinkedList();
+ // Google的接口要求把整个JSON操作数据包转成字符串,然后放到一个名叫"r"的参数里
list.add(new BasicNameValuePair("r", js.toString()));
+ // 把参数列表打包成http请求能认识的实体格式,编码用UTF-8
UrlEncodedFormEntity entity = new UrlEncodedFormEntity(list, "UTF-8");
httpPost.setEntity(entity);
- // execute the post
+ // 发送
HttpResponse response = mHttpClient.execute(httpPost);
+ // 用上面写好的方法把返回结果读出来
String jsString = getResponseContent(response.getEntity());
- return new JSONObject(jsString);
+ return new JSONObject(jsString); // 把返回的字符串转成JSON对象,方便后面解析
} catch (ClientProtocolException e) {
Log.e(TAG, e.toString());
e.printStackTrace();
- throw new NetworkFailureException("postRequest failed");
+ throw new NetworkFailureException("postRequest failed"); // 协议错误,一般是客户端代码问题
} catch (IOException e) {
Log.e(TAG, e.toString());
e.printStackTrace();
- throw new NetworkFailureException("postRequest failed");
+ throw new NetworkFailureException("postRequest failed"); // IO异常,多半是网络不通
} catch (JSONException e) {
Log.e(TAG, e.toString());
e.printStackTrace();
- throw new ActionFailureException("unable to convert response content to jsonobject");
+ throw new ActionFailureException("unable to convert response content to jsonobject"); // 返回的数据不是标准的JSON,解析不了
} catch (Exception e) {
Log.e(TAG, e.toString());
e.printStackTrace();
- throw new ActionFailureException("error occurs when posting request");
+ throw new ActionFailureException("error occurs when posting request"); // 兜底
}
}
+ // 新建一个任务
public void createTask(Task task) throws NetworkFailureException {
- commitUpdate();
+ commitUpdate(); // 发新请求之前,先把之前攒着的更新操作(比如改名字)都提交了,保证数据一致
try {
- JSONObject jsPost = new JSONObject();
- JSONArray actionList = new JSONArray();
+ JSONObject jsPost = new JSONObject(); // 创建一个JSON对象,作为整个请求的数据包
+ JSONArray actionList = new JSONArray(); // 再创建一个JSON数组,专门放具体的操作指令
- // action_list
+ // 调用Task对象自己的方法,生成一个符合Gtask API规范的'创建'操作,然后塞到actionList里
actionList.put(task.getCreateAction(getActionId()));
+ // 把actionList和客户端版本号都塞到请求数据包里
jsPost.put(GTaskStringUtils.GTASK_JSON_ACTION_LIST, actionList);
-
- // client_version
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);
+ // 从返回结果里把Google服务器给这个新任务分配的唯一ID(gid)拿出来,存回我们自己的Task对象里。修改删除都靠这个ID
task.setGid(jsResult.getString(GTaskStringUtils.GTASK_JSON_NEW_ID));
} catch (JSONException e) {
@@ -386,23 +483,23 @@ public class GTaskClient {
}
}
+ // 新建一个任务清单(就是便签夹)
public void createTaskList(TaskList tasklist) throws NetworkFailureException {
- commitUpdate();
+ commitUpdate(); // 同样,先提交本地缓存的修改
try {
+ // 下面的逻辑和createTask几乎一模一样,就是把Task对象换成了TaskList对象
JSONObject jsPost = new JSONObject();
JSONArray actionList = new JSONArray();
- // action_list
actionList.put(tasklist.getCreateAction(getActionId()));
jsPost.put(GTaskStringUtils.GTASK_JSON_ACTION_LIST, actionList);
- // client version
jsPost.put(GTaskStringUtils.GTASK_JSON_CLIENT_VERSION, mClientVersion);
- // post
JSONObject jsResponse = postRequest(jsPost);
JSONObject jsResult = (JSONObject) jsResponse.getJSONArray(
GTaskStringUtils.GTASK_JSON_RESULTS).get(0);
+ // 把服务器返回的新gid存到TaskList对象里
tasklist.setGid(jsResult.getString(GTaskStringUtils.GTASK_JSON_NEW_ID));
} catch (JSONException e) {
@@ -412,19 +509,19 @@ public class GTaskClient {
}
}
+ // 提交攒着的一批更新
public void commitUpdate() throws NetworkFailureException {
- if (mUpdateArray != null) {
+ if (mUpdateArray != null) { // 先检查下有没有攒着没提交的更新
try {
+ // 这个方法就是把mUpdateArray里攒的一堆更新操作一次性发给服务器,比一条一条发效率高
JSONObject jsPost = new JSONObject();
- // action_list
jsPost.put(GTaskStringUtils.GTASK_JSON_ACTION_LIST, mUpdateArray);
- // client_version
jsPost.put(GTaskStringUtils.GTASK_JSON_CLIENT_VERSION, mClientVersion);
postRequest(jsPost);
- mUpdateArray = null;
+ mUpdateArray = null; // 发完之后,把这个数组清空,免得下次重复提交
} catch (JSONException e) {
Log.e(TAG, e.toString());
e.printStackTrace();
@@ -433,48 +530,49 @@ public class GTaskClient {
}
}
+ // 把一个“更新”操作(比如修改标题、备注)加到待提交的数组里
public void addUpdateNode(Node node) throws NetworkFailureException {
- if (node != null) {
- // too many update items may result in an error
- // set max to 10 items
+ if (node != null) { // 先判空
+ // 这里做了个优化。如果攒的操作超过10个了,就先自动提交一次。估计是怕一次性提交太多数据,服务器那边会报错或者超时
if (mUpdateArray != null && mUpdateArray.length() > 10) {
commitUpdate();
}
- if (mUpdateArray == null)
+ if (mUpdateArray == null) // 如果数组还是空的,就new一个出来
mUpdateArray = new JSONArray();
+ // 把当前的更新操作加到待提交的数组里
mUpdateArray.put(node.getUpdateAction(getActionId()));
}
}
+ // 移动一个任务
public void moveTask(Task task, TaskList preParent, TaskList curParent)
throws NetworkFailureException {
- commitUpdate();
+ commitUpdate(); // 先提交缓存的更新
try {
JSONObject jsPost = new JSONObject();
JSONArray actionList = new JSONArray();
JSONObject action = new JSONObject();
- // action_list
+ // 告诉服务器,这次操作的类型是'move'
action.put(GTaskStringUtils.GTASK_JSON_ACTION_TYPE,
GTaskStringUtils.GTASK_JSON_ACTION_TYPE_MOVE);
action.put(GTaskStringUtils.GTASK_JSON_ACTION_ID, getActionId());
- action.put(GTaskStringUtils.GTASK_JSON_ID, task.getGid());
+ 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());
}
- action.put(GTaskStringUtils.GTASK_JSON_SOURCE_LIST, preParent.getGid());
- action.put(GTaskStringUtils.GTASK_JSON_DEST_PARENT, curParent.getGid());
+ 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
+ // 如果是跨列表移动,还得告诉服务器目标列表的ID
action.put(GTaskStringUtils.GTASK_JSON_DEST_LIST, curParent.getGid());
}
actionList.put(action);
jsPost.put(GTaskStringUtils.GTASK_JSON_ACTION_LIST, actionList);
- // client_version
jsPost.put(GTaskStringUtils.GTASK_JSON_CLIENT_VERSION, mClientVersion);
postRequest(jsPost);
@@ -486,22 +584,23 @@ public class GTaskClient {
}
}
+ // 删除一个节点(可以是任务,也可以是任务清单)
public void deleteNode(Node node) throws NetworkFailureException {
commitUpdate();
try {
JSONObject jsPost = new JSONObject();
JSONArray actionList = new JSONArray();
- // action_list
+ // 这里不是直接发一个'delete'指令,而是把节点的deleted状态改成true
node.setDeleted(true);
+ // 然后发一个'update'指令。这应该是Gtask API的设计,逻辑删除而不是物理删除
actionList.put(node.getUpdateAction(getActionId()));
jsPost.put(GTaskStringUtils.GTASK_JSON_ACTION_LIST, actionList);
- // client_version
jsPost.put(GTaskStringUtils.GTASK_JSON_CLIENT_VERSION, mClientVersion);
postRequest(jsPost);
- mUpdateArray = null;
+ mUpdateArray = null; // 这个命名不太规范,deleteNode里面还把mUpdateArray清空了,应该在commitUpdate里做才对
} catch (JSONException e) {
Log.e(TAG, e.toString());
e.printStackTrace();
@@ -509,18 +608,19 @@ public class GTaskClient {
}
}
+ // 获取所有的任务清单
public JSONArray getTaskLists() throws NetworkFailureException {
- if (!mLoggedin) {
+ if (!mLoggedin) { // 还是先检查登录状态
Log.e(TAG, "please login first");
throw new ActionFailureException("not logged in");
}
try {
- HttpGet httpGet = new HttpGet(mGetUrl);
+ HttpGet httpGet = new HttpGet(mGetUrl); // 获取列表是用GET请求
HttpResponse response = null;
response = mHttpClient.execute(httpGet);
- // get the task list
+ // 和登录时一样,返回的是网页代码,需要从_setup()里把数据截取出来
String resString = getResponseContent(response.getEntity());
String jsBegin = "_setup(";
String jsEnd = ")}";
@@ -531,6 +631,7 @@ public class GTaskClient {
jsString = resString.substring(begin + jsBegin.length(), end);
}
JSONObject js = new JSONObject(jsString);
+ // 数据在't'对象的'lists'数组里
return js.getJSONObject("t").getJSONArray(GTaskStringUtils.GTASK_JSON_LISTS);
} catch (ClientProtocolException e) {
Log.e(TAG, e.toString());
@@ -547,26 +648,26 @@ public class GTaskClient {
}
}
+ // 获取某个清单下的所有任务
public JSONArray getTaskList(String listGid) throws NetworkFailureException {
- commitUpdate();
+ commitUpdate(); // 获取某个列表的详细内容前,也先把本地的修改提交了,保证拿到的是最新的状态
try {
JSONObject jsPost = 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);
+ GTaskStringUtils.GTASK_JSON_ACTION_TYPE_GETALL); // 操作类型是'get_all'
action.put(GTaskStringUtils.GTASK_JSON_ACTION_ID, getActionId());
- action.put(GTaskStringUtils.GTASK_JSON_LIST_ID, listGid);
- action.put(GTaskStringUtils.GTASK_JSON_GET_DELETED, false);
+ action.put(GTaskStringUtils.GTASK_JSON_LIST_ID, listGid); // 告诉服务器要哪个列表的数据
+ action.put(GTaskStringUtils.GTASK_JSON_GET_DELETED, false); // 不获取已经删除的任务
actionList.put(action);
jsPost.put(GTaskStringUtils.GTASK_JSON_ACTION_LIST, actionList);
- // client_version
jsPost.put(GTaskStringUtils.GTASK_JSON_CLIENT_VERSION, mClientVersion);
JSONObject jsResponse = postRequest(jsPost);
+ // 从返回结果里拿到'tasks'这个数组,里面就是这个列表下所有的任务了
return jsResponse.getJSONArray(GTaskStringUtils.GTASK_JSON_TASKS);
} catch (JSONException e) {
Log.e(TAG, e.toString());
@@ -575,10 +676,12 @@ public class GTaskClient {
}
}
+ // 一个简单的getter,给外面获取当前是哪个账户在同步
public Account getSyncAccount() {
return mAccount;
}
+ // 把待提交的更新数组清空,一般是同步出错了或者取消同步的时候调用
public void resetUpdateArray() {
mUpdateArray = null;
}
diff --git a/src/src/net/micode/notes/gtask/remote/GTaskManager.java b/src/src/net/micode/notes/gtask/remote/GTaskManager.java
index d2b4082..1b7d832 100644
--- a/src/src/net/micode/notes/gtask/remote/GTaskManager.java
+++ b/src/src/net/micode/notes/gtask/remote/GTaskManager.java
@@ -48,46 +48,46 @@ import java.util.Iterator;
import java.util.Map;
+/**
+ * @Author: 林迪文
+ * @Updator: 林迪文
+ * @Date 2025/12/22 21:30
+ * @Description 这个是同步功能的核心管理者。它负责整个同步流程的调度,比如登录、拉取云端数据、比对本地和云端的数据差异,然后决定哪些需要上传,哪些需要下载。
+ */
public class GTaskManager {
+ // 日志标签
private static final String TAG = GTaskManager.class.getSimpleName();
- public static final int STATE_SUCCESS = 0;
-
- public static final int STATE_NETWORK_ERROR = 1;
-
- public static final int STATE_INTERNAL_ERROR = 2;
-
- public static final int STATE_SYNC_IN_PROGRESS = 3;
-
- public static final int STATE_SYNC_CANCELLED = 4;
-
- private static GTaskManager mInstance = null;
-
- private Activity mActivity;
-
- private Context mContext;
-
- private ContentResolver mContentResolver;
-
- private boolean mSyncing;
-
- private boolean mCancelled;
-
- private HashMap mGTaskListHashMap;
+ // 定义了一堆同步结果的状态码,方便给调用方(比如Service)返回结果
+ public static final int STATE_SUCCESS = 0; // 0代表成功
+ public static final int STATE_NETWORK_ERROR = 1; // 1是网络问题
+ public static final int STATE_INTERNAL_ERROR = 2; // 2是程序内部错误
+ public static final int STATE_SYNC_IN_PROGRESS = 3; // 3是正在同步中,防止重复启动
+ public static final int STATE_SYNC_CANCELLED = 4; // 4是用户手动取消了
- private HashMap mGTaskHashMap;
+ private static GTaskManager mInstance = null; // 单例模式
- private HashMap mMetaHashMap;
+ private Activity mActivity; // Activity的上下文,主要给GTaskClient登录用
+ private Context mContext; // 全局的上下文
+ private ContentResolver mContentResolver; // 内容提供者,可以理解成咱们App访问便签数据库的“管家”
- private TaskList mMetaList;
+ private boolean mSyncing; // 同步状态的标记
+ private boolean mCancelled; // 取消同步的标记
- private HashSet mLocalDeleteIdMap;
+ // HashMap是整个同步算法的核心,用来在内存里缓存和映射数据
+ private HashMap mGTaskListHashMap; // Google云端任务清单的缓存,key是清单的gid
+ private HashMap mGTaskHashMap; // Google云端所有节点的缓存(包括清单和任务),key是gid
+ private HashMap mMetaHashMap; // Google云端元数据的缓存,key是被关联任务的gid
+ private TaskList mMetaList; // 专门存meta数据的那个清单对象
- private HashMap mGidToNid;
+ private HashSet mLocalDeleteIdMap; // 存放在本地被删除了的笔记ID
- private HashMap mNidToGid;
+ // 这两个是关键,用来建立Google ID和本地数据库ID之间的对应关系
+ private HashMap mGidToNid; // Google ID到本地笔记ID的映射
+ private HashMap mNidToGid; // 本地笔记ID到Google ID的映射
private GTaskManager() {
+ // 构造函数里做一些初始化,防止空指针
mSyncing = false;
mCancelled = false;
mGTaskListHashMap = new HashMap();
@@ -106,20 +106,29 @@ public class GTaskManager {
return mInstance;
}
+ // 设置Activity上下文,因为登录Google账户需要一个Activity
public synchronized void setActivityContext(Activity activity) {
- // used for getting authtoken
mActivity = activity;
}
+ /**
+ * 这是同步操作的总入口,所有同步逻辑都从这里开始。
+ * @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();
@@ -129,33 +138,35 @@ public class GTaskManager {
try {
GTaskClient client = GTaskClient.getInstance();
- client.resetUpdateArray();
+ client.resetUpdateArray(); // 万一上次同步失败有残留,先把待提交队列清一下
- // login google task
+ // 登录
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));
+ // 从Google服务器上把所有任务清单和任务都拉下来,放到内存的HashMap里
+ asyncTask.publishProgess(mContext.getString(R.string.sync_progress_init_list)); // 通知界面,“正在初始化列表...”
initGTaskList();
- // do content sync work
- asyncTask.publishProgess(mContext.getString(R.string.sync_progress_syncing));
+ // 开始比对和同步本地与云端的内容
+ asyncTask.publishProgess(mContext.getString(R.string.sync_progress_syncing)); // 通知界面,“正在同步...”
syncContent();
+
} catch (NetworkFailureException e) {
Log.e(TAG, e.toString());
- return STATE_NETWORK_ERROR;
+ return STATE_NETWORK_ERROR; // 网络异常
} catch (ActionFailureException e) {
Log.e(TAG, e.toString());
- return STATE_INTERNAL_ERROR;
+ return STATE_INTERNAL_ERROR; // 操作失败,比如JSON解析错了
} catch (Exception e) {
Log.e(TAG, e.toString());
e.printStackTrace();
- return STATE_INTERNAL_ERROR;
+ return STATE_INTERNAL_ERROR; // 其他未知错误
} finally {
+ // 关键的收尾工作,不管同步成功、失败还是取消,都要把这些缓存清掉,把状态改回去,保证下次能正常同步
mGTaskListHashMap.clear();
mGTaskHashMap.clear();
mMetaHashMap.clear();
@@ -165,37 +176,43 @@ public class GTaskManager {
mSyncing = false;
}
+ // 最后根据取消标志返回最终状态
return mCancelled ? STATE_SYNC_CANCELLED : STATE_SUCCESS;
}
+ /**
+ * 这个方法负责从Google Tasks服务器拉取所有的数据,并初始化到内存中的各个HashMap里,为后续的数据比对做准备。
+ * @throws NetworkFailureException
+ */
private void initGTaskList() throws NetworkFailureException {
- if (mCancelled)
+ if (mCancelled) // 同步过程中随时检查是否被取消了
return;
GTaskClient client = GTaskClient.getInstance();
try {
- JSONArray jsTaskLists = client.getTaskLists();
+ JSONArray jsTaskLists = client.getTaskLists(); // 从服务器拿到所有清单的JSON数组
- // init meta list first
+ // 优先处理meta清单。这个清单是用来存一些便签的附加信息(比如颜色、提醒时间)的,在Google Tasks界面上看不到,是咱们App自己用的
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); // 清单的gid
+ String name = object.getString(GTaskStringUtils.GTASK_JSON_NAME); // 清单的名字
- if (name
- .equals(GTaskStringUtils.MIUI_FOLDER_PREFFIX + GTaskStringUtils.FOLDER_META)) {
+ // 通过一个特殊的前缀名来识别出哪个是meta清单
+ if (name.equals(GTaskStringUtils.MIUI_FOLDER_PREFFIX + GTaskStringUtils.FOLDER_META)) {
mMetaList = new TaskList();
- mMetaList.setContentByRemoteJSON(object);
+ mMetaList.setContentByRemoteJSON(object); // 把JSON数据填到TaskList对象里
- // load meta data
+ // 找到meta清单后,再去拉这个清单下面所有的meta数据
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);
- if (metaData.isWorthSaving()) {
- mMetaList.addChildTask(metaData);
+ if (metaData.isWorthSaving()) { // 检查一下数据是不是有效
+ mMetaList.addChildTask(metaData); // 加到meta清单的子任务列表里
if (metaData.getGid() != null) {
+ // 把meta数据存到缓存里,key是它关联的那个笔记的gid,方便后面查找
mMetaHashMap.put(metaData.getRelatedGid(), metaData);
}
}
@@ -203,7 +220,7 @@ public class GTaskManager {
}
}
- // create meta list if not existed
+ // 如果服务器上没有meta清单,说明是第一次同步,那咱们就在云端给它创建一个
if (mMetaList == null) {
mMetaList = new TaskList();
mMetaList.setName(GTaskStringUtils.MIUI_FOLDER_PREFFIX
@@ -211,21 +228,23 @@ public class GTaskManager {
GTaskClient.getInstance().createTaskList(mMetaList);
}
- // init task list
+ // 处理正常的便签夹(任务清单)
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);
+ // 通过前缀名过滤出我们自己App创建的清单,忽略用户在Google Tasks上创建的其他清单
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);
+ // 放到清单缓存和总节点缓存里
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);
@@ -233,9 +252,10 @@ public class GTaskManager {
Task task = new Task();
task.setContentByRemoteJSON(object);
if (task.isWorthSaving()) {
+ // 从meta缓存里找到这条便签对应的meta数据,然后关联起来
task.setMetaInfo(mMetaHashMap.get(gid));
- tasklist.addChildTask(task);
- mGTaskHashMap.put(gid, task);
+ tasklist.addChildTask(task); // 把任务加到清单的子节点里
+ mGTaskHashMap.put(gid, task); // 也放到总节点缓存里
}
}
}
@@ -247,11 +267,15 @@ public class GTaskManager {
}
}
+ /**
+ * 这个方法是数据同步的核心。它会遍历本地数据库和从云端拉下来的数据,找出两边的差异,然后调用doContentSync来决定具体执行哪种同步操作。
+ * @throws NetworkFailureException
+ */
private void syncContent() throws NetworkFailureException {
- int syncType;
- Cursor c = null;
- String gid;
- Node node;
+ int syncType; // 用来存同步操作的类型,比如是本地新增、还是远程删除
+ Cursor c = null; // 数据库查询用的游标
+ String gid; // Google ID
+ Node node; // 云端节点对象
mLocalDeleteIdMap.clear();
@@ -259,7 +283,7 @@ public class GTaskManager {
return;
}
- // for local deleted note
+ // 处理本地已经删除的笔记。这些笔记在回收站里,需要通知云端也删除。
try {
c = mContentResolver.query(Notes.CONTENT_NOTE_URI, SqlNote.PROJECTION_NOTE,
"(type<>? AND parent_id=?)", new String[] {
@@ -267,13 +291,14 @@ public class GTaskManager {
}, 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); // 拿到本地笔记对应的Google ID
+ node = mGTaskHashMap.get(gid); // 去云端数据缓存里找找,看云上还有没有
if (node != null) {
- mGTaskHashMap.remove(gid);
- doContentSync(Node.SYNC_ACTION_DEL_REMOTE, node, c);
+ // 如果云上还有,说明这是一个“本地删除,云端保留”的情况
+ mGTaskHashMap.remove(gid); // 从待处理的云端数据里把它移除,因为它已经被处理了
+ doContentSync(Node.SYNC_ACTION_DEL_REMOTE, node, c); // 告诉云端也要删除
}
-
+ // 把这个本地ID记下来,最后统一从本地数据库的data表里删掉
mLocalDeleteIdMap.add(c.getLong(SqlNote.ID_COLUMN));
}
} else {
@@ -281,16 +306,17 @@ public class GTaskManager {
}
} finally {
if (c != null) {
- c.close();
+ c.close(); // 确保游标一定被关闭,防止内存泄漏
c = null;
}
}
- // sync folder first
+ // 同步文件夹。文件夹的层级关系比较重要,所以要优先同步
syncFolder();
- // for note existing in database
+ // 处理本地数据库里还存在的笔记
try {
+ // 查询所有不在回收站里的普通笔记
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)
@@ -298,22 +324,23 @@ public class GTaskManager {
if (c != null) {
while (c.moveToNext()) {
gid = c.getString(SqlNote.GTASK_ID_COLUMN);
- node = mGTaskHashMap.get(gid);
- if (node != null) {
- mGTaskHashMap.remove(gid);
- mGidToNid.put(gid, c.getLong(SqlNote.ID_COLUMN));
+ node = mGTaskHashMap.get(gid); // 拿本地笔记的gid去云端缓存里找
+ if (node != null) { // 在云端找到了对应的笔记
+ mGTaskHashMap.remove(gid); // 从待处理的云端数据里移除
+ mGidToNid.put(gid, c.getLong(SqlNote.ID_COLUMN)); // 再次确认一下ID映射关系
mNidToGid.put(c.getLong(SqlNote.ID_COLUMN), gid);
+ // 调用node自己的方法来比对内容,判断是谁更新了,还是都更新了(冲突)
syncType = node.getSyncAction(c);
- } else {
+ } else { // 在云端没找到对应的笔记
if (c.getString(SqlNote.GTASK_ID_COLUMN).trim().length() == 0) {
- // local add
+ // 如果本地笔记的gid是空的,说明这是在本地新建的,还没同步过
syncType = Node.SYNC_ACTION_ADD_REMOTE;
} else {
- // remote delete
+ // 如果本地有gid,但云端没有,说明这个笔记在云端被删掉了
syncType = Node.SYNC_ACTION_DEL_LOCAL;
}
}
- doContentSync(syncType, node, c);
+ doContentSync(syncType, node, c); // 根据比对结果执行相应的同步操作
}
} else {
Log.w(TAG, "failed to query existing note in database");
@@ -326,31 +353,35 @@ public class GTaskManager {
}
}
- // go through remaining items
+ // 处理云端数据缓存(mGTaskHashMap)里剩下的东西
+ // 经过上面几步,缓存里剩下的就是那些“云端有,本地没有”的笔记了,说明是需要下载到本地的
Iterator> iter = mGTaskHashMap.entrySet().iterator();
while (iter.hasNext()) {
Map.Entry entry = iter.next();
node = entry.getValue();
- doContentSync(Node.SYNC_ACTION_ADD_LOCAL, node, null);
+ 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
if (!mCancelled) {
- GTaskClient.getInstance().commitUpdate();
- refreshLocalSyncId();
+ GTaskClient.getInstance().commitUpdate(); // 提交所有攒着的更新操作
+ refreshLocalSyncId(); // 最后再刷新一下本地笔记的sync_id,保证和云端一致
}
}
+ /**
+ * 这个是专门同步文件夹的逻辑,和同步笔记的流程基本一样,只是查询条件和处理的对象不同。
+ * @throws NetworkFailureException
+ */
private void syncFolder() throws NetworkFailureException {
Cursor c = null;
String gid;
@@ -361,7 +392,7 @@ public class GTaskManager {
return;
}
- // for root folder
+ // 先处理“默认便签夹”(根目录),这是个系统文件夹
try {
c = mContentResolver.query(ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI,
Notes.ID_ROOT_FOLDER), SqlNote.PROJECTION_NOTE, null, null, null);
@@ -370,14 +401,15 @@ public class GTaskManager {
gid = c.getString(SqlNote.GTASK_ID_COLUMN);
node = mGTaskHashMap.get(gid);
if (node != null) {
- mGTaskHashMap.remove(gid);
- mGidToNid.put(gid, (long) Notes.ID_ROOT_FOLDER);
+ mGTaskHashMap.remove(gid); // 从待处理缓存中移除
+ mGidToNid.put(gid, (long) Notes.ID_ROOT_FOLDER); // 建立ID映射
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 {
@@ -390,11 +422,12 @@ public class GTaskManager {
}
}
- // for call-note folder
+ // 再处理“通话便签”文件夹,也是系统文件夹
try {
+ // 这块逻辑和上面处理根目录的完全一样,只是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()) {
@@ -404,8 +437,7 @@ public class GTaskManager {
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))
@@ -424,8 +456,9 @@ public class GTaskManager {
}
}
- // for local existing folders
+ // 处理本地存在的其他普通文件夹
try {
+ // 这里的比对逻辑和syncContent里处理普通笔记的逻辑是一样的
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)
@@ -439,12 +472,10 @@ public class GTaskManager {
mGidToNid.put(gid, c.getLong(SqlNote.ID_COLUMN));
mNidToGid.put(c.getLong(SqlNote.ID_COLUMN), gid);
syncType = node.getSyncAction(c);
- } else {
+ } else {
if (c.getString(SqlNote.GTASK_ID_COLUMN).trim().length() == 0) {
- // local add
syncType = Node.SYNC_ACTION_ADD_REMOTE;
} else {
- // remote delete
syncType = Node.SYNC_ACTION_DEL_LOCAL;
}
}
@@ -460,61 +491,77 @@ public class GTaskManager {
}
}
- // for remote add folders
+ // 处理云端新增的文件夹
+ // mGTaskListHashMap里是所有云端文件夹,经过上面的处理,还在mGTaskHashMap里的就是“云端有,本地没有”的
Iterator> iter = mGTaskListHashMap.entrySet().iterator();
while (iter.hasNext()) {
Map.Entry entry = iter.next();
gid = entry.getKey();
node = entry.getValue();
- if (mGTaskHashMap.containsKey(gid)) {
+ if (mGTaskHashMap.containsKey(gid)) { // 再次确认一下
mGTaskHashMap.remove(gid);
doContentSync(Node.SYNC_ACTION_ADD_LOCAL, node, null);
}
}
if (!mCancelled)
- GTaskClient.getInstance().commitUpdate();
+ GTaskClient.getInstance().commitUpdate(); // 同步完文件夹就提交一次,免得操作太多
}
+ /**
+ * 这是一个分发器。根据传入的同步类型,决定具体是调用本地新增、远程删除还是其他方法。
+ * @param syncType 同步操作的类型
+ * @param node 云端节点对象
+ * @param c 本地数据库查询的游标
+ * @throws NetworkFailureException
+ */
private void doContentSync(int syncType, Node node, Cursor c) throws NetworkFailureException {
- if (mCancelled) {
+ if (mCancelled) { // 最后一层检查,确保取消指令能及时生效
return;
}
MetaData meta;
switch (syncType) {
case Node.SYNC_ACTION_ADD_LOCAL:
+ // 云端有,本地没有 -> 在本地数据库里新建
addLocalNode(node);
break;
case Node.SYNC_ACTION_ADD_REMOTE:
+ // 本地有,云端没有 -> 上传到Google服务器
addRemoteNode(node, c);
break;
case Node.SYNC_ACTION_DEL_LOCAL:
+ // 本地有,云端没有(但本地有gid,说明之前同步过) -> 在本地删除
+ // 删之前,先看看它有没有关联的meta数据,有的话也要一起删掉
meta = mMetaHashMap.get(c.getString(SqlNote.GTASK_ID_COLUMN));
if (meta != null) {
GTaskClient.getInstance().deleteNode(meta);
}
- mLocalDeleteIdMap.add(c.getLong(SqlNote.ID_COLUMN));
+ mLocalDeleteIdMap.add(c.getLong(SqlNote.ID_COLUMN)); // 记下ID,最后统一删
break;
case Node.SYNC_ACTION_DEL_REMOTE:
+ // 本地删了,云端还有 -> 通知云端删除
meta = mMetaHashMap.get(node.getGid());
if (meta != null) {
- GTaskClient.getInstance().deleteNode(meta);
+ GTaskClient.getInstance().deleteNode(meta); // 同样,连meta数据一起删
}
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:
@@ -522,26 +569,30 @@ public class GTaskManager {
}
}
+ // 在本地数据库里添加一个节点(文件夹或便签)
private void addLocalNode(Node node) throws NetworkFailureException {
if (mCancelled) {
return;
}
SqlNote sqlNote;
- if (node instanceof TaskList) {
+ if (node instanceof TaskList) { // 判断是文件夹还是便签
+ // 对系统文件夹做特殊处理,直接关联到固定的ID上,而不是新建
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 {
+ } 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 {
+ } else { // 是便签
sqlNote = new SqlNote(mContext);
+ // 这里有一段防御性代码,检查下载下来的笔记ID和dataID在本地是不是已经被占用了
+ // 如果被占用了,就把它ID去掉,让数据库自己生成新的,避免主键冲突
JSONObject js = node.getLocalJSONFromContent();
try {
if (js.has(GTaskStringUtils.META_HEAD_NOTE)) {
@@ -549,12 +600,11 @@ public class GTaskManager {
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);
}
}
}
-
+ // data表里的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,13 +612,10 @@ 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) {
Log.w(TAG, e.toString());
@@ -576,6 +623,7 @@ public class GTaskManager {
}
sqlNote.setContent(js);
+ // 从ID映射表里找到它的parent(父文件夹)在本地的ID
Long parentId = mGidToNid.get(((Task) node).getParent().getGid());
if (parentId == null) {
Log.e(TAG, "cannot find task's parent id locally");
@@ -584,28 +632,30 @@ public class GTaskManager {
sqlNote.setParentId(parentId.longValue());
}
- // create the local node
- sqlNote.setGtaskId(node.getGid());
- sqlNote.commit(false);
+ sqlNote.setGtaskId(node.getGid()); // 关联Google ID
+ sqlNote.commit(false); // 提交到数据库
- // update gid-nid mapping
+ // 关键一步:在ID映射表里把这个新节点的对应关系加上
mGidToNid.put(node.getGid(), sqlNote.getId());
mNidToGid.put(sqlNote.getId(), node.getGid());
- // update meta
+ // 顺便把meta数据也更新一下
updateRemoteMeta(node.getGid(), sqlNote);
}
+ // 更新本地数据库里的一个节点
private void updateLocalNode(Node node, Cursor c) throws NetworkFailureException {
if (mCancelled) {
return;
}
SqlNote sqlNote;
- // update the note locally
+ // 先用游标把数据库里现有的数据读出来
sqlNote = new SqlNote(mContext, c);
+ // 然后把云端的数据盖上去
sqlNote.setContent(node.getLocalJSONFromContent());
+ // 找到它的parent在本地的ID
Long parentId = (node instanceof Task) ? mGidToNid.get(((Task) node).getParent().getGid())
: new Long(Notes.ID_ROOT_FOLDER);
if (parentId == null) {
@@ -613,41 +663,39 @@ public class GTaskManager {
throw new ActionFailureException("cannot update local node");
}
sqlNote.setParentId(parentId.longValue());
- sqlNote.commit(true);
+ sqlNote.commit(true); // 提交更新
- // update meta info
- updateRemoteMeta(node.getGid(), sqlNote);
+ updateRemoteMeta(node.getGid(), sqlNote); // 更新meta
}
+ // 把本地新建的节点上传到云端
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()) {
+ if (sqlNote.isNoteType()) { // 是便签
Task task = new Task();
- task.setContentByLocalJSON(sqlNote.getContent());
+ task.setContentByLocalJSON(sqlNote.getContent()); // 用本地数据填充Task对象
+ // 从映射表里找到父文件夹的gid
String parentGid = mNidToGid.get(sqlNote.getParentId());
if (parentGid == null) {
Log.e(TAG, "cannot find task's parent tasklist");
throw new ActionFailureException("cannot add remote task");
}
- mGTaskListHashMap.get(parentGid).addChildTask(task);
+ mGTaskListHashMap.get(parentGid).addChildTask(task); // 在内存里也维护一下父子关系
- GTaskClient.getInstance().createTask(task);
+ GTaskClient.getInstance().createTask(task); // 发请求创建
n = (Node) task;
- // add meta
updateRemoteMeta(task.getGid(), sqlNote);
- } else {
+ } 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;
@@ -663,7 +711,7 @@ public class GTaskManager {
TaskList list = entry.getValue();
if (list.getName().equals(folderName)) {
- tasklist = list;
+ tasklist = list; // 找到了同名的
if (mGTaskHashMap.containsKey(gid)) {
mGTaskHashMap.remove(gid);
}
@@ -671,7 +719,7 @@ public class GTaskManager {
}
}
- // no match we can add now
+ // 如果没找到同名的,才真的去创建
if (tasklist == null) {
tasklist = new TaskList();
tasklist.setContentByLocalJSON(sqlNote.getContent());
@@ -681,17 +729,18 @@ public class GTaskManager {
n = (Node) tasklist;
}
- // update local note
+ // 上传成功后,把服务器返回的gid写回本地数据库
sqlNote.setGtaskId(n.getGid());
sqlNote.commit(false);
- sqlNote.resetLocalModified();
+ sqlNote.resetLocalModified(); // 清除'本地已修改'的标记
sqlNote.commit(true);
- // gid-id mapping
+ // 更新ID映射表
mGidToNid.put(n.getGid(), sqlNote.getId());
mNidToGid.put(sqlNote.getId(), n.getGid());
}
+ // 把本地修改过的节点上传到云端
private void updateRemoteNode(Node node, Cursor c) throws NetworkFailureException {
if (mCancelled) {
return;
@@ -699,59 +748,61 @@ public class GTaskManager {
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
+ // 如果是便签,还要检查一下它的父文件夹有没有变,变了的话就要发一个'move'指令
if (sqlNote.isNoteType()) {
Task task = (Task) node;
- TaskList preParentList = task.getParent();
+ TaskList preParentList = task.getParent(); // 之前的parent
String curParentGid = mNidToGid.get(sqlNote.getParentId());
if (curParentGid == null) {
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);// parent变了
if (preParentList != curParentList) {
+ // 在内存里维护父子关系
preParentList.removeChildTask(task);
curParentList.addChildTask(task);
- GTaskClient.getInstance().moveTask(task, preParentList, curParentList);
+ GTaskClient.getInstance().moveTask(task, preParentList, curParentList); // 发送移动请求
}
}
- // clear local modified flag
+ // 上传成功后,清除'本地已修改'的标记
sqlNote.resetLocalModified();
sqlNote.commit(true);
}
+ // 更新云端的meta数据
private void updateRemoteMeta(String gid, SqlNote sqlNote) throws NetworkFailureException {
if (sqlNote != null && sqlNote.isNoteType()) {
- MetaData metaData = mMetaHashMap.get(gid);
- if (metaData != null) {
+ MetaData metaData = mMetaHashMap.get(gid); // 先看看这个便签之前有没有meta数据
+ if (metaData != null) { // 有的话,直接更新
metaData.setMeta(gid, sqlNote.getContent());
GTaskClient.getInstance().addUpdateNode(metaData);
- } else {
+ } else { // 没有的话,就新建一个
metaData = new MetaData();
metaData.setMeta(gid, sqlNote.getContent());
- mMetaList.addChildTask(metaData);
- mMetaHashMap.put(gid, metaData);
- GTaskClient.getInstance().createTask(metaData);
+ mMetaList.addChildTask(metaData); // 关联到meta清单下
+ mMetaHashMap.put(gid, metaData); // 加到缓存
+ GTaskClient.getInstance().createTask(metaData); // 发请求创建
}
}
}
+ // 同步完成后,刷新本地数据库里所有笔记的sync_id,让它和云端的lastModified时间戳保持一致
private void refreshLocalSyncId() throws NetworkFailureException {
if (mCancelled) {
return;
}
- // get the latest gtask list
+ // 重新从服务器拉一遍最新的数据,因为commitUpdate之后,云端的lastModified可能变了
mGTaskHashMap.clear();
mGTaskListHashMap.clear();
mMetaHashMap.clear();
@@ -759,6 +810,7 @@ public class GTaskManager {
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)
@@ -767,13 +819,15 @@ public class GTaskManager {
while (c.moveToNext()) {
String gid = c.getString(SqlNote.GTASK_ID_COLUMN);
Node node = mGTaskHashMap.get(gid);
- if (node != null) {
+ if (node != null) { // 找到对应的云端节点
mGTaskHashMap.remove(gid);
ContentValues values = new ContentValues();
+ // 把云端节点的时间戳更新到本地的sync_id字段
values.put(NoteColumns.SYNC_ID, node.getLastModified());
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");
@@ -790,10 +844,12 @@ public class GTaskManager {
}
}
+ // 获取当前同步的账户名
public String getSyncAccount() {
return GTaskClient.getInstance().getSyncAccount().name;
}
+ // 从外部取消同步
public void cancelSync() {
mCancelled = true;
}
diff --git a/src/src/net/micode/notes/gtask/remote/GTaskSyncService.java b/src/src/net/micode/notes/gtask/remote/GTaskSyncService.java
index cca36f7..a03c96a 100644
--- a/src/src/net/micode/notes/gtask/remote/GTaskSyncService.java
+++ b/src/src/net/micode/notes/gtask/remote/GTaskSyncService.java
@@ -23,64 +23,93 @@ import android.content.Intent;
import android.os.Bundle;
import android.os.IBinder;
+/**
+ * @Author: 林迪文
+ * @Updator: 林迪文
+ * @Date 2025/12/21 19:15
+ * @Description 这个是后台同步Google任务的服务。别的页面通过发Intent来让它开始或者停止同步。它还会把同步的状态和进度广播出去,这样界面就能知道现在同步到哪一步了。
+ */
public class GTaskSyncService extends Service {
+ // 用这个名字作为key,从Intent里拿具体操作类型
public final static String ACTION_STRING_NAME = "sync_action_type";
- public final static int ACTION_START_SYNC = 0;
+ public final static int ACTION_START_SYNC = 0; // 0代表开始同步
- public final static int ACTION_CANCEL_SYNC = 1;
+ public final static int ACTION_CANCEL_SYNC = 1; // 1代表取消同步
- public final static int ACTION_INVALID = 2;
+ public final static int ACTION_INVALID = 2; // 无效操作
+ // 定义一个广播的名字,方便Activity接收
public final static String GTASK_SERVICE_BROADCAST_NAME = "net.micode.notes.gtask.remote.gtask_sync_service";
+ // 广播里用这个key存bool值,表示在不在同步
public final static String GTASK_SERVICE_BROADCAST_IS_SYNCING = "isSyncing";
+ // 广播里用这个key存进度信息
public final static String GTASK_SERVICE_BROADCAST_PROGRESS_MSG = "progressMsg";
+ // 同步任务的实例。static保证了整个应用里只有一个同步任务在跑
private static GTaskASyncTask mSyncTask = null;
+ // 存当前的同步进度信息,也是static的,方便随时获取
private static String mSyncProgress = "";
+ /**
+ * 接收到“开始同步”的指令后,执行这里的逻辑
+ */
private void startSync() {
+ // 先判断一下是不是已经在同步了,防止重复启动
if (mSyncTask == null) {
+ // new一个异步任务出来,准备开始干活
mSyncTask = new GTaskASyncTask(this, new GTaskASyncTask.OnCompleteListener() {
+ /**
+ * 这里传了个回调,等任务干完之后会执行onComplete
+ */
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();
+ mSyncTask.cancelSync(); // 调用任务自己的取消方法
}
}
@Override
public void onCreate() {
+ // 服务第一次创建的时候,确保mSyncTask是空的
mSyncTask = null;
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
+ // 从传过来的Intent里拿出数据包
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();
+ startSync(); // 开始同步
break;
case ACTION_CANCEL_SYNC:
- cancelSync();
+ cancelSync(); // 取消同步
break;
default:
break;
}
+ // 如果服务被系统杀了,系统会尝试重启服务
return START_STICKY;
}
return super.onStartCommand(intent, flags, startId);
@@ -88,41 +117,67 @@ public class GTaskSyncService extends Service {
@Override
public void onLowMemory() {
+ // 系统内存不够时调用
if (mSyncTask != null) {
+ // 内存紧张,赶紧把正在跑的同步任务停了,别占资源
mSyncTask.cancelSync();
}
}
+ @Override
public IBinder onBind(Intent intent) {
+ // 这个服务不支持绑定,所以直接返回null
return null;
}
+ /**
+ * 把同步的状态广播出去
+ * @param msg 要广播出去的进度消息
+ */
public void sendBroadcast(String msg) {
- mSyncProgress = msg;
- Intent intent = new Intent(GTASK_SERVICE_BROADCAST_NAME);
+ mSyncProgress = msg; // 先把最新的进度信息存到静态变量里
+ Intent intent = new Intent(GTASK_SERVICE_BROADCAST_NAME); // 创建一个广播Intent,用我们之前定义好的名字
+ // 把“是否在同步”和“进度消息”这两个信息塞到Intent里
intent.putExtra(GTASK_SERVICE_BROADCAST_IS_SYNCING, mSyncTask != null);
intent.putExtra(GTASK_SERVICE_BROADCAST_PROGRESS_MSG, msg);
- sendBroadcast(intent);
+ sendBroadcast(intent); // 发射,这样关心这个广播的组件(比如Activity)就能收到了
}
+ /**
+ * 这是个静态方法,给外面的Activity用的,这样调用起来比较方便
+ * @param activity 调用这个方法的Activity
+ */
public static void startSync(Activity activity) {
+ // 把Activity的上下文传给GTaskManager
GTaskManager.getInstance().setActivityContext(activity);
- Intent intent = new Intent(activity, GTaskSyncService.class);
- intent.putExtra(GTaskSyncService.ACTION_STRING_NAME, GTaskSyncService.ACTION_START_SYNC);
- activity.startService(intent);
+ Intent intent = new Intent(activity, GTaskSyncService.class); // 创建一个指向自己的Intent
+ intent.putExtra(GTaskSyncService.ACTION_STRING_NAME, GTaskSyncService.ACTION_START_SYNC); // 在Intent里放一个“开始同步”的指令
+ activity.startService(intent); // 启动服务
}
+ /**
+ * 同样是给外面用的静态方法,用来取消同步
+ * @param context 上下文环境
+ */
public static void cancelSync(Context context) {
Intent intent = new Intent(context, GTaskSyncService.class);
+ // 流程和startSync差不多,就是指令变成了“取消同步”
intent.putExtra(GTaskSyncService.ACTION_STRING_NAME, GTaskSyncService.ACTION_CANCEL_SYNC);
context.startService(intent);
}
+ /**
+ * 提供一个静态方法,让外面能随时知道是不是正在同步
+ */
public static boolean isSyncing() {
+ // 判断那个任务实例是不是null就行了
return mSyncTask != null;
}
+ /**
+ * 获取当前进度的静态方法
+ */
public static String getProgressString() {
return mSyncProgress;
}
-}
+}
\ No newline at end of file