# Building a Custom `android.jar` (Hidden APIs) From an Android Emulator *A reproducible, physical-device-free method — with the gotchas the old tutorials miss.* --- ## Why this exists Android's SDK `android.jar` is a set of **stubs**: it exposes the public API and throws away everything Google marked `@hide`, plus everything in `com.android.internal.*`. If you want to *compile* code against those hidden/internal APIs (instead of calling them through reflection), you replace the SDK's `android.jar` with a **custom `android.jar`** that contains the real class signatures. The classic recipe (see the original [*Create Your Own Android Hidden APIs*](https://hardiannicko.medium.com/create-your-own-android-hidden-apis-fa3cca02d345)) pulls `framework.jar` from a **physical device**, converts its DEX to `.class` files, and merges them into `android.jar`. It works, but it has two problems in 2024+: 1. It assumes you have a rooted-ish physical device handy. 2. It assumes **all** hidden APIs live in `framework.jar`. On modern Android they don't. This guide shows a method that (a) works entirely from an **emulator**, and (b) covers the hidden APIs that moved into **mainline (APEX) modules**. Every number and result below was measured on a real machine (macOS, `arm64` emulator, JDK 17, `dex-tools 2.4.37`), not recalled from memory. --- ## TL;DR - **Yes, an emulator is enough** — *if* you pick a system image whose `framework.jar` still ships its DEX. Modern images (API 34/35/37) do; some older images ship a stripped 300-byte stub. - **`framework.jar` alone is not enough** on modern Android. Pull **every jar on `$BOOTCLASSPATH`** (including APEX `javalib` jars) — Wi‑Fi, Bluetooth, connectivity, media, etc. moved out of `framework.jar`. But **merge only the hidden-API namespaces** (`android.*`, `com.android.internal.*`, `dalvik.*`), not the whole jar. The boot classpath also carries the ART runtime (`java/*`, `sun/*`, `jdk/*`, `libcore/*`) and repackaged libraries (okhttp, bouncycastle, apache-xml, …); overlaying those shadows the JDK and your app's dependencies and makes Kotlin/Gradle reject the jar (issue #100). - **Compile-time success ≠ runtime access.** Since Android 9 (API 28) the *hidden API blocklist* restricts non-SDK access at runtime for non-system apps. The custom jar only unblocks compilation. --- ## Background: where do the classes actually live? The public `android.jar` in `/platforms/android-XX/` is generated by Google as signature-only stubs. The *implementations* live on the device across the **boot classpath** (`$BOOTCLASSPATH`), historically dominated by `/system/framework/framework.jar`. Two things changed over the years: - **AOT pre-compilation (odex/vdex/oat).** To boot fast, the platform pre-compiles framework classes into a *boot image* (`boot.art` / `boot*.oat` / `boot*.vdex`). On some builds the shipped `framework.jar` is then **stripped of its `classes.dex`** — the jar becomes a near-empty shell while the real bytecode lives only in the boot image. - **Project Mainline (APEX).** Since Android 10+, big chunks of the framework were pulled out of `framework.jar` into updatable **APEX modules** under `/apex/*/javalib/*.jar` (Wi‑Fi, Bluetooth, connectivity/tethering, media, permissions, …). Those jars still contain DEX. So "the hidden API surface" is spread across dozens of jars, and whether any given jar contains usable DEX depends on the build. --- ## Measured reality: emulator vs. device `/system/framework/framework.jar` size, as measured with `adb shell ls -la`: | Source | API | Size | Contents | |---|---|---:|---| | Emulator, `google_apis`, arm64 | 24 | **318 bytes** | empty — only `META-INF/MANIFEST.MF` | | Emulator, `google_apis`, arm64 | 37 | **≈48 MB** | 5 full DEX files (`classes.dex`..`classes5.dex`) | | Physical phone (Samsung A52) | 34 | **≈51 MB** | full | The takeaway that trips people up: **"the emulator's `framework.jar` is tiny" is only true for older images.** The API 24 image ships a 318-byte stub because its framework classes are pre-compiled into the boot image. The API 37 image (`userdebug` / `dev-keys` flavor) ships the full DEX right there in the jar, so it's a drop-in replacement for a physical device. On the API 37 emulator, `$BOOTCLASSPATH` had **52 entries**. Confirmed example of the mainline split: `android.net.wifi.WifiManager` exists as a *stub* in the SDK `android.jar`, but its real implementation ships in `/apex/com.android.wifi/javalib/framework-wifi.jar`, **not** in `framework.jar`. **Rule of thumb:** use an emulator image at **API ≥ 34**. If a jar comes back as a few hundred bytes, it was stripped — skip it and rely on the ones that carry DEX (or, as a last resort, extract from the boot image; see *Limitations*). --- ## The method ### Prerequisites - `adb` (Android platform-tools) - A JDK providing `javac` and `jar` (JDK 17 works) - `unzip` / `zip` - [`dex-tools`](https://github.com/ThexXTURBOXx/dex2jar) (provides `d2j-dex2jar.sh`) - A running emulator at **API ≥ 34** (or a device) ### Step 1 — Pull the framework jars from the emulator ```bash # Discover the full boot classpath (dozens of jars on modern Android) adb shell 'echo $BOOTCLASSPATH' | tr ':' '\n' # Pull framework.jar (and, for full coverage, every jar above) adb pull /system/framework/framework.jar . ``` Sanity check: a real `framework.jar` is tens of MB and contains `classes*.dex`. A ~300-byte file means the DEX was stripped from that image — pick a newer image. ```bash unzip -l framework.jar | grep -E 'classes[0-9]*\.dex' ``` ### Step 2 — Convert DEX to `.class` ```bash d2j-dex2jar.sh --force -o framework-classes.jar framework.jar ``` Measured result on API 37: **36,590 classes**, of which **6,295** are under `com/android/internal/`. ### Step 3 — Merge into the SDK `android.jar` Start from the SDK `android.jar` (it provides `java.*`, `javax.*`, `org.*` and the curated public stubs), then overlay **only** the hidden-API namespaces from the converted framework jars — `android/`, `com/android/internal/`, `dalvik/`. Do **not** overlay the whole jar: the runtime packages (`java/`, `sun/`, `jdk/`, `libcore/`, `com/android/okhttp/`, `com/android/org/`, `org/apache/xml*`, …) shadow the JDK and your app's dependencies, and Kotlin/Gradle then reject the result with *"Cannot access '…' which is a supertype of '…' … missing or conflicting dependencies"* (issue #100). ```bash mkdir merged && cd merged unzip -q "/platforms/android-37.0/android.jar" # base (keep all) unzip -q -o ../framework-classes.jar 'android/*' 'com/android/internal/*' 'dalvik/*' # overlay hidden APIs only # ...repeat the filtered overlay for every APEX/framework jar you converted... rm -rf META-INF jar cf ../android-custom-37.jar -C . . ``` Two refinements the `hiddenjar` CLI adds on top of this manual merge (see `cli/BuildJar.java`): - **Prune dangling supertypes.** A few kept `android.*` classes extend impl that lives in a namespace you dropped (e.g. `com.android.ims`, `com.android.adservices`); left in, they reproduce the same "cannot access supertype" error if referenced. The CLI removes any class whose supertype chain no longer resolves, so the jar's graph is closed like the stock `android.jar`. - **Assemble in memory.** Extracting to a directory on a case-insensitive filesystem (macOS, Windows) silently drops classes that differ only in case — e.g. the public `android.media.AudioAttributes` vs. the distinct `android.media.Audioattributes` that `framework.jar` also ships. The CLI builds the jar straight from zip to zip (case-sensitive), so both survive on every OS. ### Step 4 — Verify it actually compiles hidden APIs This is the step most tutorials skip. Compilation is the real success gate — if it doesn't compile a known hidden symbol, the jar is broken regardless of how cleanly it repacked. ```java import android.app.ActivityThread; import android.os.ServiceManager; import android.os.IBinder; public class TestHidden { void test() { IBinder b = ServiceManager.getService("window"); // @hide android.app.Application app = ActivityThread.currentApplication(); // @hide CharSequence s = com.android.internal.util.CharSequences .forAsciiBytes(new byte[0]); // com.android.internal } } ``` ```bash javac -cp android-custom-37.jar TestHidden.java # → compiles cleanly; all three hidden symbols resolve ``` The CLI goes further than this one probe: it also fails the build if the jar ships any of the JDK-shadowing namespaces, if it has fewer classes than the base SDK jar, or if any public class is left with an unresolved supertype — the check that catches issue #100 *before* it installs. ### Step 5 — Install into the SDK ```bash cd "/platforms/android-37.0" cp android.jar android.jar.orig # keep a pristine backup cp /path/to/android-custom-37.jar android.jar ``` Then set `compileSdk`/`targetSdk` to that level and rebuild. To roll back, restore `android.jar.orig`. --- ## Limitations & honest caveats - **Compile-time only.** The hidden API blocklist (Android 9+/API 28+) still restricts non-SDK access at *runtime* for non-system apps; expect `NoSuchMethodError` / `ClassNotFoundException` unless your app is a platform/system app or the API is on the greylist. The custom jar changes nothing at runtime. - **Unofficial and version-fragile.** Hidden signatures change without notice. Rebuild per API level. - **Signature-only by default (so Gradle's mockable-jar step works).** `dex2jar` emits real method bodies, but those bodies carry `try/catch` blocks, and Gradle's `MockableJarGenerator` (used by `lint` and unit tests) crashes on them — `Cannot read field "outgoingEdges" because "handlerRangeBlock" is null` (issue #46). The CLI therefore strips every method to a `throw new RuntimeException("Stub!")` stub — exactly the shape the SDK's own `android.jar` ships — so the transform accepts it. `javac` only reads signatures, so hidden/internal APIs still compile, and the jar is smaller than keeping the real bodies. Pass `--keep-bodies` to keep the real bodies (useful for browsing decompiled sources), but then the mockable-jar transform will fail on that jar. - **Filtered and pruned by default.** The CLI overlays only `android.*`, `com.android.internal.*` and `dalvik.*` (never the JDK-shadowing ART runtime), then drops any class left with an unresolved supertype so the jar is self-consistent like the stock `android.jar`. Pass `--keep-dangling` to skip the prune step. - **Stripped-jar images.** If you're stuck on an older image where the jars are shells, the classes live in the boot image (`boot-framework.vdex`/`.oat`). Extracting them (e.g. `vdexExtractor` → `baksmali`) is possible but fiddly and is **not** the recommended path — use an API ≥ 34 image. - **ABI is irrelevant to the conversion.** DEX→class doesn't depend on `arm64` vs. `x86`, so an Apple-Silicon `arm64` emulator is fine. --- ## Prebuilt alternatives If you just want a jar and don't need to reproduce it yourself: - [Reginer/aosp-android-jar](https://github.com/Reginer/aosp-android-jar) — prebuilt AOSP-compiled jars. - [JetpackDuba/android-jar-with-hidden-api](https://github.com/JetpackDuba/android-jar-with-hidden-api) — steps + artifacts. Rolling your own still wins when you need an API level or an OEM/custom image that isn't published, or when you want a reproducible internal build with no third-party binaries. --- ## Credits & references - [*Create Your Own Android Hidden APIs*](https://hardiannicko.medium.com/create-your-own-android-hidden-apis-fa3cca02d345) — the original recipe this builds on. - [ThexXTURBOXx/dex2jar](https://github.com/ThexXTURBOXx/dex2jar) — the DEX→class converter used here. - [Reginer/aosp-android-jar](https://github.com/Reginer/aosp-android-jar), [JetpackDuba/android-jar-with-hidden-api](https://github.com/JetpackDuba/android-jar-with-hidden-api) — prebuilt alternatives. > This repository ships a CLI, **`hiddenjar`**, that automates every step above (including the jar > validation and the mainline/APEX coverage). Use `cli/hiddenjar` on macOS/Linux or the native > `cli/hiddenjar.ps1` on Windows (no Git Bash needed); run either with `help`, or use the Gradle > wrapper `./gradlew buildHiddenJar -Papi=`.