first commit
This commit is contained in:
commit
669f49b1d6
342 files changed
+320811
No files matched your search
@@ -0,0 +1,467 @@
|
||||
# 同行者可见即可说自动注册框架对接方案 - 快速阅读版
|
||||
|
||||
> 来源文档:`F:/AI/Obsidian/项目文档/徐工/语音/同行者可见即可说自动注册框架对接方案.docx`
|
||||
> 整理日期:2026-06-13
|
||||
> 目标:把原 Word 文档整理成工程接入时能快速查阅的 Markdown 速查文档。
|
||||
|
||||
## 1. 一句话结论
|
||||
|
||||
“可见即可说”主要依赖 Android 无障碍服务扫描当前界面控件,通过控件的 `android:id`、`android:text`、`android:contentDescription`、`clickable`、`scrollable` 等信息自动注册语音热词。用户说出界面上可见或已通过隐藏标签声明的词后,语音助手命中热词,再回调触发目标控件事件。
|
||||
|
||||
如果界面元素无法被无障碍扫描,例如小程序、网页、自绘界面,则需要应用主动通过 AIDL 或广播向语音发送插词列表,语音命中后再把命中的插词回传给应用,由应用自行执行动作。
|
||||
|
||||
## 2. 整体链路
|
||||
|
||||
### 2.1 普通 Android 控件链路
|
||||
|
||||
1. 应用界面展示控件。
|
||||
2. Android 无障碍服务扫描界面控件。
|
||||
3. 语音助手读取控件文本或隐藏标签。
|
||||
4. 语音助手把文本注册为可见即可说热词。
|
||||
5. 用户说出对应词,例如“每日推荐”或“打开每日推荐”。
|
||||
6. 热词模块命中控件节点。
|
||||
7. 无障碍服务触发控件点击或滑动等事件。
|
||||
8. 应用执行原控件逻辑。
|
||||
|
||||
### 2.2 图标控件链路
|
||||
|
||||
图标本身不显示文字时,需要通过 `contentDescription` 提供隐藏标签。例如图标按钮没有文字,但设置了 `android:contentDescription="向下滑"`,语音仍可识别“向下滑”并触发该图标。
|
||||
|
||||
### 2.3 非常规界面链路
|
||||
|
||||
适用于小程序、网页、自绘页面等无障碍拿不到元素的场景。
|
||||
|
||||
1. 应用整理当前界面可执行插词列表。
|
||||
2. 应用通过 AIDL 或广播发送给语音。
|
||||
3. 语音收到后注册插词。
|
||||
4. 用户说出插词。
|
||||
5. 语音识别命中。
|
||||
6. 语音通过 AIDL 或广播把命中插词回传给应用。
|
||||
7. 应用根据插词匹配具体控件或业务动作。
|
||||
|
||||
## 3. 接入总原则
|
||||
|
||||
| 原则 | 说明 |
|
||||
|---|---|
|
||||
| 必须有唯一 ID | 所有需要语音响应的控件都应设置唯一 `android:id`。 |
|
||||
| 文本优先 | 文本控件优先使用 `android:text` 暴露插词。 |
|
||||
| 图标补标签 | 图标类控件使用 `android:contentDescription` 暴露隐藏插词。 |
|
||||
| 多说法用 `#` | 多个插词使用 `#` 分隔,例如 `打开#启动`。 |
|
||||
| 状态要同步 | 播放/暂停、收藏/取消收藏等状态型图标必须动态更新 `contentDescription`。 |
|
||||
| 点击看 `clickable` | 控件是否响应与 `clickable` 状态有关,`true` 表示可点击。 |
|
||||
| 滚动看 `scrollable` | 滑动控件本身需要是可滑动节点,即 `Scrollable=true`。 |
|
||||
| 弹窗用标准 Dialog | 建议使用标准 `Dialog` 或依附 Activity 的自定义 Dialog。 |
|
||||
|
||||
## 4. 常见控件接入方式
|
||||
|
||||
## 4.1 文本类控件
|
||||
|
||||
适用对象:`TextView`、`Button`、带文本的布局项等。
|
||||
|
||||
推荐做法:
|
||||
|
||||
```xml
|
||||
<TextView
|
||||
android:id="@+id/tv_daily_recommend"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:clickable="true"
|
||||
android:text="每日推荐" />
|
||||
```
|
||||
|
||||
支持泛化说法时,可使用 `contentDescription`:
|
||||
|
||||
```xml
|
||||
<TextView
|
||||
android:id="@+id/tv_daily_recommend"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:clickable="true"
|
||||
android:text="每日推荐"
|
||||
android:contentDescription="每日推荐#打开每日推荐#我想打开每日推荐" />
|
||||
```
|
||||
|
||||
注意:
|
||||
|
||||
- `contentDescription` 中的插词文本必须包含 `text` 中的文本。
|
||||
- 控件要能响应点击,需确保 `clickable=true` 或实际可点击。
|
||||
|
||||
## 4.2 图标类控件
|
||||
|
||||
适用对象:只有图片、没有文本的按钮或图标。
|
||||
|
||||
```xml
|
||||
<ImageView
|
||||
android:id="@+id/iv_search"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:clickable="true"
|
||||
android:contentDescription="搜索#查找" />
|
||||
```
|
||||
|
||||
注意:
|
||||
|
||||
- 图标没有显示文本时,必须用 `contentDescription` 提供语音插词。
|
||||
- 多个插词用 `#` 分隔。
|
||||
- 控件是否响应仍与 `clickable` 状态有关。
|
||||
|
||||
## 4.3 同一图标多状态控件
|
||||
|
||||
典型场景:播放/暂停、收藏/取消收藏。
|
||||
|
||||
错误做法:
|
||||
|
||||
```xml
|
||||
android:contentDescription="播放#暂停"
|
||||
```
|
||||
|
||||
这种做法可能导致当前处于暂停状态时,用户说“暂停”反而触发点击,结果开始播放。
|
||||
|
||||
正确做法:根据状态动态更新。
|
||||
|
||||
```java
|
||||
if (isPlaying) {
|
||||
playButton.setContentDescription("暂停");
|
||||
} else {
|
||||
playButton.setContentDescription("播放");
|
||||
}
|
||||
```
|
||||
|
||||
原则:
|
||||
|
||||
- 当前正在播放,插词应是“暂停”。
|
||||
- 当前已经暂停,插词应是“播放”。
|
||||
- 当前已收藏,插词应是“取消收藏”。
|
||||
- 当前未收藏,插词应是“收藏”。
|
||||
|
||||
## 4.4 常规滑动控件
|
||||
|
||||
适用对象:`ScrollView`、`RecyclerView`、`ListView` 等。
|
||||
|
||||
基础要求:
|
||||
|
||||
- 控件本身应可滑动,即无障碍节点 `Scrollable=true`。
|
||||
- 如果每个 item 需要响应语音,item 需要有唯一 ID。
|
||||
- item 可使用 `contentDescription` 声明插词。
|
||||
- 如需响应 item 内部 child 控件,child 按文本类或图标类控件处理。
|
||||
|
||||
示例:
|
||||
|
||||
```xml
|
||||
<RecyclerView
|
||||
android:id="@+id/rv_station_list"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent" />
|
||||
```
|
||||
|
||||
item 示例:
|
||||
|
||||
```xml
|
||||
<TextView
|
||||
android:id="@+id/tv_station_name"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:clickable="true"
|
||||
android:text="88.6MHz"
|
||||
android:contentDescription="88.6MHz#播放88.6" />
|
||||
```
|
||||
|
||||
## 4.5 翻页控制
|
||||
|
||||
文档中特别强调:如果要让可见即可说支持“上一页 / 下一页”这类翻页逻辑,应用需要预留两个大小为 0 的控件。
|
||||
|
||||
要求:
|
||||
|
||||
- 控件大小为 0。
|
||||
- 不能设置为 `GONE`。
|
||||
- 分别添加“上一页”“下一页”等 `contentDescription`。
|
||||
- 应用自己实现点击事件,点击后控制对应滚动控件翻页。
|
||||
- 如果页面中有多个同方向滚动控件,只建议配置用户最可能使用的那个。
|
||||
- 多组 0 大小翻页控件可能导致目标滚动控件不符合预期。
|
||||
|
||||
示例:
|
||||
|
||||
```xml
|
||||
<View
|
||||
android:id="@+id/voice_page_prev"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:clickable="true"
|
||||
android:contentDescription="上一页" />
|
||||
|
||||
<View
|
||||
android:id="@+id/voice_page_next"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:clickable="true"
|
||||
android:contentDescription="下一页" />
|
||||
```
|
||||
|
||||
```java
|
||||
voicePagePrev.setOnClickListener(v -> recyclerView.smoothScrollBy(0, -recyclerView.getHeight()));
|
||||
voicePageNext.setOnClickListener(v -> recyclerView.smoothScrollBy(0, recyclerView.getHeight()));
|
||||
```
|
||||
|
||||
如果不预留翻页控制控件,默认不支持翻页逻辑。
|
||||
|
||||
## 4.6 SeekBar 等特殊滑动控件
|
||||
|
||||
适用对象:亮度、音量、进度条等 `SeekBar` 控件。
|
||||
|
||||
接入目标:支持类似“调高屏幕亮度”“降低音量”“屏幕亮度调大一点”等语音指令。
|
||||
|
||||
推荐做法:
|
||||
|
||||
```xml
|
||||
<SeekBar
|
||||
android:id="@+id/sb_screen_brightness"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingStart="0dp"
|
||||
android:paddingEnd="0dp"
|
||||
android:contentDescription="屏幕亮度" />
|
||||
```
|
||||
|
||||
父布局建议:
|
||||
|
||||
```xml
|
||||
android:clipChildren="false"
|
||||
android:clipToPadding="false"
|
||||
```
|
||||
|
||||
原因:
|
||||
|
||||
- 框架根据控件宽高计算百分比拖动距离。
|
||||
- 无法直接读取控件内部业务值。
|
||||
- `SeekBar` 默认内边距会造成拖动精度误差。
|
||||
- 将 `SeekBar` 内边距设为 0,并允许子控件超出父布局边界,可减少误差。
|
||||
|
||||
## 4.7 Switch 开关类控件
|
||||
|
||||
原生 `Switch`:通常无需额外适配。
|
||||
|
||||
自定义开关 View:需要重写 `onInitializeAccessibilityNodeInfo`。
|
||||
|
||||
```java
|
||||
@Override
|
||||
public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {
|
||||
super.onInitializeAccessibilityNodeInfo(info);
|
||||
info.setCheckable(true);
|
||||
info.setChecked(isChecked());
|
||||
}
|
||||
```
|
||||
|
||||
要求:
|
||||
|
||||
- 设置 `checkable=true`。
|
||||
- 根据真实开关状态同步 `checked`。
|
||||
- 状态变化时及时刷新无障碍信息。
|
||||
|
||||
## 5. 非常规界面 AIDL / 广播对接字段
|
||||
|
||||
文档流程图给出的字段含义如下。
|
||||
|
||||
## 5.1 应用发送给语音:注册或取消注册
|
||||
|
||||
| 字段 | 含义 | 示例 |
|
||||
| ------------- | --------------------- | ------------------------------------------------------ |
|
||||
| `key_type` | 要处理的类型,`2600` 表示可见即可说 | `2600` |
|
||||
| `action` | 动作 | `shadow.register` / `shadow.unregister` |
|
||||
| `packageName` | 应用包名,可不填 | `com.yaxon.carcontrol` |
|
||||
| `className` | 页面类名,可不填 | `com.yaxon.carcontrol.activity.CarControlMainActivity` |
|
||||
| `pageName` | 页面名称,可不填 | `voiceSetting` |
|
||||
| `keywordList` | 插词列表,泛化词用 `#` 分隔 | `打开#启动` |
|
||||
|
||||
示例:
|
||||
|
||||
```json
|
||||
{
|
||||
"key_type": 2600,
|
||||
"action": "shadow.register",
|
||||
"packageName": "com.yaxon.carcontrol",
|
||||
"className": "com.yaxon.carcontrol.activity.CarControlMainActivity",
|
||||
"pageName": "voiceSetting",
|
||||
"keywordList": "打开#启动"
|
||||
}
|
||||
```
|
||||
|
||||
取消注册:
|
||||
|
||||
```json
|
||||
{
|
||||
"key_type": 2600,
|
||||
"action": "shadow.unregister",
|
||||
"packageName": "com.yaxon.carcontrol",
|
||||
"pageName": "voiceSetting",
|
||||
"keywordList": "打开#启动"
|
||||
}
|
||||
```
|
||||
|
||||
## 5.2 语音回传给应用:命中执行
|
||||
|
||||
| 字段 | 含义 | 示例 |
|
||||
| ---------- | --------------------- | ---------------- |
|
||||
| `key_type` | 回传类型,`1600` 表示可见即可说执行 | `1600` |
|
||||
| `action` | 动作 | `shadow.execute` |
|
||||
| `cmd` | 命中的插词原文 | `打开` |
|
||||
|
||||
示例:
|
||||
|
||||
```json
|
||||
{
|
||||
"key_type": 1600,
|
||||
"action": "shadow.execute",
|
||||
"cmd": "打开"
|
||||
}
|
||||
```
|
||||
|
||||
应用收到后,应根据 `cmd` 做业务分发。
|
||||
|
||||
## 6. 已知问题与规避方式
|
||||
|
||||
## 6.1 `com.android.systemui` 扫描冲突
|
||||
|
||||
问题:
|
||||
|
||||
- 底部 touch 栏中可能存在很多图标插词。
|
||||
- 这些插词可能和桌面或其他应用重复,例如“车主手册”。
|
||||
- `com.android.systemui` 所在 window 可能一直可见,即使视觉上隐藏,也可能被无障碍服务扫描到。
|
||||
- 用户说“车主手册”时,可能命中 SystemUI 上隐藏的控件,导致结果不符合预期。
|
||||
|
||||
建议:
|
||||
|
||||
- SystemUI 隐藏时,临时清空相关控件的 `android:text` 和 `android:contentDescription`。
|
||||
- SystemUI 重新显示时,再恢复文本和隐藏标签。
|
||||
|
||||
示例:
|
||||
|
||||
```java
|
||||
if (isSystemUiHidden) {
|
||||
view.setContentDescription(null);
|
||||
textView.setText("");
|
||||
} else {
|
||||
view.setContentDescription("车主手册");
|
||||
textView.setText("车主手册");
|
||||
}
|
||||
```
|
||||
|
||||
## 6.2 同一图片不同插词
|
||||
|
||||
问题:
|
||||
|
||||
- 同一图片可能对应不同状态,例如播放/暂停。
|
||||
- 如果同时注册“播放#暂停”,当前状态下可能执行反向动作。
|
||||
|
||||
建议:
|
||||
|
||||
- 状态变化时动态更新 `contentDescription`。
|
||||
- 当前能执行什么动作,就只暴露什么插词。
|
||||
|
||||
## 6.3 部分弹框扫描不到
|
||||
|
||||
问题:
|
||||
|
||||
- 一些通过 Window 直接创建的弹框,可能与 Activity 界面不匹配或层级冲突。
|
||||
- 无障碍服务可能扫描不到弹框里的控件。
|
||||
|
||||
建议:
|
||||
|
||||
- 优先使用标准 `Dialog`。
|
||||
- 自定义 Dialog 应依附于 Activity。
|
||||
- 避免直接用独立 Window 承载需要语音响应的弹框。
|
||||
|
||||
## 7. 工程接入检查清单
|
||||
|
||||
## 7.1 页面级检查
|
||||
|
||||
- [ ] 页面中需要语音响应的控件是否都有唯一 `android:id`。
|
||||
- [ ] 文本控件是否有明确 `android:text`。
|
||||
- [ ] 图标控件是否设置了 `android:contentDescription`。
|
||||
- [ ] 多说法是否使用 `#` 分隔。
|
||||
- [ ] 控件是否实际可点击或可滑动。
|
||||
- [ ] 动态状态控件是否会更新 `contentDescription`。
|
||||
- [ ] 列表 item 是否有唯一 ID 或稳定的可识别标签。
|
||||
- [ ] 滚动页面是否需要预留上一页/下一页控制控件。
|
||||
- [ ] 弹框是否使用标准 Dialog 或依附 Activity。
|
||||
|
||||
## 7.2 状态型控件检查
|
||||
|
||||
- [ ] 播放状态:播放中只暴露“暂停”。
|
||||
- [ ] 暂停状态:暂停中只暴露“播放”。
|
||||
- [ ] 已收藏:只暴露“取消收藏”。
|
||||
- [ ] 未收藏:只暴露“收藏”。
|
||||
- [ ] 开关类自定义 View 是否正确设置 `checkable` 和 `checked`。
|
||||
|
||||
## 7.3 SystemUI 检查
|
||||
|
||||
- [ ] 隐藏状态是否仍可能被无障碍扫描。
|
||||
- [ ] 隐藏时是否清空 `text/contentDescription`。
|
||||
- [ ] 显示时是否恢复 `text/contentDescription`。
|
||||
- [ ] 是否存在与 Launcher 或 App 页面重复的插词。
|
||||
|
||||
## 7.4 非常规界面检查
|
||||
|
||||
- [ ] 是否确认无障碍无法获取页面元素。
|
||||
- [ ] 是否整理了当前页面插词列表。
|
||||
- [ ] 是否支持注册和取消注册。
|
||||
- [ ] 是否能接收语音命中的 `cmd`。
|
||||
- [ ] `cmd` 到业务动作是否有明确映射表。
|
||||
- [ ] 页面销毁或切换时是否取消旧插词。
|
||||
|
||||
## 8. 当前徐工项目建议落地顺序
|
||||
|
||||
1. 先验证 `YX_Voice` 与同行者之间的 `shadow.register` / `shadow.execute` 链路。
|
||||
2. 在 `YX_CarControl` 做一个测试插词,不绑定真实业务动作,只看日志是否能完整闭环。
|
||||
3. 链路确认后,再选一个真实页面做最小闭环,例如语音设置页或声音设置页。
|
||||
4. 对原生 Android 控件,优先走无障碍自动扫描,不额外走 AIDL/广播。
|
||||
5. 对自绘、网页、小程序、复杂浮窗,走 AIDL/广播注册插词。
|
||||
6. 对 SystemUI、Launcher、Music、Radio 这类插词多且易冲突的模块,先做页面级插词清单,再逐个治理冲突。
|
||||
|
||||
## 9. 日志验证建议
|
||||
|
||||
注册阶段重点看:
|
||||
|
||||
```text
|
||||
registerVisibleSpeakKeywords
|
||||
shadow.register
|
||||
keywordList
|
||||
```
|
||||
|
||||
命中阶段重点看:
|
||||
|
||||
```text
|
||||
shadow.execute
|
||||
cmd
|
||||
handleVisibleSpeakExecute
|
||||
```
|
||||
|
||||
异常排查重点看:
|
||||
|
||||
```text
|
||||
keywordList is empty
|
||||
TXZ not connected
|
||||
protocolLogic is null
|
||||
packageName or cmd is empty
|
||||
```
|
||||
|
||||
## 10. 快速判断表
|
||||
|
||||
| 现象 | 优先排查 |
|
||||
| ------------ | --------------------------------------------- |
|
||||
| 说了没反应 | 控件是否有 ID、是否 clickable、是否注册成功 |
|
||||
| 图标不能说 | 是否设置 `contentDescription` |
|
||||
| 状态动作反了 | 是否动态更新 `contentDescription` |
|
||||
| 列表不能翻页 | 是否预留 0 大小上一页/下一页控件 |
|
||||
| SeekBar 拖动不准 | 是否去掉 SeekBar 内边距,父布局是否设置 `clipChildren=false` |
|
||||
| 弹框扫不到 | 是否用标准 Dialog 或依附 Activity |
|
||||
| 命中到错误应用 | 是否存在 SystemUI / Launcher / 当前页面插词冲突 |
|
||||
| 非常规页面无元素 | 改走 AIDL 或广播插词注册 |
|
||||
|
||||
## 11. 后续需求明确后需要补充的内容
|
||||
|
||||
- 每个模块的可见即可说页面清单。
|
||||
- 每个页面的控件 ID、显示文本、隐藏标签、状态规则。
|
||||
- 插词冲突表,尤其是 SystemUI、Launcher、CarControl、Music、Radio。
|
||||
- 非常规界面的 AIDL / 广播接口定义。
|
||||
- 命中 `cmd` 到实际业务动作的映射表。
|
||||
- 现场日志验证结果。
|
||||
Reference in new issue
Block a user