Cómo hacer un bloqueador de apps para Android sin AccessibilityService

UsageStatsManager, un servicio en primer plano y una superposición, desde una app ya publicada. Por Thomas Willheim · 9 de noviembre de 2026

Hago HonorFit, un bloqueador de apps en el que te ganas el tiempo de pantalla haciendo flexiones (sistema de honor, sin cámara). La primera versión para Android bloqueaba apps como lo hacen la mayoría de los bloqueadores de Android: con un AccessibilityService. En agosto lo arranqué de raíz y lo sustituí por UsageStatsManager, un servicio en primer plano y una ventana superpuesta. El bloqueo quedó un poco más blando, el onboarding asusta mucho menos y la revisión de Play se volvió mucho más sencilla.

Esta es la arquitectura y las cosas que me mordieron por el camino. Todo el código es Kotlin dentro de un módulo local de Expo, pero nada de esto es específico de React Native.

Por qué dejar la API de accesibilidad

Un servicio de accesibilidad es la herramienta más potente que tienes en Android. Te enteras de cada cambio de ventana en el instante en que ocurre, y performGlobalAction(GLOBAL_ACTION_HOME) saca al usuario de una app bloqueada en el acto. Por eso tantos bloqueadores lo usan. Lo que cuesta:

Ese dato se puede conseguir por otro lado: los eventos de uso que hay detrás de Bienestar digital.

La forma general

Tres permisos, todos concedidos por el usuario en Ajustes:

PermisoPara qué sirve
PACKAGE_USAGE_STATS («Acceso a datos de uso»)Leer UsageEvents para saber qué app está delante. Nunca se concede en tiempo de ejecución; mandas al usuario a la pantalla de Ajustes y compruebas el app-op.
SYSTEM_ALERT_WINDOW («Mostrar sobre otras apps»)Dibujar la pantalla de bloqueo encima de una app bloqueada.
FOREGROUND_SERVICE_SPECIAL_USEEl servicio de monitorización. Bloquear apps no encaja en ninguno de los tipos de servicio en primer plano de la lista, así que es specialUse con una cadena de subtipo que explica por qué.

Las piezas:

La entrada del servicio en el manifiesto:

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

También tienes que justificar specialUse en la declaración de servicios en primer plano de Play Console. Contar la misma historia en los dos sitios ayuda.

Consultar UsageStats (y su retraso de escritura)

El bucle principal es pequeño. Recorre los eventos desde la última consulta y quédate con el último paquete con MOVE_TO_FOREGROUND (es la misma constante que ACTIVITY_RESUMED, renombrada en la 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 próxima vez, volver a recorrer los últimos 15 s: ver abajo.
windowStart = end - REQUERY_OVERLAP_MS
handleForeground(currentForeground)

Una ventaja frente a los eventos de accesibilidad: el teclado y el panel de notificaciones no son actividades, así que nunca aparecen como falsas salidas del tipo «el usuario dejó la app».

Trampa: los eventos llegan tarde

Mi primera versión movía la ventana estrictamente hacia delante: cada consulta pedía [lastEnd, now]. Casi siempre funcionaba, pero en un teléfono recién arrancado la primera apertura de una app bloqueada nunca se detectaba. UsageStats escribe los eventos en su almacén con retraso, así que un evento con marca de las 10:00:00.900 puede aparecer después de una consulta que ya cubría las 10:00:01. Una ventana que solo avanza se lo salta para siempre.

La solución: cada consulta vuelve a recorrer un solapamiento hacia atrás (yo uso 15 segundos). Ver el mismo evento dos veces no hace daño, porque en cualquier caso gana el último resume de la ventana. Un evento que llegue con más de 15 segundos de retraso se sigue perdiendo, pero no he visto ninguno.

Trampa: la primera consulta necesita mirar mucho hacia atrás

Cuando el servicio arranca (tras un reinicio, o cuando se enciende la pantalla) no hay una «última app conocida». Si el usuario lleva 20 minutos en TikTok y la primera consulta solo mira un minuto atrás, no ve nada hasta que cambie de app. Así que la primera consulta recorre la última hora. Es seguro porque el launcher también es una actividad: lo último que hizo resume es realmente lo que está delante.

Trampa: un acceso revocado parece silencio

Si el usuario desactiva el acceso a datos de uso mientras el servicio está en marcha, queryEvents no lanza ninguna excepción. Simplemente no devuelve nada. Tu última lectura («Instagram está delante») se queda obsoleta y el saldo se gasta para siempre. Así que cada consulta comprueba primero el app-op y para el reloj si ya no está:

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
}

Pantalla apagada significa parar

Cuando se apaga la pantalla, el servicio contabiliza la sesión abierta y deja de consultar por completo. Nadie está usando una app con la pantalla apagada, y un bucle de 1 Hz que solo corre mientras miras el teléfono sale barato. La primera consulta después de encender la pantalla también se encarga del cambio de día (el reinicio de medianoche, la base diaria), así que ocurre antes de que el usuario llegue a una app bloqueada, aunque nunca abra HonorFit.

La superposición de bloqueo

Sin accesibilidad no hay GLOBAL_ACTION_HOME, así que no puedes echar al usuario de una app. En su lugar la tapas: una ventana TYPE_APPLICATION_OVERLAY con un botón de «ganar más tiempo» y otro de «ir al inicio».

Mantener vivo el servicio

Esta es la parte que más cuesta dejar bien, y casi nada depende de tu código.

START_STICKY no basta

Las «apps en suspensión» de Samsung, MIUI, OnePlus y compañía matan los servicios en segundo plano y luego se tragan el reinicio sticky. Un bloqueador es el peor caso posible: la gente no lo abre mientras le queda tiempo, así que nadie nota que el monitor ha desaparecido. HonorFit programa una alarma repetitiva, inexacta y que no despierta el teléfono, cada 15 minutos, que vuelve a levantar el servicio. Nunca despierta el teléfono, y las alarmas aplazadas mientras la pantalla estaba apagada se disparan poco después de que se encienda, que es justo cuando el monitor importa.

Android 12+ no siempre te deja iniciarlo

Iniciar un servicio en primer plano desde segundo plano (una alarma, un reinicio sticky) lanza ForegroundServiceStartNotAllowedException salvo que tengas una exención. Dos que importan aquí: que el usuario haya desactivado la optimización de batería para la app, o que la app tenga SYSTEM_ALERT_WINDOW (esta solo sirve antes de Android 15). Así que:

Reinicios y actualizaciones

Un receiver para BOOT_COMPLETED y MY_PACKAGE_REPLACED vuelve a iniciar la monitorización. El segundo es fácil de olvidar: una actualización automática de Play mata tu proceso y nunca reinicia sus servicios, así que sin él cada actualización apaga en silencio tu bloqueador hasta que el usuario abre la app. Los dos broadcasts están exentos de la restricción de inicio en segundo plano, y un receiver con exported="false" los sigue recibiendo.

Cosas más pequeñas

Las concesiones

Este enfoque es más blando que la accesibilidad, y quiero ser claro con eso:

Para un bloqueador con sistema de honor, es la concesión correcta. HonorFit no intenta ser irrompible. Quien esté decidido a hacer trampa puede simplemente registrar reps que no hizo. La idea es una pausa y un precio, no una cárcel. A cambio, tienes una pantalla de permisos que dice «acceso a datos de uso» en lugar de «control total», una revisión de Play mucho más sencilla y un servicio que lee exactamente una cosa: el nombre del paquete de la app que está delante.

Si también estás desarrollando para iOS, allí es un mundo completamente distinto. Escribí sobre lo que la documentación de Screen Time de Apple no te cuenta.

HonorFit Blocker es un bloqueador de apps gratis para iPhone y Android. Gana tiempo de pantalla con flexiones, sentadillas o cualquier ejercicio que elijas. Sistema de honor, sin cámara, sin cuenta, nada sale de tu teléfono.

Sigue leyendo