Guide · 8 min read

Android app blocking: 6 things we measured that the docs don't say

Last updated:

We build a Device Owner app that makes apps unreachable on Android. These are findings from real devices — each one cost us a bug, and none of them is in the documentation. If you're building something similar, or just want to know what "blocking an app" actually does at the OS level, this is the honest version.

1. Hiding an app and suspending it are not the same thing

Android gives a Device Owner two different tools that look interchangeable:

The difference matters more than it looks. We hid apps that had a pending session and then couldn't show them in our own launcher grid — because our grid asks getApplicationInfo for the icon, the call threw, and the row was silently dropped. The user saw an app that had simply vanished, with no way to tap it and no explanation.

If your UI needs to show a blocked app — to explain why it's blocked, or to offer a way back — suspending is the right primitive. Hiding removes it from your own reach too.

2. Hiding Play Store breaks Play Integrity for other apps

This one surprised us and it's the most expensive finding here.

Hiding com.android.vending looks like the obvious way to stop new installs. What actually happens is that the package becomes disabled, and the Play Integrity API stops working. Apps that call it — banking apps, ChatGPT, fast-food loyalty apps — fail at launch with a message along the lines of"Something went wrong — check that Google Play is enabled."

These are apps the user explicitly allowed. From their side, a phone that was supposed to block distractions had broken their bank app.

The conclusion we'd hand anyone building this: Play Store has to stay enabled. Whatever you do about install access, disabling that package is not the lever — it takes the user's working apps down with it.

3. Hiding a package during install makes Play retry forever

A related trap. If you watch forACTION_PACKAGE_ADDED and immediately hide any package that isn't allowed, Play Store sees the install asfailed — because from its point of view the package it just wrote is gone.

The visible symptom is a download that reaches 100% and then restarts, over and over, without ever completing. Users read this as a broken phone, not as a policy.

The way out is a question of ordering rather than of policy: Play has to see the package it just wrote before anything takes it away. Worth knowing mostly because the loop is silent — nothing errors, so it reads as a bad connection and you spend days looking at the network.

4. DISALLOW_INSTALL_APPS also blocks your own updates

UserManager.DISALLOW_INSTALL_APPS is the obvious way to stop sideloading. We measured that it also breaksadb install -r — including the channel we use to update our own app.

If your setup or update path runs over ADB, adding this restriction will lock you out of your own device. We removed it and cut install access at other layers instead.

5. Apps can refuse to open their own screens

We wanted to send users straight to WhatsApp's chat-backup screen before a reinstall, so nobody loses their history. It doesn't work, and it's worth knowing why — the failure is not what you'd guess.

Three candidate activities, measured from a shell:

ActivityResult
…settings.chat.backup.SettingsChatHistoryBackupError type 3: does not exist
…settings.SettingsChatError type 3: does not exist
…backup.google.SettingsGoogleDriveexists — SecurityException: not exported from uid 10418

So the screen is there and correctly named. It simply isn't exported, and a shell (uid 2000) cannot start a non-exported activity. No amount of guessing at names fixes that. bmgr backupnow doesn't help either — WhatsApp isn't enrolled in Android Backup, and returnsTransport error.

The lesson generalises: before you ship a "we'll open that screen for you" button, check whether the target activity is exported. If it isn't, the honest thing is to write the path out in words.

6. Two measurement traps that produce nonsense numbers

If you show users their screen time, both of these will bite you.

queryAndAggregateUsageStats returns buckets that aren't clipped to your range. Each bucket'stotalTimeInForeground covers the whole bucket, not the window you asked for. Sum enough apps and a single day can exceed 24 hours. We shipped a screen that told someone they'd used their phone for 440 hours this week. Event-based measurement (queryEvents, pairing foreground/background transitions) gives numbers that survive arithmetic.

Empty is not zero. The system keeps usage events for a limited window (typically around a week). Query an older day and you may get nothing back — which means "the data is gone", not "the phone wasn't used". Recording that day as 0 minutes tells the user they had a perfect day they never had, and inflates any "time saved" figure built on top of it. Skip unmeasurable days; don't zero them.

Why we're publishing this

Partly because we'd have paid for it. Every item above cost us a bug report from a real user, and none of it is in the Android docs.

Partly because it's the honest version of what our product does. The marketing sentence is "apps become unreachable." The engineering reality is a set of trade-offs — keeping Play Store alive, ordering things so an install is never seen as failed, refusing a restriction that would lock us out. If you're deciding whether to trust software that manages your phone, you should be able to see that reasoning.

StillPhone is the app these findings came from. It makes chosen apps unreachable on Android — set up from a computer, and not undoable from the phone.

No card required · Setup from a computer · Android

← All guides