You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
224 lines
7.9 KiB
224 lines
7.9 KiB
|
4 months ago
|
import asyncio
|
||
|
|
import base64
|
||
|
|
from typing import Optional, List
|
||
|
|
from adbutils import adb
|
||
|
|
from app.core.device_manager import device_manager
|
||
|
|
from app.schemas.adb import (
|
||
|
|
ClickRequest, InputRequest, ScreenshotResponse,
|
||
|
|
InstallRequest, InstallResponse, LogcatRequest, LogcatResponse
|
||
|
|
)
|
||
|
|
from app.utils.log import get_logger
|
||
|
|
|
||
|
|
logger = get_logger(__name__)
|
||
|
|
|
||
|
|
class ADBService:
|
||
|
|
"""ADB服务类 - 实现ADB协议相关操作"""
|
||
|
|
|
||
|
|
def __init__(self):
|
||
|
|
self.adb_client = None
|
||
|
|
self._initialize_adb()
|
||
|
|
|
||
|
|
def _initialize_adb(self):
|
||
|
|
"""初始化ADB客户端"""
|
||
|
|
try:
|
||
|
|
self.adb_client = adb.connect()
|
||
|
|
logger.info("ADB客户端初始化成功")
|
||
|
|
except Exception as e:
|
||
|
|
logger.error(f"ADB客户端初始化失败: {e}")
|
||
|
|
self.adb_client = None
|
||
|
|
|
||
|
|
async def _get_device(self, device_id: str):
|
||
|
|
"""获取ADB设备对象"""
|
||
|
|
if not self.adb_client:
|
||
|
|
self._initialize_adb()
|
||
|
|
|
||
|
|
try:
|
||
|
|
device = self.adb_client.device(device_id)
|
||
|
|
return device
|
||
|
|
except Exception as e:
|
||
|
|
logger.error(f"获取ADB设备失败: {device_id}, 错误: {e}")
|
||
|
|
raise ValueError(f"设备 {device_id} 不存在或未连接")
|
||
|
|
|
||
|
|
async def click(self, device_id: str, x: int, y: int, duration: int = 100) -> dict:
|
||
|
|
"""执行点击操作"""
|
||
|
|
try:
|
||
|
|
device = await self._get_device(device_id)
|
||
|
|
|
||
|
|
# 使用adb shell input命令执行点击
|
||
|
|
if duration > 0:
|
||
|
|
# 长按
|
||
|
|
cmd = f"input touchscreen swipe {x} {y} {x} {y} {duration}"
|
||
|
|
else:
|
||
|
|
# 点击
|
||
|
|
cmd = f"input tap {x} {y}"
|
||
|
|
|
||
|
|
result = device.shell(cmd)
|
||
|
|
|
||
|
|
logger.info(f"ADB点击操作成功: {device_id} -> ({x}, {y})")
|
||
|
|
return {
|
||
|
|
"success": True,
|
||
|
|
"message": "点击操作执行成功",
|
||
|
|
"coordinates": {"x": x, "y": y},
|
||
|
|
"duration": duration
|
||
|
|
}
|
||
|
|
except Exception as e:
|
||
|
|
logger.error(f"ADB点击操作失败: {device_id}, 错误: {e}")
|
||
|
|
return {
|
||
|
|
"success": False,
|
||
|
|
"message": f"点击操作失败: {str(e)}"
|
||
|
|
}
|
||
|
|
|
||
|
|
async def input_text(self, device_id: str, text: str, clear_first: bool = False) -> dict:
|
||
|
|
"""输入文本"""
|
||
|
|
try:
|
||
|
|
device = await self._get_device(device_id)
|
||
|
|
|
||
|
|
if clear_first:
|
||
|
|
# 先清空输入框
|
||
|
|
device.shell("input keyevent KEYCODE_MOVE_END")
|
||
|
|
device.shell("input keyevent KEYCODE_DEL")
|
||
|
|
|
||
|
|
# 输入文本
|
||
|
|
result = device.shell(f"input text '{text}'")
|
||
|
|
|
||
|
|
logger.info(f"ADB文本输入成功: {device_id} -> {text}")
|
||
|
|
return {
|
||
|
|
"success": True,
|
||
|
|
"message": "文本输入成功",
|
||
|
|
"text": text
|
||
|
|
}
|
||
|
|
except Exception as e:
|
||
|
|
logger.error(f"ADB文本输入失败: {device_id}, 错误: {e}")
|
||
|
|
return {
|
||
|
|
"success": False,
|
||
|
|
"message": f"文本输入失败: {str(e)}"
|
||
|
|
}
|
||
|
|
|
||
|
|
async def screenshot(self, device_id: str) -> ScreenshotResponse:
|
||
|
|
"""获取屏幕截图"""
|
||
|
|
try:
|
||
|
|
device = await self._get_device(device_id)
|
||
|
|
|
||
|
|
# 获取截图
|
||
|
|
screenshot_data = device.screenshot()
|
||
|
|
|
||
|
|
# 转换为Base64
|
||
|
|
import io
|
||
|
|
from PIL import Image
|
||
|
|
|
||
|
|
img_buffer = io.BytesIO()
|
||
|
|
screenshot_data.save(img_buffer, format='PNG')
|
||
|
|
img_data = base64.b64encode(img_buffer.getvalue()).decode('utf-8')
|
||
|
|
|
||
|
|
logger.info(f"ADB截图成功: {device_id}")
|
||
|
|
return ScreenshotResponse(
|
||
|
|
image_data=img_data,
|
||
|
|
format="png",
|
||
|
|
width=screenshot_data.width,
|
||
|
|
height=screenshot_data.height
|
||
|
|
)
|
||
|
|
except Exception as e:
|
||
|
|
logger.error(f"ADB截图失败: {device_id}, 错误: {e}")
|
||
|
|
raise ValueError(f"截图失败: {str(e)}")
|
||
|
|
|
||
|
|
async def install_apk(self, device_id: str, apk_path: str, package_name: Optional[str] = None) -> InstallResponse:
|
||
|
|
"""安装APK"""
|
||
|
|
try:
|
||
|
|
device = await self._get_device(device_id)
|
||
|
|
|
||
|
|
# 安装APK
|
||
|
|
result = device.install(apk_path)
|
||
|
|
|
||
|
|
if result:
|
||
|
|
logger.info(f"ADB安装APK成功: {device_id} -> {apk_path}")
|
||
|
|
return InstallResponse(
|
||
|
|
success=True,
|
||
|
|
message="APK安装成功",
|
||
|
|
package_name=package_name
|
||
|
|
)
|
||
|
|
else:
|
||
|
|
logger.error(f"ADB安装APK失败: {device_id} -> {apk_path}")
|
||
|
|
return InstallResponse(
|
||
|
|
success=False,
|
||
|
|
message="APK安装失败"
|
||
|
|
)
|
||
|
|
except Exception as e:
|
||
|
|
logger.error(f"ADB安装APK异常: {device_id}, 错误: {e}")
|
||
|
|
return InstallResponse(
|
||
|
|
success=False,
|
||
|
|
message=f"APK安装异常: {str(e)}"
|
||
|
|
)
|
||
|
|
|
||
|
|
async def get_logcat(self, device_id: str, package_name: Optional[str] = None,
|
||
|
|
level: str = "V", max_lines: int = 100) -> LogcatResponse:
|
||
|
|
"""获取日志"""
|
||
|
|
try:
|
||
|
|
device = await self._get_device(device_id)
|
||
|
|
|
||
|
|
# 构建logcat命令
|
||
|
|
cmd = f"logcat -d -v time"
|
||
|
|
if package_name:
|
||
|
|
cmd += f" | grep {package_name}"
|
||
|
|
if level != "V":
|
||
|
|
cmd += f" *:{level}"
|
||
|
|
|
||
|
|
# 执行命令
|
||
|
|
result = device.shell(cmd)
|
||
|
|
|
||
|
|
# 处理结果
|
||
|
|
lines = result.strip().split('\n') if result.strip() else []
|
||
|
|
logs = lines[-max_lines:] if len(lines) > max_lines else lines
|
||
|
|
|
||
|
|
logger.info(f"ADB获取日志成功: {device_id}")
|
||
|
|
return LogcatResponse(
|
||
|
|
logs=logs,
|
||
|
|
total_lines=len(logs)
|
||
|
|
)
|
||
|
|
except Exception as e:
|
||
|
|
logger.error(f"ADB获取日志失败: {device_id}, 错误: {e}")
|
||
|
|
return LogcatResponse(
|
||
|
|
logs=[],
|
||
|
|
total_lines=0
|
||
|
|
)
|
||
|
|
|
||
|
|
async def get_device_info(self, device_id: str) -> dict:
|
||
|
|
"""获取设备信息"""
|
||
|
|
try:
|
||
|
|
device = await self._get_device(device_id)
|
||
|
|
|
||
|
|
# 获取设备信息
|
||
|
|
model = device.shell("getprop ro.product.model").strip()
|
||
|
|
android_version = device.shell("getprop ro.build.version.release").strip()
|
||
|
|
sdk_version = int(device.shell("getprop ro.build.version.sdk").strip())
|
||
|
|
|
||
|
|
return {
|
||
|
|
"device_id": device_id,
|
||
|
|
"model": model,
|
||
|
|
"android_version": android_version,
|
||
|
|
"sdk_version": sdk_version,
|
||
|
|
"status": "online"
|
||
|
|
}
|
||
|
|
except Exception as e:
|
||
|
|
logger.error(f"获取设备信息失败: {device_id}, 错误: {e}")
|
||
|
|
return {
|
||
|
|
"device_id": device_id,
|
||
|
|
"model": "Unknown",
|
||
|
|
"android_version": "Unknown",
|
||
|
|
"sdk_version": 0,
|
||
|
|
"status": "error"
|
||
|
|
}
|
||
|
|
|
||
|
|
async def list_devices(self) -> List[str]:
|
||
|
|
"""列出所有ADB设备"""
|
||
|
|
try:
|
||
|
|
if not self.adb_client:
|
||
|
|
self._initialize_adb()
|
||
|
|
|
||
|
|
devices = self.adb_client.device_list()
|
||
|
|
device_ids = [device.serial for device in devices]
|
||
|
|
|
||
|
|
logger.info(f"ADB设备列表: {device_ids}")
|
||
|
|
return device_ids
|
||
|
|
except Exception as e:
|
||
|
|
logger.error(f"获取ADB设备列表失败: {e}")
|
||
|
|
return []
|