From ae3e135b6ffdb3ce90f1d319fc4cbf8ca877d38d Mon Sep 17 00:00:00 2001 From: KhooLy <73142442+KhooLy@users.noreply.github.com> Date: Sat, 18 Jul 2026 15:01:38 +0300 Subject: [PATCH] Retire FluxaCoreNative's hand-written JNI in favor of core_invoke Every stateless method on FluxaCoreNative now calls the corresponding core_invoke route via FluxaCoreUniFfi instead of a hand-written external fun; only the handle-lifecycle functions (headless engine, app core state) keep their dedicated JNI bindings. NuvioCoreBridge and ExternalLibraryClient's generic coreInvoke calls move onto the same UniFFI path. Public method signatures are unchanged, so no other call site needed to change. --- .../app/ui/catalog/HomeLibraryCoordinator.kt | 4 +- .../ui/catalog/HomeSearchFocusStateHolder.kt | 6 +- .../com/fluxa/app/ui/catalog/HomeViewModel.kt | 18 +- .../HomeViewModelCoordinatorFactory.kt | 4 +- .../fluxa/app/ui/catalog/PlayerScreenState.kt | 7 +- .../app/core/rust/FluxaCoreBenchmarkTest.kt | 6 +- .../core/rust/FluxaCoreUniFfiContractTest.kt | 9 + .../fluxa/app/core/rust/FluxaCoreNative.kt | 1577 ++++++++--------- .../data/repository/ExternalLibraryClient.kt | 98 +- .../app/data/repository/NuvioCoreBridge.kt | 61 + 10 files changed, 938 insertions(+), 852 deletions(-) create mode 100644 data/src/androidMain/kotlin/com/fluxa/app/data/repository/NuvioCoreBridge.kt diff --git a/app/src/main/java/com/fluxa/app/ui/catalog/HomeLibraryCoordinator.kt b/app/src/main/java/com/fluxa/app/ui/catalog/HomeLibraryCoordinator.kt index 83c5c5d..6fe19ab 100644 --- a/app/src/main/java/com/fluxa/app/ui/catalog/HomeLibraryCoordinator.kt +++ b/app/src/main/java/com/fluxa/app/ui/catalog/HomeLibraryCoordinator.kt @@ -1,7 +1,7 @@ package com.fluxa.app.ui.catalog import android.util.Log -import com.fluxa.app.core.rust.FluxaCoreStateHandle +import com.fluxa.app.core.rust.FluxaUniFfiCoreStateHandle import com.fluxa.app.data.local.* import com.fluxa.app.data.local.UserProfile import com.fluxa.app.data.remote.Meta @@ -36,7 +36,7 @@ internal class HomeLibraryCoordinator( private val repository: StremioRepository, private val traktRepository: TraktRepository, private val scope: CoroutineScope, - private val coreState: FluxaCoreStateHandle, + private val coreState: FluxaUniFfiCoreStateHandle, private val gson: Gson ) { private val _state = MutableStateFlow(LibraryUiState()) diff --git a/app/src/main/java/com/fluxa/app/ui/catalog/HomeSearchFocusStateHolder.kt b/app/src/main/java/com/fluxa/app/ui/catalog/HomeSearchFocusStateHolder.kt index d23fa83..7f598a9 100644 --- a/app/src/main/java/com/fluxa/app/ui/catalog/HomeSearchFocusStateHolder.kt +++ b/app/src/main/java/com/fluxa/app/ui/catalog/HomeSearchFocusStateHolder.kt @@ -1,7 +1,7 @@ package com.fluxa.app.ui.catalog -import com.fluxa.app.core.rust.FluxaCoreNative -import com.fluxa.app.core.rust.FluxaCoreStateHandle +import com.fluxa.app.core.rust.FluxaCoreUniFfi +import com.fluxa.app.core.rust.FluxaUniFfiCoreStateHandle import com.fluxa.app.data.remote.Meta import com.google.gson.Gson import java.io.Closeable @@ -11,7 +11,7 @@ import kotlinx.coroutines.flow.asStateFlow class HomeSearchFocusStateHolder( initialHistory: List, - private val coreState: FluxaCoreStateHandle = FluxaCoreNative.createAppCoreState( + private val coreState: FluxaUniFfiCoreStateHandle = FluxaCoreUniFfi.createAppCoreState( mapOf( "homeSearch" to mapOf( "searchHistory" to initialHistory diff --git a/app/src/main/java/com/fluxa/app/ui/catalog/HomeViewModel.kt b/app/src/main/java/com/fluxa/app/ui/catalog/HomeViewModel.kt index 10cc57c..85ee1cc 100644 --- a/app/src/main/java/com/fluxa/app/ui/catalog/HomeViewModel.kt +++ b/app/src/main/java/com/fluxa/app/ui/catalog/HomeViewModel.kt @@ -6,7 +6,8 @@ import com.fluxa.app.data.remote.* import com.fluxa.app.data.repository.* import com.fluxa.app.core.rust.FluxaAndroidHeadlessEnvironment import com.fluxa.app.core.rust.FluxaCoreNative -import com.fluxa.app.core.rust.FluxaCoreStateHandle +import com.fluxa.app.core.rust.FluxaCoreUniFfi +import com.fluxa.app.core.rust.FluxaUniFfiCoreStateHandle import com.fluxa.app.core.rust.FluxaHeadlessRuntimeFactory import com.fluxa.app.domain.discovery.DiscoverCatalogOption import com.fluxa.app.domain.discovery.MetadataFeedOption @@ -71,7 +72,7 @@ class HomeViewModel @Inject constructor( private val addonListType = object : TypeToken>() {}.type private val headlessRuntime = FluxaHeadlessRuntimeFactory.createUniFfi(headlessEnvironment) private val initialSearchHistory = searchHistoryStore.load(null) - private val coreState: FluxaCoreStateHandle = FluxaCoreNative.createAppCoreState( + private val coreState: FluxaUniFfiCoreStateHandle = FluxaCoreUniFfi.createAppCoreState( mapOf( "home" to mapOf( "categories" to emptyList(), @@ -143,6 +144,7 @@ class HomeViewModel @Inject constructor( scope = viewModelScope, gson = gson, dispatch = ::dispatchHeadless, + activeProfile = { currentActiveProfile }, setActiveProfile = { setActiveProfileState(it) }, setWatchlist = ::setWatchlistState, setContinueWatching = ::setCurrentWatchlistState, @@ -237,6 +239,16 @@ class HomeViewModel @Inject constructor( private var currentWatchlist: List = emptyList() private val _currentContinueWatchingCount = MutableStateFlow(0) val currentContinueWatchingCount: StateFlow = _currentContinueWatchingCount.asStateFlow() + private val _syncingProviders = MutableStateFlow>(emptySet()) + val syncingProviders: StateFlow> = _syncingProviders.asStateFlow() + + fun setProviderSyncing(provider: String, syncing: Boolean) { + _syncingProviders.value = if (syncing) { + _syncingProviders.value + provider + } else { + _syncingProviders.value - provider + } + } private var externalContinueWatching: List = emptyList() private var traktWatchedState: TraktWatchedState = TraktWatchedState() private var currentActiveProfile: UserProfile? = null @@ -1086,7 +1098,7 @@ class HomeViewModel @Inject constructor( cacheSaveJob?.cancel() cacheSaveJob = viewModelScope.launch(Dispatchers.Default) { delay(600) - homeCategoryCache.save(profile, categories) + homeCategoryCache.save(profile, categories.filterNot { it.id == "continue_watching" }) } } diff --git a/app/src/main/java/com/fluxa/app/ui/catalog/HomeViewModelCoordinatorFactory.kt b/app/src/main/java/com/fluxa/app/ui/catalog/HomeViewModelCoordinatorFactory.kt index cd62bb1..3c086d8 100644 --- a/app/src/main/java/com/fluxa/app/ui/catalog/HomeViewModelCoordinatorFactory.kt +++ b/app/src/main/java/com/fluxa/app/ui/catalog/HomeViewModelCoordinatorFactory.kt @@ -1,7 +1,7 @@ package com.fluxa.app.ui.catalog import android.content.Context -import com.fluxa.app.core.rust.FluxaCoreStateHandle +import com.fluxa.app.core.rust.FluxaUniFfiCoreStateHandle import com.fluxa.app.data.local.UserProfile import com.fluxa.app.data.local.WatchlistManager import com.fluxa.app.data.remote.AddonDescriptor @@ -19,7 +19,7 @@ class HomeViewModelCoordinatorFactory @Inject constructor() { repository: StremioRepository, traktRepository: TraktRepository, scope: CoroutineScope, - coreState: FluxaCoreStateHandle, + coreState: FluxaUniFfiCoreStateHandle, gson: Gson ): HomeLibraryCoordinator { return HomeLibraryCoordinator(repository, traktRepository, scope, coreState, gson) diff --git a/app/src/main/java/com/fluxa/app/ui/catalog/PlayerScreenState.kt b/app/src/main/java/com/fluxa/app/ui/catalog/PlayerScreenState.kt index cda62e4..93b500c 100644 --- a/app/src/main/java/com/fluxa/app/ui/catalog/PlayerScreenState.kt +++ b/app/src/main/java/com/fluxa/app/ui/catalog/PlayerScreenState.kt @@ -11,8 +11,8 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.media3.ui.AspectRatioFrameLayout -import com.fluxa.app.core.rust.FluxaCoreNative -import com.fluxa.app.core.rust.FluxaCoreStateHandle +import com.fluxa.app.core.rust.FluxaCoreUniFfi +import com.fluxa.app.core.rust.FluxaUniFfiCoreStateHandle import com.fluxa.app.data.remote.IntroTimestamps import com.fluxa.app.data.remote.Stream import com.fluxa.app.data.remote.Video @@ -39,7 +39,7 @@ internal class PlayerScreenState( initialVolume: Int ) { private val gson = Gson() - private val coreState: FluxaCoreStateHandle = FluxaCoreNative.createAppCoreState( + private val coreState: FluxaUniFfiCoreStateHandle = FluxaCoreUniFfi.createAppCoreState( mapOf( "player" to mapOf( "currentVideoId" to initialVideoId, @@ -111,7 +111,6 @@ internal class PlayerScreenState( var showSettings by mutableStateOf(false) var activeSettingsTab by mutableIntStateOf(0) - var inlineDelayTarget by mutableStateOf(null) var audioDelayMs by mutableLongStateOf(0L) var subtitleDelayMs by mutableLongStateOf(0L) diff --git a/app/src/test/java/com/fluxa/app/core/rust/FluxaCoreBenchmarkTest.kt b/app/src/test/java/com/fluxa/app/core/rust/FluxaCoreBenchmarkTest.kt index ca8a7e4..eea1a13 100644 --- a/app/src/test/java/com/fluxa/app/core/rust/FluxaCoreBenchmarkTest.kt +++ b/app/src/test/java/com/fluxa/app/core/rust/FluxaCoreBenchmarkTest.kt @@ -407,11 +407,11 @@ class FluxaCoreBenchmarkTest { !name.contains("cacheTrimPolicy", ignoreCase = true) } assertTrue(nativeCacheStorageFunctions.isEmpty()) - assertTrue(nativeFunctionNames.any { it == "cacheEntryPolicyJsonNative" }) - assertTrue(nativeFunctionNames.any { it == "cacheTrimPolicyJsonNative" }) + assertTrue(nativeFunctionNames.any { it == "cacheEntryPolicy" }) + assertTrue(nativeFunctionNames.any { it == "cacheTrimPolicy" }) assertFalse(nativeFunctionNames.any { it == "startLocalStreamServerNative" }) assertFalse(nativeFunctionNames.any { it == "stopLocalStreamServerNative" }) - assertTrue(nativeFunctionNames.any { it == "parseManifestJsonNative" }) + assertTrue(nativeFunctionNames.any { it == "coreInvokeNative" }) } private fun startStaticServer(payload: ByteArray, requestCount: AtomicInteger): StaticByteServer { diff --git a/app/src/test/java/com/fluxa/app/core/rust/FluxaCoreUniFfiContractTest.kt b/app/src/test/java/com/fluxa/app/core/rust/FluxaCoreUniFfiContractTest.kt index 9f54fcc..4839020 100644 --- a/app/src/test/java/com/fluxa/app/core/rust/FluxaCoreUniFfiContractTest.kt +++ b/app/src/test/java/com/fluxa/app/core/rust/FluxaCoreUniFfiContractTest.kt @@ -34,4 +34,13 @@ class FluxaCoreUniFfiContractTest { assertEquals("Movie", meta["name"]) } } + + @Test + fun coreInvokeRoutesCalendarMethodThroughUniFfi() { + val value = FluxaCoreUniFfi.coreInvokeValue( + "calendarSeasonCandidates", + """{"seasonsCount":10,"lastVideoId":"tt1:2:3"}""" + ) + assertEquals("[2,3,10]", value.toString()) + } } diff --git a/data/src/androidMain/kotlin/com/fluxa/app/core/rust/FluxaCoreNative.kt b/data/src/androidMain/kotlin/com/fluxa/app/core/rust/FluxaCoreNative.kt index 0dd156d..90758de 100644 --- a/data/src/androidMain/kotlin/com/fluxa/app/core/rust/FluxaCoreNative.kt +++ b/data/src/androidMain/kotlin/com/fluxa/app/core/rust/FluxaCoreNative.kt @@ -69,6 +69,8 @@ import com.google.gson.stream.JsonReader import java.io.Closeable import java.io.StringReader +private fun urlArgs(url: String): String = JsonObject().apply { addProperty("url", url) }.toString() + private fun JsonObject.stringOrNull(key: String): String? = get(key)?.takeUnless { it.isJsonNull }?.asString @@ -211,13 +213,6 @@ data class NativeCloudstreamRequest( val timeoutMs: Long = 0L ) -// ── calendar_plan ───────────────────────────────────────────────────────────── - -data class NativeCalendarNotificationContent( - val items: List> = emptyList(), - val keys: List = emptyList() -) - data class NativeDetailSeasonLoadPlan( val firstSeasonToLoad: Int = 1, val savedSeason: Int? = null @@ -599,10 +594,10 @@ object FluxaCoreNative { }.getOrDefault(emptyMap()) } - fun coreCapabilities(portable: Boolean = false): NativeCoreCapabilitySet = call { - val json = coreCapabilitiesJsonNative(portable) - json.takeIf { it.isNotBlank() }?.let { gson.fromJson(it, NativeCoreCapabilitySet::class.java) } - ?: NativeCoreCapabilitySet() + fun coreCapabilities(portable: Boolean = false): NativeCoreCapabilitySet { + val args = JsonObject().apply { addProperty("portable", portable) } + val value = FluxaCoreUniFfi.coreInvokeValue("coreCapabilities", args.toString()) + return gson.fromJson(value, NativeCoreCapabilitySet::class.java) ?: NativeCoreCapabilitySet() } internal fun appCoreStateJson(handle: Long): String = call { @@ -621,53 +616,60 @@ object FluxaCoreNative { coreInvokeNative(method, argsJson).orEmpty() } - fun normalizeManifestUrl(rawUrl: String): String = call { normalizeManifestUrlNative(rawUrl) } + fun normalizeManifestUrl(rawUrl: String): String = + FluxaCoreUniFfi.coreInvokeValue("normalizeManifestUrl", urlArgs(rawUrl)).asString - fun identity(rawUrl: String): String = call { identityNative(rawUrl) } + fun identity(rawUrl: String): String = + FluxaCoreUniFfi.coreInvokeValue("identity", urlArgs(rawUrl)).asString - fun manifestCandidates(rawUrl: String): List = call { - manifestCandidatesJsonNative(rawUrl) - }.let { json -> - gson.fromJson>(json, stringListType) + fun manifestCandidates(rawUrl: String): List { + val value = FluxaCoreUniFfi.coreInvokeValue("manifestCandidates", urlArgs(rawUrl)) + return gson.fromJson(value, stringListType) ?: emptyList() } - fun manifestFetchPlan(rawUrl: String): NativeManifestFetchPlan? = call { - manifestFetchPlanJsonNative(rawUrl) - ?.takeIf { it.isNotBlank() } - ?.let { gson.fromJson(it, NativeManifestFetchPlan::class.java) } + fun manifestFetchPlan(rawUrl: String): NativeManifestFetchPlan? { + val value = FluxaCoreUniFfi.coreInvokeValue("manifestFetchPlan", urlArgs(rawUrl)) + return value.takeUnless { it.isJsonNull }?.let { gson.fromJson(it, NativeManifestFetchPlan::class.java) } } - fun baseUrl(rawUrl: String): String = call { baseUrlNative(rawUrl) } + fun baseUrl(rawUrl: String): String = + FluxaCoreUniFfi.coreInvokeValue("baseUrl", urlArgs(rawUrl)).asString fun preferHttpsAssetUrl(rawUrl: String): String? { - val resolved = call { - preferHttpsAssetUrlNative(rawUrl).takeIf { it.isNotBlank() } - } - return resolved + val value = FluxaCoreUniFfi.coreInvokeValue("preferHttpsAssetUrl", urlArgs(rawUrl)) + return value.takeUnless { it.isJsonNull }?.asString?.takeIf { it.isNotBlank() } } - fun addonStoreInputType(text: String): String = call { - addonStoreInputTypeNative(text) - } + fun addonStoreInputType(text: String): String = + FluxaCoreUniFfi.coreInvokeValue( + "addonStoreInputType", + JsonObject().apply { addProperty("input", text) }.toString() + ).asString - fun normalizeCloudstreamRepoUrl(rawUrl: String): String = call { - normalizeCloudstreamRepoUrlNative(rawUrl) - } + fun normalizeCloudstreamRepoUrl(rawUrl: String): String = + FluxaCoreUniFfi.coreInvokeValue("normalizeCloudstreamRepoUrl", urlArgs(rawUrl)).asString - fun normalizePluginRepositoryUrl(rawUrl: String): String = call { - normalizePluginRepositoryUrlNative(rawUrl) - } + fun normalizePluginRepositoryUrl(rawUrl: String): String = + FluxaCoreUniFfi.coreInvokeValue("normalizePluginRepositoryUrl", urlArgs(rawUrl)).asString - fun pluginIsSecureRemoteUrl(url: String): Boolean = call { - pluginIsSecureRemoteUrlNative(url) - } + fun pluginIsSecureRemoteUrl(url: String): Boolean = + FluxaCoreUniFfi.coreInvokeValue("isSecureRemoteUrl", urlArgs(url)).asBoolean - fun pluginSameRepositoryUrl(left: String, right: String): Boolean = call { - pluginSameRepositoryUrlNative(left, right) - } + fun pluginSameRepositoryUrl(left: String, right: String): Boolean = + FluxaCoreUniFfi.coreInvokeValue( + "samePluginRepositoryUrl", + JsonObject().apply { + addProperty("left", left) + addProperty("right", right) + }.toString() + ).asBoolean - fun extractAddonManifestUrl(detailText: String): String? = call { - extractAddonManifestUrlNative(detailText).takeIf { it.isNotBlank() } + fun extractAddonManifestUrl(detailText: String): String? { + val value = FluxaCoreUniFfi.coreInvokeValue( + "extractAddonManifestUrl", + JsonObject().apply { addProperty("text", detailText) }.toString() + ) + return value.takeUnless { it.isJsonNull }?.asString?.takeIf { it.isNotBlank() } } fun addonStoreSearchPolicy( @@ -675,54 +677,52 @@ object FluxaCoreNative { nowMillis: Long, cachedAtMillis: Long?, ttlMillis: Long - ): NativeAddonStoreSearchPolicy = call { + ): NativeAddonStoreSearchPolicy { val request = NativeAddonStoreSearchPolicyRequest( query = query, nowMillis = nowMillis, cachedAtMillis = cachedAtMillis, ttlMillis = ttlMillis ) - gson.fromJson( - addonStoreSearchPolicyJsonNative(gson.toJson(request)), - NativeAddonStoreSearchPolicy::class.java - ) ?: NativeAddonStoreSearchPolicy() + val value = FluxaCoreUniFfi.coreInvokeValue("addonStoreSearchPolicy", gson.toJson(request)) + return gson.fromJson(value, NativeAddonStoreSearchPolicy::class.java) ?: NativeAddonStoreSearchPolicy() } fun repositoryMetaDetailPlan( useConfiguredAddons: Boolean, authKey: String?, localAddons: List? - ): NativeRepositoryMetaDetailPlan = call { + ): NativeRepositoryMetaDetailPlan { val request = NativeRepositoryMetaDetailPlanRequest( useConfiguredAddons = useConfiguredAddons, authKey = authKey.orEmpty(), localAddons = localAddons.orEmpty() ) - gson.fromJson( - repositoryMetaDetailPlanJsonNative(gson.toJson(request)), - NativeRepositoryMetaDetailPlan::class.java - ) ?: NativeRepositoryMetaDetailPlan() + val value = FluxaCoreUniFfi.coreInvokeValue("repositoryMetaDetailPlan", gson.toJson(request)) + return gson.fromJson(value, NativeRepositoryMetaDetailPlan::class.java) ?: NativeRepositoryMetaDetailPlan() } - fun repositorySeasonVideos(metaDetail: MetaDetail?, seasonNumber: Int): List