Files
Hearth/docs/superpowers/specs/2026-08-16-hearth-launcher-design.md
T

321 lines
15 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Hearth 启动器 · 设计文档
> 版本:v1.0
> 日期:2026-08-16
> 包名:`top.yeij.hearth`
## 1. 概述
Hearth 是一个面向瑞芯微 RK3566 横屏开发板(立创·泰山派)的 Android 启动器(Home)。
核心特色:**界面全 H5 渲染,原生只当「能力层」**,通过 JS Bridge 向 H5 暴露系统能力,
使得桌面 UI、卡片、webAPP 均可远程下发、动态扩展,无需重装 APK。
### 目标平台
| 项 | 值 |
|---|---|
| 硬件 | 立创·泰山派 RK3566 开发板 |
| 系统 | Android 11API 30 |
| 屏幕 | 横屏 800×480 物理像素,高 DPI(≈300+,自适应系统 density |
| 兼容策略 | `minSdk = 30`,仅针对该板,不做低版本兼容 |
## 2. 需求汇总(已确认)
1. **全 H5 渲染架构**:桌面 = 全屏 WebView 承载 H5,原生只提供系统能力(查应用/启动应用/媒体状态/下载缓存等)。
2. **全局左侧边栏**Miuix NavigationRail 形态,可切换(收起 80px 纯图标 / 展开 240px 图标+文字),
用于切换 5 个页面:首页 / 沉浸首页 / H5 应用列表 / 安卓 APP 列表 / 设置。
3. **首页**:左中右三栏卡片流。大字时间卡始终保留;媒体卡动态(有活动媒体才显示);
日历/天气/小工具填充富余空间。**首页无应用入口(不做 Dock),应用只从列表页打开**。
4. **卡片插件化**:每个卡片 = 独立 HTML + 一份 manifest(声明所需数据源/权限),可后期扩展、可远程下发。
5. **沉浸首页(二期)**:左 2/3 freeform 窗口跑第三方 App + 右侧卡片列(时间/媒体/天气)。
6. **列表页**:安卓 APP 列表 = 图标网格 + 搜索;H5 应用列表 = webAPP 富卡片(缩略图 + 在线/离线标签)。
7. **webAPP 下发**:JSON 清单 + 可选离线包,远程下发 + 本地缓存。
8. **UI 风格**Miuix / HyperOS 视觉(纯视觉,H5 用 CSS 还原,非 Compose 库)。
9. **状态栏**:系统默认沉浸式(全屏隐藏,顶部下滑一次展开,几秒自动收起)。
10. **夜间模式**:跟随系统(`prefers-color-scheme`)。
11. **横屏锁定**:固定横屏。
## 3. 架构
```
┌──────────────────────── Hearth APK ────────────────────────┐
│ MainActivity(唯一 Activity
│ ├─ 窗口管理:全屏沉浸式 / 横屏锁定 / 夜间模式 │
│ └─ WebView(全屏,加载 assets/index.html
│ │ window.HearthBridge (JS Bridge) │
│ ▼ │
│ ┌────────────────────────────────────────────────────┐ │
│ │ 能力层(原生 Kotlin) │ │
│ │ ├─ AppRepository 查应用列表/图标/启动 │ │
│ │ ├─ WebAppRepository 拉 webAPP 清单/离线包 │ │
│ │ ├─ WebAppContainer 多 WebView 管理(标签/导航) │ │
│ │ ├─ CardRepository 卡片清单拉取/缓存 │ │
│ │ ├─ MediaSessionSource 媒体状态监听 │ │
│ │ ├─ CacheManager 文件缓存(清单/离线包/图标) │ │
│ │ └─ JsBridge 暴露给 H5 的方法 │ │
│ └────────────────────────────────────────────────────┘ │
└────────────────────────────────────────────────────────────┘
│ 网络(webAPP / 卡片 服务器)
┌─ 服务器 ──────────────┐
│ manifest.json │ ← webAPP 清单
│ offlines/*.zip │ ← 可选离线包
│ cards/catalog.json │ ← 卡片目录
│ cards/<id>/card.html │ ← 卡片资源
└───────────────────────┘
```
### 模块职责(单一职责)
| 模块 | 职责 | 依赖 |
|---|---|---|
| `MainActivity` | 窗口、生命周期、横屏锁、沉浸式 | WebViewManager |
| `WebViewManager` | WebView 配置(内核设置、viewport、注册 JS Bridge | JsBridge |
| `JsBridge` | 所有 `@JavascriptInterface` 方法,H5 唯一入口 | 各 Repository/Source |
| `AppRepository` | PackageManager 封装:应用列表、base64 图标、启动应用 | 无 |
| `WebAppRepository` | 拉取 webAPP 清单、下载离线包、解析 | CacheManager |
| `WebAppContainer` | 多 WebView 管理:标签页打开/切换/关闭、前进后退、重载 | 无 |
| `CardRepository` | 拉取卡片目录、下载卡片资源 | CacheManager |
| `MediaSessionSource` | 监听活跃媒体会话,推送媒体元数据 | 无 |
| `CacheManager` | 文件读写缓存(清单/离线包/卡片/图标) | 无 |
### 数据流(核心路径)
1. **启动**MainActivity 全屏 → WebView 加载 `assets/index.html`(H5 桌面,随 APK 打包,保证离线可用)。
2. **渲染 APP 列表**H5 调 `listApps()` → 原生查 PackageManager → 回调 JSON(含 base64 图标)→ H5 渲染网格。
3. **渲染 webAPP 列表**H5 调 `fetchWebApps()` → 原生拉服务器清单 → 缓存 → 回调 → H5 渲染富卡片。
4. **启动 APP**:点击 → H5 调 `launchApp(pkg)` → 原生 `startActivity`
5. **打开 webAPP**:点击 → H5 调 `openWebApp(id)` → 原生新建内容 WebView(标签页)加载 URL,桌面 H5 顶部渲染浏览器式顶栏;回退/前进/重载/标签切换均经 JS Bridge 控制。
6. **首页卡片**H5 调 `fetchCards()` → 原生拉卡片目录 → 下载卡片 HTML + 数据 → H5 渲染三栏卡片流。
7. **媒体卡**:原生 MediaSessionSource 监听媒体变化 → 事件推给 H5 → H5 显示/隐藏媒体卡。
## 4. JS Bridge 协议
H5 通过 `window.HearthBridge` 访问原生能力。所有方法异步回调(`callbackId` 模式或 Promise 封装)。
### 4.1 H5 → 原生(方法调用)
| 方法 | 参数 | 返回 | 说明 |
|---|---|---|---|
| `listApps()` | 无 | AppInfo[] | 已装应用列表(包名/名称/base64 图标) |
| `launchApp(pkg)` | 包名 | void | 启动第三方应用 |
| `fetchWebApps()` | 无 | WebApp[] | 拉取/返回 webAPP 清单(含缓存逻辑) |
| `openWebApp(id)` | webAPP id | void | 打开 webAPP(新建内容 WebView 标签页 + 显示顶栏) |
| `closeWebApp(id)` | webAPP id | void | 关闭指定标签页 |
| `switchTab(id)` | 标签 id | void | 切换可见标签页 |
| `listTabs()` | 无 | Tab[] | 返回已打开标签列表 |
| `webGoBack()` | 无 | void | 当前标签回退 |
| `webGoForward()` | 无 | void | 当前标签前进 |
| `webReload()` | 无 | void | 当前标签重载 |
| `backToHome()` | 无 | void | 关闭全部标签,回到桌面 |
| `fetchCards()` | 无 | Card[] | 拉取/返回卡片目录 |
| `getDeviceInfo()` | 无 | DeviceInfo | 分辨率、density、深色模式等 |
| `launchInBounds(pkg, x,y,w,h)` | 二期 | void | freeform 启动到指定区域 |
### 4.2 原生 → H5(事件推送)
原生通过 `HearthEvents`JS 注入回调)推送事件:
| 事件 | 载荷 | 触发时机 |
|---|---|---|
| `media-session-changed` | MediaInfo | 媒体播放状态/歌曲变化 |
| `theme-changed` | `"light"\|"dark"` | 系统夜间模式切换 |
| `webapp-updated` | WebApp[] | webAPP 清单更新 |
### 4.3 数据结构
```jsonc
// AppInfo
{ "packageName": "com.x.y", "label": "音乐", "icon": "data:image/png;base64,..." }
// WebApp
{
"id": "cloud-music", "name": "云音乐",
"icon": "https://host/icon.svg", "url": "https://host/app/index.html",
"offline": { "package": "offline/cloud-music.zip", "version": "1.0.0" }
}
// MediaInfo
{ "title": "海阔天空", "artist": "Beyond", "album": "乐与怒",
"cover": "data:image/png;base64,...", "position": 120000, "duration": 245000,
"playing": true, "packageName": "com.x.player" }
// DeviceInfo
{ "widthPx": 800, "heightPx": 480, "density": 2.0, "darkMode": true }
```
## 5. 卡片插件系统
### 5.1 卡片包结构
```
cards/<card-id>/
├── card.html # 卡片 UI(独立 HTML 片段)
├── manifest.json # 声明数据源/权限
└── assets/ # 卡片私有资源(可选)
```
### 5.2 manifest 格式
```jsonc
{
"id": "media-card",
"name": "媒体卡片",
"version": "1.0.0",
"entry": "card.html",
"dataSources": ["time", "media-session"], // 需要的数据源
"permissions": ["MEDIA_CONTENT_CONTROL"], // 对应的系统权限
"priority": 1, // 优先级(数字小者优先占位)
"size": { "min": "1x1", "max": "2x2" } // 尺寸偏好
}
```
### 5.3 数据源类型
原生能力层提供的「数据源」,卡片通过 manifest 声明所需:
| 数据源 | 说明 | 权限 |
|---|---|---|
| `time` | 时间/日期 | 无 |
| `media-session` | 媒体状态 | `MEDIA_CONTENT_CONTROL` + 通知监听授权 |
| `weather` | 天气 | 网络 + 城市配置 |
| `calendar` | 日历 | `READ_CALENDAR` |
| `system` | 系统信息(内存/存储) | 无/部分 |
### 5.4 卡片加载与降级
1. H5 获取卡片目录 → 下载卡片资源到本地缓存。
2.`priority` 排序,三栏容器依次放置。
3. 卡片数据源未授权/无数据时,该卡片不渲染,由低优先级卡片(日历/天气)填充。
4. 大字时间卡为**内置卡片**,始终保留,不参与降级。
## 6. webAPP 下发协议
### 6.1 清单格式(manifest.json
```jsonc
{
"version": 1,
"updatedAt": 1723785600,
"apps": [
{
"id": "cloud-music", "name": "云音乐",
"icon": "https://host/icon.svg", "url": "https://host/app/index.html",
"offline": { "package": "offline/cloud-music.zip", "version": "1.0.0", "size": 1048576 }
}
]
}
```
### 6.2 下发与缓存策略
1. 启动/定时拉取清单 → 对比本地 `version`
2. 有新版本 → 下载清单 + 各 webAPP 图标/离线包。
3. 本地缓存失败降级:**有旧清单用旧清单,无清单显示空态 + 错误提示**。
4. webAPP 打开时:有离线包且离线 → 加载本地解压目录;否则加载远程 URL。
### 6.3 webAPP 打开行为(浏览器式顶栏 + 多标签)
- 打开 webAPP → 原生新建内容 WebView(标签页)加载,桌面 H5 顶部渲染浏览器式顶栏。
- 顶栏:回退 / 前进 / 重载(作用于当前标签);右侧标签页入口,可切换 / 关闭已打开标签。
- 每个标签页 = 一个独立 WebView,拥有独立前进后退历史。
- 关闭全部标签 = 回到桌面。
## 7. UI 设计
### 7.1 视觉规范(Miuix / HyperOS 风格,CSS 还原)
- **配色**:双色 token(CSS 变量),浅色/深色两套;深色模式背景纯黑 `#000`,卡片 `#151518`
- **圆角**squircle 平滑圆角(卡片 14px 左右)。
- **字体**:MiSans(远程字体或系统默认)。
- **强调色**:小米橙 `#ff6900`(选中态/进度条)。
- **侧边栏选中态**:橙色高亮药丸。
### 7.2 页面布局
| 页面 | 布局 |
|---|---|
| 首页 | 三栏卡片流:大字时间(始终)+ 媒体卡(动态)+ 日历/天气/小工具(填充) |
| 沉浸首页(二期) | 左 2/3 freeform 窗口 + 右卡片列(时间/媒体/天气) |
| 安卓 APP 列表 | 顶部搜索框 + 图标网格(图标+名称) |
| H5 应用列表 | 顶部搜索框 + webAPP 富卡片(缩略图 + 在线/离线标签) |
| 设置 | Miuix Preference 分组:显示 / 网络 / 关于 |
### 7.3 侧边栏
- 收起:80px,纯图标(无文字)。
- 展开:240px,图标 + 文字横排,选中项后橙色高亮药丸。
- 左上角按钮切换收起/展开。
- **非首页时**:侧边栏最顶部(Header 区域)显示一个小时间;首页时不显示。
### 7.4 夜间模式
- H5 用 CSS 变量 + `prefers-color-scheme` 跟随系统。
- 原生保证 WebView 跟随 `Configuration.uiMode`,无需额外处理。
### 7.5 webAPP 顶栏
- 左侧:回退、前进、重载三个图标按钮。
- 右侧:标签页入口按钮(点击展开标签列表,可切换 / 关闭)。
- 顶栏随 webAPP 打开而显示,关闭全部标签后隐藏。
## 8. 窗口管理
- **全屏沉浸式**`WindowInsetsController.hide(statusBars() | navigationBars())`,行为 `BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE`(系统默认:下滑一次展开、几秒自动收起)。
- **横屏锁定**`screenOrientation = landscape`
- **默认桌面**Manifest 声明 `HOME` + `DEFAULT` intent-filter,用户手动设为默认。
- **Back 键屏蔽**:桌面状态下拦截 Back 键(按返回无响应);webAPP 打开时 Back 先作用于标签页回退,无历史可回退则关闭标签。
## 9. 权限模型
| 权限 | 用途 | 授予方式 |
|---|---|---|
| `QUERY_ALL_PACKAGES` | 查询已装应用列表 | launcher 豁免,声明即生效 |
| `MEDIA_CONTENT_CONTROL` | 读取媒体状态 | 运行时 + 通知监听授权(用户手动) |
| `READ_CALENDAR` | 日历卡片数据 | 运行时授权 |
| `INTERNET` | 拉取 webAPP/卡片 | 声明即生效 |
## 10. 错误处理
| 场景 | 处理 |
|---|---|
| webAPP 清单拉取失败 | 用本地缓存清单;无缓存则空态 + 提示 |
| 离线包下载失败 | 标记「在线」,下次拉取重试 |
| 媒体权限未授权 | 媒体卡不渲染,降级日历/天气 |
| WebView 加载失败 | 错误页 + 重试按钮 |
| 系统 WebView 缺失 | 启动检测,提示(或引导安装) |
| 第三方 App 无法 freeform(二期) | 捕获异常,提示该 App 不支持小窗 |
## 11. 测试策略
- **TDD**:先写测试再写实现。
- **原生单测**Repository 层(mock PackageManager / 网络 / CacheManager)。
- **JS Bridge 协议测试**mock H5 调用,验证方法签名与返回结构。
- **H5 侧**:卡片/页面为纯静态,用轻量 JS 单测覆盖数据拼接与降级逻辑。
- **真机集成**:adb 安装到板子,验证 HOME、沉浸式、媒体权限、webAPP 拉取。
## 12. 技术栈与依赖
| 项 | 选择 |
|---|---|
| 语言 | Kotlin |
| SDK | minSdk 30 / targetSdk 30 |
| WebView | 系统内核(需上板验证存在) |
| 网络 | OkHttp |
| JSON | Gson |
| H5 | 纯 HTML/CSS/JS,无框架(SPA 手写) |
| 构建 | GradleAndroid Gradle Plugin |
## 13. 二期规划(不在本期实现)
1. **freeform 小窗**(沉浸首页):`FreeformWindowManager` 模块 + `launchInBounds` 能力。
- 前提:固件开启 `enable_freeform_support`,需上板验证。
2. 更多首页候选卡片(随卡片插件系统扩展)。
## 14. 明确不做(YAGNI
- 完整 kiosk 锁定(Home / 最近任务 / 系统手势全锁,仅 Back 键已做)
- 屏幕常亮管理
- 多用户 / Widget 宿主
- 应用图标拖拽 / 文件夹 / Dock