- PVSM.RU - https://www.pvsm.ru -
Я делаю приложение, которое перед открытием выбранных приложений и игр показывает свой экран с коротким заданием. Чтобы это работало, нужно в каждый момент знать, какое приложение на переднем плане, не разряжать батарею и пережить агрессивную экономию энергии у разных производителей. Рассказываю, на чём я спотыкался и как это сейчас устроено.
Niyyah — блокировщик для мусульман. Человек выбирает приложения и игры, которые отнимают время. При открытии такого приложения вместо него появляется экран с заданием: счётчик тасбиха на 33 или 99 нажатий, аят Корана, который нужно набрать, или одно из 99 имён Аллаха. Выполнил — приложение открывается. Блокировка работает весь день, по расписанию или после того, как исчерпан дневной лимит времени.
Интерфейс на React Native, а всё, что касается блокировки, написано на Kotlin в нативных модулях: foreground-служба, слежение за приложениями, экран поверх других окон, приёмник загрузки.
На Android нет события «пользователь открыл приложение X», на которое можно подписаться без привилегий. Есть два пути:
AccessibilityService — получает события окон мгновенно, но Google Play строго ограничивает его использование: нужна отдельная декларация, и приложения-блокировщики регулярно получают отказы.
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+ городов; геолокация нужна только для выбора города, и её можно не давать.
Версии для 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
Нажмите здесь для печати.