Хакер - Игра вслепую. Управляем Android-смартфоном через ADB
hacker_frei
rexby
Однажды мне понадобилось, чтобы одноплатник Raspberry Pi при загрузке переводил подключенный к нему телефон в режим USB-tethering. На помощь пришел ADB — интерфейс отладки Android-устройств. Существует несколько способов автоматизировать работу приложений на Android-смартфоне с помощью этого интерфейса, и в статье мы рассмотрим один из них.
Задачу мне удалось решить так: с помощью ADB получилось открыть верхнюю шторку и через меню настроек выбрать нужный режим. После чего скрипт, выполняющий эту последовательность действий, был помещен в /etc/rc.local и стартовал при загрузке микрокомпьютера. Можно ли похожим образом автоматизировать другие процессы на Android-смартфоне? Конечно!
В конце 2021 года в Дубае проходил матч за звание чемпиона мира по шахматам. Интерес к этому событию в России был подогрет тем, что против действующего чемпиона мира — Магнуса Карлсена — выступал россиянин Ян Непомнящий. Мы с приятелем следили за ходом турнира и даже решили сыграть в тандеме против Магнуса. Но поскольку сам он был занят чемпионатом, то пришлось сражаться с его электронным клоном — программой Play Magnus для Android.
Уровень сложности в программе задается возрастом Магнуса Карлсена. Мастерство профессиональных шахматистов поражает: дойти до уровня 13 лет — возраста, в котором Магнус стал одним из самых молодых гроссмейстеров мира, — нам так и не удалось. Также впечатляет способность шахматистов играть, не глядя на доску. Рекорд принадлежит Тимуру Гарееву: одновременная игра на 48 досках вслепую! Мой приятель Женя тоже решил попрактиковать этот удивительный навык. А у меня созрел план помочь ему в этом: написать скрипт — обертку для шахматного приложения.
Логика работы скрипта проста: вводишь на клавиатуре компьютера свой ход, например 'e2e4', и программа через ADB выполняет необходимую для этого последовательность нажатий (тапов) на телефоне. Затем вся история игры копируется в буфер обмена смартфона, откуда попадает на компьютер. На экран выводится последний ход — ответ шахматной программы. Для лучшего понимания материала статьи можешь посмотреть, что получилось в итоге.

ПРО ADB
ADB (Android Debug Bridge) — это инструмент для отладки устройств на базе ОС Android. Его возможности довольно обширны: можно перемещать файлы с компьютера на телефон и обратно. Для этого служат команды adb push filename и adb pull filename.
Чтобы установить или удалить приложение, достаточно ввести adb install program.apk или adb uninstall <имя пакета>. А для подключения к командной оболочке телефона используй adb shell. ADB позволяет делать еще много чего. Но главное для нас — с его помощью можно имитировать работу с тачскрином телефона (делать тапы и свайпы). Если ты ни разу не использовал ADB, то самое время попробовать! Тем более «Хакер» уже подробно писал на эту тему.
Перед первым использованием нужно включить отладку по USB. Для этого открывай «Настройки» и переходи в раздел «Система», а затем «О телефоне». Здесь ищи пункт «Номер сборки» и кликай по нему несколько раз. После этих действий в разделе «Система» появится дополнительный пункт «Для разработчиков». Заходи туда, ищи и активируй пункт «Отладка по USB».
После включения ADB в телефоне нужно будет установить ADB-сервер на компьютере. Если ты пользователь Linux, то для этого достаточно ввести в консоли команду sudo apt install adb. Если же на твоем компе установлена Windows, то придется скачать архив с утилитами. Распакуй его в удобное место, например C:\platform-tools. После чего открывай командную строку (нажимай Win + R и вводи cmd) и переходи в папку с утилитой ADB, введя команду cd C:\platform-tools.
Теперь можешь подключать телефон к компьютеру по USB-кабелю. Возможно, тебе понадобится перевести его из режима «Зарядка» в режим «Передача файлов». После чего в командной строке набери adb devices. При первом подключении на телефоне должен появиться запрос на разрешение отладки с нового устройства. Отметь галочкой «Всегда разрешать отладку с этого компьютера» и жми OK.
Можешь продолжить пользоваться ADB через USB-кабель, но такой способ не всегда удобен. Чтобы переключить телефон на работу по сети, нужно ввести команду adb tcpip 5555. Здесь 5555 — TCP-порт, на котором служба ADB будет ожидать подключения от ПК. После чего провод можно отсоединить, а на компьютере ввести команду adb connect <IP-адрес телефона>. В результате все команды ADB будут работать так же, как если бы телефон был подключен по кабелю. Данная настройка сохраняется до перезагрузки телефона. Однако теперь нужно быть бдительным, чтобы случайно не предоставить доступ к телефону злоумышленнику. Если к телефону подключится незнакомое устройство, он покажет сообщение об этом. Отозвать выданные разрешения можно в разделе «Для разработчиков» рядом с тем пунктом, где ты разрешал отладку по USB. Переключит обратно на работу по проводу команда adb usb.
ПИШЕМ КОД
Программировать будем на Python 3. Прежде всего импортируем необходимые модули и зададим путь к утилите ADB в системе.
#!/usr/bin/env python3
import os
import sys
import time
from subprocess import Popen, PIPE
ADB = 'adb' # 'C:\platform-tools\adb.exe' для Windows
Напишем функцию help.
def help():
print(f'Usage: {sys.argv[0]} [-b | --black] [-h | --help]\n')
print(f' -b, --black: play with black pieces, by default you play with white')
print(f' -h, --help: show this help and exit\n')
Основную логику работы реализуем в классе Screen. Зададим параметры board_x0, board_y0, board_x1, board_y1 — координаты x и y левого верхнего и правого нижнего края доски соответственно. Константы menu и close_menu — положение кнопок для открытия и закрытия меню. Константы save_game, ok_button и back_button — координаты кнопки сохранения партии, кнопок OK и отмены последнего хода соответственно (см. рисунок). Pawn — массив с координатами кнопок для выбора фигур взамен пешки, когда она ходит на последнюю горизонталь.
Поскольку координаты большинства элементов зависят от разрешения экрана, то их придется определять для каждого устройства индивидуально. Делать это удобно, если активировать пункт «Отображать касания» в разделе настроек «Для разработчиков». Введем переменную game, где будем хранить ход игры. А также зададим TAP_SLEEP — задержку между тапами по экрану и MOVE_SLEEP — паузу после каждого хода. Ее необходимо установить такой, чтобы программа успевала делать ответный ход.
class Screen:
board_x0 = 0
board_y0 = 412
board_x1 = 1080
board_y1 = 1492
menu = 1000, 70
close_menu = 80, 70
save_game = 550, 580
ok_button = 545, 1115
back_button = 860, 1870
pawn = [(450, 830), (450, 1020),
(450, 1200), (450, 1380)]
game = ''
TAP_SLEEP = 0.2
MOVE_SLEEP = 3



При нажатии кнопки «Сохранить партию» все сделанные с начала игры ходы записываются в буфер обмена в специальной шахматной нотации PGN. Но проблема в том, что ADB не умеет работать с буфером обмена. Решить проблему поможет приложение Clipper, которое позволяет читать из буфера и писать в него посредством широковещательных сообщений (broadcast intents). Код приложения открыт. Но не переживай, компилировать его под Android не придется. Разработчики позаботились о том, чтобы в репозитории был готовый apk-файл. Для его установки перейди в папку со скачанным файлом программы и введи команду adb install clipper.apk.
Напишем функцию __init__(), в которой запустим приложение Clipper, а также вычислим размер одной клетки игрового поля, используя недавно заданные константы.
def __init__(self):
cmd = f'{ADB} shell am startservice ca.zgrs.clipper/.ClipboardService &>/dev/null'
p = Popen(cmd.split())
self.field_size = (self.board_x1 - self.board_x0)/8
Также определим функцию _tap_screen(), которая будет принимать координаты x и y и имитировать нажатие на экран телефона в этом месте.
def _tap_screen(self, x, y):
os.system(f'{ADB} shell input tap {x} {y}')
time.sleep(self.TAP_SLEEP)
Для удобства создадим еще одну функцию _tap_board(), которая отличается от _tap_screen() только тем, что координаты x и y будут отсчитываться относительно доски, а не экрана. За ноль при этом выберем левый нижний угол игрового поля.
def _tap_board(self, x, y):
x += self.board_x0
y = self.board_y1 - y
os.system(f'{ADB} shell input tap {x} {y}')
time.sleep(self.TAP_SLEEP)
Для копирования хода игры в буфер обмена служит функция _copy_game(). Она имитирует нажатия в нужных местах экрана. Данные из буфера получим, обратившись к приложению Clipper командой adb shell am broadcast -a clipper.get. В выводе этой команды интересующие нас данные содержатся в поле data.
def _copy_game(self):
self._tap_screen(*self.menu)
self._tap_screen(*self.save_game)
self._tap_screen(*self.ok_button)
self._tap_screen(*self.close_menu)
cmd = f'{ADB} shell am broadcast -a clipper.get'
p = Popen(cmd.split(), stdin=PIPE, stdout=PIPE)
out = p.stdout.read().decode('utf-8')
out = out.split('data=')[1]
out = out[1:-2]
return out
Чтобы делать ходы, напишем функцию move(), которая будет принимать на вход строку вида 'e2e4' в любом регистре. Если вместо хода будет передана строка 'back', то программа отменит последний сделанный ход. Для вычисления координат нужной нам клетки на доске воспользуемся строковым методом find. Он отыщет в строке abcdefgh (или 12345678) букву (или цифру) нашего хода и вернет соответствующий индекс.
Именно на столько клеток нужно будет отступить от начала доски, чтобы тапнуть по нужному нам полю. В случае игры черными фигурами порядок букв abcdefgh, как и цифр 12345678, следует поменять на обратный, так как ориентация доски в приложении изменится. Если пешка ходит на последнюю горизонталь, то дополнительная буква определит фигуру, ее заменяющую. Например, если сделать ход a7a8r, то пешка будет заменена ладьей (rook).
def move(self, mov):
mov = mov.lower()
if mov == 'back':
self._tap_screen(*self.back_button)
time.sleep(self.MOVE_SLEEP/2)
return
c = 'abcdefgh'
n = '12345678'
p = 'qrbn'
if is_black:
c = c[::-1]
n = n[::-1]
start_pos_x = int(c.find(mov[0]) * self.field_size + self.field_size/2)
start_pos_y = int(n.find(mov[1]) * self.field_size + self.field_size/2)
end_pos_x = int(c.find(mov[2]) * self.field_size + self.field_size/2)
end_pos_y = int(n.find(mov[3]) * self.field_size + self.field_size/2)
self._tap_board(start_pos_x, start_pos_y)
self._tap_board(end_pos_x, end_pos_y)
if mov[4:]:
self._tap_screen(*self.pawn[p.find(mov[4])])
time.sleep(self.MOVE_SLEEP)
Осталось написать функцию answer(), получающую ответ Магнуса. Стоит предусмотреть случай, когда история ходов не изменилась после очередного хода игрока. Это говорит о том, что был сделан некорректный ход. Это вполне возможно при игре вслепую. В таком случае игроку будет показано сообщение 'ILLEGAL MOVE!'. Если ход был сделан верно, то в переменную answer запишем последний ход в игре — ответ шахматной программы. Переменная state содержит результат игры. Пока игра не завершена, там будет символ *, как это определено в PGN. В конце игры в state окажутся начисленные игрокам очки.
def answer(self):
new_game = self._copy_game()
if new_game == self.game:
answer = 'illegal move!'.upper()
state = self.game.split()[-1]
else:
answer, state = new_game.split()[-2:]
self.game = new_game
return answer, state
Класс Screen готов. Приступим к написанию основного цикла программы. Будем выводить help, если был задан параметр -h командной строки. А также устанавливать флаг is_black, отвечающий за цвет фигур игрока, в зависимости от наличия или отсутствия параметра -b. Далее создадим экземпляр s класса Screen и запустим бесконечный цикл ходов — ответов до тех пор, пока игра не закончится. Критерием конца игры будет отсутствие символа * в переменной state. Кроме того, в случае игры черными фигурами предварительно нужно считать первый ход белых. Закончить игру в любой момент можно, нажав Ctrl + C.
if __name__ == '__main__':
if len(sys.argv) < 2:
is_black = False
else:
if sys.argv[1] in ['-h', '--help']:
help()
sys.exit()
is_black = '-b' in sys.argv[1]
if is_black:
print('You play for black')
else:
print('You play for white')
s = Screen()
if is_black:
answer, state = s.answer()
print(f"Magnus' move: {answer}")
while True:
try:
move = input('Your move: ')
s.move(move)
if move != 'back':
answer, state = s.answer()
if '*' in state:
print(f"Magnus' move: {answer}")
else:
print(f"Last move: {answer}")
print(f"Result: {state}")
break
except KeyboardInterrupt:
print()
sys.exit()
На этом все. Перед запуском скрипта не забудь убедиться, что в выводимом командой adb devices списке есть твой телефон.
Заключение
Думаю, ты обратил внимание, что, отправляя команды в телефон, мы никак не контролировали корректность их исполнения. Тоже своего рода «игра вслепую». Это, конечно, существенный недостаток такого подхода к управлению устройством.
При желании можно дополнить алгоритм работы обратной связью, например получая скриншот экрана с помощью команд
adb shell screencap /sdcard/screen.png
adb pull /sdcard/screen.png
Также стоит отметить, что ADB-сервер можно установить на сам Android и управлять телефоном с другого телефона либо даже с него самого. Для этого воспользуйся Termux — эмулятором терминала для Android. О нем я недавно опубликовал статью в «Хакере». Код программы я выложил на GitHub. Можешь смело брать его за основу для создания собственных скриптов. Желаю удачи!
Читайте ещё больше платных статей бесплатно: https://t.me/hacker_frei