From f808a6a369a37df5240bee553e7a70905b109ef1 Mon Sep 17 00:00:00 2001 From: zarzet Date: Fri, 10 Jul 2026 10:22:39 +0700 Subject: [PATCH] fix(android): stop double-destroying the shared audio_service engine MainActivity only overrode provideFlutterEngine, so the activity's fragment delegate treated the plugin-cached engine as its own (createFlutterFragment forwards shouldDestroyEngineWithHost, default true) and destroyed it when the activity finished, while the engine stayed registered in AudioServicePlugin's FlutterEngineCache. When AudioService stopped afterwards, disposeFlutterEngine() called destroy() on the already destroyed engine and crashed the app with "Cannot execute operation because FlutterJNI is not attached to native". Mirror audio_service's own AudioServiceFragmentActivity: expose the plugin engine as a cached engine (getCachedEngineId), never destroy it with the host (shouldDestroyEngineWithHost = false), and pre-create it in onCreate. The plugin remains the engine's sole owner and disposes it only once, when the service stops with no activity attached. --- .../kotlin/com/zarz/spotiflac/MainActivity.kt | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) 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 53d71a58..96aa8518 100644 --- a/android/app/src/main/kotlin/com/zarz/spotiflac/MainActivity.kt +++ b/android/app/src/main/kotlin/com/zarz/spotiflac/MainActivity.kt @@ -38,10 +38,24 @@ import java.security.MessageDigest import java.util.Locale class MainActivity: FlutterFragmentActivity() { + // Mirrors audio_service's AudioServiceFragmentActivity: the shared engine + // is owned by AudioServicePlugin's FlutterEngineCache. Without the + // cached-engine-id + shouldDestroyEngineWithHost overrides, the activity + // (via createFlutterFragment's destroyEngineWithFragment) destroys the + // provided engine on exit while it stays registered in the cache; when + // AudioService later stops, disposeFlutterEngine() destroys it a second + // time and crashes with "FlutterJNI is not attached to native". override fun provideFlutterEngine(context: Context): FlutterEngine { return AudioServicePlugin.getFlutterEngine(context) } + override fun getCachedEngineId(): String { + AudioServicePlugin.getFlutterEngine(this) + return AudioServicePlugin.getFlutterEngineId() + } + + override fun shouldDestroyEngineWithHost(): Boolean = false + private val CHANNEL = "com.zarz.spotiflac/backend" private val DOWNLOAD_PROGRESS_STREAM_CHANNEL = "com.zarz.spotiflac/download_progress_stream" @@ -2060,6 +2074,9 @@ class MainActivity: FlutterFragmentActivity() { override fun shouldHandleDeeplinking(): Boolean = false override fun onCreate(savedInstanceState: Bundle?) { + // Ensure the shared audio_service engine exists before the activity + // delegate looks it up by cached id (see getCachedEngineId above). + AudioServicePlugin.getFlutterEngine(this) super.onCreate(savedInstanceState) handleExtensionOAuthIntent(intent) }