<X />C{}JS();

Resurrecting 2013 with Qwen3.8 27b

August 29, 2026

An AI generated image of two weathered, torn 2013 Android posters lying among cables and pipes.

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 7 and targetSdk 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 /sdcard location that scoped storage restricts on Android 11 and later. That call appears exactly once, in AndroidFiles.smali (from com.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:

  1. Neutralize the ad gate. GameActivity.o()Z, the predicate that gates ad display, is rewritten to unconditionally return false, which removes every downstream ad branch without touching branch targets.
  2. Sever score submission. The GMS score submit call in c.smali (a(String;J) behind the o()Z check) is deleted, leaving the original :cond label so the remaining jump table stays valid.
  3. 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:

  1. 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.
  2. The original APK is the source of truth. The smali tree is a build artifact, never a source.
  3. 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.86bFlappy Bird 1.3
Pipelinesuccesssuccess
APK size29.2 MB547 KB
Signature schemesv1 + v2 + v3 verifiedv1 + v2 + v3 verified
Cert DNCN=Delver, OU=Mod, O=EchoCN=Flappy Modern, O=Flappy
targetSdk3434
Patch verify (baksmali on signed dex)scoped-storage rewrite presentad/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.