container history initial

This commit is contained in:
Fabian Freund
2026-07-03 17:29:02 +02:00
parent 509e0550a8
commit 7931f66f5a
45 changed files with 7893 additions and 136 deletions
@@ -7,8 +7,10 @@
package eu.weblibre.flutter_mozilla_components
import eu.weblibre.flutter_mozilla_components.api.GeckoBrowserApiImpl
import eu.weblibre.flutter_mozilla_components.api.GeckoEngineSettingsApiImpl
import eu.weblibre.flutter_mozilla_components.feature.SandboxCaptureFeature
import eu.weblibre.flutter_mozilla_components.pigeons.GeckoBrowserApi
import eu.weblibre.flutter_mozilla_components.pigeons.GeckoEngineSettingsApi
import io.flutter.embedding.engine.plugins.FlutterPlugin
import io.flutter.embedding.engine.plugins.activity.ActivityAware
@@ -23,10 +25,25 @@ class FlutterMozillaComponentsPlugin: FlutterPlugin, ActivityAware {
browserApi.attachBinding(flutterPluginBinding)
GeckoBrowserApi.setUp(flutterPluginBinding.binaryMessenger, browserApi)
SandboxCaptureFeature.wireFlutterEvents(flutterPluginBinding.binaryMessenger)
// Register the engine-settings API at attach time (before GeckoBrowserService
// .initialize) so Dart can push the history-exclusion contextId set to native
// *before* the engine starts recording restored-tab visits, closing the
// startup window where an excluded container could leak to Places.
// setExcludedHistoryContextIds only writes GlobalComponents state and needs
// no initialized components; the remaining settings methods resolve
// components lazily and are not invoked until after initialize. The same
// instance is reused by GeckoBrowserApiImpl.initialize.
val engineSettingsApiImpl = GeckoEngineSettingsApiImpl(
flutterPluginBinding.applicationContext,
)
GeckoEngineSettingsApi.setUp(flutterPluginBinding.binaryMessenger, engineSettingsApiImpl)
GlobalComponents.engineSettingsApi = engineSettingsApiImpl
}
override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) {
SandboxCaptureFeature.detachFlutterEvents(binding.binaryMessenger)
GlobalComponents.historyEvents = null
}
override fun onAttachedToActivity(binding: ActivityPluginBinding) {
@@ -8,6 +8,8 @@ package eu.weblibre.flutter_mozilla_components
import android.content.Context
import android.content.Intent
import androidx.core.content.edit
import androidx.preference.PreferenceManager
import eu.weblibre.flutter_mozilla_components.pigeons.AddonCollection
import eu.weblibre.flutter_mozilla_components.pigeons.BrowserExtensionEvents
import eu.weblibre.flutter_mozilla_components.pigeons.BounceTrackingProtectionMode
@@ -15,6 +17,7 @@ import eu.weblibre.flutter_mozilla_components.pigeons.ContentBlocking
import eu.weblibre.flutter_mozilla_components.pigeons.GeckoAddonEvents
import eu.weblibre.flutter_mozilla_components.pigeons.GeckoEngineSettings
import eu.weblibre.flutter_mozilla_components.pigeons.GeckoGestureEvents
import eu.weblibre.flutter_mozilla_components.pigeons.GeckoHistoryEvents
import eu.weblibre.flutter_mozilla_components.pigeons.GeckoSelectionActionEvents
import eu.weblibre.flutter_mozilla_components.pigeons.GeckoStateEvents
import eu.weblibre.flutter_mozilla_components.pigeons.GeckoSuggestionEvents
@@ -59,7 +62,9 @@ private const val HISTORY_METADATA_MAX_AGE_IN_MS = 14L * 24 * 60 * 60 * 1000 //
private const val DEFAULT_QUERY_PARAMETER_STRIPPING_STRIP_LIST =
"__hsfp __hssc __hstc __s _bhlid _branch_match_id _branch_referrer _gl _hsenc _kx _openstat at_recipient_id at_recipient_list bbeml bsft_clkid bsft_uid dclid et_rid fb_action_ids fb_comment_id fbclid gbraid gclid guce_referrer guce_referrer_sig hsCtaTracking igshid irclickid mc_eid mkt_tok ml_subscriber ml_subscriber_hash msclkid mtm_cid oft_c oft_ck oft_d oft_id oft_ids oft_k oft_lk oft_sk oly_anon_id oly_enc_id pk_cid rb_clickid s_cid sc_customer sc_eh sc_uid sms_click sms_source sms_uph srsltid ss_email_id syclid ttclid twclid unicorn_click_id vero_conv vero_id vgo_ee wbraid wickedid yclid ymclid ysclid"
private const val UBLOCK_FILTER_LISTS_PREF = "browser.weblibre.uBO.filterLists"
private const val EXCLUDED_HISTORY_CONTEXT_IDS_PREF =
"browser.weblibre.excludedHistoryContextIds"
object GlobalComponents {
private var _components: Components? = null
private var currentMode: ComponentsMode? = null
@@ -105,6 +110,36 @@ object GlobalComponents {
// GestureRecognizer on the UI thread.
var gestureEvents: GeckoGestureEvents? = null
// Native -> Dart history visit notifications, consumed by Core's history
// delegate to forward the visit's WebLibre container. Null on the headless
// path (no Flutter engine); the delegate still hard-excludes persisted
// container contextIds but skips Dart relation emits.
var historyEvents: GeckoHistoryEvents? = null
// Gecko contextIds of containers with hard exclude-from-history enabled.
// Pushed from Dart; read by WebLibreHistoryDelegate to skip the Places
// write for visits resolved to one of these containers.
@Volatile
var excludedHistoryContextIds: Set<String> = emptySet()
fun setExcludedHistoryContextIds(context: Context?, contextIds: Collection<String>) {
val contextIdSet = contextIds.toSet()
excludedHistoryContextIds = contextIdSet
if (context != null) {
PreferenceManager.getDefaultSharedPreferences(context).edit {
putStringSet(EXCLUDED_HISTORY_CONTEXT_IDS_PREF, contextIdSet)
}
}
}
fun loadExcludedHistoryContextIds(context: Context) {
excludedHistoryContextIds = PreferenceManager.getDefaultSharedPreferences(context)
.getStringSet(EXCLUDED_HISTORY_CONTEXT_IDS_PREF, emptySet())
.orEmpty()
.toSet()
}
@Volatile
var gestureConfig: GestureConfig? = null
@@ -461,6 +496,9 @@ object GlobalComponents {
val profileContext = ProfileContext(baseContext.applicationContext, profileFolder)
val messenger = NoopBinaryMessenger()
loadExcludedHistoryContextIds(baseContext.applicationContext)
historyEvents = null
val selectionActionEvents = GeckoSelectionActionEvents(messenger)
val selectionActionDelegate = DefaultSelectionActionDelegate(selectionActionEvents) { actions ->
val processTextAction = "android.intent.action.PROCESS_TEXT"
@@ -493,7 +531,7 @@ object GlobalComponents {
mode = ComponentsMode.EXTERNAL,
)
engineSettingsApi = GeckoEngineSettingsApiImpl()
engineSettingsApi = GeckoEngineSettingsApiImpl(baseContext.applicationContext)
return true
}
@@ -49,6 +49,7 @@ import eu.weblibre.flutter_mozilla_components.pigeons.GeckoMlApi
import eu.weblibre.flutter_mozilla_components.pigeons.GeckoPrefApi
import eu.weblibre.flutter_mozilla_components.pigeons.GeckoPwaApi
import eu.weblibre.flutter_mozilla_components.pigeons.GeckoSelectionActionController
import eu.weblibre.flutter_mozilla_components.pigeons.GeckoHistoryEvents
import eu.weblibre.flutter_mozilla_components.pigeons.GeckoSelectionActionEvents
import eu.weblibre.flutter_mozilla_components.pigeons.GeckoSessionApi
import eu.weblibre.flutter_mozilla_components.pigeons.GeckoStateEvents
@@ -265,6 +266,12 @@ class GeckoBrowserApiImpl : GeckoBrowserApi {
val syncStateEvents = GeckoSyncStateEvents(_flutterPluginBinding.binaryMessenger)
// Set before GlobalComponents.setUp (which lazily builds the engine and
// its history delegate) so Core can wrap the delegate to forward the
// visit's WebLibre container to Dart.
GlobalComponents.historyEvents =
GeckoHistoryEvents(_flutterPluginBinding.binaryMessenger)
GlobalComponents.setUp(
profileApplicationContext,
_flutterEvents,
@@ -281,12 +288,20 @@ class GeckoBrowserApiImpl : GeckoBrowserApi {
syncTokenServerOverride,
)
val engineSettingsApiImpl = GeckoEngineSettingsApiImpl()
GeckoEngineSettingsApi.setUp(
_flutterPluginBinding.binaryMessenger,
engineSettingsApiImpl
)
GlobalComponents.engineSettingsApi = engineSettingsApiImpl
// GeckoEngineSettingsApi was already registered at plugin-attach time
// (FlutterMozillaComponentsPlugin.onAttachedToEngine) so the startup
// history-exclusion push lands before the engine starts. Reuse that
// instance; only register a fresh one if attach somehow did not run.
if (GlobalComponents.engineSettingsApi == null) {
val engineSettingsApiImpl = GeckoEngineSettingsApiImpl(
_flutterPluginBinding.applicationContext,
)
GeckoEngineSettingsApi.setUp(
_flutterPluginBinding.binaryMessenger,
engineSettingsApiImpl
)
GlobalComponents.engineSettingsApi = engineSettingsApiImpl
}
GeckoAddonsApi.setUp(
_flutterPluginBinding.binaryMessenger,
GeckoAddonsApiImpl(profileApplicationContext)
@@ -6,6 +6,7 @@
package eu.weblibre.flutter_mozilla_components.api
import android.content.Context
import androidx.core.content.edit
import androidx.preference.PreferenceManager
import eu.weblibre.flutter_mozilla_components.ColorSchemePreference
@@ -74,7 +75,9 @@ internal fun TrackingProtectionPolicy.withBounceTrackingProtectionMode(
/**
* Implementation of GeckoEngineSettingsApi that manages engine-specific settings
*/
class GeckoEngineSettingsApiImpl : GeckoEngineSettingsApi {
class GeckoEngineSettingsApiImpl(
private val applicationContext: Context? = null,
) : GeckoEngineSettingsApi {
companion object {
private const val TAG = "GeckoEngineSettingsApi"
}
@@ -515,4 +518,8 @@ class GeckoEngineSettingsApiImpl : GeckoEngineSettingsApi {
activeReaderSessions,
)
}
override fun setExcludedHistoryContextIds(contextIds: List<String>) {
GlobalComponents.setExcludedHistoryContextIds(applicationContext, contextIds)
}
}
@@ -18,6 +18,7 @@ import eu.weblibre.flutter_mozilla_components.services.DownloadService
import eu.weblibre.flutter_mozilla_components.EngineProvider
import eu.weblibre.flutter_mozilla_components.EngineProvider.getOrCreateRuntime
import eu.weblibre.flutter_mozilla_components.GlobalComponents
import eu.weblibre.flutter_mozilla_components.history.WebLibreHistoryDelegate
import eu.weblibre.flutter_mozilla_components.PermissionStorage
import eu.weblibre.flutter_mozilla_components.services.MediaSessionService
import eu.weblibre.flutter_mozilla_components.activities.NotificationActivity
@@ -26,6 +27,7 @@ import eu.weblibre.flutter_mozilla_components.ext.getPreferenceKey
import eu.weblibre.flutter_mozilla_components.middleware.FlutterEventMiddleware
import eu.weblibre.flutter_mozilla_components.middleware.HistoryMetadataMiddleware
import eu.weblibre.flutter_mozilla_components.middleware.HistoryMetadataService
import eu.weblibre.flutter_mozilla_components.middleware.HistoryVisitCorrelationMiddleware
import eu.weblibre.flutter_mozilla_components.middleware.SandboxCaptureMiddleware
import eu.weblibre.flutter_mozilla_components.middleware.SaveToPDFMiddleware
import eu.weblibre.flutter_mozilla_components.pigeons.BrowserExtensionEvents
@@ -99,9 +101,14 @@ class Core(
val engineSettings by lazy {
DefaultSettings(
//historyTrackingDelegate = HistoryDelegate(lazyHistoryStorage)
requestInterceptor = requestInterceptor,
historyTrackingDelegate = HistoryDelegate(lazyHistoryStorage),
// Wrap the Places-feeding delegate so WebLibre can hard-exclude
// container visits even on the headless external path. When Flutter
// is available, the wrapper also emits visit→container relations.
historyTrackingDelegate = WebLibreHistoryDelegate(
HistoryDelegate(lazyHistoryStorage),
GlobalComponents.historyEvents,
),
testingModeEnabled = false,
remoteDebuggingEnabled = false,
automaticFontSizeAdjustment = true,
@@ -225,6 +232,9 @@ class Core(
// sandbox new-tab URLs before Gecko issues a request.
SandboxCaptureMiddleware,
HistoryMetadataMiddleware(historyMetadataService),
// Correlates url -> contextId so WebLibreHistoryDelegate can
// resolve a visit's container at record time.
HistoryVisitCorrelationMiddleware(),
FlutterEventMiddleware(flutterEvents),
DownloadMiddleware(
applicationContext = context,
@@ -0,0 +1,80 @@
package eu.weblibre.flutter_mozilla_components.history
/**
* Short-lived `url -> contextId` correlation captured at navigation time.
*
* Android Components strips the session before it reaches
* [mozilla.components.concept.engine.history.HistoryTrackingDelegate.onVisited],
* so the delegate only sees the visited URL — not which tab (and therefore which
* Gecko contextual identity / WebLibre container) produced it. The
* [eu.weblibre.flutter_mozilla_components.middleware.HistoryVisitCorrelationMiddleware]
* records the navigating tab's contextId here on every `UpdateUrlAction`; the
* delegate reads it back when the matching visit is recorded moments later.
*
* This is the primary signal: the delegate trusts a live correlation before
* falling back to the selected tab (see
* [eu.weblibre.flutter_mozilla_components.history.WebLibreHistoryDelegate]). The
* TTL is deliberately short — a record and its visit are milliseconds apart, so
* anything older is an orphan (e.g. recorded on a reload that fired its
* `onVisited` before the record) and must expire quickly, or it can leak onto a
* later same-URL visit in a different container.
*/
object HistoryVisitCorrelationCache {
private const val TTL_MS = 3_000L
private const val MAX_ENTRIES = 64
private data class Entry(val contextId: String?, val timestamp: Long)
// Insertion-ordered so we can drop the oldest entry when over capacity.
private val entries = LinkedHashMap<String, Entry>()
/** Record the [contextId] (nullable = uncontained tab) that navigated to [url]. */
@Synchronized
fun record(url: String, contextId: String?) {
val now = System.currentTimeMillis()
evictExpired(now)
// Re-insert so the most recent navigation to a URL wins and stays newest.
entries.remove(url)
entries[url] = Entry(contextId, now)
while (entries.size > MAX_ENTRIES) {
val oldest = entries.keys.firstOrNull() ?: break
entries.remove(oldest)
}
}
/** A live correlation for a URL: present in the cache, its [contextId]
* possibly null (the producing tab was uncontained). Distinct from a cache
* miss (`resolve` returns null), so the caller can tell a known-uncontained
* navigation apart from an unknown one. */
data class Resolution(val contextId: String?)
/**
* The correlation most recently recorded for [url] within the TTL, or null
* if there is no live entry. A non-null [Resolution] with a null contextId
* means the producing tab was uncontained — the caller must treat that as an
* authoritative "uncontained" answer, NOT as a miss, so it does not fall
* back to a less specific signal (e.g. a merely-loading foreground tab).
*
* Consume-on-read: the entry is removed so a stale correlation can NEVER be
* reused for a later, unrelated visit of the same URL — e.g. a page first
* visited in a container and later reopened uncontained (or in a different
* container) must not inherit the earlier container. A repeat navigation
* re-records it via the middleware.
*/
@Synchronized
fun resolve(url: String): Resolution? {
val now = System.currentTimeMillis()
evictExpired(now)
val entry = entries.remove(url) ?: return null
return Resolution(entry.contextId)
}
private fun evictExpired(now: Long) {
val iterator = entries.entries.iterator()
while (iterator.hasNext()) {
if (now - iterator.next().value.timestamp > TTL_MS) {
iterator.remove()
}
}
}
}
@@ -0,0 +1,150 @@
package eu.weblibre.flutter_mozilla_components.history
import eu.weblibre.flutter_mozilla_components.GlobalComponents
import eu.weblibre.flutter_mozilla_components.pigeons.GeckoHistoryEvents
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import mozilla.components.browser.state.selector.selectedTab
import mozilla.components.concept.engine.history.HistoryTrackingDelegate
import mozilla.components.concept.storage.PageVisit
/**
* Wraps the Android Components [HistoryTrackingDelegate] (which feeds Mozilla
* Places / FxA sync) and, on each recorded visit, additionally notifies Dart of
* the WebLibre container that produced it.
*
* Mozilla Places stays the source of truth for the visit itself (url, title,
* visit type, visit time); WebLibre only needs the one thing Places can't store:
* which container the visit belonged to. Places strips the session before
* [onVisited], so the container is recovered from the `url -> contextId`
* correlation cache populated by
* [eu.weblibre.flutter_mozilla_components.middleware.HistoryVisitCorrelationMiddleware]
* at navigation time; Dart maps the contextId to a WebLibre container and writes
* the visit→container relation row.
*
* Purely additive for Places/sync: every call delegates to [wrapped]. The one
* exception is hard exclude-from-history — for a container opted out of history
* recording, the Places write is skipped too so the visit never lands anywhere.
*/
class WebLibreHistoryDelegate(
private val wrapped: HistoryTrackingDelegate,
private val events: GeckoHistoryEvents?,
) : HistoryTrackingDelegate by wrapped {
override suspend fun onVisited(uri: String, visit: PageVisit) {
// Resolve which container produced this visit once, and reuse it for
// both the hard-exclude decision and the Dart relation emit.
val resolution = resolveContextId(uri)
// Hard exclude-from-history: for a visit whose container has
// exclude-from-history enabled, skip BOTH the Places write and the Dart
// relation emit — the visit must not land in either store.
if (isHistoryExcluded(uri, resolution)) {
return
}
wrapped.onVisited(uri, visit)
// Timestamp near the Places record time; Dart joins to the actual Places
// visit by (url, nearest visit_time), tolerating the small skew. Best
// effort for tagging: even the non-authoritative guess is emitted, since
// a wrong tag is recoverable (unlike the exclude decision above).
val visitTime = System.currentTimeMillis()
// Flutter's binary messenger must be used on the platform (main) thread.
if (events != null) {
withContext(Dispatchers.Main) {
events.onVisitRecorded(uri, visitTime, resolution.contextId) {}
}
}
}
/**
* Decide whether this visit belongs to a hard exclude-from-history
* ("incognito") container and must therefore land nowhere.
*
* Fails **closed**: unlike tagging (where a missing tag is recoverable),
* leaking an excluded container's visit to Places is not, so any ambiguity
* that could involve an excluded container is resolved as "excluded".
*
* - When [resolution] is authoritative (steps 12 pinned the producing tab),
* trust it: excluded iff that tab's contextId is in the excluded set.
* - Otherwise (the last-resort step-3 guess or a full miss), scan open tabs:
* if any non-private tab is currently on this exact URL in an excluded
* container, the visit may have originated there — drop it. This closes the
* window where a background-tab visit's correlation was evicted/expired.
*/
private fun isHistoryExcluded(uri: String, resolution: ContextResolution): Boolean {
val excluded = GlobalComponents.excludedHistoryContextIds
if (excluded.isEmpty()) {
return false
}
if (resolution.authoritative) {
return resolution.contextId != null && resolution.contextId in excluded
}
val tabs = GlobalComponents.components?.core?.store?.state?.tabs ?: return false
return tabs.any { tab ->
!tab.content.private &&
tab.content.url == uri &&
tab.contextId != null &&
tab.contextId in excluded
}
}
/**
* The producing Gecko [contextId] for a visit, plus whether it was pinned
* [authoritative]ly (a definite producer) or is only a best-effort guess.
*/
private data class ContextResolution(
val contextId: String?,
val authoritative: Boolean,
)
/**
* Resolve the visited [uri] to the Gecko contextId that produced it.
*
* Resolution order, most specific first:
*
* 1. The per-navigation correlation recorded by
* [eu.weblibre.flutter_mozilla_components.middleware.HistoryVisitCorrelationMiddleware]
* for the tab that actually navigated to [uri]. This correctly attributes
* a **background-tab** visit and — crucially — stops a loading foreground
* tab from stealing another tab's same-URL visit. A live
* [HistoryVisitCorrelationCache.Resolution] (even with a null contextId,
* i.e. known-uncontained) is authoritative.
* 2. On a cache miss, the selected tab only when it is **actively loading
* exactly [uri]** (`loading && url == uri`). With no recorded competing
* navigation, it is the best live producer signal. Authoritative.
* 3. Otherwise, the loading selected tab as a last resort: redirect chains
* can fire `onVisited(uri)` before the selected tab's url settles on [uri]
* and before the middleware records it. Marked **not** authoritative — it
* may mis-attribute a background-tab visit, so the exclude decision does
* not trust it and re-checks via [isHistoryExcluded].
*
* A full miss returns a non-authoritative uncontained (null) result.
*/
private fun resolveContextId(uri: String): ContextResolution {
val selected = GlobalComponents.components?.core?.store?.state?.selectedTab
val correlation = HistoryVisitCorrelationCache.resolve(uri)
if (correlation != null) {
return ContextResolution(correlation.contextId, authoritative = true)
}
if (selected != null && !selected.content.private &&
selected.content.loading && selected.content.url == uri
) {
return ContextResolution(selected.contextId, authoritative = true)
}
if (selected != null && !selected.content.private &&
selected.content.loading
) {
return ContextResolution(selected.contextId, authoritative = false)
}
return ContextResolution(null, authoritative = false)
}
}
@@ -6,6 +6,7 @@
package eu.weblibre.flutter_mozilla_components.middleware
import eu.weblibre.flutter_mozilla_components.GlobalComponents
import mozilla.components.browser.state.action.BrowserAction
import mozilla.components.browser.state.action.ContentAction
import mozilla.components.browser.state.action.HistoryMetadataAction
@@ -102,13 +103,30 @@ class HistoryMetadataMiddleware(
store: Store<BrowserState, BrowserAction>,
tab: TabSessionState,
) {
// Hard exclude-from-history: a tab in an excluded ("incognito") container
// must not persist anywhere in Places. WebLibreHistoryDelegate already
// skips the visit write; metadata is a separate persistent Places path
// (highlights / suggestions), so it must be gated on the same set.
if (isHistoryExcluded(tab)) {
return
}
val key = historyMetadataService.createMetadata(tab)
store.dispatch(HistoryMetadataAction.SetHistoryMetadataKeyAction(tab.id, key))
}
private fun updateHistoryMetadata(tab: TabSessionState) {
if (isHistoryExcluded(tab)) {
return
}
tab.historyMetadata?.let {
historyMetadataService.updateMetadata(it, tab)
}
}
private fun isHistoryExcluded(tab: TabSessionState): Boolean {
val contextId = tab.contextId ?: return false
return contextId in GlobalComponents.excludedHistoryContextIds
}
}
@@ -0,0 +1,48 @@
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package eu.weblibre.flutter_mozilla_components.middleware
import eu.weblibre.flutter_mozilla_components.history.HistoryVisitCorrelationCache
import mozilla.components.browser.state.action.BrowserAction
import mozilla.components.browser.state.action.ContentAction
import mozilla.components.browser.state.selector.findCustomTab
import mozilla.components.browser.state.selector.findNormalTab
import mozilla.components.browser.state.state.BrowserState
import mozilla.components.lib.state.Middleware
import mozilla.components.lib.state.Store
/**
* Records a short-lived `url -> contextId` correlation whenever a normal tab or
* custom tab/PWA URL changes, so
* [eu.weblibre.flutter_mozilla_components.history.WebLibreHistoryDelegate] can
* resolve the tab's Gecko contextual identity (and hence its WebLibre container)
* at visit time — the delegate itself only receives the visited URL.
*
* See [HistoryVisitCorrelationCache].
*/
class HistoryVisitCorrelationMiddleware :
Middleware<BrowserState, BrowserAction> {
override fun invoke(
store: Store<BrowserState, BrowserAction>,
next: (BrowserAction) -> Unit,
action: BrowserAction,
) {
next(action)
if (action is ContentAction.UpdateUrlAction) {
val normalTab = store.state.findNormalTab(action.sessionId)
if (normalTab != null) {
HistoryVisitCorrelationCache.record(action.url, normalTab.contextId)
return
}
store.state.findCustomTab(action.sessionId)?.let { customTab ->
HistoryVisitCorrelationCache.record(action.url, customTab.contextId)
}
}
}
}
@@ -7305,6 +7305,13 @@ interface GeckoEngineSettingsApi {
* cold-started reader view resolves the right value before Flutter runs.
*/
fun setReaderViewPureBlack(enabled: Boolean)
/**
* The set of Gecko contextual-identity ids ("container" contextIds) whose
* browsing history must NOT be written to Mozilla Places (hard
* exclude-from-history / "incognito container"). WebLibreHistoryDelegate
* skips the Places write for a visit resolved to one of these containers.
*/
fun setExcludedHistoryContextIds(contextIds: List<String>)
companion object {
/** The codec used by GeckoEngineSettingsApi. */
@@ -7490,6 +7497,24 @@ interface GeckoEngineSettingsApi {
channel.setMessageHandler(null)
}
}
run {
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.flutter_mozilla_components.GeckoEngineSettingsApi.setExcludedHistoryContextIds$separatedMessageChannelSuffix", codec)
if (api != null) {
channel.setMessageHandler { message, reply ->
val args = message as List<Any?>
val contextIdsArg = args[0] as List<String>
val wrapped: List<Any?> = try {
api.setExcludedHistoryContextIds(contextIdsArg)
listOf(null)
} catch (exception: Throwable) {
GeckoPigeonUtils.wrapError(exception)
}
reply.reply(wrapped)
}
} else {
channel.setMessageHandler(null)
}
}
}
}
}
@@ -8955,7 +8980,7 @@ class GeckoStateEvents(private val binaryMessenger: BinaryMessenger, private val
}
} else {
callback(Result.failure(GeckoPigeonUtils.createConnectionError(channelName)))
}
}
}
}
fun onEngineReadyStateChange(sequenceArg: Long, stateArg: Boolean, callback: (Result<Unit>) -> Unit)
@@ -10314,6 +10339,46 @@ interface GeckoDeleteBrowsingDataController {
}
}
}
/**
* Native -> Dart history visit notifications. Fired from WebLibreHistoryDelegate
* on each recorded Mozilla Places visit so WebLibre can persist the one thing
* Places can't store: which container the visit belonged to. The visit itself
* (title, visit type, exact time) stays owned by Places.
*
* Generated class from Pigeon that represents Flutter messages that can be called from Kotlin.
*/
class GeckoHistoryEvents(private val binaryMessenger: BinaryMessenger, private val messageChannelSuffix: String = "") {
companion object {
/** The codec used by GeckoHistoryEvents. */
val codec: MessageCodec<Any?> by lazy {
GeckoPigeonCodec()
}
}
/**
* [contextId] is the Gecko contextual identity of the tab that produced the
* visit, resolved via the URLcontextId correlation cache (null when it
* couldn't be resolved / the tab was uncontained). Dart maps it to a
* WebLibre container and writes the visitcontainer relation, keyed on
* ([url], [visitTime]) to join back to the Places visit.
*/
fun onVisitRecorded(urlArg: String, visitTimeArg: Long, contextIdArg: String?, callback: (Result<Unit>) -> Unit)
{
val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else ""
val channelName = "dev.flutter.pigeon.flutter_mozilla_components.GeckoHistoryEvents.onVisitRecorded$separatedMessageChannelSuffix"
val channel = BasicMessageChannel<Any?>(binaryMessenger, channelName, codec)
channel.send(listOf(urlArg, visitTimeArg, contextIdArg)) {
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 interface from Pigeon that represents a handler of messages from Flutter. */
interface GeckoHistoryApi {
fun getDetailedVisits(startMillis: Long, endMillis: Long, excludeTypes: List<VisitType>, callback: (Result<List<VisitInfo>>) -> Unit)
@@ -72,6 +72,7 @@ export 'src/pigeons/gecko.g.dart'
GeckoDeleteBrowsingDataController,
GeckoEngineSettings,
GeckoFetchResponse,
GeckoHistoryEvents,
GeckoPref,
GeckoProxySettings,
GeckoPublicSuffixListApi,
@@ -236,4 +236,8 @@ class GeckoEngineSettingsService {
Future<void> setReaderViewPureBlack(bool enabled) {
return _api.setReaderViewPureBlack(enabled);
}
Future<void> setExcludedHistoryContextIds(List<String> contextIds) {
return _api.setExcludedHistoryContextIds(contextIds);
}
}
@@ -46,11 +46,14 @@ class GeckoHistoryService {
case VisitType.redirectTemporary:
case VisitType.framedLink:
case VisitType.reload:
// A bookmark-type visit is an ordinary Places visit (the user navigated
// via a bookmark); deleting it by (url, time) removes only that visit
// record and leaves the bookmark itself intact — same path as any page
// visit.
case VisitType.bookmark:
return _api.deleteVisit(info.url, info.visitTime);
case VisitType.download:
return _api.deleteDownload(info.contentId!);
case VisitType.bookmark:
throw UnimplementedError('VisitType.bookmark deletion not implemented');
}
}
@@ -7421,6 +7421,28 @@ class GeckoEngineSettingsApi {
)
;
}
/// The set of Gecko contextual-identity ids ("container" contextIds) whose
/// browsing history must NOT be written to Mozilla Places (hard
/// exclude-from-history / "incognito container"). WebLibreHistoryDelegate
/// skips the Places write for a visit resolved to one of these containers.
Future<void> setExcludedHistoryContextIds(List<String> contextIds) async {
final pigeonVar_channelName = 'dev.flutter.pigeon.flutter_mozilla_components.GeckoEngineSettingsApi.setExcludedHistoryContextIds$pigeonVar_messageChannelSuffix';
final pigeonVar_channel = BasicMessageChannel<Object?>(
pigeonVar_channelName,
pigeonChannelCodec,
binaryMessenger: pigeonVar_binaryMessenger,
);
final Future<Object?> pigeonVar_sendFuture = pigeonVar_channel.send(<Object?>[contextIds]);
final pigeonVar_replyList = await pigeonVar_sendFuture as List<Object?>?;
_extractReplyValueOrThrow(
pigeonVar_replyList,
pigeonVar_channelName,
isNullValid: true,
)
;
}
}
class GeckoSessionApi {
@@ -10310,6 +10332,48 @@ class GeckoDeleteBrowsingDataController {
}
}
/// Native -> Dart history visit notifications. Fired from WebLibreHistoryDelegate
/// on each recorded Mozilla Places visit so WebLibre can persist the one thing
/// Places can't store: which container the visit belonged to. The visit itself
/// (title, visit type, exact time) stays owned by Places.
abstract class GeckoHistoryEvents {
static const MessageCodec<Object?> pigeonChannelCodec = _PigeonCodec();
/// [contextId] is the Gecko contextual identity of the tab that produced the
/// visit, resolved via the URL→contextId correlation cache (null when it
/// couldn't be resolved / the tab was uncontained). Dart maps it to a
/// WebLibre container and writes the visit→container relation, keyed on
/// ([url], [visitTime]) to join back to the Places visit.
void onVisitRecorded(String url, int visitTime, String? contextId);
static void setUp(GeckoHistoryEvents? api, {BinaryMessenger? binaryMessenger, String messageChannelSuffix = '',}) {
messageChannelSuffix = messageChannelSuffix.isNotEmpty ? '.$messageChannelSuffix' : '';
{
final pigeonVar_channel = BasicMessageChannel<Object?>(
'dev.flutter.pigeon.flutter_mozilla_components.GeckoHistoryEvents.onVisitRecorded$messageChannelSuffix', pigeonChannelCodec,
binaryMessenger: binaryMessenger);
if (api == null) {
pigeonVar_channel.setMessageHandler(null);
} else {
pigeonVar_channel.setMessageHandler((Object? message) async {
final List<Object?> args = message! as List<Object?>;
final String arg_url = args[0]! as String;
final int arg_visitTime = args[1]! as int;
final String? arg_contextId = args[2] as String?;
try {
api.onVisitRecorded(arg_url, arg_visitTime, arg_contextId);
return wrapResponse(empty: true);
} on PlatformException catch (e) {
return wrapResponse(error: e);
} catch (e) {
return wrapResponse(error: PlatformException(code: 'error', message: e.toString()));
}
});
}
}
}
}
class GeckoHistoryApi {
/// Constructor for [GeckoHistoryApi]. The [binaryMessenger] named argument is
/// available for dependency injection. If it is left null, the default
@@ -1586,6 +1586,12 @@ abstract class GeckoEngineSettingsApi {
/// Mozilla's reader view extension. Persisted in SharedPreferences so a
/// cold-started reader view resolves the right value before Flutter runs.
void setReaderViewPureBlack(bool enabled);
/// The set of Gecko contextual-identity ids ("container" contextIds) whose
/// browsing history must NOT be written to Mozilla Places (hard
/// exclude-from-history / "incognito container"). WebLibreHistoryDelegate
/// skips the Places write for a visit resolved to one of these containers.
void setExcludedHistoryContextIds(List<String> contextIds);
}
@HostApi()
@@ -2230,6 +2236,20 @@ enum ClearDataType {
onlyCaches,
}
/// Native -> Dart history visit notifications. Fired from WebLibreHistoryDelegate
/// on each recorded Mozilla Places visit so WebLibre can persist the one thing
/// Places can't store: which container the visit belonged to. The visit itself
/// (title, visit type, exact time) stays owned by Places.
@FlutterApi()
abstract class GeckoHistoryEvents {
/// [contextId] is the Gecko contextual identity of the tab that produced the
/// visit, resolved via the URL→contextId correlation cache (null when it
/// couldn't be resolved / the tab was uncontained). Dart maps it to a
/// WebLibre container and writes the visit→container relation, keyed on
/// ([url], [visitTime]) to join back to the Places visit.
void onVisitRecorded(String url, int visitTime, String? contextId);
}
@HostApi()
abstract class GeckoHistoryApi {
@async