fix(android): launch apps with checked activity manager commands

This commit is contained in:
chenmingxuan committed 2026-10-16 11:15:14 +08:00
1 parent ef7af9988e
commit f86db1ced6
8 files changed
+334 -15

No files matched your search

+4 -9
View File
@@ -30,6 +30,7 @@ from artemis.config.agent import load_agent_config
from artemis.config.settings import settings
from artemis.config.paths import get_temp_dir
from artemis.drivers.base import BaseDeviceDriver, KeyCode, ScreenData, SwipeDirection
from artemis.drivers.android.app_launch import launch_android_app
from artemis.toolchain import find_ffmpeg, find_scrcpy
from artemis.utils.video import build_scrcpy_record_command
from artemis.utils.ui_filter import filter_ui_hierarchy
@@ -450,15 +451,9 @@ class AndroidAdbDriver(BaseDeviceDriver):
return False
async def launch_app(self, package_name: str) -> bool:
try:
cmd = f"monkey -p {package_name} -c android.intent.category.LAUNCHER 1"
await asyncio.to_thread(
self.device.shell, cmd, timeout=settings.ARTEMIS_DEVICE_COMMAND_TIMEOUT_SECONDS
)
return True
except Exception as e:
logger.error(f"Launch app failed for '{package_name}': {e}")
return False
return await launch_android_app(
self.device, package_name, settings.ARTEMIS_DEVICE_COMMAND_TIMEOUT_SECONDS
)
async def stop_app(self, package_name: str) -> bool:
try:
+96
View File
@@ -0,0 +1,96 @@
"""Launch Android activities through Activity Manager, without requiring Monkey."""
import asyncio
import re
from adbutils import AdbDevice
from artemis.drivers.errors import AppLaunchError
_PACKAGE = re.compile(r"[A-Za-z_][A-Za-z0-9_]*(?:\.[A-Za-z_][A-Za-z0-9_]*)*")
_COMPONENT = re.compile(r"([\w.]+)/([\w.$]+)", re.ASCII)
_ERROR = re.compile(
r"^\s*(?:error\b|exception\b|securityexception\b|permission denial\b|"
r"[\w.]+Exception\b)",
re.IGNORECASE | re.MULTILINE,
)
_TRANSIENT = re.compile(r"timed? out|timeout|deadobjectexception|can't find service", re.IGNORECASE)
_UNSUPPORTED = re.compile(
r"unknown (?:command|option)|unrecognized (?:command|option)", re.IGNORECASE
)
def _failure(package: str, stage: str, code: int, output: str, **kwargs: bool) -> AppLaunchError:
return AppLaunchError(
f"应用 {package} {stage}失败(退出码 {code}):{output.strip()[:1500] or '命令没有返回结果'}",
**kwargs,
)
async def _shell(
device: AdbDevice, command: list[str], package: str, stage: str, timeout: float
) -> tuple[int, str]:
try:
result = await asyncio.to_thread(device.shell2, command, timeout=timeout)
return result.returncode, result.output
except Exception as exc:
# A transport timeout can happen after Android has already received start.
# The caller must inspect the foreground before issuing another launch.
raise AppLaunchError(
f"应用 {package} {stage}时 ADB 请求失败:{str(exc)[:1500] or type(exc).__name__}",
retryable=True,
may_have_started=stage == "启动",
) from exc
async def launch_android_app(device: AdbDevice, package: str, timeout: float) -> bool:
"""Resolve the current user's launcher and request a normal launcher-style start.
True means Activity Manager accepted the command; the shared launch workflow
still verifies the foreground. Deterministic failures raise AppLaunchError so
they do not turn into repeated foreground timeouts.
"""
if not _PACKAGE.fullmatch(package):
raise AppLaunchError(f"无效的 Android 应用包名:{package!r}")
intent = ["-a", "android.intent.action.MAIN", "-c", "android.intent.category.LAUNCHER"]
resolve = ["resolve-activity", "--brief", "--user", "current", *intent, "-p", package]
component = None
# pm is the compatibility path for systems without cmd package. If neither
# exposes resolution, let Activity Manager resolve the package-scoped intent.
for prefix in (["cmd", "package"], ["pm"]):
code, output = await _shell(device, [*prefix, *resolve], package, "查询启动入口", timeout)
if code == 127 or _UNSUPPORTED.search(output):
continue
if code != 0 or _ERROR.search(output):
raise _failure(
package, "查询启动入口", code, output, retryable=bool(_TRANSIENT.search(output))
)
candidates = [
match.group(0)
for line in output.splitlines()
if (match := _COMPONENT.fullmatch(line.strip())) and match.group(1) == package
]
if len(candidates) != 1:
raise _failure(
package,
"查询启动入口",
code,
f"未找到唯一可启动入口;应用可能未安装、已停用或没有桌面入口。{output.strip()}",
)
component = candidates[0]
break
target = ["-n", component] if component else ["-p", package]
# Match a launcher tap: NEW_TASK | RESET_TASK_IF_NEEDED, preserving warm tasks.
command = ["am", "start", "-W", "--user", "current", *intent, "-f", "0x10200000", *target]
code, output = await _shell(device, command, package, "启动", timeout)
status = re.search(r"^\s*Status:\s*(\S+)", output, re.IGNORECASE | re.MULTILINE)
if code != 0 or _ERROR.search(output) or (status and status[1].lower() != "ok"):
transient = bool(_TRANSIENT.search(output))
raise _failure(
package, "启动", code, output, retryable=transient, may_have_started=transient
)
if not status and not re.search(r"Starting:\s*Intent|Warning:\s*Activity not started", output):
raise _failure(package, "启动", code, output, retryable=True, may_have_started=True)
return True