implemented container site assignements & improved routing

This commit is contained in:
Fabian Freund
2025-11-09 07:25:54 +01:00
parent da232bf8aa
commit 107b2a9aa6
45 changed files with 1104 additions and 264 deletions
@@ -12,6 +12,7 @@ import eu.weblibre.flutter_mozilla_components.feature.BrowserExtensionFeature
import eu.weblibre.flutter_mozilla_components.feature.MLEngineFeature
import eu.weblibre.flutter_mozilla_components.pigeons.BounceTrackingProtectionMode
import eu.weblibre.flutter_mozilla_components.pigeons.BrowserExtensionEvents
import eu.weblibre.flutter_mozilla_components.pigeons.GeckoStateEvents
import eu.weblibre.flutter_mozilla_components.pigeons.QueryParameterStripping
import mozilla.components.browser.engine.gecko.GeckoEngine
import mozilla.components.browser.engine.gecko.fetch.GeckoViewFetchClient
@@ -81,7 +82,7 @@ object EngineProvider {
return runtime!!
}
fun createEngine(context: Context, defaultSettings: DefaultSettings, extensionEvents: BrowserExtensionEvents): Engine {
fun createEngine(context: Context, defaultSettings: DefaultSettings, extensionEvents: BrowserExtensionEvents, stateEvents: GeckoStateEvents): Engine {
Logger.debug("Creating Engine")
val runtime = getOrCreateRuntime(context)
@@ -89,7 +90,7 @@ object EngineProvider {
WebCompatFeature.install(it)
//CookieManagerFeature.install(it)
PrefManagerFeature.install(it)
ContainerProxyFeature.install(it)
ContainerProxyFeature.install(it, stateEvents)
BrowserExtensionFeature.install(it, extensionEvents)
MLEngineFeature.install(it)
}
@@ -25,6 +25,10 @@ class GeckoContainerProxyApiImpl : GeckoContainerProxyApi {
ContainerProxyFeature.scheduleRequest("removeContainerProxy", contextId)
}
override fun setSiteAssignments(assignments: Map<String, String>) {
ContainerProxyFeature.scheduleRequest("setSiteAssignments", JSONObject(assignments))
}
override fun healthcheck(callback: (Result<Boolean>) -> Unit) {
ContainerProxyFeature.scheduleRequestWithResponse("healthcheck", Unit, object :
ResultConsumer<JSONObject> {
@@ -114,7 +114,7 @@ class Core(
}
val engine: Engine by lazy {
EngineProvider.createEngine(context, engineSettings, extensionEvents)
EngineProvider.createEngine(context, engineSettings, extensionEvents, flutterEvents)
}
/**
@@ -158,7 +158,7 @@ class Core(
collectionUser = components.addonCollection.collectionUser,
collectionName = components.addonCollection.collectionName,
maxCacheAgeInMinutes = AMO_COLLECTION_MAX_CACHE_AGE
) else
) else
AMOAddonsProvider(
context,
client,
@@ -7,6 +7,9 @@
package eu.weblibre.flutter_mozilla_components.feature
import androidx.annotation.VisibleForTesting
import eu.weblibre.flutter_mozilla_components.GlobalComponents
import eu.weblibre.flutter_mozilla_components.pigeons.ContainerSiteAssignment
import eu.weblibre.flutter_mozilla_components.pigeons.GeckoStateEvents
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.sync.Mutex
@@ -16,20 +19,27 @@ 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.ktx.android.org.json.tryGetString
import mozilla.components.support.webextensions.BuiltInWebExtensionController
import org.json.JSONObject
import org.mozilla.gecko.util.ThreadUtils.runOnUiThread
object ContainerProxyFeature {
private val logger = Logger("container_proxy")
private const val CONTAINER_PROXY_REPORTER_EXTENSION_ID = "container-proxy@weblibre.eu"
private const val CONTAINER_PROXY_REPORTER_EXTENSION_URL = "resource://android/assets/extensions/container_proxy/"
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()
private val components by lazy {
requireNotNull(GlobalComponents.components) { "Components not initialized" }
}
@VisibleForTesting
// This is an internal var to make it mutable for unit testing purposes only
internal var extensionController = BuiltInWebExtensionController(
@@ -74,7 +84,9 @@ object ContainerProxyFeature {
}
}
private class ContainerProxyBackgroundMessageHandler() : MessageHandler {
private class ContainerProxyBackgroundMessageHandler(
private var events: GeckoStateEvents
) : MessageHandler {
override fun onPortMessage(message: Any, port: Port) {
runBlocking {
withContext(Dispatchers.Default) {
@@ -94,6 +106,25 @@ object ContainerProxyFeature {
message.getString("error")
)
}
} else if (type == "assignedSiteRequested") {
val requestId = messageJSON.getString("id")
val status = messageJSON.getString("status")
val details = messageJSON.getJSONObject("result")
if (status == "success") {
runOnUiThread {
events.onContainerSiteAssignment(
System.currentTimeMillis(),
ContainerSiteAssignment(
requestId = requestId,
tabId = components.core.store.state.selectedTabId,
originUrl = details.tryGetString("originUrl"),
url = details.getString("url"),
blocked = details.getBoolean("blocked")
)
) { _ -> }
}
}
}
}
}
@@ -108,9 +139,9 @@ object ContainerProxyFeature {
* @param productName a custom product name used to automatically label reports. Defaults to
* "android-components".
*/
fun install(runtime: WebExtensionRuntime) {
fun install(runtime: WebExtensionRuntime, events: GeckoStateEvents) {
extensionController.registerBackgroundMessageHandler(
ContainerProxyBackgroundMessageHandler()
ContainerProxyBackgroundMessageHandler(events)
)
extensionController.install(
runtime,
@@ -2277,6 +2277,46 @@ data class GeckoPref (
override fun hashCode(): Int = toList().hashCode()
}
/** Generated class from Pigeon that represents data sent in messages. */
data class ContainerSiteAssignment (
val requestId: String,
val tabId: String? = null,
val originUrl: String? = null,
val url: String,
val blocked: Boolean
)
{
companion object {
fun fromList(pigeonVar_list: List<Any?>): ContainerSiteAssignment {
val requestId = pigeonVar_list[0] as String
val tabId = pigeonVar_list[1] as String?
val originUrl = pigeonVar_list[2] as String?
val url = pigeonVar_list[3] as String
val blocked = pigeonVar_list[4] as Boolean
return ContainerSiteAssignment(requestId, tabId, originUrl, url, blocked)
}
}
fun toList(): List<Any?> {
return listOf(
requestId,
tabId,
originUrl,
url,
blocked,
)
}
override fun equals(other: Any?): Boolean {
if (other !is ContainerSiteAssignment) {
return false
}
if (this === other) {
return true
}
return GeckoPigeonUtils.deepEquals(toList(), other.toList()) }
override fun hashCode(): Int = toList().hashCode()
}
/** Generated class from Pigeon that represents data sent in messages. */
data class GeckoHeader (
val key: String,
@@ -2743,15 +2783,20 @@ private open class GeckoPigeonCodec : StandardMessageCodec() {
}
195.toByte() -> {
return (readValue(buffer) as? List<Any?>)?.let {
GeckoHeader.fromList(it)
ContainerSiteAssignment.fromList(it)
}
}
196.toByte() -> {
return (readValue(buffer) as? List<Any?>)?.let {
GeckoFetchRequest.fromList(it)
GeckoHeader.fromList(it)
}
}
197.toByte() -> {
return (readValue(buffer) as? List<Any?>)?.let {
GeckoFetchRequest.fromList(it)
}
}
198.toByte() -> {
return (readValue(buffer) as? List<Any?>)?.let {
GeckoFetchResponse.fromList(it)
}
@@ -3025,18 +3070,22 @@ private open class GeckoPigeonCodec : StandardMessageCodec() {
stream.write(194)
writeValue(stream, value.toList())
}
is GeckoHeader -> {
is ContainerSiteAssignment -> {
stream.write(195)
writeValue(stream, value.toList())
}
is GeckoFetchRequest -> {
is GeckoHeader -> {
stream.write(196)
writeValue(stream, value.toList())
}
is GeckoFetchResponse -> {
is GeckoFetchRequest -> {
stream.write(197)
writeValue(stream, value.toList())
}
is GeckoFetchResponse -> {
stream.write(198)
writeValue(stream, value.toList())
}
else -> super.writeValue(stream, value)
}
}
@@ -4257,6 +4306,7 @@ interface GeckoContainerProxyApi {
fun setProxyPort(port: Long)
fun addContainerProxy(contextId: String)
fun removeContainerProxy(contextId: String)
fun setSiteAssignments(assignments: Map<String, String>)
fun healthcheck(callback: (Result<Boolean>) -> Unit)
companion object {
@@ -4322,6 +4372,24 @@ interface GeckoContainerProxyApi {
channel.setMessageHandler(null)
}
}
run {
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.flutter_mozilla_components.GeckoContainerProxyApi.setSiteAssignments$separatedMessageChannelSuffix", codec)
if (api != null) {
channel.setMessageHandler { message, reply ->
val args = message as List<Any?>
val assignmentsArg = args[0] as Map<String, String>
val wrapped: List<Any?> = try {
api.setSiteAssignments(assignmentsArg)
listOf(null)
} catch (exception: Throwable) {
GeckoPigeonUtils.wrapError(exception)
}
reply.reply(wrapped)
}
} else {
channel.setMessageHandler(null)
}
}
run {
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.flutter_mozilla_components.GeckoContainerProxyApi.healthcheck$separatedMessageChannelSuffix", codec)
if (api != null) {
@@ -4744,6 +4812,23 @@ class GeckoStateEvents(private val binaryMessenger: BinaryMessenger, private val
}
}
}
fun onContainerSiteAssignment(timestampArg: Long, detailsArg: ContainerSiteAssignment, callback: (Result<Unit>) -> Unit)
{
val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else ""
val channelName = "dev.flutter.pigeon.flutter_mozilla_components.GeckoStateEvents.onContainerSiteAssignment$separatedMessageChannelSuffix"
val channel = BasicMessageChannel<Any?>(binaryMessenger, channelName, codec)
channel.send(listOf(timestampArg, detailsArg)) {
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(GeckoPigeonUtils.createConnectionError(channelName)))
}
}
}
}
/** Generated class from Pigeon that represents Flutter messages that can be called from Kotlin. */
class GeckoLogging(private val binaryMessenger: BinaryMessenger, private val messageChannelSuffix: String = "") {
File diff suppressed because it is too large Load Diff
@@ -5,6 +5,7 @@ import { ProxyType } from '../domain/ProxyType'
import BlockingResponse = browser.webRequest.BlockingResponse
import _OnAuthRequiredDetails = browser.webRequest._OnAuthRequiredDetails
import _OnRequestDetails = browser.proxy._OnRequestDetails
import _OnBeforeRequestDetails = browser.webRequest._OnBeforeRequestDetails
const localhosts = new Set(['localhost', '127.0.0.1', '[::1]'])
@@ -127,13 +128,79 @@ export default class BackgroundMain {
return doNotProxy
}
run(browser: { proxy: any }): void {
const filter = { urls: ['<all_urls>'] }
async onBeforeRequest(options: _OnBeforeRequestDetails, port: browser.runtime.Port): Promise<browser.webRequest.BlockingResponse> {
const tab = (options.tabId > -1) ? (await browser.tabs.get(options.tabId)) : null
browser.proxy.onRequest.addListener(this.onRequest.bind(this), filter)
if (options.frameId !== 0 || tab === null) {
return {};
}
const url = URL.parse(options.url);
if (url !== null && this.store.isSiteOriginAssigned(url)) {
let cookieStoreId: string
if (tab.cookieStoreId?.startsWith(containerIdentifier) === true) {
cookieStoreId = tab.cookieStoreId.substring(containerIdentifier.length)
} else if (tab.cookieStoreId === privateIdentifier) {
// Handle private tabs - use 'private' as identifier
cookieStoreId = 'private'
} else {
cookieStoreId = 'general'
}
if (this.store.isSiteOriginInSameContext(url, cookieStoreId)) {
if (tab.highlighted) {
port.postMessage({
"type": "assignedSiteRequested",
"id": options.requestId,
"status": "success",
"result": {
"originUrl": options.originUrl,
"url": options.url,
"blocked": false
}
});
return {};
} else {
//When tab not selected, block the request
return {
cancel: true,
};
}
} else {
//Only send events when tab is selected
if (tab.highlighted) {
port.postMessage({
"type": "assignedSiteRequested",
"id": options.requestId,
"status": "success",
"result": {
"originUrl": options.originUrl,
"url": options.url,
"blocked": true
}
});
}
return {
cancel: true,
};
}
}
return {};
}
run(browser: { proxy: any, webRequest: any }, port: browser.runtime.Port): void {
browser.proxy.onRequest.addListener(this.onRequest.bind(this), { urls: ['<all_urls>'] })
browser.proxy.onError.addListener((e: Error) => {
console.error('Proxy error', e)
})
browser.webRequest.onBeforeRequest.addListener((options: _OnBeforeRequestDetails) => {
return this.onBeforeRequest(options, port);
}, { urls: ["<all_urls>"], types: ["main_frame"] }, ["blocking"])
}
}
@@ -8,7 +8,7 @@ const store = new Store()
interface Message {
id: String | undefined;
action: 'setProxyPort' | 'addContainerProxy' | 'removeContainerProxy' | 'healthcheck';
action: 'setProxyPort' | 'addContainerProxy' | 'removeContainerProxy' | 'healthcheck' | 'setSiteAssignments';
args: any;
}
@@ -42,6 +42,11 @@ port.onMessage.addListener((raw: unknown): void => {
store.removeContainerProxyRelation(message.args, "tor")
console.log('removed container relation ' + message.args)
break
case "setSiteAssignments":
const entries = new Map(Object.entries(message.args))
console.log('set site assignments ' + JSON.stringify(message.args))
store.setSiteAssignments(entries);
break
case "healthcheck":
port.postMessage({
"type": "healthcheck",
@@ -54,4 +59,4 @@ port.onMessage.addListener((raw: unknown): void => {
});
const backgroundListener = new BackgroundMain({ store })
backgroundListener.run(browser)
backgroundListener.run(browser, port)
@@ -79,6 +79,24 @@ export class Store {
private proxies: ProxyDao[] = []
private relations: { [key: string]: string[] } = {}
private siteAssignments: Map<string, string> = new Map<string, string>()
setSiteAssignments(sites: Map<string, unknown>): void {
this.siteAssignments = new Map(
Array.from(sites, ([key, value]) => {
return [URL.parse(key)!.origin, value as string]
})
);
}
isSiteOriginAssigned(uri: URL): boolean {
return this.siteAssignments.has(uri.origin)
}
isSiteOriginInSameContext(uri: URL, contextId: string): boolean {
return this.siteAssignments.get(uri.origin) === contextId;
}
getAllProxies(): ProxySettings[] {
const proxyDaos = this.getAllProxyDaos()
return proxyDaos.map(tryFromDao).filter(p => p !== undefined) as ProxySettings[]
@@ -23,6 +23,10 @@ class GeckoContainerProxyService {
return _apiInstance.removeContainerProxy(contextId);
}
Future<void> setSiteAssignments(Map<String, String> assignments) {
return _apiInstance.setSiteAssignments(assignments);
}
Future<bool> healthcheck() async {
try {
return await _apiInstance.healthcheck().timeout(
@@ -40,6 +40,7 @@ class GeckoEventService extends GeckoStateEvents {
final _longPressSubject = PublishSubject<LongPressEvent>();
final _scrollEventSubject = PublishSubject<ScrollEvent>();
final _prefUpdateSubject = PublishSubject<GeckoPref>();
final _siteAssignementSubject = PublishSubject<ContainerSiteAssignment>();
final _tabAddedSubject = PublishSubject<String>();
@@ -61,6 +62,8 @@ class GeckoEventService extends GeckoStateEvents {
Stream<LongPressEvent> get longPressEvent => _longPressSubject.stream;
Stream<ScrollEvent> get scrollEvent => _scrollEventSubject.stream;
Stream<GeckoPref> get prefUpdateEvent => _prefUpdateSubject.stream;
Stream<ContainerSiteAssignment> get siteAssignementEvent =>
_siteAssignementSubject.stream;
Stream<String> get tabAddedStream => _tabAddedSubject.stream;
@@ -184,6 +187,18 @@ class GeckoEventService extends GeckoStateEvents {
_prefUpdateSubject.addWhenMoreRecent(timestamp, value.name, value);
}
@override
void onContainerSiteAssignment(
int timestamp,
ContainerSiteAssignment details,
) {
_siteAssignementSubject.addWhenMoreRecent(
timestamp,
details.requestId,
details,
);
}
GeckoEventService.setUp({
BinaryMessenger? binaryMessenger,
String messageChannelSuffix = '',
@@ -212,5 +227,6 @@ class GeckoEventService extends GeckoStateEvents {
unawaited(_scrollEventSubject.close());
unawaited(_tabAddedSubject.close());
unawaited(_prefUpdateSubject.close());
unawaited(_siteAssignementSubject.close());
}
}
@@ -2875,6 +2875,67 @@ class GeckoPref {
;
}
class ContainerSiteAssignment {
ContainerSiteAssignment({
required this.requestId,
this.tabId,
this.originUrl,
required this.url,
required this.blocked,
});
String requestId;
String? tabId;
String? originUrl;
String url;
bool blocked;
List<Object?> _toList() {
return <Object?>[
requestId,
tabId,
originUrl,
url,
blocked,
];
}
Object encode() {
return _toList(); }
static ContainerSiteAssignment decode(Object result) {
result as List<Object?>;
return ContainerSiteAssignment(
requestId: result[0]! as String,
tabId: result[1] as String?,
originUrl: result[2] as String?,
url: result[3]! as String,
blocked: result[4]! as bool,
);
}
@override
// ignore: avoid_equals_and_hash_code_on_mutable_classes
bool operator ==(Object other) {
if (other is! ContainerSiteAssignment || other.runtimeType != runtimeType) {
return false;
}
if (identical(this, other)) {
return true;
}
return _deepEquals(encode(), other.encode());
}
@override
// ignore: avoid_equals_and_hash_code_on_mutable_classes
int get hashCode => Object.hashAll(_toList())
;
}
class GeckoHeader {
GeckoHeader({
required this.key,
@@ -3284,15 +3345,18 @@ class _PigeonCodec extends StandardMessageCodec {
} else if (value is GeckoPref) {
buffer.putUint8(194);
writeValue(buffer, value.encode());
} else if (value is GeckoHeader) {
} else if (value is ContainerSiteAssignment) {
buffer.putUint8(195);
writeValue(buffer, value.encode());
} else if (value is GeckoFetchRequest) {
} else if (value is GeckoHeader) {
buffer.putUint8(196);
writeValue(buffer, value.encode());
} else if (value is GeckoFetchResponse) {
} else if (value is GeckoFetchRequest) {
buffer.putUint8(197);
writeValue(buffer, value.encode());
} else if (value is GeckoFetchResponse) {
buffer.putUint8(198);
writeValue(buffer, value.encode());
} else {
super.writeValue(buffer, value);
}
@@ -3456,10 +3520,12 @@ class _PigeonCodec extends StandardMessageCodec {
case 194:
return GeckoPref.decode(readValue(buffer)!);
case 195:
return GeckoHeader.decode(readValue(buffer)!);
return ContainerSiteAssignment.decode(readValue(buffer)!);
case 196:
return GeckoFetchRequest.decode(readValue(buffer)!);
return GeckoHeader.decode(readValue(buffer)!);
case 197:
return GeckoFetchRequest.decode(readValue(buffer)!);
case 198:
return GeckoFetchResponse.decode(readValue(buffer)!);
default:
return super.readValueOfType(type, buffer);
@@ -4990,6 +5056,29 @@ class GeckoContainerProxyApi {
}
}
Future<void> setSiteAssignments(Map<String, String> assignments) async {
final String pigeonVar_channelName = 'dev.flutter.pigeon.flutter_mozilla_components.GeckoContainerProxyApi.setSiteAssignments$pigeonVar_messageChannelSuffix';
final BasicMessageChannel<Object?> pigeonVar_channel = BasicMessageChannel<Object?>(
pigeonVar_channelName,
pigeonChannelCodec,
binaryMessenger: pigeonVar_binaryMessenger,
);
final Future<Object?> pigeonVar_sendFuture = pigeonVar_channel.send(<Object?>[assignments]);
final List<Object?>? pigeonVar_replyList =
await pigeonVar_sendFuture as List<Object?>?;
if (pigeonVar_replyList == null) {
throw _createConnectionError(pigeonVar_channelName);
} else if (pigeonVar_replyList.length > 1) {
throw PlatformException(
code: pigeonVar_replyList[0]! as String,
message: pigeonVar_replyList[1] as String?,
details: pigeonVar_replyList[2],
);
} else {
return;
}
}
Future<bool> healthcheck() async {
final String pigeonVar_channelName = 'dev.flutter.pigeon.flutter_mozilla_components.GeckoContainerProxyApi.healthcheck$pigeonVar_messageChannelSuffix';
final BasicMessageChannel<Object?> pigeonVar_channel = BasicMessageChannel<Object?>(
@@ -5170,6 +5259,8 @@ abstract class GeckoStateEvents {
void onPreferenceChange(int timestamp, GeckoPref value);
void onContainerSiteAssignment(int timestamp, ContainerSiteAssignment details);
static void setUp(GeckoStateEvents? api, {BinaryMessenger? binaryMessenger, String messageChannelSuffix = '',}) {
messageChannelSuffix = messageChannelSuffix.isNotEmpty ? '.$messageChannelSuffix' : '';
{
@@ -5641,6 +5732,34 @@ abstract class GeckoStateEvents {
});
}
}
{
final BasicMessageChannel<Object?> pigeonVar_channel = BasicMessageChannel<Object?>(
'dev.flutter.pigeon.flutter_mozilla_components.GeckoStateEvents.onContainerSiteAssignment$messageChannelSuffix', pigeonChannelCodec,
binaryMessenger: binaryMessenger);
if (api == null) {
pigeonVar_channel.setMessageHandler(null);
} else {
pigeonVar_channel.setMessageHandler((Object? message) async {
assert(message != null,
'Argument for dev.flutter.pigeon.flutter_mozilla_components.GeckoStateEvents.onContainerSiteAssignment was null.');
final List<Object?> args = (message as List<Object?>?)!;
final int? arg_timestamp = (args[0] as int?);
assert(arg_timestamp != null,
'Argument for dev.flutter.pigeon.flutter_mozilla_components.GeckoStateEvents.onContainerSiteAssignment was null, expected non-null int.');
final ContainerSiteAssignment? arg_details = (args[1] as ContainerSiteAssignment?);
assert(arg_details != null,
'Argument for dev.flutter.pigeon.flutter_mozilla_components.GeckoStateEvents.onContainerSiteAssignment was null, expected non-null ContainerSiteAssignment.');
try {
api.onContainerSiteAssignment(arg_timestamp!, arg_details!);
return wrapResponse(empty: true);
} on PlatformException catch (e) {
return wrapResponse(error: e);
} catch (e) {
return wrapResponse(error: PlatformException(code: 'error', message: e.toString()));
}
});
}
}
}
}
@@ -1131,6 +1131,7 @@ abstract class GeckoContainerProxyApi {
void setProxyPort(int port);
void addContainerProxy(String contextId);
void removeContainerProxy(String contextId);
void setSiteAssignments(Map<String, String> assignments);
@async
bool healthcheck();
@@ -1183,6 +1184,22 @@ abstract class GeckoCookieApi {
);
}
class ContainerSiteAssignment {
final String requestId;
final String? tabId;
final String? originUrl;
final String url;
final bool blocked;
ContainerSiteAssignment({
required this.requestId,
required this.tabId,
required this.originUrl,
required this.url,
required this.blocked,
});
}
@FlutterApi()
abstract class GeckoStateEvents {
void onViewReadyStateChange(int timestamp, bool state);
@@ -1210,6 +1227,11 @@ abstract class GeckoStateEvents {
void onScrollChange(int timestamp, String tabId, int scrollY);
void onPreferenceChange(int timestamp, GeckoPref value);
void onContainerSiteAssignment(
int timestamp,
ContainerSiteAssignment details,
);
}
@FlutterApi()