Como criar um bloqueador de apps para Android sem AccessibilityService

UsageStatsManager, um serviço em primeiro plano e uma sobreposição, direto de um app já publicado. Por Thomas Willheim · 9 de novembro de 2026

Eu faço o HonorFit, um bloqueador de apps em que você ganha tempo de tela fazendo flexões (sistema de honra, sem câmera). A primeira versão para Android bloqueava apps do jeito que a maioria dos bloqueadores de Android faz: com um AccessibilityService. Em agosto eu arranquei isso fora e troquei por UsageStatsManager, um serviço em primeiro plano e uma janela de sobreposição. O bloqueio ficou um pouco mais brando, o onboarding ficou bem menos assustador e a revisão do Play ficou bem mais simples.

É assim que essa arquitetura funciona, e estas são as coisas que me pegaram no caminho. Todo o código é Kotlin dentro de um módulo local do Expo, mas nada aqui é específico de React Native.

Por que abandonar a API de acessibilidade

Um serviço de acessibilidade é a ferramenta mais poderosa que você tem no Android. Você fica sabendo de cada mudança de janela no instante em que ela acontece, e performGlobalAction(GLOBAL_ACTION_HOME) tira o usuário de um app bloqueado na hora. É por isso que tantos bloqueadores usam. Os custos:

Essa informação está disponível em outro lugar: nos eventos de uso que alimentam o Bem-estar digital.

O formato geral

Três permissões, todas concedidas pelo usuário nas Configurações:

PermissãoPara que serve
PACKAGE_USAGE_STATS (“Acesso ao uso”)Ler UsageEvents para saber qual app está na frente. Nunca é concedida em tempo de execução; você manda o usuário para a tela de Configurações e verifica o app-op.
SYSTEM_ALERT_WINDOW (“Sobrepor a outros apps”)Desenhar a tela de bloqueio por cima de um app bloqueado.
FOREGROUND_SERVICE_SPECIAL_USEO serviço de monitoramento. Bloquear apps não se encaixa em nenhum dos tipos de serviço em primeiro plano da lista, então é specialUse com uma string de subtipo explicando o porquê.

As peças:

A entrada do serviço no manifesto:

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

Você também precisa justificar o specialUse na declaração de serviços em primeiro plano do Play Console. Contar a mesma história nos dois lugares ajuda.

Consultando o UsageStats (e o atraso de gravação)

O loop principal é pequeno. Percorra os eventos desde a última consulta e guarde o último pacote com MOVE_TO_FOREGROUND (é a mesma constante que ACTIVITY_RESUMED, renomeada na 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
  }
}
// Na próxima vez, varrer de novo os últimos 15 s: veja abaixo.
windowStart = end - REQUERY_OVERLAP_MS
handleForeground(currentForeground)

Uma vantagem em relação aos eventos de acessibilidade: o teclado e a aba de notificações não são activities, então nunca aparecem como saídas falsas do tipo “o usuário saiu do app”.

Pegadinha: os eventos chegam atrasados

Minha primeira versão movia a janela estritamente para a frente: cada consulta pedia [lastEnd, now]. Funcionava quase sempre, até que num celular recém-ligado a primeira abertura de um app bloqueado nunca era detectada. O UsageStats grava os eventos no armazenamento com atraso, então um evento com horário 10:00:00.900 pode aparecer depois de uma consulta que já cobriu 10:00:01. Uma janela que só avança pula esse evento para sempre.

A solução: toda consulta varre de novo uma sobreposição para trás (eu uso 15 segundos). Ver o mesmo evento duas vezes não faz mal, já que o último resume da janela ganha de qualquer jeito. Um evento que chegue com mais de 15 segundos de atraso ainda se perde, mas eu nunca vi um.

Pegadinha: a primeira consulta precisa olhar bem para trás

Quando o serviço começa (depois de um reinício, ou quando a tela liga) não existe um “último app conhecido”. Se o usuário está no TikTok há 20 minutos e a primeira consulta só olha um minuto para trás, ela não vê nada até ele trocar de app. Então a primeira consulta varre a última hora. Isso é seguro porque o launcher também é uma activity: o que fez resume por último é mesmo o que está na frente.

Pegadinha: acesso revogado parece silêncio

Se o usuário desliga o acesso ao uso enquanto o serviço está rodando, queryEvents não lança exceção. Ele só não retorna nada. Sua última leitura (“o Instagram está na frente”) fica velha e o saldo vai sendo consumido para sempre. Então toda consulta verifica o app-op primeiro e para o relógio se ele sumiu:

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
}

Tela desligada quer dizer parar

Quando a tela desliga, o serviço contabiliza a sessão aberta e para de consultar de vez. Ninguém está usando um app com a tela desligada, e um loop de 1 Hz que só roda enquanto você está olhando o celular sai barato. A primeira consulta depois que a tela liga também cuida da virada do dia (o reset da meia-noite, a base diária), então isso acontece antes de o usuário chegar a um app bloqueado, mesmo que ele nunca abra o HonorFit.

A sobreposição de bloqueio

Sem acessibilidade não existe GLOBAL_ACTION_HOME, então você não consegue expulsar o usuário de um app. Em vez disso, você cobre o app: uma janela TYPE_APPLICATION_OVERLAY com um botão de “ganhar mais tempo” e outro de “ir para a tela inicial”.

Mantendo o serviço vivo

Esta é a parte que mais demora para acertar, e quase nada depende do seu código.

START_STICKY não basta

Os “apps em suspensão” da Samsung, a MIUI, a OnePlus e companhia matam serviços em segundo plano e depois engolem o reinício sticky. Um bloqueador é o pior caso para isso: as pessoas não abrem o app enquanto ainda têm tempo, então ninguém percebe que o monitor sumiu. O HonorFit arma um alarme repetitivo, inexato e sem wakeup a cada 15 minutos, que reativa o serviço. Ele nunca acorda o celular, e alarmes adiados enquanto a tela estava desligada disparam logo depois que ela liga, que é exatamente quando o monitor importa.

O Android 12+ nem sempre deixa você iniciá-lo

Iniciar um serviço em primeiro plano a partir do segundo plano (um alarme, um reinício sticky) lança ForegroundServiceStartNotAllowedException, a não ser que você tenha uma isenção. Duas que importam aqui: o usuário desativou a otimização de bateria para o app, ou o app tem SYSTEM_ALERT_WINDOW (essa só ajuda antes do Android 15). Então:

Reinicializações e atualizações

Um receiver para BOOT_COMPLETED e MY_PACKAGE_REPLACED reinicia o monitoramento. O segundo é fácil de esquecer: uma atualização automática do Play mata o seu processo e nunca reinicia os serviços dele, então, sem isso, cada atualização desliga o seu bloqueador em silêncio até o usuário abrir o app. Os dois broadcasts são isentos da restrição de início em segundo plano, e um receiver com exported="false" continua recebendo os dois.

Coisas menores

As concessões

Essa abordagem é mais branda que a acessibilidade, e quero ser franco sobre isso:

Para um bloqueador no sistema de honra, essa é a troca certa. O HonorFit não tenta ser inquebrável. Quem estiver decidido a trapacear pode simplesmente registrar reps que não fez. A ideia é uma pausa e um preço, não uma prisão. Em troca, você ganha uma tela de permissão que diz “acesso ao uso” em vez de “controle total”, uma revisão do Play muito mais simples e um serviço que lê exatamente uma coisa: o nome do pacote do app que está na frente.

Se você também está desenvolvendo para iOS, lá é um mundo completamente diferente. Escrevi sobre o que a documentação do Tempo de Uso da Apple não conta.

HonorFit Blocker é um bloqueador de apps grátis para iPhone e Android. Ganhe tempo de tela com flexões, agachamentos ou qualquer exercício que você escolher. Sistema de honra, sem câmera, sem conta, nada sai do seu celular.

Continue lendo