blob: 9a462056005b36468f02dae49033afa073110dc6 [file] [edit]
#!/usr/bin/env python3
#
# Copyright (C) 2025 The Android Open Source Project
#
# 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.
"""A simplified and cleaner wrapper for ADB commands."""
from __future__ import annotations
from typing import Dict, Any, List, Optional
import os
import subprocess
import time
from utils_terminal import TerminalPrintColors
class AdbError(Exception):
"""Custom exception for ADB errors."""
class Adb:
"""A class to manage adb and fastboot paths and commands."""
_adb_path: Optional[str] = None
_fastboot_path: Optional[str] = None
def __init__(self, serial: Optional[str] = None):
"""Initializes the Adb object."""
self.serial = serial
if Adb._adb_path is None:
Adb._adb_path = Adb._find_executable("adb")
if Adb._fastboot_path is None:
Adb._fastboot_path = Adb._find_executable("fastboot")
@staticmethod
def _run_command(
cmd: List[str],
*,
capture_output: bool = True,
check: bool = True,
text: bool = True,
) -> subprocess.CompletedProcess:
"""Runs a command using subprocess."""
try:
return subprocess.run(
cmd, capture_output=capture_output, check=check, text=text
)
except FileNotFoundError as e:
raise AdbError(f"Command '{cmd[0]}' not found.") from e
except subprocess.CalledProcessError as e:
stderr = e.stderr.strip() if e.stderr else "No stderr output."
raise AdbError(
f"Command '{' '.join(cmd)}' failed with exit code "
f"{e.returncode}.\n"
f"Stderr: {stderr}"
) from e
@staticmethod
def _find_executable(name: str) -> str:
"""Checks if an executable is available, prompts for a path if not."""
try:
Adb._run_command(["which", name])
return name
except AdbError:
# The adb tool is also present in host tools in the Android tree.
username = os.environ.get("USER")
android_host_out = os.environ.get("ANDROID_HOST_OUT")
standard_paths = []
if username:
standard_paths.append(
f"/usr/local/google/home/{username}/Android/Sdk/"
f"platform-tools/{name}"
)
if android_host_out:
standard_paths.append(f"{android_host_out}/bin/{name}")
for path in standard_paths:
if os.path.isfile(path) and os.access(path, os.X_OK):
use_standard = input(
f"The '{name}' command was not found in your PATH. "
"However, it was found at the standard location "
f"'{path}'.\n\nUse this path? (Y/n): "
).lower()
if use_standard in ["y", "yes", ""]:
return path
while True:
path = input(
f"Please provide the full path to your {name} binary: ")
if os.path.isfile(path) and os.access(path, os.X_OK):
return path
print(
f"\nERROR: The path '{path}' is not a valid executable "
"file. Please try again.\n"
)
def adb_command(
self,
command: List[str], **kwargs,
) -> subprocess.CompletedProcess:
"""Runs an adb command for the specified device."""
if Adb._adb_path is None:
Adb._adb_path = Adb._find_executable("adb")
cmd = [Adb._adb_path]
if self.serial:
cmd.extend(["-s", self.serial])
cmd.extend(command)
return Adb._run_command(cmd, **kwargs)
def fastboot_command(
self,
command: List[str], **kwargs,
) -> subprocess.CompletedProcess:
"""Runs a fastboot command."""
if Adb._fastboot_path is None:
Adb._fastboot_path = Adb._find_executable("fastboot")
cmd = [Adb._fastboot_path]
cmd.extend(command)
return Adb._run_command(cmd, **kwargs)
def root(self, *, quiet_terminal_output = False) -> None:
"""Restart adbd with root permissions."""
if not self.serial:
raise AdbError("Device serial not set to run root command.")
if not quiet_terminal_output:
print("Attempting to run ADB as root...")
result = self.adb_command(["root"], check=False)
if result.returncode != 0:
raise AdbError(
f"Failed to restart in root mode: {result.stderr.strip()}")
if "restarting" in result.stdout:
print("ADB is running as root.")
print("Waiting for device to reconnect...")
self.adb_command(["wait-for-device"])
print("Device reconnected.")
elif "timeout expired" in result.stdout:
raise AdbError("`adb root` timed out.")
def get_state(self) -> str:
"""Gets the device state."""
if not self.serial:
raise AdbError("Device serial not set to get device state.")
return self.adb_command(["get-state"]).stdout.strip()
def wait_for_device_with_serial(self, timeout: int = 300):
"""Waits for the device with the given serial to be available."""
if not self.serial:
raise AdbError("Device serial not set.")
print(f"Waiting for device {self.serial} to be online...")
start_time = time.time()
while time.time() - start_time < timeout:
try:
state = self.get_state()
if state == 'device':
print(f"Device {self.serial} is online.")
return
except AdbError:
# Ignore errors while polling
pass
time.sleep(5)
raise AdbError(
f"Timeout waiting for device {self.serial} to be online.")
@staticmethod
def list_devices() -> Dict[str, Any]:
"""Fetches available android devices and their page sizes."""
if Adb._adb_path is None:
Adb._adb_path = Adb._find_executable("adb")
try:
result = Adb._run_command([Adb._adb_path, "devices"])
except AdbError:
return {}
lines = result.stdout.strip().split("\n")
devices = {}
for line in lines[1:]:
if "device" in line:
device_id = line.split('\t')[0]
try:
pagesize_result = Adb._run_command(
[
Adb._adb_path,
"-s",
device_id,
"shell",
"getconf",
"PAGESIZE",
]
)
pagesize = pagesize_result.stdout.strip()
devices[device_id] = {"page_size": pagesize}
except AdbError:
devices[device_id] = {"page_size": "unknown"}
return devices
def prompt_for_device_serial() -> str:
"""Prompts the user to select a device and returns the serial."""
while True:
serials = Adb.list_devices()
devices = list(serials.keys())
title = "Select the serial"
if not devices:
title = "No devices found. Please connect a device and refresh."
print(
f"{TerminalPrintColors.BOLD}{TerminalPrintColors.GREEN}{title}"
f"{TerminalPrintColors.RESET}"
)
for i, device_id in enumerate(devices):
pagesize = serials.get(
device_id, {}).get("page_size", "N/A")
print(f" {i + 1}: {device_id} (page size: {pagesize})")
print("\n r: Refresh device list")
print(" q: Quit")
choice = input("Enter your choice: ").strip().lower()
if choice == "q":
raise SystemExit("User cancelled device selection.")
if choice == "r":
print("Refreshing device list...")
continue
try:
choice_index = int(choice) - 1
if 0 <= choice_index < len(devices):
return devices[choice_index]
print("Invalid choice. Please try again.\n")
except ValueError:
print("Invalid input. Please enter a number, 'r', or 'q'.\n")