The device that wasn't mine
Chapter 1: The inheritance
It started with a routine ask: a family member had been paying off a financed phone, and I was the most tech-literate person around - a low bar, but a bar - so I went along to the shop for the final payment - mostly just to make sure everything was in order before it became theirs to use freely.
Before that final payment went through, the phone was already showing signs of something heavier than a normal lockscreen:
- A device admin app that kept coming back. Delete it, it reappears. Delete it offline, no network, no sync - it still reappears.
- Developer options greyed out. Not hidden - visibly present, visibly unclickable.
- Factory reset blocked entirely, the option simply not available.
- A single line on the lockscreen, easy to miss if you're not looking for it: "This device is managed by your organisation."
At the shop, the final payment was processed, and something happened on the financier's end almost immediately - some backend release, triggered the moment the account showed paid in full. Within minutes, the admin app vanished from the launcher on its own. No uninstall, no prompt, it was just gone.
Testing the rest confirmed the release was real: developer options now toggled freely, and factory reset was available in Settings for the first time. Whatever had been holding those two locked shut had let go.
One thing hadn't changed. The lockscreen still read "This device is managed by your organisation," even after a reboot.
That mismatch - almost everything released, one label stubbornly refusing to leave - is where the actual investigating started. With developer options finally unlocked, I enabled USB debugging and plugged the phone into a laptop, expecting a quick ADB cleanup. What I found instead was a reminder that "the admin app is gone" and "the thing controlling this phone is gone" are not the same claim, and Android has a whole hierarchy of control most people never see the difference between.
A few notes before we go further, because the next chapter gets technical fast: what I was looking at wasn't the lightweight "Device Admin" API most people picture when they hear "my phone has a work profile." Android actually has a hierarchy here, and almost nobody outside of IT departments knows it exists:
- Device Admin - the oldest, weakest tier. An app requests a handful of policies (lock screen, wipe data, enforce password rules). You can usually revoke this yourself in Settings in about four taps.
- Profile Owner - scoped to a work profile, used for BYOD setups where personal and work apps are kept separate.
- Device Owner - the top tier. Full device control, provisioned before the first account is ever set up, designed specifically so it can't be casually removed by the end user. This is what fleet-managed corporate phones - and, as I was about to confirm, financier-locked retail phones - actually use.
A Device Admin app, you can usually out-fight. A Device Owner doesn't negotiate. And the symptom that was still staring back at me from the lockscreen - a label the backend release hadn't touched - was about to turn out to be the exact fingerprint of a Device Owner, not a Device Admin.
I didn't know that yet. I found out by asking the phone directly.
Chapter 2: Asking the phone directly
The question wasn't "why is this phone locked" anymore - the backend release had answered most of that already. The question was narrower and stranger: why was one specific label still on the lockscreen when everything else had let go?
The obvious first move was adb shell dpm list-owners - ask Android's own Device Policy Manager who, if anyone, currently owns this device.
Exception occurred while executing 'list-owners':
java.lang.SecurityException: Calling identity is not authorized
Not quite a dead end, but a reminder that Google has spent the last few Android versions deliberately hardening exactly this kind of query against plain ADB shell. Recent security patches lock dpm list-owners down hard enough that even a correctly-connected, debugging-enabled phone won't answer it directly anymore - a defensive move, presumably aimed at exactly the kind of poking around I was doing.
The broader dumpsys interface, it turned out, was more talkative:
adb shell dumpsys device_policy
Buried in several hundred lines of policy state was the line that mattered:
Device Owner:
admin=ComponentInfo{com.customer.mdm/com.customer.mdm.main.service.CustomerDeviceAdminReceiver}
package=com.customer.mdm
isOrganizationOwnedDevice=true
There it was. Not a stray admin app, not a leftover permission - a fully active, organization-owned Device Owner binding, still sitting on a phone whose financier had, by every outward sign, just released it. isOrganizationOwnedDevice=true is the same flag used in legitimate corporate fleet deployments; here it had been used to keep one retail device on a leash, and the backend release apparently hadn't gotten around to clearing it.
The dump also explained the app that had vanished from the launcher on its own. Scrolling further down:
sourceDir=/data/app/~~4lpXXnFbpv2d3ZN6CPwCeQ==/com.customer.mdm-0EsZzqcDIvqzZcAj2qfzpw==/base.apk
The APK was still sitting right there on disk. Whatever had disappeared from the launcher was never the actual Device Owner package - just its visible UI shell, or a companion component the backend release had been able to reach. The thing actually holding the leash had never left.
And buried in the same dump was a long list of applicationHidden policy entries - WhatsApp, Instagram, banking apps, UPI apps, all individually flagged under the Device Owner's control, each resolved to BooleanPolicyValue { mValue= false }. Not currently hidden, but capable of being hidden, instantly, by whoever still held com.customer.mdm.
[!NOTE] That's the real shape of a Device Owner: not a single restriction, but standing authority over nearly everything the phone can do - authority a vanished launcher icon had done nothing to actually revoke.
One detail mattered more than the rest, for what came next:
testOnlyAdmin=false
false here is the whole ballgame. A test-only admin is something Android trusts shell commands to remove directly - convenient, and exactly the loophole I'd been hoping for. A non-test admin is the real thing, hardened specifically against the command I was about to try.
I tried it anyway, because hope is free and dpm commands are cheap:
adb shell dpm remove-active-admin --user 0 com.customer.mdm/com.customer.mdm.main.service.CustomerDeviceAdminReceiver
SecurityException: Calling identity is not authorized
Same wall, same reason. Google's own documentation is blunt about this once you go looking: shell-level removal of a genuine, non-test Device Owner is blocked by design, specifically because this exact workaround used to work and got closed.
So the investigation had an answer, just not the one I'd been angling for. This wasn't a bug, or an incomplete backend release I could nudge the rest of the way with pm uninstall attempts. It was a deliberately hardened lock, still technically in force, with only its visible symptoms removed.
The only door left open was the one the system itself was built to open eventually: a factory reset, which wipes /data - and with it, every trace of the Device Owner binding, cleanly, by design rather than by workaround.
Chapter 3: Clearing the lock, then really looking around
The reset itself was anticlimactic in the best possible way. Wipe /data, let the phone boot fresh, and the entire Device Owner binding - the admin that had quietly outlived its own backend release, the organization-owned flag, the lockscreen label - was gone. No leftover trace, no ghost entry in dumpsys device_policy. Whatever had been holding the leash lived entirely in state a factory reset is specifically designed to erase, which is exactly why it was the only real way out in Chapter 2.
With the lock gone, the phone was finally just... a phone. Which meant it was time to actually look at what else was running on it.
This is the part of the story that isn't really about MDM at all - it's about what ships on a retail Android phone by default, once you go looking properly. HyperOS (Xiaomi's current Android skin) carries an enormous amount alongside the OS itself:
- duplicate apps for nearly everything
- Xiaomi's own telemetry and analytics layer
- a second contacts-and-dialer app running in parallel with Google's
- an app store (GetApps) gated behind a Mi account login
- a long tail of services with names like
com.miui.misightserviceandcom.xiaomi.joyosethat exist purely to phone home diagnostics and usage data
None of it is malicious in the way the Device Owner lock was. It's just a lot - enough that pm list packages -s on a stock HyperOS install returns well over 250 system packages before you've installed a single app of your own.
I went at it with a simple three-tier rule, the same one I'd used on a previous debloat project:
- Tier 1 - safe to fully remove. Standalone apps with no other component depending on them: Facebook's bundled trio (
appmanager,services,system), Xiaomi's analytics and bug-report services, GetApps, Mi Pay, Mi Cloud's sync stack. Removed withpm uninstall --user 0, which - critically - only removes the package for your user profile. The APK stays on the system partition, untouched, which makes the whole process trivially reversible. - Tier 2 - disable, don't remove. Packages that resist uninstall, or where the blast radius of being wrong is annoying rather than catastrophic.
pm disable-user --user 0instead - same practical effect, but quieter to reverse. - Tier 3 - do not touch. Telephony core, the network stack, anything with
.overlay.in the package name (these are Runtime Resource Overlays - remove one and you risk breaking whatever visual element or dialog it was theming), and anything self-evidently load-bearing likecom.android.systemuiitself.
Most of the removals were uneventful. A few weren't, and those were the ones worth remembering:
The bundled dialer-and-contacts trap. com.android.contacts turned out to be a single APK serving double duty as both the contacts app and the phone dialer. One pm uninstall removed both at once - which was fine, since I'd already set Google's Phone and Contacts as defaults first, but would have been a bad surprise otherwise. "I broke your calling app" is not a sentence you ever want to say out loud. Calling was tested working immediately after. The bar for success had never been lower, and we cleared it.
The vendor-login wall. Trying to sideload an APK via adb install returned a flat INSTALL_FAILED_USER_RESTRICTED: Install canceled by user - not because anything was wrong with the APK, but because HyperOS gates "Install via USB" behind an active Mi account login, a restriction that's existed since MIUI 9 and has nothing to do with Android's own sideloading rules.
The workaround was simpler than fighting it: push the APK to /sdcard/Download and install it through the file manager instead, which uses a different, ungated permission path entirely.
The quiet dependency. Removing com.google.android.googlequicksearchbox - the app behind Google Assistant, voice search, and the "At a Glance" home screen widget - was the one change I expected to cause visible damage. It didn't. A reboot, a check of the home screen widgets, and everything rendered cleanly without it. Not every scary-looking system package turns out to be load-bearing; the only way to know for sure is to remove it, reboot, and actually look.
By the end of the pass, the app drawer had gone from a multi-screen scroll of duplicate apps and Xiaomi services to a single unscrollable screen - Signal, Proton VPN, a handful of apps actually in use, nothing else. Clean, in the most literal sense: not a metaphor, just fewer things installed.
It felt, at the time, like the end of the story.
It was not the end of the story.