Автоматизируем работу с программами на Android при помощи Accessibility Service
Еще в XIX веке Гегель сказал: «Машиноподобный труд нужно отдать машинам». И тут с одним из творцов немецкой классической философии трудно поспорить: вряд ли Георг Вильгельм Фридрих отказался бы от автоматизации таких действий, как отключение звука и нажатие кнопки «Пропустить рекламу» при просмотре ролика в YouTube или получение ежедневных бонусов за посещение приложения. Тем более что для всего этого у нас уже есть готовый инструментарий!
Как мы говорили в предыдущей статье, Accessibility Service может получать события, происходящие на экране, но он же может и вызывать их. Например, находить нужные элементы в приложении и кликать по ним.
Ставить чужое приложение с подобной функциональностью опасно — все мы знаем репутацию Google Play и примерно представляем себе, что такое приложение может сделать с твоим банковским клиентом на телефоне. Поэтому выхода два: либо декомпилировать чужое ПО и смотреть, куда именно оно нажимает, либо пилить свое, строго под поставленные задачи.
Для исследовательских целей я создал приложение, которое будет само в себя кликать из собственного сервиса :).
Подготовка к работе сервиса
Чтобы наш сервис начал работу, ему нужно предоставить права в специальном разделе настроек. Как перенаправить туда пользователя, ты уже знаешь из первой статьи. Дополнительно мы сами можем перепроверить, есть ли эти права у приложения.
Здесь accessibilityservice_id — это строка вида «имя пакета/.сервис», у нас это ru.androidtools.selfclicker/.ClickService.
Вот описание сервиса из манифеста:
Параметр label отвечает за название приложения в настройках сервиса спецвозможностей. В разделе meta-data задается указание на описание нужных функций для работы сервиса. Вот файл serviceconfig:
В нем мы описываем полномочия сервиса, типы событий, которые он может обрабатывать, и активити, которое запустится для настройки работы сервиса.
Полное описание этих параметров, как всегда, есть в документации.
checkAccess() вернул false!
Xakep #274. Твикинг Windows 11Жизненным циклом сервиса управляет система. Сами остановить сервис мы не можем. ОС самостоятельно выгрузит ненужные сервисы — к примеру, зачем крутить сервис для приложения, которое не запущено?
Мы можем привязать сервис к строго нужному приложению. Как только мы дали разрешение на работу, у сервиса вызовется метод onServiceConnected. У него мы должны вызвать метод setServiceInfo() с параметром AccessibilityServiceInfo. За фильтрацию приложений, с которыми работает сервис, отвечает строковый массив packageNames.
Да, он точно будет
Работаем с событиями AccessibilityEvent
После раздачи всех разрешений нам надо запустить нужное приложение. Если мы знаем его имя пакета, сделать это несложно:
Для запуска приложения с чистого листа используем флаг Intent.FLAG_ACTIVITY_CLEAR_TOP. В противном случае приложение может вернуться на экран со старым состоянием, очень далеким от стартового экрана.
Теперь нужно обрабатывать события в методе onAccessibilityEvent. У события есть тип, он поможет определить, что произошло (например, сменилось окно, кликнули по элементу, элемент получил фокус). Чтобы получить источник события AccessibilityNodeInfo, надо у объекта события вызвать метод getSource().
Источник имеет много полезных свойств, помогающих в работе: текст, ID, имя класса. У него могут быть родительский и дочерние элементы.
Он может быть кликабельным isClickable(), и, чтобы щелкнуть по нему, как нормальный пользователь, нужно вызвать метод performAction(AccessibilityNodeInfo.ACTION_CLICK).
Если мы хотим более глобальных действий, например нажать клавишу «Назад» на устройстве, то следует вызвать метод performGlobalAction() с нужным параметром.
Чтобы найти на экране требующуюся AccessibilityNodeInfo, мы можем вызвать один из методов: поиск по ID (findAccessibilityNodeInfosByViewId) и поиск по тексту (findAccessibilityNodeInfosByText). Будь готов к тому, что он вернет нам массив элементов или вообще ни одного.
Потренируемся на кошках, точнее — на окошках
Вот разметка нашего подопытного экрана:
У некоторых элементов есть ID и текст, у других только текст, некоторые некликабельны.
Иногда обработчики кликов устанавливают на области, превышающие своими размерами элемент с текстом или картинкой.
Поизучаем эту задачу с помощью метода debugClick.
Вот что вышло в лог:
Чтобы воспроизвести последовательность кликов, нужно сначала изучить элементы, которые будут нажиматься. Но иногда также важна и последовательность их нажатий.
Для нажатий на первые две кнопки можно использовать findAccessibilityNodeInfosByText и findAccessibilityNodeInfosByViewId. Если текст у элементов повторяется, дополнительно можно проверять на ClassName или родителя.
Чтобы кликнуть в наш LinearLayout, нужно получить его AccessibilityNodeInfo, ID у него нет, но есть дочерние элементы TextView и Button, у которых есть текст.
Для начала нам нужно получить один из них, а потом кликнуть в его родителя.
Бывают и обратные ситуации, когда есть родитель, а кликаем мы в дочерние. Для этого используй nodeInfo.getChildCount() и обращайся к элементу в цикле по ID nodeInfo.getChild(id) (если не ошибаюсь, нумерация ID идет с нуля).
Начинать работу сервиса лучше с события смены окна:
Если весь алгоритм действий уже готов, то можно запускать сервис автоматически через AlarmManager, например раз в сутки.
Отменить запуск можно вот так:
Заключение
Класс AccessibilityService позволит избавиться от рутинных операций на твоем Android-устройстве. Его возможностей достаточно, чтобы реализовать почти любую задачу, главное — дать разрешения и найти кликабельный элемент на экране.