From ca950dc35ae8b44228595c066d2b22fe99efd3b5 Mon Sep 17 00:00:00 2001 From: zarzet <42882290+zarzet@users.noreply.github.com> Date: Sun, 18 Oct 2026 22:25:04 +0700 Subject: [PATCH] refactor(android): remove backend switches that became constants Rust is the only backend, so several single-backend switches had become constants and their other branches could never run: - CoreBackend.supportsOutputDescriptors was always false. The descriptor branch in SafDownloadHandler and its pfd/detachFd cleanup never ran, and the Rust planner rejects output_fd anyway. - routesApplication was always true on Android and iOS. Drop the member, the "go" value of getBackendImplementations (the reply keys and the "rust" value Dart receives are unchanged), the iOS routing guard and the interface defaults that threw for unrouted methods. - cleanupExtensions(), waitForDownloadProgressDelta and scanCueForLibrary had no production caller. Dart reaches cleanup through the cleanupExtensions channel method, and RustCoreBackend overrides openDownloadProgress, so the polling default goes too. The UniFFI exports DownloadState.wait_progress_delta and ExtensionManager.scan_cue_file_for_library were used only by those Kotlin overrides. Bindings are generated at build time by scripts/build_rust_backend.sh; no generated binding is tracked. --- .../kotlin/com/zarz/spotiflac/CoreBackend.kt | 15 +------ .../kotlin/com/zarz/spotiflac/MainActivity.kt | 2 +- .../com/zarz/spotiflac/SafDownloadHandler.kt | 39 ++++++------------- .../com/zarz/spotiflac/SelectedCoreBackend.kt | 14 ------- ios/Runner/AppDelegate.swift | 4 +- ios/Runner/CoreBackend.swift | 13 ------- rust_backend/crates/mobile/src/progress.rs | 11 ------ rust_backend/crates/mobile/src/tags.rs | 25 ------------ 8 files changed, 16 insertions(+), 107 deletions(-) diff --git a/android/app/src/main/kotlin/com/zarz/spotiflac/CoreBackend.kt b/android/app/src/main/kotlin/com/zarz/spotiflac/CoreBackend.kt index 37ba7236..a2618434 100644 --- a/android/app/src/main/kotlin/com/zarz/spotiflac/CoreBackend.kt +++ b/android/app/src/main/kotlin/com/zarz/spotiflac/CoreBackend.kt @@ -163,13 +163,7 @@ internal fun withCoreFFmpegExecution( /** Native migration boundary; each process selects one stateful backend. */ internal interface CoreBackend { val implementation: String - val routesApplication: Boolean get() = false - val supportsOutputDescriptors: Boolean get() = false - fun invokeApplication(method: String, arguments: Any?): Any? = - error("Application routing is unavailable for $method") - fun cleanupExtensions() { - invokeApplication("cleanupExtensions", null) - } + fun invokeApplication(method: String, arguments: Any?): Any? fun setRuntimeState(dataDirectory: String, payload: String) { invokeApplication( "prepareRuntimeState", @@ -179,11 +173,7 @@ internal interface CoreBackend { fun completeAuthCallback(state: String, code: String, sessionGrant: Boolean, onResolved: (String) -> Unit) fun downloadByStrategy(requestJson: String): String = withCoreFFmpegExecution(openExtensionExecution()) { it.download(requestJson) } - fun waitForDownloadProgressDelta(since: Long, timeoutMs: Long): String - fun openDownloadProgress(): CoreDownloadProgress = object : CoreDownloadProgress { - override fun waitDelta(since: Long, timeoutMs: Long) = waitForDownloadProgressDelta(since, timeoutMs) - override fun close() {} - } + fun openDownloadProgress(): CoreDownloadProgress fun initItemProgress(itemId: String) fun clearItemProgress(itemId: String) fun cancelDownload(itemId: String) @@ -207,7 +197,6 @@ internal interface CoreBackend { fun resumeLibraryScan() fun parseCueSheet(path: String, audioDirectory: String): String fun parseCueSheetWithResolvedAudio(path: String, audioPath: String): String - fun scanCueForLibrary(path: String, audioDirectory: String, virtualPrefix: String, modTime: Long, cacheKey: String): String fun scanCueForLibraryWithResolvedAudio(path: String, audioPath: String, audioName: String, virtualPrefix: String, modTime: Long, cacheKey: String): String fun editFileMetadata(path: String, metadataJson: String): String fun reEnrichFile(requestJson: String): String diff --git a/android/app/src/main/kotlin/com/zarz/spotiflac/MainActivity.kt b/android/app/src/main/kotlin/com/zarz/spotiflac/MainActivity.kt index 53e35776..dd9c31d4 100644 --- a/android/app/src/main/kotlin/com/zarz/spotiflac/MainActivity.kt +++ b/android/app/src/main/kotlin/com/zarz/spotiflac/MainActivity.kt @@ -1169,7 +1169,7 @@ class MainActivity: FlutterFragmentActivity() { mapOf( "filename" to coreBackend.implementation, "file_metadata" to coreBackend.fileMetadataImplementation(call.argument("file_path") ?: ""), - "extensions" to if (coreBackend.routesApplication) "rust" else "go", + "extensions" to coreBackend.implementation, "downloads" to coreBackend.implementation, ) ) diff --git a/android/app/src/main/kotlin/com/zarz/spotiflac/SafDownloadHandler.kt b/android/app/src/main/kotlin/com/zarz/spotiflac/SafDownloadHandler.kt index a983d968..f05db018 100644 --- a/android/app/src/main/kotlin/com/zarz/spotiflac/SafDownloadHandler.kt +++ b/android/app/src/main/kotlin/com/zarz/spotiflac/SafDownloadHandler.kt @@ -185,30 +185,19 @@ object SafDownloadHandler { var document = createOrReuseDocumentFile(context, targetDir, stagedMimeType, stagedFileName) ?: return errorJson("Failed to create SAF file") - var pfd: android.os.ParcelFileDescriptor? = null - var detachedFd: Int? = null var workingFile: File? = null try { - if (backend.supportsOutputDescriptors) { - val descriptor = context.contentResolver.openFileDescriptor(document.uri, "rw") - ?: throw IllegalStateException("Failed to open SAF file") - pfd = descriptor - detachedFd = descriptor.detachFd() - req.put("output_path", "") - req.put("output_fd", detachedFd) - } else { - // The OS adapter retains descriptor ownership. A path-based - // backend writes into its granted staging directory, then the - // existing SAF copy/promotion publishes the completed output. - val staged = backend.createTemporaryMediaFile( - context, - "native_saf_work_", - outputExt.ifBlank { ".tmp" }, - ) - workingFile = staged - req.put("output_path", staged.absolutePath) - req.remove("output_fd") - } + // The OS adapter retains descriptor ownership. The backend writes + // into its granted staging directory, then the existing SAF + // copy/promotion publishes the completed output. + val staged = backend.createTemporaryMediaFile( + context, + "native_saf_work_", + outputExt.ifBlank { ".tmp" }, + ) + workingFile = staged + req.put("output_path", staged.absolutePath) + req.remove("output_fd") req.put("output_ext", outputExt) val response = backend.downloadByStrategy(req.toString()) val respObj = JSONObject(response) @@ -301,12 +290,6 @@ object SafDownloadHandler { return errorJson("SAF download failed: ${e.message}") } finally { workingFile?.delete() - if (detachedFd == null) { - try { - pfd?.close() - } catch (_: Exception) { - } - } } } diff --git a/android/app/src/rust/kotlin/com/zarz/spotiflac/SelectedCoreBackend.kt b/android/app/src/rust/kotlin/com/zarz/spotiflac/SelectedCoreBackend.kt index 18b93bed..ce9a3778 100644 --- a/android/app/src/rust/kotlin/com/zarz/spotiflac/SelectedCoreBackend.kt +++ b/android/app/src/rust/kotlin/com/zarz/spotiflac/SelectedCoreBackend.kt @@ -62,7 +62,6 @@ internal suspend fun MainActivity.dispatchBackendApplication(call: MethodCall, r internal object RustCoreBackend : CoreBackend { override val implementation = "rust" - override val routesApplication = true private lateinit var workspace: NativeMediaWorkspace private var manager: ExtensionManager? = null private var repository: ExtensionRepository? = null @@ -395,14 +394,6 @@ internal object RustCoreBackend : CoreBackend { } } - override fun scanCueForLibrary(path: String, audioDirectory: String, virtualPrefix: String, modTime: Long, cacheKey: String): String { - val cue = File(path).canonicalFile - val audio = if (audioDirectory.isEmpty()) cue.parentFile!! else File(audioDirectory).canonicalFile - return withLibraryDirectories(listOf(cue.parent, audio.path)) { - it.scanCueFileForLibrary(cue.path, audio.path, virtualPrefix, modTime, cacheKey, java.time.Instant.now().toString(), null) - } - } - override fun scanCueForLibraryWithResolvedAudio(path: String, audioPath: String, audioName: String, virtualPrefix: String, modTime: Long, cacheKey: String): String { val cue = File(path).canonicalFile fun scan(descriptor: Int): String { @@ -539,11 +530,6 @@ internal object RustCoreBackend : CoreBackend { } } - override fun waitForDownloadProgressDelta(since: Long, timeoutMs: Long): String { - val current = owner() - return current.withDownloadState { state -> state.waitProgressDelta(since, timeoutMs) } - } - override fun openDownloadProgress(): CoreDownloadProgress { val current = owner() val subscription = current.environment().use { environment -> diff --git a/ios/Runner/AppDelegate.swift b/ios/Runner/AppDelegate.swift index 073d1ba6..b6b3f182 100644 --- a/ios/Runner/AppDelegate.swift +++ b/ios/Runner/AppDelegate.swift @@ -277,7 +277,7 @@ import UniformTypeIdentifiers result(nil) return } - if coreBackend.routesApplication && !osMethods.contains(call.method) { + if !osMethods.contains(call.method) { if call.method == "cancelNativeDataJob" { do { result(try coreBackend.invokeApplication(method: call.method, arguments: call.arguments)) @@ -484,7 +484,7 @@ import UniformTypeIdentifiers return [ "filename": coreBackend.implementation, "file_metadata": coreBackend.fileMetadataImplementation(path: (call.arguments as? [String: Any])?["file_path"] as? String ?? ""), - "extensions": coreBackend.routesApplication ? "rust" : "go", + "extensions": coreBackend.implementation, "downloads": coreBackend.implementation, ] diff --git a/ios/Runner/CoreBackend.swift b/ios/Runner/CoreBackend.swift index b6f821ce..07e5861b 100644 --- a/ios/Runner/CoreBackend.swift +++ b/ios/Runner/CoreBackend.swift @@ -154,7 +154,6 @@ final class CoreDirectoryScope { /// Native migration boundary; each process selects one stateful backend. protocol CoreBackend { var implementation: String { get } - var routesApplication: Bool { get } func prepareApplicationArguments(method: String, arguments: Any?) throws -> Any? func invokeApplication(method: String, arguments: Any?) throws -> Any? func completeAuthCallback(state: String, code: String, sessionGrant: Bool, onResolved: (String) -> Void) throws @@ -189,21 +188,9 @@ protocol CoreBackend { func releaseMemoryUnderPressure() throws } -extension CoreBackend { - var routesApplication: Bool { false } - - func prepareApplicationArguments(method: String, arguments: Any?) throws -> Any? { arguments } - - func invokeApplication(method: String, arguments: Any?) throws -> Any? { - throw NSError(domain: "CoreBackend", code: 1, userInfo: [NSLocalizedDescriptionKey: "Application routing is unavailable for \(method)"]) - } -} - - final class RustCoreBackend: CoreBackend { static let shared = RustCoreBackend() let implementation = "rust" - let routesApplication = true private let root = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask)[0].appendingPathComponent("rust-core-pilot", isDirectory: true) private let ownerLock = NSLock() private var manager: ExtensionManager? diff --git a/rust_backend/crates/mobile/src/progress.rs b/rust_backend/crates/mobile/src/progress.rs index c9c0d018..71962302 100644 --- a/rust_backend/crates/mobile/src/progress.rs +++ b/rust_backend/crates/mobile/src/progress.rs @@ -100,17 +100,6 @@ impl DownloadState { self.inner.progress.snapshot().map_err(Into::into) } - pub fn wait_progress_delta( - &self, - since: i64, - timeout_ms: i64, - ) -> Result { - self.inner - .progress - .wait_delta(since, timeout_ms) - .map_err(Into::into) - } - pub fn subscribe_progress( &self, ) -> Result, DownloadProgressError> { diff --git a/rust_backend/crates/mobile/src/tags.rs b/rust_backend/crates/mobile/src/tags.rs index 90b39b8b..2e8e2f10 100644 --- a/rust_backend/crates/mobile/src/tags.rs +++ b/rust_backend/crates/mobile/src/tags.rs @@ -126,31 +126,6 @@ impl ExtensionManager { .map_err(ExtensionManagerError::Operation) } - #[allow(clippy::too_many_arguments)] - pub fn scan_cue_file_for_library( - &self, - path: String, - audio_directory: String, - virtual_prefix: String, - mod_time: i64, - cache_key: String, - scan_time: String, - lease: Option>, - ) -> Result { - self.inner - .scan_cue_file_for_library( - &path, - &audio_directory, - &virtual_prefix, - mod_time, - &cache_key, - &scan_time, - &|| check_lease(lease.as_deref()), - ) - .map(|value| value.to_string()) - .map_err(ExtensionManagerError::Operation) - } - pub fn scan_library_folder_incremental( &self, folder: String,