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

21 KiB

Unyo Core JNI Specification

Version: 2.0 (Post-Multi-Module Refactor)
Target: Flutter JNI integration for unyo-core-1.0.0.jar
Main Class: dev.k3vinb5.unyo.core.AniyomiBridge


Overview

This document specifies the public Java API exposed by AniyomiBridge.java for consumption via JNI from Flutter. It covers every method, its signature, the data models that cross the JNI boundary, and the behavioral changes introduced during the multi-module refactor.

Key architectural fact: AniyomiBridge is a singleton-like stateful object. It maintains internal maps of loaded anime/manga sources keyed by packageName. You create one instance and call methods on it. All source lookup is done internally via these maps.


Table of Contents

  1. Initialization
  2. Extension Lifecycle (Anime)
  3. Extension Lifecycle (Manga)
  4. Querying Loaded Extensions
  5. Preferences API
  6. Content Fetching (Anime)
  7. Content Fetching (Manga)
  8. Data Models
  9. Error Handling
  10. Breaking Changes from v1
  11. JNI Implementation Notes

1. Initialization

static void init()

Must be called once before any other operation. Performs global JVM-level setup:

  • Installs ByteBuddy agent for runtime bytecode patching
  • Initializes Injekt dependency injection module

JNI signature:

(env)->CallStaticVoidMethod(aniyomiBridgeClass, initMethod);

Thread safety: Safe to call from any thread, but should only be called once. Not synchronized externally — caller must ensure single invocation.

Throws: Nothing directly, but ByteBuddy installation can fail if the JVM doesn't support instrumentation.


2. Extension Lifecycle (Anime)

void loadAnimeExtension(String extensionUrl, String basePath, String packageName)

Downloads an APK from extensionUrl, converts it to JAR via dex2jar, loads the main extension class, and registers it as an AnimeHttpSource.

Parameter Description
extensionUrl Full HTTPS URL to the .apk file
basePath Local directory path where the converted .jar will be cached
packageName The Android package name (e.g., eu.kanade.tachiyomi.animeextension.en.animepahe). Used as the internal key for all subsequent lookups.

Behavior:

  • Downloads APK if not already present
  • Converts APK → JAR using dex2jar
  • Inspects JAR to find the main extension class
  • Loads class via isolated URLClassLoader
  • Applies ByteBuddy patches to the extension's JVM classes
  • If the source implements ConfigurableAnimeSource, automatically calls setupPreferenceScreen() with a managed PreferenceScreen
  • Stores the source in an internal ConcurrentHashMap keyed by packageName

Throws: UnyoCoreException on any failure (download, dex2jar, class loading, casting).


void unloadAnimeExtension(String packageName)

Unloads an anime extension and cleans up resources.

Parameter Description
packageName The same packageName passed to loadAnimeExtension

Behavior:

  • Closes the extension's URLClassLoader
  • Deletes the cached JAR file from disk
  • Removes the source from internal maps

Throws: UnyoCoreException on cleanup failure.


3. Extension Lifecycle (Manga)

void loadMangaExtension(String extensionUrl, String basePath, String packageName)

Identical to loadAnimeExtension but for manga sources implementing CatalogueSource.

Parameter Description
extensionUrl Full HTTPS URL to the .apk file
basePath Local directory path where the converted .jar will be cached
packageName Android package name, used as internal key

Behavior: Same as anime version, but casts to CatalogueSource and calls ConfigurableSource.setupPreferenceScreen() if applicable.

Throws: UnyoCoreException


void unloadMangaExtension(String packageName)

Unloads a manga extension.

Parameter Description
packageName The same packageName passed to loadMangaExtension

Throws: UnyoCoreException


4. Querying Loaded Extensions

List<String> getAnimeLoadedExtensions()

Returns a list of loaded anime extension package names.

Return: java.util.List<java.lang.String> — package names of all currently loaded anime sources.


List<String> getMangaLoadedExtensions()

Returns a list of loaded manga extension package names.

Return: java.util.List<java.lang.String> — package names of all currently loaded manga sources.


5. Preferences API

New in v2. Previously, preferences were not exposed to Flutter. Now they are fully readable and writable.

When a ConfigurableAnimeSource or ConfigurableSource is loaded, AniyomiBridge automatically calls setupPreferenceScreen(screen) and creates a PreferenceScreen backed by file-based SharedPreferences (stored under ~/.aniyomibridge/shared_prefs/<source-id>.xml).

List<PreferenceItem> getAnimePreferences(String packageName)

Reads all preferences registered by an anime extension.

Parameter Description
packageName Package name of the loaded anime extension

Return: java.util.List<PreferenceItem>

Behavior:

  • Returns empty list if source is not configurable
  • Iterates all Preference objects on the source's PreferenceScreen
  • Reads current value from SharedPreferences (or default if never set)
  • Returns typed values based on preference type

Supported preference types:

Type Java Type Value Type
SwitchPreferenceCompat Boolean true / false
ListPreference String Selected entry value
EditTextPreference String User input text
MultiSelectListPreference Set<String> Selected values
Preference (fallback) null No value

void setAnimePreference(String packageName, String key, Object value)

Sets a single preference value for an anime extension.

Parameter Description
packageName Package name of the loaded anime extension
key Preference key (from getAnimePreferences)
value Value to set. Must match the expected type: Boolean, String, or Set<String>

Throws: UnyoCoreException if source not configurable, key not found, or type mismatch.


void setAnimePreferences(String packageName, Map<String, Object> values)

Batch-updates multiple preferences for an anime extension.

Parameter Description
packageName Package name of the loaded anime extension
values java.util.Map of key → value pairs

Behavior: Iterates the map and calls setAnimePreference logic for each entry. Fails fast on first error.

Throws: UnyoCoreException

Note: Manga preferences API is not yet exposed in AniyomiBridge.java. If needed, the same pattern applies using CatalogueSource / ConfigurableSource.


6. Content Fetching (Anime)

All content-fetching methods are synchronous blocking calls that internally run Kotlin coroutines via runBlocking. Call them from background threads in Flutter.

List<SAnime> getAnimeSearchResults(String query, int page, String packageName)

Searches for anime using the specified extension.

Parameter Description
query Search query string
page Page number (1-based)
packageName Package name of the loaded anime extension

Return: java.util.List<SAnime>

Throws: UnyoCoreException wrapping any network or parsing error.


List<SEpisode> getEpisodeList(SAnime anime, String packageName)

Fetches the episode list for a given anime.

Parameter Description
anime SAnime object (from search results or previous call)
packageName Package name of the loaded anime extension

Return: java.util.List<SEpisode>

Throws: UnyoCoreException


List<Video> getVideoList(SEpisode episode, String packageName)

Fetches video URLs/metadata for a given episode.

Parameter Description
episode SEpisode object
packageName Package name of the loaded anime extension

Return: java.util.List<Video>

Throws: UnyoCoreException

Important: The Video class contains okhttp3.Headers which is not serializable by default. If you need to pass video data across JNI/Flutter isolates, consider using SerializableVideo (see Data Models).


7. Content Fetching (Manga)

List<SManga> getMangaSearchResults(String query, int page, String packageName)

Searches for manga.

Parameter Description
query Search query string
page Page number (1-based)
packageName Package name of the loaded manga extension

Return: java.util.List<SManga>

Throws: UnyoCoreException


List<SChapter> getChapterList(SManga manga, String packageName)

Fetches chapter list for a manga.

Parameter Description
manga SManga object
packageName Package name of the loaded manga extension

Return: java.util.List<SChapter>

Throws: UnyoCoreException


List<Page> getPageList(SChapter chapter, String packageName)

Fetches image page URLs for a chapter.

Parameter Description
chapter SChapter object
packageName Package name of the loaded manga extension

Return: java.util.List<Page>

Throws: UnyoCoreException


8. Data Models

PreferenceItem

Package: dev.k3vinb5.unyo.core.PreferenceItem

Fields:

Field Type Description
key String Unique preference identifier
title String Human-readable title
type String One of: SwitchPreferenceCompat, ListPreference, EditTextPreference, MultiSelectListPreference, Preference
value Object Current value (Boolean, String, or Set<String>)
entries List<String> Display labels (for List/MultiSelect)
entryValues List<String> Internal values (for List/MultiSelect)
summary String Optional description text

JNI tip: Access fields directly (they are public). No getters/setters.


SAnime

Package: eu.kanade.tachiyomi.animesource.model.SAnime

Interface fields (all public):

Field Type Notes
url String Relative or absolute URL to the anime page
title String Display title
artist String? Nullable
author String? Nullable
description String? Nullable
genre String? Nullable; comma-separated genres
status int UNKNOWN=0, ONGOING=1, COMPLETED=2, LICENSED=3, PUBLISHING_FINISHED=4, CANCELLED=5, ON_HIATUS=6
thumbnail_url String? Cover image URL
update_strategy AnimeUpdateStrategy Enum
fetch_type FetchType Enum
season_number double Season number
initialized boolean Whether full metadata has been fetched

Method: List<String> getGenres() — parses genre string into a list.


SEpisode

Package: eu.kanade.tachiyomi.animesource.model.SEpisode

Interface fields:

Field Type Notes
url String Episode page URL
name String Episode title/number
date_upload long Unix timestamp (ms)
episode_number float Numeric episode number
scanlator String? Nullable

Video

Package: eu.kanade.tachiyomi.animesource.model.Video

Fields:

Field Type Notes
videoUrl String Direct video stream URL
videoTitle String Quality label (e.g., "1080p")
resolution Integer? Nullable resolution in pixels
bitrate Integer? Nullable bitrate
headers okhttp3.Headers? HTTP headers required for playback
preferred boolean Whether this is the preferred quality
subtitleTracks List<Track> Subtitle tracks
audioTracks List<Track> Audio tracks
timestamps List<TimeStamp> Chapter/intro markers
mpvArgs List<Pair<String,String>> MPV player arguments
ffmpegStreamArgs List<Pair<String,String>> FFmpeg stream arguments
ffmpegVideoArgs List<Pair<String,String>> FFmpeg video arguments
internalData String Opaque internal data from extractor
initialized boolean Whether video metadata is resolved

Deprecation notice: Old extensions may use quality (maps to videoTitle) and url (maps to internal videoPageUrl). New code should use videoTitle and videoUrl.


SManga

Package: eu.kanade.tachiyomi.source.model.SManga

Identical structure to SAnime but without fetch_type, season_number, and update_strategy is UpdateStrategy instead of AnimeUpdateStrategy.


SChapter

Package: eu.kanade.tachiyomi.source.model.SChapter

Identical structure to SEpisode but chapter_number instead of episode_number.


Page

Package: eu.kanade.tachiyomi.source.model.Page

Fields:

Field Type Notes
index int Zero-based page index
url String Page URL (may be relative)
imageUrl String? Resolved direct image URL
number int Human-readable page number (index + 1)

9. Error Handling

UnyoCoreException

Package: dev.k3vinb5.unyo.core.exceptions.UnyoCoreException

Extends RuntimeException.

Constructors:

  • UnyoCoreException(String message)
  • UnyoCoreException(String message, Throwable cause)

Behavior: Every public method in AniyomiBridge wraps failures in UnyoCoreException with the full Java stack trace in the message string. When catching via JNI, you get both the user-friendly message and the developer stack trace.

Recommended Flutter handling:

// Pseudocode — depends on your JNI bindings
try {
  final results = bridge.getAnimeSearchResults(query, page, packageName);
} on JniException catch (e) {
  // e.message contains both the summary and the full Java stack trace
  log.error(e.message);
}

10. Breaking Changes from v1

If your Flutter code was written against the pre-refactor API (before multi-module split), these are the critical changes:

1. Extension keys changed from extensionName to packageName

Before:

loadAnimeExtension(String extensionUrl, String basePath)
// Key was extracted from URL: "animepahe", "torrentio", etc.

unloadAnimeExtension(String extensionName, String version, String basePath)
// You passed extensionName + version, bridge constructed filename

After:

loadAnimeExtension(String extensionUrl, String basePath, String packageName)
// You pass the full Android package name as key

unloadAnimeExtension(String packageName)
// Just packageName. Bridge tracks the JAR path internally.

Migration:

  • Stop extracting extension names from URLs
  • Use the Android package name (from the extension repo JSON) as the key everywhere
  • Update your Flutter model to pass packageName to all methods

2. getAnimeSearchResults / getMangaSearchResults signature change

Before:

getAnimeSearchResults(String query, int page, String animeSource)
// 'animeSource' was the extension name

After:

getAnimeSearchResults(String query, int page, String packageName)
// 'packageName' is the full package name

Migration: Pass packageName instead of extension name.

3. Content fetching methods signature change

Before:

getEpisodeList(SAnime anime, String animeSource)
getVideoList(SEpisode episode, String animeSource)

After:

getEpisodeList(SAnime anime, String packageName)
getVideoList(SEpisode episode, String packageName)

Migration: Same as above — replace animeSource string with packageName.

4. Removal of correctedNames map

Before: The bridge internally mapped names like "zoro" → "hianime" and "aniwatch" → "aniwatchtv".

After: No internal name correction. You must use the exact packageName.

Migration: Remove any logic in Flutter that relied on these aliases. Use package names directly from the repo metadata.

5. Unload methods no longer take version or basePath

Before:

unloadAnimeExtension(String extensionName, String version, String basePath)

After:

unloadAnimeExtension(String packageName)

Migration: Simpler — just pass packageName.

6. Preferences API is new

Before: No way to read/write extension preferences from Flutter.

After: Full getAnimePreferences, setAnimePreference, setAnimePreferences API.

Migration: If your app previously couldn't handle configurable sources, now you can/should expose these preferences in your UI.

7. MediaSourceUtils now uses per-source filters

Before:

animeSource.getSearchAnime(page, query, filters = defaultAnimeFilterList)

After:

animeSource.getSearchAnime(page, query, filters = animeSource.getFilterList())

Impact: Search results may differ if an extension had custom filters. This is generally more correct but could change behavior for extensions that rely on non-default filter states.


11. JNI Implementation Notes

Class Loading

The fat JAR (unyo-core-1.0.0.jar) contains everything including the Android SDK stubs, Kotlin stdlib, and all dependencies. Load it with a standard URLClassLoader or from the classpath.

Important: The Main.class (dev.k3vinb5.unyo.core.cli.Main) is excluded from the fat JAR (consumed by external Flutter app). The class you instantiate is dev.k3vinb5.unyo.core.AniyomiBridge.

Threading

All content-fetching methods use kotlinx.coroutines.runBlocking internally. They will block the calling thread. In Flutter, always call these methods through a background isolate or compute() / Isolate.run().

The internal maps (animeSources, mangaSources, classLoaders) use ConcurrentHashMap, so load* and unload* are thread-safe. However, you should not load and unload the same extension concurrently.

Object Lifetimes

SAnime, SEpisode, SManga, SChapter are Serializable. You can safely pass them across JNI boundaries and cache them in Dart if needed.

Video contains okhttp3.Headers which is not serializable. If you need to serialize video data (e.g., for Dart isolates or JSON), convert Headers to a Map<String, List<String>> or use the SerializableVideo Kotlin helper class.

Memory

Each loaded extension gets its own URLClassLoader. When you call unload*Extension, the classloader is closed and the JAR file is deleted. This should release most memory, but some classes may remain in the JVM's metaspace until GC.

Logging

The bridge uses JBoss Logging. Logs go to stdout/stderr by default. In a Flutter desktop app, you'll see them in the console. Consider redirecting java.util.logging or JBoss LogManager to your Dart logging system if you need structured logs.


Appendix: Quick Reference — Method Signatures

// Initialization
public static void init()

// Anime lifecycle
public void loadAnimeExtension(String extensionUrl, String basePath, String packageName)
public void unloadAnimeExtension(String packageName)

// Manga lifecycle
public void loadMangaExtension(String extensionUrl, String basePath, String packageName)
public void unloadMangaExtension(String packageName)

// Query loaded
public List<String> getAnimeLoadedExtensions()
public List<String> getMangaLoadedExtensions()

// Preferences (anime only, currently)
public List<PreferenceItem> getAnimePreferences(String packageName)
public void setAnimePreference(String packageName, String key, Object value)
public void setAnimePreferences(String packageName, Map<String, Object> values)

// Anime content
public List<SAnime> getAnimeSearchResults(String query, int page, String packageName)
public List<SEpisode> getEpisodeList(SAnime anime, String packageName)
public List<Video> getVideoList(SEpisode episode, String packageName)

// Manga content
public List<SManga> getMangaSearchResults(String query, int page, String packageName)
public List<SChapter> getChapterList(SManga manga, String packageName)
public List<Page> getPageList(SChapter chapter, String packageName)