AAB to APK with bundletool, and How to Extract APK Assets
On this page
Short answer: an AAB can’t be installed, but bundletool turns it into one installable APK: java -jar bundletool.jar build-apks --bundle=app.aab --output=app.apks --mode=universal, then unzip app.apks universal.apk. An APK is a ZIP file, so unzip app.apk 'assets/*' -d out extracts its assets. You don’t even need bundletool to read an AAB’s assets: they sit under base/assets/ in the bundle.
All output below comes from a hello-world bundle I built for this guide (com.example.helloassets: two asset files, English, French and German strings, and a native library for arm64-v8a and x86_64), run with bundletool 1.18.3 from its GitHub releases on Temurin JDK 17.0.18, plus an Android 14 emulator for the install steps.
What is inside an AAB
An AAB is a ZIP too, but laid out per module, with protobuf manifests and resources instead of the binary XML an APK uses:
$ unzip -l hello-assets.aab
Length Date Time Name
--------- ---------- ----- ----
10 01-01-2010 00:00 BundleConfig.pb
5008 01-01-2010 00:00 base/lib/arm64-v8a/libhello.so
4704 01-01-2010 00:00 base/lib/x86_64/libhello.so
2616 01-01-2010 00:00 base/dex/classes.dex
53 01-01-2010 00:00 base/assets/config.json
64 01-01-2010 00:00 base/assets/www/index.html
1433 01-01-2010 00:00 base/manifest/AndroidManifest.xml
28 01-01-2010 00:00 base/assets.pb
43 01-01-2010 00:00 base/native.pb
390 01-01-2010 00:00 base/resources.pb
--------- -------
14349 10 files
adb refuses it before it reaches the device:
$ adb install hello-assets.aab
adb: filename doesn't end .apk or .apex: hello-assets.aab
Step 1: Get bundletool
Download bundletool-all-<version>.jar from the releases page (1.18.3 was the latest on 2026-11-02) and run it with Java (I used JDK 17):
java -jar bundletool-all-1.18.3.jar version
1.18.3
The examples below call the JAR bundletool.jar.
Step 2: Convert the AAB to a universal APK
java -jar bundletool.jar build-apks --bundle=hello-assets.aab \
--output=hello-universal.apks --mode=universal
INFO: The APKs will be signed with the debug keystore found at '/Users/longmon/.android/debug.keystore'.
The output is an .apks file, an “APK set” that is itself a ZIP. In universal mode it holds a table of contents and one APK:
$ unzip -l hello-universal.apks
Length Date Time Name
--------- ---------- ----- ----
96 01-01-1981 01:01 toc.pb
12827 01-01-1981 01:01 universal.apk
--------- -------
12923 2 files
$ unzip -o hello-universal.apks universal.apk -d out
extracting: out/universal.apk
$ adb install out/universal.apk
Performing Streamed Install
Success
The universal APK has everything in one file: both ABIs, all three languages and the assets.
$ unzip -l out/universal.apk
Length Date Time Name
--------- ---------- ----- ----
2228 01-01-1981 01:01 AndroidManifest.xml
53 01-01-1981 01:01 assets/config.json
64 01-01-1981 01:01 assets/www/index.html
2616 01-01-1981 01:01 classes.dex
5008 01-01-1981 01:01 lib/arm64-v8a/libhello.so
4704 01-01-1981 01:01 lib/x86_64/libhello.so
556 01-01-1981 01:01 res/xml/splits0.xml
1036 01-01-1981 01:01 resources.arsc
--------- -------
16265 8 files
Things to know:
- Signing. Without
--ks, bundletool signs with~/.android/debug.keystore, as the INFO line says.apksigner verify --print-certs out/universal.apkshowedCN=Android Debug. That is fine for testing and analysis. To give a universal APK to users, sign it with your release key: add--ks=release.jks --ks-key-alias=release(passwords go in--ks-passand--key-pass). - Existing output. Running the same command again failed with
[BT:1.18.3] Error: File 'hello.apks' already exists.followed by a Java stack trace. Add--overwrite, or delete the file. - Feature modules. Only modules that declare
<dist:fusing dist:include="true"/>go into the universal APK. If a feature is missing from it, check that flag before concluding the app doesn’t have the feature. - Re-signed bytes. A universal APK you build is not byte-identical to what Play delivers, which is signed with the app signing key. Don’t compare hashes across the two.
Step 3: Build the split APKs a device would get
Without --mode, bundletool builds the full split set Play would serve. For my bundle that was 16 files:
$ java -jar bundletool.jar build-apks --bundle=hello-assets.aab --output=hello.apks
$ unzip -l hello.apks
1057 01-01-1981 01:01 toc.pb
28830 01-01-1981 01:01 splits/base-arm64_v8a.apk
28830 01-01-1981 01:01 splits/base-arm64_v8a_2.apk
28830 01-01-1981 01:01 splits/base-arm64_v8a_3.apk
8339 01-01-1981 01:01 splits/base-de.apk
...
8592 01-01-1981 01:01 splits/base-master.apk
12688 01-01-1981 01:01 splits/base-master_2.apk
12688 01-01-1981 01:01 splits/base-master_3.apk
28827 01-01-1981 01:01 splits/base-x86_64.apk
...
That is one master APK plus one split per ABI and per language, each in three variants (no suffix, _2, _3). You don’t pick among them by hand: describe the device in JSON and let extract-apks choose.
cat device-spec.json
# {"supportedAbis":["arm64-v8a"],"supportedLocales":["fr-FR"],"screenDensity":420,"sdkVersion":34}
java -jar bundletool.jar extract-apks --apks=hello.apks \
--output-dir=fr-arm64 --device-spec=device-spec.json
The APKs have been extracted in the directory: fr-arm64
$ ls fr-arm64
base-arm64_v8a_3.apk base-fr_3.apk base-master_3.apk
For an SDK 34 device it took the _3 variants: master, the arm64 split and the French split.
java -jar bundletool.jar get-device-spec --output=emulator-spec.json writes that JSON for the connected device. For my emulator it reported "supportedAbis": ["arm64-v8a"], "supportedLocales": ["zh-CN"], density 420 and SDK 34. build-apks --connected-device builds only what that device needs, which here was base-master.apk and base-arm64_v8a.apk.
To install a split set, use install-apks (or adb install-multiple with every file). Installing one split on its own fails, as shown in package appears to be invalid.
$ java -jar bundletool.jar install-apks --apks=hello.apks
$ adb shell pm path com.example.helloassets
package:/data/app/~~9-EMT_sEmxxD3fGvBiMVvw==/com.example.helloassets-2074h57ADobth13GxZbeZg==/base.apk
package:/data/app/~~9-EMT_sEmxxD3fGvBiMVvw==/com.example.helloassets-2074h57ADobth13GxZbeZg==/split_config.arm64_v8a.apk
No language split was installed, because the emulator’s only locale was zh-CN and the bundle has no Chinese strings.
get-size total estimates the download size per configuration, in bytes:
$ java -jar bundletool.jar get-size total --apks=hello.apks --dimensions=ABI,LANGUAGE
ABI,LANGUAGE,MIN,MAX
arm64-v8a,de,10685,10717
x86_64,de,10600,10629
x86_64,fr,10595,10626
arm64-v8a,fr,10680,10714
Step 4: Extract the APK’s assets
Assets are stored as ordinary files, so unzip with a pattern is enough:
$ unzip -o out/universal.apk 'assets/*' -d universal-assets
inflating: universal-assets/assets/config.json
inflating: universal-assets/assets/www/index.html
$ cat universal-assets/assets/config.json
{"apiBase":"https://example.com/api","theme":"dark"}
From the AAB, the same files come out without converting anything:
$ unzip -o hello-assets.aab 'base/assets/*' -d aab-assets
inflating: aab-assets/base/assets/config.json
inflating: aab-assets/base/assets/www/index.html
The same works for res/raw/, lib/<abi>/*.so and classes*.dex. What unzip does not give you is readable XML. The manifest and resources.arsc are compiled:
$ unzip -p out/universal.apk AndroidManifest.xml | file -
/dev/stdin: Android binary XML
Read them with the SDK tools instead:
$ aapt2 dump badging out/universal.apk
package: name='com.example.helloassets' versionCode='3' versionName='1.2' platformBuildVersionName='15' platformBuildVersionCode='35' compileSdkVersion='35' compileSdkVersionCodename='15'
minSdkVersion:'24'
targetSdkVersion:'35'
application-label:'Hello Assets'
...
launchable-activity: name='com.example.helloassets.MainActivity' label='' icon=''
$ java -jar bundletool.jar dump manifest --bundle=hello-assets.aab --xpath=/manifest/@android:versionCode
3
bundletool dump manifest --bundle=... without --xpath prints the whole manifest as XML. For a fully decoded res/ tree, run apktool d app.apk on a copy of the APK. To browse the manifest, signing certificate, native libraries and every entry without installing anything, drop the APK or AAB into this site’s APK & AAB analyzer, which runs in your browser.
macOS gotcha: names that differ only in case. Some release builds contain resource paths like res/2f.xml and res/2F.xml (the F-Droid 2.0.2 client does). The default macOS volume is case-insensitive, and with a test ZIP holding both names, unzip -o printed extracting for both but left only res/2F.xml on disk. Without -o it stopped at a replace ...? prompt. Extract on Linux or on a case-sensitive volume if you need every file.
What the assets tell you
File names are hints and headers are better evidence (xxd -l 16 file, file file). This is useful for checking how your own build, or an open-source app, is packaged:
- React Native. The Gradle plugin packs the JavaScript bundle as
assets/index.android.bundleby default (bundleAssetNameinReactExtension.kt).libhermes.soinlib/<abi>/hints that Hermes is the engine. Hermes bytecode starts with the magic0x1F1903C103BC1FC6(MAGICin Hermes’s BytecodeFileFormat.h), whichxxdshows little-endian asc6 1f bc 03 c1 03 19 1f. A plain-JavaScript bundle can be worked on as in recovering JavaScript source from a bundle. - Flutter.
lib/<abi>/libapp.sopluslibflutter.so. See Flutter app reverse engineering. - Unity IL2CPP.
libil2cpp.sotogether withglobal-metadata.dat. - Everything else. Images, fonts, audio, JSON and WebView HTML sit under
assets/andres/raw/as ordinary files.
Only unpack apps you wrote, open-source apps, or apps you have permission to examine. The licenses of the extracted files still apply.
Optional: REA for an inventory with hashes
REA, a third-party open-source CLI, can inventory and extract an APK or AAB with a SHA-256 for every entry. In 6.7.0, rea inspect-artifact hello-assets.aab --json classified the bundle as a zip container and the two libhello.so files as ELF dynamic-library entries. There is no AAB format: its Android tools expect a standalone APK, so convert first. In an earlier run, inspect-android-package refused an .aab with “is zip; provide one standalone APK”, and decode-android-resources returned empty fields for one without failing. rea extract-artifact checks every written file against the inventory hash and refuses archives with case-colliding names on a case-insensitive volume instead of overwriting, which avoids the macOS problem above.