I care about media preservation. Software is cultural material the same way film, music, and print are - and an app that can no longer run is a story that has been lost. Keeping old software running takes deliberate effort in the face of an industry that tirelessly chases what's just ahead.
Google has not been the best steward here. Android's history is a long string of breaking changes applied in the name of security and performance: dwindling runtime permissions, scoped storage, 64-bit-only ABI requirements, background execution limits, non-SDK interface restrictions, per-app targeting requirements, and now even memory limits - introduced in Android 17 and expanding across the ecosystem, where an app that exceeds its budget is slowed down and may be terminated.
Google Play's escalating target API requirements add a policy layer on top of the platform one, arbitrarily walling older apps off from existing (and sometimes even paying) users on newer devices. Backward compatibility on Android has been best-effort at the platform level and disposable at the store level with a Google Play app list of non-installable apps presenting a stinging reminder of what Google has withheld from users as burnt out indie developers understandably allow their older games to languish in an ever-changing and evaporating sea of API changes.
But what can I do about it? On my own, not much. I don't have the capacity to take on reverse engineering efforts at the moment, and I honestly don't have a massive amount of Android development experience yet. Then again, I don't need to.
Enter Echo

Echo is my Hermes (formerly OpenClaw) agent. She trades my stocks. She pushes code to my self-hosted GitLab instance. She writes reports and study guides for me. Could she reverse engineer applications, too?
Echo started built on GLM 4.7 Flash. When Qwen3.6 27b became available, I upgraded her immediately based on Alibaba's reputation in the local model community and Qwen's benchmark results. When Qwen3.8 27b was released, Echo was upgraded again, and this was a perfect opportunity to see what would happen if I gave her a complex task to benchmark the new model. Even if the task failed, it would still be educational in determining Qwen3.8 27b's limits.
Echo assisted in writing her journey out below.
Delver
Delver was an Android game released in 2013 that I remember fondly. It functioned on my devices over the years until Google required scoped storage and 64 bit app builds - it was the perfect candidate.

Intake and Diagnosis
Echo's first hour went to establishing what the game was and why it didn't
run. apktool d and jadx on the 0.86b APK showed a 2013 build:
minSdk 7 / targetSdk 18, natives for armeabi/armeabi-v7a only,
and 425 game classes on libGDX 0.9.8. jadx decompiled 424 of them
cleanly; the single method it choked on (Player.Use) was reconstructed
from the original smali bytecode.
Echo discovered the following blockers:
- 32-bit-only natives. A 64-bit-only device can't run 32-bit code, so the package's libraries can't load and installation fails.
minSdk 7andtargetSdk 18, both disqualifying. Android 14 (API level 34) and later refuse to install apps targeting below API level 23, so the package is blocked at install time even on a device that could run it. Google Play is a separate constraint: as of August 31, 2026, new apps and app updates must target API level 36, and existing apps must target API level 35 to remain available to users on newer devices. These builds target API level 34, which is fine for direct installation but would not pass Play's submission or availability requirements.- Delver resolved its save path through
Environment.getExternalStorageDirectory()- the/sdcardlocation that scoped storage restricts on Android 11 and later. That call appears exactly once, inAndroidFiles.smali(fromcom.badlogic.gdx.backends.android.AndroidFiles). Smali is the human-readable assembly of Android's DEX bytecode produced by apktool, a third-party utility that disassembles an APK - one smali file per class, mirroring the Java package path.
Echo also discovered that the upstream engine repo exists, but it is a 2018+ rewrite with a different asset format - a dead end for a 2013 build. The closest artifact on Maven Central was gdx 0.9.9, the bug-fix release immediately after 0.9.8, which narrowed the plan to a faithful port at the smali level plus a native rebuild.
Rebuilding the Natives
The 2013 APK shipped libgdx.so and libandroidgl20.so for 32-bit ARM
only. Both were rebuilt from the libGDX 0.9.8 JNI sources with
NDK r25c for arm64-v8a and
armeabi-v7a. AndroidGL20.cpp from the 1.9.6 tag turned out to cover
all 143 natives the libGDX 0.9.8 Java declares - the 14 initially "missing"
were just __-mangled overloads. Symbol verification after the build:
264 JNI symbols in libgdx.so, 151 exports in libandroidgl20.so. The
rebuilt binaries landed in prebuilt/; the ~53 MB JNI source tree was
excluded from the git commit.
The Storage Patch
The game's AndroidFiles class resolves its writable directory through
Environment.getExternalStorageDirectory(), which Android 11 and later
restrict under scoped storage. The decompiled source confirmed it was the
only storage call site in the game.
Echo wrote an assistive script - patch_storage.py (127 lines) rewrites that
call chain in AndroidFiles.smali to Context.getFilesDir(). There was a smali
subtlety: p0 in the AndroidFiles constructor is a reference to this - the
current object - rather than a Context, and the constructor takes no
Context parameter either -
libGDX 0.9.8 builds it around an AssetManager instead. The patch therefore
threads the Context through a static field: AndroidApplication, libGDX's
Activity subclass that instantiates AndroidFiles, stores itself in a
static field at startup, and the rewritten storage code reads that field and
calls getFilesDir() on it.
The first run of this script produced the first bug: the register-count bump was computed relative to a ~400-character search window but applied to the full file text, so the offsets drifted and the rebuilt dex failed to assemble. The fix was to make the bump window-relative. The dex-assembly failure - not a silent success - is how the bug surfaced.
The rewrite itself was surgical: exactly two files changed, verified by a clean smali/dex round-trip.
First Green Build - and the Manifest Defect
The rebuilt APK got a modern manifest (minSdk 21 / targetSdk 34, no
legacy storage permissions, versionCode 40) and a CN=Delver, OU=Mod, O=Echo keystore, then was signed v1+v2+v3 and verified with
baksmali, the disassembler half of the
smali toolchain: it reverses the assembler's job, unpacking the signed APK's
classes.dex back into smali so the patched code can be confirmed present
inside the finished artifact.
Echo delivered the signed APK, 29.2 MB, the same evening a few hours later.
Unfortunately, Echo celebrated prematurely. My phone reported the manifest as
"corrupted", when I tried to install it, and I conveyed the issues back to Echo.
She discovered that the launcher activity declared an <intent-filter> without
android:exported. That filter is what puts the app icon on the home screen,
and any component that has one can be started from outside the app, so Android
12 (API level 31) started requiring the attribute explicitly - in 2013 it was
optional and defaulted to exported. The check happens at installation, not at
build time, which is why aapt2 parsed the manifest cleanly the whole time.
Echo fixed the manifest, rebuilt and re-signed the APK, and delivered the fix
with the root-cause explanation.
It Worked?
Echo/Qwen3.8 27b stunned me. The next APK she delivered and I installed worked without a single issue. I asked her to reverse engineer an abandoned legacy Android application, and a hours later I was playing it again for the first time in years. Could she do it again?
Flappy Bird
Flappy Bird needs little introduction: .GEARS Studios' 2013 one-tap mobile game where you tap to flap a bird through gaps in green pipes became a global obsession before its creator pulled it from the app stores, saying it was too addictive.

At least I can still play it if I want to, right?
Intake
I passed Flappy Bird 1.3
(com.dotgears.flappybird) to Echo who quickly discovered a different problem set
thanks to her patches to her Android development skill from her prior
efforts: dead dependencies. The 2013 build carried 698 legacy
AdMob classes,
Google Play Games v1
integration (leaderboards, score submission), a 32-bit-only
libandengine.so, and targetSdk 18. On a modern
device the dead ad and GMS stack is what crashes the game at startup.
The engine is AndEngine (a
libGDX fork). Its JNI sources are public,
and they match the five native symbols the dex expects, so
libandengine.so was rebuilt for both modern ABIs from the originals
(BufferUtils.cpp, GLES20Fix.c). Echo mapped call graph for the ad and GMS
code next and confirmed they were cleanly isolatable, which made the
stripping work below possible.
The Ad and GMS Surgery
Echo wrote patch_ads.py (294 lines), which performs three rewrites:
- Neutralize the ad gate.
GameActivity.o()Z, the predicate that gates ad display, is rewritten to unconditionally returnfalse, which removes every downstream ad branch without touching branch targets. - Sever score submission. The GMS score submit call in
c.smali(a(String;J)behind theo()Zcheck) is deleted, leaving the original:condlabel so the remaining jump table stays valid. - Drop the leaderboard UI. The leaderboard dialog path is stubbed the same way.
The bird, the pipes, the art, the sounds, and the AndEngine rendering
code are untouched apart from the rebuilt native. An early version of
the patch got two details wrong: gutted method bodies must still account
for parameter registers, and the real onCreate calls super.onCreate
before anything else, so that call had to be preserved. The script was
corrected against the actual smali rather than the intended shape of it.
The 2013 build is signed by the original developer (CN=Dong Nguyen, O=.GEARS
Studios). An APK contains only the certificate, never the private key, so that
signature cannot be recovered. The modern build therefore carries a fresh
CN=Flappy Modern keystore. Its password, generated at the start, was never
written down, and it was recovered by testing candidates from session history
against the keystore with keytool -list -storepass until one matched. The
consequence of the new signature: the modern APK is a replacement install, not
an update, and the 2013 original must be uninstalled first if it were somehow
still installed. Delver's original signature was in the same situation, so both
modern builds are new identities under the same package names.
About three hours after intake, Flappy Bird was rebuilt, verified, and delivered back to me, fully functional the first time this time.
The CI Push
GitLab Setup
Echo pushed both projects to my self-hosted GitLab under her user account with
identical pipelines, committed the original APKs as the source of truth
(original-*.apk is exempted from .gitignore for that purpose), and set up CI
to download its own tools from links she had proven work. That last part took a
second pass: the initial .gitlab-ci.yml pointed at a bitbucket apktool mirror
that now 403s and at com/android/tools/smali on Maven Central, which doesn't
exist. smali 2.5.2 now lives under org/smali, and apktool 2.9.3 downloads from
the GitHub releases.
The pipeline itself, in order:
original-*.apk
→ apktool d # decode: smali tree + manifest + res + assets
→ patch_*.py # idempotent Python patches, run against a FRESH decode
→ manifest/libs # modern targetSdk manifest + NDK-rebuilt natives
→ apktool b # rebuild (aapt2)
→ apksigner # v1+v2+v3 sign, keystore from CI/CD variables
→ baksmali verify # disassemble the SIGNED dex, assert patches landed
Three properties define it:
- Patches run against a fresh decode. Each script takes the decoded
tree as
argv[1]and never mutates a previously patched tree, so CI can decode from the committed original APK at any time and land in the same place. - The original APK is the source of truth. The smali tree is a build artifact, never a source.
- The verify gate fails on mismatch. After signing, the build disassembles the signed APK's dex with baksmali and asserts the patches are present, exiting 1 with a full log otherwise.
Build Results
Echo verified that both pipelines ran to green. Artifacts were pulled from the CI bundles and re-verified locally with apksigner:
| Delver 0.86b | Flappy Bird 1.3 | |
|---|---|---|
| Pipeline | success | success |
| APK size | 29.2 MB | 547 KB |
| Signature schemes | v1 + v2 + v3 verified | v1 + v2 + v3 verified |
| Cert DN | CN=Delver, OU=Mod, O=Echo | CN=Flappy Modern, O=Flappy |
targetSdk | 34 | 34 |
| Patch verify (baksmali on signed dex) | scoped-storage rewrite present | ad/GMS strip present |
One caveat: the Delver CI hash differs from the local build's hash even though nothing in the patch changed. Zip entry timestamps make byte-identical APKs across machines impractical without extra work. Flappy Bird's CI and local builds happened to match.
Closing Thoughts
The effort came to roughly two working days for my poor overworked, overheating local AI server: the first for Delver from APK intake to a verified, signed build, the second for Flappy Bird plus the longer tail of CI setup, secret handling, and pipeline verification. Both games now build from their committed original APKs into signed, verifiable, modern-Android artifacts on every push.
This article was partially drafted by Qwen3.8 27b, proofread by GLM 5.3 Flash, and rewritten in stages by Zackary Lowery.
