browser-session
component to the browser-state
component for state handling. Finally, we were able to delete browser-session
and all state is now maintained and updated by the redux-like BrowserStore
.
The following blog posting describes some of the possible follow-up changes to the architecture that we would consider, depending on the outcome of further discussions and prototyping.
In the current architecture every browser-engine
implements concept-engine
and exposes an abstracted mechanism for observing events. For every EngineSession
an EngineObserver
gets created, which will dispatch a BrowserAction
for every event, updating the centralized state.
Now that the migration to browser-state
is completed, we can reverse this dependency. With a browser-engine
implementation depending on browser-state
directly, it can dispatch actions without an observer in between.
This removes the requirement for a shared, abstracted observer interface in concept-engine
. It would no longer be required to have shared “glue code” for connecting an abstract browser engine with the state handling component.
A potential downside to this approach is that the BrowserStore
and related BrowserAction
s become the new “interface” that a browser engine has to dispatch correctly, which can be harder to understand and follow than simply implementing actual interfaces.
Overall this seems to be worthwhile exploring and potentially discussing further in an RFC.
Android’s new UI toolkit, Jetpack Compose, will significantly change how we build user-facing features in components and apps.
The good news is that Jetpack Compose works very nicely with our browser-state component. In Android Components we will provide bindings that allow subscribing to any lib-state
baked Store
, causing a recomposition if the observed state changes.
@Composable
fun SimpleToolbar(
store: BrowserStore
) {
// Subscribe to the URL of the selected tab
val url = store.observeAsState { state -> state.selectedTab?.content?.url }
// Will automatically get recomposed if the URL changes
Text(url.value ?: "")
}
Today we have many components that optionally take a nullable tabId: String?
parameter. If a tab ID is provided then the component is supposed to observe this specific tab. And if the parameter is null
then the component will automatically track the currently selected tab. This has caused issues in the past when a null
value was provided accidentally, causing the wrong tab to be tracked. With Jetpack Compose we want to make this more explicit and will provide a Target
class, that lets the caller explicily define what tab should be targeted. In addition to that this allows us to provide extension functions for easily observing this tab.
@Composable
fun Example(store: BrowserStore) {
// Explicitly observe specific tabs
SimpleToolbar(store, Target.SelectedTab)
SimpleToolbar(store, Target.Tab("tabId"))
SimpleToolbar(store, Target.CustomTab("customTabId"))
}
@Composable
fun SimpleToolbar(
store: BrowserStore,
target: Target
) {
// Observe the URL of the target. Only when the URL changes, this will
// cause a recomposition. Other changes of the tab get ignored.
val tab: SessionState? by target.observeAsStateFrom(
store = store,
observe = { tab -> tab?.content?.url }
)
Text(tab?.content?.url ?: "")
}
A central piece of our current component architecture is splitting the implementation into three pieces: A concept component, an implementation, and a glue/feature component for integration. This allows us to easily swap (concept) implementations, without having to change any other code. The downside of this approach is that all implementations need to abide by the interface abstractions.
Let’s look at the toolbar component as an example.
concept-toolbar
contains the interface and data classes to describe a toolbar and how other components can interact with it.browser-toolbar
is an implementation of concept-toolbar.feature-toolbar
contains the glue code, subscribing to state updates in order to update a toolbar (presenter), and reacting to toolbar events in order to invoke use cases (interactor).Using the two concepts above, reversing the dependency and using Jetpack Compose, we can simplify this architecture and reduce it to a single component. A (UI) component written in Jetpack Compose can directly observe BrowserStore
for state updates and delegate events to a function callback parameter or UseCase
class directly.
Our browser applications maintain state adhering to three different scopes: browser state, screen state and app state.
“Browser State” is the state the browser is in (e.g. “which tabs are open?”) and the state that is shared with our Android Components. It’s available through the browser-state
component to other components and the application.
With the bindings mentioned above, browser-state
works well with Jetpack Compose.
“Screen State” is the state for the currently displayed screen (e.g. “what text is the user entering on the search screen?”). In Firefox for Android, we are using lib-state
backed stores for each screen (e.g. SearchFragmentStore
).
As for browser-state
above, with the Jetpack Compose bindings for every lib-state
implementation, we can continue to use our existing screen-scoped stores.
Alternatives, used by the Android community, are ViewModel
s using LiveData
or StateFlow
. Or, at a lower level, Jetpack Compose idioms like rememberSaveable()
.
Currently we do not offer bindings in lib-state
for saving and restoring state to survive activity or process recreation. This is something we could add (based on Saver
), specifically for scoped stores.
val store = scopedSaveableStore<BrowserScreenState, BrowserScreenAction> { restoredState ->
BrowserScreenStore(restoredState ?: BrowserScreenState())
}
“App state” is the state the app is in, independently from the currently displayed screen (e.g. “Is the app in light or dark mode?”).
Currently in Fenix there’s no centralized app state. For some parts there are manager singletons (e.g. ThemeManager
) or the state is read from SharedPreferences
.
We could try using an global app store and the same patterns we use for the browser store and the screen scoped stores. That’s something we are currently trying in Focus (AppStore). In fact, in Focus, the screen scoped state is a sub state of the application-wide state (Screen state).
With Jetpack Compose it is preferred to write stateless composables (State hoisting). This means that state is passed down as function parameters and events are passed up by invoking functions. Listening to the store and dispatching actions sidesteps this mechanism. Only subscribing to state changes at the top layer and passing everything down/up is cumbersome and may introduce a lot of duplicated “glue” code across our projects.
Let’s look at a simplified example of a browser toolbar. There are two options:
Example A: All state (e.g. the URL to display) gets passed down to the toolbar:
@Composable
fun Toolbar(
url: String
) {
Text(url)
}
Example B: The toolbar subscribes to state it needs itself.
@Composable
fun Toolbar(
store: BrowserStore
) {
val url = store.observeAsState { state -> state.selectedTab?.content?.url }
Text(url.value ?: "")
}
The code from example A is the most reusable. The app is in full control of what state gets displayed. But this also introduces duplicate code across apps (for getting the state and passing it down) and makes it more likely to introduce bugs (security and spoofing). Example B is guaranteed to be consistent across apps. But the composable is strictly tied to the state and how it gets observed.
When writing (UI) components using Jetpack Compose, we will have to find the right balance between the two patterns.
In the best case the composition of composables make both patterns possible, depending on the needs of the component consumer:
@Composable
fun Toolbar(
store: BrowserStore
) {
val url = store.observeAsState { state -> state.selectedTab?.content?.url }
Toolbar(url.value ?: "")
}
@Composable
fun Toolbar(
url: String
) {
Text(url)
}
browser-engine-gecko-nightly
. Manually maintaining a feature flag that gradually gets enabled in build variants as the required functionality becomes available in more stable engine versions (browser-engine-gecko-beta
, browser-engine-gecko
) is cumbersome, error-prone and potentially a multi-week long process.
To help build feature flags, that need to incorporate the engine version, every Engine
exposes a version
property. This property is an instance of EngineVersion
, which makes it easy to match against specific engine versions.
Let’s say you are adding a new feature to Firefox Preview. This new feature requires brand new functionality that was just introduced in GeckoView Nightly 77.0. Using isAtLeast()
you can create a feature flag that will enable this feature in all build variants that are using GeckoView 77.0 or higher.
// Enable feature with GeckoView 77+
val useNewFeature = components.engine.version.isAtLeast(77)
This also works for minor and patch versions as well as additional metadata that is appended to the version number.
// Feature requires GeckoView 77.2 or higher.
val useNewFeature = components.engine.version.isAtLeast(77, 2)
If needed you can access the individual parts of the version number manually:
// For GeckoView (Nightly) 77.0a1
engine.version.major // 77
engine.version.minor // 0
engine.version.patch // 0
engine.version.metadata // a1
Note that for GeckoView versions we are using the MOZILLA_VERSION
that GeckoView exposes (e.g. 78.0a1
) which can be different from version of the maven dependency (e.g. 78.0.20200528032513
).
In browser-engine-system
, which is using WebView
, we are parsing the Chrome version from the User-Agent.
// Mozilla/5.0 (Linux; Android 10) Build/RPP2.200227.014.A1; wv)
// AppleWebKit/537.36 (KHTML, like Gecko) Version 4.0
// Chrome/82.0.4062.3 Mobile Safari/537.36
// On a device with a WebView with the User-Agent above:
engine.version.major // 82
engine.version.minor // 0
engine.version.patch // 4062
engine.version.metadata // .3
]]>browser-state
to eventually replace browser-session
. Now we are ready to start migrating components from browser-session
to browser-state
. This blog posting explains why we want to decommission browser-session
, describes how browser-state
works and what our migration plans are.
browser-session
?For maintaining the global browser state (e.g. “What tabs are open? What URLs are they pointing to?”) the Android Components project provides the browser-session
component. The initial implementation of browser-session
was a clean, generic re-implementation of what we had developed (more organically) for Firefox Focus.
In 2018 we noticed some flaws in the design of browser-session
. Those flaws came down to being able to observe the state while being able to modify it at the same time (“mutable state”). This unintended behavior could lead to “event order issues” and observers not really seeing a particular state change. Luckily back then we hadn’t seen those issues causing any problems in our apps yet.
We looked at multiple ways to prevent those side effects but that turned out to be almost impossible as long as the state is mutable. After more brainstorming, researching and prototyping we came up with a new design for a completely new component called browser-state
to eventually replace browser-session
.
In 2019 we completed and tweaked the design of the new browser-state
component until we felt that it was ready to be used in other components.
browser-state
Concepts used in the browser-state
component are similar to Redux - a state container library for JavaScript. The Redux documentation is a great way to get familiar with some of the concepts:
The global state of the browser is represented by an instance of an immutable data class: BrowserState
(API). Since it is immutable, an instance of this data class can be observed and processed without any side effects changing it. A state change is represented by the creation of a new BrowserState
instance.
The BrowserStore
(API) is the single source of truth. It holds the current BrowserState
instance and components, as well as app code, can observe it in order to always receive the latest BrowserState
. The only way to change the state is by dispatching a BrowserAction
(API) on the store. A dispatched BrowserAction
will be processed internally and a new BrowserState
object will be emitted by the store.
browser-state
?The browser-session
component is at the heart of many components and most apps using our components. It is obvious that we cannot migrate all components and apps from browser-session
to browser-state
from one Android Components release to the next one. Therefore the Android Components team made it possible to use browser-state
and browser-session
simultaneously and keep the state in both components synchronized.
val store = BrowserStore()
// Passing the BrowserStore instance to SessionManager makes sure that both
// components will be kept in sync.
val sessionManager = SessionManager(engine, store)
With the ability to use both components simultaneously, the Android Components team will start migrating components over from browser-session
to browser-state
. As part of this work the Android Components team will extend and add to BrowserState
to eventually reach feature parity with the state in SessionManager
and Session
. The only thing that may be different for app teams is that some components may require a BrowserStore
instance instead of a SessionManager
instance after migration.
// Before the migration
feature = ToolbarFeature(
layout.toolbar,
components.sessionManager,
components.sessionUseCases.loadUrl,
components.defaultSearchUseCase)
// After the migration
feature = ToolbarFeature(
layout.toolbar,
components.store,
components.sessionUseCases.loadUrl,
components.defaultSearchUseCase)
Once the migration of components is largely done, the Android Components team will start to help the app teams to plan migrating app code from browser-session
to browser-state
.
feature-session-bundling
, ui-doorhanger
and ui-progress
. Primary classes of those components have been marked with the @Deprecated
annotation. With 0.54.0 those components will still be released. However in a future release those components will be removed.
The feature-session-bundling
provided the functionality for an early feature called “sessions” in the Fenix project. This feature has been replaced with “collections” and the functionality for this new feature is provided by the new feature-tab-collections
component.
This component allowed apps to create “doorhangers” - floating heads-up popup that can be anchored to a view; like in Firefox for Android. This implementation was based on Android’s PopupWindow class. The implementation caused multiple layout issues and component using it (like feature-sitepermissions
) switched to using DialogFragment
s instead.
The AnimatedProgressBar
was first introduced in Firefox for Android and later used in Firefox Focus and Firefox Lite. A recent performance measurement revealed that the animation of the progress bar can have a negative impact on page load performance. While there was no noticeable difference on the latest high-end devices, on older devices, like a Nexus 5, we saw pages load about ~400ms slower.
Mozilla’s Android Components come with two implementations of session storage and helpers to write your own easily.
The SessionStorage class that comes with the browser-session component saves the state as a file on disk (using AtomicFile under the hood). It can be used for a browser that wants to have a single state that gets saved and restored (like Fennec or Chrome).
val sessionStorage SessionStorage(applicationContext, engine)
val sessionManager = sessionManager(engine).apply {
sessionStorage.restore()?.let { snapshot -> restore(snapshot) }
}
ℹ️ Since restoring the last state requires a disk read, it is recommended to perform it off the main thread. This requires the app to gracefully handle the situation where the app starts without any sessions at first. SessionManager will invoke onSessionsRestored() on a registered SessionManager.Observer after restoring has completed.
Other than SessionStorage the SessionBundleStorage implementation can save and restore from multiple states. State is saved as a Bundle in a database.
The storage is set up with a bundle lifetime. SessionBundleStorage will only restore the last bundle if its lifetime has not expired. If there’s no active bundle then a new empty bundle will be created to save the state.
val engine: Engine = ...
val sessionStorage = SessionBundleStorage(
applicationContext,
bundleLifetime = Pair(1, TimeUnit.HOURS)
val sessionManager = sessionManager(engine).apply {
// We launch a coroutine on the main thread. Once a snapshot has been restored
// we want to continue with it on the main thread.
GlobalScope.launch(Dispatchers.Main) {
// We restore on the IO dispatcher to not block the main thread:
val snapshot = async(Dispatchers.IO) {
val bundle = sessionStorage.restore()
// If we got a bundle then restore the snapshot from it
bundle.restoreSnapshot(engine)
}
// If we got a snapshot then restore it now:
snapshot.await()?.let { sessionManager.restore(it) }
}
}
The storage comes with an API that allows apps to build UIs to list, restore, save and remove bundles.
Knowing when to save state, by calling SessionStorage.save() or SessionBundleStorage.save(), can be complicated. Restoring an outdated state can be an as bad a user experience as restoring no state at all.
The AutoSave class is a helper for configuring automatic saving of the browser state - and you can use it with SessionStorage as well as SessionBundleStorage.
sessionStorage.autoSave(sessionManager)
// Automatically save the state every 30 seconds:
.periodicallyInForeground(interval = 30, unit = TimeUnit.SECONDS)
// Save the state when the app goes to the background:
.whenGoingToBackground()
// Save the state whenever sessions change (e.g. a new tab got added or a website
// has completed loading).
.whenSessionsChange()
If neither SessionStorage nor SessionBundleStorage satisfy the requirements of your app (e.g. you want to save the state in your favorite database or in a cloud-enabled storage) then it is possible to implement a custom storage.
The AutoSave.Storage interface from the browser-session component defines the methods that are expected from a session storage. Technically it is not required to implement the interface if your app code is the only one interacting with the session store; but implementing the interface makes your implementation compatible with other components code. Specifically you can use AutoSave with any class implementing SessionStorage without any additional code.
The SnapshotSerializer class is helpful when translating snapshots from and to JSON.
class MyCustomStorage(
private val engine: Engine
) : AutoSave.Storage {
private val serializer = SnapshotSerializer()
override fun save(snapshot: SessionManager.Snapshot): Boolean {
val json = serializer.toJSON(snapshot)
// TODO: Save JSON custom storage.
// Signal that save operation was successful:
return true
}
fun restore(): SessionManager.Snapshot {
// TODO: Get JSON from custom storage.
val json = ...
return serializer.fromJSON(engine, json)
}
}
ℹ️ For simplicity the implementation above does not handle JSONException which can be thrown by SnapshotSerializer.
]]>Firefox for Android (codenamed Fennec) started using KeepSafe’s Switchboard library and server component in order to run A/B testing or staged rollout of features, but since then the code changed significantly. It used a special switchboard server that decided which experiments a client is part of and returned a simplified list for the client to consume. However, this required the client to send data (including a unique id) to the server.
To avoid this Firefox moved to using Kinto as storage and server of the experiment configuration. Clients now download the whole list of experiments and decide locally what experiments they are enrolled in (the configuration for Fennec looks like this).
The purpose of this Google Summer of Code project, which is called Fretboard, was to develop an A/B testing framework written in Kotlin based on the existing code from Fennec, but making it independent of both the server used and the local storage mechanism, allowing it to be used on other apps, such as Firefox Focus, which started to need to do some A/B tests.
The source code implemented as part of this project is located at the android-components GitHub repo, more specifically here
This is a basic and non-exhaustive list of features, for all details you can view the README here
Fretboard allows you to specify the following filters:
There are two open pull requests. The first one is open pending a review from the security team and the last one is waiting for a non-alpha version of WorkManager to be released by Google:
Prior to starting the project, I became familiar with Kinto and the diff response format, as well as with the existing code of the Switchboard fork from Fennec. I also thought it was a good idea to send a pull request to Firefox Focus because this library was going to be integrated into it, and also to become familiar with a code review process at Mozilla. I looked at the issue list and discovered a problem with display cutouts, so I sent two pull requests to address the issue: the first one and the second one.
At the beginning of the project it became necessary to familiarize myself with tools like Taskcluster, which I had never used (although I used similar tools like Travis CI).
The most difficult pull requests for me were the ones related to certificate pinning and experiment collection signature verification. For the first one I had to broaden my knowledge about it, as well as research how to properly implement it on Android, avoiding common mistakes.
For the second one the most difficult part was to understand what algorithm Mozilla was using to validate the signatures, and how it worked. I discovered from the Kinto collection mode
field that it was p384ecdsa
, and then I had to research how to properly implement it in Kotlin. For this later I needed the help of Julien Vehent and Franziskus Kiefer, which pointed me to a great talk and also a Go and C++ implementation. After seeing the two implementations I realized my solution wasn’t working because I didn’t know that the signature actually contained two values concatenated (r and s), which then needed to be encoded using DER syntax
Overall I think I learned a lot doing this project and I really loved working with Mozilla.
Right now there is an ongoing discussion about enhancing Fretboard with an expression language (being that JEXL/CEL/etc) for the matchers values instead of regular expressions like it’s using now.
I would like to thank my mentor Sebastian Kaspari for all the help and guidance, for being so friendly and available to talk at any moment I needed, as well as reviewing my pull requests quickly.
I would also like to thank Franziskus Kiefer and Julien Vehent for helping me understand the signature validation system used by Kinto.