Unyo Core
A JVM runtime for running Aniyomi (anime/manga streaming) Android extensions on desktop. This project bridges the gap between Android's framework and desktop Java/Kotlin by providing a compatibility layer that lets Aniyomi extensions execute outside of the Android ecosystem.
Quick Start
# Build the fat JAR (includes all dependencies)
mvn clean package
# Output: aniyomi-bridge/target/aniyomiBridge-1.0.0.jar
# Main class: dev.k3vinb5.unyo.core.cli.Main
Project Architecture
This is a multi-module Maven project with two main modules:
unyo-core/
├── pom.xml # Parent POM — coordinates modules
├── androidcompat/ # Android compatibility layer
│ ├── src/main/java/android/ # Custom Android stub implementations
│ ├── src/main/java/androidx/ # AndroidX stub implementations
│ ├── scripts/get-android.sh # Android SDK JAR download & patch script
│ └── pom.xml
├── aniyomi-bridge/ # Core bridge module
│ ├── src/main/java/ # Java sources (bridge core)
│ ├── src/main/kotlin/ # Kotlin sources (Tachiyomi/Aniyomi interfaces)
│ └── pom.xml
└── assets/ # JavaScript files for extractors
Module: androidcompat
Provides Android framework classes on desktop JVM using a hybrid approach inspired by Suwayomi:
- Base SDK: Downloads the official Android SDK
android.jarfrom Sable/android-platforms - Custom Stubs: Implements desktop-specific behavior for classes that need it (e.g.,
Context,WebView,SharedPreferences) - Jar Patching: Removes SDK classes that have custom implementations to prevent conflicts
Result: A merged Android API where ~4,000 SDK classes work as-is, and ~30+ critical classes have desktop-aware implementations.
Module: aniyomi-bridge
The core bridge that loads and executes Aniyomi/Tachiyomi extensions.
Key Classes:
AniyomiBridge.java— Main API for loading/unloading extensionsExtensionLoader.java— Loads extension classes from JAR filesApkUtils.java— Converts Android APK → JAR using dex2jar
Extension Loading Flow:
- Download APK from URL
ApkUtils.dex2jar()converts APK → JARExtensionLoader.loadClassFromJar()loads the main extension class- Cast to
AnimeHttpSourceorCatalogueSource - Store in
animeSources/mangaSourcesmaps
The Build Process Explained
When you type mvn clean package, here's exactly what happens, phase by phase, across both modules.
Module Build Order
Maven builds modules in dependency order:
- Parent POM (coordinates children, does not produce a JAR)
androidcompat(no internal dependencies)aniyomi-bridge(depends onandroidcompat)
androidcompat must finish its package phase before aniyomi-bridge can even start compiling.
Phase-by-Phase Breakdown
1. clean phase
Plugin: maven-clean-plugin
- Deletes
androidcompat/target/andaniyomi-bridge/target/ - Fresh slate. Any previously downloaded
android.jaror compiled classes are removed.
2. validate through process-sources
Plugins: maven-compiler-plugin, kotlin-maven-plugin (configuration only)
- Maven validates all POMs are well-formed
- Standard lifecycle phases proceed normally
3. generate-resources phase (androidcompat only)
Plugin: exec-maven-plugin
<phase>generate-resources</phase>
<goal>exec</goal>
- Maven runs
scripts/get-android.sh - The script:
- Downloads
android.jarfrom Sable/android-platforms (if not cached) - Verifies it's a valid JAR
- Uses
zip -dto delete classes that have custom stubs (e.g.,android.content.Context,android.app.Application) - Saves result as
androidcompat/target/patched-android.jar
- Downloads
Result: A stripped Android SDK JAR with "holes" where custom implementations will go.
4. process-resources phase (androidcompat only)
Plugin: maven-antrun-plugin
<phase>process-resources</phase>
- Maven runs the Ant task that unpacks
patched-android.jarintoandroidcompat/target/classes/ - This dumps ~4,000
.classfiles (the SDK stubs) into the build output directory
Result:
androidcompat/target/classes/now contains all Android SDK classes you didn't write custom stubs for.
5. compile phase
Plugins: maven-compiler-plugin + kotlin-maven-plugin
- Compiles custom Java stubs from
androidcompat/src/main/java/intoandroidcompat/target/classes/ - Compiles Kotlin sources too
- Critical: Compiled output goes to the same
target/classes/directory, so custom stubs overwrite the SDK classes that were unpacked there
Result:
target/classes/now contains:
- SDK classes (the ones you don't override)
- Your custom stubs (overwriting the SDK versions)
- Your project-specific classes
This is the hybrid magic — the directory contains a merged Android API.
6. process-classes through prepare-package
- Nothing special in this project
- Annotation processors or bytecode weaving would happen here if configured
7. package phase
Two different things happen per module:
In androidcompat:
Plugin: maven-jar-plugin
- Packages
target/classes/intoandroidcompat/target/androidcompat-1.0.0.jar - This JAR contains the merged Android API (SDK + custom stubs)
In aniyomi-bridge:
Plugins: maven-jar-plugin → maven-shade-plugin
-
maven-jar-plugin creates the "thin" JAR:
aniyomiBridge-1.0.0.jar -
maven-shade-plugin then runs:
- Takes the thin JAR
- Unpacks the
androidcompatdependency JAR into it - Unpacks all other dependencies (OkHttp, Kotlin stdlib, Gson, etc.)
- Applies filter rules:
minimizeJar=truestrips unused classes from dependencies
- A
<filter>forandroidcompatforces all its classes to be included (otherwise they'd be stripped because extensions load them dynamically, not via direct references in core code) - Excludes
Main.classfrom the final JAR (consumed by Flutter externally) - Excludes signing files (
*.SF,*.DSA,*.RSA) andmodule-info.class - Creates the fat JAR:
aniyomi-bridge/target/aniyomiBridge-1.0.0.jar
Result: A single ~90MB JAR containing your core code + the entire merged Android API + all dependencies.
Visual Timeline
$ mvn clean package
[MODULE: parent pom]
validate → compile → package (pom packaging, just validates)
[MODULE: androidcompat]
clean → delete target/
validate → check pom
...
generate-resources → exec-maven-plugin runs get-android.sh
↳ downloads & patches android.jar
process-resources → maven-antrun-plugin unpacks patched JAR
↳ target/classes/ ← 4,000 SDK .class files
compile → maven-compiler-plugin compiles custom stubs
↳ target/classes/ ← custom stubs OVERWRITE SDK versions
...
package → maven-jar-plugin creates androidcompat-1.0.0.jar
↳ contains merged Android API
[MODULE: aniyomi-bridge]
validate → check pom
...
compile → compile core sources (can reference androidcompat classes)
...
package → maven-jar-plugin creates thin JAR
→ maven-shade-plugin creates fat JAR
↳ unpacks androidcompat + all deps
↳ filters: include ALL androidcompat, exclude Main.class
↳ outputs aniyomiBridge-1.0.0.jar (~90MB)
Why This Order Matters
-
androidcompatmust build first —aniyomi-bridgedeclares it as a dependency, so Maven enforces this order automatically. -
generate-resourcesmust happen beforeprocess-resources— You can't unpack the JAR until it exists. The download script creates it duringgenerate-resources, then Ant unpacks it duringprocess-resources. -
Unpacking must happen before
compile— The compiler needs to see the Android SDK classes to resolve imports in your custom stubs. Ifmaven-antrun-pluginran aftercompile, compilation would fail with "cannot find symbol" for Android classes. -
Custom stubs overwrite SDK classes in
target/classes/— This is by design. The compiler writes to the same directory that Ant unpacked into, so yourContext.javaoverwrites the SDK'sContext.class. -
minimizeJar+ filter is critical — Without the shade filter forandroidcompat, Maven Shade would see thataniyomi-bridgenever directly referencesandroid.graphics.Bitmap(extensions load it via reflection/ClassLoader), so it would strip it out. The filter forces inclusion of the entire merged Android API.
Key Technologies
| Technology | Purpose |
|---|---|
| Android SDK JAR (patched) | Base Android API classes |
| dex2jar | Converts Android APK DEX → JAR |
| HtmlUnit / JavaFX WebView | Desktop WebView implementation |
| OkHttp + DoH | HTTP client for extensions |
| Kotlin Coroutines | Async operations |
| kotlinx-serialization-json | JSON handling |
| Gson | JSON preference storage |
| ByteBuddy | Runtime bytecode patching |
| Rhino / GraalVM JS | JavaScript execution |
Build Details
- Java Version: 17 (source and target)
- Kotlin Version: 2.1.0
- Build Tool: Maven (multi-module)
- Packaging: Shade plugin creates minimized fat JAR
Development Scripts
| Script | Purpose |
|---|---|
generate-new-core-linux.sh |
Build + copy JAR to Flutter app assets (Linux path) |
generate-new-core-mac.sh |
Build + copy JAR to Flutter app assets (Mac path) |
androidcompat/scripts/get-android.sh |
Download and patch Android SDK JAR |
Both build scripts copy the output to a sibling project unyo-lib/assets/.
IntelliJ IDEA Setup
See androidcompat/README.md for detailed IntelliJ setup instructions, including how to get code navigation working for the Android SDK stubs.
Quick version:
- Run
mvn clean compile -pl androidcompatonce to download the SDK - Right-click
androidcompat/pom.xml→ Maven → Reload Project - Ctrl+Click on Android classes to navigate between SDK stubs and custom implementations
Adding New Android Stubs
When an extension requires an Android class that doesn't exist or crashes:
- Identify which class needs a desktop implementation
- Create the stub in
androidcompat/src/main/java/android/<package>/ - Add the class path to
scripts/get-android.shin theCUSTOM_CLASSESarray - Rebuild with
mvn clean compile -pl androidcompat
See androidcompat/README.md for more details on the three options: minimal stub, full desktop implementation, or SDK class.
Troubleshooting
ClassNotFoundException for Android classes
- Check if the class is in the downloaded
android.jar - If not, create a stub in
src/main/java/android/... - Add the class to
CUSTOM_CLASSESinget-android.sh
MethodNotFoundError
- The Android SDK version might be too old
- Update
ANDROID_SDK_VERSIONinget-android.sh - Or create a custom stub with the required method
Build fails with "cannot find symbol android.***"
- The patched
android.jarhasn't been unpacked yet - Run
mvn clean compile -pl androidcompatfirst - Check that
androidcompat/target/classes/contains.classfiles
References
- Suwayomi's AndroidCompat — Inspiration for the hybrid stub approach
- Sable/android-platforms — Android SDK JAR source
- dex2jar — APK to JAR conversion
- Android SDK Reference