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
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
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
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
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
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