Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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")
Expand All @@ -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
}
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<String, Any>()
data["lat"] = location.latitude
data["lon"] = location.longitude
Expand All @@ -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")
}
})
}
}

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()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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()
}

/**
Expand Down
18 changes: 9 additions & 9 deletions example/pubspec.lock
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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:
Expand Down
16 changes: 8 additions & 8 deletions pubspec.lock
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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
Expand All @@ -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:
Expand Down Expand Up @@ -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"