improve lifecycle management
This commit is contained in:
@@ -20,18 +20,53 @@
|
|||||||
package eu.weblibre.gecko
|
package eu.weblibre.gecko
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.util.Log
|
||||||
import io.flutter.embedding.android.FlutterFragmentActivity
|
import io.flutter.embedding.android.FlutterFragmentActivity
|
||||||
import io.flutter.embedding.engine.FlutterEngine
|
import io.flutter.embedding.engine.FlutterEngine
|
||||||
import io.flutter.embedding.engine.FlutterEngineCache
|
import io.flutter.embedding.engine.FlutterEngineCache
|
||||||
|
import io.flutter.embedding.engine.FlutterJNI
|
||||||
import io.flutter.embedding.engine.dart.DartExecutor
|
import io.flutter.embedding.engine.dart.DartExecutor
|
||||||
import io.flutter.plugin.common.MethodChannel
|
import io.flutter.plugin.common.MethodChannel
|
||||||
|
|
||||||
class MainActivity: FlutterFragmentActivity() {
|
class MainActivity: FlutterFragmentActivity() {
|
||||||
|
companion object {
|
||||||
|
private const val TAG = "MainActivity"
|
||||||
|
}
|
||||||
|
|
||||||
private val TRIM_MEMORY_CHANNEL = "eu.weblibre.flutter_mozilla_components/trim_memory"
|
private val TRIM_MEMORY_CHANNEL = "eu.weblibre.flutter_mozilla_components/trim_memory"
|
||||||
private val ACTIVITY_CHANNEL = "eu.weblibre.gecko/activity"
|
private val ACTIVITY_CHANNEL = "eu.weblibre.gecko/activity"
|
||||||
private val ENGINE_ID = "engine_id"
|
private val ENGINE_ID = "engine_id"
|
||||||
private var trimMemoryChannel: MethodChannel? = null
|
private var trimMemoryChannel: MethodChannel? = null
|
||||||
|
|
||||||
|
private fun engineTag(engine: FlutterEngine?): String {
|
||||||
|
return engine?.let { "0x${System.identityHashCode(it).toString(16)}" } ?: "null"
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
Log.d(TAG, "onCreate: savedInstanceState=${savedInstanceState != null}, " +
|
||||||
|
"cachedEngine=${engineTag(FlutterEngineCache.getInstance().get(ENGINE_ID))}")
|
||||||
|
|
||||||
|
super.onCreate(null)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check whether the FlutterEngine's native JNI layer is still attached.
|
||||||
|
* Note: binaryMessenger.send() does NOT throw when JNI is detached — it just
|
||||||
|
* logs a warning. We must use reflection to access FlutterJNI.isAttachedToJni().
|
||||||
|
*/
|
||||||
|
private fun isEngineNativeAlive(engine: FlutterEngine): Boolean {
|
||||||
|
return try {
|
||||||
|
val field = FlutterEngine::class.java.getDeclaredField("flutterJNI")
|
||||||
|
field.isAccessible = true
|
||||||
|
val jni = field.get(engine) as FlutterJNI
|
||||||
|
jni.isAttached
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.w(TAG, "Could not check JNI attachment state: ${e.message}")
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
|
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
|
||||||
super.configureFlutterEngine(flutterEngine)
|
super.configureFlutterEngine(flutterEngine)
|
||||||
|
|
||||||
@@ -48,6 +83,31 @@ class MainActivity: FlutterFragmentActivity() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onPause() {
|
||||||
|
Log.d(TAG, "onPause")
|
||||||
|
super.onPause()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDestroy() {
|
||||||
|
Log.d(TAG, "onDestroy: isFinishing=$isFinishing, " +
|
||||||
|
"cachedEngine=${engineTag(FlutterEngineCache.getInstance().get(ENGINE_ID))}")
|
||||||
|
super.onDestroy()
|
||||||
|
|
||||||
|
if (!isFinishing) {
|
||||||
|
val cache = FlutterEngineCache.getInstance()
|
||||||
|
val engine = cache.get(ENGINE_ID)
|
||||||
|
if (engine != null) {
|
||||||
|
Log.d(TAG, "onDestroy: system-initiated destroy, clearing stale engine")
|
||||||
|
cache.remove(ENGINE_ID)
|
||||||
|
try {
|
||||||
|
engine.destroy()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.w(TAG, "Error destroying engine in onDestroy", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun onTrimMemory(level: Int) {
|
override fun onTrimMemory(level: Int) {
|
||||||
super.onTrimMemory(level)
|
super.onTrimMemory(level)
|
||||||
trimMemoryChannel?.invokeMethod("onTrimMemory", level)
|
trimMemoryChannel?.invokeMethod("onTrimMemory", level)
|
||||||
@@ -56,13 +116,26 @@ class MainActivity: FlutterFragmentActivity() {
|
|||||||
override fun provideFlutterEngine(context: Context): FlutterEngine {
|
override fun provideFlutterEngine(context: Context): FlutterEngine {
|
||||||
val cache = FlutterEngineCache.getInstance()
|
val cache = FlutterEngineCache.getInstance()
|
||||||
val cachedEngine = cache.get(ENGINE_ID)
|
val cachedEngine = cache.get(ENGINE_ID)
|
||||||
if (cachedEngine != null && cachedEngine.dartExecutor.isExecutingDart) {
|
|
||||||
return cachedEngine
|
|
||||||
}
|
|
||||||
|
|
||||||
if (cachedEngine != null) {
|
if (cachedEngine != null) {
|
||||||
|
val isHealthy = try {
|
||||||
|
cachedEngine.dartExecutor.isExecutingDart && isEngineNativeAlive(cachedEngine)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.w(TAG, "Cached engine health check failed", e)
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isHealthy) {
|
||||||
|
Log.d(TAG, "provideFlutterEngine: reusing cached engine ${engineTag(cachedEngine)}")
|
||||||
|
return cachedEngine
|
||||||
|
}
|
||||||
|
|
||||||
|
Log.w(TAG, "provideFlutterEngine: cached engine ${engineTag(cachedEngine)} is stale, creating fresh")
|
||||||
cache.remove(ENGINE_ID)
|
cache.remove(ENGINE_ID)
|
||||||
cachedEngine.destroy()
|
try {
|
||||||
|
cachedEngine.destroy()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.w(TAG, "Error destroying stale engine", e)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val flutterEngine = FlutterEngine(context.applicationContext)
|
val flutterEngine = FlutterEngine(context.applicationContext)
|
||||||
@@ -72,6 +145,7 @@ class MainActivity: FlutterFragmentActivity() {
|
|||||||
)
|
)
|
||||||
cache.put(ENGINE_ID, flutterEngine)
|
cache.put(ENGINE_ID, flutterEngine)
|
||||||
|
|
||||||
|
Log.d(TAG, "provideFlutterEngine: created new engine ${engineTag(flutterEngine)}")
|
||||||
return flutterEngine
|
return flutterEngine
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+4
-2
@@ -18,13 +18,15 @@ import io.flutter.plugin.platform.PlatformView
|
|||||||
import io.flutter.plugin.platform.PlatformViewFactory
|
import io.flutter.plugin.platform.PlatformViewFactory
|
||||||
|
|
||||||
class GeckoViewFactory(
|
class GeckoViewFactory(
|
||||||
private val activity: Activity,
|
private val activityProvider: () -> Activity?,
|
||||||
private val containerId: Int,
|
private val containerId: Int,
|
||||||
private val flutterEvents: GeckoStateEvents
|
private val flutterEvents: GeckoStateEvents
|
||||||
) : PlatformViewFactory(
|
) : PlatformViewFactory(
|
||||||
StandardMessageCodec.INSTANCE) {
|
StandardMessageCodec.INSTANCE) {
|
||||||
override fun create(context: Context?, id: Int, args: Any?): PlatformView {
|
override fun create(context: Context?, id: Int, args: Any?): PlatformView {
|
||||||
return NativeFragmentView(this.activity, this.containerId, this.flutterEvents)
|
val activity = activityProvider()
|
||||||
|
?: throw IllegalStateException("No activity available when creating GeckoView platform view")
|
||||||
|
return NativeFragmentView(activity, this.containerId, this.flutterEvents)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+12
-11
@@ -134,26 +134,27 @@ class GeckoBrowserApiImpl : GeckoBrowserApi {
|
|||||||
_flutterPluginBinding = flutterPluginBinding
|
_flutterPluginBinding = flutterPluginBinding
|
||||||
_flutterEvents = GeckoStateEvents(_flutterPluginBinding.binaryMessenger)
|
_flutterEvents = GeckoStateEvents(_flutterPluginBinding.binaryMessenger)
|
||||||
|
|
||||||
|
// Register platform view factory once per engine binding.
|
||||||
|
// The factory resolves the current activity lazily via activityProvider,
|
||||||
|
// so it always uses the latest activity after recreation/config changes.
|
||||||
|
_flutterPluginBinding.platformViewRegistry.registerViewFactory(
|
||||||
|
"eu.weblibre/gecko", GeckoViewFactory(
|
||||||
|
activityProvider = { this.activity },
|
||||||
|
FRAGMENT_CONTAINER_ID,
|
||||||
|
_flutterEvents
|
||||||
|
)
|
||||||
|
)
|
||||||
|
isPlatformViewRegistered = true
|
||||||
|
|
||||||
isGeckoInitialized = false
|
isGeckoInitialized = false
|
||||||
}
|
}
|
||||||
|
|
||||||
fun attachActivity(activity: Activity) {
|
fun attachActivity(activity: Activity) {
|
||||||
this.activity = activity
|
this.activity = activity
|
||||||
|
|
||||||
_flutterPluginBinding.platformViewRegistry.registerViewFactory(
|
|
||||||
"eu.weblibre/gecko", GeckoViewFactory(
|
|
||||||
activity,
|
|
||||||
FRAGMENT_CONTAINER_ID,
|
|
||||||
_flutterEvents
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
isPlatformViewRegistered = true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun detachActivity() {
|
fun detachActivity() {
|
||||||
this.activity = null
|
this.activity = null
|
||||||
isPlatformViewRegistered = false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getGeckoVersion(): String {
|
override fun getGeckoVersion(): String {
|
||||||
|
|||||||
+18
-1
@@ -10,8 +10,14 @@ import java.io.File
|
|||||||
/**
|
/**
|
||||||
* Manages pluggable transports via IPtProxy
|
* Manages pluggable transports via IPtProxy
|
||||||
* Supports: obfs4, snowflake, meek, webtunnel
|
* Supports: obfs4, snowflake, meek, webtunnel
|
||||||
|
*
|
||||||
|
* IMPORTANT: This is a process-level singleton. The IPtProxy.Controller (Go object bound
|
||||||
|
* via gomobile) uses reference tracking that breaks if multiple Controller instances are
|
||||||
|
* created in the same process. By keeping a single PluggableTransportManager (and thus a
|
||||||
|
* single lazy Controller), we avoid "trackGoRef called with Java refnum" crashes when
|
||||||
|
* TorService is destroyed and recreated.
|
||||||
*/
|
*/
|
||||||
class PluggableTransportManager(private val context: Context) {
|
class PluggableTransportManager private constructor(private val context: Context) {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val TAG = "PTManager"
|
private const val TAG = "PTManager"
|
||||||
@@ -23,6 +29,17 @@ class PluggableTransportManager(private val context: Context) {
|
|||||||
private val SNOWFLAKE_FRONTS = listOf("foursquare.com", "github.githubassets.com")
|
private val SNOWFLAKE_FRONTS = listOf("foursquare.com", "github.githubassets.com")
|
||||||
private val SNOWFLAKE_AMP_FRONTS = listOf("www.google.com")
|
private val SNOWFLAKE_AMP_FRONTS = listOf("www.google.com")
|
||||||
private const val SNOWFLAKE_ICE_SERVERS = "stun:stun.l.google.com:19302,stun:stun.antisip.com:3478,stun:stun.bluesip.net:3478,stun:stun.dus.net:3478,stun:stun.epygi.com:3478,stun:stun.sonetel.com:3478,stun:stun.uls.co.za:3478,stun:stun.voipgate.com:3478,stun:stun.voys.nl:3478"
|
private const val SNOWFLAKE_ICE_SERVERS = "stun:stun.l.google.com:19302,stun:stun.antisip.com:3478,stun:stun.bluesip.net:3478,stun:stun.dus.net:3478,stun:stun.epygi.com:3478,stun:stun.sonetel.com:3478,stun:stun.uls.co.za:3478,stun:stun.voipgate.com:3478,stun:stun.voys.nl:3478"
|
||||||
|
|
||||||
|
@Volatile
|
||||||
|
private var instance: PluggableTransportManager? = null
|
||||||
|
|
||||||
|
fun getInstance(context: Context): PluggableTransportManager {
|
||||||
|
return instance ?: synchronized(this) {
|
||||||
|
instance ?: PluggableTransportManager(context.applicationContext).also {
|
||||||
|
instance = it
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private val stateDir = File(context.cacheDir, "iptproxy")
|
private val stateDir = File(context.cacheDir, "iptproxy")
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ class TorManager(
|
|||||||
private var controlConnection: TorControlConnection? = null
|
private var controlConnection: TorControlConnection? = null
|
||||||
private var torService: TorService? = null
|
private var torService: TorService? = null
|
||||||
|
|
||||||
val pluggableTransportManager = PluggableTransportManager(context)
|
val pluggableTransportManager = PluggableTransportManager.getInstance(context)
|
||||||
private val geoIpManager = GeoIpManager(context)
|
private val geoIpManager = GeoIpManager(context)
|
||||||
|
|
||||||
private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
|
private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
|
||||||
|
|||||||
@@ -199,9 +199,20 @@ class TorService : Service() {
|
|||||||
super.onDestroy()
|
super.onDestroy()
|
||||||
Log.d(TAG, "Service destroyed")
|
Log.d(TAG, "Service destroyed")
|
||||||
|
|
||||||
scope.launch {
|
// Stop pluggable transports synchronously to release Go references.
|
||||||
torManager?.destroy()
|
// Previously this was scope.launch { torManager?.destroy() } followed by
|
||||||
}
|
// scope.cancel(), which meant the cleanup coroutine was immediately cancelled
|
||||||
|
// and never ran — causing "trackGoRef called with Java refnum" crashes when
|
||||||
|
// TorService was recreated and tried to create a new IPtProxy.Controller.
|
||||||
|
//
|
||||||
|
// Note: We only stop transports here. The PluggableTransportManager singleton
|
||||||
|
// and its Controller persist across service restarts by design.
|
||||||
|
// Full TorManager.destroy() is not called because it would deadlock
|
||||||
|
// (cleanup() uses runBlocking(Dispatchers.Main) while onDestroy runs on Main).
|
||||||
|
torManager?.pluggableTransportManager?.stopAll()
|
||||||
|
|
||||||
|
torManager = null
|
||||||
|
logHandler = null
|
||||||
scope.cancel()
|
scope.cancel()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user