LeOS-Ice-browser/android-components/docs/_posts/2019-02-19-saving-state.md

7.2 KiB
Raw Blame History

layout title date author
post 💾 Saving and restoring browser session state 2019-02-18 14:35:00 +0200 sebastian

Losing open tabs in a browser can be a painful experience for the user. By itself SessionManager keeps its state only in memory. This means that something as simple as switching to a different app can cause Android to kill the browser app's process and reclaim its resources. The result of that: The next time the user switches back to the browser app they start with a fresh browser without any tabs open.

Mozilla's Android Components come with two implementations of session storage and helpers to write your own easily.

SessionStorage

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.

SessionBundleStorage

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.

AutoSave

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()

Implementing your own SessionStorage

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](ad https://developer.android.com/reference/org/json/JSONException.html) which can be thrown by SnapshotSerializer.