Skip to content
Open
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
Original file line number Diff line number Diff line change
@@ -1,188 +1,256 @@
package com.icapps.background_location_tracker

import android.app.Activity
import android.app.Application
import android.content.Context
import android.os.Bundle
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.LifecycleRegistry
import com.icapps.background_location_tracker.flutter.FlutterBackgroundManager
import com.icapps.background_location_tracker.flutter.FlutterLifecycleAdapter
import com.icapps.background_location_tracker.utils.ActivityCounter
import com.icapps.background_location_tracker.utils.Logger
import io.flutter.embedding.engine.FlutterEngine
import io.flutter.embedding.engine.FlutterEngineCache
import io.flutter.embedding.engine.plugins.FlutterPlugin
import io.flutter.embedding.engine.plugins.activity.ActivityAware
import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding
import io.flutter.plugin.common.BinaryMessenger
import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel
import io.flutter.plugin.common.MethodChannel.MethodCallHandler
import io.flutter.plugin.common.MethodChannel.Result
import io.flutter.embedding.engine.plugins.FlutterPlugin.FlutterPluginBinding
import io.flutter.embedding.engine.FlutterEngine
import java.util.concurrent.locks.ReentrantReadWriteLock
import kotlin.concurrent.read
import kotlin.concurrent.write

/**
* BackgroundLocationTrackerPlugin handles location tracking in both foreground and background modes.
* This plugin manages Flutter engine lifecycle for background execution and provides location updates.
*/
class BackgroundLocationTrackerPlugin : FlutterPlugin, MethodCallHandler, ActivityAware {

private var lifecycle: Lifecycle? = null
private var methodCallHelper: MethodCallHelper? = null
private var channel: MethodChannel? = null
private var applicationContext: Context? = null

override fun onAttachedToEngine(binding: FlutterPluginBinding) {
override fun onAttachedToEngine(binding: FlutterPlugin.FlutterPluginBinding) {
Logger.debug(TAG, "Plugin attached to engine")

applicationContext = binding.applicationContext
channel = MethodChannel(binding.binaryMessenger, FOREGROUND_CHANNEL_NAME)
channel?.setMethodCallHandler(this)
channel = MethodChannel(binding.binaryMessenger, FOREGROUND_CHANNEL_NAME).also {
it.setMethodCallHandler(this)
}

if (methodCallHelper == null) {
methodCallHelper = MethodCallHelper.getInstance(binding.applicationContext)
}
}

override fun onDetachedFromEngine(binding: FlutterPluginBinding) {
override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) {
Logger.debug(TAG, "Plugin detached from engine")

// Clean up lifecycle observers first
lifecycle?.let { lifecycle ->
methodCallHelper?.let { helper ->
lifecycle.removeObserver(helper)
}
}

// Clean up method call helper
methodCallHelper?.cleanup()
methodCallHelper = null

// Clean up channel
channel?.setMethodCallHandler(null)
channel = null

// Clean up context references
applicationContext = null
lifecycle = null

// Clean up the background manager when plugin is detached
// Clean up background manager
FlutterBackgroundManager.cleanup()
}

override fun onMethodCall(call: MethodCall, result: Result) {
methodCallHelper?.handle(call, result)
methodCallHelper?.handle(call, result) ?: run {
Logger.debug(TAG, "MethodCallHelper is null, method call not handled")
result.notImplemented()
}
}

override fun onAttachedToActivity(binding: ActivityPluginBinding) {
Logger.debug(TAG, "Plugin attached to activity")

lifecycle = FlutterLifecycleAdapter.getActivityLifecycle(binding)
ActivityCounter.attach(binding.activity)
methodCallHelper?.let {
lifecycle?.removeObserver(it)
lifecycle?.addObserver(it)

methodCallHelper?.let { helper ->
lifecycle?.let { lifecycle ->
lifecycle.removeObserver(helper)
lifecycle.addObserver(helper)
}
}
}

override fun onDetachedFromActivity() {}
override fun onDetachedFromActivity() {
Logger.debug(TAG, "Plugin detached from activity")

// Clean up lifecycle observers
lifecycle?.let { lifecycle ->
methodCallHelper?.let { helper ->
lifecycle.removeObserver(helper)
}
}
lifecycle = null
}

override fun onReattachedToActivityForConfigChanges(binding: ActivityPluginBinding) {
Logger.debug(TAG, "Plugin reattached to activity for config changes")
onAttachedToActivity(binding)
}

override fun onDetachedFromActivityForConfigChanges() {
onDetachedFromActivity()
Logger.debug(TAG, "Plugin detached from activity for config changes")
// Don't clean up lifecycle here as it's just a config change
}

companion object {
private const val TAG = "FBLTPlugin"
private const val FOREGROUND_CHANNEL_NAME = "com.icapps.background_location_tracker/foreground_channel"
private const val CACHED_ENGINE_ID = "background_location_tracker_engine"

// New static properties for background execution
// Thread-safe engine management
@Volatile
private var flutterEngine: FlutterEngine? = null
@Volatile
private var isEngineInitialized = false
private val engineLock = ReentrantReadWriteLock()

// For compatibility with older plugins
@Deprecated("Use FlutterEngine's plugin registry instead")
private var pluginRegistrantCallback: ((FlutterEngine) -> Unit)? = null

@JvmStatic
@Deprecated("Use the Android embedding v2 instead")
fun setPluginRegistrantCallback(callback: ((FlutterEngine) -> Unit)) {
pluginRegistrantCallback = callback
}

// Method to get or create the Flutter engine for background execution
/**
* Gets or creates a Flutter engine for background execution.
* Uses FlutterEngineCache for proper lifecycle management.
*
* @param context Application context
* @return FlutterEngine instance for background execution
*/
@JvmStatic
fun getFlutterEngine(context: Context): FlutterEngine {
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")
// First try to read with read lock
engineLock.read {
flutterEngine?.let {
Logger.debug(TAG, "Reusing existing Flutter engine")
return it
}
}

// Need to create engine, use write lock
return engineLock.write {
// Double-check pattern
flutterEngine?.let {
Logger.debug(TAG, "Reusing existing Flutter engine (double-check)")
return@write it
}

Logger.debug(TAG, "Creating new Flutter engine for background execution")

// Try to get from cache first
val cache = FlutterEngineCache.getInstance()
var engine = cache.get(CACHED_ENGINE_ID)

if (engine == null) {
// Create new engine and cache it
engine = FlutterEngine(context.applicationContext).also { newEngine ->
cache.put(CACHED_ENGINE_ID, newEngine)
Logger.debug(TAG, "Flutter engine created and cached")
}
} else {
Logger.debug(TAG, "Reusing existing Flutter engine")
Logger.debug(TAG, "Retrieved Flutter engine from cache")
}
return flutterEngine!!

flutterEngine = engine
isEngineInitialized = true

engine
}
}

// Method to properly cleanup the Flutter engine
/**
* Safely cleans up the Flutter engine and removes it from cache.
* This method is thread-safe and handles cleanup gracefully.
*/
@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()
engineLock.write {
try {
Logger.debug(TAG, "Starting Flutter engine cleanup")

// Remove from cache first
val cache = FlutterEngineCache.getInstance()
cache.remove(CACHED_ENGINE_ID)

flutterEngine?.let { engine ->
try {
// Graceful shutdown
if (engine.dartExecutor.isExecutingDart) {
Logger.debug(TAG, "Dart is executing, destroying engine gracefully")
}

// Destroy the engine (this handles Dart isolate cleanup internally)
engine.destroy()
Logger.debug(TAG, "Flutter engine destroyed successfully")

} catch (e: Exception) {
Logger.error(TAG, "Error during Flutter engine cleanup: ${e.message}")
// Re-throw critical exceptions, but handle them gracefully
when (e) {
is IllegalStateException,
is SecurityException -> {
Logger.error(TAG, "Critical error during engine cleanup: ${e.message}")
// Don't re-throw to avoid crashing the app
}
else -> {
Logger.debug(TAG, "Non-critical error during cleanup, continuing: ${e.message}")
}
}
}
// 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}")
}

} finally {
// Always reset state
flutterEngine = null
isEngineInitialized = false
Logger.debug(TAG, "Flutter engine cleanup completed")
}
}
}
}

@Deprecated("Use the Android embedding v2 instead")
private class ProxyLifecycleProvider internal constructor(activity: Activity) : Application.ActivityLifecycleCallbacks, LifecycleOwner {
override val lifecycle = LifecycleRegistry(this)
private val registrarActivityHashCode: Int = activity.hashCode()

init {
activity.application.registerActivityLifecycleCallbacks(this)
}

override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) {
if (activity.hashCode() != registrarActivityHashCode) {
return
}
lifecycle.handleLifecycleEvent(Lifecycle.Event.ON_CREATE)
}

override fun onActivityStarted(activity: Activity) {
if (activity.hashCode() != registrarActivityHashCode) {
return
}
lifecycle.handleLifecycleEvent(Lifecycle.Event.ON_START)
}

override fun onActivityResumed(activity: Activity) {
if (activity.hashCode() != registrarActivityHashCode) {
return
}
lifecycle.handleLifecycleEvent(Lifecycle.Event.ON_RESUME)
}

override fun onActivityPaused(activity: Activity) {
if (activity.hashCode() != registrarActivityHashCode) {
return
}
lifecycle.handleLifecycleEvent(Lifecycle.Event.ON_PAUSE)
}

override fun onActivityStopped(activity: Activity) {
if (activity.hashCode() != registrarActivityHashCode) {
return
}
lifecycle.handleLifecycleEvent(Lifecycle.Event.ON_STOP)

/**
* Checks if the Flutter engine is currently initialized and available.
*
* @return true if engine is initialized, false otherwise
*/
@JvmStatic
fun isEngineInitialized(): Boolean {
return engineLock.read { isEngineInitialized && flutterEngine != null }
}

override fun onActivitySaveInstanceState(activity: Activity, outState: Bundle) {}

override fun onActivityDestroyed(activity: Activity) {
if (activity.hashCode() != registrarActivityHashCode) {
return
}
activity.application.unregisterActivityLifecycleCallbacks(this)
lifecycle.handleLifecycleEvent(Lifecycle.Event.ON_DESTROY)

/**
* Forces cleanup of all static references. Use with caution.
* This is typically called when the application is being destroyed.
*/
@JvmStatic
fun forceCleanup() {
Logger.debug(TAG, "Force cleanup requested")
cleanupFlutterEngine()
// Additional cleanup can be added here if needed
}
}
}

/**
* Extension function to add cleanup method to MethodCallHelper if needed.
* This should be implemented in the MethodCallHelper class.
*/
private fun MethodCallHelper.cleanup() {
// This method should be implemented in MethodCallHelper class
// to clean up any resources, listeners, or observers
Logger.debug("MethodCallHelper", "Cleanup called")
}
1 change: 0 additions & 1 deletion example/android/app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application
android:label="example"
android:name=".Application"
android:icon="@mipmap/ic_launcher">
<activity
android:name=".MainActivity"
Expand Down

This file was deleted.

Loading