Разработчики приложений для Android, а также тестировщики знают про команды adb shell input tap X Y и adb shell input swipe X1 Y1 X2 Y2 [DurationMs]. Но у каждой из них есть свой фатальный недостаток. Что это за недостатки, как их исправить с помощью event'ов и о нюансах я расскажу в этой статье. Да, чуть не забыл: сторонние приложения не используются, рут не нужен.

adb input tap

Разберём недостатки adb input tap. Вообще, эта команда работает хорошо и надёжно, функцию выполняет, но медленно. Под капотом происходит приблизительно следующее: adb соединяется с локальным "серверным" adb-процессом; запрос уходит через него в устройство; на устройстве запускается shell и выполняется скрипт для команды input; этот скрипт запускает новый java-процесс; java-процесс имитирует нажатие. Самым затратным по времени, как пишут по сссылке, является запуск java-процесса. К примеру, на моём Galaxy Note 4, adb input tap выполняется за 1160 мс.

Есть способ получше: пользоваться командой shell'а sendevent. И в интернетах обычно так и советуют поступать (или написать свой "sendevent"). Действительно, его использование ускоряет процесс (на том же телефоне один тап проходит за 860 мс), но главный недостаток - в надёжности. Изредка (приблизительно один случай на тысячу или несколько) некоторые сообщения как будто портятся или неполностью обрабатываются. К примеру, два последовательных тапа превращаются в проведение по экрану (из точки первого тапа в точку второго). Есть способ ещё лучше: писать данные напрямую в event-устройство, но сначала немного теории.

Разберёмся, какие команды нужно послать, чтобы сымитировать тап по экрану. Как следует из документации, существуют два типа протоколов: A и B. Причём первый помечен как устаревший, и, видимо, более не используется (ни на телефоне, ни на эмуляторе Memu он не работает). Будем пользоваться протоколом B.

Самый простой способ "на коленке" составить список сообщений - это записать сообщения, которые происходят при реальном физическом тапе по экрану. Сначала определим, какое устройство используется для собственно тачскрина. Воспользуемся командой adb shell getevent -p. То устройство, у которого в списке ABS (0003) будут значения 0030, 0035 и 0036, и является тачскрином. Что значат эти числа, я объясню чуть ниже. (строго говоря так выглядит любое устройство, которое реагирует на касания на некоторой области, например тачпад, но такие случаи для упрощения в этой статье не рассматриваются)

Теперь, зная название устройства (к примеру, пусть это будет /dev/input/event6) воспользуемся командой adb shell getevent /dev/input/event6. Если вывод идёт с задержкой и большими блоками, замените shell на exec-out: adb exec-out getevent /dev/input/event6. Пример вывода от одного тапа по экрану (с телефона):

0003 0039 0000046E
0001 014a 00000001
0001 0145 00000001
0003 0035 0000039C
0003 0036 000005AC
0003 0030 00000007
0000 0000 00000000
0003 0039 FFFFFFFF
0001 014a 00000000
0001 0145 00000000
0000 0000 00000000

Каждая строка состоит из трёх шестнадцатеричных чисел. Первое число - это тип сообщения, второе - код сообщения, третье - некторое значение, смысл которого определяется первыми двумя. Разберём подробнее, что значат эти числа, посмотрим в исходники. Типы сообщений:

EV_SYN 0 // синхронизация пакетов, используем только один код:
SYN_REPORT 0 // значение для этого кода всегда 0

EV_KEY 1 // кнопки и "кнопки", мы будем использовать следующие коды сообщений:
BTN_TOOL_FINGER 0x0145 // значения: 0 - нет пальца, 1 - есть палец
BTN_TOUCH 0x014A // значения: 0 - нет нажатия, 1 - есть нажатие

EV_ABS 3 // абсолютные оси и также MT (multitouch) события, используем следующие коды сообщений:
ABS_MT_TRACKING_ID 0x0039 // идентификатор контакта с тач-скрином
ABS_MT_POSITION_X 0x0035 // координата X
ABS_MT_POSITION_Y 0x0036 // координата Y
ABS_MT_TOUCH_MAJOR 0x0030 // сила нажатия

Сообщения для взаимодействия складываются в пакеты а пакеты в пакеты с пакетами. В первом пакете с прикосновением должен находиться неотрицательный идентификатор контакта с поверхностью. А в последнем для отпускания идентификатор равен -1. Настоящий драйвер для каждого контакта инкрементирует идентификатор, но для простых, не мультитач касаний, можно всегда использовать один и тот же. Например, ноль. Мультитач-касания чуть сложнее и хорошо описаны в документации (продублирую ещё раз ссылку), но выходят за рамки этой статьи.

Единичный тап состоит из касания в указанной точке и из отпускания. Это два пакета:

EV_ABS, ABS_MT_TRACKING_ID, 0
EV_ABS, ABS_MT_POSITION_X, <координата X>
EV_ABS, ABS_MT_POSITION_Y, <координата Y>
EV_ABS, ABS_MT_TOUCH_MAJOR, 5
EV_KEY, BTN_TOUCH, 1
EV_KEY, BTN_TOOL_FINGER, 1
EV_SYN, SYN_REPORT, 0

EV_ABS, ABS_MT_TRACKING_ID, -1
EV_KEY, BTN_TOUCH, 0
EV_KEY, BTN_TOOL_FINGER, 0
EV_ABS, ABS_MT_TOUCH_MAJOR, 0
EV_SYN, SYN_REPORT, 0

Если пользоваться методом adb shell sendevent, и даже если перечислять sendevent'ы через точку с запятой, это всё равно 12 вызовов команды. Посмотрим формат сообщения в исходнике sendevent.c. Нас интересует то, что пишется непосредственно в устройство:

struct input_event {
struct timeval time;
__u16 type;
__u16 code;
__s32 value;
};

Всё про всё - 16 байт. Структура timeval - это 8 байт, она не используется, все нули. Порядок байт - LSB. К примеру, отправка сообщенияEV_ABS, ABS_MT_TRACKING_ID, -1 приводит к записи в устройство следующих байт:

0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x03, 0x00, 0x39, 0x00, 0xFF, 0xFF, 0xFF, 0xFF

В линуксе есть два способа вывода произвольных значений: printf и echo -en. Но printf заканчивает вывод после того, как встретит первый 0 (но при этом, к примеру, printf "\x31\x00\x32\x00">test.txt создаст файл размером 2 байта). У echo такого недостатка нет. В итоге команда выглядит так: (в кавычках нужно записать все данные пакетов, но я сократил здесь для наглядности)

adb shell echo -en "\x00\x00\x00\x00\x00\x00\x00\x00\x03\x00\x39\x00...">/dev/input/event6

Чтобы каждый раз не запускать процесс adb, можно запустить его один раз с параметром shell и записывать ему в stdin команды (после записи не забывать делать flush для stdin).

Для контроля выполнения нажатия запустим второй процесс adb с параметром shell getevent /dev/input/event6 или exec-out getevent /dev/input/event6 и будем слушать его stdout. Пример вывода - см. выше (три hex-числа в каждой непустой строке). После отправки пакетов для нажатия, ожидаем получения сообщений EV_ABS, ABS_MT_TRACKING_ID, -1, а затем EV_SYN, SYN_REPORT, 0. Это можно считать подтверждением того, что нажатие было обработано. (да, технически пользователь может тыкнуть в экран, и мы считаем его последний пакет, но речь идёт про автоматизированное взаимодействие и пользователя здесь быть не должно)

Выбор shell или exec-out для adb getevent таки зависит от устройства. При старте программы делаем автоопределение: нужно отправить пустышку из четырёх сообщений: EV_ABS, ABS_MT_TRACKING_ID, 0; EV_SYN, SYN_REPORT, 0; EV_ABS, ABS_MT_TRACKING_ID, -1; EV_SYN, SYN_REPORT, 0, и если за, скажем, одну десятую секунды ничего не было получено, пробуем другой параметр.

В итоге всё время от отправки команды до получения подтверждения на том же подопытном телефоне составляет около 60 мс, т.е. раз в двадцать быстрее, чем adb input tap.

adb input swipe

Недостатки: как и tap, во-первых, выполняется долго, а во-вторых "от себя" добавляет инерцию. Т.е. это не просто "прикоснуться в точке A, провести до B и отпустить". Реальное отпускание может произойти немного дальше или ближе от указанной точки B. Это зависит от параметра длительности.

Для прямой записи в устройство используются три пакета: прикосновение, перемещение и отпускание завершением:

EV_ABS, ABS_MT_TRACKING_ID, 0
EV_ABS, ABS_MT_POSITION_X, <координата X1>
EV_ABS, ABS_MT_POSITION_Y, <координата Y1>
EV_ABS, ABS_MT_TOUCH_MAJOR, 5
EV_KEY, BTN_TOUCH, 1
EV_KEY, BTN_TOOL_FINGER, 1
EV_SYN, SYN_REPORT, 0

EV_ABS, ABS_MT_POSITION_X, <координата X2>
EV_ABS, ABS_MT_POSITION_Y, <координата Y2>
EV_SYN, SYN_REPORT, 0

EV_ABS, ABS_MT_TRACKING_ID, -1
EV_KEY, BTN_TOUCH, 0
EV_KEY, BTN_TOOL_FINGER, 0
EV_ABS, ABS_MT_TOUCH_MAJOR, 0
EV_SYN, SYN_REPORT, 0

Контроль со стороны getevent аналогичен тому, что применялся для тапа. Скорость выполнения тоже практически такая же, как и для тапа.

Примечания и выводы

1. В Android до версии 7.1.2 было ограничение на размер shell-команды в 1024 байта. Начиная с 7.1.2 ограничение снято (к сожалению, не смог найти официальной информации, как и размера нового ограничения). Тем не менее, приведённые выше команды укладываются и в лимит 1024: для tap длина команды получается 797 байт, для swipe - 989.

2. Можно вести по экрану не просто из точки A в точку B, но и добавлять любые промежуточные точки. А чтобы это происходило не мгновенно, а с задержкой, команду echo можно разбивать таким образом:

echo -en "данные_пакета">dev/input/event6;sleep delaySec;echo -en "данные_следующего_пакета">dev/input/event6 ... и так далее

, где delaySec - задержка в секундах. Допускается использование дробных чисел, т.е. sleep 0.1 задержит на 100 мс.

3. При использовании echo-способа, тап и тап с перемещением выполняются существенно быстрее, чем adb input tap/swipe и гарантируют точность.

Комментарии (0)