Membuat aplikasi blokir Android tanpa AccessibilityService

UsageStatsManager, foreground service, dan overlay, dari aplikasi yang sudah dirilis. Oleh Thomas Willheim · 9 November 2026

Aku membuat HonorFit, aplikasi blokir tempat kamu mendapatkan screen time dengan push up (sistem kejujuran, tanpa kamera). Versi Android pertamanya memblokir aplikasi dengan cara yang dipakai kebanyakan aplikasi blokir Android: lewat AccessibilityService. Bulan Agustus aku mencabut semua itu dan menggantinya dengan UsageStatsManager, sebuah foreground service, dan jendela overlay. Blokirnya jadi sedikit lebih longgar, onboarding-nya jadi jauh tidak semenakutkan dulu, dan review Play jadi jauh lebih sederhana.

Ini gambaran arsitekturnya dan hal-hal yang menjebakku di sepanjang jalan. Semua kodenya Kotlin di dalam modul Expo lokal, tapi tidak ada yang khusus React Native di sini.

Kenapa membuang Accessibility API

Accessibility service adalah alat paling kuat yang kamu dapat di Android. Kamu dikabari setiap kali ada perubahan jendela, saat itu juga, dan performGlobalAction(GLOBAL_ACTION_HOME) langsung menendang pengguna keluar dari aplikasi yang diblokir. Makanya banyak sekali aplikasi blokir memakainya. Harganya:

Fakta itu tersedia di tempat lain: event pemakaian yang ada di balik Digital Wellbeing.

Bentuk keseluruhannya

Tiga izin, semuanya diberikan pengguna di Pengaturan:

IzinUntuk apa
PACKAGE_USAGE_STATS (“Usage access”)Membaca UsageEvents untuk tahu aplikasi mana yang di depan. Tidak pernah diberikan saat runtime; kamu mengarahkan pengguna ke layar Pengaturan lalu mengecek app-op-nya.
SYSTEM_ALERT_WINDOW (“Display over other apps”)Menggambar layar kunci di atas aplikasi yang diblokir.
FOREGROUND_SERVICE_SPECIAL_USEService monitornya. Blokir aplikasi tidak cocok dengan tipe foreground service mana pun yang tersedia, jadi pakai specialUse dengan string subtipe yang menjelaskan alasannya.

Komponen-komponennya:

Entri manifest untuk service-nya:

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

Kamu juga perlu menjelaskan alasan specialUse di deklarasi foreground service di Play Console. Menceritakan hal yang sama di kedua tempat itu membantu.

Polling UsageStats (dan jeda penulisannya)

Loop utamanya kecil. Pindai event sejak polling terakhir dan simpan package MOVE_TO_FOREGROUND yang terakhir (konstantanya sama dengan ACTIVITY_RESUMED, hanya ganti nama di 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
  }
}
// Pindai ulang 15 detik terakhir di polling berikutnya: lihat di bawah.
windowStart = end - REQUERY_OVERLAP_MS
handleForeground(currentForeground)

Bonus dibanding event aksesibilitas: keyboard dan panel notifikasi bukan activity, jadi keduanya tidak pernah muncul sebagai “pengguna keluar dari aplikasi” palsu.

Jebakan: event datang terlambat

Versi pertamaku menggeser jendela waktunya selalu maju: tiap polling meng-query [lastEnd, now]. Biasanya jalan, tapi di HP yang baru dinyalakan, pembukaan aplikasi terblokir yang pertama tidak pernah terlihat. UsageStats menulis event ke penyimpanannya dengan jeda, jadi event bertanda waktu 10:00:00.900 bisa muncul setelah polling yang sudah mencakup 10:00:01. Jendela yang selalu maju akan melewatkannya selamanya.

Solusinya: setiap polling memindai ulang tumpang-tindih di belakangnya (aku pakai 15 detik). Melihat event yang sama dua kali tidak masalah, karena resume terakhir di jendela itu yang menang. Event yang datang lebih dari 15 detik terlambat tetap akan hilang, tapi aku belum pernah melihatnya.

Jebakan: polling pertama butuh lookback yang panjang

Saat service mulai (setelah restart, atau saat layar menyala) belum ada “aplikasi terakhir yang diketahui”. Kalau pengguna sudah 20 menit di TikTok dan polling pertama cuma melihat satu menit ke belakang, service tidak melihat apa-apa sampai pengguna pindah aplikasi. Jadi polling pertama memindai satu jam terakhir. Itu aman karena launcher juga sebuah activity: apa pun yang terakhir di-resume memang benar-benar yang ada di depan.

Jebakan: akses yang dicabut kelihatan seperti hening

Kalau pengguna mematikan Usage access saat service berjalan, queryEvents tidak melempar exception. Ia cuma mengembalikan hasil kosong. Bacaan terakhirmu (“Instagram ada di depan”) jadi basi dan saldonya terkuras terus. Jadi setiap polling mengecek app-op dulu dan menghentikan jam kalau aksesnya sudah hilang:

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
}

Layar mati berarti berhenti

Saat layar mati, service menyimpan sesi yang sedang terbuka dan berhenti polling sama sekali. Tidak ada yang memakai aplikasi dengan layar mati, dan loop 1 Hz yang hanya berjalan saat kamu melihat HP itu murah. Polling pertama setelah layar menyala lagi juga menangani pergantian hari (reset tengah malam, jatah harian), jadi itu terjadi sebelum pengguna sampai ke aplikasi yang diblokir, bahkan kalau mereka tidak pernah membuka HonorFit.

Overlay layar kunci

Tanpa aksesibilitas tidak ada GLOBAL_ACTION_HOME, jadi kamu tidak bisa melempar pengguna keluar dari aplikasi. Sebagai gantinya kamu menutupinya: jendela TYPE_APPLICATION_OVERLAY dengan tombol “dapatkan waktu lagi” dan tombol “kembali ke layar utama”.

Menjaga service tetap hidup

Bagian inilah yang paling lama untuk dibuat benar, dan sebagian besar bukan soal kodemu.

START_STICKY saja tidak cukup

“Aplikasi tidur” di Samsung, MIUI, OnePlus, dan kawan-kawannya mematikan service latar belakang lalu menelan restart sticky-nya. Aplikasi blokir adalah kasus terburuk untuk ini: orang tidak membukanya selama mereka masih punya waktu, jadi tidak ada yang sadar monitornya sudah hilang. HonorFit memasang alarm berulang yang inexact dan non-wakeup setiap 15 menit untuk menyalakan ulang service-nya. Alarm ini tidak pernah membangunkan HP, dan alarm yang tertunda selama layar mati akan berbunyi tidak lama setelah layar menyala lagi, tepat saat monitornya dibutuhkan.

Android 12+ tidak selalu mengizinkanmu memulainya

Memulai foreground service dari latar belakang (dari alarm, dari restart sticky) melempar ForegroundServiceStartNotAllowedException kecuali kamu punya pengecualian. Dua yang relevan di sini: pengguna sudah mematikan optimasi baterai untuk aplikasinya, atau aplikasinya punya SYSTEM_ALERT_WINDOW (yang ini cuma membantu sebelum Android 15). Jadi:

Reboot dan update

Sebuah receiver untuk BOOT_COMPLETED dan MY_PACKAGE_REPLACED memulai ulang monitornya. Yang kedua gampang terlupa: update otomatis dari Play mematikan prosesmu dan tidak pernah memulai ulang service-nya, jadi tanpa itu setiap update diam-diam mematikan aplikasi blokirmu sampai pengguna membuka aplikasinya. Kedua broadcast itu dikecualikan dari pembatasan start dari latar belakang, dan receiver exported="false" tetap menerimanya.

Hal-hal kecil lainnya

Kompromi yang diambil

Pendekatan ini lebih longgar dibanding aksesibilitas, dan aku mau terus terang soal itu:

Untuk aplikasi blokir dengan sistem kejujuran, itu kompromi yang tepat. HonorFit tidak berusaha jadi tidak bisa dibobol. Orang yang memang niat curang bisa saja mencatat rep yang tidak mereka lakukan. Intinya adalah jeda dan harga, bukan penjara. Sebagai gantinya kamu mendapat layar izin yang bilang “akses penggunaan”, bukan “kontrol penuh”, review Play yang jauh lebih sederhana, dan service yang membaca persis satu hal: nama package aplikasi yang ada di depan.

Kalau kamu juga membangun untuk iOS, di sana dunianya benar-benar lain. Aku sudah menulis soal yang tidak diceritakan dokumentasi Screen Time Apple.

HonorFit Blocker adalah aplikasi blokir gratis untuk iPhone dan Android. Dapatkan screen time dengan push up, squat, atau latihan apa pun yang kamu pilih. Sistem kejujuran, tanpa kamera, tanpa akun, tidak ada yang keluar dari HP-mu.

Baca juga