Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/release-tauri.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
name: Release Tauri (cross-platform)
# meta: ensure Actions indexes this workflow on forks (no behavior change).
# 触发条件:
# - 推 v*.*.*-tauri 形式的 tag(与老 Swift 版的 vX.Y.Z 区分开,不冲突)
# - 手动 dispatch(用于测试构建,不发版)
Expand Down
1 change: 1 addition & 0 deletions openless-android/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
build/
81 changes: 81 additions & 0 deletions openless-android/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.openless.android">

<uses-sdk
android:minSdkVersion="26"
android:targetSdkVersion="34" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MICROPHONE" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />

<application
android:allowBackup="false"
android:fullBackupContent="false"
android:icon="@drawable/ic_launcher_foreground"
android:label="@string/app_name"
android:theme="@style/OpenLessTheme">
<activity
android:name=".MainActivity"
android:exported="true"
android:screenOrientation="portrait">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity
android:name=".QaPanelActivity"
android:exported="false"
android:screenOrientation="portrait" />
<activity
android:name=".SettingsActivity"
android:exported="false"
android:screenOrientation="portrait" />
<activity
android:name=".DictionaryActivity"
android:exported="false"
android:screenOrientation="portrait" />
<activity
android:name=".ModelListActivity"
android:exported="false"
android:screenOrientation="portrait" />
<activity
android:name=".HistoryDetailActivity"
android:exported="false"
android:screenOrientation="portrait" />
<activity
android:name=".ErrorDetailActivity"
android:exported="false"
android:screenOrientation="portrait" />
<activity
android:name=".ProcessTextActivity"
android:exported="true"
android:label="@string/process_text_label"
android:screenOrientation="portrait">
<intent-filter>
<action android:name="android.intent.action.PROCESS_TEXT" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="text/plain" />
</intent-filter>
</activity>
<service
android:name=".FloatingTriggerService"
android:exported="false"
android:foregroundServiceType="microphone" />
<service
android:name=".OpenLessInputMethodService"
android:exported="true"
android:label="@string/ime_name"
android:permission="android.permission.BIND_INPUT_METHOD">
<intent-filter>
<action android:name="android.view.InputMethod" />
</intent-filter>
<meta-data
android:name="android.view.im"
android:resource="@xml/openless_input_method" />
</service>
</application>
</manifest>
Empty file.
168 changes: 168 additions & 0 deletions openless-android/PORT_STATUS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
# OpenLess Android 迁移状态

本模块是 OpenLess 桌面端听写链路的 Android 原生重写版本。它不是逐文件照抄,而是把协议形状、状态流转、提示词行为、持久化结构迁移到 Android Java 实现中。

实施计划:

- `../docs/plans/2026-05-04-openless-android-full-port.md`
- `../docs/plans/2026-05-05-openless-android-ui-rebuild-execution.md`

## 源码映射

| 原始源码 | Android 端 | 状态 |
| --- | --- | --- |
| `openless-all/app/src-tauri/src/coordinator.rs` | `src/com/openless/android/AndroidDictationCoordinator.java` | 已迁移核心状态流:idle / start / listen / process / cancel |
| `openless-all/app/src-tauri/src/asr/frame.rs` | `src/com/openless/android/VolcengineFrameCodec.java` | 已迁移二进制帧编解码 |
| `openless-all/app/src-tauri/src/asr/volcengine.rs` | `VolcengineStreamingSession.java`, `VolcengineAsrProvider.java`, `SimpleWebSocket.java` | 已迁移流式 SAUC 生命周期 |
| `openless-all/app/src-tauri/src/asr/whisper.rs` | `WhisperAsrProvider.java`, `WavEncoder.java` | 已迁移 OpenAI 兼容转写路径 |
| `openless-all/app/src-tauri/src/polish.rs` | `OpenAiPolishProvider.java`, `OpenLessHttp.java` | 已迁移 OpenAI 兼容润色/问答请求 |
| `openless-all/app/src-tauri/src/types.rs` `PolishMode` | `PolishMode.java` | 已迁移 `raw/light/structured/formal` |
| `openless-all/app/src-tauri/src/types.rs` `DictationSession` | `HistoryStore.java` | 已迁移历史结构到 Android SharedPreferences JSON |
| 桌面词典持久化 | `DictionaryStore.java` | 已迁移词条结构、热词启停、命中计数 |
| 桌面凭据存储 | `SecureValueStore.java` | 已映射到 Android Keystore AES-GCM |
| 桌面热键 + capsule | `FloatingTriggerService.java` | 已映射为 Android 悬浮窗前台服务 |
| 桌面插入层 | `OpenLessInputMethodService.java`, `TextInserter.java` | 已映射为 Android IME 直接插入 + 剪贴板兜底 |

## 已完成

- 悬浮触发器已替代桌面端全局热键
- 支持单击开始/结束听写、拖动定位、长按取消
- Android 14 兼容的麦克风前台服务
- 16 kHz 单声道实时录音
- 火山 SAUC 流式 ASR
- Whisper 兼容批量 ASR
- OpenAI 兼容润色链路
- `原文 / 轻润色 / 结构化 / 正式` 四种模式
- 词典热词同时注入 ASR 上下文与润色提示词
- 词条支持:`id / phrase / note / enabled / hits / createdAt`
- 历史记录支持清空、单条删除、点击复制、长按问答
- 设置项已覆盖:
- ASR / LLM 配置
- LLM 提供商预设(Ark / DeepSeek / SiliconFlow / OpenAI / 自定义)
- 工作语言
- 翻译目标语言
- 悬浮胶囊开关
- 剪贴板兜底开关
- 问答历史开关
- 设置页已改为结构化控件:
- ASR provider 单选
- LLM provider 单选预设
- 模式多选
- 布尔值开关
- 主界面已具备:
- Android 诊断
- 提供商诊断
- 历史记录
- 词典入口
- 问答入口
- 翻译一次
- `显示悬浮胶囊` 设置已接入实际悬浮服务行为:
- 关闭后可隐藏悬浮气泡
- 仍可通过常驻通知执行开始/停止听写、翻译、问答
- 常驻通知已按状态动态裁剪 action:
- 空闲态显示 3 个核心入口
- 录音/处理中切换为取消、问答、停止
- 避免超过 Android 通知 action 的稳定显示上限
- 翻译已接入实际链路,并写入历史元数据
- OpenLess IME 激活时,可捕获目标包名并写入历史
- 问答已支持:
- 粘贴上下文
- 多轮会话
- 流式回答
- 语音提问
- 历史转问答
- Android `PROCESS_TEXT`
- 已吸收一轮新的 UI 视觉改造:
- `QaPanelActivity` 的卡片/气泡/间距样式已合入
- `FloatingTriggerService` 的悬浮气泡绘制已合入
- `MainActivity` 已手工吸收一部分安全的视觉细化(历史列表、权限诊断、按钮尺寸)
- 主界面已完成第一轮信息架构重组:
- 顶部增加分区导航:听写 / 历史 / 工具
- 听写页新增概览卡片,集中展示 ASR / LLM / 模式 / 历史 / 翻译目标
- 词典 / 问答快捷入口已收拢到工具页,减少主听写页按钮堆叠
- 设置已完成第一轮页面化迁移:
- 新增 `SettingsActivity`
- 主界面设置入口不再依赖长 `AlertDialog`
- provider / 语言 / 听写 / 问答配置已按分区重组
- 页面化重构继续推进:
- `QaPanelActivity` 已加入会话概览与设置/词典快捷入口
- `DictionaryActivity` 已新增并接管主界面词典入口
- 词典支持页面内新增、启停、删除、复制导出、剪贴板覆盖导入、清空
- `SettingsActivity` 已支持按分区深链打开,并补充相关工具入口
- `ModelListActivity` 已新增并接管模型列表展示,不再通过模型弹窗查看
- `HistoryDetailActivity` 已新增并接管历史详情展示,不再通过详情弹窗查看
- Manifest 对外标签已资源化
- 版本号已与桌面源同步,并在以下文件间做一致性校验:
- `package.json`
- `tauri.conf.json`
- `Cargo.toml`
- `verify.ps1` 已覆盖 APK 基本校验
- `verify.ps1` 现已支持自动发现 `adb` 并可通过 `-CheckDevice` 报告设备连接状态
- `deploy.ps1` 已新增,可在有设备连接时执行 APK 安装与可选启动
- `QA_CHECKLIST.md`、`RELEASE.md`、`STORE_SUBMISSION_CHECKLIST.md` 已建立并中文化
- 历史项收尾迁移继续推进:
- 长按历史项不再弹系统菜单,统一进入 `HistoryDetailActivity`
- `HistoryDetailActivity` 已补删除动作
- 错误展示收尾迁移继续推进:
- 新增 `ErrorDetailActivity`
- `MainActivity` / `QaPanelActivity` 的错误不再走 `AlertDialog`
- 页面反馈层收口继续推进:
- `MainActivity` 多处反馈已统一收敛到页内状态行
- `QaPanelActivity` 的未输入、语音空识别、复制回答/对话等反馈已改为页内状态
- `DictionaryActivity` / `HistoryDetailActivity` / `ModelListActivity` / `ErrorDetailActivity` 已补齐页内状态行
- `SettingsActivity` 已拆分为页面状态与诊断状态,不再依赖 `Toast` 反馈保存/诊断结果
- 已完成一轮模拟器页面验收:
- 已处理麦克风权限与通知权限弹窗
- `MainActivity` 听写分区可正常显示
- `MainActivity` 历史分区可正常显示筛选与空状态
- `MainActivity` 工具分区可正常显示诊断卡与快捷工具卡
- `SettingsActivity` 已修复启动时 `NullPointerException`,当前可正常打开
- `DictionaryActivity` 可正常打开
- `QaPanelActivity` 可正常打开

## 当前差距

- 尚未完成带真实凭据的完整听写/翻译/问答端到端录音验证
- 已确认本机可用 `adb` 路径:`C:\Users\16014\AppData\Local\Android\Sdk\platform-tools\adb.exe`
- 已确认可用模拟器:`emulator-5554`
- 发布签名、商店元数据、最终图标素材、商店截图仍未完全收口
- Android UI 已可用,但仍不是桌面 React UI 的完整视觉复刻
- 主界面和设置页已明显接近工具化产品,但尚未完整复刻桌面端完整设置/导航面
- 剩余 `Toast` 已只存在于 `FloatingTriggerService` / `AndroidDictationCoordinator` 的服务态无页面宿主链路
- 选中文本问答依赖 Android `PROCESS_TEXT`,无法像桌面端那样做完全通用的跨应用选区捕获
- Android 对跨应用上下文访问有限;非 IME 路径下的目标 app 元数据仍不稳定
- 提供商诊断目前以配置有效性检查为主,还不是完整实录音 round-trip 验证
- 火山流式链路仍需要带真实凭据的真机实测

## 当前构建

构建命令:

```powershell
.\build.ps1
```

输出:

```text
build\OpenLessAndroid-debug.apk
```

当前最新本地构建结果:

- 可成功编译
- 2026-05-05 本轮在以下改动后再次通过:
- `MainActivity` 分区骨架
- `SettingsActivity` 新增与 Manifest 注册
- 听写页概览卡片与工具入口整理
- `SettingsActivity` 初始化空指针修复
- `ErrorDetailActivity` 新增与错误页接管
- `HistoryDetailActivity` 删除动作补齐
- 页面反馈层收口:设置页/错误页去 `Toast` 化
- 通过 `apksigner verify`
- `aapt dump badging` 可确认:
- package:`com.openless.android`
- version:与桌面源同步
- launchable activity:`com.openless.android.MainActivity`
- IME 组件存在
- 麦克风 / 悬浮窗 / 前台服务 / 通知等权限已声明
77 changes: 77 additions & 0 deletions openless-android/QA_CHECKLIST.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
# OpenLess Android 验收清单

## 安装与启动

- 安装 `build/OpenLessAndroid-debug.apk`
- 启动应用,确认主界面可正常打开且不闪退
- 确认可见以下卡片:悬浮触发器、Android 诊断、提供商诊断、模式、转写结果、历史记录

## 设置与提供商配置

- 打开“设置”
- 保存一组有效或占位的 ASR / LLM 配置
- 重新打开“设置”,确认配置会持久化
- 切换剪贴板兜底、问答历史、悬浮胶囊等开关,重新打开后确认状态持久化

## Android 诊断

- 授予或拒绝麦克风权限后,确认状态会更新
- 打开悬浮窗设置后,确认 overlay 状态会更新
- 在 Android 13+ 上确认通知权限状态会更新
- 启用或切换键盘后,确认 IME 启用/激活状态会更新

## 悬浮触发器

- 授予悬浮窗权限
- 启动悬浮触发器
- 确认悬浮气泡出现
- 拖动气泡并确认位置会持久化
- 单击一次开始听写
- 再单击一次结束听写
- 长按取消本次听写
- 打开前台通知,确认存在这些操作:`剪贴板问答`、`问答面板`、`翻译`、`取消`、`停止`

## 听写

- 在有效的 ASR / LLM 配置下录一段短语音
- 确认原文转写与处理结果会更新
- 确认历史记录新增一条会话
- 如果命中了词典词条,确认命中计数会写入

## 直接插入与剪贴板兜底

- 启用 OpenLess 键盘
- 在目标输入框中切换到 OpenLess 输入法
- 执行听写,确认直接插入成功
- 关闭 IME、保留剪贴板兜底,再次听写,确认结果会复制到剪贴板
- 关闭剪贴板兜底,确认 IME 未激活时会清晰地报失败,而不是静默失效

## 翻译

- 设置翻译目标语言
- 使用“翻译一次”按钮或通知里的“翻译”操作
- 录制一段短语音
- 确认得到翻译后的输出,并写入历史记录

## 问答

- 打开“问答面板”,输入一个问题
- 使用“按住提问”,说出一个问题并松开
- 确认回答会流式更新
- 使用“剪贴板问答”处理已复制文本
- 从历史记录触发“问答”
- 在支持的应用中选中文本并选择 `OpenLess`,确认问答面板会带着该上下文打开

## 提供商诊断

- 运行“检测 LLM”
- 运行“检测 ASR”
- 运行“列出 LLM 模型”
- 确认报错会通过对话框、状态或 Toast 暴露,而不是静默失败

## 回归检查

- 将应用切后台后重新打开
- 确认悬浮触发器仍能正常启动
- 确认“清空历史”和“长按删除单条记录”都可用
- 确认关闭问答历史保存时不会引发崩溃
Loading