Blokada aplikacji na Androida bez AccessibilityService

UsageStatsManager, usługa pierwszoplanowa i nakładka, na przykładzie aplikacji, która jest już w sklepie. Thomas Willheim · 9 listopada 2026

Tworzę HonorFit, blokadę aplikacji, w której zarabiasz czas przed ekranem, robiąc pompki (na słowo honoru, bez kamery). Pierwsza wersja na Androida blokowała aplikacje tak, jak robi to większość blokad na Androidzie: przez AccessibilityService. W sierpniu to wyrwałem i zastąpiłem przez UsageStatsManager, usługę pierwszoplanową (foreground service) i okno nakładki. Blokowanie zrobiło się trochę łagodniejsze, onboarding o wiele mniej straszny, a weryfikacja w Google Play dużo prostsza.

Oto jak wygląda ta architektura i na czym się po drodze przejechałem. Cały kod to Kotlin w lokalnym module Expo, ale nic tu nie jest specyficzne dla React Native.

Po co rezygnować z Accessibility API

Usługa ułatwień dostępu to najmocniejsze narzędzie, jakie dostajesz na Androidzie. Dowiadujesz się o każdej zmianie okna w chwili, gdy następuje, a performGlobalAction(GLOBAL_ACTION_HOME) od razu wyrzuca użytkownika z zablokowanej aplikacji. Dlatego korzysta z niej tyle blokad. Koszty:

Ten jeden fakt można zdobyć gdzie indziej: ze zdarzeń użycia, na których opiera się Cyfrowa równowaga (Digital Wellbeing).

Ogólny kształt

Trzy uprawnienia, wszystkie przyznawane przez użytkownika w Ustawieniach:

UprawnienieDo czego służy
PACKAGE_USAGE_STATS („Dostęp do danych o użyciu”)Odczyt UsageEvents, żeby wiedzieć, która aplikacja jest na wierzchu. Nigdy nie jest przyznawane w czasie działania; odsyłasz użytkownika na ekran Ustawień i sprawdzasz app-op.
SYSTEM_ALERT_WINDOW („Wyświetlanie nad innymi aplikacjami”)Rysowanie ekranu blokady nad zablokowaną aplikacją.
FOREGROUND_SERVICE_SPECIAL_USEUsługa monitorująca. Blokowanie aplikacji nie pasuje do żadnego z wymienionych typów usług pierwszoplanowych, więc to specialUse z ciągiem podtypu, który wyjaśnia dlaczego.

Elementy:

Wpis usługi w manifeście:

<service
  android:name=".ForegroundMonitorService"
  android:exported="false"
  android:foregroundServiceType="specialUse">
  <property
    android:name="android.app.PROPERTY_SPECIAL_USE_FGS_SUBTYPE"
    android:value="User-configured app blocking for digital wellbeing: monitors
      which app is in the foreground (UsageStats) so apps the user chose to block
      can be locked when their earned screen time runs out." />
</service>

Musisz też uzasadnić specialUse w deklaracji usług pierwszoplanowych w Play Console. Opowiedzenie tej samej historii w obu miejscach pomaga.

Odpytywanie UsageStats (i jego opóźniony zapis)

Główna pętla jest niewielka. Przejrzyj zdarzenia od ostatniego odpytania i zapamiętaj pakiet z ostatniego MOVE_TO_FOREGROUND (to ta sama stała co ACTIVITY_RESUMED, przemianowana w API 29):

val end = System.currentTimeMillis()
val events = usm.queryEvents(windowStart, end)
val event = UsageEvents.Event()
while (events.hasNextEvent()) {
  events.getNextEvent(event)
  if (event.eventType == UsageEvents.Event.MOVE_TO_FOREGROUND) {
    currentForeground = event.packageName
  }
}
// Re-scan the last 15 s next time: see below.
windowStart = end - REQUERY_OVERLAP_MS
handleForeground(currentForeground)

Miły efekt uboczny w porównaniu ze zdarzeniami ułatwień dostępu: klawiatura i panel powiadomień nie są aktywnościami, więc nigdy nie pojawiają się jako fałszywe wyjścia w stylu „użytkownik opuścił aplikację”.

Pułapka: zdarzenia przychodzą z opóźnieniem

Moja pierwsza wersja przesuwała okno wyłącznie do przodu: każde odpytanie obejmowało [lastEnd, now]. Zwykle działało, aż na telefonie uruchomionym na zimno pierwsze otwarcie zablokowanej aplikacji w ogóle nie zostało zauważone. UsageStats zapisuje zdarzenia w swoim magazynie z opóźnieniem, więc zdarzenie ze znacznikiem 10:00:00.900 może się pojawić po odpytaniu, które już objęło 10:00:01. Okno przesuwane wyłącznie do przodu pomija je na zawsze.

Rozwiązanie: każde odpytanie skanuje ponownie zakładkę z przeszłości (ja używam 15 sekund). Zobaczenie tego samego zdarzenia dwa razy niczym nie grozi, bo i tak wygrywa ostatnie wznowienie w oknie. Zdarzenie, które dotrze z ponad 15-sekundowym opóźnieniem, nadal przepadnie, ale takiego nie widziałem.

Pułapka: pierwsze odpytanie musi sięgać daleko wstecz

Kiedy usługa startuje (po restarcie albo gdy włącza się ekran), nie ma „ostatniej znanej aplikacji”. Jeśli użytkownik siedzi na TikToku od 20 minut, a pierwsze odpytanie patrzy tylko minutę wstecz, nie zobaczy niczego, dopóki ten nie przełączy aplikacji. Dlatego pierwsze odpytanie skanuje ostatnią godzinę. To bezpieczne, bo launcher też jest aktywnością: to, co zostało wznowione jako ostatnie, naprawdę jest na wierzchu.

Pułapka: odebrany dostęp wygląda jak cisza

Jeśli użytkownik wyłączy dostęp do danych o użyciu, gdy usługa działa, queryEvents nie rzuca wyjątku. Po prostu nic nie zwraca. Ostatni odczyt („Instagram jest na wierzchu”) traci aktualność, a saldo spływa w nieskończoność. Dlatego każde odpytanie najpierw sprawdza app-op i zatrzymuje zegar, jeśli uprawnienia już nie ma:

fun hasUsageAccess(context: Context): Boolean {
  val appOps = context.getSystemService(Context.APP_OPS_SERVICE) as AppOpsManager
  val mode = appOps.checkOpNoThrow(
    AppOpsManager.OPSTR_GET_USAGE_STATS, Process.myUid(), context.packageName)
  return mode == AppOpsManager.MODE_ALLOWED
}

Wyłączony ekran oznacza stop

Gdy ekran się wyłącza, usługa rozlicza otwartą sesję w banku i całkiem przestaje odpytywać. Nikt nie używa aplikacji przy wyłączonym ekranie, a pętla 1 Hz, która działa tylko wtedy, gdy patrzysz na telefon, jest tania. Pierwsze odpytanie po ponownym włączeniu ekranu obsługuje też zmianę dnia (reset o północy, dzienna baza), więc dzieje się to, zanim użytkownik dotrze do zablokowanej aplikacji, nawet jeśli nigdy nie otworzy HonorFit.

Nakładka blokady

Bez ułatwień dostępu nie ma GLOBAL_ACTION_HOME, więc nie możesz wyrzucić użytkownika z aplikacji. Zamiast tego ją zasłaniasz: oknem TYPE_APPLICATION_OVERLAY z przyciskiem „zdobądź więcej czasu” i przyciskiem „wróć do ekranu głównego”.

Jak utrzymać usługę przy życiu

Dopracowanie tej części zajmuje najwięcej czasu i w większości nie chodzi w niej o twój kod.

START_STICKY nie wystarcza

„Uśpione aplikacje” Samsunga, MIUI, OnePlus i spółka zabijają usługi w tle, a potem połykają restart sticky. Blokada to tu najgorszy przypadek: ludzie jej nie otwierają, dopóki mają czas, więc nikt nie zauważa, że monitora już nie ma. HonorFit ustawia niedokładny, niewybudzający, powtarzalny alarm co 15 minut, który przywraca usługę. Nigdy nie wybudza telefonu, a alarmy odroczone przy wyłączonym ekranie odpalają się wkrótce po jego włączeniu, czyli dokładnie wtedy, gdy monitor ma znaczenie.

Android 12+ nie zawsze pozwoli ci ją uruchomić

Uruchomienie usługi pierwszoplanowej z tła (z alarmu, przy restarcie sticky) rzuca ForegroundServiceStartNotAllowedException, chyba że masz wyjątek. Dwa, które się tu liczą: użytkownik wyłączył optymalizację baterii dla aplikacji albo aplikacja ma SYSTEM_ALERT_WINDOW (to pomaga tylko przed Androidem 15). Więc:

Restarty i aktualizacje

Odbiornik (receiver) dla BOOT_COMPLETED i MY_PACKAGE_REPLACED wznawia monitorowanie. O drugim łatwo zapomnieć: automatyczna aktualizacja z Play zabija twój proces i nigdy nie uruchamia ponownie jego usług, więc bez tego każda aktualizacja po cichu wyłącza blokadę, dopóki użytkownik nie otworzy aplikacji. Oba broadcasty są zwolnione z ograniczenia uruchamiania z tła, a odbiornik z exported="false" nadal je dostaje.

Drobniejsze sprawy

Kompromisy

To podejście jest łagodniejsze niż ułatwienia dostępu i chcę to powiedzieć wprost:

Dla blokady na słowo honoru to właściwy kompromis. HonorFit nie próbuje być nie do obejścia. Ktoś zdeterminowany, żeby oszukiwać, może po prostu zapisać powtórzenia, których nie zrobił. Chodzi o pauzę i cenę, a nie o więzienie. W zamian dostajesz ekran uprawnień, na którym jest „dostęp do danych o użyciu” zamiast „pełnej kontroli”, dużo prostszą weryfikację w Play i usługę, która odczytuje dokładnie jedną rzecz: nazwę pakietu aplikacji na wierzchu.

Jeśli budujesz też na iOS, tam jest zupełnie inny świat. Opisałem, czego nie mówi dokumentacja Screen Time od Apple.

HonorFit Blocker to darmowa blokada aplikacji na iPhone'a i Androida. Zarabiaj czas przed ekranem pompkami, przysiadami albo dowolnym wybranym ćwiczeniem. Na słowo honoru, bez kamery, bez konta, nic nie opuszcza twojego telefonu.

Czytaj dalej