根本原因找到了: mobilerun 的 AndroidDriver.tap() 始终用 adb shell input tap,而 HyperOS 2.0 (Android 15) 禁止了 INJECT_EVENTS 权限。但 Portal 的 HTTP API /tap 端点通过无障碍服务执行点击,不受此限制。
这是 mobilerun 代码的一个 bug——即使开了 TCP 模式,tap 和 swipe 仍然走的是 adb 而不是 Portal HTTP API。需要修复 AndroidDriver 让它在 TCP 模式下使用 Portal API。
问题: HyperOS 2.0 (Android 15) 禁止 adb shell input tap/swipe 的 INJECT_EVENTS 权限,导致所有点击/滑动操作无效
修复: 在 PortalClient 中新增 tap() / swipe() 方法,通过 Portal HTTP API(无障碍服务)执行操作;修改 AndroidDriver 在 TCP 模式下优先使用 Portal API,非 TCP 模式回退到 adb input
修改文件:
mobilerun/tools/android/portal_client.py — 新增 tap/swipe 及其 TCP/fallback 实现
mobilerun/tools/driver/android.py — tap/swipe 方法增加 Portal TCP 路径判断
async def tap(self, x: int, y: int) -> None:
"""
Tap at screen coordinates via Portal accessibility service.
Auto-selects TCP or content provider fallback.
Args:
x: X coordinate
y: Y coordinate
"""
await self._ensure_connected()
if self.tcp_available:
await self._tap_tcp(x, y)
else:
await self._tap_content_provider(x, y)
async def _tap_tcp(self, x: int, y: int) -> None:
"""Tap via TCP (Portal HTTP API)."""
try:
async with httpx.AsyncClient() as client:
response = await self._tcp_request(
client,
"POST",
f"{self.tcp_base_url}/tap",
extra_headers={"Content-Type": "application/json"},
json={"x": x, "y": y},
timeout=10,
)
if response.status_code == 200:
logger.debug(f"TCP tap successful at ({x}, {y})")
else:
logger.debug(
f"TCP tap failed ({response.status_code}), using fallback"
)
await self._tap_content_provider(x, y)
except Exception as e:
logger.debug(f"TCP tap error: {e}, using fallback")
await self._tap_content_provider(x, y)
async def _tap_content_provider(self, x: int, y: int) -> None:
"""Tap via adb shell input (fallback)."""
await self.device.shell(f"input tap {x} {y}")
async def swipe(
self,
x1: int,
y1: int,
x2: int,
y2: int,
duration_ms: int = 1000,
) -> None:
"""
Swipe via Portal accessibility service.
Auto-selects TCP or content provider fallback.
Args:
x1, y1: Start coordinates
x2, y2: End coordinates
duration_ms: Swipe duration in milliseconds
"""
await self._ensure_connected()
if self.tcp_available:
await self._swipe_tcp(x1, y1, x2, y2, duration_ms)
else:
await self._swipe_content_provider(x1, y1, x2, y2, duration_ms)
async def _swipe_tcp(
self, x1: int, y1: int, x2: int, y2: int, duration_ms: int
) -> None:
"""Swipe via TCP (Portal HTTP API)."""
try:
payload = {
"start_x": x1,
"start_y": y1,
"end_x": x2,
"end_y": y2,
"duration": duration_ms,
}
async with httpx.AsyncClient() as client:
response = await self._tcp_request(
client,
"POST",
f"{self.tcp_base_url}/swipe",
extra_headers={"Content-Type": "application/json"},
json=payload,
timeout=10,
)
if response.status_code == 200:
logger.debug(
f"TCP swipe successful from ({x1}, {y1}) to ({x2}, {y2})"
)
else:
logger.debug(
f"TCP swipe failed ({response.status_code}), using fallback"
)
await self._swipe_content_provider(x1, y1, x2, y2, duration_ms)
except Exception as e:
logger.debug(f"TCP swipe error: {e}, using fallback")
await self._swipe_content_provider(x1, y1, x2, y2, duration_ms)
async def _swipe_content_provider(
self, x1: int, y1: int, x2: int, y2: int, duration_ms: int
) -> None:
"""Swipe via adb shell input (fallback)."""
await self.device.shell(f"input swipe {x1} {y1} {x2} {y2} {duration_ms}")
async def tap(self, x: int, y: int) -> None:
await self.ensure_connected()
if self.portal and self.portal.tcp_available:
await self.portal.tap(x, y)
else:
await self.device.click(x, y)
async def swipe(
self,
x1: int,
y1: int,
x2: int,
y2: int,
duration_ms: float = 1000,
) -> None:
await self.ensure_connected()
if self.portal and self.portal.tcp_available:
await self.portal.swipe(x1, y1, x2, y2, int(duration_ms))
else:
await self.device.swipe(x1, y1, x2, y2, float(duration_ms / 1000))
await asyncio.sleep(duration_ms / 1000)
使用设备是 Redmi K80,系统是 HyperOS 2.0 (Android 15),调试时无法触发点击。
调试命令:
mobilerun run "Open the settings app and click on WLAN"以下是Claude 修复信息:
成功了! 蓝牙页面已经打开了。设备是 Redmi K80,蓝牙当前已关闭。
根本原因找到了: mobilerun 的 AndroidDriver.tap() 始终用 adb shell input tap,而 HyperOS 2.0 (Android 15) 禁止了 INJECT_EVENTS 权限。但 Portal 的 HTTP API /tap 端点通过无障碍服务执行点击,不受此限制。
这是 mobilerun 代码的一个 bug——即使开了 TCP 模式,tap 和 swipe 仍然走的是 adb 而不是 Portal HTTP API。需要修复 AndroidDriver 让它在 TCP 模式下使用 Portal API。
修复总结:
问题: HyperOS 2.0 (Android 15) 禁止 adb shell input tap/swipe 的 INJECT_EVENTS 权限,导致所有点击/滑动操作无效
修复: 在 PortalClient 中新增 tap() / swipe() 方法,通过 Portal HTTP API(无障碍服务)执行操作;修改 AndroidDriver 在 TCP 模式下优先使用 Portal API,非 TCP 模式回退到 adb input
修改文件:
mobilerun/tools/android/portal_client.py — 新增 tap/swipe 及其 TCP/fallback 实现
mobilerun/tools/driver/android.py — tap/swipe 方法增加 Portal TCP 路径判断