diff --git a/doc/members/b/README.md b/doc/members/b/README.md new file mode 100644 index 00000000..596b991a --- /dev/null +++ b/doc/members/b/README.md @@ -0,0 +1,25 @@ +# B 同学第一阶段成果 + +姓名:胡嘉豪 学号:202430224010 日期:2026-10-21(北京时间) + +分工:提醒、小组件、工具模块及后续快捷提醒。个人分支:`member/b-quick-reminder`。 +仓库:。起点为 `origin/develop` 的 +`81a66024fe48eaa2206b854430856fe3875ff3ba`,建立分支时与 `origin/master` 相同。 + +本阶段完成项目拉取、个人分支、13 个范围内顶层类的职责清单、3 个关键类的详细阅读及源码注释,以及 2 个原始代码质量问题记录。快捷提醒实现和应用运行测试留待第二阶段。 + +| 成果 | 位置 | +| --- | --- | +| 模块类清单、三个关键类详读、提醒调用链 | [phase-one-reading.md](phase-one-reading.md) | +| 两个质量问题的原始证据、复现步骤、修复建议 | [quality-issues.md](quality-issues.md) | +| 用例、包依赖、类关系、提醒流程图 | [diagrams.md](diagrams.md) 与 [assets](assets/) | +| 环境、构建报错、已执行检查、下一阶段测试表 | [environment-and-checks.md](environment-and-checks.md) | +| 可重跑的只读检查与本次输出 | [verify_phase_one.py](verify_phase_one.py)、[verification.txt](verification.txt) | +| 按提供模板填写的泛读标注维护报告 | [胡嘉豪_泛读标注维护报告_第一阶段.docx](胡嘉豪_泛读标注维护报告_第一阶段.docx) | +| 按提供模板填写的质量分析报告 | [胡嘉豪_质量分析报告_第一阶段.docx](胡嘉豪_质量分析报告_第一阶段.docx) | + +源码注释位于 `NoteWidgetProvider`、`DataUtils`、`DateTimePickerDialog`,搜索“B 阅读标注”或“B 阶段一阅读标注”可定位。共增加 39 个注释物理行,包含 Javadoc 的起止行;三个文件去除注释和空白后的 Java token 序列与基线一致。 + +首次提交 SHA 由 Git 在提交完成后生成,见本次交付消息或运行 `git log --reverse 81a66024fe48eaa2206b854430856fe3875ff3ba..member/b-quick-reminder --format="%H %s"`。不将本提交自身 SHA 写回本提交,避免产生循环依赖。 + +本目录是个人阶段成果,后续由小组汇总到总报告。原始模板和作业要求保留在工作区根目录,未被覆盖。根据本人要求,本阶段不向群聊发送消息。 diff --git a/doc/members/b/assets/classes.png b/doc/members/b/assets/classes.png new file mode 100644 index 00000000..e5251454 Binary files /dev/null and b/doc/members/b/assets/classes.png differ diff --git a/doc/members/b/assets/packages.png b/doc/members/b/assets/packages.png new file mode 100644 index 00000000..3dfb828a Binary files /dev/null and b/doc/members/b/assets/packages.png differ diff --git a/doc/members/b/assets/reminder-flow.png b/doc/members/b/assets/reminder-flow.png new file mode 100644 index 00000000..4ec72f29 Binary files /dev/null and b/doc/members/b/assets/reminder-flow.png differ diff --git a/doc/members/b/assets/use-cases.png b/doc/members/b/assets/use-cases.png new file mode 100644 index 00000000..2e802c61 Binary files /dev/null and b/doc/members/b/assets/use-cases.png differ diff --git a/doc/members/b/diagrams.md b/doc/members/b/diagrams.md new file mode 100644 index 00000000..df3b5003 --- /dev/null +++ b/doc/members/b/diagrams.md @@ -0,0 +1,80 @@ +# B 模块源代码结构图 + +图示依据基线及本阶段阅读;只展示 B 负责的功能和相关依赖,不代表整个应用的完整 UML。PNG 在 assets 下,可供 Word 报告和后续小组 PPT 复用。箭头含义分别标明,不能将依赖箭头当成继承。 + +## 范围内用例 + +![范围内用例](assets/use-cases.png) + +用例参与者为用户;范围内功能包括设置自定义提醒、取消提醒、查看提醒、通过桌面小组件查看和编辑便签。快捷提醒是下一阶段需求,未列为现有实现。 + +## 包依赖 + +![包依赖](assets/packages.png) + +```mermaid +flowchart LR + ui[ui 提醒和编辑界面] --> model[model 工作模型] + ui --> tool[tool 数据查询和资源工具] + ui --> widget[widget 桌面小组件] + model --> data[data 便签数据与契约] + widget --> data + widget --> tool + widget --> ui + tool --> data + tool --> ui +``` + +此图是 B 范围的源码包依赖视图,不包含全部 gtask 依赖。tool → ui 的证据是 AppWidgetAttribute 与偏好键引用,widget → ui 的证据是点击目标 Activity 引用。没有独立 Widget Service。 + +## 关键类关系 + +![关键类关系](assets/classes.png) + +```mermaid +classDiagram + AppWidgetProvider <|-- NoteWidgetProvider + NoteWidgetProvider <|-- NoteWidgetProvider_2x + NoteWidgetProvider <|-- NoteWidgetProvider_4x + NoteWidgetProvider ..> ResourceParser : 背景资源 + NoteWidgetProvider ..> NoteEditActivity : 点击Intent + NoteWidgetProvider : +onDeleted(context, ids) + NoteWidgetProvider : #update(context, manager, ids) + NoteWidgetProvider : #getLayoutId()* + NoteWidgetProvider : #getBgResourceId(bgId)* + NoteWidgetProvider : #getWidgetType()* + AlertDialog <|-- DateTimePickerDialog + DateTimePickerDialog --> DateTimePicker : 持有控件 + NoteEditActivity ..> DateTimePickerDialog : 创建和回调 + NoteEditActivity --> WorkingNote : 工作状态 + WorkingNote ..> NoteEditActivity : 状态监听回调 + NoteEditActivity ..> AlarmReceiver : PendingIntent + AlarmInitReceiver ..> AlarmReceiver : 开机重新注册 + AlarmReceiver ..> AlarmAlertActivity : 启动 + AlarmAlertActivity ..> DataUtils : 摘要和可见性 +``` + +## 提醒流程 + +![提醒流程](assets/reminder-flow.png) + +```mermaid +flowchart TD + A[编辑页 设置提醒] --> B[DateTimePickerDialog 自定义时间] + B -->|确定| C[WorkingNote.setAlertDate 标记待保存值] + C --> D[onClockAlertChanged 未保存便签先尝试保存] + D --> E{有有效便签ID} + E -->|否| F[显示空便签错误提示] + E -->|是| G[按便签URI建立PendingIntent并注册RTC_WAKEUP] + C -.编辑页onPause.-> H[saveNote 经Note.syncNote持久化 按需刷新组件] + G -->|到时| I[AlarmReceiver] + I --> J[AlarmAlertActivity 查询可见性 展示并播放铃声] + J --> K[关闭对话框 停止并释放播放器] + L[取消提醒菜单] --> M[setAlertDate 0 false] + M --> N[相同便签URI的PendingIntent 调用AlarmManager.cancel] + M -.编辑页onPause.-> H + O[设备BOOT_COMPLETED] --> P[AlarmInitReceiver 查询未来普通便签提醒] + P --> G +``` + +现有便签设置提醒后,系统注册发生在回调中,数据库持久化依赖保存流程;两者不是同一个操作。普通应用重启与 BOOT_COMPLETED 恢复路径应分别验证。 diff --git a/doc/members/b/environment-and-checks.md b/doc/members/b/environment-and-checks.md new file mode 100644 index 00000000..2b5815cb --- /dev/null +++ b/doc/members/b/environment-and-checks.md @@ -0,0 +1,85 @@ +# 第一阶段环境和验证记录 + +胡嘉豪 202430224010 2026-10-21(Asia/Shanghai) + +## 环境与仓库 + +| 项目 | 本次记录 | +| --- | --- | +| 系统与终端 | Windows,PowerShell | +| Git | 2.53.0.windows.2 | +| 仓库和个人分支 | ttt587/Notes;member/b-quick-reminder | +| 基线 | 81a66024fe48eaa2206b854430856fe3875ff3ba;建立分支时 develop 与 master 一致 | +| 源码 | Android Java,Manifest versionName 0.1,minSdkVersion 14;未声明 targetSdkVersion | +| JDK | C:\jdk-24.0.2;直接执行 java.exe -version 显示 24.0.2 | +| Android SDK | ANDROID_HOME=E:\Sdk;platforms 有 android-34、35、36、36.1;build-tools 有 34.0.0、36.0.0、36.1.0 | +| 构建入口 | 无 gradlew、build.gradle、settings.gradle、build.xml;project.properties 被 .gitignore 忽略且本次未提供 | + +## 实际尝试与完整错误 + +拉取命令:`git clone https://github.com/ttt587/Notes.git Notes`。首次受沙箱网络限制: + +```text +Cloning into 'Notes'... +fatal: unable to access 'https://github.com/ttt587/Notes.git/': getaddrinfo() thread failed to start +``` + +在允许 Git 访问网络后重试成功。随后执行: + +```powershell +git switch -c member/b-quick-reminder origin/develop +``` + +结果:成功创建个人分支,起点为上述基线。完成提交后将 upstream 指向同名远程个人分支。 + +在 Notes 根目录尝试 `& .\gradlew.bat assembleDebug`,PowerShell 返回: + +```text +&: +Line | + 2 | … ('ANDROID_SDK_ROOT='+$env:ANDROID_SDK_ROOT); & .\gradlew.bat assembl … + | ~~~~~~~~~~~~~ + | The term '.\gradlew.bat' is not recognized as a name of a cmdlet, function, script file, or executable program. +Check the spelling of the name, or if a path was included, verify that the path is correct and try again. +``` + +这是构建入口缺失,尚未进入 Java 编译,不等于“编译器已发现源码错误”。现有较新 SDK 也不能单独证明旧工程可构建或不兼容。下一阶段需与负责构建的成员协调可重复的构建方案及 JDK/SDK 组合,再执行 APK 构建和设备验证。本次未擅自改造全组构建配置。 + +文档排版验证:打包的 render_docx.py 首次报 `FileNotFoundError: LibreOffice soffice.exe was not found on PATH`。改为通过已安装的 Word 只读导出 PDF,再使用同一打包渲染器与 Poppler 生成页面图片检查;未修改原始模板。 + +## 本阶段已执行检查 + +| 检查 | 结果与意义 | +| --- | --- | +| 原始代码清点 | 39 个 Java 文件,8 个包,10,002 物理行,包含注释和空行;由 git show 基线读取并统计 | +| 三个注释文件的 Java token 对比 | 通过;剥离注释和空白后与基线逐 token 相同,字符串和字符字面量保留 | +| 注释数量 | NoteWidgetProvider 13 行、DataUtils 15 行、DateTimePickerDialog 11 行,共 39 个净新增注释物理行 | +| git diff --check | 通过;无补丁空白错误 | +| Q-B-01 静态检查 | 两个分支均 FORMAT_24HOUR;Android 展示待实测 | +| Q-B-02 控制流检查 | 成功 return 跳过 cursor.close;Android 资源检测待执行 | +| APK 构建 | 阻塞于缺少 gradlew.bat;未生成 APK | +| Android 单元测试、设备测试、SonarQube | 未执行,不填报通过率或扫描指标 | + +复核命令:`python doc/members/b/verify_phase_one.py`(Python 3,标准库)。脚本为只读,不修改源码;本次输出见 [verification.txt](verification.txt)。本阶段仅增加注释,因此没有添加与注释实现镜像的单元测试。 + +## 第二阶段测试计划 + +以下均为待执行用例,尚无通过结论。快捷项具体时间规则在实现前确定;时区默认跟随设备设置,测试固定 now 并记录时区。 + +| 编号 | 场景 | 输入或操作 | 预期检查 | 状态 | +| --- | --- | --- | --- | --- | +| B-T01 | 跨天 | 23:50 选择“明天上午 9:00” | 次日 09:00,秒及毫秒按约定归零 | 待执行 | +| B-T02 | 月末 | 1 月 31 日选择明天 | 进入 2 月 1 日,不产生非法日期 | 待执行 | +| B-T03 | 年末 | 12 月 31 日选择明天 | 次年 1 月 1 日 09:00 | 待执行 | +| B-T04 | 一周后 | 月末或年末选择一周后 | Calendar 跨界正确,时分规则符合约定 | 待执行 | +| B-T05 | 过去时间 | 自定义过去时间或当天快捷时刻已过 | 按明确策略提示或调整,不静默设错 | 待执行 | +| B-T06 | 取消提醒 | 设提醒后取消并退出编辑页 | 数据库时间清零,对应闹钟取消,原时刻不触发 | 待执行 | +| B-T07 | 应用重启 | 设置后正常退出,再启动应用 | 数据持久化、界面提示及计划提醒一致;与强制停止区分 | 待执行 | +| B-T08 | 设备重启 | 设置未来提醒后重启设备 | BOOT_COMPLETED 后恢复未来闹钟,已过期策略符合设计 | 待执行 | +| B-T09 | 提醒触发 | 设近期提醒,等待到时 | 显示正确便签和铃声,关闭后释放播放资源 | 待执行 | +| B-T10 | 小组件回归 | 分别放置 2×2 和 4×4,编辑内容和提醒 | 摘要、背景、点击目标及删除解绑正常 | 待执行 | +| B-T11 | 空白便签 | 新建空白便签并设提醒 | 给出原有错误提示,不注册无有效便签 ID 的提醒 | 待执行 | +| B-T12 | 12/24 小时 | 切换设置,测试 00:00、12:00、15:30 | 标题、滚轮与保存时间一致,回归 Q-B-01 | 待执行 | +| B-T13 | Cursor 释放 | 通话查询命中、未命中、null 和读取异常 | 可关闭对象均在完成后关闭,回归 Q-B-02 | 待执行 | + +下一阶段需保留原界面和修改后界面截图、每项测试的设备版本、操作步骤、实际结果和对应 commit SHA。本阶段没有应用运行截图;图示是源代码结构图,不能替代运行证据。 diff --git a/doc/members/b/phase-one-reading.md b/doc/members/b/phase-one-reading.md new file mode 100644 index 00000000..64ba16a2 --- /dev/null +++ b/doc/members/b/phase-one-reading.md @@ -0,0 +1,95 @@ +# 提醒 小组件和工具模块阅读说明 + +胡嘉豪 202430224010 B 同学 2026-10-21 + +本次阅读基线为 `81a66024fe48eaa2206b854430856fe3875ff3ba`。个人分支为 `member/b-quick-reminder`。以下结论来自源代码阅读,未把静态推断写成应用实测结果。除特别注明“原始行号”外,行号均对应本阶段注释后的文件。 + +## 阅读范围和整体功能 + +MiCode Notes 是 Android 便签应用,具有便签编辑、文件夹、清单模式、提醒、桌面小组件、文本导出和任务同步等功能。本次负责 `src/net/micode/notes/widget/`、`src/net/micode/notes/tool/` 以及 `ui` 中提醒选择、注册、恢复和触发相关代码。 + +基线共有 39 个 Java 文件、8 个包、10,002 个物理行(包含注释和空行,不等于可执行代码行)。`widget` 有 3 个顶层类,`tool` 有 4 个顶层类,本次还梳理 6 个相关 UI 顶层类,共 13 个。本仓库没有单独的 Widget Service;小组件由 `AppWidgetProvider`、`RemoteViews` 和编辑页发出的更新广播配合完成。 + +架构分为 UI、工作模型、数据访问、工具、小组件以及三个 `gtask` 包。UI 通过 `WorkingNote` 修改工作状态,经 `Note` 和 `ContentResolver` 写入数据;小组件读取便签表并显示摘要。包依赖不是严格的单向分层,例如 `DataUtils` 依赖 `NotesListAdapter.AppWidgetAttribute`,`ResourceParser` 依赖 UI 偏好设置键。 + +## 类与职责清单 + +各路径均以 `src/net/micode/notes/` 为前缀。清单列出顶层类;内部类单独注明,不混入顶层类统计。 + +| 包或路径 | 顶层类 | 主要职责和关键入口 | +| --- | --- | --- | +| widget | NoteWidgetProvider | 小组件公共刷新骨架;`onDeleted()` 解除绑定,`update()` 查询数据并绑定 RemoteViews 和点击 PendingIntent。 | +| widget | NoteWidgetProvider_2x | 2×2 小组件入口;`onUpdate()` 委托父类,提供布局、背景及 `TYPE_WIDGET_2X`。 | +| widget | NoteWidgetProvider_4x | 4×4 小组件入口;与 2×2 共用更新逻辑,提供另一组资源和类型。 | +| tool | DataUtils | 批量删除、移动、可见性、通话便签和摘要查询;`getFolderNoteWidget()` 为组件更新收集 ID 和类型。 | +| tool | ResourceParser | 颜色、字体索引到资源 ID 的映射,以及默认背景策略;内部类 NoteBgResources、NoteItemBgResources、WidgetBgResources、TextAppearanceResources 分别服务不同界面。 | +| tool | BackupUtils | 便签文本导出、导出结果及文件路径管理;内部 TextExport 执行查询、格式化和文件写入。 | +| tool | GTaskStringUtils | Google Tasks 协议 JSON 键、动作名称及元数据常量;本阶段只记录职责,不修改同步协议。 | +| ui | NoteEditActivity | 编辑页提醒入口;`setReminder()` 打开选择器,`onClockAlertChanged()` 注册或取消闹钟,`showAlertHeader()` 更新提示,`updateWidget()` 广播刷新。 | +| ui | DateTimePickerDialog | 包装选择控件、维护所选 Calendar、格式化标题;确定后通过 OnDateTimeSetListener 返回毫秒时间戳。 | +| ui | DateTimePicker | 日期、小时、分钟、AM/PM 滚轮;管理 12/24 小时模式、日期进退和 OnDateTimeChangedListener 回调。 | +| ui | AlarmReceiver | 收到提醒广播后将 Intent 转给 AlarmAlertActivity,并添加新任务标记。 | +| ui | AlarmInitReceiver | 接收开机广播;查询未来的提醒时间并重新注册 AlarmManager 闹钟。不会恢复已经过期的提醒。 | +| ui | AlarmAlertActivity | 从 URI 读取便签 ID 和摘要、检查可见性,展示提醒并播放铃声;关闭对话框时停止并释放播放器。 | + +跨模块依赖:`model/WorkingNote.java` 的 `setAlertDate()` 保存待写入值并回调编辑页,`saveNote()` 持久化且按需通知组件;`data/Notes.java` 定义 URI、列名、便签及组件类型;`model/Note.java` 执行数据同步写入。这些依赖用于解释调用链,不计入上述 13 类。 + +布局与声明:`res/layout/datetime_picker.xml`、`widget_2x.xml`、`widget_4x.xml`;菜单 `res/menu/note_edit.xml`、`call_note_edit.xml`;组件元数据 `res/xml/widget_2x_info.xml`、`widget_4x_info.xml`;组件及提醒 Receiver 在 `AndroidManifest.xml` 中声明。 + +## 关键类一 NoteWidgetProvider + +文件:[NoteWidgetProvider.java](../../../src/net/micode/notes/widget/NoteWidgetProvider.java)。父类是 Android `AppWidgetProvider`;两个具体 Provider 继承它。`getLayoutId()`、`getBgResourceId()`、`getWidgetType()` 是尺寸差异的扩展点,共享的 `update()` 则承担更新骨架。 + +1. `onDeleted()` 将指定组件关联记录的 `WIDGET_ID` 设为无效值,保留便签。 +2. `getNoteWidgetInfo()` 按 widgetId 查询 ID、背景色、摘要,排除回收站;调用者拥有 Cursor 的关闭责任。 +3. `update()` 遍历组件。存在唯一绑定便签时,点击 Intent 为 ACTION_VIEW,并携带便签 ID;没有绑定时显示占位文本,点击进入 ACTION_INSERT_OR_EDIT。 +4. 根据尺寸选择布局,根据颜色索引调用 `ResourceParser.WidgetBgResources` 获取背景,然后以 widgetId 作为 PendingIntent requestCode,最后更新 RemoteViews。 +5. 私有重载保留 privacyMode 分支,但公开的受保护重载固定传 false;仅凭 Manifest 中的隐私广播声明,不能推断隐私模式已经生效。 + +边界:无效组件 ID 被跳过;多条便签错误地绑定同一组件时关闭 Cursor 后直接 return,整批后续组件也不会刷新;无绑定和已移入回收站都走占位分支。后续快捷提醒不能改变 widgetId、widgetType、背景映射和摘要刷新行为。 + +本次增加 13 行注释,覆盖类职责、解除绑定、查询过滤、线程位置、异常批次退出、无数据分支、资源映射、隐私分支和 PendingIntent 区分方式。未改变行为。 + +## 关键类二 DataUtils + +文件:[DataUtils.java](../../../src/net/micode/notes/tool/DataUtils.java)。该类集中使用 ContentResolver,避免 UI 重复构造查询与批量操作。 + +`batchDeleteNotes()` 将多个 ID 转换为 ContentProviderOperation 并 applyBatch,跳过系统根目录;`batchMoveToFolder()` 更新父目录和 LOCAL_MODIFIED。两者只验证结果数组及首项,不能由返回 true 推导每个目标都被实际修改。 + +`visibleInNoteDatabase()` 同时检查便签类型和非回收站状态,被提醒弹窗用于判断是否继续显示。`getFolderNoteWidget()` 返回目录下的组件属性集合,未查到记录时可能为 null,调用方不能直接假设为空集合。 + +`getSnippetById()` 用于提醒摘要读取:非空 Cursor 但没有记录时返回空字符串,null Cursor 时抛 IllegalArgumentException。`getFormattedSnippet()` 对内容 trim 后只保留首行,null 输入仍返回 null。 + +资源管理重点:`getCallNumberByNoteId()` 的空结果路径不进入 finally;`getNoteIdByPhoneNumberAndCallDate()` 在成功读取后直接 return,跳过 cursor.close()。后者作为 Q-B-02 记录,需在后续阶段统一 Cursor 生命周期。 + +本次增加 15 行注释,覆盖批量操作、可见性、组件信息、摘要语义以及 Cursor 泄漏路径。保留原有 API 拼写 `moveNoteToFoler`,避免阅读阶段引入调用接口变更。 + +## 关键类三 DateTimePickerDialog + +文件:[DateTimePickerDialog.java](../../../src/net/micode/notes/ui/DateTimePickerDialog.java)。它继承 AlertDialog,封装 DateTimePicker,通过 OnDateTimeSetListener 把时间交给编辑页,不直接访问数据库或 AlarmManager。 + +构造时创建控件并注册变化监听。每次变化依次设置 Calendar 的年、月、日、小时和分钟,然后更新标题;Calendar.MONTH 使用 0~11,监听器传入的月份与 Calendar 一致。初始时间来自参数,清零秒后同步控件,但没有清零毫秒。 + +`set24HourView()` 只修改对话框状态;内部控件在自身构造时读取系统格式。`updateTitle()` 合并年月日时间标志,但三元表达式两个分支都为 FORMAT_24HOUR,导致系统设置为 12 小时时标题仍强制 24 小时。该确定性静态问题记录为 Q-B-01。 + +确定按钮执行 `onClick()` 并回传毫秒时间戳;取消按钮监听器为 null,不写入提醒。对话框和现有设置调用链没有拒绝过去时间,因此后续快捷选项需要明确过去时间策略。该判断为代码阅读结论,实际系统触发行为尚未实测。 + +本次增加 11 行注释,覆盖类边界、时间单位、控件回调、秒毫秒处理、格式设置、质量问题和确认回调。 + +## 提醒流程与持久化 + +1. 编辑页菜单 `menu_alert` → `NoteEditActivity.setReminder()`(556 行)→ `DateTimePickerDialog`。 +2. 确定 → `WorkingNote.setAlertDate(date, true)`(232 行),设置 ALERTED_DATE 待写值并调用监听器。 +3. `NoteEditActivity.onClockAlertChanged()`(615 行)对未保存便签先尝试保存,构造含便签 URI 的 PendingIntent,然后用 AlarmManager.RTC_WAKEUP 注册。空白未保存便签没有有效 ID 时显示错误提示。 +4. 已存在便签的数据通过编辑页 `onPause()` 调用保存流程;`WorkingNote.saveNote()`(190 行)经 Note.syncNote 持久化,同时按需触发组件刷新。不能把仅注册系统闹钟当作已完成数据库持久化。 +5. 到时 → AlarmReceiver → AlarmAlertActivity → DataUtils 摘要和可见性查询 → 对话框与铃声 → 关闭时停止播放并释放。 +6. 取消菜单 → `setAlertDate(0, false)` → 相同便签 URI 的 PendingIntent → AlarmManager.cancel;退出编辑页后保存取消状态。 +7. 设备开机 → AlarmInitReceiver 查询 ALERTED_DATE 大于当前时间的普通便签 → 重新注册。普通应用重启不等于设备开机,该 Receiver 只监听 BOOT_COMPLETED。 + +组件刷新链:WorkingNote.saveNote / markDeleted → 监听器 onWidgetChanged → NoteEditActivity.updateWidget(409 行)→ 对应尺寸 Provider.onUpdate → 父类 update → RemoteViews。源代码关系图见 [diagrams.md](diagrams.md)。 + +## 后续快捷提醒设计入口 + +下一阶段在 `setReminder()` 前增加快捷项选择,同时保留现有自定义时间对话框。建议用独立的时间计算工具接受明确的 now、时区和选项,便于对跨天、月底、年底做确定性测试;仍复用 `WorkingNote.setAlertDate()` 和现有注册、取消流程。 + +“今天晚些时候”的具体时刻或间隔、当晚间时刻已过时如何处理、“一周后”是否保留当前时分应先在小组内固定。本阶段只记录设计方向,不把这些语义写成已经实现的产品规则。测试计划见 [environment-and-checks.md](environment-and-checks.md)。 diff --git a/doc/members/b/quality-issues.md b/doc/members/b/quality-issues.md new file mode 100644 index 00000000..177553a0 --- /dev/null +++ b/doc/members/b/quality-issues.md @@ -0,0 +1,56 @@ +# 原始代码质量问题记录 + +记录人:胡嘉豪 202430224010 日期:2026-10-21 + +分析基线:`81a66024fe48eaa2206b854430856fe3875ff3ba`。本阶段仅新增阅读注释;以下两个问题尚未修复。证据来自源码和控制流检查,尚未在 Android 设备上复现。 + +## Q-B-01 提醒时间标题忽略 12 小时设置 + +- 类和方法:`ui/DateTimePickerDialog.java` 的 `updateTitle(long date)`。 +- 位置:基线第 80 行;注释后第 90 行。 +- 严重程度:中。影响提醒时间展示一致性,不据此推断时间戳被错误保存。 +- 状态:静态确认,设备复现待执行,修复待第二阶段。 + +原始证据: + +```java +flag |= mIs24HourView ? DateUtils.FORMAT_24HOUR : DateUtils.FORMAT_24HOUR; +``` + +预期:系统使用 12 小时时,标题与选择控件采用一致的 12 小时表示;24 小时时显示 24 小时。实际代码在布尔值两种取值下都加入 FORMAT_24HOUR,`mIs24HourView` 对标题格式不起区分作用。 + +建议复现步骤:在设备关闭“使用 24 小时制”;新建有内容的便签,打开提醒选择器并选择下午 15:30;比较 AM/PM 控件与标题;再开启 24 小时制重复。预期设备现象是标题在两种设置下都显示 15:30,该现象尚未实测。 + +修复建议:false 分支使用适当的 12 小时格式标志,并检查 `set24HourView()` 是否需要同步内部控件及刷新标题。第二阶段分别覆盖上午、下午、00:00、12:00、23:59;校验标题、控件、回调时间戳三者一致。 + +## Q-B-02 通话便签查询成功时未关闭 Cursor + +- 类和方法:`tool/DataUtils.java` 的 `getNoteIdByPhoneNumberAndCallDate(...)`。 +- 位置:基线第 254~262 行,成功返回在第 257 行;注释后成功返回在第 270 行。 +- 严重程度:中。正常命中查询即可走到泄漏路径,重复调用有资源积累风险;未测量实际内存或句柄增长。 +- 状态:控制流静态确认,资源检测待执行,修复待后续维护。 + +原始证据: + +```java +if (cursor != null) { + if (cursor.moveToFirst()) { + try { + return cursor.getLong(0); + } catch (IndexOutOfBoundsException e) { + Log.e(TAG, "Get call note id fails " + e.toString()); + } + } + cursor.close(); +} +``` + +控制流:query 返回非空 Cursor → moveToFirst 为 true → getLong 成功 → return 结束方法 → 后面的 close 不执行。相比之下,空结果路径会执行 close。该问题不依赖异常路径。 + +建议复现步骤:准备一条号码和通话时间可匹配的通话便签,调用该方法;用可记录 close 调用的 Cursor 或资源监测确认成功返回路径未关闭;对空结果、null Cursor、读取异常路径分别验证。尚未执行这些 Android 测试。 + +修复建议:将整个非空 Cursor 使用范围置于 try/finally,确保成功返回、空结果与异常都会关闭。保留当前返回语义(未命中返回 0),并协调共享工具类的其他调用方。相邻 `getCallNumberByNoteId()` 的空结果关闭路径也值得一并检查,但不混同为已测试缺陷。 + +## 阅读结论的限制 + +以上两项均可从原始代码定位,不是新增注释引入的问题。本阶段没有运行 SonarQube,也没有完成 APK 构建、模拟器运行或界面截图;不填报扫描分数、通过率或设备测试结果。下一阶段修复应单独提交并补充回归证据。 diff --git a/doc/members/b/verification.txt b/doc/members/b/verification.txt new file mode 100644 index 00000000..8ce21c1d --- /dev/null +++ b/doc/members/b/verification.txt @@ -0,0 +1,12 @@ +B phase-one verification +BASE: 81a66024fe48eaa2206b854430856fe3875ff3ba +Branch: member/b-quick-reminder +Annotation counts are net added physical comment lines, including Javadoc delimiters. +PASS executable tokens unchanged; +13 comment lines: src/net/micode/notes/widget/NoteWidgetProvider.java +PASS executable tokens unchanged; +15 comment lines: src/net/micode/notes/tool/DataUtils.java +PASS executable tokens unchanged; +11 comment lines: src/net/micode/notes/ui/DateTimePickerDialog.java +Baseline inventory: 39 Java source files, 8 packages, 10002 physical lines (comments and blank lines included). +Evidence: src/net/micode/notes/ui/DateTimePickerDialog.java:90: flag |= mIs24HourView ? DateUtils.FORMAT_24HOUR : DateUtils.FORMAT_24HOUR; +Evidence: src/net/micode/notes/tool/DataUtils.java:270: return cursor.getLong(0); +PASS git diff --check BASE +Android build, unit tests, device tests and SonarQube: NOT RUN / build entry unavailable. diff --git a/doc/members/b/verify_phase_one.py b/doc/members/b/verify_phase_one.py new file mode 100644 index 00000000..a26e527b --- /dev/null +++ b/doc/members/b/verify_phase_one.py @@ -0,0 +1,54 @@ +"""Read-only evidence checks for B's phase-one comment and reading commit.""" +from pathlib import Path +import re +import subprocess + +ROOT = Path(__file__).resolve().parents[3] +BASE = '81a66024fe48eaa2206b854430856fe3875ff3ba' +FILES = [ + 'src/net/micode/notes/widget/NoteWidgetProvider.java', + 'src/net/micode/notes/tool/DataUtils.java', + 'src/net/micode/notes/ui/DateTimePickerDialog.java', +] + +def git(*args): + return subprocess.check_output(['git', '-C', str(ROOT), *args]).decode('utf-8') + +def tokens(text): + # Strings and character literals are matched before comments, preserving // in URIs. + pattern = r'"(?:\\.|[^"\\])*"|\x27(?:\\.|[^\x27\\])*\x27|/\*.*?\*/|//[^\r\n]*|\s+|[\w$]+|[^\s]' + result = [] + for match in re.finditer(pattern, text, re.S): + part = match.group() + if not part.isspace() and not part.startswith(('/*', '//')): + result.append(part) + return result + +print('B phase-one verification') +print('BASE:', BASE) +print('Branch:', git('branch', '--show-current').strip()) +print('Annotation counts are net added physical comment lines, including Javadoc delimiters.') +for filename in FILES: + original = git('show', f'{BASE}:{filename}') + current = (ROOT / filename).read_text(encoding='utf-8') + assert tokens(original) == tokens(current), f'Executable token changed: {filename}' + delta = len(current.splitlines()) - len(original.splitlines()) + print(f'PASS executable tokens unchanged; +{delta} comment lines: {filename}') + +baseline_files = git('ls-tree', '-r', '--name-only', BASE, 'src').splitlines() +java_files = [p for p in baseline_files if p.endswith('.java')] +source = [git('show', f'{BASE}:{p}') for p in java_files] +packages = {re.search(r'^package\s+([^;]+)', s, re.M).group(1) for s in source} +print(f'Baseline inventory: {len(java_files)} Java source files, {len(packages)} packages, ' + f'{sum(len(s.splitlines()) for s in source)} physical lines (comments and blank lines included).') +for filename, needle in [ + (FILES[2], 'flag |= mIs24HourView'), + (FILES[1], 'return cursor.getLong(0);'), +]: + for n, line in enumerate((ROOT / filename).read_text(encoding='utf-8').splitlines(), 1): + if needle in line: + print(f'Evidence: {filename}:{n}: {line.strip()}') +check = subprocess.run(['git', '-C', str(ROOT), 'diff', '--check', BASE], capture_output=True) +assert check.returncode == 0, check.stdout.decode('utf-8', errors='replace') +print('PASS git diff --check BASE') +print('Android build, unit tests, device tests and SonarQube: NOT RUN / build entry unavailable.') diff --git "a/doc/members/b/\350\203\241\345\230\211\350\261\252_\346\263\233\350\257\273\346\240\207\346\263\250\347\273\264\346\212\244\346\212\245\345\221\212_\347\254\254\344\270\200\351\230\266\346\256\265.docx" "b/doc/members/b/\350\203\241\345\230\211\350\261\252_\346\263\233\350\257\273\346\240\207\346\263\250\347\273\264\346\212\244\346\212\245\345\221\212_\347\254\254\344\270\200\351\230\266\346\256\265.docx" new file mode 100644 index 00000000..f1e8deac Binary files /dev/null and "b/doc/members/b/\350\203\241\345\230\211\350\261\252_\346\263\233\350\257\273\346\240\207\346\263\250\347\273\264\346\212\244\346\212\245\345\221\212_\347\254\254\344\270\200\351\230\266\346\256\265.docx" differ diff --git "a/doc/members/b/\350\203\241\345\230\211\350\261\252_\350\264\250\351\207\217\345\210\206\346\236\220\346\212\245\345\221\212_\347\254\254\344\270\200\351\230\266\346\256\265.docx" "b/doc/members/b/\350\203\241\345\230\211\350\261\252_\350\264\250\351\207\217\345\210\206\346\236\220\346\212\245\345\221\212_\347\254\254\344\270\200\351\230\266\346\256\265.docx" new file mode 100644 index 00000000..ea6ed888 Binary files /dev/null and "b/doc/members/b/\350\203\241\345\230\211\350\261\252_\350\264\250\351\207\217\345\210\206\346\236\220\346\212\245\345\221\212_\347\254\254\344\270\200\351\230\266\346\256\265.docx" differ diff --git a/src/net/micode/notes/tool/DataUtils.java b/src/net/micode/notes/tool/DataUtils.java index 2a14982e..25f521fc 100644 --- a/src/net/micode/notes/tool/DataUtils.java +++ b/src/net/micode/notes/tool/DataUtils.java @@ -35,9 +35,15 @@ import java.util.HashSet; +/** + * B 阶段一阅读标注 胡嘉豪 202430224010 + * 通过 ContentResolver 集中处理便签批量操作、可见性查询和摘要读取。 + * 小组件和提醒界面复用这些方法;调用者仍需处理返回值和 Provider 异常。 + */ public class DataUtils { public static final String TAG = "DataUtils"; public static boolean batchDeleteNotes(ContentResolver resolver, HashSet ids) { + // B 阅读标注:空集合视为无需操作,系统根目录在构建删除操作时被跳过。 if (ids == null) { Log.d(TAG, "the ids is null"); return true; @@ -59,6 +65,7 @@ public static boolean batchDeleteNotes(ContentResolver resolver, HashSet i } try { ContentProviderResult[] results = resolver.applyBatch(Notes.AUTHORITY, operationList); + // B 阅读标注:这里只检查首个结果,不能据此证明每条删除都影响了记录。 if (results == null || results.length == 0 || results[0] == null) { Log.d(TAG, "delete notes failed, ids:" + ids.toString()); return false; @@ -82,6 +89,7 @@ public static void moveNoteToFoler(ContentResolver resolver, long id, long srcFo public static boolean batchMoveToFolder(ContentResolver resolver, HashSet ids, long folderId) { + // B 阅读标注:批量移动同时置本地修改标记,以便后续同步识别变更。 if (ids == null) { Log.d(TAG, "the ids is null"); return true; @@ -137,6 +145,7 @@ public static int getUserFolderCount(ContentResolver resolver) { } public static boolean visibleInNoteDatabase(ContentResolver resolver, long noteId, int type) { + // B 阅读标注:提醒展示前用此方法检查便签类型及回收站状态。 Cursor cursor = resolver.query(ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, noteId), null, NoteColumns.TYPE + "=? AND " + NoteColumns.PARENT_ID + "<>" + Notes.ID_TRASH_FOLER, @@ -198,6 +207,7 @@ public static boolean checkVisibleFolderName(ContentResolver resolver, String na } public static HashSet getFolderNoteWidget(ContentResolver resolver, long folderId) { + // B 阅读标注:返回目录下的组件 ID 和类型;无记录或查询为空时可能返回 null。 Cursor c = resolver.query(Notes.CONTENT_NOTE_URI, new String[] { NoteColumns.WIDGET_ID, NoteColumns.WIDGET_TYPE }, NoteColumns.PARENT_ID + "=?", @@ -225,6 +235,7 @@ public static HashSet getFolderNoteWidget(ContentResolver re } public static String getCallNumberByNoteId(ContentResolver resolver, long noteId) { + // B 阅读标注:空 Cursor 不进入下方 finally,资源关闭存在遗漏路径。 Cursor cursor = resolver.query(Notes.CONTENT_DATA_URI, new String [] { CallNote.PHONE_NUMBER }, CallNote.NOTE_ID + "=? AND " + CallNote.MIME_TYPE + "=?", @@ -244,6 +255,8 @@ public static String getCallNumberByNoteId(ContentResolver resolver, long noteId } public static long getNoteIdByPhoneNumberAndCallDate(ContentResolver resolver, String phoneNumber, long callDate) { + // B 阅读标注 Q-B-02:成功读取后直接 return,跳过下方 cursor.close()。 + // 修复应在后续阶段把整个非空 Cursor 生命周期放入 try/finally。 Cursor cursor = resolver.query(Notes.CONTENT_DATA_URI, new String [] { CallNote.NOTE_ID }, CallNote.CALL_DATE + "=? AND " + CallNote.MIME_TYPE + "=? AND PHONE_NUMBERS_EQUAL(" @@ -265,6 +278,7 @@ public static long getNoteIdByPhoneNumberAndCallDate(ContentResolver resolver, S } public static String getSnippetById(ContentResolver resolver, long noteId) { + // B 阅读标注:提醒弹窗从此处取摘要;空结果返回空串,null Cursor 则抛出异常。 Cursor cursor = resolver.query(Notes.CONTENT_NOTE_URI, new String [] { NoteColumns.SNIPPET }, NoteColumns.ID + "=?", @@ -283,6 +297,7 @@ public static String getSnippetById(ContentResolver resolver, long noteId) { } public static String getFormattedSnippet(String snippet) { + // B 阅读标注:仅保留 trim 后的首行;null 输入仍返回 null。 if (snippet != null) { snippet = snippet.trim(); int index = snippet.indexOf('\n'); diff --git a/src/net/micode/notes/ui/DateTimePickerDialog.java b/src/net/micode/notes/ui/DateTimePickerDialog.java index 2c47ba41..785ab9a3 100644 --- a/src/net/micode/notes/ui/DateTimePickerDialog.java +++ b/src/net/micode/notes/ui/DateTimePickerDialog.java @@ -29,6 +29,11 @@ import android.text.format.DateFormat; import android.text.format.DateUtils; +/** + * B 阶段一阅读标注 胡嘉豪 202430224010 + * 包装日期时间控件,通过监听器向编辑页交付毫秒时间戳。 + * 本类只负责选择与标题展示,保存便签和注册闹钟在模型及编辑页中完成。 + */ public class DateTimePickerDialog extends AlertDialog implements OnClickListener { private Calendar mDate = Calendar.getInstance(); @@ -37,6 +42,7 @@ public class DateTimePickerDialog extends AlertDialog implements OnClickListener private DateTimePicker mDateTimePicker; public interface OnDateTimeSetListener { + // B 阅读标注:date 是 Unix 毫秒时间戳,月份转换由 Calendar 和控件处理。 void OnDateTimeSet(AlertDialog dialog, long date); } @@ -47,6 +53,7 @@ public DateTimePickerDialog(Context context, long date) { mDateTimePicker.setOnDateTimeChangedListener(new OnDateTimeChangedListener() { public void onDateTimeChanged(DateTimePicker view, int year, int month, int dayOfMonth, int hourOfDay, int minute) { + // B 阅读标注:每次滚轮变化同步 Calendar,并刷新对话框标题。 mDate.set(Calendar.YEAR, year); mDate.set(Calendar.MONTH, month); mDate.set(Calendar.DAY_OF_MONTH, dayOfMonth); @@ -56,6 +63,7 @@ public void onDateTimeChanged(DateTimePicker view, int year, int month, } }); mDate.setTimeInMillis(date); + // B 阅读标注:这里只清零秒,毫秒仍被保留;快捷时间应明确秒和毫秒策略。 mDate.set(Calendar.SECOND, 0); mDateTimePicker.setCurrentDate(mDate.getTimeInMillis()); setButton(context.getString(R.string.datetime_dialog_ok), this); @@ -65,6 +73,7 @@ public void onDateTimeChanged(DateTimePicker view, int year, int month, } public void set24HourView(boolean is24HourView) { + // B 阅读标注:此方法仅更新标题状态,未调用内部控件的 set24HourView。 mIs24HourView = is24HourView; } @@ -77,14 +86,16 @@ private void updateTitle(long date) { DateUtils.FORMAT_SHOW_YEAR | DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_SHOW_TIME; + // B 阅读标注 Q-B-01:两个分支相同,12 小时设置也被强制按 24 小时格式显示。 flag |= mIs24HourView ? DateUtils.FORMAT_24HOUR : DateUtils.FORMAT_24HOUR; setTitle(DateUtils.formatDateTime(this.getContext(), date, flag)); } public void onClick(DialogInterface arg0, int arg1) { + // B 阅读标注:确定时回传所选时间;此处未拒绝过去时间,调用链也需核查。 if (mOnDateTimeSetListener != null) { mOnDateTimeSetListener.OnDateTimeSet(this, mDate.getTimeInMillis()); } } -} \ No newline at end of file +} diff --git a/src/net/micode/notes/widget/NoteWidgetProvider.java b/src/net/micode/notes/widget/NoteWidgetProvider.java index ec6f8193..e254088e 100644 --- a/src/net/micode/notes/widget/NoteWidgetProvider.java +++ b/src/net/micode/notes/widget/NoteWidgetProvider.java @@ -32,6 +32,11 @@ import net.micode.notes.ui.NoteEditActivity; import net.micode.notes.ui.NotesListActivity; +/** + * B 阶段一阅读标注 胡嘉豪 202430224010 + * 两种桌面小组件共用的更新骨架:读取便签、绑定 RemoteViews 和点击行为。 + * 子类提供尺寸对应的布局、背景和类型;本包没有独立的小组件 Service。 + */ public abstract class NoteWidgetProvider extends AppWidgetProvider { public static final String [] PROJECTION = new String [] { NoteColumns.ID, @@ -47,6 +52,7 @@ public abstract class NoteWidgetProvider extends AppWidgetProvider { @Override public void onDeleted(Context context, int[] appWidgetIds) { + // B 阅读标注:删除桌面组件时解除数据库中的绑定,不删除便签内容。 ContentValues values = new ContentValues(); values.put(NoteColumns.WIDGET_ID, AppWidgetManager.INVALID_APPWIDGET_ID); for (int i = 0; i < appWidgetIds.length; i++) { @@ -58,6 +64,7 @@ public void onDeleted(Context context, int[] appWidgetIds) { } private Cursor getNoteWidgetInfo(Context context, int widgetId) { + // B 阅读标注:排除回收站便签;返回的 Cursor 由 update 方法负责关闭。 return context.getContentResolver().query(Notes.CONTENT_NOTE_URI, PROJECTION, NoteColumns.WIDGET_ID + "=? AND " + NoteColumns.PARENT_ID + "<>?", @@ -71,6 +78,7 @@ protected void update(Context context, AppWidgetManager appWidgetManager, int[] private void update(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds, boolean privacyMode) { + // B 阅读标注:广播回调中逐个刷新组件;数据库查询当前在回调线程执行。 for (int i = 0; i < appWidgetIds.length; i++) { if (appWidgetIds[i] != AppWidgetManager.INVALID_APPWIDGET_ID) { int bgId = ResourceParser.getDefaultBgId(context); @@ -83,6 +91,7 @@ private void update(Context context, AppWidgetManager appWidgetManager, int[] ap Cursor c = getNoteWidgetInfo(context, appWidgetIds[i]); if (c != null && c.moveToFirst()) { if (c.getCount() > 1) { + // B 阅读标注:这里的 return 会终止整批刷新,后续组件也不会更新。 Log.e(TAG, "Multiple message with same widget id:" + appWidgetIds[i]); c.close(); return; @@ -92,6 +101,7 @@ private void update(Context context, AppWidgetManager appWidgetManager, int[] ap intent.putExtra(Intent.EXTRA_UID, c.getLong(COLUMN_ID)); intent.setAction(Intent.ACTION_VIEW); } else { + // B 阅读标注:未绑定或便签不可见时,点击组件进入新建便签流程。 snippet = context.getResources().getString(R.string.widget_havenot_content); intent.setAction(Intent.ACTION_INSERT_OR_EDIT); } @@ -101,6 +111,7 @@ private void update(Context context, AppWidgetManager appWidgetManager, int[] ap } RemoteViews rv = new RemoteViews(context.getPackageName(), getLayoutId()); + // B 阅读标注:背景来自 ResourceParser 的颜色索引到资源 ID 的映射。 rv.setImageViewResource(R.id.widget_bg_image, getBgResourceId(bgId)); intent.putExtra(Notes.INTENT_EXTRA_BACKGROUND_ID, bgId); /** @@ -108,12 +119,14 @@ private void update(Context context, AppWidgetManager appWidgetManager, int[] ap */ PendingIntent pendingIntent = null; if (privacyMode) { + // B 阅读标注:隐私分支隐藏摘要;当前公开 update 重载固定传入 false。 rv.setTextViewText(R.id.widget_text, context.getString(R.string.widget_under_visit_mode)); pendingIntent = PendingIntent.getActivity(context, appWidgetIds[i], new Intent( context, NotesListActivity.class), PendingIntent.FLAG_UPDATE_CURRENT); } else { rv.setTextViewText(R.id.widget_text, snippet); + // B 阅读标注:以 widgetId 区分点击 PendingIntent,避免多个组件相互覆盖。 pendingIntent = PendingIntent.getActivity(context, appWidgetIds[i], intent, PendingIntent.FLAG_UPDATE_CURRENT); }