intermediate

This commit is contained in:
Fabian Freund
2024-09-13 09:11:06 +02:00
parent 3b047b5d8a
commit 37d2c84d7f
155 changed files with 5666 additions and 1816 deletions
@@ -0,0 +1,85 @@
'use strict';
console.log("Connecting to native port...")
const port = browser.runtime.connectNative("cookieManager");
console.log("Native port connected!")
function cookieToMap(cookie) {
let partitionKey = {}
if (cookie.partitionKey) {
partitionKey["topLevelSite"] = cookie.partitionKey.topLevelSite
}
return {
"domain": cookie.domain,
"expirationDate": cookie.expirationDate,
"firstPartyDomain": cookie.firstPartyDomain,
"hostOnly": cookie.hostOnly,
"httpOnly": cookie.httpOnly,
"name": cookie.name,
"partitionKey": partitionKey,
"path": cookie.path,
"secure": cookie.secure,
"session": cookie.session,
"sameSite": cookie.sameSite,
"storeId": cookie.storeId,
"value": cookie.value
}
}
function sendCookieResultForRequest(id) {
return function (cookie) {
port.postMessage({
"id": id,
"status": "success",
"result": cookieToMap(cookie)
})
}
}
function sendCookieListResultForRequest(id) {
return function (cookies) {
port.postMessage({
"id": id,
"status": "success",
"result": cookies.map((cookie) => cookieToMap(cookie))
})
}
}
function sendErrorForRequest(id) {
return function (error) {
console.error(error);
port.postMessage({
"id": id,
"status": "error",
"error": error
});
}
}
port.onMessage.addListener(message => {
let requestId = message["id"]
switch (message["action"]) {
case "get":
browser.cookies.get(message["args"])
.then(sendCookieResultForRequest(requestId))
.catch(sendErrorForRequest(requestId))
break
case "getAll":
browser.cookies.getAll(message["args"])
.then(sendCookieListResultForRequest(requestId))
.catch(sendErrorForRequest(requestId))
break
case "remove":
browser.cookies.remove(message["args"])
.then(sendCookieResultForRequest(requestId))
.catch(sendErrorForRequest(requestId))
break
case "set":
browser.cookies.set(message["args"])
.then(sendCookieResultForRequest(requestId))
.catch(sendErrorForRequest(requestId))
break
}
});
@@ -0,0 +1,23 @@
{
"manifest_version": 2,
"name": "cookie-manager",
"version": "1.0",
"description": "Cookie manager",
"browser_specific_settings": {
"gecko": {
"id": "cookie-manager@lensai.eu"
}
},
"background": {
"scripts": [
"background.js"
]
},
"permissions": [
"nativeMessaging",
"nativeMessagingFromContent",
"geckoViewAddons",
"cookies",
"<all_urls>"
]
}
@@ -8,6 +8,7 @@ import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.FrameLayout
import eu.lensai.flutter_mozilla_components.pigeons.GeckoStateEvents
import mozilla.components.browser.thumbnails.BrowserThumbnails
import mozilla.components.concept.engine.EngineView
import mozilla.components.feature.media.fullscreen.MediaSessionFullscreenFeature
@@ -9,9 +9,18 @@ import android.util.Log
import android.widget.Toast
import androidx.core.app.NotificationManagerCompat
import androidx.core.content.ContextCompat
import eu.lensai.flutter_mozilla_components.ext.toWebPBytes
import eu.lensai.flutter_mozilla_components.middleware.FlutterEventMiddleware
import eu.lensai.flutter_mozilla_components.pigeons.GeckoStateEvents
import eu.lensai.flutter_mozilla_components.pigeons.HistoryItem
import eu.lensai.flutter_mozilla_components.pigeons.HistoryState
import eu.lensai.flutter_mozilla_components.pigeons.ReaderableState
import eu.lensai.flutter_mozilla_components.pigeons.SecurityInfoState
import eu.lensai.flutter_mozilla_components.pigeons.TabContentState
import kotlinx.coroutines.MainScope
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.distinctUntilChangedBy
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.mapNotNull
import kotlinx.coroutines.launch
import mozilla.components.browser.engine.gecko.GeckoEngine
@@ -66,7 +75,7 @@ private const val DAY_IN_MINUTES = 24 * 60L
@SuppressLint("NewApi")
@Suppress("LargeClass")
open class DefaultComponents(private val applicationContext: Context) {
open class DefaultComponents(private val applicationContext: Context, private val flutterEvents: GeckoStateEvents) {
companion object {
const val SAMPLE_BROWSER_PREFERENCES = "sample_browser_preferences"
const val PREF_LAUNCH_EXTERNAL_APP = "sample_browser_launch_external_app"
@@ -133,7 +142,7 @@ open class DefaultComponents(private val applicationContext: Context) {
val store by lazy {
BrowserStore(
middleware = listOf(
FlutterEventMiddleware(),
FlutterEventMiddleware(flutterEvents),
DownloadMiddleware(applicationContext, DownloadService::class.java),
ReaderViewMiddleware(),
ThumbnailsMiddleware(thumbnailStorage),
@@ -145,12 +154,63 @@ open class DefaultComponents(private val applicationContext: Context) {
) + EngineMiddleware.create(engine),
).apply {
this.flowScoped { flow ->
flow.mapNotNull { state -> state.selectedTab }
.distinctUntilChangedBy {
it.content.url
flow.map { state -> state.selectedTabId }
.collect { tabId ->
flutterEvents.onSelectedTabChange(
tabId
) { _ -> }
}
}
this.flowScoped { flow ->
flow.mapNotNull { state -> state.tabs }
.filterChanged {
it.content.icon
}
.collect { tab ->
Log.d("URL_CHANGE", tab.content.url)
val iconBytes = tab.content.icon?.toWebPBytes()
flutterEvents.onIconChange(
tab.id,
iconBytes
) { _ -> }
}
}
this.flowScoped { flow ->
flow.mapNotNull { state -> state.tabs }
.filterChanged {
it.content.securityInfo
}
.collect { tab ->
flutterEvents.onSecurityInfoStateChange(
tab.id,
SecurityInfoState(
tab.content.securityInfo.secure,
tab.content.securityInfo.host,
tab.content.securityInfo.issuer,
)
) { _ -> }
}
}
this.flowScoped { flow ->
flow.mapNotNull { state -> state.tabs }
.filterChanged {
it.readerState
}
.ifAnyChanged { arrayOf(
it.readerState.readerable,
it.readerState.active,
)
}
.collect { tab ->
flutterEvents.onReaderableStateChange(
tab.id,
ReaderableState(
tab.readerState.readerable,
tab.readerState.active,
)
) { _ -> }
}
}
@@ -159,9 +219,61 @@ open class DefaultComponents(private val applicationContext: Context) {
.filterChanged {
it.content
}
.ifAnyChanged { arrayOf(it.content.url) }
.ifAnyChanged { arrayOf(
it.content.history,
it.content.canGoBack,
it.content.canGoForward,
)
}
.collect { tab ->
Log.d("URL_CHANGE2", tab.content.url)
flutterEvents.onHistoryStateChange(
tab.id,
HistoryState(
items = tab.content.history.items.map { item -> HistoryItem(
url = item.uri,
title = item.title
) },
currentIndex = tab.content.history.currentIndex.toLong(),
canGoBack = tab.content.canGoBack,
canGoForward = tab.content.canGoForward,
)
) { _ -> }
}
}
this.flowScoped { flow ->
flow.mapNotNull { state -> state.tabs.map {tab -> tab.id} }
.distinctUntilChanged()
.collect { tabs ->
flutterEvents.onTabListChange(tabs) { _ -> }
}
}
this.flowScoped { flow ->
flow.mapNotNull { state -> state.tabs }
.filterChanged {
it.content
}
.ifAnyChanged { arrayOf(
it.content.url,
it.content.title,
it.content.private,
it.content.fullScreen,
it.content.progress,
it.content.loading) }
.collect { tab ->
flutterEvents.onTabContentStateChange(
TabContentState(
id = tab.id,
contextId = tab.contextId,
url = tab.content.url,
title = tab.content.title,
progress = tab.content.progress.toLong(),
isPrivate = tab.content.private,
isFullScreen = tab.content.fullScreen,
isLoading = tab.content.loading
)
) { _ -> }
}
}
@@ -1,8 +1,5 @@
package eu.lensai.flutter_mozilla_components
import GeckoBrowserApi
import GeckoSessionApi
import GeckoTabsApi
import android.app.Activity
import android.content.Context
import android.content.Intent
@@ -10,8 +7,17 @@ import androidx.annotation.NonNull
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentActivity
import eu.lensai.flutter_mozilla_components.api.GeckoBrowserApiImpl
import eu.lensai.flutter_mozilla_components.api.GeckoCookieApiImpl
import eu.lensai.flutter_mozilla_components.api.GeckoIconsApiImpl
import eu.lensai.flutter_mozilla_components.api.GeckoSessionApiImpl
import eu.lensai.flutter_mozilla_components.api.GeckoTabsApiImpl
import eu.lensai.flutter_mozilla_components.feature.CookieManagerFeature
import eu.lensai.flutter_mozilla_components.pigeons.GeckoBrowserApi
import eu.lensai.flutter_mozilla_components.pigeons.GeckoCookieApi
import eu.lensai.flutter_mozilla_components.pigeons.GeckoIconsApi
import eu.lensai.flutter_mozilla_components.pigeons.GeckoSessionApi
import eu.lensai.flutter_mozilla_components.pigeons.GeckoStateEvents
import eu.lensai.flutter_mozilla_components.pigeons.GeckoTabsApi
import io.flutter.embedding.engine.plugins.FlutterPlugin
import io.flutter.embedding.engine.plugins.activity.ActivityAware
@@ -34,7 +40,7 @@ import org.mozilla.geckoview.GeckoRuntimeSettings
/**
* Helper class for lazily instantiating components needed by the application.
*/
class Components(private val applicationContext: Context) : DefaultComponents(applicationContext) {
class Components(private val applicationContext: Context, flutterEvents: GeckoStateEvents) : DefaultComponents(applicationContext, flutterEvents) {
private val runtime by lazy {
// Allow for exfiltrating Gecko metrics through the Glean SDK.
val builder = GeckoRuntimeSettings.Builder().aboutConfigEnabled(true)
@@ -56,6 +62,7 @@ class Components(private val applicationContext: Context) : DefaultComponents(ap
// }
WebCompatFeature.install(it)
CookieManagerFeature.install(it)
//WebCompatReporterFeature.install(it)
}
}
@@ -77,7 +84,9 @@ class FlutterMozillaComponentsPlugin: FlutterPlugin, ActivityAware {
override fun onAttachedToEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
_flutterPluginBinding = flutterPluginBinding
GlobalComponents.setUp(flutterPluginBinding.applicationContext)
val flutterEvents = GeckoStateEvents(_flutterPluginBinding.binaryMessenger)
GlobalComponents.setUp(flutterPluginBinding.applicationContext, flutterEvents)
val intent = Intent(flutterPluginBinding.applicationContext, NotificationActivity::class.java)
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
@@ -89,6 +98,8 @@ class FlutterMozillaComponentsPlugin: FlutterPlugin, ActivityAware {
GeckoSessionApi.setUp(_flutterPluginBinding.binaryMessenger, GeckoSessionApiImpl())
GeckoTabsApi.setUp(_flutterPluginBinding.binaryMessenger, GeckoTabsApiImpl())
GeckoIconsApi.setUp(_flutterPluginBinding.binaryMessenger, GeckoIconsApiImpl())
GeckoCookieApi.setUp(_flutterPluginBinding.binaryMessenger, GeckoCookieApiImpl())
}
private fun showNativeFragment() {
@@ -1,6 +1,7 @@
package eu.lensai.flutter_mozilla_components
import android.content.Context
import eu.lensai.flutter_mozilla_components.pigeons.GeckoStateEvents
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
@@ -28,8 +29,8 @@ object GlobalComponents {
.whenSessionsChange()
}
fun setUp(applicationContext: Context) {
val newComponents = Components(applicationContext)
fun setUp(applicationContext: Context, flutterEvents: GeckoStateEvents) {
val newComponents = Components(applicationContext, flutterEvents)
newComponents.crashReporter.install(applicationContext)
Facts.registerProcessor(LogFactProcessor())
@@ -1,10 +1,8 @@
package eu.lensai.flutter_mozilla_components.api
import GeckoBrowserApi
import eu.lensai.flutter_mozilla_components.pigeons.GeckoBrowserApi
class GeckoBrowserApiImpl(
private val showFragmentCallback: () -> Unit
) : GeckoBrowserApi {
class GeckoBrowserApiImpl(private val showFragmentCallback: () -> Unit) : GeckoBrowserApi {
override fun showNativeFragment() {
showFragmentCallback.invoke();
}
@@ -0,0 +1,309 @@
package eu.lensai.flutter_mozilla_components.api
import eu.lensai.flutter_mozilla_components.feature.CookieManagerFeature
import eu.lensai.flutter_mozilla_components.feature.ResultConsumer
import eu.lensai.flutter_mozilla_components.pigeons.Cookie
import eu.lensai.flutter_mozilla_components.pigeons.CookiePartitionKey
import eu.lensai.flutter_mozilla_components.pigeons.CookieSameSiteStatus
import eu.lensai.flutter_mozilla_components.pigeons.GeckoCookieApi
import org.json.JSONObject
class GeckoCookieApiImpl : GeckoCookieApi {
private fun toValueOrNull(json: JSONObject, key: String): Any? {
if (!json.has(key)) {
throw RuntimeException("Invalid map key")
}
if (!json.isNull(key)) {
return json.get(key)
}
return null
}
private fun cookiePartitionKeyFromJSON(inputJSON: JSONObject): CookiePartitionKey {
return CookiePartitionKey(
topLevelSite = inputJSON.getString("topLevelSite")
)
}
private fun CookiePartitionKey.toJSON(): JSONObject {
val json = JSONObject()
json.put("topLevelSite", topLevelSite)
return json
}
private fun cookieFromJSON(inputJSON: JSONObject): Cookie {
val partitionKeyRaw = toValueOrNull(inputJSON, "partitionKey") as JSONObject?
val partitionKey = if (partitionKeyRaw == null || partitionKeyRaw.length() == 0) null
else cookiePartitionKeyFromJSON(partitionKeyRaw)
return Cookie (
domain = inputJSON.getString("domain"),
expirationDate = (toValueOrNull(inputJSON, "expirationDate") as Int?)?.toLong(),
firstPartyDomain = inputJSON.getString("firstPartyDomain"),
hostOnly = inputJSON.getBoolean("hostOnly"),
httpOnly = inputJSON.getBoolean("httpOnly"),
name = inputJSON.getString("name"),
partitionKey = partitionKey,
path = inputJSON.getString("path"),
secure = inputJSON.getBoolean("secure"),
session = inputJSON.getBoolean("session"),
sameSite = when(inputJSON.getString("sameSite")) {
"no_restriction" -> CookieSameSiteStatus.NO_RESTRICTION
"lax" -> CookieSameSiteStatus.LAX
"strict" -> CookieSameSiteStatus.STRICT
else -> CookieSameSiteStatus.UNSPECIFIED
},
storeId = inputJSON.getString("storeId"),
value = inputJSON.getString("value")
)
}
override fun getCookie(
firstPartyDomain: String?,
name: String,
partitionKey: CookiePartitionKey?,
storeId: String?,
url: String,
callback: (Result<Cookie>) -> Unit
) {
val args = JSONObject()
if (firstPartyDomain == null) {
args.put("firstPartyDomain", JSONObject.NULL)
} else {
args.put("firstPartyDomain", firstPartyDomain)
}
args.put("name", name)
if (partitionKey == null) {
args.put("partitionKey", JSONObject.NULL)
} else {
args.put("partitionKey", partitionKey.toJSON())
}
if (storeId == null) {
args.put("storeId", JSONObject.NULL)
} else {
args.put("storeId", storeId)
}
args.put("url", url)
CookieManagerFeature.scheduleRequest("get", args, object: ResultConsumer<JSONObject> {
override fun success(result: JSONObject) {
callback(Result.success(cookieFromJSON(result.getJSONObject("result"))))
}
override fun error(errorCode: String, errorMessage: String?, errorDetails: Any?) {
callback(Result.failure(Exception("$errorCode $errorMessage $errorDetails")))
}
})
}
override fun getAllCookies(
domain: String?,
firstPartyDomain: String?,
name: String?,
partitionKey: CookiePartitionKey?,
storeId: String?,
url: String,
callback: (Result<List<Cookie>>) -> Unit
) {
val args = JSONObject()
if (domain == null) {
args.put("domain", JSONObject.NULL)
} else {
args.put("domain", domain)
}
if (firstPartyDomain == null) {
args.put("firstPartyDomain", JSONObject.NULL)
} else {
args.put("firstPartyDomain", firstPartyDomain)
}
args.put("name", name)
if (partitionKey == null) {
args.put("partitionKey", JSONObject.NULL)
} else {
args.put("partitionKey", partitionKey.toJSON())
}
if (storeId == null) {
args.put("storeId", JSONObject.NULL)
} else {
args.put("storeId", storeId)
}
args.put("url", url)
CookieManagerFeature.scheduleRequest("getAll", args, object: ResultConsumer<JSONObject> {
override fun success(result: JSONObject) {
val jsonArray = result.getJSONArray("result")
val cookies: MutableList<Cookie> = mutableListOf()
repeat(jsonArray.length()) {
index ->
cookies.add(cookieFromJSON(jsonArray.getJSONObject(index)))
}
callback(Result.success(cookies))
}
override fun error(errorCode: String, errorMessage: String?, errorDetails: Any?) {
callback(Result.failure(Exception("$errorCode $errorMessage $errorDetails")))
}
})
}
override fun setCookie(
domain: String?,
expirationDate: Long?,
firstPartyDomain: String?,
httpOnly: Boolean?,
name: String?,
partitionKey: CookiePartitionKey?,
path: String?,
sameSite: CookieSameSiteStatus?,
secure: Boolean?,
storeId: String?,
url: String,
value: String?,
callback: (Result<Unit>) -> Unit
) {
val args = JSONObject()
if (domain == null) {
args.put("domain", JSONObject.NULL)
} else {
args.put("domain", domain)
}
if (expirationDate == null) {
args.put("expirationDate", JSONObject.NULL)
} else {
args.put("expirationDate", domain)
}
if (firstPartyDomain == null) {
args.put("firstPartyDomain", JSONObject.NULL)
} else {
args.put("firstPartyDomain", firstPartyDomain)
}
if (httpOnly == null) {
args.put("httpOnly", JSONObject.NULL)
} else {
args.put("httpOnly", httpOnly)
}
if (name == null) {
args.put("name", JSONObject.NULL)
} else {
args.put("name", name)
}
if (partitionKey == null) {
args.put("partitionKey", JSONObject.NULL)
} else {
args.put("partitionKey", partitionKey.toJSON())
}
if (path == null) {
args.put("path", JSONObject.NULL)
} else {
args.put("path", path)
}
if (sameSite == null) {
args.put("sameSite", JSONObject.NULL)
} else {
args.put("sameSite", when(sameSite) {
CookieSameSiteStatus.NO_RESTRICTION -> "no_restriction"
CookieSameSiteStatus.LAX -> "lax"
CookieSameSiteStatus.STRICT -> "strict"
CookieSameSiteStatus.UNSPECIFIED -> ""
})
}
if (secure == null) {
args.put("secure", JSONObject.NULL)
} else {
args.put("secure", secure)
}
if (storeId == null) {
args.put("storeId", JSONObject.NULL)
} else {
args.put("storeId", storeId)
}
args.put("url", url)
if (value == null) {
args.put("value", JSONObject.NULL)
} else {
args.put("value", value)
}
CookieManagerFeature.scheduleRequest("set", args, object: ResultConsumer<JSONObject> {
override fun success(result: JSONObject) {
callback(Result.success(Unit))
}
override fun error(errorCode: String, errorMessage: String?, errorDetails: Any?) {
callback(Result.failure(Exception("$errorCode $errorMessage $errorDetails")))
}
})
}
override fun removeCookie(
firstPartyDomain: String?,
name: String,
partitionKey: CookiePartitionKey?,
storeId: String?,
url: String,
callback: (Result<Unit>) -> Unit
) {
val args = JSONObject()
if (firstPartyDomain == null) {
args.put("firstPartyDomain", JSONObject.NULL)
} else {
args.put("firstPartyDomain", firstPartyDomain)
}
args.put("name", name)
if (partitionKey == null) {
args.put("partitionKey", JSONObject.NULL)
} else {
args.put("partitionKey", partitionKey.toJSON())
}
if (storeId == null) {
args.put("storeId", JSONObject.NULL)
} else {
args.put("storeId", storeId)
}
args.put("url", url)
CookieManagerFeature.scheduleRequest("remove", args, object: ResultConsumer<JSONObject> {
override fun success(result: JSONObject) {
callback(Result.success(Unit))
}
override fun error(errorCode: String, errorMessage: String?, errorDetails: Any?) {
callback(Result.failure(Exception("$errorCode $errorMessage $errorDetails")))
}
})
}
}
@@ -0,0 +1,96 @@
package eu.lensai.flutter_mozilla_components.api
import android.graphics.Bitmap
import android.os.Build
import eu.lensai.flutter_mozilla_components.GlobalComponents
import eu.lensai.flutter_mozilla_components.ext.toWebPBytes
import eu.lensai.flutter_mozilla_components.pigeons.*
import kotlinx.coroutines.*
import mozilla.components.browser.icons.BrowserIcons
import mozilla.components.browser.icons.Icon
import mozilla.components.concept.engine.manifest.Size as HtmlSize
import java.io.ByteArrayOutputStream
typealias MozillaIconRequest = mozilla.components.browser.icons.IconRequest
typealias MozillaIconSize = mozilla.components.browser.icons.IconRequest.Size
typealias MozillaIconResource = mozilla.components.browser.icons.IconRequest.Resource
typealias MozillaIconResourceType = mozilla.components.browser.icons.IconRequest.Resource.Type
class GeckoIconsApiImpl() : GeckoIconsApi {
private val icons: BrowserIcons by lazy { GlobalComponents.components!!.icons }
override fun loadIcon(request: IconRequest, callback: (Result<IconResult>) -> Unit) {
CoroutineScope(Dispatchers.Default).launch {
runCatching {
loadIconAsync(request.toMozillaIconRequest())
}.fold(
onSuccess = { result ->
withContext(Dispatchers.Main) {
callback(Result.success(result))
}
},
onFailure = { error ->
withContext(Dispatchers.Main) {
callback(Result.failure(error))
}
}
)
}
}
private suspend fun loadIconAsync(request: MozillaIconRequest): IconResult {
val result = icons.loadIcon(request).await()
val imageBytes = result.bitmap.toWebPBytes()
return IconResult(
image = imageBytes,
maskable = result.maskable,
color = result.color?.toLong(),
source = result.source.toApiSource()
)
}
private fun IconRequest.toMozillaIconRequest(): MozillaIconRequest =
MozillaIconRequest(
url = url,
size = size.toMozillaSize(),
color = color?.toInt(),
waitOnNetworkLoad = waitOnNetworkLoad,
isPrivate = isPrivate,
resources = resources.filterNotNull().map { it.toMozillaResource() }
)
private fun IconSize.toMozillaSize(): MozillaIconSize = when (this) {
IconSize.DEFAULT_SIZE -> MozillaIconSize.DEFAULT
IconSize.LAUNCHER -> MozillaIconSize.LAUNCHER
IconSize.LAUNCHER_ADAPTIVE -> MozillaIconSize.LAUNCHER_ADAPTIVE
}
private fun Resource.toMozillaResource(): MozillaIconResource =
MozillaIconResource(
url = url,
mimeType = mimeType,
maskable = maskable,
type = type.toMozillaType(),
sizes = sizes.filterNotNull().map { HtmlSize(it.height.toInt(), it.width.toInt()) }
)
private fun IconType.toMozillaType(): MozillaIconResourceType = when (this) {
IconType.FAVICON -> MozillaIconResourceType.FAVICON
IconType.APPLE_TOUCH_ICON -> MozillaIconResourceType.APPLE_TOUCH_ICON
IconType.FLUID_ICON -> MozillaIconResourceType.FLUID_ICON
IconType.IMAGE_SRC -> MozillaIconResourceType.IMAGE_SRC
IconType.OPEN_GRAPH -> MozillaIconResourceType.OPENGRAPH
IconType.TWITTER -> MozillaIconResourceType.TWITTER
IconType.MICROSOFT_TILE -> MozillaIconResourceType.MICROSOFT_TILE
IconType.TIPPY_TOP -> MozillaIconResourceType.TIPPY_TOP
IconType.MANIFEST_ICON -> MozillaIconResourceType.MANIFEST_ICON
}
private fun Icon.Source.toApiSource(): Source = when (this) {
Icon.Source.GENERATOR -> Source.GENERATOR
Icon.Source.DOWNLOAD -> Source.DOWNLOAD
Icon.Source.INLINE -> Source.INLINE
Icon.Source.MEMORY -> Source.MEMORY
Icon.Source.DISK -> Source.DISK
}
}
@@ -1,16 +1,16 @@
package eu.lensai.flutter_mozilla_components.api
import GeckoSessionApi
import LoadUrlFlagsValue
import android.util.Log
import eu.lensai.flutter_mozilla_components.GlobalComponents
import eu.lensai.flutter_mozilla_components.pigeons.GeckoSessionApi
import eu.lensai.flutter_mozilla_components.pigeons.LoadUrlFlagsValue
import eu.lensai.flutter_mozilla_components.pigeons.TranslationOptions as PigeonTranslationOptions
import mozilla.components.browser.state.state.BrowserState
import mozilla.components.concept.engine.EngineSession
import mozilla.components.concept.engine.translate.TranslationOptions
import TranslationOptions as PigeonTranslationOptions
import mozilla.components.feature.session.SessionUseCases
class GeckoSessionApiImpl() : GeckoSessionApi {
class GeckoSessionApiImpl : GeckoSessionApi {
private val sessionUseCases: SessionUseCases by lazy { GlobalComponents.components!!.sessionUseCases }
private val state: BrowserState by lazy { GlobalComponents.components!!.store.state }
@@ -33,21 +33,19 @@ class GeckoSessionApiImpl() : GeckoSessionApi {
data = data,
tabId = tabId ?: state.selectedTabId,
mimeType = mimeType,
encoding = encoding,
encoding = encoding
)
}
override fun reload(tabId: String?, flags: LoadUrlFlagsValue) {
sessionUseCases.reload(
tabId = tabId ?: state.selectedTabId,
flags = EngineSession.LoadUrlFlags.select(flags.value.toInt()),
flags = EngineSession.LoadUrlFlags.select(flags.value.toInt())
)
}
override fun stopLoading(tabId: String?) {
sessionUseCases.stopLoading(
tabId = tabId
)
sessionUseCases.stopLoading(tabId = tabId ?: state.selectedTabId)
}
override fun goBack(tabId: String?, userInteraction: Boolean) {
@@ -74,26 +72,20 @@ class GeckoSessionApiImpl() : GeckoSessionApi {
override fun requestDesktopSite(tabId: String?, enable: Boolean) {
sessionUseCases.requestDesktopSite(
tabId = tabId ?: state.selectedTabId,
enable = enable,
enable = enable
)
}
override fun exitFullscreen(tabId: String?) {
sessionUseCases.exitFullscreen(
tabId = tabId ?: state.selectedTabId,
)
sessionUseCases.exitFullscreen(tabId = tabId ?: state.selectedTabId)
}
override fun saveToPdf(tabId: String?) {
sessionUseCases.saveToPdf(
tabId = tabId ?: state.selectedTabId,
)
sessionUseCases.saveToPdf(tabId = tabId ?: state.selectedTabId)
}
override fun printContent(tabId: String?) {
sessionUseCases.printContent(
tabId = tabId ?: state.selectedTabId,
)
sessionUseCases.printContent(tabId = tabId ?: state.selectedTabId)
}
override fun translate(
@@ -106,18 +98,16 @@ class GeckoSessionApiImpl() : GeckoSessionApi {
tabId = tabId ?: state.selectedTabId,
fromLanguage = fromLanguage,
toLanguage = toLanguage,
options = if (options != null) TranslationOptions(downloadModel = options.downloadModel) else null,
options = options?.let { TranslationOptions(downloadModel = it.downloadModel) }
)
}
override fun translateRestore(tabId: String?) {
sessionUseCases.translateRestore(
tabId = tabId ?: state.selectedTabId,
)
sessionUseCases.translateRestore(tabId = tabId ?: state.selectedTabId)
}
override fun crashRecovery(tabIds: List<String>?) {
if(tabIds != null) {
if (tabIds != null) {
sessionUseCases.crashRecovery.invoke(tabIds = tabIds)
} else {
sessionUseCases.crashRecovery.invoke()
@@ -134,4 +124,4 @@ class GeckoSessionApiImpl() : GeckoSessionApi {
lastAccess = lastAccess ?: System.currentTimeMillis()
)
}
}
}
@@ -1,13 +1,14 @@
package eu.lensai.flutter_mozilla_components.api
import GeckoTabsApi
import HistoryMetadataKey as PigeonHistoryMetadataKey
import LoadUrlFlagsValue
import RecoverableBrowserState as PigeonRecoverableBrowserState
import RestoreLocation as PigeonRestoreLocation
import RecoverableTab as PigeonRecoverableTab
import SourceValue
import eu.lensai.flutter_mozilla_components.pigeons.GeckoTabsApi
import eu.lensai.flutter_mozilla_components.pigeons.HistoryMetadataKey as PigeonHistoryMetadataKey
import eu.lensai.flutter_mozilla_components.pigeons.LoadUrlFlagsValue
import eu.lensai.flutter_mozilla_components.pigeons.RecoverableBrowserState as PigeonRecoverableBrowserState
import eu.lensai.flutter_mozilla_components.pigeons.RestoreLocation as PigeonRestoreLocation
import eu.lensai.flutter_mozilla_components.pigeons.RecoverableTab as PigeonRecoverableTab
import eu.lensai.flutter_mozilla_components.pigeons.SourceValue
import eu.lensai.flutter_mozilla_components.GlobalComponents
import eu.lensai.flutter_mozilla_components.pigeons.RestoreLocation
import mozilla.components.browser.session.storage.RecoverableBrowserState
import mozilla.components.browser.state.action.TabListAction
import mozilla.components.browser.state.selector.findTab
@@ -80,7 +81,11 @@ class GeckoTabsApiImpl() : GeckoTabsApi {
}
private fun mapRestoreLocation(t: PigeonRestoreLocation) : TabListAction.RestoreAction.RestoreLocation {
return TabListAction.RestoreAction.RestoreLocation.entries.first { it.ordinal == t.ordinal }
return when(t) {
RestoreLocation.BEGINNING -> TabListAction.RestoreAction.RestoreLocation.BEGINNING
RestoreLocation.END -> TabListAction.RestoreAction.RestoreLocation.END
RestoreLocation.AT_INDEX -> TabListAction.RestoreAction.RestoreLocation.AT_INDEX
}
}
override fun selectTab(tabId: String) {
@@ -0,0 +1,17 @@
package eu.lensai.flutter_mozilla_components.ext
import android.graphics.Bitmap
import android.os.Build
import java.io.ByteArrayOutputStream
fun Bitmap.toWebPBytes(): ByteArray {
val stream = ByteArrayOutputStream()
val compressFormat = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
Bitmap.CompressFormat.WEBP_LOSSLESS
} else {
@Suppress("DEPRECATION")
Bitmap.CompressFormat.WEBP
}
compress(compressFormat, 100, stream)
return stream.toByteArray()
}
@@ -0,0 +1,109 @@
package eu.lensai.flutter_mozilla_components.feature
import androidx.annotation.VisibleForTesting
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withContext
import mozilla.components.concept.engine.webextension.MessageHandler
import mozilla.components.concept.engine.webextension.Port
import mozilla.components.concept.engine.webextension.WebExtensionRuntime
import mozilla.components.support.base.log.logger.Logger
import mozilla.components.support.webextensions.WebExtensionController
import org.json.JSONObject
interface ResultConsumer<T> {
fun success(result: T)
fun error(errorCode: String, errorMessage: String?, errorDetails: Any?)
}
/**
* A feature that enables users to report site issues to Mozilla's Web Compatibility team for
* further diagnosis.
*/
object CookieManagerFeature {
private val logger = Logger("cookie-manager")
private const val COOKIE_MANAGER_REPORTER_EXTENSION_ID = "cookie-manager@lensai.eu"
private const val COOKIE_MANAGER_REPORTER_EXTENSION_URL = "resource://android/assets/extensions/cookie-manager/"
private const val COOKIE_MANAGER_REPORTER_MESSAGING_ID = "cookieManager"
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(
COOKIE_MANAGER_REPORTER_EXTENSION_ID,
COOKIE_MANAGER_REPORTER_EXTENSION_URL,
COOKIE_MANAGER_REPORTER_MESSAGING_ID,
)
fun scheduleRequest(command: String, args: JSONObject, 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 CookieManagerReporterBackgroundMessageHandler() : MessageHandler {
override fun onPortMessage(message: Any, port: Port) {
runBlocking {
withContext(Dispatchers.Default) {
mutex.withLock {
val messageJSON = message as JSONObject;
val requestId = messageJSON.getInt("id")
val status = messageJSON.getString("status")
if (status == "success") {
requestHandlers[requestId]?.success(message)
} else {
requestHandlers[requestId]?.error(
"Cookie Manager",
"Failed to perform operation",
message.getString("error")
)
}
}
}
}
}
}
/**
* Installs the web extension in the runtime through the WebExtensionRuntime install method
*
* @param runtime a WebExtensionRuntime.
* @param productName a custom product name used to automatically label reports. Defaults to
* "android-components".
*/
fun install(runtime: WebExtensionRuntime) {
extensionController.registerBackgroundMessageHandler(
CookieManagerReporterBackgroundMessageHandler(),
)
extensionController.install(
runtime,
onSuccess = {
logger.debug("Installed CookieManager webextension: ${it.id}")
},
onError = { throwable ->
logger.error("Failed to install CookieManager webextension: ", throwable)
},
)
}
}
@@ -3,18 +3,24 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package eu.lensai.flutter_mozilla_components.middleware
import android.graphics.Bitmap
import android.util.Log
import eu.lensai.flutter_mozilla_components.ext.toWebPBytes
import eu.lensai.flutter_mozilla_components.pigeons.GeckoStateEvents
import mozilla.components.browser.state.action.BrowserAction
import mozilla.components.browser.state.action.ContentAction
import mozilla.components.browser.state.state.BrowserState
import mozilla.components.lib.state.Middleware
import mozilla.components.lib.state.MiddlewareContext
import java.io.ByteArrayOutputStream
/**
* [Middleware] implementation for handling [ContentAction.UpdateThumbnailAction] and storing
* the thumbnail to the disk cache.
*/
class FlutterEventMiddleware() : Middleware<BrowserState, BrowserAction> {
class FlutterEventMiddleware(private val flutterEvents: GeckoStateEvents) : Middleware<BrowserState, BrowserAction> {
@Suppress("ComplexMethod")
override fun invoke(
context: MiddlewareContext<BrowserState, BrowserAction>,
@@ -23,10 +29,8 @@ class FlutterEventMiddleware() : Middleware<BrowserState, BrowserAction> {
) {
when (action) {
is ContentAction.UpdateThumbnailAction -> {
Log.d("UpdateThumbnailAction", action.sessionId)
}
is ContentAction.UpdateTitleAction -> {
Log.d("UpdateTitleAction", action.title)
val bytes = action.thumbnail.toWebPBytes()
flutterEvents.onThumbnailChange(action.sessionId, bytes) { _ -> }
}
else -> {
// no-op