trabajando con ADB en Python / Sudo Null IT News

Un día, en una de las noches de insomnio, estaba pensando en cómo podría automatizar acciones rutinarias en un teléfono inteligente Android. Bueno, ya conoces a los programadores: no les des pan, simplemente déjales automatizar algo. Ya sea simplemente creando algunos informes u ordenando archivos.

Y entonces descubrí una utilidad maravillosa: Android Debug Bridge, ADB. Una excelente herramienta para trabajar con Android: conecta tu teléfono y haz lo que quieras. Pero a veces eres demasiado vago para recordar diferentes comandos e ingresarlos cada vez… Aquí es donde Python viene al rescate. Un lenguaje maravilloso con grandes capacidades.

En este artículo veremos cómo trabajar con ADB a través de Python.

Si es un desarrollador o probador de aplicaciones de Android, o simplemente un programador o usuario común y corriente, ha venido al lugar correcto.

Antes de comenzar, vale la pena entender por qué Python.

Python es uno de mis lenguajes de programación favoritos. Sintaxis simple y legible, en versiones recientes ha habido un aumento en la optimización y el rendimiento, así como la adición de varias características; por ejemplo, en Python 3.10 se agregó la construcción match-case.

http_status = 404

match http_status:
	case 400:
		print("Bad Request")
	case 404:
		print("Not Found")
	case 200:
		print("Success")
	case _:
		print('Other')

También vale la pena señalar aquí que Python es un “lenguaje colectivo” centrado en trabajar con bibliotecas, módulos y servicios externos.

Vale la pena señalar que este es un lenguaje escrito dinámicamente, pero existen anotaciones de tipo que se usan con mayor frecuencia para depuración y documentación:

number: int = 10
fnum: float = 10.10
text: str = "Hello"
is_normal: bool = True

# И другие, такие как списки, кортежи и другие типы данных

Hablemos un poco del concepto de Python. Ofrece soporte para programación funcional en la tradición Lisp. Sí, Python tiene funciones. filter, map y reduce.

También de Lisp se tomaron los conceptos de listas, diccionarios, conjuntos y generadores de listas. La biblioteca estándar contiene dos módulos (itertools y functools), que implementan herramientas tomadas de Haskell.

Según el “Zen” de Python, en lugar de integrar toda la funcionalidad en el núcleo del lenguaje, fue diseñado para ser extensible. Python llegó a ser así debido a la experiencia negativa del lenguaje ABC, el primer proyecto de Guido Van Rossum, que tenía un núcleo grande.

Además, si necesita mejorar el rendimiento, Python le permite incrustar bibliotecas C (.so en Linux, .dll en Windows) en el código Python utilizando el módulo ctypes.

Python ha sido adaptado y se ejecuta en todas las plataformas y sistemas operativos conocidos. diré aún más puedes portarlo a tu propio sistema operativo!

Lo elegí porque es un lenguaje de programación completo (y no un lenguaje de scripting como bash), es muy popular y tiene muchas bibliotecas. Y además es ideal para nuestros propósitos.

ADB, depuración USB

Averigüemos qué es un “puente de depuración de Android”, es decir, ADB.

ADB fue desarrollado por Google para facilitar la depuración, prueba y desarrollo de aplicaciones de Android. Naturalmente, no sólo se puede utilizar para esto. Está diseñado para resolver una serie de problemas relacionados con la gestión de dispositivos.

Entre las funciones principales, quizás, se puedan distinguir las siguientes:

  • Instalación, desinstalación y limpieza de aplicaciones.

  • Trabajar con procesos en el sistema.

  • Ejecutar scripts y comandos de shell

  • Emulación de clics, entrada de texto, deslizamientos, etc.

  • Trabajar con archivos

  • Capturas de pantalla y vídeos

Se puede utilizar, por ejemplo, para automatizar determinadas acciones.

Instalación:

sudo apt install android-tools-adb # Ubuntu/debian
sudo pacman -S android-tools # Arch

Para poder utilizar ADB, debe habilitar el modo de depuración USB.

Depuración USB

Depuración USB

Por cierto, en la pantalla uso scrcpy – utilidad para compartir pantalla desde el teléfono a la PC a través de ADB

En su teléfono, vaya a las opciones de desarrollador y habilite la depuración de USB. Luego conecte su teléfono a su computadora mediante un cable USB y confirme la conexión.

Luego ejecuta el comando adb usb y adb devices. El segundo comando mostrará los dispositivos; lo más probable es que tenga un dispositivo allí. La mayoría de las veces, no se requieren comandos adicionales, ahora puede trabajar.

Comandos básicos de ADB

Veamos los comandos básicos para la depuración.

  • adb reboot – reinicie el dispositivo.

  • adb reboot recovery – reinicia el dispositivo en modo de recuperación

  • adb reboot bootloader – reinicie el dispositivo en modo fastboot.

  • adb install <APK> – instalación del archivo APK. También debe configurar el permiso de instalación en la configuración de su teléfono. Por cierto, este comando simplemente crea una ventana en el teléfono para confirmar la instalación, no crea ninguna otra ventana.

  • adb install -t <APK> – instala la aplicación para realizar pruebas.

  • adb install -r <APK> – reinstala la aplicación mientras guarda todos los datos.

  • adb shell pm list packages – Obtiene una lista de aplicaciones instaladas.

  • adb uninstall com.example.example – Elimina una aplicación por su ID de paquete.

  • adb shell – llama al shell, comando shell

  • adb shell screencap /sdcard/screenshot.png – crea y guarda una captura de pantalla

  • adb shell screenrecord --size 1280x720 --bit-rate 6000000 --time-limit 20 --verbose /sdcard/video.mp – captura de pantalla. Los parámetros indican tamaño, tasa de bits, límite de tiempo (en segundos).

  • adb shell dumpsys package com.app.example – mostrar información sobre la aplicación

  • adb logcat – salida de registros del sistema

  • adb bugreport – informe sobre los pecados. Recopilar un informe de estado del dispositivo

  • adb shell dumpsys – obtener información sobre los servicios del sistema

  • adb push <local> <remote> – copiar un archivo al dispositivo

  • adb pull <remote> <local> – copiar un archivo desde el dispositivo

  • adb shell am start -a android.intent.action.VIEW ' – realizar una acción, por ejemplo abrir un sitio web

  • adb shell input tap X Y – toque ciertas coordenadas en la pantalla

  • adb shell input swipe X1 Y1 X2 Y2 <Длительность в мс> – golpe fuerte.

  • adb shell dumpsys – volcar información sobre el sistema.

  • adb shell dumpsys battery – información sobre la batería.

  • adb shell input text "Hello, Habr!" – imitación de entrada de texto

¿Qué hay debajo del capó?

¿Cómo funciona, por ejemplo? adb input tap/swipe? ADB se conecta a un proceso adb de “servidor” local, la solicitud va al dispositivo, se inicia un shell en el dispositivo y se ejecuta un script, el script inicia un proceso Java y este proceso ya imita un clic o un deslizamiento. También hay un comando alternativo para tocar y deslizar: adb shell input touchscreen tap X Y y adb shell input touchscreen X1 Y1 X2 Y2 respectivamente. También puedes deslizar hacia abajo, hacia arriba, etc. mediante el comando adb shell touchscreen motionevent DOWN/MOVE/UP X Y.

Este comando es generalmente bastante común y necesario, porque literalmente puede ayudarte a realizar acciones por ti.

También adb shell input le permite trabajar con botones. Por ejemplo, el comando adb shell input keyboard keyevent 3 simula presionar el botón de inicio.

Los códigos clave se pueden encontrar en este enlace.

¿Cómo puedo trabajar con ADB a través de Python?

Hay dos maneras. El primero es a través de la biblioteca de subprocesos incorporada y el segundo es a través de la biblioteca ppadb.

Primero, creemos un entorno para trabajar:

python3 -m venv venv
source venv/bin/activate

Ahora creemos un archivo y comencemos a escribir código:

import subprocess 		# Импортируем библиотеку для вызова команд


def get_devices(self) -> list:
	"""
	Функция для получения доступных устройств.

	:return: Возвращает список устройств
	"""
	# Вызываем команду adb devices
	command = subprocess.run(('adb', 'devices'), stdout=subprocess.PIPE,
							stderr=subprocess.PIPE, text=True)

	# Если произошла ошибка, то есть код возвращает не 0, то сообщаем об этом
	if command.returncode != 0:
		print(f'Error receiving devices: {command.stderr}')
		return None

	# Получаем сообщение, очищаем его от лишнего и сплитим по строкам
	result = command.stdout.strip().split('\n')
	# От получившийся списка берем все элементы с индексом 1 и больше
	devices = result(1:)

	# Создаем список названий устройств, удаляя лишнее
	devices = (device.replace('\tdevice', '') for device in devices)

	# Возвращаем список устройств
	return devices


print(f'Доступные устройства: {" ".join(devices)}')

Y ejecutamos:

Доступные устройства: da7045007d79

Como puedes ver, todo funciona muy bien. Ahora creemos más funciones, por ejemplo, reiniciar

def reboot(reboot_type: str="plain") -> bool:
	"""
	Функция для перезагрузки устройства

	:param reboot_type: Тип ребута, по умолчанию plain. Также: recovery, fastboot
	:return: True при успешном выполнении команды, False в противном случае
	"""
	if reboot_type == 'plain':
		command = ('adb', 'reboot')
	elif reboot_type == 'recovery':
		command = ('adb', 'reboot', 'recovery')
	elif reboot_type == 'fastboot':
		command = ('adb', 'reboot', 'fastboot')

	result = subprocess.run(command, 
							stdout=subprocess.PIPE,
							stderr=subprocess.PIPE, text=True)

	if result.returncode != 0:
		return False

	return True

Creo que ahora está claro cómo se puede implementar esta o aquella funcionalidad en Python. Escribamos una utilidad simple que le permitirá encontrar información sobre el sistema.

Para hacer esto, reescribamos un poco nuestros ejemplos anteriores y creemos un código estructurado modular.

Primero, acortemos el código y coloquemos el código de inicio del comando en una función separada:

import subprocess
from typing import Union


def execute(command: Union(list, str)) -> subprocess.CompletedProcess:
	"""
	Вспомогательная функция для выполнения команды через subprocess.

	:param command: Команда в виде списка или строки
	:return: Объект процесса subprocess
	"""
	if isinstance(command, str):
		command = command.split(' ')

	result = subprocess.run(command, 
					stdout=subprocess.PIPE,
					stderr=subprocess.PIPE, text=True)

	if result.returncode != 0:
		print(f'Failed execute command: {result.stderr}')

	return result

Ahora comencemos a crear funcionalidad: cree una clase Keyevent especial, heredada de Enum.

Lo necesitamos para indicar más claramente eventos clave para el comando. adb shell input keyeventque le permite simular pulsaciones de teclas

from enum import Enum


class Keyevent(Enum):
	"""
	Числовые обозначения событий нажатия клавиш (adb shell keyevent)
	
	Правила префиксов: 
	DIGIT - цифровые клавиши
	CONTROL - клавиши управленияы
	WORD - клавиши алфавита
	"""
	DIGIT_0 = 7
	DIGIT_1 = 8
	DIGIT_2 = 9
	DIGIT_3 = 10
	DIGIT_4 = 11
	DIGIT_5 = 12
	DIGIT_6 = 13
	DIGIT_7 = 14
	DIGIT_8 = 15
	DIGIT_9 = 16

	CONTROL_BACK = 4
	CONTROL_CALL = 5
	CONTROL_CAMERA = 27
	CONTROL_CHANNEL_DOWN = 167
	CONTROL_CHANNEL_UP = 166
	CONTROL_DPAD_CENTER = 23
	CONTROL_DPAD_DOWN = 20
	CONTROL_DPAD_LEFT = 21
	CONTROL_DPAD_RIGHT = 22
	CONTROL_DPAD_UP = 19
	CONTROL_ENDCALL = 6
	CONTROL_ENTER = 66
	CONTROL_ESCAPE = 111
	CONTROL_HEADSETHOOK = 79
	CONTROL_HOME = 3
	CONTROL_MENU = 82
	CONTROL_MUTE = 91
	CONTROL_NOTIFICATION = 83
	CONTROL_POWER = 26
	CONTROL_SEARCH = 84
	CONTROL_SETTINGS = 176
	CONTROL_VOLUME_DOWN = 25
	CONTROL_VOLUME_UP = 24
	CONTROL_VOLUME_MUTE = 164
	CONTROL_ZOOM_IN = 168
	CONTROL_ZOOM_OUT = 169
	CONTROL_BRIGHTNESS_DOWN = 220
	CONTROL_BRIGHTNESS_UP = 221
	CONTROL_MEDIA_PREVIOUS = 89
	CONTROL_MEDIA_NEXT = 87
	CONTROL_MEDIA_PLAY_PAUSE = 85
	CONTROL_MEDIA_STOP = 86
	CONTROL_MEDIA_REWIND = 90
	CONTROL_MEDIA_FAST_FORWARD = 88

	WORD_A = 29
	WORD_B = 30
	WORD_C = 31
	WORD_D = 32
	WORD_E = 33
	WORD_F = 34
	WORD_G = 35
	WORD_H = 36
	WORD_I = 37
	WORD_J = 38
	WORD_K = 39
	WORD_L = 40
	WORD_M = 41
	WORD_N = 42
	WORD_O = 43
	WORD_P = 44
	WORD_Q = 45
	WORD_R = 46
	WORD_S = 47
	WORD_T = 48
	WORD_U = 49
	WORD_V = 50
	WORD_W = 51
	WORD_X = 52
	WORD_Y = 53
	WORD_Z = 54

Echemos un vistazo a la clase de dispositivo especial: Dispositivo.

class Device:
	"""
	Класс устройства в ADB

	Каждое устройство имеет серийный номер.
	"""
	def __init__(self, serial: str) -> None:
		"""
		Инициализация устройства

		:param serial: Серийный номер устройства
		"""
		self.serial: str = serial
		self.shell_template: str="adb shell"		# Шаблон вызова шелл-команды

Y el código final para la clase ADB:

class ADB:
	"""
	Класс, представляющий собой основу для взаимодействия с ADB
	"""
	def __init__(self):
		"""
		Инициализация класса
		"""
		self.devices: list = ()
		execute(('adb', 'start-server'))

	def get_devices(self) -> list:
		"""
		Метод для получения доступных устройств.

		:return: Список с объектами класса Device
		"""
		result = execute(('adb', 'devices'))

		result = result.stdout.strip().split('\n')
		devices = result(1:)

		devices = (Device(device.replace('\tdevice', '')) for device in devices)

		if not devices:
			print('No devices attached')

		return devices

Entonces, comencemos a crear funcionalidad. Creemos un método de shell en la clase Dispositivo que será responsable de llamar a los comandos de shell:

	def shell(self, command: str) -> subprocess:
		"""
		Вызов shell-команды.

		Шаблон: adb shell <command>

		:param command: Команда для запуска
		:return: Объект процесса subprocess
		"""
		command = f'{self.shell_template} {command}'
		command_list = command.split(' ')
		
		result = execute(command_list)
		print(f'Command {command} has been processed')

		return result

Como puede ver, he minimizado los pasos innecesarios: solo he creado la línea de comando completa. Este método no sólo será llamado fuera de la clase, sino también dentro de la clase misma. Por ejemplo, creemos un método para manejar eventos de pulsación de teclas:

	def input_key(self, keyevent: Keyevent) -> subprocess:
		"""
		Метод для выполнения команды имитирования нажатия различных клавиш

		:param keyevent: Номер клавиши для обработки события
		:return: Объект процесса subprocess
		"""
		result = self.shell(f'input keyevent {keyevent.value}')
		print(f'Event of pressing {keyevent.value} ({keyevent}) key has been processed')

		return result

En los parámetros vemos que se requiere Keyevent. Esta es exactamente la clase Enum que creamos al principio. En lugar de especificar, por ejemplo, que presione el botón de inicio, el número 3 (número), usaremos un formato más legible para humanos: transmitiremos Keyevent.CONTROL_HOME. Se ha vuelto más claro y le permite aumentar la legibilidad del código.

Y justo en este método usamos otro para acortar las líneas de código. Lo simple es mejor que lo complejo.

Ahora tomemos un método ligeramente diferente: un método para simular la entrada de texto usando el comando adb shell input text.

	def input_text(self, text: str) -> subprocess:
		"""
		Метод для имитирования ввода текста.

		:param text: Текст для ввода
		:return: Объект процесса subprocess
		"""
		result = self.shell(f'input text "{text}"')
		print(f'Text input event processed: {text}')

		return result

Tenga en cuenta que solo puede ingresar texto en inglés en este comando.

Propongo agregar dos tipos más de entrada: tocar y deslizar:

	def input_tap(self, x: int, y: int) -> subprocess.CompletedProcess:
		"""
		Имитация нажатия при помощи команды adb shell input tap x y

		:param x: Координата X
		:param y: Координата Y

		:return: Объект процесса subprocess
		"""
		result = self.shell(f'input tap {x} {y}')
		print(f'Action processed: tap {x} {y}')

		return result

	def input_swipe(self, x1: int, y1: int, 
					x2: int, y2: int, duration: int) -> subprocess.CompletedProcess:
		"""
		Имитация нажатия при помощи команды adb shell input swipe x1 y1 x2 y2 duration.

		:param x1: Первая координата X
		:param y1: Первая координата Y
		:param x2: Вторая координата X
		:param y2: Вторая координата Y
		:param duration: Длительность в мс

		:return: Объект процесса subprocess
		"""
		result = self.shell(f'input swipe {x1} {y1} {x2} {y2} {duration}')
		print(f'Action processed: swipe {x1} {y1} {x2} {y2} ({duration}ms)')

		return result

Diversifiquemos nuestra clase y agreguemos un nuevo método funcional, por ejemplo, obtener información sobre el estado de la batería.

	def _get_battery_status(self, status_code: int) -> str:
		"""
		Вспомогательный внутренний метод получения статуса батареи из статус-кода

		:param status_code: Код статуса батареи
		:return: Человекочитаемый статус
		"""
		match status_code:
			case 1:
				status="Charing"
			case 2:
				status="Discharging"
			case 3:
				status = "Not charging"
			case 4:
				status = "Full"

		return status

	def _get_battery_health(self, status_code: int) -> str:
		"""
		Вспомогательный внутренний метод получения здоровья батареи из статус-кода.

		:param status_code: Код статуса здоровья батареи
		:return: Человекочитаемый статус здоровья
		"""
		match status_code:
			case 1:
				status="Good"
			case 2:
				status = "Overheat"
			case 3:
				status="Dead"
			case 4:
				status = "Over voltage"
			case 5:
				status="Unspecified failure"
			case 6:
				status = "Cold"

		return status

	def get_battery_info(self) -> dict:
		"""
		Метод получения информации о батарее в телефоне.

		:return: Словарь с уровнем заряда, уровнем аккумулятора, статус батареи
		"""
		result = self.shell('dumpsys battery')

		for line in result.stdout.splitlines():
			if "level" in line:
				level = int(line.split(':')(1).strip())
			elif 'scale' in line:
				scale = int(line.split(':')(1).strip())
			elif 'status' in line:
				status = self._get_battery_status(int(line.split(':')(1).strip()))
			elif 'health' in line:
				health = self._get_battery_health(int(line.split(':')(1).strip()))
			elif 'temperature' in line:
				temp = float(line.split(':')(1).strip()) / 10
			elif 'technology' in line:
				technology = line.split(':')(1).strip()

		return {
			'level': level,
			'scale': scale,
			'status': status,
			'health': health,
			'temp': temp,
			'technology': technology
		}

Por cierto, un dato interesante, en el código donde obtenemos el estado de la batería, si se conecta a través de USB, entonces su teléfono puede mostrar que se está cargando, pero si ejecutamos el código, entonces habrá un estado que indica que se está cargando. descargando (es decir, 2). Lo que sucede es que el modo de depuración puede consumir más energía de la que puede proporcionar el puerto USB.

También puede ver métodos internos adicionales: son necesarios para mejorar la legibilidad del método para obtener información sobre la batería.

Ahora veamos el método para obtener información sobre el dispositivo a través del comando adb shell getprop:

	def _split_getprop(self, line: str) -> str:
		"""
		Вспомогательный внутренний метод для get_system_info, парсит значение из вывода
		команды adb shell getprop.

		:param line: Строка для парсинга
		:return: Конвертированная строка
		"""
		return line.split("(")(2).split(")")(0)

	def get_system_info(self) -> dict:
		"""
		Метод для получения информации о системе через команду adb shell getprop.

		:return: Словарь со всеми нужными данными о системе
		"""
		result = self.shell("getprop")
		other_info = dict()

		for line in result.stdout.splitlines():
			try:
				param_name = line.split(':')(0).replace('(', '').replace(')', '')
				other_info(param_name) = self._split_getprop(line)
			except IndexError:
				continue

		return other_info

Ahora propongo agregar la funcionalidad de comando. adb shell pm list packagesque muestra el ID del paquete de las aplicaciones instaladas:

	def get_packages(self) -> list:
		"""
		Получение списка установленных пакетов в виде bundle id.

		:return: Список пакетов
		"""
		installed_packages = list()

		result = self.shell('pm list packages')

		for line in result.stdout.splitlines():
			try:
				installed_packages.append(line.split(':')(-1))
			except IndexError:
				continue

		return installed_packages

	def get_package_by_bundle(self, bundle_name: str) -> list:
		"""
		Метод получения установленного пакета по его bundle id.

		:param bundle_name: Bundle ID искомого пакета
		:return: Список совпадений
		"""
		result = self.shell('pm list packages')
		matches = list()

		for line in result.stdout.splitlines():
			if bundle_name in line.split(':')(-1).split('.')(-1):
				matches.append(line.split(':')(-1))

		return matches

Propongo aumentar la funcionalidad agregando métodos para instalar y desinstalar la aplicación:

import os

# ...
	def install_apk(self, apk_path: str, test=False) -> subprocess.CompletedProcess:
		"""
		Метод для установки приложения через adb install <APK>.

		:param apk_path: Путь до APK-файла
		:param test: Тестовый режим

		:return: Объект процесса subprocess
		"""
		apk_path = os.path.join(apk_path)

		if not os.path.isfile(apk_path):
			print(f'APK {apk_path} for installing is not exists')
			return None

		match test:
			case True:
				result = execute(f'adb install {apk_path} -t')
			case False:
				result = execute(f'adb install {apk_path} -t')

		return result

	def remove_apk(self, bundle_name: str) -> subprocess.CompletedProcess:
		"""
		Удаления пакета по его Bundle ID.

		:param bundle_name: Bundle ID приложения

		:return: Объект процесса subprocess
		"""

		result = execute(f'adb uninstall {bundle_name}')

		return result

Ahora echemos un vistazo a dos pequeños métodos de depuración: logcat y bugreport:

	def logcat(self, loglevel: str) -> str:
		"""
		Метод для просмотра логов через команду adb logcat.

		:param loglevel: Уровет логов (Verbose, Debug, Info, Warn, Error, Fatal, Silent)
		:return: Логи
		"""
		if loglevel not in ('V', 'D', 'I', 'W', 'E', "F", "S"):
			return f'Invalid log level {loglevel}'

		result = execute(f"adb logcat '*:{loglevel}")

		return result.stdout

	def bugreport(self) -> str:
		"""
		Метод для получения доклада об ошибках, используя команду adb bugreport.

		:return: Отчет об ошибках
		"""
		result = execute('adb bugreport')

		return result.stdout

Ahora implementemos un método para iniciar una acción a través de adb shell start -a android.intent.action.<ДЕЙСТВИЕ>:

	def input_action(self, action: str, metadata: dict) -> subprocess.CompletedProcess:
		"""
		Метод для запуска определенного действия.

		:param action: Действие (send, call, sms, email, app, shutdown).
		:param metadata: Словарь с мета-данными, нужен для send, call, sms, emaiil, app.
		:return: Объект процесса subprocess
		"""
		pattern = 'am start -a android.intent.action.'

		print(action, metadata)

		if action == 'view':
			# Открытие ссылки
			pattern = f"{pattern}VIEW '{metadata('URL')}'"
		elif action == 'call':
			# Звонок
			pattern = f'{pattern}DIAL "{metadata("tel")}"'
		elif action == 'sms':
			# СМС
			pattern = f'{pattern}SENDTO "{metadata("tel")}"'
		elif action == 'email':
			# Письмо на емайл
			pattern = f'{pattern}SEND --es subject "{metadata("subject")}"' \
					f' --es text {metadata("text")}" --es email "{metadata("email")}"'
		elif action == 'app':
			# Открытие приложение
			pattern = f'{pattern}MAIN {metadata("app")}/.MainActivity'
		elif action == 'shutdown':
			# Выключение телефона
			pattern = f'{pattern}REQUEST_SHUTDOWN'
		else:
			print(f'Unsupported action: {action}')

		result = self.shell(pattern)

		return result

¡Estamos en la línea de meta! Para una funcionalidad más o menos normal, todo lo que queda es crear métodos de captura de pantalla y captura de pantalla, y luego cargar y descargar archivos en el teléfono.

Vayamos a la captura de pantalla y al screencasting:

	def screenshot(self, filepath: str) -> subprocess.CompletedProcess:
		"""
		Метод для создания и сохранения скриншота.

		:param filepath: путь для сохранения
		:return: Объект процесса subprocess
		"""
		result = self.shell(f'screencap {filepath}')

		return result

	def screencast(self, filepath: str, size: str=None, bitrate: int=6000000,
				time_limit: int=20) -> subprocess.CompletedProcess:
		"""
		Метод для создания скринкаста.

		:param filepath: Путь для сохранения
		:param size: Размер видео (например 1280x720)
		:param bitrate: Битрейт
		:param time_limit: Время записи (в секундах)

		:return: Объект процесса subprocess
		"""
		command = f'screenrecord --bit-rate {bitrate} --time-limit {time_limit}'

		if size is not None:
			command += f' --size {size}'

		result = self.shell(f'{command} --verbose {filepath}')

		return result

Y ahora, finalmente, el final: creemos métodos para cargar y descargar archivos:

	def push_file(self, local_path: str, 
					remote_path: str) -> subprocess.CompletedProcess:
		"""
		Метод, отвечающий за загрузку файла на телефон.

		:param local_path: Путь до файла для загрузки на телефон
		:param remote_path: Путь до файла для сохранения

		:return: Объект процесса subprocess
		"""
		local_path = os.path.join(local_path)

		if not os.path.isfile(local_path):
			print(f'Local file {local_path} is not exists')
			return None

		result = execute(f'adb push {local_path} {remote_path}')

		print(result.stdout.strip())
		return result

	def pull_file(self, remote_path: str, 
					local_path: str) -> subprocess.CompletedProcess:
		"""
		Метод, отвечающий за выгрузку файла с телефона.

		:param remote_path: Путь до файла на телефоне для выгрузки
		:param local_path: Путь до файла для загрузки

		:return: Объект процесса subprocess
		"""
		result = execute(f'adb pull {remote_path} {local_path}')

		print(result.stdout.strip())
		return result

Entonces, aquí estamos, creando un conjunto básico de métodos. Gracias a esto, por ejemplo, puede descargar y cargar archivos rápidamente o, por ejemplo, utilizar el teclado de la computadora para ingresar texto en su teléfono (aunque solo texto en inglés).

Puede idear diferentes escenarios para usar y probar aplicaciones.

Pero por ahora consideraremos el segundo método, menos costoso: usar la biblioteca. ppadb.

ADB de Python puro

Abreviatura ppadb significa Pure Python ADB.

Bibliotecas de repositorio: enlace.

Para instalar la biblioteca, ejecute el siguiente comando:

pip3 install pure-python-adb

Escribamos un código simple para mostrar dispositivos:

from ppadb.client import Client as ADBClient

client = ADBClient()
devices = client.devices()

for device in devices:
	print(f'Устройство: {device.serial}')

Ahora puede instalar la aplicación en todos los dispositivos, verificar si el paquete está instalado y eliminarlo:

from ppadb.client import Client as AdbClient

apk_path = "example.apk"

client = AdbClient(host="127.0.0.1", port=5037)
devices = client.devices()

for device in devices:
    device.install(apk_path)

for device in devices:
    print(device.is_installed("example.package"))

for device in devices:
    device.uninstall("example.package")

¿Qué pasa si necesitamos llamar a comandos de shell?

from ppadb.client import Client as AdbClient

client = AdbClient(host="127.0.0.1", port=5037)
devices = client.devices()

for device in devices:
    device.shell('echo Hello, Habr!')

Capturas de pantalla:

from ppadb.client import Client as AdbClient

client = AdbClient(host="127.0.0.1", port=5037)
device = client.device("emulator-5554")
result = device.screencap()

with open("screen.png", "wb") as fp:
    fp.write(result)

Subiendo archivos:

from ppadb.client import Client as AdbClient
client = AdbClient(host="127.0.0.1", port=5037)
device = client.device("emulator-5554")

device.push("example.apk", "/sdcard/example.apk")

Subiendo archivos:

from ppadb.client import Client as AdbClient
client = AdbClient(host="127.0.0.1", port=5037)
device = client.device("emulator-5554")

device.shell("screencap -p /sdcard/screen.png")
device.pull("/sdcard/screen.png", "screen.png")

Y también en ppadb hay soporte para asincronía:

import asyncio
import aiofiles
from ppadb.client_async import ClientAsync as AdbClient

async def _save_screenshot(device):
    result = await device.screencap()
    file_name = f"{device.serial}.png"
    async with aiofiles.open(f"{file_name}", mode="wb") as f:
        await f.write(result)

    return file_name

async def main():
    client = AdbClient(host="127.0.0.1", port=5037)
    devices = await client.devices()
    for device in devices:
        print(device.serial)

    result = await asyncio.gather(*(_save_screenshot(device) for device in devices))
    print(result)

asyncio.run(main())

Conclusión

Si desea automatizar pruebas u otras acciones rutinarias en un teléfono Android a través de adb, puede utilizar Python como intermediario. Una vez escrito, puedes mejorarlo en todas las formas posibles y el código será más legible.

¡Gracias a todos por leer!

Fuentes de información

A continuación proporcioné fuentes y otros enlaces útiles.

Publicaciones Similares

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *