SplitAudio is alpha. Please read the limits below before relying on it.
- The routing helper (
route/AppRoute.java) runs as the shell user (started through Shizuku, or through the Play build's built-in ADB client). That is what lets it register a system audio policy (MODIFY_AUDIO_ROUTING). - The app writes
approute.dex/approute.shand a few small setting files (approute.music,approute.pkgmap,approute.volpkg, flag files) into/data/local/tmpand runs the script there. Package names written toapproute.musicandapproute.pkgmapare validated first ([A-Za-z0-9_.]only) and the script only ever uses them as data. Anything else that already has shell access could replace those files — this is the same trust level as shell/Shizuku itself, not an extra hole. - The accessibility service (fdroid build only) filters volume keys and reads only the foreground package name
(
rootInActiveWindow.packageName). It is declared withcanRetrieveWindowContentbecause Android requires that forrootInActiveWindow; screen text is never read. The Play build has no accessibility service. - The Play build's built-in ADB client keeps its key pair in the app's private storage (no backup), pairs with this phone's own
Wireless debugging, and grants itself
WRITE_SECURE_SETTINGSonce (used only to switch Wireless debugging back on after a reboot). ItsINTERNETpermission is used to reach this phone's own adbd and the helper's local control port (below). The fdroid build declaresINTERNETtoo, for that control port only. Nothing is ever sent off the phone.
- The helper listens on TCP 127.0.0.1:47821 only (loopback; it cannot be reached from the network). Any app that has the
INTERNETpermission can connect to a loopback port, so both builds declareINTERNET, and every connection has to pass two checks before a command is even read:- Token. The first line must be
auth <token>. The token is 32 random hex characters (SecureRandom), made by the app on first use and kept in its private storage (routepreferences, no backup). The app puts it in the environment of the start command (SPLITAUDIO_TOKEN);approute.shpasses it on to the helper and also writes it to/data/local/tmp/approute.token(mode 0600, owned by the shell user, so no app can read it) forapproute.sh ask. It is compared in constant time. - Uid. The helper looks the connection up by its source port in
/proc/net/tcp6and/proc/net/tcp(the shell user can read them, ordinary apps cannot since Android 10) and takes the uid that owns the client socket. Only the app that started the helper (its uid is handed over in the environment variableSPLITAUDIO_UIDof the start command), the shell user (2000) and root (0) are served. When the helper starts it tries this lookup on a connection to itself. Where that works, a connection that cannot be found in the tables is refused too (a client that hangs up right after sending would otherwise slip through); where it does not work at all (e.g. a ROM that hides those files from the shell user) the token alone decides, and the helper logs that at start. Anything else getsdeniedand is disconnected: another app can reach the port, but it gets nothing out of it. A helper started withoutSPLITAUDIO_UIDserves shell and root only; without a usable token it does not open the port at all.
- Token. The first line must be
- Commands (one line after the
authline, at most 512 characters, parsed strictly, nothing is passed to a shell):state→on/off;mode→map/legacy;vol up|down <package>→ changes the phone speaker volume by one step (about 1/15 of the music stream's range, read from the audio service), only while the rule is on and only for apps that are being routed to the speaker. Anything else →err. Commands run one at a time; at most 8 connections are served at once and each has 1.5 s to send its lines, so a silent client cannot block the others. - Limits you should know about: the token travels unencrypted over the loopback interface, so a program that grabs port 47821
before the helper starts would see the token the app sends. It still could not use it against the real helper (check 2 turns
that program away), and nothing sensitive crosses this channel (a status word, one volume step), but it could give the app wrong
status answers (a nuisance, not a data leak). Clearing the app's data changes the token (and reinstalling usually changes the uid
too); a helper that is still running then answers
denieduntil it is stopped ("Stop split" works without the token) or ends by itself (Bluetooth gone for 2 minutes / music silent for 10 minutes). - For debugging from a shell:
sh /data/local/tmp/approute.sh ask state(reads the token from the 0600 file; the shell user is allowed by check 2). - 中文摘要:指令口是本機
127.0.0.1:47821的 TCP;每個連線第一行要帶 App 自己的隨機 token(auth <token>),而且小程式查/proc/net/tcp確認對方 uid 是本 App(或 shell/root),其他 App 連得進埠也一律denied。所以兩個版本都需要INTERNET權限, 但沒有任何資料離開手機。
- Native AudioPolicy keeps at most 20 uids per mix and 50 mixes per policy → at most 1000 app uids are routed; any extra apps stay on Bluetooth (user-installed apps are listed first).
- Music apps are chosen in the app (default: YouTube Music + the Morphe build) and passed to the helper as package names; apps that are not installed are ignored.
- Verified on one device (Xiaomi 15, HyperOS 3, Android 16). Other ROMs may ignore or reject the policy; if the helper dies or the policy is rejected, audio simply stays on Bluetooth.
- Apps installed after routing started are not included until the next start (music stop → start again).
Please open a GitHub issue. The only listener is bound to 127.0.0.1 (nothing is reachable from the network), so there is no private disclosure channel.