how to work with ADB faster and easier
One day, on one of the sleepless nights, I was thinking about how I could automate routine actions on an Android smartphone. Well, you know programmers – don’t feed them bread, just let them automate something. Be it just creating some reports or sorting files.
I do a lot I'm writing about mobile development and recently in discussions, we discussed one excellent utility – Android Debug Bridge, ADB. This is a great tool to work with Android — connect your phone, and do whatever you want. But sometimes it’s too lazy to remember different commands and enter them every time… This is where Python comes to the rescue. A wonderful language with great capabilities.
In this article I will look at how to work with ADB via python.
If you are a developer or tester of Android applications, or just an ordinary programmer or user, then you have come to the right place.
Before we get started, it's worth understanding why Python.
Python is one of my favorite programming languages. Simple and readable syntax, in recent versions there has been an increase in optimization and performance, as well as the addition of various features – for example, in Python 3.10 the match-case construct was added.
http_status = 404
match http_status:
case 400:
print("Bad Request")
case 404:
print("Not Found")
case 200:
print("Success")
case _:
print('Other')
It’s also worth noting here that Python is a “collective language” focused on working with external libraries, modules, and services.
It's worth noting that this is a dynamically typed language, but there are type annotations that are most often used for debugging and documentation:
number: int = 10
fnum: float = 10.10
text: str = "Hello"
is_normal: bool = True
# И другие, такие как списки, кортежи и другие типы данных
Let's talk a little about the concept of Python. It offers support for functional programming in the Lisp tradition. Yes, Python has functions filter
, map
And reduce
.
Also from Lisp were taken the concepts of lists, dictionaries, sets and list generators. The standard library contains two modules (itertools
And functools
), which implement tools borrowed from Haskell.
According to the “Zen” of Python, instead of building all the functionality into the core of the language, it was designed to be extensible. Python became this way because of the negative experience of the ABC language – Guido Van Rossum's first project, which had a large kernel.
Also, if you need to improve performance, Python allows you to embed C libraries (.so on Linux, .dll on Windows) into Python code using the ctypes module.
Python has been ported and runs on all known platforms and OSes. I'll say even more you can port it to your own OS!
I chose it because it is a full-fledged programming language (and not a scripting language like bash), is very popular, and has many libraries. And also it is ideal for our purposes.
Let's find out what an “android debugging bridge” is – that is, ADB.
ADB was developed by Google to make it easier to debug, test, and develop Android apps. Naturally, it can be used not only for this. It is designed to solve a number of problems related to device management.
Among the main functions, perhaps, the following can be distinguished:
- Installing, uninstalling, cleaning applications
- Working with processes in the system
- Executing shell scripts and commands
- Emulation of clicks, text input, swipes, etc.
- Working with files
- Screenshots and videos
It can be used, for example, to automate certain actions.
Installation:
sudo apt install android-tools-adb # Ubuntu/debian
sudo pacman -S android-tools # Arch
In order to be able to use ADB, you need to enable USB debugging mode.
By the way, on the screen I use
scrcpy
— a utility for sharing screen from phone to PC via ADB
On your phone, go to developer options and enable USB debugging. Then connect your phone to your computer via USB cable and confirm the connection.
Then run the command adb usb
And adb devices
. The second command will display the devices, most likely you will have one device there. Most often, no additional commands are required, you can now work.
Let's look at the basic commands for debugging.
adb reboot
— reboot the device.adb reboot recovery
– reboot the device in recovery modeadb reboot bootloader
— reboot the device into fastboot mode.adb install <APK>
— installation of the APK file. You also need to set the installation permission in your phone settings. By the way, this command simply creates a window on the phone to confirm the installation, it does not create any other windows.adb install -t <APK>
— installs the application for testing.adb install -r <APK>
— reinstalls the application while saving all data.adb shell pm list packages
— gets a list of installed applications.adb uninstall com.example.example
— Removes an application by its Bundle ID.adb shell
– calls the shell, command shelladb shell screencap /sdcard/screenshot.png
– creates and saves a screenshotadb shell screenrecord --size 1280x720 --bit-rate 6000000 --time-limit 20 --verbose /sdcard/video.mp
— screencast. The parameters indicate size, bitrate, time limit (in seconds).adb shell dumpsys package com.app.example
— display information about the applicationadb logcat
— output of system logsadb bugreport
– report on sins. Collecting a device status reportadb shell dumpsys
— obtaining information about system servicesadb push <local> <remote>
— copying a file to the deviceadb pull <remote> <local>
— copying a file from the deviceadb shell am start -a android.intent.action.VIEW 'https://nometa.xyz'
— performing an action, for example opening a websiteadb shell input tap X Y
— tap on certain coordinates on the screenadb shell input swipe X1 Y1 X2 Y2 <Длительность в мс>
— swipe.adb shell dumpsys
– dump information about the system.adb shell dumpsys battery
— information about the battery.adb shell input text "Hello, Habr!"
— imitation of text input
How does it work, for example adb input tap/swipe
? ADB connects to a local “server” adb process, the request goes to the device, a shell is launched on the device and a script is executed, the script starts a Java process, and this process already imitates a click or swipe. There is also an alternative command for tap and swipe – adb shell input touchscreen tap X Y
And adb shell input touchscreen X1 Y1 X2 Y2
respectively. You can also swipe down, up, etc. through the command adb shell touchscreen motionevent DOWN/MOVE/UP X Y
.
This command is generally quite common and necessary, because it can literally help you do actions for you.
Also adb shell input
allows you to work with buttons. For example, the command adb shell input keyboard keyevent 3
simulates pressing the home button.
Keycodes can be found at this link.
There are two ways. The first is through the built-in subprocess library, and the second is through the ppadb library.
First, let's create an environment to work in:
python3 -m venv venv
source venv/bin/activate
Now let's create some file and start writing code:
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)}')
And we run:
Доступные устройства: da7045007d79
As you can see, everything works great. Now let's create more functionality – for example, reboot
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
I think it’s now clear how you can implement this or that functionality in Python. Let's write a simple utility that will allow you to find out information about the system.
To do this, let's rewrite our previous examples a little and create a modular structured code.
First, let's make the code shorter and put the command launch code into a separate function:
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
Now let's start creating the functionality – let's create a special Keyevent class, inherited from Enum.
We need it to more clearly indicate key events for the command adb shell input keyevent
which allows you to simulate keystrokes
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
Let's take a look at the special device class – Device.
class Device:
"""
Класс устройства в ADB
Каждое устройство имеет серийный номер.
"""
def __init__(self, serial: str) -> None:
"""
Инициализация устройства
:param serial: Серийный номер устройства
"""
self.serial: str = serial
self.shell_template: str="adb shell" # Шаблон вызова шелл-команды
And the final code for the ADB class:
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
So, let's start creating functionality. Let's create a shell method in the Device class that will be responsible for calling shell commands:
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
As you can see, I have minimized unnecessary steps – only creating the full command line. This method will not only be called outside the class, but also within the class itself. Let's, for example, create a method to handle keypress events:
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
In the parameters we see that Keyevent is required. This is exactly the Enum class that we created at the beginning. Instead of specifying, for example, to press the home button, the number 3 (number), we will use a more human-readable format – we will transmit Keyevent.CONTROL_HOME
. It has become clearer and allows you to increase the readability of the code.
And just in this method we use another one to shorten the lines of code. Simple is better than complex.
Now let's take a slightly different method – a method for simulating text input using the command 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
Please note that you can only enter English text in this command.
I propose to add two more types of input – tap and 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
Let's diversify our class and add a new functional method – for example, obtaining information about the battery status.
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
}
By the way, an interesting fact, in the code where we get the battery status, if you connect via USB, then your phone may show that it is charging, but if we run the code, then there will be a status that it is discharging (that is, 2). What happens is that debug mode may consume more power than the USB port can provide.
You can also see additional internal methods – they are needed to improve the readability of the method for obtaining information about the battery.
Now let's deal with the method of obtaining information about the device through the command 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
Now I propose to add the command functionality adb shell pm list packages
which displays the Bundle ID of installed applications:
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
I propose to increase the functionality by adding methods for installing and uninstalling the application:
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
Now let’s take a look at two small methods for debugging – logcat and 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
Now let's implement a method to launch an action via 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
We're at the finish line! For more or less normal functionality, all that remains is to create screencast and screenshot methods, and then upload and download files on the phone.
Let's get down to screenshotting and 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
And now, finally, the finale – let’s create methods for loading and unloading files:
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
So, here we are, creating a basic set of methods. Thanks to this, for example, you can quickly download and upload files, or, for example, use a computer keyboard to enter text on your phone (though only English text).
You can come up with different scenarios for using and testing applications.
But for now we will consider the second, less expensive method – using the library ppadb.
Abbreviation ppadb
stands for Pure Python ADB.
Library repository: link.
To install the library, run the following command:
pip3 install pure-python-adb
Let's write some simple code to display devices:
from ppadb.client import Client as ADBClient
client = ADBClient()
devices = client.devices()
for device in devices:
print(f'Устройство: {device.serial}')
Now you can install the application on all devices, check whether the package is installed, and delete it:
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")
What if we need to call shell commands?
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!')
Screenshots:
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)
Uploading files:
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")
Uploading files:
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")
And also in ppadb there is support for asynchrony:
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())
If you want to automate testing or other routine actions on an Android phone via adb, then you can use python as an intermediary. Once written, you can improve it in every possible way, and the code will be more readable.
If you are interested in mobile development and love Android, here in the channel I have collected a lot of interesting tools, guides, lessons and useful material.
Information sources
Below I have provided sources and other useful links.