switch from timestamps to event sequence for events

This commit is contained in:
Fabian Freund
2026-02-10 16:20:35 +01:00
parent e0cce93ffb
commit 592c6afdc5
30 changed files with 840 additions and 254 deletions
@@ -11,6 +11,7 @@ import android.app.Activity
import android.content.Context
import android.view.ViewGroup
import android.widget.FrameLayout
import eu.weblibre.flutter_mozilla_components.ext.EventSequence
import eu.weblibre.flutter_mozilla_components.pigeons.GeckoStateEvents
import io.flutter.plugin.common.StandardMessageCodec
import io.flutter.plugin.platform.PlatformView
@@ -58,7 +59,7 @@ private class NativeFragmentView(
super.onFlutterViewAttached(flutterView)
components.engineReportedInitialized = false
flutterEvents.onViewReadyStateChange(System.currentTimeMillis(), true) { _ -> }
flutterEvents.onViewReadyStateChange(EventSequence.next(), true) { _ -> }
}
override fun getView(): View {
@@ -1,5 +1,6 @@
package eu.weblibre.flutter_mozilla_components.api
import eu.weblibre.flutter_mozilla_components.ext.EventSequence
import eu.weblibre.flutter_mozilla_components.feature.MLEngineFeature
import eu.weblibre.flutter_mozilla_components.feature.ResultConsumer
import eu.weblibre.flutter_mozilla_components.pigeons.GeckoMlApi
@@ -68,7 +69,7 @@ class GeckoMlApiImpl(
)
runOnUiThread {
stateEvents.onMlProgress(System.currentTimeMillis(), mlProgress) { result ->
stateEvents.onMlProgress(EventSequence.next(), mlProgress) { result ->
result.onFailure { error ->
android.util.Log.e("GeckoMlApi", "Failed to emit progress event: ${error.message}")
}
@@ -7,6 +7,7 @@
package eu.weblibre.flutter_mozilla_components.api
import eu.weblibre.flutter_mozilla_components.GlobalComponents
import eu.weblibre.flutter_mozilla_components.ext.EventSequence
import eu.weblibre.flutter_mozilla_components.feature.PrefManagerFeature
import eu.weblibre.flutter_mozilla_components.feature.ResultConsumer
import eu.weblibre.flutter_mozilla_components.pigeons.GeckoPref
@@ -201,7 +202,7 @@ class GeckoPrefApiImpl : GeckoPrefApi, BrowserPrefObserverDelegate {
override fun onPreferenceChange(observedPreference: BrowserPreference<*>) {
components.flutterEvents.onPreferenceChange(
System.currentTimeMillis(), GeckoPref(
EventSequence.next(), GeckoPref(
name = observedPreference.pref,
value = observedPreference.value,
defaultValue = observedPreference.defaultValue,
@@ -7,6 +7,7 @@
package eu.weblibre.flutter_mozilla_components.api
import eu.weblibre.flutter_mozilla_components.GlobalComponents
import eu.weblibre.flutter_mozilla_components.ext.EventSequence
import eu.weblibre.flutter_mozilla_components.api.GeckoDeleteBrowsingDataControllerImpl.Companion
import eu.weblibre.flutter_mozilla_components.ext.toWebPBytes
import eu.weblibre.flutter_mozilla_components.pigeons.AutocompleteResult
@@ -77,7 +78,7 @@ class GeckoSuggestionApiImpl(
runOnUiThread {
suggestionEvents.onSuggestionResult(
System.currentTimeMillis(),
EventSequence.next(),
provider,
mappedResults
) { }
@@ -17,6 +17,7 @@ import eu.weblibre.flutter_mozilla_components.pigeons.SourceValue
import eu.weblibre.flutter_mozilla_components.pigeons.WebExtensionActionType
import eu.weblibre.flutter_mozilla_components.pigeons.WebExtensionData
import eu.weblibre.flutter_mozilla_components.GlobalComponents
import eu.weblibre.flutter_mozilla_components.ext.EventSequence
import eu.weblibre.flutter_mozilla_components.ext.toWebPBytes
import eu.weblibre.flutter_mozilla_components.pigeons.FindResultState
import eu.weblibre.flutter_mozilla_components.pigeons.GeckoStateEvents
@@ -135,7 +136,7 @@ class GeckoTabsApiImpl : GeckoTabsApi {
withContext(Dispatchers.Main) {
components.flutterEvents.onIconChange(
System.currentTimeMillis(),
EventSequence.next(),
tab.id,
iconBytes
) { }
@@ -159,7 +160,7 @@ class GeckoTabsApiImpl : GeckoTabsApi {
val bytes = it.toWebPBytes()
withContext(Dispatchers.Main) {
components.flutterEvents.onThumbnailChange(
System.currentTimeMillis(),
EventSequence.next(),
tab.id,
bytes
) { }
@@ -191,21 +192,21 @@ class GeckoTabsApiImpl : GeckoTabsApi {
if (onSelectedTabChange) {
components.flutterEvents.onSelectedTabChange(
System.currentTimeMillis(),
EventSequence.next(),
selectedTab
) { }
}
if (onTabListChange) {
components.flutterEvents.onTabListChange(
System.currentTimeMillis(),
EventSequence.next(),
tabs.map { it.id }) { }
}
tabs.forEach { tab ->
if (onTabContentStateChange) {
components.flutterEvents.onTabContentStateChange(
System.currentTimeMillis(),
EventSequence.next(),
TabContentState(
id = tab.id,
parentId = tab.parentId,
@@ -226,7 +227,7 @@ class GeckoTabsApiImpl : GeckoTabsApi {
if (onSecurityInfoStateChange) {
components.flutterEvents.onSecurityInfoStateChange(
System.currentTimeMillis(),
EventSequence.next(),
tab.id,
SecurityInfoState(
tab.content.securityInfo.isSecure,
@@ -238,7 +239,7 @@ class GeckoTabsApiImpl : GeckoTabsApi {
if (onReaderableStateChange) {
components.flutterEvents.onReaderableStateChange(
System.currentTimeMillis(),
EventSequence.next(),
tab.id,
ReaderableState(
tab.readerState.readerable,
@@ -249,7 +250,7 @@ class GeckoTabsApiImpl : GeckoTabsApi {
if (onHistoryStateChange) {
components.flutterEvents.onHistoryStateChange(
System.currentTimeMillis(),
EventSequence.next(),
tab.id,
HistoryState(
items = tab.content.history.items.map { item ->
@@ -264,7 +265,7 @@ class GeckoTabsApiImpl : GeckoTabsApi {
if (onFindResults) {
components.flutterEvents.onFindResults(
System.currentTimeMillis(),
EventSequence.next(),
tab.id,
tab.content.findResults.map { result ->
FindResultState(
@@ -321,7 +322,7 @@ class GeckoTabsApiImpl : GeckoTabsApi {
badgeBackgroundColor = browserAction.badgeBackgroundColor?.toLong(),
)
components.addonEvents.onUpsertWebExtensionAction(
System.currentTimeMillis(),
EventSequence.next(),
extension.id,
WebExtensionActionType.BROWSER,
data
@@ -336,7 +337,7 @@ class GeckoTabsApiImpl : GeckoTabsApi {
val imageBytes = icon.toWebPBytes()
withContext(Dispatchers.Main) {
components.addonEvents.onUpdateWebExtensionIcon(
System.currentTimeMillis(),
EventSequence.next(),
extension.id,
WebExtensionActionType.BROWSER,
imageBytes
@@ -362,7 +363,7 @@ class GeckoTabsApiImpl : GeckoTabsApi {
badgeBackgroundColor = pageAction.badgeBackgroundColor?.toLong(),
)
components.addonEvents.onUpsertWebExtensionAction(
System.currentTimeMillis(),
EventSequence.next(),
extension.id,
WebExtensionActionType.PAGE,
data
@@ -377,7 +378,7 @@ class GeckoTabsApiImpl : GeckoTabsApi {
val imageBytes = icon.toWebPBytes()
withContext(Dispatchers.Main) {
components.addonEvents.onUpdateWebExtensionIcon(
System.currentTimeMillis(),
EventSequence.next(),
extension.id,
WebExtensionActionType.PAGE,
imageBytes
@@ -6,7 +6,9 @@
package eu.weblibre.flutter_mozilla_components.components
import eu.weblibre.flutter_mozilla_components.GlobalComponents
import eu.weblibre.flutter_mozilla_components.api.ReaderViewEventsImpl
import eu.weblibre.flutter_mozilla_components.ext.EventSequence
import eu.weblibre.flutter_mozilla_components.ext.toWebPBytes
import eu.weblibre.flutter_mozilla_components.pigeons.FindResultState
import eu.weblibre.flutter_mozilla_components.pigeons.GeckoAddonEvents
@@ -44,7 +46,7 @@ class Events(
.debounce { 50 }
.collect { tabId ->
flutterEvents.onSelectedTabChange(
System.currentTimeMillis(),
EventSequence.next(),
tabId
) { _ -> }
}
@@ -70,7 +72,7 @@ class Events(
.collect { tab ->
val iconBytes = tab.content.icon?.toWebPBytes()
flutterEvents.onIconChange(
System.currentTimeMillis(),
EventSequence.next(),
tab.id,
iconBytes
) { _ -> }
@@ -85,7 +87,7 @@ class Events(
.debounce(15)
.collect { tab ->
flutterEvents.onSecurityInfoStateChange(
System.currentTimeMillis(),
EventSequence.next(),
tab.id,
SecurityInfoState(
tab.content.securityInfo.isSecure,
@@ -110,7 +112,7 @@ class Events(
.debounce(25)
.collect { tab ->
flutterEvents.onReaderableStateChange(
System.currentTimeMillis(),
EventSequence.next(),
tab.id,
ReaderableState(
tab.readerState.readerable,
@@ -135,7 +137,7 @@ class Events(
.debounce(15)
.collect { tab ->
flutterEvents.onHistoryStateChange(
System.currentTimeMillis(),
EventSequence.next(),
tab.id,
HistoryState(
items = tab.content.history.items.map { item ->
@@ -170,7 +172,7 @@ class Events(
.debounce(15)
.collect { tab ->
flutterEvents.onTabContentStateChange(
System.currentTimeMillis(),
EventSequence.next(),
TabContentState(
id = tab.id,
parentId = tab.parentId,
@@ -0,0 +1,22 @@
/*
* 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.ext
import java.util.concurrent.atomic.AtomicLong
/**
* Thread-safe monotonic sequence counter for event ordering.
*
* Replaces System.currentTimeMillis() for event timestamps sent to Flutter,
* guaranteeing strictly increasing values regardless of wall-clock changes
* and eliminating same-millisecond collisions.
*/
object EventSequence {
private val counter = AtomicLong(0)
fun next(): Long = counter.incrementAndGet()
}
@@ -7,6 +7,7 @@
package eu.weblibre.flutter_mozilla_components.feature
import androidx.annotation.VisibleForTesting
import eu.weblibre.flutter_mozilla_components.ext.EventSequence
import eu.weblibre.flutter_mozilla_components.pigeons.BrowserExtensionEvents
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.runBlocking
@@ -86,7 +87,7 @@ object BrowserExtensionFeature {
runOnUiThread {
extensionEvents.onFeedRequested(
System.currentTimeMillis(),
EventSequence.next(),
url
) { _ -> }
}
@@ -8,6 +8,7 @@ package eu.weblibre.flutter_mozilla_components.feature
import androidx.annotation.VisibleForTesting
import eu.weblibre.flutter_mozilla_components.GlobalComponents
import eu.weblibre.flutter_mozilla_components.ext.EventSequence
import eu.weblibre.flutter_mozilla_components.pigeons.ContainerSiteAssignment
import eu.weblibre.flutter_mozilla_components.pigeons.GeckoStateEvents
import kotlinx.coroutines.Dispatchers
@@ -115,7 +116,7 @@ object ContainerProxyFeature {
if (status == "success") {
runOnUiThread {
events.onContainerSiteAssignment(
System.currentTimeMillis(),
EventSequence.next(),
ContainerSiteAssignment(
requestId = requestId,
tabId = components.core.store.state.selectedTabId,
@@ -14,6 +14,7 @@ import androidx.core.graphics.Insets
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsAnimationCompat
import androidx.core.view.WindowInsetsCompat
import eu.weblibre.flutter_mozilla_components.ext.EventSequence
import eu.weblibre.flutter_mozilla_components.pigeons.GeckoViewportEvents
import mozilla.components.support.base.log.logger.Logger
import org.mozilla.gecko.util.ThreadUtils.runOnUiThread
@@ -185,7 +186,7 @@ class KeyboardVisibilityFeature(
lastKeyboardHeight = heightPx
lastKeyboardVisible = isVisible
val timestamp = System.currentTimeMillis()
val timestamp = EventSequence.next()
logger.debug("$TAG: Keyboard change - height=$heightPx, visible=$isVisible, animating=$isAnimating")
@@ -6,6 +6,7 @@
package eu.weblibre.flutter_mozilla_components.feature
import eu.weblibre.flutter_mozilla_components.ext.EventSequence
import eu.weblibre.flutter_mozilla_components.pigeons.GeckoTabContentEvents
import eu.weblibre.flutter_mozilla_components.pigeons.TabContent
import kotlinx.coroutines.CoroutineScope
@@ -50,7 +51,7 @@ class ReadabilityExtractFeature(
override fun onPortMessage(message: Any, port: Port) {
if (message is JSONObject) {
tabContentEvents.onContentUpdate(
System.currentTimeMillis(),
EventSequence.next(),
contentArg = TabContent(
tabId = tabId,
fullContentMarkdown = message.tryGetString("fullContentMarkdown"),
@@ -7,6 +7,7 @@ package eu.weblibre.flutter_mozilla_components.feature
import android.os.Handler
import android.os.HandlerThread
import androidx.annotation.VisibleForTesting
import eu.weblibre.flutter_mozilla_components.ext.EventSequence
import eu.weblibre.flutter_mozilla_components.ext.toWebPBytes
import eu.weblibre.flutter_mozilla_components.pigeons.GeckoAddonEvents
import eu.weblibre.flutter_mozilla_components.pigeons.WebExtensionActionType
@@ -66,7 +67,7 @@ class WebExtensionToolbarFeature(
.filter { !store.state.extensions.containsKey(it) || store.state.extensions[it]?.enabled == false }
.forEach { extensionId ->
addonEvents.onRemoveWebExtensionAction(
timestampArg = System.currentTimeMillis(),
timestampArg = EventSequence.next(),
extensionIdArg = extensionId,
actionTypeArg = WebExtensionActionType.BROWSER,
) {}
@@ -77,7 +78,7 @@ class WebExtensionToolbarFeature(
.filter { !store.state.extensions.containsKey(it) || store.state.extensions[it]?.enabled == false }
.forEach { extensionId ->
addonEvents.onRemoveWebExtensionAction(
timestampArg = System.currentTimeMillis(),
timestampArg = EventSequence.next(),
extensionIdArg = extensionId,
actionTypeArg = WebExtensionActionType.PAGE,
) {}
@@ -106,7 +107,7 @@ class WebExtensionToolbarFeature(
webExtensionBrowserActions.keys.toList().forEach { extensionId ->
if (extensionId !in enabledExtensionIds) {
addonEvents.onRemoveWebExtensionAction(
timestampArg = System.currentTimeMillis(),
timestampArg = EventSequence.next(),
extensionIdArg = extensionId,
actionTypeArg = WebExtensionActionType.BROWSER,
) {}
@@ -117,7 +118,7 @@ class WebExtensionToolbarFeature(
webExtensionPageActions.keys.toList().forEach { extensionId ->
if (extensionId !in enabledExtensionIds) {
addonEvents.onRemoveWebExtensionAction(
timestampArg = System.currentTimeMillis(),
timestampArg = EventSequence.next(),
extensionIdArg = extensionId,
actionTypeArg = WebExtensionActionType.PAGE,
) {}
@@ -130,7 +131,7 @@ class WebExtensionToolbarFeature(
if (extensionNotAllowedInTab(extension, tab)) {
webExtensionPageActions[extension.id]?.let {
addonEvents.onRemoveWebExtensionAction(
timestampArg = System.currentTimeMillis(),
timestampArg = EventSequence.next(),
extensionIdArg = extension.id,
actionTypeArg = WebExtensionActionType.PAGE,
) {}
@@ -138,7 +139,7 @@ class WebExtensionToolbarFeature(
}
webExtensionBrowserActions[extension.id]?.let {
addonEvents.onRemoveWebExtensionAction(
timestampArg = System.currentTimeMillis(),
timestampArg = EventSequence.next(),
extensionIdArg = extension.id,
actionTypeArg = WebExtensionActionType.BROWSER,
) {}
@@ -220,7 +221,7 @@ class WebExtensionToolbarFeature(
)
addonEvents.onUpsertWebExtensionAction(
timestampArg = System.currentTimeMillis(),
timestampArg = EventSequence.next(),
extensionIdArg = extension.id,
actionTypeArg = if (isPageAction) WebExtensionActionType.PAGE else WebExtensionActionType.BROWSER,
extensionDataArg = data
@@ -235,7 +236,7 @@ class WebExtensionToolbarFeature(
val imageBytes = icon.toWebPBytes()
runOnUiThread {
addonEvents.onUpdateWebExtensionIcon(
timestampArg = System.currentTimeMillis(),
timestampArg = EventSequence.next(),
extensionIdArg = extensionId,
actionTypeArg = if (isPageAction) WebExtensionActionType.PAGE else WebExtensionActionType.BROWSER,
iconArg = imageBytes
@@ -10,6 +10,7 @@ import android.content.Context
import android.graphics.drawable.Drawable
import androidx.core.content.ContextCompat
import eu.weblibre.flutter_mozilla_components.GlobalComponents
import eu.weblibre.flutter_mozilla_components.ext.EventSequence
import eu.weblibre.flutter_mozilla_components.api.ReaderViewEventsImpl
import eu.weblibre.flutter_mozilla_components.api.ReaderViewControllerListener
import eu.weblibre.flutter_mozilla_components.pigeons.ReaderViewController
@@ -36,12 +37,12 @@ class ReaderViewIntegration(
if (enabled) {
feature.showReaderView()
readerViewController.appearanceButtonVisibility(System.currentTimeMillis(),true) { _ -> };
readerViewController.appearanceButtonVisibility(EventSequence.next(),true) { _ -> };
} else {
feature.hideReaderView()
feature.hideControls()
readerViewController.appearanceButtonVisibility(System.currentTimeMillis(),false) { _ -> };
readerViewController.appearanceButtonVisibility(EventSequence.next(),false) { _ -> };
}
}
@@ -6,6 +6,7 @@ package eu.weblibre.flutter_mozilla_components.middleware
import android.graphics.Bitmap
import android.util.Log
import eu.weblibre.flutter_mozilla_components.GlobalComponents
import eu.weblibre.flutter_mozilla_components.ext.EventSequence
import eu.weblibre.flutter_mozilla_components.ext.resize
import eu.weblibre.flutter_mozilla_components.ext.toWebPBytes
import eu.weblibre.flutter_mozilla_components.pigeons.AudioHitResult
@@ -55,7 +56,7 @@ class FlutterEventMiddleware(private val flutterEvents: GeckoStateEvents) : Midd
val bytes = resized.toWebPBytes()
runOnUiThread {
flutterEvents.onThumbnailChange(System.currentTimeMillis(), action.sessionId, bytes) { _ -> }
flutterEvents.onThumbnailChange(EventSequence.next(), action.sessionId, bytes) { _ -> }
}
}
//UpdateReaderConnectRequiredAction seems to be the only event that is called predictable
@@ -64,7 +65,7 @@ class FlutterEventMiddleware(private val flutterEvents: GeckoStateEvents) : Midd
if(!components.engineReportedInitialized) {
runOnUiThread {
flutterEvents.onEngineReadyStateChange(
System.currentTimeMillis(),
EventSequence.next(),
true
) { _ -> }
}
@@ -75,7 +76,7 @@ class FlutterEventMiddleware(private val flutterEvents: GeckoStateEvents) : Midd
is TabListAction.AddTabAction -> {
runOnUiThread {
flutterEvents.onTabAdded(
System.currentTimeMillis(),
EventSequence.next(),
action.tab.id
) { _ -> }
}
@@ -85,7 +86,7 @@ class FlutterEventMiddleware(private val flutterEvents: GeckoStateEvents) : Midd
runOnUiThread {
flutterEvents.onIconUpdate(
System.currentTimeMillis(),
EventSequence.next(),
action.pageUrl,
bytes
) { _ -> }
@@ -94,7 +95,7 @@ class FlutterEventMiddleware(private val flutterEvents: GeckoStateEvents) : Midd
is ContentAction.UpdateHitResultAction -> {
runOnUiThread {
flutterEvents.onLongPress(
System.currentTimeMillis(),
EventSequence.next(),
action.sessionId,
when(val result = action.hitResult) {
is HitResult.AUDIO -> AudioHitResult(result.src, result.title)
@@ -112,7 +113,7 @@ class FlutterEventMiddleware(private val flutterEvents: GeckoStateEvents) : Midd
is ContentAction.AddFindResultAction -> {
runOnUiThread {
flutterEvents.onFindResults(
System.currentTimeMillis(),
EventSequence.next(),
action.sessionId,
listOf(
FindResultState(
@@ -127,7 +128,7 @@ class FlutterEventMiddleware(private val flutterEvents: GeckoStateEvents) : Midd
is ContentAction.ClearFindResultsAction -> {
runOnUiThread {
flutterEvents.onFindResults(
System.currentTimeMillis(),
EventSequence.next(),
action.sessionId,
listOf()
) { _ -> }
@@ -136,7 +137,7 @@ class FlutterEventMiddleware(private val flutterEvents: GeckoStateEvents) : Midd
// is ReaderAction.UpdateReaderScrollYAction -> {
// runOnUiThread {
// flutterEvents.onScrollChange(
// System.currentTimeMillis(),
// EventSequence.next(),
// action.tabId,
// action.scrollY.toLong()
// ) { _ -> }