支持鸿蒙OSNEXT_HDC

This commit is contained in:
floatingstarZ
2025-12-16 14:28:49 +08:00
parent 4d427bcd31
commit 95f5921887
12 changed files with 2314 additions and 102 deletions

View File

@@ -0,0 +1,337 @@
# HDC 实现总结
本文档总结了为 Open-AutoGLM 项目添加鸿蒙系统 HDC 支持的所有修改。
## 更新日期
2025-12-16
## 最新更新
### v1.2.0 - 鸿蒙应用列表支持
- ✅ 创建独立的鸿蒙应用配置文件 `apps_harmonyos.py`
- ✅ 内置 80+ 常用鸿蒙应用的 Bundle Name 映射
- ✅ 支持列出鸿蒙应用:`python main.py --device-type hdc --list-apps`
- ✅ 应用启动失败时提示可用应用列表
- ✅ 更新文档添加应用列表说明和查找方法
## 主要改进
### 1. 使用正确的 HDC 命令格式
根据鸿蒙系统的实际命令格式,更新了所有 UI 交互命令:
#### 点击操作
- **旧格式**: `hdc shell input tap X Y`
- **新格式**: `hdc shell uitest uiInput click X Y`
#### 双击操作
- **旧格式**: 两次 `hdc shell input tap X Y`
- **新格式**: `hdc shell uitest uiInput doubleClick X Y`
#### 长按操作
- **旧格式**: `hdc shell input swipe X Y X Y DURATION`
- **新格式**: `hdc shell uitest uiInput longClick X Y`
#### 滑动操作
- **旧格式**: `hdc shell input swipe X1 Y1 X2 Y2 DURATION`
- **新格式**: `hdc shell uitest uiInput swipe X1 Y1 X2 Y2 DURATION`
#### 返回键
- **旧格式**: `hdc shell input keyevent 2`
- **新格式**: `hdc shell uitest uiInput keyEvent Back`
#### Home键
- **旧格式**: `hdc shell input keyevent 1`
- **新格式**: `hdc shell uitest uiInput keyEvent Home`
### 2. 截屏命令兼容性
支持新旧两种鸿蒙版本的截屏命令(**注意:鸿蒙 HDC 只支持 JPEG 格式**
```bash
# 新版本(优先尝试)
hdc shell screenshot /data/local/tmp/tmp_screenshot.jpeg
# 旧版本(兜底方案)
hdc shell snapshot_display -f /data/local/tmp/tmp_screenshot.jpeg
```
**格式转换说明:**
- 设备上保存为 JPEG 格式HDC 限制)
- 拉取到本地后自动转换为 PNG 格式
- 模型推理使用 PNG 格式的 base64 编码
### 3. 文本输入改进
鸿蒙的文本输入与 Android 不同,需要提供坐标:
```python
# 带坐标的输入(鸿蒙特有)
type_text("hello", x=100, y=100) # hdc shell uitest uiInput inputText 100 100 hello
# 无坐标输入(需要先点击输入框)
tap(100, 100) # 先点击输入框
type_text("hello") # 然后输入文本
```
### 4. 命令输出显示
所有 HDC 命令都会自动显示在控制台:
```
[HDC] Running command: hdc shell uitest uiInput click 500 1000
[HDC] Running command: hdc shell screenshot /data/local/tmp/tmp.png
[HDC] Running command: hdc file recv /data/local/tmp/tmp.png /tmp/screenshot_abc.png
```
这有助于:
- 调试问题
- 学习 HDC 命令
- 发现命令执行失败的原因
## 修改的文件清单
### 核心实现文件
0. **phone_agent/config/apps_harmonyos.py** ⭐ 新增
- 鸿蒙应用 Bundle Name 映射表
- 支持 80+ 常用应用
- 包括第三方应用、华为系统应用、华为服务
- 提供 `get_harmonyos_app_package()``list_harmonyos_apps()` 函数
1. **phone_agent/hdc/connection.py**
- 添加 `_run_hdc_command()` 函数用于命令输出
- 添加 `set_hdc_verbose()` 控制输出开关
- 所有 subprocess.run() 替换为 _run_hdc_command()
2. **phone_agent/hdc/device.py**
-`tap()` - 使用 `uitest uiInput click`
-`double_tap()` - 使用 `uitest uiInput doubleClick`
-`long_press()` - 使用 `uitest uiInput longClick`
-`swipe()` - 使用 `uitest uiInput swipe`
-`back()` - 使用 `uitest uiInput keyEvent Back`
-`home()` - 使用 `uitest uiInput keyEvent Home`
-`launch_app()` - 使用鸿蒙应用列表,启动命令包含 `-a EntryAbility`
-`get_current_app()` - 使用鸿蒙应用列表识别当前应用
3. **phone_agent/hdc/screenshot.py**
- 支持 `screenshot``snapshot_display` 两种命令
- 自动降级处理
4. **phone_agent/hdc/input.py**
- 支持带坐标的文本输入
- 改进清除文本方法(使用组合键)
5. **phone_agent/hdc/__init__.py**
- 导出 `set_hdc_verbose` 函数
### 主程序文件
6. **main.py**
- 使用 HDC 时自动启用 verbose 模式
- 支持 `--list-apps` 列出鸿蒙应用
- 根据设备类型显示不同的应用列表
### 文档文件
7. **HDC_USAGE.md**
- 更新所有命令对照表
- 添加 `uitest uiInput` 命令说明
- 添加方向滑动、组合键等鸿蒙特有功能
- 更新示例输出
- 添加常见问题解答
8. **HDC_IMPLEMENTATION_SUMMARY.md**(本文件)
- 实现总结文档
## 命令对照速查表
### 必须使用 uitest uiInput 的命令
| 操作 | 命令格式 |
|------|---------|
| 点击 | `hdc shell uitest uiInput click X Y` |
| 双击 | `hdc shell uitest uiInput doubleClick X Y` |
| 长按 | `hdc shell uitest uiInput longClick X Y` |
| 滑动 | `hdc shell uitest uiInput swipe X1 Y1 X2 Y2 DURATION` |
| 快速滑动 | `hdc shell uitest uiInput fling X1 Y1 X2 Y2 500` |
| 拖拽 | `hdc shell uitest uiInput drag X1 Y1 X2 Y2 500` |
| 返回键 | `hdc shell uitest uiInput keyEvent Back` |
| Home键 | `hdc shell uitest uiInput keyEvent Home` |
| 文本输入 | `hdc shell uitest uiInput inputText X Y text` |
### 方向滑动(鸿蒙特有)
| 方向 | 命令格式 | 参数说明 |
|------|---------|---------|
| 左滑 | `hdc shell uitest uiInput dircFling 0 500` | 0=左500=速度 |
| 右滑 | `hdc shell uitest uiInput dircFling 1 600` | 1=右600=速度 |
| 上滑 | `hdc shell uitest uiInput dircFling 2` | 2=上 |
| 下滑 | `hdc shell uitest uiInput dircFling 3` | 3=下 |
## 使用示例
### 查看支持的应用
```bash
# 列出所有支持的鸿蒙应用
python main.py --device-type hdc --list-apps
```
输出:
```
Supported HarmonyOS apps:
- WPS
- 阿里巴巴
- 百度
- 抖音
- 淘宝
- 小红书
- 浏览器
- 相机
- 设置
- 80+ 应用)
```
### 基本使用
```bash
# 使用 HDC 控制鸿蒙设备(自动显示所有命令)
python main.py --device-type hdc --base-url http://localhost:8000/v1 --model "autoglm-phone-9b" "打开小红书"
```
### 你会看到的输出
```
[HDC] Running command: hdc list targets
[HDC] Running command: hdc shell screenshot /data/local/tmp/tmp_screenshot.png
[HDC] Running command: hdc file recv /data/local/tmp/tmp_screenshot.png /tmp/screenshot_xxx.png
[HDC] Running command: hdc shell uitest uiInput click 500 1000
[HDC] Running command: hdc shell aa start -b com.tencent.mm
```
### 手动测试命令
```bash
# 点击测试
hdc shell uitest uiInput click 500 1000
# 双击测试
hdc shell uitest uiInput doubleClick 500 1000
# 滑动测试
hdc shell uitest uiInput swipe 100 500 900 500 500
# 返回键
hdc shell uitest uiInput keyEvent Back
```
## 关键改进点
1. **命令格式完全正确** - 所有 UI 操作都使用 `uitest uiInput`
2. **自动命令显示** - 便于调试和学习
3. **兼容性处理** - 截屏等命令支持新旧版本
4. **详细文档** - 包含完整的命令对照和使用说明
5. **易于切换** - 通过 `--device-type` 参数在 ADB 和 HDC 之间切换
6. **独立应用列表** - 鸿蒙使用专用的应用配置,包含 80+ 常用应用
7. **智能提示** - 应用启动失败时显示可用应用列表
## 测试建议
1. **连接测试**
```bash
hdc list targets
python main.py --device-type hdc --list-devices
```
2. **截屏测试**
```bash
hdc shell screenshot /data/local/tmp/test.jpeg
hdc file recv /data/local/tmp/test.jpeg ~/test.jpeg
```
3. **点击测试**
```bash
hdc shell uitest uiInput click 500 1000
```
4. **完整流程测试**
```bash
python main.py --device-type hdc "打开设置"
```
## 支持的鸿蒙应用
系统内置 80+ 常用鸿蒙应用,包括:
### 第三方应用13个
百度、淘宝、WPS、快手、飞书、抖音、企业微信、同程旅行、唯品会、喜马拉雅、小红书等
### 华为系统应用40+
浏览器、计算器、日历、相机、时钟、云盘、邮件、文件管理器、录音机、笔记、相册、联系人、短信、电话、设置、健康、地图、钱包、智慧生活等
### 华为服务10+
应用市场、音乐、主题、天气、视频、阅读、游戏中心、搜索、我的华为等
### 添加新应用
如需添加新应用,编辑 `phone_agent/config/apps_harmonyos.py`
```python
HARMONYOS_APP_PACKAGES = {
"应用名": "com.example.bundle.name",
}
```
查找 Bundle Name
```bash
hdc shell bm dump -a | grep "关键词"
```
## 注意事项
1. ⚠️ **文本输入**需要先点击输入框或提供坐标
2. ⚠️ **应用列表**鸿蒙使用独立的应用配置(`apps_harmonyos.py`
3. ⚠️ **Bundle Name**与 Android 的 Package Name 不同
4. ⚠️ **设备权限**确保开启 USB 调试和相关权限
5. ✅ **命令输出**默认启用,方便调试
6. ✅ **降级处理**截屏等命令有兜底方案
7. ✅ **智能提示**应用未找到时会显示可用应用列表
## 参考资料
- [awesome-hdc](https://github.com/codematrixer/awesome-hdc) - HDC 命令参考
- [HarmonyOS 官方文档](https://developer.harmonyos.com/)
- [OpenHarmony 文档](https://gitee.com/openharmony/docs)
## 版本信息
- 实现版本v1.2.0
- 支持的鸿蒙版本HarmonyOS 2.0+
- 支持的 HDC 版本2.0.0a+
- 支持的应用数量80+ 常用应用
## 更新日志
### v1.2.0 (2025-12-16)
- ✅ 添加独立的鸿蒙应用配置文件
- ✅ 内置 80+ 常用应用的 Bundle Name
- ✅ 支持列出鸿蒙应用
- ✅ 应用启动失败时智能提示
- ✅ 更新文档添加应用查找方法
### v1.1.0 (2025-12-16)
- ✅ 修正所有 HDC 命令格式为 `uitest uiInput`
- ✅ 添加命令输出显示功能
- ✅ 支持截屏命令降级处理
- ✅ 完善文档和使用说明
### v1.0.0 (2025-12-16)
- ✅ 初始 HDC 支持实现
- ✅ 基础设备操作功能
- ✅ ADB/HDC 切换支持
---
**实现完成!现在可以完美支持鸿蒙设备,包含 80+ 常用应用,所有命令都使用正确的 uitest uiInput 格式。**

453
HDC_USAGE.md Normal file
View File

@@ -0,0 +1,453 @@
# HDC 鸿蒙系统支持使用说明
本项目已添加对 HarmonyOS鸿蒙系统的支持通过 HDCHarmonyOS Device Connector工具进行设备控制。
## 功能特性
- ✅ 完整的 HDC 命令支持
- ✅ 自动显示所有执行的 HDC 命令(便于调试)
- ✅ 支持 USB 和远程连接
- ✅ 与 ADB 相同的操作接口
- ✅ 通过 `--device-type` 参数快速切换
## 安装 HDC
### 1. 下载 HDC 工具
从以下来源获取 HDC
- HarmonyOS SDK
- OpenHarmony 官方仓库https://gitee.com/openharmony/docs
### 2. 配置环境变量
将 HDC 可执行文件路径添加到系统 PATH
**macOS/Linux:**
```bash
export PATH=$PATH:/path/to/hdc
```
**Windows:**
在系统环境变量中添加 HDC 所在目录到 PATH。
### 3. 验证安装
```bash
hdc -v
```
应该输出 HDC 版本信息。
## 使用方法
### 基本使用
#### 使用 HDC 控制鸿蒙设备
```bash
python main.py --device-type hdc --base-url http://localhost:8000/v1 --model "autoglm-phone-9b" "打开微信"
```
#### 查看 HDC 命令输出
使用 HDC 时,所有命令都会自动显示,格式如下:
```
[HDC] Running command: hdc list targets
[HDC] Running command: hdc shell snapshot_display -f /data/local/tmp/tmp.png
[HDC] Running command: hdc file recv /data/local/tmp/tmp.png /tmp/screenshot_xxx.png
[HDC] Running command: hdc shell input tap 500 1000
```
#### 使用环境变量
```bash
# 设置默认使用 HDC
export PHONE_AGENT_DEVICE_TYPE=hdc
# 控制 HDC 命令输出(默认已启用)
export HDC_VERBOSE=true
# 运行
python main.py "打开美团搜索附近的火锅店"
```
### 设备管理
#### 列出连接的鸿蒙设备
```bash
python main.py --device-type hdc --list-devices
```
#### 连接远程鸿蒙设备
```bash
# 通过 WiFi 连接
python main.py --device-type hdc --connect 192.168.1.100:5555
# 连接后执行任务
python main.py --device-type hdc --device-id 192.168.1.100:5555 "打开淘宝"
```
#### 启用 TCP/IP 模式
```bash
# 首先通过 USB 连接设备,然后启用 TCP/IP
python main.py --device-type hdc --enable-tcpip 5555
```
### 列出支持的鸿蒙应用
```bash
python main.py --device-type hdc --list-apps
```
输出示例:
```
Supported HarmonyOS apps:
- WPS
- 阿里巴巴
- 百度
- 抖音
- 淘宝
- 小红书
- 浏览器
- 相机
- 设置
- ...(更多应用)
```
### 交互模式
```bash
python main.py --device-type hdc --base-url http://localhost:8000/v1
```
然后输入任务:
```
Enter your task: 打开小红书搜索美食
Enter your task: 打开抖音刷视频
Enter your task: quit
```
## HDC 与 ADB 命令对照
### 基础命令对照
| 功能 | ADB 命令 | HDC 命令 |
|------|----------|----------|
| 列出设备 | `adb devices` | `hdc list targets` |
| 连接远程设备 | `adb connect IP:PORT` | `hdc tconn IP:PORT` |
| 断开设备 | `adb disconnect` | `hdc tdisconn` |
| 启动应用 | `adb shell monkey -p PACKAGE` | `hdc shell aa start -b BUNDLE` |
| 文件拉取 | `adb pull` | `hdc file recv` |
| 文件推送 | `adb push` | `hdc file send` |
### UI 交互命令对照(重要)
鸿蒙系统使用 `uitest uiInput` 进行 UI 交互,与 Android 的 `input` 命令有很大不同:
| 功能 | ADB 命令 | HDC 命令 |
|------|----------|----------|
| 截屏 | `adb shell screencap -p` | `hdc shell screenshot /path/to/screenshot.jpeg` (仅支持JPEG) |
| 点击 | `adb shell input tap X Y` | `hdc shell uitest uiInput click X Y` |
| 双击 | `adb shell input tap X Y`(执行两次) | `hdc shell uitest uiInput doubleClick X Y` |
| 长按 | `adb shell input swipe X Y X Y 1000` | `hdc shell uitest uiInput longClick X Y` |
| 滑动 | `adb shell input swipe X1 Y1 X2 Y2 DURATION` | `hdc shell uitest uiInput swipe X1 Y1 X2 Y2 DURATION` |
| 快速滑动 | - | `hdc shell uitest uiInput fling X1 Y1 X2 Y2 500` |
| 拖拽 | - | `hdc shell uitest uiInput drag X1 Y1 X2 Y2 500` |
| 返回键 | `adb shell input keyevent 4` | `hdc shell uitest uiInput keyEvent Back` |
| Home键 | `adb shell input keyevent KEYCODE_HOME` | `hdc shell uitest uiInput keyEvent Home` |
| 输入文本 | `adb shell input text "hello"` | `hdc shell uitest uiInput inputText X Y hello` |
### 方向滑动命令(鸿蒙特有)
```bash
# 左滑
hdc shell uitest uiInput dircFling 0 500
# 右滑
hdc shell uitest uiInput dircFling 1 600
# 上滑
hdc shell uitest uiInput dircFling 2
# 下滑
hdc shell uitest uiInput dircFling 3
```
### 组合键(鸿蒙特有)
```bash
# 组合键粘贴操作
hdc shell uitest uiInput keyEvent 2072 2038
```
## 命令输出示例
当使用 HDC 时,你会看到类似以下的输出:
```bash
$ python main.py --device-type hdc "打开微信并点击某个按钮"
🔍 Checking system requirements...
--------------------------------------------------
1. Checking HDC installation... ✅ OK (Ver: 2.0.0a)
2. Checking connected devices... ✅ OK (1 device(s): FMR0223C13000649)
3. Skipping keyboard check for HarmonyOS... ✅ OK (using native input)
--------------------------------------------------
✅ All system checks passed!
==================================================
Phone Agent - AI-powered phone automation
==================================================
Model: autoglm-phone-9b
Base URL: http://localhost:8000/v1
Max Steps: 100
Language: cn
Device Type: HDC
Device: FMR0223C13000649 (auto-detected)
==================================================
Task: 打开微信并点击某个按钮
[HDC] Running command: hdc shell screenshot /data/local/tmp/tmp_screenshot.png
[HDC] Running command: hdc file recv /data/local/tmp/tmp_screenshot.png /tmp/screenshot_abc123.png
[HDC] Running command: hdc shell hidumper -s WindowManagerService -a -a
==================================================
💭 思考过程:
--------------------------------------------------
需要启动微信应用
--------------------------------------------------
🎯 执行动作:
{
"_metadata": "do",
"action": "Launch",
"app": "微信"
}
==================================================
[HDC] Running command: hdc shell aa start -b com.tencent.mm
[HDC] Running command: hdc shell screenshot /data/local/tmp/tmp_screenshot.png
[HDC] Running command: hdc file recv /data/local/tmp/tmp_screenshot.png /tmp/screenshot_def456.png
==================================================
💭 思考过程:
--------------------------------------------------
识别到按钮在坐标 (500, 1000),需要点击
--------------------------------------------------
🎯 执行动作:
{
"_metadata": "do",
"action": "Tap",
"element": [500, 1000]
}
==================================================
[HDC] Running command: hdc shell uitest uiInput click 540 2400
🎉 ================================================
✅ 任务完成: 已成功完成任务
==================================================
```
## 调试技巧
### 1. 查看详细的 HDC 命令
HDC 模式下默认启用命令输出。如果需要关闭:
```python
from phone_agent.hdc import set_hdc_verbose
set_hdc_verbose(False)
```
或通过环境变量:
```bash
export HDC_VERBOSE=false
python main.py --device-type hdc "你的任务"
```
### 2. 检查设备连接
```bash
# 直接使用 HDC 命令检查
hdc list targets
# 通过程序检查
python main.py --device-type hdc --list-devices
```
### 3. 测试单个命令
```bash
# 截图测试(注意:必须使用 .jpeg 扩展名)
hdc shell screenshot /data/local/tmp/test.jpeg
hdc file recv /data/local/tmp/test.jpeg ~/test.jpeg
# 点击测试(使用 uitest
hdc shell uitest uiInput click 500 1000
# 双击测试
hdc shell uitest uiInput doubleClick 500 1000
# 长按测试
hdc shell uitest uiInput longClick 500 1000
# 滑动测试
hdc shell uitest uiInput swipe 100 500 900 500 500
# 返回键
hdc shell uitest uiInput keyEvent Back
# Home键
hdc shell uitest uiInput keyEvent Home
# 启动应用测试
hdc shell aa start -b com.example.app
```
## 支持的鸿蒙应用
系统已内置常用鸿蒙应用的 Bundle Name 映射,包括:
### 第三方应用
- 百度、淘宝、WPS、快手、飞书、抖音、企业微信
- 同程旅行、唯品会、喜马拉雅、小红书
### 华为系统应用
- 工具类:浏览器、计算器、日历、相机、时钟、云盘、邮件、文件管理器、录音机、笔记
- 媒体类:相册、图库
- 通讯类:联系人、短信、电话
- 设置类:设置
- 生活服务:健康、地图、钱包、智慧生活、小艺
### 华为服务
- 应用市场、音乐、主题、天气、视频、阅读、游戏中心、搜索、我的华为
查看完整列表:
```bash
python main.py --device-type hdc --list-apps
```
### 添加新应用
如果你需要的应用不在列表中,可以手动添加到 `phone_agent/config/apps_harmonyos.py`
```python
HARMONYOS_APP_PACKAGES = {
# ...
"你的应用名": "com.example.bundle.name",
}
```
或者在运行时查找应用的 Bundle Name
```bash
# 列出设备上所有已安装的应用
hdc shell bm dump -a
# 查找特定应用的包名
hdc shell bm dump -a | grep "应用关键词"
```
## 注意事项
1. **UI 交互命令差异**
- 鸿蒙使用 `uitest uiInput` 系列命令,与 Android 的 `input` 命令完全不同
- 所有点击、滑动等操作都通过 `uitest uiInput` 执行
- 文本输入在鸿蒙上需要提供坐标:`inputText X Y text`
2. **应用包名差异**
- 鸿蒙应用使用 Bundle Name 而非 Android 的 Package Name
- 已内置常用应用的 Bundle Name 映射
- 启动应用时使用 `-a EntryAbility` 指定主入口
3. **输入法支持**
- 鸿蒙系统使用原生输入法,不需要安装 ADB Keyboard
- 文本输入通过 `uitest uiInput inputText` 命令,需要先点击输入框获得焦点
- 或者直接提供输入框坐标进行输入
4. **权限设置**
- 确保鸿蒙设备已开启 USB 调试
- 某些操作可能需要额外的安全设置权限
- 部分设备可能需要开启"允许模拟位置"等选项
5. **命令输出**:所有 HDC 命令都会显示在控制台,这有助于:
- 调试问题
- 了解系统如何与设备交互
- 学习 HDC 命令的使用
- 发现命令执行失败的原因
6. **截屏命令兼容性**
- ⚠️ **重要**:鸿蒙 HDC 只支持 JPEG 格式,不支持 PNG
- 新版本鸿蒙:`hdc shell screenshot /data/local/tmp/screenshot.jpeg`
- 旧版本鸿蒙:`hdc shell snapshot_display -f /data/local/tmp/screenshot.jpeg`
- 系统会自动尝试两种方法,并将 JPEG 转换为 PNG 供模型使用
## 常见问题
### Q: HDC 命令输出太多,如何关闭?
A: 设置环境变量 `HDC_VERBOSE=false` 或在代码中调用 `set_hdc_verbose(False)`
### Q: 如何在 Android 和鸿蒙设备之间切换?
A: 使用 `--device-type` 参数:
- Android: `--device-type adb`(默认)
- 鸿蒙: `--device-type hdc`
### Q: 能否同时连接 Android 和鸿蒙设备?
A: 同一时间只能使用一种设备类型。如需切换,重新运行程序并指定不同的 `--device-type`
### Q: HDC 与 ADB 的功能有什么区别?
A: 核心功能相同,但有重要差异:
- **UI 交互命令完全不同**HDC 使用 `uitest uiInput` 而不是 `input`
- 应用管理方式不同Bundle vs Package
- 某些系统服务名称不同
- 文本输入方式不同HDC 需要坐标)
### Q: 为什么文本输入不工作?
A: 鸿蒙的文本输入与 Android 不同:
1. 方式一:先用 `click` 点击输入框,然后系统会尝试使用 `input text` 命令
2. 方式二:直接使用 `uitest uiInput inputText X Y text`,需要知道输入框坐标
3. 推荐先点击输入框获得焦点,然后输入文本
### Q: 我的命令执行失败了怎么办?
A: 查看控制台输出的 HDC 命令:
1. 检查命令格式是否正确(特别注意 `uitest uiInput` 格式)
2. 手动在终端执行相同命令测试
3. 检查设备权限设置
4. 确认鸿蒙版本是否支持该命令
### Q: 如何找到应用的 Bundle Name
A: 使用以下方法:
1. 列出所有应用:`hdc shell bm dump -a`
2. 搜索特定应用:`hdc shell bm dump -a | grep "关键词"`
3. 查看应用详情:`hdc shell bm dump -n <bundle_name>`
4. 将找到的 Bundle Name 添加到 `apps_harmonyos.py` 配置文件
### Q: 为什么应用启动失败?
A: 可能的原因:
1. Bundle Name 不正确 - 检查应用是否在 `apps_harmonyos.py`
2. Ability 名称不对 - 大部分应用使用 "EntryAbility",少数可能不同
3. 应用未安装 - 确认设备上已安装该应用
4. 权限问题 - 某些应用可能需要额外权限
## 技术支持
如遇到问题,请提供以下信息:
1. HDC 版本:`hdc -v`
2. 设备信息:`hdc list targets`
3. 错误日志(包含命令输出)
4. 鸿蒙系统版本
提交 Issuehttps://github.com/zai-org/Open-AutoGLM/issues

194
main.py
View File

@@ -23,20 +23,24 @@ from urllib.parse import urlparse
from openai import OpenAI
from phone_agent import PhoneAgent
from phone_agent.adb import ADBConnection, list_devices
from phone_agent.agent import AgentConfig
from phone_agent.config.apps import list_supported_apps
from phone_agent.config.apps_harmonyos import list_supported_apps as list_harmonyos_apps
from phone_agent.device_factory import DeviceType, get_device_factory, set_device_type
from phone_agent.model import ModelConfig
def check_system_requirements() -> bool:
def check_system_requirements(device_type: DeviceType = DeviceType.ADB) -> bool:
"""
Check system requirements before running the agent.
Checks:
1. ADB tools installed
1. ADB/HDC tools installed
2. At least one device connected
3. ADB Keyboard installed on the device
3. ADB Keyboard installed on the device (for ADB only)
Args:
device_type: Type of device tool (ADB or HDC).
Returns:
True if all checks pass, False otherwise.
@@ -46,38 +50,47 @@ def check_system_requirements() -> bool:
all_passed = True
# Check 1: ADB installed
print("1. Checking ADB installation...", end=" ")
if shutil.which("adb") is None:
# Determine tool name and command
tool_name = "ADB" if device_type == DeviceType.ADB else "HDC"
tool_cmd = "adb" if device_type == DeviceType.ADB else "hdc"
# Check 1: Tool installed
print(f"1. Checking {tool_name} installation...", end=" ")
if shutil.which(tool_cmd) is None:
print("❌ FAILED")
print(" Error: ADB is not installed or not in PATH.")
print(" Solution: Install Android SDK Platform Tools:")
print(" - macOS: brew install android-platform-tools")
print(" - Linux: sudo apt install android-tools-adb")
print(
" - Windows: Download from https://developer.android.com/studio/releases/platform-tools"
)
print(f" Error: {tool_name} is not installed or not in PATH.")
print(f" Solution: Install {tool_name}:")
if device_type == DeviceType.ADB:
print(" - macOS: brew install android-platform-tools")
print(" - Linux: sudo apt install android-tools-adb")
print(
" - Windows: Download from https://developer.android.com/studio/releases/platform-tools"
)
else:
print(" - Download from HarmonyOS SDK or https://gitee.com/openharmony/docs")
print(" - Add to PATH environment variable")
all_passed = False
else:
# Double check by running adb version
# Double check by running version command
try:
version_cmd = [tool_cmd, "version"] if device_type == DeviceType.ADB else [tool_cmd, "-v"]
result = subprocess.run(
["adb", "version"], capture_output=True, text=True, timeout=10
version_cmd, capture_output=True, text=True, timeout=10
)
if result.returncode == 0:
version_line = result.stdout.strip().split("\n")[0]
print(f"✅ OK ({version_line})")
else:
print("❌ FAILED")
print(" Error: ADB command failed to run.")
print(f" Error: {tool_name} command failed to run.")
all_passed = False
except FileNotFoundError:
print("❌ FAILED")
print(" Error: ADB command not found.")
print(f" Error: {tool_name} command not found.")
all_passed = False
except subprocess.TimeoutExpired:
print("❌ FAILED")
print(" Error: ADB command timed out.")
print(f" Error: {tool_name} command timed out.")
all_passed = False
# If ADB is not installed, skip remaining checks
@@ -89,27 +102,42 @@ def check_system_requirements() -> bool:
# Check 2: Device connected
print("2. Checking connected devices...", end=" ")
try:
result = subprocess.run(
["adb", "devices"], capture_output=True, text=True, timeout=10
)
lines = result.stdout.strip().split("\n")
# Filter out header and empty lines, look for 'device' status
devices = [line for line in lines[1:] if line.strip() and "\tdevice" in line]
if device_type == DeviceType.ADB:
result = subprocess.run(
["adb", "devices"], capture_output=True, text=True, timeout=10
)
lines = result.stdout.strip().split("\n")
# Filter out header and empty lines, look for 'device' status
devices = [line for line in lines[1:] if line.strip() and "\tdevice" in line]
else: # HDC
result = subprocess.run(
["hdc", "list", "targets"], capture_output=True, text=True, timeout=10
)
lines = result.stdout.strip().split("\n")
devices = [line for line in lines if line.strip()]
if not devices:
print("❌ FAILED")
print(" Error: No devices connected.")
print(" Solution:")
print(" 1. Enable USB debugging on your Android device")
print(" 2. Connect via USB and authorize the connection")
print(" 3. Or connect remotely: python main.py --connect <ip>:<port>")
if device_type == DeviceType.ADB:
print(" 1. Enable USB debugging on your Android device")
print(" 2. Connect via USB and authorize the connection")
print(" 3. Or connect remotely: python main.py --connect <ip>:<port>")
else:
print(" 1. Enable USB debugging on your HarmonyOS device")
print(" 2. Connect via USB and authorize the connection")
print(" 3. Or connect remotely: python main.py --device-type hdc --connect <ip>:<port>")
all_passed = False
else:
device_ids = [d.split("\t")[0] for d in devices]
if device_type == DeviceType.ADB:
device_ids = [d.split("\t")[0] for d in devices]
else:
device_ids = [d.strip() for d in devices]
print(f"✅ OK ({len(devices)} device(s): {', '.join(device_ids)})")
except subprocess.TimeoutExpired:
print("❌ FAILED")
print(" Error: ADB command timed out.")
print(f" Error: {tool_name} command timed out.")
all_passed = False
except Exception as e:
print("❌ FAILED")
@@ -122,40 +150,45 @@ def check_system_requirements() -> bool:
print("❌ System check failed. Please fix the issues above.")
return False
# Check 3: ADB Keyboard installed
print("3. Checking ADB Keyboard...", end=" ")
try:
result = subprocess.run(
["adb", "shell", "ime", "list", "-s"],
capture_output=True,
text=True,
timeout=10,
)
ime_list = result.stdout.strip()
# Check 3: ADB Keyboard installed (only for ADB)
if device_type == DeviceType.ADB:
print("3. Checking ADB Keyboard...", end=" ")
try:
result = subprocess.run(
["adb", "shell", "ime", "list", "-s"],
capture_output=True,
text=True,
timeout=10,
)
ime_list = result.stdout.strip()
if "com.android.adbkeyboard/.AdbIME" in ime_list:
print("✅ OK")
else:
if "com.android.adbkeyboard/.AdbIME" in ime_list:
print("✅ OK")
else:
print("❌ FAILED")
print(" Error: ADB Keyboard is not installed on the device.")
print(" Solution:")
print(" 1. Download ADB Keyboard APK from:")
print(
" https://github.com/senzhk/ADBKeyBoard/blob/master/ADBKeyboard.apk"
)
print(" 2. Install it on your device: adb install ADBKeyboard.apk")
print(
" 3. Enable it in Settings > System > Languages & Input > Virtual Keyboard"
)
all_passed = False
except subprocess.TimeoutExpired:
print("❌ FAILED")
print(" Error: ADB Keyboard is not installed on the device.")
print(" Solution:")
print(" 1. Download ADB Keyboard APK from:")
print(
" https://github.com/senzhk/ADBKeyBoard/blob/master/ADBKeyboard.apk"
)
print(" 2. Install it on your device: adb install ADBKeyboard.apk")
print(
" 3. Enable it in Settings > System > Languages & Input > Virtual Keyboard"
)
print(" Error: ADB command timed out.")
all_passed = False
except subprocess.TimeoutExpired:
print("❌ FAILED")
print(" Error: ADB command timed out.")
all_passed = False
except Exception as e:
print("❌ FAILED")
print(f" Error: {e}")
all_passed = False
except Exception as e:
print("❌ FAILED")
print(f" Error: {e}")
all_passed = False
else:
# For HDC, skip keyboard check as it uses different input method
print("3. Skipping keyboard check for HarmonyOS...", end=" ")
print("✅ OK (using native input)")
print("-" * 50)
@@ -368,6 +401,14 @@ Examples:
help="Language for system prompt (cn or en, default: cn)",
)
parser.add_argument(
"--device-type",
type=str,
choices=["adb", "hdc"],
default=os.getenv("PHONE_AGENT_DEVICE_TYPE", "adb"),
help="Device type: adb for Android, hdc for HarmonyOS (default: adb)",
)
parser.add_argument(
"task",
nargs="?",
@@ -385,11 +426,13 @@ def handle_device_commands(args) -> bool:
Returns:
True if a device command was handled (should exit), False otherwise.
"""
conn = ADBConnection()
device_factory = get_device_factory()
ConnectionClass = device_factory.get_connection_class()
conn = ConnectionClass()
# Handle --list-devices
if args.list_devices:
devices = list_devices()
devices = device_factory.list_devices()
if not devices:
print("No devices connected.")
else:
@@ -452,10 +495,25 @@ def main():
"""Main entry point."""
args = parse_args()
# Set device type globally based on args
device_type = DeviceType.ADB if args.device_type == "adb" else DeviceType.HDC
set_device_type(device_type)
# Enable HDC verbose mode if using HDC
if device_type == DeviceType.HDC:
from phone_agent.hdc import set_hdc_verbose
set_hdc_verbose(True)
# Handle --list-apps (no system check needed)
if args.list_apps:
print("Supported apps:")
for app in sorted(list_supported_apps()):
if device_type == DeviceType.HDC:
print("Supported HarmonyOS apps:")
apps = list_harmonyos_apps()
else:
print("Supported Android apps:")
apps = list_supported_apps()
for app in apps:
print(f" - {app}")
return
@@ -464,7 +522,7 @@ def main():
return
# Run system requirements check before proceeding
if not check_system_requirements():
if not check_system_requirements(device_type):
sys.exit(1)
# Check model API connectivity and model availability
@@ -500,9 +558,11 @@ def main():
print(f"Base URL: {model_config.base_url}")
print(f"Max Steps: {agent_config.max_steps}")
print(f"Language: {agent_config.lang}")
print(f"Device Type: {args.device_type.upper()}")
# Show device info
devices = list_devices()
device_factory = get_device_factory()
devices = device_factory.list_devices()
if agent_config.device_id:
print(f"Device: {agent_config.device_id}")
elif devices:

View File

@@ -2,24 +2,13 @@
import ast
import re
import subprocess
import time
from dataclasses import dataclass
from typing import Any, Callable
from phone_agent.adb import (
back,
clear_text,
detect_and_set_adb_keyboard,
double_tap,
home,
launch_app,
long_press,
restore_keyboard,
swipe,
tap,
type_text,
)
from phone_agent.config.timing import TIMING_CONFIG
from phone_agent.device_factory import get_device_factory
@dataclass
@@ -132,7 +121,8 @@ class ActionHandler:
if not app_name:
return ActionResult(False, False, "No app name specified")
success = launch_app(app_name, self.device_id)
device_factory = get_device_factory()
success = device_factory.launch_app(app_name, self.device_id)
if success:
return ActionResult(True, False)
return ActionResult(False, False, f"App not found: {app_name}")
@@ -154,19 +144,22 @@ class ActionHandler:
message="User cancelled sensitive operation",
)
tap(x, y, self.device_id)
device_factory = get_device_factory()
device_factory.tap(x, y, self.device_id)
return ActionResult(True, False)
def _handle_type(self, action: dict, width: int, height: int) -> ActionResult:
"""Handle text input action."""
text = action.get("text", "")
device_factory = get_device_factory()
# Switch to ADB keyboard
original_ime = detect_and_set_adb_keyboard(self.device_id)
original_ime = device_factory.detect_and_set_adb_keyboard(self.device_id)
time.sleep(TIMING_CONFIG.action.keyboard_switch_delay)
# Clear existing text and type new text
clear_text(self.device_id)
device_factory.clear_text(self.device_id)
time.sleep(TIMING_CONFIG.action.text_clear_delay)
# Handle multiline text by splitting on newlines
@@ -174,7 +167,7 @@ class ActionHandler:
lines = text.split('\n')
for i, line in enumerate(lines):
if line: # Only type non-empty lines
type_text(line, self.device_id)
device_factory.type_text(line, self.device_id)
time.sleep(0.01)
# Send ENTER key between lines (not after the last line)
@@ -182,11 +175,11 @@ class ActionHandler:
self._send_keyevent("KEYCODE_ENTER")
time.sleep(0.01)
else:
type_text(text, self.device_id)
device_factory.type_text(text, self.device_id)
time.sleep(TIMING_CONFIG.action.text_input_delay)
# Restore original keyboard
restore_keyboard(original_ime, self.device_id)
device_factory.restore_keyboard(original_ime, self.device_id)
time.sleep(TIMING_CONFIG.action.keyboard_restore_delay)
return ActionResult(True, False)
@@ -202,17 +195,20 @@ class ActionHandler:
start_x, start_y = self._convert_relative_to_absolute(start, width, height)
end_x, end_y = self._convert_relative_to_absolute(end, width, height)
swipe(start_x, start_y, end_x, end_y, device_id=self.device_id)
device_factory = get_device_factory()
device_factory.swipe(start_x, start_y, end_x, end_y, device_id=self.device_id)
return ActionResult(True, False)
def _handle_back(self, action: dict, width: int, height: int) -> ActionResult:
"""Handle back button action."""
back(self.device_id)
device_factory = get_device_factory()
device_factory.back(self.device_id)
return ActionResult(True, False)
def _handle_home(self, action: dict, width: int, height: int) -> ActionResult:
"""Handle home button action."""
home(self.device_id)
device_factory = get_device_factory()
device_factory.home(self.device_id)
return ActionResult(True, False)
def _handle_double_tap(self, action: dict, width: int, height: int) -> ActionResult:
@@ -222,7 +218,8 @@ class ActionHandler:
return ActionResult(False, False, "No element coordinates")
x, y = self._convert_relative_to_absolute(element, width, height)
double_tap(x, y, self.device_id)
device_factory = get_device_factory()
device_factory.double_tap(x, y, self.device_id)
return ActionResult(True, False)
def _handle_long_press(self, action: dict, width: int, height: int) -> ActionResult:
@@ -232,7 +229,8 @@ class ActionHandler:
return ActionResult(False, False, "No element coordinates")
x, y = self._convert_relative_to_absolute(element, width, height)
long_press(x, y, device_id=self.device_id)
device_factory = get_device_factory()
device_factory.long_press(x, y, device_id=self.device_id)
return ActionResult(True, False)
def _handle_wait(self, action: dict, width: int, height: int) -> ActionResult:
@@ -271,13 +269,65 @@ class ActionHandler:
def _send_keyevent(self, keycode: str) -> None:
"""Send a keyevent to the device."""
import subprocess
adb_prefix = ["adb", "-s", self.device_id] if self.device_id else ["adb"]
subprocess.run(
adb_prefix + ["shell", "input", "keyevent", keycode],
capture_output=True,
text=True,
)
from phone_agent.device_factory import DeviceType, get_device_factory
from phone_agent.hdc.connection import _run_hdc_command
device_factory = get_device_factory()
# Handle HDC devices with HarmonyOS-specific keyEvent command
if device_factory.device_type == DeviceType.HDC:
hdc_prefix = ["hdc", "-t", self.device_id] if self.device_id else ["hdc"]
# Map common keycodes to HarmonyOS keyEvent codes
# KEYCODE_ENTER (66) -> 2054 (HarmonyOS Enter key code)
if keycode == "KEYCODE_ENTER" or keycode == "66":
_run_hdc_command(
hdc_prefix + ["shell", "uitest", "uiInput", "keyEvent", "2054"],
capture_output=True,
text=True,
)
else:
# For other keys, try to use the numeric code directly
# If keycode is a string like "KEYCODE_ENTER", convert it
try:
# Try to extract numeric code from string or use as-is
if keycode.startswith("KEYCODE_"):
# For now, only handle ENTER, other keys may need mapping
if "ENTER" in keycode:
_run_hdc_command(
hdc_prefix + ["shell", "uitest", "uiInput", "keyEvent", "2054"],
capture_output=True,
text=True,
)
else:
# Fallback to ADB-style command for unsupported keys
subprocess.run(
hdc_prefix + ["shell", "input", "keyevent", keycode],
capture_output=True,
text=True,
)
else:
# Assume it's a numeric code
_run_hdc_command(
hdc_prefix + ["shell", "uitest", "uiInput", "keyEvent", str(keycode)],
capture_output=True,
text=True,
)
except Exception:
# Fallback to ADB-style command
subprocess.run(
hdc_prefix + ["shell", "input", "keyevent", keycode],
capture_output=True,
text=True,
)
else:
# ADB devices use standard input keyevent command
cmd_prefix = ["adb", "-s", self.device_id] if self.device_id else ["adb"]
subprocess.run(
cmd_prefix + ["shell", "input", "keyevent", keycode],
capture_output=True,
text=True,
)
@staticmethod
def _default_confirmation(message: str) -> bool:

View File

@@ -7,8 +7,8 @@ from typing import Any, Callable
from phone_agent.actions import ActionHandler
from phone_agent.actions.handler import do, finish, parse_action
from phone_agent.adb import get_current_app, get_screenshot
from phone_agent.config import get_messages, get_system_prompt
from phone_agent.device_factory import get_device_factory
from phone_agent.model import ModelClient, ModelConfig
from phone_agent.model.client import MessageBuilder
@@ -140,8 +140,9 @@ class PhoneAgent:
self._step_count += 1
# Capture current screen state
screenshot = get_screenshot(self.agent_config.device_id)
current_app = get_current_app(self.agent_config.device_id)
device_factory = get_device_factory()
screenshot = device_factory.get_screenshot(self.agent_config.device_id)
current_app = device_factory.get_current_app(self.agent_config.device_id)
# Build messages
if is_first:

View File

@@ -0,0 +1,209 @@
"""HarmonyOS application package name mappings.
Maps user-friendly app names to HarmonyOS bundle names.
These bundle names are used with the 'hdc shell aa start -b <bundle>' command.
"""
APP_PACKAGES: dict[str, str] = {
# Social & Messaging
"微信": "com.tencent.wechat",
"QQ": "com.tencent.mqq",
"微博": "com.sina.weibo.stage",
# E-commerce
"淘宝": "com.taobao.taobao4hmos",
"京东": "com.jd.hm.mall",
"拼多多": "com.xunmeng.pinduoduo.hos",
"淘宝闪购": "com.taobao.taobao4hmos",
"京东秒送": "com.jd.hm.mall",
# Lifestyle & Social
"小红书": "com.xingin.xhs_hos",
# "豆瓣": "com.douban.frodo", # 未在 hdc 列表中找到
# "知乎": "com.zhihu.android", # 未在 hdc 列表中找到
# Maps & Navigation
"高德地图": "com.amap.hmapp",
"百度地图": "com.baidu.baiduapp",
# Food & Services
"美团": "com.sankuai.hmeituan",
"大众点评": "com.sankuai.dianping",
"饿了么": "me.ele.eleme",
# "肯德基": "com.yek.android.kfc.activitys", # 未在 hdc 列表中找到
# Travel
# "携程": "ctrip.android.view", # 未在 hdc 列表中找到
"铁路12306": "com.tmri.app.harmony12123",
"12306": "com.tmri.app.harmony12123",
# "去哪儿": "com.Qunar", # 未在 hdc 列表中找到
# "去哪儿旅行": "com.Qunar", # 未在 hdc 列表中找到
"滴滴出行": "com.sdu.didi.hmos.psnger",
# Video & Entertainment
"bilibili": "yylx.danmaku.bili",
"抖音": "com.ss.hm.ugc.aweme",
"快手": "com.kuaishou.hmapp",
"腾讯视频": "com.tencent.videohm",
"爱奇艺": "com.qiyi.video.hmy",
# "优酷视频": "com.youku.phone", # 未在 hdc 列表中找到
# "芒果TV": "com.hunantv.imgo.activity", # 未在 hdc 列表中找到
# "红果短剧": "com.phoenix.read", # 未在 hdc 列表中找到
# Music & Audio
# "网易云音乐": "com.netease.cloudmusic", # 未在 hdc 列表中找到
"QQ音乐": "com.tencent.hm.qqmusic",
# "汽水音乐": "com.luna.music", # 未在 hdc 列表中找到,但有 com.luna.hm.music
"喜马拉雅": "com.ximalaya.ting.xmharmony",
# Reading
# "番茄小说": "com.dragon.read", # 未在 hdc 列表中找到
# "番茄免费小说": "com.dragon.read", # 未在 hdc 列表中找到
# "七猫免费小说": "com.kmxs.reader", # 未在 hdc 列表中找到
# Productivity
"飞书": "com.ss.feishu",
# "QQ邮箱": "com.tencent.androidqqmail", # 未在 hdc 列表中找到
# AI & Tools
"豆包": "com.larus.nova.hm",
# Health & Fitness
# "keep": "com.gotokeep.keep", # 未在 hdc 列表中找到
# "美柚": "com.lingan.seeyou", # 未在 hdc 列表中找到
# News & Information
# "腾讯新闻": "com.tencent.news", # 未在 hdc 列表中找到
"今日头条": "com.ss.hm.article.news",
# Real Estate
# "贝壳找房": "com.lianjia.beike", # 未在 hdc 列表中找到
# "安居客": "com.anjuke.android.app", # 未在 hdc 列表中找到
# Finance
# "同花顺": "com.hexin.plat.android", # 未在 hdc 列表中找到
# Games
# "星穹铁道": "com.miHoYo.hkrpg", # 未在 hdc 列表中找到
# "崩坏:星穹铁道": "com.miHoYo.hkrpg", # 未在 hdc 列表中找到
# "恋与深空": "com.papegames.lysk.cn", # 未在 hdc 列表中找到
# HarmonyOS 第三方应用
"百度": "com.baidu.baiduapp",
"阿里巴巴": "com.alibaba.wireless_hmos",
"WPS": "cn.wps.mobileoffice.hap",
"企业微信": "com.tencent.wework.hmos",
"同程": "com.tongcheng.hmos",
"同程旅行": "com.tongcheng.hmos",
"唯品会": "com.vip.hosapp",
"支付宝": "com.alipay.mobile.client",
"UC浏览器": "com.uc.mobile",
"搜狗输入法": "com.sogou.input",
"扫描全能王": "com.intsig.camscanner.hap",
"美图秀秀": "com.meitu.meitupic",
"58同城": "com.wuba.life",
"得物": "com.dewu.hos",
"海底捞": "com.haidilao.haros",
"中国移动": "com.droi.tong",
"中国联通": "com.sinovatech.unicom.ha",
"国家税务总局": "cn.gov.chinatax.gt4.hm",
# HarmonyOS 系统应用 - 工具类
"浏览器": "com.huawei.hmos.browser",
"计算器": "com.huawei.hmos.calculator",
"日历": "com.huawei.hmos.calendar",
"相机": "com.huawei.hmos.camera",
"时钟": "com.huawei.hmos.clock",
"云盘": "com.huawei.hmos.clouddrive",
"云空间": "com.huawei.hmos.clouddrive",
"邮件": "com.huawei.hmos.email",
"文件管理器": "com.huawei.hmos.filemanager",
"文件": "com.huawei.hmos.files",
"查找设备": "com.huawei.hmos.finddevice",
"查找手机": "com.huawei.hmos.finddevice",
"录音机": "com.huawei.hmos.soundrecorder",
"录音": "com.huawei.hmos.soundrecorder",
"录屏": "com.huawei.hmos.screenrecorder",
"截屏": "com.huawei.hmos.screenshot",
"笔记": "com.huawei.hmos.notepad",
"备忘录": "com.huawei.hmos.notepad",
# HarmonyOS 系统应用 - 媒体类
"相册": "com.huawei.hmos.photos",
"图库": "com.huawei.hmos.photos",
# "视频": "com.huawei.hmos.mediaplayer", # 未在 hdc 列表中找到,但有 com.huawei.hmsapp.himovie
# HarmonyOS 系统应用 - 通讯类
"联系人": "com.ohos.contacts",
"通讯录": "com.ohos.contacts",
"短信": "com.ohos.mms",
"信息": "com.ohos.mms",
"电话": "com.ohos.callui",
"拨号": "com.ohos.callui",
# HarmonyOS 系统应用 - 设置类
"设置": "com.huawei.hmos.settings",
"系统设置": "com.huawei.hmos.settings",
"AndroidSystemSettings": "com.huawei.hmos.settings",
"Android System Settings": "com.huawei.hmos.settings",
"Android System Settings": "com.huawei.hmos.settings",
"Android-System-Settings": "com.huawei.hmos.settings",
"Settings": "com.huawei.hmos.settings",
# HarmonyOS 系统应用 - 生活服务
"健康": "com.huawei.hmos.health",
"运动健康": "com.huawei.hmos.health",
"地图": "com.huawei.hmos.maps.app",
"华为地图": "com.huawei.hmos.maps.app",
"钱包": "com.huawei.hmos.wallet",
"华为钱包": "com.huawei.hmos.wallet",
"智慧生活": "com.huawei.hmos.ailife",
"智能助手": "com.huawei.hmos.vassistant",
"小艺": "com.huawei.hmos.vassistant",
# HarmonyOS 服务
"应用市场": "com.huawei.hmsapp.appgallery",
"华为应用市场": "com.huawei.hmsapp.appgallery",
"音乐": "com.huawei.hmsapp.music",
"华为音乐": "com.huawei.hmsapp.music",
"主题": "com.huawei.hmsapp.thememanager",
"主题管理": "com.huawei.hmsapp.thememanager",
"天气": "com.huawei.hmsapp.totemweather",
"华为天气": "com.huawei.hmsapp.totemweather",
"视频": "com.huawei.hmsapp.himovie",
"华为视频": "com.huawei.hmsapp.himovie",
"阅读": "com.huawei.hmsapp.books",
"华为阅读": "com.huawei.hmsapp.books",
"游戏中心": "com.huawei.hmsapp.gamecenter",
"华为游戏中心": "com.huawei.hmsapp.gamecenter",
"搜索": "com.huawei.hmsapp.hisearch",
"华为搜索": "com.huawei.hmsapp.hisearch",
"指南针": "com.huawei.hmsapp.compass",
"会员中心": "com.huawei.hmos.myhuawei",
"我的华为": "com.huawei.hmos.myhuawei",
"华为会员": "com.huawei.hmos.myhuawei",
}
def get_package_name(app_name: str) -> str | None:
"""
Get the package name for an app.
Args:
app_name: The display name of the app.
Returns:
The HarmonyOS bundle name, or None if not found.
"""
return APP_PACKAGES.get(app_name)
def get_app_name(package_name: str) -> str | None:
"""
Get the app name from a package name.
Args:
package_name: The HarmonyOS bundle name.
Returns:
The display name of the app, or None if not found.
"""
for name, package in APP_PACKAGES.items():
if package == package_name:
return name
return None
def list_supported_apps() -> list[str]:
"""
Get a list of all supported app names.
Returns:
List of app names.
"""
return list(APP_PACKAGES.keys())

View File

@@ -0,0 +1,138 @@
"""Device factory for selecting ADB or HDC based on device type."""
from enum import Enum
from typing import Any
class DeviceType(Enum):
"""Type of device connection tool."""
ADB = "adb"
HDC = "hdc"
class DeviceFactory:
"""
Factory class for getting device-specific implementations.
This allows the system to work with both Android (ADB) and HarmonyOS (HDC) devices.
"""
def __init__(self, device_type: DeviceType = DeviceType.ADB):
"""
Initialize the device factory.
Args:
device_type: The type of device to use (ADB or HDC).
"""
self.device_type = device_type
self._module = None
@property
def module(self):
"""Get the appropriate device module (adb or hdc)."""
if self._module is None:
if self.device_type == DeviceType.ADB:
from phone_agent import adb
self._module = adb
elif self.device_type == DeviceType.HDC:
from phone_agent import hdc
self._module = hdc
else:
raise ValueError(f"Unknown device type: {self.device_type}")
return self._module
def get_screenshot(self, device_id: str | None = None, timeout: int = 10):
"""Get screenshot from device."""
return self.module.get_screenshot(device_id, timeout)
def get_current_app(self, device_id: str | None = None) -> str:
"""Get current app name."""
return self.module.get_current_app(device_id)
def tap(self, x: int, y: int, device_id: str | None = None, delay: float | None = None):
"""Tap at coordinates."""
return self.module.tap(x, y, device_id, delay)
def double_tap(self, x: int, y: int, device_id: str | None = None, delay: float | None = None):
"""Double tap at coordinates."""
return self.module.double_tap(x, y, device_id, delay)
def long_press(self, x: int, y: int, duration_ms: int = 3000, device_id: str | None = None, delay: float | None = None):
"""Long press at coordinates."""
return self.module.long_press(x, y, duration_ms, device_id, delay)
def swipe(self, start_x: int, start_y: int, end_x: int, end_y: int, duration_ms: int | None = None, device_id: str | None = None, delay: float | None = None):
"""Swipe from start to end."""
return self.module.swipe(start_x, start_y, end_x, end_y, duration_ms, device_id, delay)
def back(self, device_id: str | None = None, delay: float | None = None):
"""Press back button."""
return self.module.back(device_id, delay)
def home(self, device_id: str | None = None, delay: float | None = None):
"""Press home button."""
return self.module.home(device_id, delay)
def launch_app(self, app_name: str, device_id: str | None = None, delay: float | None = None) -> bool:
"""Launch an app."""
return self.module.launch_app(app_name, device_id, delay)
def type_text(self, text: str, device_id: str | None = None):
"""Type text."""
return self.module.type_text(text, device_id)
def clear_text(self, device_id: str | None = None):
"""Clear text."""
return self.module.clear_text(device_id)
def detect_and_set_adb_keyboard(self, device_id: str | None = None) -> str:
"""Detect and set keyboard."""
return self.module.detect_and_set_adb_keyboard(device_id)
def restore_keyboard(self, ime: str, device_id: str | None = None):
"""Restore keyboard."""
return self.module.restore_keyboard(ime, device_id)
def list_devices(self):
"""List connected devices."""
return self.module.list_devices()
def get_connection_class(self):
"""Get the connection class (ADBConnection or HDCConnection)."""
if self.device_type == DeviceType.ADB:
from phone_agent.adb import ADBConnection
return ADBConnection
elif self.device_type == DeviceType.HDC:
from phone_agent.hdc import HDCConnection
return HDCConnection
else:
raise ValueError(f"Unknown device type: {self.device_type}")
# Global device factory instance
_device_factory: DeviceFactory | None = None
def set_device_type(device_type: DeviceType):
"""
Set the global device type.
Args:
device_type: The device type to use (ADB or HDC).
"""
global _device_factory
_device_factory = DeviceFactory(device_type)
def get_device_factory() -> DeviceFactory:
"""
Get the global device factory instance.
Returns:
The device factory instance.
"""
global _device_factory
if _device_factory is None:
_device_factory = DeviceFactory(DeviceType.ADB) # Default to ADB
return _device_factory

View File

@@ -0,0 +1,53 @@
"""HDC utilities for HarmonyOS device interaction."""
from phone_agent.hdc.connection import (
HDCConnection,
ConnectionType,
DeviceInfo,
list_devices,
quick_connect,
set_hdc_verbose,
)
from phone_agent.hdc.device import (
back,
double_tap,
get_current_app,
home,
launch_app,
long_press,
swipe,
tap,
)
from phone_agent.hdc.input import (
clear_text,
detect_and_set_adb_keyboard,
restore_keyboard,
type_text,
)
from phone_agent.hdc.screenshot import get_screenshot
__all__ = [
# Screenshot
"get_screenshot",
# Input
"type_text",
"clear_text",
"detect_and_set_adb_keyboard",
"restore_keyboard",
# Device control
"get_current_app",
"tap",
"swipe",
"back",
"home",
"double_tap",
"long_press",
"launch_app",
# Connection management
"HDCConnection",
"DeviceInfo",
"ConnectionType",
"quick_connect",
"list_devices",
"set_hdc_verbose",
]

View File

@@ -0,0 +1,381 @@
"""HDC connection management for HarmonyOS devices."""
import os
import subprocess
import time
from dataclasses import dataclass
from enum import Enum
from typing import Optional
from phone_agent.config.timing import TIMING_CONFIG
# Global flag to control HDC command output
_HDC_VERBOSE = os.getenv("HDC_VERBOSE", "false").lower() in ("true", "1", "yes")
def _run_hdc_command(cmd: list, **kwargs) -> subprocess.CompletedProcess:
"""
Run HDC command with optional verbose output.
Args:
cmd: Command list to execute.
**kwargs: Additional arguments for subprocess.run.
Returns:
CompletedProcess result.
"""
if _HDC_VERBOSE:
print(f"[HDC] Running command: {' '.join(cmd)}")
result = subprocess.run(cmd, **kwargs)
if _HDC_VERBOSE and result.returncode != 0:
print(f"[HDC] Command failed with return code {result.returncode}")
if hasattr(result, 'stderr') and result.stderr:
print(f"[HDC] Error: {result.stderr}")
return result
def set_hdc_verbose(verbose: bool):
"""Set HDC verbose mode globally."""
global _HDC_VERBOSE
_HDC_VERBOSE = verbose
class ConnectionType(Enum):
"""Type of HDC connection."""
USB = "usb"
WIFI = "wifi"
REMOTE = "remote"
@dataclass
class DeviceInfo:
"""Information about a connected device."""
device_id: str
status: str
connection_type: ConnectionType
model: str | None = None
harmony_version: str | None = None
class HDCConnection:
"""
Manages HDC connections to HarmonyOS devices.
Supports USB, WiFi, and remote TCP/IP connections.
Example:
>>> conn = HDCConnection()
>>> # Connect to remote device
>>> conn.connect("192.168.1.100:5555")
>>> # List devices
>>> devices = conn.list_devices()
>>> # Disconnect
>>> conn.disconnect("192.168.1.100:5555")
"""
def __init__(self, hdc_path: str = "hdc"):
"""
Initialize HDC connection manager.
Args:
hdc_path: Path to HDC executable.
"""
self.hdc_path = hdc_path
def connect(self, address: str, timeout: int = 10) -> tuple[bool, str]:
"""
Connect to a remote device via TCP/IP.
Args:
address: Device address in format "host:port" (e.g., "192.168.1.100:5555").
timeout: Connection timeout in seconds.
Returns:
Tuple of (success, message).
Note:
The remote device must have TCP/IP debugging enabled.
"""
# Validate address format
if ":" not in address:
address = f"{address}:5555" # Default HDC port
try:
result = _run_hdc_command(
[self.hdc_path, "tconn", address],
capture_output=True,
text=True,
timeout=timeout,
)
output = result.stdout + result.stderr
if "Connect OK" in output or "connected" in output.lower():
return True, f"Connected to {address}"
elif "already connected" in output.lower():
return True, f"Already connected to {address}"
else:
return False, output.strip()
except subprocess.TimeoutExpired:
return False, f"Connection timeout after {timeout}s"
except Exception as e:
return False, f"Connection error: {e}"
def disconnect(self, address: str | None = None) -> tuple[bool, str]:
"""
Disconnect from a remote device.
Args:
address: Device address to disconnect. If None, disconnects all.
Returns:
Tuple of (success, message).
"""
try:
if address:
cmd = [self.hdc_path, "tdisconn", address]
else:
# HDC doesn't have a "disconnect all" command, so we need to list and disconnect each
devices = self.list_devices()
for device in devices:
if ":" in device.device_id: # Remote device
_run_hdc_command(
[self.hdc_path, "tdisconn", device.device_id],
capture_output=True,
text=True,
timeout=5
)
return True, "Disconnected all remote devices"
result = _run_hdc_command(cmd, capture_output=True, text=True, encoding="utf-8", timeout=5)
output = result.stdout + result.stderr
return True, output.strip() or "Disconnected"
except Exception as e:
return False, f"Disconnect error: {e}"
def list_devices(self) -> list[DeviceInfo]:
"""
List all connected devices.
Returns:
List of DeviceInfo objects.
"""
try:
result = _run_hdc_command(
[self.hdc_path, "list", "targets"],
capture_output=True,
text=True,
timeout=5,
)
devices = []
for line in result.stdout.strip().split("\n"):
if not line.strip():
continue
# HDC output format: device_id (status)
# Example: "192.168.1.100:5555" or "FMR0223C13000649"
device_id = line.strip()
# Determine connection type
if ":" in device_id:
conn_type = ConnectionType.REMOTE
else:
conn_type = ConnectionType.USB
# HDC doesn't provide detailed status in list command
# We assume "Connected" status for devices that appear
devices.append(
DeviceInfo(
device_id=device_id,
status="device",
connection_type=conn_type,
model=None,
)
)
return devices
except Exception as e:
print(f"Error listing devices: {e}")
return []
def get_device_info(self, device_id: str | None = None) -> DeviceInfo | None:
"""
Get detailed information about a device.
Args:
device_id: Device ID. If None, uses first available device.
Returns:
DeviceInfo or None if not found.
"""
devices = self.list_devices()
if not devices:
return None
if device_id is None:
return devices[0]
for device in devices:
if device.device_id == device_id:
return device
return None
def is_connected(self, device_id: str | None = None) -> bool:
"""
Check if a device is connected.
Args:
device_id: Device ID to check. If None, checks if any device is connected.
Returns:
True if connected, False otherwise.
"""
devices = self.list_devices()
if not devices:
return False
if device_id is None:
return len(devices) > 0
return any(d.device_id == device_id for d in devices)
def enable_tcpip(
self, port: int = 5555, device_id: str | None = None
) -> tuple[bool, str]:
"""
Enable TCP/IP debugging on a USB-connected device.
This allows subsequent wireless connections to the device.
Args:
port: TCP port for HDC (default: 5555).
device_id: Device ID. If None, uses first available device.
Returns:
Tuple of (success, message).
Note:
The device must be connected via USB first.
After this, you can disconnect USB and connect via WiFi.
"""
try:
cmd = [self.hdc_path]
if device_id:
cmd.extend(["-t", device_id])
cmd.extend(["tmode", "port", str(port)])
result = _run_hdc_command(cmd, capture_output=True, text=True, encoding="utf-8", timeout=10)
output = result.stdout + result.stderr
if result.returncode == 0 or "success" in output.lower():
time.sleep(TIMING_CONFIG.connection.adb_restart_delay)
return True, f"TCP/IP mode enabled on port {port}"
else:
return False, output.strip()
except Exception as e:
return False, f"Error enabling TCP/IP: {e}"
def get_device_ip(self, device_id: str | None = None) -> str | None:
"""
Get the IP address of a connected device.
Args:
device_id: Device ID. If None, uses first available device.
Returns:
IP address string or None if not found.
"""
try:
cmd = [self.hdc_path]
if device_id:
cmd.extend(["-t", device_id])
cmd.extend(["shell", "ifconfig"])
result = _run_hdc_command(cmd, capture_output=True, text=True, encoding="utf-8", timeout=5)
# Parse IP from ifconfig output
for line in result.stdout.split("\n"):
if "inet addr:" in line or "inet " in line:
parts = line.strip().split()
for i, part in enumerate(parts):
if "addr:" in part:
ip = part.split(":")[1]
# Filter out localhost
if not ip.startswith("127."):
return ip
elif part == "inet" and i + 1 < len(parts):
ip = parts[i + 1].split("/")[0]
if not ip.startswith("127."):
return ip
return None
except Exception as e:
print(f"Error getting device IP: {e}")
return None
def restart_server(self) -> tuple[bool, str]:
"""
Restart the HDC server.
Returns:
Tuple of (success, message).
"""
try:
# Kill server
_run_hdc_command(
[self.hdc_path, "kill"], capture_output=True, timeout=5
)
time.sleep(TIMING_CONFIG.connection.server_restart_delay)
# Start server (HDC auto-starts when running commands)
_run_hdc_command(
[self.hdc_path, "start", "-r"], capture_output=True, timeout=5
)
return True, "HDC server restarted"
except Exception as e:
return False, f"Error restarting server: {e}"
def quick_connect(address: str) -> tuple[bool, str]:
"""
Quick helper to connect to a remote device.
Args:
address: Device address (e.g., "192.168.1.100" or "192.168.1.100:5555").
Returns:
Tuple of (success, message).
"""
conn = HDCConnection()
return conn.connect(address)
def list_devices() -> list[DeviceInfo]:
"""
Quick helper to list connected devices.
Returns:
List of DeviceInfo objects.
"""
conn = HDCConnection()
return conn.list_devices()

269
phone_agent/hdc/device.py Normal file
View File

@@ -0,0 +1,269 @@
"""Device control utilities for HarmonyOS automation."""
import os
import subprocess
import time
from typing import List, Optional, Tuple
from phone_agent.config.apps_harmonyos import APP_PACKAGES
from phone_agent.config.timing import TIMING_CONFIG
from phone_agent.hdc.connection import _run_hdc_command
def get_current_app(device_id: str | None = None) -> str:
"""
Get the currently focused app name.
Args:
device_id: Optional HDC device ID for multi-device setups.
Returns:
The app name if recognized, otherwise "System Home".
"""
hdc_prefix = _get_hdc_prefix(device_id)
result = _run_hdc_command(
hdc_prefix + ["shell", "hidumper", "-s", "WindowManagerService", "-a", "-a"],
capture_output=True,
text=True,
encoding="utf-8"
)
output = result.stdout
if not output:
raise ValueError("No output from hidumper")
# Parse window focus info
for line in output.split("\n"):
if "focused" in line.lower() or "current" in line.lower():
for app_name, package in APP_PACKAGES.items():
if package in line:
return app_name
return "System Home"
def tap(
x: int, y: int, device_id: str | None = None, delay: float | None = None
) -> None:
"""
Tap at the specified coordinates.
Args:
x: X coordinate.
y: Y coordinate.
device_id: Optional HDC device ID.
delay: Delay in seconds after tap. If None, uses configured default.
"""
if delay is None:
delay = TIMING_CONFIG.device.default_tap_delay
hdc_prefix = _get_hdc_prefix(device_id)
# HarmonyOS uses uitest uiInput click
_run_hdc_command(
hdc_prefix + ["shell", "uitest", "uiInput", "click", str(x), str(y)],
capture_output=True
)
time.sleep(delay)
def double_tap(
x: int, y: int, device_id: str | None = None, delay: float | None = None
) -> None:
"""
Double tap at the specified coordinates.
Args:
x: X coordinate.
y: Y coordinate.
device_id: Optional HDC device ID.
delay: Delay in seconds after double tap. If None, uses configured default.
"""
if delay is None:
delay = TIMING_CONFIG.device.default_double_tap_delay
hdc_prefix = _get_hdc_prefix(device_id)
# HarmonyOS uses uitest uiInput doubleClick
_run_hdc_command(
hdc_prefix + ["shell", "uitest", "uiInput", "doubleClick", str(x), str(y)],
capture_output=True
)
time.sleep(delay)
def long_press(
x: int,
y: int,
duration_ms: int = 3000,
device_id: str | None = None,
delay: float | None = None,
) -> None:
"""
Long press at the specified coordinates.
Args:
x: X coordinate.
y: Y coordinate.
duration_ms: Duration of press in milliseconds (note: HarmonyOS longClick may not support duration).
device_id: Optional HDC device ID.
delay: Delay in seconds after long press. If None, uses configured default.
"""
if delay is None:
delay = TIMING_CONFIG.device.default_long_press_delay
hdc_prefix = _get_hdc_prefix(device_id)
# HarmonyOS uses uitest uiInput longClick
# Note: longClick may have a fixed duration, duration_ms parameter might not be supported
_run_hdc_command(
hdc_prefix + ["shell", "uitest", "uiInput", "longClick", str(x), str(y)],
capture_output=True,
)
time.sleep(delay)
def swipe(
start_x: int,
start_y: int,
end_x: int,
end_y: int,
duration_ms: int | None = None,
device_id: str | None = None,
delay: float | None = None,
) -> None:
"""
Swipe from start to end coordinates.
Args:
start_x: Starting X coordinate.
start_y: Starting Y coordinate.
end_x: Ending X coordinate.
end_y: Ending Y coordinate.
duration_ms: Duration of swipe in milliseconds (auto-calculated if None).
device_id: Optional HDC device ID.
delay: Delay in seconds after swipe. If None, uses configured default.
"""
if delay is None:
delay = TIMING_CONFIG.device.default_swipe_delay
hdc_prefix = _get_hdc_prefix(device_id)
if duration_ms is None:
# Calculate duration based on distance
dist_sq = (start_x - end_x) ** 2 + (start_y - end_y) ** 2
duration_ms = int(dist_sq / 1000)
duration_ms = max(500, min(duration_ms, 1000)) # Clamp between 500-1000ms
# HarmonyOS uses uitest uiInput swipe
# Format: swipe startX startY endX endY duration
_run_hdc_command(
hdc_prefix
+ [
"shell",
"uitest",
"uiInput",
"swipe",
str(start_x),
str(start_y),
str(end_x),
str(end_y),
str(duration_ms),
],
capture_output=True,
)
time.sleep(delay)
def back(device_id: str | None = None, delay: float | None = None) -> None:
"""
Press the back button.
Args:
device_id: Optional HDC device ID.
delay: Delay in seconds after pressing back. If None, uses configured default.
"""
if delay is None:
delay = TIMING_CONFIG.device.default_back_delay
hdc_prefix = _get_hdc_prefix(device_id)
# HarmonyOS uses uitest uiInput keyEvent Back
_run_hdc_command(
hdc_prefix + ["shell", "uitest", "uiInput", "keyEvent", "Back"],
capture_output=True
)
time.sleep(delay)
def home(device_id: str | None = None, delay: float | None = None) -> None:
"""
Press the home button.
Args:
device_id: Optional HDC device ID.
delay: Delay in seconds after pressing home. If None, uses configured default.
"""
if delay is None:
delay = TIMING_CONFIG.device.default_home_delay
hdc_prefix = _get_hdc_prefix(device_id)
# HarmonyOS uses uitest uiInput keyEvent Home
_run_hdc_command(
hdc_prefix + ["shell", "uitest", "uiInput", "keyEvent", "Home"],
capture_output=True
)
time.sleep(delay)
def launch_app(
app_name: str, device_id: str | None = None, delay: float | None = None
) -> bool:
"""
Launch an app by name.
Args:
app_name: The app name (must be in APP_PACKAGES).
device_id: Optional HDC device ID.
delay: Delay in seconds after launching. If None, uses configured default.
Returns:
True if app was launched, False if app not found.
"""
if delay is None:
delay = TIMING_CONFIG.device.default_launch_delay
if app_name not in APP_PACKAGES:
print(f"[HDC] App '{app_name}' not found in HarmonyOS app list")
print(f"[HDC] Available apps: {', '.join(sorted(APP_PACKAGES.keys())[:10])}...")
return False
hdc_prefix = _get_hdc_prefix(device_id)
bundle = APP_PACKAGES[app_name]
# HarmonyOS uses 'aa start' command to launch apps
# Format: aa start -b {bundle} -a {ability}
# Most HarmonyOS apps use "EntryAbility" as the main ability name
_run_hdc_command(
hdc_prefix
+ [
"shell",
"aa",
"start",
"-b",
bundle,
"-a",
"EntryAbility",
],
capture_output=True,
)
time.sleep(delay)
return True
def _get_hdc_prefix(device_id: str | None) -> list:
"""Get HDC command prefix with optional device specifier."""
if device_id:
return ["hdc", "-t", device_id]
return ["hdc"]

136
phone_agent/hdc/input.py Normal file
View File

@@ -0,0 +1,136 @@
"""Input utilities for HarmonyOS device text input."""
import base64
import subprocess
from typing import Optional
from phone_agent.hdc.connection import _run_hdc_command
def type_text(text: str, device_id: str | None = None, x: int = None, y: int = None) -> None:
"""
Type text into the currently focused input field.
Args:
text: The text to type.
device_id: Optional HDC device ID for multi-device setups.
x: Optional X coordinate for input field (deprecated, kept for compatibility).
y: Optional Y coordinate for input field (deprecated, kept for compatibility).
Note:
HarmonyOS uses: hdc shell uitest uiInput text "文本内容"
This command works without coordinates when input field is focused.
Recommendation: Click on the input field first to focus it, then use this function.
"""
hdc_prefix = _get_hdc_prefix(device_id)
# Escape special characters for shell (keep quotes for proper text handling)
# The text will be wrapped in quotes in the command
escaped_text = text.replace('"', '\\"').replace("$", "\\$")
try:
# HarmonyOS uitest uiInput text command
# Format: hdc shell uitest uiInput text "文本内容"
_run_hdc_command(
hdc_prefix + ["shell", "uitest", "uiInput", "text", escaped_text],
capture_output=True,
text=True,
)
except Exception as e:
print(f"[HDC] Text input failed: {e}")
# Fallback: try with coordinates if provided (for older HarmonyOS versions)
if x is not None and y is not None:
try:
_run_hdc_command(
hdc_prefix + ["shell", "uitest", "uiInput", "inputText", str(x), str(y), escaped_text],
capture_output=True,
text=True,
)
except Exception:
pass
def clear_text(device_id: str | None = None) -> None:
"""
Clear text in the currently focused input field.
Args:
device_id: Optional HDC device ID for multi-device setups.
Note:
This method uses repeated delete key events to clear text.
For HarmonyOS, you might also use select all + delete for better efficiency.
"""
hdc_prefix = _get_hdc_prefix(device_id)
# Ctrl+A to select all (key code 2072 for Ctrl, 2017 for A)
# Then delete
_run_hdc_command(
hdc_prefix + ["shell", "uitest", "uiInput", "keyEvent", "2072", "2017"],
capture_output=True,
text=True,
)
_run_hdc_command(
hdc_prefix + ["shell", "uitest", "uiInput", "keyEvent", "2055"], # Delete key
capture_output=True,
text=True,
)
def detect_and_set_adb_keyboard(device_id: str | None = None) -> str:
"""
Detect current keyboard and switch to ADB Keyboard if available.
Args:
device_id: Optional HDC device ID for multi-device setups.
Returns:
The original keyboard IME identifier for later restoration.
Note:
This is a placeholder. HarmonyOS may not support ADB Keyboard.
If there's a similar tool for HarmonyOS, integrate it here.
"""
hdc_prefix = _get_hdc_prefix(device_id)
# Get current IME (if HarmonyOS supports this)
try:
result = _run_hdc_command(
hdc_prefix + ["shell", "settings", "get", "secure", "default_input_method"],
capture_output=True,
text=True,
)
current_ime = (result.stdout + result.stderr).strip()
# If ADB Keyboard equivalent exists for HarmonyOS, switch to it
# For now, we'll just return the current IME
return current_ime
except Exception:
return ""
def restore_keyboard(ime: str, device_id: str | None = None) -> None:
"""
Restore the original keyboard IME.
Args:
ime: The IME identifier to restore.
device_id: Optional HDC device ID for multi-device setups.
"""
if not ime:
return
hdc_prefix = _get_hdc_prefix(device_id)
try:
_run_hdc_command(
hdc_prefix + ["shell", "ime", "set", ime], capture_output=True, text=True
)
except Exception:
pass
def _get_hdc_prefix(device_id: str | None) -> list:
"""Get HDC command prefix with optional device specifier."""
if device_id:
return ["hdc", "-t", device_id]
return ["hdc"]

View File

@@ -0,0 +1,125 @@
"""Screenshot utilities for capturing HarmonyOS device screen."""
import base64
import os
import subprocess
import tempfile
import uuid
from dataclasses import dataclass
from io import BytesIO
from typing import Tuple
from PIL import Image
from phone_agent.hdc.connection import _run_hdc_command
@dataclass
class Screenshot:
"""Represents a captured screenshot."""
base64_data: str
width: int
height: int
is_sensitive: bool = False
def get_screenshot(device_id: str | None = None, timeout: int = 10) -> Screenshot:
"""
Capture a screenshot from the connected HarmonyOS device.
Args:
device_id: Optional HDC device ID for multi-device setups.
timeout: Timeout in seconds for screenshot operations.
Returns:
Screenshot object containing base64 data and dimensions.
Note:
If the screenshot fails (e.g., on sensitive screens like payment pages),
a black fallback image is returned with is_sensitive=True.
"""
temp_path = os.path.join(tempfile.gettempdir(), f"screenshot_{uuid.uuid4()}.png")
hdc_prefix = _get_hdc_prefix(device_id)
try:
# Execute screenshot command
# HarmonyOS HDC only supports JPEG format
remote_path = "/data/local/tmp/tmp_screenshot.jpeg"
# Try method 1: hdc shell screenshot (newer HarmonyOS versions)
result = _run_hdc_command(
hdc_prefix + ["shell", "screenshot", remote_path],
capture_output=True,
text=True,
timeout=timeout,
)
# Check for screenshot failure (sensitive screen)
output = result.stdout + result.stderr
if "fail" in output.lower() or "error" in output.lower() or "not found" in output.lower():
# Try method 2: snapshot_display (older versions or different devices)
result = _run_hdc_command(
hdc_prefix + ["shell", "snapshot_display", "-f", remote_path],
capture_output=True,
text=True,
timeout=timeout,
)
output = result.stdout + result.stderr
if "fail" in output.lower() or "error" in output.lower():
return _create_fallback_screenshot(is_sensitive=True)
# Pull screenshot to local temp path
# Note: remote file is JPEG, but PIL can open it regardless of local extension
_run_hdc_command(
hdc_prefix + ["file", "recv", remote_path, temp_path],
capture_output=True,
text=True,
timeout=5,
)
if not os.path.exists(temp_path):
return _create_fallback_screenshot(is_sensitive=False)
# Read JPEG image and convert to PNG for model inference
# PIL automatically detects the image format from file content
img = Image.open(temp_path)
width, height = img.size
buffered = BytesIO()
img.save(buffered, format="PNG")
base64_data = base64.b64encode(buffered.getvalue()).decode("utf-8")
# Cleanup
os.remove(temp_path)
return Screenshot(
base64_data=base64_data, width=width, height=height, is_sensitive=False
)
except Exception as e:
print(f"Screenshot error: {e}")
return _create_fallback_screenshot(is_sensitive=False)
def _get_hdc_prefix(device_id: str | None) -> list:
"""Get HDC command prefix with optional device specifier."""
if device_id:
return ["hdc", "-t", device_id]
return ["hdc"]
def _create_fallback_screenshot(is_sensitive: bool) -> Screenshot:
"""Create a black fallback image when screenshot fails."""
default_width, default_height = 1080, 2400
black_img = Image.new("RGB", (default_width, default_height), color="black")
buffered = BytesIO()
black_img.save(buffered, format="PNG")
base64_data = base64.b64encode(buffered.getvalue()).decode("utf-8")
return Screenshot(
base64_data=base64_data,
width=default_width,
height=default_height,
is_sensitive=is_sensitive,
)