#!/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}"