diff --git a/CHANGELOG.md b/CHANGELOG.md index 2e89801..885f16e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +## 1.6.1 - 2025-07-17 +- 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 - Deprecated Registrar, PluginRegistrantCallback, and ShimPluginRegistry usage 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/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) } 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..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 @@ -19,40 +19,124 @@ 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 var isSettingUp = false 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) + } + } - val backgroundChannel = MethodChannel(engine.dartExecutor, BACKGROUND_CHANNEL_NAME) - backgroundChannel.setMethodCallHandler { call, result -> + private fun setupBackgroundChannelIfNeeded(ctx: Context) { + 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" -> handleInitialized(call, result, ctx, backgroundChannel, location, engine) + "initialized" -> { + Logger.debug("BackgroundManager", "Dart background isolate initialized") + isInitialized = true + isSettingUp = false + 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() } } } 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 + } } } - 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 +157,37 @@ 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 + 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"