diff --git a/components/browser/engine-gecko/src/main/java/mozilla/components/browser/engine/gecko/GeckoEngineSession.kt b/components/browser/engine-gecko/src/main/java/mozilla/components/browser/engine/gecko/GeckoEngineSession.kt index ee56f031d08..7c9cd7ba65a 100644 --- a/components/browser/engine-gecko/src/main/java/mozilla/components/browser/engine/gecko/GeckoEngineSession.kt +++ b/components/browser/engine-gecko/src/main/java/mozilla/components/browser/engine/gecko/GeckoEngineSession.kt @@ -429,6 +429,12 @@ class GeckoEngineSession( runtime.webExtensionController.setTabActive(geckoSession, active) } + /** + * See [EngineSession.updateSessionPriority]. + */ + override fun updateSessionPriority(priority: SessionPriority) { + geckoSession.setPriorityHint(priority.id) + } /** * Purges the history for the session (back and forward history). */ diff --git a/components/browser/state/src/main/java/mozilla/components/browser/state/engine/middleware/SessionPrioritizationMiddleware.kt b/components/browser/state/src/main/java/mozilla/components/browser/state/engine/middleware/SessionPrioritizationMiddleware.kt new file mode 100644 index 00000000000..b659956e76e --- /dev/null +++ b/components/browser/state/src/main/java/mozilla/components/browser/state/engine/middleware/SessionPrioritizationMiddleware.kt @@ -0,0 +1,80 @@ +/* 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 http://mozilla.org/MPL/2.0/. */ + +package mozilla.components.browser.state.engine.middleware + +import androidx.annotation.VisibleForTesting +import mozilla.components.browser.state.action.BrowserAction +import mozilla.components.browser.state.action.EngineAction +import mozilla.components.browser.state.action.TabListAction +import mozilla.components.browser.state.selector.findTab +import mozilla.components.browser.state.state.BrowserState +import mozilla.components.concept.engine.EngineSession +import mozilla.components.concept.engine.EngineSession.SessionPriority.DEFAULT +import mozilla.components.concept.engine.EngineSession.SessionPriority.HIGH +import mozilla.components.lib.state.Middleware +import mozilla.components.lib.state.MiddlewareContext +import mozilla.components.support.base.log.logger.Logger + +/** + * [Middleware] implementation responsible for updating the priority of the selected [EngineSession] + * to [HIGH] and the rest to [DEFAULT]. + */ +class SessionPrioritizationMiddleware : Middleware { + private val logger = Logger("SessionPrioritizationMiddleware") + + @VisibleForTesting + internal var previousHighestPriorityTabId = "" + + override fun invoke( + context: MiddlewareContext, + next: (BrowserAction) -> Unit, + action: BrowserAction + ) { + + when (action) { + is EngineAction.UnlinkEngineSessionAction -> { + val activeTab = context.state.findTab(action.tabId) + activeTab?.engineState?.engineSession?.updateSessionPriority(DEFAULT) + logger.info("Update the tab ${activeTab?.id} priority to ${DEFAULT.name}") + } + else -> { + // no-op + } + } + + next(action) + + when (action) { + is TabListAction, + is EngineAction.LinkEngineSessionAction -> { + val state = context.state + if (previousHighestPriorityTabId != state.selectedTabId) { + updatePriorityIfNeeded(state) + } + } + else -> { + // no-op + } + } + } + + private fun updatePriorityIfNeeded(state: BrowserState) { + val currentSelectedTab = state.selectedTabId?.let { state.findTab(it) } + val previousSelectedTab = state.findTab(previousHighestPriorityTabId) + val currentEngineSession: EngineSession? = currentSelectedTab?.engineState?.engineSession + + // We need to make sure, we alter the previousHighestPriorityTabId, after the session is linked. + // So we update the priority on the engine session, as we could get actions where the tab + // is selected but not linked yet, causing out sync issues, + // when previousHighestPriorityTabId didn't call updateSessionPriority() + if (currentEngineSession != null) { + previousSelectedTab?.engineState?.engineSession?.updateSessionPriority(DEFAULT) + currentEngineSession.updateSessionPriority(HIGH) + logger.info("Update the currentSelectedTab ${currentSelectedTab.id} priority to ${HIGH.name}") + logger.info("Update the previousSelectedTab ${previousSelectedTab?.id} priority to ${DEFAULT.name}") + previousHighestPriorityTabId = currentSelectedTab.id + } + } +} diff --git a/components/browser/state/src/test/java/mozilla/components/browser/state/engine/middleware/SessionPrioritizationMiddlewareTest.kt b/components/browser/state/src/test/java/mozilla/components/browser/state/engine/middleware/SessionPrioritizationMiddlewareTest.kt new file mode 100644 index 00000000000..eba60ac168f --- /dev/null +++ b/components/browser/state/src/test/java/mozilla/components/browser/state/engine/middleware/SessionPrioritizationMiddlewareTest.kt @@ -0,0 +1,118 @@ +/* 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 http://mozilla.org/MPL/2.0/. */ + +package mozilla.components.browser.state.engine.middleware + +import mozilla.components.browser.state.action.EngineAction +import mozilla.components.browser.state.action.TabListAction +import mozilla.components.browser.state.state.BrowserState +import mozilla.components.browser.state.state.createTab +import mozilla.components.browser.state.store.BrowserStore +import mozilla.components.concept.engine.EngineSession +import mozilla.components.concept.engine.EngineSession.SessionPriority.DEFAULT +import mozilla.components.concept.engine.EngineSession.SessionPriority.HIGH +import mozilla.components.support.test.ext.joinBlocking +import mozilla.components.support.test.mock +import org.junit.Assert.assertEquals +import org.junit.Test +import org.mockito.Mockito.verify + +class SessionPrioritizationMiddlewareTest { + + @Test + fun `GIVEN a linked session WHEN UnlinkEngineSessionAction THEN set the DEFAULT priority to the unlinked tab`() { + val middleware = SessionPrioritizationMiddleware() + val store = BrowserStore( + initialState = BrowserState( + tabs = listOf( + createTab("https://www.mozilla.org", id = "1"), + ) + ), + middleware = listOf(middleware) + ) + val engineSession1: EngineSession = mock() + + store.dispatch(EngineAction.LinkEngineSessionAction("1", engineSession1)).joinBlocking() + store.dispatch(EngineAction.UnlinkEngineSessionAction("1")).joinBlocking() + + verify(engineSession1).updateSessionPriority(DEFAULT) + assertEquals("", middleware.previousHighestPriorityTabId) + } + + @Test + fun `GIVEN a previous selected tab WHEN LinkEngineSessionAction THEN update the selected linked tab priority to HIGH`() { + val middleware = SessionPrioritizationMiddleware() + val store = BrowserStore( + initialState = BrowserState( + tabs = listOf( + createTab("https://www.mozilla.org", id = "1"), + ) + ), + middleware = listOf(middleware) + ) + val engineSession1: EngineSession = mock() + + store.dispatch(TabListAction.SelectTabAction("1")).joinBlocking() + + assertEquals("", middleware.previousHighestPriorityTabId) + + store.dispatch(EngineAction.LinkEngineSessionAction("1", engineSession1)).joinBlocking() + + assertEquals("1", middleware.previousHighestPriorityTabId) + verify(engineSession1).updateSessionPriority(HIGH) + } + + @Test + fun `GIVEN a previous selected tab with priority DEFAULT WHEN selecting and linking a new tab THEN update the previous tab to DEFAULT and the new one to HIGH`() { + val middleware = SessionPrioritizationMiddleware() + val store = BrowserStore( + initialState = BrowserState( + tabs = listOf( + createTab("https://www.mozilla.org", id = "1"), + createTab("https://www.firefox.com", id = "2") + ) + ), + middleware = listOf(middleware) + ) + val engineSession1: EngineSession = mock() + val engineSession2: EngineSession = mock() + + store.dispatch(TabListAction.SelectTabAction("1")).joinBlocking() + + assertEquals("", middleware.previousHighestPriorityTabId) + + store.dispatch(EngineAction.LinkEngineSessionAction("1", engineSession1)).joinBlocking() + + assertEquals("1", middleware.previousHighestPriorityTabId) + verify(engineSession1).updateSessionPriority(HIGH) + + store.dispatch(TabListAction.SelectTabAction("2")).joinBlocking() + + assertEquals("1", middleware.previousHighestPriorityTabId) + + store.dispatch(EngineAction.LinkEngineSessionAction("2", engineSession2)).joinBlocking() + + assertEquals("2", middleware.previousHighestPriorityTabId) + verify(engineSession1).updateSessionPriority(DEFAULT) + verify(engineSession2).updateSessionPriority(HIGH) + } + + @Test + fun `GIVEN no linked tab WHEN SelectTabAction THEN no changes in priority show happened`() { + val middleware = SessionPrioritizationMiddleware() + val store = BrowserStore( + initialState = BrowserState( + tabs = listOf( + createTab("https://www.mozilla.org", id = "1"), + createTab("https://www.firefox.com", id = "2") + ) + ), + middleware = listOf(middleware) + ) + + store.dispatch(TabListAction.SelectTabAction("1")).joinBlocking() + + assertEquals("", middleware.previousHighestPriorityTabId) + } +} diff --git a/components/concept/engine/src/main/java/mozilla/components/concept/engine/EngineSession.kt b/components/concept/engine/src/main/java/mozilla/components/concept/engine/EngineSession.kt index 77ada0e4ea8..20901f8f3c4 100644 --- a/components/concept/engine/src/main/java/mozilla/components/concept/engine/EngineSession.kt +++ b/components/concept/engine/src/main/java/mozilla/components/concept/engine/EngineSession.kt @@ -569,6 +569,23 @@ abstract class EngineSession( override fun hashCode() = value } + /** + * Represents a session priority, which signals to the engine that it should give + * a different prioritization to a given session. + */ + @Suppress("MagicNumber") + enum class SessionPriority(val id: Int) { + /** + * Signals to the engine that this session has a default priority. + */ + DEFAULT(0), + /** + * Signals to the engine that this session is important, and the Engine should keep + * the session alive for as long as possible. + */ + HIGH(1) + } + /** * Loads the given URL. * @@ -695,6 +712,13 @@ abstract class EngineSession( */ open fun markActiveForWebExtensions(active: Boolean) = Unit + /** + * Updates the priority for this session. + * + * @param priority the new priority for this session. + */ + open fun updateSessionPriority(priority: SessionPriority) = Unit + /** * Purges the history for the session (back and forward history). */ diff --git a/docs/changelog.md b/docs/changelog.md index 169c25e3f5e..1ab17c2cdbd 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -11,6 +11,9 @@ permalink: /changelog/ * [Gecko](https://github.com/mozilla-mobile/android-components/blob/main/buildSrc/src/main/java/Gecko.kt) * [Configuration](https://github.com/mozilla-mobile/android-components/blob/main/.config.yml) +* **browser-state**: + * 🌟️ Add support for tab prioritization via `SessionPrioritizationMiddleware` for more information see [#12190](https://github.com/mozilla-mobile/android-components/issues/12190). + * **service-pocket** * 🌟️ Add support for rotating and pacing Pocket sponsored stories. [#12184](https://github.com/mozilla-mobile/android-components/issues/12184) * See component's [README](https://github.com/mozilla-mobile/android-components/blob/main/components/service/pocket/README.md) to get more info. diff --git a/samples/browser/src/main/java/org/mozilla/samples/browser/DefaultComponents.kt b/samples/browser/src/main/java/org/mozilla/samples/browser/DefaultComponents.kt index 16fe137a7de..3891851b48c 100644 --- a/samples/browser/src/main/java/org/mozilla/samples/browser/DefaultComponents.kt +++ b/samples/browser/src/main/java/org/mozilla/samples/browser/DefaultComponents.kt @@ -24,6 +24,7 @@ import mozilla.components.browser.menu.item.BrowserMenuItemToolbar import mozilla.components.browser.menu.item.SimpleBrowserMenuItem import mozilla.components.browser.session.storage.SessionStorage import mozilla.components.browser.state.engine.EngineMiddleware +import mozilla.components.browser.state.engine.middleware.SessionPrioritizationMiddleware import mozilla.components.browser.state.selector.selectedTab import mozilla.components.browser.state.store.BrowserStore import mozilla.components.browser.storage.sync.PlacesHistoryStorage @@ -162,7 +163,8 @@ open class DefaultComponents(private val applicationContext: Context) { SearchMiddleware(applicationContext), RecordingDevicesMiddleware(applicationContext), LastAccessMiddleware(), - PromptMiddleware() + PromptMiddleware(), + SessionPrioritizationMiddleware() ) + EngineMiddleware.create(engine) ) }