working with ADB in Python

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.

And so, then I learned about one wonderful utility – Android Debug Bridge, ADB. An excellent tool for working with Android – connect your phone and do whatever you want. But sometimes you are 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.

ADB, USB debugging

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.

USB Debug

USB Debug

By the way, on the screen I use scrcpy – 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.

Basic ADB commands

Let's look at the basic commands for debugging.

  • adb reboot – reboot the device.

  • adb reboot recovery – reboot the device in recovery mode

  • adb reboot bootloader – reboot the device into fastboot mode.

  • adb install <APK> – installation of 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 shell, command shell

  • adb shell screencap /sdcard/screenshot.png – creates and saves a screenshot

  • adb 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 application

  • adb logcat – output of system logs

  • adb bugreport – report on sins. Collecting a device status report

  • adb shell dumpsys – obtaining information about system services

  • adb push <local> <remote> – copying a file to the device

  • adb pull <remote> <local> – copying a file from the device

  • adb shell am start -a android.intent.action.VIEW 'https://nometa.xyz' – performing an action, for example opening a website

  • adb shell input tap X Y – tap on certain coordinates on the screen

  • adb 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

What's under the hood?

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.

How can I work with ADB through python?

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 functionality – create a special Keyevent class, inherited from Enum.

We need it to more clearly indicate key events for the command adb shell input keyeventwhich 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 packageswhich 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.

Pure Python ADB

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

Conclusion

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.

Thanks everyone for reading!

Information sources

Below I have provided sources and other useful links.

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *