Add execution incidents, action bursts and shared Flash history. Expose verification and Explorer settings with streamed check results. Support graceful cancellation, recording recovery and bundled UI resources. Add CI quality checks and clean up comments and documentation.
181 lines
5.7 KiB
Python
181 lines
5.7 KiB
Python
# Copyright 2026 Google LLC
|
|
#
|
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
# you may not use this file except in compliance with the License.
|
|
# You may obtain a copy of the License at
|
|
#
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
#
|
|
# Unless required by applicable law or agreed to in writing, software
|
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
# See the License for the specific language governing permissions and
|
|
# limitations under the License.
|
|
|
|
"""Device discovery, ADB resolution, and emulator management for MCP workflows."""
|
|
|
|
import os
|
|
import shutil
|
|
import subprocess
|
|
import sys
|
|
import time
|
|
|
|
|
|
def resolve_adb_path() -> str:
|
|
"""Resolves the absolute path to the adb binary across platforms."""
|
|
# 1. Check if adb is in PATH
|
|
which_adb = shutil.which("adb")
|
|
if which_adb:
|
|
return which_adb
|
|
|
|
# 2. Check ANDROID_HOME / ANDROID_SDK_ROOT
|
|
for env_var in ("ANDROID_HOME", "ANDROID_SDK_ROOT"):
|
|
sdk_root = os.getenv(env_var)
|
|
if sdk_root:
|
|
candidate = os.path.join(
|
|
sdk_root, "platform-tools", "adb.exe" if sys.platform == "win32" else "adb"
|
|
)
|
|
if os.path.exists(candidate):
|
|
return candidate
|
|
|
|
# 3. Check standard OS-specific SDK locations
|
|
candidates = []
|
|
if sys.platform == "win32":
|
|
local_appdata = os.getenv("LOCALAPPDATA", "")
|
|
if local_appdata:
|
|
candidates.append(
|
|
os.path.join(local_appdata, "Android", "Sdk", "platform-tools", "adb.exe")
|
|
)
|
|
elif sys.platform == "darwin":
|
|
candidates.extend(
|
|
[
|
|
os.path.expanduser("~/Library/Android/sdk/platform-tools/adb"),
|
|
"/opt/homebrew/bin/adb",
|
|
"/usr/local/bin/adb",
|
|
]
|
|
)
|
|
else: # Linux
|
|
candidates.extend(
|
|
[
|
|
"/usr/bin/adb",
|
|
os.path.expanduser("~/Android/Sdk/platform-tools/adb"),
|
|
"/usr/local/bin/adb",
|
|
]
|
|
)
|
|
|
|
for cand in candidates:
|
|
if os.path.exists(cand):
|
|
return cand
|
|
|
|
return "adb"
|
|
|
|
|
|
def resolve_emulator_path() -> str | None:
|
|
"""Resolves the path to the Android emulator executable."""
|
|
which_emulator = shutil.which("emulator")
|
|
if which_emulator:
|
|
return which_emulator
|
|
|
|
candidates = [
|
|
os.path.expanduser("~/Android/Sdk/emulator/emulator"),
|
|
os.path.expanduser("~/Library/Android/sdk/emulator/emulator"),
|
|
]
|
|
if sys.platform == "win32":
|
|
local_appdata = os.getenv("LOCALAPPDATA", "")
|
|
if local_appdata:
|
|
candidates.append(
|
|
os.path.join(local_appdata, "Android", "Sdk", "emulator", "emulator.exe")
|
|
)
|
|
|
|
for cand in candidates:
|
|
if os.path.exists(cand):
|
|
return cand
|
|
return None
|
|
|
|
|
|
def get_connected_devices(adb_path: str | None = None) -> list[str]:
|
|
"""Returns a list of connected and authorized Android device serials."""
|
|
adb = adb_path or resolve_adb_path()
|
|
try:
|
|
res = subprocess.run(
|
|
[adb, "devices"],
|
|
stdin=subprocess.DEVNULL,
|
|
capture_output=True,
|
|
text=True,
|
|
timeout=5,
|
|
)
|
|
devices: list[str] = []
|
|
for line in res.stdout.splitlines():
|
|
line_str = line.strip()
|
|
if line_str and not line_str.startswith("List of devices"):
|
|
parts = line_str.split()
|
|
if len(parts) >= 2 and parts[1] == "device":
|
|
devices.append(parts[0])
|
|
return devices
|
|
except Exception:
|
|
return []
|
|
|
|
|
|
def is_emulator_running(adb_path: str | None = None) -> bool:
|
|
"""Checks if an Android emulator (e.g. emulator-5554) is running and accessible."""
|
|
adb = adb_path or resolve_adb_path()
|
|
devices = get_connected_devices(adb)
|
|
return any(d.startswith("emulator-") for d in devices)
|
|
|
|
|
|
def ensure_emulator(
|
|
avd_name: str = "AndroidWorldAvd",
|
|
adb_path: str | None = None,
|
|
emulator_path: str | None = None,
|
|
timeout_seconds: int = 90,
|
|
) -> bool:
|
|
"""Ensures an emulator is running. If not, boots the specified AVD in the background."""
|
|
adb = adb_path or resolve_adb_path()
|
|
if is_emulator_running(adb):
|
|
return True
|
|
|
|
emu_exe = emulator_path or resolve_emulator_path()
|
|
if not emu_exe or not os.path.exists(emu_exe):
|
|
return False
|
|
|
|
try:
|
|
command = [emu_exe, "-avd", avd_name, "-no-snapshot", "-grpc", "8554"]
|
|
if sys.platform == "win32":
|
|
subprocess.Popen(
|
|
command,
|
|
stdin=subprocess.DEVNULL,
|
|
stdout=subprocess.DEVNULL,
|
|
stderr=subprocess.DEVNULL,
|
|
creationflags=(subprocess.CREATE_NEW_PROCESS_GROUP | subprocess.DETACHED_PROCESS),
|
|
)
|
|
else:
|
|
subprocess.Popen(
|
|
command,
|
|
stdin=subprocess.DEVNULL,
|
|
stdout=subprocess.DEVNULL,
|
|
stderr=subprocess.DEVNULL,
|
|
start_new_session=True,
|
|
)
|
|
except Exception:
|
|
return False
|
|
|
|
start_time = time.time()
|
|
while time.time() - start_time < timeout_seconds:
|
|
try:
|
|
res = subprocess.run(
|
|
[adb, "-s", "emulator-5554", "shell", "getprop", "sys.boot_completed"],
|
|
stdin=subprocess.DEVNULL,
|
|
capture_output=True,
|
|
text=True,
|
|
timeout=3,
|
|
)
|
|
if "1" in res.stdout:
|
|
time.sleep(2) # Stabilization
|
|
return True
|
|
except (subprocess.SubprocessError, OSError):
|
|
# adb not ready yet while the emulator boots: keep polling.
|
|
pass
|
|
time.sleep(2)
|
|
|
|
return False
|