课程笔记、待办、语音转写、AI 总结。纯原生 Java 实现,零第三方依赖,APK 约 232 KB。 当前版本 1.8.1。
功能构成与交互设计参考了 @lkx478482771-star 的开源项目 「课堂整理」:课程、笔记、待办三块的功能划分、数据字段与整体交互取自原作者的设计,在此致谢。
代码不是在该项目上修改,而是按同样的功能目标在 Android 原生环境下重新实现——本项目为原生
Java + Android SDK,不含 WebView 与前端框架,也不依赖任何第三方库。参考项目采用 Capacitor +
WebView 方案,几项核心能力在 WebView 里无法工作(webkitSpeechRecognition 不存在,录音转写
整块失效;blob 下载静默失败,导出点了没反应),这也是改用原生实现的原因:
| 项目 | 参考项目(Capacitor + WebView) | 本项目(原生 Java) |
|---|---|---|
| 技术栈 | Capacitor + WebView + 前端 JS | 原生 Java + Android SDK,零依赖 |
| 数据存储 | localStorage(JSON 整存整取) | SQLite(增量读写 + 索引) |
| 语音识别 / 录音 | Web Speech API,WebView 不支持 | 系统 SpeechRecognizer + 原生 MediaRecorder |
| 导入导出 | a.download,WebView 里点不动 |
SAF 系统文件选择器 |
| APK 体积 | 3.7 MB | 约 159 KB |
后续的功能迭代与问题修复(云转写上传的 MIME 修正、Windows 构建脚本 build-pc.sh、语音识别
兼容、设置页重构等)见下方「更新日志」。
应用图标里的鹿是 "Deer" by Caro Asercion,来自 game-icons.net,
以 CC BY 3.0 授权使用(该授权允许商用,要求署名)。
本项目在原图基础上做了两件事:按品牌色(蓝 #4F7DF7 → 紫 #9A6BEE → 粉 #FF6FA8)重新着色,
并归一化到 Android 自适应图标的安全区。图形本身未改动。
素材出处与授权逐条记在 tools/icon-source/SOURCES.md,
生成流程见 tools/icon-from-svg.py 与 tools/make-icons.py。
- 课程:增删改查、自定义标识色、归档(上完的课收进「已归档」,数据保留、随时恢复),笔记与待办计数实时同步
- 笔记:标题 / 日期 / 正文 / 重点清单,支持置顶、展开阅读
- 搜索:首页一次搜全部课程的标题、正文与重点清单;课程页只搜当前课程
- 待办:优先级与截止日期,自动按「进行中 / 已完成」分组
- 待办提醒:给待办设一个提醒时刻,到点弹通知、点通知直接跳到那门课的待办页
- 全部待办:跨课程汇总未完成的事,按「已过期 / 今天 / 明天 / 本周内 / 以后」分段
- 语音转写:系统语音识别实时出字;设备不支持时自动把录音上传云端转写(OpenAI 兼容接口)
- AI 总结:接入任意 OpenAI 兼容服务或国内厂商原生接口(通义 / 文心 / 智谱等),生成摘要与重点,一键回写笔记
- 备份:zip 完整导入导出(含配图;包内
data.json仍是 classroom-v2 格式,与网页版互通),另可导出 Markdown;旧版.json也能导入(无配图,数据完整)
- 应用(国内可直连):https://lamireuxp.github.io/classroom-notes/dist/classroom-1.8.1.apk
- 发布页:https://github.com/LAmireuxP/classroom-notes/releases
从 1.0 起各版本签名一致,可直接覆盖升级,笔记数据不会丢。
系统语音识别能不能用,不只取决于麦克风权限。 国产 ROM 的识别服务普遍加了一道
「跨应用放行」判定,小米就是 AsrService 里的 CTA / 机型白名单(实测日志):
AsrService: handleCTAAndPerms: isCTAAllow=false isRecordPermGranted=true
↑ 麦克风权限是给足的
AsrService: onStartListening: isCTAAllow=false …
AsrService: onDestroy ← 服务不放行,随即自毁,App 收到 ERROR_SERVER_DISCONNECTED
同为 HyperOS 4 / Android 17(SDK 37)的两台机器结果就相反,可见这是按设备、而不是按 ROM 版本的策略:
| 机型 | OS 版本 | 结果 |
|---|---|---|
| Redmi M2007J3SC | OS4.0.0.7 | 实时识别正常出字 |
| Redmi 22127RK46C | OS4.0.0.26 | isCTAAllow=false 直接拒绝并自毁 |
被拒的机器上,唯一的识别服务是 com.xiaomi.mibrain.speech/.asr.AsrService(isDefault=true,
且没装任何其它 RecognitionService),所以「默认服务优先、显式组件兜底」两条路落在同一个服务上,
它不放行就没有第二条可走。系统设置里没有能让第三方 App 通过的开关,只能去厂商那一侧
同意一次:
| 厂商 | 同意入口 |
|---|---|
| 小米 / 红米 | 小爱同学 → 我的 → 设置(隐私 / 跨应用语音识别)。MIUI 未实测,判定机制与澎湃 OS 相同 |
| OPPO / 一加 | 小布助手 → 设置 |
| vivo | Jovi 语音 → 设置 |
| 华为 / 荣耀 | 小艺 → 设置 |
| 其他 | 系统设置 → 语音输入(android.settings.VOICE_INPUT_SETTINGS) |
同意状态记在 Settings.Global 的几个键上(xiaoai_cta_change、
soundrecorder_cta_net_accepted、soundrecorder_cta_permission_accepted),
取值随机型 / OS 小版本变化,键名也可能随版本变动,所以 App 不去读它们猜状态,
只在识别真的被拒时把用户送到语音助手那边。
App 里的对应行为:
- 识别被设备侧拦下(一次回调都没给过就被拒)时,录音页主动弹一个模态引导框(不是会消失
的提示条),主按钮「去同意」依次尝试:小米识别服务自带的授权弹窗
(
com.xiaomi.mibrain.speech.cta,实测可被外部唤起)→ 各家厂商语音助手(小米 / OPPO / 一加 / vivo / 华为 / 荣耀 / 魅族 / 三星)→ 系统「语音输入」设置页。这些入口全部写进了 manifest 的<queries>——targetSdk ≥ 30 的包可见性过滤会让没声明的包解析不到,点了等于没点。 - 引导是主动的:授权是恢复实时识别的唯一出路,不能赌用户看得见一条小提示。除了录音时弹框, 「设置 → 语音转写设置」底部还有一个常驻的**「打开厂商授权页」**入口——用户当场点了「稍后」, 之后也能自己找到地方,而不是这条路只响一次。
- 点「去同意」会清掉「被拦下」标记:授权很可能就在这一步完成,下次录音必须重新试系统识别, 不能被一次旧失败永久堵死。「拦下」标记本身也只记在进程内——重启 / 升级 / 换机后自动重试, 识别真的出过字也立刻解除;只记「零回调被拒」,偶发故障不记。
- 无论哪种设备,录音都不会因为识别失败被丢掉:识别用不了时会话降级成纯录音继续跑, 结束后自动走云端转写。
设置 → 语音转写设置,三选一:
| 模式 | 说明 |
|---|---|
| 关闭 | 只用系统语音识别,不保留录音 |
| 服务端转写 | 把录音上传到自建服务转写,填 http://192.168.1.10:8080/v1 这类根地址 |
| API 直连 | 把录音上传到云 API,填地址 + Key(OpenAI 兼容的 /audio/transcriptions) |
两个设置页都有「测试连接(GET /models)」:向 {地址}/models 发一次 GET,当场告诉你地址 / Key
通不通,不用录完一节课才发现 404。注意要填根地址而不是完整接口路径——/audio/transcriptions
是转写接口,纯对话 API(如 DeepSeek)不提供它,填了也只会 404。
自建服务用 whisper.cpp、FunASR 等任意实现了 OpenAI 兼容转写接口的服务都可以。
src/java/com/lamireuxp/classroom/,约 5500 行 / 21 个文件:
MainActivity:首页课程列表 / 搜索 / 统计 / 导入导出,课程菜单底部抽屉CourseActivity:课程详情——笔记 / 待办 / 录音 / AI 总结SpeechSession:语音会话——系统识别 + 原生录音,识别被拒时降级成纯录音VoiceAuth:厂商授权页入口链(小米 CTA → 各厂商语音助手 → 系统语音输入设置)Db/Backup:SQLite 数据层;JSON 备份导入导出、Markdown 导出(SAF)Net/AiProto:网络层与接口协议——AI 总结 / 云转写 / 连接自检(HttpURLConnection);四种协议的差异与厂商预设都收在 AiProtoPrefs:设置存储——主题、转写模式与地址 Key、AI 配置(含转写 → AI 的单向同步)SettingsActivity/TsSettingsActivity/AiSettingsActivity/BaseSettingsActivity:设置页与公共基类Ui/Dialogs/Icons/Tip/Loading/ThemeSwitch:设计系统与控件(色 token、弹窗、图标、底部状态药丸、加载条、主题开关)Dates/Id/Extract:日期、ID 生成、本地关键词提取(AI 不可用时的兜底)
- 图标换回第一版:应用内顶栏标志从设计稿的「鹿角 + 话筒 + 声波」换回第一版的
五角星(
ic_brand删除);启动图标换回 1.0 的未经量化版本(图形与 1.3 起发布的 完全一致,画质更准)。顺带恢复第一版有、1.2.1 被误删的ic_stats柱状图图标备用。
- 界面整体重构(按 DESIGN.md 设计系统):学习 awesome-design-md 收录的
Notion / Apple / Linear 三家 DESIGN.md 后为本 App 定稿
docs/DESIGN.md, 并按它换血——浅色模式从冷白紫蓝改成 Notion 暖纸系(暖白底#FAFAF8、 暖炭字#37352F),深色改成中性深灰(不用纯黑);层级改用表面阶梯表达, 全面去卡片阴影(阴影只留给 FAB 与对话框);首页顶栏改大标题式 (动作行 + 28sp 大字),课程卡的色点改成 Notion 式左缘色条。 - 笔记编辑页推倒重做(沉浸式):去掉「标签 + 输入框」的表单结构——标题 26sp 大字直接写、日期弱化小字点选、正文 17sp 宽行距铺满、重点/配图全靠留白 分隔;底部长驻工具栏(日期 / 重点 / 图片直达),保存统一在顶栏 ✓。
- 待办功能详细化:待办支持编辑(原来建错只能删了重来,现在课程页与 「全部待办」页点任意一条进编辑页);新增备注字段(作业要求、考试范围, 列表行上直接露摘要);记录完成时刻(已完成分组显示「完成于 X」); 编辑保存后提醒自动重排。数据库 v5→v6,老库就地升级。
- 新增
docs/DESIGN.md(本项目设计系统文档:七条原则、表面阶梯、字号阶梯、 组件约定),后续改 UI 照着它来。
- 备份纳入配图:1.6 加了笔记配图,但 JSON 备份只覆盖文本——换机/重装后笔记内容
回来了,配图全丢。这一版把备份从纯
.json改成.zip:包内data.json(仍是 classroom-v2 格式,与网页版互通)+note-img/下被引用的配图文件。导入时按文件头 判断格式——新 zip 含配图,旧.json也能导(无配图,数据完整)。只打包被引用的 图片,孤立文件不带。 - 设置页「导出 JSON 备份」/「导入 JSON 备份」改成「导出备份」/「导入备份」,导出
文件名
.json→.zip。 - 课程归档:上完的课从课程菜单里「归档课程」,退出首页列表、搜索、统计、
「全部待办」,提醒跟着安静;数据一个不少,首页底部「已归档课程」入口随时进去翻、
一键恢复(回到原来的排序位置)。归档状态跟着备份走(导入导出都保留);在归档
列表里点开归档课时,标题带「(已归档)」标记。数据层 DB v4→v5(courses 加
archived列,老库自动就地升级)。
- 新建/编辑笔记、新建待办改成独立页面:原来是小弹窗,笔记的正文与重点清单在弹窗里 可写空间太小。现在和设置页同一套骨架(顶栏 + 滚动正文 + 底部保存按钮),长文本给足 书写空间;标题必填校验不过时页面不关、已填内容不丢。待办的优先级点选与提醒选择器 照旧,选了只重建表单那一块。
- 拖动排序推倒重做:1.5 的拖动走的是框架的
startDragAndDrop,真机上三个问题叠在 一起——起拖即崩(拖动阴影的尺寸取不到,系统直接抛「Drag shadow dimensions must be positive」)、拖到列表末尾松手落不了位(被拖的行隐藏后列表缩短,指针已在容器之外, 系统按「取消」处理)、待办行完全拖不动(行本身没有点击行为,按下时触摸流被 ScrollView 截走,长按后影子冻在原地)。这一版改为容器自己拦截触摸流:长按起拖、 浮动卡片跟手、原行就地转 INVISIBLE 占位(列表总高不变,空档始终看得见)、其余行滑开 让位;落点按槽位判定,组内首尾都能到达;拖到边缘自动滚动;落库顺序在松手那一刻 从视图序列现读,不会与看到的不一致。 - 首页课程列表也能长按拖动排序:数据层的
sort字段当初就留好了,只差界面这一层。 - 界面细节:首页课程列表改卡片式(圆角 + 微弱浮起 + 按压回弹);列表入场从「咣当 拍上来」变成淡入 + 上移的「浮入」(错峰,封顶 180ms);顶栏补一条极细分隔线;搜索框 带清除按钮;空状态图标垫一层 8% 主题色;首页课程数与笔记数统一成同一段弱化文字。
- 修复回收站操作后页面空白:清空 / 彻底删除 / 恢复之后代码先
render()再buildUi(), 而buildUi()会重建整个内容视图——刚画好的内容被丢掉、它自己又不渲染,页面就空白 在那里,直到退出重进。这是 1.5 就存在的问题。 - 换主题不再闪:课程页跟随系统切深浅色时从
recreate()降为就地重建;窗口层 (状态栏 / 导航栏)的重着色统一收进基类——课程页原来漏了这一步,主题在别的页面被 改过时回来自动着色是失效的。 - 搜索栏不再随列表一起重建:原来每增删一门课、每次回到首页,输入焦点和软键盘就 丢一次;现在它只在进页面时建一次。
- 笔记与待办支持长按拖动排序:长按某一行拿起来——那一行变成一张带阴影的浮动卡片
跟着手指走,原行就地隐藏、列表实时收拢让位;拖过哪一行,其余行就滑开让出位置
(160ms 的过渡);松手即落位,不重绘(视觉顺序已经是最终顺序,所以没有跳变)。
拖到列表上下边缘会自动滚动,否则长列表里根本拖不到远处。排序落在数据库的
sort字段上,退出重进、换设备都还在。 第一版是「被拖的行压暗待在原地、落点描个高亮」——能用但一点也不丝滑:手指带着一整行 系统影子、列表纹丝不动、松手整页重绘,三件事叠起来就是「拖着一个色块,松手咣当一下」。 这一版改成上面那样,三个要点: ① 起拖瞬间先截图再隐藏——GONE 的视图 draw 出来是空白,顺序反了影子就是空的; ② 只在拖动期间挂 LayoutTransition——它会让容器里所有子 View 的增删都带动画, 一直挂着的话正常重绘也会慢半拍; ③ 松手不重绘——隐藏的那一行就是空档,列表的视觉顺序已经是最终顺序。 几处其它的取舍: ① 只在同组内可拖——笔记的「置顶 / 未置顶」、待办的「进行中 / 已完成」各自成组。 列表排序是「分组字段 DESC, sort ASC」,跨组改 sort 不会改变实际位置,用户只会看到那一行 「弹回去」,比直接不许拖更让人困惑,所以跨组不给高亮、也不生效。 ② 搜索过滤时关闭拖动:列表只剩匹配项,重排会把没显示出来的条目的 sort 写乱。 ③ 新条目仍然出现在最上方(拿当前最小 sort 减一),保住加拖动之前的行为—— 否则长列表里新建完还得往下翻去找它。没拖过的老数据 sort 全是 0,回退到created DESC, 所以升级后顺序完全不变。 实现上没有引入任何依赖:ItemTouchHelper属于 androidx.recyclerview,引进来会同时破坏 「零第三方依赖」和当前「直接对着 android.jar 编译、不走 Gradle」的构建方式, 所以用框架自带的startDragAndDrop+OnDragListener自己实现(见DragSort.java)。
-
换用「侧视鹿头 + 声波」图标:原来的图标(鹿角 + 话筒 + 两对声波,正面构图)在 512 尺寸下好看,但启动器实际显示的是 48dp——那里最外一对声波只有 0.5px,会直接消失成灰雾, 话筒又是白填充描边、在白底上融进背景。根本原因是三组元素在 48dp 下只能读成 「一坨带角的紫色东西」。中途试过整只侧视鹿,问题变成元素太多:腿、蹄、尾在 48dp 上 全是噪点,立姿的直腿也显得生硬;而且整只鹿的高度被腿占掉,头反而小。 最终只留头 + 颈 + 一点肩,另加两道粗声波贴在嘴前表达「音」:两道都够粗 (48dp 下约 1.2~1.5px)不会断;也不另画话筒,多一圈实心色块等于把 48dp 的问题搬回来。 裁剪高度由脚本自动判定(找「宽度突增」的那一行 = 身体起点,再多留一点肩胸, 否则头会像悬空吊着)。鹿的素材取自 game-icons.net 的 “Deer”(作者 Caro Asercion, CC BY 3.0,见上方「图标与素材授权」),按品牌三色渐变重新着色;裁剪与声波是本项目做的。 顺带把图标资源从 98 KB 压到 45 KB:渐变图形产生的独特色数以万计,直接存真彩 PNG 很大, 量化到 256 色后肉眼无差(逐像素核过,且没有引入色带),APK 因此比 1.3 版还小。
-
首页顶栏换回原标志:顶栏那枚五角星换成设计稿里那个「鹿角 + 话筒 + 声波」—— 应用图标换成侧视鹿是为了在 48dp 的启动器里立得住,而顶栏是应用内标识,旁边就是 「课堂整理」四个字,不承担「在三十个图标中被认出来」的压力,原设计在这里反而更有辨识度。 实现上转成了 Android 矢量(
tools/svg-to-vectordrawable.py):原设计是描边渐变,而 VectorDrawable 不支持描边渐变,好在应用内图标本来就用SRC_IN整体套主题色、渐变没有意义, 改成纯色即可转矢量。外圈两道声波的opacity=0.7转成了strokeAlpha,SRC_IN只换颜色不换 alpha,那层「弱化」保住了。 -
图标生成流程脚本化:
tools/icon-from-svg.py(任意 SVG → 品牌渐变,含剥满画布底色) 、tools/icon-deer-sound.py(鹿 + 自绘声波合成品牌图形)与tools/make-icons.py(→ 5 个密度的自适应前景 + 旧版方形 + 旧版圆形);共用逻辑在tools/iconlib.py。 以前换一次图标要手算三种产物、五种密度的尺寸,现在两条命令。 脚本带尺寸自检:旧版图标必须按 48dp 生成,混用 108dp 会让像素多出 5 倍、密度桶对不上, 而且图形比例看着完全正常,只有比对尺寸才能发现。 -
回收站:删除不再是真的删掉。课程、笔记、待办都先进回收站(设置 → 回收站), 可以逐条恢复,也可以彻底删除 / 清空。整门课删掉时,它的笔记与待办会跟着一起进回收站, 恢复课程能把它们原样凑回来——做法是级联删除时给整批写同一个时间戳,恢复时按它捞回一组, 这样既不会漏掉随课程删的笔记,也不会顺手复活用户此前单独删过的那些。 回收站里的内容不参与统计、搜索、待办汇总,也不会触发提醒。 两个连带问题也处理了:删除时撤掉闹钟(否则到点会弹一条已经不存在的任务); 先单独恢复一条笔记、而它的课程还在回收站里时,把课程一起恢复—— 否则这条笔记「活着但无处可去」:课程列表按删除标记把它藏了,从搜索结果点进去 又因为课程取不到而直接关页面。确认框与提示语里的「不可恢复」也一并改掉了, 现在会写明「删掉的内容会放进回收站」,并指路去哪恢复。
-
待办提醒:新建待办时可以设一个提醒时刻(系统的日期 + 时间选择器,不用手输格式), 到点弹通知,点通知直接进那门课的待办页。几处容易漏的地方都处理了:勾掉待办、删待办、 删课程都会顺手撤掉对应的闹钟与已发出的通知(否则到点会弹一条已经不存在的任务); 开机后重排(
AlarmManager的闹钟不跨重启,且部分 ROM 在被「强制停止」时也会清掉它, 所以主页面每次启动再兜底重排一次);导入备份后重排(导入是整体替换,旧闹钟全作废)。 提醒时刻也写进待办行的副标题,设没设上一眼可见。 故意没用精确闹钟:setExactAndAllowWhileIdle要SCHEDULE_EXACT_ALARM,而 Android 14 之后 这个权限对新装应用默认是拒绝的,被拒之后提醒会彻底不响——比晚几分钟糟得多。 改用setAndAllowWhileIdle,不需要任何权限且允许在 Doze 里触发,代价是系统可能挪后几分钟。 -
全部待办页:跨课程汇总所有未完成的待办,按截止日期分成 已过期 / 今天 / 明天 / 本周内 / 以后 / 无截止日期 六段,段内高优先级在前—— 回答的是「我接下来要交什么」,而不是「我做了多少」。入口是首页那张统计卡(整卡可点, 右侧加了「全部 ›」);没有待办时这张卡也照样出现,否则这个页面就没有入口了。 每行带课程色点与课程名,点进去到那门课的待办页签;勾选框就地完成。
-
待办排序按截止日期:原来课程内的待办只按创建时间倒序,最近记的排最前, 最紧急的反而沉在底下。现在「全部待办」页先把没有截止日期的排到最后,再按日期升序。
-
修好首页那个一直没生效的搜索框:它画在那儿、能输入,但一个字都不搜——
query只有 三处出现(声明、回填、赋值),没有任何一行拿它去过滤,Db里也压根没有跨课程的查询, 当初是界面先画了、数据层没跟上。现在补齐Db.searchNotes()(一条 LEFT JOIN 扫全库, 标题 / 正文 / 重点清单任一命中),首页接上结果列表:每行给出所属课程名与课程色点 (跨课程搜出来的标题不标明出处,用户得逐个点进去确认,反而比翻列表更慢)、日期与正文摘要。 搜索态是「专注模式」——统计卡与课程列表同时隐去,免得分不清哪块是结果。 -
搜索结果可以直接跳到位:点一条结果会打开所属课程,把那条笔记展开并滚到眼前, 而不是只跳到课程页让用户自己再找一遍。滚动用
onPreDraw拿布局后的真实坐标异步做, 坐标按父链逐级累加到 ScrollView 的直接子节点(getTop()是相对父容器的,只取行自己的 会漏掉 TabBar 与搜索框那一段高度)。 -
课程页的搜索也覆盖重点清单:原来只查标题与正文,「中值定理」这类只写在重点里的词 搜不到;现在两处共用同一套匹配范围,同一个关键词在首页和课程页结果一致。
-
构建脚本不再吞掉编译错误:
build-pc.sh的 javac 那一步原来写了2>/dev/null, 本意是挡掉「使用了过时的 API」这类无害提示,代价却是编译错误也一起没了——脚本只剩一句 退出码 1,报错信息一个字都看不到,只能手工照着那行命令重跑一遍。现在提示原样输出。
- AI 总结支持四种接口协议:原来只认 OpenAI 那一种形状(
POST {地址}/chat/completions+ Bearer +choices[0].message.content),等于只能连 DeepSeek 那类服务。现在可选 OpenAI 兼容 / 通义千问(原生)/ 百度文心 / 自定义,字段跟着协议变。各家的差异比想象中大: 通义的消息在input.messages、正文在output.text;文心要先拿 API Key + Secret Key 换 access_token(参数挂在查询串上)、模型名嵌在 URL 路径里、system是顶层字段,而且 出错也返回 HTTP 200——只看状态码会把「Access token invalid」当成一次成功的总结。 自定义协议可以自己填请求路径、鉴权方式与取值字段,覆盖没有预设的厂商。 - 13 条厂商预设:DeepSeek / 通义(兼容与原生)/ 智谱 / Kimi / 混元 / 豆包方舟 / 讯飞 / 百度(千帆与老接口)/ 硅基流动 / OpenAI / 自建,地址都实测过(无 Key 请求返回 401 说明 路径存在);拿不准的模型名不硬塞,只写进备注,配合下面的「读取可用模型」挑。
- 读取可用模型:填好地址和 Key 后直接拉
GET /models列出清单(带过滤框),点一个填进 模型框。厂商的模型名会下架——智谱的glm-4-flash现在就已经查不到,照旧文档填会一路 报「模型不存在」。同一个账号里不同档位的可用性也不同(有的会回「余额不足」)。 - 还没有 API Key?:按厂商列出控制台地址,点一行用浏览器打开,省得去搜「XX 的 Key 在哪申请」。页面上写明 Key 只存在本机私有设置里、不会上传。
- 待办进度按优先级分行:主页面统计卡每行一个优先级(色点 + 「高 0/1」+ 进度条), 颜色为高 = 红 / 中 = 主题色 / 低 = 绿;每门课程的待办页也有同一套,只统计当前课程。 顺带修掉一个存在已久的问题:原来的单色进度线无论完成多少都画满——它用一个「权重 = 完成比」的子 View 表示进度,而 LinearLayout 是按权重之和分配剩余空间的,独苗子 View 权重再小也会吃掉整段。
- 「新建」统一成右下角加号:课程页原来靠页签下面两个文字按钮(新建笔记 / 新建待办), 现在和主页面一样是右下角加号,点一下建当前页签对应的东西;课程页的搜索框也移到页签下面 (搜索只作用于当前那一类内容)。主页面则把搜索框挪到待办进度前面。
- 优先级改成点选:新建待办原来要在输入框里敲
high / medium / low,敲错一个字母会被 静默改写成「中优先级」,而且那套颜色也没法在输入框里表达;现在是横排三段加对应颜色的圆点。 - 全局纵向收紧:纵向间距统一 ×0.78(左右留白不动,横向的呼吸感保留),可点区域高度 保底 44dp。同时修掉顶栏重复留状态栏高度的问题——系统已经按状态栏把内容让开了,代码里 再补一次就是两份留白,整页凭空低 30dp。
- 构建链两处修复:d8 把每个
.class都当参数传,class 一多就撞上 Windows 命令行长度 上限(报出来只有一句「命令行太长」),改成先打成 jar 再交给它;签名指纹核对那行grep SHA256一直是空操作——中文环境下 keytool 打印的是「证书指纹 (SHA-256)」(带连字符), 而且它的输出不是 UTF-8,grep 会当二进制只回一句 "Binary file matches"。
- 两个 API 设置不再重复填:保存「语音转写设置 → 服务端 / API 直连」时,默认把服务地址和 Key 单向同步给「AI 总结」设置(模型名不动,各自保留)。同步带开关可关;AI 总结里的修改 不会反向影响转写。转写 Key 留空时同步也不覆盖 AI 总结已有的 Key。
- 新增「测试连接」:两个设置页都有,向
{地址}/models发一次 GET,当场告诉你 地址 / Key 通不通,不用录完一节课才发现 404。没填 Key 的 401 会明确说「服务在线,就差 Key」。 - 转写 / AI 报错全部翻译成人话:UnknownHost、证书、超时、明文拦截等网络异常不再抛英文原文; 404 会提示「转写走 /audio/transcriptions,纯对话 API(如 DeepSeek)不提供这个接口」—— API 直连跑不通,多数就是这个原因。
- 语音授权改为主动弹出:识别被厂商策略拦下(小米 CTA / 机型白名单这类,表现是给了麦克风权限
但识别服务零回调直接拒绝)时,不再只在一条会消失的提示条上挂「去授权」按钮——录音页主动弹模态
引导框,一键把授权页拉起来;「设置 → 语音转写设置」底部同时加了常驻的「打开厂商授权页」
入口,当场点了「稍后」的用户之后也能自己找到。授权入口新增小米识别服务自带弹窗
(
com.xiaomi.mibrain.speech.cta)为第一优先,厂商语音助手兜底,并补进 manifest<queries>; 点「去同意」后清除「被拦下」标记,下次录音重新试系统识别。
- 重做轻提示:原来每次操作反馈都是一条通栏色带(占满屏宽、8dp 直角、48dp 大方块关闭按钮), 一句「课程已创建」也要占近 80dp 高的紫色横幅,又重又丑。改成底部居中的状态药丸: 宽度跟内容走、16dp 圆角;成功 = 深色(深主题为浅灰)药丸 + 绿对勾,错误才整条用 error 色, 颜色只花在该注意的地方;同屏只留一条,新的顶掉旧的,不再叠罗汉。
- 操作反馈统一成语义态:所有「写入完成」提示(创建 / 更新 / 删除 / 保存 / 导入导出)统一走 成功态绿勾,不再和中性信息混用一种样式。
- 修复课程菜单抽屉选完不关:点「更多 → 删除课程」时底部抽屉不关,确认框关掉后会露出一层 内容对不上的 stale 抽屉。现在选中动作即收起抽屉。
这一版是代码清理与写入路径优化,没有改动任何界面与功能行为——你在 1.2 里看到的东西, 1.2.1 一模一样。
- 修复「按下缩放」会挤掉触摸监听:
Ui.pressScale()原来用OnTouchListener实现按下缩放。 Android 没有getOnTouchListener(),谁先setOnTouchListener谁就永久占住这个位置, 第二个想监听触摸的人只能把它顶掉——两个需求只能活一个。改成StateListAnimator(挂在state_pressed上的系统正规机制),完全退出触摸链路,涟漪 / 点击 / 将来任何手势都不再冲突。 行为肉眼无差别。 - 批量写入优化:
- 单条写入
saveCourse/saveNote/saveTodo不再「先 SELECT 判存在」,改用update()返回的 受影响行数判定(rows == 0即不存在):每条语句少一次查询,同时消除了「查完到写之间 那行被删掉」的竞态。 - 新增
Db.replaceAll():导入专用,清空 + 重建全程一个事务,用预编译语句批量写,且完全不查 存在性。顺带去掉了外层事务里嵌套内层事务:deleteCourse()自带事务,嵌在外层里一旦失败 只回滚内层、外层照常提交,会留下半新半旧的库。 - 实测(10 门课 / 400 条笔记 / 100 条待办):82.8 ms → 39.2 ms,约 2.1×。
- 单条写入
- 清理死代码:删掉 23 个无调用点的方法、未引用的资源与失效 import,净减约 124 行, APK 无功能变化。
- 修复录音丢失:识别服务中途失效(被拒 / 连接断开 / 启动抛异常)时,会话降级成纯录音继续跑,
已经录下的音频不再被删。原来这类错误一律按致命处理,调用方
cancel()会把录音文件一并删除—— 识别出的毛病,赔进去的是用户整段录音。真机对照:旧版在识别被拒后 93 毫秒就MediaRecorder: stop+并删文件,新版继续录到用户手动结束。 - 修复导入备份丢数据:导入改成「先全量解析、再单事务清空重建」。损坏的备份文件不再清空已有数据
(原来先删后写,文件里任意一条记录不合法,用户拿到的是「导入失败」加一个已经被清空的库),
并校验
format字段、修掉空 id 在主键上互相覆盖的问题。真机对照:同一个损坏文件,旧版把课程 清空并替换成半个文件,新版提示「第 2 门课程不是有效记录」且数据一字未动。 - 设备侧识别放行:识别被厂商策略拦下时(小米 CTA / 机型白名单,见上文),提示条给「去授权」
入口直达厂商语音助手,并把「拦下」记入进程,后续录音不再反复戳识别服务;文案不再引导用户去
系统设置里改默认识别服务(那条路在这些机型上走不通)。新增
VoiceAuth与 manifest<queries>声明。 - 提示条只在真的有出路时才挂按钮:「还能继续录」的那支不再错挂「去设置」。
- 修复语音识别无法启动:Android 11 起启用的包可见性过滤会让应用查不到系统识别服务,
导致识别直接不可用。补上
<queries>声明,并改为「系统默认识别服务优先、显式组件兜底」, 兼容国内 ROM 未设置默认识别服务的情况。 - 修复误导性的权限提示:原来把识别服务返回的错误码直接说成「缺少麦克风权限」, 而实际权限是给了的。现在会先自查权限,真缺才说缺;识别服务无响应时如实提示, 不再出现「界面显示正在录音、实际谁都没在录」的假面板。
- 新增主题模式:跟随系统 / 浅色 / 深色,默认跟随系统。顶栏开关改为按下即生效。
- 设置改为独立页面:不再使用底部抽屉,AI 设置与语音转写设置同样拆成独立页面。
- 界面与动效:统一缓动曲线与时长档位、按钮圆角改为 8dp、分段控件选中态重做、 笔记行补展开箭头、录音状态改为脉冲指示、联网期间补加载条。
- Windows:
./build-pc.sh(需 JDK 17 + Android SDK build-tools 34) src/build.sh是早期在手机上直接构建的旧配方(签名口令已失效),留作参考,不是发布路径。
所有版本(1.0 起)均使用同一把签名密钥,用户可正常覆盖升级。
| 项目 | 值 |
|---|---|
| 文件名 | keystore.jks |
| 密码(storepass / keypass) | classroom-v2 |
| alias | classroom |
| 算法 | RSA 2048 / SHA256withRSA |
| 有效期 | 2026-09-20 → 2056-09-12(30 年) |
证书指纹(SHA-256)—— 发布前请核对:
32:A8:95:D3:77:4D:CA:F7:83:AB:4B:F5:1A:09:7E:E0:6C:B3:63:24:64:5B:87:DA:82:C9:8C:35:09:7E:E8:B0
核对命令:
keytool -list -keystore keystore.jks -storepass classroom-v2 | grep -a -i 'SHA-256\|SHA256'
apksigner verify --print-certs build/课堂笔记-v*.apk | grep 'SHA-256 digest'中文环境下 keytool 打印的是「证书指纹 (SHA-256)」——带连字符。只 grep
SHA256会一条都搜不到,检查看起来「跑过了」但其实什么都没核对。输出里的中文也不是 UTF-8,-a让 grep 按文本处理,否则只会回一句 "Binary file matches"。
⚠️ 密钥必须长期存档(云盘 + 本地各一份)。 Android 只允许「签名一致」的 APK 覆盖升级;一旦密钥丢失,此后所有版本都变成 新签名,用户必须卸载重装并会丢失数据(除非先导出备份)。
⚠️ build-pc.sh在找不到keystore.jks时会静默生成一把新的。 密钥若因故不在原目录,跑脚本会悄悄换掉签名,而apksigner verify不会报错 —— 它只验签名有效性,不比对历史指纹。发布前务必按上面的命令核对指纹。