major update

This commit is contained in:
Fabian Freund
2025-03-03 15:15:05 +01:00
parent 200c82a442
commit 20bdc4df3e
103 changed files with 2664 additions and 783 deletions
@@ -0,0 +1,40 @@
const FEED_MIME_TYPES = [
"application/atom",
"application/rss"
];
// Listen for any webRequest that might be a feed
browser.webRequest.onHeadersReceived.addListener(
function (details) {
let isFeed = false;
for (let header of details.responseHeaders) {
if (header.name.toLowerCase() === "content-type") {
const contentType = header.value.toLowerCase();
for (const mimeType of FEED_MIME_TYPES) {
if (contentType.includes(mimeType)) {
isFeed = true;
break;
}
}
if (isFeed) break;
}
}
if (isFeed) {
port.postMessage({
"type": "feedRequest",
"url": details.url
});
return { cancel: true };
}
// Not a feed, let the browser handle it normally
return { responseHeaders: details.responseHeaders };
},
{ urls: ["<all_urls>"] },
["blocking", "responseHeaders"]
);
@@ -2,20 +2,24 @@
"manifest_version": 2,
"browser_specific_settings": {
"gecko": {
"id": "turndown@movenext.me"
"id": "browser_extension@movenext.me"
}
},
"name": "Converts html to markdown",
"name": "Misc extensions",
"version": "1.0",
"background": {
"scripts": [
"readability.min.js",
"background.js"
"port.js",
"turndown.js",
"feed.js"
]
},
"permissions": [
"geckoViewAddons",
"nativeMessaging",
"webRequest",
"webRequestBlocking",
"<all_urls>"
]
}
@@ -0,0 +1 @@
const port = browser.runtime.connectNative("mozacBrowserExtension");
@@ -1,4 +1,3 @@
const port = browser.runtime.connectNative("mozacTurndownHtml");
const parser = new DOMParser();
port.onMessage.addListener(message => {
@@ -15,6 +14,7 @@ port.onMessage.addListener(message => {
});
port.postMessage({
"type": "turndown",
"id": requestId,
"status": "success",
"result": results
@@ -8,6 +8,7 @@ import eu.lensai.flutter_mozilla_components.components.Features
import eu.lensai.flutter_mozilla_components.components.Search
import eu.lensai.flutter_mozilla_components.components.Services
import eu.lensai.flutter_mozilla_components.components.UseCases
import eu.lensai.flutter_mozilla_components.pigeons.BrowserExtensionEvents
import eu.lensai.flutter_mozilla_components.pigeons.GeckoAddonEvents
import eu.lensai.flutter_mozilla_components.pigeons.GeckoStateEvents
import eu.lensai.flutter_mozilla_components.pigeons.GeckoSuggestionEvents
@@ -21,10 +22,11 @@ class Components(private val context: Context,
val flutterEvents: GeckoStateEvents,
val readerViewController: ReaderViewController,
val selectionAction: SelectionActionDelegate,
val addonEvents: GeckoAddonEvents,
val tabContentEvents: GeckoTabContentEvents
private val addonEvents: GeckoAddonEvents,
private val tabContentEvents: GeckoTabContentEvents,
private val extensionEvents: BrowserExtensionEvents
) {
val core by lazy { Core(context, this, flutterEvents) }
val core by lazy { Core(context, this, flutterEvents, extensionEvents) }
val events by lazy { Events(flutterEvents) }
val useCases by lazy { UseCases(context, core.engine, core.store) }
val services by lazy { Services(context, useCases.tabsUseCases) }
@@ -8,7 +8,8 @@ import android.content.Context
import eu.lensai.flutter_mozilla_components.feature.ContainerProxyFeature
import eu.lensai.flutter_mozilla_components.feature.CookieManagerFeature
import eu.lensai.flutter_mozilla_components.feature.PrefManagerFeature
import eu.lensai.flutter_mozilla_components.feature.TurndownFeature
import eu.lensai.flutter_mozilla_components.feature.BrowserExtensionFeature
import eu.lensai.flutter_mozilla_components.pigeons.BrowserExtensionEvents
import mozilla.components.browser.engine.gecko.GeckoEngine
import mozilla.components.browser.engine.gecko.fetch.GeckoViewFetchClient
import mozilla.components.concept.engine.DefaultSettings
@@ -44,7 +45,7 @@ object EngineProvider {
return runtime!!
}
fun createEngine(context: Context, defaultSettings: DefaultSettings): Engine {
fun createEngine(context: Context, defaultSettings: DefaultSettings, extensionEvents: BrowserExtensionEvents): Engine {
Logger.debug("Creating Engine")
val runtime = getOrCreateRuntime(context)
@@ -53,7 +54,7 @@ object EngineProvider {
CookieManagerFeature.install(it)
PrefManagerFeature.install(it)
ContainerProxyFeature.install(it)
TurndownFeature.install(it)
BrowserExtensionFeature.install(it, extensionEvents)
}
}
@@ -2,6 +2,7 @@ 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
@@ -18,11 +19,13 @@ import eu.lensai.flutter_mozilla_components.api.GeckoSelectionActionControllerIm
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.GeckoTurndownApiImpl
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
@@ -39,7 +42,6 @@ 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.GeckoTurndownApi
import eu.lensai.flutter_mozilla_components.pigeons.ReaderViewController
import eu.lensai.flutter_mozilla_components.pigeons.ReaderViewEvents
@@ -68,7 +70,6 @@ class FlutterMozillaComponentsPlugin: FlutterPlugin, ActivityAware {
private lateinit var _flutterEvents : GeckoStateEvents
private var isPlatformViewRegistered = false
private var pendingFragmentShow = false
override fun onAttachedToEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
synchronized(this) {
@@ -92,6 +93,8 @@ class FlutterMozillaComponentsPlugin: FlutterPlugin, ActivityAware {
val readerViewController =
ReaderViewController(_flutterPluginBinding.binaryMessenger)
val extensionEvents = BrowserExtensionEvents(_flutterPluginBinding.binaryMessenger)
val addonEvents = GeckoAddonEvents(_flutterPluginBinding.binaryMessenger)
val tabContentEvents = GeckoTabContentEvents(_flutterPluginBinding.binaryMessenger)
@@ -105,6 +108,7 @@ class FlutterMozillaComponentsPlugin: FlutterPlugin, ActivityAware {
selectionActionDelegate,
addonEvents,
tabContentEvents,
extensionEvents
)
GeckoBrowserApi.setUp(_flutterPluginBinding.binaryMessenger, GeckoBrowserApiImpl {
@@ -125,7 +129,7 @@ class FlutterMozillaComponentsPlugin: FlutterPlugin, ActivityAware {
))
GeckoDeleteBrowsingDataController.setUp(_flutterPluginBinding.binaryMessenger, GeckoDeleteBrowsingDataControllerImpl())
GeckoDownloadsApi.setUp(_flutterPluginBinding.binaryMessenger, GeckoDownloadsApiImpl())
GeckoTurndownApi.setUp(_flutterPluginBinding.binaryMessenger, GeckoTurndownApiImpl())
GeckoBrowserExtensionApi.setUp(_flutterPluginBinding.binaryMessenger, GeckoBrowserExtensionApiImpl())
ReaderViewEvents.setUp(
_flutterPluginBinding.binaryMessenger,
@@ -137,21 +141,31 @@ class FlutterMozillaComponentsPlugin: FlutterPlugin, ActivityAware {
flutterPluginBinding.applicationContext.startActivity(intent)
}
private fun showNativeFragment() {
private fun showNativeFragment(): Boolean {
if (!isPlatformViewRegistered) {
pendingFragmentShow = true
return
return false
}
if (activity == null) {
return
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 = (activity as FragmentActivity).supportFragmentManager
val fm = fragmentActivity.supportFragmentManager
fm.beginTransaction()
.replace(FRAGMENT_CONTAINER_ID, nativeFragment)
.commitAllowingStateLoss()
return true
}
override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) {
@@ -170,12 +184,6 @@ class FlutterMozillaComponentsPlugin: FlutterPlugin, ActivityAware {
)
isPlatformViewRegistered = true
// Process any pending fragment show request
if (pendingFragmentShow) {
pendingFragmentShow = false
showNativeFragment()
}
}
override fun onDetachedFromActivityForConfigChanges() {
@@ -189,6 +197,5 @@ class FlutterMozillaComponentsPlugin: FlutterPlugin, ActivityAware {
override fun onDetachedFromActivity() {
this.activity = null
isPlatformViewRegistered = false
pendingFragmentShow = false
}
}
@@ -37,7 +37,13 @@ private class NativeFragmentView(
FrameLayout.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT
)
container = FrameLayout(activity!!)
// Ensure activity is not null before creating the container
if (activity == null) {
throw IllegalStateException("Activity cannot be null when creating NativeFragmentView")
}
container = FrameLayout(activity)
container.layoutParams = vParams
container.id = containerId
}
@@ -45,8 +51,8 @@ private class NativeFragmentView(
override fun onFlutterViewAttached(flutterView: View) {
super.onFlutterViewAttached(flutterView)
components.engineReportedInitialized = false;
flutterEvents.onViewReadyStateChange(System.currentTimeMillis(),true) { _ -> }
components.engineReportedInitialized = false
flutterEvents.onViewReadyStateChange(System.currentTimeMillis(), true) { _ -> }
}
override fun getView(): View {
@@ -1,6 +1,7 @@
package eu.lensai.flutter_mozilla_components
import android.content.Context
import eu.lensai.flutter_mozilla_components.pigeons.BrowserExtensionEvents
import eu.lensai.flutter_mozilla_components.pigeons.GeckoAddonEvents
import eu.lensai.flutter_mozilla_components.pigeons.GeckoStateEvents
import eu.lensai.flutter_mozilla_components.pigeons.GeckoSuggestionEvents
@@ -44,7 +45,8 @@ object GlobalComponents {
readerViewController: ReaderViewController,
selectionAction: SelectionActionDelegate,
addonEvents: GeckoAddonEvents,
tabContentEvents: GeckoTabContentEvents
tabContentEvents: GeckoTabContentEvents,
extensionEvents: BrowserExtensionEvents
) {
Logger.debug("Creating new components")
@@ -54,7 +56,8 @@ object GlobalComponents {
readerViewController,
selectionAction,
addonEvents,
tabContentEvents
tabContentEvents,
extensionEvents
)
//newComponents.crashReporter.install(applicationContext)
@@ -9,17 +9,19 @@ import mozilla.components.feature.addons.logger
* Implementation of GeckoBrowserApi that handles browser-related operations
* @param showFragmentCallback Callback function to show native fragment
*/
class GeckoBrowserApiImpl(private val showFragmentCallback: () -> Unit) : GeckoBrowserApi {
class GeckoBrowserApiImpl(private val showFragmentCallback: () -> Boolean) : GeckoBrowserApi {
companion object {
private const val TAG = "GeckoBrowserApiImpl"
}
override fun showNativeFragment() {
override fun showNativeFragment(): Boolean {
try {
showFragmentCallback()
return showFragmentCallback()
} catch (e: Exception) {
logger.error("Failed to show native fragment", e)
}
return false
}
override fun onTrimMemory(level: Long) {
@@ -1,12 +1,12 @@
package eu.lensai.flutter_mozilla_components.api
import eu.lensai.flutter_mozilla_components.feature.ResultConsumer
import eu.lensai.flutter_mozilla_components.feature.TurndownFeature
import eu.lensai.flutter_mozilla_components.pigeons.GeckoTurndownApi
import eu.lensai.flutter_mozilla_components.feature.BrowserExtensionFeature
import eu.lensai.flutter_mozilla_components.pigeons.GeckoBrowserExtensionApi
import org.json.JSONArray
import org.json.JSONObject
class GeckoTurndownApiImpl : GeckoTurndownApi {
class GeckoBrowserExtensionApiImpl : GeckoBrowserExtensionApi {
private fun JSONObject.toMap(): Map<String, Any> {
val map = mutableMapOf<String, Any>()
val keys = this.keys()
@@ -38,7 +38,7 @@ class GeckoTurndownApiImpl : GeckoTurndownApi {
}
override fun getMarkdown(htmlList: List<String>, callback: (Result<List<Any>>) -> Unit) {
TurndownFeature.scheduleRequest("turndown", htmlList, object :
BrowserExtensionFeature.scheduleRequest("turndown", htmlList, object :
ResultConsumer<JSONObject> {
override fun success(result: JSONObject) {
val resultArray = result.getJSONArray("result")
@@ -14,6 +14,7 @@ import eu.lensai.flutter_mozilla_components.activities.NotificationActivity
import eu.lensai.flutter_mozilla_components.R
import eu.lensai.flutter_mozilla_components.ext.getPreferenceKey
import eu.lensai.flutter_mozilla_components.middleware.FlutterEventMiddleware
import eu.lensai.flutter_mozilla_components.pigeons.BrowserExtensionEvents
import eu.lensai.flutter_mozilla_components.pigeons.GeckoStateEvents
import kotlinx.coroutines.FlowPreview
import mozilla.components.browser.engine.gecko.permission.GeckoSitePermissionsStorage
@@ -54,6 +55,7 @@ private const val DAY_IN_MINUTES = 24 * 60L
class Core(private val context: Context,
private val components: Components,
private val flutterEvents: GeckoStateEvents,
private val extensionEvents: BrowserExtensionEvents
) {
val prefs by lazy {
PreferenceManager.getDefaultSharedPreferences(context)
@@ -95,7 +97,7 @@ class Core(private val context: Context,
}
val engine: Engine by lazy {
EngineProvider.createEngine(context, engineSettings)
EngineProvider.createEngine(context, engineSettings, extensionEvents)
}
/**
@@ -1,6 +1,7 @@
package eu.lensai.flutter_mozilla_components.feature
import androidx.annotation.VisibleForTesting
import eu.lensai.flutter_mozilla_components.pigeons.BrowserExtensionEvents
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.sync.Mutex
@@ -13,13 +14,15 @@ import mozilla.components.support.base.log.logger.Logger
import mozilla.components.support.webextensions.WebExtensionController
import org.json.JSONArray
import org.json.JSONObject
import org.mozilla.gecko.util.ThreadUtils.runOnUiThread
object TurndownFeature {
private val logger = Logger("turndown")
object BrowserExtensionFeature {
private val logger = Logger("browser_extension")
private const val PREF_MANAGER_REPORTER_EXTENSION_ID = "turndown@movenext.me"
private const val PREF_MANAGER_REPORTER_EXTENSION_URL = "resource://android/assets/extensions/turndown/"
private const val PREF_MANAGER_REPORTER_MESSAGING_ID = "mozacTurndownHtml"
private const val PREF_MANAGER_REPORTER_EXTENSION_ID = "browser_extension@movenext.me"
private const val PREF_MANAGER_REPORTER_EXTENSION_URL =
"resource://android/assets/extensions/browser_extension/"
private const val PREF_MANAGER_REPORTER_MESSAGING_ID = "mozacBrowserExtension"
private var nextRequestId: Int = 0
private val requestHandlers = HashMap<Int, ResultConsumer<JSONObject>>()
@@ -30,16 +33,22 @@ object TurndownFeature {
internal var extensionController = WebExtensionController(
PREF_MANAGER_REPORTER_EXTENSION_ID,
PREF_MANAGER_REPORTER_EXTENSION_URL,
PREF_MANAGER_REPORTER_MESSAGING_ID,
PREF_MANAGER_REPORTER_MESSAGING_ID
)
fun scheduleRequest(command: String, args: Any, callback: ResultConsumer<JSONObject>) {
fun scheduleRequest(
command: String,
args: Any,
callback: ResultConsumer<JSONObject>
) {
val message = JSONObject()
message.put("action", command);
message.put("args", when (args) {
is List<*> -> JSONArray(args)
else -> args
})
message.put(
"args", when (args) {
is List<*> -> JSONArray(args)
else -> args
}
)
runBlocking {
withContext(Dispatchers.Default) {
@@ -56,23 +65,37 @@ object TurndownFeature {
}
}
private class TurndownBackgroundMessageHandler() : MessageHandler {
private class ExtensionBackgroundMessageHandler(
private val extensionEvents: BrowserExtensionEvents
) : MessageHandler {
override fun onPortMessage(message: Any, port: Port) {
runBlocking {
withContext(Dispatchers.Default) {
mutex.withLock {
val messageJSON = message as JSONObject;
val type = messageJSON.getString("type")
val requestId = messageJSON.getInt("id")
val status = messageJSON.getString("status")
if (status == "success") {
requestHandlers[requestId]?.success(message)
} else {
requestHandlers[requestId]?.error(
"Pref Manager",
"Failed to perform operation",
message.getString("error")
)
if (type == "feedRequest") {
val url = messageJSON.getString("url")
runOnUiThread {
extensionEvents.onFeedRequested(
System.currentTimeMillis(),
url
) { _ -> }
}
} else if (type == "turndown") {
val requestId = messageJSON.getInt("id")
val status = messageJSON.getString("status")
if (status == "success") {
requestHandlers[requestId]?.success(message)
} else {
requestHandlers[requestId]?.error(
"Pref Manager",
"Failed to perform operation",
message.getString("error")
)
}
}
}
}
@@ -87,17 +110,17 @@ object TurndownFeature {
* @param productName a custom product name used to automatically label reports. Defaults to
* "android-components".
*/
fun install(runtime: WebExtensionRuntime) {
fun install(runtime: WebExtensionRuntime, extensionEvents: BrowserExtensionEvents) {
extensionController.registerBackgroundMessageHandler(
TurndownBackgroundMessageHandler(),
ExtensionBackgroundMessageHandler(extensionEvents)
)
extensionController.install(
runtime,
onSuccess = {
logger.debug("Installed Turndown webextension: ${it.id}")
logger.debug("Installed browser_extension webextension: ${it.id}")
},
onError = { throwable ->
logger.error("Failed to install Turndown webextension: ", throwable)
logger.error("Failed to install browser_extension webextension: ", throwable)
},
)
}
@@ -1994,7 +1994,7 @@ private open class GeckoPigeonCodec : StandardMessageCodec() {
/** Generated interface from Pigeon that represents a handler of messages from Flutter. */
interface GeckoBrowserApi {
fun showNativeFragment()
fun showNativeFragment(): Boolean
fun onTrimMemory(level: Long)
companion object {
@@ -2011,8 +2011,7 @@ interface GeckoBrowserApi {
if (api != null) {
channel.setMessageHandler { _, reply ->
val wrapped: List<Any?> = try {
api.showNativeFragment()
listOf(null)
listOf(api.showNativeFragment())
} catch (exception: Throwable) {
wrapError(exception)
}
@@ -2980,20 +2979,20 @@ interface GeckoPrefApi {
}
}
/** Generated interface from Pigeon that represents a handler of messages from Flutter. */
interface GeckoTurndownApi {
interface GeckoBrowserExtensionApi {
fun getMarkdown(htmlList: List<String>, callback: (Result<List<Any>>) -> Unit)
companion object {
/** The codec used by GeckoTurndownApi. */
/** The codec used by GeckoBrowserExtensionApi. */
val codec: MessageCodec<Any?> by lazy {
GeckoPigeonCodec()
}
/** Sets up an instance of `GeckoTurndownApi` to handle messages through the `binaryMessenger`. */
/** Sets up an instance of `GeckoBrowserExtensionApi` to handle messages through the `binaryMessenger`. */
@JvmOverloads
fun setUp(binaryMessenger: BinaryMessenger, api: GeckoTurndownApi?, messageChannelSuffix: String = "") {
fun setUp(binaryMessenger: BinaryMessenger, api: GeckoBrowserExtensionApi?, messageChannelSuffix: String = "") {
val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else ""
run {
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.flutter_mozilla_components.GeckoTurndownApi.getMarkdown$separatedMessageChannelSuffix", codec)
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.flutter_mozilla_components.GeckoBrowserExtensionApi.getMarkdown$separatedMessageChannelSuffix", codec)
if (api != null) {
channel.setMessageHandler { message, reply ->
val args = message as List<Any?>
@@ -4010,3 +4009,29 @@ interface GeckoDownloadsApi {
}
}
}
/** Generated class from Pigeon that represents Flutter messages that can be called from Kotlin. */
class BrowserExtensionEvents(private val binaryMessenger: BinaryMessenger, private val messageChannelSuffix: String = "") {
companion object {
/** The codec used by BrowserExtensionEvents. */
val codec: MessageCodec<Any?> by lazy {
GeckoPigeonCodec()
}
}
fun onFeedRequested(timestampArg: Long, urlArg: String, callback: (Result<Unit>) -> Unit)
{
val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else ""
val channelName = "dev.flutter.pigeon.flutter_mozilla_components.BrowserExtensionEvents.onFeedRequested$separatedMessageChannelSuffix"
val channel = BasicMessageChannel<Any?>(binaryMessenger, channelName, codec)
channel.send(listOf(timestampArg, urlArg)) {
if (it is List<*>) {
if (it.size > 1) {
callback(Result.failure(FlutterError(it[0] as String, it[1] as String, it[2] as String?)))
} else {
callback(Result.success(Unit))
}
} else {
callback(Result.failure(createConnectionError(channelName)))
}
}
}
}