The Allow button that did nothing
- homelab
- android
A family member's Pixel runs GrapheneOS. One day the Messages app stopped showing contact names — every thread was just a phone number. It had worked for months. Nothing was uninstalled, nothing obviously changed.
The fix took three taps. Finding the three taps took a debugging session over adb, because the obvious fix — the Allow button on the Contacts permission screen — visually worked and did absolutely nothing.
The symptom
With USB debugging on, the first thing worth checking is what the OS thinks the permission state is:
adb shell dumpsys package com.android.messaging | grep READ_CONTACTSandroid.permission.READ_CONTACTS: granted=false, flags=[ USER_SET|GRANTED_BY_DEFAULT ... ]Denied. Fine — that explains the numbers. The contacts database itself was healthy (615 rows, readable by other apps), there was only one user profile, and logcat showed no crashes. The app wasn't failing to look names up; it was politely not allowed to.
GRANTED_BY_DEFAULT says the system SMS app got this permission automatically once. USER_SET says someone — or something — later turned it off. So: re-grant it.
adb shell pm grant com.android.messaging android.permission.READ_CONTACTSExit code 0. No error. And granted=false, unchanged.
The part where the OS says no quietly
A grant command that succeeds and does nothing is much stranger than one that fails. The answer was sitting in logcat:
PermissionManager: refusing to grant android.permission.READ_CONTACTS
to com.android.messaging: Contact Scopes is enabledContact Scopes is a GrapheneOS feature — a middle ground between Allow and Deny. The app is told "you have Contacts access," but it only sees the specific contacts you've hand-picked. Pick none and the app sees an empty address book, forever, without a single error anywhere in its UI.
And while Contact Scopes is on, GrapheneOS refuses every other path to the real permission. pm grant over adb: refused. Tapping Allow in Settings: the radio button selects, and the grant is silently rejected underneath. The permission screen happily lies to you. The only tell is one line of flavor text — the deny option reads "Don't allow (+ Contact Scopes)" — and one line in logcat that nothing ever surfaces.
Someone had hit the Contact Scopes button on a permission prompt at some point, most likely after an OS update re-asked. Easy to do; the button sits right next to Allow.
The three taps
Settings → Apps → Messaging → Permissions → Contacts → Contact Scopes → Turn off. Then back, and now tap Allow — this time it actually takes:
android.permission.READ_CONTACTS: granted=trueRestart Messages, and every thread has its name back.
Two side notes for anyone poking at this layer:
- Permission-controller screens are
FLAG_SECURE, soadb screencapreturns a black rectangle.adb shell uiautomator dumpstill works — you get the full view hierarchy as XML, which is arguably better than pixels. - Order matters. Turn scopes off first, then Allow. In the other order the grant is refused again and the UI still looks correct.
The sequel: leaving for stock, and what "backup" really means
The episode shook confidence enough that the owner asked to move the phone back to stock Pixel Android. Which is supported and reversible — but flashing stock means unlocking the bootloader, and unlocking wipes the device. And this phone's 613 contacts existed only on the device. No sync, no cloud copy. Same for ten thousand texts.
The wipe forces you to answer, honestly, per data type: where does this actually live, and what file format survives the crossing? For the curious, the whole export ran over adb from a Mac, read-only:
- Contacts — exported twice, independently: once from the Contacts app's own export (which carries the 29 contact photos), and once rebuilt into vCards straight from the provider with
adb shell content queryover the raw data table. Two exporters, two codebases, same count at the end: 613 = 613. For irreplaceable data, two independent paths beat one verified path. - SMS/MMS + call log — dumped from
content://sms,content://mms(plus per-message address tables and attachment blobs), then converted into the XML format that SMS Backup & Restore reads, with attachments base64-embedded. 10,763 texts, 2,867 MMS, 793 calls. - The long tail — a local-only calendar hiding 54 real events among 250 auto-generated holidays (exported to
.ics), photos verified by file count against the phone, WhatsApp's localmsgstore.db.crypt14, a notes app's daily auto-backups sitting quietly inAndroid/data.
One performance note that generalizes: pulling 2,867 MMS address rows as 2,867 separate adb shell content query invocations crawls — every call pays the adb handshake. Pushing the ID list to the phone and running one shell loop on-device, output to a file, then pulling the file, turned a half-hour job into about two minutes. Six of those loops in parallel finished the rest. The bottleneck was never the data; it was the per-call overhead.
Everything landed in one directory with a manifest and SHA-256 sums — 1.3 GB, 1,080 files — before the phone gets anywhere near fastboot flashing unlock.
What I'm keeping from this
Security features that lie to apps will, occasionally, also confuse humans. Contact Scopes is a good feature — it exists precisely so you can give a nosy app nothing while it believes it got everything. But the same misdirection that fools the app fooled the permission screen's own Allow button, and the only honest witness was logcat.
When a UI toggle doesn't stick, stop toggling. Ask the system what it actually did:
adb logcat -d | grep -i "refusing"The OS usually tells you why. Just never where you're looking.