session_id: 5c7c8951-d475-4d01-af3f-ead40c1a76
exported_at: 2026-04-26T00:06:38+08:00
work_dir: /Users/destiny/dev/emoon/demos/card_demo
message_count: 73
/init slash command. The system has analyzed the…
The user just ran /init slash command. The system has analyzed the codebase and generated an AGENTS.md file. Latest AGENTS.md file content:
本文件面向 AI 编程助手。项目主要使用中文进行文档和注释编写,代码标识符采用中文拼音或英文混合风格。
card_demo 是一个面向甘肃省中医院的 AI 智慧医疗导诊演示系统,包含:
medical-card-demo/ —— 核心 Web 应用,基于 SpringBoot + Vue 的卡片式对话导诊与挂号系统。RobotSample-main/ —— 猎户星空(Orion Star)机器人 Android SDK 示例工程,用于在豹小秘系列机器人上运行原生 APK,实现导航、语音、视觉等硬件能力。项目总体目标是:在机器人终端上部署智慧医疗导诊系统,实现“语音/触屏对话 → AI 导诊推荐 → 机器人带路导航”的完整闭环。
| 层级 | 技术 |
|---|---|
| 后端 | Spring Boot 3.2、Java 17、Maven 3.6+ |
| AI 接入 | 阿里云百炼(DashScope)qwen-turbo / qwen-vl-plus |
| 前端 | Vue 3.3、Vue CLI 5、Pinia 2、Axios |
| 其他 | ZXing(二维码生成)、Lombok、SSE 流式响应 |
| 层级 | 技术 |
|---|---|
| 平台 | Android 9.0(API 28)、RobotOS |
| 构建 | Gradle 5.6.4、Android Gradle Plugin 3.6.1 |
| 语言 | Java 8 |
| SDK | 猎户星空 robotservice.jar |
card_demo/
├── medical-card-demo/ # 医疗卡片挂号演示系统
│ ├── backend/
│ │ ├── pom.xml # Maven 构建配置
│ │ ├── startup.sh # 一键构建启动脚本
│ │ ├── src/main/java/com/medical/demo/
│ │ │ ├── MedicalCardDemoApplication.java
│ │ │ ├── config/ # WebConfig(CORS 等)
│ │ │ ├── controller/ # ChatController、ReportAnalysisController
│ │ │ ├── dto/ # 数据传输对象(ChatRequestDTO、DepartmentDTO 等)
│ │ │ └── service/ # 业务逻辑层
│ │ │ ├── ChatService.java # 主流程编排
│ │ │ ├── IntentRecognitionService.java # 意图识别
│ │ │ ├── DepartmentRecommendationService.java
│ │ │ ├── DoctorScheduleService.java
│ │ │ ├── AppointmentService.java
│ │ │ ├── IDCardOCRService.java # 身份证 OCR(VL 模型)
│ │ │ ├── ReportAnalysisService.java # 检验/检查报告解读
│ │ │ ├── TongueDiagnosisService.java # 中医舌诊
│ │ │ ├── VoiceToTextService.java # 语音转文字
│ │ │ ├── FAQService.java # 常见问题匹配
│ │ │ └── ...
│ │ └── src/main/resources/
│ │ ├── application.yml # 服务端配置(含百炼 API Key)
│ │ └── static/ # 前端构建产物(由 Maven 自动复制)
│ ├── frontend/
│ │ ├── package.json # npm 配置
│ │ ├── vue.config.js # Vue CLI 配置、代理、页面标题
│ │ └── src/
│ │ ├── main.js # Vue 应用入口(Pinia)
│ │ ├── App.vue # 根组件(仅挂载 ChatInterface)
│ │ ├── api/chat.js # 前端 API 封装(SSE + axios)
│ │ └── components/
│ │ ├── ChatInterface.vue
│ │ └── cards/ # 各类卡片组件
│ ├── docs/
│ │ └── 省中导诊台.txt # FAQ 问答语料(问题+回答格式)
│ ├── CALL_FLOW.md # 完整调用流程与架构图
│ ├── CONVERSATION_UPGRADE.md # 对话式交互改造说明
│ ├── FAQ_TEST_CASES.md # FAQ 批量测试用例
│ └── README.md # 项目主文档
│
├── RobotSample-main/ # 猎户星空机器人 Android 示例
│ ├── build.gradle # 顶层 Gradle 配置
│ ├── settings.gradle # 仅包含 ':app'
│ └── app/
│ ├── build.gradle # App 模块配置(applicationId: com.ainirobot.robotos)
│ ├── libs/robotservice.jar # 猎户星空 SDK
│ └── src/main/java/com/ainirobot/robotos/
│ ├── MainActivity.java
│ ├── application/ # RobotOSApplication、SpeechCallback、ModuleCallback
│ ├── fragment/ # 各业务场景 Fragment
│ ├── view/ # 公共自定义 View
│ └── maputils/ # 地图/定位工具类
│
├── 仿鸿蒙机器人系统技术方案.md
├── 猎户星空API完整参考手册.md
├── 猎户星空合作分析报告.md
└── 猎户星空合作分析报告_详细版.md
cd medical-card-demo/backend
./startup.sh
该脚本会依次完成:
java、mvn、node 环境;mvn clean package -DskipTests;npm install && npm run build;frontend/dist/ 复制到 backend/src/main/resources/static/;3380);访问地址:http://localhost:3380
# 终端1:后端
cd medical-card-demo/backend
mvn spring-boot:run # 默认端口 8080(application.yml)
# 生产/一键脚本通常期望 3380
# 终端2:前端
cd medical-card-demo/frontend
npm install
npm run serve # 端口 8080,代理 /api -> localhost:3380
cd medical-card-demo/frontend
npm install && npm run build
cd ../backend
mvn clean package
java -jar target/medical-card-demo-1.0.0.jar
cd RobotSample-main
./gradlew assembleDebug
注意:必须在机器人屏幕上手动点击启动 APK,不能通过 IDE 的 Debug 按钮运行,否则无法获取 API 权限。
所有聊天相关接口均返回 text/event-stream,前端通过 handleSSEStream() 解析。
| 接口 | 路径 | 说明 |
|---|---|---|
| 发送消息 | POST /api/v1/chat/messages |
意图识别 → 功能路由 |
| 选择科室 | POST /api/v1/chat/select-department |
返回医生排班卡片 |
| 选择医生 | POST /api/v1/chat/select-doctor |
返回具体排班时段 |
| 提交挂号 | POST /api/v1/chat/appointment |
返回挂号成功卡片 |
| 身份证 OCR | POST /api/v1/chat/idcard-ocr |
VL 模型识别身份证信息 |
| 提交建档 | POST /api/v1/chat/record |
保存患者档案 |
| 语音转文字 | POST /api/v1/chat/audio |
上传音频 Blob |
| 舌象校验 | POST /api/v1/chat/check-tongue-image |
判断是否为有效舌象照片 |
| 舌诊分析 | POST /api/v1/chat/ai-diagnosis |
根据舌象+主诉生成报告 |
| 报告解读 | POST /api/v1/chat/report-analysis |
VL 模型解读检验/检查报告 |
| 获取日期 | GET /api/v1/chat/available-dates |
返回今天/明天/后天 |
KeywordExtractionService.extractKeywords() 提取意图;ChatService 根据意图分发到挂号/建档/报告解读/舌诊/FAQ;IDCardOCRService → 推荐科室 → 选择医生/日期/时段 → 生成二维码;FAQService 读取 docs/省中导诊台.txt,按关键词覆盖率 / Jaccard 相似度匹配;ReportAnalysisService 调用 qwen-vl-plus 分析图片并返回 Markdown;TongueDiagnosisService 校验舌象合法性后生成中医诊断报告。com.medical.demo@Data 等),字段命名采用 camelCase;*Service.java 命名,使用 @Service + @Autowired 注入;@CrossOrigin(origins = "*");lombok.extern.slf4j.Slf4j;所有流式输出通过 SseEmitter 推送 JSON 事件,格式示例:
{ "type": "text", "content": "..." }
{ "type": "card", "cardType": "department-selection", "data": { ... } }
{ "type": "message_end" }
src/api/chat.js;DepartmentSelectionCard.vue);public/ 或 src/assets/;vue.config.js 的 chainWebpack 中配置为 "甘肃省中医院"。spring-boot-starter-test,但当前没有大量覆盖的单元测试类;FAQ_TEST_CASES.md 和 CALL_FLOW.md 作为测试依据;curl 示例和 Bash 批量脚本,可直接运行验证;medical-card-demo/backend/src/main/resources/application.yml 中硬编码了阿里云百炼 API Key(bailian.api-key)。严禁提交到公开仓库,生产环境应改为环境变量或配置中心注入。application.yml 和 WebConfig 中 allowed-origins: "*",生产环境需收紧为具体域名。ConcurrentHashMap 中,重启后数据丢失,且存在并发安全和数据持久化风险。如需修改 FAQ 内容,直接编辑 medical-card-demo/docs/省中导诊台.txt,格式固定为:
问题:xxx?
回答:xxx。
如需调整前端页面标题或代理端口,修改 medical-card-demo/frontend/vue.config.js。
后端端口在 application.yml 中设为 8080,但 startup.sh 和演示环境通常使用 3380,注意保持一致。
机器人 APK(RobotSample)与 Web 应用通过 JSBridge 方案进行通信(详见 仿鸿蒙机器人系统技术方案.md),前端点击“带我去”后触发机器人导航。
阅读文档 @仿鸿蒙机器人系统技术方案.md,由于技术方案的调整,现在我需要将文档中 代码块内的 Kotlin→Java 重写,调整时请参考 @RobotSample-main/ 文件夹 demo 工程内对应各个模块的实现及调用方案(如导航、语音等)
读取技术方案文档){
"description": "读取技术方案文档",
"prompt": "请读取 /Users/destiny/dev/emoon/demos/card_demo/仿鸿蒙机器人系统技术方案.md 文档,找出文档中所有的 Kotlin 代码块。对每个代码块,记录:\n1. 代码块所在的大致位置(前后文标题/段落)\n2. 代码块的内容\n3. 代码块的功能描述(如导航模块、语音模块等)\n\n请完整输出所有 Kotlin 代码块的信息。",
"subagent_type": "explore"
}
探索RobotSample工程){
"description": "探索RobotSample工程",
"prompt": "请深入探索 /Users/destiny/dev/emoon/demos/card_demo/RobotSample-main/ 目录下的 Android demo 工程,重点关注以下内容:\n\n1. 工程的整体结构(所有 Java 文件)\n2. 导航模块的实现方式(如 LeadActivity、导航相关 Fragment/Activity)\n3. 语音模块的实现方式(如语音播放、语音识别、TTS 等)\n4. 地图相关模块\n5. 硬件控制相关(如 LED、运动控制等)\n6. 各个模块中调用 robotservice.jar SDK 的具体 API 和方法签名\n\n请详细列出关键 Java 文件的路径、类名、关键方法名及其签名,特别是那些可能被 Kotlin 代码引用的对应实现。需要看到具体的代码实现。",
"subagent_type": "explore"
}
a756444c9
resumed: false
actual_subagent_type: explore
status: completed
[summary]
我已完整读取了文档的全部 5246 行内容。经过逐行分析,文档中标记为 `java` 但实际上使用 **Kotlin 语法**(`val`/`var`、`fun`、`class X : Y()`、`data class`、`when`、`apply { }`、`override fun`、`?.` 安全调用等)的代码块共有 **20 处**。
以下是所有 Kotlin 代码块的完整信息,按文档出现顺序排列:
---
## 1. LauncherActivity 下拉手势检测(简化版)
**位置**:三、仿鸿蒙 Launcher App 详细设计 → 3.3 技术实现要点 → 下拉手势拦截
**功能**:Launcher 桌面主页的下拉手势检测,用于触发仿 HarmonyOS 控制中心。
```kotlin
class LauncherActivity : Activity() {
private val gestureDetector by lazy {
GestureDetector(this, object : GestureDetector.SimpleOnGestureListener() {
override fun onFling(e1: MotionEvent?, e2: MotionEvent?, vx: Float, vy: Float): Boolean {
if (e1 != null && e1.y < 100 && vy > 200) {
showControlCenter()
return true
}
return false
}
})
}
override fun onTouchEvent(event: MotionEvent): Boolean {
return gestureDetector.onTouchEvent(event)
}
}
```
---
## 2. 全屏沉浸式设置(简化版)
**位置**:三、仿鸿蒙 Launcher App 详细设计 → 3.3 技术实现要点 → 全屏沉浸式
**功能**:隐藏系统状态栏和导航栏,实现仿鸿蒙桌面的全屏沉浸式体验。
```kotlin
window.decorView.systemUiVisibility = (
View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY
or View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
or View.SYSTEM_UI_FLAG_FULLSCREEN
)
```
---
## 3. LauncherActivity.java 完整代码
**位置**:三、仿鸿蒙 Launcher App 详细设计 → 3.5.1 LauncherActivity.java 完整代码
**功能**:仿鸿蒙桌面主页核心 Activity,负责全屏沉浸、图标网格展示、手势检测、时间日期更新、控制中心显示/隐藏。
```kotlin
package com.emoon.harmony.launcher
import android.app.Activity
import android.content.Intent
import android.os.Bundle
import android.view.GestureDetector
import android.view.MotionEvent
import android.view.View
import android.view.WindowManager
import android.widget.TextView
import android.widget.Toast
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.RecyclerView
import java.util.Calendar
/**
* 桌面主页 Activity
* 这是用户开机后看到的第一个页面,负责展示仿鸿蒙桌面 UI
*/
class LauncherActivity : Activity() {
// 桌面图标网格 RecyclerView(类比 Vue 的列表渲染)
private lateinit var recyclerView: RecyclerView
// 下拉控制中心容器视图
private lateinit var controlCenterView: View
// 手势检测器(用于识别下拉手势)
private lateinit var gestureDetector: GestureDetector
/**
* Activity 创建时调用(类比 Vue 的 mounted 生命周期)
*/
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// 第一步:设置全屏沉浸式,隐藏状态栏和导航栏
hideSystemUI()
// 第二步:加载 XML 布局文件
setContentView(R.layout.activity_launcher)
// 第三步:初始化桌面图标网格
setupAppGrid()
// 第四步:初始化下拉手势检测
setupGestureDetector()
// 第五步:启动时间显示更新
updateTimeDisplay()
}
/**
* 隐藏系统状态栏和导航栏,实现全屏沉浸式体验
* 这是仿鸿蒙桌面的关键:必须完全隐藏系统 UI,否则会暴露 Android 原生界面
*/
private fun hideSystemUI() {
// 设置窗口为全屏模式(隐藏顶部状态栏)
window.setFlags(
WindowManager.LayoutParams.FLAG_FULLSCREEN,
WindowManager.LayoutParams.FLAG_FULLSCREEN
)
// 设置系统 UI 可见性标志(沉浸式模式)
window.decorView.systemUiVisibility = (
// 粘性沉浸模式:用户滑动边缘时系统 UI 暂时出现,几秒后自动隐藏
View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY
// 隐藏导航栏(底部虚拟按键)
or View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
// 隐藏状态栏(顶部时间、电量等)
or View.SYSTEM_UI_FLAG_FULLSCREEN
// 保持布局稳定,防止系统栏显示/隐藏时布局跳动
or View.SYSTEM_UI_FLAG_LAYOUT_STABLE
// 允许布局延伸到全屏区域
or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
// 允许布局延伸到导航栏区域
or View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
)
}
/**
* 初始化桌面图标网格 RecyclerView
* 使用 5 列网格布局,展示所有应用图标
*/
private fun setupAppGrid() {
recyclerView = findViewById(R.id.rv_app_grid)
// 设置网格布局管理器:5 列(横屏下每行显示 5 个图标)
recyclerView.layoutManager = GridLayoutManager(this, 5)
// 创建预定义的应用列表(10 个应用图标)
val appList = listOf(
AppItem("智慧医疗", R.drawable.ic_medical, AppItem.Type.BUSINESS),
AppItem("设置", R.drawable.ic_settings, AppItem.Type.SETTINGS),
AppItem("相机", R.drawable.ic_camera, AppItem.Type.FAKE),
AppItem("文件管理", R.drawable.ic_files, AppItem.Type.FAKE),
AppItem("日历", R.drawable.ic_calendar, AppItem.Type.FAKE),
AppItem("时钟", R.drawable.ic_clock, AppItem.Type.FAKE),
AppItem("计算器", R.drawable.ic_calculator, AppItem.Type.FAKE),
AppItem("天气", R.drawable.ic_weather, AppItem.Type.FAKE),
AppItem("音乐", R.drawable.ic_music, AppItem.Type.FAKE),
AppItem("图库", R.drawable.ic_gallery, AppItem.Type.FAKE)
)
// 设置适配器,传入应用列表和点击回调
recyclerView.adapter = AppGridAdapter(appList) { item ->
onAppClick(item)
}
}
/**
* 处理应用图标点击事件
* 根据应用类型执行不同操作:跳转业务 App、打开设置页、或显示提示
*/
private fun onAppClick(item: AppItem) {
when (item.type) {
AppItem.Type.BUSINESS -> {
// 业务应用:通过包名跳转到业务 App
val intent = packageManager.getLaunchIntentForPackage(
"com.emoon.harmony.robot" // 业务 App 的包名(需与业务 App 保持一致)
)
if (intent != null) {
startActivity(intent)
} else {
// 开发阶段友好提示:业务 App 未安装
Toast.makeText(this, "业务应用未安装", Toast.LENGTH_SHORT).show()
}
}
AppItem.Type.SETTINGS -> {
// 设置应用:打开假设置页面
startActivity(Intent(this, FakeSettingsActivity::class.java))
}
AppItem.Type.FAKE -> {
// 装饰图标:点击后显示"即将推出"提示,保持桌面完整性
Toast.makeText(this, "${item.name} 即将推出", Toast.LENGTH_SHORT).show()
}
}
}
/**
* 初始化下拉手势检测器
* 从屏幕顶部向下滑动时显示控制中心(仿 HarmonyOS 操作逻辑)
*/
private fun setupGestureDetector() {
controlCenterView = findViewById(R.id.control_center_container)
gestureDetector = GestureDetector(this, object : GestureDetector.SimpleOnGestureListener() {
/**
* 检测快速滑动手势(Fling)
* @param e1 手势起点(按下位置)
* @param e2 手势终点(抬起位置)
* @param velocityX X 方向滑动速度
* @param velocityY Y 方向滑动速度(正值表示向下)
*/
override fun onFling(
e1: MotionEvent?,
e2: MotionEvent,
velocityX: Float,
velocityY: Float
): Boolean {
if (e1 == null) return false
// 判断条件:从屏幕顶部 100px 内开始(e1.y < 100)
// 且向下滑动速度大于 500(velocityY > 500)
// 且滑动距离大于 100px(e2.y - e1.y > 100)
if (e1.y < 100 && velocityY > 500 && e2.y - e1.y > 100) {
showControlCenter()
return true // 返回 true 表示已消费此手势
}
return false
}
})
}
/**
* 显示仿鸿蒙控制中心
* 带动画效果:从屏幕上方滑入
*/
private fun showControlCenter() {
controlCenterView.visibility = View.VISIBLE
// 初始位置:在屏幕上方(Y 轴负方向偏移控件高度)
controlCenterView.translationY = -controlCenterView.height.toFloat()
// 执行滑入动画:300ms 内从上方滑到正常位置
controlCenterView.animate()
.translationY(0f)
.setDuration(300)
.start()
}
/**
* 隐藏控制中心
* 带动画效果:向上滑出屏幕
*/
fun hideControlCenter() {
controlCenterView.animate()
.translationY(-controlCenterView.height.toFloat())
.setDuration(300)
.withEndAction {
// 动画结束后将视图设为不可见(节省渲染资源)
controlCenterView.visibility = View.GONE
}
.start()
}
/**
* 更新桌面时间日期显示
* 使用 Handler 每分钟刷新一次
*/
private fun updateTimeDisplay() {
val handler = android.os.Handler(mainLooper)
val timeView = findViewById(R.id.tv_time)
val dateView = findViewById(R.id.tv_date)
// 创建定时任务 Runnable
val runnable = object : Runnable {
override fun run() {
val now = Calendar.getInstance()
// 更新时间:HH:mm 格式(如 14:30)
timeView.text = String.format(
"%02d:%02d",
now.get(Calendar.HOUR_OF_DAY),
now.get(Calendar.MINUTE)
)
// 星期数组(周日开始)
val weekDays = arrayOf("日", "一", "二", "三", "四", "五", "六")
// 更新日期:MM月dd日 星期X 格式
dateView.text = String.format(
"%d月%d日 星期%s",
now.get(Calendar.MONTH) + 1, // 月份从 0 开始,需 +1
now.get(Calendar.DAY_OF_MONTH),
weekDays[now.get(Calendar.DAY_OF_WEEK) - 1]
)
// 60 秒后再次执行(60000 毫秒 = 1 分钟)
handler.postDelayed(this, 60000)
}
}
// 立即执行第一次(避免等待 1 分钟才显示时间)
handler.post(runnable)
}
/**
* 拦截触摸事件,传递给手势检测器
* 必须重写此方法,否则 GestureDetector 无法接收到触摸事件
*/
override fun dispatchTouchEvent(ev: MotionEvent): Boolean {
gestureDetector.onTouchEvent(ev)
return super.dispatchTouchEvent(ev)
}
/**
* 拦截返回键
* 桌面作为系统入口,不允许通过返回键退出(否则回到系统桌面)
*/
override fun onBackPressed() {
// 如果控制中心正在显示,优先关闭控制中心
if (controlCenterView.visibility == View.VISIBLE) {
hideControlCenter()
}
// 否则不做任何操作(桌面不响应返回键退出)
}
/**
* 窗口焦点变化时重新隐藏系统 UI
* 当从其他 Activity 返回桌面时,确保系统栏保持隐藏
*/
override fun onWindowFocusChanged(hasFocus: Boolean) {
super.onWindowFocusChanged(hasFocus)
if (hasFocus) hideSystemUI()
}
}
```
---
## 4. AppItem.java 数据类
**位置**:三、仿鸿蒙 Launcher App 详细设计 → 3.5.3 AppItem.java 数据类
**功能**:桌面应用的数据模型,定义应用名称、图标资源和类型枚举(BUSINESS/SETTINGS/FAKE)。
```kotlin
package com.emoon.harmony.launcher
/**
* 桌面应用项数据类
* 用于存储每个应用图标的名称、图标资源和类型
*/
data class AppItem(
val name: String, // 应用显示名称(如"智慧医疗")
val iconResId: Int, // 图标资源 ID(指向 drawable 中的图标)
val type: Type // 应用类型,决定点击后的行为
) {
/**
* 应用类型枚举
* BUSINESS: 真实业务应用(点击跳转)
* SETTINGS: 设置入口(点击打开假设置页)
* FAKE: 装饰性图标(点击显示提示)
*/
enum class Type {
BUSINESS, // 业务应用(点击跳转到业务 App)
SETTINGS, // 设置(点击打开假设置页)
FAKE // 装饰图标(点击提示"即将推出")
}
}
```
---
## 5. AppGridAdapter.java 图标网格适配器
**位置**:三、仿鸿蒙 Launcher App 详细设计 → 3.5.4 AppGridAdapter.java 图标网格适配器
**功能**:RecyclerView 的适配器实现,负责将 AppItem 数据渲染为桌面图标卡片,包含点击事件和按下态效果。
```kotlin
package com.emoon.harmony.launcher
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import android.widget.TextView
import androidx.cardview.widget.CardView
import androidx.recyclerview.widget.RecyclerView
/**
* 桌面图标网格适配器
* 负责将 AppItem 数据列表渲染为桌面上的图标卡片
*/
class AppGridAdapter(
private val appList: List, // 应用列表数据源
private val onItemClick: (AppItem) -> Unit // 点击回调函数(lambda)
) : RecyclerView.Adapter() {
/**
* ViewHolder:缓存每个列表项的视图引用
* 避免每次滚动时都调用 findViewById,提升性能(类比 Vue 的虚拟 DOM 复用)
*/
class AppViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
// 图标卡片容器(CardView,实现圆角和阴影)
val cardView: CardView = itemView.findViewById(R.id.card_app_icon)
// 应用图标图片
val iconImage: ImageView = itemView.findViewById(R.id.iv_app_icon)
// 应用名称文字
val nameText: TextView = itemView.findViewById(R.id.tv_app_name)
}
/**
* 创建新的 ViewHolder(当列表需要展示新的项时调用)
* @param parent 父视图容器
* @param viewType 视图类型(多类型列表时使用,此处只有一种类型)
*/
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): AppViewHolder {
// 加载 item_app_icon.xml 布局文件
val view = LayoutInflater.from(parent.context)
.inflate(R.layout.item_app_icon, parent, false)
return AppViewHolder(view)
}
/**
* 绑定数据到 ViewHolder(将数据填充到视图中)
* @param holder 要绑定的 ViewHolder
* @param position 数据在列表中的索引位置
*/
override fun onBindViewHolder(holder: AppViewHolder, position: Int) {
val item = appList[position]
// 设置应用图标图片
holder.iconImage.setImageResource(item.iconResId)
// 设置应用名称文字
holder.nameText.text = item.name
// 设置点击事件监听器
holder.cardView.setOnClickListener {
onItemClick(item) // 调用外部传入的点击回调
}
// 设置按下效果:点击时临时改变背景色(视觉反馈)
holder.cardView.setOnTouchListener { v, event ->
when (event.action) {
// 手指按下:切换到按下态背景
android.view.MotionEvent.ACTION_DOWN -> {
holder.cardView.setCardBackgroundColor(
v.context.getColor(R.color.icon_card_pressed)
)
}
// 手指抬起或取消:恢复常态背景
android.view.MotionEvent.ACTION_UP,
android.view.MotionEvent.ACTION_CANCEL -> {
holder.cardView.setCardBackgroundColor(
v.context.getColor(R.color.icon_card_normal)
)
}
}
false // 返回 false 表示不拦截触摸事件,继续传递
}
}
/**
* 返回列表项总数
* RecyclerView 通过此方法知道需要渲染多少个列表项
*/
override fun getItemCount(): Int = appList.size
}
```
---
## 6. ControlCenterView.java 下拉控制中心
**位置**:三、仿鸿蒙 Launcher App 详细设计 → 3.5.6 ControlCenterView.java 下拉控制中心
**功能**:仿 HarmonyOS 下拉控制中心自定义 View,包含快捷开关(WiFi/蓝牙/移动数据/飞行模式/位置服务)、亮度/音量滑块和关闭按钮。所有交互仅切换 UI 状态,不做真实系统调用。
```kotlin
package com.emoon.harmony.launcher
import android.content.Context
import android.util.AttributeSet
import android.view.LayoutInflater
import android.view.View
import android.widget.FrameLayout
import android.widget.ImageView
import android.widget.SeekBar
import android.widget.TextView
/**
* 仿 HarmonyOS 下拉控制中心自定义 View
* 从屏幕顶部下滑触发,包含快捷开关和亮度/音量滑块
*/
class ControlCenterView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : FrameLayout(context, attrs, defStyleAttr) {
// 开关状态存储表(HashMap,键为开关名称,值为开启/关闭状态)
private val switchStates = HashMap().apply {
put("wifi", true) // WiFi:默认开启
put("bluetooth", false) // 蓝牙:默认关闭
put("mobile", true) // 移动数据:默认开启
put("airplane", false) // 飞行模式:默认关闭
put("location", true) // 位置服务:默认开启
}
// 开关视图映射表(键为开关名称,值为对应的 ImageView)
private val switchViews = HashMap()
init {
// 加载控制中心的布局 XML
LayoutInflater.from(context).inflate(R.layout.control_center, this, true)
// 初始化所有开关
initSwitches()
// 初始化亮度滑块
initBrightnessSlider()
// 初始化音量滑块
initVolumeSlider()
// 初始化关闭按钮
initCloseButton()
}
/**
* 初始化快捷开关
* 每个开关点击时仅切换本地 UI 状态,不调用真实系统 API
*/
private fun initSwitches() {
// 定义开关配置:(开关名称,图标 View 的 ID,标签 View 的 ID)
val switchConfigs = listOf(
Triple("wifi", R.id.iv_wifi, R.id.tv_wifi),
Triple("bluetooth", R.id.iv_bluetooth, R.id.tv_bluetooth),
Triple("mobile", R.id.iv_mobile, R.id.tv_mobile),
Triple("airplane", R.id.iv_airplane, R.id.tv_airplane),
Triple("location", R.id.iv_location, R.id.tv_location)
)
switchConfigs.forEach { (name, iconId, labelId) ->
val iconView = findViewById(iconId)
val labelView = findViewById(labelId)
switchViews[name] = iconView
// 设置初始状态
updateSwitchUI(name, iconView, labelView)
// 设置点击监听器
iconView.setOnClickListener {
// 切换开关状态(取反当前状态)
val newState = !(switchStates[name] ?: false)
switchStates[name] = newState
// 更新 UI 显示
updateSwitchUI(name, iconView, labelView)
}
}
}
/**
* 更新开关的 UI 显示
* @param name 开关名称
* @param iconView 开关图标视图
* @param labelView 开关标签视图
*/
private fun updateSwitchUI(name: String, iconView: ImageView, labelView: TextView) {
val isOn = switchStates[name] ?: false
if (isOn) {
// 开启态:鸿蒙蓝色背景 + 白色图标
iconView.setBackgroundResource(R.drawable.bg_switch_on)
iconView.setColorFilter(context.getColor(android.R.color.white))
labelView.setTextColor(context.getColor(R.color.harmony_blue))
} else {
// 关闭态:深灰背景 + 浅灰图标
iconView.setBackgroundResource(R.drawable.bg_switch_off)
iconView.setColorFilter(context.getColor(R.color.gray_text))
labelView.setTextColor(context.getColor(R.color.gray_text))
}
}
/**
* 初始化亮度滑块
* 仅改变滑块位置,不修改系统实际亮度
*/
private fun initBrightnessSlider() {
val seekBar = findViewById(R.id.seekbar_brightness)
val valueText = findViewById(R.id.tv_brightness_value)
// 设置初始值 70%
seekBar.progress = 70
valueText.text = "70%"
seekBar.setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener {
// 滑块拖动时实时更新显示
override fun onProgressChanged(seekBar: SeekBar?, progress: Int, fromUser: Boolean) {
valueText.text = "${progress}%"
}
// 开始拖动(空实现,但接口要求覆写)
override fun onStartTrackingTouch(seekBar: SeekBar?) {}
// 结束拖动(空实现,但接口要求覆写)
override fun onStopTrackingTouch(seekBar: SeekBar?) {}
})
}
/**
* 初始化音量滑块
* 仅改变滑块位置,不修改系统实际音量
*/
private fun initVolumeSlider() {
val seekBar = findViewById(R.id.seekbar_volume)
val valueText = findViewById(R.id.tv_volume_value)
// 设置初始值 50%
seekBar.progress = 50
valueText.text = "50%"
seekBar.setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener {
override fun onProgressChanged(seekBar: SeekBar?, progress: Int, fromUser: Boolean) {
valueText.text = "${progress}%"
}
override fun onStartTrackingTouch(seekBar: SeekBar?) {}
override fun onStopTrackingTouch(seekBar: SeekBar?) {}
})
}
/**
* 初始化关闭按钮
* 点击后隐藏控制中心
*/
private fun initCloseButton() {
findViewById(R.id.btn_close_control).setOnClickListener {
// 获取父 Activity 并调用其 hideControlCenter 方法
(context as? LauncherActivity)?.hideControlCenter()
}
}
}
```
---
## 7. FakeSettingsActivity.java 假设置页
**位置**:三、仿鸿蒙 Launcher App 详细设计 → 3.5.8 FakeSettingsActivity.java 假设置页
**功能**:假设置页面 Activity,仿 HarmonyOS 设置页风格,包含设置列表和"关于本机"对话框,展示假设备信息。
```kotlin
package com.emoon.harmony.launcher
import android.app.Activity
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import android.widget.TextView
import android.widget.Toast
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
/**
* 假设置页面 Activity
* 仿 HarmonyOS 设置页风格,展示假设备信息
*/
class FakeSettingsActivity : Activity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_fake_settings)
// 初始化设置列表
setupSettingsList()
}
/**
* 初始化设置列表 RecyclerView
*/
private fun setupSettingsList() {
val recyclerView = findViewById(R.id.rv_settings)
// 垂直线性布局(类似 Vue 的垂直列表)
recyclerView.layoutManager = LinearLayoutManager(this)
// 定义设置项数据列表
val settingsItems = listOf(
SettingItem("WLAN", "已连接", R.drawable.ic_wifi),
SettingItem("蓝牙", "已开启", R.drawable.ic_bluetooth),
SettingItem("显示和亮度", "", R.drawable.ic_brightness),
SettingItem("声音和振动", "", R.drawable.ic_volume),
SettingItem("关于本机", "", R.drawable.ic_info)
)
// 设置适配器
recyclerView.adapter = SettingsAdapter(settingsItems) { item ->
onSettingClick(item)
}
}
/**
* 处理设置项点击事件
*/
private fun onSettingClick(item: SettingItem) {
when (item.title) {
"关于本机" -> showAboutDialog()
else -> Toast.makeText(this, "${item.title} 功能即将推出", Toast.LENGTH_SHORT).show()
}
}
/**
* 显示"关于本机"对话框
* 展示仿造的设备信息,营造 HarmonyOS 系统的视觉假象
*/
private fun showAboutDialog() {
// 使用 AlertDialog 展示关于信息
val aboutView = LayoutInflater.from(this).inflate(R.layout.dialog_about, null)
// 填充设备信息数据
aboutView.findViewById(R.id.tv_device_name).text = "设备名称:豹小秘 Pro"
aboutView.findViewById(R.id.tv_system_theme).text = "系统主题:HarmonyOS 风格 4.0"
aboutView.findViewById(R.id.tv_processor).text = "处理器:Kirin 9000S"
aboutView.findViewById(R.id.tv_ram).text = "运行内存:4 GB"
aboutView.findViewById(R.id.tv_storage).text = "存储空间:64 GB"
aboutView.findViewById(R.id.tv_resolution).text = "分辨率:1280 × 800"
android.app.AlertDialog.Builder(this)
.setView(aboutView)
.setPositiveButton("确定", null)
.show()
}
/**
* 设置项数据类
*/
data class SettingItem(
val title: String, // 设置项标题
val subtitle: String, // 副标题(如"已连接")
val iconResId: Int // 左侧图标资源 ID
)
/**
* 设置列表适配器
*/
class SettingsAdapter(
private val items: List,
private val onClick: (SettingItem) -> Unit
) : RecyclerView.Adapter() {
class SettingViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
val iconView: ImageView = itemView.findViewById(R.id.iv_setting_icon)
val titleView: TextView = itemView.findViewById(R.id.tv_setting_title)
val subtitleView: TextView = itemView.findViewById(R.id.tv_setting_subtitle)
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SettingViewHolder {
val view = LayoutInflater.from(parent.context)
.inflate(R.layout.item_setting, parent, false)
return SettingViewHolder(view)
}
override fun onBindViewHolder(holder: SettingViewHolder, position: Int) {
val item = items[position]
holder.iconView.setImageResource(item.iconResId)
holder.titleView.text = item.title
holder.subtitleView.text = item.subtitle
holder.itemView.setOnClickListener { onClick(item) }
}
override fun getItemCount(): Int = items.size
}
}
```
---
## 8. BootReceiver.java 开机自启接收器
**位置**:三、仿鸿蒙 Launcher App 详细设计 → 3.5.10 BootReceiver.java 开机自启接收器(备用方案)
**功能**:接收系统 `BOOT_COMPLETED` 广播,自动启动 LauncherActivity(备用自启方案)。
```kotlin
package com.emoon.harmony.launcher
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
/**
* 开机自启广播接收器
* 接收系统开机完成广播(BOOT_COMPLETED),自动启动 LauncherActivity
*/
class BootReceiver : BroadcastReceiver() {
/**
* 当收到广播时调用
* @param context 应用上下文
* @param intent 收到的广播 Intent
*/
override fun onReceive(context: Context, intent: Intent) {
// 判断广播动作是否为开机完成
if (intent.action == Intent.ACTION_BOOT_COMPLETED) {
// 创建启动 LauncherActivity 的 Intent
val launchIntent = Intent(context, LauncherActivity::class.java).apply {
// FLAG_ACTIVITY_NEW_TASK 是必须的:从非 Activity 上下文启动 Activity 需要此标志
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
}
// 启动桌面 Activity
context.startActivity(launchIntent)
}
}
}
```
---
## 9. WebView 容器配置
**位置**:四、原生业务 App 详细设计 → 4.2 WebView 容器配置
**功能**:配置 WebView 的 JavaScript、DOM 存储、文件访问、缩放等设置,并注入 RobotBridge。
```kotlin
val webView = findViewById(R.id.webView)
webView.settings.apply {
javaScriptEnabled = true
domStorageEnabled = true
allowFileAccess = true
allowUniversalAccessFromFileURLs = true
useWideViewPort = true
loadWithOverviewMode = true
setSupportZoom(false)
}
webView.setLayerType(View.LAYER_TYPE_HARDWARE, null)
webView.webChromeClient = WebChromeClient()
webView.addJavascriptInterface(RobotBridge(this, webView), "RobotBridge")
// 调试模式:联调阶段开启,支持 Chrome DevTools 远程调试
if (BuildConfig.DEBUG) {
WebView.setWebContentsDebuggingEnabled(true)
}
// 加载本地 H5(Spring Boot 后端部署在 localhost:8080)
webView.loadUrl("http://localhost:8080")
```
---
## 10. JSBridge Android 端实现(简化版)
**位置**:四、原生业务 App 详细设计 → 4.3 JSBridge 通信协议设计 → Android 端 Bridge 实现
**功能**:RobotBridge 核心接口的简化实现,包含导航、获取位置点列表、TTS 播报、获取电量四个接口,通过 `callbackId` 模式回调 H5。
```kotlin
class RobotBridge(private val context: Context, private val webView: WebView) {
@JavascriptInterface
fun navigate(destination: String, callbackId: String) {
thread {
RobotApi.startNavigation(destination)
val result = JSONObject().apply {
put("code", 0)
put("msg", "navigation_started")
put("destination", destination)
}
webView.post {
webView.evaluateJavascript(
"window.__robotCallbacks && window.__robotCallbacks['$callbackId'] && window.__robotCallbacks['$callbackId']($result)",
null
)
}
}
}
@JavascriptInterface
fun getPlaceList(callbackId: String) {
thread {
val places = RobotApi.getPlaceList()
val result = JSONArray(places)
webView.post {
webView.evaluateJavascript(
"window.__robotCallbacks && window.__robotCallbacks['$callbackId'] && window.__robotCallbacks['$callbackId']($result)",
null
)
}
}
}
@JavascriptInterface
fun playTTS(text: String, callbackId: String) {
thread {
speechApi.playText(text)
val result = JSONObject().apply {
put("code", 0)
put("msg", "tts_started")
}
webView.post {
webView.evaluateJavascript(
"window.__robotCallbacks && window.__robotCallbacks['$callbackId'] && window.__robotCallbacks['$callbackId']($result)",
null
)
}
}
}
@JavascriptInterface
fun getBattery(callbackId: String) {
thread {
val level = RobotApi.getBatteryLevel()
val result = JSONObject().apply {
put("code", 0)
put("level", level)
}
webView.post {
webView.evaluateJavascript(
"window.__robotCallbacks && window.__robotCallbacks['$callbackId'] && window.__robotCallbacks['$callbackId']($result)",
null
)
}
}
}
}
```
---
## 11. SDK Application 初始化
**位置**:四、原生业务 App 详细设计 → 4.4 猎户星空 SDK 集成步骤 → Application 中初始化
**功能**:在 Application.onCreate() 中初始化猎户星空 SDK 并连接服务。
```kotlin
class RobotApp : Application() {
override fun onCreate() {
super.onCreate()
RobotApi.init(this)
RobotApi.connectServer { status ->
Log.d("RobotSDK", "Server 连接状态: $status")
}
}
}
```
---
## 12. 导航状态监听
**位置**:四、原生业务 App 详细设计 → 4.4 猎户星空 SDK 集成步骤 → 导航状态监听
**功能**:设置导航状态监听器,处理开始导航、避障、堵死、定位丢失、到达目的地、未定位、路径规划失败等状态码。
```kotlin
RobotApi.setNavigationListener(object : NavigationListener {
override fun onStatus(statusCode: Int, data: String?) {
when (statusCode) {
32730001 -> Log.d("NAV", "开始导航")
32730004 -> Log.d("NAV", "避障中")
32730011 -> Log.d("NAV", "堵死")
32730009 -> Log.d("NAV", "定位丢失")
}
}
override fun onResult(resultCode: Int, data: String?) {
when (resultCode) {
32610007 -> Log.d("NAV", "到达目的地")
-32620001 -> Log.d("NAV", "未定位")
-32620009 -> Log.d("NAV", "路径规划失败")
}
}
})
```
---
## 13. build.gradle(Module: app)— Gradle Kotlin DSL
**位置**:四、原生业务 App 详细设计 → 4.7 补充:完整的 build.gradle(Module: app)
**功能**:业务 App 模块的 Gradle 构建配置(使用 Kotlin DSL),定义编译版本、依赖库(AndroidX、猎户星空 SDK、Gson)、构建变体等。
```kotlin
/**
* app 模块构建配置文件
* 作用:定义编译版本、依赖库、构建变体等
* 位置:MedicalRobotApp/app/build.gradle
*/
// 插件声明:应用 Android 应用程序插件和 Kotlin Android 插件
plugins {
// Android 应用插件,提供构建 APK 的能力
alias(libs.plugins.android.application)
// Kotlin Android 插件,支持 Kotlin 语言编译
alias(libs.plugins.kotlin.android)
}
// Android 构建设置
android {
// 命名空间:与 AndroidManifest.xml 中的 package 属性一致
// 用于生成 R 类和其他资源引用
namespace = "com.emoon.medical.robot"
// 编译 SDK 版本:使用 API 34(Android 14)进行编译
// 决定了可以使用的最新 Android API
compileSdk = 34
// 默认配置:所有构建变体共享的基础配置
defaultConfig {
// 应用包名,设备上唯一标识此应用
applicationId = "com.emoon.medical.robot"
// 最低支持的 Android 版本:API 24(Android 7.0)
// 低于此版本的设备无法安装此应用
minSdk = 24
// 目标 SDK 版本:API 34(Android 14)
// 表示应用已在此版本上充分测试,系统会启用该版本的行为特性
targetSdk = 34
// 版本号:内部版本标识,每次发布必须递增
versionCode = 1
// 版本名称:对外展示的用户友好版本号
versionName = "1.0.0"
// 测试运行器:使用 AndroidJUnit4 进行单元测试
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}
// 构建类型配置
buildTypes {
// 发布(Release)构建配置
release {
// 是否启用代码压缩和混淆(发布时建议开启以减小体积)
isMinifyEnabled = false
// 混淆规则文件:proguard-rules.pro 中定义了保留哪些类不被混淆
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
}
// 调试(Debug)构建配置
debug {
// Debug 模式不启用混淆,方便调试和查看堆栈
isMinifyEnabled = false
// 开启 Debug 签名,无需手动配置签名密钥
isDebuggable = true
}
}
// 编译选项
compileOptions {
// 源码兼容性:Java 17
sourceCompatibility = JavaVersion.VERSION_17
// 目标兼容性:Java 17
targetCompatibility = JavaVersion.VERSION_17
}
// Kotlin 编译选项
kotlinOptions {
// JVM 目标版本:与 Java 版本保持一致
jvmTarget = "17"
}
// 构建特性开关
buildFeatures {
// 启用 ViewBinding:自动生成绑定类,替代 findViewById,类型安全且无需额外依赖
viewBinding = true
}
// 打包选项
packaging {
resources {
// 排除重复的资源文件,避免打包冲突
excludes += "/META-INF/{AL2.0,LGPL2.1}"
}
}
}
// 依赖声明:项目所需的外部库
dependencies {
// ===== AndroidX 核心库 =====
// AppCompat 库:提供向后兼容的 ActionBar 和主题支持
// 即使 minSdk 是 24,也能使用新版 API 的兼容实现
implementation("androidx.appcompat:appcompat:1.6.1")
// Material Design 组件库:提供按钮、卡片、对话框等符合 Material 规范的 UI 组件
implementation("com.google.android.material:material:1.11.0")
// ConstraintLayout:灵活高效的布局容器,适合复杂界面
// 相比嵌套 LinearLayout,性能更好且更易于维护
implementation("androidx.constraintlayout:constraintlayout:2.1.4")
// WebKit 库:提供增强型 WebView 支持
// 包含现代 WebView API,如 WebViewClient、WebChromeClient 等
implementation("androidx.webkit:webkit:1.9.0")
// Core KTX:Kotlin 扩展函数,简化 Android 核心 API 的调用
implementation("androidx.core:core-ktx:1.12.0")
// Activity KTX:Activity 相关的 Kotlin 扩展
implementation("androidx.activity:activity-ktx:1.8.0")
// Lifecycle 相关库:支持 ViewModel、LiveData 等生命周期感知组件
implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.7.0")
// ===== 猎户星空 RobotOS SDK =====
// 方式一:通过本地 AAR 文件引入(推荐,当前团队使用此方式)
// 将 robotos-sdk.aar 文件放入 app/libs/ 目录下,然后使用以下方式引入
implementation(files("libs/robotos-sdk.aar"))
// 方式二:通过 Maven 仓库引入(如果猎户星空后续开放 Maven 仓库)
// 取消下面一行的注释,并注释掉上面的 files 方式
// implementation("com.orionstar:robotos-sdk:2.x.x")
// ===== JSON 处理库 =====
// Gson:Google 的 JSON 序列化/反序列化库
// 用于 JSBridge 中 Native 与 H5 之间的 JSON 数据转换
implementation("com.google.code.gson:gson:2.10.1")
// ===== 测试库(开发阶段使用) =====
// JUnit 4:单元测试框架
testImplementation("junit:junit:4.13.2")
// AndroidX Test:Android 测试扩展库
androidTestImplementation("androidx.test.ext:junit:1.1.5")
// Espresso:UI 自动化测试框架
androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1")
}
```
---
## 14. MedicalRobotApplication.java 完整代码
**位置**:四、原生业务 App 详细设计 → 4.10 补充:MedicalRobotApplication.java 完整代码
**功能**:应用全局初始化类(Application),负责初始化猎户星空 SDK、建立机器人系统服务连接、管理 Mock 模式开关和全局 SDK 管理器实例。
```kotlin
package com.emoon.medical.robot
import android.app.Application
import android.util.Log
/**
* 应用全局初始化类
* 职责:
* 1. 初始化猎户星空 RobotOS SDK
* 2. 建立与机器人系统服务的连接
* 3. 管理全局状态(SDK 连接状态、Mock 模式开关等)
*
* 生命周期:
* - 应用进程启动时,系统首先创建此类的实例并调用 onCreate()
* - 在应用运行期间保持单例,直到进程被杀死
* - 任何 Activity、Service 都可以通过 (application as MedicalRobotApplication) 访问
*
* 注意:必须在 AndroidManifest.xml 的 标签中通过 android:name 属性声明此类,
* 否则系统不会调用它。
*/
class MedicalRobotApplication : Application() {
// ===== Companion Object:Kotlin 的静态成员替代方案 =====
// 用于存放全局可访问的静态常量和变量
companion object {
// 日志标签:所有此类相关的日志都使用此标签,方便在 logcat 中过滤
const val TAG = "MedicalRobot"
// 全局 SDK 管理器实例
// lateinit 表示延迟初始化,在 onCreate() 中赋值
// private set 表示外部只能读取不能修改
lateinit var sdkManager: RobotSDKManager
private set
// Mock 模式开关:true 表示使用模拟数据(无需真机即可开发调试)
// false 表示调用真实 SDK(需要部署到机器人真机)
// 开发阶段建议设为 true,联调阶段设为 false
var useMockMode: Boolean = true
}
/**
* 应用创建时的初始化方法
* 系统回调:应用进程启动后第一个被调用的方法
* 注意:此方法执行时间过长会阻塞应用启动,因此只应做轻量级初始化
*/
override fun onCreate() {
// 调用父类实现,确保框架级初始化正常完成
super.onCreate()
// 输出应用启动日志,方便在 logcat 中确认初始化流程
Log.i(TAG, "========================================")
Log.i(TAG, "应用启动,开始初始化...")
Log.i(TAG, "Mock 模式: $useMockMode")
Log.i(TAG, "========================================")
// 初始化 SDK 管理器,传入 Application 上下文
// 上下文(Context)是 Android 中访问系统资源和服务的关键对象
sdkManager = RobotSDKManager(this)
// 执行 SDK 初始化(连接机器人系统服务)
// 如果是 Mock 模式,初始化会快速完成并返回模拟的连接成功状态
sdkManager.initialize()
Log.i(TAG, "应用初始化完成")
}
/**
* 应用终止时的清理方法
* 系统回调:应用进程即将被杀死时调用(不保证一定被调用)
* 用于释放资源、断开连接等清理操作
*/
override fun onTerminate() {
Log.i(TAG, "应用终止,执行清理...")
// 断开 SDK 连接,释放资源
sdkManager.release()
// 调用父类实现
super.onTerminate()
}
/**
* 内存不足时的回调
* 系统回调:系统内存紧张时调用
* 应在此释放不必要的缓存和资源
*/
override fun onLowMemory() {
Log.w(TAG, "系统内存不足,释放资源...")
super.onLowMemory()
}
}
```
---
## 15. RobotSDKManager.java 完整代码
**位置**:四、原生业务 App 详细设计 → 4.11 补充:RobotSDKManager.java 完整代码
**功能**:猎户星空 RobotOS SDK 管理封装类(外观模式),统一封装导航、TTS、位置查询、电量等 API,支持 Mock 模式。
```kotlin
package com.emoon.medical.robot
import android.content.Context
import android.util.Log
import org.json.JSONArray
import org.json.JSONObject
/**
* 猎户星空 RobotOS SDK 管理封装类
* 职责:
* 1. 封装 SDK 的初始化和连接管理
* 2. 提供简洁的业务 API(导航、TTS、位置查询、电量等)
* 3. 统一管理回调接口,将 SDK 的异步结果转换为业务友好的回调
* 4. 支持 Mock 模式,在没有真机时返回模拟数据
*
* 设计模式:外观模式(Facade Pattern),对外隐藏 SDK 的复杂调用细节
*/
class RobotSDKManager(private val context: Context) {
// 日志标签
private val tag = "RobotSDKManager"
// ===== Mock 模式标志 =====
// 通过 MedicalRobotApplication.useMockMode 统一控制
private val isMock: Boolean
get() = MedicalRobotApplication.useMockMode
// ===== 连接状态 =====
// 记录 SDK 与机器人系统服务的连接状态
private var isConnected = false
// ===== 回调接口定义 =====
// 使用接口(Interface)定义回调规范,调用方实现此接口接收结果
/**
* 通用操作回调接口
* 适用于导航、TTS 等只需要知道成功/失败的操作
*/
interface OperationCallback {
/**
* 操作成功时调用
* @param data 可选的返回数据(JSON 格式字符串)
*/
fun onSuccess(data: String = "{}")
/**
* 操作失败时调用
* @param code 错误码
* @param message 错误描述
*/
fun onError(code: Int, message: String)
}
/**
* 导航状态监听接口
* 用于接收导航过程中的实时状态更新(开始导航、避障、堵死、到达等)
*/
interface NavigationListener {
/**
* 导航状态变化时调用
* @param statusCode 状态码(如 32730001 表示开始导航)
* @param data 附加数据
*/
fun onStatus(statusCode: Int, data: String?)
/**
* 导航结果回调
* @param resultCode 结果码(如 32610007 表示到达目的地)
* @param data 附加数据
*/
fun onResult(resultCode: Int, data: String?)
}
/**
* 位置信息数据类
* 封装机器人的当前坐标和定位状态
*/
data class Position(
val x: Double, // X 坐标(地图坐标系,单位:米)
val y: Double, // Y 坐标
val theta: Double, // 朝向角度(弧度,0 表示正东方向)
val isEstimated: Boolean // 是否已完成定位
)
/**
* 位置点数据类
* 封装地图中预设的导航目标点信息
*/
data class Place(
val name: String, // 位置点名称(如 "导诊台"、"神经内科")
val x: Double, // X 坐标
val y: Double, // Y 坐标
val theta: Double // 到达后的朝向角度
)
// ===== 初始化与连接管理 =====
/**
* 初始化 SDK 并建立与机器人系统服务的连接
* 应在 Application.onCreate() 中调用
*/
fun initialize() {
if (isMock) {
// Mock 模式:模拟初始化成功,无需连接真实服务
Log.i(tag, "[Mock] SDK 初始化成功(模拟模式)")
isConnected = true
return
}
// 真实 SDK 初始化路径
try {
Log.i(tag, "开始初始化猎户星空 SDK...")
// 初始化 RobotApi,传入 Application Context
// RobotApi.init(context)
// 连接 SDK Server(机器人系统后台服务)
// RobotApi.connectServer { status ->
// Log.i(tag, "SDK Server 连接状态: $status")
// isConnected = status == 1 // 1 表示连接成功
// }
// 设置导航状态监听
// RobotApi.setNavigationListener(object : NavigationListener { ... })
Log.i(tag, "SDK 初始化完成,等待连接...")
} catch (e: Exception) {
Log.e(tag, "SDK 初始化失败: ${e.message}", e)
isConnected = false
}
}
/**
* 释放 SDK 资源,断开连接
* 应在 Application.onTerminate() 或 Activity.onDestroy() 中调用
*/
fun release() {
if (isMock) {
Log.i(tag, "[Mock] SDK 资源已释放")
isConnected = false
return
}
try {
// RobotApi.disconnectServer()
isConnected = false
Log.i(tag, "SDK 连接已断开")
} catch (e: Exception) {
Log.e(tag, "释放 SDK 资源失败: ${e.message}", e)
}
}
/**
* 获取当前 SDK 连接状态
* @return true 表示已连接,false 表示未连接
*/
fun isConnected(): Boolean = isConnected
// ===== 导航 API =====
/**
* 启动导航到指定位置点
* @param destination 目标位置点名称(如 "导诊台"、"神经内科")
* @param callback 操作结果回调
*/
fun startNavigation(destination: String, callback: OperationCallback) {
Log.i(tag, "开始导航到: $destination")
if (isMock) {
// Mock 模式:模拟导航成功,延迟 500ms 后回调
android.os.Handler(android.os.Looper.getMainLooper()).postDelayed({
Log.i(tag, "[Mock] 导航到 [$destination] 成功")
callback.onSuccess(
JSONObject().apply {
put("destination", destination)
put("mock", true)
}.toString()
)
}, 500)
return
}
// 真实 SDK 调用路径
try {
if (!isConnected) {
callback.onError(-1, "SDK 未连接")
return
}
// 调用猎户星空 SDK 导航 API
// RobotApi.startNavigation(destination)
// 设置导航结果监听
// RobotApi.setNavigationListener(object : NavigationListener {
// override fun onResult(resultCode: Int, data: String?) {
// when (resultCode) {
// 32610007 -> callback.onSuccess() // 到达目的地
// -32620001 -> callback.onError(resultCode, "未定位")
// -32620009 -> callback.onError(resultCode, "路径规划失败")
// else -> callback.onError(resultCode, "导航失败: $data")
// }
// }
// })
callback.onSuccess()
} catch (e: Exception) {
Log.e(tag, "导航调用异常: ${e.message}", e)
callback.onError(-2, "导航异常: ${e.message}")
}
}
/**
* 停止当前导航
* @param callback 操作结果回调
*/
fun stopNavigation(callback: OperationCallback) {
Log.i(tag, "停止导航")
if (isMock) {
android.os.Handler(android.os.Looper.getMainLooper()).postDelayed({
Log.i(tag, "[Mock] 导航已停止")
callback.onSuccess()
}, 200)
return
}
try {
if (!isConnected) {
callback.onError(-1, "SDK 未连接")
return
}
// 调用猎户星空 SDK 停止运动 API
// RobotApi.stopMove()
callback.onSuccess()
} catch (e: Exception) {
Log.e(tag, "停止导航异常: ${e.message}", e)
callback.onError(-2, "停止导航异常: ${e.message}")
}
}
// ===== 位置与地图 API =====
/**
* 获取地图中所有预设位置点列表
* @param callback 结果回调,返回位置点列表的 JSON 字符串
*/
fun getPlaceList(callback: OperationCallback) {
Log.i(tag, "获取位置点列表")
if (isMock) {
// Mock 模式:返回模拟的医院位置点数据
android.os.Handler(android.os.Looper.getMainLooper()).postDelayed({
val mockPlaces = JSONArray().apply {
put(JSONObject().apply {
put("name", "导诊台")
put("x", 1.5)
put("y", 2.0)
put("theta", 1.57)
})
put(JSONObject().apply {
put("name", "神经内科")
put("x", 5.2)
put("y", 3.8)
put("theta", 0.0)
})
put(JSONObject().apply {
put("name", "心血管内科")
put("x", 5.2)
put("y", 6.5)
put("theta", 0.0)
})
put(JSONObject().apply {
put("name", "检验科")
put("x", 8.0)
put("y", 2.0)
put("theta", -1.57)
})
put(JSONObject().apply {
put("name", "药房")
put("x", 10.5)
put("y", 5.0)
put("theta", 3.14)
})
}
Log.i(tag, "[Mock] 返回 ${mockPlaces.length()} 个位置点")
callback.onSuccess(mockPlaces.toString())
}, 300)
return
}
try {
if (!isConnected) {
callback.onError(-1, "SDK 未连接")
return
}
// 调用猎户星空 SDK 获取位置点列表
// val places = RobotApi.getPlaceList()
// callback.onSuccess(JSONArray(places).toString())
callback.onSuccess("[]")
} catch (e: Exception) {
Log.e(tag, "获取位置点列表异常: ${e.message}", e)
callback.onError(-2, "获取位置点列表异常: ${e.message}")
}
}
/**
* 获取机器人当前位置坐标
* @param callback 结果回调,返回坐标 JSON 字符串
*/
fun getPosition(callback: OperationCallback) {
Log.i(tag, "获取当前位置")
if (isMock) {
android.os.Handler(android.os.Looper.getMainLooper()).postDelayed({
val mockPosition = JSONObject().apply {
put("x", 3.5)
put("y", 4.2)
put("theta", 0.78)
put("isEstimated", true)
}
Log.i(tag, "[Mock] 当前位置: (3.5, 4.2)")
callback.onSuccess(mockPosition.toString())
}, 200)
return
}
try {
if (!isConnected) {
callback.onError(-1, "SDK 未连接")
return
}
// 调用猎户星空 SDK 获取当前位置
// val pos = RobotApi.getPosition()
// val isEstimated = RobotApi.isRobotEstimate()
// val result = JSONObject().apply {
// put("x", pos.x)
// put("y", pos.y)
// put("theta", pos.theta)
// put("isEstimated", isEstimated)
// }
// callback.onSuccess(result.toString())
callback.onSuccess("{}")
} catch (e: Exception) {
Log.e(tag, "获取位置异常: ${e.message}", e)
callback.onError(-2, "获取位置异常: ${e.message}")
}
}
/**
* 检查机器人是否已完成定位
* @param callback 结果回调,返回 JSON 字符串 {"isEstimated": true/false}
*/
fun isRobotEstimate(callback: OperationCallback) {
if (isMock) {
callback.onSuccess("{\"isEstimated\": true}")
return
}
try {
// val result = RobotApi.isRobotEstimate()
// callback.onSuccess("{\"isEstimated\": $result}")
callback.onSuccess("{\"isEstimated\": false}")
} catch (e: Exception) {
callback.onError(-2, "检查定位状态异常: ${e.message}")
}
}
// ===== TTS 语音 API =====
/**
* 播放 TTS 语音播报
* @param text 要播报的文本内容
* @param callback 操作结果回调
*/
fun playTTS(text: String, callback: OperationCallback) {
Log.i(tag, "TTS 播报: $text")
if (isMock) {
android.os.Handler(android.os.Looper.getMainLooper()).postDelayed({
Log.i(tag, "[Mock] TTS 播报完成: $text")
callback.onSuccess(
JSONObject().apply {
put("text", text)
put("mock", true)
}.toString()
)
}, text.length * 200L) // 模拟朗读时间:每个字 200ms
return
}
try {
if (!isConnected) {
callback.onError(-1, "SDK 未连接")
return
}
// 调用猎户星空 SDK TTS API
// speechApi.playText(text)
callback.onSuccess()
} catch (e: Exception) {
Log.e(tag, "TTS 播报异常: ${e.message}", e)
callback.onError(-2, "TTS 播报异常: ${e.message}")
}
}
/**
* 停止 TTS 语音播报
* @param callback 操作结果回调
*/
fun stopTTS(callback: OperationCallback) {
Log.i(tag, "停止 TTS")
if (isMock) {
callback.onSuccess()
return
}
try {
// speechApi.stopTTS()
callback.onSuccess()
} catch (e: Exception) {
Log.e(tag, "停止 TTS 异常: ${e.message}", e)
callback.onError(-2, "停止 TTS 异常: ${e.message}")
}
}
// ===== 电量 API =====
/**
* 获取机器人当前电量
* @param callback 结果回调,返回 JSON 字符串 {"level": 85}
*/
fun getBatteryLevel(callback: OperationCallback) {
Log.i(tag, "获取电量")
if (isMock) {
android.os.Handler(android.os.Looper.getMainLooper()).postDelayed({
val mockLevel = 85 // 模拟电量 85%
Log.i(tag, "[Mock] 当前电量: $mockLevel%")
callback.onSuccess("{\"level\": $mockLevel}")
}, 100)
return
}
try {
if (!isConnected) {
callback.onError(-1, "SDK 未连接")
return
}
// 调用猎户星空 SDK 电量 API
// val level = RobotApi.getBatteryLevel()
// callback.onSuccess("{\"level\": $level}")
callback.onSuccess("{\"level\": 0}")
} catch (e: Exception) {
Log.e(tag, "获取电量异常: ${e.message}", e)
callback.onError(-2, "获取电量异常: ${e.message}")
}
}
}
```
---
## 16. MainActivity.java 完整代码(WebView 容器)
**位置**:四、原生业务 App 详细设计 → 4.12 补充:MainActivity.java 完整代码(WebView 容器)
**功能**:业务 App 的唯一直接可见界面,全屏 WebView 容器,负责加载 H5 页面、注入 JSBridge、处理页面加载事件、管理 WebView 生命周期。
```kotlin
package com.emoon.medical.robot
import android.annotation.SuppressLint
import android.content.pm.ActivityInfo
import android.os.Build
import android.os.Bundle
import android.view.View
import android.view.WindowInsets
import android.view.WindowInsetsController
import android.view.WindowManager
import android.webkit.WebChromeClient
import android.webkit.WebResourceError
import android.webkit.WebResourceRequest
import android.webkit.WebSettings
import android.webkit.WebView
import android.webkit.WebViewClient
import android.widget.ProgressBar
import android.widget.Toast
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
/**
* 主界面 Activity:WebView 容器
* 职责:
* 1. 提供全屏沉浸式的 WebView 环境( kiosk 模式,类似银行的自助终端)
* 2. 加载 H5 页面(远程服务器或本地 assets)
* 3. 注入 JSBridge,使 H5 页面能够调用机器人的导航、语音等能力
* 4. 管理 WebView 生命周期和系统 UI 状态
*
* 生命周期:
* onCreate → onStart → onResume → [运行中] → onPause → onStop → onDestroy
* 对应 Web 概念:页面创建 → 可见 → 可交互 → 后台 → 销毁
*/
class MainActivity : AppCompatActivity() {
// ===== 视图组件声明 =====
// lateinit 表示延迟初始化,在 onCreate() 中通过 findViewById 赋值
// WebView:Android 内置浏览器组件,用于加载和显示 H5 页面
private lateinit var webView: WebView
// ProgressBar:页面加载时的进度指示器(转圈动画)
private lateinit var progressBar: ProgressBar
// RobotBridge:JSBridge 实例,负责 Native 与 H5 之间的通信
private lateinit var robotBridge: RobotBridge
// 页面加载失败标志:用于记录当前是否处于错误状态
private var hasLoadError = false
/**
* Activity 创建时调用(系统回调)
* 这是设置布局、初始化组件的核心方法
*/
@SuppressLint("SetJavaScriptEnabled") // 抑制 "启用 JavaScript 可能有安全风险" 的编译器警告
override fun onCreate(savedInstanceState: Bundle?) {
// 调用父类实现,确保框架级初始化完成
super.onCreate(savedInstanceState)
// ===== 第 1 步:全屏沉浸式设置 =====
// 隐藏状态栏和导航栏,提供 kiosk 模式体验
hideSystemUI()
// 强制横屏:机器人屏幕为横屏,锁定方向防止旋转
requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE
// ===== 第 2 步:设置布局 =====
// 将 activity_main.xml 中定义的视图结构加载到 Activity 中
setContentView(R.layout.activity_main)
// ===== 第 3 步:查找视图组件 =====
// 通过视图 ID 获取布局文件中定义的组件实例
webView = findViewById(R.id.webview)
progressBar = findViewById(R.id.progress_bar)
// ===== 第 4 步:初始化 WebView =====
setupWebView()
// ===== 第 5 步:注入 JSBridge =====
// 将 RobotBridge 对象注入到 WebView 的 JavaScript 环境中
// H5 页面可以通过 window.RobotBridge 访问 Native 方法
injectJSBridge()
// ===== 第 6 步:加载页面 =====
loadPage()
}
/**
* 隐藏系统 UI(状态栏和导航栏),实现全屏沉浸式体验
* 原理:设置窗口的 systemUiVisibility 标志,告诉系统不要显示状态栏和导航栏
*/
private fun hideSystemUI() {
// 如果 Android 版本 >= 11(API 30),使用新的 WindowInsetsController API
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
// 设置窗口为全屏布局模式,内容延伸到状态栏和导航栏下方
window.setDecorFitsSystemWindows(false)
// 获取窗口的 Insets 控制器,控制系统栏的显示/隐藏
window.insetsController?.let { controller ->
// 隐藏状态栏(显示时间、电量等系统信息的顶部栏)
controller.hide(WindowInsets.Type.statusBars())
// 隐藏导航栏(底部的返回/主页/多任务键)
controller.hide(WindowInsets.Type.navigationBars())
// 设置系统栏行为:用户交互时自动隐藏(防止用户滑动调出导航栏)
controller.systemBarsBehavior = WindowInsetsController.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
}
} else {
// Android 10 及以下版本使用传统的 systemUiVisibility 标志
@Suppress("DEPRECATION")
window.decorView.systemUiVisibility = (
// 全屏模式:内容延伸到状态栏后面
View.SYSTEM_UI_FLAG_FULLSCREEN
// 隐藏导航栏
or View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
// 沉浸式模式:用户交互后仍保持隐藏
or View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY
// 内容延伸到导航栏后面
or View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
// 内容延伸到状态栏后面
or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
// 保持布局稳定,防止系统栏显示/隐藏时内容跳动
or View.SYSTEM_UI_FLAG_LAYOUT_STABLE
)
}
// 保持屏幕常亮:迎检演示时防止屏幕自动熄灭
window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
}
/**
* 配置 WebView 的各项设置
* WebView 是 Android 内置的浏览器引擎(基于 Chromium),用于渲染 H5 页面
*/
@SuppressLint("SetJavaScriptEnabled")
private fun setupWebView() {
webView.settings.apply {
// ===== JavaScript 支持 =====
// 启用 JavaScript:Vue 等现代前端框架需要 JS 才能运行,必须开启
javaScriptEnabled = true
// 启用 DOM Storage(Web Storage API):Vuex/Pinia 等状态管理库依赖此特性
domStorageEnabled = true
// 启用数据库存储:部分 H5 应用使用 Web SQL 或 IndexedDB
databaseEnabled = true
// 允许文件访问:WebView 可以加载本地文件(如 assets 中的资源)
allowFileAccess = true
// 允许内容访问:WebView 可以访问 ContentProvider 提供的内容
allowContentAccess = true
// 允许从文件 URL 访问其他文件 URL:本地 H5 页面可能需要加载本地其他资源
allowFileAccessFromFileURLs = true
// 允许从文件 URL 访问任意来源:本地页面可能需要访问网络资源
allowUniversalAccessFromFileURLs = true
// 允许自动播放媒体(音频/视频):语音播报功能需要自动播放
mediaPlaybackRequiresUserGesture = false
// 允许混合内容(HTTP + HTTPS):内网环境可能同时存在两种协议
mixedContentMode = WebSettings.MIXED_CONTENT_ALWAYS_ALLOW
// 使用宽视口:H5 页面可以设置自己的 viewport,WebView 按页面要求渲染
useWideViewPort = true
// 以概览模式加载页面:页面宽度适配屏幕宽度
loadWithOverviewMode = true
// 禁用缩放按钮:kiosk 模式下不允许用户手动缩放
setSupportZoom(false)
builtInZoomControls = false
displayZoomControls = false
// 设置缓存策略:优先使用缓存,加快页面加载速度
cacheMode = WebSettings.LOAD_DEFAULT
// 设置 User-Agent:追加自定义标识,H5 侧可通过此判断是否在机器人环境中
// H5 代码示例:if (navigator.userAgent.includes('MedicalRobot')) { ... }
userAgentString = "$userAgentString MedicalRobot/1.0"
// 启用硬件加速:利用 GPU 渲染页面,提升动画和滚动性能
webView.setLayerType(View.LAYER_TYPE_HARDWARE, null)
}
// ===== 调试模式配置 =====
// BuildConfig.DEBUG 在 Debug 构建时为 true,Release 构建时为 false
if (BuildConfig.DEBUG) {
// 启用 WebView 远程调试:允许 Chrome DevTools 连接到此 WebView
// 配合 Chrome 浏览器的 chrome://inspect 页面使用
WebView.setWebContentsDebuggingEnabled(true)
}
// ===== WebViewClient:处理页面加载事件 =====
// WebViewClient 控制 WebView 如何处理 URL 加载和页面事件
webView.webViewClient = object : WebViewClient() {
/**
* 页面开始加载时调用
* @param view WebView 实例
* @param url 正在加载的 URL
*/
override fun onPageStarted(view: WebView?, url: String?, favicon: android.graphics.Bitmap?) {
super.onPageStarted(view, url, favicon)
// 显示加载动画,提示用户页面正在加载
progressBar.visibility = View.VISIBLE
hasLoadError = false
}
/**
* 页面加载完成时调用
* @param view WebView 实例
* @param url 已加载完成的 URL
*/
override fun onPageFinished(view: WebView?, url: String?) {
super.onPageFinished(view, url)
// 隐藏加载动画
progressBar.visibility = View.GONE
// 如果之前加载失败,现在成功了,清除错误标志
if (hasLoadError) {
hasLoadError = false
}
}
/**
* 页面加载出错时调用
* @param view WebView 实例
* @param request 失败的请求信息
* @param error 错误详情
*/
override fun onReceivedError(
view: WebView?,
request: WebResourceRequest?,
error: WebResourceError?
) {
super.onReceivedError(view, request, error)
// 标记加载失败状态
hasLoadError = true
progressBar.visibility = View.GONE
// 在主线程显示错误提示
runOnUiThread {
Toast.makeText(
this@MainActivity,
"页面加载失败,请检查网络连接",
Toast.LENGTH_LONG
).show()
}
// 可选:加载本地离线提示页
// webView.loadUrl("file:///android_asset/error.html")
}
/**
* 拦截 URL 加载请求
* 返回 true 表示由应用处理此 URL,返回 false 表示由 WebView 继续加载
* @param view WebView 实例
* @param request 加载请求
*/
override fun shouldOverrideUrlLoading(
view: WebView?,
request: WebResourceRequest?
): Boolean {
val url = request?.url?.toString() ?: return false
// 示例:拦截特定的自定义协议(如 robot://settings)
// if (url.startsWith("robot://")) {
// handleRobotProtocol(url)
// return true
// }
// 默认行为:由 WebView 继续加载
return false
}
}
// ===== WebChromeClient:处理 JS 弹窗和高级功能 =====
// WebChromeClient 处理需要与 UI 交互的 Web 功能
webView.webChromeClient = object : WebChromeClient() {
/**
* 处理 JavaScript 的 alert() 弹窗
* 默认行为是弹出系统对话框,这里使用原生 AlertDialog 替代
*/
override fun onJsAlert(
view: WebView?,
url: String?,
message: String?,
result: android.webkit.JsResult?
): Boolean {
// 创建 AlertDialog 替代默认弹窗
AlertDialog.Builder(this@MainActivity)
.setTitle("提示") // 对话框标题
.setMessage(message) // 显示 JS 传来的消息内容
.setPositiveButton("确定") { _, _ ->
// 用户点击确定后,通知 JS 弹窗已确认
result?.confirm()
}
.setCancelable(false) // 禁止点击外部取消,确保 JS 流程继续
.show()
return true // 返回 true 表示已处理此弹窗
}
/**
* 处理文件选择(如 )
* 用于 H5 页面上传图片、拍照等功能
*/
override fun onShowFileChooser(
view: WebView?,
filePathCallback: android.webkit.ValueCallback>?,
fileChooserParams: FileChooserParams?
): Boolean {
// 实际实现需要启动相机或文件选择器
// 简化示例:返回取消,H5 侧可降级处理
filePathCallback?.onReceiveValue(null)
return true
}
}
}
/**
* 注入 JSBridge 到 WebView
* 通过 @JavascriptInterface 注解,将 Kotlin 方法暴露给 JavaScript 调用
*/
private fun injectJSBridge() {
// 创建 RobotBridge 实例,传入 WebView 和 SDK 管理器
robotBridge = RobotBridge(webView, MedicalRobotApplication.sdkManager)
// 将 RobotBridge 对象注入到 WebView 的 JavaScript 环境中
// 第二个参数 "RobotBridge" 是 JS 侧访问此对象时使用的名称
// JS 调用方式:window.RobotBridge.navigate("导诊台", "cb_001")
webView.addJavascriptInterface(robotBridge, "RobotBridge")
}
/**
* 加载 H5 页面
* 支持两种方式:远程服务器(开发推荐)或本地 assets(离线模式)
*/
private fun loadPage() {
// ===== 方式 A:加载远程服务器页面(推荐,开发阶段使用) =====
// 优势:修改 H5 代码后无需重新打包 APK,刷新即可生效
// 替换为实际的后端服务器 IP 地址和端口
webView.loadUrl("http://192.168.1.100:8080")
// ===== 方式 B:加载本地 assets 中的打包文件(离线模式) =====
// 优势:无需网络,适合演示环境或网络不稳定场景
// 使用方法:将 Vue 打包后的 dist 目录内容复制到 app/src/main/assets/web/ 下
// 取消下面一行的注释即可切换到本地模式
// webView.loadUrl("file:///android_asset/web/index.html")
}
/**
* 处理返回键事件
* 系统回调:用户按下物理返回键时调用
*/
override fun onBackPressed() {
// 检查 WebView 是否有历史记录可以后退
if (webView.canGoBack()) {
// WebView 可以后退(如从报告详情页返回到首页)
webView.goBack()
} else {
// WebView 已无历史记录,返回到 Launcher(不退出应用进程)
// moveTaskToBack 将当前任务移到后台,类似点击 Home 键
// true 表示即使当前 Activity 是根 Activity 也执行
moveTaskToBack(true)
}
}
/**
* 窗口焦点变化时调用
* 系统回调:Activity 获得或失去焦点时调用
* 用于在弹窗关闭后重新隐藏系统 UI
*/
override fun onWindowFocusChanged(hasFocus: Boolean) {
super.onWindowFocusChanged(hasFocus)
// 当 Activity 重新获得焦点时,再次隐藏系统 UI
// 防止用户通过滑动调出导航栏后,焦点变化导致系统 UI 保持显示
if (hasFocus) {
hideSystemUI()
}
}
/**
* 系统内存不足时调用
* 系统回调:系统内存紧张时调用,应释放不必要的资源
*/
override fun onLowMemory() {
super.onLowMemory()
// 清理 WebView 缓存,释放内存
webView.clearCache(true)
// 可选:清理历史记录、Cookie 等
// webView.clearHistory()
}
/**
* Activity 销毁时调用
* 系统回调:Activity 被销毁前调用,用于清理资源
*/
override fun onDestroy() {
// 释放 WebView 资源,防止内存泄漏
// WebView 持有大量 native 资源,必须显式释放
webView.stopLoading() // 停止正在进行的加载
webView.loadUrl("about:blank") // 加载空白页,释放当前页面资源
webView.clearHistory() // 清除历史记录
webView.removeAllViews() // 移除所有子视图
webView.destroy() // 销毁 WebView 实例,释放 native 内存
// 调用父类实现
super.onDestroy()
}
}
```
---
## 17. RobotBridge.java 完整 JSBridge 实现
**位置**:四、原生业务 App 详细设计 → 4.14 补充:RobotBridge.java 完整 JSBridge 实现
**功能**:完整的 JSBridge 桥接类,实现 7 个 Native 接口(导航、停止导航、位置点列表、当前位置、TTS、停止 TTS、电量),通过 `callbackId` 模式与 H5 双向通信,支持 Mock 模式。
```kotlin
package com.emoon.medical.robot
import android.os.Handler
import android.os.Looper
import android.util.Log
import android.webkit.JavascriptInterface
import android.webkit.WebView
import org.json.JSONObject
/**
* JSBridge 桥接类:连接 H5 JavaScript 与 Android Native 代码
* 职责:
* 1. 接收 H5 通过 window.RobotBridge 发起的调用请求
* 2. 转发请求到 RobotSDKManager 执行实际的 SDK 操作
* 3. 将 SDK 执行结果通过 evaluateJavascript 回传给 H5
* 4. 提供 Mock 模式,支持在 PC 模拟器上完整调试
*
* 通信协议(callbackId 模式):
* 1. H5 生成唯一 callbackId,将回调函数注册到 window.__robotCallbacks[callbackId]
* 2. H5 调用 window.RobotBridge.methodName(arg1, arg2, ..., callbackId)
* 3. Native 的 @JavascriptInterface 方法被触发,在子线程执行 SDK 调用
* 4. SDK 返回结果后,Native 通过 webView.evaluateJavascript 执行:
* window.__robotCallbacks[callbackId](resultJson)
* 5. H5 的回调函数被执行,处理返回数据
*/
class RobotBridge(
private val webView: WebView,
private val sdkManager: RobotSDKManager
) {
// 日志标签
private val tag = "RobotBridge"
// 主线程 Handler:用于从子线程切换回主线程操作 WebView
// WebView 的所有操作必须在主线程执行
private val mainHandler = Handler(Looper.getMainLooper())
// Mock 模式标志:从 Application 全局配置读取
private val isMock: Boolean
get() = MedicalRobotApplication.useMockMode
/**
* 统一回调方法:将结果 JSON 字符串回传给 H5
* @param callbackId H5 传入的回调标识
* @param resultJson 结果数据的 JSON 字符串
*/
private fun callbackToH5(callbackId: String, resultJson: String) {
// 构造要执行的 JavaScript 代码
// 先检查 __robotCallbacks 和指定 callbackId 是否存在,避免空指针
val jsCode = """
(function() {
var cb = window.__robotCallbacks && window.__robotCallbacks['$callbackId'];
if (typeof cb === 'function') {
cb($resultJson);
delete window.__robotCallbacks['$callbackId'];
return 'callback_executed';
} else {
return 'callback_not_found';
}
})()
""".trimIndent()
// 切换到主线程执行(WebView 必须在主线程操作)
mainHandler.post {
// evaluateJavascript:在 WebView 中执行 JavaScript 代码
// 第二个参数是结果回调(此处不需要,传 null)
webView.evaluateJavascript(jsCode, null)
}
}
/**
* 构造标准成功响应 JSON
* @param data 业务数据(可选)
* @return JSON 字符串 {"code": 0, "msg": "success", ...data}
*/
private fun successJson(data: Map = emptyMap()): String {
val json = JSONObject()
json.put("code", 0)
json.put("msg", "success")
data.forEach { (key, value) -> json.put(key, value) }
return json.toString()
}
/**
* 构造标准错误响应 JSON
* @param code 错误码
* @param message 错误描述
* @return JSON 字符串 {"code": code, "msg": "message"}
*/
private fun errorJson(code: Int, message: String): String {
val json = JSONObject()
json.put("code", code)
json.put("msg", message)
return json.toString()
}
// ===== 接口 1:导航到指定位置 =====
/**
* 导航到指定位置点
* @JavascriptInterface 注解:将此方法暴露给 JavaScript 调用
* @param destination 目标位置点名称(如 "导诊台"、"神经内科")
* @param callbackId H5 生成的回调标识
*/
@JavascriptInterface
fun navigate(destination: String, callbackId: String) {
Log.i(tag, "JSBridge 收到导航请求: destination=$destination, callbackId=$callbackId")
// 在子线程中执行 SDK 调用,避免阻塞 WebView 的 JS 线程
Thread {
// 调用 SDK 管理器的导航方法
sdkManager.startNavigation(destination, object : RobotSDKManager.OperationCallback {
override fun onSuccess(data: String) {
// 导航成功,构造成功响应并回传
val result = successJson(mapOf(
"destination" to destination,
"sdkData" to data
))
Log.i(tag, "导航成功,回传结果: $result")
callbackToH5(callbackId, result)
}
override fun onError(code: Int, message: String) {
// 导航失败,构造错误响应并回传
val result = errorJson(code, message)
Log.e(tag, "导航失败,回传结果: $result")
callbackToH5(callbackId, result)
}
})
}.start()
}
// ===== 接口 2:停止导航 =====
/**
* 停止当前导航
* @param callbackId H5 生成的回调标识
*/
@JavascriptInterface
fun stopNavigation(callbackId: String) {
Log.i(tag, "JSBridge 收到停止导航请求: callbackId=$callbackId")
Thread {
sdkManager.stopNavigation(object : RobotSDKManager.OperationCallback {
override fun onSuccess(data: String) {
val result = successJson()
Log.i(tag, "停止导航成功")
callbackToH5(callbackId, result)
}
override fun onError(code: Int, message: String) {
val result = errorJson(code, message)
Log.e(tag, "停止导航失败: $message")
callbackToH5(callbackId, result)
}
})
}.start()
}
// ===== 接口 3:获取位置点列表 =====
/**
* 获取地图中所有预设位置点列表
* @param callbackId H5 生成的回调标识
*/
@JavascriptInterface
fun getPlaceList(callbackId: String) {
Log.i(tag, "JSBridge 收到获取位置点列表请求: callbackId=$callbackId")
Thread {
sdkManager.getPlaceList(object : RobotSDKManager.OperationCallback {
override fun onSuccess(data: String) {
// data 是 JSON 数组字符串,直接放入响应中
val result = successJson(mapOf("places" to data))
Log.i(tag, "获取位置点列表成功")
callbackToH5(callbackId, result)
}
override fun onError(code: Int, message: String) {
val result = errorJson(code, message)
Log.e(tag, "获取位置点列表失败: $message")
callbackToH5(callbackId, result)
}
})
}.start()
}
// ===== 接口 4:获取当前位置 =====
/**
* 获取机器人当前坐标位置
* @param callbackId H5 生成的回调标识
*/
@JavascriptInterface
fun getPosition(callbackId: String) {
Log.i(tag, "JSBridge 收到获取位置请求: callbackId=$callbackId")
Thread {
sdkManager.getPosition(object : RobotSDKManager.OperationCallback {
override fun onSuccess(data: String) {
val result = successJson(mapOf("position" to data))
Log.i(tag, "获取位置成功: $data")
callbackToH5(callbackId, result)
}
override fun onError(code: Int, message: String) {
val result = errorJson(code, message)
Log.e(tag, "获取位置失败: $message")
callbackToH5(callbackId, result)
}
})
}.start()
}
// ===== 接口 5:TTS 语音播报 =====
/**
* 播放 TTS 语音播报
* @param text 要播报的文本内容
* @param callbackId H5 生成的回调标识
*/
@JavascriptInterface
fun playTTS(text: String, callbackId: String) {
Log.i(tag, "JSBridge 收到 TTS 请求: text=$text, callbackId=$callbackId")
// 输入校验:文本不能为空
if (text.isBlank()) {
val result = errorJson(-3, "TTS 文本不能为空")
callbackToH5(callbackId, result)
return
}
Thread {
sdkManager.playTTS(text, object : RobotSDKManager.OperationCallback {
override fun onSuccess(data: String) {
val result = successJson(mapOf("text" to text))
Log.i(tag, "TTS 播报成功: $text")
callbackToH5(callbackId, result)
}
override fun onError(code: Int, message: String) {
val result = errorJson(code, message)
Log.e(tag, "TTS 播报失败: $message")
callbackToH5(callbackId, result)
}
})
}.start()
}
// ===== 接口 6:停止 TTS =====
/**
* 停止 TTS 语音播报
* @param callbackId H5 生成的回调标识
*/
@JavascriptInterface
fun stopTTS(callbackId: String) {
Log.i(tag, "JSBridge 收到停止 TTS 请求: callbackId=$callbackId")
Thread {
sdkManager.stopTTS(object : RobotSDKManager.OperationCallback {
override fun onSuccess(data: String) {
val result = successJson()
Log.i(tag, "停止 TTS 成功")
callbackToH5(callbackId, result)
}
override fun onError(code: Int, message: String) {
val result = errorJson(code, message)
Log.e(tag, "停止 TTS 失败: $message")
callbackToH5(callbackId, result)
}
})
}.start()
}
// ===== 接口 7:获取电量 =====
/**
* 获取机器人当前电量
* @param callbackId H5 生成的回调标识
*/
@JavascriptInterface
fun getBattery(callbackId: String) {
Log.i(tag, "JSBridge 收到获取电量请求: callbackId=$callbackId")
Thread {
sdkManager.getBatteryLevel(object : RobotSDKManager.OperationCallback {
override fun onSuccess(data: String) {
// data 是 JSON 字符串 {"level": 85}
val result = successJson(mapOf("battery" to data))
Log.i(tag, "获取电量成功: $data")
callbackToH5(callbackId, result)
}
override fun onError(code: Int, message: String) {
val result = errorJson(code, message)
Log.e(tag, "获取电量失败: $message")
callbackToH5(callbackId, result)
}
})
}.start()
}
}
```
---
## 18. WebView 远程调试配置
**位置**:四、原生业务 App 详细设计 → 4.16.5 远程调试 WebView
**功能**:在 Debug 模式下启用 WebView 远程调试,支持 Chrome DevTools 连接。
```kotlin
if (BuildConfig.DEBUG) {
WebView.setWebContentsDebuggingEnabled(true)
}
```
---
## 19. WebView 常用设置
**位置**:五、H5 前端适配改造 → 5.5 H5 页面在 WebView 中的常见坑和解决方案
**功能**:WebView 核心初始化配置(JavaScript、DOM 存储、媒体自动播放、硬件加速)。
```kotlin
webView.settings.apply {
javaScriptEnabled = true
domStorageEnabled = true
mediaPlaybackRequiresUserGesture = false
}
webView.setLayerType(View.LAYER_TYPE_HARDWARE, null)
```
---
## 20. MedicalWebViewClient 降级加载
**位置**:五、H5 前端适配改造 → 5.6 前端打包部署到机器人的完整流程 → 混合模式
**功能**:WebViewClient 实现,当远程服务器页面加载失败时,自动降级加载本地 assets 中的离线 H5 资源。
```kotlin
inner class MedicalWebViewClient : WebViewClient() {
private var hasError = false
override fun onReceivedError(
view: WebView?,
request: WebResourceRequest?,
error: WebResourceError?
) {
super.onReceivedError(view, request, error)
// 仅处理主框架错误,忽略子资源(图片、CSS)加载失败
if (request?.isForMainFrame == true && !hasError) {
hasError = true
Log.w("MainActivity", "远程页面加载失败,降级到本地 assets: ${error?.description}")
view?.loadUrl("file:///android_asset/web/index.html")
}
}
override fun onPageFinished(view: WebView?, url: String?) {
super.onPageFinished(view, url)
// 本地 assets 加载成功时重置错误标志
if (url?.startsWith("file:///android_asset") == true) {
hasError = false
}
}
}
```
---
## 汇总
| 序号 | 功能模块 | 位置(章节) | 代码性质 |
|------|---------|-------------|---------|
| 1 | 下拉手势检测(简化) | 3.3 技术实现要点 | Kotlin |
| 2 | 全屏沉浸式(简化) | 3.3 技术实现要点 | Kotlin |
| 3 | LauncherActivity | 3.5.1 | Kotlin |
| 4 | AppItem 数据类 | 3.5.3 | Kotlin |
| 5 | AppGridAdapter | 3.5.4 | Kotlin |
| 6 | ControlCenterView | 3.5.6 | Kotlin |
| 7 | FakeSettingsActivity | 3.5.8 | Kotlin |
| 8 | BootReceiver | 3.5.10 | Kotlin |
| 9 | WebView 容器配置 | 4.2 | Kotlin |
| 10 | RobotBridge(简化版) | 4.3 | Kotlin |
| 11 | SDK Application 初始化 | 4.4 | Kotlin |
| 12 | 导航状态监听 | 4.4 | Kotlin |
| 13 | build.gradle(Kotlin DSL) | 4.7 | Gradle Kotlin DSL |
| 14 | MedicalRobotApplication | 4.10 | Kotlin |
| 15 | RobotSDKManager | 4.11 | Kotlin |
| 16 | MainActivity | 4.12 | Kotlin |
| 17 | RobotBridge(完整版) | 4.14 | Kotlin |
| 18 | WebView 远程调试 | 4.16.5 | Kotlin |
| 19 | WebView 常用设置 | 5.5 | Kotlin |
| 20 | MedicalWebViewClient | 5.6 | Kotlin |
**说明**:文档中所有代码块均标记为 `java`,但实际语法均为 Kotlin(使用了 `val`/`var`、`fun`、`data class`、`apply { }`、`when`、`?.` 安全调用、`override fun`、字符串模板等 Kotlin 特性)。其中第 13 项为 Gradle Kotlin DSL 构建脚本,其余均为 Android 应用层 Kotlin 代码。
a45675098
resumed: false
actual_subagent_type: explore
status: completed
[summary]
以下是针对 `RobotSample-main` Android Demo 工程的深入探索报告。
---
## 1. 工程整体结构
### 1.1 目录与构建配置
- **应用模块**: `app/`
- **compileSdk**: 29, **minSdk**: 16, **Java 8**
- **核心依赖**: `app/libs/robotservice.jar`(猎户星空/ OrionStar SDK)
- **其他依赖**: AndroidX AppCompat、ConstraintLayout、Gson 2.7
### 1.2 全部 Java 文件清单
| 路径 | 类名 | 说明 |
|---|---|---|
| `com.ainirobot.robotos` | | |
| `MainActivity.java` | `MainActivity` | 入口 Activity,Splash + Fragment 切换容器 |
| `LogTools.java` | `LogTools` | 全局日志工具,支持 OnLogListener 回调 |
| `application/RobotOSApplication.java` | `RobotOSApplication` | Application,初始化 RobotApi 与 SkillApi |
| `application/ModuleCallback.java` | `ModuleCallback` | 继承 `ModuleCallbackApi`,接收系统请求与硬件异常 |
| `application/SpeechCallback.java` | `SpeechCallback` | 继承 `SkillCallback`,语音播放/识别回调 |
| `audio/AudioManager.java` | `AudioManager` | 麦克风音频录制管理(PCM/WAV) |
| `audio/WavHeader.java` | `WavHeader` | WAV 文件头生成 |
| `fragment/BaseFragment.java` | `BaseFragment` | Fragment 基类,封装返回/结果视图 |
| `fragment/MainFragment.java` | `MainFragment` | 主菜单,跳转各功能 Fragment |
| `fragment/LeadFragment.java` | `LeadFragment` | **引领/巡逻**功能 |
| `fragment/NavigationFragment.java` | `NavigationFragment` | **导航**功能(含闸机路线判断) |
| `fragment/NavFragment.java` | `NavFragment` | **地图可视化导航**(带地图显示) |
| `fragment/SpeechFragment.java` | `SpeechFragment` | **语音 TTS / 语义查询** |
| `fragment/AudioFragment.java` | `AudioFragment` | 麦克风录音 Demo |
| `fragment/VisionFragment.java` | `VisionFragment` | 视觉/人脸检测与注册 |
| `fragment/BodyFollowFragment.java` | `BodyFollowFragment` | 人体跟随(Deprecated) |
| `fragment/ChargeFragment.java` | `ChargeFragment` | **自动回充/充电管理** |
| `fragment/LocationFragment.java` | `LocationFragment` | 定位、位置点设置/获取 |
| `fragment/SportFragment.java` | `SportFragment` | **运动控制(底盘+头部)** |
| `fragment/ElectricDoorControlFragment.java` | `ElectricDoorControlFragment` | 电动门状态监听+控制 |
| `fragment/ElectricDoorActionControlFragment.java` | `ElectricDoorActionControlFragment` | 电动门动作封装控制 |
| `fragment/FailedFragment.java` | `FailedFragment` | SDK 连接失败页 |
| `view/MapView.java` | `MapView` | 自定义地图绘制 View(Bitmap+Matrix) |
| `view/BackView.java` | `BackView` | 返回按钮 View |
| `view/ResultView.java` | `ResultView` | 结果展示 View |
| `maputils/MapppUtils.java` | `MapppUtils` | 地图 PGM 解析、坐标转换工具 |
| `maputils/RoverMap.java` | `RoverMap` | 地图数据实体(Bitmap、分辨率、坐标原点) |
| `maputils/Pose2d.java` | `Pose2d` | 二维坐标实体 (x, y, theta, status) |
| `maputils/PoseBean.java` | `PoseBean` | 带名称的点位实体 |
| `maputils/Constant.java` | `Constant` | 地图颜色常量、导航点枚举 |
| `maputils/...` | `DialogUtils`/`GlobalData`/`GsonUtil`/`SpecialPlaceUtil` 等 | 辅助工具 |
---
## 2. 导航模块实现
### 2.1 核心类与职责
| 类 | 职责 |
|---|---|
| `LeadFragment` | **引领(带人走)** + **巡航(巡逻)** |
| `NavigationFragment` | 基于**点位名称**的导航,含闸机路线判断 |
| `NavFragment` | **地图交互式导航**:加载地图 Bitmap、显示点位、点击导航 |
### 2.2 关键代码实现与 SDK API
#### LeadFragment(引领 & 巡航)
```java
// 文件: fragment/LeadFragment.java
// 1. 获取地图所有位置点
List route = RobotApi.getInstance().getPlaceList();
// 2. 开始巡航
RobotApi.getInstance().startCruise(int reqId, List route,
int startPoint, List dockingPoints, ActionListener listener);
// 3. 开始引领(需先获取视野内人体)
List personList = PersonApi.getInstance().getAllBodyList();
Person person = PersonUtils.getBestBody(personList, 3);
LeadingParams params = new LeadingParams();
params.setPersonId(person.getId());
params.setDestinationName("目标点位");
params.setLostTimer(10 * 1000);
params.setDetectDelay(5 * 1000);
params.setMaxDistance(3);
RobotApi.getInstance().startLead(int reqId, LeadingParams params, ActionListener listener);
// 4. 停止引领
RobotApi.getInstance().stopLead(int reqId, boolean isResetHW);
```
#### NavigationFragment(普通导航)
```java
// 文件: fragment/NavigationFragment.java
// 1. 开始导航到点位
RobotApi.getInstance().startNavigation(int reqId, String placeName,
double coordinateDeviation, long time, ActionListener listener);
// 2. 停止导航
RobotApi.getInstance().stopNavigation(int reqId);
// 3. 转向目标点(不移动)
RobotApi.getInstance().resumeSpecialPlaceTheta(int reqId, String placeName, CommandListener listener);
// 4. 获取闸机通行路线
RobotApi.getInstance().getGatePassingRoute(int reqId, String placeName, CommandListener listener);
// 5. 计算两点距离
double distance = RobotApi.getInstance().getPlaceOrPoseDistance(String name, Pose pose);
```
#### NavFragment(地图导航)
```java
// 文件: fragment/NavFragment.java
// 1. 获取当前地图名
RobotApi.getInstance().getMapName(int reqId, CommandListener listener);
// 2. 获取地图 PGM 文件描述符(共享内存)
ParcelFileDescriptor pfd = ShareMemoryApi.getInstance().getMapPgmPFD(String mapName);
// 3. 获取国际化点位列表
RobotApi.getInstance().getInternationalPlaceList(int reqId, String mapName, CommandListener listener);
// 4. 注册位姿状态监听
RobotApi.getInstance().registerStatusListener(String type, StatusListener listener);
RobotApi.getInstance().unregisterStatusListener(StatusListener listener);
// 5. 静态方法封装导航(供地图点击调用)
public static void startNavigation(String placeName) {
RobotApi.getInstance().startNavigation(0, placeName, 1.5, 10 * 1000, mNavigationListener);
}
```
### 2.3 回调监听器签名
```java
// ActionListener(用于 startNavigation/startLead/startCruise)
public interface ActionListener {
void onResult(int status, String responseString) throws RemoteException;
void onError(int errorCode, String errorString) throws RemoteException;
void onStatusUpdate(int status, String data) throws RemoteException;
}
// CommandListener(用于 getMapName/resumeSpecialPlaceTheta 等)
public interface CommandListener {
void onResult(int result, String message, String extraData);
void onStatusUpdate(int status, String data, String extraData);
void onError(int errorCode, String errorString, String extraData) throws RemoteException;
}
// StatusListener(用于 registerStatusListener)
public interface StatusListener {
void onStatusUpdate(String type, String value);
}
```
---
## 3. 语音模块实现
### 3.1 核心类与职责
| 类 | 职责 |
|---|---|
| `SpeechFragment` | UI 层:TTS 播放、停止、语义查询 |
| `RobotOSApplication` | 初始化 `SkillApi`,注册 `SpeechCallback` |
| `SpeechCallback` | 继承 `SkillCallback`,接收语音事件(开始/停止/音量/ASR 结果) |
| `AudioManager` | 独立音频录制(AudioRecord),保存 WAV |
### 3.2 关键代码实现与 SDK API
#### RobotOSApplication 中语音初始化
```java
// 文件: application/RobotOSApplication.java
private void initSkillApi() {
mSkillApi = new SkillApi();
mSkillApi.addApiEventListener(new ApiListener() {
@Override
public void handleApiConnected() {
mSkillApi.registerCallBack(mSkillCallback); // SpeechCallback
}
});
mSkillApi.connectApi(mContext);
}
public SkillApi getSkillApi() {
if (mSkillApi.isApiConnectedService()) {
return mSkillApi;
}
return null;
}
```
#### SpeechFragment(TTS & 语义查询)
```java
// 文件: fragment/SpeechFragment.java
private SkillApi mSkillApi = RobotOSApplication.getInstance().getSkillApi();
// 1. 播放 TTS
private void playText(String text) {
mSkillApi.playText(new TTSEntity("sid-1234567890", text), mTextListener);
}
// 2. 停止 TTS
private void stopTTS() {
mSkillApi.stopTTS();
}
// 3. 文本语义查询(直接通过语音技能查询)
private void queryByText(String text) {
mSkillApi.queryByText(text);
}
// 4. TTS 播放监听器
private TextListener mTextListener = new TextListener() {
@Override public void onStart() { }
@Override public void onStop() { }
@Override public void onComplete() { }
@Override public void onError() { }
};
```
#### SpeechCallback(语音系统回调)
```java
// 文件: application/SpeechCallback.java
public class SpeechCallback extends SkillCallback {
@Override public void onSpeechParResult(String s) throws RemoteException { }
@Override public void onStart() throws RemoteException { }
@Override public void onStop() throws RemoteException { }
@Override public void onVolumeChange(int i) throws RemoteException { }
@Override public void onQueryEnded(int i) throws RemoteException { }
@Override public void onQueryAsrResult(String asrResult) throws RemoteException { }
}
```
#### ModuleCallback(接收系统语音指令)
```java
// 文件: application/ModuleCallback.java
public class ModuleCallback extends ModuleCallbackApi {
@Override
public boolean onSendRequest(int reqId, String reqType, String reqText, String reqParam) throws RemoteException {
// 接收底层发起的语音 request 请求
return true;
}
}
```
#### AudioManager(本地录音)
```java
// 文件: audio/AudioManager.java
// 使用 Android AudioRecord 直接录制麦克风
public int startRecord(int channelConfig, int bufferSize, AudioRecordCallback callback)
public String stopRecord()
```
---
## 4. 地图相关模块
### 4.1 核心类
| 类 | 职责 |
|---|---|
| `NavFragment` | 地图业务逻辑:加载地图、获取点位、点击导航 |
| `MapView` | 自定义 View:地图缩放/拖拽/点位绘制/机器人位姿绘制 |
| `RoverMap` | 地图数据实体:`Bitmap bitmap`, `double x, y, res`, `byte[] extra` |
| `MapppUtils` | PGM 地图解析(zip/pfd)、像素坐标与物理坐标转换 |
| `Pose2d` / `PoseBean` | 坐标模型 |
### 4.2 关键代码实现
#### NavFragment 地图加载流程
```java
// 文件: fragment/NavFragment.java
// 1. 获取地图名 -> 获取地图文件描述符 -> 解析 RoverMap
RobotApi.getInstance().getMapName(0, new CommandListener() {
@Override
public void onResult(int result, String message, String extraData) {
getMap(message); // 加载地图
}
});
private void getMap(String name) {
// 通过共享内存获取 PGM
ParcelFileDescriptor mapPgmPFD = ShareMemoryApi.getInstance().getMapPgmPFD(name);
FileDescriptor fd = mapPgmPFD.getFileDescriptor();
mRoverMap = MapppUtils.loadPFD2RoverMap(new FileInputStream(fd));
mMapView.setBitmap(mRoverMap.bitmap);
mMapView.setResolution(mRoverMap.res);
// 获取点位列表
RobotApi.getInstance().getInternationalPlaceList(0, name, new CommandListener() {
@Override
public void onResult(int result, String message, String extraData) {
List placeBeanList = gson.fromJson(message, ...);
// 转为 PoseBean 并绘制
mMapView.setPoseBeans(poseBeans);
}
});
}
```
#### MapppUtils 坐标转换
```java
// 文件: maputils/MapppUtils.java
public static Pose2d pose2PixelByRoverMap(RoverMap costMap, Pose2d pose) {
Pose2d newPose = new Pose2d(pose.x, pose.y, pose.t, pose.status);
if (costMap != null) {
newPose.x -= costMap.x;
newPose.x /= costMap.res;
newPose.y -= costMap.y;
newPose.y /= costMap.res;
newPose.y = costMap.height - newPose.y;
}
return newPose;
}
```
#### MapView 核心功能
- **MapMode**: `PREVIEW`(预览)、`EDIT`(编辑)、`COLLECT`(采集)、`POINT`(点击选点)
- **绘制内容**: 地图 Bitmap、机器人位姿(红色箭头)、点位图标、比例尺、网格线
- **交互**: 双指缩放/旋转、单指拖拽、点位点击触发 `OnPlaceClickListener`
---
## 5. 硬件控制相关
> **注意**: 该 Demo 中**未包含 LED 灯效控制**的具体示例代码,但 `robotservice.jar` 中存在 `LedLightBean` 类。以下列出 Demo 中实际实现的硬件控制。
### 5.1 运动控制(SportFragment)
```java
// 文件: fragment/SportFragment.java
// 底盘运动
RobotApi.getInstance().goForward(int reqId, float speed, CommandListener listener); // 前进
RobotApi.getInstance().goBackward(int reqId, float speed, CommandListener listener); // 后退
RobotApi.getInstance().turnLeft(int reqId, float angle, CommandListener listener); // 左转
RobotApi.getInstance().turnRight(int reqId, float angle, CommandListener listener); // 右转
RobotApi.getInstance().stopMove(int reqId, CommandListener listener); // 停止
// 头部云台控制
RobotApi.getInstance().moveHead(int reqId,
String horizontalMode, String verticalMode,
int horizontalAngle, int verticalAngle,
CommandListener listener);
// 示例: moveHead(reqId, "relative", "relative", 0, -10, listener) // 抬头
```
### 5.2 充电控制(ChargeFragment)
```java
// 文件: fragment/ChargeFragment.java
// 自动回充
RobotApi.getInstance().startNaviToAutoChargeAction(int reqId, long timeout, ActionListener listener);
RobotApi.getInstance().stopAutoChargeAction(int reqId, boolean forceStop);
// 离开充电桩
RobotApi.getInstance().leaveChargingPile(int reqId, float distance, float speed, CommandListener listener);
// 禁用/启用自动充电策略
RobotApi.getInstance().disableBattery();
RobotApi.getInstance().enableBattery();
// 获取电量
RobotSettingApi.getInstance().getRobotString(Definition.ROBOT_SETTINGS_BATTERY_INFO);
```
### 5.3 电动门控制(两个 Fragment)
#### ElectricDoorControlFragment(状态监听 + 控制)
```java
// 文件: fragment/ElectricDoorControlFragment.java
// 注册电动门状态监听
RobotApi.getInstance().registerStatusListener(
Definition.STATUS_CAN_ELECTRIC_DOOR_CTRL, statusListener);
// 控制电动门
RobotApi.getInstance().setElectricDoorCtrl(int reqId, int doorCmd, CommandListener listener);
// doorCmd 取值:
// Definition.CAN_DOOR_DOOR1_DOOR2_OPEN / CLOSE
// Definition.CAN_DOOR_DOOR3_DOOR4_OPEN / CLOSE
// Definition.CAN_DOOR_ALL_OPEN / CLOSE
// 查询状态
RobotApi.getInstance().getElectricDoorStatus(int reqId, CommandListener listener);
```
#### ElectricDoorActionControlFragment(动作封装)
```java
// 文件: fragment/ElectricDoorActionControlFragment.java
RobotApi.getInstance().startControlElectricDoor(int reqId, int doorCmd, ActionListener listener);
```
### 5.4 人体跟随(BodyFollowFragment,Deprecated)
```java
// 文件: fragment/BodyFollowFragment.java
PersonApi.getInstance().registerPersonListener(mPersonListener);
RobotApi.getInstance().startBodyFollowAction(int reqId, int personId, ActionListener listener);
RobotApi.getInstance().stopBodyFollowAction(int reqId);
```
---
## 6. robotservice.jar SDK API 汇总
### 6.1 核心入口类
| 类 | 说明 |
|---|---|
| `com.ainirobot.coreservice.client.RobotApi` | **主 API 入口**(单例) |
| `com.ainirobot.coreservice.client.speech.SkillApi` | **语音技能 API** |
| `com.ainirobot.coreservice.client.person.PersonApi` | **人体/人脸 API** |
| `com.ainirobot.coreservice.client.robotsetting.RobotSettingApi` | **机器人设置 API** |
| `com.ainirobot.coreservice.client.ashmem.ShareMemoryApi` | **共享内存 API**(地图/图像) |
### 6.2 RobotApi 关键方法签名(由 Demo 推断)
```java
// 连接与状态
void connectServer(Context context, ApiListener listener)
boolean isApiConnectedService()
boolean isActive()
void setCallback(ModuleCallbackApi callback)
void setResponseThread(HandlerThread thread)
// 导航
void startNavigation(int reqId, String placeName, double deviation, long time, ActionListener listener)
void startNavigation(int reqId, Pose pose, double deviation, long time, ActionListener listener)
void stopNavigation(int reqId)
// 引领
void startLead(int reqId, LeadingParams params, ActionListener listener)
void stopLead(int reqId, boolean isResetHW)
// 巡航
void startCruise(int reqId, List route, int startPoint, List dockingPoints, ActionListener listener)
// 运动控制
void goForward(int reqId, float speed, CommandListener listener)
void goBackward(int reqId, float speed, CommandListener listener)
void turnLeft(int reqId, float angle, CommandListener listener)
void turnRight(int reqId, float angle, CommandListener listener)
void stopMove(int reqId, CommandListener listener)
void moveHead(int reqId, String hMode, String vMode, int hAngle, int vAngle, CommandListener listener)
// 回充
void startNaviToAutoChargeAction(int reqId, long timeout, ActionListener listener)
void stopAutoChargeAction(int reqId, boolean forceStop)
void leaveChargingPile(int reqId, float distance, float speed, CommandListener listener)
void disableBattery()
void enableBattery()
// 电动门
void setElectricDoorCtrl(int reqId, int cmd, CommandListener listener)
void getElectricDoorStatus(int reqId, CommandListener listener)
void startControlElectricDoor(int reqId, int cmd, ActionListener listener)
// 人体跟随
void startBodyFollowAction(int reqId, int personId, ActionListener listener)
void stopBodyFollowAction(int reqId)
// 地图与位置
List getPlaceList()
void getMapName(int reqId, CommandListener listener)
void getInternationalPlaceList(int reqId, String mapName, CommandListener listener)
void getGatePassingRoute(int reqId, String placeName, CommandListener listener)
double getPlaceOrPoseDistance(String name, Pose pose)
void getPosition(int reqId, CommandListener listener)
void setLocation(int reqId, String name, CommandListener listener)
void removeLocation(int reqId, String name, CommandListener listener)
void setPoseEstimate(int reqId, String jsonParams, CommandListener listener)
void isRobotEstimate(int reqId, CommandListener listener)
void isRobotInlocations(int reqId, String jsonParams, CommandListener listener)
boolean isRobotEstimate()
boolean isChargePileExits()
// 转向
void resumeSpecialPlaceTheta(int reqId, String placeName, CommandListener listener)
// 状态监听
void registerStatusListener(String type, StatusListener listener)
void unregisterStatusListener(StatusListener listener)
// 视觉/人脸
void getPictureById(int reqId, int personId, int type, CommandListener listener)
void getPersonInfoFromNet(int reqId, String userId, List facePics, CommandListener listener)
void startRegister(int reqId, String name, int timeout, int count, int type, ActionListener listener)
```
### 6.3 SkillApi 关键方法签名
```java
// 文件: com.ainirobot.coreservice.client.speech.SkillApi
void connectApi(Context context)
boolean isApiConnectedService()
void addApiEventListener(ApiListener listener)
void registerCallBack(SkillCallback callback)
// TTS
void playText(TTSEntity ttsEntity, TextListener listener)
void stopTTS()
// 语义查询
void queryByText(String text)
```
### 6.4 PersonApi 关键方法签名
```java
// 文件: com.ainirobot.coreservice.client.person.PersonApi
void registerPersonListener(PersonListener listener)
void unregisterPersonListener(PersonListener listener)
List getAllBodyList()
List getAllFaceList()
List getCompleteFaceList()
List getAllPersons()
```
### 6.5 关键 Bean / Listener 类
```java
// ActionBean
com.ainirobot.coreservice.client.actionbean.Pose
com.ainirobot.coreservice.client.actionbean.LeadingParams
com.ainirobot.coreservice.client.actionbean.PlaceBean
// Listeners
com.ainirobot.coreservice.client.listener.ActionListener
com.ainirobot.coreservice.client.listener.CommandListener
com.ainirobot.coreservice.client.listener.StatusListener
com.ainirobot.coreservice.client.listener.TextListener
com.ainirobot.coreservice.client.person.PersonListener
com.ainirobot.coreservice.client.speech.SkillCallback
com.ainirobot.coreservice.client.module.ModuleCallbackApi
com.ainirobot.coreservice.client.ApiListener
```
---
## 7. 给 Kotlin 引用的对应说明
由于 Demo 全部为 Java 编写,若要在 Kotlin 中调用,需注意以下对应关系:
| Java 概念 | Kotlin 对应写法 |
|---|---|
| `RobotApi.getInstance()` | `RobotApi.getInstance()`(单例调用不变) |
| `new ActionListener() { ... }` | `object : ActionListener { ... }` |
| `List route = ...` | `val route: List = ...` |
| `PersonUtils.getBestBody(list, 3)` | `PersonUtils.getBestBody(list, 3)`(静态方法变为伴生对象或 @JvmStatic) |
| `LeadingParams params = new LeadingParams()` | `val params = LeadingParams()` |
| `CommandListener` / `ActionListener` | 可用 SAM 转换(若接口为函数式接口):`CommandListener { result, msg, extra -> ... }` |
| `throws RemoteException` | Kotlin 无需显式捕获,但建议用 try-catch |
---
## 总结
该 Demo 完整覆盖了猎户星空机器人 SDK 的核心能力:
1. **导航**:普通导航、引领、巡航、闸机路线规划。
2. **语音**:TTS 播放、语义查询、ASR 结果回调。
3. **地图**:PGM 地图解析、共享内存读取、自定义 View 绘制与交互。
4. **硬件**:底盘运动、头部云台、自动回充、电动门控制。
5. **视觉**:人脸检测、人体跟随(旧版)、人脸注册。
若需进一步 Kotlin 化或封装为 Repository/ViewModel 层,可直接基于上述 `RobotApi` / `SkillApi` / `PersonApi` 单例进行包装。
{
"todos": [
{
"title": "读取原始文档,分析结构和代码块位置",
"status": "in_progress"
},
{
"title": "转换 Launcher 相关 Kotlin 代码(5个代码块)",
"status": "pending"
},
{
"title": "转换 WebView/JSBridge 相关 Kotlin 代码(4个代码块)",
"status": "pending"
},
{
"title": "转换 SDK 集成相关 Kotlin 代码(4个代码块)",
"status": "pending"
},
{
"title": "转换 Application/Manager/Bridge 完整类(5个代码块)",
"status": "pending"
},
{
"title": "转换 Gradle 和杂项代码块(2个代码块)",
"status": "pending"
},
{
"title": "验证替换完整性",
"status": "pending"
}
]
}
有 token 了,继续跑吧
/Users/destiny/dev/emoon/demos/card_demo/仿鸿蒙机器人系统技术方案.md){
"path": "/Users/destiny/dev/emoon/demos/card_demo/仿鸿蒙机器人系统技术方案.md",
"line_offset": 1,
"n_lines": 1000
}
/Users/destiny/dev/emoon/demos/card_demo/仿鸿蒙机器人系统技术方案.md){
"path": "/Users/destiny/dev/emoon/demos/card_demo/仿鸿蒙机器人系统技术方案.md",
"line_offset": 1001,
"n_lines": 1000
}
/Users/destiny/dev/emoon/demos/card_demo/仿鸿蒙机器人系统技术方案.md){
"path": "/Users/destiny/dev/emoon/demos/card_demo/仿鸿蒙机器人系统技术方案.md",
"line_offset": 2001,
"n_lines": 1000
}
/Users/destiny/dev/emoon/demos/card_demo/仿鸿蒙机器人系统技术方案.md){
"path": "/Users/destiny/dev/emoon/demos/card_demo/仿鸿蒙机器人系统技术方案.md",
"line_offset": 3001,
"n_lines": 1000
}
/Users/destiny/dev/emoon/demos/card_demo/仿鸿蒙机器人系统技术方案.md){
"path": "/Users/destiny/dev/emoon/demos/card_demo/仿鸿蒙机器人系统技术方案.md",
"line_offset": 4001,
"n_lines": 1246
}