Initial Android project setup with Compose, WebSocket, and VoiceInteractionService
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,256 @@
|
||||
# 01 — 系统语音助手集成规范
|
||||
|
||||
> **目标**:让昔涟成为 Android 系统级默认语音助手,替换 Google Assistant / Bixby
|
||||
> **核心 API**:`VoiceInteractionService` + `VoiceInteractionSession`
|
||||
|
||||
---
|
||||
|
||||
## 1. 功能目标
|
||||
|
||||
- 用户可在 **系统设置 → 默认应用 → 数字助理** 中选择昔涟
|
||||
- 长按 Home 键呼出昔涟(非全屏,悬浮覆盖层)
|
||||
- 屏幕底部两角向内滑动触发昔涟
|
||||
- 长按电源键可配置为呼出昔涟
|
||||
- 息屏状态下热词唤醒昔涟
|
||||
- 有线/蓝牙耳机按键呼出昔涟
|
||||
|
||||
## 2. AndroidManifest.xml 声明
|
||||
|
||||
```xml
|
||||
<!-- VoiceInteractionService -->
|
||||
<service
|
||||
android:name=".service.CyreneVoiceInteractionService"
|
||||
android:exported="true"
|
||||
android:permission="android.permission.BIND_VOICE_INTERACTION">
|
||||
|
||||
<meta-data
|
||||
android:name="android.voice_interaction"
|
||||
android:resource="@xml/voice_interaction_config" />
|
||||
|
||||
<intent-filter>
|
||||
<action android:name="android.service.voice.VoiceInteractionService" />
|
||||
</intent-filter>
|
||||
</service>
|
||||
|
||||
<!-- AssistService (Android 14+) -->
|
||||
<service
|
||||
android:name=".service.CyreneAssistService"
|
||||
android:exported="true"
|
||||
android:permission="android.permission.BIND_ASSIST">
|
||||
|
||||
<intent-filter>
|
||||
<action android:name="android.service.voice.AssistService" />
|
||||
</intent-filter>
|
||||
</service>
|
||||
```
|
||||
|
||||
## 3. 配置文件
|
||||
|
||||
### res/xml/voice_interaction_config.xml
|
||||
|
||||
```xml
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<voice-interaction-service
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:sessionService=".service.CyreneVoiceInteractionSession"
|
||||
android:recognitionService=".service.CyreneRecognitionService"
|
||||
android:supportsAssist="true"
|
||||
android:supportsLaunchVoiceAssistFromKeyguard="true"
|
||||
android:supportsLocalRecognition="true"
|
||||
android:serviceIcon="@drawable/ic_cyrene"
|
||||
android:serviceLabel="@string/voice_assistant_name" />
|
||||
```
|
||||
|
||||
## 4. VoiceInteractionService 实现
|
||||
|
||||
```kotlin
|
||||
class CyreneVoiceInteractionService : VoiceInteractionService() {
|
||||
|
||||
override fun onReady() {
|
||||
super.onReady()
|
||||
// 服务就绪,可在此初始化 TTS 引擎等
|
||||
}
|
||||
|
||||
override fun onCreateSession(args: Bundle?): VoiceInteractionSession {
|
||||
return CyreneVoiceInteractionSession(this)
|
||||
}
|
||||
|
||||
override fun onLaunchVoiceAssistFromKeyguard() {
|
||||
// 锁屏启动 → 进入简化模式,仅显示对话,IoT 控制等需先解锁
|
||||
}
|
||||
|
||||
// Android 14+: AssistAction 回调
|
||||
override fun onHandleAssist(
|
||||
request: AssistRequest?,
|
||||
cancellationSignal: CancellationSignal?,
|
||||
callback: OutcomeCallback<AssistResult?>?
|
||||
) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
|
||||
request?.let {
|
||||
val assistContent = it.assistContent
|
||||
// 提取当前屏幕上下文(可选,用于后续上下文感知)
|
||||
callback?.onResult(AssistResult(assistContent))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 5. VoiceInteractionSession 实现(悬浮窗界面)
|
||||
|
||||
```kotlin
|
||||
class CyreneVoiceInteractionSession(context: Context) :
|
||||
VoiceInteractionSession(context) {
|
||||
|
||||
override fun onCreateContentView(): View {
|
||||
// 返回 ComposeView 作为悬浮窗的内容
|
||||
return ComposeView(context).apply {
|
||||
setContent {
|
||||
CyreneTheme {
|
||||
OverlayScreen(
|
||||
viewModel = overlayViewModel,
|
||||
onDismiss = { finish() }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onShow(args: Bundle?, showFlags: Int) {
|
||||
super.onShow(args, showFlags)
|
||||
// 设置窗口属性:透明背景 + 底部卡片式布局
|
||||
window?.apply {
|
||||
// 半透明遮罩
|
||||
setBackgroundDrawable(ColorDrawable(0x80000000.toInt()))
|
||||
// FLAG_DIM_BEHIND 可实现模糊效果
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||
addSystemGestureExclusionRects(...)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onComputeInsets(outInsets: Insets?) {
|
||||
super.onComputeInsets(outInsets)
|
||||
// 控制悬浮窗内容区域
|
||||
}
|
||||
|
||||
override fun onHide() {
|
||||
super.onHide()
|
||||
// 悬浮窗隐藏时清理状态
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 关键窗口属性
|
||||
|
||||
| 属性 | 值 | 说明 |
|
||||
|------|-----|------|
|
||||
| 背景 | `ColorDrawable(0x80000000)` | 半透明黑色遮罩,透出底层 APP |
|
||||
| 内容区域 | 自适应高度 | 底部弹出,类似 Google Assistant |
|
||||
| 触摸外区域行为 | 关闭悬浮窗 | 用户点击遮罩区域关闭 |
|
||||
| 键盘弹出 | 推高内容区域 | 文本输入时自动调整 |
|
||||
|
||||
## 6. 权限清单
|
||||
|
||||
```xml
|
||||
<!-- 核心语音助手权限 -->
|
||||
<uses-permission android:name="android.permission.BIND_VOICE_INTERACTION" />
|
||||
<uses-permission android:name="android.permission.BIND_ASSIST" />
|
||||
|
||||
<!-- 音频相关 -->
|
||||
<uses-permission android:name="android.permission.RECORD_AUDIO" />
|
||||
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
|
||||
|
||||
<!-- 热词唤醒 -->
|
||||
<uses-permission android:name="android.permission.CAPTURE_AUDIO_HOTWORD" />
|
||||
|
||||
<!-- 后台服务 -->
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MICROPHONE" />
|
||||
|
||||
<!-- 网络 -->
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||
|
||||
<!-- 推送 -->
|
||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||
|
||||
<!-- 锁屏交互 -->
|
||||
<uses-permission android:name="android.permission.USE_FULL_SCREEN_INTENT" />
|
||||
```
|
||||
|
||||
## 7. 引导用户设为默认助手
|
||||
|
||||
首次启动时检测并引导:
|
||||
|
||||
```kotlin
|
||||
fun checkAndPromptDefaultAssistant(context: Context) {
|
||||
val isDefault = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
val componentName = ComponentName(context, CyreneVoiceInteractionService::class.java)
|
||||
context.packageManager
|
||||
.queryIntentServices(
|
||||
Intent(VoiceInteractionService.SERVICE_INTERFACE),
|
||||
PackageManager.MATCH_DEFAULT_ONLY
|
||||
)
|
||||
.any { it.serviceInfo.packageName == context.packageName }
|
||||
} else {
|
||||
false
|
||||
}
|
||||
|
||||
if (!isDefault) {
|
||||
// 显示引导 UI → 跳转到 Settings.ACTION_VOICE_INPUT_SETTINGS
|
||||
val intent = Intent(Settings.ACTION_VOICE_INPUT_SETTINGS)
|
||||
context.startActivity(intent)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 8. 热词唤醒检测
|
||||
|
||||
### 方案选型
|
||||
|
||||
| 方案 | 优点 | 缺点 | 适用场景 |
|
||||
|------|------|------|---------|
|
||||
| 系统 Always-On Hotword API | 低功耗、系统级支持 | 限 Android 8+,某些 ROM 不支持 | **首选** |
|
||||
| Porcupine (Picovoice) | 跨平台、离线 | 商业许可,需额外集成 | 兜底 |
|
||||
| 自建模型 (openWakeWord) | 完全可控、低成本 | 需要本地推理能力 | 长期方案 |
|
||||
|
||||
### 唤醒词配置
|
||||
|
||||
| 优先级 | 唤醒词 | 说明 |
|
||||
|--------|--------|------|
|
||||
| P0 | "昔涟" (Xī Lián) | 角色名,默认唤醒词 |
|
||||
| P1 | "Hey 昔涟" | 与 "Hey Google" 习惯对齐 |
|
||||
| P2 | 自定义 | 用户可在设置中自定义 |
|
||||
|
||||
### 息屏唤醒流程
|
||||
|
||||
```
|
||||
用户说出唤醒词
|
||||
→ HotwordDetector 识别成功(<800ms)
|
||||
→ 系统触发 VoiceInteractionService
|
||||
→ CyreneVoiceInteractionSession.onCreateContentView()
|
||||
→ Overlay 显示,播放连接提示音
|
||||
→ 用户说话 → STT → AI-Core → TTS → 语音回复
|
||||
→ 对话结束 → finish() → 息屏
|
||||
```
|
||||
|
||||
## 9. Dismiss 时机
|
||||
|
||||
悬浮窗在以下情况关闭:
|
||||
|
||||
| 条件 | 行为 |
|
||||
|------|------|
|
||||
| 用户说"再见" / "退下" | 自然对话结束,收起悬浮窗 |
|
||||
| 用户点击遮罩区域 | 立即关闭 |
|
||||
| 对话静默 10 秒 | 自动收起 |
|
||||
| 用户主动滑动关闭 | 手势关闭,同 Google Assistant |
|
||||
| 收到系统电话等中断 | 暂停语音,进入后台等待 |
|
||||
|
||||
## 10. 降级策略
|
||||
|
||||
当系统不支持 `VoiceInteractionService` 或未设为默认助手时:
|
||||
|
||||
- **保底方案**:PWA(利用主项目已有的 PWA 支持)
|
||||
- **WebView 封装**:内嵌 H5 对话界面作为过渡
|
||||
- **通知栏常驻**:提供快速对话入口,但功能受限
|
||||
Reference in New Issue
Block a user