Software
Software

I Added Drag-and-Drop XAPK Install to scrcpy

I extended scrcpy's drag-and-drop APK install to XAPK/APKS/APKM bundles. A thin C patch, a Python helper that does the work, and the SDL2, argv and security snags I hit along the way.

Installing an XAPK on my phone drove me up the wall every single time. Copy the file to the phone, open some ad-riddled XAPK installer app, sit through an ad, dig through folders to find the file, install, watch another ad. Getting a simple game on the device could eat minutes. Meanwhile I already control my phone from my computer with scrcpy, and dropping a plain .apk onto the window installs it automatically. So the question was simple: why not the same for XAPK?

Before writing anything I had to be clear on what an XAPK actually is. Turns out there is no magic: an XAPK is just a ZIP archive. Inside it there's a base APK, a handful of split APKs (language, screen density, architecture pieces) and optionally OBB data files. So "install an XAPK" really means "install all the splits in one session with adb install-multiple, and if there are OBBs put them under /sdcard/Android/obb/". Once that clicked, it suddenly looked doable.

Tools Used
  • scrcpy v3.3.3the patched base (last SDL2-based release)
  • adbinstall-multiple and OBB push
  • Python 3 (stdlib)the helper; no pip dependencies
  • notify-senddesktop notification (optional)
  • meson / ninjato build scrcpy from source

Two parts: a thin C patch and a Python helper that does the work

I made one design decision up front: touch scrcpy's C side as little as possible and keep the real logic in a separate Python helper. Three reasons. First, scrcpy moves fast; the thinner the C patch, the easier it is to rebase on a new release (the diff stayed at about 40 lines). Second, with the real work (unzipping, reading the manifest, building adb commands) in Python, I can test it without a device attached. Third, the helper works on its own; I can install an XAPK from the terminal without scrcpy at all.

Where I hooked into scrcpy

Here's what happens when you drop a file on the scrcpy window: input_manager.c hands the dropped file to file_pusher.c, which runs adb install if the file is .apk, and otherwise pushes it as-is to /sdcard/Download. So XAPKs were quietly being copied into the Downloads folder all this time. What I needed was a check that recognizes the extension and a new action type (INSTALL_XAPK).

c
static bool
is_xapk(const char *file) {
    const char *ext = strrchr(file, '.');
    return ext && (ext_iequals(ext, ".xapk") || ext_iequals(ext, ".apks")
                                             || ext_iequals(ext, ".apkm"));
}

// if the dropped file is .apk -> adb install, if it's in the XAPK family ->
// call the helper, otherwise push to /sdcard/Download like before
if (is_xapk(file)) {
    action = SC_FILE_PUSHER_ACTION_INSTALL_XAPK;
} else if (is_apk(file)) {
    action = SC_FILE_PUSHER_ACTION_INSTALL_APK;
} else {
    action = SC_FILE_PUSHER_ACTION_PUSH_FILE;
}
Problem

On the first run the helper never fired properly. The cause was dumb but instructive: while building the argv array to call the helper I was always appending the serial. But scrcpy sometimes passes the device serial as NULL. Put a NULL in the middle of argv and the array is treated as ending right there, so the actual file argument fell off entirely. The helper was being called with no file and nothing got installed.

c
// serial can be NULL; when it is I have to skip --serial entirely, otherwise
// the argv array ends early on a NULL in the middle and the file arg drops off
const char *argv[6];
size_t n = 0;
argv[n++] = helper;              // "xapk-install"
if (serial) {
    argv[n++] = "--serial";
    argv[n++] = serial;
}
argv[n++] = file;
argv[n] = NULL;

The same patch surfaced one more small detail: scrcpy's log line decided "install vs push" with a binary condition. Once I added a third action (INSTALL_XAPK), the assumption "if not INSTALL_APK then push" became wrong, so I had to fix that line too. Adding a third case to two-case logic is exactly where these silent assumptions catch you.

Two formats, and a bit of security paranoia

Not all XAPKs are the same. APKPure/APKMirror bundles carry a manifest.json; I read the package name, split list and OBBs from there. But .apks files produced by tools like SAI have no manifest. In that case I treat every .apk in the archive as a split, collect the .obb files, and derive the package name from the standard OBB filename. That classic naming rule, starting with main/patch and carrying the package name in the middle, came in handy.

python
def _package_from_obb_name(name):
    """`main.123.com.example.app.obb` -> `com.example.app` (None otherwise)."""
    base = posixpath.basename(name)
    if not base.endswith(".obb"):
        return None
    tokens = base[:-4].split(".")
    if len(tokens) < 3 or tokens[0] not in ("main", "patch"):
        return None
    return ".".join(tokens[2:])

Where I actually struggled: SDL2 or SDL3

The most annoying part wasn't the XAPK logic, it was getting scrcpy to build. scrcpy v4.0 and later want SDL3 (libsdl3-dev), but SDL3 isn't in some distros' repos yet (Ubuntu 24.04, for example). So I targeted v3.3.3, the last SDL2-based release; SDL2 is in every repo and builds out of the box.

The second trap was sneakier. I had a newer, out-of-repo libsdl2 runtime installed by hand on my system. So trying to apt install the stock libsdl2-dev gave a version conflict. I didn't want to touch the system and break my runtime. As a fix, install.sh downloads and unpacks the stock SDL2 dev headers into a local folder, points a symlink at the existing runtime, and builds scrcpy against it via PKG_CONFIG_PATH. Since the 2.30 headers and the 2.31 runtime are ABI-compatible, it compiles cleanly and my custom SDL2 stays exactly as it was. Not something to recommend to anyone, but that's what real machines look like.

I tested it without a device, and upstream is next

Moving the real logic into Python paid off in testing. I generate small fake .xapk and .apks ZIPs at runtime (with empty placeholder .apk/.obb entries and a manifest.json) and mock subprocess. That let me verify file classification, manifest parsing and the generated adb command sequence, all without plugging in a phone. The --dry-run flag also prints the commands it would run without installing anything, which helped for both testing and peace of mind.

bash
# works standalone from a plain terminal, no scrcpy needed
xapk-install game.xapk               # install to the default device
xapk-install -s SERIAL game.xapk     # install to a specific device
xapk-install --dry-run game.xapk     # don't install, just print the adb commands it would run

The question on my mind now is proposing this to scrcpy upstream. There's already an open issue for the request (Genymobile/scrcpy#5378). Doing it in native C has two open points: the client currently has no ZIP reader (a single-header library like miniz would be needed) and the manifest would pull in a JSON dependency. The second can be skipped entirely: go format-agnostic, install-multiple every .apk in the archive and derive the OBB package from the filename again. We'll see whether the maintainers are keen.

What I Learned

The part that ate the most time in this project wasn't the XAPK format, as I'd guessed, it was the ecosystem around it: SDL versions, argv corner cases, not trusting a ZIP. The decision to push the work into Python and keep the C as thin as possible saved both testing and future rebasing. It started as a small feature and ended up teaching me how scrcpy and the Android packaging world actually work.