为 466 × 466 圆形表盘从零设计的 Wear OS 邮箱客户端
多账户统一收件箱 · IMAP 收信 · SMTP 发信 · 新邮件通知 · 手机扫码配置 · Outlook OAuth2
一个完整的、可编译运行的 Wear OS 邮箱客户端:手写 SQLite + JavaMail 收信发信、Android Keystore 加密保管凭据、局域网扫码从手机配置账户、Outlook/Office 365 走 OAuth2 设备码流。
项目不是"能跑就行"的演示:273 个纯 JVM 单元测试 + 22 项静态检查全绿,圆屏布局按几何公式推导, 每一处设计取舍与踩过的坑都写在文档里(包括至今仍未在真机验证的部分)。
技术文档(完整功能清单 / 构建 / 目录结构 / 安全说明 / 已知限制)见 docs/TECHNICAL.md。
| 🧮 圆屏几何当作数学问题 | 安全半径 210px、逐项按 Y 坐标计算弦长收窄列表项、纵向预算被写成静态检查:信息流可视带 < 100dp 或按钮热区 < 48px 会直接让 CI 失败 |
| 📱 手机扫码配置账户 | 手表起一个一次性 token 的局域网 HTTP 服务并显示二维码 → 手机扫码填表 → 凭据经 Keystore 加密落盘。手表上打字体验太差,这是最省事的路径;打开前会检查 Wi-Fi,微软邮箱会被拦下并引导到手表端 OAuth2 授权 |
| 🎛 按钮只有一半大 | 自绘 24dp 迷你胶囊(= 常规 52dp 按钮的一半,恰好等于 48px 触控下限),把 233dp 表盘的信息流可视带从 65dp 提到 123dp(约 3 行邮件) |
| 🔐 凭据不落明文 | AES-256-GCM + Android Keystore;OAuth2 的 refresh token 同样加密存储;日志门面在编译期之外还有静态红线检查 |
| 🔄 版本自动检查 | 启动时静默请求 GitHub Releases 并做语义化版本比较(不是字符串比较),有新版本弹窗提示、设置页可手动检查;失败不打扰、不拖慢冷启动 |
| 🧪 可测性优先 | 几何、HTML 剥离、地址解析、加密、OAuth2 轮询状态机、通知自检全部下沉为纯函数,无需模拟器即可回归 |
| 类别 | 说明 |
|---|---|
| 多账户 | Gmail / Outlook / QQ / 163 / 企业邮箱预设,支持自定义 IMAP/SMTP 主机、端口与加密方式 |
| 统一收件箱 | 所有账户按时间倒序合并、账户筛选、下拉刷新、未读标记与来源账户色点 |
| 邮件详情 | 纯文本正文(HTML 已剥离)、发件人/时间/主题、底部回复/删除/已读操作栏;可选开启 HTML 渲染 |
| 邮件 HTML 渲染(可选) | 设置里一键开启(默认关闭,性能足够的手表再用):详情页用 WebView 渲染原始排版,可在「HTML / 纯文本」间切换;禁 JS、默认禁网络(屏蔽追踪像素),HTML 先经清理 |
| 撰写发送 | 收件人(常用联系人快捷选择)、主题、正文、发件账户选择;失败自动存草稿,联网后补投 |
| 收信 | JavaMail;首次拉 50 封元数据、增量按 UID、IDLE 推送(支持时)、10 秒超时、指数退避重连 |
| 通知 | 「发件人 + 主题前 30 字」,点击直达详情;全局/账户级开关;内置测试通知与链路自检 |
| Outlook OAuth2 | 设备码流授权(手表显示短码 + 二维码,手机浏览器登录)+ refresh token 自动续期 |
| 缓存 | 元数据 500 封 / 正文 50 封,超出按 LRU 淘汰 |
| 检查更新 | 启动时自动比对 GitHub Releases 最新版本(haloged/Watch-Mail),有新版本弹窗提示;设置页「关于」可手动检查 |
| 安全 | Android Keystore 加密凭据、allowBackup=false、全程 TLS/STARTTLS、日志不写敏感值 |
- JDK 17(AGP 8.7 要求)
- Android SDK:
compileSdk 35/build-tools 35.0.0,在local.properties里配置sdk.dir - Gradle 8.11.1 由 wrapper 提供(首次构建会联网下载)
./gradlew test # 273 个单元测试(纯 JVM,无需设备)
./gradlew assembleDebug # 调试包
./gradlew assembleRelease # 发布包(R8 压缩+混淆+优化 + 资源压缩,约 3.48 MB)
./gradlew installDebug # 安装到已连接的手表
python tools/static_checks.py # 22 项静态检查(不需要 Gradle/SDK)Windows 下把 ./gradlew 换成 gradlew.bat。
仓库不含签名证书:
keystore/已被.gitignore排除。克隆后assembleDebug会自动使用 AGP 的 默认调试证书;需要自己的证书时用-Pwearmail.storeFile=... -Pwearmail.storePassword=...指定。
- 手机扫码(推荐):账户管理 → 添加账户 → 用手机扫码配置;手机与手表连同一 Wi-Fi, 扫码后填邮箱与授权码即可。
- 手表手动/自动探测:输入邮箱与授权码 →「自动探测」回填服务器参数 →「验证连接」→ 保存。
- Outlook / Office 365:必须用 OAuth2,见下节。
多数服务商需要先在网页端开启 IMAP/SMTP 并生成应用专用密码/授权码(QQ、163、Gmail 均如此)。
Tip
Release版本已配置
微软已对 Exchange Online / Outlook.com 停用 IMAP/SMTP 基础认证,所以 Outlook 账户只能走 OAuth2。 本项目已完整实现设备码流与令牌续期,但客户端 ID 需要你自己注册(没人能替你注册):
- 打开 Azure 门户 → 应用注册 →「新注册」;
- 「受支持的账户类型」选**「任何组织目录中的账户和个人 Microsoft 账户」**(个人 outlook.com 也能用);
- 「重定向 URI」留空 —— 设备码流不需要;
- 进入「身份验证」→ 底部「高级设置」→ 「允许公共客户端流」选「是」(不开启设备码流会被直接拒绝);
- 复制概览页的**「应用程序(客户端) ID」**,填到手表「设置 → Outlook OAuth2 → OAuth2 客户端 ID 配置」(租户保持
common)。
然后在「添加账户」里邮箱填 Outlook 地址、登录方式选 OAuth2 令牌 →「获取授权」→ 手机扫码打开授权页并输入手表显示的短码 → 授权成功后保存。
公共客户端 + 设备码流不需要客户端密码。权限在授权时动态申请:
offline_access、https://outlook.office.com/IMAP.AccessAsUser.All、https://outlook.office.com/SMTP.Send。 为什么不用"跳浏览器登录":手表没有可用的浏览器控件,而设备码流正是为输入受限设备设计的。
MainActivity ── HorizontalPager(设置 │ 统一收件箱 │ 账户管理)
└── 覆盖层:邮件详情 / 撰写 / 添加账户 / Outlook OAuth2 配置
core/AppContainer(手写依赖容器,无 Hilt/KSP)
├── data/ crypto(Keystore + AES-GCM)· db(手写 SQLiteOpenHelper + 5 个 DAO)· prefs · repo
├── mail/ ImapClient · SmtpClient · ServerProbe · HtmlTextExtractor · oauth/(设备码流 + 令牌续期)
├── sync/ SyncService / SyncEngine / SyncWorker(单飞 + 离线优先 + 指数退避)
├── notify/ NotificationCenter + 链路自检
├── pairing/ 局域网 HTTP 配置服务 + 二维码
├── update/ 检查更新(GitHub Releases + 语义化版本比较)
└── ui/ theme · kit(圆屏几何组件)· nav · screen/*
四个值得说明的取舍:
- 不用 Room / Hilt / KSP / navigation-compose:数量有限时手写更可控,也避免注解处理器拖慢构建、
增大手表上的方法数。路由用
HorizontalPager+ 覆盖层状态机。 - 可测性驱动分层:凡是能下沉为纯函数的逻辑(几何、解析、状态机)都不碰 Android API, 这样在没有设备/模拟器的环境里也能有 273 个测试兜底。
- 不可自动验证的部分写成静态规则:覆盖层透明度、Keystore 的 IV 约束、信息流纵向预算、 密文列写入语义、正文缓存三态、列表项坐标换算这六类"只在真机/联调暴露"的问题, 全部固化成静态检查并验证过规则对缺陷版本会 FAIL。
- 安全默认值:凭据只以密文落盘、令牌不进入任何
data class(避免toString()泄露)、 日志门面 + 静态红线、allowBackup=false、扫码服务用一次性 token 且离开页面立即关端口。
app/src/main/java/com/wm/wearmail/
├── core/ AppContainer · Logs · RotaryBus · WearMailApp
├── model/ Account · EmailMeta · Draft · ProviderPresets · OAuthTokens
├── mail/ ImapClient · SmtpClient · ServerProbe · MailError · oauth/
├── data/ crypto/ · db/ · prefs/ · repo/
├── sync/ SyncEngine · SyncService · SyncWorker
├── notify/ NotificationCenter · NotificationDiagnostics
├── pairing/ PairingController · ConfigWebServer · PairingPage · QrCodeRenderer
├── ui/ theme/ · kit/ · nav/ · screen/{inbox,detail,compose,accounts,settings}
└── util/ TimeFormat
app/src/test/ 21 个测试类 / 273 个用例
tools/ static_checks.py · test_summary.py · mirrors-init.gradle
docs/ TECHNICAL.md · ARCHITECTURE.md · UI_SPEC.md · TESTING.md
| 文档 | 内容 |
|---|---|
| docs/TECHNICAL.md | 技术与验证文档:完整功能清单、构建、目录结构、安全与隐私、已知限制、发布前检查 |
| docs/ARCHITECTURE.md | 分层结构、数据库表、同步策略、安全设计、OAuth2 授权与令牌续期、性能取舍、依赖矩阵 |
| docs/UI_SPEC.md | 圆屏坐标系与弦长表、纵向预算、按钮尺寸层级、覆盖层规范、六页布局、手势映射 |
| docs/TESTING.md | 测试策略与覆盖矩阵、静态检查、真实缺陷案例、未验证项、38 条真机验收清单 |
- 附件只做启发式标记(
multipart/mixed),不下载、不展示内容; - 文件夹管理:IMAP 侧有
listFolders能力,UI 目前只用收件箱;删除优先移到\Trash; - 左右滑删除邮件改为「长按菜单 + 二次确认」,因为该手势与顶层页面切换冲突;
- OAuth2 仅实现 Microsoft。协议层(
FormPoster+MicrosoftOAuth)是提供商无关的, 加 Google 只需再写一个同形状的类; release构建开启 R8,keep 规则已覆盖 JavaMail 的反射用法,但未做真机回归。
MIT
本项目为个人独立实现,与 Microsoft、Google 无任何关联;Outlook、Wear OS、Android 等名称与商标 归各自所有者。请自行确认你的邮箱服务商条款允许第三方客户端通过 IMAP/SMTP 访问。