- * 主要负责根据电话号码从Android数据库中检索联系人姓名。 - *
- * - *Type: Text
*/ public static final String MIME_TYPE = "mime_type"; @@ -256,7 +241,6 @@ 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 @@ -264,7 +248,6 @@ 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"; @@ -287,10 +270,8 @@ 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 be0f476..ffe5d57 100644 --- a/src/src/net/micode/notes/data/NotesDatabaseHelper.java +++ b/src/src/net/micode/notes/data/NotesDatabaseHelper.java @@ -26,216 +26,185 @@ 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"; // 存储笔记的元数据(如ID、创建时间、父文件夹) + public static final String NOTE = "note"; - 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" + - ")"; - - // 创建 DATA 表的 SQL 语句 - // data 表通过 note_id 关联到 note 表 + "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" + + ")"; + 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 ''" + - ")"; - - // 为 note_id 创建索引,加速根据笔记 ID 查询内容的效率 + "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 ''" + + ")"; + 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 + ");"; /** - * 触发器逻辑:当笔记被移动到新文件夹时(Update操作), - * 自动将新文件夹(new.parent_id)内的笔记数量 +1。 + * Increase folder's note count when move note to the folder */ 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"; /** - * 触发器逻辑:当笔记从旧文件夹移出时(Update操作), - * 自动将旧文件夹(old.parent_id)内的笔记数量 -1。 - * 增加了 >0 的判断,防止数据异常导致计数器变成负数。 + * Decrease folder's note count when move note from folder */ private static final String NOTE_DECREASE_FOLDER_COUNT_ON_UPDATE_TRIGGER = - "CREATE TRIGGER decrease_folder_count_on_update " + - " AFTER UPDATE OF " + NoteColumns.PARENT_ID + " ON " + TABLE.NOTE + - " BEGIN " + - " UPDATE " + TABLE.NOTE + - " SET " + NoteColumns.NOTES_COUNT + "=" + NoteColumns.NOTES_COUNT + "-1" + - " WHERE " + NoteColumns.ID + "=old." + NoteColumns.PARENT_ID + - " AND " + NoteColumns.NOTES_COUNT + ">0" + ";" + - " END"; + "CREATE TRIGGER decrease_folder_count_on_update " + + " AFTER UPDATE OF " + NoteColumns.PARENT_ID + " ON " + TABLE.NOTE + + " BEGIN " + + " UPDATE " + TABLE.NOTE + + " SET " + NoteColumns.NOTES_COUNT + "=" + NoteColumns.NOTES_COUNT + "-1" + + " WHERE " + NoteColumns.ID + "=old." + NoteColumns.PARENT_ID + + " AND " + NoteColumns.NOTES_COUNT + ">0" + ";" + + " END"; /** - * 触发器逻辑:当在文件夹下新建笔记时(Insert操作), - * 自动将该文件夹的笔记数量 +1。 + * Increase folder's note count when insert new note to the folder */ private static final String NOTE_INCREASE_FOLDER_COUNT_ON_INSERT_TRIGGER = - "CREATE TRIGGER increase_folder_count_on_insert " + - " AFTER INSERT ON " + TABLE.NOTE + - " BEGIN " + - " UPDATE " + TABLE.NOTE + - " SET " + NoteColumns.NOTES_COUNT + "=" + NoteColumns.NOTES_COUNT + " + 1" + - " WHERE " + NoteColumns.ID + "=new." + NoteColumns.PARENT_ID + ";" + - " END"; + "CREATE TRIGGER increase_folder_count_on_insert " + + " AFTER INSERT ON " + TABLE.NOTE + + " BEGIN " + + " UPDATE " + TABLE.NOTE + + " SET " + NoteColumns.NOTES_COUNT + "=" + NoteColumns.NOTES_COUNT + " + 1" + + " WHERE " + NoteColumns.ID + "=new." + NoteColumns.PARENT_ID + ";" + + " END"; /** - * 触发器逻辑:当删除笔记时(Delete操作), - * 自动将所在文件夹的笔记数量 -1。 + * Decrease folder's note count when delete note from the folder */ private static final String NOTE_DECREASE_FOLDER_COUNT_ON_DELETE_TRIGGER = - "CREATE TRIGGER decrease_folder_count_on_delete " + - " AFTER DELETE ON " + TABLE.NOTE + - " BEGIN " + - " UPDATE " + TABLE.NOTE + - " SET " + NoteColumns.NOTES_COUNT + "=" + NoteColumns.NOTES_COUNT + "-1" + - " WHERE " + NoteColumns.ID + "=old." + NoteColumns.PARENT_ID + - " AND " + NoteColumns.NOTES_COUNT + ">0;" + - " END"; + "CREATE TRIGGER decrease_folder_count_on_delete " + + " AFTER DELETE ON " + TABLE.NOTE + + " BEGIN " + + " UPDATE " + TABLE.NOTE + + " SET " + NoteColumns.NOTES_COUNT + "=" + NoteColumns.NOTES_COUNT + "-1" + + " WHERE " + NoteColumns.ID + "=old." + NoteColumns.PARENT_ID + + " AND " + NoteColumns.NOTES_COUNT + ">0;" + + " END"; /** - * 触发器逻辑:当 DATA 表插入新内容且类型为 Note 时, - * 自动把内容同步更新到 NOTE 表的 snippet(摘要)字段。 - * 这样列表页可以直接读取 NOTE 表显示预览,无需联表查询。 + * Update note's content when insert data with type {@link DataConstants#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"; /** - * 触发器逻辑:当 DATA 表内容发生变更时,同步更新 NOTE 表的摘要。 + * Update note's content when data with {@link DataConstants#NOTE} type has changed */ 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"; /** - * 触发器逻辑:当 DATA 表内容被删除时,清空 NOTE 表对应的摘要。 + * Update note's content when data with {@link DataConstants#NOTE} type has deleted */ 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"; /** - * 触发器逻辑:级联删除。 - * 当 NOTE 表的记录被删除时,自动删除 DATA 表中对应的详细内容,防止产生脏数据。 + * Delete datas belong to note which has been deleted */ 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"; /** - * 触发器逻辑:级联移动到废纸篓。 - * 当文件夹被移入废纸篓(ID_TRASH_FOLER)时, - * 自动将该文件夹下的所有笔记也修改为废纸篓状态。 + * Move notes belong to folder which has been moved to trash folder */ 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); @@ -243,12 +212,11 @@ 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"); @@ -267,14 +235,12 @@ 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); @@ -282,7 +248,6 @@ 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); @@ -291,7 +256,6 @@ 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); @@ -300,7 +264,6 @@ 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); @@ -310,7 +273,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"); } @@ -324,7 +287,6 @@ 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); @@ -338,46 +300,39 @@ 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; // 标记跳过 v2 的独立判断逻辑 + skipV2 = true; // this upgrade including the upgrade from v2 to v3 oldVersion++; } - // 升级逻辑:v2 -> v3 (或从 v1 升上来后继续执行) if (oldVersion == 2 && !skipV2) { upgradeToV3(db); - reCreateTriggers = true; // v3 变更需要重置触发器 + reCreateTriggers = true; oldVersion++; } - // 升级逻辑:v3 -> v4 if (oldVersion == 3) { upgradeToV4(db); oldVersion++; } - // 如果升级过程中涉及触发器逻辑变更,统一重新创建 if (reCreateTriggers) { reCreateNoteTableTriggers(db); reCreateDataTableTriggers(db); } - // 升级后校验版本号,不匹配则抛出异常 if (oldVersion != newVersion) { throw new IllegalStateException("Upgrade notes database to version " + newVersion + "fails"); } } - // v1 升 v2:删除旧表并重建,重置整个数据库结构 private void upgradeToV2(SQLiteDatabase db) { db.execSQL("DROP TABLE IF EXISTS " + TABLE.NOTE); db.execSQL("DROP TABLE IF EXISTS " + TABLE.DATA); @@ -385,14 +340,12 @@ 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 @@ -402,9 +355,8 @@ 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 97c73bd..edb0a60 100644 --- a/src/src/net/micode/notes/data/NotesProvider.java +++ b/src/src/net/micode/notes/data/NotesProvider.java @@ -32,45 +32,27 @@ 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"; - // 定义 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_NOTE = 1; + private static final int URI_NOTE_ITEM = 2; + private static final int URI_DATA = 3; + private static final int URI_DATA_ITEM = 4; - private static final int URI_SEARCH = 5; // 搜索 - private static final int URI_SEARCH_SUGGEST = 6; // 搜索建议(给搜索框用的) + 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); @@ -82,49 +64,39 @@ 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 + "," - + "'" + 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 + "," + + "'" + Notes.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); @@ -140,14 +112,12 @@ 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); @@ -161,7 +131,6 @@ public class NotesProvider extends ContentProvider { } try { - // 加上 % 拼成模糊查询 searchString = String.format("%%%s%%", searchString); c = db.rawQuery(NOTES_SNIPPET_SEARCH_QUERY, new String[] { searchString }); @@ -173,13 +142,11 @@ 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(); @@ -189,7 +156,6 @@ 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 { @@ -200,23 +166,21 @@ public class NotesProvider extends ContentProvider { default: throw new IllegalArgumentException("Unknown URI " + uri); } - // 通知 Note 表有变化,UI 该刷新了 + // Notify the note uri if (noteId > 0) { getContext().getContentResolver().notifyChange( ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, noteId), null); } - // 通知 Data 表有变化 + // Notify the data uri 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; @@ -225,7 +189,6 @@ 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; @@ -235,7 +198,6 @@ 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; @@ -256,7 +218,6 @@ 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); @@ -266,7 +227,6 @@ public class NotesProvider extends ContentProvider { return count; } - // 改数据接口 @Override public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) { int count = 0; @@ -275,13 +235,11 @@ 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); @@ -309,12 +267,10 @@ 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 "); @@ -323,7 +279,6 @@ public class NotesProvider extends ContentProvider { sql.append(NoteColumns.VERSION); sql.append("=" + NoteColumns.VERSION + "+1 "); - // 拼接 WHERE 条件 if (id > 0 || !TextUtils.isEmpty(selection)) { sql.append(" WHERE "); } @@ -332,8 +287,6 @@ 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); } @@ -349,4 +302,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 b04ac70..3a2050b 100644 --- a/src/src/net/micode/notes/gtask/data/MetaData.java +++ b/src/src/net/micode/notes/gtask/data/MetaData.java @@ -24,62 +24,37 @@ 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"); @@ -88,9 +63,6 @@ public class MetaData extends Task { } } - // 下面这三个方法直接抛 Error,说明 MetaData 这个类很特殊 - // 它不需要走本地数据库(Local JSON/Cursor)那一套流程 - @Override public void setContentByLocalJSON(JSONObject js) { // this function should not be called @@ -107,4 +79,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 01f6fbb..63950e0 100644 --- a/src/src/net/micode/notes/gtask/data/Node.java +++ b/src/src/net/micode/notes/gtask/data/Node.java @@ -3,7 +3,6 @@ * * 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 @@ -21,103 +20,52 @@ 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; // Google那边给的唯一ID + private String mGid; - 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; } @@ -149,4 +97,5 @@ 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 04b128e..d3ec3be 100644 --- a/src/src/net/micode/notes/gtask/data/SqlData.java +++ b/src/src/net/micode/notes/gtask/data/SqlData.java @@ -9,7 +9,6 @@ * * 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. @@ -35,70 +34,61 @@ 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(); // TAG,打日志用 + private static final String TAG = SqlData.class.getSimpleName(); - private static final int INVALID_ID = -99999; // 无效ID,-99999 这个值选得有点随意啊? + private static final int INVALID_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(); // 从 context 里拿到 ContentResolver - mIsCreate = true; // 标记为“新建” + mContentResolver = context.getContentResolver(); + 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); @@ -107,14 +97,8 @@ 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); } @@ -146,12 +130,8 @@ 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; } @@ -164,52 +144,41 @@ 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); // 别忘了把所属的 noteId 加上 + mDiffDataValues.put(DataColumns.NOTE_ID, 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; } @@ -217,4 +186,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 bf22f49..79a4095 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 not use a copy of the License at + * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * @@ -38,20 +38,11 @@ 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, @@ -61,63 +52,83 @@ 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; // 安卓系统里专门用来跟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 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 ContentValues mDiffNoteValues; - private ArrayList- * 职责: - * 1. 监听系统启动完成广播 (BOOT_COMPLETED)。 - * 2. 扫描数据库中所有“未来需要提醒”的便签。 - * 3. 将这些提醒重新注册到 Android 系统 AlarmManager 中。 - *
- * 背景知识:
- * Android 的 AlarmManager 注册的闹钟在设备重启后会丢失(非持久化)。
- * 因此必须通过此类在开机时重建闹钟队列,保证提醒功能不会因为重启而失效。
- */
-public class AlarmInitReceiver extends BroadcastReceiver {
-
- // 数据库查询投影:仅查询 ID 和 提醒时间,节省内存
- private static final String [] PROJECTION = new String [] {
- NoteColumns.ID,
- NoteColumns.ALERTED_DATE
- };
-
- private static final int COLUMN_ID = 0;
- private static final int COLUMN_ALERTED_DATE = 1;
+public class AlarmReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
- long currentDate = System.currentTimeMillis();
-
- // 查询数据库:查找所有提醒时间晚于当前时间 (ALERTED_DATE > now) 的便签
- // 即只恢复那些“还没过期”的闹钟,过期的就不管了
- Cursor c = context.getContentResolver().query(Notes.CONTENT_NOTE_URI,
- PROJECTION,
- NoteColumns.ALERTED_DATE + ">? AND " + NoteColumns.TYPE + "=" + Notes.TYPE_NOTE,
- new String[] { String.valueOf(currentDate) },
- null);
-
- if (c != null) {
- if (c.moveToFirst()) {
- do {
- long alertDate = c.getLong(COLUMN_ALERTED_DATE);
-
- // 构建发送给 AlarmReceiver 的 Intent
- // 这里必须与设置闹钟时的 Intent 结构完全一致,否则无法触发目标逻辑
- Intent sender = new Intent(context, AlarmReceiver.class);
- sender.setData(ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, c.getLong(COLUMN_ID)));
-
- PendingIntent pendingIntent = PendingIntent.getBroadcast(context, 0, sender, 0);
-
- // 获取系统 AlarmManager 服务
- AlarmManager alermManager = (AlarmManager) context
- .getSystemService(Context.ALARM_SERVICE);
-
- // 重新设定闹钟
- // RTC_WAKEUP: 使用绝对时间,并在触发时唤醒设备(如果设备处于休眠状态)
- alermManager.set(AlarmManager.RTC_WAKEUP, alertDate, pendingIntent);
- } while (c.moveToNext());
- }
- c.close();
- }
+ intent.setClass(context, AlarmAlertActivity.class);
+ intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ context.startActivity(intent);
}
}
diff --git a/src/src/net/micode/notes/ui/NoteEditActivity.java b/src/src/net/micode/notes/ui/NoteEditActivity.java
index 5a8bf80..e0df11b 100644
--- a/src/src/net/micode/notes/ui/NoteEditActivity.java
+++ b/src/src/net/micode/notes/ui/NoteEditActivity.java
@@ -119,7 +119,13 @@ public class NoteEditActivity extends Activity implements OnClickListener,
// AI 菜单项的 ID (虽然主要逻辑已迁移至 XML 按钮,但保留此常量兼容旧代码)
private static final int MENU_AI_OPT_ID = 999;
+ // ================= 图片功能相关常量与变量 =================
+ private static final int REQUEST_CODE_TAKE_PHOTO = 1001;
+ private static final int REQUEST_CODE_OPEN_ALBUM = 1002;
+ // 用于临时存储相机拍摄照片的路径
+ private String mCurrentPhotoPath;
+ // ========================================================
// 静态映射表:字体选中状态映射
private static final Map
- * 职责:
- * 1. 管理应用配置,主要是 Google Task 同步账户的设置。
- * 2. 提供手动触发同步的按钮。
- * 3. 实时显示同步状态(正在同步、最后同步时间)。
- * 4. 监听同步服务广播,更新 UI。
- */
+
public class NotesPreferenceActivity extends PreferenceActivity {
public static final String PREFERENCE_NAME = "notes_preferences";
+
public static final String PREFERENCE_SYNC_ACCOUNT_NAME = "pref_key_account_name";
+
public static final String PREFERENCE_LAST_SYNC_TIME = "pref_last_sync_time";
+
public static final String PREFERENCE_SET_BG_COLOR_KEY = "pref_key_bg_random_appear";
private static final String PREFERENCE_SYNC_ACCOUNT_KEY = "pref_sync_account_key";
+
private static final String AUTHORITIES_FILTER_KEY = "authorities";
private PreferenceCategory mAccountCategory;
- private GTaskReceiver mReceiver; // 广播接收器,监听同步服务状态
- private Account[] mOriAccounts; // 原始账户列表
+
+ private GTaskReceiver mReceiver;
+
+ private Account[] mOriAccounts;
+
private boolean mHasAddedAccount;
@Override
protected void onCreate(Bundle icicle) {
super.onCreate(icicle);
- // 启用 ActionBar 的返回按钮
+ /* using the app icon for navigation */
getActionBar().setDisplayHomeAsUpEnabled(true);
- // 加载 XML 偏好设置布局
addPreferencesFromResource(R.xml.preferences);
mAccountCategory = (PreferenceCategory) findPreference(PREFERENCE_SYNC_ACCOUNT_KEY);
-
- // 注册广播接收器,监听 GTaskSyncService 发出的状态广播
mReceiver = new GTaskReceiver();
IntentFilter filter = new IntentFilter();
filter.addAction(GTaskSyncService.GTASK_SERVICE_BROADCAST_NAME);
@@ -85,8 +92,8 @@ public class NotesPreferenceActivity extends PreferenceActivity {
protected void onResume() {
super.onResume();
- // 自动检测是否添加了新账户
- // 如果用户刚才跳转到系统设置页添加了账户,回来后自动选中新账户
+ // need to set sync account automatically if user has added a new
+ // account
if (mHasAddedAccount) {
Account[] accounts = getGoogleAccounts();
if (mOriAccounts != null && accounts.length > mOriAccounts.length) {
@@ -117,7 +124,6 @@ public class NotesPreferenceActivity extends PreferenceActivity {
super.onDestroy();
}
- // 加载账户设置项
private void loadAccountPreference() {
mAccountCategory.removeAll();
@@ -125,21 +131,20 @@ public class NotesPreferenceActivity extends PreferenceActivity {
final String defaultAccount = getSyncAccountName(this);
accountPref.setTitle(getString(R.string.preferences_account_title));
accountPref.setSummary(getString(R.string.preferences_account_summary));
-
- // 设置点击事件:根据当前是否已设置账户,弹出不同的对话框
accountPref.setOnPreferenceClickListener(new OnPreferenceClickListener() {
public boolean onPreferenceClick(Preference preference) {
if (!GTaskSyncService.isSyncing()) {
if (TextUtils.isEmpty(defaultAccount)) {
- // 首次设置:显示账户选择列表
+ // the first time to set account
showSelectAccountAlertDialog();
} else {
- // 已设置:显示更改/删除确认框
+ // if the account has already been set, we need to promp
+ // user about the risk
showChangeAccountConfirmAlertDialog();
}
} else {
Toast.makeText(NotesPreferenceActivity.this,
- R.string.preferences_toast_cannot_change_account, Toast.LENGTH_SHORT)
+ R.string.preferences_toast_cannot_change_account, Toast.LENGTH_SHORT)
.show();
}
return true;
@@ -149,12 +154,11 @@ public class NotesPreferenceActivity extends PreferenceActivity {
mAccountCategory.addPreference(accountPref);
}
- // 加载同步按钮状态
private void loadSyncButton() {
Button syncButton = (Button) findViewById(R.id.preference_sync_button);
TextView lastSyncTimeView = (TextView) findViewById(R.id.prefenerece_sync_status_textview);
- // 根据是否正在同步,切换按钮文字(立即同步 / 取消同步)
+ // set button state
if (GTaskSyncService.isSyncing()) {
syncButton.setText(getString(R.string.preferences_button_sync_cancel));
syncButton.setOnClickListener(new View.OnClickListener() {
@@ -172,7 +176,7 @@ public class NotesPreferenceActivity extends PreferenceActivity {
}
syncButton.setEnabled(!TextUtils.isEmpty(getSyncAccountName(this)));
- // 显示最后同步时间或当前进度
+ // set last sync time
if (GTaskSyncService.isSyncing()) {
lastSyncTimeView.setText(GTaskSyncService.getProgressString());
lastSyncTimeView.setVisibility(View.VISIBLE);
@@ -194,20 +198,55 @@ public class NotesPreferenceActivity extends PreferenceActivity {
loadSyncButton();
}
- // 弹出账户选择对话框
private void showSelectAccountAlertDialog() {
AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(this);
- // ... (省略 UI 构建代码) ...
- // 获取系统账户列表并显示
+
+ View titleView = LayoutInflater.from(this).inflate(R.layout.account_dialog_title, null);
+ TextView titleTextView = (TextView) titleView.findViewById(R.id.account_dialog_title);
+ titleTextView.setText(getString(R.string.preferences_dialog_select_account_title));
+ TextView subtitleTextView = (TextView) titleView.findViewById(R.id.account_dialog_subtitle);
+ subtitleTextView.setText(getString(R.string.preferences_dialog_select_account_tips));
+
+ dialogBuilder.setCustomTitle(titleView);
+ dialogBuilder.setPositiveButton(null, null);
+
Account[] accounts = getGoogleAccounts();
- // ...
- // 提供“添加账户”按钮,跳转到系统设置
+ String defAccount = getSyncAccountName(this);
+
+ mOriAccounts = accounts;
+ mHasAddedAccount = false;
+
+ if (accounts.length > 0) {
+ CharSequence[] items = new CharSequence[accounts.length];
+ final CharSequence[] itemMapping = items;
+ int checkedItem = -1;
+ int index = 0;
+ for (Account account : accounts) {
+ if (TextUtils.equals(account.name, defAccount)) {
+ checkedItem = index;
+ }
+ items[index++] = account.name;
+ }
+ dialogBuilder.setSingleChoiceItems(items, checkedItem,
+ new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int which) {
+ setSyncAccount(itemMapping[which].toString());
+ dialog.dismiss();
+ refreshUI();
+ }
+ });
+ }
+
+ View addAccountView = LayoutInflater.from(this).inflate(R.layout.add_account_text, null);
+ dialogBuilder.setView(addAccountView);
+
+ final AlertDialog dialog = dialogBuilder.show();
addAccountView.setOnClickListener(new View.OnClickListener() {
public void onClick(View v) {
mHasAddedAccount = true;
Intent intent = new Intent("android.settings.ADD_ACCOUNT_SETTINGS");
intent.putExtra(AUTHORITIES_FILTER_KEY, new String[] {
- "gmail-ls"
+ "gmail-ls"
});
startActivityForResult(intent, -1);
dialog.dismiss();
@@ -215,21 +254,42 @@ public class NotesPreferenceActivity extends PreferenceActivity {
});
}
- // 弹出更改/移除账户确认框
private void showChangeAccountConfirmAlertDialog() {
- // ...
+ AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(this);
+
+ View titleView = LayoutInflater.from(this).inflate(R.layout.account_dialog_title, null);
+ TextView titleTextView = (TextView) titleView.findViewById(R.id.account_dialog_title);
+ titleTextView.setText(getString(R.string.preferences_dialog_change_account_title,
+ getSyncAccountName(this)));
+ TextView subtitleTextView = (TextView) titleView.findViewById(R.id.account_dialog_subtitle);
+ subtitleTextView.setText(getString(R.string.preferences_dialog_change_account_warn_msg));
+ dialogBuilder.setCustomTitle(titleView);
+
+ CharSequence[] menuItemArray = new CharSequence[] {
+ getString(R.string.preferences_menu_change_account),
+ getString(R.string.preferences_menu_remove_account),
+ getString(R.string.preferences_menu_cancel)
+ };
+ dialogBuilder.setItems(menuItemArray, new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int which) {
+ if (which == 0) {
+ showSelectAccountAlertDialog();
+ } else if (which == 1) {
+ removeSyncAccount();
+ refreshUI();
+ }
+ }
+ });
+ dialogBuilder.show();
}
- // 获取系统中的 Google 账户
private Account[] getGoogleAccounts() {
AccountManager accountManager = AccountManager.get(this);
return accountManager.getAccountsByType("com.google");
}
- // 设置选中的同步账户
private void setSyncAccount(String account) {
if (!getSyncAccountName(this).equals(account)) {
- // 保存账户名到 SharedPreferences
SharedPreferences settings = getSharedPreferences(PREFERENCE_NAME, Context.MODE_PRIVATE);
SharedPreferences.Editor editor = settings.edit();
if (account != null) {
@@ -239,11 +299,10 @@ public class NotesPreferenceActivity extends PreferenceActivity {
}
editor.commit();
- // 重置最后同步时间
+ // clean up last sync time
setLastSyncTime(this, 0);
- // 关键步骤:清理本地数据库中的 GTask ID 映射
- // 因为切换账户后,本地便签与云端的对应关系失效,必须重置
+ // clean up local gtask related info
new Thread(new Runnable() {
public void run() {
ContentValues values = new ContentValues();
@@ -260,8 +319,25 @@ public class NotesPreferenceActivity extends PreferenceActivity {
}
private void removeSyncAccount() {
- // 逻辑类似 setSyncAccount,只是移除配置
- // ...
+ SharedPreferences settings = getSharedPreferences(PREFERENCE_NAME, Context.MODE_PRIVATE);
+ SharedPreferences.Editor editor = settings.edit();
+ if (settings.contains(PREFERENCE_SYNC_ACCOUNT_NAME)) {
+ editor.remove(PREFERENCE_SYNC_ACCOUNT_NAME);
+ }
+ if (settings.contains(PREFERENCE_LAST_SYNC_TIME)) {
+ editor.remove(PREFERENCE_LAST_SYNC_TIME);
+ }
+ editor.commit();
+
+ // clean up local gtask related info
+ new Thread(new Runnable() {
+ public void run() {
+ ContentValues values = new ContentValues();
+ values.put(NoteColumns.GTASK_ID, "");
+ values.put(NoteColumns.SYNC_ID, 0);
+ getContentResolver().update(Notes.CONTENT_NOTE_URI, values, null, null);
+ }
+ }).start();
}
public static String getSyncAccountName(Context context) {
@@ -284,11 +360,8 @@ public class NotesPreferenceActivity extends PreferenceActivity {
return settings.getLong(PREFERENCE_LAST_SYNC_TIME, 0);
}
- /**
- * 内部广播接收器
- * 监听同步服务的状态变化(开始、进行中、结束),刷新 UI
- */
private class GTaskReceiver extends BroadcastReceiver {
+
@Override
public void onReceive(Context context, Intent intent) {
refreshUI();
@@ -297,6 +370,7 @@ public class NotesPreferenceActivity extends PreferenceActivity {
syncStatus.setText(intent
.getStringExtra(GTaskSyncService.GTASK_SERVICE_BROADCAST_PROGRESS_MSG));
}
+
}
}
@@ -311,4 +385,4 @@ public class NotesPreferenceActivity extends PreferenceActivity {
return false;
}
}
-}
\ No newline at end of file
+}
";
+
+ // 插入文本
+ int start = getSelectionStart();
+ getText().insert(start, "\n" + tag + "\n");
+
+ // 注意:这里我们不再手动 setSpan,而是依赖下面的 setTextWithImages 统一处理
+ // 这样逻辑更统一,不会出现“刚插进去有图,刷新后没图”的尴尬
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ }
+ public void setTextWithImages(String text) {
+ // 1. 先设置纯文本
+ setText(text);
+
+ // 2. 寻找所有的
标签
+ android.text.Editable editable = getText();
+ String pattern = "
"; // 正则表达式抓取路径
+ java.util.regex.Pattern p = java.util.regex.Pattern.compile(pattern);
+ java.util.regex.Matcher m = p.matcher(text);
+
+ while (m.find()) {
+ // 获取路径
+ String imagePath = m.group(1);
+ int start = m.start();
+ int end = m.end();
+
+ try {
+ // 3. 加载图片 (复用之前的逻辑)
+ int width = getWidth() - getPaddingLeft() - getPaddingRight();
+ if (width <= 0) width = 1000;
+ int height = 1000;
+
+ android.graphics.Bitmap bitmap = net.micode.notes.tool.MediaUtils.getCompressedBitmap(imagePath, width, height);
+ if (bitmap != null) {
+ android.graphics.drawable.Drawable drawable = new android.graphics.drawable.BitmapDrawable(getResources(), bitmap);
+ int imgWidth = drawable.getIntrinsicWidth();
+ int imgHeight = drawable.getIntrinsicHeight();
+
+ if (imgWidth > width) {
+ float ratio = (float) width / imgWidth;
+ imgWidth = width;
+ imgHeight = (int) (imgHeight * ratio);
+ }
+ drawable.setBounds(0, 0, imgWidth, imgHeight);
+
+ // 4. 使用我们修复版 CenterImageSpan
+ CenterImageSpan span = new CenterImageSpan(drawable, imagePath);
+ editable.setSpan(span, start, end, android.text.Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
+ }
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ }
+ }
public void setIndex(int index) {
mIndex = index;
}
diff --git a/src/src/net/micode/notes/ui/NotesPreferenceActivity.java b/src/src/net/micode/notes/ui/NotesPreferenceActivity.java
index 3a0ba00..07c5f7e 100644
--- a/src/src/net/micode/notes/ui/NotesPreferenceActivity.java
+++ b/src/src/net/micode/notes/ui/NotesPreferenceActivity.java
@@ -1,6 +1,17 @@
/*
* Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net)
- * ... (版权声明略) ...
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
*/
package net.micode.notes.ui;
@@ -36,41 +47,37 @@ import net.micode.notes.data.Notes;
import net.micode.notes.data.Notes.NoteColumns;
import net.micode.notes.gtask.remote.GTaskSyncService;
-/**
- * 偏好设置页面 (UI Layer - Settings)
- *