diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/Amethyst.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/Amethyst.kt index 171905009d..f9756c9b54 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/Amethyst.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/Amethyst.kt @@ -248,7 +248,7 @@ class Amethyst : Application() { // memory stays freed until the user comes back. val pressure = level >= ComponentCallbacks2.TRIM_MEMORY_BACKGROUND if (pressure && Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { - EmbeddedTabHost.rebuildAll() + EmbeddedTabHost.rebuildAll(keepPages = true) } } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/browser/EmbeddedWebAppController.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/browser/EmbeddedWebAppController.kt index a893e8ba30..e521f7a1c9 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/browser/EmbeddedWebAppController.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/browser/EmbeddedWebAppController.kt @@ -166,6 +166,24 @@ class EmbeddedWebAppController( var lastCanGoForward = false private set + /** A tab's page and per-tab settings, carried to the controller that replaces this one on a rebuild. */ + class PageSnapshot( + val url: String?, + val useTor: Boolean, + val textZoom: Int, + val desktopSite: Boolean, + ) + + fun snapshot() = PageSnapshot(lastUrl, useTor, textZoom, desktopSite) + + /** Takes over a torn-down predecessor's page and settings; call before [bind], which creates the session. */ + fun restore(snapshot: PageSnapshot) { + lastUrl = snapshot.url + useTor = snapshot.useTor + textZoom = snapshot.textZoom + desktopSite = snapshot.desktopSite + } + /** The user's per-tab settings as last set, for a screen coming back to this tab. */ val isTorOn: Boolean get() = useTor @@ -462,6 +480,17 @@ class EmbeddedWebAppController( publishLoadStatus(EmbeddedLoadStatus(isLoading = true)) } + // Set by the user's Retry: the next session starts over at [startUrl]. Every other re-creation (a crashed + // renderer, a `:napplet` restart, a memory-trim rebuild) resumes the page the user was on. + private var restartAtStart = false + + /** Where a new session opens: the page on screen before it was lost, else the tab's own [startUrl]. */ + private fun sessionUrl(): String { + val resume = lastUrl?.takeUnless { restartAtStart || it.isBlankPage() } + restartAtStart = false + return resume ?: startUrl + } + private fun sendCreateSession() { awaitingReady = true uiDisplayed = false @@ -471,7 +500,7 @@ class EmbeddedWebAppController( data = Bundle().apply { putString(NappletBrowserContract.KEY_SESSION_ID, sessionId) - putString(NappletBrowserContract.KEY_URL, startUrl) + putString(NappletBrowserContract.KEY_URL, sessionUrl()) putInt(NappletBrowserContract.KEY_PROXY_PORT, proxyPort()) putBoolean(NappletBrowserContract.KEY_USE_TOR, useTor) putInt(NappletBrowserContract.KEY_BG_COLOR, backgroundColor) @@ -683,7 +712,12 @@ class EmbeddedWebAppController( recovery.clearPending() showRecovering() // A surface that never opened has nothing to navigate: only a new session can paint it. - if (sessionDead || (sandboxedSdkView != null && !uiDisplayed)) rearmSession() else navigate(startUrl) + if (sessionDead || (sandboxedSdkView != null && !uiDisplayed)) { + restartAtStart = true + rearmSession() + } else { + navigate(startUrl) + } } private fun onLoadState( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/embed/EmbeddedTabFactory.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/embed/EmbeddedTabFactory.kt index 667c222e26..b761283ace 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/embed/EmbeddedTabFactory.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/embed/EmbeddedTabFactory.kt @@ -73,7 +73,10 @@ object EmbeddedTabFactory { if (nightMask == Configuration.UI_MODE_NIGHT_YES) "DARK" else "LIGHT" } } - EmbeddedWebAppController(context.applicationContext, ::currentTorPort, initialUseTor, backgroundColor, theme).also { it.bind(url) } + EmbeddedWebAppController(context.applicationContext, ::currentTorPort, initialUseTor, backgroundColor, theme).also { + EmbeddedTabHost.takePageSnapshot(webAppId(url))?.let(it::restore) + it.bind(url) + } } as EmbeddedWebAppController /** diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/embed/EmbeddedTabHost.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/embed/EmbeddedTabHost.kt index c9968d6de6..2d7c1a1080 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/embed/EmbeddedTabHost.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/embed/EmbeddedTabHost.kt @@ -33,6 +33,7 @@ import androidx.lifecycle.Lifecycle import androidx.lifecycle.LifecycleEventObserver import androidx.lifecycle.LifecycleOwner import com.vitorpamplona.amethyst.napplethost.NappletHostContract +import com.vitorpamplona.amethyst.ui.screen.loggedIn.browser.EmbeddedWebAppController /** * Process-level holder of **warm embedded sessions** — the persistent-surface-layer half of keep-warm. @@ -178,7 +179,7 @@ object EmbeddedTabHost { val previous = builtDark builtDark = dark // The first report only records what the sessions (built from the same preference) already use. - if (previous != null && previous != dark) rebuildAll() + if (previous != null && previous != dark) rebuildAll(keepPages = true) } /** @@ -332,16 +333,31 @@ object EmbeddedTabHost { * screen and the preloader re-acquire freshly built sessions. This keeps [activeId], so the visible tab * re-activates the instant its screen re-acquires — the user just sees the current tab reload, not a * blanked-out surface. + * + * [keepPages]: each browser tab's rebuilt controller resumes the page it showed, with the user's Tor, zoom + * and desktop choices ([EmbeddedWebAppController.PageSnapshot]). Android sends the memory trim routinely, + * about a minute into the background, so without this every pinned site came back on its start URL. Never + * across an account switch: the next account must not open the previous one's pages. */ - fun rebuildAll() { + fun rebuildAll(keepPages: Boolean) { // Every page is about to be rebuilt from scratch, so no field survives to restore a keyboard onto. keyboardUpOnLeave.clear() val copy = warm.toList() warm.clear() + pageSnapshots.clear() + if (keepPages) { + copy.forEach { w -> (w.controller as? EmbeddedWebAppController)?.let { pageSnapshots[w.id] = it.snapshot() } } + } copy.forEach { it.controller.teardown() } rebuildEpoch += 1 } + // Page state carried from a torn-down browser tab to its rebuilt controller (see [rebuildAll]). + private val pageSnapshots = mutableMapOf() + + /** The page state [rebuildAll] saved for tab [id], handed over once. */ + fun takePageSnapshot(id: String): EmbeddedWebAppController.PageSnapshot? = pageSnapshots.remove(id) + /** * Account the warm sessions were built for, as the opaque WebView storage-profile name (null while * logged out). Kept HERE, next to the sessions it describes, rather than in a composable's `remember`: @@ -372,6 +388,6 @@ object EmbeddedTabHost { builtForProfile = profileName // Seeding on the first call (app start) must not bump the epoch: nothing is stale yet, and a // needless bump would restart the preload sweep that is just getting going. - if (!isFirstCall) rebuildAll() + if (!isFirstCall) rebuildAll(keepPages = false) } }