August 6, 2026
I Gaslit EMUI Into Adding My Media Player
Why did I have to do that in the first place?
So, I have a phone (surprise!). It's a Huawei P30 Lite (MAR-LX3Bm), which means it's ancient enough to have opinions, and I have waaay tooo many opinions of it too. It runs EMUI 12 on top of what I think its Android 10. As expected of Huawei, it has a locked down bootloader, show no root, and the top of the cake: Linux just rejects it. I couldn't connect this phone to my computer if my life depended on it.
This specific build of EMUI (don't know about the others) has a small media center that shows what's playing. Copied from Apple? Sure. It's nice to have? Of course. It supports the stock app, Spotify, QQ Music, Deezer, and even some obscure Chinese apps... except the one app I use, which is Musicolet, a free (this time, as in price), no-account music player and a few other nice features.
Musicolet plays fine. The notification... sure, fine. But that small piece of the screen that it's entire purpose is to show the media playing? Nothing. Not even my reflection.
This wouldn't do.
It wasn't my fault
First, the scientific approach. I wrote a tiny app that listed every active MediaSession on the phone, plus packages that post a media notification. It also makes some naive checks based on what it may filter out.
So Musicolet's row effectively registered and my checks passed. Under every rule of the Android media universe, that app should appear on the media center. Of course, it didn't.
So there was only explanation: somewhere between the app and the media center, there was someone not allowing apps into the door. As any effective bouncer, it must had a list.
Is that a signature check I smell?
The phone's system apps are not the friendlist to inspect. There's no root and no adb, but there is the fact that Android gives apps read access to /system. I'm not sure though if that's intended behaviour, or just Huawei sucks. Probably the late.
I used a very handy App Manager I had installed to extract interesting system APKs and JARs.
The first two suspects: com.huawei.mediacontroller and con.android.mediacenter (stock Huawei music player, yes, they did mess up the package name).
My reasoning was that the package responsible for everything is com.huawei.mediacontroller, that either had a whitelist or some special checking in place. It didn't. This package was some sort of client consuming the actual media API? Not sure of it yet, but it definitely wasn't what I was looking for.
The next package was the stock app, because that one does show, so it must have some sort of hidden API. Well, it didn't. It just used standard Android MediaSessions. Weird.
So next step? Looking inside EMUI.
Of course, between all of this, I found some interesting snippets of code that practically explain the whole state of Huawei's codebase. I may share them some day.
I tracked that API that com.huawei.mediacontroller uses. Found /system/framework.jar, followed it until reaching /system/hwframework.jar, which actually contains the media control center SDK:
MediaControlCenter → MediaControlService → MediaControlAdapter
→ ISessionManager.getControlManager()
As if it didn't disappoint me enough today though, it was just the messenger. The real decision lives in system_server, which meant pulling services.vdex and hwServices.vdex.
However baksmali, the tool for reading dex files, looked at these and said "idk". Not "unsupported" or "corrupt". Just a shrug. The .cdex magic number was, shall we say, unconventional.
This is where I have to humble-brag about being too stubborn to give up. So I asked my AI slop machine for help. In summary, it patched vdexExtractor (a -Werror and a const qualifier didn't seem to stop it) and wrote a custom CdexConv.java that coverts cursed compact dex files into actual dex files using dexlib2. At least that was the sloppy output said.
Then, I just pointed jadx at the result and finally, finally got to read the bouncer's rulebook.
The bouncer had exactly two rules
The money function is HwMediaSessionServiceEx.getPlayerInfoList() (line 848, not that I'm keeping receipts). It iterates every active media controller and keeps only the ones that survive checkController() (line 2045, okay I am keeping receipts). And the first thing checkController() does is ask:
if (!isInMediaWhiteList(controller.getPackageName())) {
return false; // sorry, not on the list
}
Then it delegates to MediaControlUtils.isInMediaSessionOrStyleList(pkgName, ctx, userId, 2), which walks a whitelist built from an XML file (as reference, it's at /system/emui/base/thirdappfilter/third-app-filter.xml, I may share my copy someday).
Entries look something like this:
<function name="mediasession">
<enable name="Deezer Music Player" package="deezer.android.app" version="5.4.13.62" options=""/>
<enable name="Spotify" package="com.spotify.music" version="8.4.74.463" options=""/>
...
</function>
Spotify. QQ Music. Netease. TIDAL. Anghami. Yandex. Twenty-nine players, sitting in a file called third_app_filter.xml, delivered to your phone by Huawei's cloud config system like a celestial shopping list.
And this is where I want you to pause and appreciate the sheer audacity of this whitelist.
The security check is:
- Is the package name on the list?
- Is the installed version >= the version on the list?
That's it. That's the entire security model. Fairly enough, something that I would do myself too.
The very fun version check
The one remotely interesting part of the check is the version comparison, so of course it's also where I almost tripped:
String currentVersionName = versionName.replaceAll("[^(0-9.)]", "");
String[] versions = currentVersionName.split("\\.");
It deletes every character that isn't a digit or a dot. Then it splits on dots and requires:
- the same number of segments, and
- each segment numerically >= the corresponding minimum.
Here's the thing that made me laugh out loud at 2am. The Yandex entry has version 2021.07.2 #3977. A version with a space and a hash in it.
And when the check strips the non-numeric characters? "2021.07.2 #3977" becomes "2021.07.23977". The 2 and the 3977 get glued together into one segment. Three segments, not four. My first attempt at claiming Yandex used a four-segment version and failed the count check so gracefully it might as well have bowed. Does it even work for Yandex this way?
So the rules of the game, final form:
- Claim a package name that's on the list
- Pick a version with the exact same stripped segment count, each segment >= the min
- Pray
Identity theft via applicationId
Here's the beautiful part. The whitelist checks package and version. But which package does a media session claim to belong to? The one derived from the app's UID, that is whatever applicationId the app was installed under.
Only one app can own a package name, so I needed a whitelisted package that isn't installed on the phone. Turns out the list is full of them: of course, oriented at a chinese population and very limited occident support.
So the plan:
- Build an app with applicationId = "deezer.android.app" and versionName = "99.99.99.99"
- That app creates its own MediaSession. The system attributes it to deezer.android.app, because that's the UID.
- The app registers as a notification listener, which Android graciously uses to grant it getActiveSessions() across every app, including Musicolet.
- It mirrors Musicolet's metadata and playback state into its own session, and forwards any transport control (play, pause, skip, seek) right back.
And just to be clear about the architecture: the mediacontroller app itself is a system app, so it is the one allowed to call the framework API. My proxy doesn't need any privileges. It just needs to hold the door open and look like Deezer while Musicolet's music walks through.
The user-visible payoff: both the app name and the media center card now says "Deezer Music Player" (or at least should, but didn't find it anywhere), shows Musicolet's current track, album art, and a live progress bar, and every button on the card actually controls Musicolet. It is, functionally, a hostage situation where the hostage is cooperating.
The yandex-shaped trap
Since the identity is baked into the APK at build time (you can't change your package name at runtime, that's not how being a package works), I made the identity a build flavor.
For the record, I verified each one against the actual decompiled comparison logic before shipping.
That last '2021.07.23977' will live in my head rent-free forever.
It supports different masks as flavors, and custom targets for listening to the MediaSession.
So... what now?
I'm a web developer. I do not know what I'm even doing touching Java or Android at this point, but it was in fact fun.
I'm not sure why the whitelist at all at such mundane place. If I was an Android vendor, and any app that exposes a MediaSession didn't show on the Media Center I would definitely consider it a bug. So either Huawei wanted to protect its delicate widget with a whitelist of stable MediaSessions, or they just wanted to blacklist Google services from showing up (their code already has support for blacklisting instead of this).
As a remark, I do not have root access in the phone. This was purely user-space, which makes it funnier.
Can Huawei break this? Sure, a PackageManager.getPackageInfo with the right flag and we're done. But they won't.
I may release some funny code I found, along with the complete filter list.
Until then, happy (reverse) engineering!