141 lines
4.0 KiB
Python
141 lines
4.0 KiB
Python
#!/usr/bin/env python3
|
||
# -*- coding: utf-8 -*-
|
||
"""
|
||
Android device connection helpers shared by Airtest, DroidBot and ADB wrappers.
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import logging
|
||
import re
|
||
import subprocess
|
||
from typing import Any, Dict, Optional
|
||
from urllib.parse import urlencode
|
||
|
||
|
||
logger = logging.getLogger(__name__)
|
||
|
||
WIRELESS_SERIAL_RE = re.compile(r"^[A-Za-z0-9_.-]+:\d+$")
|
||
|
||
|
||
def _clean(value: Any) -> str:
|
||
return str(value or "").strip()
|
||
|
||
|
||
def _load_config_if_needed(config: Optional[Dict[str, Any]]) -> Dict[str, Any]:
|
||
if config is not None:
|
||
return config
|
||
try:
|
||
from config_loader import load_config
|
||
|
||
return load_config()
|
||
except Exception as exc:
|
||
logger.debug(f"加载 Android 设备配置失败,使用默认 ADB 设备: {exc}")
|
||
return {}
|
||
|
||
|
||
def _is_emulator(config: Dict[str, Any]) -> bool:
|
||
return bool(config.get("IS_EMULATOR", True))
|
||
|
||
|
||
def is_wireless_serial(serial: Optional[str]) -> bool:
|
||
return bool(WIRELESS_SERIAL_RE.match(_clean(serial)))
|
||
|
||
|
||
def get_android_device_serial(config: Optional[Dict[str, Any]] = None) -> Optional[str]:
|
||
"""Return the configured adb serial, or None to let adb auto-select."""
|
||
config = _load_config_if_needed(config)
|
||
|
||
serial = (
|
||
_clean(config.get("ANDROID_DEVICE_SERIAL"))
|
||
or _clean(config.get("PHYSICAL_DEVICE_SERIAL"))
|
||
or _clean(config.get("PHYSICAL_ADB_SERIAL"))
|
||
or _clean(config.get("ADB_DEVICE_SERIAL"))
|
||
)
|
||
if serial:
|
||
return serial
|
||
|
||
if _is_emulator(config):
|
||
return None
|
||
|
||
ip = (
|
||
_clean(config.get("PHYSICAL_DEVICE_IP"))
|
||
or _clean(config.get("PHYSICAL_ADB_IP"))
|
||
or _clean(config.get("ANDROID_DEVICE_IP"))
|
||
or _clean(config.get("ADB_DEVICE_IP"))
|
||
)
|
||
if not ip:
|
||
return None
|
||
if ":" in ip:
|
||
return ip
|
||
|
||
port = (
|
||
_clean(config.get("PHYSICAL_ADB_PORT"))
|
||
or _clean(config.get("ANDROID_ADB_PORT"))
|
||
or "5555"
|
||
)
|
||
return f"{ip}:{port}"
|
||
|
||
|
||
def ensure_android_wireless_connected(
|
||
config: Optional[Dict[str, Any]] = None,
|
||
serial: Optional[str] = None,
|
||
timeout: int = 15,
|
||
) -> bool:
|
||
"""Connect adb to a configured wireless device when serial is ip:port."""
|
||
config = _load_config_if_needed(config)
|
||
serial = _clean(serial) or get_android_device_serial(config)
|
||
if not is_wireless_serial(serial):
|
||
return True
|
||
|
||
try:
|
||
result = subprocess.run(
|
||
["adb", "connect", serial],
|
||
capture_output=True,
|
||
encoding="utf-8",
|
||
errors="ignore",
|
||
timeout=timeout,
|
||
)
|
||
except FileNotFoundError:
|
||
logger.error("未找到 adb,可执行文件不可用")
|
||
return False
|
||
except subprocess.TimeoutExpired:
|
||
logger.warning(f"无线 ADB 连接超时: {serial}")
|
||
return False
|
||
except Exception as exc:
|
||
logger.warning(f"无线 ADB 连接异常: {serial} - {exc}")
|
||
return False
|
||
|
||
output = (result.stdout or result.stderr or "").strip()
|
||
if result.returncode == 0 and (
|
||
"connected" in output.lower() or "already connected" in output.lower()
|
||
):
|
||
logger.info(f"无线 ADB 已连接: {serial}")
|
||
return True
|
||
|
||
logger.warning(f"无线 ADB 连接失败: {serial} - {output or result.returncode}")
|
||
return False
|
||
|
||
|
||
def build_airtest_android_uri(
|
||
config: Optional[Dict[str, Any]] = None,
|
||
*,
|
||
serial: Optional[str] = None,
|
||
**params: Any,
|
||
) -> str:
|
||
"""Build an Airtest Android URI pinned to the configured adb serial."""
|
||
config = _load_config_if_needed(config)
|
||
serial = _clean(serial) or get_android_device_serial(config)
|
||
host = _clean(config.get("AIRTEST_ADB_HOST")) or "127.0.0.1:5037"
|
||
|
||
clean_params = {
|
||
key: value
|
||
for key, value in params.items()
|
||
if value is not None and _clean(value) != ""
|
||
}
|
||
query = f"?{urlencode(clean_params)}" if clean_params else ""
|
||
|
||
if serial:
|
||
return f"Android://{host}/{serial}{query}"
|
||
return f"Android:///{query}"
|