good state

This commit is contained in:
Fabian Freund
2025-03-07 20:35:35 +01:00
parent 70d1c1b50e
commit 25c9faa3ff
50 changed files with 993 additions and 439 deletions
@@ -1,171 +1,20 @@
package eu.lensai.flutter_mozilla_components
import android.app.Activity
import android.content.Intent
import android.view.View
import androidx.fragment.app.FragmentActivity
import eu.lensai.flutter_mozilla_components.activities.NotificationActivity
import eu.lensai.flutter_mozilla_components.api.GeckoAddonsApiImpl
import eu.lensai.flutter_mozilla_components.api.GeckoBrowserApiImpl
import eu.lensai.flutter_mozilla_components.api.GeckoContainerProxyApiImpl
import eu.lensai.flutter_mozilla_components.api.GeckoCookieApiImpl
import eu.lensai.flutter_mozilla_components.api.GeckoDeleteBrowsingDataControllerImpl
import eu.lensai.flutter_mozilla_components.api.GeckoDownloadsApiImpl
import eu.lensai.flutter_mozilla_components.api.GeckoEngineSettingsApiImpl
import eu.lensai.flutter_mozilla_components.api.GeckoFindApiImpl
import eu.lensai.flutter_mozilla_components.api.GeckoIconsApiImpl
import eu.lensai.flutter_mozilla_components.api.GeckoPrefApiImpl
import eu.lensai.flutter_mozilla_components.api.GeckoSelectionActionControllerImpl
import eu.lensai.flutter_mozilla_components.api.GeckoSessionApiImpl
import eu.lensai.flutter_mozilla_components.api.GeckoSuggestionApiImpl
import eu.lensai.flutter_mozilla_components.api.GeckoTabsApiImpl
import eu.lensai.flutter_mozilla_components.api.GeckoBrowserExtensionApiImpl
import eu.lensai.flutter_mozilla_components.feature.DefaultSelectionActionDelegate
import eu.lensai.flutter_mozilla_components.pigeons.BrowserExtensionEvents
import eu.lensai.flutter_mozilla_components.pigeons.GeckoAddonEvents
import eu.lensai.flutter_mozilla_components.pigeons.GeckoAddonsApi
import eu.lensai.flutter_mozilla_components.pigeons.GeckoBrowserApi
import eu.lensai.flutter_mozilla_components.pigeons.GeckoBrowserExtensionApi
import eu.lensai.flutter_mozilla_components.pigeons.GeckoContainerProxyApi
import eu.lensai.flutter_mozilla_components.pigeons.GeckoCookieApi
import eu.lensai.flutter_mozilla_components.pigeons.GeckoDeleteBrowsingDataController
import eu.lensai.flutter_mozilla_components.pigeons.GeckoDownloadsApi
import eu.lensai.flutter_mozilla_components.pigeons.GeckoEngineSettingsApi
import eu.lensai.flutter_mozilla_components.pigeons.GeckoFindApi
import eu.lensai.flutter_mozilla_components.pigeons.GeckoIconsApi
import eu.lensai.flutter_mozilla_components.pigeons.GeckoPrefApi
import eu.lensai.flutter_mozilla_components.pigeons.GeckoSelectionActionController
import eu.lensai.flutter_mozilla_components.pigeons.GeckoSelectionActionEvents
import eu.lensai.flutter_mozilla_components.pigeons.GeckoSessionApi
import eu.lensai.flutter_mozilla_components.pigeons.GeckoStateEvents
import eu.lensai.flutter_mozilla_components.pigeons.GeckoSuggestionApi
import eu.lensai.flutter_mozilla_components.pigeons.GeckoSuggestionEvents
import eu.lensai.flutter_mozilla_components.pigeons.GeckoTabContentEvents
import eu.lensai.flutter_mozilla_components.pigeons.GeckoTabsApi
import eu.lensai.flutter_mozilla_components.pigeons.ReaderViewController
import eu.lensai.flutter_mozilla_components.pigeons.ReaderViewEvents
import io.flutter.embedding.engine.plugins.FlutterPlugin
import io.flutter.embedding.engine.plugins.activity.ActivityAware
import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding
import mozilla.components.support.base.log.Log
import mozilla.components.support.base.log.sink.AndroidLogSink
/** FlutterMozillaComponentsPlugin */
class FlutterMozillaComponentsPlugin: FlutterPlugin, ActivityAware {
companion object {
private const val FRAGMENT_CONTAINER_ID = 0xBEEF
private var isGeckoInitialized = false
}
private val components by lazy {
requireNotNull(GlobalComponents.components) { "Components not initialized" }
}
private var activity: Activity? = null
private lateinit var _flutterPluginBinding: FlutterPlugin.FlutterPluginBinding;
private lateinit var _flutterEvents : GeckoStateEvents
private var isPlatformViewRegistered = false
private val browserApi = GeckoBrowserApiImpl()
override fun onAttachedToEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
synchronized(this) {
if(!isGeckoInitialized) {
Log.addSink(AndroidLogSink())
setupGeckoEngine(flutterPluginBinding)
isGeckoInitialized = true
}
}
}
private fun setupGeckoEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
_flutterPluginBinding = flutterPluginBinding
_flutterEvents = GeckoStateEvents(_flutterPluginBinding.binaryMessenger)
val selectionActionEvents = GeckoSelectionActionEvents(_flutterPluginBinding.binaryMessenger)
val selectionActionDelegate = DefaultSelectionActionDelegate(selectionActionEvents)
val readerViewController =
ReaderViewController(_flutterPluginBinding.binaryMessenger)
val extensionEvents = BrowserExtensionEvents(_flutterPluginBinding.binaryMessenger)
val addonEvents = GeckoAddonEvents(_flutterPluginBinding.binaryMessenger)
val tabContentEvents = GeckoTabContentEvents(_flutterPluginBinding.binaryMessenger)
val suggestionEvents = GeckoSuggestionEvents(_flutterPluginBinding.binaryMessenger)
GeckoSuggestionApi.setUp(_flutterPluginBinding.binaryMessenger, GeckoSuggestionApiImpl(suggestionEvents))
GlobalComponents.setUp(
flutterPluginBinding.applicationContext,
_flutterEvents,
readerViewController,
selectionActionDelegate,
addonEvents,
tabContentEvents,
extensionEvents
)
GeckoBrowserApi.setUp(_flutterPluginBinding.binaryMessenger, GeckoBrowserApiImpl {
showNativeFragment()
})
GeckoEngineSettingsApi.setUp(_flutterPluginBinding.binaryMessenger, GeckoEngineSettingsApiImpl())
GeckoAddonsApi.setUp(_flutterPluginBinding.binaryMessenger, GeckoAddonsApiImpl(flutterPluginBinding.applicationContext))
GeckoSessionApi.setUp(_flutterPluginBinding.binaryMessenger, GeckoSessionApiImpl())
GeckoTabsApi.setUp(_flutterPluginBinding.binaryMessenger, GeckoTabsApiImpl())
GeckoIconsApi.setUp(_flutterPluginBinding.binaryMessenger, GeckoIconsApiImpl())
GeckoCookieApi.setUp(_flutterPluginBinding.binaryMessenger, GeckoCookieApiImpl())
GeckoPrefApi.setUp(_flutterPluginBinding.binaryMessenger, GeckoPrefApiImpl())
GeckoContainerProxyApi.setUp(_flutterPluginBinding.binaryMessenger, GeckoContainerProxyApiImpl())
GeckoFindApi.setUp(_flutterPluginBinding.binaryMessenger, GeckoFindApiImpl())
GeckoSelectionActionController.setUp(_flutterPluginBinding.binaryMessenger, GeckoSelectionActionControllerImpl(
selectionActionDelegate
))
GeckoDeleteBrowsingDataController.setUp(_flutterPluginBinding.binaryMessenger, GeckoDeleteBrowsingDataControllerImpl())
GeckoDownloadsApi.setUp(_flutterPluginBinding.binaryMessenger, GeckoDownloadsApiImpl())
GeckoBrowserExtensionApi.setUp(_flutterPluginBinding.binaryMessenger, GeckoBrowserExtensionApiImpl())
ReaderViewEvents.setUp(
_flutterPluginBinding.binaryMessenger,
components.events.readerViewEvents
)
val intent = Intent(flutterPluginBinding.applicationContext, NotificationActivity::class.java)
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
flutterPluginBinding.applicationContext.startActivity(intent)
}
private fun showNativeFragment(): Boolean {
if (!isPlatformViewRegistered) {
return false
}
if (activity == null || activity !is FragmentActivity) {
return false
}
val fragmentActivity = activity as FragmentActivity
// Check if the container view exists in the view hierarchy
val container = fragmentActivity.findViewById<View>(FRAGMENT_CONTAINER_ID)
if (container == null) {
// Container doesn't exist yet, retry later
return false
}
val nativeFragment = BrowserFragment.create()
val fm = fragmentActivity.supportFragmentManager
fm.beginTransaction()
.replace(FRAGMENT_CONTAINER_ID, nativeFragment)
.commitAllowingStateLoss()
return true
browserApi.attachBinding(flutterPluginBinding)
GeckoBrowserApi.setUp(flutterPluginBinding.binaryMessenger, browserApi)
}
override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) {
@@ -173,17 +22,7 @@ class FlutterMozillaComponentsPlugin: FlutterPlugin, ActivityAware {
}
override fun onAttachedToActivity(binding: ActivityPluginBinding) {
this.activity = binding.activity
_flutterPluginBinding.platformViewRegistry.registerViewFactory(
"eu.lensai/gecko", GeckoViewFactory(
binding.activity,
FRAGMENT_CONTAINER_ID,
_flutterEvents
)
)
isPlatformViewRegistered = true
browserApi.attachActivity(binding.activity)
}
override fun onDetachedFromActivityForConfigChanges() {
@@ -195,7 +34,6 @@ class FlutterMozillaComponentsPlugin: FlutterPlugin, ActivityAware {
}
override fun onDetachedFromActivity() {
this.activity = null
isPlatformViewRegistered = false
browserApi.detachActivity()
}
}
@@ -1,17 +1,99 @@
package eu.lensai.flutter_mozilla_components.api
import android.app.Activity
import android.content.Intent
import android.view.View
import androidx.fragment.app.FragmentActivity
import eu.lensai.flutter_mozilla_components.BrowserFragment
import eu.lensai.flutter_mozilla_components.GeckoViewFactory
import eu.lensai.flutter_mozilla_components.GlobalComponents
import eu.lensai.flutter_mozilla_components.activities.NotificationActivity
import eu.lensai.flutter_mozilla_components.feature.DefaultSelectionActionDelegate
import eu.lensai.flutter_mozilla_components.pigeons.BrowserExtensionEvents
import eu.lensai.flutter_mozilla_components.pigeons.GeckoAddonEvents
import eu.lensai.flutter_mozilla_components.pigeons.GeckoAddonsApi
import eu.lensai.flutter_mozilla_components.pigeons.GeckoBrowserApi
import eu.lensai.flutter_mozilla_components.pigeons.GeckoBrowserExtensionApi
import eu.lensai.flutter_mozilla_components.pigeons.GeckoContainerProxyApi
import eu.lensai.flutter_mozilla_components.pigeons.GeckoCookieApi
import eu.lensai.flutter_mozilla_components.pigeons.GeckoDeleteBrowsingDataController
import eu.lensai.flutter_mozilla_components.pigeons.GeckoDownloadsApi
import eu.lensai.flutter_mozilla_components.pigeons.GeckoEngineSettingsApi
import eu.lensai.flutter_mozilla_components.pigeons.GeckoFindApi
import eu.lensai.flutter_mozilla_components.pigeons.GeckoIconsApi
import eu.lensai.flutter_mozilla_components.pigeons.GeckoPrefApi
import eu.lensai.flutter_mozilla_components.pigeons.GeckoSelectionActionController
import eu.lensai.flutter_mozilla_components.pigeons.GeckoSelectionActionEvents
import eu.lensai.flutter_mozilla_components.pigeons.GeckoSessionApi
import eu.lensai.flutter_mozilla_components.pigeons.GeckoStateEvents
import eu.lensai.flutter_mozilla_components.pigeons.GeckoSuggestionApi
import eu.lensai.flutter_mozilla_components.pigeons.GeckoSuggestionEvents
import eu.lensai.flutter_mozilla_components.pigeons.GeckoTabContentEvents
import eu.lensai.flutter_mozilla_components.pigeons.GeckoTabsApi
import eu.lensai.flutter_mozilla_components.pigeons.ReaderViewController
import eu.lensai.flutter_mozilla_components.pigeons.ReaderViewEvents
import io.flutter.embedding.engine.plugins.FlutterPlugin
import io.flutter.embedding.engine.plugins.FlutterPlugin.FlutterPluginBinding
import mozilla.components.browser.state.action.SystemAction
import mozilla.components.feature.addons.logger
import mozilla.components.support.base.log.Log
import mozilla.components.support.base.log.sink.AndroidLogSink
/**
* Implementation of GeckoBrowserApi that handles browser-related operations
* @param showFragmentCallback Callback function to show native fragment
*/
class GeckoBrowserApiImpl(private val showFragmentCallback: () -> Boolean) : GeckoBrowserApi {
class GeckoBrowserApiImpl : GeckoBrowserApi {
companion object {
private const val TAG = "GeckoBrowserApiImpl"
private const val FRAGMENT_CONTAINER_ID = 0xBEEF
private var isGeckoInitialized = false
}
private val components by lazy {
requireNotNull(GlobalComponents.components) { "Components not initialized" }
}
private var activity: Activity? = null
private var isPlatformViewRegistered = false
private lateinit var _flutterPluginBinding: FlutterPlugin.FlutterPluginBinding
private lateinit var _flutterEvents : GeckoStateEvents
fun attachBinding(flutterPluginBinding: FlutterPluginBinding) {
_flutterPluginBinding = flutterPluginBinding
_flutterEvents = GeckoStateEvents(_flutterPluginBinding.binaryMessenger)
}
fun attachActivity(activity: Activity) {
this.activity = activity
_flutterPluginBinding.platformViewRegistry.registerViewFactory(
"eu.lensai/gecko", GeckoViewFactory(
activity,
FRAGMENT_CONTAINER_ID,
_flutterEvents
)
)
isPlatformViewRegistered = true
}
fun detachActivity() {
this.activity = null
isPlatformViewRegistered = false
}
override fun initialize() {
synchronized(this) {
if(!isGeckoInitialized) {
Log.addSink(AndroidLogSink())
setupGeckoEngine()
isGeckoInitialized = true
}
}
}
override fun showNativeFragment(): Boolean {
@@ -24,6 +106,85 @@ class GeckoBrowserApiImpl(private val showFragmentCallback: () -> Boolean) : Gec
return false
}
private fun setupGeckoEngine() {
val selectionActionEvents = GeckoSelectionActionEvents(_flutterPluginBinding.binaryMessenger)
val selectionActionDelegate = DefaultSelectionActionDelegate(selectionActionEvents)
val readerViewController =
ReaderViewController(_flutterPluginBinding.binaryMessenger)
val extensionEvents = BrowserExtensionEvents(_flutterPluginBinding.binaryMessenger)
val addonEvents = GeckoAddonEvents(_flutterPluginBinding.binaryMessenger)
val tabContentEvents = GeckoTabContentEvents(_flutterPluginBinding.binaryMessenger)
val suggestionEvents = GeckoSuggestionEvents(_flutterPluginBinding.binaryMessenger)
GeckoSuggestionApi.setUp(_flutterPluginBinding.binaryMessenger, GeckoSuggestionApiImpl(suggestionEvents))
GlobalComponents.setUp(
_flutterPluginBinding.applicationContext,
_flutterEvents,
readerViewController,
selectionActionDelegate,
addonEvents,
tabContentEvents,
extensionEvents
)
GeckoEngineSettingsApi.setUp(_flutterPluginBinding.binaryMessenger, GeckoEngineSettingsApiImpl())
GeckoAddonsApi.setUp(_flutterPluginBinding.binaryMessenger, GeckoAddonsApiImpl(_flutterPluginBinding.applicationContext))
GeckoSessionApi.setUp(_flutterPluginBinding.binaryMessenger, GeckoSessionApiImpl())
GeckoTabsApi.setUp(_flutterPluginBinding.binaryMessenger, GeckoTabsApiImpl())
GeckoIconsApi.setUp(_flutterPluginBinding.binaryMessenger, GeckoIconsApiImpl())
GeckoCookieApi.setUp(_flutterPluginBinding.binaryMessenger, GeckoCookieApiImpl())
GeckoPrefApi.setUp(_flutterPluginBinding.binaryMessenger, GeckoPrefApiImpl())
GeckoContainerProxyApi.setUp(_flutterPluginBinding.binaryMessenger, GeckoContainerProxyApiImpl())
GeckoFindApi.setUp(_flutterPluginBinding.binaryMessenger, GeckoFindApiImpl())
GeckoSelectionActionController.setUp(_flutterPluginBinding.binaryMessenger, GeckoSelectionActionControllerImpl(
selectionActionDelegate
))
GeckoDeleteBrowsingDataController.setUp(_flutterPluginBinding.binaryMessenger, GeckoDeleteBrowsingDataControllerImpl())
GeckoDownloadsApi.setUp(_flutterPluginBinding.binaryMessenger, GeckoDownloadsApiImpl())
GeckoBrowserExtensionApi.setUp(_flutterPluginBinding.binaryMessenger, GeckoBrowserExtensionApiImpl())
ReaderViewEvents.setUp(
_flutterPluginBinding.binaryMessenger,
components.events.readerViewEvents
)
val intent = Intent(_flutterPluginBinding.applicationContext, NotificationActivity::class.java)
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
_flutterPluginBinding.applicationContext.startActivity(intent)
}
private fun showFragmentCallback(): Boolean {
if (!isPlatformViewRegistered) {
return false
}
if (activity == null || activity !is FragmentActivity) {
return false
}
val fragmentActivity = activity as FragmentActivity
// Check if the container view exists in the view hierarchy
val container = fragmentActivity.findViewById<View>(FRAGMENT_CONTAINER_ID)
if (container == null) {
// Container doesn't exist yet, retry later
return false
}
val nativeFragment = BrowserFragment.create()
val fm = fragmentActivity.supportFragmentManager
fm.beginTransaction()
.replace(FRAGMENT_CONTAINER_ID, nativeFragment)
.commitAllowingStateLoss()
return true
}
override fun onTrimMemory(level: Long) {
requireNotNull(GlobalComponents.components) { "Components not initialized" }
@@ -1,7 +1,10 @@
package eu.lensai.flutter_mozilla_components.api
import eu.lensai.flutter_mozilla_components.feature.BrowserExtensionFeature
import eu.lensai.flutter_mozilla_components.feature.ContainerProxyFeature
import eu.lensai.flutter_mozilla_components.feature.ResultConsumer
import eu.lensai.flutter_mozilla_components.pigeons.GeckoContainerProxyApi
import org.json.JSONObject
class GeckoContainerProxyApiImpl : GeckoContainerProxyApi {
override fun setProxyPort(port: Long) {
@@ -15,4 +18,18 @@ class GeckoContainerProxyApiImpl : GeckoContainerProxyApi {
override fun removeContainerProxy(contextId: String) {
ContainerProxyFeature.scheduleRequest("removeContainerProxy", contextId)
}
override fun healthcheck(callback: (Result<Boolean>) -> Unit) {
ContainerProxyFeature.scheduleRequestWithResponse("healthcheck", Unit, object :
ResultConsumer<JSONObject> {
override fun success(result: JSONObject) {
val resultStatus = result.getBoolean("result")
callback(Result.success(resultStatus))
}
override fun error(errorCode: String, errorMessage: String?, errorDetails: Any?) {
callback(Result.failure(Exception("$errorCode $errorMessage $errorDetails")))
}
})
}
}
@@ -20,6 +20,10 @@ object ContainerProxyFeature {
private const val CONTAINER_PROXY_REPORTER_EXTENSION_URL = "resource://android/assets/extensions/container_proxy/"
private const val CONTAINER_PROXY_REPORTER_MESSAGING_ID = "containerProxy"
private var nextRequestId: Int = 0
private val requestHandlers = HashMap<Int, ResultConsumer<JSONObject>>()
private val mutex = Mutex()
@VisibleForTesting
// This is an internal var to make it mutable for unit testing purposes only
internal var extensionController = WebExtensionController(
@@ -40,7 +44,55 @@ object ContainerProxyFeature {
}
}
fun scheduleRequestWithResponse(
command: String,
args: Any,
callback: ResultConsumer<JSONObject>
) {
val message = JSONObject()
message.put("action", command);
message.put("args", args)
runBlocking {
withContext(Dispatchers.Default) {
mutex.withLock {
message.put("id", nextRequestId)
requestHandlers[nextRequestId] = callback
nextRequestId += 1
extensionController.sendBackgroundMessage(message)
}
}
}
}
private class ContainerProxyBackgroundMessageHandler() : MessageHandler {
override fun onPortMessage(message: Any, port: Port) {
runBlocking {
withContext(Dispatchers.Default) {
mutex.withLock {
val messageJSON = message as JSONObject;
val type = messageJSON.getString("type")
if (type == "healthcheck") {
val requestId = messageJSON.getInt("id")
val status = messageJSON.getString("status")
if (status == "success") {
requestHandlers[requestId]?.success(message)
} else {
requestHandlers[requestId]?.error(
"Container Proxy",
"Failed to perform operation",
message.getString("error")
)
}
}
}
}
}
}
}
/**
@@ -50,7 +50,7 @@ class FlutterEventMiddleware(private val flutterEvents: GeckoStateEvents) : Midd
) {
when (action) {
is ContentAction.UpdateThumbnailAction -> {
val resized = action.thumbnail.resize(maxWidth = 640, maxHeight = 480);
val resized = action.thumbnail.resize(maxWidth = 1280, maxHeight = 800);
val bytes = resized.toWebPBytes()
runOnUiThread {
@@ -1994,6 +1994,7 @@ private open class GeckoPigeonCodec : StandardMessageCodec() {
/** Generated interface from Pigeon that represents a handler of messages from Flutter. */
interface GeckoBrowserApi {
fun initialize()
fun showNativeFragment(): Boolean
fun onTrimMemory(level: Long)
@@ -2006,6 +2007,22 @@ interface GeckoBrowserApi {
@JvmOverloads
fun setUp(binaryMessenger: BinaryMessenger, api: GeckoBrowserApi?, messageChannelSuffix: String = "") {
val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else ""
run {
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.flutter_mozilla_components.GeckoBrowserApi.initialize$separatedMessageChannelSuffix", codec)
if (api != null) {
channel.setMessageHandler { _, reply ->
val wrapped: List<Any?> = try {
api.initialize()
listOf(null)
} catch (exception: Throwable) {
wrapError(exception)
}
reply.reply(wrapped)
}
} else {
channel.setMessageHandler(null)
}
}
run {
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.flutter_mozilla_components.GeckoBrowserApi.showNativeFragment$separatedMessageChannelSuffix", codec)
if (api != null) {
@@ -3019,6 +3036,7 @@ interface GeckoContainerProxyApi {
fun setProxyPort(port: Long)
fun addContainerProxy(contextId: String)
fun removeContainerProxy(contextId: String)
fun healthcheck(callback: (Result<Boolean>) -> Unit)
companion object {
/** The codec used by GeckoContainerProxyApi. */
@@ -3083,6 +3101,24 @@ interface GeckoContainerProxyApi {
channel.setMessageHandler(null)
}
}
run {
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.flutter_mozilla_components.GeckoContainerProxyApi.healthcheck$separatedMessageChannelSuffix", codec)
if (api != null) {
channel.setMessageHandler { _, reply ->
api.healthcheck{ result: Result<Boolean> ->
val error = result.exceptionOrNull()
if (error != null) {
reply.reply(wrapError(error))
} else {
val data = result.getOrNull()
reply.reply(wrapResult(data))
}
}
}
} else {
channel.setMessageHandler(null)
}
}
}
}
}