Files
unyo-core/README.md
Kevin Rodrigues Borges e336275363 Changed name to unyo-core
2026-05-05 00:49:27 +01:00

12 KiB

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:

  1. Base SDK: Downloads the official Android SDK android.jar from Sable/android-platforms
  2. Custom Stubs: Implements desktop-specific behavior for classes that need it (e.g., Context, WebView, SharedPreferences)
  3. 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 extensions
  • ExtensionLoader.java — Loads extension classes from JAR files
  • ApkUtils.java — Converts Android APK → JAR using dex2jar

Extension Loading Flow:

  1. Download APK from URL
  2. ApkUtils.dex2jar() converts APK → JAR
  3. ExtensionLoader.loadClassFromJar() loads the main extension class
  4. Cast to AnimeHttpSource or CatalogueSource
  5. Store in animeSources/mangaSources maps

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:

  1. Parent POM (coordinates children, does not produce a JAR)
  2. androidcompat (no internal dependencies)
  3. aniyomi-bridge (depends on androidcompat)

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/ and aniyomi-bridge/target/
  • Fresh slate. Any previously downloaded android.jar or 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:
    1. Downloads android.jar from Sable/android-platforms (if not cached)
    2. Verifies it's a valid JAR
    3. Uses zip -d to delete classes that have custom stubs (e.g., android.content.Context, android.app.Application)
    4. Saves result as androidcompat/target/patched-android.jar

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.jar into androidcompat/target/classes/
  • This dumps ~4,000 .class files (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/ into androidcompat/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/ into androidcompat/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

  1. maven-jar-plugin creates the "thin" JAR: aniyomiBridge-1.0.0.jar

  2. maven-shade-plugin then runs:

    • Takes the thin JAR
    • Unpacks the androidcompat dependency JAR into it
    • Unpacks all other dependencies (OkHttp, Kotlin stdlib, Gson, etc.)
    • Applies filter rules:
      • minimizeJar=true strips unused classes from dependencies
    • A <filter> for androidcompat forces 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.class from the final JAR (consumed by Flutter externally)
    • Excludes signing files (*.SF, *.DSA, *.RSA) and module-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

  1. androidcompat must build first — aniyomi-bridge declares it as a dependency, so Maven enforces this order automatically.

  2. generate-resources must happen before process-resources — You can't unpack the JAR until it exists. The download script creates it during generate-resources, then Ant unpacks it during process-resources.

  3. Unpacking must happen before compile — The compiler needs to see the Android SDK classes to resolve imports in your custom stubs. If maven-antrun-plugin ran after compile, compilation would fail with "cannot find symbol" for Android classes.

  4. Custom stubs overwrite SDK classes in target/classes/ — This is by design. The compiler writes to the same directory that Ant unpacked into, so your Context.java overwrites the SDK's Context.class.

  5. minimizeJar + filter is critical — Without the shade filter for androidcompat, Maven Shade would see that aniyomi-bridge never directly references android.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:

  1. Run mvn clean compile -pl androidcompat once to download the SDK
  2. Right-click androidcompat/pom.xml → Maven → Reload Project
  3. 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:

  1. Identify which class needs a desktop implementation
  2. Create the stub in androidcompat/src/main/java/android/<package>/
  3. Add the class path to scripts/get-android.sh in the CUSTOM_CLASSES array
  4. 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_CLASSES in get-android.sh

MethodNotFoundError

  • The Android SDK version might be too old
  • Update ANDROID_SDK_VERSION in get-android.sh
  • Or create a custom stub with the required method

Build fails with "cannot find symbol android.***"

  • The patched android.jar hasn't been unpacked yet
  • Run mvn clean compile -pl androidcompat first
  • Check that androidcompat/target/classes/ contains .class files

References