类型: INTEGER (long)
+ */ + public static final String ID = "_id"; + + /** + * 笔记或文件夹的父ID + *类型: INTEGER (long)
+ */ + public static final String PARENT_ID = "parent_id"; + + /** + * 笔记或文件夹的创建日期 + *类型: INTEGER (long)
+ */ + public static final String CREATED_DATE = "created_date"; + + /** + * 最后修改日期 + *类型: INTEGER (long)
+ */ + public static final String MODIFIED_DATE = "modified_date"; + + /** + * 提醒日期 + *类型: INTEGER (long)
+ */ + public static final String ALERTED_DATE = "alert_date"; + + /** + * 文件夹名称或笔记的文本内容 + *类型: TEXT
+ */ + public static final String SNIPPET = "snippet"; + + /** + * 笔记的小部件ID + *类型: INTEGER (long)
+ */ + public static final String WIDGET_ID = "widget_id"; + + /** + * 笔记的小部件类型 + *类型: INTEGER (long)
+ */ + public static final String WIDGET_TYPE = "widget_type"; + + /** + * 笔记的背景色ID + *类型: INTEGER (long)
+ */ + public static final String BG_COLOR_ID = "bg_color_id"; + + /** + * 文本笔记没有附件,多媒体笔记至少有一个附件 + *类型: INTEGER
+ */ + public static final String HAS_ATTACHMENT = "has_attachment"; + + /** + * 文件夹中的笔记数量 + *类型: INTEGER (long)
+ */ + public static final String NOTES_COUNT = "notes_count"; + + /** + * 文件类型:文件夹或笔记 + *类型: INTEGER
+ */ + public static final String TYPE = "type"; + + /** + * 最后同步ID + *类型: INTEGER (long)
+ */ + public static final String SYNC_ID = "sync_id"; + + /** + * 标记是否本地修改过 + *类型: INTEGER
+ */ + public static final String LOCAL_MODIFIED = "local_modified"; + + /** + * 移动到临时文件夹前的原始父ID + *类型: INTEGER
+ */ + public static final String ORIGIN_PARENT_ID = "origin_parent_id"; + + /** + * Google任务ID + *类型: TEXT
+ */ + public static final String GTASK_ID = "gtask_id"; + + /** + * 版本号 + *类型: INTEGER (long)
+ */ + public static final String VERSION = "version"; + } + + // 数据表列名接口 + public interface DataColumns { + /** + * 行的唯一ID + *类型: INTEGER (long)
+ */ + public static final String ID = "_id"; + + /** + * 该行数据项的MIME类型 + *类型: Text
+ */ + public static final String MIME_TYPE = "mime_type"; + + /** + * 该数据所属笔记的引用ID + *类型: INTEGER (long)
+ */ + public static final String NOTE_ID = "note_id"; + + /** + * 数据项的创建日期 + *类型: INTEGER (long)
+ */ + public static final String CREATED_DATE = "created_date"; + + /** + * 最后修改日期 + *类型: INTEGER (long)
+ */ + public static final String MODIFIED_DATE = "modified_date"; + + /** + * 数据内容 + *类型: TEXT
+ */ + public static final String CONTENT = "content"; + + /** + * 通用数据列1,含义由MIME_TYPE决定,用于整数类型 + *类型: INTEGER
+ */ + public static final String DATA1 = "data1"; + + /** + * 通用数据列2,含义由MIME_TYPE决定,用于整数类型 + *类型: INTEGER
+ */ + public static final String DATA2 = "data2"; + + /** + * 通用数据列3,含义由MIME_TYPE决定,用于文本类型 + *类型: TEXT
+ */ + public static final String DATA3 = "data3"; + + /** + * 通用数据列4,含义由MIME_TYPE决定,用于文本类型 + *类型: TEXT
+ */ + public static final String DATA4 = "data4"; + + /** + * 通用数据列5,含义由MIME_TYPE决定,用于文本类型 + *类型: TEXT
+ */ + public static final String DATA5 = "data5"; + } + + // 文本笔记数据列 + public static final class TextNote implements DataColumns { + /** + * 标记文本是否为检查清单模式 + *类型: Integer 1:检查清单模式 0: 普通模式
+ */ + public static final String MODE = DATA1; + + public static final int MODE_CHECK_LIST = 1; // 检查清单模式 + + public static final String CONTENT_TYPE = "vnd.android.cursor.dir/text_note"; // 多文本笔记的MIME类型 + + public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/text_note"; // 单文本笔记的MIME类型 + + public static final Uri CONTENT_URI = Uri.parse("content://" + AUTHORITY + "/text_note"); // 文本笔记的URI + } + + // 通话笔记数据列 + public static final class CallNote implements DataColumns { + /** + * 该记录的通话日期 + *类型: INTEGER (long)
+ */ + public static final String CALL_DATE = DATA1; + + /** + * 该记录的电话号码 + *类型: TEXT
+ */ + public static final String PHONE_NUMBER = DATA3; + + public static final String CONTENT_TYPE = "vnd.android.cursor.dir/call_note"; // 多通话笔记的MIME类型 + + public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/call_note"; // 单通话笔记的MIME类型 + + public static final Uri CONTENT_URI = Uri.parse("content://" + AUTHORITY + "/call_note"); // 通话笔记的URI + } +} \ No newline at end of file diff --git a/src/mi_note/app/src/main/java/net/micode/notes/data/NotesDatabaseHelper.java b/src/mi_note/app/src/main/java/net/micode/notes/data/NotesDatabaseHelper.java new file mode 100644 index 0000000..f0094fe --- /dev/null +++ b/src/mi_note/app/src/main/java/net/micode/notes/data/NotesDatabaseHelper.java @@ -0,0 +1,389 @@ +/* + * 版权所有 (c) 2010-2011,MiCode 开源社区 (www.micode.net) + * 根据 Apache 许可证 2.0 版本("许可证")授权; + * 除非符合许可证的规定,否则不得使用本文件。 + * 您可以从以下网址获取许可证副本: + * http://www.apache.org/licenses/LICENSE-2.0 + * 除非适用法律要求或书面同意,本软件按"原样"分发, + * 没有任何明示或暗示的保证或条件。 + * 详见许可证中规定的权限和限制。 + *(注:这是一份标准的Apache许可证2.0版本的开源声明) +*/ + +// 笔记应用的数据库辅助类,用于创建和管理SQLite数据库 +package net.micode.notes.data; + +// 导入所需的Android类 +import android.content.ContentValues; +import android.content.Context; +import android.database.sqlite.SQLiteDatabase; +import android.database.sqlite.SQLiteOpenHelper; +import android.util.Log; + +// 导入项目中的笔记数据相关常量 +import net.micode.notes.data.Notes.DataColumns; +import net.micode.notes.data.Notes.DataConstants; +import net.micode.notes.data.Notes.NoteColumns; + +// SQLiteOpenHelper的子类,用于管理笔记数据库 +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 DATA = "data"; // 数据表名 + } + + // 日志标签 + private static final String TAG = "NotesDatabaseHelper"; + + // 单例实例 + private static NotesDatabaseHelper mInstance; + + // 创建笔记表的SQL语句 + private static final String CREATE_NOTE_TABLE_SQL = + "CREATE TABLE " + TABLE.NOTE + "(" + + NoteColumns.ID + " INTEGER PRIMARY KEY," + // ID主键 + NoteColumns.PARENT_ID + " INTEGER NOT NULL DEFAULT 0," + // 父ID + NoteColumns.ALERTED_DATE + " INTEGER NOT NULL DEFAULT 0," + // 提醒日期 + NoteColumns.BG_COLOR_ID + " INTEGER NOT NULL DEFAULT 0," + // 背景色ID + 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," + // 小部件ID + NoteColumns.WIDGET_TYPE + " INTEGER NOT NULL DEFAULT -1," + // 小部件类型 + NoteColumns.SYNC_ID + " INTEGER NOT NULL DEFAULT 0," + // 同步ID + NoteColumns.LOCAL_MODIFIED + " INTEGER NOT NULL DEFAULT 0," + // 本地修改标志 + NoteColumns.ORIGIN_PARENT_ID + " INTEGER NOT NULL DEFAULT 0," + // 原始父ID + NoteColumns.GTASK_ID + " TEXT NOT NULL DEFAULT ''," + // Google任务ID + NoteColumns.VERSION + " INTEGER NOT NULL DEFAULT 0" + // 版本号 + ")"; + + // 创建数据表的SQL语句 + private static final String CREATE_DATA_TABLE_SQL = + "CREATE TABLE " + TABLE.DATA + "(" + + DataColumns.ID + " INTEGER PRIMARY KEY," + // ID主键 + DataColumns.MIME_TYPE + " TEXT NOT NULL," + // MIME类型 + DataColumns.NOTE_ID + " INTEGER NOT NULL DEFAULT 0," + // 关联的笔记ID + 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," + // 通用数据列1 + DataColumns.DATA2 + " INTEGER," + // 通用数据列2 + DataColumns.DATA3 + " TEXT NOT NULL DEFAULT ''," + // 通用数据列3 + DataColumns.DATA4 + " TEXT NOT NULL DEFAULT ''," + // 通用数据列4 + DataColumns.DATA5 + " TEXT NOT NULL DEFAULT ''" + // 通用数据列5 + ")"; + + // 创建数据表note_id索引的SQL语句 + private static final String CREATE_DATA_NOTE_ID_INDEX_SQL = + "CREATE INDEX IF NOT EXISTS note_id_index ON " + + TABLE.DATA + "(" + DataColumns.NOTE_ID + ");"; + + /** + * 触发器:当笔记的父ID更新时增加文件夹的笔记计数 + */ + 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"; + + /** + * 触发器:当笔记的父ID更新时减少原文件夹的笔记计数 + */ + 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"; + + /** + * 触发器:当插入新笔记时增加文件夹的笔记计数 + */ + 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"; + + /** + * 触发器:当删除笔记时减少文件夹的笔记计数 + */ + 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"; + + /** + * 触发器:当插入数据且类型为普通笔记时更新笔记内容 + */ + 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"; + + /** + * 触发器:当更新数据且类型为普通笔记时更新笔记内容 + */ + 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"; + + /** + * 触发器:当删除数据且类型为普通笔记时清空笔记内容 + */ + 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"; + + /** + * 触发器:当删除笔记时删除关联的所有数据 + */ + 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"; + + /** + * 触发器:当删除文件夹时删除其中的所有笔记 + */ + 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"; + + /** + * 触发器:当文件夹被移动到回收站时,将其中的笔记也移动到回收站 + */ + 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"; + + // 构造函数 + public NotesDatabaseHelper(Context context) { + super(context, DB_NAME, null, DB_VERSION); + } + + // 创建笔记表 + public void createNoteTable(SQLiteDatabase db) { + db.execSQL(CREATE_NOTE_TABLE_SQL); // 执行建表SQL + reCreateNoteTableTriggers(db); // 重建触发器 + createSystemFolder(db); // 创建系统文件夹 + Log.d(TAG, "note table has been created"); // 日志记录 + } + + // 重建笔记表触发器 + private void reCreateNoteTableTriggers(SQLiteDatabase db) { + // 删除旧触发器 + db.execSQL("DROP TRIGGER IF EXISTS increase_folder_count_on_update"); + db.execSQL("DROP TRIGGER IF EXISTS decrease_folder_count_on_update"); + db.execSQL("DROP TRIGGER IF EXISTS decrease_folder_count_on_delete"); + db.execSQL("DROP TRIGGER IF EXISTS delete_data_on_delete"); + db.execSQL("DROP TRIGGER IF EXISTS increase_folder_count_on_insert"); + db.execSQL("DROP TRIGGER IF EXISTS folder_delete_notes_on_delete"); + db.execSQL("DROP TRIGGER IF EXISTS folder_move_notes_on_trash"); + + // 创建新触发器 + db.execSQL(NOTE_INCREASE_FOLDER_COUNT_ON_UPDATE_TRIGGER); + db.execSQL(NOTE_DECREASE_FOLDER_COUNT_ON_UPDATE_TRIGGER); + db.execSQL(NOTE_DECREASE_FOLDER_COUNT_ON_DELETE_TRIGGER); + db.execSQL(NOTE_DELETE_DATA_ON_DELETE_TRIGGER); + db.execSQL(NOTE_INCREASE_FOLDER_COUNT_ON_INSERT_TRIGGER); + db.execSQL(FOLDER_DELETE_NOTES_ON_DELETE_TRIGGER); + db.execSQL(FOLDER_MOVE_NOTES_ON_TRASH_TRIGGER); + } + + // 创建系统文件夹 + private void createSystemFolder(SQLiteDatabase db) { + ContentValues values = new ContentValues(); + + /* + * 创建通话记录文件夹 + */ + values.put(NoteColumns.ID, Notes.ID_CALL_RECORD_FOLDER); + values.put(NoteColumns.TYPE, Notes.TYPE_SYSTEM); + db.insert(TABLE.NOTE, null, values); + + /* + * 创建根文件夹(默认文件夹) + */ + values.clear(); + values.put(NoteColumns.ID, Notes.ID_ROOT_FOLDER); + values.put(NoteColumns.TYPE, Notes.TYPE_SYSTEM); + db.insert(TABLE.NOTE, null, values); + /* + * 创建临时文件夹(用于移动笔记) + */ + values.clear(); + values.put(NoteColumns.ID, Notes.ID_TEMPARAY_FOLDER); + values.put(NoteColumns.TYPE, Notes.TYPE_SYSTEM); + db.insert(TABLE.NOTE, null, values); + /* + * 创建回收站文件夹 + */ + values.clear(); + values.put(NoteColumns.ID, Notes.ID_TRASH_FOLER); + values.put(NoteColumns.TYPE, Notes.TYPE_SYSTEM); + db.insert(TABLE.NOTE, null, values); + } + + // 创建数据表 + public void createDataTable(SQLiteDatabase db) { + db.execSQL(CREATE_DATA_TABLE_SQL); // 执行建表SQL + reCreateDataTableTriggers(db); // 重建触发器 + db.execSQL(CREATE_DATA_NOTE_ID_INDEX_SQL); // 创建索引 + Log.d(TAG, "data table has been created"); // 日志记录 + } + + // 重建数据表触发器 + private void reCreateDataTableTriggers(SQLiteDatabase db) { + // 删除旧触发器 + db.execSQL("DROP TRIGGER IF EXISTS update_note_content_on_insert"); + db.execSQL("DROP TRIGGER IF EXISTS update_note_content_on_update"); + db.execSQL("DROP TRIGGER IF EXISTS update_note_content_on_delete"); + + // 创建新触发器 + db.execSQL(DATA_UPDATE_NOTE_CONTENT_ON_INSERT_TRIGGER); + db.execSQL(DATA_UPDATE_NOTE_CONTENT_ON_UPDATE_TRIGGER); + db.execSQL(DATA_UPDATE_NOTE_CONTENT_ON_DELETE_TRIGGER); + } + + // 获取单例实例(线程安全) + static synchronized NotesDatabaseHelper getInstance(Context context) { + if (mInstance == null) { + mInstance = new NotesDatabaseHelper(context); + } + return mInstance; + } + + // 数据库创建时的回调方法 + @Override + public void onCreate(SQLiteDatabase db) { + createNoteTable(db); // 创建笔记表 + createDataTable(db); // 创建数据表 + } + + // 数据库升级时的回调方法 + @Override + public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { + boolean reCreateTriggers = false; // 是否需要重建触发器 + boolean skipV2 = false; // 是否跳过V2升级 + + // 从版本1升级到版本2 + if (oldVersion == 1) { + upgradeToV2(db); // 执行V2升级 + skipV2 = true; // 此次升级包含从V2到V3的升级 + oldVersion++; // 版本号递增 + } + + // 从版本2升级到版本3(如果不需要跳过) + if (oldVersion == 2 && !skipV2) { + upgradeToV3(db); // 执行V3升级 + reCreateTriggers = true; // 需要重建触发器 + oldVersion++; // 版本号递增 + } + + // 从版本3升级到版本4 + if (oldVersion == 3) { + upgradeToV4(db); // 执行V4升级 + oldVersion++; // 版本号递增 + } + + // 如果需要重建触发器 + if (reCreateTriggers) { + reCreateNoteTableTriggers(db); // 重建笔记表触发器 + reCreateDataTableTriggers(db); // 重建数据表触发器 + } + + // 如果升级未完成,抛出异常 + if (oldVersion != newVersion) { + throw new IllegalStateException("Upgrade notes database to version " + newVersion + "fails"); + } + } + + // 升级到版本2的实现 + private void upgradeToV2(SQLiteDatabase db) { + // 删除旧表(如果存在) + db.execSQL("DROP TABLE IF EXISTS " + TABLE.NOTE); + db.execSQL("DROP TABLE IF EXISTS " + TABLE.DATA); + // 重新创建表 + createNoteTable(db); // 创建笔记表 + createDataTable(db); // 创建数据表 + } + + // 升级到版本3的实现 + private void upgradeToV3(SQLiteDatabase db) { + // 删除不再使用的触发器 + db.execSQL("DROP TRIGGER IF EXISTS update_note_modified_date_on_insert"); + db.execSQL("DROP TRIGGER IF EXISTS update_note_modified_date_on_delete"); + db.execSQL("DROP TRIGGER IF EXISTS update_note_modified_date_on_update"); + + // 添加gtask_id列 + db.execSQL("ALTER TABLE " + TABLE.NOTE + " ADD COLUMN " + NoteColumns.GTASK_ID + " TEXT NOT NULL DEFAULT ''"); + + // 添加回收站系统文件夹 + ContentValues values = new ContentValues(); + values.put(NoteColumns.ID, Notes.ID_TRASH_FOLER); + values.put(NoteColumns.TYPE, Notes.TYPE_SYSTEM); + db.insert(TABLE.NOTE, null, values); + } + + // 升级到版本4的实现 + private void upgradeToV4(SQLiteDatabase db) { + // 添加version列 + 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/mi_note/app/src/main/java/net/micode/notes/data/NotesProvider.java b/src/mi_note/app/src/main/java/net/micode/notes/data/NotesProvider.java new file mode 100644 index 0000000..cb18f06 --- /dev/null +++ b/src/mi_note/app/src/main/java/net/micode/notes/data/NotesProvider.java @@ -0,0 +1,314 @@ +/* + * 版权所有 (c) 2010-2011,MiCode 开源社区 (www.micode.net) + * 根据 Apache 许可证 2.0 版本("许可证")授权; + * 除非符合许可证的规定,否则不得使用本文件。 + * 您可以从以下网址获取许可证副本: + * http://www.apache.org/licenses/LICENSE-2.0 + * 除非适用法律要求或书面同意,本软件按"原样"分发, + * 没有任何明示或暗示的保证或条件。 + * 详见许可证中规定的权限和限制。 + * (注:这是一份标准的Apache许可证2.0版本的开源声明) + */ + +// 笔记内容提供者的实现类,提供对笔记数据的CRUD操作 +package net.micode.notes.data; + +// 导入所需的Android类 +import android.app.SearchManager; // 搜索相关功能 +import android.content.ContentProvider; // 内容提供者基类 +import android.content.ContentUris; // URI工具类 +import android.content.ContentValues; // 键值对存储类 +import android.content.Intent; // 意图相关 +import android.content.UriMatcher; // URI匹配器 +import android.database.Cursor; // 数据库查询结果 +import android.database.sqlite.SQLiteDatabase; // SQLite数据库 +import android.net.Uri; // URI处理 +import android.text.TextUtils; // 文本处理工具 +import android.util.Log; // 日志工具 + +// 导入项目资源 +import androidx.annotation.NonNull; + +import net.micode.notes.R; // 资源文件 +import net.micode.notes.data.Notes.DataColumns; // 数据列常量 +import net.micode.notes.data.Notes.NoteColumns; // 笔记列常量 +import net.micode.notes.data.NotesDatabaseHelper.TABLE; // 数据库表名 + +import java.util.Objects; + +// 内容提供者实现类 +public class NotesProvider extends ContentProvider { + private static final UriMatcher mMatcher; // URI匹配器,用于匹配不同的URI请求 + + private NotesDatabaseHelper mHelper; // 数据库帮助类 + + private static final String TAG = "NotesProvider"; // 日志标签 + + // URI类型常量 + private static final int URI_NOTE = 1; // 笔记表操作 + private static final int URI_NOTE_ITEM = 2; // 单条笔记操作 + private static final int URI_DATA = 3; // 数据表操作 + private static final int URI_DATA_ITEM = 4; // 单条数据操作 + private static final int URI_SEARCH = 5; // 搜索操作 + private static final int URI_SEARCH_SUGGEST = 6; // 搜索建议 + + // 静态初始化块,配置URI匹配规则 + static { + mMatcher = new UriMatcher(UriMatcher.NO_MATCH); // 创建URI匹配器 + mMatcher.addURI(Notes.AUTHORITY, "note", URI_NOTE); // 匹配笔记表URI + mMatcher.addURI(Notes.AUTHORITY, "note/#", URI_NOTE_ITEM); // 匹配单条笔记URI + mMatcher.addURI(Notes.AUTHORITY, "data", URI_DATA); // 匹配数据表URI + mMatcher.addURI(Notes.AUTHORITY, "data/#", URI_DATA_ITEM); // 匹配单条数据URI + mMatcher.addURI(Notes.AUTHORITY, "search", URI_SEARCH); // 匹配搜索URI + mMatcher.addURI(Notes.AUTHORITY, SearchManager.SUGGEST_URI_PATH_QUERY, URI_SEARCH_SUGGEST); // 匹配搜索建议URI + mMatcher.addURI(Notes.AUTHORITY, SearchManager.SUGGEST_URI_PATH_QUERY + "/*", URI_SEARCH_SUGGEST); // 匹配带参数的搜索建议URI + } + + /** + * x'0A'代表SQLite中的换行符。对于搜索结果中的标题和内容, + * 我们将删除换行符和空格以显示更多信息。 + */ + // 搜索结果的投影(列)定义 + private static final String NOTES_SEARCH_PROJECTION = NoteColumns.ID + "," // 笔记ID + + NoteColumns.ID + " AS " + SearchManager.SUGGEST_COLUMN_INTENT_EXTRA_DATA + "," // 建议项的额外数据 + + "TRIM(REPLACE(" + NoteColumns.SNIPPET + ", x'0A','')) AS " + SearchManager.SUGGEST_COLUMN_TEXT_1 + "," // 建议项主文本 + + "TRIM(REPLACE(" + NoteColumns.SNIPPET + ", x'0A','')) AS " + SearchManager.SUGGEST_COLUMN_TEXT_2 + "," // 建议项次文本 + + R.drawable.search_result + " AS " + SearchManager.SUGGEST_COLUMN_ICON_1 + "," // 建议项图标 + + "'" + Intent.ACTION_VIEW + "' AS " + SearchManager.SUGGEST_COLUMN_INTENT_ACTION + "," // 建议项点击动作 + + "'" + Notes.TextNote.CONTENT_TYPE + "' AS " + SearchManager.SUGGEST_COLUMN_INTENT_DATA; // 建议项数据 + + // 搜索查询SQL语句 + private static final 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; // 只查询普通笔记 + + // 内容提供者创建时调用 + @Override + public boolean onCreate() { + mHelper = NotesDatabaseHelper.getInstance(getContext()); // 获取数据库帮助类实例 + return true; + } + + // 查询方法 + @Override + public Cursor query(@NonNull Uri uri, String[] projection, String selection, String[] selectionArgs, + String sortOrder) { + Cursor c = null; // 查询结果游标 + SQLiteDatabase db = mHelper.getReadableDatabase(); // 获取可读数据库 + String id = null; // ID变量 + switch (mMatcher.match(uri)) { // 根据URI类型处理不同查询 + case URI_NOTE: // 查询笔记表 + c = db.query(TABLE.NOTE, projection, selection, selectionArgs, null, null, + sortOrder); + break; + case URI_NOTE_ITEM: // 查询单条笔记 + id = uri.getPathSegments().get(1); // 从URI中获取笔记ID + c = db.query(TABLE.NOTE, projection, NoteColumns.ID + "=" + id + + parseSelection(selection), selectionArgs, null, null, sortOrder); + break; + case URI_DATA: // 查询数据表 + c = db.query(TABLE.DATA, projection, selection, selectionArgs, null, null, + sortOrder); + break; + case URI_DATA_ITEM: // 查询单条数据 + id = uri.getPathSegments().get(1); // 从URI中获取数据ID + c = db.query(TABLE.DATA, projection, DataColumns.ID + "=" + id + + parseSelection(selection), selectionArgs, null, null, sortOrder); + break; + case URI_SEARCH: // 搜索请求 + case URI_SEARCH_SUGGEST: // 搜索建议请求 + if (sortOrder != null || projection != null) { + throw new IllegalArgumentException( + "do not specify sortOrder, selection, selectionArgs, or projection" + "with this query"); + } + + String searchString = null; // 搜索关键字 + if (mMatcher.match(uri) == URI_SEARCH_SUGGEST) { // 处理搜索建议请求 + if (uri.getPathSegments().size() > 1) { + searchString = uri.getPathSegments().get(1); // 从URI获取搜索词 + } + } else { // 处理普通搜索请求 + searchString = uri.getQueryParameter("pattern"); // 从查询参数获取搜索模式 + } + + if (TextUtils.isEmpty(searchString)) { // 搜索词为空则返回空 + return null; + } + + try { + searchString = String.format("%%%s%%", searchString); // 添加通配符 + c = db.rawQuery(NOTES_SNIPPET_SEARCH_QUERY, // 执行原始查询 + new String[] { searchString }); + } catch (IllegalStateException ex) { + Log.e(TAG, "got exception: " + ex.toString()); // 记录异常 + } + break; + default: + throw new IllegalArgumentException("Unknown URI " + uri); // 未知URI异常 + } + if (c != null) { + c.setNotificationUri(Objects.requireNonNull(getContext()).getContentResolver(), uri); // 设置通知URI + } + return c; // 返回查询结果 + } + + // 插入方法 + @Override + public Uri insert(@NonNull Uri uri, ContentValues values) { + SQLiteDatabase db = mHelper.getWritableDatabase(); // 获取可写数据库 + long dataId = 0, noteId = 0, insertedId = 0; // ID变量 + insertedId = switch (mMatcher.match(uri)) { // 根据URI类型处理不同插入 + case URI_NOTE -> // 插入笔记 + noteId = db.insert(TABLE.NOTE, null, values); + case URI_DATA -> { + if (values.containsKey(DataColumns.NOTE_ID)) { // 检查是否包含笔记ID + noteId = values.getAsLong(DataColumns.NOTE_ID); + } else { + Log.d(TAG, "Wrong data format without note id:" + values.toString()); + } + yield dataId = db.insert(TABLE.DATA, null, values); + } + default -> throw new IllegalArgumentException("Unknown URI " + uri); // 未知URI异常 + }; + // 通知笔记URI变更 + if (noteId > 0) { + Objects.requireNonNull(getContext()).getContentResolver().notifyChange( + ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, noteId), null); + } + + // 通知数据URI变更 + if (dataId > 0) { + Objects.requireNonNull(getContext()).getContentResolver().notifyChange( + ContentUris.withAppendedId(Notes.CONTENT_DATA_URI, dataId), null); + } + + return ContentUris.withAppendedId(uri, insertedId); // 返回插入数据的URI + } + + // 删除方法 + @Override + public int delete(@NonNull Uri uri, String selection, String[] selectionArgs) { + int count = 0; // 删除计数 + String id = null; // ID变量 + SQLiteDatabase db = mHelper.getWritableDatabase(); // 获取可写数据库 + boolean deleteData = false; // 数据删除标志 + switch (mMatcher.match(uri)) { // 根据URI类型处理不同删除 + case URI_NOTE: // 删除笔记 + selection = "(" + selection + ") AND " + NoteColumns.ID + ">0 "; // 只删除ID大于0的笔记 + count = db.delete(TABLE.NOTE, selection, selectionArgs); + break; + case URI_NOTE_ITEM: // 删除单条笔记 + id = uri.getPathSegments().get(1); // 从URI获取笔记ID + /* + * 小于0的ID是系统文件夹,不允许删除 + */ + long noteId = Long.parseLong(id); + if (noteId <= 0) { // 系统文件夹不删除 + break; + } + count = db.delete(TABLE.NOTE, + NoteColumns.ID + "=" + id + parseSelection(selection), selectionArgs); + break; + case URI_DATA: // 删除数据 + count = db.delete(TABLE.DATA, selection, selectionArgs); + deleteData = true; // 标记为数据删除 + break; + case URI_DATA_ITEM: // 删除单条数据 + id = uri.getPathSegments().get(1); // 从URI获取数据ID + count = db.delete(TABLE.DATA, + DataColumns.ID + "=" + id + parseSelection(selection), selectionArgs); + deleteData = true; // 标记为数据删除 + break; + default: + throw new IllegalArgumentException("Unknown URI " + uri); // 未知URI异常 + } + if (count > 0) { // 如果有数据被删除 + if (deleteData) { // 如果是数据删除,通知笔记URI变更 + Objects.requireNonNull(getContext()).getContentResolver().notifyChange(Notes.CONTENT_NOTE_URI, null); + } + Objects.requireNonNull(getContext()).getContentResolver().notifyChange(uri, null); // 通知当前URI变更 + } + return count; // 返回删除数量 + } + + // 更新方法 + @Override + public int update(@NonNull Uri uri, ContentValues values, String selection, String[] selectionArgs) { + int count = 0; // 更新计数 + String id = null; // ID变量 + SQLiteDatabase db = mHelper.getWritableDatabase(); // 获取可写数据库 + boolean updateData = false; // 数据更新标志 + switch (mMatcher.match(uri)) { // 根据URI类型处理不同更新 + case URI_NOTE: // 更新笔记表 + increaseNoteVersion(-1, selection, selectionArgs); // 增加笔记版本 + count = db.update(TABLE.NOTE, values, selection, selectionArgs); + break; + case URI_NOTE_ITEM: // 更新单条笔记 + id = uri.getPathSegments().get(1); // 从URI获取笔记ID + increaseNoteVersion(Long.parseLong(id), selection, selectionArgs); // 增加笔记版本 + count = db.update(TABLE.NOTE, values, NoteColumns.ID + "=" + id + + parseSelection(selection), selectionArgs); + break; + case URI_DATA: // 更新数据表 + count = db.update(TABLE.DATA, values, selection, selectionArgs); + updateData = true; // 标记为数据更新 + break; + case URI_DATA_ITEM: // 更新单条数据 + id = uri.getPathSegments().get(1); // 从URI获取数据ID + count = db.update(TABLE.DATA, values, DataColumns.ID + "=" + id + + parseSelection(selection), selectionArgs); + updateData = true; // 标记为数据更新 + break; + default: + throw new IllegalArgumentException("Unknown URI " + uri); // 未知URI异常 + } + + if (count > 0) { // 如果有数据被更新 + if (updateData) { // 如果是数据更新,通知笔记URI变更 + Objects.requireNonNull(getContext()).getContentResolver().notifyChange(Notes.CONTENT_NOTE_URI, null); + } + Objects.requireNonNull(getContext()).getContentResolver().notifyChange(uri, null); // 通知当前URI变更 + } + return count; // 返回更新数量 + } + + // 解析选择条件,添加AND连接 + 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语句构建器 + sql.append("UPDATE "); + sql.append(TABLE.NOTE); + sql.append(" SET "); + sql.append(NoteColumns.VERSION); + sql.append("=" + NoteColumns.VERSION + "+1 "); // 版本号+1 + + if (id > 0 || !TextUtils.isEmpty(selection)) { // 有条件时添加WHERE + sql.append(" WHERE "); + } + if (id > 0) { // 指定ID的条件 + sql.append(NoteColumns.ID + "=").append(id); + } + if (!TextUtils.isEmpty(selection)) { // 处理其他条件 + String selectString = id > 0 ? parseSelection(selection) : selection; + for (String args : selectionArgs) { // 替换参数占位符 + selectString = selectString.replaceFirst("\\?", args); + } + sql.append(selectString); + } + + mHelper.getWritableDatabase().execSQL(sql.toString()); // 执行SQL + } + + // 获取URI类型(未实现) + @Override + public String getType(@NonNull Uri uri) { + // TODO Auto-generated method stub + return null; + } +} \ No newline at end of file diff --git a/src/mi_note/app/src/main/java/net/micode/notes/gtask/data/MetaData.java b/src/mi_note/app/src/main/java/net/micode/notes/gtask/data/MetaData.java new file mode 100644 index 0000000..377521f --- /dev/null +++ b/src/mi_note/app/src/main/java/net/micode/notes/gtask/data/MetaData.java @@ -0,0 +1,132 @@ +/* + * 版权所有 (c) 2010-2011,MiCode 开源社区 (www.micode.net) + * 根据 Apache 许可证 2.0 版本("许可证")授权; + * 除非符合许可证的规定,否则不得使用本文件。 + * 您可以从以下网址获取许可证副本: + * http://www.apache.org/licenses/LICENSE-2.0 + * 除非适用法律要求或书面同意,本软件按"原样"分发, + * 没有任何明示或暗示的保证或条件。 + * 详见许可证中规定的权限和限制。 + * (注:这是一份标准的Apache许可证2.0版本的开源声明) + */ + +// 定义MetaData类所在的包路径 +package net.micode.notes.gtask.data; + +// 导入Android数据库Cursor类 +import android.database.Cursor; +// 导入Android日志工具类 +import android.util.Log; + +// 导入应用中自定义的字符串工具类 +import net.micode.notes.tool.GTaskStringUtils; + +// 导入JSON处理相关类 +import org.json.JSONException; +import org.json.JSONObject; + +/** + * MetaData类 - 继承自Task类 + * 功能:处理Google Tasks元数据信息 + * 特性: + * 1. 存储关联任务的GID(Google ID) + * 2. 将元数据信息以JSON格式存储在notes字段中 + * 3. 仅用于远程数据同步,不支持本地操作 + */ +public class MetaData extends Task { + // 日志标签,使用类名作为标识 + private final static String TAG = MetaData.class.getSimpleName(); + + // 存储关联任务的Google ID + private String mRelatedGid = null; + + /** + * 设置元数据信息 + * @param gid 关联任务的Google ID + * @param metaInfo 包含元数据的JSON对象 + */ + public void setMeta(String gid, JSONObject metaInfo) { + try { + // 将GID添加到元数据JSON中 + metaInfo.put(GTaskStringUtils.META_HEAD_GTASK_ID, gid); + } catch (JSONException e) { + Log.e(TAG, "failed to put related gid"); // 记录错误日志 + } + // 将JSON对象转为字符串存储到notes字段 + setNotes(metaInfo.toString()); + // 设置固定名称标识这是一个元数据任务 + setName(GTaskStringUtils.META_NOTE_NAME); + } + + /** + * 获取关联任务的Google ID + * @return 关联GID,可能为null + */ + public String getRelatedGid() { + return mRelatedGid; + } + + /** + * 判断是否值得保存 + * @return notes字段不为空时返回true + */ + @Override + public boolean isWorthSaving() { + return getNotes() != null; + } + + /** + * 从远程JSON数据设置内容 + * @param js 包含远程数据的JSON对象 + */ + @Override + public void setContentByRemoteJSON(JSONObject js) { + // 先调用父类方法处理基础字段 + super.setContentByRemoteJSON(js); + + // 如果notes字段不为空,解析其中的GID + if (getNotes() != null) { + try { + // 将notes字符串转为JSON对象 + JSONObject metaInfo = new JSONObject(getNotes().trim()); + // 从JSON中获取关联GID + mRelatedGid = metaInfo.getString(GTaskStringUtils.META_HEAD_GTASK_ID); + } catch (JSONException e) { + Log.w(TAG, "failed to get related gid"); // 记录警告日志 + mRelatedGid = null; // 解析失败时重置GID + } + } + } + + /** + * 从本地JSON设置内容(不支持) + * @param js JSON对象 + * @throws IllegalAccessError 总是抛出此异常 + */ + @Override + public void setContentByLocalJSON(JSONObject js) { + // 元数据不应从本地JSON设置,直接抛出异常 + throw new IllegalAccessError("MetaData:setContentByLocalJSON should not be called"); + } + + /** + * 获取本地JSON内容(不支持) + * @throws IllegalAccessError 总是抛出此异常 + */ + @Override + public JSONObject getLocalJSONFromContent() { + // 元数据不应生成本地JSON,直接抛出异常 + throw new IllegalAccessError("MetaData:getLocalJSONFromContent should not be called"); + } + + /** + * 获取同步动作(不支持) + * @param c 数据库Cursor + * @throws IllegalAccessError 总是抛出此异常 + */ + @Override + public int getSyncAction(Cursor c) { + // 元数据不支持同步动作查询,直接抛出异常 + throw new IllegalAccessError("MetaData:getSyncAction should not be called"); + } +} \ No newline at end of file diff --git a/src/mi_note/app/src/main/java/net/micode/notes/gtask/data/Node.java b/src/mi_note/app/src/main/java/net/micode/notes/gtask/data/Node.java new file mode 100644 index 0000000..ec33700 --- /dev/null +++ b/src/mi_note/app/src/main/java/net/micode/notes/gtask/data/Node.java @@ -0,0 +1,157 @@ +/* + * 版权所有 (c) 2010-2011,MiCode 开源社区 (www.micode.net) + * 根据 Apache 许可证 2.0 版本("许可证")授权; + * 除非符合许可证的规定,否则不得使用本文件。 + * 您可以从以下网址获取许可证副本: + * http://www.apache.org/licenses/LICENSE-2.0 + * 除非适用法律要求或书面同意,本软件按"原样"分发, + * 没有任何明示或暗示的保证或条件。 + * 详见许可证中规定的权限和限制。 + * (注:这是一份标准的Apache许可证2.0版本的开源声明) + */ +// 定义Node类所在的包路径 +package net.micode.notes.gtask.data; +// 导入Android数据库Cursor类(用于本地数据操作) +import android.database.Cursor; +// 导入JSON处理类 +import org.json.JSONObject; +/** + * 抽象Node类 - GTasks数据同步的基础节点类 + * 功能: + * 1. 定义通用的同步动作常量 + * 2. 提供基本属性管理(GID、名称、修改时间等) + * 3. 声明数据同步的核心抽象方法 + */ +public abstract class Node { + // 同步动作常量:无操作 + public static final int SYNC_ACTION_NONE = 0; + // 同步动作常量:添加到远程 + public static final int SYNC_ACTION_ADD_REMOTE = 1; + // 同步动作常量:添加到本地 + public static final int SYNC_ACTION_ADD_LOCAL = 2; + // 同步动作常量:从远程删除 + public static final int SYNC_ACTION_DEL_REMOTE = 3; + // 同步动作常量:从本地删除 + public static final int SYNC_ACTION_DEL_LOCAL = 4; + // 同步动作常量:更新远程 + public static final int SYNC_ACTION_UPDATE_REMOTE = 5; + // 同步动作常量:更新本地 + public static final int SYNC_ACTION_UPDATE_LOCAL = 6; + // 同步动作常量:更新冲突 + public static final int SYNC_ACTION_UPDATE_CONFLICT = 7; + // 同步动作常量:错误状态 + public static final int SYNC_ACTION_ERROR = 8; + // Google任务ID(全局唯一标识) + private String mGid; + // 节点名称 + private String mName; + // 最后修改时间戳(毫秒) + private long mLastModified; + // 删除标记 + private boolean mDeleted; + + /** + * 构造函数 + * 初始化默认值: + * - GID为null + * - 名称为空字符串 + * - 最后修改时间为0 + * - 未删除状态 + */ + public Node() { + mGid = null; + mName = ""; + mLastModified = 0; + mDeleted = false; + } + /** + * 抽象方法:获取创建动作的JSON数据 + * @param actionId 动作ID + * @return 包含创建动作数据的JSON对象 + */ + public abstract JSONObject getCreateAction(int actionId); + + /** + * 抽象方法:获取更新动作的JSON数据 + * @param actionId 动作ID + * @return 包含更新动作数据的JSON对象 + */ + public abstract JSONObject getUpdateAction(int actionId); + /** + * 抽象方法:从远程JSON数据设置内容 + * @param js 包含远程数据的JSON对象 + */ + public abstract void setContentByRemoteJSON(JSONObject js); + /** + * 抽象方法:从本地JSON数据设置内容 + * @param js 包含本地数据的JSON对象 + */ + public abstract void setContentByLocalJSON(JSONObject js); + /** + * 抽象方法:从内容生成本地JSON数据 + * @return 包含本地数据的JSON对象 + */ + public abstract JSONObject getLocalJSONFromContent(); + /** + * 抽象方法:获取同步动作类型 + * @param c 数据库Cursor对象 + * @return 同步动作常量(SYNC_ACTION_*) + */ + public abstract int getSyncAction(Cursor c); + /** + * 设置Google任务ID + * @param gid 全局唯一标识 + */ + public void setGid(String gid) { + this.mGid = gid; + } + /** + * 设置节点名称 + * @param name 显示名称 + */ + public void setName(String name) { + this.mName = name; + } + /** + * 设置最后修改时间 + * @param lastModified 时间戳(毫秒) + */ + public void setLastModified(long lastModified) { + this.mLastModified = lastModified; + } + /** + * 设置删除标记 + * @param deleted 是否已删除 + */ + public void setDeleted(boolean deleted) { + this.mDeleted = deleted; + } + /** + * 获取Google任务ID + * @return 全局唯一标识(可能为null) + */ + public String getGid() { + return this.mGid; + } + /** + * 获取节点名称 + * @return 显示名称 + */ + public String getName() { + return this.mName; + } + /** + * 获取最后修改时间 + * @return 时间戳(毫秒) + */ + public long getLastModified() { + return this.mLastModified; + } + /** + * 获取删除状态 + * @return 是否已删除 + */ + public boolean getDeleted() { + return this.mDeleted; + } +} \ No newline at end of file diff --git a/src/mi_note/app/src/main/java/net/micode/notes/gtask/data/SqlData.java b/src/mi_note/app/src/main/java/net/micode/notes/gtask/data/SqlData.java new file mode 100644 index 0000000..aa230c3 --- /dev/null +++ b/src/mi_note/app/src/main/java/net/micode/notes/gtask/data/SqlData.java @@ -0,0 +1,258 @@ +/* + * 版权所有 (c) 2010-2011,MiCode 开源社区 (www.micode.net) + * 根据 Apache 许可证 2.0 版本("许可证")授权; + * 除非符合许可证的规定,否则不得使用本文件。 + * 您可以从以下网址获取许可证副本: + * http://www.apache.org/licenses/LICENSE-2.0 + * 除非适用法律要求或书面同意,本软件按"原样"分发, + * 没有任何明示或暗示的保证或条件。 + * 详见许可证中规定的权限和限制。 + * (注:这是一份标准的Apache许可证2.0版本的开源声明) + */ + +// 定义当前类所在的包路径 +package net.micode.notes.gtask.data; + +// 导入Android内容提供者相关类 +import android.content.ContentResolver; // 内容解析器,用于访问ContentProvider +import android.content.ContentUris; // 用于构建带有ID的URI +import android.content.ContentValues; // 键值对存储,用于数据库操作 +import android.content.Context; // 应用上下文 +import android.database.Cursor; // 数据库查询结果集 +import android.net.Uri; // 通用资源标识符 +import android.os.Build; // 获取Android版本信息 +import android.util.Log; // 日志工具 + +// 导入AndroidX注解(版本检查) +import androidx.annotation.RequiresApi; + +// 导入笔记应用数据相关常量 +import net.micode.notes.data.Notes; // 笔记数据常量 +import net.micode.notes.data.Notes.DataColumns; // 数据列名常量 +import net.micode.notes.data.Notes.DataConstants; // 数据相关常量 +import net.micode.notes.data.Notes.NoteColumns; // 笔记列名常量 +import net.micode.notes.data.NotesDatabaseHelper.TABLE; // 数据库表名 +// 导入同步异常类 +import net.micode.notes.gtask.exception.ActionFailureException; + +// 导入JSON处理相关类 +import org.json.JSONException; // JSON异常 +import org.json.JSONObject; // JSON对象 + +// 导入Java工具类 +import java.util.Objects; // 对象工具类 + +/** + * SqlData类 - 处理笔记数据的SQLite数据库操作 + * 功能: + * 1. 封装笔记数据的增删改查操作 + * 2. 管理本地数据与JSON格式的相互转换 + * 3. 处理数据版本控制 + */ +public class SqlData { + // 日志标签 + private static final String TAG = SqlData.class.getSimpleName(); + + // 无效ID常量 + private static final int INVALID_ID = -99999; + + // 数据查询的列投影(指定需要查询的列) + public static final String[] PROJECTION_DATA = new String[] { + DataColumns.ID, // 数据ID + DataColumns.MIME_TYPE, // MIME类型 + DataColumns.CONTENT, // 内容 + DataColumns.DATA1, // 扩展数据1 + DataColumns.DATA3 // 扩展数据3 + }; + + // 列索引常量 + public static final int DATA_ID_COLUMN = 0; // ID列索引 + public static final int DATA_MIME_TYPE_COLUMN = 1; // MIME类型列索引 + public static final int DATA_CONTENT_COLUMN = 2; // 内容列索引 + public static final int DATA_CONTENT_DATA_1_COLUMN = 3; // 扩展数据1列索引 + public static final int DATA_CONTENT_DATA_3_COLUMN = 4; // 扩展数据3列索引 + + // 内容解析器实例 + private final ContentResolver mContentResolver; + + // 是否为新创建数据的标志 + private boolean mIsCreate; + + // 数据字段 + private long mDataId; // 数据ID + private String mDataMimeType; // MIME类型 + private String mDataContent; // 内容 + private long mDataContentData1; // 扩展数据1(长整型) + private String mDataContentData3; // 扩展数据3(字符串) + + // 存储有变动的数据值(用于增量更新) + private final ContentValues mDiffDataValues; + + /** + * 构造函数(用于创建新数据) + * @param context 应用上下文 + */ + public SqlData(Context context) { + mContentResolver = context.getContentResolver(); + mIsCreate = true; // 标记为新创建 + mDataId = INVALID_ID; // 初始化无效ID + mDataMimeType = DataConstants.NOTE; // 默认MIME类型 + mDataContent = ""; // 空内容 + mDataContentData1 = 0; // 默认扩展数据1 + mDataContentData3 = ""; // 空扩展数据3 + mDiffDataValues = new ContentValues(); // 初始化变更集合 + } + + /** + * 构造函数(基于Cursor加载已有数据) + * @param context 应用上下文 + * @param c 数据库Cursor对象 + */ + public SqlData(Context context, Cursor c) { + mContentResolver = context.getContentResolver(); + mIsCreate = false; // 标记为已存在数据 + loadFromCursor(c); // 从Cursor加载数据 + mDiffDataValues = new ContentValues(); // 初始化变更集合 + } + + /** + * 从Cursor加载数据 + * @param c 数据库Cursor对象 + */ + private void loadFromCursor(Cursor c) { + mDataId = c.getLong(DATA_ID_COLUMN); + mDataMimeType = c.getString(DATA_MIME_TYPE_COLUMN); + mDataContent = c.getString(DATA_CONTENT_COLUMN); + mDataContentData1 = c.getLong(DATA_CONTENT_DATA_1_COLUMN); + mDataContentData3 = c.getString(DATA_CONTENT_DATA_3_COLUMN); + } + + /** + * 设置内容(从JSON对象) + * @param js 包含数据的JSON对象 + * @throws JSONException JSON解析异常 + */ + public void setContent(JSONObject js) throws JSONException { + // 处理数据ID + long dataId = js.has(DataColumns.ID) ? js.getLong(DataColumns.ID) : INVALID_ID; + if (mIsCreate || mDataId != dataId) { + mDiffDataValues.put(DataColumns.ID, dataId); + } + mDataId = dataId; + + // 处理MIME类型 + String dataMimeType = js.has(DataColumns.MIME_TYPE) ? js.getString(DataColumns.MIME_TYPE) + : DataConstants.NOTE; + if (mIsCreate || !mDataMimeType.equals(dataMimeType)) { + mDiffDataValues.put(DataColumns.MIME_TYPE, dataMimeType); + } + mDataMimeType = dataMimeType; + + // 处理内容 + String dataContent = js.has(DataColumns.CONTENT) ? js.getString(DataColumns.CONTENT) : ""; + if (mIsCreate || !mDataContent.equals(dataContent)) { + mDiffDataValues.put(DataColumns.CONTENT, dataContent); + } + mDataContent = dataContent; + + // 处理扩展数据1 + long dataContentData1 = js.has(DataColumns.DATA1) ? js.getLong(DataColumns.DATA1) : 0; + if (mIsCreate || mDataContentData1 != dataContentData1) { + mDiffDataValues.put(DataColumns.DATA1, dataContentData1); + } + mDataContentData1 = dataContentData1; + + // 处理扩展数据3 + String dataContentData3 = js.has(DataColumns.DATA3) ? js.getString(DataColumns.DATA3) : ""; + if (mIsCreate || !mDataContentData3.equals(dataContentData3)) { + mDiffDataValues.put(DataColumns.DATA3, dataContentData3); + } + mDataContentData3 = dataContentData3; + } + + /** + * 获取内容(转换为JSON对象) + * @return 包含数据的JSON对象 + * @throws JSONException JSON构造异常 + */ + public JSONObject getContent() throws JSONException { + if (mIsCreate) { + Log.e(TAG, "it seems that we haven't created this in database yet"); + return null; + } + // 构建JSON对象 + JSONObject js = new JSONObject(); + js.put(DataColumns.ID, mDataId); + js.put(DataColumns.MIME_TYPE, mDataMimeType); + js.put(DataColumns.CONTENT, mDataContent); + js.put(DataColumns.DATA1, mDataContentData1); + js.put(DataColumns.DATA3, mDataContentData3); + return js; + } + + /** + * 提交数据到数据库 + * @param noteId 关联的笔记ID + * @param validateVersion 是否验证版本 + * @param version 版本号(当validateVersion为true时使用) + */ + @RequiresApi(api = Build.VERSION_CODES.R) + public void commit(long noteId, boolean validateVersion, long version) { + // 处理新建数据的情况 + if (mIsCreate) { + // 如果ID无效且有ID变更,移除无效ID + if (mDataId == INVALID_ID && mDiffDataValues.containsKey(DataColumns.ID)) { + mDiffDataValues.remove(DataColumns.ID); + } + + // 设置关联笔记ID + mDiffDataValues.put(DataColumns.NOTE_ID, noteId); + // 插入新数据 + Uri uri = mContentResolver.insert(Notes.CONTENT_DATA_URI, mDiffDataValues); + try { + // 从返回的URI中解析出新数据的ID + mDataId = Long.parseLong(Objects.requireNonNull(uri).getPathSegments().get(1)); + } catch (NumberFormatException e) { + Log.e(TAG, "Get note id error :" + e); + throw new ActionFailureException("create note failed"); + } + } else { + // 处理更新已有数据的情况 + if (!mDiffDataValues.isEmpty()) { + int result; + 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, + " ? 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; + } + /** + * 获取数据ID + * @return 数据ID + */ + public long getId() { + return mDataId; + } +} diff --git a/src/mi_note/app/src/main/java/net/micode/notes/gtask/data/SqlNote.java b/src/mi_note/app/src/main/java/net/micode/notes/gtask/data/SqlNote.java new file mode 100644 index 0000000..75751f6 --- /dev/null +++ b/src/mi_note/app/src/main/java/net/micode/notes/gtask/data/SqlNote.java @@ -0,0 +1,628 @@ +/* + * 版权所有 (c) 2010-2011,MiCode 开源社区 (www.micode.net) + * 根据 Apache 许可证 2.0 版本("许可证")授权; + * 除非符合许可证的规定,否则不得使用本文件。 + * 您可以从以下网址获取许可证副本: + * http://www.apache.org/licenses/LICENSE-2.0 + * 除非适用法律要求或书面同意,本软件按"原样"分发, + * 没有任何明示或暗示的保证或条件。 + * 详见许可证中规定的权限和限制。 + * (注:这是一份标准的Apache许可证2.0版本的开源声明) + */ + +// 定义当前类所在的包路径 +package net.micode.notes.gtask.data; +// Android Widget相关类 +import android.appwidget.AppWidgetManager; // 管理应用小部件 +// Android内容提供者相关类 +import android.content.ContentResolver; // 内容解析器,用于访问ContentProvider +import android.content.ContentValues; // 键值对存储,用于数据库操作 +import android.content.Context; // 应用上下文 +// Android数据库相关类 +import android.database.Cursor; // 数据库查询结果集 +// Android网络相关类 +import android.net.Uri; // 通用资源标识符 +// Android系统相关类 +import android.os.Build; // 获取Android版本信息 +import android.util.Log; // 日志工具 +// AndroidX注解支持 +import androidx.annotation.RequiresApi; // 版本要求注解 +// 笔记应用数据相关类 +import net.micode.notes.data.Notes; // 笔记数据常量 +import net.micode.notes.data.Notes.DataColumns; // 数据表列名常量 +import net.micode.notes.data.Notes.NoteColumns; // 笔记表列名常量 +// 异常处理类 +import net.micode.notes.gtask.exception.ActionFailureException; // 同步操作失败异常 +// 工具类 +import net.micode.notes.tool.GTaskStringUtils; // Google Task字符串工具 +import net.micode.notes.tool.ResourceParser; // 资源解析工具 +// JSON处理相关类 +import org.json.JSONArray; // JSON数组处理 +import org.json.JSONException; // JSON异常 +import org.json.JSONObject; // JSON对象处理 +// Java集合类 +import java.util.ArrayList; // 动态数组 +import java.util.Objects; // 对象工具类 + +// 定义SqlNote类,用于管理笔记数据的数据库操作 +public class SqlNote { + // 日志标签 + private static final String TAG = SqlNote.class.getSimpleName(); + + // 无效ID常量 + private static final int INVALID_ID = -99999; + + // 笔记数据查询投影列(指定需要查询的字段) + public static final String[] PROJECTION_NOTE = new String[] { + NoteColumns.ID, // 笔记ID + NoteColumns.ALERTED_DATE, // 提醒日期 + NoteColumns.BG_COLOR_ID, // 背景颜色ID + NoteColumns.CREATED_DATE, // 创建日期 + NoteColumns.HAS_ATTACHMENT, // 是否有附件 + NoteColumns.MODIFIED_DATE, // 修改日期 + NoteColumns.NOTES_COUNT, // 子笔记数量 + NoteColumns.PARENT_ID, // 父笔记ID + NoteColumns.SNIPPET, // 内容摘要 + NoteColumns.TYPE, // 笔记类型 + NoteColumns.WIDGET_ID, // 小部件ID + NoteColumns.WIDGET_TYPE, // 小部件类型 + NoteColumns.SYNC_ID, // 同步ID + NoteColumns.LOCAL_MODIFIED, // 本地修改标记 + NoteColumns.ORIGIN_PARENT_ID, // 原始父笔记ID + NoteColumns.GTASK_ID, // Google任务ID + NoteColumns.VERSION // 数据版本 + }; + + // 列索引常量定义 + public static final int ID_COLUMN = 0; // ID列索引 + public static final int ALERTED_DATE_COLUMN = 1; // 提醒日期列索引 + public static final int BG_COLOR_ID_COLUMN = 2; // 背景颜色列索引 + public static final int CREATED_DATE_COLUMN = 3; // 创建日期列索引 + public static final int HAS_ATTACHMENT_COLUMN = 4; // 附件标记列索引 + public static final int MODIFIED_DATE_COLUMN = 5; // 修改日期列索引 + public static final int NOTES_COUNT_COLUMN = 6; // 子笔记数列索引 + public static final int PARENT_ID_COLUMN = 7; // 父笔记ID列索引 + public static final int SNIPPET_COLUMN = 8; // 摘要列索引 + public static final int TYPE_COLUMN = 9; // 类型列索引 + public static final int WIDGET_ID_COLUMN = 10; // 小部件ID列索引 + public static final int WIDGET_TYPE_COLUMN = 11; // 小部件类型列索引 + public static final int SYNC_ID_COLUMN = 12; // 同步ID列索引 + public static final int LOCAL_MODIFIED_COLUMN = 13; // 本地修改列索引 + public static final int ORIGIN_PARENT_ID_COLUMN = 14; // 原始父笔记ID列索引 + public static final int GTASK_ID_COLUMN = 15; // Google任务ID列索引 + public static final int VERSION_COLUMN = 16; // 版本列索引 + + // 上下文对象 + private final Context mContext; + // 内容解析器 + private final ContentResolver mContentResolver; + // 是否为新建笔记标记 + private boolean mIsCreate; + + // 笔记字段 + private long mId; // 笔记ID + private long mAlertDate; // 提醒时间 + private int mBgColorId; // 背景颜色ID + private long mCreatedDate; // 创建时间 + private int mHasAttachment; // 是否有附件(0/1) + 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; // 数据版本 + + // 存储变动的笔记值 + private final ContentValues mDiffNoteValues; + // 笔记关联的数据列表 + private final ArrayList`))jlv3Kr#7T8rx>Okb$?!JU{~mfU(lm15P=nCu-=n