- PVSM.RU - https://www.pvsm.ru -

Блокировщик, который нельзя смахнуть: как я следил за чужими приложениями на Android без AccessibilityService

Я делаю приложение, которое перед открытием выбранных приложений и игр показывает свой экран с коротким заданием. Чтобы это работало, нужно в каждый момент знать, какое приложение на переднем плане, не разряжать батарею и пережить агрессивную экономию энергии у разных производителей. Рассказываю, на чём я спотыкался и как это сейчас устроено.

Что делает приложение

Niyyah — блокировщик для мусульман. Человек выбирает приложения и игры, которые отнимают время. При открытии такого приложения вместо него появляется экран с заданием: счётчик тасбиха на 33 или 99 нажатий, аят Корана, который нужно набрать, или одно из 99 имён Аллаха. Выполнил — приложение открывается. Блокировка работает весь день, по расписанию или после того, как исчерпан дневной лимит времени.

Интерфейс на React Native, а всё, что касается блокировки, написано на Kotlin в нативных модулях: foreground-служба, слежение за приложениями, экран поверх других окон, приёмник загрузки.

Как узнать, какое приложение открыто

На Android нет события «пользователь открыл приложение X», на которое можно подписаться без привилегий. Есть два пути:

  1. AccessibilityService — получает события окон мгновенно, но Google Play строго ограничивает его использование: нужна отдельная декларация, и приложения-блокировщики регулярно получают отказы.

  2. UsageStatsManager с разрешением «Доступ к истории использования» — то же, на чём работает встроенный «Цифровой баланс».

Я выбрал второй путь. Служба периодически запрашивает события за последние секунды и смотрит, какое приложение последним вышло на передний план:

val events = usageStatsManager.queryEvents(startTime, endTime)
val event = UsageEvents.Event()
while (events.hasNextEvent()) {
    events.getNextEvent(event)
    when (event.eventType) {
        UsageEvents.Event.MOVE_TO_FOREGROUND,
        UsageEvents.Event.ACTIVITY_RESUMED -> lastForeground = event.packageName
        UsageEvents.Event.MOVE_TO_BACKGROUND,
        UsageEvents.Event.ACTIVITY_PAUSED -> if (event.packageName == lastForeground) lastForeground = null
    }
}

Если пакет есть в списке заблокированных, служба показывает экран Niyyah поверх него. Для этого нужно разрешение «Показ поверх других приложений».

Ошибка первой версии: экран загорался ночью

Первая версия опрашивала события раз в секунду всегда и включала блокировку ровно в момент начала расписания. Если расписание начиналось в 23:00, а телефон лежал экраном вниз на тумбочке, экран загорался посреди ночи. Заодно служба впустую крутила цикл, пока экран выключен.

Сейчас цикл опрашивает события только пока экран включён и телефон разблокирован. Когда экран гаснет, служба засыпает и ждёт системного события ACTION_SCREEN_ON:

while (running) {
    if (powerManager.isInteractive) {
        checkForeground()
        Thread.sleep(1000L)
    } else {
        waitForScreenOn() // ждём ACTION_SCREEN_ON, с ограничением по времени
    }
}

ACTION_SCREEN_ON — защищённое системное событие, его можно получить только приёмником, зарегистрированным в коде, а не в манифесте. Поэтому приёмник живёт внутри самой службы. Экран Niyyah теперь появляется только тогда, когда человек сам берёт телефон, и батарея расходуется меньше.

Производители, которые убивают службы

Foreground-служба с уведомлением по документации Android должна жить, пока её не остановят. На практике часть оболочек (MIUI, EMUI, ColorOS и другие) останавливает её своей экономией энергии, и блокировка молча перестаёт работать. Пользователь видит не «служба остановлена», а «блокировщик не работает».

Что помогло:

  • служба перезапускается после перезагрузки телефона через BOOT_COMPLETED и после обновления приложения;

  • приложение проверяет, что служба действительно работает и разрешения выданы, и если нет — показывает предупреждение с кнопкой «Исправить», которая ведёт в нужные системные настройки.

Второй пункт оказался важнее первого. Полностью победить чужую экономию энергии нельзя, но можно честно сказать человеку, что блокировка сейчас не работает, и одной кнопкой отправить его туда, где это исправляется.

Сравнение арабского текста

Одно из заданий — набрать аят. Люди набирают арабский по-разному: кто-то с огласовками, кто-то без, разные клавиатуры дают разные формы алифа, в мусхафе стоят знаки остановки и номера аятов. Если сравнивать посимвольно, правильный ответ не засчитается.

Перед сравнением оба текста нормализуются:

const TASHKEEL = /[u064B-u065Fu0670u06D6-u06ED]/g; // огласовки и знаки остановки
const AYAH_NUMBER = /u06DD[u0660-u0669d]*/g;       // знак конца аята с номером
const TATWEEL = /u0640/g;                              // кашида

function normalizeAlif(text: string): string {
  return text.replace(/[إأآٱ]/g, 'ا');
}

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

Почему без рекламы и аккаунта

Блокировщик, который зарабатывает на рекламе, зарабатывает на времени у экрана, то есть на том, с чем он должен помогать бороться. Поэтому в Niyyah нет рекламы и регистрации, данные хранятся на телефоне. Время намаза тоже считается на устройстве, без интернета, для 10 000+ городов; геолокация нужна только для выбора города, и её можно не давать.

iOS

Версии для iPhone пока нет. На iOS сторонние приложения не могут так же следить за другими приложениями и показывать поверх них свой экран: для этого есть Screen Time API с FamilyControls, у которого другая модель и свои ограничения. Это отдельная задача.

Итог

Niyyah опубликован в Google Play в октябре, у него 100+ установок и первые оценки. Я делаю его один, поэтому буду рад вопросам и критике в комментариях, особенно от тех, кто воевал с фоновыми службами на конкретных оболочках.

Niyyah в Google Play: Google Play [1]

Автор: AliLeimo

Источник [2]


Сайт-источник PVSM.RU: https://www.pvsm.ru

Путь до страницы источника: https://www.pvsm.ru/android/461740

Ссылки в тексте:

[1] Google Play: https://play.google.com/store/apps/details?id=focus.dhikr&referrer=utm_source%3Dhabr%26utm_medium%3Darticle%26utm_campaign%3Dniyyah_android

[2] Источник: https://habr.com/ru/articles/1104802/?utm_source=habrahabr&utm_medium=rss&utm_campaign=1104802