Einen Android-App-Blocker ohne AccessibilityService bauen

UsageStatsManager, ein Foreground-Service und ein Overlay, aus einer App, die wirklich im Store ist. Von Thomas Willheim · 9. November 2026

Ich mache HonorFit, einen App-Blocker, bei dem du dir Bildschirmzeit mit Liegestützen verdienst (Ehrensystem, keine Kamera). Die erste Android-Version hat Apps so gesperrt wie die meisten Android-Blocker: mit einem AccessibilityService. Im August habe ich den rausgeworfen und durch UsageStatsManager, einen Foreground-Service und ein Overlay-Fenster ersetzt. Die Sperre ist dadurch etwas weicher geworden, das Onboarding viel weniger furchteinflößend und die Prüfung bei Google Play viel einfacher.

Hier steht, wie diese Architektur aussieht und worüber ich unterwegs gestolpert bin. Der ganze Code ist Kotlin in einem lokalen Expo-Modul, aber nichts davon ist spezifisch für React Native.

Warum die Accessibility API rausfliegt

Ein Accessibility-Service ist das mächtigste Werkzeug, das du auf Android bekommst. Du erfährst von jedem Fensterwechsel in dem Moment, in dem er passiert, und performGlobalAction(GLOBAL_ACTION_HOME) wirft den Nutzer sofort aus einer gesperrten App. Deshalb nutzen ihn so viele Blocker. Der Preis dafür:

Genau diese Information gibt es auch woanders: in den Nutzungsereignissen, auf denen Digital Wellbeing aufbaut.

Der Aufbau

Drei Berechtigungen, alle vom Nutzer in den Einstellungen erteilt:

BerechtigungWofür
PACKAGE_USAGE_STATS („Zugriff auf Nutzungsdaten“)UsageEvents lesen, um zu erfahren, welche App vorne ist. Wird nie zur Laufzeit erteilt; du schickst den Nutzer in die Einstellungen und prüfst den App-Op.
SYSTEM_ALERT_WINDOW („Über anderen Apps einblenden“)Den Sperrbildschirm über eine gesperrte App zeichnen.
FOREGROUND_SERVICE_SPECIAL_USEDer Monitor-Service. App-Sperren passt in keinen der aufgelisteten Foreground-Service-Typen, also ist es specialUse, mit einem Subtyp-String, der erklärt, warum.

Die Bausteine:

Der Manifest-Eintrag für den Service:

<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>

Außerdem begründest du specialUse in der Foreground-Service-Erklärung in der Play Console. Es hilft, an beiden Stellen dieselbe Geschichte zu erzählen.

UsageStats abfragen (und die Schreibverzögerung)

Die Kernschleife ist klein. Geh die Ereignisse seit der letzten Abfrage durch und merk dir das Paket des letzten MOVE_TO_FOREGROUND (das ist dieselbe Konstante wie ACTIVITY_RESUMED, in API 29 umbenannt):

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
  }
}
// Beim nächsten Mal die letzten 15 s erneut scannen: siehe unten.
windowStart = end - REQUERY_OVERLAP_MS
handleForeground(currentForeground)

Ein schöner Nebeneffekt gegenüber Accessibility-Events: Die Tastatur und die Benachrichtigungsleiste sind keine Activities, also tauchen sie nie als falsches „der Nutzer hat die App verlassen“ auf.

Stolperfalle: Ereignisse kommen verspätet an

Meine erste Version hat das Fenster strikt vorwärts geschoben: Jede Abfrage hat [lastEnd, now] abgefragt. Das hat meistens funktioniert, aber auf einem frisch gestarteten Handy wurde das allererste Öffnen einer gesperrten App nie erkannt. UsageStats schreibt Ereignisse mit Verzögerung in seinen Speicher, ein Ereignis mit dem Zeitstempel 10:00:00.900 kann also nach einer Abfrage auftauchen, die 10:00:01 schon abgedeckt hat. Ein strikt vorrückendes Fenster überspringt es für immer.

Die Lösung: Jede Abfrage scannt eine nachlaufende Überlappung erneut (ich nehme 15 Sekunden). Dasselbe Ereignis zweimal zu sehen schadet nicht, denn das letzte Resume im Fenster gewinnt so oder so. Ein Ereignis, das mehr als 15 Sekunden zu spät eintrifft, geht trotzdem verloren, aber das habe ich noch nicht erlebt.

Stolperfalle: Die erste Abfrage braucht einen langen Rückblick

Wenn der Service startet (nach einem Neustart oder wenn der Bildschirm angeht), gibt es keine „zuletzt bekannte App“. Wenn der Nutzer schon 20 Minuten in TikTok sitzt und die erste Abfrage nur eine Minute zurückschaut, sieht sie nichts, bis er die App wechselt. Deshalb scannt die erste Abfrage die letzte Stunde. Das ist unbedenklich, weil der Launcher auch eine Activity ist: Was zuletzt fortgesetzt wurde, ist wirklich das, was vorne ist.

Stolperfalle: Entzogener Zugriff sieht aus wie Stille

Wenn der Nutzer den Zugriff auf Nutzungsdaten abschaltet, während der Service läuft, wirft queryEvents keinen Fehler. Es liefert einfach nichts. Dein letzter Stand („Instagram ist vorne“) veraltet, und das Guthaben läuft für immer ab. Deshalb prüft jede Abfrage zuerst den App-Op und hält die Uhr an, wenn er weg ist:

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
}

Bildschirm aus heißt Stopp

Wenn der Bildschirm ausgeht, verbucht der Service die offene Sitzung und hört komplett auf abzufragen. Bei ausgeschaltetem Bildschirm nutzt niemand eine App, und eine 1-Hz-Schleife, die nur läuft, während du aufs Handy schaust, kostet wenig. Die erste Abfrage nach dem Wiedereinschalten kümmert sich außerdem um den Tageswechsel (Zurücksetzen um Mitternacht, Tagesbasis), damit das passiert, bevor der Nutzer eine gesperrte App erreicht, auch wenn er HonorFit nie öffnet.

Das Sperr-Overlay

Ohne Accessibility gibt es kein GLOBAL_ACTION_HOME, du kannst den Nutzer also nicht aus einer App werfen. Stattdessen deckst du sie ab: ein TYPE_APPLICATION_OVERLAY-Fenster mit einem Button „Mehr Zeit verdienen“ und einem Button „zum Startbildschirm“.

Den Service am Leben halten

Das ist der Teil, bei dem es am längsten dauert, bis er sitzt, und meistens liegt es nicht an deinem Code.

START_STICKY reicht nicht

Samsungs „schlafende Apps“, MIUI, OnePlus und Co. beenden Hintergrund-Services und schlucken dann den Sticky-Neustart. Ein Blocker ist dafür der schlimmste Fall: Solange die Leute noch Zeit übrig haben, öffnen sie ihn nicht, also merkt niemand, dass der Monitor weg ist. HonorFit stellt alle 15 Minuten einen ungenauen, nicht weckenden, wiederkehrenden Alarm, der den Service wieder startet. Er weckt das Handy nie, und Alarme, die bei ausgeschaltetem Bildschirm aufgeschoben wurden, feuern kurz nach dem Einschalten, also genau dann, wenn der Monitor wichtig ist.

Android 12+ lässt dich ihn nicht immer starten

Einen Foreground-Service aus dem Hintergrund zu starten (ein Alarm, ein Sticky-Neustart) wirft ForegroundServiceStartNotAllowedException, außer du hast eine Ausnahme. Zwei, die hier zählen: Der Nutzer hat die Akkuoptimierung für die App ausgeschaltet, oder die App hat SYSTEM_ALERT_WINDOW (das hilft nur vor Android 15). Also:

Neustarts und Updates

Ein Receiver für BOOT_COMPLETED und MY_PACKAGE_REPLACED startet das Monitoring neu. Den zweiten vergisst man leicht: Ein automatisches Update über Play beendet deinen Prozess und startet seine Services nie neu, ohne ihn schaltet also jedes Update deinen Blocker still ab, bis der Nutzer die App öffnet. Beide Broadcasts sind von der Einschränkung für Starts aus dem Hintergrund ausgenommen, und ein Receiver mit exported="false" bekommt sie trotzdem.

Kleinere Dinge

Die Kompromisse

Dieser Ansatz ist weicher als Accessibility, und das will ich offen sagen:

Für einen Blocker nach dem Ehrensystem ist das der richtige Kompromiss. HonorFit will nicht unknackbar sein. Wer unbedingt schummeln will, kann einfach Wiederholungen eintragen, die er nicht gemacht hat. Es geht um eine Pause und einen Preis, nicht um ein Gefängnis. Im Gegenzug bekommst du einen Berechtigungsbildschirm, auf dem „Zugriff auf Nutzungsdaten“ statt „volle Kontrolle“ steht, eine viel einfachere Play-Prüfung und einen Service, der genau eine Sache liest: den Paketnamen der App, die vorne ist.

Wenn du auch für iOS baust: Da drüben ist es eine komplett andere Welt. Ich habe aufgeschrieben, was Apples Screen-Time-Dokumentation dir nicht sagt.

HonorFit Blocker ist ein kostenloser App-Blocker für iPhone und Android. Verdien dir Bildschirmzeit mit Liegestützen, Kniebeugen oder jeder Übung, die du willst. Ehrensystem, keine Kamera, kein Konto, nichts verlässt dein Handy.

Weiterlesen