From edbb3750ee1646e3e1aac3fef600cb2c62166ebe Mon Sep 17 00:00:00 2001 From: David <86196556+DH-555@users.noreply.github.com> Date: Thu, 17 Jul 2025 13:10:34 +0200 Subject: [PATCH 1/7] Update MethodCallHelper.kt --- .../com/icapps/background_location_tracker/MethodCallHelper.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/android/src/main/kotlin/com/icapps/background_location_tracker/MethodCallHelper.kt b/android/src/main/kotlin/com/icapps/background_location_tracker/MethodCallHelper.kt index b694ad9..8f38473 100644 --- a/android/src/main/kotlin/com/icapps/background_location_tracker/MethodCallHelper.kt +++ b/android/src/main/kotlin/com/icapps/background_location_tracker/MethodCallHelper.kt @@ -105,6 +105,7 @@ internal class MethodCallHelper(private val ctx: Context) : MethodChannel.Method private fun stopTracking(ctx: Context, call: MethodCall, result: MethodChannel.Result) { serviceConnection.service?.stopTracking() + FlutterBackgroundManager.cleanup() result.success(true) } From bf61b8598f6ea34d101a3513c3b704713e4daa08 Mon Sep 17 00:00:00 2001 From: David <86196556+DH-555@users.noreply.github.com> Date: Thu, 17 Jul 2025 13:11:33 +0200 Subject: [PATCH 2/7] Update FlutterBackgroundManager.kt --- .../flutter/FlutterBackgroundManager.kt | 65 +++++++++++++++---- 1 file changed, 51 insertions(+), 14 deletions(-) diff --git a/android/src/main/kotlin/com/icapps/background_location_tracker/flutter/FlutterBackgroundManager.kt b/android/src/main/kotlin/com/icapps/background_location_tracker/flutter/FlutterBackgroundManager.kt index 0f64dfa..45ae361 100644 --- a/android/src/main/kotlin/com/icapps/background_location_tracker/flutter/FlutterBackgroundManager.kt +++ b/android/src/main/kotlin/com/icapps/background_location_tracker/flutter/FlutterBackgroundManager.kt @@ -19,24 +19,54 @@ internal object FlutterBackgroundManager { private const val BACKGROUND_CHANNEL_NAME = "com.icapps.background_location_tracker/background_channel" private val flutterLoader = FlutterLoader() + private var backgroundChannel: MethodChannel? = null + private var isInitialized = false + private var pendingLocation: Location? = null private fun getInitializedFlutterEngine(ctx: Context): FlutterEngine { - Logger.debug("BackgroundManager", "Creating new engine") + Logger.debug("BackgroundManager", "Getting Flutter engine") return BackgroundLocationTrackerPlugin.getFlutterEngine(ctx) } fun sendLocation(ctx: Context, location: Location) { Logger.debug("BackgroundManager", "Location: ${location.latitude}: ${location.longitude}") - val engine = getInitializedFlutterEngine(ctx) + + if (isInitialized) { + // Engine is already initialized, send location immediately + sendLocationToChannel(ctx, location) + } else { + // Store the location and initialize if needed + pendingLocation = location + setupBackgroundChannelIfNeeded(ctx) + } + } + + private fun setupBackgroundChannelIfNeeded(ctx: Context) { + if (backgroundChannel != null) { + return // Already setup + } - val backgroundChannel = MethodChannel(engine.dartExecutor, BACKGROUND_CHANNEL_NAME) - backgroundChannel.setMethodCallHandler { call, result -> + Logger.debug("BackgroundManager", "Setting up background channel and dart executor") + val engine = getInitializedFlutterEngine(ctx) + + backgroundChannel = MethodChannel(engine.dartExecutor, BACKGROUND_CHANNEL_NAME) + backgroundChannel?.setMethodCallHandler { call, result -> when (call.method) { - "initialized" -> handleInitialized(call, result, ctx, backgroundChannel, location, engine) + "initialized" -> { + Logger.debug("BackgroundManager", "Dart background isolate initialized") + isInitialized = true + result.success(true) + + // Send any pending location + pendingLocation?.let { location -> + Logger.debug("BackgroundManager", "Sending pending location after initialization") + sendLocationToChannel(ctx, location) + pendingLocation = null + } + } else -> { result.notImplemented() - engine.destroy() } } } @@ -52,7 +82,10 @@ internal object FlutterBackgroundManager { } } - private fun handleInitialized(call: MethodCall, result: MethodChannel.Result, ctx: Context, channel: MethodChannel, location: Location, engine: FlutterEngine) { + private fun sendLocationToChannel(ctx: Context, location: Location) { + val channel = backgroundChannel ?: return + Logger.debug("BackgroundManager", "Sending location to initialized channel") + val data = mutableMapOf() data["lat"] = location.latitude data["lon"] = location.longitude @@ -73,19 +106,23 @@ internal object FlutterBackgroundManager { channel.invokeMethod("onLocationUpdate", data, object : MethodChannel.Result { override fun success(result: Any?) { - Logger.debug("BackgroundManager", "Got success, destroy engine!") - engine.destroy() + Logger.debug("BackgroundManager", "Successfully sent location update") } override fun error(errorCode: String, errorMessage: String?, errorDetails: Any?) { - Logger.debug("BackgroundManager", "Got error, destroy engine! $errorCode - $errorMessage : $errorDetails") - engine.destroy() + Logger.debug("BackgroundManager", "Error sending location update: $errorCode - $errorMessage : $errorDetails") } override fun notImplemented() { - Logger.debug("BackgroundManager", "Got not implemented, destroy engine!") - engine.destroy() + Logger.debug("BackgroundManager", "Method not implemented for location update") } }) } -} \ No newline at end of file + + fun cleanup() { + Logger.debug("BackgroundManager", "Cleaning up background resources") + isInitialized = false + backgroundChannel = null + pendingLocation = null + } +} From d4a8afc539082ad02d2a912b9b9f2c3a84153bab Mon Sep 17 00:00:00 2001 From: David <86196556+DH-555@users.noreply.github.com> Date: Thu, 17 Jul 2025 13:32:17 +0200 Subject: [PATCH 3/7] Update CHANGELOG.md --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2e89801..8a864bb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,6 @@ +## 1.6.1 - 2025-05-06 +- Fixes callback being called once in Android (#91) + ## 1.6.0 - 2025-05-06 - Updated to use the modern FlutterPlugin.FlutterPluginBinding pattern - Deprecated Registrar, PluginRegistrantCallback, and ShimPluginRegistry usage From 71a06d382903d901de67700ae5cb7119d32d47d0 Mon Sep 17 00:00:00 2001 From: David <86196556+DH-555@users.noreply.github.com> Date: Thu, 17 Jul 2025 13:33:15 +0200 Subject: [PATCH 4/7] Update CHANGELOG.md --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8a864bb..53d5557 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,4 @@ -## 1.6.1 - 2025-05-06 +## 1.6.1 - 2025-07-17 - Fixes callback being called once in Android (#91) ## 1.6.0 - 2025-05-06 From 4c3c47e16b651d11bf7948b0a625761b89e13be9 Mon Sep 17 00:00:00 2001 From: David <86196556+DH-555@users.noreply.github.com> Date: Thu, 17 Jul 2025 13:38:51 +0200 Subject: [PATCH 5/7] Update CHANGELOG.md --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 53d5557..e601ad2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,5 @@ ## 1.6.1 - 2025-07-17 -- Fixes callback being called once in Android (#91) +- Fixes callback being called once in Android (improved handling) (#91) ## 1.6.0 - 2025-05-06 - Updated to use the modern FlutterPlugin.FlutterPluginBinding pattern From 4f27045514173b7ec8b94876755c9d07b209aaa9 Mon Sep 17 00:00:00 2001 From: David Date: Thu, 17 Jul 2025 16:33:22 +0200 Subject: [PATCH 6/7] Fix FlutterJNI.loadLibrary called more than once Added aditional cleanup for the flutter engine and darExecutor + destrying so it's not re-loaded. Also, added logic to avoid unnecesary reload and reuse instead --- .../BackgroundLocationTrackerPlugin.kt | 43 ++++++++++- .../flutter/FlutterBackgroundManager.kt | 73 ++++++++++++++++++- .../service/LocationUpdatesService.kt | 3 + example/pubspec.lock | 18 ++--- pubspec.lock | 16 ++-- 5 files changed, 129 insertions(+), 24 deletions(-) diff --git a/android/src/main/kotlin/com/icapps/background_location_tracker/BackgroundLocationTrackerPlugin.kt b/android/src/main/kotlin/com/icapps/background_location_tracker/BackgroundLocationTrackerPlugin.kt index 0b6593a..b1c25b7 100644 --- a/android/src/main/kotlin/com/icapps/background_location_tracker/BackgroundLocationTrackerPlugin.kt +++ b/android/src/main/kotlin/com/icapps/background_location_tracker/BackgroundLocationTrackerPlugin.kt @@ -42,6 +42,9 @@ class BackgroundLocationTrackerPlugin : FlutterPlugin, MethodCallHandler, Activi channel?.setMethodCallHandler(null) channel = null applicationContext = null + + // Clean up the background manager when plugin is detached + FlutterBackgroundManager.cleanup() } override fun onMethodCall(call: MethodCall, result: Result) { @@ -73,6 +76,7 @@ class BackgroundLocationTrackerPlugin : FlutterPlugin, MethodCallHandler, Activi // New static properties for background execution private var flutterEngine: FlutterEngine? = null + private var isEngineInitialized = false // For compatibility with older plugins @Deprecated("Use FlutterEngine's plugin registry instead") @@ -87,9 +91,42 @@ class BackgroundLocationTrackerPlugin : FlutterPlugin, MethodCallHandler, Activi // Method to get or create the Flutter engine for background execution @JvmStatic fun getFlutterEngine(context: Context): FlutterEngine { - return flutterEngine ?: FlutterEngine(context).also { - flutterEngine = it - pluginRegistrantCallback?.invoke(it) + synchronized(this) { + if (flutterEngine == null || !isEngineInitialized) { + Logger.debug(TAG, "Creating new Flutter engine for background execution") + flutterEngine = FlutterEngine(context).also { engine -> + pluginRegistrantCallback?.invoke(engine) + isEngineInitialized = true + Logger.debug(TAG, "Flutter engine created and initialized") + } + } else { + Logger.debug(TAG, "Reusing existing Flutter engine") + } + return flutterEngine!! + } + } + + // Method to properly cleanup the Flutter engine + @JvmStatic + fun cleanupFlutterEngine() { + synchronized(this) { + flutterEngine?.let { engine -> + try { + Logger.debug(TAG, "Cleaning up Flutter engine") + // Stop the Dart isolate if it's running + if (engine.dartExecutor.isExecutingDart) { + engine.dartExecutor.onDetachedFromJNI() + } + // Destroy the engine + engine.destroy() + Logger.debug(TAG, "Flutter engine destroyed") + } catch (e: Exception) { + // Log the exception but don't crash + Logger.debug(TAG, "Error during Flutter engine cleanup: ${e.message}") + } + flutterEngine = null + isEngineInitialized = false + } } } } diff --git a/android/src/main/kotlin/com/icapps/background_location_tracker/flutter/FlutterBackgroundManager.kt b/android/src/main/kotlin/com/icapps/background_location_tracker/flutter/FlutterBackgroundManager.kt index 45ae361..c655d9d 100644 --- a/android/src/main/kotlin/com/icapps/background_location_tracker/flutter/FlutterBackgroundManager.kt +++ b/android/src/main/kotlin/com/icapps/background_location_tracker/flutter/FlutterBackgroundManager.kt @@ -22,10 +22,10 @@ internal object FlutterBackgroundManager { private var backgroundChannel: MethodChannel? = null private var isInitialized = false private var pendingLocation: Location? = null + private var isSettingUp = false private fun getInitializedFlutterEngine(ctx: Context): FlutterEngine { Logger.debug("BackgroundManager", "Getting Flutter engine") - return BackgroundLocationTrackerPlugin.getFlutterEngine(ctx) } @@ -43,19 +43,61 @@ internal object FlutterBackgroundManager { } private fun setupBackgroundChannelIfNeeded(ctx: Context) { - if (backgroundChannel != null) { - return // Already setup + if (backgroundChannel != null || isSettingUp) { + Logger.debug("BackgroundManager", "Setup already in progress or completed") + return // Already setup or in progress } + isSettingUp = true Logger.debug("BackgroundManager", "Setting up background channel and dart executor") val engine = getInitializedFlutterEngine(ctx) + // Check if the DartExecutor is already running to prevent the error + if (engine.dartExecutor.isExecutingDart) { + Logger.debug("BackgroundManager", "DartExecutor is already running, reusing existing executor") + backgroundChannel = MethodChannel(engine.dartExecutor, BACKGROUND_CHANNEL_NAME) + backgroundChannel?.setMethodCallHandler { call, result -> + when (call.method) { + "initialized" -> { + Logger.debug("BackgroundManager", "Dart background isolate already initialized") + isInitialized = true + isSettingUp = false + result.success(true) + + // Send any pending location + pendingLocation?.let { location -> + Logger.debug("BackgroundManager", "Sending pending location after reuse") + sendLocationToChannel(ctx, location) + pendingLocation = null + } + } + else -> { + result.notImplemented() + } + } + } + + // Mark as initialized since the executor is already running + isInitialized = true + isSettingUp = false + + // Send any pending location immediately if we have one + pendingLocation?.let { location -> + Logger.debug("BackgroundManager", "Sending pending location immediately (executor already running)") + sendLocationToChannel(ctx, location) + pendingLocation = null + } + + return + } + backgroundChannel = MethodChannel(engine.dartExecutor, BACKGROUND_CHANNEL_NAME) backgroundChannel?.setMethodCallHandler { call, result -> when (call.method) { "initialized" -> { Logger.debug("BackgroundManager", "Dart background isolate initialized") isInitialized = true + isSettingUp = false result.success(true) // Send any pending location @@ -72,13 +114,22 @@ internal object FlutterBackgroundManager { } if (!flutterLoader.initialized()) { + Logger.debug("BackgroundManager", "Initializing FlutterLoader") flutterLoader.startInitialization(ctx) } + flutterLoader.ensureInitializationCompleteAsync(ctx, null, Handler(Looper.getMainLooper())) { + Logger.debug("BackgroundManager", "FlutterLoader initialization complete, executing Dart callback") val callbackHandle = SharedPrefsUtil.getCallbackHandle(ctx) val callbackInfo = FlutterCallbackInformation.lookupCallbackInformation(callbackHandle) val dartBundlePath = flutterLoader.findAppBundlePath() - engine.dartExecutor.executeDartCallback(DartExecutor.DartCallback(ctx.assets, dartBundlePath, callbackInfo)) + + try { + engine.dartExecutor.executeDartCallback(DartExecutor.DartCallback(ctx.assets, dartBundlePath, callbackInfo)) + } catch (e: Exception) { + Logger.debug("BackgroundManager", "Error executing Dart callback: ${e.message}") + isSettingUp = false + } } } @@ -122,7 +173,21 @@ internal object FlutterBackgroundManager { fun cleanup() { Logger.debug("BackgroundManager", "Cleaning up background resources") isInitialized = false + isSettingUp = false + backgroundChannel?.setMethodCallHandler(null) backgroundChannel = null pendingLocation = null + + // Instead of destroying the engine completely, just mark it as not initialized + // This allows for reuse without the "DartExecutor already running" error + Logger.debug("BackgroundManager", "Background channel cleaned up, ready for reuse") + } + + fun forceCleanup() { + Logger.debug("BackgroundManager", "Force cleaning up all background resources") + cleanup() + + // Clean up the Flutter engine completely to prevent DartExecutor reuse issues + BackgroundLocationTrackerPlugin.cleanupFlutterEngine() } } diff --git a/android/src/main/kotlin/com/icapps/background_location_tracker/service/LocationUpdatesService.kt b/android/src/main/kotlin/com/icapps/background_location_tracker/service/LocationUpdatesService.kt index 64b766a..6b29bcd 100644 --- a/android/src/main/kotlin/com/icapps/background_location_tracker/service/LocationUpdatesService.kt +++ b/android/src/main/kotlin/com/icapps/background_location_tracker/service/LocationUpdatesService.kt @@ -160,6 +160,9 @@ internal class LocationUpdatesService : Service() { if (wakeLock?.isHeld == true) { wakeLock?.release() } + + // Force clean up Flutter background resources when service is destroyed + FlutterBackgroundManager.forceCleanup() } /** diff --git a/example/pubspec.lock b/example/pubspec.lock index 8a8390f..130284a 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -13,17 +13,17 @@ packages: dependency: transitive description: name: async - sha256: d2872f9c19731c2e5f10444b14686eb7cc85c76274bd6c16e1816bff9a3bab63 + sha256: "758e6d74e971c3e5aceb4110bfd6698efc7f501675bcfe0c775459a8140750eb" url: "https://pub.dev" source: hosted - version: "2.12.0" + version: "2.13.0" background_location_tracker: dependency: "direct main" description: path: ".." relative: true source: path - version: "1.5.0" + version: "1.6.0" boolean_selector: dependency: transitive description: @@ -68,10 +68,10 @@ packages: dependency: transitive description: name: fake_async - sha256: "6a95e56b2449df2273fd8c45a662d6947ce1ebb7aafe80e550a3f68297f3cacc" + sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44" url: "https://pub.dev" source: hosted - version: "1.3.2" + version: "1.3.3" ffi: dependency: transitive description: @@ -155,10 +155,10 @@ packages: dependency: transitive description: name: leak_tracker - sha256: c35baad643ba394b40aac41080300150a4f08fd0fd6a10378f8f7c6bc161acec + sha256: "6bb818ecbdffe216e81182c2f0714a2e62b593f4a4f13098713ff1685dfb6ab0" url: "https://pub.dev" source: hosted - version: "10.0.8" + version: "10.0.9" leak_tracker_flutter_testing: dependency: transitive description: @@ -456,10 +456,10 @@ packages: dependency: transitive description: name: vm_service - sha256: "0968250880a6c5fe7edc067ed0a13d4bae1577fe2771dcf3010d52c4a9d3ca14" + sha256: ddfa8d30d89985b96407efce8acbdd124701f96741f2d981ca860662f1c0dc02 url: "https://pub.dev" source: hosted - version: "14.3.1" + version: "15.0.0" web: dependency: transitive description: diff --git a/pubspec.lock b/pubspec.lock index a91a1d4..f4bbb72 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -5,10 +5,10 @@ packages: dependency: transitive description: name: async - sha256: d2872f9c19731c2e5f10444b14686eb7cc85c76274bd6c16e1816bff9a3bab63 + sha256: "758e6d74e971c3e5aceb4110bfd6698efc7f501675bcfe0c775459a8140750eb" url: "https://pub.dev" source: hosted - version: "2.12.0" + version: "2.13.0" boolean_selector: dependency: transitive description: @@ -45,10 +45,10 @@ packages: dependency: transitive description: name: fake_async - sha256: "6a95e56b2449df2273fd8c45a662d6947ce1ebb7aafe80e550a3f68297f3cacc" + sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44" url: "https://pub.dev" source: hosted - version: "1.3.2" + version: "1.3.3" flutter: dependency: "direct main" description: flutter @@ -63,10 +63,10 @@ packages: dependency: transitive description: name: leak_tracker - sha256: c35baad643ba394b40aac41080300150a4f08fd0fd6a10378f8f7c6bc161acec + sha256: "6bb818ecbdffe216e81182c2f0714a2e62b593f4a4f13098713ff1685dfb6ab0" url: "https://pub.dev" source: hosted - version: "10.0.8" + version: "10.0.9" leak_tracker_flutter_testing: dependency: transitive description: @@ -180,10 +180,10 @@ packages: dependency: transitive description: name: vm_service - sha256: "0968250880a6c5fe7edc067ed0a13d4bae1577fe2771dcf3010d52c4a9d3ca14" + sha256: ddfa8d30d89985b96407efce8acbdd124701f96741f2d981ca860662f1c0dc02 url: "https://pub.dev" source: hosted - version: "14.3.1" + version: "15.0.0" sdks: dart: ">=3.7.0-0 <4.0.0" flutter: ">=3.18.0-18.0.pre.54" From 67f685a966a0e409bb1d496b794c929d7fa2dd28 Mon Sep 17 00:00:00 2001 From: David Date: Thu, 17 Jul 2025 16:35:06 +0200 Subject: [PATCH 7/7] Update CHANGELOG.md --- CHANGELOG.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e601ad2..885f16e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,6 @@ ## 1.6.1 - 2025-07-17 -- Fixes callback being called once in Android (improved handling) (#91) +- Fixed callback being called once in Android (improved handling) (#91) +- Fixed "FlutterJNI.loadLibrary/prefetchDefaultFontManager/init called more than once" ## 1.6.0 - 2025-05-06 - Updated to use the modern FlutterPlugin.FlutterPluginBinding pattern