Creare un’app per bloccare le app su Android senza AccessibilityService

UsageStatsManager, un servizio in primo piano e un overlay, da un’app già pubblicata. Di Thomas Willheim · 9 novembre 2026

Sviluppo HonorFit, un’app per bloccare le app in cui il tempo schermo te lo guadagni facendo flessioni (sulla parola, niente fotocamera). La prima versione per Android bloccava le app come fa la maggior parte dei blocker su Android: con un AccessibilityService. Ad agosto l’ho tolto di mezzo e l’ho sostituito con UsageStatsManager, un servizio in primo piano e una finestra in overlay. Il blocco è diventato un po’ più morbido, l’onboarding molto meno spaventoso e la revisione su Play molto più semplice.

Ecco com’è fatta questa architettura e le cose che mi hanno fregato lungo la strada. Tutto il codice è Kotlin dentro un modulo Expo locale, ma niente di quello che c’è qui è specifico di React Native.

Perché abbandonare l’API di accessibilità

Un servizio di accessibilità è lo strumento più potente che hai su Android. Vieni a sapere di ogni cambio di finestra nell’istante in cui succede, e performGlobalAction(GLOBAL_ACTION_HOME) butta fuori l’utente da un’app bloccata all’istante. Per questo tanti blocker lo usano. I costi:

Quell’informazione si trova anche altrove: negli eventi di utilizzo su cui si basa Benessere digitale.

Com’è fatta

Tre autorizzazioni, tutte concesse dall’utente nelle Impostazioni:

AutorizzazioneA cosa serve
PACKAGE_USAGE_STATS (“Accesso ai dati di utilizzo”)Leggere gli UsageEvents per sapere quale app è davanti. Non si concede mai a runtime: mandi l’utente nella schermata delle Impostazioni e controlli l’app-op.
SYSTEM_ALERT_WINDOW (“Mostra sopra altre app”)Disegnare la schermata di blocco sopra un’app bloccata.
FOREGROUND_SERVICE_SPECIAL_USEIl servizio di monitoraggio. Il blocco delle app non rientra in nessuno dei tipi di servizio in primo piano previsti, quindi è specialUse, con una stringa di sottotipo che spiega il perché.

I pezzi:

La voce del servizio nel manifest:

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

Devi anche giustificare specialUse nella dichiarazione dei servizi in primo piano di Play Console. Raccontare la stessa storia in entrambi i posti aiuta.

Interrogare UsageStats (e il suo ritardo di scrittura)

Il ciclo principale è piccolo. Scorri gli eventi dall’ultima interrogazione e tieni l’ultimo pacchetto in MOVE_TO_FOREGROUND (è la stessa costante di ACTIVITY_RESUMED, rinominata nell’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
  }
}
// La prossima volta riscansiona gli ultimi 15 s: vedi sotto.
windowStart = end - REQUERY_OVERLAP_MS
handleForeground(currentForeground)

Un bell’effetto collaterale rispetto agli eventi di accessibilità: la tastiera e la tendina delle notifiche non sono activity, quindi non compaiono mai come finte uscite del tipo “l’utente ha lasciato l’app”.

Insidia: gli eventi arrivano in ritardo

La mia prima versione faceva avanzare la finestra in modo rigoroso: ogni interrogazione chiedeva [lastEnd, now]. Funzionava quasi sempre, poi su un telefono appena avviato la primissima apertura di un’app bloccata non veniva mai vista. UsageStats scrive gli eventi nel suo archivio con un ritardo, quindi un evento con timestamp 10:00:00.900 può comparire dopo un’interrogazione che copriva già le 10:00:01. Una finestra che avanza rigorosamente lo salta per sempre.

La soluzione: ogni interrogazione riscansiona una sovrapposizione all’indietro (io uso 15 secondi). Vedere lo stesso evento due volte non fa danni, perché in ogni caso vince l’ultima ripresa nella finestra. Un evento che arriva con più di 15 secondi di ritardo si perde comunque, ma non ne ho mai visto uno.

Insidia: la prima interrogazione deve guardare indietro a lungo

Quando il servizio parte (dopo un riavvio, o quando si accende lo schermo) non c’è nessuna “ultima app conosciuta”. Se l’utente è su TikTok da 20 minuti e la prima interrogazione guarda indietro solo di un minuto, non vede niente finché non cambia app. Quindi la prima interrogazione scansiona l’ultima ora. È sicuro perché anche il launcher è un’activity: quello che è ripreso per ultimo è davvero quello che c’è davanti.

Insidia: un accesso revocato sembra silenzio

Se l’utente disattiva l’accesso ai dati di utilizzo mentre il servizio gira, queryEvents non lancia eccezioni. Semplicemente non restituisce niente. La tua ultima lettura (“Instagram è davanti”) diventa vecchia e il saldo si consuma all’infinito. Quindi ogni interrogazione controlla prima l’app-op e ferma il cronometro se l’accesso non c’è più:

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
}

Schermo spento vuol dire stop

Quando lo schermo si spegne, il servizio mette in banca la sessione aperta e smette del tutto di interrogare. Nessuno usa un’app a schermo spento, e un ciclo a 1 Hz che gira solo mentre guardi il telefono costa poco. La prima interrogazione dopo la riaccensione dello schermo gestisce anche il cambio di giorno (azzeramento a mezzanotte, base giornaliera), così avviene prima che l’utente arrivi a un’app bloccata, anche se non apre mai HonorFit.

L’overlay di blocco

Senza accessibilità non c’è GLOBAL_ACTION_HOME, quindi non puoi buttare l’utente fuori da un’app. La copri invece: una finestra TYPE_APPLICATION_OVERLAY con un pulsante “guadagna altro tempo” e un pulsante “torna alla home”.

Tenere vivo il servizio

Questa è la parte che richiede più tempo per venire bene, e quasi non dipende dal tuo codice.

START_STICKY non basta

Le “app in sospensione” di Samsung, MIUI, OnePlus e compagnia uccidono i servizi in background e poi si mangiano il riavvio sticky. Un blocker è il caso peggiore: la gente non lo apre finché ha tempo a disposizione, quindi nessuno si accorge che il monitoraggio è sparito. HonorFit programma ogni 15 minuti un allarme ripetuto, inesatto e senza wakeup, che rilancia il servizio. Non sveglia mai il telefono, e gli allarmi rimandati mentre lo schermo era spento scattano poco dopo che si riaccende, che è proprio il momento in cui il monitoraggio conta.

Android 12+ non sempre ti lascia avviarlo

Avviare un servizio in primo piano dal background (un allarme, un riavvio sticky) lancia ForegroundServiceStartNotAllowedException, a meno che tu non abbia un’esenzione. Due contano qui: l’utente ha disattivato l’ottimizzazione della batteria per l’app, oppure l’app ha SYSTEM_ALERT_WINDOW (quest’ultima aiuta solo prima di Android 15). Quindi:

Riavvii e aggiornamenti

Un receiver per BOOT_COMPLETED e MY_PACKAGE_REPLACED fa ripartire il monitoraggio. Il secondo è facile da dimenticare: un aggiornamento automatico da Play uccide il tuo processo e non riavvia mai i suoi servizi, quindi senza di lui ogni aggiornamento spegne di nascosto il tuo blocker finché l’utente non apre l’app. Entrambi i broadcast sono esenti dalla restrizione sull’avvio in background, e un receiver exported="false" li riceve comunque.

Dettagli più piccoli

I compromessi

Questo approccio è più morbido dell’accessibilità, e voglio dirlo chiaramente:

Per un blocker che funziona sulla parola, è il compromesso giusto. HonorFit non cerca di essere inattaccabile. Chi è deciso a imbrogliare può semplicemente segnare ripetizioni che non ha fatto. Lo scopo è una pausa e un prezzo, non una prigione. In cambio hai una schermata di autorizzazione che dice “accesso ai dati di utilizzo” invece di “controllo completo”, una revisione su Play molto più semplice e un servizio che legge esattamente una cosa: il nome del pacchetto dell’app in primo piano.

Se sviluppi anche per iOS, lì è tutto un altro mondo. Ho scritto quello che la documentazione di Screen Time di Apple non ti dice.

HonorFit Blocker è un’app gratuita per iPhone e Android che blocca le app. Guadagna tempo schermo con flessioni, squat o qualsiasi esercizio tu scelga. Funziona sulla parola: niente fotocamera, niente account, nulla esce dal tuo telefono.

Continua a leggere