21 KiB
Unyo Core JNI Specification
Version: 2.0 (Post-Multi-Module Refactor)
Target: Flutter JNI integration forunyo-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
- Initialization
- Extension Lifecycle (Anime)
- Extension Lifecycle (Manga)
- Querying Loaded Extensions
- Preferences API
- Content Fetching (Anime)
- Content Fetching (Manga)
- Data Models
- Error Handling
- Breaking Changes from v1
- 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 callssetupPreferenceScreen()with a managedPreferenceScreen - Stores the source in an internal
ConcurrentHashMapkeyed bypackageName
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
Preferenceobjects on the source'sPreferenceScreen - 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 usingCatalogueSource/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
Videoclass containsokhttp3.Headerswhich is not serializable by default. If you need to pass video data across JNI/Flutter isolates, consider usingSerializableVideo(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
packageNameto 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)