From f9b3ff0b87d1efd03be691aff06b556c947e0127 Mon Sep 17 00:00:00 2001 From: ANAN-dot <379951462@qq.com> Date: Sat, 25 Apr 2026 16:20:21 +0800 Subject: [PATCH 1/6] data --- .../micode/notes/data/room/NotesRoomDao.kt | 128 +++++++++++++++++- 1 file changed, 123 insertions(+), 5 deletions(-) diff --git a/app/src/main/kotlin/net/micode/notes/data/room/NotesRoomDao.kt b/app/src/main/kotlin/net/micode/notes/data/room/NotesRoomDao.kt index 9acaa13..e581180 100644 --- a/app/src/main/kotlin/net/micode/notes/data/room/NotesRoomDao.kt +++ b/app/src/main/kotlin/net/micode/notes/data/room/NotesRoomDao.kt @@ -1,12 +1,34 @@ +// 声明包路径,属于 data 模块下的 Room 数据库相关类 package net.micode.notes.data.room -import androidx.room.Dao -import androidx.room.Insert -import androidx.room.Query -import androidx.room.Update +// 导入 Room 注解和基础类 +import androidx.room.Dao // 标识该接口为数据访问对象 +import androidx.room.Insert // 标识插入操作 +import androidx.room.Query // 自定义 SQL 查询 +import androidx.room.Update // 标识更新操作 +/** + * 小米便签 Room 数据库的 DAO 接口。 + * 封装了对 note(便签/文件夹)和 data(便签内容)表的所有原子操作。 + * 所有公开方法均为 suspend 函数,适合在协程中调用,避免阻塞主线程。 + */ @Dao interface NotesRoomDao { + + /** + * 获取主列表(根目录)显示的便签/文件夹列表。 + * 查询逻辑: + * - 属于当前文件夹(folderId)且不是系统类型的笔记; + * - 或者当前文件夹本身是通话记录文件夹时,也显示其包含子笔记的通话记录条目; + * - 计算每个笔记的子笔记数量(非文件夹类型的 notes_count 置为 0); + * - 排序列:类型降序(文件夹在前),再按修改时间降序。 + * + * @param folderId 当前文件夹(父级)ID + * @param systemType 系统笔记类型(如通话记录、系统文件夹),在 WHERE 中会排除 + * @param noteType 普通笔记的类型值,用于计算子笔记数时判断 + * @param callRecordFolderId 通话记录文件夹的特殊 ID(允许展示有子笔记的通话记录) + * @return 列表行数据的集合,字段映射到 NoteListRow + */ @Query( """ SELECT noteItem._id, noteItem.alert_date, noteItem.bg_color_id, noteItem.has_attachment, @@ -31,6 +53,11 @@ interface NotesRoomDao { callRecordFolderId: Long ): List + /** + * 在主列表中搜索(根据摘要文本过滤)。 + * SQL 与 getRootListItems 基本相同,仅增加了 snippet LIKE 条件。 + * 参数多了一个 searchText,使用 % 通配符需调用方构造(如 "%keyword%")。 + */ @Query( """ SELECT noteItem._id, noteItem.alert_date, noteItem.bg_color_id, noteItem.has_attachment, @@ -57,6 +84,11 @@ interface NotesRoomDao { searchText: String ): List + /** + * 获取某个文件夹内的直接子项目列表(不区分系统类型)。 + * 简单条件:parent_id = folderId。 + * 同样计算 notes_count。 + */ @Query( """ SELECT noteItem._id, noteItem.alert_date, noteItem.bg_color_id, noteItem.has_attachment, @@ -74,6 +106,10 @@ interface NotesRoomDao { ) suspend fun getFolderListItems(folderId: Long, noteType: Int): List + /** + * 在特定文件夹内按摘要文本搜索。 + * 条件:parent_id = folderId 且 snippet 包含关键字。 + */ @Query( """ SELECT noteItem._id, noteItem.alert_date, noteItem.bg_color_id, noteItem.has_attachment, @@ -91,6 +127,11 @@ interface NotesRoomDao { ) suspend fun searchFolderListItems(folderId: Long, noteType: Int, searchText: String): List + /** + * 检查是否有可见同名文件夹。 + * 用于新建文件夹时避免重名(回收站里的同名文件夹不冲突)。 + * EXISTS 子查询,返回布尔值。 + */ @Query( """ SELECT EXISTS( @@ -108,15 +149,32 @@ interface NotesRoomDao { trashFolderId: Long ): Boolean + /** + * 插入一条便签/文件夹记录。 + * 返回新插入行的 row ID。 + */ @Insert suspend fun insertNote(note: RoomNoteEntity): Long + /** + * 更新一条便签/文件夹记录(通过主键 _id 匹配)。 + * 返回被更新的行数。 + */ @Update suspend fun updateNote(note: RoomNoteEntity): Int + /** + * 根据 ID 获取单条便签/文件夹记录。 + * 如果不存在则返回 null。 + */ @Query("SELECT * FROM note WHERE _id = :noteId LIMIT 1") suspend fun getNoteById(noteId: Long): RoomNoteEntity? + /** + * 重命名文件夹。 + * 同时修改 snippet(名称)和 type(保持为文件夹类型),并标记本地已修改,便于同步。 + * 返回更新行数。 + */ @Query( """ UPDATE note @@ -128,6 +186,12 @@ interface NotesRoomDao { ) suspend fun renameFolder(folderId: Long, name: String, folderType: Int): Int + /** + * 递归查询所有子孙笔记/文件夹的 ID。 + * 使用 WITH RECURSIVE 公用表表达式,从给定的根节点 ID 列表开始, + * 逐层查找 child.parent_id 等于上一级 _id 的子节点,直到没有更多后代。 + * 通常用于删除文件夹时批量级联删除其下所有子内容。 + */ @Query( """ WITH RECURSIVE descendants(_id) AS ( @@ -144,12 +208,24 @@ interface NotesRoomDao { ) suspend fun getDescendantNoteIds(rootIds: List): List + /** + * 根据 note ID 列表批量删除 data 表中的相关数据(如文本内容、附件引用等)。 + * 返回删除行数。 + */ @Query("DELETE FROM data WHERE note_id IN (:noteIds)") suspend fun deleteDataByNoteIds(noteIds: List): Int + /** + * 根据 ID 列表批量删除 note 表中的行。 + * 返回删除行数。 + */ @Query("DELETE FROM note WHERE _id IN (:noteIds)") suspend fun deleteNotesByIds(noteIds: List): Int + /** + * 获取指定文件夹下的所有小部件引用信息。 + * 返回 widget_id 和 widget_type,供桌面小部件更新使用。 + */ @Query( """ SELECT widget_id, widget_type @@ -159,6 +235,10 @@ interface NotesRoomDao { ) suspend fun getFolderWidgets(folderId: Long): List + /** + * 判断某笔记是否可见(即存在且不在回收站中)。 + * 用于操作前验证笔记状态。 + */ @Query( """ SELECT EXISTS( @@ -172,9 +252,16 @@ interface NotesRoomDao { ) suspend fun isVisibleNote(noteId: Long, type: Int, trashFolderId: Long): Boolean + /** + * 获取某笔记的文本摘要(snippet),通常用于快速显示。 + */ @Query("SELECT snippet FROM note WHERE _id = :noteId LIMIT 1") suspend fun getSnippetById(noteId: Long): String? + /** + * 获取未来将要提醒的笔记信息(提醒时间大于给定当前时间的)。 + * 用于设置或更新系统闹钟提醒。 + */ @Query( """ SELECT _id AS noteId, alert_date @@ -185,6 +272,10 @@ interface NotesRoomDao { ) suspend fun getFutureAlertNotes(currentDate: Long, noteType: Int): List + /** + * 根据桌面小部件 ID 获取其绑定的便签摘要信息(排除回收站内的)。 + * 用于小部件上展示对应便签内容。 + */ @Query( """ SELECT _id, bg_color_id, snippet @@ -195,6 +286,11 @@ interface NotesRoomDao { ) suspend fun getNotesByWidgetId(appWidgetId: Int, trashFolderId: Long): List + /** + * 将指定小部件 ID 的绑定清除(重置为无效 ID)。 + * 用于用户移除桌面小部件后清除数据库关联。 + * 返回更新行数。 + */ @Query( """ UPDATE note @@ -204,18 +300,40 @@ interface NotesRoomDao { ) suspend fun clearWidgetBinding(appWidgetId: Int, invalidWidgetId: Int): Int + /** + * 获取某个便签的所有内容数据(data 表)。 + * 一条便签可能有多条 data,例如文本、附件等,通过 mime_type 区分。 + */ @Query("SELECT * FROM data WHERE note_id = :noteId") suspend fun getDataByNoteId(noteId: Long): List + /** + * 根据便签 ID 和 MIME 类型获取单条内容数据。 + * 例如,获取纯文本内容时可指定 mimeType = "text/plain"。 + * 如果不存在则返回 null。 + */ @Query("SELECT * FROM data WHERE note_id = :noteId AND mime_type = :mimeType LIMIT 1") suspend fun getDataByNoteIdAndMimeType(noteId: Long, mimeType: String): RoomDataEntity? + /** + * 插入一条内容数据。 + * 返回新行的 row ID。 + */ @Insert suspend fun insertData(data: RoomDataEntity): Long + /** + * 更新一条内容数据(基于主键匹配)。 + * 返回更新行数。 + */ @Update suspend fun updateData(data: RoomDataEntity): Int + /** + * 通过通话日期查找对应的通话记录便签。 + * data3 字段存储电话号码,data1 存储呼叫日期。 + * 返回一份简化的查询结果,包含 noteId 和 phoneNumber。 + */ @Query( """ SELECT note_id AS noteId, data3 AS phoneNumber @@ -226,4 +344,4 @@ interface NotesRoomDao { ) suspend fun findCallNotesByDate(callDate: Long, mimeType: String): List -} +} \ No newline at end of file From 3adcfc244f49687e371eb4bb019836f0a3c72dac Mon Sep 17 00:00:00 2001 From: ANAN-dot <379951462@qq.com> Date: Sat, 25 Apr 2026 16:45:29 +0800 Subject: [PATCH 2/6] data_room.2 --- .../notes/data/room/NotesRoomDatabase.kt | 121 +++++++++++++++++- 1 file changed, 114 insertions(+), 7 deletions(-) diff --git a/app/src/main/kotlin/net/micode/notes/data/room/NotesRoomDatabase.kt b/app/src/main/kotlin/net/micode/notes/data/room/NotesRoomDatabase.kt index 8210e8b..fb54937 100644 --- a/app/src/main/kotlin/net/micode/notes/data/room/NotesRoomDatabase.kt +++ b/app/src/main/kotlin/net/micode/notes/data/room/NotesRoomDatabase.kt @@ -1,23 +1,63 @@ package net.micode.notes.data.room import android.content.Context -import androidx.room.Database -import androidx.room.Room -import androidx.room.RoomDatabase -import androidx.room.migration.Migration -import androidx.sqlite.db.SupportSQLiteDatabase -import net.micode.notes.data.Notes +import androidx.room.Database // 声明 Room 数据库 +import androidx.room.Room // Room 数据库构建器 +import androidx.room.RoomDatabase // Room 数据库抽象基类 +import androidx.room.migration.Migration // 数据库迁移基类 +import androidx.sqlite.db.SupportSQLiteDatabase // 底层 SQLite 数据库对象,用于迁移和回调中执行 SQL +import net.micode.notes.data.Notes // 导入常量定义(如系统文件夹 ID、类型等) +/** + * 小米便签 Room 数据库定义。 + * 包含两张核心表:note(便签/文件夹)和 data(便签内容)。 + * 数据库版本当前为 4,支持从 1、2、3 到 4 的迁移。 + * 采用单例模式,全局共享一个数据库实例。 + * + * @Database 注解: + * entities: 数据库包含的实体类,Room 会据此创建表 + * version: 数据库版本号,用于迁移判断 + * exportSchema: 是否导出 schema 文件到项目(此处关闭) + */ @Database( entities = [RoomNoteEntity::class, RoomDataEntity::class], version = 4, exportSchema = false ) abstract class NotesRoomDatabase : RoomDatabase() { + + /** + * 抽象方法,由 Room 在编译时生成实现,返回 DAO 接口。 + * 调用方通过此方法获取数据库操作对象。 + */ abstract fun notesDao(): NotesRoomDao companion object { + // 数据库文件名 private const val DB_NAME = "note.db" + + /** + * 创建 note 表的 SQL 语句。 + * 如果表不存在则创建。 + * 各字段含义(与旧版本 ContentProvider 中对应): + * _id 主键 + * parent_id 父文件夹 ID,0 表示根目录 + * alert_date 提醒时间(毫秒时间戳) + * bg_color_id 背景颜色 ID + * created_date 创建时间(默认当前时间) + * has_attachment 是否有附件 + * modified_date 最后修改时间 + * notes_count 子笔记数量(冗余字段,加速显示) + * snippet 文本摘要(文件夹名或便签内容摘要) + * type 类型(便签/文件夹/系统文件夹等) + * widget_id 绑定的桌面小部件 ID + * widget_type 小部件类型 + * sync_id 同步 ID(用于云端同步) + * local_modified 本地是否有未同步的修改(0/1) + * origin_parent_id 原始父文件夹 ID(用于还原) + * gtask_id Google Task 任务 ID(旧版同步相关) + * version 数据版本号(用于同步冲突处理) + */ private const val CREATE_NOTE_TABLE_SQL = """ CREATE TABLE IF NOT EXISTS note ( _id INTEGER PRIMARY KEY, @@ -39,6 +79,20 @@ abstract class NotesRoomDatabase : RoomDatabase() { version INTEGER NOT NULL DEFAULT 0 ) """ + + /** + * 创建 data 表的 SQL 语句。 + * 该表存储便签的实际内容,如文本、联系人信息等。 + * 字段说明: + * _id 主键 + * mime_type 数据类型(如 text/plain, vnd.android.cursor.item/phone) + * note_id 关联的便签 ID + * created_date 创建时间 + * modified_date 修改时间 + * content 文本内容(主要用于拼接显示) + * data1~data5 通用扩展字段,根据 mime_type 存储不同含义的值 + * (如 data3 常存储电话号码) + */ private const val CREATE_DATA_TABLE_SQL = """ CREATE TABLE IF NOT EXISTS data ( _id INTEGER PRIMARY KEY, @@ -54,12 +108,27 @@ abstract class NotesRoomDatabase : RoomDatabase() { data5 TEXT NOT NULL DEFAULT '' ) """ + + /** + * 在 data 表的 note_id 列上创建索引。 + * 加速通过 note_id 查询内容数据的操作(非常高频)。 + */ private const val CREATE_DATA_NOTE_ID_INDEX_SQL = "CREATE INDEX IF NOT EXISTS note_id_index ON data(note_id)" + /** + * 单例实例,使用 @Volatile 保证多线程可见性。 + */ @Volatile private var instance: NotesRoomDatabase? = null + /** + * 获取数据库单例的入口。 + * 使用双重检查锁定(DCL)确保线程安全且仅在第一次调用时创建。 + * + * @param context 应用上下文(内部会转为 applicationContext 防止泄漏) + * @return NotesRoomDatabase 实例 + */ operator fun invoke(context: Context): NotesRoomDatabase { return instance ?: synchronized(this) { instance ?: buildDatabase(context.applicationContext).also { db -> @@ -68,6 +137,10 @@ abstract class NotesRoomDatabase : RoomDatabase() { } } + /** + * 构建 Room 数据库实例。 + * 添加了从版本 1、2、3 到 4 的迁移策略,以及打开数据库时的回调(用于插入系统文件夹)。 + */ private fun buildDatabase(context: Context): NotesRoomDatabase { return Room.databaseBuilder(context, NotesRoomDatabase::class.java, DB_NAME) .addMigrations(MIGRATION_1_4, MIGRATION_2_4, MIGRATION_3_4) @@ -75,6 +148,10 @@ abstract class NotesRoomDatabase : RoomDatabase() { .build() } + /** + * 数据库打开回调。 + * 每次打开数据库时都会执行,用于确保系统文件夹必须存在。 + */ private val SystemFoldersCallback = object : RoomDatabase.Callback() { override fun onOpen(db: SupportSQLiteDatabase) { super.onOpen(db) @@ -82,6 +159,11 @@ abstract class NotesRoomDatabase : RoomDatabase() { } } + /** + * 版本 1 → 4 的迁移策略。 + * 由于旧版本结构差异较大(可能没有部分字段),直接删除旧表并重新创建新表。 + * 数据会丢失,适合早期测试或数据结构大改的情况。 + */ private val MIGRATION_1_4 = object : Migration(1, 4) { override fun migrate(db: SupportSQLiteDatabase) { db.execSQL("DROP TABLE IF EXISTS note") @@ -91,15 +173,28 @@ abstract class NotesRoomDatabase : RoomDatabase() { } } + /** + * 版本 2 → 4 的迁移策略。 + * 版本 2 缺少 gtask_id 和 version 列,以及 note_id 索引。 + * 使用 ALTER TABLE 增加列,保留原有数据。 + */ private val MIGRATION_2_4 = object : Migration(2, 4) { override fun migrate(db: SupportSQLiteDatabase) { + // 增加 Google 任务同步 ID 列 db.execSQL("ALTER TABLE note ADD COLUMN gtask_id TEXT NOT NULL DEFAULT ''") + // 增加数据版本列 db.execSQL("ALTER TABLE note ADD COLUMN version INTEGER NOT NULL DEFAULT 0") + // 创建数据表索引 db.execSQL(CREATE_DATA_NOTE_ID_INDEX_SQL) + // 插入系统文件夹(如果尚未存在) insertSystemFolders(db) } } + /** + * 版本 3 → 4 的迁移策略。 + * 版本 3 仅缺少 version 列和索引,结构接近最新。 + */ private val MIGRATION_3_4 = object : Migration(3, 4) { override fun migrate(db: SupportSQLiteDatabase) { db.execSQL("ALTER TABLE note ADD COLUMN version INTEGER NOT NULL DEFAULT 0") @@ -108,12 +203,24 @@ abstract class NotesRoomDatabase : RoomDatabase() { } } + /** + * 创建数据库表结构。 + */ private fun createSchema(db: SupportSQLiteDatabase) { db.execSQL(CREATE_NOTE_TABLE_SQL) db.execSQL(CREATE_DATA_TABLE_SQL) db.execSQL(CREATE_DATA_NOTE_ID_INDEX_SQL) } + /** + * 插入系统必需的三个文件夹(如果它们尚不存在)。 + * 使用 INSERT OR IGNORE 避免重复插入。 + * 文件夹 ID 定义在 Notes 工具类中: + * ID_CALL_RECORD_FOLDER 通话记录文件夹 + * ID_ROOT_FOLDER 根目录(便签列表中“便签”项) + * ID_TRASH_FOLER 回收站 + * 三个文件夹的类型均为 TYPE_SYSTEM,即系统文件夹。 + */ private fun insertSystemFolders(db: SupportSQLiteDatabase) { db.execSQL( "INSERT OR IGNORE INTO note (_id, type) VALUES " + @@ -123,4 +230,4 @@ abstract class NotesRoomDatabase : RoomDatabase() { ) } } -} +} \ No newline at end of file From 45701a14022669aca09d1c2a9fb9ea1583d2c65f Mon Sep 17 00:00:00 2001 From: ANAN-dot <379951462@qq.com> Date: Sat, 25 Apr 2026 17:15:07 +0800 Subject: [PATCH 3/6] data.romm.3 --- .../notes/data/room/NotesRoomEntities.kt | 121 +++++++++++------- 1 file changed, 73 insertions(+), 48 deletions(-) diff --git a/app/src/main/kotlin/net/micode/notes/data/room/NotesRoomEntities.kt b/app/src/main/kotlin/net/micode/notes/data/room/NotesRoomEntities.kt index 887edac..c7391d6 100644 --- a/app/src/main/kotlin/net/micode/notes/data/room/NotesRoomEntities.kt +++ b/app/src/main/kotlin/net/micode/notes/data/room/NotesRoomEntities.kt @@ -1,76 +1,101 @@ -package net.micode.notes.data.room - -import androidx.room.ColumnInfo -import androidx.room.Entity -import androidx.room.Index -import androidx.room.PrimaryKey -import net.micode.notes.data.Notes - -@Entity(tableName = "note") -data class RoomNoteEntity( - @PrimaryKey - @ColumnInfo(name = Notes.NoteColumns.ID) - val id: Long? = null, - @ColumnInfo(name = Notes.NoteColumns.PARENT_ID, defaultValue = "0") - val parentId: Long = 0, - @ColumnInfo(name = Notes.NoteColumns.ALERTED_DATE, defaultValue = "0") - val alertedDate: Long = 0, - @ColumnInfo(name = Notes.NoteColumns.BG_COLOR_ID, defaultValue = "0") - val bgColorId: Int = 0, - @ColumnInfo(name = Notes.NoteColumns.CREATED_DATE, defaultValue = "(strftime('%s','now') * 1000)") - val createdDate: Long = 0, - @ColumnInfo(name = Notes.NoteColumns.HAS_ATTACHMENT, defaultValue = "0") - val hasAttachment: Int = 0, - @ColumnInfo(name = Notes.NoteColumns.MODIFIED_DATE, defaultValue = "(strftime('%s','now') * 1000)") - val modifiedDate: Long = 0, - @ColumnInfo(name = Notes.NoteColumns.NOTES_COUNT, defaultValue = "0") - val notesCount: Int = 0, - @ColumnInfo(name = Notes.NoteColumns.SNIPPET, defaultValue = "''") - val snippet: String = "", - @ColumnInfo(name = Notes.NoteColumns.TYPE, defaultValue = "0") - val type: Int = 0, - @ColumnInfo(name = Notes.NoteColumns.WIDGET_ID, defaultValue = "0") - val widgetId: Int = 0, - @ColumnInfo(name = Notes.NoteColumns.WIDGET_TYPE, defaultValue = "-1") - val widgetType: Int = -1, - @ColumnInfo(name = Notes.NoteColumns.SYNC_ID, defaultValue = "0") - val syncId: Long = 0, - @ColumnInfo(name = Notes.NoteColumns.LOCAL_MODIFIED, defaultValue = "0") - val localModified: Int = 0, - @ColumnInfo(name = Notes.NoteColumns.ORIGIN_PARENT_ID, defaultValue = "0") - val originParentId: Long = 0, - @ColumnInfo(name = Notes.NoteColumns.GTASK_ID, defaultValue = "''") - val gtaskId: String = "", - @ColumnInfo(name = Notes.NoteColumns.VERSION, defaultValue = "0") - val version: Long = 0 -) - +/** + * 便签内容数据的 Room 实体,映射 data 表。 + * + * 一条便签可以对应多条 data 记录(例如一条便签同时包含文本和一张图片的引用), + * 通过 mime_type 区分数据类型,data1~data5 为通用扩展字段。 + * + * @Entity 注解: + * tableName = "data" 表名 + * indices 为 note_id 列创建索引,优化按便签查询内容的性能 + */ @Entity( tableName = "data", indices = [Index(value = [Notes.DataColumns.NOTE_ID], name = "note_id_index")] ) data class RoomDataEntity( + // ==================== 主键 ==================== + /** + * 主键 ID,自动生成。 + */ @PrimaryKey @ColumnInfo(name = Notes.DataColumns.ID) val id: Long? = null, + + // ==================== 关联信息 ==================== + /** + * 内容类型(MIME 类型)。 + * 例如: + * "text/plain" 纯文本 + * "vnd.android.cursor.item/phone" 电话号码(通话记录) + * 该字段告知系统如何解析 data1~data5 和 content 字段。 + */ @ColumnInfo(name = Notes.DataColumns.MIME_TYPE) val mimeType: String, + + /** + * 关联的便签 ID(note 表的主键)。 + * 默认值 0。 + */ @ColumnInfo(name = Notes.DataColumns.NOTE_ID, defaultValue = "0") val noteId: Long = 0, + + // ==================== 时间信息 ==================== + /** + * 创建日期(毫秒时间戳),默认当前时间。 + */ @ColumnInfo(name = Notes.DataColumns.CREATED_DATE, defaultValue = "(strftime('%s','now') * 1000)") val createdDate: Long = 0, + + /** + * 最后修改日期(毫秒时间戳),默认当前时间。 + */ @ColumnInfo(name = Notes.DataColumns.MODIFIED_DATE, defaultValue = "(strftime('%s','now') * 1000)") val modifiedDate: Long = 0, + + // ==================== 内容数据 ==================== + /** + * 文本内容。 + * 对于纯文本便签,这里存储完整文本;对于其他类型,可能存储辅助说明文本。 + * 默认空字符串。 + */ @ColumnInfo(name = Notes.DataColumns.CONTENT, defaultValue = "''") val content: String = "", + + /** + * 通用数据字段 1。 + * 用途取决于 mime_type,例如: + * - 通话记录:存储通话日期 + * - 日历事件:存储事件开始时间 + * 可为 null。 + */ @ColumnInfo(name = Notes.DataColumns.DATA1) val data1: Long? = null, + + /** + * 通用数据字段 2。 + * 用途同上,根据 mime_type 灵活使用,可为 null。 + */ @ColumnInfo(name = Notes.DataColumns.DATA2) val data2: Long? = null, + + /** + * 通用数据字段 3(文本)。 + * 例如通话记录中存储电话号码。 + */ @ColumnInfo(name = Notes.DataColumns.DATA3, defaultValue = "''") val data3: String = "", + + /** + * 通用数据字段 4(文本)。 + * 备用扩展字段。 + */ @ColumnInfo(name = Notes.DataColumns.DATA4, defaultValue = "''") val data4: String = "", + + /** + * 通用数据字段 5(文本)。 + * 备用扩展字段。 + */ @ColumnInfo(name = Notes.DataColumns.DATA5, defaultValue = "''") val data5: String = "" -) +) \ No newline at end of file From 5e10553a3c6e18e8cef19719d4dfa2a54e9d9e01 Mon Sep 17 00:00:00 2001 From: ANAN-dot <379951462@qq.com> Date: Sat, 25 Apr 2026 17:42:25 +0800 Subject: [PATCH 4/6] data_room.4 --- .../micode/notes/data/room/NotesRoomModels.kt | 70 ++++++++++++++++++- 1 file changed, 69 insertions(+), 1 deletion(-) diff --git a/app/src/main/kotlin/net/micode/notes/data/room/NotesRoomModels.kt b/app/src/main/kotlin/net/micode/notes/data/room/NotesRoomModels.kt index 222acde..74b6207 100644 --- a/app/src/main/kotlin/net/micode/notes/data/room/NotesRoomModels.kt +++ b/app/src/main/kotlin/net/micode/notes/data/room/NotesRoomModels.kt @@ -3,61 +3,129 @@ package net.micode.notes.data.room import androidx.room.ColumnInfo import net.micode.notes.data.Notes +/** + * 主列表/文件夹列表的单条数据行。 + * 对应 DAO 中 getRootListItems、searchRootListItems、getFolderListItems、searchFolderListItems 的返回结果。 + * 包含了前端列表展示所需的核心字段。 + */ data class NoteListRow( + // 便签/文件夹的 _id @ColumnInfo(name = Notes.NoteColumns.ID) val id: Long, + + // 提醒时间(毫秒时间戳),0 表示无提醒 @ColumnInfo(name = Notes.NoteColumns.ALERTED_DATE) val alertedDate: Long, + + // 背景颜色 ID,用于显示不同颜色区分 @ColumnInfo(name = Notes.NoteColumns.BG_COLOR_ID) val bgColorId: Int, + + // 是否有附件 (0/1),用于显示附件图标 @ColumnInfo(name = Notes.NoteColumns.HAS_ATTACHMENT) val hasAttachment: Int, + + // 最后修改时间(毫秒时间戳),用于排序和显示 @ColumnInfo(name = Notes.NoteColumns.MODIFIED_DATE) val modifiedDate: Long, + + // 子笔记数量(对于文件夹),对于便签固定为 0 @ColumnInfo(name = Notes.NoteColumns.NOTES_COUNT) val notesCount: Int, + + // 父文件夹 ID,0 表示根目录 @ColumnInfo(name = Notes.NoteColumns.PARENT_ID) val parentId: Long, + + // 文本摘要(便签内容前部或文件夹名) @ColumnInfo(name = Notes.NoteColumns.SNIPPET) val snippet: String, + + // 类型:0 普通便签,1 文件夹,系统类型等 @ColumnInfo(name = Notes.NoteColumns.TYPE) val type: Int, + + // 绑定的桌面小部件 ID,0 表示无 @ColumnInfo(name = Notes.NoteColumns.WIDGET_ID) val widgetId: Int, + + // 桌面小部件类型,-1 表示无 @ColumnInfo(name = Notes.NoteColumns.WIDGET_TYPE) val widgetType: Int ) +/** + * 桌面小部件绑定信息行。 + * 用于 getFolderWidgets 查询,返回某个文件夹下所有便签的小部件关联信息。 + */ data class WidgetRow( + // 小部件 ID @ColumnInfo(name = Notes.NoteColumns.WIDGET_ID) val widgetId: Int, + + // 小部件类型 @ColumnInfo(name = Notes.NoteColumns.WIDGET_TYPE) val widgetType: Int ) +/** + * 未来的提醒行数据。 + * 用于 getFutureAlertNotes 查询,返回需要设置下一次闹钟的便签。 + */ data class AlarmRow( + // 便签 ID(查询中使用 AS noteId 别名) @ColumnInfo(name = "noteId") val noteId: Long, + + // 提醒日期(毫秒时间戳) @ColumnInfo(name = Notes.NoteColumns.ALERTED_DATE) val alertedDate: Long ) +/** + * 桌面小部件上需要显示的便签简要信息。 + * 用于 getNotesByWidgetId 查询,根据小部件 ID 获取其绑定的便签摘要。 + */ data class WidgetNoteRow( + // 便签 ID @ColumnInfo(name = Notes.NoteColumns.ID) val id: Long, + + // 背景颜色 ID,用于小部件背景色 @ColumnInfo(name = Notes.NoteColumns.BG_COLOR_ID) val bgColorId: Int, + + // 文本摘要,显示在小部件上 @ColumnInfo(name = Notes.NoteColumns.SNIPPET) val snippet: String ) +/** + * 通话记录查找结果行。 + * 通过 findCallNotesByDate 查询,用于根据通话日期找到对应的便签及号码。 + */ data class CallNoteLookupRow( + // 关联的便签 ID (查询中 note_id AS noteId) val noteId: Long, + + // 电话号码 (查询中 data3 AS phoneNumber) val phoneNumber: String ) +/** + * 封装保存便签时所需的完整数据负载。 + * 通常在编辑界面保存时,由业务逻辑组装: + * - note:便签元数据(标题、类型、提醒等) + * - textData:文本内容数据,可能为 null(例如纯附件便签或没有文字) + * - callData:通话记录附加数据,可能为 null(非通话记录便签) + * + * 这样设计可以将多个实体的插入/更新操作在一次事务中完成,保证数据一致性。 + */ data class StoredNotePayload( + // 便签主实体,必填 val note: RoomNoteEntity, + // 文本类型的数据实体(mime_type = "text/plain"),可选 val textData: RoomDataEntity?, + // 通话记录类型的数据实体,可选 val callData: RoomDataEntity? -) +) \ No newline at end of file From 66a9817fb77573c256cf47f39e4ae401536cfbf8 Mon Sep 17 00:00:00 2001 From: ANAN-dot <379951462@qq.com> Date: Sat, 25 Apr 2026 19:11:28 +0800 Subject: [PATCH 5/6] data_room.5 --- .../kotlin/net/micode/notes/data/Notes.kt | 228 ++++++++---------- 1 file changed, 101 insertions(+), 127 deletions(-) diff --git a/app/src/main/kotlin/net/micode/notes/data/Notes.kt b/app/src/main/kotlin/net/micode/notes/data/Notes.kt index af76abc..ef0a585 100644 --- a/app/src/main/kotlin/net/micode/notes/data/Notes.kt +++ b/app/src/main/kotlin/net/micode/notes/data/Notes.kt @@ -15,240 +15,214 @@ */ package net.micode.notes.data +/** + * 小米便签核心常量定义类。 + * + * 集中管理所有与数据存储、类型识别、跨组件通信相关的常量, + * 包括数据库列名、便签/文件夹类型、系统文件夹 ID、Intent Extra 键等。 + * 设计为单例 object,外部可直接通过 `Notes.XXX` 访问。 + */ object Notes { + // ==================== 便签与文件夹类型 ==================== + + /** 普通便签类型 */ const val TYPE_NOTE: Int = 0 + + /** 文件夹类型 */ const val TYPE_FOLDER: Int = 1 + + /** 系统类型(通话记录文件夹、根文件夹、回收站等) */ const val TYPE_SYSTEM: Int = 2 + // ==================== 固定系统文件夹 ID ==================== /** - * Following IDs are system folders' identifiers - * [Notes.ID_ROOT_FOLDER] is default folder - * [Notes.ID_CALL_RECORD_FOLDER] is to store call records + * 系统文件夹标识。 + * [ID_ROOT_FOLDER] 根文件夹(默认顶层文件夹) + * [ID_CALL_RECORD_FOLDER] 通话记录文件夹 + * [ID_TRASH_FOLER] 回收站文件夹 */ - const val ID_ROOT_FOLDER: Int = 0 - val ID_CALL_RECORD_FOLDER: Int = -2 - val ID_TRASH_FOLER: Int = -3 + const val ID_ROOT_FOLDER: Int = 0 // 根文件夹,parent_id = 0 即表示在此文件夹下 + val ID_CALL_RECORD_FOLDER: Int = -2 // 通话记录专用文件夹,ID 为 -2,避免与正常 ID 冲突 + val ID_TRASH_FOLER: Int = -3 // 回收站文件夹,ID 为 -3 + // ==================== Intent 额外数据键名 ==================== + /** Intent 中携带提醒日期的键 */ const val INTENT_EXTRA_ALERT_DATE: String = "net.micode.notes.alert_date" + + /** Intent 中携带背景颜色 ID 的键 */ const val INTENT_EXTRA_BACKGROUND_ID: String = "net.micode.notes.background_color_id" + + /** Intent 中携带小部件 ID 的键 */ const val INTENT_EXTRA_WIDGET_ID: String = "net.micode.notes.widget_id" + + /** Intent 中携带小部件类型的键 */ const val INTENT_EXTRA_WIDGET_TYPE: String = "net.micode.notes.widget_type" + + /** Intent 中携带目标文件夹 ID 的键 */ const val INTENT_EXTRA_FOLDER_ID: String = "net.micode.notes.folder_id" + + /** Intent 中携带通话日期的键 */ const val INTENT_EXTRA_CALL_DATE: String = "net.micode.notes.call_date" + // ==================== 桌面小部件类型 ==================== + /** 无效小部件类型,表示未绑定或默认值 */ val TYPE_WIDGET_INVALIDE: Int = -1 + + /** 2x 规格小部件 */ const val TYPE_WIDGET_2X: Int = 0 + + /** 4x 规格小部件 */ const val TYPE_WIDGET_4X: Int = 1 + // ==================== 数据库 note 表列名定义 ==================== + /** + * 定义 note 表的所有列名。 + * 通过接口 + 伴生对象组织,方便被 TextNote、CallNote 等子对象复用, + * 同时供 ContentProvider 和 Room Entity 统一引用。 + */ interface NoteColumns { companion object { - /** - * The unique ID for a row - *

Type: INTEGER (long)

- */ + /** 主键 ID,类型 INTEGER (long) */ const val ID: String = "_id" - /** - * The parent's id for note or folder - *

Type: INTEGER (long)

- */ + /** 父文件夹 ID,类型 INTEGER (long) */ const val PARENT_ID: String = "parent_id" - /** - * Created data for note or folder - *

Type: INTEGER (long)

- */ + /** 创建日期(毫秒时间戳),类型 INTEGER (long) */ const val CREATED_DATE: String = "created_date" - /** - * Latest modified date - *

Type: INTEGER (long)

- */ + /** 最后修改日期(毫秒时间戳),类型 INTEGER (long) */ const val MODIFIED_DATE: String = "modified_date" - - /** - * Alert date - *

Type: INTEGER (long)

- */ + /** 提醒日期(毫秒时间戳),类型 INTEGER (long) */ const val ALERTED_DATE: String = "alert_date" - /** - * Folder's name or text content of note - *

Type: TEXT

- */ + /** 文本摘要(文件夹名或便签内容开头),类型 TEXT */ const val SNIPPET: String = "snippet" - /** - * Note's widget id - *

Type: INTEGER (long)

- */ + /** 桌面小部件 ID,类型 INTEGER (long) */ const val WIDGET_ID: String = "widget_id" - /** - * Note's widget type - *

Type: INTEGER (long)

- */ + /** 桌面小部件类型,类型 INTEGER (long) */ const val WIDGET_TYPE: String = "widget_type" - /** - * Note's background color's id - *

Type: INTEGER (long)

- */ + /** 背景颜色 ID,类型 INTEGER (long) */ const val BG_COLOR_ID: String = "bg_color_id" - /** - * For text note, it doesn't has attachment, for multi-media - * note, it has at least one attachment - *

Type: INTEGER

- */ + /** 是否有附件 (0/1),类型 INTEGER */ const val HAS_ATTACHMENT: String = "has_attachment" - /** - * Folder's count of notes - *

Type: INTEGER (long)

- */ + /** 文件夹内子笔记数量,类型 INTEGER (long) */ const val NOTES_COUNT: String = "notes_count" - /** - * The file type: folder or note - *

Type: INTEGER

- */ + /** 类型(便签/文件夹/系统),类型 INTEGER */ const val TYPE: String = "type" - /** - * The last sync id - *

Type: INTEGER (long)

- */ + /** 最近一次同步 ID,类型 INTEGER (long) */ const val SYNC_ID: String = "sync_id" - /** - * Sign to indicate local modified or not - *

Type: INTEGER

- */ + /** 本地修改标记 (0:未修改, 1:已修改),类型 INTEGER */ const val LOCAL_MODIFIED: String = "local_modified" - /** - * Original parent id before moving into temporary folder - *

Type : INTEGER

- */ + /** 移动到回收站前的原始父文件夹 ID,用于还原,类型 INTEGER */ const val ORIGIN_PARENT_ID: String = "origin_parent_id" - /** - * The gtask id - *

Type : TEXT

- */ + /** Google Tasks 任务 ID,类型 TEXT */ const val GTASK_ID: String = "gtask_id" - /** - * The version code - *

Type : INTEGER (long)

- */ + /** 数据版本号,用于同步冲突处理,类型 INTEGER (long) */ const val VERSION: String = "version" } } + // ==================== 数据库 data 表列名定义 ==================== + /** + * 定义 data 表的所有列名。 + * 结构同 NoteColumns,提供统一引用。 + */ interface DataColumns { companion object { - /** - * The unique ID for a row - *

Type: INTEGER (long)

- */ + /** 主键 ID */ const val ID: String = "_id" - /** - * The MIME type of the item represented by this row. - *

Type: Text

- */ + /** MIME 类型,标识该行数据的格式(如 text/plain, vnd.android.cursor.item/phone) */ const val MIME_TYPE: String = "mime_type" - /** - * The reference id to note that this data belongs to - *

Type: INTEGER (long)

- */ + /** 所属便签的 ID,类型 INTEGER (long) */ const val NOTE_ID: String = "note_id" - /** - * Created data for note or folder - *

Type: INTEGER (long)

- */ + /** 创建日期(毫秒时间戳) */ const val CREATED_DATE: String = "created_date" - /** - * Latest modified date - *

Type: INTEGER (long)

- */ + /** 最后修改日期(毫秒时间戳) */ const val MODIFIED_DATE: String = "modified_date" - /** - * Data's content - *

Type: TEXT

- */ + /** 文本内容(主要用于纯文本便签) */ const val CONTENT: String = "content" - /** - * Generic data column, the meaning is [.MIMETYPE] specific, used for - * integer data type - *

Type: INTEGER

+ * 通用整型数据列 1,用途由 MIME_TYPE 决定 + * (例如通话记录中为通话日期) */ const val DATA1: String = "data1" /** - * Generic data column, the meaning is [.MIMETYPE] specific, used for - * integer data type - *

Type: INTEGER

+ * 通用整型数据列 2,用途由 MIME_TYPE 决定 */ const val DATA2: String = "data2" /** - * Generic data column, the meaning is [.MIMETYPE] specific, used for - * TEXT data type - *

Type: TEXT

+ * 通用文本数据列 3,用途由 MIME_TYPE 决定 + * (例如通话记录中为电话号码) */ const val DATA3: String = "data3" - /** - * Generic data column, the meaning is [.MIMETYPE] specific, used for - * TEXT data type - *

Type: TEXT

- */ + /** 通用文本数据列 4 */ const val DATA4: String = "data4" - /** - * Generic data column, the meaning is [.MIMETYPE] specific, used for - * TEXT data type - *

Type: TEXT

- */ + /** 通用文本数据列 5 */ const val DATA5: String = "data5" } } + // ==================== 特定内容类型映射 ==================== + /** + * 纯文本便签的数据定义。 + * 继承 DataColumns,复用列名常量,并声明文本模式。 + */ object TextNote : DataColumns { /** - * Mode to indicate the text in check list mode or not - *

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

+ * 清单模式标识。 + * 数据位置:对应 data 表的 DATA1 列。 + * 值为 MODE_CHECK_LIST (1) 时表示清单模式,否则普通文本模式。 */ val MODE: String = DataColumns.Companion.DATA1 + /** 清单模式标识值 */ const val MODE_CHECK_LIST: Int = 1 + /** 文本便签对应的 MIME 类型 */ const val CONTENT_ITEM_TYPE: String = "vnd.android.cursor.item/text_note" } + /** + * 通话记录便签的数据定义。 + * 通过 DataColumns 的通用列存储具体业务数据。 + */ object CallNote : DataColumns { /** - * Call date for this record - *

Type: INTEGER (long)

+ * 通话日期。 + * 映射到 data 表的 DATA1 列,类型 INTEGER (long)。 */ val CALL_DATE: String = DataColumns.Companion.DATA1 /** - * Phone number for this record - *

Type: TEXT

+ * 电话号码。 + * 映射到 data 表的 DATA3 列,类型 TEXT。 */ val PHONE_NUMBER: String = DataColumns.Companion.DATA3 + /** 通话记录便签对应的 MIME 类型 */ const val CONTENT_ITEM_TYPE: String = "vnd.android.cursor.item/call_note" } -} +} \ No newline at end of file From 112f91a1ac80a71a37306025e622a6dfe9e393dd Mon Sep 17 00:00:00 2001 From: ANAN-dot <379951462@qq.com> Date: Sat, 25 Apr 2026 21:15:26 +0800 Subject: [PATCH 6/6] data_room.6 --- .../net/micode/notes/data/NotesRepository.kt | 188 ++++++++++++++++-- 1 file changed, 166 insertions(+), 22 deletions(-) diff --git a/app/src/main/kotlin/net/micode/notes/data/NotesRepository.kt b/app/src/main/kotlin/net/micode/notes/data/NotesRepository.kt index 6ece7ba..9b66394 100644 --- a/app/src/main/kotlin/net/micode/notes/data/NotesRepository.kt +++ b/app/src/main/kotlin/net/micode/notes/data/NotesRepository.kt @@ -14,36 +14,64 @@ import net.micode.notes.data.room.WidgetNoteRow import net.micode.notes.tool.NotesSortMode import net.micode.notes.ui.AppWidgetAttribute +/** + * 前端调用保存便签时的请求体。 + * 封装所有需要传递给仓库层的字段,避免参数过多。 + */ data class NoteSaveRequest( - val noteId: Long, - val folderId: Long, - val content: String, - val checkListMode: Int, - val alertDate: Long, - val bgColorId: Int, - val widgetId: Int, - val widgetType: Int, - val modifiedDate: Long, - val callDate: Long?, - val phoneNumber: String? + val noteId: Long, // 现有便签 ID,新建时为 0 或负数 + val folderId: Long, // 目标文件夹 ID + val content: String, // 便签文本内容(片段,用于 snippet) + val checkListMode: Int, // 清单模式:0 正常,1 清单 + val alertDate: Long, // 提醒时间(毫秒) + val bgColorId: Int, // 背景颜色 ID + val widgetId: Int, // 绑定小部件 ID(0 为无) + val widgetType: Int, // 小部件类型 + val modifiedDate: Long, // 用户指定的修改时间,0 则使用当前时间 + val callDate: Long?, // 通话日期(仅通话记录便签) + val phoneNumber: String? // 电话号码(仅通话记录便签) ) +/** + * 保存便签后返回的结果。 + */ data class NoteSaveResult( - val noteId: Long, - val modifiedDate: Long + val noteId: Long, // 最终保存后的便签 ID + val modifiedDate: Long // 实际写入的修改时间戳 ) +/** + * 便签数据仓库。 + * + * 负责协调 Room 数据库的所有操作,向上层提供业务级 API。 + * 单例模式,保证全局仅有一个数据库实例和仓库实例。 + */ class NotesRepository private constructor(context: Context) { + // 获取 Room 数据库实例 private val database = NotesRoomDatabase(context.applicationContext) + // 获取 DAO 接口实例 private val notesDao = database.notesDao() + /** + * 加载指定文件夹下的便签/文件夹列表。 + * 支持搜索过滤和多种排序方式。 + * + * @param folderId 文件夹 ID,根文件夹使用 ID_ROOT_FOLDER(0) + * @param searchText 搜索关键词,空字符串表示不过滤 + * @param sortMode 排序模式(按修改时间降序/升序、按标题升序) + * @return 列表行数据 + */ suspend fun loadListItems( folderId: Long, searchText: String, sortMode: NotesSortMode ): List { + // 去除搜索文本前后空格 val trimmedSearchText = searchText.trim() + + // 根据是否根文件夹以及是否搜索选择不同的 DAO 方法 val rows = if (folderId == Notes.ID_ROOT_FOLDER.toLong()) { + // 根文件夹需要特殊处理:排除系统文件夹,但显示通话记录文件夹(若有子记录) if (trimmedSearchText.isEmpty()) { notesDao.getRootListItems( folderId = folderId, @@ -52,6 +80,7 @@ class NotesRepository private constructor(context: Context) { callRecordFolderId = Notes.ID_CALL_RECORD_FOLDER.toLong() ) } else { + // 带搜索的根列表,搜索关键字两边加 % 实现模糊匹配 notesDao.searchRootListItems( folderId = folderId, systemType = Notes.TYPE_SYSTEM, @@ -61,6 +90,7 @@ class NotesRepository private constructor(context: Context) { ) } } else { + // 普通文件夹直接查询其子项 if (trimmedSearchText.isEmpty()) { notesDao.getFolderListItems(folderId, Notes.TYPE_NOTE) } else { @@ -71,9 +101,15 @@ class NotesRepository private constructor(context: Context) { ) } } + + // 返回排序后的结果(客户端排序,不依赖 SQL 动态排序) return rows.sortedWith(noteListComparator(sortMode)) } + /** + * 检查是否存在可见的同名文件夹(回收站中的同名文件夹不冲突)。 + * 用于新建文件夹时的重名校验。 + */ suspend fun hasVisibleFolderNamed(name: String): Boolean { return notesDao.hasVisibleFolderNamed( name = name, @@ -82,6 +118,11 @@ class NotesRepository private constructor(context: Context) { ) } + /** + * 在根目录下创建一个新文件夹。 + * @param name 文件夹名称 + * @return 新文件夹的 ID,插入失败返回 -1 + */ suspend fun createFolder(name: String): Long { val now = System.currentTimeMillis() return notesDao.insertNote( @@ -89,12 +130,16 @@ class NotesRepository private constructor(context: Context) { parentId = Notes.ID_ROOT_FOLDER.toLong(), createdDate = now, modifiedDate = now, - snippet = name, + snippet = name, // 文件夹名存储在 snippet 中 type = Notes.TYPE_FOLDER ) ) } + /** + * 重命名文件夹。 + * @return 是否成功更新(影响行数 > 0) + */ suspend fun renameFolder(folderId: Long, name: String): Boolean { return notesDao.renameFolder( folderId = folderId, @@ -103,6 +148,10 @@ class NotesRepository private constructor(context: Context) { ) > 0 } + /** + * 收集指定文件夹下所有笔记绑定的桌面小部件信息。 + * 返回去重的小部件属性集合(LinkedSet 保持插入顺序)。 + */ suspend fun collectFolderWidgets(folderId: Long): Set { return notesDao.getFolderWidgets(folderId) .mapTo(linkedSetOf()) { widget -> @@ -113,37 +162,61 @@ class NotesRepository private constructor(context: Context) { } } + /** + * 批量删除笔记(包括文件夹及其所有子孙)。 + * 采用递归收集所有后代 ID,然后在一个事务中先删 data 再删 note。 + * 不允许删除根文件夹(ID_ROOT_FOLDER)。 + * + * @param noteIds 待删除的笔记 ID 集合 + * @return 是否删除成功 + */ suspend fun deleteNotes(noteIds: Set): Boolean { + // 过滤掉根文件夹的 ID,防止误删 val validNoteIds = noteIds .filter { it != Notes.ID_ROOT_FOLDER.toLong() } .distinct() if (validNoteIds.isEmpty()) { - return true + return true // 没有要删的内容,视为成功 } + // 开启事务,确保 data 和 note 同步删除 return database.withTransaction { + // 递归查询所有后代 ID val allNoteIds = notesDao.getDescendantNoteIds(validNoteIds) if (allNoteIds.isEmpty()) { return@withTransaction false } + // 先删除所有关联的 data 行 notesDao.deleteDataByNoteIds(allNoteIds) + // 再删除所有 note 行,返回影响行数 > 0 表示成功 notesDao.deleteNotesByIds(allNoteIds) > 0 } } + /** + * 删除单个文件夹(本质就是删除它及其子孙)。 + * 特殊处理:不允许删除根文件夹,直接返回 false。 + */ suspend fun deleteFolder(folderId: Long): Boolean { if (folderId == Notes.ID_ROOT_FOLDER.toLong()) { return false } - return deleteNotes(setOf(folderId)) } + /** + * 判断笔记是否可见(即存在且不在回收站中)。 + */ suspend fun isVisibleNote(noteId: Long, type: Int): Boolean { return notesDao.isVisibleNote(noteId, type, Notes.ID_TRASH_FOLER.toLong()) } + /** + * 加载指定便签的完整存储数据。 + * 返回一个 StoredNotePayload,包含 note 实体和对应的文本/call data。 + * 如果便签不存在,返回 null。 + */ suspend fun loadStoredNote(noteId: Long): StoredNotePayload? { val note = notesDao.getNoteById(noteId) ?: return null val dataRows = notesDao.getDataByNoteId(noteId) @@ -154,18 +227,33 @@ class NotesRepository private constructor(context: Context) { ) } + /** + * 保存便签(新建或更新)。 + * 这是一次事务操作,内部会: + * 1. 判断是新建还是更新 + * 2. 插入/更新 note 表 + * 3. upsert 文本类型 data(TextNote) + * 4. upsert 通话记录类型 data(CallNote) + * + * @return NoteSaveResult 包含最终 ID 和修改时间,若失败返回 null + */ suspend fun saveNote(request: NoteSaveRequest): NoteSaveResult? { return database.withTransaction { + // 确定修改时间:优先使用传入值,否则用当前时间 val now = request.modifiedDate.takeIf { it > 0L } ?: System.currentTimeMillis() + + // 如果提供了 noteId > 0,则查询现有实体 val existingNote = if (request.noteId > 0L) { notesDao.getNoteById(request.noteId) } else { null } + // 要求更新的便签必须存在,否则失败 if (request.noteId > 0L && existingNote == null) { return@withTransaction null } + // 如果是新建,先插入得到 noteId;否则使用现有 noteId val noteId = existingNote?.id ?: notesDao.insertNote( RoomNoteEntity( parentId = request.folderId, @@ -177,14 +265,16 @@ class NotesRepository private constructor(context: Context) { type = Notes.TYPE_NOTE, widgetId = request.widgetId, widgetType = request.widgetType, - localModified = 1 + localModified = 1 // 标记为本地已修改,用于同步 ) ) + // 插入失败则返回 null if (noteId <= 0L) { return@withTransaction null } + // 如果是更新现有笔记,执行 update if (existingNote != null) { notesDao.updateNote( existingNote.copy( @@ -201,7 +291,9 @@ class NotesRepository private constructor(context: Context) { ) } + // 处理文本内容 upsert upsertTextData(noteId, request, now) + // 处理通话记录 upsert(如果有的话) upsertCallData(noteId, request, now) NoteSaveResult( @@ -211,6 +303,11 @@ class NotesRepository private constructor(context: Context) { } } + /** + * 通过电话号码和通话日期查找对应的通话记录便签 ID。 + * 首先通过日期找到所有候选,再用 PhoneNumberUtils 进行号码精确匹配。 + * @return 匹配的 noteId,未找到返回 0 + */ suspend fun getNoteIdByPhoneNumberAndCallDate(phoneNumber: String, callDate: Long): Long { val candidates = notesDao.findCallNotesByDate( callDate = callDate, @@ -221,14 +318,23 @@ class NotesRepository private constructor(context: Context) { }?.noteId ?: 0L } + /** + * 获取便签的文本摘要(用于快速预览)。 + */ suspend fun getSnippetById(noteId: Long): String? { return notesDao.getSnippetById(noteId) } + /** + * 获取所有未来需要提醒的普通便签(提醒时间 > currentDate)。 + */ suspend fun getFutureAlertNotes(currentDate: Long): List { return notesDao.getFutureAlertNotes(currentDate, Notes.TYPE_NOTE) } + /** + * 根据桌面小部件 ID 获取其绑定的便签列表(排除回收站中的)。 + */ suspend fun getNotesByWidgetId(appWidgetId: Int): List { return notesDao.getNotesByWidgetId( appWidgetId = appWidgetId, @@ -236,20 +342,29 @@ class NotesRepository private constructor(context: Context) { ) } + /** + * 清除指定小部件的绑定关系(将 widget_id 设置为无效值)。 + * @return 操作是否成功 + */ suspend fun clearWidgetBinding(appWidgetId: Int, invalidWidgetId: Int): Boolean { return notesDao.clearWidgetBinding(appWidgetId, invalidWidgetId) > 0 } + /** + * 对文本类型 data 行执行 upsert(存在则更新,不存在则插入)。 + */ private suspend fun upsertTextData( noteId: Long, request: NoteSaveRequest, now: Long ) { + // 查找当前便签已有的文本数据 val existingTextData = notesDao.getDataByNoteIdAndMimeType(noteId, Notes.TextNote.CONTENT_ITEM_TYPE) - val content = request.content val modeValue = request.checkListMode.toLong() + if (existingTextData == null) { + // 插入新行 notesDao.insertData( RoomDataEntity( mimeType = Notes.TextNote.CONTENT_ITEM_TYPE, @@ -257,10 +372,11 @@ class NotesRepository private constructor(context: Context) { createdDate = now, modifiedDate = now, content = content, - data1 = modeValue + data1 = modeValue // 清单模式标志存入 data1 ) ) } else { + // 更新现存行 notesDao.updateData( existingTextData.copy( noteId = noteId, @@ -272,12 +388,18 @@ class NotesRepository private constructor(context: Context) { } } + /** + * 对通话记录类型 data 行执行 upsert。 + * 如果请求中没有提供电话号码且 callDate 为 null,且之前也没有老数据,则什么都不做。 + */ private suspend fun upsertCallData( noteId: Long, request: NoteSaveRequest, now: Long ) { val existingCallData = notesDao.getDataByNoteIdAndMimeType(noteId, Notes.CallNote.CONTENT_ITEM_TYPE) + + // 既没有已有数据,也没有新数据提供,直接返回 if (existingCallData == null && request.phoneNumber.isNullOrBlank() && request.callDate == null) { return } @@ -289,8 +411,8 @@ class NotesRepository private constructor(context: Context) { noteId = noteId, createdDate = now, modifiedDate = now, - data1 = request.callDate, - data3 = request.phoneNumber.orEmpty() + data1 = request.callDate, // 通话日期存入 data1 + data3 = request.phoneNumber.orEmpty() // 电话号码存入 data3 ) ) } else { @@ -305,13 +427,29 @@ class NotesRepository private constructor(context: Context) { } } + /** + * 利用系统 PhoneNumberUtils 判断两个号码是否相同(忽略格式差异)。 + * 参数中的 candidate 来自数据库查询结果。 + */ @Suppress("DEPRECATION") private fun areSamePhoneNumber(phoneNumber: String, candidate: CallNoteLookupRow): Boolean { return PhoneNumberUtils.compare(phoneNumber, candidate.phoneNumber) } + // ==================== 伴生对象:单例与排序工具 ==================== companion object { + /** + * 根据排序模式返回对应的比较器。 + * + * 排序规则: + * 1. 首先按类型分组:系统文件夹(0) → 文件夹(1) → 便签(2) + * 2. 然后在组内: + * - MODIFIED_DESC:修改时间降序,相同则按标题字母升序 + * - MODIFIED_ASC:修改时间升序,相同则按标题字母升序 + * - TITLE_ASC:标题字母升序,相同则按修改时间降序 + */ private fun noteListComparator(sortMode: NotesSortMode): Comparator { + // 分组比较器:类型分组保证系统文件夹总是在最前面 val groupComparator = compareBy { when (it.type) { Notes.TYPE_SYSTEM -> 0 @@ -319,6 +457,7 @@ class NotesRepository private constructor(context: Context) { else -> 2 } } + // 标题提取器:去除首尾空格并小写化,实现不区分大小写的排序 val titleSelector: (NoteListRow) -> String = { row -> row.snippet.trim().lowercase() } @@ -338,9 +477,14 @@ class NotesRepository private constructor(context: Context) { } } + /** 单例实例,@Volatile 保证可见性 */ @Volatile private var instance: NotesRepository? = null + /** + * 获取仓库单例的入口。 + * 双重检查锁定线程安全,传入 Context 采用 applicationContext 防止内存泄漏。 + */ operator fun invoke(context: Context): NotesRepository { return instance ?: synchronized(this) { instance ?: NotesRepository(context.applicationContext).also { repository -> @@ -349,4 +493,4 @@ class NotesRepository private constructor(context: Context) { } } } -} +} \ No newline at end of file