Дроиды и питоны: работаем с ADB на Python

Kate

Administrator
Команда форума
Python - один из моих любимых языков программирования. Простой и читаемый синтаксис, в последних версиях наблюдается увеличение оптимизации и производительности, а также добавления разных фич - например в Python 3.10 была добавления конструкция 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')

Здесь стоить также отметить, что Python - "коллективный язык", ориентированный на работу с внешними библиотеками, модулями, сервисами.

Стоит отметить, что это язык с динамической типизацией, но существуют аннотации типов, которые чаще всего используют для дебаггинга и документаций:

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

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

Поговорим немного о концепции Python. Он предлагает поддержку функционального программирования в традициях лиспа. Так, в питоне есть функции filter, map и reduce.

Также из лиспа были взяты понятия списков, словарей, множеств и генераторов списков. Стандартная библиотека содержит два модуля (itertools и functools), которые реализуют инструменты, заимствованные из Haskell.

Согласно "дзену" питона, вместо того, чтобы встроить в ядро языка всю функциональность, он был спроектирован таким образом, чтобы быть расширяемым. Питон стал таким из-за негативного опыта языка ABC - первого проекта Гвидо Ван Россума, который имел большое ядро.

Также, если нужно повысить производительность - в Python предусмотрено встраивание C-библиотек (.so в линуксе, .dll в Windows) в код на питоне при помощи модуля ctypes.

Python портирован и работает на всех известных платформах и ОС. Даже больше скажу, можно портировать его на свою собственную ОС!

Выбрал я его из-за того что это полноценный язык программирования (а не скриптовой, как bash), является очень популярным, и имеет множество библиотек. А также он идеально подходит для наших целей.

ADB, отладка по USB​

Давайте узнаем, что такое "мост отладки android" - то есть ADB.

ADB разработан компанией Google для упрощения отладки, тестирования и разработки приложений на Android. Естественно, использовать его можно не только для этого. Он призван решать целый ряд задач, связанных с управлением устройствами.

Среди основных функций, пожалуй, можно выделить следующие:

  • Установка, удаление, очистка приложений
  • Работа с процессами в системе
  • Выполнение shell-скриптов и команд
  • Эмуляция нажатий, ввода текста, свайпов и тд
  • Работа с файлами
  • Снимки и видео с экрана
Его можно использовать, например, для автоматизации некоторых действий.

Установка:

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

Для того чтобы можно было пользоваться ADB, надо включить режим отладки по USB.

USB Debug

USB Debug
Кстати, на скрине я использую scrcpy - утилиту для шаринга экрана с телефона на ПК через ADB
Перейдите на вашем телефоне в настройки для разработчиков и включите отладку по USB. После подключите к вашему компьютеру телефон через USB-провод и подтвердите подключение.

После запустите команду adb usb и adb devices. Вторая команда выведет девайсы, скорее всего у вас там будет один девайс. Чаще всего, дополнительных команд не требуется, можно теперь работать.

Основные команды ADB​

Рассмотрим основные команды для дебага.

  • adb reboot - перезагрузка устройства.
  • adb reboot recovery - перезагрузка устройства в режиме восстановления (recovery)
  • adb reboot bootloader - перезагрузка устройства в режим fastboot.
  • adb install <APK> - установка APK файла. Требуется также задать в настройках телефона разрешение для установки. Кстати, эта команда просто создает окно на телефоне для подтверждения установки, никаких других окон не создает.
  • adb install -t <APK> - устанавливает приложение для тестирования.
  • adb install -r <APK> - переустанавливает приложение с сохранением всех данных.
  • adb shell pm list packages - получает список установленных приложений.
  • adb uninstall com.example.example - Удаляет приложение по его Bundle ID.
  • adb shell - вызывает шелл, командную оболочку
  • adb shell screencap /sdcard/screenshot.png - создает и сохраняет снимок экрана
  • adb shell screenrecord --size 1280x720 --bit-rate 6000000 --time-limit 20 --verbose /sdcard/video.mp - скринкаст. Параметры обозначают размер, битрейт, лимит по времени (в секундах).
  • adb shell dumpsys package com.app.example - вывод информации о приложении
  • adb logcat - вывод системных логов
  • adb bugreport - доклад о грешках. Сбор отчета о состоянии устройства
  • adb shell dumpsys - получение информации о системных службах
  • adb push <local> <remote> - копирование файла на устройство
  • adb pull <remote> <local> - копирование файла с устройства
  • adb shell am start -a android.intent.action.VIEW 'https://nometa.xyz' - выполнение действия, например открытие сайта
  • adb shell input tap X Y - тап на определенные координаты на экране
  • adb shell input swipe X1 Y1 X2 Y2 <Длительность в мс> - свайп.
  • adb shell dumpsys - дамп информации о системе.
  • adb shell dumpsys battery - информация о батарее.
  • adb shell input text "Hello, Habr!" - имитация ввода текста

Что под капотом?​

Как работает, например, adb input tap/swipe? ADB соединяется с локальным "серверным" adb-процессом, запрос уходит на устройство, в устройстве запускается шелл и выполняется скрипт, скрипт запускает Java-процесс, а уже этот процесс уже имитирует нажатие или свайп. Также для тапа и свайпа есть альтернативная команда - adb shell input touchscreen tap X Y и adb shell input touchscreen X1 Y1 X2 Y2 соответственно. Также можно сделать свайп вниз, вверх и тд через команду adb shell touchscreen motionevent DOWN/MOVE/UP X Y.

Эта команда вообще довольно распространенная и нужная, ведь она может помочь буквально делать действия за вас.

Также adb shell input позволяет работать с кнопками. Например, команда adb shell input keyboard keyevent 3 имитирует нажатия кнопки домой.

Кейкоды можно найти по этой ссылке.

Как можно работать с ADB через python?​

Есть два способа. Первый - через встроенную библиотеку subprocess, а второй - через библиотеку ppadb.

Для начала, давайте создадим окружение для работы:

python3 -m venv venv
source venv/bin/activate

Теперь создадим какой-нибудь файл и начнем писать код:

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)}')

И запускаем:

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

Как вы видите, все прекрасно работает. Теперь давайте создадим еще функционал - например перезагрузка

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

Я думаю что теперь понятно, как можно реализовать тот или иной функционал на Python. Давайте напишем простую утилиту, которая позволит узнать информацию о системе.

Для этого немного перепишем наши прошлые примеры, создадим модульный структурированный код.

Для начала - давайте сделаем код короче, и код запуска команды отнесем в отдельную функцию:

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

А теперь давайте займемся созданием функционала - создадим специальный класс Keyevent, наследуемый от Enum.

Он нам нужен для более понятного обозначения событий клавиш для команды adb shell input keyevent, который позволяет имитировать нажатия клавиш

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

Займемся специальным классом устройства - Device.

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

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

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

И финальный, код класса 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

Итак, начнем создавать функционал. Давайте создадим в классе Device метод shell, который будет отвечать за вызов 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

Как вы видите, я минимизировал лишние действия - только создание полной строки команды. Этот метод будет вызывать не только вне класса, но и в самом классе. Давайте, для примера, создадим метод для обработки событий нажатия клавиш:

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

В параметрах мы видем, что требуется Keyevent. Это как раз тот Enum-класс, который мы создали вначале. Вместо того, чтобы указывать, например для нажатия кнопки домой, число 3 (номер), мы будем использовать более человекочитаемый формат - будем передавать Keyevent.CONTROL_HOME. Стало более понятней, и позволяет повысить читабельность кода.

И как раз в этом методе мы задействуем другой, для сокращения строк кода. Простое лучше, чем сложное.

А теперь займемся немного другим методом - методом для имитации ввода текста, задействуя команду 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

Учтите, что в этой команде можно вводить только текст на английском.

Предлагаю добавить еще два вида input'а - tap и swipe:

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

Давайте разнообразим наш класс, и добавим новый функциональный метод - например получения информации о состоянии батареи.

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
}

Кстати, интересный факт, в коде где мы получаем статус батареи, если вы подключаете через USB, то ваш телефон может показывать что он заряжается, но если мы запустим код, то там будет статус что он разряжается (то есть 2). Так происходит, что режим отладки может потреблять больше энергии, чем может обеспечить USB порт.

А также вы можете увидеть дополнительные внутрение методы - они нужны для улучшения читабельности метода получения информации о батарее.

Теперь займемся методом получения информации об устройстве через команду 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

А теперь предлагаю добавить функционал команды adb shell pm list packages, которая выводит Bundle ID установленных приложений:

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

Предлагаю увеличить функционал путем добавления методов для установки и удаления приложения:

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

А теперь займемся двумя небольшими методами для дебага - logcat и 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

А теперь займемся реализацией метода для запуска действия через 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

Мы на финишной прямой! Для более менее нормального функционала осталось создать методы скринкаста и скриншота, а после - загрузку и выгрузку файлов на телефоне.

Давайте займемся скриншотом и скринкастом:

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

А теперь наконец то финал - создадим методы для загрузки и выгрузки файлов:

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

Итак, вот мы и создали базовый набор методов. Благодаря этому, например, можно сделать быструю загрузку и выгрузку файлов, или например использовать компьютерную клавиатуру для ввода текста на телефоне (правда, только английского текста).

Можно придумать разные сценарии использования и тестирования приложений.

Но а мы пока что рассмотрим второй, менее затратный способ - использование библиотеки ppadb.

Pure Python ADB​

Аббревиатура ppadb расшифровывается как Pure Python ADB.

hdxr-fnary2wp50xksljlzs7ba4.png

Репозиторий библиотеки: ссылка.

Для того чтобы установить библиотеку, выполните следующую команду:

pip3 install pure-python-adb

Давайте напишем простой код для вывода устройств:

from ppadb.client import Client as ADBClient

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

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

А теперь можно установить на все девайсы приложение, проверить установлен ли пакет, и удалить его:

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")

А если нам нужен вызов шелл-команд?

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!')

Скриншоты:

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)

Загрузка файлов:

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")

Выгрузка файлов:

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")

А также в ppadb есть поддержка асинхронности:

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())

Заключение​

Если вы хотите автоматизировать тестирование или другие рутинные действия на андроид телефоне через adb, то можно использовать python как посредника. Один раз написав, можно его всячески улучшать, а также код будет более читабелен.

Благодарю всех за прочтение!

 
Сверху