From 18f239d819785c312f6c0d11abfcb4c7e9dc0bd9 Mon Sep 17 00:00:00 2001 From: Sebastian Kaspari Date: Wed, 29 Jul 2020 15:28:18 +0200 Subject: [PATCH] Issue #7867: Move EngineSession from SessionManager to BrowserState. Co-authored-by: Christian Sadilek --- components/browser/session/build.gradle | 7 + .../browser/session/LegacySessionManager.kt | 127 +-- .../components/browser/session/Session.kt | 47 +- .../browser/session/SessionManager.kt | 247 +----- .../session/engine/EngineMiddleware.kt | 121 +++ .../browser/session/engine/EngineObserver.kt | 21 +- .../session/engine/EngineSessionHolder.kt | 19 - .../browser/session/engine/MediaObserver.kt | 5 +- .../engine/middleware/CrashMiddleware.kt | 57 ++ .../CreateEngineSessionMiddleware.kt | 61 ++ .../middleware/EngineDelegateMiddleware.kt | 162 ++++ .../engine/middleware/LinkingMiddleware.kt | 83 ++ .../engine/middleware/SuspendMiddleware.kt | 71 ++ .../middleware/TabsRemovedMiddleware.kt | 67 ++ .../engine/middleware/TrimMemoryMiddleware.kt | 66 ++ .../middleware/WebExtensionMiddleware.kt | 75 ++ .../browser/session/ext/AtomicFile.kt | 10 +- .../browser/session/ext/SessionExtensions.kt | 3 +- .../browser/session/storage/AutoSave.kt | 108 ++- .../session/storage/BrowserStateSerializer.kt | 65 ++ .../browser/session/storage/SessionStorage.kt | 27 +- .../session/storage/SnapshotSerializer.kt | 18 +- .../session/usecases/EngineSessionUseCases.kt | 49 -- .../SelectionAwareSessionObserverTest.kt | 3 - .../session/SessionManagerMigrationTest.kt | 623 +-------------- .../browser/session/SessionManagerTest.kt | 296 ------- .../components/browser/session/SessionTest.kt | 62 +- .../session/engine/EngineObserverTest.kt | 117 +-- .../session/engine/EngineSessionHolderTest.kt | 39 - .../engine/middleware/CrashMiddlewareTest.kt | 163 ++++ .../CreateEngineSessionMiddlewareTest.kt | 141 ++++ .../EngineDelegateMiddlewareTest.kt | 754 ++++++++++++++++++ .../middleware/LinkingMiddlewareTest.kt | 176 ++++ .../middleware/SuspendMiddlewareTest.kt | 93 +++ .../middleware/TabsRemovedMiddlewareTest.kt | 226 ++++++ .../middleware/TrimMemoryMiddlewareTest.kt | 122 +++ .../middleware/WebExtensionMiddlewareTest.kt | 135 ++++ .../browser/session/ext/AtomicFileKtTest.kt | 31 +- .../session/storage/SessionStorageTest.kt | 179 +++-- .../session/utils/AllSessionsObserverTest.kt | 6 - .../browser/state/action/BrowserAction.kt | 135 +++- .../state/ext/CustomTabSessionState.kt | 19 + .../state/reducer/BrowserStateReducer.kt | 2 + .../browser/state/reducer/CrashReducer.kt | 24 + .../state/reducer/CustomTabListReducer.kt | 15 + .../state/reducer/EngineStateReducer.kt | 28 +- .../browser/state/reducer/SystemReducer.kt | 67 +- .../state/state/CustomTabSessionState.kt | 9 +- .../browser/state/state/EngineState.kt | 4 +- .../browser/state/state/SessionState.kt | 9 +- .../browser/state/state/TabSessionState.kt | 13 +- .../browser/state/action/EngineActionTest.kt | 16 + .../browser/state/action/SystemActionTest.kt | 60 +- .../contextmenu/ContextMenuCandidateTest.kt | 46 +- .../CustomTabIntentProcessorTest.kt | 68 +- .../processing/TabIntentProcessorTest.kt | 145 ++-- .../components/feature/p2p/P2PFeature.kt | 32 +- .../feature/search/SearchUseCases.kt | 22 +- .../feature/search/SearchUseCasesTest.kt | 45 +- .../feature/session/SessionFeature.kt | 4 +- .../feature/session/SessionUseCases.kt | 160 ++-- .../session/engine/EngineViewPresenter.kt | 35 +- .../feature/session/SessionFeatureTest.kt | 311 +++----- .../feature/session/SessionUseCasesTest.kt | 302 +++---- .../components/feature/tabs/TabsUseCases.kt | 20 +- .../feature/tabs/TabsUseCasesTest.kt | 166 ++-- .../samples/browser/BaseBrowserFragment.kt | 1 - .../samples/browser/BrowserFragment.kt | 3 +- .../samples/browser/DefaultComponents.kt | 20 +- .../samples/browser/SampleApplication.kt | 4 +- 70 files changed, 3950 insertions(+), 2517 deletions(-) create mode 100644 components/browser/session/src/main/java/mozilla/components/browser/session/engine/EngineMiddleware.kt delete mode 100644 components/browser/session/src/main/java/mozilla/components/browser/session/engine/EngineSessionHolder.kt create mode 100644 components/browser/session/src/main/java/mozilla/components/browser/session/engine/middleware/CrashMiddleware.kt create mode 100644 components/browser/session/src/main/java/mozilla/components/browser/session/engine/middleware/CreateEngineSessionMiddleware.kt create mode 100644 components/browser/session/src/main/java/mozilla/components/browser/session/engine/middleware/EngineDelegateMiddleware.kt create mode 100644 components/browser/session/src/main/java/mozilla/components/browser/session/engine/middleware/LinkingMiddleware.kt create mode 100644 components/browser/session/src/main/java/mozilla/components/browser/session/engine/middleware/SuspendMiddleware.kt create mode 100644 components/browser/session/src/main/java/mozilla/components/browser/session/engine/middleware/TabsRemovedMiddleware.kt create mode 100644 components/browser/session/src/main/java/mozilla/components/browser/session/engine/middleware/TrimMemoryMiddleware.kt create mode 100644 components/browser/session/src/main/java/mozilla/components/browser/session/engine/middleware/WebExtensionMiddleware.kt create mode 100644 components/browser/session/src/main/java/mozilla/components/browser/session/storage/BrowserStateSerializer.kt delete mode 100644 components/browser/session/src/main/java/mozilla/components/browser/session/usecases/EngineSessionUseCases.kt delete mode 100644 components/browser/session/src/test/java/mozilla/components/browser/session/engine/EngineSessionHolderTest.kt create mode 100644 components/browser/session/src/test/java/mozilla/components/browser/session/engine/middleware/CrashMiddlewareTest.kt create mode 100644 components/browser/session/src/test/java/mozilla/components/browser/session/engine/middleware/CreateEngineSessionMiddlewareTest.kt create mode 100644 components/browser/session/src/test/java/mozilla/components/browser/session/engine/middleware/EngineDelegateMiddlewareTest.kt create mode 100644 components/browser/session/src/test/java/mozilla/components/browser/session/engine/middleware/LinkingMiddlewareTest.kt create mode 100644 components/browser/session/src/test/java/mozilla/components/browser/session/engine/middleware/SuspendMiddlewareTest.kt create mode 100644 components/browser/session/src/test/java/mozilla/components/browser/session/engine/middleware/TabsRemovedMiddlewareTest.kt create mode 100644 components/browser/session/src/test/java/mozilla/components/browser/session/engine/middleware/TrimMemoryMiddlewareTest.kt create mode 100644 components/browser/session/src/test/java/mozilla/components/browser/session/engine/middleware/WebExtensionMiddlewareTest.kt create mode 100644 components/browser/state/src/main/java/mozilla/components/browser/state/ext/CustomTabSessionState.kt create mode 100644 components/browser/state/src/main/java/mozilla/components/browser/state/reducer/CrashReducer.kt diff --git a/components/browser/session/build.gradle b/components/browser/session/build.gradle index a761c7f6117..05be170dd12 100644 --- a/components/browser/session/build.gradle +++ b/components/browser/session/build.gradle @@ -21,6 +21,12 @@ android { } } +tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).configureEach() { + kotlinOptions.freeCompilerArgs += [ + "-Xuse-experimental=kotlinx.coroutines.ExperimentalCoroutinesApi" + ] +} + dependencies { api project(':browser-state') @@ -47,6 +53,7 @@ dependencies { testImplementation Dependencies.androidx_test_junit testImplementation Dependencies.testing_robolectric testImplementation Dependencies.testing_mockito + testImplementation Dependencies.testing_coroutines } apply from: '../../../publish.gradle' diff --git a/components/browser/session/src/main/java/mozilla/components/browser/session/LegacySessionManager.kt b/components/browser/session/src/main/java/mozilla/components/browser/session/LegacySessionManager.kt index 0353e22f5f5..b604acec373 100644 --- a/components/browser/session/src/main/java/mozilla/components/browser/session/LegacySessionManager.kt +++ b/components/browser/session/src/main/java/mozilla/components/browser/session/LegacySessionManager.kt @@ -6,8 +6,6 @@ package mozilla.components.browser.session import androidx.annotation.GuardedBy import mozilla.components.concept.engine.Engine -import mozilla.components.concept.engine.EngineSession -import mozilla.components.concept.engine.EngineSessionState import mozilla.components.support.base.observer.Observable import mozilla.components.support.base.observer.ObserverRegistry import kotlin.math.max @@ -19,7 +17,6 @@ import kotlin.math.min @Suppress("TooManyFunctions", "LargeClass") class LegacySessionManager( val engine: Engine, - private val engineSessionLinker: SessionManager.EngineSessionLinker, delegate: Observable = ObserverRegistry() ) : Observable by delegate { // It's important that any access to `values` is synchronized; @@ -35,60 +32,6 @@ class LegacySessionManager( val size: Int get() = synchronized(values) { values.size } - /** - * Produces a snapshot of this manager's state, suitable for restoring via [SessionManager.restore]. - * Only regular sessions are included in the snapshot. Private and Custom Tab sessions are omitted. - * - * @return [SessionManager.Snapshot] of the current session state. - */ - fun createSnapshot(): SessionManager.Snapshot = synchronized(values) { - if (values.isEmpty()) { - return SessionManager.Snapshot.empty() - } - - // Filter out CustomTab and private sessions. - // We're using 'values' directly instead of 'sessions' to get benefits of a sequence. - val sessionStateTuples = values.asSequence() - .filter { !it.isCustomTabSession() } - .filter { !it.private } - .map { session -> createSessionSnapshot(session) } - .toList() - - // We might have some sessions (private, custom tab) but none we'd include in the snapshot. - if (sessionStateTuples.isEmpty()) { - return SessionManager.Snapshot.empty() - } - - // We need to find out the index of our selected session in the filtered list. If we have a - // mix of private, custom tab and regular sessions, global selectedIndex isn't good enough. - // We must have a selectedSession if there is at least one "regular" (non-CustomTabs) session - // present. Selected session might be private, in which case we reset our selection index to 0. - var selectedIndexAfterFiltering = 0 - selectedSession?.takeIf { !it.private }?.let { selected -> - sessionStateTuples.find { it.session.id == selected.id }?.let { selectedTuple -> - selectedIndexAfterFiltering = sessionStateTuples.indexOf(selectedTuple) - } - } - - // Sanity check to guard against producing invalid snapshots. - checkNotNull(sessionStateTuples.getOrNull(selectedIndexAfterFiltering)) { - "Selection index after filtering session must be valid" - } - - SessionManager.Snapshot( - sessions = sessionStateTuples, - selectedSessionIndex = selectedIndexAfterFiltering - ) - } - - fun createSessionSnapshot(session: Session): SessionManager.Snapshot.Item { - return SessionManager.Snapshot.Item( - session, - session.engineSessionHolder.engineSession, - session.engineSessionHolder.engineSessionState - ) - } - /** * Gets the currently selected session if there is one. * @@ -136,19 +79,15 @@ class LegacySessionManager( fun add( session: Session, selected: Boolean = false, - engineSession: EngineSession? = null, - engineSessionState: EngineSessionState? = null, parent: Session? = null ) = synchronized(values) { - addInternal(session, selected, engineSession, engineSessionState, parent = parent, viaRestore = false) + addInternal(session, selected, parent = parent, viaRestore = false) } @Suppress("LongParameterList", "ComplexMethod") private fun addInternal( session: Session, selected: Boolean = false, - engineSession: EngineSession? = null, - engineSessionState: EngineSessionState? = null, parent: Session? = null, viaRestore: Boolean = false, notifyObservers: Boolean = true @@ -173,12 +112,6 @@ class LegacySessionManager( } } - if (engineSession != null) { - link(session, engineSession) - } else if (engineSessionState != null) { - session.engineSessionHolder.engineSessionState = engineSessionState - } - // If session is being added via restore, skip notification and auto-selection. // Restore will handle these actions as appropriate. if (viaRestore || !notifyObservers) { @@ -241,8 +174,6 @@ class LegacySessionManager( snapshot.sessions.asReversed().forEach { addInternal( it.session, - engineSession = it.engineSession, - engineSessionState = it.engineSessionState, parent = null, viaRestore = true ) @@ -262,52 +193,6 @@ class LegacySessionManager( notifyObservers { onSessionsRestored() } } - /** - * Gets the linked engine session for the provided session (if it exists). - */ - fun getEngineSession(session: Session = selectedSessionOrThrow) = session.engineSessionHolder.engineSession - - /** - * Gets the linked engine session for the provided session and creates it if needed. - */ - fun getOrCreateEngineSession( - session: Session = selectedSessionOrThrow, - skipLoading: Boolean = false - ): EngineSession { - getEngineSession(session)?.let { return it } - - return engine.createSession(session.private, session.contextId).apply { - var restored = false - session.engineSessionHolder.engineSessionState?.let { state -> - restored = restoreState(state) - session.engineSessionHolder.engineSessionState = null - } - - link(session, this, restored, skipLoading) - } - } - - private fun link( - session: Session, - engineSession: EngineSession, - restored: Boolean = false, - skipLoading: Boolean = false - ) { - val parent = values.find { it.id == session.parentId }?.let { - this.getEngineSession(it) - } - engineSessionLinker.link(session, engineSession, parent, restored, skipLoading) - - if (session == selectedSession) { - engineSession.markActiveForWebExtensions(true) - } - } - - private fun unlink(session: Session) { - getEngineSession(session)?.markActiveForWebExtensions(false) - engineSessionLinker.unlink(session) - } - /** * Removes the provided session. If no session is provided then the selected session is removed. */ @@ -324,8 +209,6 @@ class LegacySessionManager( values.removeAt(indexToRemove) - unlink(session) - recalculateSelectionIndex( indexToRemove, selectParentIfExists, @@ -339,7 +222,6 @@ class LegacySessionManager( notifyObservers { onSessionRemoved(session) } if (selectedBeforeRemove != selectedSession && selectedIndex != NO_SELECTION) { - getEngineSession(selectedSessionOrThrow)?.markActiveForWebExtensions(true) notifyObservers { onSessionSelected(selectedSessionOrThrow) } } } @@ -472,7 +354,6 @@ class LegacySessionManager( */ fun removeSessions() = synchronized(values) { sessions.forEach { - unlink(it) values.remove(it) } @@ -487,7 +368,6 @@ class LegacySessionManager( * Removes all sessions including CustomTab sessions. */ fun removeAll() = synchronized(values) { - values.forEach { unlink(it) } values.clear() selectedIndex = NO_SELECTION @@ -507,13 +387,8 @@ class LegacySessionManager( "Value to select is not in list" } - selectedSession?.let { - getEngineSession(it)?.markActiveForWebExtensions(false) - } - selectedIndex = index - getEngineSession(session)?.markActiveForWebExtensions(true) notifyObservers { onSessionSelected(session) } } diff --git a/components/browser/session/src/main/java/mozilla/components/browser/session/Session.kt b/components/browser/session/src/main/java/mozilla/components/browser/session/Session.kt index 12104810e66..ef1c57e42fd 100644 --- a/components/browser/session/src/main/java/mozilla/components/browser/session/Session.kt +++ b/components/browser/session/src/main/java/mozilla/components/browser/session/Session.kt @@ -6,14 +6,11 @@ package mozilla.components.browser.session import android.content.Intent import android.graphics.Bitmap -import mozilla.components.browser.session.engine.EngineSessionHolder import mozilla.components.browser.session.engine.request.LaunchIntentMetadata import mozilla.components.browser.session.engine.request.LoadRequestMetadata import mozilla.components.browser.session.engine.request.LoadRequestOption import mozilla.components.browser.session.ext.syncDispatch import mozilla.components.browser.session.ext.toSecurityInfoState -import mozilla.components.browser.session.ext.toTabSessionState -import mozilla.components.browser.state.action.ContentAction.RemoveThumbnailAction import mozilla.components.browser.state.action.ContentAction.RemoveWebAppManifestAction import mozilla.components.browser.state.action.ContentAction.UpdateBackNavigationStateAction import mozilla.components.browser.state.action.ContentAction.UpdateForwardNavigationStateAction @@ -21,13 +18,10 @@ import mozilla.components.browser.state.action.ContentAction.UpdateLoadingStateA import mozilla.components.browser.state.action.ContentAction.UpdateProgressAction import mozilla.components.browser.state.action.ContentAction.UpdateSearchTermsAction import mozilla.components.browser.state.action.ContentAction.UpdateSecurityInfoAction -import mozilla.components.browser.state.action.ContentAction.UpdateThumbnailAction import mozilla.components.browser.state.action.ContentAction.UpdateTitleAction import mozilla.components.browser.state.action.ContentAction.UpdateUrlAction import mozilla.components.browser.state.action.ContentAction.UpdateWebAppManifestAction -import mozilla.components.browser.state.action.CustomTabListAction.RemoveCustomTabAction -import mozilla.components.browser.state.action.EngineAction -import mozilla.components.browser.state.action.TabListAction.AddTabAction +import mozilla.components.browser.state.action.CustomTabListAction import mozilla.components.browser.state.action.TrackingProtectionAction import mozilla.components.browser.state.selector.findTabOrCustomTab import mozilla.components.browser.state.state.CustomTabConfig @@ -55,12 +49,6 @@ class Session( val contextId: String? = null, delegate: Observable = ObserverRegistry() ) : Observable by delegate { - /** - * Holder for keeping a reference to an engine session and its observer to update this session - * object. - */ - internal val engineSessionHolder = EngineSessionHolder() - // For migration purposes every `Session` has a reference to the `BrowserStore` (if used) in order to dispatch // actions to it when the `Session` changes. internal var store: BrowserStore? = null @@ -94,10 +82,8 @@ class Session( fun onTrackerBlocked(session: Session, tracker: Tracker, all: List) = Unit fun onTrackerLoaded(session: Session, tracker: Tracker, all: List) = Unit fun onDesktopModeChanged(session: Session, enabled: Boolean) = Unit - fun onThumbnailChanged(session: Session, bitmap: Bitmap?) = Unit fun onContentPermissionRequested(session: Session, permissionRequest: PermissionRequest): Boolean = false fun onAppPermissionRequested(session: Session, permissionRequest: PermissionRequest): Boolean = false - fun onCrashStateChanged(session: Session, crashed: Boolean) = Unit fun onRecordingDevicesChanged(session: Session, devices: List) = Unit fun onLaunchIntentRequest(session: Session, url: String, appIntent: Intent?) = Unit } @@ -218,11 +204,9 @@ class Session( // tabs to regular tabs, so we have to dispatch the corresponding // browser actions to keep the store in sync. if (old != new && new == null) { - store?.syncDispatch(RemoveCustomTabAction(id)) - store?.syncDispatch(AddTabAction(toTabSessionState())) - engineSessionHolder.engineSession?.let { engineSession -> - store?.syncDispatch(EngineAction.LinkEngineSessionAction(id, engineSession)) - } + store?.syncDispatch( + CustomTabListAction.TurnCustomTabIntoNormalTabAction(id) + ) } } @@ -293,16 +277,6 @@ class Session( } } - /** - * The target of the latest thumbnail. - */ - var thumbnail: Bitmap? by Delegates.observable(null) { _, _, new -> - notifyObservers { onThumbnailChanged(this@Session, new) } - - val action = if (new != null) UpdateThumbnailAction(id, new) else RemoveThumbnailAction(id) - store?.syncDispatch(action) - } - /** * Desktop Mode state, true if the desktop mode is requested, otherwise false. */ @@ -341,19 +315,6 @@ class Session( !request.consumeBy(consumers) } - /** - * Whether this [Session] has crashed. - * - * In conjunction with a `concept-engine` implementation that uses a multi-process architecture, single sessions - * can crash without crashing the whole app. - * - * A crashed session may still be operational (since the underlying engine implementation has recovered its content - * process), but further action may be needed to restore the last state before the session has crashed (if desired). - */ - var crashed: Boolean by Delegates.observable(false) { _, old, new -> - notifyObservers(old, new) { onCrashStateChanged(this@Session, new) } - } - /** * List of recording devices (e.g. camera or microphone) currently in use by web content. */ diff --git a/components/browser/session/src/main/java/mozilla/components/browser/session/SessionManager.kt b/components/browser/session/src/main/java/mozilla/components/browser/session/SessionManager.kt index b870219b534..474d457b71e 100644 --- a/components/browser/session/src/main/java/mozilla/components/browser/session/SessionManager.kt +++ b/components/browser/session/src/main/java/mozilla/components/browser/session/SessionManager.kt @@ -4,17 +4,13 @@ package mozilla.components.browser.session -import android.content.ComponentCallbacks2 -import mozilla.components.browser.session.engine.EngineObserver import mozilla.components.browser.session.ext.syncDispatch import mozilla.components.browser.session.ext.toCustomTabSessionState import mozilla.components.browser.session.ext.toTabSessionState import mozilla.components.browser.state.action.CustomTabListAction import mozilla.components.browser.state.action.EngineAction.LinkEngineSessionAction import mozilla.components.browser.state.action.EngineAction.UpdateEngineSessionStateAction -import mozilla.components.browser.state.action.EngineAction.UnlinkEngineSessionAction import mozilla.components.browser.state.action.ReaderAction -import mozilla.components.browser.state.action.SystemAction import mozilla.components.browser.state.action.TabListAction import mozilla.components.browser.state.selector.findTab import mozilla.components.browser.state.state.ReaderState @@ -22,11 +18,7 @@ import mozilla.components.browser.state.store.BrowserStore import mozilla.components.concept.engine.Engine import mozilla.components.concept.engine.EngineSession import mozilla.components.concept.engine.EngineSessionState -import mozilla.components.support.base.log.logger.Logger -import mozilla.components.support.base.memory.MemoryConsumer import mozilla.components.support.base.observer.Observable -import mozilla.components.support.ktx.kotlin.isExtensionUrl -import java.lang.IllegalArgumentException /** * This class provides access to a centralized registry of all active sessions. @@ -35,117 +27,25 @@ import java.lang.IllegalArgumentException class SessionManager( val engine: Engine, private val store: BrowserStore? = null, - private val linker: EngineSessionLinker = EngineSessionLinker(store), - private val delegate: LegacySessionManager = LegacySessionManager(engine, linker) -) : Observable by delegate, MemoryConsumer { - private val logger = Logger("SessionManager") - - /** - * This class only exists for migrating from browser-session - * to browser-state. We need a way to dispatch the corresponding browser - * actions when an engine session is linked and unlinked. - */ - class EngineSessionLinker(private val store: BrowserStore?) { - /** - * Links the provided [Session] and [EngineSession]. - */ - fun link( - session: Session, - engineSession: EngineSession, - parentEngineSession: EngineSession?, - sessionRestored: Boolean = false, - skipLoading: Boolean = false - ) { - unlink(session) - - session.engineSessionHolder.apply { - this.engineSession = engineSession - this.engineObserver = EngineObserver(session, store).also { observer -> - engineSession.register(observer) - if (!sessionRestored && !skipLoading) { - if (session.url.isExtensionUrl()) { - // The parent tab/session is used as a referrer which is not accurate - // for extension pages. The extension page is not loaded by the parent - // tab, but opened by an extension e.g. via browser.tabs.update. - engineSession.loadUrl(session.url) - } else { - engineSession.loadUrl(session.url, parentEngineSession) - } - } - } - } - - store?.syncDispatch(LinkEngineSessionAction(session.id, engineSession)) - } - - fun unlink(session: Session) { - val observer = session.engineSessionHolder.engineObserver - val engineSession = session.engineSessionHolder.engineSession - - if (observer != null && engineSession != null) { - engineSession.unregister(observer) - } - - session.engineSessionHolder.engineSession = null - session.engineSessionHolder.engineObserver = null - - // Note: We could consider not clearing the engine session state here. Instead we could - // try to actively set it here by calling save() on the engine session (if we have one). - // That way adding the same Session again could restore the previous state automatically - // (although we would need to make sure we do not override it with null in link()). - // Clearing the engine session state would be left to the garbage collector whenever the - // session itself gets collected. - session.engineSessionHolder.engineSessionState = null - - store?.syncDispatch(UnlinkEngineSessionAction(session.id)) - - // Now, that neither the session manager nor the store keep a reference to the engine - // session, we can close it. - engineSession?.close() - } - } - + private val delegate: LegacySessionManager = LegacySessionManager(engine) +) : Observable by delegate { /** * Returns the number of session including CustomTab sessions. */ val size: Int get() = delegate.size - /** - * Produces a snapshot of this manager's state, suitable for restoring via [SessionManager.restore]. - * Only regular sessions are included in the snapshot. Private and Custom Tab sessions are omitted. - * - * @return [Snapshot] of the current session state. - */ - fun createSnapshot(): Snapshot { - val snapshot = delegate.createSnapshot() - - // The reader state is no longer part of the browser session so we copy - // it from the store if it exists. This can be removed once browser-state - // migration is complete and the session storage uses the store instead - // of the session manager. - return snapshot.copy( - sessions = snapshot.sessions.map { item -> - store?.state?.findTab(item.session.id)?.let { - item.copy(readerState = it.readerState) - } ?: item - } - ) - } - /** * Produces a [Snapshot.Item] of a single [Session], suitable for restoring via [SessionManager.restore]. */ fun createSessionSnapshot(session: Session): Snapshot.Item { - val item = delegate.createSessionSnapshot(session) - - // The reader state is no longer part of the browser session so we copy - // it from the store if it exists. This can be removed once browser-state - // migration is complete and the session storage uses the store instead - // of the session manager. - return store?.state?.findTab(item.session.id)?.let { - item.copy(readerState = it.readerState) - } ?: item + val tab = store?.state?.findTab(session.id) + + return Snapshot.Item( + session, + tab?.engineState?.engineSession, + tab?.engineState?.engineSessionState, + tab?.readerState) } /** @@ -216,6 +116,16 @@ class SessionManager( ) } + delegate.add(session, selected, parent) + + if (engineSession != null) { + store?.syncDispatch(LinkEngineSessionAction( + session.id, + engineSession, + skipLoading = true + )) + } + if (engineSessionState != null && engineSession == null) { // If the caller passed us an engine session state then also notify the store. We only // do this if there is no engine session, because this mirrors the behavior in @@ -225,8 +135,6 @@ class SessionManager( engineSessionState )) } - - delegate.add(session, selected, engineSession, engineSessionState, parent) } /** @@ -304,7 +212,13 @@ class SessionManager( items.forEach { item -> item.engineSession?.let { store?.syncDispatch(LinkEngineSessionAction(item.session.id, it)) } - item.engineSessionState?.let { store?.syncDispatch(UpdateEngineSessionStateAction(item.session.id, it)) } + + if (item.engineSession == null) { + item.engineSessionState?.let { + store?.syncDispatch(UpdateEngineSessionStateAction(item.session.id, it)) + } + } + item.readerState?.let { store?.syncDispatch(ReaderAction.UpdateReaderActiveAction(item.session.id, it.active)) it.activeUrl?.let { activeUrl -> @@ -314,22 +228,6 @@ class SessionManager( } } - /** - * Gets the linked engine session for the provided session (if it exists). - */ - @Deprecated("Read EngineSession from BrowserStore instead") - fun getEngineSession(session: Session = selectedSessionOrThrow) = delegate.getEngineSession(session) - - /** - * Gets the linked engine session for the provided session and creates it if needed. - */ - fun getOrCreateEngineSession( - session: Session = selectedSessionOrThrow, - skipLoading: Boolean = false - ): EngineSession { - return delegate.getOrCreateEngineSession(session, skipLoading) - } - /** * Removes the provided session. If no session is provided then the selected session is removed. */ @@ -390,62 +288,6 @@ class SessionManager( */ fun findSessionById(id: String) = delegate.findSessionById(id) - /** - * Informs this [SessionManager] that the OS is in low memory condition so it - * can reduce its allocated objects. - */ - @Deprecated("Use onTrimMemory instead.", replaceWith = ReplaceWith("onTrimMemory")) - fun onLowMemory() { - onTrimMemory(ComponentCallbacks2.TRIM_MEMORY_MODERATE) - } - - @Synchronized - @Suppress("NestedBlockDepth") - override fun onTrimMemory(level: Int) { - val clearThumbnails = shouldClearThumbnails(level) - val closeEngineSessions = shouldCloseEngineSessions(level) - - logger.debug("onTrimMemory($level): clearThumbnails=$clearThumbnails, closeEngineSessions=$closeEngineSessions") - - if (!clearThumbnails && !closeEngineSessions) { - // Nothing to do for now. - return - } - - val selectedSession = selectedSession - val states = mutableMapOf() - val sessions = sessions - - var clearedThumbnails = 0 - var unlinkedEngineSessions = 0 - - sessions.forEach { - if (it != selectedSession) { - if (clearThumbnails) { - it.thumbnail = null - clearedThumbnails++ - } - - if (closeEngineSessions) { - val engineSession = it.engineSessionHolder.engineSession - if (engineSession != null) { - val state = engineSession.saveState() - linker.unlink(it) - - it.engineSessionHolder.engineSessionState = state - states[it.id] = state - - unlinkedEngineSessions++ - } - } - } - } - - logger.debug("Cleared $clearedThumbnails thumbnail(s) and unlinked $unlinkedEngineSessions engine sessions(s)") - - store?.syncDispatch(SystemAction.LowMemoryAction(states)) - } - companion object { const val NO_SELECTION = -1 } @@ -542,40 +384,3 @@ fun SessionManager.runWithSessionIdOrSelected( return false } - -private fun shouldClearThumbnails(level: Int): Boolean { - return when (level) { - // Foreground: The device is running much lower on memory. The app is running and not killable, but the - // system wants us to release unused resources to improve system performance. - ComponentCallbacks2.TRIM_MEMORY_RUNNING_LOW, - // Foreground: The device is running extremely low on memory. The app is not yet considered a killable - // process, but the system will begin killing background processes if apps do not release resources. - ComponentCallbacks2.TRIM_MEMORY_RUNNING_CRITICAL -> true - - // Background: The system is running low on memory and our process is near the middle of the LRU list. - // If the system becomes further constrained for memory, there's a chance our process will be killed. - ComponentCallbacks2.TRIM_MEMORY_MODERATE, - // Background: The system is running low on memory and our process is one of the first to be killed - // if the system does not recover memory now. - ComponentCallbacks2.TRIM_MEMORY_COMPLETE -> true - - else -> false - } -} - -private fun shouldCloseEngineSessions(level: Int): Boolean { - return when (level) { - // Foreground: The device is running extremely low on memory. The app is not yet considered a killable - // process, but the system will begin killing background processes if apps do not release resources. - ComponentCallbacks2.TRIM_MEMORY_RUNNING_CRITICAL -> true - - // Background: The system is running low on memory and our process is near the middle of the LRU list. - // If the system becomes further constrained for memory, there's a chance our process will be killed. - ComponentCallbacks2.TRIM_MEMORY_MODERATE, - // Background: The system is running low on memory and our process is one of the first to be killed - // if the system does not recover memory now. - ComponentCallbacks2.TRIM_MEMORY_COMPLETE -> true - - else -> false - } -} diff --git a/components/browser/session/src/main/java/mozilla/components/browser/session/engine/EngineMiddleware.kt b/components/browser/session/src/main/java/mozilla/components/browser/session/engine/EngineMiddleware.kt new file mode 100644 index 00000000000..ae9436a3f54 --- /dev/null +++ b/components/browser/session/src/main/java/mozilla/components/browser/session/engine/EngineMiddleware.kt @@ -0,0 +1,121 @@ +/* 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.session.engine + +import androidx.annotation.MainThread +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.MainScope +import mozilla.components.browser.session.Session +import mozilla.components.browser.session.engine.middleware.CrashMiddleware +import mozilla.components.browser.session.engine.middleware.CreateEngineSessionMiddleware +import mozilla.components.browser.session.engine.middleware.EngineDelegateMiddleware +import mozilla.components.browser.session.engine.middleware.LinkingMiddleware +import mozilla.components.browser.session.engine.middleware.SuspendMiddleware +import mozilla.components.browser.session.engine.middleware.TabsRemovedMiddleware +import mozilla.components.browser.session.engine.middleware.TrimMemoryMiddleware +import mozilla.components.browser.session.engine.middleware.WebExtensionMiddleware +import mozilla.components.browser.state.action.BrowserAction +import mozilla.components.browser.state.action.EngineAction +import mozilla.components.browser.state.selector.findTabOrCustomTab +import mozilla.components.browser.state.state.BrowserState +import mozilla.components.browser.state.state.SessionState +import mozilla.components.browser.state.store.BrowserStore +import mozilla.components.concept.engine.Engine +import mozilla.components.concept.engine.EngineSession +import mozilla.components.lib.state.Middleware +import mozilla.components.lib.state.MiddlewareStore +import mozilla.components.support.base.log.logger.Logger + +/** + * Helper for creating a list of [Middleware] instances for supporting all [EngineAction]s. + */ +object EngineMiddleware { + /** + * Creates a list of [Middleware] to be installed on a [BrowserStore] in order to support all + * [EngineAction]s. + */ + fun create( + engine: Engine, + sessionLookup: (String) -> Session?, + scope: CoroutineScope = MainScope() + ): List> { + return listOf( + EngineDelegateMiddleware( + engine, + sessionLookup, + scope + ), + CreateEngineSessionMiddleware( + engine, + sessionLookup, + scope + ), + LinkingMiddleware(sessionLookup), + TabsRemovedMiddleware(scope), + SuspendMiddleware(scope), + WebExtensionMiddleware(), + TrimMemoryMiddleware(), + CrashMiddleware( + engine, + sessionLookup, + scope + ) + ) + } +} + +@MainThread +internal fun getOrCreateEngineSession( + engine: Engine, + logger: Logger, + sessionLookup: (String) -> Session?, + store: MiddlewareStore, + tabId: String +): EngineSession? { + val tab = store.state.findTabOrCustomTab(tabId) + if (tab == null) { + logger.warn("Requested engine session for tab. But tab does not exist. ($tabId)") + return null + } + + tab.engineState.engineSession?.let { + logger.debug("Engine Session already exists for tab $tabId") + return it + } + + return createEngineSession(engine, logger, sessionLookup, store, tab) +} + +@MainThread +private fun createEngineSession( + engine: Engine, + logger: Logger, + sessionLookup: (String) -> Session?, + store: MiddlewareStore, + tab: SessionState +): EngineSession? { + val session = sessionLookup(tab.id) + if (session == null) { + logger.error("Requested creation of EngineSession without matching Session (${tab.id})") + return null + } + + val engineSession = engine.createSession(tab.content.private, tab.contextId) + logger.debug("Created engine session for tab ${tab.id}") + + val engineSessionState = tab.engineState.engineSessionState + val skipLoading = if (engineSessionState != null) { + engineSession.restoreState(engineSessionState) + true + } else { + false + } + + store.dispatch( + EngineAction.LinkEngineSessionAction(tab.id, engineSession, skipLoading = skipLoading) + ) + + return engineSession +} diff --git a/components/browser/session/src/main/java/mozilla/components/browser/session/engine/EngineObserver.kt b/components/browser/session/src/main/java/mozilla/components/browser/session/engine/EngineObserver.kt index b51c479abd5..4525e76a85b 100644 --- a/components/browser/session/src/main/java/mozilla/components/browser/session/engine/EngineObserver.kt +++ b/components/browser/session/src/main/java/mozilla/components/browser/session/engine/EngineObserver.kt @@ -13,15 +13,16 @@ import mozilla.components.browser.session.Session import mozilla.components.browser.session.engine.request.LaunchIntentMetadata import mozilla.components.browser.session.engine.request.LoadRequestMetadata import mozilla.components.browser.session.engine.request.LoadRequestOption -import mozilla.components.browser.session.ext.syncDispatch import mozilla.components.browser.session.ext.toElement +import mozilla.components.browser.state.action.BrowserAction import mozilla.components.browser.state.action.ContentAction +import mozilla.components.browser.state.action.CrashAction import mozilla.components.browser.state.action.MediaAction import mozilla.components.browser.state.action.TrackingProtectionAction +import mozilla.components.browser.state.state.BrowserState import mozilla.components.browser.state.state.content.DownloadState import mozilla.components.browser.state.state.content.FindResultState import mozilla.components.browser.state.state.content.DownloadState.Status.INITIATED -import mozilla.components.browser.state.store.BrowserStore import mozilla.components.concept.engine.EngineSession import mozilla.components.concept.engine.HitResult import mozilla.components.concept.engine.content.blocking.Tracker @@ -32,6 +33,7 @@ import mozilla.components.concept.engine.media.RecordingDevice import mozilla.components.concept.engine.permission.PermissionRequest import mozilla.components.concept.engine.prompt.PromptRequest import mozilla.components.concept.engine.window.WindowRequest +import mozilla.components.lib.state.MiddlewareStore import mozilla.components.support.base.observer.Consumable import mozilla.components.support.ktx.android.net.isInScope import mozilla.components.support.ktx.kotlin.isSameOriginAs @@ -43,7 +45,7 @@ import mozilla.components.support.ktx.kotlin.isSameOriginAs @Suppress("TooManyFunctions", "LargeClass") internal class EngineObserver( private val session: Session, - private val store: BrowserStore? = null + private val store: MiddlewareStore? ) : EngineSession.Observer { private val mediaMap: MutableMap = mutableMapOf() @@ -160,7 +162,7 @@ internal class EngineObserver( } override fun onExcludedOnTrackingProtectionChange(excluded: Boolean) { - store?.syncDispatch(TrackingProtectionAction.ToggleExclusionListAction(session.id, excluded)) + store?.dispatch(TrackingProtectionAction.ToggleExclusionListAction(session.id, excluded)) } override fun onTrackerBlockingEnabledChange(enabled: Boolean) { @@ -233,7 +235,12 @@ internal class EngineObserver( } override fun onThumbnailChange(bitmap: Bitmap?) { - session.thumbnail = bitmap + store?.dispatch(if (bitmap == null) { + ContentAction.RemoveThumbnailAction(session.id) + } else { + ContentAction.UpdateThumbnailAction(session.id, bitmap) + } + ) } override fun onContentPermissionRequest(permissionRequest: PermissionRequest) { @@ -295,7 +302,9 @@ internal class EngineObserver( } override fun onCrash() { - session.crashed = true + store?.dispatch(CrashAction.SessionCrashedAction( + session.id + )) } override fun onRecordingStateChanged(devices: List) { diff --git a/components/browser/session/src/main/java/mozilla/components/browser/session/engine/EngineSessionHolder.kt b/components/browser/session/src/main/java/mozilla/components/browser/session/engine/EngineSessionHolder.kt deleted file mode 100644 index b37a136fe52..00000000000 --- a/components/browser/session/src/main/java/mozilla/components/browser/session/engine/EngineSessionHolder.kt +++ /dev/null @@ -1,19 +0,0 @@ -/* 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.session.engine - -import mozilla.components.browser.session.Session -import mozilla.components.concept.engine.EngineSession -import mozilla.components.concept.engine.EngineSessionState - -/** - * Used for linking a [Session] to an [EngineSession] or the [EngineSessionState] to create an [EngineSession] from it. - * The attached [EngineObserver] is used to update the [Session] whenever the [EngineSession] emits events. - */ -internal class EngineSessionHolder { - @Volatile var engineSession: EngineSession? = null - @Volatile var engineObserver: EngineObserver? = null - @Volatile var engineSessionState: EngineSessionState? = null -} diff --git a/components/browser/session/src/main/java/mozilla/components/browser/session/engine/MediaObserver.kt b/components/browser/session/src/main/java/mozilla/components/browser/session/engine/MediaObserver.kt index 7774164cc85..7c28f86a6f9 100644 --- a/components/browser/session/src/main/java/mozilla/components/browser/session/engine/MediaObserver.kt +++ b/components/browser/session/src/main/java/mozilla/components/browser/session/engine/MediaObserver.kt @@ -4,10 +4,13 @@ package mozilla.components.browser.session.engine +import mozilla.components.browser.state.action.BrowserAction import mozilla.components.browser.state.action.MediaAction +import mozilla.components.browser.state.state.BrowserState import mozilla.components.browser.state.state.MediaState import mozilla.components.browser.state.store.BrowserStore import mozilla.components.concept.engine.media.Media +import mozilla.components.lib.state.MiddlewareStore /** * [Media.Observer] implementation responsible for dispatching actions updating the state in @@ -16,7 +19,7 @@ import mozilla.components.concept.engine.media.Media internal class MediaObserver( val media: Media, val element: MediaState.Element, - val store: BrowserStore, + val store: MiddlewareStore, val tabId: String ) : Media.Observer { override fun onStateChanged(media: Media, state: Media.State) { diff --git a/components/browser/session/src/main/java/mozilla/components/browser/session/engine/middleware/CrashMiddleware.kt b/components/browser/session/src/main/java/mozilla/components/browser/session/engine/middleware/CrashMiddleware.kt new file mode 100644 index 00000000000..e3c00221b1a --- /dev/null +++ b/components/browser/session/src/main/java/mozilla/components/browser/session/engine/middleware/CrashMiddleware.kt @@ -0,0 +1,57 @@ +/* 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.session.engine.middleware + +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch +import mozilla.components.browser.session.Session +import mozilla.components.browser.session.engine.getOrCreateEngineSession +import mozilla.components.browser.state.action.BrowserAction +import mozilla.components.browser.state.action.CrashAction +import mozilla.components.browser.state.selector.findTabOrCustomTab +import mozilla.components.browser.state.state.BrowserState +import mozilla.components.concept.engine.Engine +import mozilla.components.concept.engine.EngineSession +import mozilla.components.lib.state.Middleware +import mozilla.components.lib.state.MiddlewareStore +import mozilla.components.support.base.log.logger.Logger + +/** + * [Middleware] responsible for recovering crashed [EngineSession] instances. + */ +internal class CrashMiddleware( + private val engine: Engine, + private val sessionLookup: (String) -> Session?, + private val scope: CoroutineScope +) : Middleware { + private val logger = Logger("CrashMiddleware") + + override fun invoke( + store: MiddlewareStore, + next: (BrowserAction) -> Unit, + action: BrowserAction + ) { + if (action is CrashAction.RestoreCrashedSessionAction) { + restore(store, action) + } + + next(action) + } + + private fun restore( + store: MiddlewareStore, + action: CrashAction.RestoreCrashedSessionAction + ) = scope.launch { + val tab = store.state.findTabOrCustomTab(action.tabId) ?: return@launch + + // Currently we are forcing the creation of an engine session here. This is mimicing + // the previous behavior. But it is questionable if this is the right approach: + // - How did this tab crash if it does not have an engine session? + // - Instead of creating the engine session, could we turn it into a suspended + // session with the "crash state" as the last state? + val engineSession = getOrCreateEngineSession(engine, logger, sessionLookup, store, tab.id) + engineSession?.recoverFromCrash() + } +} diff --git a/components/browser/session/src/main/java/mozilla/components/browser/session/engine/middleware/CreateEngineSessionMiddleware.kt b/components/browser/session/src/main/java/mozilla/components/browser/session/engine/middleware/CreateEngineSessionMiddleware.kt new file mode 100644 index 00000000000..26336e05f6d --- /dev/null +++ b/components/browser/session/src/main/java/mozilla/components/browser/session/engine/middleware/CreateEngineSessionMiddleware.kt @@ -0,0 +1,61 @@ +/* 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.session.engine.middleware + +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch +import mozilla.components.browser.session.Session +import mozilla.components.browser.session.engine.getOrCreateEngineSession +import mozilla.components.browser.state.action.BrowserAction +import mozilla.components.browser.state.action.EngineAction +import mozilla.components.browser.state.state.BrowserState +import mozilla.components.concept.engine.Engine +import mozilla.components.concept.engine.EngineSession +import mozilla.components.lib.state.Middleware +import mozilla.components.lib.state.MiddlewareStore +import mozilla.components.support.base.log.logger.Logger + +/** + * [Middleware] responsible for creating [EngineSession] instances whenever an [EngineAction.CreateEngineSessionAction] + * is getting dispatched. + */ +internal class CreateEngineSessionMiddleware( + private val engine: Engine, + private val sessionLookup: (String) -> Session?, + private val scope: CoroutineScope +) : Middleware { + private val logger = Logger("CreateEngineSessionMiddleware") + + override fun invoke( + store: MiddlewareStore, + next: (BrowserAction) -> Unit, + action: BrowserAction + ) { + if (action is EngineAction.CreateEngineSessionAction) { + createEngineSession(store, action) + } else { + next(action) + } + } + + private fun createEngineSession( + store: MiddlewareStore, + action: EngineAction.CreateEngineSessionAction + ) { + logger.debug("Request to create engine session for tab ${action.tabId}") + + scope.launch { + // We only need to ask for an EngineSession here. If needed this method will internally + // create one and dispatch a LinkEngineSessionAction to add it to BrowserState. + getOrCreateEngineSession( + engine, + logger, + sessionLookup, + store, + action.tabId + ) + } + } +} diff --git a/components/browser/session/src/main/java/mozilla/components/browser/session/engine/middleware/EngineDelegateMiddleware.kt b/components/browser/session/src/main/java/mozilla/components/browser/session/engine/middleware/EngineDelegateMiddleware.kt new file mode 100644 index 00000000000..43b3b4093bf --- /dev/null +++ b/components/browser/session/src/main/java/mozilla/components/browser/session/engine/middleware/EngineDelegateMiddleware.kt @@ -0,0 +1,162 @@ +/* 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.session.engine.middleware + +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch +import mozilla.components.browser.session.Session +import mozilla.components.browser.session.engine.getOrCreateEngineSession +import mozilla.components.browser.state.action.BrowserAction +import mozilla.components.browser.state.action.EngineAction +import mozilla.components.browser.state.selector.findTabOrCustomTab +import mozilla.components.browser.state.state.BrowserState +import mozilla.components.browser.state.state.TabSessionState +import mozilla.components.concept.engine.Engine +import mozilla.components.concept.engine.EngineSession +import mozilla.components.lib.state.Middleware +import mozilla.components.lib.state.MiddlewareStore +import mozilla.components.support.base.log.logger.Logger + +/** + * [Middleware] responsible for delegating calls to the appropriate [EngineSession] instance for + * actions like [EngineAction.LoadUrlAction]. + */ +internal class EngineDelegateMiddleware( + private val engine: Engine, + private val sessionLookup: (String) -> Session?, + private val scope: CoroutineScope +) : Middleware { + private val logger = Logger("EngineSessionMiddleware") + + @Suppress("LongMethod") + override fun invoke( + store: MiddlewareStore, + next: (BrowserAction) -> Unit, + action: BrowserAction + ) { + when (action) { + is EngineAction.LoadUrlAction -> scope.launch { + val tab = store.state.findTabOrCustomTab(action.sessionId) ?: return@launch + val parentEngineSession = if (tab is TabSessionState) { + tab.parentId?.let { store.state.findTabOrCustomTab(it)?.engineState?.engineSession } + } else { + null + } + + if (tab.engineState.engineSession == null && tab.content.url == action.url) { + // This tab does not have an engine session and we are asked to load the URL this + // session is already pointing to. Creating an EngineSession will do exactly + // that in the linking step. So let's do that. Otherwise we would load the URL + // twice. + store.dispatch(EngineAction.CreateEngineSessionAction(action.sessionId)) + return@launch + } + + getOrCreateEngineSession( + engine, + logger, + sessionLookup, + store, + tabId = action.sessionId + )?.loadUrl( + url = action.url, + parent = parentEngineSession, + flags = action.flags, + additionalHeaders = action.additionalHeaders + ) + } + + is EngineAction.LoadDataAction -> scope.launch { + val engineSession = getOrCreateEngineSession( + engine, + logger, + sessionLookup, + store, + tabId = action.sessionId + ) + engineSession?.loadData(action.data, action.mimeType, action.encoding) + } + + is EngineAction.ReloadAction -> scope.launch { + val engineSession = getOrCreateEngineSession( + engine, + logger, + sessionLookup, + store, + tabId = action.sessionId + ) + engineSession?.reload(action.flags) + } + + is EngineAction.GoBackAction -> scope.launch { + val engineSession = getOrCreateEngineSession( + engine, + logger, + sessionLookup, + store, + tabId = action.sessionId + ) + engineSession?.goBack() + } + + is EngineAction.GoForwardAction -> scope.launch { + val engineSession = getOrCreateEngineSession( + engine, + logger, + sessionLookup, + store, + tabId = action.sessionId + ) + engineSession?.goForward() + } + + is EngineAction.GoToHistoryIndexAction -> scope.launch { + val engineSession = getOrCreateEngineSession( + engine, + logger, + sessionLookup, + store, + tabId = action.sessionId + ) + engineSession?.goToHistoryIndex(action.index) + } + + is EngineAction.ToggleDesktopModeAction -> scope.launch { + val engineSession = getOrCreateEngineSession( + engine, + logger, + sessionLookup, + store, + tabId = action.sessionId + ) + engineSession?.toggleDesktopMode(action.enable, reload = true) + } + + is EngineAction.ExitFullscreenModeAction -> scope.launch { + val engineSession = getOrCreateEngineSession( + engine, + logger, + sessionLookup, + store, + tabId = action.sessionId + ) + engineSession?.exitFullScreenMode() + } + + is EngineAction.ClearDataAction -> scope.launch { + val engineSession = getOrCreateEngineSession( + engine, + logger, + sessionLookup, + store, + tabId = action.sessionId + ) + engineSession?.clearData(action.data) + } + + else -> next(action) + } + } +} diff --git a/components/browser/session/src/main/java/mozilla/components/browser/session/engine/middleware/LinkingMiddleware.kt b/components/browser/session/src/main/java/mozilla/components/browser/session/engine/middleware/LinkingMiddleware.kt new file mode 100644 index 00000000000..9ec60526d27 --- /dev/null +++ b/components/browser/session/src/main/java/mozilla/components/browser/session/engine/middleware/LinkingMiddleware.kt @@ -0,0 +1,83 @@ +/* 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.session.engine.middleware + +import mozilla.components.browser.session.Session +import mozilla.components.browser.session.engine.EngineObserver +import mozilla.components.browser.state.action.BrowserAction +import mozilla.components.browser.state.action.EngineAction +import mozilla.components.browser.state.selector.findTabOrCustomTab +import mozilla.components.browser.state.state.BrowserState +import mozilla.components.browser.state.state.TabSessionState +import mozilla.components.lib.state.Middleware +import mozilla.components.lib.state.MiddlewareStore +import mozilla.components.support.ktx.kotlin.isExtensionUrl + +/** + * [Middleware] that handles side-effects of linking a session to an engine session. + */ +internal class LinkingMiddleware( + private val sessionLookup: (String) -> Session? +) : Middleware { + override fun invoke( + store: MiddlewareStore, + next: (BrowserAction) -> Unit, + action: BrowserAction + ) { + if (action is EngineAction.UnlinkEngineSessionAction) { + unlink(store, action) + } + + next(action) + + if (action is EngineAction.LinkEngineSessionAction) { + link(store, action) + } + } + + private fun link( + store: MiddlewareStore, + action: EngineAction.LinkEngineSessionAction + ) { + val tab = store.state.findTabOrCustomTab(action.sessionId) ?: return + + val session = sessionLookup(action.sessionId) + if (session != null) { + val observer = EngineObserver(session, store) + action.engineSession.register(observer) + store.dispatch(EngineAction.UpdateEngineSessionObserverAction(session.id, observer)) + } + + if (action.skipLoading) { + return + } + + if (tab.content.url.isExtensionUrl()) { + // The parent tab/session is used as a referrer which is not accurate + // for extension pages. The extension page is not loaded by the parent + // tab, but opened by an extension e.g. via browser.tabs.update. + action.engineSession.loadUrl(tab.content.url) + } else { + val parentEngineSession = if (tab is TabSessionState) { + tab.parentId?.let { store.state.findTabOrCustomTab(it)?.engineState?.engineSession } + } else { + null + } + + action.engineSession.loadUrl(tab.content.url, parent = parentEngineSession) + } + } + + private fun unlink( + store: MiddlewareStore, + action: EngineAction.UnlinkEngineSessionAction + ) { + val tab = store.state.findTabOrCustomTab(action.sessionId) ?: return + + tab.engineState.engineObserver?.let { + tab.engineState.engineSession?.unregister(it) + } + } +} diff --git a/components/browser/session/src/main/java/mozilla/components/browser/session/engine/middleware/SuspendMiddleware.kt b/components/browser/session/src/main/java/mozilla/components/browser/session/engine/middleware/SuspendMiddleware.kt new file mode 100644 index 00000000000..2e804b8de89 --- /dev/null +++ b/components/browser/session/src/main/java/mozilla/components/browser/session/engine/middleware/SuspendMiddleware.kt @@ -0,0 +1,71 @@ +/* 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.session.engine.middleware + +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch +import mozilla.components.browser.state.action.BrowserAction +import mozilla.components.browser.state.action.EngineAction +import mozilla.components.browser.state.selector.findTab +import mozilla.components.browser.state.state.BrowserState +import mozilla.components.browser.state.state.EngineState +import mozilla.components.concept.engine.EngineSession +import mozilla.components.concept.engine.EngineSessionState +import mozilla.components.lib.state.Middleware +import mozilla.components.lib.state.MiddlewareStore + +/** + * [Middleware] implementation responsible for suspending an [EngineSession]. + * + * Suspending an [EngineSession] means that we will take the last [EngineSessionState], attach that + * to [EngineState] and then clear the [EngineSession] reference and close it. The next time we + * need an [EngineSession] for this tab we will create a new instance and restore the attached + * [EngineSessionState]. + */ +internal class SuspendMiddleware( + private val scope: CoroutineScope +) : Middleware { + override fun invoke( + store: MiddlewareStore, + next: (BrowserAction) -> Unit, + action: BrowserAction + ) { + if (action is EngineAction.SuspendEngineSessionAction) { + suspend(store, action) + } else { + next(action) + } + } + + private fun suspend( + store: MiddlewareStore, + action: EngineAction.SuspendEngineSessionAction + ) { + val tab = store.state.findTab(action.sessionId) + val state = tab?.engineState?.engineSession?.saveState() + + if (tab == null || state == null) { + // If we can't find this tab or if there's no state for this tab then there's nothing + // to do here. + return + } + + // First we unlink (which clearsEngineSession and state) + store.dispatch(EngineAction.UnlinkEngineSessionAction( + tab.id + )) + + // Then we attach the saved state to it. + store.dispatch(EngineAction.UpdateEngineSessionStateAction( + tab.id, + state + )) + + // Now we can close the unlinked EngineSession (on the main thread). + scope.launch { + tab.engineState.engineSession?.close() + } + } +} diff --git a/components/browser/session/src/main/java/mozilla/components/browser/session/engine/middleware/TabsRemovedMiddleware.kt b/components/browser/session/src/main/java/mozilla/components/browser/session/engine/middleware/TabsRemovedMiddleware.kt new file mode 100644 index 00000000000..df0e0342840 --- /dev/null +++ b/components/browser/session/src/main/java/mozilla/components/browser/session/engine/middleware/TabsRemovedMiddleware.kt @@ -0,0 +1,67 @@ +/* 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.session.engine.middleware + +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch +import mozilla.components.browser.state.action.BrowserAction +import mozilla.components.browser.state.action.CustomTabListAction +import mozilla.components.browser.state.action.EngineAction +import mozilla.components.browser.state.action.TabListAction +import mozilla.components.browser.state.selector.findCustomTab +import mozilla.components.browser.state.selector.findTab +import mozilla.components.browser.state.selector.normalTabs +import mozilla.components.browser.state.selector.privateTabs +import mozilla.components.browser.state.state.BrowserState +import mozilla.components.browser.state.state.SessionState +import mozilla.components.concept.engine.EngineSession +import mozilla.components.lib.state.Middleware +import mozilla.components.lib.state.MiddlewareStore + +/** + * [Middleware] responsible for closing and unlinking [EngineSession] instances whenever tabs get + * removed. + */ +internal class TabsRemovedMiddleware( + private val scope: CoroutineScope +) : Middleware { + override fun invoke( + store: MiddlewareStore, + next: (BrowserAction) -> Unit, + action: BrowserAction + ) { + when (action) { + is TabListAction.RemoveAllNormalTabsAction -> onTabsRemoved(store, store.state.normalTabs) + is TabListAction.RemoveAllPrivateTabsAction -> onTabsRemoved(store, store.state.privateTabs) + is TabListAction.RemoveAllTabsAction -> onTabsRemoved(store, store.state.tabs) + is TabListAction.RemoveTabAction -> store.state.findTab(action.tabId)?.let { + onTabsRemoved(store, listOf(it)) + } + is CustomTabListAction.RemoveAllCustomTabsAction -> onTabsRemoved(store, store.state.customTabs) + is CustomTabListAction.RemoveCustomTabAction -> store.state.findCustomTab(action.tabId)?.let { + onTabsRemoved(store, listOf(it)) + } + } + + next(action) + } + + private fun onTabsRemoved( + store: MiddlewareStore, + tabs: List + ) { + tabs.forEach { tab -> + if (tab.engineState.engineSession != null) { + store.dispatch( + EngineAction.UnlinkEngineSessionAction( + tab.id + )) + scope.launch { + tab.engineState.engineSession?.close() + } + } + } + } +} diff --git a/components/browser/session/src/main/java/mozilla/components/browser/session/engine/middleware/TrimMemoryMiddleware.kt b/components/browser/session/src/main/java/mozilla/components/browser/session/engine/middleware/TrimMemoryMiddleware.kt new file mode 100644 index 00000000000..3385d3b55b6 --- /dev/null +++ b/components/browser/session/src/main/java/mozilla/components/browser/session/engine/middleware/TrimMemoryMiddleware.kt @@ -0,0 +1,66 @@ +/* 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.session.engine.middleware + +import android.content.ComponentCallbacks2 +import mozilla.components.browser.state.action.BrowserAction +import mozilla.components.browser.state.action.EngineAction +import mozilla.components.browser.state.action.SystemAction +import mozilla.components.browser.state.state.BrowserState +import mozilla.components.lib.state.Middleware +import mozilla.components.lib.state.MiddlewareStore + +/** + * [Middleware] responsible for suspending [EngineSession] instances on low memory. + */ +internal class TrimMemoryMiddleware : Middleware { + override fun invoke( + store: MiddlewareStore, + next: (BrowserAction) -> Unit, + action: BrowserAction + ) { + next(action) + + if (action is SystemAction.LowMemoryAction) { + trimMemory(store, action) + } + } + + private fun trimMemory( + store: MiddlewareStore, + action: SystemAction.LowMemoryAction + ) { + if (!shouldCloseEngineSessions(action.level)) { + return + } + + // This is not the most efficient way of doing this. We are looping over all tabs and then + // dispatching a SuspendEngineSessionAction for each tab that is no longer needed. + (store.state.tabs + store.state.customTabs).forEach { tab -> + if (tab.id != store.state.selectedTabId) { + store.dispatch( + EngineAction.SuspendEngineSessionAction(tab.id) + ) + } + } + } +} + +private fun shouldCloseEngineSessions(level: Int): Boolean { + return when (level) { + // Foreground: The device is running extremely low on memory. The app is not yet considered a killable + // process, but the system will begin killing background processes if apps do not release resources. + ComponentCallbacks2.TRIM_MEMORY_RUNNING_CRITICAL -> true + + // Background: The system is running low on memory and our process is near the middle of the LRU list. + // If the system becomes further constrained for memory, there's a chance our process will be killed. + ComponentCallbacks2.TRIM_MEMORY_MODERATE, + // Background: The system is running low on memory and our process is one of the first to be killed + // if the system does not recover memory now. + ComponentCallbacks2.TRIM_MEMORY_COMPLETE -> true + + else -> false + } +} diff --git a/components/browser/session/src/main/java/mozilla/components/browser/session/engine/middleware/WebExtensionMiddleware.kt b/components/browser/session/src/main/java/mozilla/components/browser/session/engine/middleware/WebExtensionMiddleware.kt new file mode 100644 index 00000000000..9bf939bf818 --- /dev/null +++ b/components/browser/session/src/main/java/mozilla/components/browser/session/engine/middleware/WebExtensionMiddleware.kt @@ -0,0 +1,75 @@ +/* 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.session.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.lib.state.Middleware +import mozilla.components.lib.state.MiddlewareStore +import mozilla.components.support.base.log.logger.Logger + +/** + * [Middleware] implementation responsible for calling [EngineSession.markActiveForWebExtensions] on + * [EngineSession] instances. + */ +internal class WebExtensionMiddleware : Middleware { + private val logger = Logger("WebExtensionsMiddleware") + // This is state. As such it should be in BrowserState (WebExtensionState) and not here. + // https://github.com/mozilla-mobile/android-components/issues/7884 + @VisibleForTesting + internal var activeWebExtensionTabId: String? = null + + override fun invoke( + store: MiddlewareStore, + next: (BrowserAction) -> Unit, + action: BrowserAction + ) { + + when (action) { + is EngineAction.UnlinkEngineSessionAction -> { + if (activeWebExtensionTabId == action.sessionId) { + val activeTab = store.state.findTab(action.sessionId) + activeTab?.engineState?.engineSession?.markActiveForWebExtensions(false) + } + } + } + + next(action) + + when (action) { + is TabListAction, + is EngineAction.LinkEngineSessionAction -> { + switchActiveStateIfNeeded(store.state) + } + } + } + + private fun switchActiveStateIfNeeded(state: BrowserState) { + if (activeWebExtensionTabId == state.selectedTabId) { + return + } + + val previousActiveTab = activeWebExtensionTabId?.let { state.findTab(it) } + previousActiveTab?.engineState?.engineSession?.markActiveForWebExtensions(false) + + val nextActiveTab = state.selectedTabId?.let { state.findTab(it) } + val engineSession = nextActiveTab?.engineState?.engineSession + + if (engineSession == null) { + logger.debug("No engine session for new active tab (${nextActiveTab?.id})") + activeWebExtensionTabId = null + return + } else { + logger.debug("New active tab (${nextActiveTab.id})") + engineSession.markActiveForWebExtensions(true) + activeWebExtensionTabId = nextActiveTab.id + } + } +} diff --git a/components/browser/session/src/main/java/mozilla/components/browser/session/ext/AtomicFile.kt b/components/browser/session/src/main/java/mozilla/components/browser/session/ext/AtomicFile.kt index 84c87874911..b2705767f2b 100644 --- a/components/browser/session/src/main/java/mozilla/components/browser/session/ext/AtomicFile.kt +++ b/components/browser/session/src/main/java/mozilla/components/browser/session/ext/AtomicFile.kt @@ -6,7 +6,9 @@ package mozilla.components.browser.session.ext import android.util.AtomicFile import mozilla.components.browser.session.SessionManager +import mozilla.components.browser.session.storage.BrowserStateSerializer import mozilla.components.browser.session.storage.SnapshotSerializer +import mozilla.components.browser.state.state.BrowserState import mozilla.components.concept.engine.Engine import mozilla.components.support.ktx.util.readAndDeserialize import mozilla.components.support.ktx.util.writeString @@ -33,11 +35,11 @@ fun AtomicFile.readSnapshot( /** * Saves the given [SessionManager.Snapshot] to this [AtomicFile]. */ -fun AtomicFile.writeSnapshot( - snapshot: SessionManager.Snapshot, - serializer: SnapshotSerializer = SnapshotSerializer() +fun AtomicFile.writeState( + state: BrowserState, + serializer: BrowserStateSerializer = BrowserStateSerializer() ): Boolean { - return writeString { serializer.toJSON(snapshot) } + return writeString { serializer.toJSON(state) } } /** diff --git a/components/browser/session/src/main/java/mozilla/components/browser/session/ext/SessionExtensions.kt b/components/browser/session/src/main/java/mozilla/components/browser/session/ext/SessionExtensions.kt index 9f2010d1005..85587b52400 100644 --- a/components/browser/session/src/main/java/mozilla/components/browser/session/ext/SessionExtensions.kt +++ b/components/browser/session/src/main/java/mozilla/components/browser/session/ext/SessionExtensions.kt @@ -48,8 +48,7 @@ private fun Session.toContentState(): ContentState { progress, loading, searchTerms, - securityInfo.toSecurityInfoState(), - thumbnail + securityInfo.toSecurityInfoState() ) } diff --git a/components/browser/session/src/main/java/mozilla/components/browser/session/storage/AutoSave.kt b/components/browser/session/src/main/java/mozilla/components/browser/session/storage/AutoSave.kt index d5723107ffa..0aa4b627288 100644 --- a/components/browser/session/src/main/java/mozilla/components/browser/session/storage/AutoSave.kt +++ b/components/browser/session/src/main/java/mozilla/components/browser/session/storage/AutoSave.kt @@ -10,27 +10,40 @@ import androidx.lifecycle.Lifecycle import androidx.lifecycle.LifecycleObserver import androidx.lifecycle.OnLifecycleEvent import androidx.lifecycle.ProcessLifecycleOwner +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.Job import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch -import mozilla.components.browser.session.SelectionAwareSessionObserver -import mozilla.components.browser.session.Session -import mozilla.components.browser.session.SessionManager +import mozilla.components.browser.state.selector.normalTabs +import mozilla.components.browser.state.selector.selectedTab +import mozilla.components.browser.state.state.BrowserState +import mozilla.components.browser.state.store.BrowserStore +import mozilla.components.lib.state.ext.flow import mozilla.components.support.base.log.logger.Logger +import mozilla.components.support.ktx.kotlinx.coroutines.flow.ifChanged import java.util.concurrent.Executors import java.util.concurrent.ScheduledExecutorService import java.util.concurrent.ScheduledFuture import java.util.concurrent.TimeUnit class AutoSave( - private val sessionManager: SessionManager, + private val store: BrowserStore, private val sessionStorage: Storage, private val minimumIntervalMs: Long ) { interface Storage { - fun save(snapshot: SessionManager.Snapshot): Boolean + /** + * Saves the provided [BrowserState]. + * + * @param state the state to save. + * @return true if save was successful, otherwise false. + */ + fun save(state: BrowserState): Boolean } internal val logger = Logger("SessionStorage/AutoSave") @@ -73,11 +86,13 @@ class AutoSave( /** * Saves the state automatically when the sessions change, e.g. sessions get added and removed. */ - fun whenSessionsChange(): AutoSave { - AutoSaveSessionChange( - this, - sessionManager - ).observeSelected() + fun whenSessionsChange( + scope: CoroutineScope = CoroutineScope(Dispatchers.IO) + ): AutoSave { + scope.launch { + val monitoring = StateMonitoring(this@AutoSave) + monitoring.monitor(store.flow()) + } return this } @@ -111,8 +126,8 @@ class AutoSave( val start = now() try { - val snapshot = sessionManager.createSnapshot() - sessionStorage.save(snapshot) + val state = store.state + sessionStorage.save(state) } finally { val took = now() - start logger.debug("Saved state to disk [${took}ms]") @@ -176,38 +191,55 @@ private class AutoSaveBackground( } } -/** - * [SelectionAwareSessionObserver] to save the state whenever sessions change. - */ -private class AutoSaveSessionChange( - private val autoSave: AutoSave, - sessionManager: SessionManager -) : SelectionAwareSessionObserver(sessionManager) { - override fun onSessionSelected(session: Session) { - super.onSessionSelected(session) - autoSave.logger.info("Save: Session selected") - autoSave.triggerSave() +private class StateMonitoring( + private val autoSave: AutoSave +) { + private var lastObservation: Observation? = null + + suspend fun monitor(flow: Flow) { + flow + .map { state -> + Observation( + state.selectedTabId, + state.normalTabs.size, + state.selectedTab?.content?.loading + ) + } + .ifChanged() + .collect { observation -> onChange(observation) } } - override fun onLoadingStateChanged(session: Session, loading: Boolean) { - if (!loading) { + private fun onChange(observation: Observation) { + if (lastObservation == null) { + // If this is the first observation then just remember it. We only want to react to + // changes and not the initial state. + lastObservation = observation + return + } + + val triggerSave = if (lastObservation!!.selectedTabId != observation.selectedTabId) { + autoSave.logger.info("Save: New tab selected") + true + } else if (lastObservation!!.tabs != observation.tabs) { + autoSave.logger.info("Save: Number of tabs changed") + true + } else if (lastObservation!!.loading != observation.loading && observation.loading == false) { autoSave.logger.info("Save: Load finished") - autoSave.triggerSave() + true + } else { + false } - } - override fun onSessionAdded(session: Session) { - autoSave.logger.info("Save: Session added") - autoSave.triggerSave() - } + lastObservation = observation - override fun onSessionRemoved(session: Session) { - autoSave.logger.info("Save: Session removed") - autoSave.triggerSave() + if (triggerSave) { + autoSave.triggerSave() + } } - override fun onAllSessionsRemoved() { - autoSave.logger.info("Save: All sessions removed") - autoSave.triggerSave() - } + private data class Observation( + val selectedTabId: String?, + val tabs: Int, + val loading: Boolean? + ) } diff --git a/components/browser/session/src/main/java/mozilla/components/browser/session/storage/BrowserStateSerializer.kt b/components/browser/session/src/main/java/mozilla/components/browser/session/storage/BrowserStateSerializer.kt new file mode 100644 index 00000000000..f981aafa583 --- /dev/null +++ b/components/browser/session/src/main/java/mozilla/components/browser/session/storage/BrowserStateSerializer.kt @@ -0,0 +1,65 @@ +/* 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.session.storage + +import mozilla.components.browser.state.state.BrowserState +import mozilla.components.browser.state.state.TabSessionState +import org.json.JSONArray +import org.json.JSONObject + +private const val VERSION = 2 + +/** + * Helper to transform [BrowserState] instances to JSON and. + */ +class BrowserStateSerializer { + + /** + * Serializes the provided [BrowserState] to JSON. + * + * @param state the state to serialize. + * @return the serialized state as JSON. + */ + fun toJSON(state: BrowserState): String { + val json = JSONObject() + json.put(Keys.VERSION_KEY, VERSION) + json.put(Keys.SELECTED_TAB_ID_KEY, state.selectedTabId) + + val sessions = JSONArray() + state.tabs.filter { !it.content.private }.forEachIndexed { index, tab -> + sessions.put(index, tabToJSON(tab)) + } + + json.put(Keys.SESSION_STATE_TUPLES_KEY, sessions) + + return json.toString() + } + + private fun tabToJSON(tab: TabSessionState): JSONObject { + val itemJson = JSONObject() + + val sessionJson = JSONObject().apply { + put(Keys.SESSION_URL_KEY, tab.content.url) + put(Keys.SESSION_UUID_KEY, tab.id) + put(Keys.SESSION_PARENT_UUID_KEY, tab.parentId ?: "") + put(Keys.SESSION_TITLE, tab.content.title) + put(Keys.SESSION_CONTEXT_ID_KEY, tab.contextId) + } + + sessionJson.put(Keys.SESSION_READER_MODE_KEY, tab.readerState.active) + if (tab.readerState.active && tab.readerState.activeUrl != null) { + sessionJson.put(Keys.SESSION_READER_MODE_ACTIVE_URL_KEY, tab.readerState.activeUrl) + } + itemJson.put(Keys.SESSION_KEY, sessionJson) + + val engineSessionState = tab.engineState.engineSessionState + val engineSessionStateJson = engineSessionState?.toJSON() + ?: (tab.engineState.engineSession?.saveState()?.toJSON() + ?: JSONObject()) + itemJson.put(Keys.ENGINE_SESSION_KEY, engineSessionStateJson) + + return itemJson + } +} diff --git a/components/browser/session/src/main/java/mozilla/components/browser/session/storage/SessionStorage.kt b/components/browser/session/src/main/java/mozilla/components/browser/session/storage/SessionStorage.kt index b769269bb77..6b42c0aa4e1 100644 --- a/components/browser/session/src/main/java/mozilla/components/browser/session/storage/SessionStorage.kt +++ b/components/browser/session/src/main/java/mozilla/components/browser/session/storage/SessionStorage.kt @@ -11,7 +11,11 @@ import androidx.annotation.VisibleForTesting import androidx.annotation.WorkerThread import mozilla.components.browser.session.SessionManager import mozilla.components.browser.session.ext.readSnapshot -import mozilla.components.browser.session.ext.writeSnapshot +import mozilla.components.browser.session.ext.writeState +import mozilla.components.browser.state.selector.normalTabs +import mozilla.components.browser.state.selector.selectedTab +import mozilla.components.browser.state.state.BrowserState +import mozilla.components.browser.state.store.BrowserStore import mozilla.components.concept.engine.Engine import java.io.File import java.util.Locale @@ -28,7 +32,8 @@ class SessionStorage( private val context: Context, private val engine: Engine ) : AutoSave.Storage { - private val serializer = SnapshotSerializer() + private val snapshotSerializer = SnapshotSerializer() + private val stateSerializer = BrowserStateSerializer() /** * Reads the saved state from disk. Returns null if no state was found on disk or if reading the file failed. @@ -37,7 +42,7 @@ class SessionStorage( fun restore(): SessionManager.Snapshot? { synchronized(sessionFileLock) { return getFileForEngine(context, engine) - .readSnapshot(engine, serializer) + .readSnapshot(engine, snapshotSerializer) } } @@ -53,19 +58,21 @@ class SessionStorage( * Saves the given state to disk. */ @WorkerThread - override fun save(snapshot: SessionManager.Snapshot): Boolean { - if (snapshot.isEmpty()) { + override fun save(state: BrowserState): Boolean { + if (state.normalTabs.isEmpty()) { clear() return true } - requireNotNull(snapshot.sessions.getOrNull(snapshot.selectedSessionIndex)) { - "SessionSnapshot's selected index must be in bounds" + if (state.selectedTabId != null) { + requireNotNull(state.selectedTab) { + "Selected session must exist" + } } synchronized(sessionFileLock) { return getFileForEngine(context, engine) - .writeSnapshot(snapshot, serializer) + .writeState(state, stateSerializer) } } @@ -74,11 +81,11 @@ class SessionStorage( */ @CheckResult fun autoSave( - sessionManager: SessionManager, + store: BrowserStore, interval: Long = AutoSave.DEFAULT_INTERVAL_MILLISECONDS, unit: TimeUnit = TimeUnit.MILLISECONDS ): AutoSave { - return AutoSave(sessionManager, this, unit.toMillis(interval)) + return AutoSave(store, this, unit.toMillis(interval)) } } diff --git a/components/browser/session/src/main/java/mozilla/components/browser/session/storage/SnapshotSerializer.kt b/components/browser/session/src/main/java/mozilla/components/browser/session/storage/SnapshotSerializer.kt index 8c06c18752f..64a09a0f3b9 100644 --- a/components/browser/session/src/main/java/mozilla/components/browser/session/storage/SnapshotSerializer.kt +++ b/components/browser/session/src/main/java/mozilla/components/browser/session/storage/SnapshotSerializer.kt @@ -14,6 +14,7 @@ import mozilla.components.support.ktx.android.org.json.tryGetString import org.json.JSONArray import org.json.JSONException import org.json.JSONObject +import java.lang.IllegalStateException import java.util.UUID // Current version of the format used. @@ -70,7 +71,8 @@ class SnapshotSerializer( val tuples: MutableList = mutableListOf() val jsonRoot = JSONObject(json) - val selectedSessionIndex = jsonRoot.getInt(Keys.SELECTED_SESSION_INDEX_KEY) + + val version = jsonRoot.getInt(Keys.VERSION_KEY) val sessionStateTuples = jsonRoot.getJSONArray(Keys.SESSION_STATE_TUPLES_KEY) for (i in 0 until sessionStateTuples.length()) { @@ -78,6 +80,17 @@ class SnapshotSerializer( tuples.add(itemFromJSON(engine, sessionStateTupleJson)) } + val selectedSessionIndex = when (version) { + 1 -> jsonRoot.getInt(Keys.SELECTED_SESSION_INDEX_KEY) + 2 -> { + val selectedTabId = jsonRoot.getString(Keys.SELECTED_TAB_ID_KEY) + tuples.indexOfFirst { it.session.id == selectedTabId } + } + else -> { + throw IllegalStateException("Unknown session store format version ($version") + } + } + return SessionManager.Snapshot( sessions = tuples, selectedSessionIndex = selectedSessionIndex @@ -141,8 +154,9 @@ internal fun deserializeSession( return session } -private object Keys { +internal object Keys { const val SELECTED_SESSION_INDEX_KEY = "selectedSessionIndex" + const val SELECTED_TAB_ID_KEY = "selectedTabId" const val SESSION_STATE_TUPLES_KEY = "sessionStateTuples" const val SESSION_URL_KEY = "url" diff --git a/components/browser/session/src/main/java/mozilla/components/browser/session/usecases/EngineSessionUseCases.kt b/components/browser/session/src/main/java/mozilla/components/browser/session/usecases/EngineSessionUseCases.kt deleted file mode 100644 index d141df75175..00000000000 --- a/components/browser/session/src/main/java/mozilla/components/browser/session/usecases/EngineSessionUseCases.kt +++ /dev/null @@ -1,49 +0,0 @@ -/* 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.session.usecases - -import mozilla.components.browser.session.SessionManager -import mozilla.components.concept.engine.EngineSession - -/** - * UseCases for getting and creating [EngineSession] instances for tabs. - * - * This class and its use cases are an interim solution in order to migrate components away from - * using SessionManager for getting and creating [EngineSession] instances. - */ -class EngineSessionUseCases( - sessionManager: SessionManager -) { - /** - * Use case for getting or creating an [EngineSession] for a tab. - */ - class GetOrCreateUseCase internal constructor( - private val sessionManager: SessionManager - ) { - /** - * Gets the linked engine session for a tab or creates (and links) one if needed. - */ - operator fun invoke(tabId: String): EngineSession? { - sessionManager.findSessionById(tabId)?.let { - return sessionManager.getOrCreateEngineSession(it) - } - - // SessionManager and BrowserStore are "eventually consistent". If this method gets - // invoked with an ID from a BrowserStore state then it is possible that is tab is - // already removed in SessionManager (and this will be synced with BrowserStore - // eventually). - // - // In that situation we can't look up the tab anymore and can't return an EngineSession - // here. So we need to weaken the contract and allow returning null from here. - // - // Eventually, once only BrowserStore keeps EngineSession references, this will no - // longer happen. - - return null - } - } - - val getOrCreateEngineSession = GetOrCreateUseCase(sessionManager) -} diff --git a/components/browser/session/src/test/java/mozilla/components/browser/session/SelectionAwareSessionObserverTest.kt b/components/browser/session/src/test/java/mozilla/components/browser/session/SelectionAwareSessionObserverTest.kt index f7a24df7b9c..2519240eacc 100644 --- a/components/browser/session/src/test/java/mozilla/components/browser/session/SelectionAwareSessionObserverTest.kt +++ b/components/browser/session/src/test/java/mozilla/components/browser/session/SelectionAwareSessionObserverTest.kt @@ -4,14 +4,12 @@ package mozilla.components.browser.session -import android.graphics.Bitmap import mozilla.components.concept.engine.content.blocking.Tracker import mozilla.components.support.test.mock import org.junit.Assert.assertEquals import org.junit.Assert.assertNull import org.junit.Before import org.junit.Test -import org.mockito.Mockito class SelectionAwareSessionObserverTest { private lateinit var sessionManager: SessionManager @@ -148,7 +146,6 @@ class SelectionAwareSessionObserverTest { observer.onTrackerBlockingEnabledChanged(session, true) observer.onTrackerBlocked(session, Tracker(""), emptyList()) observer.onDesktopModeChanged(session, true) - observer.onThumbnailChanged(session, Mockito.spy(Bitmap::class.java)) observer.onSessionRemoved(session) observer.onAllSessionsRemoved() } diff --git a/components/browser/session/src/test/java/mozilla/components/browser/session/SessionManagerMigrationTest.kt b/components/browser/session/src/test/java/mozilla/components/browser/session/SessionManagerMigrationTest.kt index ecc482d6e60..301d07c317c 100644 --- a/components/browser/session/src/test/java/mozilla/components/browser/session/SessionManagerMigrationTest.kt +++ b/components/browser/session/src/test/java/mozilla/components/browser/session/SessionManagerMigrationTest.kt @@ -4,7 +4,8 @@ package mozilla.components.browser.session -import android.content.ComponentCallbacks2 +import androidx.test.ext.junit.runners.AndroidJUnit4 +import mozilla.components.browser.session.storage.SessionStorage import mozilla.components.browser.state.action.ReaderAction import mozilla.components.browser.state.selector.findTab import mozilla.components.browser.state.selector.selectedTab @@ -17,20 +18,19 @@ import mozilla.components.concept.engine.EngineSessionState import mozilla.components.concept.engine.content.blocking.Tracker import mozilla.components.support.test.ext.joinBlocking import mozilla.components.support.test.mock +import mozilla.components.support.test.robolectric.testContext import org.junit.Assert.assertEquals import org.junit.Assert.assertFalse import org.junit.Assert.assertNotNull import org.junit.Assert.assertNull -import org.junit.Assert.assertSame import org.junit.Assert.assertTrue import org.junit.Test -import org.mockito.Mockito.doReturn -import org.mockito.Mockito.never -import org.mockito.Mockito.verify +import org.junit.runner.RunWith /** * This test suite validates that calls on [SessionManager] update [BrowserStore] to create a matching state. */ +@RunWith(AndroidJUnit4::class) class SessionManagerMigrationTest { @Test fun `Add session`() { @@ -125,8 +125,6 @@ class SessionManagerMigrationTest { val customTabSession = Session("https://www.mozilla.org") customTabSession.customTabConfig = mock() - val engineSession: EngineSession = mock() - customTabSession.engineSessionHolder.engineSession = engineSession sessionManager.add(customTabSession) assertEquals(0, sessionManager.sessions.size) @@ -145,7 +143,6 @@ class SessionManagerMigrationTest { val tab = store.state.tabs[0] assertEquals("https://www.mozilla.org", tab.content.url) - assertSame(engineSession, tab.engineState.engineSession) } @Test @@ -423,7 +420,11 @@ class SessionManagerMigrationTest { store.state.findTab(session2.id)?.readerState ) - val snapshot = manager.createSnapshot() + val storage = SessionStorage(testContext, mock()) + assertTrue(storage.save(store.state)) + + val snapshot = storage.restore()!! + manager.removeAll() assertEquals(0, manager.size) assertEquals(0, store.state.tabs.size) @@ -613,34 +614,6 @@ class SessionManagerMigrationTest { } } - @Test - @Suppress("Deprecation") - fun `thumbnails of all but selected session should be removed on low memory`() { - val store = BrowserStore() - val sessionManager = SessionManager(engine = mock(), store = store) - - val session1 = Session("https://www.mozilla.org") - val session2 = Session("https://getpocket.com") - val session3 = Session("https://www.firefox.com") - - sessionManager.add(session1, false) - session1.thumbnail = mock() - sessionManager.add(session2, false) - session2.thumbnail = mock() - sessionManager.add(session3, true) - session3.thumbnail = mock() - - val allSessionsMustHaveAThumbnail = store.state.tabs.all { it.content.thumbnail != null } - assertTrue(allSessionsMustHaveAThumbnail) - - sessionManager.onLowMemory() - - assertNull(store.state.tabs[0].content.thumbnail) - assertNull(store.state.tabs[1].content.thumbnail) - // Thumbnail of selected session should not have been removed - assertNotNull(store.state.tabs[2].content.thumbnail) - } - @Test fun `Adding multiple sessions into empty manager and store`() { val store = BrowserStore() @@ -809,59 +782,6 @@ class SessionManagerMigrationTest { } } - @Test - fun `Linking session to engine session`() { - val store = BrowserStore() - val engine: Engine = mock() - - val engineSession1: EngineSession = mock() - doReturn(engineSession1).`when`(engine).createSession(false) - - val sessionManager = SessionManager(engine, store) - - val session = Session(id = "session", initialUrl = "https://www.mozilla.org") - sessionManager.add(session) - - assertNull(store.state.findTab("session")!!.engineState.engineSession) - assertEquals(engineSession1, sessionManager.getOrCreateEngineSession(session)) - store.state.findTab("session")!!.also { tab -> - assertEquals(engineSession1, tab.engineState.engineSession) - } - - // Force unlink and link again - val engineSession2: EngineSession = mock() - doReturn(engineSession2).`when`(engine).createSession(false) - session.engineSessionHolder.engineSession = null - assertEquals(engineSession2, sessionManager.getOrCreateEngineSession(session)) - store.state.findTab("session")!!.also { tab -> - assertEquals(engineSession2, tab.engineState.engineSession) - } - } - - @Test - fun `Session is added to store before engine session can be created and linked`() { - val store = BrowserStore() - val engine: Engine = mock() - - val engineSession1: EngineSession = mock() - doReturn(engineSession1).`when`(engine).createSession(false) - - val sessionManager = SessionManager(engine, store) - sessionManager.register(object : SessionManager.Observer { - override fun onSessionAdded(session: Session) { - sessionManager.getOrCreateEngineSession(session) - } - }) - - val session = Session(id = "session", initialUrl = "https://www.mozilla.org") - sessionManager.add(session) - - assertEquals(engineSession1, sessionManager.getOrCreateEngineSession(session)) - store.state.findTab("session")!!.also { tab -> - assertEquals(engineSession1, tab.engineState.engineSession) - } - } - @Test fun `Restoring engine session with state`() { val engine: Engine = mock() @@ -897,510 +817,6 @@ class SessionManagerMigrationTest { } } - @Test - fun `Trimming memory - RUNNING_LOW - Removes thumbnails`() { - val store = BrowserStore() - val manager = SessionManager(engine = mock(), store = store) - - val engineSession1 = createMockEngineSessionWithState() - val engineSession2 = createMockEngineSessionWithState() - val engineSession3 = createMockEngineSessionWithState() - val engineSession4 = createMockEngineSessionWithState() - - manager.add(Session("https://www.mozilla.org").apply { - thumbnail = mock() - }, engineSession = engineSession1) - - manager.add(Session("https://www.firefox.com").apply { - thumbnail = mock() - }, engineSession = engineSession2) - - manager.add(Session("https://getpocket.com").apply { - thumbnail = mock() - }, engineSession = engineSession3) - - manager.add(Session("https://www.allizom.org").apply { - thumbnail = mock() - }, engineSession = engineSession4) - - manager.select(manager.sessions[2]) - - // SessionManager: - - manager.sessions[0].apply { - assertNotNull(thumbnail) - assertNotNull(engineSessionHolder.engineSession) - assertNull(engineSessionHolder.engineSessionState) - } - - manager.sessions[1].apply { - assertNotNull(thumbnail) - assertNotNull(engineSessionHolder.engineSession) - assertNull(engineSessionHolder.engineSessionState) - } - - manager.sessions[2].apply { - assertNotNull(thumbnail) - assertNotNull(engineSessionHolder.engineSession) - assertNull(engineSessionHolder.engineSessionState) - } - - manager.sessions[3].apply { - assertNotNull(thumbnail) - assertNotNull(engineSessionHolder.engineSession) - assertNull(engineSessionHolder.engineSessionState) - } - - // BrowserStore: - - store.state.tabs[0].apply { - assertNotNull(content.thumbnail) - assertNotNull(engineState.engineSession) - assertNull(engineState.engineSessionState) - } - - store.state.tabs[1].apply { - assertNotNull(content.thumbnail) - assertNotNull(engineState.engineSession) - assertNull(engineState.engineSessionState) - } - - store.state.tabs[2].apply { - assertNotNull(content.thumbnail) - assertNotNull(engineState.engineSession) - assertNull(engineState.engineSessionState) - } - - store.state.tabs[3].apply { - assertNotNull(content.thumbnail) - assertNotNull(engineState.engineSession) - assertNull(engineState.engineSessionState) - } - - // Now trim memory - - manager.onTrimMemory(ComponentCallbacks2.TRIM_MEMORY_RUNNING_LOW) - - // SessionManager: - - manager.sessions[0].apply { - assertNull(thumbnail) - assertNotNull(engineSessionHolder.engineSession) - assertNull(engineSessionHolder.engineSessionState) - } - - manager.sessions[1].apply { - assertNull(thumbnail) - assertNotNull(engineSessionHolder.engineSession) - assertNull(engineSessionHolder.engineSessionState) - } - - manager.sessions[2].apply { - assertNotNull(thumbnail) - assertNotNull(engineSessionHolder.engineSession) - assertNull(engineSessionHolder.engineSessionState) - } - - manager.sessions[3].apply { - assertNull(thumbnail) - assertNotNull(engineSessionHolder.engineSession) - assertNull(engineSessionHolder.engineSessionState) - } - - // BrowserStore: - - store.state.tabs[0].apply { - assertNull(content.thumbnail) - assertNotNull(engineState.engineSession) - assertNull(engineState.engineSessionState) - } - - store.state.tabs[1].apply { - assertNull(content.thumbnail) - assertNotNull(engineState.engineSession) - assertNull(engineState.engineSessionState) - } - - store.state.tabs[2].apply { - assertNotNull(content.thumbnail) - assertNotNull(engineState.engineSession) - assertNull(engineState.engineSessionState) - } - - store.state.tabs[3].apply { - assertNull(content.thumbnail) - assertNotNull(engineState.engineSession) - assertNull(engineState.engineSessionState) - } - - verify(engineSession1, never()).close() - verify(engineSession2, never()).close() - verify(engineSession3, never()).close() - verify(engineSession4, never()).close() - } - - @Test - fun `Trimming memory - RUNNING_CRITICAL - Removes thumbnails and closes engine sessions`() { - val store = BrowserStore() - val manager = SessionManager(engine = mock(), store = store) - - val engineSession1 = createMockEngineSessionWithState() - val engineSession2 = createMockEngineSessionWithState() - val engineSession3 = createMockEngineSessionWithState() - val engineSession4 = createMockEngineSessionWithState() - - manager.add(Session("https://www.mozilla.org").apply { - thumbnail = mock() - }, engineSession = engineSession1) - - manager.add(Session("https://www.firefox.com").apply { - thumbnail = mock() - }, engineSession = engineSession2) - - manager.add(Session("https://getpocket.com").apply { - thumbnail = mock() - }, engineSession = engineSession3) - - manager.add(Session("https://www.allizom.org").apply { - thumbnail = mock() - }, engineSession = engineSession4) - - manager.select(manager.sessions[2]) - - // SessionManager: - - manager.sessions[0].apply { - assertNotNull(thumbnail) - assertNotNull(engineSessionHolder.engineSession) - assertNull(engineSessionHolder.engineSessionState) - } - - manager.sessions[1].apply { - assertNotNull(thumbnail) - assertNotNull(engineSessionHolder.engineSession) - assertNull(engineSessionHolder.engineSessionState) - } - - manager.sessions[2].apply { - assertNotNull(thumbnail) - assertNotNull(engineSessionHolder.engineSession) - assertNull(engineSessionHolder.engineSessionState) - } - - manager.sessions[3].apply { - assertNotNull(thumbnail) - assertNotNull(engineSessionHolder.engineSession) - assertNull(engineSessionHolder.engineSessionState) - } - - // BrowserStore: - - store.state.tabs[0].apply { - assertNotNull(content.thumbnail) - assertNotNull(engineState.engineSession) - assertNull(engineState.engineSessionState) - } - - store.state.tabs[1].apply { - assertNotNull(content.thumbnail) - assertNotNull(engineState.engineSession) - assertNull(engineState.engineSessionState) - } - - store.state.tabs[2].apply { - assertNotNull(content.thumbnail) - assertNotNull(engineState.engineSession) - assertNull(engineState.engineSessionState) - } - - store.state.tabs[3].apply { - assertNotNull(content.thumbnail) - assertNotNull(engineState.engineSession) - assertNull(engineState.engineSessionState) - } - - // Now trim memory - - manager.onTrimMemory(ComponentCallbacks2.TRIM_MEMORY_RUNNING_CRITICAL) - - // SessionManager: - - manager.sessions[0].apply { - assertNull(thumbnail) - assertNull(engineSessionHolder.engineSession) - assertNotNull(engineSessionHolder.engineSessionState) - } - - manager.sessions[1].apply { - assertNull(thumbnail) - assertNull(engineSessionHolder.engineSession) - assertNotNull(engineSessionHolder.engineSessionState) - } - - manager.sessions[2].apply { - assertNotNull(thumbnail) - assertNotNull(engineSessionHolder.engineSession) - assertNull(engineSessionHolder.engineSessionState) - } - - manager.sessions[3].apply { - assertNull(thumbnail) - assertNull(engineSessionHolder.engineSession) - assertNotNull(engineSessionHolder.engineSessionState) - } - - // BrowserStore: - - store.state.tabs[0].apply { - assertNull(content.thumbnail) - assertNull(engineState.engineSession) - assertNotNull(engineState.engineSessionState) - } - - store.state.tabs[1].apply { - assertNull(content.thumbnail) - assertNull(engineState.engineSession) - assertNotNull(engineState.engineSessionState) - } - - store.state.tabs[2].apply { - assertNotNull(content.thumbnail) - assertNotNull(engineState.engineSession) - assertNull(engineState.engineSessionState) - } - - store.state.tabs[3].apply { - assertNull(content.thumbnail) - assertNull(engineState.engineSession) - assertNotNull(engineState.engineSessionState) - } - - verify(engineSession1).close() - verify(engineSession2).close() - verify(engineSession3, never()).close() - verify(engineSession4).close() - } - - @Test - fun `Trimming memory - RUNNING_CRITICAL - Does not remove existing state from session`() { - val store = BrowserStore() - val manager = SessionManager(engine = mock(), store = store) - - val engineSession1 = createMockEngineSessionWithState() - val engineSession2 = createMockEngineSessionWithState() - val engineSessionState = mock() - - manager.add(Session("https://www.mozilla.org").apply { - thumbnail = mock() - }, engineSession = engineSession1) - - manager.add(Session("https://www.firefox.com").apply { - thumbnail = mock() - }, engineSession = engineSession2) - - manager.add(Session("https://getpocket.com").apply { - thumbnail = mock() - }, engineSessionState = engineSessionState) - - // SessionManager: - - manager.sessions[0].apply { - assertNotNull(engineSessionHolder.engineSession) - assertNull(engineSessionHolder.engineSessionState) - } - - manager.sessions[1].apply { - assertNotNull(engineSessionHolder.engineSession) - assertNull(engineSessionHolder.engineSessionState) - } - - manager.sessions[2].apply { - assertNull(engineSessionHolder.engineSession) - assertNotNull(engineSessionHolder.engineSessionState) - } - - // BrowserStore: - - store.state.tabs[0].apply { - assertNotNull(engineState.engineSession) - assertNull(engineState.engineSessionState) - } - - store.state.tabs[1].apply { - assertNotNull(engineState.engineSession) - assertNull(engineState.engineSessionState) - } - - store.state.tabs[2].apply { - assertNull(engineState.engineSession) - assertNotNull(engineState.engineSessionState) - } - - // Now trim memory - - manager.onTrimMemory(ComponentCallbacks2.TRIM_MEMORY_RUNNING_CRITICAL) - - // SessionManager: - - manager.sessions[0].apply { - assertNotNull(engineSessionHolder.engineSession) - assertNull(engineSessionHolder.engineSessionState) - } - - manager.sessions[1].apply { - assertNull(engineSessionHolder.engineSession) - assertNotNull(engineSessionHolder.engineSessionState) - } - - manager.sessions[2].apply { - assertNull(engineSessionHolder.engineSession) - assertNotNull(engineSessionHolder.engineSessionState) - } - - // BrowserStore: - - store.state.tabs[0].apply { - assertNotNull(engineState.engineSession) - assertNull(engineState.engineSessionState) - } - - store.state.tabs[1].apply { - assertNull(engineState.engineSession) - assertNotNull(engineState.engineSessionState) - } - - store.state.tabs[2].apply { - assertNull(engineState.engineSession) - assertNotNull(engineState.engineSessionState) - } - - // Now trim memory AGAIN - - manager.onTrimMemory(ComponentCallbacks2.TRIM_MEMORY_RUNNING_CRITICAL) - - // SessionManager: - - manager.sessions[0].apply { - assertNotNull(engineSessionHolder.engineSession) - assertNull(engineSessionHolder.engineSessionState) - } - - manager.sessions[1].apply { - assertNull(engineSessionHolder.engineSession) - assertNotNull(engineSessionHolder.engineSessionState) - } - - manager.sessions[2].apply { - assertNull(engineSessionHolder.engineSession) - assertNotNull(engineSessionHolder.engineSessionState) - } - - // BrowserStore: - - store.state.tabs[0].apply { - assertNotNull(engineState.engineSession) - assertNull(engineState.engineSessionState) - } - - store.state.tabs[1].apply { - assertNull(engineState.engineSession) - assertNotNull(engineState.engineSessionState) - } - - store.state.tabs[2].apply { - assertNull(engineState.engineSession) - assertNotNull(engineState.engineSessionState) - } - } - - @Test - fun `Trimming memory - RUNNING_MODERATE - Does not affect SessionManager`() { - val store = BrowserStore() - val manager = SessionManager(engine = mock(), store = store) - - val engineSession1 = createMockEngineSessionWithState() - val engineSession2 = createMockEngineSessionWithState() - val engineSession3 = createMockEngineSessionWithState() - val engineSession4 = createMockEngineSessionWithState() - - manager.add(Session("https://www.mozilla.org").apply { - thumbnail = mock() - }, engineSession = engineSession1) - - manager.add(Session("https://www.firefox.com").apply { - thumbnail = mock() - }, engineSession = engineSession2) - - manager.add(Session("https://getpocket.com").apply { - thumbnail = mock() - }, engineSession = engineSession3) - - manager.add(Session("https://www.allizom.org").apply { - thumbnail = mock() - }, engineSession = engineSession4) - - manager.select(manager.sessions[2]) - - manager.onTrimMemory(ComponentCallbacks2.TRIM_MEMORY_RUNNING_MODERATE) - - // SessionManager: - - manager.sessions[0].apply { - assertNotNull(thumbnail) - assertNotNull(engineSessionHolder.engineSession) - assertNull(engineSessionHolder.engineSessionState) - } - - manager.sessions[1].apply { - assertNotNull(thumbnail) - assertNotNull(engineSessionHolder.engineSession) - assertNull(engineSessionHolder.engineSessionState) - } - - manager.sessions[2].apply { - assertNotNull(thumbnail) - assertNotNull(engineSessionHolder.engineSession) - assertNull(engineSessionHolder.engineSessionState) - } - - manager.sessions[3].apply { - assertNotNull(thumbnail) - assertNotNull(engineSessionHolder.engineSession) - assertNull(engineSessionHolder.engineSessionState) - } - - // BrowserStore: - - store.state.tabs[0].apply { - assertNotNull(content.thumbnail) - assertNotNull(engineState.engineSession) - assertNull(engineState.engineSessionState) - } - - store.state.tabs[1].apply { - assertNotNull(content.thumbnail) - assertNotNull(engineState.engineSession) - assertNull(engineState.engineSessionState) - } - - store.state.tabs[2].apply { - assertNotNull(content.thumbnail) - assertNotNull(engineState.engineSession) - assertNull(engineState.engineSessionState) - } - - store.state.tabs[3].apply { - assertNotNull(content.thumbnail) - assertNotNull(engineState.engineSession) - assertNull(engineState.engineSessionState) - } - - verify(engineSession1, never()).close() - verify(engineSession2, never()).close() - verify(engineSession3, never()).close() - verify(engineSession4, never()).close() - } - @Test fun `Adding session with engine session state`() { val store = BrowserStore() @@ -1411,9 +827,6 @@ class SessionManagerMigrationTest { manager.add(session, engineSessionState = state) - assertEquals(state, session.engineSessionHolder.engineSessionState) - assertNull(session.engineSessionHolder.engineSession) - assertEquals(state, store.state.tabs[0].engineState.engineSessionState) assertNull(store.state.tabs[0].engineState.engineSession) } @@ -1429,11 +842,8 @@ class SessionManagerMigrationTest { manager.add(session, engineSession = engineSession, engineSessionState = state) - assertNull(session.engineSessionHolder.engineSessionState) - assertEquals(engineSession, session.engineSessionHolder.engineSession) - assertNull(store.state.tabs[0].engineState.engineSessionState) - assertEquals(engineSession, session.engineSessionHolder.engineSession) + assertEquals(engineSession, store.state.tabs[0].engineState.engineSession) } @Test @@ -1446,16 +856,7 @@ class SessionManagerMigrationTest { manager.add(session, engineSession = engineSession) - assertNull(session.engineSessionHolder.engineSessionState) - assertEquals(engineSession, session.engineSessionHolder.engineSession) - assertNull(store.state.tabs[0].engineState.engineSessionState) - assertEquals(engineSession, session.engineSessionHolder.engineSession) + assertEquals(engineSession, store.state.tabs[0].engineState.engineSession) } } - -private fun createMockEngineSessionWithState(): EngineSession { - val engineSession: EngineSession = mock() - doReturn(mock()).`when`(engineSession).saveState() - return engineSession -} diff --git a/components/browser/session/src/test/java/mozilla/components/browser/session/SessionManagerTest.kt b/components/browser/session/src/test/java/mozilla/components/browser/session/SessionManagerTest.kt index c5978a8446f..3aae61f1b44 100644 --- a/components/browser/session/src/test/java/mozilla/components/browser/session/SessionManagerTest.kt +++ b/components/browser/session/src/test/java/mozilla/components/browser/session/SessionManagerTest.kt @@ -4,27 +4,19 @@ package mozilla.components.browser.session -import android.graphics.Bitmap -import mozilla.components.browser.state.selector.findTab import mozilla.components.browser.state.state.CustomTabConfig -import mozilla.components.browser.state.state.SessionState -import mozilla.components.browser.state.store.BrowserStore -import mozilla.components.concept.engine.Engine import mozilla.components.concept.engine.EngineSession import mozilla.components.concept.engine.EngineSessionState import mozilla.components.support.test.any import mozilla.components.support.test.mock import org.junit.Assert.assertEquals import org.junit.Assert.assertFalse -import org.junit.Assert.assertNotNull import org.junit.Assert.assertNull import org.junit.Assert.assertTrue import org.junit.Test -import org.mockito.ArgumentMatchers.anyBoolean import org.mockito.ArgumentMatchers.anyString import org.mockito.Mockito.`when` -import org.mockito.Mockito.doReturn import org.mockito.Mockito.mock import org.mockito.Mockito.never import org.mockito.Mockito.reset @@ -132,46 +124,6 @@ class SessionManagerTest { manager.select(Session("https://getpocket.com")) } - @Test - fun `selected session is marked as active for web extensions`() { - val engine = mock(Engine::class.java) - val engineSession1 = mock(EngineSession::class.java) - val engineSession2 = mock(EngineSession::class.java) - `when`(engine.name()).thenReturn("gecko") - `when`(engine.createSession(anyBoolean(), any())).thenReturn(engineSession1) - - val session1 = Session("http://www.mozilla.org") - val session2 = Session("http://www.firefox.com") - - val manager = SessionManager(engine) - manager.add(session1) - manager.add(session2) - assertEquals("http://www.mozilla.org", manager.selectedSessionOrThrow.url) - - // Session1 was selected but never linked to an engine session so we have never marked it as active - verify(engineSession1, never()).markActiveForWebExtensions(true) - - // Creating an engine session and linking to session1 should mark it as active - manager.getOrCreateEngineSession(session1) - verify(engineSession1).markActiveForWebExtensions(true) - - // Selecting a new session should mark the new session as active and the previous one as inactive - `when`(engine.createSession(anyBoolean(), any())).thenReturn(engineSession2) - verify(engineSession2, never()).markActiveForWebExtensions(true) - manager.getOrCreateEngineSession(session2) - manager.select(session2) - assertEquals("http://www.firefox.com", manager.selectedSessionOrThrow.url) - verify(engineSession1).markActiveForWebExtensions(false) - verify(engineSession2).markActiveForWebExtensions(true) - - // Removing the selected session should mark it as inactive and the new selection as active - `when`(engine.createSession(anyBoolean(), any())).thenReturn(engineSession1) - manager.remove(session2) - assertEquals("http://www.mozilla.org", manager.selectedSessionOrThrow.url) - verify(engineSession2).markActiveForWebExtensions(false) - verify(engineSession1, times(2)).markActiveForWebExtensions(true) - } - @Test fun `observer does not get notified after unregistering`() { val session1 = Session("http://www.mozilla.org") @@ -280,41 +232,6 @@ class SessionManagerTest { assertEquals(0, manager.size) } - @Test - fun `createSnapshot works when manager has no sessions`() { - val manager = SessionManager(mock()) - assertTrue(manager.createSnapshot().isEmpty()) - } - - @Test - fun `createSnapshot ignores private sessions`() { - val manager = SessionManager(mock()) - val session = Session("http://mozilla.org", true) - manager.add(session) - - assertTrue(manager.createSnapshot().isEmpty()) - } - - @Test - fun `createSnapshot ignores CustomTab sessions`() { - val manager = SessionManager(mock()) - val session = Session("http://mozilla.org") - session.customTabConfig = mock(CustomTabConfig::class.java) - manager.add(session) - - assertTrue(manager.createSnapshot().isEmpty()) - } - - @Test - fun `createSnapshot ignores private CustomTab sessions`() { - val manager = SessionManager(mock()) - val session = Session("http://mozilla.org", true) - session.customTabConfig = mock(CustomTabConfig::class.java) - manager.add(session) - - assertTrue(manager.createSnapshot().isEmpty()) - } - @Test fun `restore checks validity of a snapshot - empty`() { val manager = SessionManager(mock()) @@ -427,8 +344,6 @@ class SessionManagerTest { manager.restore(snapshot) assertEquals(3, manager.size) assertEquals("http://www.firefox.com", manager.selectedSessionOrThrow.url) - assertEquals(engineSession, manager.selectedSessionOrThrow.engineSessionHolder.engineSession) - assertNull(manager.selectedSessionOrThrow.engineSessionHolder.engineSessionState) } @Test @@ -475,63 +390,6 @@ class SessionManagerTest { verify(observer, times(1)).onSessionSelected(session3) } - @Test - fun `createSnapshot produces a correct snapshot of sessions`() { - val manager = SessionManager(mock()) - val customTabSession = Session("http://mozilla.org") - customTabSession.customTabConfig = mock(CustomTabConfig::class.java) - val privateSession = Session("http://www.secret.com", true) - val privateCustomTabSession = Session("http://very.secret.com", true) - privateCustomTabSession.customTabConfig = mock(CustomTabConfig::class.java) - - val regularSession = Session("http://www.firefox.com") - val engineSessionState: EngineSessionState = mock() - val engineSession = mock(EngineSession::class.java) - `when`(engineSession.saveState()).thenReturn(engineSessionState) - - val engine = mock(Engine::class.java) - `when`(engine.name()).thenReturn("gecko") - `when`(engine.createSession()).thenReturn(mock(EngineSession::class.java)) - - manager.add(regularSession, false, engineSession) - manager.add(Session("http://firefox.com"), true, engineSession) - manager.add(Session("http://wikipedia.org"), false, engineSession) - manager.add(privateSession) - manager.add(customTabSession) - manager.add(privateCustomTabSession) - - val snapshot = manager.createSnapshot() - assertEquals(3, snapshot.sessions.size) - assertEquals(1, snapshot.selectedSessionIndex) - - val snapshotSession = snapshot.sessions[0] - assertEquals("http://www.firefox.com", snapshotSession.session.url) - - val snapshotState = snapshotSession.engineSession!!.saveState() - assertEquals(engineSessionState, snapshotState) - } - - @Test - fun `createSnapshot resets selection index if selected session was private`() { - val manager = SessionManager(mock()) - - val privateSession = Session("http://www.secret.com", true) - val regularSession1 = Session("http://www.firefox.com") - val regularSession2 = Session("http://www.mozilla.org") - - val engine = mock(Engine::class.java) - `when`(engine.name()).thenReturn("gecko") - `when`(engine.createSession()).thenReturn(mock(EngineSession::class.java)) - - manager.add(regularSession1, false) - manager.add(regularSession2, false) - manager.add(privateSession, true) - - val snapshot = manager.createSnapshot() - assertEquals(2, snapshot.sessions.size) - assertEquals(0, snapshot.selectedSessionIndex) - } - @Test fun `selectedSession is null with no selection`() { val manager = SessionManager(mock()) @@ -658,128 +516,6 @@ class SessionManagerTest { assertNull(manager.findSessionById("banana")) } - @Test - fun `session manager creates and links engine session`() { - val engine: Engine = mock() - - val actualEngineSession: EngineSession = mock() - doReturn(actualEngineSession).`when`(engine).createSession(false) - val privateEngineSession: EngineSession = mock() - doReturn(privateEngineSession).`when`(engine).createSession(true) - - val store = BrowserStore() - val sessionManager = SessionManager(engine, store) - - val session = Session("https://www.mozilla.org") - sessionManager.add(session) - - assertNull(store.state.findTab(session.id)!!.engineState.engineSession) - assertEquals(actualEngineSession, sessionManager.getOrCreateEngineSession(session)) - assertEquals(actualEngineSession, store.state.findTab(session.id)!!.engineState.engineSession) - assertEquals(actualEngineSession, sessionManager.getOrCreateEngineSession(session)) - assertEquals(actualEngineSession, session.engineSessionHolder.engineSession) - - val privateSession = Session("https://www.mozilla.org", true, SessionState.Source.NONE) - sessionManager.add(privateSession) - assertNull(store.state.findTab(privateSession.id)!!.engineState.engineSession) - assertEquals(privateEngineSession, sessionManager.getOrCreateEngineSession(privateSession)) - assertEquals(privateEngineSession, privateSession.engineSessionHolder.engineSession) - } - - @Test - fun `session manager considers parent when creating and linking engine session`() { - val engine: Engine = mock() - - val parent = Session(id = "parent", initialUrl = "") - val parentEngineSession: EngineSession = mock() - val session = Session("https://www.mozilla.org") - session.parentId = parent.id - val engineSession: EngineSession = mock() - - val sessionManager = SessionManager(engine) - sessionManager.add(parent) - sessionManager.add(session) - - doReturn(parentEngineSession, engineSession).`when`(engine).createSession(false) - - assertEquals(parentEngineSession, sessionManager.getOrCreateEngineSession(parent)) - assertEquals(parentEngineSession, parent.engineSessionHolder.engineSession) - - assertEquals(engineSession, sessionManager.getOrCreateEngineSession(session)) - assertEquals(engineSession, session.engineSessionHolder.engineSession) - - verify(engineSession).loadUrl(session.url, parentEngineSession, EngineSession.LoadUrlFlags.none()) - } - - @Test - fun `parent is not provided when linking engine session for extension page`() { - val engine: Engine = mock() - - val parent = Session(id = "parent", initialUrl = "") - val parentEngineSession: EngineSession = mock() - val session = Session("moz-extension://test-1234") - session.parentId = parent.id - val engineSession: EngineSession = mock() - - val sessionManager = SessionManager(engine) - sessionManager.add(parent) - sessionManager.add(session) - - doReturn(parentEngineSession, engineSession).`when`(engine).createSession(false) - - assertEquals(parentEngineSession, sessionManager.getOrCreateEngineSession(parent)) - assertEquals(parentEngineSession, parent.engineSessionHolder.engineSession) - - assertEquals(engineSession, sessionManager.getOrCreateEngineSession(session)) - assertEquals(engineSession, session.engineSessionHolder.engineSession) - - verify(engineSession).loadUrl(session.url, null, EngineSession.LoadUrlFlags.none()) - } - - @Test - fun `removing a session unlinks the engine session`() { - val engine: Engine = mock() - - val actualEngineSession: EngineSession = mock() - doReturn(actualEngineSession).`when`(engine).createSession() - - val sessionManager = SessionManager(engine) - - val session = Session("https://www.mozilla.org") - sessionManager.add(session) - - assertNotNull(sessionManager.getOrCreateEngineSession(session)) - assertNotNull(session.engineSessionHolder.engineSession) - assertNotNull(session.engineSessionHolder.engineObserver) - - sessionManager.remove(session) - - assertNull(session.engineSessionHolder.engineSession) - assertNull(session.engineSessionHolder.engineObserver) - } - - @Test - fun `add will link an engine session if provided`() { - val engine: Engine = mock() - - val store = BrowserStore() - val actualEngineSession: EngineSession = mock() - val sessionManager = SessionManager(engine, store) - - val session = Session("https://www.mozilla.org") - assertNull(session.engineSessionHolder.engineSession) - assertNull(session.engineSessionHolder.engineObserver) - - sessionManager.add(session, engineSession = actualEngineSession) - - assertNotNull(session.engineSessionHolder.engineSession) - assertNotNull(session.engineSessionHolder.engineObserver) - - assertEquals(actualEngineSession, sessionManager.getOrCreateEngineSession(session)) - assertEquals(actualEngineSession, store.state.findTab(session.id)!!.engineState.engineSession) - assertEquals(actualEngineSession, sessionManager.getOrCreateEngineSession(session)) - } - @Test fun `removeSessions retains customtab sessions`() { val manager = SessionManager(mock()) @@ -813,38 +549,6 @@ class SessionManagerTest { manager.selectedSessionOrThrow } - @Test - @Suppress("Deprecation") - fun `thumbnails of all but selected session should be removed on low memory`() { - val manager = SessionManager(mock()) - - val emptyBitmap = spy(Bitmap::class.java) - - val session1 = Session("https://www.mozilla.org") - session1.thumbnail = emptyBitmap - - val session2 = Session("https://getPocket.com") - session2.thumbnail = emptyBitmap - - val session3 = Session("https://www.firefox.com") - session3.thumbnail = emptyBitmap - - manager.add(session1, true) - manager.add(session2, false) - manager.add(session3, false) - - val allSessionsMustHaveAThumbnail = manager.all.all { it.thumbnail != null } - - assertTrue(allSessionsMustHaveAThumbnail) - - manager.onLowMemory() - - val onlySelectedSessionMustHaveAThumbnail = - session1.thumbnail != null && session2.thumbnail == null && session3.thumbnail == null - - assertTrue(onlySelectedSessionMustHaveAThumbnail) - } - @Test fun `custom tab session will not be selected if it is the first session`() { val session = Session("about:blank") diff --git a/components/browser/session/src/test/java/mozilla/components/browser/session/SessionTest.kt b/components/browser/session/src/test/java/mozilla/components/browser/session/SessionTest.kt index a814a844e25..fc5ad5ccb6f 100644 --- a/components/browser/session/src/test/java/mozilla/components/browser/session/SessionTest.kt +++ b/components/browser/session/src/test/java/mozilla/components/browser/session/SessionTest.kt @@ -4,7 +4,6 @@ package mozilla.components.browser.session -import android.graphics.Bitmap import kotlinx.coroutines.Dispatchers.IO import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.async @@ -41,7 +40,6 @@ import org.mockito.Mockito.`when` import org.mockito.Mockito.mock import org.mockito.Mockito.never import org.mockito.Mockito.reset -import org.mockito.Mockito.spy import org.mockito.Mockito.times import org.mockito.Mockito.verify import org.mockito.Mockito.verifyNoMoreInteractions @@ -334,8 +332,7 @@ class SessionTest { verify(store, never()).dispatch(TabListAction.AddTabAction(session.toTabSessionState())) session.customTabConfig = null - verify(store).dispatch(CustomTabListAction.RemoveCustomTabAction(session.id)) - verify(store).dispatch(TabListAction.AddTabAction(session.toTabSessionState())) + verify(store).dispatch(CustomTabListAction.TurnCustomTabIntoNormalTabAction(session.id)) verifyNoMoreInteractions(store) } @@ -577,38 +574,6 @@ class SessionTest { assertTrue(session.desktopMode) } - @Test - fun `observer is notified on on thumbnail changed `() { - val observer = mock(Session.Observer::class.java) - val session = Session("https://www.mozilla.org") - val emptyThumbnail = spy(Bitmap::class.java) - session.register(observer) - session.thumbnail = emptyThumbnail - verify(observer).onThumbnailChanged(session, emptyThumbnail) - reset(observer) - session.unregister(observer) - session.thumbnail = emptyThumbnail - verify(observer, never()).onThumbnailChanged(session, emptyThumbnail) - } - - @Test - fun `action is dispatched when thumbnail changes`() { - val store: BrowserStore = mock() - `when`(store.dispatch(any())).thenReturn(mock()) - - val session = Session("https://www.mozilla.org") - session.store = store - - val emptyThumbnail = spy(Bitmap::class.java) - session.thumbnail = emptyThumbnail - verify(store).dispatch(ContentAction.UpdateThumbnailAction(session.id, emptyThumbnail)) - - session.thumbnail = null - verify(store).dispatch(ContentAction.RemoveThumbnailAction(session.id)) - - verifyNoMoreInteractions(store) - } - @Test fun `session observer has default methods`() { val session = Session("") @@ -629,7 +594,6 @@ class SessionTest { defaultObserver.onTrackerBlockingEnabledChanged(session, true) defaultObserver.onTrackerBlocked(session, mock(), emptyList()) defaultObserver.onDesktopModeChanged(session, true) - defaultObserver.onThumbnailChanged(session, spy(Bitmap::class.java)) defaultObserver.onContentPermissionRequested(session, contentPermissionRequest) defaultObserver.onAppPermissionRequested(session, appPermissionRequest) defaultObserver.onWebAppManifestChanged(session, mock()) @@ -751,30 +715,6 @@ class SessionTest { assertEquals("Session(my-session-id, https://www.mozilla.org)", session.toString()) } - @Test - fun `WHEN crashed state changes THEN observers are notified`() { - val session = Session("https://www.mozilla.org") - - var observedCrashState: Boolean? = null - - session.register(object : Session.Observer { - override fun onCrashStateChanged(session: Session, crashed: Boolean) { - observedCrashState = crashed - } - }) - - assertFalse(session.crashed) - - session.crashed = true - assertTrue(session.crashed) - assertTrue(observedCrashState!!) - observedCrashState = null - - session.crashed = false - assertFalse(session.crashed) - assertFalse(observedCrashState!!) - } - @Test fun `observer is notified when recording devices change`() { val observer = mock(Session.Observer::class.java) diff --git a/components/browser/session/src/test/java/mozilla/components/browser/session/engine/EngineObserverTest.kt b/components/browser/session/src/test/java/mozilla/components/browser/session/engine/EngineObserverTest.kt index 72c855b2e6a..31c50d245df 100644 --- a/components/browser/session/src/test/java/mozilla/components/browser/session/engine/EngineObserverTest.kt +++ b/components/browser/session/src/test/java/mozilla/components/browser/session/engine/EngineObserverTest.kt @@ -11,9 +11,12 @@ import androidx.test.ext.junit.runners.AndroidJUnit4 import mozilla.components.browser.session.Session import mozilla.components.browser.session.SessionManager import mozilla.components.browser.session.engine.request.LoadRequestOption +import mozilla.components.browser.state.action.BrowserAction import mozilla.components.browser.state.action.ContentAction +import mozilla.components.browser.state.action.CrashAction import mozilla.components.browser.state.action.TrackingProtectionAction import mozilla.components.browser.state.selector.findTab +import mozilla.components.browser.state.state.BrowserState import mozilla.components.browser.state.state.content.FindResultState import mozilla.components.browser.state.store.BrowserStore import mozilla.components.concept.engine.EngineSession @@ -27,6 +30,7 @@ import mozilla.components.concept.engine.media.Media import mozilla.components.concept.engine.permission.PermissionRequest import mozilla.components.concept.engine.prompt.PromptRequest import mozilla.components.concept.engine.window.WindowRequest +import mozilla.components.lib.state.MiddlewareStore import mozilla.components.support.base.observer.Consumable import mozilla.components.support.test.any import mozilla.components.support.test.libstate.ext.waitUntilIdle @@ -90,7 +94,7 @@ class EngineObserverTest { notifyObservers { onNavigationStateChange(true, true) } } } - engineSession.register(EngineObserver(session)) + engineSession.register(EngineObserver(session, mock())) engineSession.loadUrl("http://mozilla.org") engineSession.toggleDesktopMode(true) @@ -139,7 +143,7 @@ class EngineObserverTest { } } } - engineSession.register(EngineObserver(session)) + engineSession.register(EngineObserver(session, mock())) engineSession.loadUrl("http://mozilla.org") assertEquals(Session.SecurityInfo(false), session.securityInfo) @@ -183,7 +187,7 @@ class EngineObserverTest { override fun exitFullScreenMode() {} override fun recoverFromCrash(): Boolean { return false } } - val observer = EngineObserver(session) + val observer = EngineObserver(session, mock()) engineSession.register(observer) engineSession.enableTrackingProtection() @@ -205,10 +209,9 @@ class EngineObserverTest { @Test fun engineSessionObserverExcludedOnTrackingProtection() { val session = Session("") - val store = mock(BrowserStore::class.java) + val store: MiddlewareStore = mock() val observer = EngineObserver(session, store) - whenever(store.dispatch(any())).thenReturn(mock()) observer.onExcludedOnTrackingProtectionChange(true) verify(store).dispatch( @@ -224,7 +227,7 @@ class EngineObserverTest { val session = Session("https://www.mozilla.org") session.title = "Hello World" - val observer = EngineObserver(session) + val observer = EngineObserver(session, mock()) observer.onTitleChange("Mozilla") assertEquals("Mozilla", session.title) @@ -239,7 +242,7 @@ class EngineObserverTest { val session = Session("https://www.mozilla.org") session.title = "Hello World" - val observer = EngineObserver(session) + val observer = EngineObserver(session, mock()) observer.onTitleChange("Mozilla") assertEquals("Mozilla", session.title) @@ -254,7 +257,7 @@ class EngineObserverTest { val session = Session("https://www.mozilla.org") session.title = "Hello World" - val observer = EngineObserver(session) + val observer = EngineObserver(session, mock()) observer.onTitleChange("Mozilla") assertEquals("Mozilla", session.title) @@ -267,7 +270,7 @@ class EngineObserverTest { @Test fun engineObserverClearsBlockedTrackersIfNewPageStartsLoading() { val session = Session("https://www.mozilla.org") - val observer = EngineObserver(session) + val observer = EngineObserver(session, mock()) val tracker1 = Tracker("tracker1") val tracker2 = Tracker("tracker2") @@ -282,7 +285,7 @@ class EngineObserverTest { @Test fun engineObserverClearsLoadedTrackersIfNewPageStartsLoading() { val session = Session("https://www.mozilla.org") - val observer = EngineObserver(session) + val observer = EngineObserver(session, mock()) val tracker1 = Tracker("tracker1") val tracker2 = Tracker("tracker2") @@ -299,7 +302,7 @@ class EngineObserverTest { val session = Session("https://www.mozilla.org") val manifest = WebAppManifest(name = "Mozilla", startUrl = "https://mozilla.org") - val observer = EngineObserver(session) + val observer = EngineObserver(session, mock()) observer.onWebAppManifestLoaded(manifest) assertEquals(manifest, session.webAppManifest) @@ -313,7 +316,7 @@ class EngineObserverTest { fun engineObserverClearsContentPermissionRequestIfNewPageStartsLoading() { val session = Session("https://www.mozilla.org") val permissionRequest: PermissionRequest = mock() - val observer = EngineObserver(session) + val observer = EngineObserver(session, mock()) observer.onContentPermissionRequest(permissionRequest) @@ -327,7 +330,7 @@ class EngineObserverTest { fun engineObserverDoesNotClearContentPermissionRequestIfSamePageStartsLoading() { val session = Session("https://www.mozilla.org") val permissionRequest: PermissionRequest = mock() - val observer = EngineObserver(session) + val observer = EngineObserver(session, mock()) observer.onContentPermissionRequest(permissionRequest) @@ -342,7 +345,7 @@ class EngineObserverTest { val session = Session("https://www.mozilla.org") val manifest = WebAppManifest(name = "Mozilla", startUrl = "https://www.mozilla.org") - val observer = EngineObserver(session) + val observer = EngineObserver(session, mock()) observer.onWebAppManifestLoaded(manifest) assertEquals(manifest, session.webAppManifest) @@ -361,7 +364,7 @@ class EngineObserverTest { scope = "https://www.mozilla.org/hello/" ) - val observer = EngineObserver(session) + val observer = EngineObserver(session, mock()) observer.onWebAppManifestLoaded(manifest) assertEquals(manifest, session.webAppManifest) @@ -376,7 +379,7 @@ class EngineObserverTest { @Test fun engineObserverPassingHitResult() { val session = Session("https://www.mozilla.org", id = "test-id") - val store: BrowserStore = mock() + val store: MiddlewareStore = mock() val observer = EngineObserver(session, store) val hitResult = HitResult.UNKNOWN("data://foobar") @@ -390,7 +393,7 @@ class EngineObserverTest { @Test fun engineObserverClearsFindResults() { val session = Session("https://www.mozilla.org", id = "test-id") - val store: BrowserStore = mock() + val store: MiddlewareStore = mock() val observer = EngineObserver(session, store) observer.onFindResult(0, 1, false) @@ -410,7 +413,7 @@ class EngineObserverTest { @Test fun engineObserverClearsFindResultIfNewPageStartsLoading() { val session = Session("https://www.mozilla.org", id = "test-id") - val store: BrowserStore = mock() + val store: MiddlewareStore = mock() val observer = EngineObserver(session, store) observer.onFindResult(0, 1, false) @@ -435,7 +438,7 @@ class EngineObserverTest { @Test fun engineObserverNotifiesFullscreenMode() { val session = Session("https://www.mozilla.org", id = "test-id") - val store: BrowserStore = mock() + val store: MiddlewareStore = mock() val observer = EngineObserver(session, store) observer.onFullScreenChange(true) @@ -454,7 +457,7 @@ class EngineObserverTest { @Test fun engineObserverNotifiesMetaViewportFitChange() { - val store: BrowserStore = mock() + val store: MiddlewareStore = mock() val session = Session("https://www.mozilla.org", id = "test-id") val observer = EngineObserver(session, store) @@ -485,17 +488,21 @@ class EngineObserverTest { @Test fun `Engine observer notified when thumbnail is assigned`() { - val session = Session("https://www.mozilla.org") - val observer = EngineObserver(session) + val session = Session("https://www.mozilla.org", id = "test-id") + val store: MiddlewareStore = mock() + val observer = EngineObserver(session, store) val emptyBitmap = spy(Bitmap::class.java) observer.onThumbnailChange(emptyBitmap) - assertEquals(emptyBitmap, session.thumbnail) + + verify(store).dispatch(ContentAction.UpdateThumbnailAction( + "test-id", emptyBitmap + )) } @Test fun engineObserverNotifiesWebAppManifest() { val session = Session("https://www.mozilla.org") - val observer = EngineObserver(session) + val observer = EngineObserver(session, mock()) val manifest = WebAppManifest( name = "Minimal", startUrl = "/" @@ -509,7 +516,7 @@ class EngineObserverTest { fun engineSessionObserverWithContentPermissionRequests() { val permissionRequest = mock(PermissionRequest::class.java) val session = Session("") - val observer = EngineObserver(session) + val observer = EngineObserver(session, mock()) assertTrue(session.contentPermissionRequest.isConsumed()) observer.onContentPermissionRequest(permissionRequest) @@ -523,7 +530,7 @@ class EngineObserverTest { fun engineSessionObserverWithAppPermissionRequests() { val permissionRequest = mock(PermissionRequest::class.java) val session = Session("") - val observer = EngineObserver(session) + val observer = EngineObserver(session, mock()) assertTrue(session.appPermissionRequest.isConsumed()) observer.onAppPermissionRequest(permissionRequest) @@ -536,7 +543,7 @@ class EngineObserverTest { val session = Session("https://www.mozilla.org") session.contentPermissionRequest = Consumable.from(permissionRequest) - val observer = EngineObserver(session) + val observer = EngineObserver(session, mock()) observer.onLocationChange("https://getpocket.com") verify(permissionRequest).reject() @@ -547,7 +554,7 @@ class EngineObserverTest { fun engineObserverHandlesPromptRequest() { val promptRequest = mock(PromptRequest::class.java) val session = Session(id = "test-session", initialUrl = "") - val store = mock(BrowserStore::class.java) + val store: MiddlewareStore = mock() val observer = EngineObserver(session, store) observer.onPromptRequest(promptRequest) @@ -561,7 +568,7 @@ class EngineObserverTest { fun engineObserverHandlesWindowRequest() { val windowRequest = mock(WindowRequest::class.java) val session = Session("") - val store = mock(BrowserStore::class.java) + val store: MiddlewareStore = mock() whenever(store.state).thenReturn(mock()) val observer = EngineObserver(session, store) @@ -575,7 +582,7 @@ class EngineObserverTest { @Test fun engineObserverHandlesFirstContentfulPaint() { val session = Session("") - val store = mock(BrowserStore::class.java) + val store: MiddlewareStore = mock() whenever(store.state).thenReturn(mock()) val observer = EngineObserver(session, store) @@ -595,7 +602,7 @@ class EngineObserverTest { val session = Session("https://www.mozilla.org", id = "test-tab").also { sessionManager.add(it) } - val observer = EngineObserver(session, store) + val observer = EngineObserver(session, store.toMiddlewareStore()) assertEquals(0, store.state.media.elements.size) val media1: Media = spy(object : Media() { @@ -647,7 +654,7 @@ class EngineObserverTest { val session = Session("https://www.mozilla.org", id = "test-tab").also { sessionManager.add(it) } - val observer = EngineObserver(session, store) + val observer = EngineObserver(session, store.toMiddlewareStore()) val media1: Media = spy(object : Media() { override val controller: Controller = mock() @@ -695,7 +702,7 @@ class EngineObserverTest { sessionManager.add(it) } - val observer = EngineObserver(session, store) + val observer = EngineObserver(session, store.toMiddlewareStore()) observer.onExternalResource( url = "mozilla.org/file.txt", @@ -724,7 +731,7 @@ class EngineObserverTest { val session = Session("https://www.mozilla.org", id = "test-tab").also { sessionManager.add(it) } - val observer = EngineObserver(session, store) + val observer = EngineObserver(session, store.toMiddlewareStore()) observer.onExternalResource(url = "mozilla.org/file.txt", contentLength = -1) @@ -737,18 +744,16 @@ class EngineObserverTest { @Test fun `onCrashStateChanged will update session and notify observer`() { - val session = Session("https://www.mozilla.org") - assertFalse(session.crashed) + val session = Session("https://www.mozilla.org", id = "test-id") - val observer = EngineObserver(session) + val store: MiddlewareStore = mock() + val observer = EngineObserver(session, store) observer.onCrash() - assertTrue(session.crashed) - - session.crashed = false - observer.onCrash() - assertTrue(session.crashed) + verify(store).dispatch(CrashAction.SessionCrashedAction( + "test-id" + )) } @Test @@ -756,7 +761,7 @@ class EngineObserverTest { val session = Session("https://www.mozilla.org") session.searchTerms = "Mozilla Foundation" - val observer = EngineObserver(session) + val observer = EngineObserver(session, mock()) observer.onLocationChange("https://www.mozilla.org/en-US/") assertEquals("Mozilla Foundation", session.searchTerms) @@ -768,7 +773,7 @@ class EngineObserverTest { val session = Session(url) session.searchTerms = "Mozilla Foundation" - val observer = EngineObserver(session) + val observer = EngineObserver(session, mock()) observer.onLoadRequest(url = url, triggeredByRedirect = false, triggeredByWebContent = true) assertEquals("", session.searchTerms) @@ -785,7 +790,7 @@ class EngineObserverTest { val session = Session(url) session.searchTerms = "Mozilla Foundation" - val observer = EngineObserver(session) + val observer = EngineObserver(session, mock()) observer.onLoadRequest(url = url, triggeredByRedirect = true, triggeredByWebContent = false) assertEquals("", session.searchTerms) @@ -802,7 +807,7 @@ class EngineObserverTest { val session = Session(url) session.searchTerms = "Mozilla Foundation" - val observer = EngineObserver(session) + val observer = EngineObserver(session, mock()) observer.onLoadRequest(url = url, triggeredByRedirect = false, triggeredByWebContent = false) assertEquals("Mozilla Foundation", session.searchTerms) @@ -819,7 +824,7 @@ class EngineObserverTest { val url = "https://www.mozilla.org" val session = Session(url) - val observer = EngineObserver(session) + val observer = EngineObserver(session, mock()) val intent: Intent = mock() observer.onLaunchIntentRequest(url = url, appIntent = intent) @@ -837,7 +842,7 @@ class EngineObserverTest { session.searchTerms = "Mozilla Foundation" session.canGoBack = true - val observer = EngineObserver(session) + val observer = EngineObserver(session, mock()) observer.onNavigateBack() assertEquals("", session.searchTerms) @@ -846,9 +851,8 @@ class EngineObserverTest { @Test fun `onHistoryStateChanged dispatches UpdateHistoryStateAction`() { val session = Session("") - val store = mock(BrowserStore::class.java) + val store: MiddlewareStore = mock() val observer = EngineObserver(session, store) - whenever(store.dispatch(any())).thenReturn(mock()) observer.onHistoryStateChanged(emptyList(), 0) verify(store).dispatch( @@ -863,6 +867,7 @@ class EngineObserverTest { HistoryItem("Firefox", "https://firefox.com"), HistoryItem("Mozilla", "http://mozilla.org") ), 1) + verify(store).dispatch( ContentAction.UpdateHistoryStateAction( session.id, @@ -875,3 +880,15 @@ class EngineObserverTest { ) } } + +private fun BrowserStore.toMiddlewareStore(): MiddlewareStore { + val store = this + return object : MiddlewareStore { + override val state: BrowserState + get() = store.state + + override fun dispatch(action: BrowserAction) { + store.dispatch(action) + } + } +} diff --git a/components/browser/session/src/test/java/mozilla/components/browser/session/engine/EngineSessionHolderTest.kt b/components/browser/session/src/test/java/mozilla/components/browser/session/engine/EngineSessionHolderTest.kt deleted file mode 100644 index 4a35ecc228b..00000000000 --- a/components/browser/session/src/test/java/mozilla/components/browser/session/engine/EngineSessionHolderTest.kt +++ /dev/null @@ -1,39 +0,0 @@ -/* 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.session.engine - -import mozilla.components.concept.engine.EngineSession -import org.junit.Assert.assertTrue -import org.junit.Test -import org.mockito.Mockito.mock -import java.util.concurrent.CountDownLatch -import java.util.concurrent.Executors -import java.util.concurrent.TimeUnit - -class EngineSessionHolderTest { - - @Test - fun `engine session holder changes are visible across threads`() { - val engineSessionHolder = EngineSessionHolder() - val countDownLatch = CountDownLatch(1) - - val executor = Executors.newScheduledThreadPool(2) - - executor.submit { - engineSessionHolder.engineObserver = mock(EngineObserver::class.java) - engineSessionHolder.engineSession = mock(EngineSession::class.java) - } - - executor.submit { - while (engineSessionHolder.engineObserver == null || engineSessionHolder.engineSession == null) { } - countDownLatch.countDown() - } - - // Setting a timeout in case this test fails in the future. As long as - // the engine session holder fields are volatile, await will return - // true immediately, otherwise false after the timeout expired. - assertTrue(countDownLatch.await(10, TimeUnit.SECONDS)) - } -} \ No newline at end of file diff --git a/components/browser/session/src/test/java/mozilla/components/browser/session/engine/middleware/CrashMiddlewareTest.kt b/components/browser/session/src/test/java/mozilla/components/browser/session/engine/middleware/CrashMiddlewareTest.kt new file mode 100644 index 00000000000..d08f27b6c9f --- /dev/null +++ b/components/browser/session/src/test/java/mozilla/components/browser/session/engine/middleware/CrashMiddlewareTest.kt @@ -0,0 +1,163 @@ +/* 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.session.engine.middleware + +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.test.TestCoroutineDispatcher +import mozilla.components.browser.session.Session +import mozilla.components.browser.session.engine.EngineMiddleware +import mozilla.components.browser.state.action.CrashAction +import mozilla.components.browser.state.state.BrowserState +import mozilla.components.browser.state.state.EngineState +import mozilla.components.browser.state.state.createTab +import mozilla.components.browser.state.store.BrowserStore +import mozilla.components.concept.engine.Engine +import mozilla.components.concept.engine.EngineSession +import mozilla.components.support.test.ext.joinBlocking +import mozilla.components.support.test.mock +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue +import org.junit.Test +import org.mockito.Mockito.doReturn +import org.mockito.Mockito.never +import org.mockito.Mockito.reset +import org.mockito.Mockito.verify + +class CrashMiddlewareTest { + @Test + fun `Crash and restore scenario`() { + val engineSession1: EngineSession = mock() + val engineSession2: EngineSession = mock() + val engineSession3: EngineSession = mock() + + val engine: Engine = mock() + val dispatcher = TestCoroutineDispatcher() + val scope = CoroutineScope(dispatcher) + + val store = BrowserStore( + middleware = EngineMiddleware.create( + engine = engine, + sessionLookup = { mock() }, + scope = scope + ), + initialState = BrowserState( + tabs = listOf( + createTab("https://www.mozilla.org", id = "tab1").copy( + engineState = EngineState(engineSession1) + ), + createTab("https://www.firefox.com", id = "tab2").copy( + engineState = EngineState(engineSession2) + ), + createTab("https://getpocket.com", id = "tab3").copy( + engineState = EngineState(engineSession3) + ) + ) + ) + ) + + store.dispatch(CrashAction.SessionCrashedAction( + "tab1" + )).joinBlocking() + + store.dispatch(CrashAction.SessionCrashedAction( + "tab3" + )).joinBlocking() + + assertTrue(store.state.tabs[0].crashed) + assertFalse(store.state.tabs[1].crashed) + assertTrue(store.state.tabs[2].crashed) + + verify(engineSession1, never()).recoverFromCrash() + verify(engineSession2, never()).recoverFromCrash() + verify(engineSession3, never()).recoverFromCrash() + + // Restoring crashed session + store.dispatch(CrashAction.RestoreCrashedSessionAction( + "tab1" + )).joinBlocking() + + dispatcher.advanceUntilIdle() + + verify(engineSession1).recoverFromCrash() + verify(engineSession2, never()).recoverFromCrash() + verify(engineSession3, never()).recoverFromCrash() + reset(engineSession1, engineSession2, engineSession3) + + assertFalse(store.state.tabs[0].crashed) + assertFalse(store.state.tabs[1].crashed) + assertTrue(store.state.tabs[2].crashed) + + // Restoring a non crashed session + store.dispatch(CrashAction.RestoreCrashedSessionAction( + "tab2" + )).joinBlocking() + + dispatcher.advanceUntilIdle() + + // EngineSession.recoverFromCrash() handles internally the situation where there's no + // crashed state. + verify(engineSession1, never()).recoverFromCrash() + verify(engineSession2).recoverFromCrash() + verify(engineSession3, never()).recoverFromCrash() + reset(engineSession1, engineSession2, engineSession3) + + // Restoring unknown session + store.dispatch(CrashAction.RestoreCrashedSessionAction( + "unknown" + )).joinBlocking() + + dispatcher.advanceUntilIdle() + + verify(engineSession1, never()).recoverFromCrash() + verify(engineSession2, never()).recoverFromCrash() + verify(engineSession3, never()).recoverFromCrash() + + assertFalse(store.state.tabs[0].crashed) + assertFalse(store.state.tabs[1].crashed) + assertTrue(store.state.tabs[2].crashed) + } + + @Test + fun `Restoring a crashed session without an engine session`() { + val engineSession: EngineSession = mock() + val engine: Engine = mock() + doReturn(engineSession).`when`(engine).createSession() + + val dispatcher = TestCoroutineDispatcher() + val scope = CoroutineScope(dispatcher) + + val store = BrowserStore( + middleware = EngineMiddleware.create( + engine = engine, + sessionLookup = { Session("https://www.mozilla.org") }, + scope = scope + ), + initialState = BrowserState( + tabs = listOf( + createTab("https://www.mozilla.org", id = "tab1") + ) + ) + ) + + store.dispatch(CrashAction.SessionCrashedAction( + "tab1" + )).joinBlocking() + + dispatcher.advanceUntilIdle() + + assertTrue(store.state.tabs[0].crashed) + + store.dispatch(CrashAction.RestoreCrashedSessionAction( + "tab1" + )).joinBlocking() + + dispatcher.advanceUntilIdle() + + verify(engine).createSession() + verify(engineSession).recoverFromCrash() + + assertFalse(store.state.tabs[0].crashed) + } +} diff --git a/components/browser/session/src/test/java/mozilla/components/browser/session/engine/middleware/CreateEngineSessionMiddlewareTest.kt b/components/browser/session/src/test/java/mozilla/components/browser/session/engine/middleware/CreateEngineSessionMiddlewareTest.kt new file mode 100644 index 00000000000..0b16f4a8b67 --- /dev/null +++ b/components/browser/session/src/test/java/mozilla/components/browser/session/engine/middleware/CreateEngineSessionMiddlewareTest.kt @@ -0,0 +1,141 @@ +/* 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.session.engine.middleware + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.test.TestCoroutineDispatcher +import mozilla.components.browser.session.Session +import mozilla.components.browser.state.action.EngineAction +import mozilla.components.browser.state.selector.findTab +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.Engine +import mozilla.components.concept.engine.EngineSession +import mozilla.components.concept.engine.EngineSessionState +import mozilla.components.support.test.any +import mozilla.components.support.test.ext.joinBlocking +import mozilla.components.support.test.libstate.ext.waitUntilIdle +import mozilla.components.support.test.mock +import mozilla.components.support.test.whenever +import org.junit.After +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNull +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.ArgumentMatchers.anyBoolean +import org.mockito.Mockito.never +import org.mockito.Mockito.times +import org.mockito.Mockito.verify + +@RunWith(AndroidJUnit4::class) +class CreateEngineSessionMiddlewareTest { + + private val dispatcher = TestCoroutineDispatcher() + private val scope = CoroutineScope(dispatcher) + + @After + fun tearDown() { + dispatcher.cleanupTestCoroutines() + } + + @Test + fun `creates engine session if needed`() = runBlocking { + val engine: Engine = mock() + val engineSession: EngineSession = mock() + whenever(engine.createSession(anyBoolean(), any())).thenReturn(engineSession) + + val session: Session = mock() + val sessionLookup = { _: String -> session } + + val middleware = CreateEngineSessionMiddleware(engine, sessionLookup, scope) + val tab = createTab("https://www.mozilla.org", id = "1") + val store = BrowserStore( + initialState = BrowserState(tabs = listOf(tab)), + middleware = listOf(middleware) + ) + assertNull(store.state.findTab(tab.id)?.engineState?.engineSession) + + store.dispatch(EngineAction.CreateEngineSessionAction(tab.id)).joinBlocking() + store.waitUntilIdle() + dispatcher.advanceUntilIdle() + verify(engine, times(1)).createSession(false) + assertEquals(engineSession, store.state.findTab(tab.id)?.engineState?.engineSession) + + store.dispatch(EngineAction.CreateEngineSessionAction(tab.id)).joinBlocking() + store.waitUntilIdle() + dispatcher.advanceUntilIdle() + verify(engine, times(1)).createSession(false) + assertEquals(engineSession, store.state.findTab(tab.id)?.engineState?.engineSession) + } + + @Test + fun `restores engine session state if available`() = runBlocking { + val engine: Engine = mock() + val engineSession: EngineSession = mock() + whenever(engine.createSession(anyBoolean(), any())).thenReturn(engineSession) + val engineSessionState: EngineSessionState = mock() + val session: Session = mock() + val sessionLookup = { _: String -> session } + + val middleware = CreateEngineSessionMiddleware(engine, sessionLookup, scope) + val tab = createTab("https://www.mozilla.org", id = "1") + val store = BrowserStore( + initialState = BrowserState(tabs = listOf(tab)), + middleware = listOf(middleware) + ) + assertNull(store.state.findTab(tab.id)?.engineState?.engineSession) + + store.dispatch(EngineAction.UpdateEngineSessionStateAction(tab.id, engineSessionState)).joinBlocking() + store.dispatch(EngineAction.CreateEngineSessionAction(tab.id)).joinBlocking() + store.waitUntilIdle() + dispatcher.advanceUntilIdle() + + verify(engineSession).restoreState(engineSessionState) + Unit + } + + @Test + fun `creates no engine session if tab does not exist`() = runBlocking { + val engine: Engine = mock() + val session: Session = mock() + val sessionLookup = { _: String -> session } + + val middleware = CreateEngineSessionMiddleware(engine, sessionLookup, scope) + val store = BrowserStore( + initialState = BrowserState(tabs = listOf()), + middleware = listOf(middleware) + ) + + store.dispatch(EngineAction.CreateEngineSessionAction("invalid")).joinBlocking() + store.waitUntilIdle() + dispatcher.advanceUntilIdle() + + verify(engine, never()).createSession(anyBoolean(), any()) + Unit + } + + @Test + fun `creates no engine session if session does not exist`() = runBlocking { + val engine: Engine = mock() + val sessionLookup = { _: String -> null } + + val middleware = CreateEngineSessionMiddleware(engine, sessionLookup, scope) + val tab = createTab("https://www.mozilla.org", id = "1") + val store = BrowserStore( + initialState = BrowserState(tabs = listOf(tab)), + middleware = listOf(middleware) + ) + + store.dispatch(EngineAction.CreateEngineSessionAction(tab.id)).joinBlocking() + store.waitUntilIdle() + dispatcher.advanceUntilIdle() + + verify(engine, never()).createSession(anyBoolean(), any()) + Unit + } +} diff --git a/components/browser/session/src/test/java/mozilla/components/browser/session/engine/middleware/EngineDelegateMiddlewareTest.kt b/components/browser/session/src/test/java/mozilla/components/browser/session/engine/middleware/EngineDelegateMiddlewareTest.kt new file mode 100644 index 00000000000..fc8478b171b --- /dev/null +++ b/components/browser/session/src/test/java/mozilla/components/browser/session/engine/middleware/EngineDelegateMiddlewareTest.kt @@ -0,0 +1,754 @@ +/* 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.session.engine.middleware + +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.test.TestCoroutineDispatcher +import mozilla.components.browser.session.Session +import mozilla.components.browser.session.engine.EngineMiddleware +import mozilla.components.browser.state.action.EngineAction +import mozilla.components.browser.state.state.BrowserState +import mozilla.components.browser.state.state.EngineState +import mozilla.components.browser.state.state.createTab +import mozilla.components.browser.state.store.BrowserStore +import mozilla.components.concept.engine.Engine +import mozilla.components.concept.engine.EngineSession +import mozilla.components.support.test.ext.joinBlocking +import mozilla.components.support.test.mock +import mozilla.components.support.test.whenever +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNull +import org.junit.Test +import org.mockito.ArgumentMatchers +import org.mockito.Mockito.doReturn +import org.mockito.Mockito.never +import org.mockito.Mockito.times +import org.mockito.Mockito.verify + +class EngineDelegateMiddlewareTest { + @Test + fun `LoadUrlAction for tab without engine session`() { + val engineSession: EngineSession = mock() + val engine: Engine = mock() + doReturn(engineSession).`when`(engine).createSession() + + val dispatcher = TestCoroutineDispatcher() + val scope = CoroutineScope(dispatcher) + + val tab = createTab("https://www.mozilla.org", id = "test-tab") + val session: Session = mock() + whenever(session.id).thenReturn(tab.id) + val store = BrowserStore( + middleware = EngineMiddleware.create( + engine = engine, + sessionLookup = { session }, + scope = scope + ), + initialState = BrowserState( + tabs = listOf(tab) + ) + ) + + store.dispatch(EngineAction.LoadUrlAction( + "test-tab", + "https://www.firefox.com" + )).joinBlocking() + + dispatcher.advanceUntilIdle() + + verify(engine).createSession(private = false, contextId = null) + verify(engineSession, times(1)).loadUrl("https://www.firefox.com") + assertEquals(engineSession, store.state.tabs[0].engineState.engineSession) + } + + @Test + fun `LoadUrlAction for private tab without engine session`() { + val engineSession: EngineSession = mock() + val engine: Engine = mock() + doReturn(engineSession).`when`(engine).createSession(private = true) + + val dispatcher = TestCoroutineDispatcher() + val scope = CoroutineScope(dispatcher) + + val tab = createTab("https://www.mozilla.org", id = "test-tab", private = true) + val session: Session = mock() + whenever(session.id).thenReturn(tab.id) + val store = BrowserStore( + middleware = EngineMiddleware.create( + engine = engine, + sessionLookup = { session }, + scope = scope + ), + initialState = BrowserState( + tabs = listOf(tab) + ) + ) + + store.dispatch( + EngineAction.LoadUrlAction( + "test-tab", + "https://www.firefox.com" + ) + ).joinBlocking() + + dispatcher.advanceUntilIdle() + + verify(engine).createSession(private = true, contextId = null) + verify(engineSession, times(1)).loadUrl("https://www.firefox.com") + assertEquals(engineSession, store.state.tabs[0].engineState.engineSession) + } + + @Test + fun `LoadUrlAction for container tab without engine session`() { + val engineSession: EngineSession = mock() + val engine: Engine = mock() + doReturn(engineSession).`when`(engine).createSession(contextId = "test-container") + + val dispatcher = TestCoroutineDispatcher() + val scope = CoroutineScope(dispatcher) + + val tab = createTab("https://www.mozilla.org", id = "test-tab", contextId = "test-container") + val session: Session = mock() + whenever(session.id).thenReturn(tab.id) + val store = BrowserStore( + middleware = EngineMiddleware.create( + engine = engine, + sessionLookup = { session }, + scope = scope + ), + initialState = BrowserState( + tabs = listOf(tab) + ) + ) + + store.dispatch(EngineAction.LoadUrlAction( + "test-tab", + "https://www.firefox.com" + )).joinBlocking() + + dispatcher.advanceUntilIdle() + + verify(engine).createSession(private = false, contextId = "test-container") + verify(engineSession, times(1)).loadUrl("https://www.firefox.com") + assertEquals(engineSession, store.state.tabs[0].engineState.engineSession) + } + + @Test + fun `LoadUrlAction for tab with engine session`() { + val engineSession: EngineSession = mock() + val engine: Engine = mock() + + val dispatcher = TestCoroutineDispatcher() + val scope = CoroutineScope(dispatcher) + + val store = BrowserStore( + middleware = EngineMiddleware.create( + engine = engine, + sessionLookup = { mock() }, + scope = scope + ), + initialState = BrowserState( + tabs = listOf( + createTab("https://www.mozilla.org", id = "test-tab").copy( + engineState = EngineState(engineSession) + ) + ) + ) + ) + + store.dispatch(EngineAction.LoadUrlAction( + "test-tab", + "https://www.firefox.com" + )).joinBlocking() + + dispatcher.advanceUntilIdle() + + verify(engine, never()).createSession(ArgumentMatchers.anyBoolean(), ArgumentMatchers.anyString()) + verify(engineSession, times(1)).loadUrl("https://www.firefox.com") + assertEquals(engineSession, store.state.tabs[0].engineState.engineSession) + } + + @Test + fun `LoadUrlAction for private tab with engine session`() { + val engineSession: EngineSession = mock() + val engine: Engine = mock() + + val dispatcher = TestCoroutineDispatcher() + val scope = CoroutineScope(dispatcher) + + val store = BrowserStore( + middleware = EngineMiddleware.create( + engine = engine, + sessionLookup = { mock() }, + scope = scope + ), + initialState = BrowserState( + tabs = listOf( + createTab("https://www.mozilla.org", id = "test-tab", private = true).copy( + engineState = EngineState(engineSession) + ) + ) + ) + ) + + store.dispatch( + EngineAction.LoadUrlAction( + "test-tab", + "https://www.firefox.com" + ) + ).joinBlocking() + + dispatcher.advanceUntilIdle() + + verify(engine, never()).createSession(ArgumentMatchers.anyBoolean(), ArgumentMatchers.anyString()) + verify(engineSession, times(1)).loadUrl("https://www.firefox.com") + assertEquals(engineSession, store.state.tabs[0].engineState.engineSession) + } + + @Test + fun `LoadUrlAction for container tab with engine session`() { + val engineSession: EngineSession = mock() + val engine: Engine = mock() + + val dispatcher = TestCoroutineDispatcher() + val scope = CoroutineScope(dispatcher) + + val store = BrowserStore( + middleware = EngineMiddleware.create( + engine = engine, + sessionLookup = { mock() }, + scope = scope + ), + initialState = BrowserState( + tabs = listOf( + createTab("https://www.mozilla.org", id = "test-tab", contextId = "test-container").copy( + engineState = EngineState(engineSession) + ) + ) + ) + ) + + store.dispatch(EngineAction.LoadUrlAction( + "test-tab", + "https://www.firefox.com" + )).joinBlocking() + + dispatcher.advanceUntilIdle() + + verify(engine, never()).createSession(ArgumentMatchers.anyBoolean(), ArgumentMatchers.anyString()) + verify(engineSession, times(1)).loadUrl("https://www.firefox.com") + assertEquals(engineSession, store.state.tabs[0].engineState.engineSession) + } + + @Test + fun `LoadUrlAction for tab with parent tab`() { + val engineSession: EngineSession = mock() + val engine: Engine = mock() + doReturn(engineSession).`when`(engine).createSession() + + val parentEngineSession: EngineSession = mock() + + val dispatcher = TestCoroutineDispatcher() + val scope = CoroutineScope(dispatcher) + + val parent = createTab("https://getpocket.com", id = "parent-tab").copy( + engineState = EngineState(parentEngineSession) + ) + val tab = createTab("https://www.mozilla.org", id = "test-tab", parent = parent) + + val session: Session = mock() + whenever(session.id).thenReturn(tab.id) + val store = BrowserStore( + middleware = EngineMiddleware.create( + engine = engine, + sessionLookup = { session }, + scope = scope + ), + initialState = BrowserState( + tabs = listOf(parent, tab) + ) + ) + + store.dispatch(EngineAction.LoadUrlAction( + "test-tab", + "https://www.firefox.com" + )).joinBlocking() + + dispatcher.advanceUntilIdle() + + verify(engine).createSession(private = false, contextId = null) + verify(engineSession, times(1)).loadUrl("https://www.firefox.com", parentEngineSession) + assertEquals(parentEngineSession, store.state.tabs[0].engineState.engineSession) + assertEquals(engineSession, store.state.tabs[1].engineState.engineSession) + } + + @Test + fun `LoadUrlAction for tab with parent tab without engine session`() { + val engineSession: EngineSession = mock() + val engine: Engine = mock() + doReturn(engineSession).`when`(engine).createSession() + + val dispatcher = TestCoroutineDispatcher() + val scope = CoroutineScope(dispatcher) + + val parent = createTab("https://getpocket.com", id = "parent-tab") + val tab = createTab("https://www.mozilla.org", id = "test-tab", parent = parent) + + val session: Session = mock() + whenever(session.id).thenReturn(tab.id) + val store = BrowserStore( + middleware = EngineMiddleware.create( + engine = engine, + sessionLookup = { session }, + scope = scope + ), + initialState = BrowserState( + tabs = listOf(parent, tab) + ) + ) + + store.dispatch(EngineAction.LoadUrlAction( + "test-tab", + "https://www.firefox.com" + )).joinBlocking() + + dispatcher.advanceUntilIdle() + + verify(engine, times(1)).createSession(private = false, contextId = null) + verify(engineSession, times(1)).loadUrl("https://www.firefox.com") + assertEquals(engineSession, store.state.tabs[1].engineState.engineSession) + } + + @Test + fun `LoadUrlAction with flags and additional headers`() { + val engineSession: EngineSession = mock() + + val dispatcher = TestCoroutineDispatcher() + val scope = CoroutineScope(dispatcher) + + val store = BrowserStore( + middleware = EngineMiddleware.create( + engine = mock(), + sessionLookup = { mock() }, + scope = scope + ), + initialState = BrowserState( + tabs = listOf( + createTab("https://www.mozilla.org", id = "test-tab").copy( + engineState = EngineState(engineSession) + ) + ) + ) + ) + + store.dispatch(EngineAction.LoadUrlAction( + "test-tab", + "https://www.firefox.com", + EngineSession.LoadUrlFlags.external(), + mapOf( + "X-Coffee" to "Large", + "X-Sugar" to "None" + ) + )).joinBlocking() + + dispatcher.advanceUntilIdle() + + verify(engineSession, times(1)).loadUrl( + "https://www.firefox.com", + flags = EngineSession.LoadUrlFlags.external(), + additionalHeaders = mapOf( + "X-Coffee" to "Large", + "X-Sugar" to "None" + ) + ) + assertEquals(engineSession, store.state.tabs[0].engineState.engineSession) + } + + @Test + fun `LoadUrlAction for tab with same url and without engine session`() { + val engineSession: EngineSession = mock() + val engine: Engine = mock() + doReturn(engineSession).`when`(engine).createSession() + + val dispatcher = TestCoroutineDispatcher() + val scope = CoroutineScope(dispatcher) + + val tab = createTab("https://www.mozilla.org", id = "test-tab") + val session: Session = mock() + whenever(session.id).thenReturn(tab.id) + val store = BrowserStore( + middleware = EngineMiddleware.create( + engine = engine, + sessionLookup = { session }, + scope = scope + ), + initialState = BrowserState( + tabs = listOf(tab) + ) + ) + + store.dispatch(EngineAction.LoadUrlAction( + "test-tab", + "https://www.mozilla.org" + )).joinBlocking() + + dispatcher.advanceUntilIdle() + + verify(engine).createSession(private = false, contextId = null) + verify(engineSession, times(1)).loadUrl("https://www.mozilla.org") + assertEquals(engineSession, store.state.tabs[0].engineState.engineSession) + } + + @Test + fun `LoadUrlAction for not existing tab`() { + val engine: Engine = mock() + + val dispatcher = TestCoroutineDispatcher() + val scope = CoroutineScope(dispatcher) + + val store = BrowserStore( + middleware = EngineMiddleware.create( + engine = engine, + sessionLookup = { mock() }, + scope = scope + ), + initialState = BrowserState( + tabs = listOf( + createTab("https://www.mozilla.org", id = "test-tab") + ) + ) + ) + + store.dispatch(EngineAction.LoadUrlAction( + "unknown-tab", + "https://www.mozilla.org" + )).joinBlocking() + + dispatcher.advanceUntilIdle() + + verify(engine, never()).createSession(ArgumentMatchers.anyBoolean(), ArgumentMatchers.anyString()) + assertNull(store.state.tabs[0].engineState.engineSession) + } + + @Test + fun `LoadDataAction for tab without EngineSession`() { + val engineSession: EngineSession = mock() + val engine: Engine = mock() + doReturn(engineSession).`when`(engine).createSession() + + val dispatcher = TestCoroutineDispatcher() + val scope = CoroutineScope(dispatcher) + + val tab = createTab("https://www.mozilla.org", id = "test-tab") + val session: Session = mock() + whenever(session.id).thenReturn(tab.id) + val store = BrowserStore( + middleware = EngineMiddleware.create( + engine = engine, + sessionLookup = { session }, + scope = scope + ), + initialState = BrowserState( + tabs = listOf(tab) + ) + ) + + store.dispatch(EngineAction.LoadDataAction( + "test-tab", + data = "foobar data", + mimeType = "something/important", + encoding = "UTF-16" + )).joinBlocking() + + dispatcher.advanceUntilIdle() + + verify(engine).createSession(private = false, contextId = null) + verify(engineSession, times(1)).loadData( + data = "foobar data", + mimeType = "something/important", + encoding = "UTF-16" + ) + assertEquals(engineSession, store.state.tabs[0].engineState.engineSession) + } + + @Test + fun `ReloadAction for tab without EngineSession`() { + val engineSession: EngineSession = mock() + val engine: Engine = mock() + doReturn(engineSession).`when`(engine).createSession() + + val dispatcher = TestCoroutineDispatcher() + val scope = CoroutineScope(dispatcher) + + val tab = createTab("https://www.mozilla.org", id = "test-tab") + val session: Session = mock() + whenever(session.id).thenReturn(tab.id) + val store = BrowserStore( + middleware = EngineMiddleware.create( + engine = engine, + sessionLookup = { session }, + scope = scope + ), + initialState = BrowserState( + tabs = listOf(tab) + ) + ) + + store.dispatch(EngineAction.ReloadAction( + "test-tab", + flags = EngineSession.LoadUrlFlags.select(EngineSession.LoadUrlFlags.BYPASS_CACHE) + )).joinBlocking() + + dispatcher.advanceUntilIdle() + + verify(engine).createSession(private = false, contextId = null) + verify(engineSession, times(1)).reload( + EngineSession.LoadUrlFlags.select(EngineSession.LoadUrlFlags.BYPASS_CACHE) + ) + assertEquals(engineSession, store.state.tabs[0].engineState.engineSession) + } + + @Test + fun `GoForwardAction for tab without EngineSession`() { + val engineSession: EngineSession = mock() + val engine: Engine = mock() + doReturn(engineSession).`when`(engine).createSession() + + val dispatcher = TestCoroutineDispatcher() + val scope = CoroutineScope(dispatcher) + + val tab = createTab("https://www.mozilla.org", id = "test-tab") + val session: Session = mock() + whenever(session.id).thenReturn(tab.id) + val store = BrowserStore( + middleware = EngineMiddleware.create( + engine = engine, + sessionLookup = { session }, + scope = scope + ), + initialState = BrowserState( + tabs = listOf(tab) + ) + ) + + store.dispatch(EngineAction.GoForwardAction( + "test-tab" + )).joinBlocking() + + dispatcher.advanceUntilIdle() + + verify(engine).createSession(private = false, contextId = null) + verify(engineSession, times(1)).goForward() + assertEquals(engineSession, store.state.tabs[0].engineState.engineSession) + } + + @Test + fun `GoBackAction for tab without EngineSession`() { + val engineSession: EngineSession = mock() + val engine: Engine = mock() + doReturn(engineSession).`when`(engine).createSession() + + val dispatcher = TestCoroutineDispatcher() + val scope = CoroutineScope(dispatcher) + + val tab = createTab("https://www.mozilla.org", id = "test-tab") + val session: Session = mock() + whenever(session.id).thenReturn(tab.id) + val store = BrowserStore( + middleware = EngineMiddleware.create( + engine = engine, + sessionLookup = { session }, + scope = scope + ), + initialState = BrowserState( + tabs = listOf(tab) + ) + ) + + store.dispatch(EngineAction.GoBackAction( + "test-tab" + )).joinBlocking() + + dispatcher.advanceUntilIdle() + + verify(engine).createSession(private = false, contextId = null) + verify(engineSession, times(1)).goBack() + assertEquals(engineSession, store.state.tabs[0].engineState.engineSession) + } + + @Test + fun `GoToHistoryIndexAction for tab without EngineSession`() { + val engineSession: EngineSession = mock() + val engine: Engine = mock() + doReturn(engineSession).`when`(engine).createSession() + + val dispatcher = TestCoroutineDispatcher() + val scope = CoroutineScope(dispatcher) + + val tab = createTab("https://www.mozilla.org", id = "test-tab") + val session: Session = mock() + whenever(session.id).thenReturn(tab.id) + val store = BrowserStore( + middleware = EngineMiddleware.create( + engine = engine, + sessionLookup = { session }, + scope = scope + ), + initialState = BrowserState( + tabs = listOf(tab) + ) + ) + + store.dispatch(EngineAction.GoToHistoryIndexAction( + "test-tab", + index = 42 + )).joinBlocking() + + dispatcher.advanceUntilIdle() + + verify(engine).createSession(private = false, contextId = null) + verify(engineSession, times(1)).goToHistoryIndex(42) + assertEquals(engineSession, store.state.tabs[0].engineState.engineSession) + } + + @Test + fun `ToggleDesktopModeAction - Enable desktop mode`() { + val engineSession: EngineSession = mock() + val engine: Engine = mock() + doReturn(engineSession).`when`(engine).createSession() + + val dispatcher = TestCoroutineDispatcher() + val scope = CoroutineScope(dispatcher) + + val tab = createTab("https://www.mozilla.org", id = "test-tab") + val session: Session = mock() + whenever(session.id).thenReturn(tab.id) + val store = BrowserStore( + middleware = EngineMiddleware.create( + engine = engine, + sessionLookup = { session }, + scope = scope + ), + initialState = BrowserState( + tabs = listOf(tab) + ) + ) + + store.dispatch(EngineAction.ToggleDesktopModeAction( + "test-tab", + enable = true + )).joinBlocking() + + dispatcher.advanceUntilIdle() + + verify(engine).createSession(private = false, contextId = null) + verify(engineSession, times(1)).toggleDesktopMode(enable = true, reload = true) + assertEquals(engineSession, store.state.tabs[0].engineState.engineSession) + } + + @Test + fun `ToggleDesktopModeAction - Disable desktop mode`() { + val engineSession: EngineSession = mock() + val engine: Engine = mock() + doReturn(engineSession).`when`(engine).createSession() + + val dispatcher = TestCoroutineDispatcher() + val scope = CoroutineScope(dispatcher) + + val tab = createTab("https://www.mozilla.org", id = "test-tab") + val session: Session = mock() + whenever(session.id).thenReturn(tab.id) + val store = BrowserStore( + middleware = EngineMiddleware.create( + engine = engine, + sessionLookup = { session }, + scope = scope + ), + initialState = BrowserState( + tabs = listOf(tab) + ) + ) + + store.dispatch(EngineAction.ToggleDesktopModeAction( + "test-tab", + enable = false + )).joinBlocking() + + dispatcher.advanceUntilIdle() + + verify(engine).createSession(private = false, contextId = null) + verify(engineSession, times(1)).toggleDesktopMode(enable = false, reload = true) + assertEquals(engineSession, store.state.tabs[0].engineState.engineSession) + } + + @Test + fun `ExitFullscreenModeAction for tab without EngineSession`() { + val engineSession: EngineSession = mock() + val engine: Engine = mock() + doReturn(engineSession).`when`(engine).createSession() + + val dispatcher = TestCoroutineDispatcher() + val scope = CoroutineScope(dispatcher) + + val tab = createTab("https://www.mozilla.org", id = "test-tab") + val session: Session = mock() + whenever(session.id).thenReturn(tab.id) + val store = BrowserStore( + middleware = EngineMiddleware.create( + engine = engine, + sessionLookup = { session }, + scope = scope + ), + initialState = BrowserState( + tabs = listOf(tab) + ) + ) + + store.dispatch(EngineAction.ExitFullscreenModeAction( + "test-tab" + )).joinBlocking() + + dispatcher.advanceUntilIdle() + + verify(engine).createSession(private = false, contextId = null) + verify(engineSession, times(1)).exitFullScreenMode() + assertEquals(engineSession, store.state.tabs[0].engineState.engineSession) + } + + @Test + fun `ClearDataAction for tab without EngineSession`() { + val engineSession: EngineSession = mock() + val engine: Engine = mock() + doReturn(engineSession).`when`(engine).createSession() + + val dispatcher = TestCoroutineDispatcher() + val scope = CoroutineScope(dispatcher) + + val tab = createTab("https://www.mozilla.org", id = "test-tab") + val session: Session = mock() + whenever(session.id).thenReturn(tab.id) + val store = BrowserStore( + middleware = EngineMiddleware.create( + engine = engine, + sessionLookup = { session }, + scope = scope + ), + initialState = BrowserState( + tabs = listOf(tab) + ) + ) + + store.dispatch(EngineAction.ClearDataAction( + "test-tab", + data = Engine.BrowsingData.allCaches() + )).joinBlocking() + + dispatcher.advanceUntilIdle() + + verify(engine).createSession(private = false, contextId = null) + verify(engineSession, times(1)).clearData(Engine.BrowsingData.allCaches()) + assertEquals(engineSession, store.state.tabs[0].engineState.engineSession) + } +} diff --git a/components/browser/session/src/test/java/mozilla/components/browser/session/engine/middleware/LinkingMiddlewareTest.kt b/components/browser/session/src/test/java/mozilla/components/browser/session/engine/middleware/LinkingMiddlewareTest.kt new file mode 100644 index 00000000000..b83002752fe --- /dev/null +++ b/components/browser/session/src/test/java/mozilla/components/browser/session/engine/middleware/LinkingMiddlewareTest.kt @@ -0,0 +1,176 @@ +/* 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.session.engine.middleware + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import kotlinx.coroutines.runBlocking +import mozilla.components.browser.session.Session +import mozilla.components.browser.state.action.EngineAction +import mozilla.components.browser.state.selector.findTab +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.support.test.any +import mozilla.components.support.test.ext.joinBlocking +import mozilla.components.support.test.libstate.ext.waitUntilIdle +import mozilla.components.support.test.mock +import mozilla.components.support.test.whenever +import org.junit.Assert.assertNotNull +import org.junit.Assert.assertNull +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mockito.anyString +import org.mockito.Mockito.never +import org.mockito.Mockito.verify + +@RunWith(AndroidJUnit4::class) +class LinkingMiddlewareTest { + + @Test + fun `loads URL after linking`() { + val middleware = LinkingMiddleware { null } + + val tab = createTab("https://www.mozilla.org", id = "1") + val store = BrowserStore( + initialState = BrowserState(tabs = listOf(tab)), + middleware = listOf(middleware) + ) + + val engineSession: EngineSession = mock() + store.dispatch(EngineAction.LinkEngineSessionAction(tab.id, engineSession)).joinBlocking() + verify(engineSession).loadUrl(tab.content.url) + } + + @Test + fun `loads URL with parent after linking`() { + val middleware = LinkingMiddleware { null } + + val parent = createTab("https://www.mozilla.org", id = "1") + val child = createTab("https://www.firefox.com", id = "2", parent = parent) + val store = BrowserStore( + initialState = BrowserState( + tabs = listOf(parent, child) + ), + middleware = listOf(middleware) + ) + + val parentEngineSession: EngineSession = mock() + store.dispatch(EngineAction.LinkEngineSessionAction(parent.id, parentEngineSession)).joinBlocking() + + val childEngineSession: EngineSession = mock() + store.dispatch(EngineAction.LinkEngineSessionAction(child.id, childEngineSession)).joinBlocking() + verify(childEngineSession).loadUrl(child.content.url, parentEngineSession) + } + + @Test + fun `loads URL without parent for extension URLs`() { + val middleware = LinkingMiddleware { null } + + val parent = createTab("https://www.mozilla.org", id = "1") + val child = createTab("moz-extension://1234", id = "2", parent = parent) + val store = BrowserStore( + initialState = BrowserState( + tabs = listOf(parent, child) + ), + middleware = listOf(middleware) + ) + + val parentEngineSession: EngineSession = mock() + store.dispatch(EngineAction.LinkEngineSessionAction(parent.id, parentEngineSession)).joinBlocking() + + val childEngineSession: EngineSession = mock() + store.dispatch(EngineAction.LinkEngineSessionAction(child.id, childEngineSession)).joinBlocking() + verify(childEngineSession).loadUrl(child.content.url) + } + + @Test + fun `skips loading URL if specified in action`() { + val middleware = LinkingMiddleware { null } + + val tab = createTab("https://www.mozilla.org", id = "1") + val store = BrowserStore( + initialState = BrowserState(tabs = listOf(tab)), + middleware = listOf(middleware) + ) + + val engineSession: EngineSession = mock() + store.dispatch(EngineAction.LinkEngineSessionAction(tab.id, engineSession, skipLoading = true)).joinBlocking() + verify(engineSession, never()).loadUrl(tab.content.url) + } + + @Test + fun `does nothing if linked tab does not exist`() { + val middleware = LinkingMiddleware { null } + + val store = BrowserStore( + initialState = BrowserState(tabs = listOf()), + middleware = listOf(middleware) + ) + + val engineSession: EngineSession = mock() + store.dispatch(EngineAction.LinkEngineSessionAction("invalid", engineSession)).joinBlocking() + verify(engineSession, never()).loadUrl(anyString(), any(), any(), any()) + } + + @Test + fun `registers engine observer after linking`() = runBlocking { + val tab1 = createTab("https://www.mozilla.org", id = "1") + val tab2 = createTab("https://www.mozilla.org", id = "2") + + val session2: Session = mock() + whenever(session2.id).thenReturn(tab2.id) + val sessionLookup = { id: String -> if (id == tab2.id) session2 else null } + val middleware = LinkingMiddleware(sessionLookup) + + val store = BrowserStore( + initialState = BrowserState(tabs = listOf(tab1, tab2)), + middleware = listOf(middleware) + ) + + val engineSession1: EngineSession = mock() + val engineSession2: EngineSession = mock() + store.dispatch(EngineAction.LinkEngineSessionAction(tab1.id, engineSession1)).joinBlocking() + store.dispatch(EngineAction.LinkEngineSessionAction(tab2.id, engineSession2)).joinBlocking() + store.waitUntilIdle() + + // We only have a session for tab2 so we should only register an observer for tab2 + val engineObserver = store.state.findTab(tab2.id)?.engineState?.engineObserver + assertNotNull(engineObserver) + + verify(engineSession2).register(engineObserver!!) + engineObserver.onTitleChange("test") + verify(session2).title = "test" + } + + @Test + fun `unregisters engine observer before unlinking`() = runBlocking { + val tab1 = createTab("https://www.mozilla.org", id = "1") + val tab2 = createTab("https://www.mozilla.org", id = "2") + + val session1: Session = mock() + whenever(session1.id).thenReturn(tab1.id) + + val sessionLookup = { id: String -> if (id == tab1.id) session1 else null } + val middleware = LinkingMiddleware(sessionLookup) + + val store = BrowserStore( + initialState = BrowserState(tabs = listOf(tab1, tab2)), + middleware = listOf(middleware) + ) + + val engineSession: EngineSession = mock() + store.dispatch(EngineAction.LinkEngineSessionAction(tab1.id, engineSession)).joinBlocking() + store.waitUntilIdle() + assertNotNull(store.state.findTab(tab1.id)?.engineState?.engineObserver) + assertNull(store.state.findTab(tab2.id)?.engineState?.engineObserver) + + store.dispatch(EngineAction.UnlinkEngineSessionAction(tab1.id)).joinBlocking() + store.dispatch(EngineAction.UnlinkEngineSessionAction(tab2.id)).joinBlocking() + store.waitUntilIdle() + assertNull(store.state.findTab(tab1.id)?.engineState?.engineObserver) + assertNull(store.state.findTab(tab2.id)?.engineState?.engineObserver) + } +} diff --git a/components/browser/session/src/test/java/mozilla/components/browser/session/engine/middleware/SuspendMiddlewareTest.kt b/components/browser/session/src/test/java/mozilla/components/browser/session/engine/middleware/SuspendMiddlewareTest.kt new file mode 100644 index 00000000000..3df91d5e902 --- /dev/null +++ b/components/browser/session/src/test/java/mozilla/components/browser/session/engine/middleware/SuspendMiddlewareTest.kt @@ -0,0 +1,93 @@ +/* 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.session.engine.middleware + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.test.TestCoroutineDispatcher +import mozilla.components.browser.state.action.EngineAction +import mozilla.components.browser.state.selector.findTab +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.EngineSessionState +import mozilla.components.support.test.ext.joinBlocking +import mozilla.components.support.test.libstate.ext.waitUntilIdle +import mozilla.components.support.test.mock +import mozilla.components.support.test.whenever +import org.junit.After +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNull +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mockito.never +import org.mockito.Mockito.spy +import org.mockito.Mockito.verify + +@RunWith(AndroidJUnit4::class) +class SuspendMiddlewareTest { + + private val dispatcher = TestCoroutineDispatcher() + private val scope = CoroutineScope(dispatcher) + + @After + fun tearDown() { + dispatcher.cleanupTestCoroutines() + } + + @Test + fun `suspends engine session`() = runBlocking { + val middleware = SuspendMiddleware(scope) + + val tab = createTab("https://www.mozilla.org", id = "1") + val store = BrowserStore( + initialState = BrowserState(tabs = listOf(tab)), + middleware = listOf(middleware) + ) + + val engineSession: EngineSession = mock() + val state: EngineSessionState = mock() + whenever(engineSession.saveState()).thenReturn(state) + store.dispatch(EngineAction.LinkEngineSessionAction(tab.id, engineSession)).joinBlocking() + + store.dispatch(EngineAction.SuspendEngineSessionAction(tab.id)).joinBlocking() + verify(engineSession).saveState() + + store.waitUntilIdle() + dispatcher.advanceUntilIdle() + assertNull(store.state.findTab(tab.id)?.engineState?.engineSession) + assertEquals(state, store.state.findTab(tab.id)?.engineState?.engineSessionState) + verify(engineSession).close() + } + + @Test + fun `does nothing if tab doesn't exist`() { + val middleware = SuspendMiddleware(scope) + + val store = spy(BrowserStore( + initialState = BrowserState(tabs = listOf()), + middleware = listOf(middleware) + )) + + store.dispatch(EngineAction.SuspendEngineSessionAction("invalid")).joinBlocking() + verify(store, never()).dispatch(EngineAction.UnlinkEngineSessionAction("invalid")) + } + + @Test + fun `does nothing if engine session doesn't exist`() { + val middleware = SuspendMiddleware(scope) + + val tab = createTab("https://www.mozilla.org", id = "1") + val store = spy(BrowserStore( + initialState = BrowserState(tabs = listOf(tab)), + middleware = listOf(middleware) + )) + + store.dispatch(EngineAction.SuspendEngineSessionAction(tab.id)).joinBlocking() + verify(store, never()).dispatch(EngineAction.UnlinkEngineSessionAction(tab.id)) + } +} diff --git a/components/browser/session/src/test/java/mozilla/components/browser/session/engine/middleware/TabsRemovedMiddlewareTest.kt b/components/browser/session/src/test/java/mozilla/components/browser/session/engine/middleware/TabsRemovedMiddlewareTest.kt new file mode 100644 index 00000000000..743c15995d5 --- /dev/null +++ b/components/browser/session/src/test/java/mozilla/components/browser/session/engine/middleware/TabsRemovedMiddlewareTest.kt @@ -0,0 +1,226 @@ +/* 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.session.engine.middleware + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.test.TestCoroutineDispatcher +import mozilla.components.browser.state.action.BrowserAction +import mozilla.components.browser.state.action.CustomTabListAction +import mozilla.components.browser.state.action.EngineAction +import mozilla.components.browser.state.action.TabListAction +import mozilla.components.browser.state.selector.findCustomTab +import mozilla.components.browser.state.selector.findTab +import mozilla.components.browser.state.selector.findTabOrCustomTab +import mozilla.components.browser.state.state.BrowserState +import mozilla.components.browser.state.state.createCustomTab +import mozilla.components.browser.state.state.createTab +import mozilla.components.browser.state.store.BrowserStore +import mozilla.components.concept.engine.EngineSession +import mozilla.components.lib.state.Middleware +import mozilla.components.lib.state.MiddlewareStore +import mozilla.components.support.test.ext.joinBlocking +import mozilla.components.support.test.libstate.ext.waitUntilIdle +import mozilla.components.support.test.mock +import org.junit.After +import org.junit.Assert.assertNotNull +import org.junit.Assert.assertNull +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mockito.never +import org.mockito.Mockito.spy +import org.mockito.Mockito.verify + +@RunWith(AndroidJUnit4::class) +class TabsRemovedMiddlewareTest { + + private val dispatcher = TestCoroutineDispatcher() + private val scope = CoroutineScope(dispatcher) + + @After + fun tearDown() { + dispatcher.cleanupTestCoroutines() + } + + @Test + fun `closes and unlinks engine session when tab is removed`() = runBlocking { + val middleware = TabsRemovedMiddleware(scope) + + val tab = createTab("https://www.mozilla.org", id = "1") + val store = spy(BrowserStore( + initialState = BrowserState(tabs = listOf(tab)), + middleware = listOf(middleware, ConsumeRemoveTabActionsMiddleware()) + )) + + val engineSession = linkEngineSession(store, tab.id) + store.dispatch(TabListAction.RemoveTabAction(tab.id)).joinBlocking() + store.waitUntilIdle() + dispatcher.advanceUntilIdle() + + assertNull(store.state.findTab(tab.id)?.engineState?.engineSession) + verify(engineSession).close() + } + + @Test + fun `closes and unlinks engine session when all normal tabs are removed`() = runBlocking { + val middleware = TabsRemovedMiddleware(scope) + + val tab1 = createTab("https://www.mozilla.org", id = "1", private = false) + val tab2 = createTab("https://www.firefox.com", id = "2", private = false) + val tab3 = createTab("https://www.getpocket.com", id = "3", private = true) + val store = spy(BrowserStore( + initialState = BrowserState(tabs = listOf(tab1, tab2, tab3)), + middleware = listOf(middleware, ConsumeRemoveTabActionsMiddleware()) + )) + + val engineSession1 = linkEngineSession(store, tab1.id) + val engineSession2 = linkEngineSession(store, tab2.id) + val engineSession3 = linkEngineSession(store, tab3.id) + + store.dispatch(TabListAction.RemoveAllNormalTabsAction).joinBlocking() + store.waitUntilIdle() + dispatcher.advanceUntilIdle() + + assertNull(store.state.findTab(tab1.id)?.engineState?.engineSession) + assertNull(store.state.findTab(tab2.id)?.engineState?.engineSession) + assertNotNull(store.state.findTab(tab3.id)?.engineState?.engineSession) + verify(engineSession1).close() + verify(engineSession2).close() + verify(engineSession3, never()).close() + } + + @Test + fun `closes and unlinks engine session when all private tabs are removed`() = runBlocking { + val middleware = TabsRemovedMiddleware(scope) + + val tab1 = createTab("https://www.mozilla.org", id = "1", private = true) + val tab2 = createTab("https://www.firefox.com", id = "2", private = true) + val tab3 = createTab("https://www.getpocket.com", id = "3", private = false) + val store = spy(BrowserStore( + initialState = BrowserState(tabs = listOf(tab1, tab2, tab3)), + middleware = listOf(middleware, ConsumeRemoveTabActionsMiddleware()) + )) + + val engineSession1 = linkEngineSession(store, tab1.id) + val engineSession2 = linkEngineSession(store, tab2.id) + val engineSession3 = linkEngineSession(store, tab3.id) + + store.dispatch(TabListAction.RemoveAllPrivateTabsAction).joinBlocking() + store.waitUntilIdle() + dispatcher.advanceUntilIdle() + + assertNull(store.state.findTab(tab1.id)?.engineState?.engineSession) + assertNull(store.state.findTab(tab2.id)?.engineState?.engineSession) + assertNotNull(store.state.findTab(tab3.id)?.engineState?.engineSession) + verify(engineSession1).close() + verify(engineSession2).close() + verify(engineSession3, never()).close() + } + + @Test + fun `closes and unlinks engine session when all tabs are removed`() = runBlocking { + val middleware = TabsRemovedMiddleware(scope) + + val tab1 = createTab("https://www.mozilla.org", id = "1", private = true) + val tab2 = createTab("https://www.firefox.com", id = "2", private = false) + val tab3 = createCustomTab("https://www.getpocket.com", id = "3") + val store = spy(BrowserStore( + initialState = BrowserState(tabs = listOf(tab1, tab2), customTabs = listOf(tab3)), + middleware = listOf(middleware, ConsumeRemoveTabActionsMiddleware()) + )) + + val engineSession1 = linkEngineSession(store, tab1.id) + val engineSession2 = linkEngineSession(store, tab2.id) + val engineSession3 = linkEngineSession(store, tab3.id) + + store.dispatch(TabListAction.RemoveAllTabsAction).joinBlocking() + store.waitUntilIdle() + dispatcher.advanceUntilIdle() + + assertNull(store.state.findTab(tab1.id)?.engineState?.engineSession) + assertNull(store.state.findTab(tab2.id)?.engineState?.engineSession) + assertNotNull(store.state.findCustomTab(tab3.id)?.engineState?.engineSession) + verify(engineSession1).close() + verify(engineSession2).close() + verify(engineSession3, never()).close() + } + + @Test + fun `closes and unlinks engine session when custom tab is removed`() = runBlocking { + val middleware = TabsRemovedMiddleware(scope) + + val tab = createCustomTab("https://www.mozilla.org", id = "1") + val store = spy(BrowserStore( + initialState = BrowserState(customTabs = listOf(tab)), + middleware = listOf(middleware, ConsumeRemoveTabActionsMiddleware()) + )) + + val engineSession = linkEngineSession(store, tab.id) + store.dispatch(CustomTabListAction.RemoveCustomTabAction(tab.id)).joinBlocking() + store.waitUntilIdle() + dispatcher.advanceUntilIdle() + + assertNull(store.state.findTab(tab.id)?.engineState?.engineSession) + verify(engineSession).close() + } + + @Test + fun `closes and unlinks engine session when all custom tabs are removed`() = runBlocking { + val middleware = TabsRemovedMiddleware(scope) + + val tab1 = createCustomTab("https://www.mozilla.org", id = "1") + val tab2 = createCustomTab("https://www.firefox.com", id = "2") + val tab3 = createTab("https://www.getpocket.com", id = "3") + val store = spy(BrowserStore( + initialState = BrowserState(customTabs = listOf(tab1, tab2), tabs = listOf(tab3)), + middleware = listOf(middleware, ConsumeRemoveTabActionsMiddleware()) + )) + + val engineSession1 = linkEngineSession(store, tab1.id) + val engineSession2 = linkEngineSession(store, tab2.id) + val engineSession3 = linkEngineSession(store, tab3.id) + + store.dispatch(CustomTabListAction.RemoveAllCustomTabsAction).joinBlocking() + store.waitUntilIdle() + dispatcher.advanceUntilIdle() + + assertNull(store.state.findCustomTab(tab1.id)?.engineState?.engineSession) + assertNull(store.state.findCustomTab(tab2.id)?.engineState?.engineSession) + assertNotNull(store.state.findTab(tab3.id)?.engineState?.engineSession) + verify(engineSession1).close() + verify(engineSession2).close() + verify(engineSession3, never()).close() + } + + private fun linkEngineSession(store: BrowserStore, tabId: String): EngineSession { + val engineSession: EngineSession = mock() + store.dispatch(EngineAction.LinkEngineSessionAction(tabId, engineSession)).joinBlocking() + assertNotNull(store.state.findTabOrCustomTab(tabId)?.engineState?.engineSession) + return engineSession + } + + // This is to consume remove tab actions so we can assert that we properly unlink tabs + // before they get removed. If we didn't do this the tab would already be gone once + // TabsRemovedMiddleware processed the action. + private class ConsumeRemoveTabActionsMiddleware : Middleware { + override fun invoke( + store: MiddlewareStore, + next: (BrowserAction) -> Unit, + action: BrowserAction + ) { + when (action) { + is TabListAction.RemoveAllNormalTabsAction, + is TabListAction.RemoveAllPrivateTabsAction, + is TabListAction.RemoveAllTabsAction, + is TabListAction.RemoveTabAction, + is CustomTabListAction.RemoveAllCustomTabsAction, + is CustomTabListAction.RemoveCustomTabAction -> return + } + + next(action) + } + } +} diff --git a/components/browser/session/src/test/java/mozilla/components/browser/session/engine/middleware/TrimMemoryMiddlewareTest.kt b/components/browser/session/src/test/java/mozilla/components/browser/session/engine/middleware/TrimMemoryMiddlewareTest.kt new file mode 100644 index 00000000000..9e2ff1c2906 --- /dev/null +++ b/components/browser/session/src/test/java/mozilla/components/browser/session/engine/middleware/TrimMemoryMiddlewareTest.kt @@ -0,0 +1,122 @@ +/* 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.session.engine.middleware + +import android.content.ComponentCallbacks2 +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.test.TestCoroutineDispatcher +import mozilla.components.browser.state.action.SystemAction +import mozilla.components.browser.state.selector.findCustomTab +import mozilla.components.browser.state.selector.findTab +import mozilla.components.browser.state.state.BrowserState +import mozilla.components.browser.state.state.EngineState +import mozilla.components.browser.state.state.createCustomTab +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.EngineSessionState +import mozilla.components.support.test.ext.joinBlocking +import mozilla.components.support.test.mock +import org.junit.Assert.assertNotNull +import org.junit.Assert.assertNull +import org.junit.Before +import org.junit.Test +import org.mockito.Mockito +import org.mockito.Mockito.doReturn + +class TrimMemoryMiddlewareTest { + private lateinit var engineSessionReddit: EngineSession + private lateinit var engineSessionTheVerge: EngineSession + private lateinit var engineSessionTwitch: EngineSession + private lateinit var store: BrowserStore + private val dispatcher = TestCoroutineDispatcher() + private val scope = CoroutineScope(dispatcher) + + private lateinit var engineSessionStateReddit: EngineSessionState + private lateinit var engineSessionStateTheVerge: EngineSessionState + private lateinit var engineSessionStateTwitch: EngineSessionState + + @Before + fun setUp() { + engineSessionTheVerge = mock() + engineSessionReddit = mock() + engineSessionTwitch = mock() + + engineSessionStateReddit = mock() + engineSessionStateTheVerge = mock() + engineSessionStateTwitch = mock() + + doReturn(engineSessionStateReddit).`when`(engineSessionReddit).saveState() + doReturn(engineSessionStateTheVerge).`when`(engineSessionTheVerge).saveState() + doReturn(engineSessionStateTwitch).`when`(engineSessionTwitch).saveState() + + store = Mockito.spy( + BrowserStore( + middleware = listOf( + TrimMemoryMiddleware(), + SuspendMiddleware(scope) + ), + initialState = BrowserState( + tabs = listOf( + createTab("https://www.mozilla.org", id = "mozilla"), + createTab("https://www.theverge.com/", id = "theverge").copy( + engineState = EngineState( + engineSession = engineSessionTheVerge, + engineObserver = mock()) + ), + createTab( + "https://www.reddit.com/r/firefox/", + id = "reddit", + private = true + ).copy( + engineState = EngineState( + engineSession = engineSessionReddit, + engineObserver = mock() + ) + ), + createTab("https://github.com/", id = "github") + ), + customTabs = listOf( + createCustomTab("https://www.twitch.tv/", id = "twitch").copy( + engineState = EngineState( + engineSession = engineSessionTwitch, + engineObserver = mock() + ) + ), + createCustomTab("https://twitter.com/home", id = "twitter") + ), + selectedTabId = "reddit" + ) + ) + ) + } + + @Test + fun `TrimMemoryMiddleware - TRIM_MEMORY_UI_HIDDEN`() { + store.dispatch(SystemAction.LowMemoryAction( + level = ComponentCallbacks2.TRIM_MEMORY_UI_HIDDEN + )).joinBlocking() + + dispatcher.advanceUntilIdle() + + store.state.findTab("theverge")!!.engineState.apply { + assertNotNull(engineSession) + assertNotNull(engineObserver) + assertNull(engineSessionState) + } + + store.state.findTab("reddit")!!.engineState.apply { + assertNotNull(engineSession) + assertNotNull(engineObserver) + assertNull(engineSessionState) + } + + store.state.findCustomTab("twitch")!!.engineState.apply { + assertNotNull(engineSession) + assertNotNull(engineObserver) + assertNull(engineSessionState) + } + } +} diff --git a/components/browser/session/src/test/java/mozilla/components/browser/session/engine/middleware/WebExtensionMiddlewareTest.kt b/components/browser/session/src/test/java/mozilla/components/browser/session/engine/middleware/WebExtensionMiddlewareTest.kt new file mode 100644 index 00000000000..f77f56cd3f0 --- /dev/null +++ b/components/browser/session/src/test/java/mozilla/components/browser/session/engine/middleware/WebExtensionMiddlewareTest.kt @@ -0,0 +1,135 @@ +/* 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.session.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.support.test.ext.joinBlocking +import mozilla.components.support.test.mock +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNull +import org.junit.Test +import org.mockito.ArgumentMatchers.anyBoolean +import org.mockito.Mockito.never +import org.mockito.Mockito.verify + +class WebExtensionMiddlewareTest { + + @Test + fun `marks engine session as active when selected`() { + val middleware = WebExtensionMiddleware() + + 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(EngineAction.LinkEngineSessionAction("1", engineSession1)).joinBlocking() + store.dispatch(EngineAction.LinkEngineSessionAction("2", engineSession2)).joinBlocking() + + assertNull(middleware.activeWebExtensionTabId) + verify(engineSession1, never()).markActiveForWebExtensions(anyBoolean()) + verify(engineSession2, never()).markActiveForWebExtensions(anyBoolean()) + + store.dispatch(TabListAction.SelectTabAction("1")).joinBlocking() + assertEquals("1", middleware.activeWebExtensionTabId) + verify(engineSession1).markActiveForWebExtensions(true) + verify(engineSession2, never()).markActiveForWebExtensions(anyBoolean()) + + store.dispatch(TabListAction.SelectTabAction("2")).joinBlocking() + assertEquals("2", middleware.activeWebExtensionTabId) + verify(engineSession1).markActiveForWebExtensions(false) + verify(engineSession2).markActiveForWebExtensions(true) + } + + @Test + fun `marks selected engine session as active when linked`() { + val middleware = WebExtensionMiddleware() + + val store = BrowserStore( + initialState = BrowserState( + tabs = listOf( + createTab("https://www.mozilla.org", id = "1"), + createTab("https://www.firefox.com", id = "2") + ), + selectedTabId = "1" + ), + middleware = listOf(middleware) + ) + + val engineSession1: EngineSession = mock() + val engineSession2: EngineSession = mock() + assertNull(middleware.activeWebExtensionTabId) + verify(engineSession1, never()).markActiveForWebExtensions(anyBoolean()) + verify(engineSession2, never()).markActiveForWebExtensions(anyBoolean()) + + store.dispatch(EngineAction.LinkEngineSessionAction("1", engineSession1)).joinBlocking() + assertEquals("1", middleware.activeWebExtensionTabId) + verify(engineSession1).markActiveForWebExtensions(true) + verify(engineSession2, never()).markActiveForWebExtensions(anyBoolean()) + } + + @Test + fun `marks selected engine session as inactive when unlinked`() { + val middleware = WebExtensionMiddleware() + + val store = BrowserStore( + initialState = BrowserState( + tabs = listOf( + createTab("https://www.mozilla.org", id = "1") + ), + selectedTabId = "1" + ), + middleware = listOf(middleware) + ) + + val engineSession1: EngineSession = mock() + store.dispatch(EngineAction.LinkEngineSessionAction("1", engineSession1)).joinBlocking() + assertEquals("1", middleware.activeWebExtensionTabId) + verify(engineSession1).markActiveForWebExtensions(true) + + store.dispatch(EngineAction.UnlinkEngineSessionAction("1")).joinBlocking() + verify(engineSession1).markActiveForWebExtensions(false) + } + + @Test + fun `marks new selected engine session as active when previous one is removed`() { + val middleware = WebExtensionMiddleware() + + 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(EngineAction.LinkEngineSessionAction("1", engineSession1)).joinBlocking() + store.dispatch(EngineAction.LinkEngineSessionAction("2", engineSession2)).joinBlocking() + + store.dispatch(TabListAction.SelectTabAction("1")).joinBlocking() + assertEquals("1", middleware.activeWebExtensionTabId) + verify(engineSession2, never()).markActiveForWebExtensions(anyBoolean()) + + store.dispatch(TabListAction.RemoveTabAction("1")).joinBlocking() + assertEquals("2", middleware.activeWebExtensionTabId) + verify(engineSession2).markActiveForWebExtensions(true) + } +} diff --git a/components/browser/session/src/test/java/mozilla/components/browser/session/ext/AtomicFileKtTest.kt b/components/browser/session/src/test/java/mozilla/components/browser/session/ext/AtomicFileKtTest.kt index 00e93ad3d7d..126634d874b 100644 --- a/components/browser/session/src/test/java/mozilla/components/browser/session/ext/AtomicFileKtTest.kt +++ b/components/browser/session/src/test/java/mozilla/components/browser/session/ext/AtomicFileKtTest.kt @@ -8,8 +8,11 @@ import android.util.AtomicFile import androidx.test.ext.junit.runners.AndroidJUnit4 import mozilla.components.browser.session.Session import mozilla.components.browser.session.SessionManager +import mozilla.components.browser.session.storage.BrowserStateSerializer import mozilla.components.browser.session.storage.SnapshotSerializer import mozilla.components.browser.session.storage.getFileForEngine +import mozilla.components.browser.state.state.BrowserState +import mozilla.components.browser.state.state.createTab import mozilla.components.concept.engine.Engine import mozilla.components.concept.engine.EngineSession import mozilla.components.concept.engine.EngineSessionState @@ -40,14 +43,11 @@ class AtomicFileKtTest { val file: AtomicFile = mock() doThrow(IOException::class.java).`when`(file).startWrite() - val snapshot = SessionManager.Snapshot( - sessions = listOf( - SessionManager.Snapshot.Item(Session("http://mozilla.org")) - ), - selectedSessionIndex = 0 + val state = BrowserState( + tabs = listOf(createTab("http://mozilla.org")) ) - file.writeSnapshot(snapshot, SnapshotSerializer()) + file.writeState(state, BrowserStateSerializer()) verify(file).failWrite(any()) } @@ -92,21 +92,18 @@ class AtomicFileKtTest { `when`(engine.createSession()).thenReturn(mock(EngineSession::class.java)) `when`(engine.createSessionState(any())).thenReturn(engineSessionState) - // Engine session just for one of the sessions for simplicity. - val sessionsSnapshot = SessionManager.Snapshot( - sessions = listOf( - SessionManager.Snapshot.Item(session1), - SessionManager.Snapshot.Item(session2), - SessionManager.Snapshot.Item(session3) - ), - selectedSessionIndex = 0 - ) - val file = AtomicFile(File.createTempFile( UUID.randomUUID().toString(), UUID.randomUUID().toString())) - file.writeSnapshot(sessionsSnapshot) + val state = BrowserState(tabs = listOf( + session1.toTabSessionState(), + session2.toTabSessionState(), + session3.toTabSessionState() + ), + selectedTabId = session1.id + ) + file.writeState(state) // Read it back val restoredSnapshot = file.readSnapshot(engine) diff --git a/components/browser/session/src/test/java/mozilla/components/browser/session/storage/SessionStorageTest.kt b/components/browser/session/src/test/java/mozilla/components/browser/session/storage/SessionStorageTest.kt index 16a53b41dec..19f4f894d30 100644 --- a/components/browser/session/src/test/java/mozilla/components/browser/session/storage/SessionStorageTest.kt +++ b/components/browser/session/src/test/java/mozilla/components/browser/session/storage/SessionStorageTest.kt @@ -8,11 +8,16 @@ import androidx.lifecycle.Lifecycle import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.LifecycleRegistry import androidx.test.ext.junit.runners.AndroidJUnit4 +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Job import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.test.TestCoroutineDispatcher import mozilla.components.browser.session.Session -import mozilla.components.browser.state.state.SessionState.Source import mozilla.components.browser.session.SessionManager +import mozilla.components.browser.session.ext.toTabSessionState +import mozilla.components.browser.state.state.BrowserState +import mozilla.components.browser.state.state.SessionState.Source +import mozilla.components.browser.state.store.BrowserStore import mozilla.components.concept.engine.Engine import mozilla.components.concept.engine.EngineSession import mozilla.components.concept.engine.EngineSessionState @@ -62,19 +67,17 @@ class SessionStorageTest { `when`(engine.createSession()).thenReturn(mock(EngineSession::class.java)) `when`(engine.createSessionState(any())).thenReturn(engineSessionState) - // Engine session just for one of the sessions for simplicity. - val sessionsSnapshot = SessionManager.Snapshot( - sessions = listOf( - SessionManager.Snapshot.Item(session1), - SessionManager.Snapshot.Item(session2), - SessionManager.Snapshot.Item(session3) + // Persist the state + val state = BrowserState(tabs = listOf( + session1.toTabSessionState(), + session2.toTabSessionState(), + session3.toTabSessionState() ), - selectedSessionIndex = 0 + selectedTabId = session1.id ) - // Persist the snapshot val storage = SessionStorage(testContext, engine) - val persisted = storage.save(sessionsSnapshot) + val persisted = storage.save(state) assertTrue(persisted) // Read it back @@ -106,12 +109,12 @@ class SessionStorageTest { } @Test - fun `Saving empty snapshot`() { + fun `Saving empty state`() { val engine = mock(Engine::class.java) `when`(engine.name()).thenReturn("gecko") val storage = spy(SessionStorage(testContext, engine)) - storage.save(SessionManager.Snapshot(emptyList(), SessionManager.NO_SELECTION)) + storage.save(BrowserState()) verify(storage).clear() @@ -124,24 +127,20 @@ class SessionStorageTest { val session1 = Session("http://mozilla.org", id = "session1") val session2 = Session("http://getpocket.com", id = "session2") - val engineSession = mock(EngineSession::class.java) - val engine = mock(Engine::class.java) `when`(engine.name()).thenReturn("gecko") `when`(engine.createSession()).thenReturn(mock(EngineSession::class.java)) - // Engine session just for one of the sessions for simplicity. - val sessionsSnapshot = SessionManager.Snapshot( - sessions = listOf( - SessionManager.Snapshot.Item(session1, engineSession), - SessionManager.Snapshot.Item(session2) + // Persist the state + val state = BrowserState(tabs = listOf( + session1.toTabSessionState(), + session2.toTabSessionState() ), - selectedSessionIndex = 0 + selectedTabId = session1.id ) - // Persist the snapshot val storage = SessionStorage(testContext, engine) - val persisted = storage.save(sessionsSnapshot) + val persisted = storage.save(state) assertTrue(persisted) storage.clear() @@ -152,24 +151,14 @@ class SessionStorageTest { } @Test(expected = IllegalArgumentException::class) - fun `Should throw when saving illegal snapshot`() { + fun `Should throw when saving illegal state`() { val engine = mock(Engine::class.java) `when`(engine.name()).thenReturn("gecko") val storage = SessionStorage(testContext, engine) - val session = Session("http://mozilla.org") - val engineSession = mock(EngineSession::class.java) - val sessionsSnapshot = SessionManager.Snapshot( - sessions = listOf( - SessionManager.Snapshot.Item( - session, - engineSession - ) - ), - selectedSessionIndex = 1 - ) - storage.save(sessionsSnapshot) + val state = BrowserState(selectedTabId = "invalid", tabs = listOf(session.toTabSessionState())) + storage.save(state) } @Test @@ -180,14 +169,12 @@ class SessionStorageTest { val owner = mock(LifecycleOwner::class.java) val lifecycle = LifecycleRegistry(owner) - val snapshot: SessionManager.Snapshot = mock() - val sessionManager: SessionManager = mock() - doReturn(snapshot).`when`(sessionManager).createSnapshot() - val sessionStorage: SessionStorage = mock() + val state = BrowserState() + val store = BrowserStore(state) val autoSave = AutoSave( - sessionManager = sessionManager, + store = store, sessionStorage = sessionStorage, minimumIntervalMs = 0 ).whenGoingToBackground(lifecycle) @@ -205,27 +192,35 @@ class SessionStorageTest { autoSave.saveJob!!.join() - verify(sessionStorage).save(snapshot) + verify(sessionStorage).save(state) } } @Test fun `AutoSave - when session gets added`() { runBlocking { - val sessionManager = SessionManager(mock()) + val state = BrowserState() + val store = BrowserStore(state) + val sessionManager = SessionManager(mock(), store) val sessionStorage: SessionStorage = mock() + val dispatcher = TestCoroutineDispatcher() + val scope = CoroutineScope(dispatcher) + val autoSave = AutoSave( - sessionManager = sessionManager, + store = store, sessionStorage = sessionStorage, minimumIntervalMs = 0 - ).whenSessionsChange() + ).whenSessionsChange(scope) + + dispatcher.advanceUntilIdle() assertNull(autoSave.saveJob) verify(sessionStorage, never()).save(any()) sessionManager.add(Session("https://www.mozilla.org")) + dispatcher.advanceUntilIdle() autoSave.saveJob?.join() @@ -236,24 +231,33 @@ class SessionStorageTest { @Test fun `AutoSave - when session gets removed`() { runBlocking { - val sessionManager = SessionManager(mock()) + val sessionStorage: SessionStorage = mock() + + val state = BrowserState() + val store = BrowserStore(state) + + val sessionManager = SessionManager(mock(), store) sessionManager.add(Session("https://www.firefox.com")) val session = Session("https://www.mozilla.org").also { sessionManager.add(it) } - val sessionStorage: SessionStorage = mock() + val dispatcher = TestCoroutineDispatcher() + val scope = CoroutineScope(dispatcher) val autoSave = AutoSave( - sessionManager = sessionManager, + store = store, sessionStorage = sessionStorage, minimumIntervalMs = 0 - ).whenSessionsChange() + ).whenSessionsChange(scope) + + dispatcher.advanceUntilIdle() assertNull(autoSave.saveJob) verify(sessionStorage, never()).save(any()) sessionManager.remove(session) + dispatcher.advanceUntilIdle() autoSave.saveJob?.join() @@ -264,22 +268,31 @@ class SessionStorageTest { @Test fun `AutoSave - when all sessions get removed`() { runBlocking { - val sessionManager = SessionManager(mock()) + val state = BrowserState() + val store = BrowserStore(state) + + val sessionManager = SessionManager(mock(), store) sessionManager.add(Session("https://www.firefox.com")) sessionManager.add(Session("https://www.mozilla.org")) val sessionStorage: SessionStorage = mock() + val dispatcher = TestCoroutineDispatcher() + val scope = CoroutineScope(dispatcher) + val autoSave = AutoSave( - sessionManager = sessionManager, + store = store, sessionStorage = sessionStorage, minimumIntervalMs = 0 - ).whenSessionsChange() + ).whenSessionsChange(scope) + + dispatcher.advanceUntilIdle() assertNull(autoSave.saveJob) verify(sessionStorage, never()).save(any()) sessionManager.removeAll() + dispatcher.advanceUntilIdle() autoSave.saveJob?.join() @@ -290,17 +303,25 @@ class SessionStorageTest { @Test fun `AutoSave - when no sessions left`() { runBlocking { + val state = BrowserState() + val store = BrowserStore(state) + val session = Session("https://www.firefox.com") - val sessionManager = SessionManager(mock()) + val sessionManager = SessionManager(mock(), store) sessionManager.add(session) val sessionStorage: SessionStorage = mock() + val dispatcher = TestCoroutineDispatcher() + val scope = CoroutineScope(dispatcher) + val autoSave = AutoSave( - sessionManager = sessionManager, - sessionStorage = sessionStorage, - minimumIntervalMs = 0 - ).whenSessionsChange() + store = store, + sessionStorage = sessionStorage, + minimumIntervalMs = 0 + ).whenSessionsChange(scope) + + dispatcher.advanceUntilIdle() assertNull(autoSave.saveJob) verify(sessionStorage, never()).save(any()) @@ -308,6 +329,7 @@ class SessionStorageTest { // We didn't specify a default session lambda so this will // leave us without a session sessionManager.remove(session) + dispatcher.advanceUntilIdle() assertEquals(0, sessionManager.size) autoSave.saveJob?.join() @@ -319,7 +341,10 @@ class SessionStorageTest { @Test fun `AutoSave - when session gets selected`() { runBlocking { - val sessionManager = SessionManager(mock()) + val state = BrowserState() + val store = BrowserStore(state) + + val sessionManager = SessionManager(mock(), store) sessionManager.add(Session("https://www.firefox.com")) val session = Session("https://www.mozilla.org").also { sessionManager.add(it) @@ -327,16 +352,22 @@ class SessionStorageTest { val sessionStorage: SessionStorage = mock() + val dispatcher = TestCoroutineDispatcher() + val scope = CoroutineScope(dispatcher) + val autoSave = AutoSave( - sessionManager = sessionManager, + store = store, sessionStorage = sessionStorage, minimumIntervalMs = 0 - ).whenSessionsChange() + ).whenSessionsChange(scope) + + dispatcher.advanceUntilIdle() assertNull(autoSave.saveJob) verify(sessionStorage, never()).save(any()) sessionManager.select(session) + dispatcher.advanceUntilIdle() autoSave.saveJob?.join() @@ -347,25 +378,33 @@ class SessionStorageTest { @Test fun `AutoSave - when session loading state changes`() { runBlocking { - val sessionManager = SessionManager(mock()) + val sessionStorage: SessionStorage = mock() + + val state = BrowserState() + val store = BrowserStore(state) + + val sessionManager = SessionManager(mock(), store) val session = Session("https://www.mozilla.org").also { sessionManager.add(it) } - val sessionStorage: SessionStorage = mock() + val dispatcher = TestCoroutineDispatcher() + val scope = CoroutineScope(dispatcher) val autoSave = AutoSave( - sessionManager = sessionManager, + store = store, sessionStorage = sessionStorage, minimumIntervalMs = 0 - ).whenSessionsChange() + ).whenSessionsChange(scope) session.loading = true + dispatcher.advanceUntilIdle() assertNull(autoSave.saveJob) verify(sessionStorage, never()).save(any()) session.loading = false + dispatcher.advanceUntilIdle() autoSave.saveJob?.join() @@ -389,8 +428,10 @@ class SessionStorageTest { val owner = mock(LifecycleOwner::class.java) val lifecycle = LifecycleRegistry(owner) + val state = BrowserState() + val store = BrowserStore(state) val storage = SessionStorage(testContext, engine) - storage.autoSave(mock()) + storage.autoSave(store) .periodicallyInForeground(300, TimeUnit.SECONDS, scheduler, lifecycle) verifyNoMoreInteractions(scheduler) @@ -411,11 +452,12 @@ class SessionStorageTest { @Test fun `AutoSave - No new job triggered while save in flight`() { - val sessionManager = SessionManager(mock()) val sessionStorage: SessionStorage = mock() + val state = BrowserState() + val store = BrowserStore(state) val autoSave = AutoSave( - sessionManager = sessionManager, + store = store, sessionStorage = sessionStorage, minimumIntervalMs = 0 ) @@ -429,11 +471,12 @@ class SessionStorageTest { @Test fun `AutoSave - New job triggered if current job is done`() { - val sessionManager = SessionManager(mock()) val sessionStorage: SessionStorage = mock() + val state = BrowserState() + val store = BrowserStore(state) val autoSave = AutoSave( - sessionManager = sessionManager, + store = store, sessionStorage = sessionStorage, minimumIntervalMs = 0 ) diff --git a/components/browser/session/src/test/java/mozilla/components/browser/session/utils/AllSessionsObserverTest.kt b/components/browser/session/src/test/java/mozilla/components/browser/session/utils/AllSessionsObserverTest.kt index 44668b780f3..ea2f945e265 100644 --- a/components/browser/session/src/test/java/mozilla/components/browser/session/utils/AllSessionsObserverTest.kt +++ b/components/browser/session/src/test/java/mozilla/components/browser/session/utils/AllSessionsObserverTest.kt @@ -20,10 +20,8 @@ class AllSessionsObserverTest { fun `Observer will be registered on all already existing Sessions`() { val session1: Session = mock() `when`(session1.id).thenReturn("1") - `when`(session1.engineSessionHolder).thenReturn(mock()) val session2: Session = mock() `when`(session2.id).thenReturn("2") - `when`(session2.engineSessionHolder).thenReturn(mock()) val sessionManager = SessionManager(engine = mock()).apply { add(session1) @@ -50,10 +48,8 @@ class AllSessionsObserverTest { val session1: Session = mock() `when`(session1.id).thenReturn("1") - `when`(session1.engineSessionHolder).thenReturn(mock()) val session2: Session = mock() `when`(session2.id).thenReturn("2") - `when`(session2.engineSessionHolder).thenReturn(mock()) sessionManager.add(session1) sessionManager.add(session2) @@ -65,9 +61,7 @@ class AllSessionsObserverTest { @Test fun `Observer will be unregistered if Session gets removed`() { val session1: Session = spy(Session("https://www.mozilla.org")) - `when`(session1.engineSessionHolder).thenReturn(mock()) val session2: Session = mock() - `when`(session2.engineSessionHolder).thenReturn(mock()) val sessionManager = SessionManager(engine = mock()).apply { add(session1) diff --git a/components/browser/state/src/main/java/mozilla/components/browser/state/action/BrowserAction.kt b/components/browser/state/src/main/java/mozilla/components/browser/state/action/BrowserAction.kt index 8631569cd9e..c3449162823 100644 --- a/components/browser/state/src/main/java/mozilla/components/browser/state/action/BrowserAction.kt +++ b/components/browser/state/src/main/java/mozilla/components/browser/state/action/BrowserAction.kt @@ -4,6 +4,7 @@ package mozilla.components.browser.state.action +import android.content.ComponentCallbacks2 import android.graphics.Bitmap import mozilla.components.browser.state.search.SearchEngine import mozilla.components.browser.state.state.BrowserState @@ -11,6 +12,7 @@ import mozilla.components.browser.state.state.ContainerState import mozilla.components.browser.state.state.ContentState import mozilla.components.browser.state.state.CustomTabSessionState import mozilla.components.browser.state.state.EngineState +import mozilla.components.browser.state.state.MediaState import mozilla.components.browser.state.state.ReaderState import mozilla.components.browser.state.state.SecurityInfoState import mozilla.components.browser.state.state.SessionState @@ -19,8 +21,8 @@ import mozilla.components.browser.state.state.TrackingProtectionState import mozilla.components.browser.state.state.WebExtensionState import mozilla.components.browser.state.state.content.DownloadState import mozilla.components.browser.state.state.content.FindResultState -import mozilla.components.browser.state.state.MediaState import mozilla.components.browser.state.state.SearchState +import mozilla.components.concept.engine.Engine import mozilla.components.concept.engine.EngineSession import mozilla.components.concept.engine.EngineSessionState import mozilla.components.concept.engine.HitResult @@ -45,14 +47,14 @@ sealed class BrowserAction : Action */ sealed class SystemAction : BrowserAction() { /** - * Optimizes the [BrowserState] by removing unneeded and optional - * resources if the system is in a low memory condition. + * Optimizes the [BrowserState] by removing unneeded and optional resources if the system is in + * a low memory condition. * - * @param states map of session ids to engine session states where the engine session was closed - * by SessionManager. + * @param level The context of the trim, giving a hint of the amount of trimming the application + * may like to perform. See constants in [ComponentCallbacks2]. */ data class LowMemoryAction( - val states: Map + val level: Int ) : SystemAction() } @@ -131,6 +133,11 @@ sealed class CustomTabListAction : BrowserAction() { */ data class RemoveCustomTabAction(val tabId: String) : CustomTabListAction() + /** + * Converts an existing [CustomTabSessionState] to a regular/normal [TabSessionState]. + */ + data class TurnCustomTabIntoNormalTabAction(val tabId: String) : CustomTabListAction() + /** * Removes all custom tabs [TabSessionState]s. */ @@ -429,11 +436,102 @@ sealed class WebExtensionAction : BrowserAction() { * [BrowserState]. */ sealed class EngineAction : BrowserAction() { + /** + * Creates an [EngineSession] for the given [tabId] if none exists yet. + */ + data class CreateEngineSessionAction( + val tabId: String, + val skipLoading: Boolean = false + ) : EngineAction() + + /** + * Loads the given [url] in the tab with the given [sessionId]. + */ + data class LoadUrlAction( + val sessionId: String, + val url: String, + val flags: EngineSession.LoadUrlFlags = EngineSession.LoadUrlFlags.none(), + val additionalHeaders: Map? = null + ) : EngineAction() + + /** + * Loads [data] in the tab with the given [sessionId]. + */ + data class LoadDataAction( + val sessionId: String, + val data: String, + val mimeType: String = "text/html", + val encoding: String = "UTF-8" + ) : EngineAction() + + /** + * Reloads the tab with the given [sessionId]. + */ + data class ReloadAction( + val sessionId: String, + val flags: EngineSession.LoadUrlFlags = EngineSession.LoadUrlFlags.none() + ) : EngineAction() + + /** + * Navigates back in the tab with the given [sessionId]. + */ + data class GoBackAction( + val sessionId: String + ) : EngineAction() + + /** + * Navigates forward in the tab with the given [sessionId]. + */ + data class GoForwardAction( + val sessionId: String + ) : EngineAction() + + /** + * Navigates to the specified index in the history of the tab with the given [sessionId]. + */ + data class GoToHistoryIndexAction( + val sessionId: String, + val index: Int + ) : EngineAction() + + /** + * Enables/disables desktop mode in the tabs with the given [sessionId]. + */ + data class ToggleDesktopModeAction( + val sessionId: String, + val enable: Boolean + ) : EngineAction() + + /** + * Exits fullscreen mode in the tabs with the given [sessionId]. + */ + data class ExitFullscreenModeAction( + val sessionId: String + ) : EngineAction() + + /** + * Clears browsing data for the tab with the given [sessionId]. + */ + data class ClearDataAction( + val sessionId: String, + val data: Engine.BrowsingData + ) : EngineAction() /** * Attaches the provided [EngineSession] to the session with the provided [sessionId]. */ - data class LinkEngineSessionAction(val sessionId: String, val engineSession: EngineSession) : EngineAction() + data class LinkEngineSessionAction( + val sessionId: String, + val engineSession: EngineSession, + val skipLoading: Boolean = false + ) : EngineAction() + + /** + * Suspends the [EngineSession] of the session with the provided [sessionId]. + */ + data class SuspendEngineSessionAction( + val sessionId: String + ) : EngineAction() /** * Detaches the current [EngineSession] from the session with the provided [sessionId]. @@ -447,6 +545,29 @@ sealed class EngineAction : BrowserAction() { val sessionId: String, val engineSessionState: EngineSessionState ) : EngineAction() + + /** + * Updates the [EngineSession.Observer] of the session with the provided [sessionId]. + */ + data class UpdateEngineSessionObserverAction( + val sessionId: String, + val engineSessionObserver: EngineSession.Observer + ) : EngineAction() +} + +/** + * [BrowserAction] implementations to react to crashes. + */ +sealed class CrashAction : BrowserAction() { + /** + * Updates the [SessionState] of the session with provided ID to mark it as crashed. + */ + data class SessionCrashedAction(val tabId: String) : CrashAction() + + /** + * Updates the [SessionState] of the session with provided ID to mark it as restored. + */ + data class RestoreCrashedSessionAction(val tabId: String) : CrashAction() } /** diff --git a/components/browser/state/src/main/java/mozilla/components/browser/state/ext/CustomTabSessionState.kt b/components/browser/state/src/main/java/mozilla/components/browser/state/ext/CustomTabSessionState.kt new file mode 100644 index 00000000000..451a8bd8070 --- /dev/null +++ b/components/browser/state/src/main/java/mozilla/components/browser/state/ext/CustomTabSessionState.kt @@ -0,0 +1,19 @@ +/* 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.ext + +import mozilla.components.browser.state.state.CustomTabSessionState +import mozilla.components.browser.state.state.TabSessionState + +internal fun CustomTabSessionState.toTab(): TabSessionState { + return TabSessionState( + id = id, + content = content, + trackingProtection = trackingProtection, + engineState = engineState, + extensionState = extensionState, + contextId = contextId + ) +} diff --git a/components/browser/state/src/main/java/mozilla/components/browser/state/reducer/BrowserStateReducer.kt b/components/browser/state/src/main/java/mozilla/components/browser/state/reducer/BrowserStateReducer.kt index 88d03926c2f..2d1e9b94426 100644 --- a/components/browser/state/src/main/java/mozilla/components/browser/state/reducer/BrowserStateReducer.kt +++ b/components/browser/state/src/main/java/mozilla/components/browser/state/reducer/BrowserStateReducer.kt @@ -7,6 +7,7 @@ package mozilla.components.browser.state.reducer import mozilla.components.browser.state.action.BrowserAction import mozilla.components.browser.state.action.ContainerAction import mozilla.components.browser.state.action.ContentAction +import mozilla.components.browser.state.action.CrashAction import mozilla.components.browser.state.action.CustomTabListAction import mozilla.components.browser.state.action.DownloadAction import mozilla.components.browser.state.action.EngineAction @@ -45,6 +46,7 @@ internal object BrowserStateReducer { is MediaAction -> MediaReducer.reduce(state, action) is DownloadAction -> DownloadStateReducer.reduce(state, action) is SearchAction -> SearchReducer.reduce(state, action) + is CrashAction -> CrashReducer.reduce(state, action) } } } diff --git a/components/browser/state/src/main/java/mozilla/components/browser/state/reducer/CrashReducer.kt b/components/browser/state/src/main/java/mozilla/components/browser/state/reducer/CrashReducer.kt new file mode 100644 index 00000000000..ed9c299a589 --- /dev/null +++ b/components/browser/state/src/main/java/mozilla/components/browser/state/reducer/CrashReducer.kt @@ -0,0 +1,24 @@ +/* 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.reducer + +import mozilla.components.browser.state.action.CrashAction +import mozilla.components.browser.state.state.BrowserState + +internal object CrashReducer { + /** + * [CrashAction] Reducer function for modifying [BrowserState]. + */ + fun reduce(state: BrowserState, action: CrashAction): BrowserState = when (action) { + is CrashAction.SessionCrashedAction -> state.updateTabState(action.tabId) { tab -> + tab.createCopy(crashed = true) + } + is CrashAction.RestoreCrashedSessionAction -> state.updateTabState(action.tabId) { tab -> + // We only update the flag in the reducer. A middleware is responsible for actually + // performing a restoring side effect. + tab.createCopy(crashed = false) + } + } +} diff --git a/components/browser/state/src/main/java/mozilla/components/browser/state/reducer/CustomTabListReducer.kt b/components/browser/state/src/main/java/mozilla/components/browser/state/reducer/CustomTabListReducer.kt index 43184732537..49a7f15c672 100644 --- a/components/browser/state/src/main/java/mozilla/components/browser/state/reducer/CustomTabListReducer.kt +++ b/components/browser/state/src/main/java/mozilla/components/browser/state/reducer/CustomTabListReducer.kt @@ -5,6 +5,8 @@ package mozilla.components.browser.state.reducer import mozilla.components.browser.state.action.CustomTabListAction +import mozilla.components.browser.state.ext.toTab +import mozilla.components.browser.state.selector.findCustomTab import mozilla.components.browser.state.state.BrowserState internal object CustomTabListReducer { @@ -22,6 +24,19 @@ internal object CustomTabListReducer { is CustomTabListAction.RemoveAllCustomTabsAction -> { state.copy(customTabs = emptyList()) } + + is CustomTabListAction.TurnCustomTabIntoNormalTabAction -> { + val customTab = state.findCustomTab(action.tabId) + if (customTab == null) { + state + } else { + val tab = customTab.toTab() + state.copy( + customTabs = state.customTabs - customTab, + tabs = state.tabs + tab + ) + } + } } } } diff --git a/components/browser/state/src/main/java/mozilla/components/browser/state/reducer/EngineStateReducer.kt b/components/browser/state/src/main/java/mozilla/components/browser/state/reducer/EngineStateReducer.kt index 6dce59442b2..99fd1b1c701 100644 --- a/components/browser/state/src/main/java/mozilla/components/browser/state/reducer/EngineStateReducer.kt +++ b/components/browser/state/src/main/java/mozilla/components/browser/state/reducer/EngineStateReducer.kt @@ -10,21 +10,43 @@ import mozilla.components.browser.state.state.EngineState import mozilla.components.browser.state.state.SessionState internal object EngineStateReducer { - /** * [EngineAction] Reducer function for modifying a specific [EngineState] * of a [SessionState]. */ fun reduce(state: BrowserState, action: EngineAction): BrowserState = when (action) { is EngineAction.LinkEngineSessionAction -> state.copyWithEngineState(action.sessionId) { - it.copy(engineSession = action.engineSession) + it.copy( + engineSession = action.engineSession, + engineSessionState = null + ) } is EngineAction.UnlinkEngineSessionAction -> state.copyWithEngineState(action.sessionId) { - it.copy(engineSession = null, engineSessionState = null) + it.copy( + engineSession = null, + engineSessionState = null, + engineObserver = null + ) + } + is EngineAction.UpdateEngineSessionObserverAction -> state.copyWithEngineState(action.sessionId) { + it.copy(engineObserver = action.engineSessionObserver) } is EngineAction.UpdateEngineSessionStateAction -> state.copyWithEngineState(action.sessionId) { it.copy(engineSessionState = action.engineSessionState) } + is EngineAction.SuspendEngineSessionAction, + is EngineAction.CreateEngineSessionAction, + is EngineAction.LoadDataAction, + is EngineAction.LoadUrlAction, + is EngineAction.ReloadAction, + is EngineAction.GoBackAction, + is EngineAction.GoForwardAction, + is EngineAction.GoToHistoryIndexAction, + is EngineAction.ToggleDesktopModeAction, + is EngineAction.ExitFullscreenModeAction, + is EngineAction.ClearDataAction -> { + throw IllegalStateException("You need to add EngineMiddleware to your BrowserStore. ($action)") + } } } diff --git a/components/browser/state/src/main/java/mozilla/components/browser/state/reducer/SystemReducer.kt b/components/browser/state/src/main/java/mozilla/components/browser/state/reducer/SystemReducer.kt index 436e7e4e9a0..28e2ac39fb6 100644 --- a/components/browser/state/src/main/java/mozilla/components/browser/state/reducer/SystemReducer.kt +++ b/components/browser/state/src/main/java/mozilla/components/browser/state/reducer/SystemReducer.kt @@ -4,9 +4,9 @@ package mozilla.components.browser.state.reducer +import android.content.ComponentCallbacks2 import mozilla.components.browser.state.action.SystemAction import mozilla.components.browser.state.state.BrowserState -import mozilla.components.browser.state.state.EngineState internal object SystemReducer { /** @@ -14,26 +14,53 @@ internal object SystemReducer { */ fun reduce(state: BrowserState, action: SystemAction): BrowserState { return when (action) { - is SystemAction.LowMemoryAction -> { - val updatedTabs = state.tabs.map { - if (state.selectedTabId != it.id) { - it.copy( - content = it.content.copy(thumbnail = null), - engineState = if (it.id in action.states) { - EngineState( - engineSession = null, - engineSessionState = action.states[it.id] - ) - } else { - it.engineState - } - ) - } else { - it - } + is SystemAction.LowMemoryAction -> trimMemory(state, action.level) + } + } + + private fun trimMemory(state: BrowserState, level: Int): BrowserState { + // We only take care of thumbnails here. EngineMiddleware deals with suspending + // EngineSessions if needed. + + if (!shouldClearThumbnails(level)) { + return state + } + + return state.copy( + tabs = state.tabs.map { tab -> + if (tab.id != state.selectedTabId) { + tab.copy( + content = tab.content.copy(thumbnail = null) + ) + } else { + tab } - state.copy(tabs = updatedTabs) + }, + customTabs = state.customTabs.map { tab -> + tab.copy( + content = tab.content.copy(thumbnail = null) + ) } - } + ) + } +} + +private fun shouldClearThumbnails(level: Int): Boolean { + return when (level) { + // Foreground: The device is running much lower on memory. The app is running and not killable, but the + // system wants us to release unused resources to improve system performance. + ComponentCallbacks2.TRIM_MEMORY_RUNNING_LOW, + // Foreground: The device is running extremely low on memory. The app is not yet considered a killable + // process, but the system will begin killing background processes if apps do not release resources. + ComponentCallbacks2.TRIM_MEMORY_RUNNING_CRITICAL -> true + + // Background: The system is running low on memory and our process is near the middle of the LRU list. + // If the system becomes further constrained for memory, there's a chance our process will be killed. + ComponentCallbacks2.TRIM_MEMORY_MODERATE, + // Background: The system is running low on memory and our process is one of the first to be killed + // if the system does not recover memory now. + ComponentCallbacks2.TRIM_MEMORY_COMPLETE -> true + + else -> false } } diff --git a/components/browser/state/src/main/java/mozilla/components/browser/state/state/CustomTabSessionState.kt b/components/browser/state/src/main/java/mozilla/components/browser/state/state/CustomTabSessionState.kt index e2e86635fc1..1cf800a26a2 100644 --- a/components/browser/state/src/main/java/mozilla/components/browser/state/state/CustomTabSessionState.kt +++ b/components/browser/state/src/main/java/mozilla/components/browser/state/state/CustomTabSessionState.kt @@ -25,7 +25,8 @@ data class CustomTabSessionState( override val engineState: EngineState = EngineState(), override val extensionState: Map = emptyMap(), override val contextId: String? = null, - override val source: SessionState.Source = SessionState.Source.CUSTOM_TAB + override val source: SessionState.Source = SessionState.Source.CUSTOM_TAB, + override val crashed: Boolean = false ) : SessionState { override fun createCopy( @@ -34,14 +35,16 @@ data class CustomTabSessionState( trackingProtection: TrackingProtectionState, engineState: EngineState, extensionState: Map, - contextId: String? + contextId: String?, + crashed: Boolean ) = copy( id = id, content = content, trackingProtection = trackingProtection, engineState = engineState, extensionState = extensionState, - contextId = contextId + contextId = contextId, + crashed = crashed ) } diff --git a/components/browser/state/src/main/java/mozilla/components/browser/state/state/EngineState.kt b/components/browser/state/src/main/java/mozilla/components/browser/state/state/EngineState.kt index ebd7695c4be..81391b671ec 100644 --- a/components/browser/state/src/main/java/mozilla/components/browser/state/state/EngineState.kt +++ b/components/browser/state/src/main/java/mozilla/components/browser/state/state/EngineState.kt @@ -13,8 +13,10 @@ import mozilla.components.concept.engine.EngineSessionState * @property engineSession the engine's representation of this session. * @property engineSessionState serializable and restorable state of an engine session, see * [EngineSession.saveState] and [EngineSession.restoreState]. + * @property engineObserver TODO */ data class EngineState( val engineSession: EngineSession? = null, - val engineSessionState: EngineSessionState? = null + val engineSessionState: EngineSessionState? = null, + val engineObserver: EngineSession.Observer? = null ) diff --git a/components/browser/state/src/main/java/mozilla/components/browser/state/state/SessionState.kt b/components/browser/state/src/main/java/mozilla/components/browser/state/state/SessionState.kt index 3dc6f403a6e..f25bd3dd267 100644 --- a/components/browser/state/src/main/java/mozilla/components/browser/state/state/SessionState.kt +++ b/components/browser/state/src/main/java/mozilla/components/browser/state/state/SessionState.kt @@ -17,6 +17,11 @@ package mozilla.components.browser.state.state * contextual identity to use for the session's cookie store. * https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/Work_with_contextual_identities * @property source the [Source] of this session to describe how and why it was created. + * @property crashed Whether this session has crashed. In conjunction with a `concept-engine` + * implementation that uses a multi-process architecture, single sessions can crash without crashing + * the whole app. A crashed session may still be operational (since the underlying engine implementation + * has recovered its content process), but further action may be needed to restore the last state + * before the session has crashed (if desired). */ interface SessionState { val id: String @@ -26,6 +31,7 @@ interface SessionState { val extensionState: Map val contextId: String? val source: Source + val crashed: Boolean /** * Copy the class and override some parameters. @@ -37,7 +43,8 @@ interface SessionState { trackingProtection: TrackingProtectionState = this.trackingProtection, engineState: EngineState = this.engineState, extensionState: Map = this.extensionState, - contextId: String? = this.contextId + contextId: String? = this.contextId, + crashed: Boolean = this.crashed ): SessionState /** diff --git a/components/browser/state/src/main/java/mozilla/components/browser/state/state/TabSessionState.kt b/components/browser/state/src/main/java/mozilla/components/browser/state/state/TabSessionState.kt index 887bf6c5c59..4969f5ef84a 100644 --- a/components/browser/state/src/main/java/mozilla/components/browser/state/state/TabSessionState.kt +++ b/components/browser/state/src/main/java/mozilla/components/browser/state/state/TabSessionState.kt @@ -27,12 +27,13 @@ data class TabSessionState( override val content: ContentState, override val trackingProtection: TrackingProtectionState = TrackingProtectionState(), override val engineState: EngineState = EngineState(), - val parentId: String? = null, override val extensionState: Map = emptyMap(), - val readerState: ReaderState = ReaderState(), override val contextId: String? = null, + override val source: SessionState.Source = SessionState.Source.NONE, + override val crashed: Boolean = false, + val parentId: String? = null, val lastAccess: Long = 0L, - override val source: SessionState.Source = SessionState.Source.NONE + val readerState: ReaderState = ReaderState() ) : SessionState { override fun createCopy( @@ -41,14 +42,16 @@ data class TabSessionState( trackingProtection: TrackingProtectionState, engineState: EngineState, extensionState: Map, - contextId: String? + contextId: String?, + crashed: Boolean ): SessionState = copy( id = id, content = content, trackingProtection = trackingProtection, engineState = engineState, extensionState = extensionState, - contextId = contextId + contextId = contextId, + crashed = crashed ) } diff --git a/components/browser/state/src/test/java/mozilla/components/browser/state/action/EngineActionTest.kt b/components/browser/state/src/test/java/mozilla/components/browser/state/action/EngineActionTest.kt index b0b292138ed..2331b452eb9 100644 --- a/components/browser/state/src/test/java/mozilla/components/browser/state/action/EngineActionTest.kt +++ b/components/browser/state/src/test/java/mozilla/components/browser/state/action/EngineActionTest.kt @@ -50,10 +50,16 @@ class EngineActionTest { @Test fun `UnlinkEngineSessionAction - Detaches engine session`() { store.dispatch(EngineAction.LinkEngineSessionAction(tab.id, mock())).joinBlocking() + store.dispatch(EngineAction.UpdateEngineSessionStateAction(tab.id, mock())).joinBlocking() + store.dispatch(EngineAction.UpdateEngineSessionObserverAction(tab.id, mock())).joinBlocking() assertNotNull(engineState().engineSession) + assertNotNull(engineState().engineSessionState) + assertNotNull(engineState().engineObserver) store.dispatch(EngineAction.UnlinkEngineSessionAction(tab.id)).joinBlocking() assertNull(engineState().engineSession) + assertNull(engineState().engineSessionState) + assertNull(engineState().engineObserver) } @Test @@ -65,4 +71,14 @@ class EngineActionTest { assertNotNull(engineState().engineSessionState) assertEquals(engineSessionState, engineState().engineSessionState) } + + @Test + fun `UpdateEngineSessionObserverAction - Updates engine session observer`() { + assertNull(engineState().engineObserver) + + val engineObserver: EngineSession.Observer = mock() + store.dispatch(EngineAction.UpdateEngineSessionObserverAction(tab.id, engineObserver)).joinBlocking() + assertNotNull(engineState().engineObserver) + assertEquals(engineObserver, engineState().engineObserver) + } } diff --git a/components/browser/state/src/test/java/mozilla/components/browser/state/action/SystemActionTest.kt b/components/browser/state/src/test/java/mozilla/components/browser/state/action/SystemActionTest.kt index 62c78a344f0..5567465d21e 100644 --- a/components/browser/state/src/test/java/mozilla/components/browser/state/action/SystemActionTest.kt +++ b/components/browser/state/src/test/java/mozilla/components/browser/state/action/SystemActionTest.kt @@ -4,13 +4,13 @@ package mozilla.components.browser.state.action +import android.content.ComponentCallbacks2 import mozilla.components.browser.state.state.BrowserState import mozilla.components.browser.state.state.ContentState import mozilla.components.browser.state.state.EngineState import mozilla.components.browser.state.state.TabSessionState import mozilla.components.browser.state.state.createTab import mozilla.components.browser.state.store.BrowserStore -import mozilla.components.concept.engine.EngineSessionState import mozilla.components.support.test.ext.joinBlocking import mozilla.components.support.test.mock import org.junit.Assert.assertNotNull @@ -34,13 +34,14 @@ class SystemActionTest { store.dispatch(ContentAction.UpdateThumbnailAction("1", mock())).joinBlocking() store.dispatch(ContentAction.UpdateThumbnailAction("2", mock())).joinBlocking() store.dispatch(TabListAction.SelectTabAction(tabId = "2")).joinBlocking() + assertNotNull(store.state.tabs[0].content.thumbnail) assertNotNull(store.state.tabs[1].content.thumbnail) assertNotNull(store.state.tabs[2].content.thumbnail) store.dispatch( SystemAction.LowMemoryAction( - states = emptyMap() + ComponentCallbacks2.TRIM_MEMORY_RUNNING_CRITICAL ) ).joinBlocking() @@ -49,61 +50,6 @@ class SystemActionTest { // Thumbnail of selected tab should not have been removed assertNotNull(store.state.tabs[2].content.thumbnail) } - - @Test - fun `LowMemoryAction removes EngineSession references and adds state`() { - val initialState = BrowserState( - tabs = listOf( - createTabWithMockEngineSession(url = "https://www.mozilla.org", id = "0"), - createTabWithMockEngineSession(url = "https://www.firefox.com", id = "1"), - createTabWithMockEngineSession(url = "https://www.firefox.com", id = "2") - ), - selectedTabId = "1" - ) - val store = BrowserStore(initialState) - - val state0: EngineSessionState = mock() - val state2: EngineSessionState = mock() - - store.state.tabs[0].apply { - assertNotNull(engineState.engineSession) - assertNull(engineState.engineSessionState) - } - - store.state.tabs[1].apply { - assertNotNull(engineState.engineSession) - assertNull(engineState.engineSessionState) - } - - store.state.tabs[2].apply { - assertNotNull(engineState.engineSession) - assertNull(engineState.engineSessionState) - } - - store.dispatch( - SystemAction.LowMemoryAction( - states = mapOf( - "0" to state0, - "2" to state2 - ) - ) - ).joinBlocking() - - store.state.tabs[0].apply { - assertNull(engineState.engineSession) - assertNotNull(engineState.engineSessionState) - } - - store.state.tabs[1].apply { - assertNotNull(engineState.engineSession) - assertNull(engineState.engineSessionState) - } - - store.state.tabs[2].apply { - assertNull(engineState.engineSession) - assertNotNull(engineState.engineSessionState) - } - } } private fun createTabWithMockEngineSession( diff --git a/components/feature/contextmenu/src/test/java/mozilla/components/feature/contextmenu/ContextMenuCandidateTest.kt b/components/feature/contextmenu/src/test/java/mozilla/components/feature/contextmenu/ContextMenuCandidateTest.kt index f6114fe78a8..55d2f90a5c3 100644 --- a/components/feature/contextmenu/src/test/java/mozilla/components/feature/contextmenu/ContextMenuCandidateTest.kt +++ b/components/feature/contextmenu/src/test/java/mozilla/components/feature/contextmenu/ContextMenuCandidateTest.kt @@ -9,15 +9,16 @@ import android.content.Context import android.view.View import androidx.coordinatorlayout.widget.CoordinatorLayout import androidx.test.ext.junit.runners.AndroidJUnit4 +import kotlinx.coroutines.MainScope import mozilla.components.browser.session.Session import mozilla.components.browser.session.SessionManager +import mozilla.components.browser.session.engine.EngineMiddleware import mozilla.components.browser.state.selector.selectedTab import mozilla.components.browser.state.state.BrowserState import mozilla.components.browser.state.state.ContentState import mozilla.components.browser.state.state.TabSessionState 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.HitResult import mozilla.components.feature.app.links.AppLinkRedirect import mozilla.components.feature.app.links.AppLinksUseCases @@ -63,7 +64,6 @@ class ContextMenuCandidateTest { fun `Candidate "Open Link in New Tab" showFor displayed in correct cases`() { val store = BrowserStore() val sessionManager = spy(SessionManager(mock(), store)) - doReturn(mock()).`when`(sessionManager).getOrCreateEngineSession(any(), anyBoolean()) val tabsUseCases = TabsUseCases(store, sessionManager) val parentView = CoordinatorLayout(testContext) val openInNewTab = ContextMenuCandidate.createOpenInNewTabCandidate( @@ -94,7 +94,6 @@ class ContextMenuCandidateTest { fun `Candidate "Open Link in New Tab" action properly executes for session with a contextId`() { val store = BrowserStore() val sessionManager = spy(SessionManager(mock(), store)) - doReturn(mock()).`when`(sessionManager).getOrCreateEngineSession(any(), anyBoolean()) sessionManager.add(Session("https://www.mozilla.org", contextId = "1")) val tabsUseCases = TabsUseCases(store, sessionManager) @@ -116,7 +115,6 @@ class ContextMenuCandidateTest { fun `Candidate "Open Link in New Tab" action properly executes and shows snackbar`() { val store = BrowserStore() val sessionManager = spy(SessionManager(mock(), store)) - doReturn(mock()).`when`(sessionManager).getOrCreateEngineSession(any(), anyBoolean()) sessionManager.add(Session("https://www.mozilla.org")) val tabsUseCases = TabsUseCases(store, sessionManager) @@ -137,9 +135,12 @@ class ContextMenuCandidateTest { @Test fun `Candidate "Open Link in New Tab" snackbar action works`() { - val store = BrowserStore() - val sessionManager = spy(SessionManager(mock(), store)) - doReturn(mock()).`when`(sessionManager).getOrCreateEngineSession(any(), anyBoolean()) + val store = BrowserStore(middleware = EngineMiddleware.create( + engine = mock(), + sessionLookup = { null }, + scope = MainScope() + )) + val sessionManager = SessionManager(mock(), store) sessionManager.add(Session("https://www.mozilla.org")) val tabsUseCases = TabsUseCases(store, sessionManager) @@ -163,7 +164,6 @@ class ContextMenuCandidateTest { fun `Candidate "Open Link in New Tab" action properly handles link with an image`() { val store = BrowserStore() val sessionManager = spy(SessionManager(mock(), store)) - doReturn(mock()).`when`(sessionManager).getOrCreateEngineSession(any(), anyBoolean()) sessionManager.add(Session("https://www.mozilla.org")) val tabsUseCases = TabsUseCases(store, sessionManager) @@ -185,7 +185,6 @@ class ContextMenuCandidateTest { fun `Candidate "Open Link in Private Tab" showFor displayed in correct cases`() { val store = BrowserStore() val sessionManager = spy(SessionManager(mock(), store)) - doReturn(mock()).`when`(sessionManager).getOrCreateEngineSession(any(), anyBoolean()) sessionManager.add(Session("https://www.mozilla.org")) val tabsUseCases = TabsUseCases(store, sessionManager) @@ -218,7 +217,6 @@ class ContextMenuCandidateTest { fun `Candidate "Open Link in Private Tab" action properly executes and shows snackbar`() { val store = BrowserStore() val sessionManager = spy(SessionManager(mock(), store)) - doReturn(mock()).`when`(sessionManager).getOrCreateEngineSession(any(), anyBoolean()) sessionManager.add(Session("https://www.mozilla.org")) val tabsUseCases = TabsUseCases(store, sessionManager) @@ -239,9 +237,12 @@ class ContextMenuCandidateTest { @Test fun `Candidate "Open Link in Private Tab" snackbar action works`() { - val store = BrowserStore() + val store = BrowserStore(middleware = EngineMiddleware.create( + engine = mock(), + sessionLookup = { null }, + scope = MainScope() + )) val sessionManager = spy(SessionManager(mock(), store)) - doReturn(mock()).`when`(sessionManager).getOrCreateEngineSession(any(), anyBoolean()) sessionManager.add(Session("https://www.mozilla.org")) val tabsUseCases = TabsUseCases(store, sessionManager) @@ -254,9 +255,9 @@ class ContextMenuCandidateTest { openInPrivateTab.action.invoke(store.state.tabs.first(), HitResult.UNKNOWN("https://firefox.com")) assertEquals("https://www.mozilla.org", store.state.selectedTab!!.content.url) + assertEquals(2, store.state.tabs.size) snackbarDelegate.lastActionListener!!.invoke(mock()) - assertEquals("https://firefox.com", store.state.selectedTab!!.content.url) } @@ -264,7 +265,6 @@ class ContextMenuCandidateTest { fun `Candidate "Open Link in Private Tab" action properly handles link with an image`() { val store = BrowserStore() val sessionManager = spy(SessionManager(mock(), store)) - doReturn(mock()).`when`(sessionManager).getOrCreateEngineSession(any(), anyBoolean()) sessionManager.add(Session("https://www.mozilla.org")) val tabsUseCases = TabsUseCases(store, sessionManager) @@ -281,9 +281,12 @@ class ContextMenuCandidateTest { @Test fun `Candidate "Open Image in New Tab"`() { - val store = BrowserStore() + val store = BrowserStore(middleware = EngineMiddleware.create( + engine = mock(), + sessionLookup = { null }, + scope = MainScope() + )) val sessionManager = spy(SessionManager(mock(), store)) - doReturn(mock()).`when`(sessionManager).getOrCreateEngineSession(any(), anyBoolean()) sessionManager.add(Session("https://www.mozilla.org")) val tabsUseCases = TabsUseCases(store, sessionManager) @@ -342,7 +345,6 @@ class ContextMenuCandidateTest { fun `Candidate "Open Image in New Tab" opens in private tab if session is private`() { val store = BrowserStore() val sessionManager = spy(SessionManager(mock(), store)) - doReturn(mock()).`when`(sessionManager).getOrCreateEngineSession(any(), anyBoolean()) sessionManager.add(Session("https://www.mozilla.org", private = true)) val tabsUseCases = TabsUseCases(store, sessionManager) @@ -366,7 +368,6 @@ class ContextMenuCandidateTest { fun `Candidate "Open Image in New Tab" opens with the session's contextId`() { val store = BrowserStore() val sessionManager = spy(SessionManager(mock(), store)) - doReturn(mock()).`when`(sessionManager).getOrCreateEngineSession(any(), anyBoolean()) sessionManager.add(Session("https://www.mozilla.org", contextId = "1")) val tabsUseCases = TabsUseCases(store, sessionManager) @@ -391,7 +392,6 @@ class ContextMenuCandidateTest { fun `Candidate "Save image"`() { val store = BrowserStore() val sessionManager = spy(SessionManager(mock(), store)) - doReturn(mock()).`when`(sessionManager).getOrCreateEngineSession(any(), anyBoolean()) sessionManager.add(Session("https://www.mozilla.org", private = true)) val saveImage = ContextMenuCandidate.createSaveImageCandidate( @@ -443,7 +443,6 @@ class ContextMenuCandidateTest { fun `Candidate "Save video and audio"`() { val store = BrowserStore() val sessionManager = spy(SessionManager(mock(), store)) - doReturn(mock()).`when`(sessionManager).getOrCreateEngineSession(any(), anyBoolean()) sessionManager.add(Session("https://www.mozilla.org", private = true)) val saveVideoAudio = ContextMenuCandidate.createSaveVideoAudioCandidate( @@ -498,7 +497,6 @@ class ContextMenuCandidateTest { fun `Candidate "download link"`() { val store = BrowserStore() val sessionManager = spy(SessionManager(mock(), store)) - doReturn(mock()).`when`(sessionManager).getOrCreateEngineSession(any(), anyBoolean()) sessionManager.add(Session("https://www.mozilla.org", private = true)) val downloadLink = ContextMenuCandidate.createDownloadLinkCandidate( @@ -652,7 +650,6 @@ class ContextMenuCandidateTest { val store = BrowserStore() val sessionManager = spy(SessionManager(mock(), store)) - doReturn(mock()).`when`(sessionManager).getOrCreateEngineSession(any(), anyBoolean()) sessionManager.add(Session("https://www.mozilla.org", private = true)) shareLink.action.invoke( @@ -729,7 +726,6 @@ class ContextMenuCandidateTest { val store = BrowserStore() val sessionManager = spy(SessionManager(mock(), store)) - doReturn(mock()).`when`(sessionManager).getOrCreateEngineSession(any(), anyBoolean()) sessionManager.add(Session("https://www.mozilla.org", private = true)) copyLink.action.invoke( @@ -778,7 +774,6 @@ class ContextMenuCandidateTest { val store = BrowserStore() val sessionManager = spy(SessionManager(mock(), store)) - doReturn(mock()).`when`(sessionManager).getOrCreateEngineSession(any(), anyBoolean()) sessionManager.add(Session("https://www.mozilla.org", private = true)) copyImageLocation.action.invoke( @@ -904,7 +899,6 @@ class ContextMenuCandidateTest { val store = BrowserStore() val sessionManager = spy(SessionManager(mock(), store)) - doReturn(mock()).`when`(sessionManager).getOrCreateEngineSession(any(), anyBoolean()) sessionManager.add(Session("https://www.mozilla.org", private = true)) copyEmailAddress.action.invoke( @@ -948,7 +942,6 @@ class ContextMenuCandidateTest { val store = BrowserStore() val sessionManager = spy(SessionManager(mock(), store)) - doReturn(mock()).`when`(sessionManager).getOrCreateEngineSession(any(), anyBoolean()) sessionManager.add(Session("https://www.mozilla.org", private = true)) shareEmailAddress.action.invoke( @@ -986,7 +979,6 @@ class ContextMenuCandidateTest { val store = BrowserStore() val sessionManager = spy(SessionManager(mock(), store)) - doReturn(mock()).`when`(sessionManager).getOrCreateEngineSession(any(), anyBoolean()) sessionManager.add(Session("https://www.mozilla.org", private = true)) addToContacts.action.invoke( diff --git a/components/feature/customtabs/src/test/java/mozilla/components/feature/customtabs/CustomTabIntentProcessorTest.kt b/components/feature/customtabs/src/test/java/mozilla/components/feature/customtabs/CustomTabIntentProcessorTest.kt index e7a89b600c0..30a4b7ce34d 100644 --- a/components/feature/customtabs/src/test/java/mozilla/components/feature/customtabs/CustomTabIntentProcessorTest.kt +++ b/components/feature/customtabs/src/test/java/mozilla/components/feature/customtabs/CustomTabIntentProcessorTest.kt @@ -11,28 +11,27 @@ import androidx.browser.customtabs.CustomTabsIntent import androidx.test.ext.junit.runners.AndroidJUnit4 import kotlinx.coroutines.ExperimentalCoroutinesApi import mozilla.components.browser.session.Session -import mozilla.components.browser.state.state.SessionState.Source import mozilla.components.browser.session.SessionManager +import mozilla.components.browser.state.action.EngineAction +import mozilla.components.browser.state.state.SessionState.Source +import mozilla.components.browser.state.store.BrowserStore import mozilla.components.concept.engine.Engine -import mozilla.components.concept.engine.EngineSession import mozilla.components.concept.engine.EngineSession.LoadUrlFlags import mozilla.components.feature.intent.ext.EXTRA_SESSION_ID import mozilla.components.feature.session.SessionUseCases import mozilla.components.support.test.any +import mozilla.components.support.test.argumentCaptor import mozilla.components.support.test.mock import mozilla.components.support.test.robolectric.testContext import mozilla.components.support.test.whenever import mozilla.components.support.utils.toSafeIntent import org.junit.Assert.assertEquals -import org.junit.Assert.assertNotNull import org.junit.Assert.assertFalse +import org.junit.Assert.assertNotNull import org.junit.Assert.assertTrue -import org.junit.Before import org.junit.Test import org.junit.runner.RunWith -import org.mockito.ArgumentMatchers.anyBoolean import org.mockito.ArgumentMatchers.eq -import org.mockito.Mockito.doReturn import org.mockito.Mockito.spy import org.mockito.Mockito.verify @@ -40,21 +39,12 @@ import org.mockito.Mockito.verify @ExperimentalCoroutinesApi class CustomTabIntentProcessorTest { - private val sessionManager = mock() - private val session = mock() - private val engineSession = mock() - - @Before - fun setup() { - whenever(sessionManager.getOrCreateEngineSession(session)).thenReturn(engineSession) - } - @Test fun processCustomTabIntentWithDefaultHandlers() { - val engine = mock() + val store: BrowserStore = mock() + val engine: Engine = mock() val sessionManager = spy(SessionManager(engine)) - doReturn(engineSession).`when`(sessionManager).getOrCreateEngineSession(anySession(), anyBoolean()) - val useCases = SessionUseCases(sessionManager) + val useCases = SessionUseCases(store, sessionManager) val handler = CustomTabIntentProcessor(sessionManager, useCases.loadUrl, testContext.resources) @@ -66,8 +56,12 @@ class CustomTabIntentProcessorTest { whenever(intent.putExtra(any(), any())).thenReturn(intent) handler.process(intent) - verify(sessionManager).add(anySession(), eq(false), eq(null), eq(null), eq(null)) - verify(engineSession).loadUrl("http://mozilla.org", flags = LoadUrlFlags.external()) + + val sessionCaptor = argumentCaptor() + verify(sessionManager).add(sessionCaptor.capture(), eq(false), eq(null), eq(null), eq(null)) + verify(store).dispatch(EngineAction.LoadUrlAction( + sessionCaptor.value.id, "http://mozilla.org", LoadUrlFlags.external()) + ) verify(intent).putExtra(eq(EXTRA_SESSION_ID), any()) val customTabSession = sessionManager.all[0] @@ -80,10 +74,10 @@ class CustomTabIntentProcessorTest { @Test fun processCustomTabIntentWithAdditionalHeaders() { - val engine = mock() + val store: BrowserStore = mock() + val engine: Engine = mock() val sessionManager = spy(SessionManager(engine)) - doReturn(engineSession).`when`(sessionManager).getOrCreateEngineSession(anySession(), anyBoolean()) - val useCases = SessionUseCases(sessionManager) + val useCases = SessionUseCases(store, sessionManager) val handler = CustomTabIntentProcessor(sessionManager, useCases.loadUrl, testContext.resources) @@ -101,8 +95,12 @@ class CustomTabIntentProcessorTest { val headers = handler.getAdditionalHeaders(intent.toSafeIntent()) handler.process(intent) - verify(sessionManager).add(anySession(), eq(false), eq(null), eq(null), eq(null)) - verify(engineSession).loadUrl("http://mozilla.org", flags = LoadUrlFlags.external(), additionalHeaders = headers) + + val sessionCaptor = argumentCaptor() + verify(sessionManager).add(sessionCaptor.capture(), eq(false), eq(null), eq(null), eq(null)) + verify(store).dispatch(EngineAction.LoadUrlAction( + sessionCaptor.value.id, "http://mozilla.org", LoadUrlFlags.external(), headers) + ) verify(intent).putExtra(eq(EXTRA_SESSION_ID), any()) val customTabSession = sessionManager.all[0] @@ -115,10 +113,10 @@ class CustomTabIntentProcessorTest { @Test fun processPrivateCustomTabIntentWithDefaultHandlers() { - val engine = mock() + val store: BrowserStore = mock() + val engine: Engine = mock() val sessionManager = spy(SessionManager(engine)) - doReturn(engineSession).`when`(sessionManager).getOrCreateEngineSession(anySession(), anyBoolean()) - val useCases = SessionUseCases(sessionManager) + val useCases = SessionUseCases(store, sessionManager) val handler = CustomTabIntentProcessor(sessionManager, useCases.loadUrl, testContext.resources, true) @@ -130,8 +128,12 @@ class CustomTabIntentProcessorTest { whenever(intent.putExtra(any(), any())).thenReturn(intent) handler.process(intent) - verify(sessionManager).add(anySession(), eq(false), eq(null), eq(null), eq(null)) - verify(engineSession).loadUrl("http://mozilla.org", flags = LoadUrlFlags.external()) + + val sessionCaptor = argumentCaptor() + verify(sessionManager).add(sessionCaptor.capture(), eq(false), eq(null), eq(null), eq(null)) + verify(store).dispatch(EngineAction.LoadUrlAction( + sessionCaptor.value.id, "http://mozilla.org", LoadUrlFlags.external()) + ) verify(intent).putExtra(eq(EXTRA_SESSION_ID), any()) val customTabSession = sessionManager.all[0] @@ -141,10 +143,4 @@ class CustomTabIntentProcessorTest { assertNotNull(customTabSession.customTabConfig) assertTrue(customTabSession.private) } - - @Suppress("UNCHECKED_CAST") - private fun anySession(): T { - any() - return null as T - } } diff --git a/components/feature/intent/src/test/java/mozilla/components/feature/intent/processing/TabIntentProcessorTest.kt b/components/feature/intent/src/test/java/mozilla/components/feature/intent/processing/TabIntentProcessorTest.kt index 5fd79441630..769c7b3e99d 100644 --- a/components/feature/intent/src/test/java/mozilla/components/feature/intent/processing/TabIntentProcessorTest.kt +++ b/components/feature/intent/src/test/java/mozilla/components/feature/intent/processing/TabIntentProcessorTest.kt @@ -12,14 +12,17 @@ import kotlinx.coroutines.ExperimentalCoroutinesApi import mozilla.components.browser.search.SearchEngine import mozilla.components.browser.search.SearchEngineManager import mozilla.components.browser.session.Session -import mozilla.components.browser.state.state.SessionState.Source import mozilla.components.browser.session.SessionManager +import mozilla.components.browser.state.action.EngineAction +import mozilla.components.browser.state.state.SessionState.Source +import mozilla.components.browser.state.store.BrowserStore import mozilla.components.concept.engine.Engine import mozilla.components.concept.engine.EngineSession import mozilla.components.concept.engine.EngineSession.LoadUrlFlags import mozilla.components.feature.search.SearchUseCases import mozilla.components.feature.session.SessionUseCases import mozilla.components.support.test.any +import mozilla.components.support.test.argumentCaptor import mozilla.components.support.test.eq import mozilla.components.support.test.mock import mozilla.components.support.test.robolectric.testContext @@ -30,50 +33,51 @@ import org.junit.Assert.assertNotNull import org.junit.Before import org.junit.Test import org.junit.runner.RunWith -import org.mockito.ArgumentMatchers.anyBoolean import org.mockito.ArgumentMatchers.anyList -import org.mockito.Mockito.doReturn import org.mockito.Mockito.never import org.mockito.Mockito.spy +import org.mockito.Mockito.times import org.mockito.Mockito.verify @RunWith(AndroidJUnit4::class) @ExperimentalCoroutinesApi class TabIntentProcessorTest { + private val store = mock() private val sessionManager = mock() private val session = mock() - private val engineSession = mock() - private val sessionUseCases = SessionUseCases(sessionManager) + private val sessionUseCases = SessionUseCases(store, sessionManager) private val searchEngineManager = mock() - private val searchUseCases = SearchUseCases(testContext, searchEngineManager, sessionManager) + private val searchUseCases = SearchUseCases(testContext, store, searchEngineManager, sessionManager) @Before fun setup() { whenever(sessionManager.selectedSession).thenReturn(session) - whenever(sessionManager.getOrCreateEngineSession(eq(session), anyBoolean())).thenReturn(engineSession) } @Test fun processViewIntent() { val engine = mock() val sessionManager = spy(SessionManager(engine)) - val useCases = SessionUseCases(sessionManager) + val useCases = SessionUseCases(store, sessionManager) val handler = TabIntentProcessor(sessionManager, useCases.loadUrl, searchUseCases.newTabSearch) val intent = mock() whenever(intent.action).thenReturn(Intent.ACTION_VIEW) val engineSession = mock() - doReturn(engineSession).`when`(sessionManager).getOrCreateEngineSession(anySession(), anyBoolean()) - whenever(intent.dataString).thenReturn("") handler.process(intent) verify(engineSession, never()).loadUrl("") whenever(intent.dataString).thenReturn("http://mozilla.org") handler.process(intent) - verify(engineSession).loadUrl("http://mozilla.org", flags = LoadUrlFlags.external()) + + val sessionCaptor = argumentCaptor() + verify(sessionManager).add(sessionCaptor.capture(), eq(true), eq(null), eq(null), eq(null)) + verify(store).dispatch(EngineAction.LoadUrlAction( + sessionCaptor.value.id, "http://mozilla.org", LoadUrlFlags.external()) + ) // try to send a request to open a tab with the same url as before whenever(intent.dataString).thenReturn("http://mozilla.org") @@ -91,24 +95,29 @@ class TabIntentProcessorTest { @Test fun processViewIntentUsingSelectedSession() { + val engine = mock() + val sessionManager = spy(SessionManager(engine)) + val session = Session("http://mozilla.org") val handler = TabIntentProcessor( sessionManager, sessionUseCases.loadUrl, searchUseCases.newTabSearch, openNewTab = false ) + val intent = mock() whenever(intent.action).thenReturn(Intent.ACTION_VIEW) whenever(intent.dataString).thenReturn("http://mozilla.org") + sessionManager.add(session) handler.process(intent) - verify(engineSession).loadUrl("http://mozilla.org", flags = LoadUrlFlags.external()) + verify(sessionManager).select(session) + verify(store, never()).dispatch(any()) } @Test fun processViewIntentHavingNoSelectedSession() { whenever(sessionManager.selectedSession).thenReturn(null) - doReturn(engineSession).`when`(sessionManager).getOrCreateEngineSession(anySession(), anyBoolean()) val handler = TabIntentProcessor( sessionManager, @@ -121,21 +130,22 @@ class TabIntentProcessorTest { whenever(intent.dataString).thenReturn("http://mozilla.org") handler.process(intent) - verify(engineSession).loadUrl("http://mozilla.org", flags = LoadUrlFlags.external()) + val actionCaptor = argumentCaptor() + verify(store).dispatch(actionCaptor.capture()) + assertEquals("http://mozilla.org", actionCaptor.value.url) } @Test fun processNfcIntent() { val engine = mock() val sessionManager = spy(SessionManager(engine)) - val useCases = SessionUseCases(sessionManager) + val useCases = SessionUseCases(store, sessionManager) val handler = TabIntentProcessor(sessionManager, useCases.loadUrl, searchUseCases.newTabSearch) val intent = mock() whenever(intent.action).thenReturn(ACTION_NDEF_DISCOVERED) val engineSession = mock() - doReturn(engineSession).`when`(sessionManager).getOrCreateEngineSession(anySession(), anyBoolean()) whenever(intent.dataString).thenReturn("") handler.process(intent) @@ -143,7 +153,11 @@ class TabIntentProcessorTest { whenever(intent.dataString).thenReturn("http://mozilla.org") handler.process(intent) - verify(engineSession).loadUrl("http://mozilla.org", flags = LoadUrlFlags.external()) + val sessionCaptor = argumentCaptor() + verify(sessionManager).add(sessionCaptor.capture(), eq(true), eq(null), eq(null), eq(null)) + verify(store).dispatch(EngineAction.LoadUrlAction( + sessionCaptor.value.id, "http://mozilla.org", LoadUrlFlags.external()) + ) val session = sessionManager.all[0] assertNotNull(session) @@ -153,6 +167,9 @@ class TabIntentProcessorTest { @Test fun processNfcIntentUsingSelectedSession() { + val engine = mock() + val sessionManager = spy(SessionManager(engine)) + val session = Session("http://mozilla.org") val handler = TabIntentProcessor( sessionManager, sessionUseCases.loadUrl, @@ -162,15 +179,16 @@ class TabIntentProcessorTest { val intent = mock() whenever(intent.action).thenReturn(ACTION_NDEF_DISCOVERED) whenever(intent.dataString).thenReturn("http://mozilla.org") + sessionManager.add(session) handler.process(intent) - verify(engineSession).loadUrl("http://mozilla.org", flags = LoadUrlFlags.external()) + verify(sessionManager).select(session) + verify(store, never()).dispatch(any()) } @Test fun processNfcIntentHavingNoSelectedSession() { whenever(sessionManager.selectedSession).thenReturn(null) - doReturn(engineSession).`when`(sessionManager).getOrCreateEngineSession(anySession(), anyBoolean()) val handler = TabIntentProcessor( sessionManager, @@ -183,13 +201,13 @@ class TabIntentProcessorTest { whenever(intent.dataString).thenReturn("http://mozilla.org") handler.process(intent) - verify(engineSession).loadUrl("http://mozilla.org", flags = LoadUrlFlags.external()) + val actionCaptor = argumentCaptor() + verify(store).dispatch(actionCaptor.capture()) + assertEquals("http://mozilla.org", actionCaptor.value.url) } @Test fun `load URL on ACTION_SEND if text contains URL`() { - doReturn(engineSession).`when`(sessionManager).getOrCreateEngineSession(anySession(), anyBoolean()) - val handler = TabIntentProcessor(sessionManager, sessionUseCases.loadUrl, searchUseCases.newTabSearch) val intent = mock() @@ -197,33 +215,48 @@ class TabIntentProcessorTest { whenever(intent.getStringExtra(Intent.EXTRA_TEXT)).thenReturn("http://mozilla.org") handler.process(intent) - verify(engineSession).loadUrl("http://mozilla.org", flags = LoadUrlFlags.external()) + val sessionCaptor = argumentCaptor() + verify(sessionManager).add(sessionCaptor.capture(), eq(true), eq(null), eq(null), eq(null)) + verify(store).dispatch(EngineAction.LoadUrlAction( + sessionCaptor.value.id, "http://mozilla.org", LoadUrlFlags.external()) + ) whenever(intent.getStringExtra(Intent.EXTRA_TEXT)).thenReturn("see http://getpocket.com") handler.process(intent) - verify(engineSession).loadUrl("http://getpocket.com", flags = LoadUrlFlags.external()) + verify(sessionManager, times(2)).add(sessionCaptor.capture(), eq(true), eq(null), eq(null), eq(null)) + verify(store).dispatch(EngineAction.LoadUrlAction( + sessionCaptor.allValues.last().id, "http://getpocket.com", LoadUrlFlags.external()) + ) whenever(intent.getStringExtra(Intent.EXTRA_TEXT)).thenReturn("see http://mozilla.com and http://getpocket.com") handler.process(intent) - verify(engineSession).loadUrl("http://mozilla.com", flags = LoadUrlFlags.external()) + verify(sessionManager, times(3)).add(sessionCaptor.capture(), eq(true), eq(null), eq(null), eq(null)) + verify(store).dispatch(EngineAction.LoadUrlAction( + sessionCaptor.allValues.last().id, "http://mozilla.com", LoadUrlFlags.external()) + ) whenever(intent.getStringExtra(Intent.EXTRA_TEXT)).thenReturn("checkout the Tweet: http://tweets.mozilla.com") handler.process(intent) - verify(engineSession).loadUrl("http://tweets.mozilla.com", flags = LoadUrlFlags.external()) + verify(sessionManager, times(4)).add(sessionCaptor.capture(), eq(true), eq(null), eq(null), eq(null)) + verify(store).dispatch(EngineAction.LoadUrlAction( + sessionCaptor.allValues.last().id, "http://tweets.mozilla.com", LoadUrlFlags.external()) + ) - whenever(intent.getStringExtra(Intent.EXTRA_TEXT)).thenReturn("checkout the Tweet: HTTP://tweets.mozilla.com") + whenever(intent.getStringExtra(Intent.EXTRA_TEXT)).thenReturn("checkout the Tweet: HTTP://tweets.mozilla.org") handler.process(intent) - verify(engineSession).loadUrl("http://tweets.mozilla.com", flags = LoadUrlFlags.external()) + verify(sessionManager, times(5)).add(sessionCaptor.capture(), eq(true), eq(null), eq(null), eq(null)) + verify(store).dispatch(EngineAction.LoadUrlAction( + sessionCaptor.allValues.last().id, "HTTP://tweets.mozilla.org", LoadUrlFlags.external()) + ) } @Test fun `perform search on ACTION_SEND if text (no URL) provided`() { val engine = mock() val sessionManager = spy(SessionManager(engine)) - doReturn(engineSession).`when`(sessionManager).getOrCreateEngineSession(anySession(), anyBoolean()) - val searchUseCases = SearchUseCases(testContext, searchEngineManager, sessionManager) - val sessionUseCases = SessionUseCases(sessionManager) + val searchUseCases = SearchUseCases(testContext, store, searchEngineManager, sessionManager) + val sessionUseCases = SessionUseCases(store, sessionManager) val searchTerms = "mozilla android" val searchUrl = "http://search-url.com?$searchTerms" @@ -239,7 +272,11 @@ class TabIntentProcessorTest { whenever(searchEngineManager.getDefaultSearchEngine(testContext)).thenReturn(searchEngine) handler.process(intent) - verify(engineSession).loadUrl(searchUrl) + val sessionCaptor = argumentCaptor() + verify(sessionManager).add(sessionCaptor.capture(), eq(true), eq(null), eq(null), eq(null)) + verify(store).dispatch(EngineAction.LoadUrlAction( + sessionCaptor.value.id, searchUrl, LoadUrlFlags.none()) + ) assertEquals(searchUrl, sessionManager.selectedSession?.url) assertEquals(searchTerms, sessionManager.selectedSession?.searchTerms) } @@ -270,8 +307,6 @@ class TabIntentProcessorTest { @Test fun `load URL on ACTION_SEARCH if text is an URL`() { - doReturn(engineSession).`when`(sessionManager).getOrCreateEngineSession(anySession(), anyBoolean()) - val handler = TabIntentProcessor(sessionManager, sessionUseCases.loadUrl, searchUseCases.newTabSearch) val intent = mock() @@ -279,17 +314,20 @@ class TabIntentProcessorTest { whenever(intent.getStringExtra(SearchManager.QUERY)).thenReturn("http://mozilla.org") handler.process(intent) - verify(engineSession).loadUrl("http://mozilla.org", flags = LoadUrlFlags.external()) + val sessionCaptor = argumentCaptor() + verify(sessionManager).add(sessionCaptor.capture(), eq(true), eq(null), eq(null), eq(null)) + verify(store).dispatch(EngineAction.LoadUrlAction( + sessionCaptor.value.id, "http://mozilla.org", LoadUrlFlags.external()) + ) } @Test fun `perform search on ACTION_SEARCH if text (no URL) provided`() { val engine = mock() val sessionManager = spy(SessionManager(engine)) - doReturn(engineSession).`when`(sessionManager).getOrCreateEngineSession(anySession(), anyBoolean()) - val searchUseCases = SearchUseCases(testContext, searchEngineManager, sessionManager) - val sessionUseCases = SessionUseCases(sessionManager) + val searchUseCases = SearchUseCases(testContext, store, searchEngineManager, sessionManager) + val sessionUseCases = SessionUseCases(store, sessionManager) val searchTerms = "mozilla android" val searchUrl = "http://search-url.com?$searchTerms" @@ -305,7 +343,11 @@ class TabIntentProcessorTest { whenever(searchEngineManager.getDefaultSearchEngine(testContext)).thenReturn(searchEngine) handler.process(intent) - verify(engineSession).loadUrl(searchUrl) + val sessionCaptor = argumentCaptor() + verify(sessionManager).add(sessionCaptor.capture(), eq(true), eq(null), eq(null), eq(null)) + verify(store).dispatch(EngineAction.LoadUrlAction( + sessionCaptor.value.id, searchUrl, LoadUrlFlags.none()) + ) assertEquals(searchUrl, sessionManager.selectedSession?.url) assertEquals(searchTerms, sessionManager.selectedSession?.searchTerms) } @@ -324,8 +366,6 @@ class TabIntentProcessorTest { @Test fun `load URL on ACTION_WEB_SEARCH if text is an URL`() { - doReturn(engineSession).`when`(sessionManager).getOrCreateEngineSession(anySession(), anyBoolean()) - val handler = TabIntentProcessor(sessionManager, sessionUseCases.loadUrl, searchUseCases.newTabSearch) val intent = mock() @@ -333,17 +373,20 @@ class TabIntentProcessorTest { whenever(intent.getStringExtra(SearchManager.QUERY)).thenReturn("http://mozilla.org") handler.process(intent) - verify(engineSession).loadUrl("http://mozilla.org", flags = LoadUrlFlags.external()) + val sessionCaptor = argumentCaptor() + verify(sessionManager).add(sessionCaptor.capture(), eq(true), eq(null), eq(null), eq(null)) + verify(store).dispatch(EngineAction.LoadUrlAction( + sessionCaptor.value.id, "http://mozilla.org", LoadUrlFlags.external()) + ) } @Test fun `perform search on ACTION_WEB_SEARCH if text (no URL) provided`() { val engine = mock() val sessionManager = spy(SessionManager(engine)) - doReturn(engineSession).`when`(sessionManager).getOrCreateEngineSession(anySession(), anyBoolean()) - val searchUseCases = SearchUseCases(testContext, searchEngineManager, sessionManager) - val sessionUseCases = SessionUseCases(sessionManager) + val searchUseCases = SearchUseCases(testContext, store, searchEngineManager, sessionManager) + val sessionUseCases = SessionUseCases(store, sessionManager) val searchTerms = "mozilla android" val searchUrl = "http://search-url.com?$searchTerms" @@ -359,14 +402,12 @@ class TabIntentProcessorTest { whenever(searchEngineManager.getDefaultSearchEngine(testContext)).thenReturn(searchEngine) handler.process(intent) - verify(engineSession).loadUrl(searchUrl) + val sessionCaptor = argumentCaptor() + verify(sessionManager).add(sessionCaptor.capture(), eq(true), eq(null), eq(null), eq(null)) + verify(store).dispatch(EngineAction.LoadUrlAction( + sessionCaptor.value.id, searchUrl, LoadUrlFlags.none()) + ) assertEquals(searchUrl, sessionManager.selectedSession?.url) assertEquals(searchTerms, sessionManager.selectedSession?.searchTerms) } - - @Suppress("UNCHECKED_CAST") - private fun anySession(): T { - any() - return null as T - } } diff --git a/components/feature/p2p/src/main/java/mozilla/components/feature/p2p/P2PFeature.kt b/components/feature/p2p/src/main/java/mozilla/components/feature/p2p/P2PFeature.kt index 2c05ef13dc1..6633c1c2b1b 100644 --- a/components/feature/p2p/src/main/java/mozilla/components/feature/p2p/P2PFeature.kt +++ b/components/feature/p2p/src/main/java/mozilla/components/feature/p2p/P2PFeature.kt @@ -132,9 +132,16 @@ class P2PFeature( return } - val engineSession = sessionManager.getOrCreateEngineSession(session) - val messageHandler = P2PContentMessageHandler() - extensionController?.registerContentMessageHandler(engineSession, messageHandler) + // TODO Action for registering content message handler? Do we really need that? + /* + store.dispatch(EngineAction.CreateEngineSessionAction( + session.id, + sideEffect = { engineSession -> + val messageHandler = P2PContentMessageHandler() + extensionController?.registerContentMessageHandler(engineSession, messageHandler) + } + )) + */ } private inner class P2PContentMessageHandler : MessageHandler { @@ -160,12 +167,19 @@ class P2PFeature( } private fun sendMessage(json: JSONObject) { - activeSession?.let { - extensionController?.sendContentMessage( - json, - sessionManager.getOrCreateEngineSession(it) - ) - } + val session = activeSession ?: return + + // TODO Action for sending content message. Do we really need that? + json.hashCode() + session.hashCode() + /* + store.dispatch(EngineAction.CreateEngineSessionAction( + session.id, + sideEffect = { engineSession -> + extensionController?.sendContentMessage(json, engineSession) + } + )) + */ } } diff --git a/components/feature/search/src/main/java/mozilla/components/feature/search/SearchUseCases.kt b/components/feature/search/src/main/java/mozilla/components/feature/search/SearchUseCases.kt index 5b484d234a1..8ffc28f2d86 100644 --- a/components/feature/search/src/main/java/mozilla/components/feature/search/SearchUseCases.kt +++ b/components/feature/search/src/main/java/mozilla/components/feature/search/SearchUseCases.kt @@ -9,7 +9,9 @@ import mozilla.components.browser.search.SearchEngine import mozilla.components.browser.search.SearchEngineManager import mozilla.components.browser.session.Session import mozilla.components.browser.session.SessionManager +import mozilla.components.browser.state.action.EngineAction import mozilla.components.browser.state.state.SessionState +import mozilla.components.browser.state.store.BrowserStore /** * Contains use cases related to the search feature. @@ -20,6 +22,7 @@ import mozilla.components.browser.state.state.SessionState */ class SearchUseCases( context: Context, + store: BrowserStore, searchEngineManager: SearchEngineManager, sessionManager: SessionManager, onNoSession: (String) -> Session = { url -> @@ -39,6 +42,7 @@ class SearchUseCases( class DefaultSearchUseCase( private val context: Context, + private val store: BrowserStore, private val searchEngineManager: SearchEngineManager, private val sessionManager: SessionManager, private val onNoSession: (String) -> Session @@ -75,12 +79,16 @@ class SearchUseCases( searchSession.searchTerms = searchTerms - sessionManager.getOrCreateEngineSession(searchSession).loadUrl(searchUrl) + store.dispatch(EngineAction.LoadUrlAction( + searchSession.id, + searchUrl + )) } } class NewTabSearchUseCase( private val context: Context, + private val store: BrowserStore, private val searchEngineManager: SearchEngineManager, private val sessionManager: SessionManager, private val isPrivate: Boolean @@ -127,19 +135,23 @@ class SearchUseCases( session.searchTerms = searchTerms sessionManager.add(session, selected, parent = parentSession) - sessionManager.getOrCreateEngineSession(session).loadUrl(searchUrl) + + store.dispatch(EngineAction.LoadUrlAction( + session.id, + searchUrl + )) } } val defaultSearch: DefaultSearchUseCase by lazy { - DefaultSearchUseCase(context, searchEngineManager, sessionManager, onNoSession) + DefaultSearchUseCase(context, store, searchEngineManager, sessionManager, onNoSession) } val newTabSearch: NewTabSearchUseCase by lazy { - NewTabSearchUseCase(context, searchEngineManager, sessionManager, false) + NewTabSearchUseCase(context, store, searchEngineManager, sessionManager, false) } val newPrivateTabSearch: NewTabSearchUseCase by lazy { - NewTabSearchUseCase(context, searchEngineManager, sessionManager, true) + NewTabSearchUseCase(context, store, searchEngineManager, sessionManager, true) } } diff --git a/components/feature/search/src/test/java/mozilla/components/feature/search/SearchUseCasesTest.kt b/components/feature/search/src/test/java/mozilla/components/feature/search/SearchUseCasesTest.kt index e60768c44ba..33d3210b70d 100644 --- a/components/feature/search/src/test/java/mozilla/components/feature/search/SearchUseCasesTest.kt +++ b/components/feature/search/src/test/java/mozilla/components/feature/search/SearchUseCasesTest.kt @@ -9,9 +9,9 @@ import mozilla.components.browser.search.SearchEngine import mozilla.components.browser.search.SearchEngineManager import mozilla.components.browser.session.Session import mozilla.components.browser.session.SessionManager +import mozilla.components.browser.state.action.EngineAction import mozilla.components.browser.state.state.SessionState -import mozilla.components.concept.engine.EngineSession -import mozilla.components.support.test.any +import mozilla.components.browser.state.store.BrowserStore import mozilla.components.support.test.argumentCaptor import mozilla.components.support.test.eq import mozilla.components.support.test.mock @@ -23,7 +23,6 @@ import org.junit.Assert.assertTrue import org.junit.Before import org.junit.Test import org.junit.runner.RunWith -import org.mockito.ArgumentMatchers.anyBoolean import org.mockito.Mockito.verify @RunWith(AndroidJUnit4::class) @@ -32,6 +31,7 @@ class SearchUseCasesTest { private lateinit var searchEngine: SearchEngine private lateinit var searchEngineManager: SearchEngineManager private lateinit var sessionManager: SessionManager + private lateinit var store: BrowserStore private lateinit var useCases: SearchUseCases @Before @@ -39,25 +39,24 @@ class SearchUseCasesTest { searchEngine = mock() searchEngineManager = mock() sessionManager = mock() - useCases = SearchUseCases(testContext, searchEngineManager, sessionManager) + store = mock() + useCases = SearchUseCases(testContext, store, searchEngineManager, sessionManager) } @Test fun defaultSearch() { val searchTerms = "mozilla android" val searchUrl = "http://search-url.com?$searchTerms" - val session = Session("mozilla.org") - val engineSession = mock() whenever(searchEngine.buildSearchUrl(searchTerms)).thenReturn(searchUrl) whenever(searchEngineManager.getDefaultSearchEngine(testContext)).thenReturn(searchEngine) - whenever(sessionManager.getOrCreateEngineSession(session)).thenReturn(engineSession) useCases.defaultSearch(searchTerms, session) - assertEquals(searchTerms, session.searchTerms) - verify(engineSession).loadUrl(searchUrl) + val actionCaptor = argumentCaptor() + verify(store).dispatch(actionCaptor.capture()) + assertEquals(searchUrl, actionCaptor.value.url) } @Test @@ -65,13 +64,13 @@ class SearchUseCasesTest { val searchTerms = "mozilla android" val searchUrl = "http://search-url.com?$searchTerms" - val engineSession = mock() whenever(searchEngine.buildSearchUrl(searchTerms)).thenReturn(searchUrl) whenever(searchEngineManager.getDefaultSearchEngine(testContext)).thenReturn(searchEngine) - whenever(sessionManager.getOrCreateEngineSession(any(), anyBoolean())).thenReturn(engineSession) useCases.newTabSearch(searchTerms, SessionState.Source.NEW_TAB) - verify(engineSession).loadUrl(searchUrl) + val actionCaptor = argumentCaptor() + verify(store).dispatch(actionCaptor.capture()) + assertEquals(searchUrl, actionCaptor.value.url) } @Test @@ -80,20 +79,18 @@ class SearchUseCasesTest { whenever(searchEngine.buildSearchUrl("test")).thenReturn("https://search.example.com") whenever(searchEngineManager.getDefaultSearchEngine(testContext)).thenReturn(searchEngine) - whenever(sessionManager.getOrCreateEngineSession(any(), anyBoolean())).thenReturn(mock()) var sessionCreatedForUrl: String? = null - val searchUseCases = SearchUseCases(testContext, searchEngineManager, sessionManager) { url -> + val searchUseCases = SearchUseCases(testContext, store, searchEngineManager, sessionManager) { url -> sessionCreatedForUrl = url Session(url).also { createdSession = it } } searchUseCases.defaultSearch("test") - assertEquals("https://search.example.com", sessionCreatedForUrl) - assertNotNull(createdSession) - verify(sessionManager).getOrCreateEngineSession(createdSession!!) + assertNotNull(createdSession!!) + verify(store).dispatch(EngineAction.LoadUrlAction(createdSession!!.id, sessionCreatedForUrl!!)) } @Test @@ -101,17 +98,16 @@ class SearchUseCasesTest { val searchTerms = "mozilla android" val searchUrl = "http://search-url.com?$searchTerms" - val engineSession = mock() whenever(searchEngine.buildSearchUrl(searchTerms)).thenReturn(searchUrl) whenever(searchEngineManager.getDefaultSearchEngine(testContext)).thenReturn(searchEngine) - whenever(sessionManager.getOrCreateEngineSession(any(), anyBoolean())).thenReturn(engineSession) - useCases.newPrivateTabSearch.invoke(searchTerms) val captor = argumentCaptor() verify(sessionManager).add(captor.capture(), eq(true), eq(null), eq(null), eq(null)) assertTrue(captor.value.private) - verify(engineSession).loadUrl(searchUrl) + val actionCaptor = argumentCaptor() + verify(store).dispatch(actionCaptor.capture()) + assertEquals(searchUrl, actionCaptor.value.url) } @Test @@ -119,10 +115,8 @@ class SearchUseCasesTest { val searchTerms = "mozilla android" val searchUrl = "http://search-url.com?$searchTerms" - val engineSession = mock() whenever(searchEngine.buildSearchUrl(searchTerms)).thenReturn(searchUrl) whenever(searchEngineManager.getDefaultSearchEngine(testContext)).thenReturn(searchEngine) - whenever(sessionManager.getOrCreateEngineSession(any(), anyBoolean())).thenReturn(engineSession) val parentSession = mock() useCases.newPrivateTabSearch.invoke(searchTerms, parentSession = parentSession) @@ -130,6 +124,9 @@ class SearchUseCasesTest { val captor = argumentCaptor() verify(sessionManager).add(captor.capture(), eq(true), eq(null), eq(null), eq(parentSession)) assertTrue(captor.value.private) - verify(engineSession).loadUrl(searchUrl) + + val actionCaptor = argumentCaptor() + verify(store).dispatch(actionCaptor.capture()) + assertEquals(searchUrl, actionCaptor.value.url) } } diff --git a/components/feature/session/src/main/java/mozilla/components/feature/session/SessionFeature.kt b/components/feature/session/src/main/java/mozilla/components/feature/session/SessionFeature.kt index dc96a3bb6dc..8482eda95d2 100644 --- a/components/feature/session/src/main/java/mozilla/components/feature/session/SessionFeature.kt +++ b/components/feature/session/src/main/java/mozilla/components/feature/session/SessionFeature.kt @@ -4,7 +4,6 @@ package mozilla.components.feature.session -import mozilla.components.browser.session.usecases.EngineSessionUseCases import mozilla.components.browser.state.selector.findTabOrCustomTabOrSelectedTab import mozilla.components.browser.state.store.BrowserStore import mozilla.components.concept.engine.EngineView @@ -18,11 +17,10 @@ import mozilla.components.support.base.feature.UserInteractionHandler class SessionFeature( private val store: BrowserStore, private val goBackUseCase: SessionUseCases.GoBackUseCase, - engineSessionUseCases: EngineSessionUseCases, private val engineView: EngineView, private val tabId: String? = null ) : LifecycleAwareFeature, UserInteractionHandler { - internal val presenter = EngineViewPresenter(store, engineView, engineSessionUseCases, tabId) + internal val presenter = EngineViewPresenter(store, engineView, tabId) /** * Start feature: App is in the foreground. diff --git a/components/feature/session/src/main/java/mozilla/components/feature/session/SessionUseCases.kt b/components/feature/session/src/main/java/mozilla/components/feature/session/SessionUseCases.kt index 9ee64a19301..f0ebe1a2a5c 100644 --- a/components/feature/session/src/main/java/mozilla/components/feature/session/SessionUseCases.kt +++ b/components/feature/session/src/main/java/mozilla/components/feature/session/SessionUseCases.kt @@ -6,6 +6,10 @@ package mozilla.components.feature.session import mozilla.components.browser.session.Session import mozilla.components.browser.session.SessionManager +import mozilla.components.browser.state.action.CrashAction +import mozilla.components.browser.state.action.EngineAction +import mozilla.components.browser.state.selector.findTabOrCustomTab +import mozilla.components.browser.state.store.BrowserStore import mozilla.components.concept.engine.Engine.BrowsingData import mozilla.components.concept.engine.EngineSession.LoadUrlFlags @@ -18,6 +22,7 @@ import mozilla.components.concept.engine.EngineSession.LoadUrlFlags * it to the [SessionManager]. */ class SessionUseCases( + store: BrowserStore, sessionManager: SessionManager, onNoSession: (String) -> Session = { url -> Session(url).apply { sessionManager.add(this) } @@ -39,6 +44,7 @@ class SessionUseCases( } class DefaultLoadUrlUseCase internal constructor( + private val store: BrowserStore, private val sessionManager: SessionManager, private val onNoSession: (String) -> Session ) : LoadUrlUseCase { @@ -77,12 +83,18 @@ class SessionUseCases( additionalHeaders: Map? = null ) { val loadSession = session ?: onNoSession.invoke(url) - sessionManager.getOrCreateEngineSession(loadSession) - .loadUrl(url, flags = flags, additionalHeaders = additionalHeaders) + + store.dispatch(EngineAction.LoadUrlAction( + loadSession.id, + url, + flags, + additionalHeaders + )) } } class LoadDataUseCase internal constructor( + private val store: BrowserStore, private val sessionManager: SessionManager, private val onNoSession: (String) -> Session ) { @@ -97,11 +109,18 @@ class SessionUseCases( session: Session? = sessionManager.selectedSession ) { val loadSession = session ?: onNoSession.invoke("about:blank") - sessionManager.getOrCreateEngineSession(loadSession).loadData(data, mimeType, encoding) + + store.dispatch(EngineAction.LoadDataAction( + loadSession.id, + data, + mimeType, + encoding + )) } } class ReloadUrlUseCase internal constructor( + private val store: BrowserStore, private val sessionManager: SessionManager ) { /** @@ -115,9 +134,14 @@ class SessionUseCases( session: Session? = sessionManager.selectedSession, flags: LoadUrlFlags = LoadUrlFlags.none() ) { - if (session != null) { - sessionManager.getOrCreateEngineSession(session).reload(flags) + if (session == null) { + return } + + store.dispatch(EngineAction.ReloadAction( + session.id, + flags + )) } /** @@ -134,6 +158,7 @@ class SessionUseCases( } class StopLoadingUseCase internal constructor( + private val store: BrowserStore, private val sessionManager: SessionManager ) { /** @@ -142,22 +167,32 @@ class SessionUseCases( * @param session the session for which loading should be stopped. */ operator fun invoke(session: Session? = sessionManager.selectedSession) { - if (session != null) { - sessionManager.getOrCreateEngineSession(session).stopLoading() + if (session == null) { + return } + + store.state.findTabOrCustomTab(session.id) + ?.engineState + ?.engineSession + ?.stopLoading() } } class GoBackUseCase internal constructor( + private val store: BrowserStore, private val sessionManager: SessionManager ) { /** * Navigates back in the history of the currently selected session */ operator fun invoke(session: Session? = sessionManager.selectedSession) { - if (session != null) { - sessionManager.getOrCreateEngineSession(session).goBack() + if (session == null) { + return } + + store.dispatch(EngineAction.GoBackAction( + session.id + )) } /** @@ -171,15 +206,20 @@ class SessionUseCases( } class GoForwardUseCase internal constructor( + private val store: BrowserStore, private val sessionManager: SessionManager ) { /** * Navigates forward in the history of the currently selected session */ operator fun invoke(session: Session? = sessionManager.selectedSession) { - if (session != null) { - sessionManager.getOrCreateEngineSession(session).goForward() + if (session == null) { + return } + + store.dispatch(EngineAction.GoForwardAction( + session.id + )) } } @@ -187,6 +227,7 @@ class SessionUseCases( * Use case to jump to an arbitrary history index in a session's backstack. */ class GoToHistoryIndexUseCase internal constructor( + private val store: BrowserStore, private val sessionManager: SessionManager ) { /** @@ -197,35 +238,51 @@ class SessionUseCases( * @param session the session whose [HistoryState] is being accessed */ operator fun invoke(index: Int, session: Session? = sessionManager.selectedSession) { - if (session != null) { - sessionManager.getOrCreateEngineSession(session).goToHistoryIndex(index) + if (session == null) { + return } + + store.dispatch(EngineAction.GoToHistoryIndexAction( + session.id, + index + )) } } class RequestDesktopSiteUseCase internal constructor( + private val store: BrowserStore, private val sessionManager: SessionManager ) { /** * Requests the desktop version of the current session and reloads the page. */ operator fun invoke(enable: Boolean, session: Session? = sessionManager.selectedSession) { - if (session != null) { - sessionManager.getOrCreateEngineSession(session).toggleDesktopMode(enable, true) + if (session == null) { + return } + + store.dispatch(EngineAction.ToggleDesktopModeAction( + session.id, + enable + )) } } class ExitFullScreenUseCase internal constructor( + private val store: BrowserStore, private val sessionManager: SessionManager ) { /** * Exits fullscreen mode of the current session. */ operator fun invoke(session: Session? = sessionManager.selectedSession) { - if (session != null) { - sessionManager.getOrCreateEngineSession(session).exitFullScreenMode() + if (session == null) { + return } + + store.dispatch(EngineAction.ExitFullscreenModeAction( + session.id + )) } /** @@ -237,6 +294,7 @@ class SessionUseCases( } class ClearDataUseCase internal constructor( + private val store: BrowserStore, private val sessionManager: SessionManager ) { /** @@ -247,56 +305,60 @@ class SessionUseCases( data: BrowsingData = BrowsingData.all() ) { sessionManager.engine.clearData(data) - if (session != null) { - sessionManager.getOrCreateEngineSession(session).clearData(data) + + if (session == null) { + return } + + store.dispatch(EngineAction.ClearDataAction( + session.id, + data + )) } } /** * Tries to recover from a crash by restoring the last know state. - * - * Executing this use case on a [Session] will clear the [Session.crashed] flag. */ class CrashRecoveryUseCase internal constructor( - private val sessionManager: SessionManager + private val store: BrowserStore ) { /** - * Tries to recover the state of the provided [Session]. + * Tries to recover the state of all crashed sessions. */ - fun invoke(session: Session): Boolean = invoke(listOf(session)) + fun invoke() { + val tabIds = store.state.let { + it.tabs + it.customTabs + }.filter { + it.crashed + }.map { + it.id + } - /** - * Tries to recover the state of all crashed [Session]s (with [Session.crashed] flag set). - */ - fun invoke(): Boolean { - return invoke(sessionManager.sessions.filter { it.crashed }) + return invoke(tabIds) } /** - * Tries to recover the state of all [sessions]. + * Tries to recover the state of all sessions. */ - fun invoke(sessions: List): Boolean { - return sessions.fold(true) { recovered, session -> - val sessionRecovered = sessionManager.getOrCreateEngineSession(session) - .recoverFromCrash() - - session.crashed = false - - sessionRecovered && recovered + fun invoke(tabIds: List) { + tabIds.forEach { tabId -> + store.dispatch( + CrashAction.RestoreCrashedSessionAction(tabId) + ) } } } - val loadUrl: DefaultLoadUrlUseCase by lazy { DefaultLoadUrlUseCase(sessionManager, onNoSession) } - val loadData: LoadDataUseCase by lazy { LoadDataUseCase(sessionManager, onNoSession) } - val reload: ReloadUrlUseCase by lazy { ReloadUrlUseCase(sessionManager) } - val stopLoading: StopLoadingUseCase by lazy { StopLoadingUseCase(sessionManager) } - val goBack: GoBackUseCase by lazy { GoBackUseCase(sessionManager) } - val goForward: GoForwardUseCase by lazy { GoForwardUseCase(sessionManager) } - val goToHistoryIndex: GoToHistoryIndexUseCase by lazy { GoToHistoryIndexUseCase(sessionManager) } - val requestDesktopSite: RequestDesktopSiteUseCase by lazy { RequestDesktopSiteUseCase(sessionManager) } - val exitFullscreen: ExitFullScreenUseCase by lazy { ExitFullScreenUseCase(sessionManager) } - val clearData: ClearDataUseCase by lazy { ClearDataUseCase(sessionManager) } - val crashRecovery: CrashRecoveryUseCase by lazy { CrashRecoveryUseCase(sessionManager) } + val loadUrl: DefaultLoadUrlUseCase by lazy { DefaultLoadUrlUseCase(store, sessionManager, onNoSession) } + val loadData: LoadDataUseCase by lazy { LoadDataUseCase(store, sessionManager, onNoSession) } + val reload: ReloadUrlUseCase by lazy { ReloadUrlUseCase(store, sessionManager) } + val stopLoading: StopLoadingUseCase by lazy { StopLoadingUseCase(store, sessionManager) } + val goBack: GoBackUseCase by lazy { GoBackUseCase(store, sessionManager) } + val goForward: GoForwardUseCase by lazy { GoForwardUseCase(store, sessionManager) } + val goToHistoryIndex: GoToHistoryIndexUseCase by lazy { GoToHistoryIndexUseCase(store, sessionManager) } + val requestDesktopSite: RequestDesktopSiteUseCase by lazy { RequestDesktopSiteUseCase(store, sessionManager) } + val exitFullscreen: ExitFullScreenUseCase by lazy { ExitFullScreenUseCase(store, sessionManager) } + val clearData: ClearDataUseCase by lazy { ClearDataUseCase(store, sessionManager) } + val crashRecovery: CrashRecoveryUseCase by lazy { CrashRecoveryUseCase(store) } } diff --git a/components/feature/session/src/main/java/mozilla/components/feature/session/engine/EngineViewPresenter.kt b/components/feature/session/src/main/java/mozilla/components/feature/session/engine/EngineViewPresenter.kt index 5b29be86c87..453e9032f08 100644 --- a/components/feature/session/src/main/java/mozilla/components/feature/session/engine/EngineViewPresenter.kt +++ b/components/feature/session/src/main/java/mozilla/components/feature/session/engine/EngineViewPresenter.kt @@ -8,13 +8,13 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.cancel import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.map -import mozilla.components.browser.session.usecases.EngineSessionUseCases +import mozilla.components.browser.state.action.EngineAction import mozilla.components.browser.state.selector.findTabOrCustomTabOrSelectedTab +import mozilla.components.browser.state.state.SessionState import mozilla.components.browser.state.store.BrowserStore -import mozilla.components.concept.engine.EngineSession import mozilla.components.concept.engine.EngineView import mozilla.components.lib.state.ext.flowScoped -import mozilla.components.support.ktx.kotlinx.coroutines.flow.ifChanged +import mozilla.components.support.ktx.kotlinx.coroutines.flow.ifAnyChanged /** * Presenter implementation for EngineView. @@ -22,7 +22,6 @@ import mozilla.components.support.ktx.kotlinx.coroutines.flow.ifChanged internal class EngineViewPresenter( private val store: BrowserStore, private val engineView: EngineView, - private val useCases: EngineSessionUseCases, private val tabId: String? ) { private var scope: CoroutineScope? = null @@ -33,9 +32,9 @@ internal class EngineViewPresenter( fun start() { scope = store.flowScoped { flow -> flow.map { state -> state.findTabOrCustomTabOrSelectedTab(tabId) } - .map { tab -> tab?.let { useCases.getOrCreateEngineSession(it.id) } } - .ifChanged() - .collect { engineSession -> onEngineSession(engineSession) } + // Render if the tab itself changed and when an engine session is linked + .ifAnyChanged { tab -> arrayOf(tab?.id, tab?.engineState?.engineSession) } + .collect { tab -> onTabToRender(tab) } } } @@ -46,14 +45,22 @@ internal class EngineViewPresenter( scope?.cancel() } - private fun onEngineSession(engineSession: EngineSession?) { + private fun onTabToRender(tab: SessionState?) { + if (tab == null) { + engineView.release() + } else { + renderTab(tab) + } + } + + private fun renderTab(tab: SessionState) { + val engineSession = tab.engineState.engineSession + if (engineSession == null) { - // With no EngineSession being available to render anymore we could call release here - // to make sure GeckoView also frees any references to the previous session. However - // we are seeing problems with that and are temporarily disabling this to investigate. - // https://github.com/mozilla-mobile/android-components/issues/7753 - // - // engineView.release() + // This tab does not have an EngineSession that we can render yet. Let's dispatch an + // action to request creating one. Once one was created and linked to this session, this + // method will get invoked again. + store.dispatch(EngineAction.CreateEngineSessionAction(tab.id)) } else { engineView.render(engineSession) } diff --git a/components/feature/session/src/test/java/mozilla/components/feature/session/SessionFeatureTest.kt b/components/feature/session/src/test/java/mozilla/components/feature/session/SessionFeatureTest.kt index dd50554871b..98eec0b2c89 100644 --- a/components/feature/session/src/test/java/mozilla/components/feature/session/SessionFeatureTest.kt +++ b/components/feature/session/src/test/java/mozilla/components/feature/session/SessionFeatureTest.kt @@ -7,11 +7,13 @@ package mozilla.components.feature.session import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.TestCoroutineDispatcher +import kotlinx.coroutines.test.TestCoroutineScope import kotlinx.coroutines.test.resetMain import kotlinx.coroutines.test.setMain -import mozilla.components.browser.session.usecases.EngineSessionUseCases +import mozilla.components.browser.session.engine.EngineMiddleware import mozilla.components.browser.state.action.ContentAction import mozilla.components.browser.state.action.CustomTabListAction +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.createCustomTab @@ -27,17 +29,15 @@ import org.junit.After import org.junit.Assert.assertFalse import org.junit.Assert.assertTrue import org.junit.Before -import org.junit.Ignore import org.junit.Test -import org.mockito.ArgumentMatchers -import org.mockito.Mockito.anyString import org.mockito.Mockito.doReturn import org.mockito.Mockito.never -import org.mockito.Mockito.reset +import org.mockito.Mockito.spy import org.mockito.Mockito.verify class SessionFeatureTest { private val testDispatcher = TestCoroutineDispatcher() + private val scope = TestCoroutineScope(testDispatcher) @Before @ExperimentalCoroutinesApi @@ -53,333 +53,185 @@ class SessionFeatureTest { } @Test - fun `start() renders selected session`() { - val store = BrowserStore(BrowserState( - tabs = listOf( - createTab("https://www.mozilla.org", id = "A"), - createTab("https://getpocket.com", id = "B"), - createTab("https://www.firefox.com", id = "C") - ), - selectedTabId = "B" - )) - + fun `start renders selected session`() { + val store = prepareStore() val view: EngineView = mock() - val useCases: EngineSessionUseCases = mock() - val getOrCreateUseCase: EngineSessionUseCases.GetOrCreateUseCase = mock() - doReturn(getOrCreateUseCase).`when`(useCases).getOrCreateEngineSession val engineSession: EngineSession = mock() - doReturn(engineSession).`when`(getOrCreateUseCase).invoke(ArgumentMatchers.anyString()) - - val feature = SessionFeature(store, mock(), useCases, view) + store.dispatch(EngineAction.LinkEngineSessionAction("B", engineSession)).joinBlocking() - verify(getOrCreateUseCase, never()).invoke(anyString()) + val feature = SessionFeature(store, mock(), view) verify(view, never()).render(any()) feature.start() - testDispatcher.advanceUntilIdle() store.waitUntilIdle() - - verify(getOrCreateUseCase).invoke("B") verify(view).render(engineSession) } @Test - fun `start() renders fixed session`() { - val store = BrowserStore(BrowserState( - tabs = listOf( - createTab("https://www.mozilla.org", id = "A"), - createTab("https://getpocket.com", id = "B"), - createTab("https://www.firefox.com", id = "C") - ), - selectedTabId = "B" - )) - + fun `start renders fixed session`() { + val store = prepareStore() val view: EngineView = mock() - val useCases: EngineSessionUseCases = mock() - val getOrCreateUseCase: EngineSessionUseCases.GetOrCreateUseCase = mock() - doReturn(getOrCreateUseCase).`when`(useCases).getOrCreateEngineSession val engineSession: EngineSession = mock() - doReturn(engineSession).`when`(getOrCreateUseCase).invoke(ArgumentMatchers.anyString()) + store.dispatch(EngineAction.LinkEngineSessionAction("C", engineSession)).joinBlocking() - val feature = SessionFeature(store, mock(), useCases, view, tabId = "C") - - verify(getOrCreateUseCase, never()).invoke(anyString()) + val feature = SessionFeature(store, mock(), view, tabId = "C") verify(view, never()).render(any()) feature.start() - testDispatcher.advanceUntilIdle() store.waitUntilIdle() - - verify(getOrCreateUseCase).invoke("C") verify(view).render(engineSession) } @Test - fun `start() renders custom tab session`() { - val store = BrowserStore(BrowserState( - tabs = listOf( - createTab("https://www.mozilla.org", id = "A"), - createTab("https://getpocket.com", id = "B"), - createTab("https://www.firefox.com", id = "C") - ), - customTabs = listOf( - createCustomTab("https://hubs.mozilla.com/", id = "D") - ), - selectedTabId = "B" - )) + fun `start renders custom tab session`() { + val store = prepareStore() val view: EngineView = mock() - val useCases: EngineSessionUseCases = mock() - val getOrCreateUseCase: EngineSessionUseCases.GetOrCreateUseCase = mock() - doReturn(getOrCreateUseCase).`when`(useCases).getOrCreateEngineSession val engineSession: EngineSession = mock() - doReturn(engineSession).`when`(getOrCreateUseCase).invoke(ArgumentMatchers.anyString()) - - val feature = SessionFeature(store, mock(), useCases, view, tabId = "D") + store.dispatch(EngineAction.LinkEngineSessionAction("D", engineSession)).joinBlocking() - verify(getOrCreateUseCase, never()).invoke(anyString()) + val feature = SessionFeature(store, mock(), view, tabId = "D") verify(view, never()).render(any()) - feature.start() testDispatcher.advanceUntilIdle() store.waitUntilIdle() - - verify(getOrCreateUseCase).invoke("D") verify(view).render(engineSession) } @Test - fun `Renders selected tab after changes`() { - val store = BrowserStore(BrowserState( - tabs = listOf( - createTab("https://www.mozilla.org", id = "A"), - createTab("https://getpocket.com", id = "B"), - createTab("https://www.firefox.com", id = "C") - ), - selectedTabId = "B" - )) - + fun `renders selected tab after changes`() { + val store = prepareStore() val view: EngineView = mock() - val useCases: EngineSessionUseCases = mock() - val getOrCreateUseCase: EngineSessionUseCases.GetOrCreateUseCase = mock() - doReturn(getOrCreateUseCase).`when`(useCases).getOrCreateEngineSession - - val engineSession: EngineSession = mock() - doReturn(engineSession).`when`(getOrCreateUseCase).invoke(ArgumentMatchers.anyString()) - - val feature = SessionFeature(store, mock(), useCases, view) + val engineSessionA: EngineSession = mock() + val engineSessionB: EngineSession = mock() + store.dispatch(EngineAction.LinkEngineSessionAction("A", engineSessionA)).joinBlocking() + store.dispatch(EngineAction.LinkEngineSessionAction("B", engineSessionB)).joinBlocking() - verify(getOrCreateUseCase, never()).invoke(anyString()) + val feature = SessionFeature(store, mock(), view) verify(view, never()).render(any()) feature.start() - testDispatcher.advanceUntilIdle() store.waitUntilIdle() + verify(view).render(engineSessionB) - verify(getOrCreateUseCase).invoke("B") - verify(view).render(engineSession) - - reset(view) - reset(getOrCreateUseCase) - - val newEngineSession: EngineSession = mock() - doReturn(newEngineSession).`when`(getOrCreateUseCase).invoke(ArgumentMatchers.anyString()) - - store.dispatch( - TabListAction.SelectTabAction("A") - ).joinBlocking() - - verify(getOrCreateUseCase).invoke("A") - verify(view).render(newEngineSession) + store.dispatch(TabListAction.SelectTabAction("A")).joinBlocking() + testDispatcher.advanceUntilIdle() + store.waitUntilIdle() + verify(view).render(engineSessionA) } @Test - fun `Does not render new selected session after stop()`() { - val store = BrowserStore(BrowserState( - tabs = listOf( - createTab("https://www.mozilla.org", id = "A"), - createTab("https://getpocket.com", id = "B"), - createTab("https://www.firefox.com", id = "C") - ), - selectedTabId = "B" - )) - + fun `creates engine session if needed`() { + val store = spy(prepareStore()) val view: EngineView = mock() - val useCases: EngineSessionUseCases = mock() - val getOrCreateUseCase: EngineSessionUseCases.GetOrCreateUseCase = mock() - doReturn(getOrCreateUseCase).`when`(useCases).getOrCreateEngineSession - - val engineSession: EngineSession = mock() - doReturn(engineSession).`when`(getOrCreateUseCase).invoke(ArgumentMatchers.anyString()) - val feature = SessionFeature(store, mock(), useCases, view) - - verify(getOrCreateUseCase, never()).invoke(anyString()) + val feature = SessionFeature(store, mock(), view) verify(view, never()).render(any()) feature.start() - testDispatcher.advanceUntilIdle() store.waitUntilIdle() + verify(store).dispatch(EngineAction.CreateEngineSessionAction("B")) + } - verify(getOrCreateUseCase).invoke("B") - verify(view).render(engineSession) + @Test + fun `does not render new selected session after stop`() { + val store = prepareStore() + val view: EngineView = mock() + val engineSessionA: EngineSession = mock() + val engineSessionB: EngineSession = mock() + store.dispatch(EngineAction.LinkEngineSessionAction("A", engineSessionA)).joinBlocking() + store.dispatch(EngineAction.LinkEngineSessionAction("B", engineSessionB)).joinBlocking() - reset(view) - reset(getOrCreateUseCase) + val feature = SessionFeature(store, mock(), view) + verify(view, never()).render(any()) - val newEngineSession: EngineSession = mock() - doReturn(newEngineSession).`when`(getOrCreateUseCase).invoke(ArgumentMatchers.anyString()) + feature.start() + testDispatcher.advanceUntilIdle() + store.waitUntilIdle() + verify(view).render(engineSessionB) feature.stop() - store.dispatch( - TabListAction.SelectTabAction("A") - ).joinBlocking() - - verify(getOrCreateUseCase, never()).invoke("A") - verify(view, never()).render(newEngineSession) + store.dispatch(TabListAction.SelectTabAction("A")).joinBlocking() + testDispatcher.advanceUntilIdle() + store.waitUntilIdle() + verify(view, never()).render(engineSessionA) } @Test - @Ignore("https://github.com/mozilla-mobile/android-components/issues/7753") - fun `Releases when last selected session gets removed`() { - val store = BrowserStore(BrowserState( - tabs = listOf( - createTab("https://www.mozilla.org", id = "A") - ), - selectedTabId = "A" - )) - + fun `releases when last selected session gets removed`() { + val store = prepareStore() val view: EngineView = mock() - val useCases: EngineSessionUseCases = mock() - val getOrCreateUseCase: EngineSessionUseCases.GetOrCreateUseCase = mock() - doReturn(getOrCreateUseCase).`when`(useCases).getOrCreateEngineSession val engineSession: EngineSession = mock() - doReturn(engineSession).`when`(getOrCreateUseCase).invoke(ArgumentMatchers.anyString()) - - val feature = SessionFeature(store, mock(), useCases, view) + store.dispatch(EngineAction.LinkEngineSessionAction("B", engineSession)).joinBlocking() + val feature = SessionFeature(store, mock(), view) feature.start() - testDispatcher.advanceUntilIdle() store.waitUntilIdle() - - verify(getOrCreateUseCase).invoke("A") verify(view).render(engineSession) verify(view, never()).release() - store.dispatch( - TabListAction.RemoveTabAction("A") - ).joinBlocking() - + store.dispatch(TabListAction.RemoveAllTabsAction).joinBlocking() verify(view).release() } @Test - fun `release() stops observing and releases session from view`() { - val store = BrowserStore(BrowserState( - tabs = listOf( - createTab("https://www.mozilla.org", id = "A"), - createTab("https://getpocket.com", id = "B"), - createTab("https://www.firefox.com", id = "C") - ), - selectedTabId = "B" - )) - + fun `release stops observing and releases session from view`() { + val store = prepareStore() val view: EngineView = mock() - val useCases: EngineSessionUseCases = mock() - val getOrCreateUseCase: EngineSessionUseCases.GetOrCreateUseCase = mock() - doReturn(getOrCreateUseCase).`when`(useCases).getOrCreateEngineSession - val engineSession: EngineSession = mock() - doReturn(engineSession).`when`(getOrCreateUseCase).invoke(ArgumentMatchers.anyString()) + store.dispatch(EngineAction.LinkEngineSessionAction("B", engineSession)).joinBlocking() - val feature = SessionFeature(store, mock(), useCases, view) - - verify(getOrCreateUseCase, never()).invoke(anyString()) + val feature = SessionFeature(store, mock(), view) verify(view, never()).render(any()) feature.start() - testDispatcher.advanceUntilIdle() store.waitUntilIdle() - - verify(getOrCreateUseCase).invoke("B") verify(view).render(engineSession) - reset(view) - reset(getOrCreateUseCase) - val newEngineSession: EngineSession = mock() - doReturn(newEngineSession).`when`(getOrCreateUseCase).invoke(ArgumentMatchers.anyString()) - feature.release() - verify(view).release() - store.dispatch( - TabListAction.SelectTabAction("A") - ).joinBlocking() - - verify(getOrCreateUseCase, never()).invoke("A") + store.dispatch(TabListAction.SelectTabAction("A")).joinBlocking() verify(view, never()).render(newEngineSession) } @Test - @Ignore("https://github.com/mozilla-mobile/android-components/issues/7753") - fun `Releases when custom tab gets removed`() { - val store = BrowserStore(BrowserState( - tabs = listOf( - createTab("https://www.mozilla.org", id = "A"), - createTab("https://getpocket.com", id = "B"), - createTab("https://www.firefox.com", id = "C") - ), - customTabs = listOf( - createCustomTab("https://hubs.mozilla.com/", id = "D") - ), - selectedTabId = "B" - )) + fun `releases when custom tab gets removed`() { + val store = prepareStore() val view: EngineView = mock() - val useCases: EngineSessionUseCases = mock() - val getOrCreateUseCase: EngineSessionUseCases.GetOrCreateUseCase = mock() - doReturn(getOrCreateUseCase).`when`(useCases).getOrCreateEngineSession val engineSession: EngineSession = mock() - doReturn(engineSession).`when`(getOrCreateUseCase).invoke(ArgumentMatchers.anyString()) - - val feature = SessionFeature(store, mock(), useCases, view, tabId = "D") + store.dispatch(EngineAction.LinkEngineSessionAction("D", engineSession)).joinBlocking() - verify(getOrCreateUseCase, never()).invoke(anyString()) + val feature = SessionFeature(store, mock(), view, tabId = "D") verify(view, never()).render(any()) feature.start() - testDispatcher.advanceUntilIdle() store.waitUntilIdle() - - verify(getOrCreateUseCase).invoke("D") verify(view).render(engineSession) verify(view, never()).release() - store.dispatch( - CustomTabListAction.RemoveCustomTabAction("D") - ).joinBlocking() - + store.dispatch(CustomTabListAction.RemoveCustomTabAction("D")).joinBlocking() verify(view).release() } @Test - fun `onBackPressed() clears selection if it exists`() { + fun `onBackPressed clears selection if it exists`() { run { val view: EngineView = mock() doReturn(false).`when`(view).canClearSelection() - val feature = SessionFeature(BrowserStore(), mock(), mock(), view) + val feature = SessionFeature(BrowserStore(), mock(), view) assertFalse(feature.onBackPressed()) verify(view, never()).clearSelection() @@ -389,7 +241,7 @@ class SessionFeatureTest { val view: EngineView = mock() doReturn(true).`when`(view).canClearSelection() - val feature = SessionFeature(BrowserStore(), mock(), mock(), view) + val feature = SessionFeature(BrowserStore(), mock(), view) assertTrue(feature.onBackPressed()) verify(view).clearSelection() @@ -406,7 +258,7 @@ class SessionFeatureTest { val useCase: SessionUseCases.GoBackUseCase = mock() - val feature = SessionFeature(store, useCase, mock(), mock()) + val feature = SessionFeature(store, useCase, mock()) assertFalse(feature.onBackPressed()) verify(useCase, never()).invoke("A") @@ -425,10 +277,29 @@ class SessionFeatureTest { val useCase: SessionUseCases.GoBackUseCase = mock() - val feature = SessionFeature(store, useCase, mock(), mock()) + val feature = SessionFeature(store, useCase, mock()) assertTrue(feature.onBackPressed()) verify(useCase).invoke("A") } } + + private fun prepareStore(): BrowserStore = BrowserStore( + BrowserState( + tabs = listOf( + createTab("https://www.mozilla.org", id = "A"), + createTab("https://getpocket.com", id = "B"), + createTab("https://www.firefox.com", id = "C") + ), + customTabs = listOf( + createCustomTab("https://hubs.mozilla.com/", id = "D") + ), + selectedTabId = "B" + ), + middleware = EngineMiddleware.create( + engine = mock(), + sessionLookup = { null }, + scope = scope + ) + ) } diff --git a/components/feature/session/src/test/java/mozilla/components/feature/session/SessionUseCasesTest.kt b/components/feature/session/src/test/java/mozilla/components/feature/session/SessionUseCasesTest.kt index 3747f928bd3..7d295db2a0c 100644 --- a/components/feature/session/src/test/java/mozilla/components/feature/session/SessionUseCasesTest.kt +++ b/components/feature/session/src/test/java/mozilla/components/feature/session/SessionUseCasesTest.kt @@ -4,229 +4,217 @@ package mozilla.components.feature.session +import kotlinx.coroutines.runBlocking import mozilla.components.browser.session.Session import mozilla.components.browser.session.SessionManager +import mozilla.components.browser.state.action.CrashAction +import mozilla.components.browser.state.action.CustomTabListAction +import mozilla.components.browser.state.action.EngineAction +import mozilla.components.browser.state.action.TabListAction +import mozilla.components.browser.state.state.createCustomTab +import mozilla.components.browser.state.state.createTab +import mozilla.components.browser.state.store.BrowserStore import mozilla.components.concept.engine.Engine import mozilla.components.concept.engine.Engine.BrowsingData import mozilla.components.concept.engine.EngineSession import mozilla.components.concept.engine.EngineSession.LoadUrlFlags -import mozilla.components.support.test.any -import mozilla.components.support.test.eq +import mozilla.components.support.test.argumentCaptor +import mozilla.components.support.test.ext.joinBlocking import mozilla.components.support.test.mock import mozilla.components.support.test.whenever import org.junit.Assert.assertEquals import org.junit.Assert.assertNotNull -import org.junit.Assert.assertTrue import org.junit.Before import org.junit.Test -import org.mockito.ArgumentMatchers.anyBoolean -import org.mockito.Mockito.doReturn import org.mockito.Mockito.never +import org.mockito.Mockito.spy +import org.mockito.Mockito.times import org.mockito.Mockito.verify class SessionUseCasesTest { - private val sessionManager = mock() - private val selectedEngineSession = mock() - private val selectedSession = mock() - private val useCases = SessionUseCases(sessionManager) + private val sessionManager: SessionManager = mock() + private val selectedSessionId = "testSession" + private val selectedSession: Session = mock() + private val store: BrowserStore = mock() + private val useCases = SessionUseCases(store, sessionManager) @Before fun setup() { + whenever(selectedSession.id).thenReturn(selectedSessionId) whenever(sessionManager.selectedSessionOrThrow).thenReturn(selectedSession) whenever(sessionManager.selectedSession).thenReturn(selectedSession) - whenever(sessionManager.getOrCreateEngineSession()).thenReturn(selectedEngineSession) } @Test fun loadUrl() { useCases.loadUrl("http://mozilla.org") - verify(selectedEngineSession).loadUrl("http://mozilla.org") + verify(store).dispatch(EngineAction.LoadUrlAction(selectedSessionId, "http://mozilla.org")) useCases.loadUrl("http://www.mozilla.org", LoadUrlFlags.select(LoadUrlFlags.EXTERNAL)) - verify(selectedEngineSession).loadUrl("http://www.mozilla.org", flags = LoadUrlFlags.select(LoadUrlFlags.EXTERNAL)) + verify(store).dispatch(EngineAction.LoadUrlAction( + selectedSessionId, + "http://www.mozilla.org", + LoadUrlFlags.select(LoadUrlFlags.EXTERNAL) + )) useCases.loadUrl("http://getpocket.com", selectedSession) - verify(selectedEngineSession).loadUrl("http://getpocket.com") - - useCases.loadUrl.invoke("http://www.getpocket.com", selectedSession, - LoadUrlFlags.select(LoadUrlFlags.BYPASS_PROXY)) - verify(selectedEngineSession).loadUrl("http://www.getpocket.com", - flags = LoadUrlFlags.select(LoadUrlFlags.BYPASS_PROXY)) + verify(store).dispatch(EngineAction.LoadUrlAction(selectedSessionId, "http://getpocket.com")) + + useCases.loadUrl.invoke( + "http://getpocket.com", + selectedSession, + LoadUrlFlags.select(LoadUrlFlags.BYPASS_PROXY) + ) + verify(store).dispatch(EngineAction.LoadUrlAction( + selectedSessionId, + "http://getpocket.com", + LoadUrlFlags.select(LoadUrlFlags.BYPASS_PROXY) + )) } @Test fun loadData() { useCases.loadData("", "text/html") - verify(selectedEngineSession).loadData("", "text/html", "UTF-8") - + verify(store).dispatch(EngineAction.LoadDataAction( + selectedSessionId, + "", + "text/html", + "UTF-8") + ) useCases.loadData("Should load in WebView", "text/plain", session = selectedSession) - verify(selectedEngineSession).loadData("Should load in WebView", "text/plain", "UTF-8") + verify(store).dispatch(EngineAction.LoadDataAction( + selectedSessionId, + "Should load in WebView", + "text/plain", + "UTF-8") + ) useCases.loadData("Should also load in WebView", "text/plain", "base64", selectedSession) - verify(selectedEngineSession).loadData("Should also load in WebView", "text/plain", "base64") + verify(store).dispatch(EngineAction.LoadDataAction( + selectedSessionId, + "Should also load in WebView", + "text/plain", + "base64") + ) } @Test fun reload() { - val engineSession = mock() - val session = mock() - whenever(sessionManager.getOrCreateEngineSession(session)).thenReturn(engineSession) - - useCases.reload(session) - verify(engineSession).reload() - - whenever(sessionManager.getOrCreateEngineSession(selectedSession)).thenReturn(selectedEngineSession) useCases.reload() - verify(selectedEngineSession).reload() + verify(store).dispatch(EngineAction.ReloadAction(selectedSessionId)) + + whenever(sessionManager.findSessionById("testSession")).thenReturn(selectedSession) + useCases.reload(selectedSessionId, LoadUrlFlags.select(LoadUrlFlags.EXTERNAL)) + verify(store).dispatch(EngineAction.ReloadAction(selectedSessionId, LoadUrlFlags.select(LoadUrlFlags.EXTERNAL))) } @Test fun reloadBypassCache() { - val engineSession = mock() - val session = mock() val flags = LoadUrlFlags.select(LoadUrlFlags.BYPASS_CACHE) - whenever(sessionManager.getOrCreateEngineSession(session)).thenReturn(engineSession) - - useCases.reload(session, flags = flags) - verify(engineSession).reload(flags = flags) - - whenever(sessionManager.getOrCreateEngineSession(selectedSession)).thenReturn(selectedEngineSession) - useCases.reload(flags = flags) - verify(selectedEngineSession).reload(flags = flags) + useCases.reload(selectedSession, flags = flags) + verify(store).dispatch(EngineAction.ReloadAction(selectedSessionId, flags)) } @Test - fun stopLoading() { - val engineSession = mock() - val session = mock() - whenever(sessionManager.getOrCreateEngineSession(session)).thenReturn(engineSession) + fun stopLoading() = runBlocking { + val store = spy(BrowserStore()) + val useCases = SessionUseCases(store, sessionManager) + val engineSession: EngineSession = mock() + store.dispatch(TabListAction.AddTabAction(createTab("https://wwww.mozilla.org", id = selectedSessionId))).joinBlocking() + store.dispatch(EngineAction.LinkEngineSessionAction(selectedSessionId, engineSession)).joinBlocking() - useCases.stopLoading(session) + useCases.stopLoading() verify(engineSession).stopLoading() - whenever(sessionManager.getOrCreateEngineSession(selectedSession)).thenReturn(selectedEngineSession) - useCases.stopLoading() - verify(selectedEngineSession).stopLoading() + useCases.stopLoading(selectedSession) + verify(engineSession, times(2)).stopLoading() } @Test fun goBack() { - val engineSession = mock() - val session = mock() - useCases.goBack(null) - verify(engineSession, never()).goBack() - verify(selectedEngineSession, never()).goBack() - - whenever(sessionManager.getOrCreateEngineSession(session)).thenReturn(engineSession) + verify(store, never()).dispatch(EngineAction.GoBackAction(selectedSessionId)) - useCases.goBack(session) - verify(engineSession).goBack() + useCases.goBack(selectedSession) + verify(store).dispatch(EngineAction.GoBackAction(selectedSessionId)) - whenever(sessionManager.getOrCreateEngineSession(selectedSession)).thenReturn(selectedEngineSession) useCases.goBack() - verify(selectedEngineSession).goBack() + verify(store, times(2)).dispatch(EngineAction.GoBackAction(selectedSessionId)) } @Test fun goForward() { - val engineSession = mock() - val session = mock() - useCases.goForward(null) - verify(engineSession, never()).goForward() - verify(selectedEngineSession, never()).goForward() + verify(store, never()).dispatch(EngineAction.GoForwardAction(selectedSessionId)) - whenever(sessionManager.getOrCreateEngineSession(session)).thenReturn(engineSession) + useCases.goForward(selectedSession) + verify(store).dispatch(EngineAction.GoForwardAction(selectedSessionId)) - useCases.goForward(session) - verify(engineSession).goForward() - - whenever(sessionManager.getOrCreateEngineSession(selectedSession)).thenReturn(selectedEngineSession) useCases.goForward() - verify(selectedEngineSession).goForward() + verify(store, times(2)).dispatch(EngineAction.GoForwardAction(selectedSessionId)) } @Test fun goToHistoryIndex() { - val engineSession = mock() - val session = mock() - useCases.goToHistoryIndex(session = null, index = 0) - verify(engineSession, never()).goToHistoryIndex(0) - verify(selectedEngineSession, never()).goToHistoryIndex(0) + verify(store, never()).dispatch(EngineAction.GoToHistoryIndexAction(selectedSessionId, 0)) - whenever(sessionManager.getOrCreateEngineSession(session)).thenReturn(engineSession) + useCases.goToHistoryIndex(session = selectedSession, index = 0) + verify(store).dispatch(EngineAction.GoToHistoryIndexAction(selectedSessionId, 0)) - useCases.goToHistoryIndex(session = session, index = 0) - verify(engineSession).goToHistoryIndex(0) - - whenever(sessionManager.getOrCreateEngineSession(selectedSession)).thenReturn(selectedEngineSession) useCases.goToHistoryIndex(index = 0) - verify(selectedEngineSession).goToHistoryIndex(0) + verify(store, times(2)).dispatch(EngineAction.GoToHistoryIndexAction(selectedSessionId, 0)) } @Test fun requestDesktopSite() { - val engineSession = mock() - val session = mock() - whenever(sessionManager.getOrCreateEngineSession(session)).thenReturn(engineSession) - - useCases.requestDesktopSite(true, session) - verify(engineSession).toggleDesktopMode(true, reload = true) + useCases.requestDesktopSite(true, selectedSession) + verify(store).dispatch(EngineAction.ToggleDesktopModeAction(selectedSessionId, true)) - whenever(sessionManager.getOrCreateEngineSession(selectedSession)).thenReturn(selectedEngineSession) - useCases.requestDesktopSite(true) - verify(selectedEngineSession).toggleDesktopMode(true, reload = true) + useCases.requestDesktopSite(false) + verify(store).dispatch(EngineAction.ToggleDesktopModeAction(selectedSessionId, false)) } @Test fun exitFullscreen() { - val engineSession = mock() - val session = mock() - whenever(sessionManager.getOrCreateEngineSession(session)).thenReturn(engineSession) - - useCases.exitFullscreen(session) - verify(engineSession).exitFullScreenMode() + useCases.exitFullscreen(selectedSession) + verify(store).dispatch(EngineAction.ExitFullscreenModeAction(selectedSessionId)) - whenever(sessionManager.getOrCreateEngineSession(selectedSession)).thenReturn(selectedEngineSession) useCases.exitFullscreen() - verify(selectedEngineSession).exitFullScreenMode() + verify(store, times(2)).dispatch(EngineAction.ExitFullscreenModeAction(selectedSessionId)) } @Test fun clearData() { - val engineSession = mock() - val session = mock() - val engine = mock() + val engine: Engine = mock() whenever(sessionManager.engine).thenReturn(engine) - whenever(sessionManager.getOrCreateEngineSession(session)).thenReturn(engineSession) - useCases.clearData(session) + useCases.clearData(selectedSession) verify(engine).clearData() - verify(engineSession).clearData() + verify(store).dispatch(EngineAction.ClearDataAction(selectedSessionId, BrowsingData.all())) useCases.clearData(data = BrowsingData.select(BrowsingData.COOKIES)) - verify(engine).clearData(eq(BrowsingData.select(BrowsingData.COOKIES)), eq(null), any(), any()) + verify(store).dispatch(EngineAction.ClearDataAction(selectedSessionId, + BrowsingData.select(BrowsingData.COOKIES)) + ) - useCases.clearData(session, data = BrowsingData.select(BrowsingData.COOKIES)) - verify(engineSession).clearData(eq(BrowsingData.select(BrowsingData.COOKIES)), eq(null), any(), any()) + useCases.clearData(selectedSession, data = BrowsingData.select(BrowsingData.IMAGE_CACHE)) + verify(store).dispatch(EngineAction.ClearDataAction(selectedSessionId, + BrowsingData.select(BrowsingData.IMAGE_CACHE)) + ) - whenever(sessionManager.getOrCreateEngineSession(selectedSession)).thenReturn(selectedEngineSession) useCases.clearData() - verify(selectedEngineSession).clearData() + verify(store, times(2)).dispatch(EngineAction.ClearDataAction(selectedSessionId, BrowsingData.all())) } @Test fun `LoadUrlUseCase will invoke onNoSession lambda if no selected session exists`() { var createdSession: Session? = null var sessionCreatedForUrl: String? = null - whenever(sessionManager.selectedSession).thenReturn(null) - whenever(sessionManager.getOrCreateEngineSession(any(), anyBoolean())).thenReturn(mock()) - val loadUseCase = SessionUseCases.DefaultLoadUrlUseCase(sessionManager) { url -> + val loadUseCase = SessionUseCases.DefaultLoadUrlUseCase(store, sessionManager) { url -> sessionCreatedForUrl = url Session(url).also { createdSession = it } } @@ -235,91 +223,55 @@ class SessionUseCasesTest { assertEquals("https://www.example.com", sessionCreatedForUrl) assertNotNull(createdSession) - verify(sessionManager).getOrCreateEngineSession(createdSession!!) + + val actionCaptor = argumentCaptor() + verify(store).dispatch(actionCaptor.capture()) + assertEquals(createdSession!!.id, actionCaptor.value.sessionId) + assertEquals(sessionCreatedForUrl, actionCaptor.value.url) } @Test fun `LoadDataUseCase will invoke onNoSession lambda if no selected session exists`() { var createdSession: Session? = null var sessionCreatedForUrl: String? = null - - val engineSession: EngineSession = mock() - whenever(sessionManager.selectedSession).thenReturn(null) - whenever(sessionManager.getOrCreateEngineSession(any(), anyBoolean())).thenReturn(engineSession) - val loadUseCase = SessionUseCases.LoadDataUseCase(sessionManager) { url -> + val loadUseCase = SessionUseCases.LoadDataUseCase(store, sessionManager) { url -> sessionCreatedForUrl = url Session(url).also { createdSession = it } } - loadUseCase("Hello", mimeType = "plain/text", encoding = "UTF-8") + loadUseCase("Hello", mimeType = "text/plain", encoding = "UTF-8") assertEquals("about:blank", sessionCreatedForUrl) assertNotNull(createdSession) - verify(sessionManager).getOrCreateEngineSession(createdSession!!) - verify(engineSession).loadData("Hello", mimeType = "plain/text", encoding = "UTF-8") - } - - @Test - fun `CrashRecoveryUseCase will invoke recoverFromCrash on engine session and reset flag`() { - val engineSession = mock() - doReturn(true).`when`(engineSession).recoverFromCrash() - - val session = mock() - whenever(sessionManager.getOrCreateEngineSession(session)).thenReturn(engineSession) - - assertTrue(useCases.crashRecovery.invoke(session)) - verify(engineSession).recoverFromCrash() - verify(session).crashed = false + val actionCaptor = argumentCaptor() + verify(store).dispatch(actionCaptor.capture()) + assertEquals(createdSession!!.id, actionCaptor.value.sessionId) + assertEquals("Hello", actionCaptor.value.data) + assertEquals("text/plain", actionCaptor.value.mimeType) + assertEquals("UTF-8", actionCaptor.value.encoding) } @Test - fun `CrashRecoveryUseCase will restore list of crashed sessions`() { - val engineSession1 = mock() - doReturn(true).`when`(engineSession1).recoverFromCrash() - - val engineSession2 = mock() - doReturn(true).`when`(engineSession2).recoverFromCrash() - - val session1 = mock() - whenever(sessionManager.getOrCreateEngineSession(session1)).thenReturn(engineSession1) - - val session2 = mock() - whenever(sessionManager.getOrCreateEngineSession(session2)).thenReturn(engineSession2) - - assertTrue(useCases.crashRecovery.invoke(listOf(session1, session2))) - - verify(engineSession1).recoverFromCrash() - verify(engineSession2).recoverFromCrash() - verify(session1).crashed = false - verify(session2).crashed = false + fun `CrashRecoveryUseCase will restore specified session`() { + useCases.crashRecovery.invoke(listOf(selectedSessionId)) + verify(store).dispatch(CrashAction.RestoreCrashedSessionAction(selectedSessionId)) } @Test - fun `CrashRecoveryUseCase will restore crashed sessions`() { - val engineSession1 = mock() - doReturn(true).`when`(engineSession1).recoverFromCrash() - - val engineSession2 = mock() - doReturn(true).`when`(engineSession2).recoverFromCrash() - - val session1 = mock() - whenever(sessionManager.getOrCreateEngineSession(session1)).thenReturn(engineSession1) - doReturn(true).`when`(session1).crashed - - val session2 = mock() - doReturn(false).`when`(session2).crashed - whenever(sessionManager.getOrCreateEngineSession(session2)).thenReturn(engineSession2) - - doReturn(listOf(session1, session2)).`when`(sessionManager).sessions + fun `CrashRecoveryUseCase will restore list of crashed sessions`() { + val store = spy(BrowserStore()) + val useCases = SessionUseCases(store, sessionManager) - assertTrue(useCases.crashRecovery.invoke()) + store.dispatch(TabListAction.AddTabAction(createTab("https://wwww.mozilla.org", id = "tab1"))).joinBlocking() + store.dispatch(CustomTabListAction.AddCustomTabAction(createCustomTab("https://wwww.mozilla.org", id = "customTab1"))).joinBlocking() + store.dispatch(CrashAction.SessionCrashedAction("tab1")).joinBlocking() + store.dispatch(CrashAction.SessionCrashedAction("customTab1")).joinBlocking() - verify(engineSession1).recoverFromCrash() - verify(engineSession2, never()).recoverFromCrash() - verify(session1).crashed = false - verify(session2, never()).crashed = false + useCases.crashRecovery.invoke() + verify(store).dispatch(CrashAction.RestoreCrashedSessionAction("tab1")) + verify(store).dispatch(CrashAction.RestoreCrashedSessionAction("customTab1")) } } diff --git a/components/feature/tabs/src/main/java/mozilla/components/feature/tabs/TabsUseCases.kt b/components/feature/tabs/src/main/java/mozilla/components/feature/tabs/TabsUseCases.kt index b52e6390229..f2922b5dfe0 100644 --- a/components/feature/tabs/src/main/java/mozilla/components/feature/tabs/TabsUseCases.kt +++ b/components/feature/tabs/src/main/java/mozilla/components/feature/tabs/TabsUseCases.kt @@ -7,7 +7,7 @@ package mozilla.components.feature.tabs import mozilla.components.browser.session.Session import mozilla.components.browser.state.state.SessionState.Source import mozilla.components.browser.session.SessionManager -import mozilla.components.browser.state.selector.findTabOrCustomTab +import mozilla.components.browser.state.action.EngineAction import mozilla.components.browser.state.store.BrowserStore import mozilla.components.concept.engine.EngineSession import mozilla.components.concept.engine.EngineSession.LoadUrlFlags @@ -153,10 +153,11 @@ class TabsUseCases( // If an engine session is specified then loading will have already started // during sessionManager.add when linking the session to its engine session. if (startLoading && engineSession == null) { - val parentEngineSession = parent?.let { - store.state.findTabOrCustomTab(it.id)?.engineState?.engineSession - } - sessionManager.getOrCreateEngineSession(session, true).loadUrl(url, parentEngineSession, flags) + store.dispatch(EngineAction.LoadUrlAction( + session.id, + url, + flags + )) } return session @@ -204,10 +205,11 @@ class TabsUseCases( // If an engine session is specified then loading will have already started // during sessionManager.add when linking the session to its engine session. if (startLoading && engineSession == null) { - val parentEngineSession = parent?.let { - store.state.findTabOrCustomTab(it.id)?.engineState?.engineSession - } - sessionManager.getOrCreateEngineSession(session, true).loadUrl(url, parentEngineSession, flags) + store.dispatch(EngineAction.LoadUrlAction( + session.id, + url, + flags + )) } return session diff --git a/components/feature/tabs/src/test/java/mozilla/components/feature/tabs/TabsUseCasesTest.kt b/components/feature/tabs/src/test/java/mozilla/components/feature/tabs/TabsUseCasesTest.kt index d37c9f60f8d..a1c05411c14 100644 --- a/components/feature/tabs/src/test/java/mozilla/components/feature/tabs/TabsUseCasesTest.kt +++ b/components/feature/tabs/src/test/java/mozilla/components/feature/tabs/TabsUseCasesTest.kt @@ -6,27 +6,21 @@ package mozilla.components.feature.tabs import androidx.test.ext.junit.runners.AndroidJUnit4 import mozilla.components.browser.session.Session -import mozilla.components.browser.state.state.SessionState.Source import mozilla.components.browser.session.SessionManager import mozilla.components.browser.state.action.EngineAction +import mozilla.components.browser.state.state.SessionState.Source import mozilla.components.browser.state.store.BrowserStore -import mozilla.components.concept.engine.Engine import mozilla.components.concept.engine.EngineSession import mozilla.components.concept.engine.EngineSession.LoadUrlFlags -import mozilla.components.support.test.any -import mozilla.components.support.test.ext.joinBlocking +import mozilla.components.support.test.argumentCaptor import mozilla.components.support.test.mock -import mozilla.components.support.test.whenever import org.junit.Assert.assertEquals import org.junit.Assert.assertFalse import org.junit.Assert.assertTrue import org.junit.Test import org.junit.runner.RunWith -import org.mockito.ArgumentMatchers.anyBoolean -import org.mockito.Mockito.doReturn import org.mockito.Mockito.never import org.mockito.Mockito.spy -import org.mockito.Mockito.times import org.mockito.Mockito.verify @RunWith(AndroidJUnit4::class) @@ -69,10 +63,8 @@ class TabsUseCasesTest { @Test fun `AddNewTabUseCase - session will be added to session manager`() { val sessionManager = spy(SessionManager(mock())) - val engineSession: EngineSession = mock() - doReturn(engineSession).`when`(sessionManager).getOrCreateEngineSession(any(), anyBoolean()) - - val useCases = TabsUseCases(BrowserStore(), sessionManager) + val store: BrowserStore = mock() + val useCases = TabsUseCases(store, sessionManager) assertEquals(0, sessionManager.size) @@ -83,16 +75,16 @@ class TabsUseCasesTest { assertEquals(Source.NEW_TAB, sessionManager.selectedSessionOrThrow.source) assertFalse(sessionManager.selectedSessionOrThrow.private) - verify(engineSession).loadUrl("https://www.mozilla.org") + val actionCaptor = argumentCaptor() + verify(store).dispatch(actionCaptor.capture()) + assertEquals("https://www.mozilla.org", actionCaptor.value.url) } @Test fun `AddNewPrivateTabUseCase - private session will be added to session manager`() { val sessionManager = spy(SessionManager(mock())) - val engineSession: EngineSession = mock() - doReturn(engineSession).`when`(sessionManager).getOrCreateEngineSession(any(), anyBoolean()) - - val useCases = TabsUseCases(BrowserStore(), sessionManager) + val store: BrowserStore = mock() + val useCases = TabsUseCases(store, sessionManager) assertEquals(0, sessionManager.size) @@ -103,82 +95,63 @@ class TabsUseCasesTest { assertEquals(Source.NEW_TAB, sessionManager.selectedSessionOrThrow.source) assertTrue(sessionManager.selectedSessionOrThrow.private) - verify(engineSession).loadUrl("https://www.mozilla.org") + val actionCaptor = argumentCaptor() + verify(store).dispatch(actionCaptor.capture()) + assertEquals("https://www.mozilla.org", actionCaptor.value.url) } @Test fun `AddNewTabUseCase will not load URL if flag is set to false`() { val sessionManager = spy(SessionManager(mock())) - val engineSession: EngineSession = mock() - doReturn(engineSession).`when`(sessionManager).getOrCreateEngineSession(any(), anyBoolean()) - - val useCases = TabsUseCases(BrowserStore(), sessionManager) + val store: BrowserStore = mock() + val useCases = TabsUseCases(store, sessionManager) useCases.addTab("https://www.mozilla.org", startLoading = false) - verify(engineSession, never()).loadUrl("https://www.mozilla.org") + val actionCaptor = argumentCaptor() + verify(store, never()).dispatch(actionCaptor.capture()) } @Test fun `AddNewTabUseCase will load URL if flag is set to true`() { - val engineSession: EngineSession = mock() - val engine: Engine = mock() - whenever(engine.createSession(false, null)).thenReturn(engineSession) - val sessionManager = SessionManager(engine) + val sessionManager: SessionManager = mock() + val store: BrowserStore = mock() + val useCases = TabsUseCases(store, sessionManager) - val useCases = TabsUseCases(BrowserStore(), sessionManager) useCases.addTab("https://www.mozilla.org", startLoading = true) - verify(engineSession, times(1)).loadUrl("https://www.mozilla.org") + val actionCaptor = argumentCaptor() + verify(store).dispatch(actionCaptor.capture()) + assertEquals("https://www.mozilla.org", actionCaptor.value.url) } @Test fun `AddNewTabUseCase forwards load flags to engine`() { - val sessionManager = spy(SessionManager(mock())) - val engineSession: EngineSession = mock() - doReturn(engineSession).`when`(sessionManager).getOrCreateEngineSession(any(), anyBoolean()) - - val useCases = TabsUseCases(BrowserStore(), sessionManager) + val sessionManager: SessionManager = mock() + val store: BrowserStore = mock() + val useCases = TabsUseCases(store, sessionManager) useCases.addTab.invoke("https://www.mozilla.org", LoadUrlFlags.select(LoadUrlFlags.EXTERNAL)) - verify(engineSession).loadUrl("https://www.mozilla.org", flags = LoadUrlFlags.select(LoadUrlFlags.EXTERNAL)) + val actionCaptor = argumentCaptor() + verify(store).dispatch(actionCaptor.capture()) + assertEquals("https://www.mozilla.org", actionCaptor.value.url) + assertEquals(LoadUrlFlags.select(LoadUrlFlags.EXTERNAL), actionCaptor.value.flags) } @Test - fun `AddNewTabUseCase forwards parent session to engine`() { + fun `AddNewTabUseCase uses provided engine session`() { val store = BrowserStore() val sessionManager = spy(SessionManager(mock(), store)) val engineSession: EngineSession = mock() - doReturn(engineSession).`when`(sessionManager).getOrCreateEngineSession(any(), anyBoolean()) - - val parentSession = Session(id = "parent", initialUrl = "") - sessionManager.add(parentSession) - val parentEngineSession: EngineSession = mock() - - store.dispatch(EngineAction.LinkEngineSessionAction( - "parent", parentEngineSession - )).joinBlocking() val useCases = TabsUseCases(store, sessionManager) - useCases.addTab.invoke("https://www.mozilla.org", parentId = "parent", startLoading = true) - verify(engineSession).loadUrl("https://www.mozilla.org", parentEngineSession, LoadUrlFlags.none()) - } - - @Test - fun `AddNewTabUseCase uses provided engine session`() { - val sessionManager = spy(SessionManager(mock())) - val engineSession: EngineSession = mock() - - val useCases = TabsUseCases(BrowserStore(), sessionManager) useCases.addTab.invoke("https://www.mozilla.org", engineSession = engineSession, startLoading = true) - verify(engineSession).loadUrl("https://www.mozilla.org", null, LoadUrlFlags.none()) + assertEquals(1, store.state.tabs.size) + assertEquals(engineSession, store.state.tabs.first().engineState.engineSession) } @Test fun `AddNewTabUseCase uses provided contextId`() { val sessionManager = spy(SessionManager(mock())) - val engineSession: EngineSession = mock() - doReturn(engineSession).`when`(sessionManager).getOrCreateEngineSession(any(), anyBoolean()) - val useCases = TabsUseCases(BrowserStore(), sessionManager) assertEquals(0, sessionManager.size) @@ -190,126 +163,93 @@ class TabsUseCasesTest { assertEquals("1", sessionManager.selectedSessionOrThrow.contextId) assertEquals(Source.NEW_TAB, sessionManager.selectedSessionOrThrow.source) assertFalse(sessionManager.selectedSessionOrThrow.private) - - verify(engineSession).loadUrl("https://www.mozilla.org") } @Test fun `AddNewPrivateTabUseCase will not load URL if flag is set to false`() { val sessionManager = spy(SessionManager(mock())) - val engineSession: EngineSession = mock() - doReturn(engineSession).`when`(sessionManager).getOrCreateEngineSession(any(), anyBoolean()) - - val useCases = TabsUseCases(BrowserStore(), sessionManager) + val store: BrowserStore = mock() + val useCases = TabsUseCases(store, sessionManager) useCases.addPrivateTab("https://www.mozilla.org", startLoading = false) - verify(engineSession, never()).loadUrl("https://www.mozilla.org") + val actionCaptor = argumentCaptor() + verify(store, never()).dispatch(actionCaptor.capture()) } @Test fun `AddNewPrivateTabUseCase will load URL if flag is set to true`() { - val engineSession: EngineSession = mock() - val engine: Engine = mock() - whenever(engine.createSession(true, null)).thenReturn(engineSession) - val sessionManager = SessionManager(engine) + val sessionManager = spy(SessionManager(mock())) + val store: BrowserStore = mock() + val useCases = TabsUseCases(store, sessionManager) - val useCases = TabsUseCases(BrowserStore(), sessionManager) useCases.addPrivateTab("https://www.mozilla.org", startLoading = true) - verify(engineSession, times(1)).loadUrl("https://www.mozilla.org") + + val actionCaptor = argumentCaptor() + verify(store).dispatch(actionCaptor.capture()) + assertEquals("https://www.mozilla.org", actionCaptor.value.url) } @Test fun `AddNewPrivateTabUseCase forwards load flags to engine`() { - val sessionManager = spy(SessionManager(mock())) - val engineSession: EngineSession = mock() - doReturn(engineSession).`when`(sessionManager).getOrCreateEngineSession(any(), anyBoolean()) - - val useCases = TabsUseCases(BrowserStore(), sessionManager) + val sessionManager: SessionManager = mock() + val store: BrowserStore = mock() + val useCases = TabsUseCases(store, sessionManager) useCases.addPrivateTab.invoke("https://www.mozilla.org", LoadUrlFlags.select(LoadUrlFlags.EXTERNAL)) - verify(engineSession).loadUrl("https://www.mozilla.org", flags = LoadUrlFlags.select(LoadUrlFlags.EXTERNAL)) + val actionCaptor = argumentCaptor() + verify(store).dispatch(actionCaptor.capture()) + assertEquals("https://www.mozilla.org", actionCaptor.value.url) + assertEquals(LoadUrlFlags.select(LoadUrlFlags.EXTERNAL), actionCaptor.value.flags) } @Test - fun `AddNewPrivateTabUseCase forwards parent session to engine`() { + fun `AddNewPrivateTabUseCase uses provided engine session`() { val store = BrowserStore() val sessionManager = spy(SessionManager(mock(), store)) val engineSession: EngineSession = mock() - doReturn(engineSession).`when`(sessionManager).getOrCreateEngineSession(any(), anyBoolean()) - - val parentSession = Session(id = "parent", initialUrl = "") - sessionManager.add(parentSession) - val parentEngineSession: EngineSession = mock() - - store.dispatch(EngineAction.LinkEngineSessionAction( - "parent", parentEngineSession - )).joinBlocking() val useCases = TabsUseCases(store, sessionManager) - useCases.addPrivateTab.invoke("https://www.mozilla.org", parentId = "parent", startLoading = true) - verify(engineSession).loadUrl("https://www.mozilla.org", parentEngineSession, LoadUrlFlags.none()) - } - - @Test - fun `AddNewPrivateTabUseCase uses provided engine session`() { - val sessionManager = spy(SessionManager(mock())) - val engineSession: EngineSession = mock() - - val useCases = TabsUseCases(BrowserStore(), sessionManager) useCases.addPrivateTab.invoke("https://www.mozilla.org", engineSession = engineSession, startLoading = true) - verify(engineSession).loadUrl("https://www.mozilla.org", null, LoadUrlFlags.none()) + assertEquals(1, store.state.tabs.size) + assertEquals(engineSession, store.state.tabs.first().engineState.engineSession) } @Test fun `RemoveAllTabsUseCase will remove all sessions`() { val sessionManager = spy(SessionManager(mock())) - val engineSession: EngineSession = mock() - doReturn(engineSession).`when`(sessionManager).getOrCreateEngineSession(any(), anyBoolean()) - val useCases = TabsUseCases(BrowserStore(), sessionManager) useCases.addPrivateTab("https://www.mozilla.org") useCases.addTab("https://www.mozilla.org") - assertEquals(2, sessionManager.size) useCases.removeAllTabs() - assertEquals(0, sessionManager.size) - verify(sessionManager).removeSessions() } @Test fun `RemoveAllTabsOfTypeUseCase will remove sessions for particular type of tabs private or normal`() { val sessionManager = spy(SessionManager(mock())) - val engineSession: EngineSession = mock() - doReturn(engineSession).`when`(sessionManager).getOrCreateEngineSession(any(), anyBoolean()) - val useCases = TabsUseCases(BrowserStore(), sessionManager) val session1 = Session("https://www.mozilla.org") session1.customTabConfig = mock() sessionManager.add(session1) - useCases.addPrivateTab("https://www.mozilla.org") useCases.addTab("https://www.mozilla.org") - assertEquals(3, sessionManager.size) useCases.removeAllTabsOfType(private = false) - assertEquals(2, sessionManager.all.size) useCases.addPrivateTab("https://www.mozilla.org") useCases.addTab("https://www.mozilla.org") useCases.addTab("https://www.mozilla.org") - assertEquals(5, sessionManager.size) useCases.removeAllTabsOfType(private = true) - assertEquals(3, sessionManager.size) useCases.removeAllTabsOfType(private = false) diff --git a/samples/browser/src/main/java/org/mozilla/samples/browser/BaseBrowserFragment.kt b/samples/browser/src/main/java/org/mozilla/samples/browser/BaseBrowserFragment.kt index 0da70e73d88..816fec1e2be 100644 --- a/samples/browser/src/main/java/org/mozilla/samples/browser/BaseBrowserFragment.kt +++ b/samples/browser/src/main/java/org/mozilla/samples/browser/BaseBrowserFragment.kt @@ -67,7 +67,6 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler { feature = SessionFeature( components.store, components.sessionUseCases.goBack, - components.engineSessionUseCases, layout.engineView, sessionId), owner = this, diff --git a/samples/browser/src/main/java/org/mozilla/samples/browser/BrowserFragment.kt b/samples/browser/src/main/java/org/mozilla/samples/browser/BrowserFragment.kt index 5443605eb38..1b3b76c251b 100644 --- a/samples/browser/src/main/java/org/mozilla/samples/browser/BrowserFragment.kt +++ b/samples/browser/src/main/java/org/mozilla/samples/browser/BrowserFragment.kt @@ -15,7 +15,6 @@ import mozilla.components.feature.awesomebar.provider.SearchSuggestionProvider import mozilla.components.feature.media.fullscreen.MediaFullscreenOrientationFeature import mozilla.components.feature.search.SearchFeature import mozilla.components.feature.session.FullScreenFeature -import mozilla.components.feature.session.SessionUseCases import mozilla.components.feature.tabs.WindowFeature import mozilla.components.feature.tabs.toolbar.TabsToolbarFeature import mozilla.components.feature.toolbar.ToolbarAutocompleteFeature @@ -104,7 +103,7 @@ class BrowserFragment : BaseBrowserFragment(), UserInteractionHandler { fullScreenFeature.set( feature = FullScreenFeature( components.store, - SessionUseCases(components.sessionManager), + components.sessionUseCases, sessionId ) { inFullScreen -> if (inFullScreen) { 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 6d0d12b0002..cac2ed57c52 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 @@ -27,8 +27,8 @@ import mozilla.components.browser.menu.item.SimpleBrowserMenuItem import mozilla.components.browser.search.SearchEngineManager import mozilla.components.browser.session.Session import mozilla.components.browser.session.SessionManager +import mozilla.components.browser.session.engine.EngineMiddleware import mozilla.components.browser.session.storage.SessionStorage -import mozilla.components.browser.session.usecases.EngineSessionUseCases import mozilla.components.browser.state.store.BrowserStore import mozilla.components.browser.storage.sync.PlacesHistoryStorage import mozilla.components.browser.thumbnails.ThumbnailsMiddleware @@ -128,20 +128,26 @@ open class DefaultComponents(private val applicationContext: Context) { DownloadMiddleware(applicationContext, DownloadService::class.java), ReaderViewMiddleware(), ThumbnailsMiddleware(thumbnailStorage) - )) + ) + EngineMiddleware.create(engine, ::findSessionById)) } val customTabsStore by lazy { CustomTabsServiceStore() } + private fun findSessionById(tabId: String): Session? { + return sessionManager.findSessionById(tabId) + } + val sessionManager by lazy { SessionManager(engine, store).apply { - sessionStorage.restore()?.let { snapshot -> restore(snapshot) } + sessionStorage.restore()?.let { + snapshot -> restore(snapshot) + } if (size == 0) { add(Session("about:blank")) } - sessionStorage.autoSave(this) + sessionStorage.autoSave(store) .periodicallyInForeground(interval = 30, unit = TimeUnit.SECONDS) .whenGoingToBackground() .whenSessionsChange() @@ -156,9 +162,7 @@ open class DefaultComponents(private val applicationContext: Context) { } } - val sessionUseCases by lazy { SessionUseCases(sessionManager) } - - val engineSessionUseCases by lazy { EngineSessionUseCases(sessionManager) } + val sessionUseCases by lazy { SessionUseCases(store, sessionManager) } // Addons val addonManager by lazy { @@ -187,7 +191,7 @@ open class DefaultComponents(private val applicationContext: Context) { } } - val searchUseCases by lazy { SearchUseCases(applicationContext, searchEngineManager, sessionManager) } + val searchUseCases by lazy { SearchUseCases(applicationContext, store, searchEngineManager, sessionManager) } val defaultSearchUseCase by lazy { { searchTerms: String -> searchUseCases.defaultSearch.invoke( diff --git a/samples/browser/src/main/java/org/mozilla/samples/browser/SampleApplication.kt b/samples/browser/src/main/java/org/mozilla/samples/browser/SampleApplication.kt index a585158e7cf..17de70cf2af 100644 --- a/samples/browser/src/main/java/org/mozilla/samples/browser/SampleApplication.kt +++ b/samples/browser/src/main/java/org/mozilla/samples/browser/SampleApplication.kt @@ -10,6 +10,7 @@ import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch import mozilla.appservices.Megazord import mozilla.components.browser.session.Session +import mozilla.components.browser.state.action.SystemAction import mozilla.components.concept.fetch.Client import mozilla.components.feature.addons.update.GlobalAddonDependencyProvider import mozilla.components.lib.fetch.httpurlconnection.HttpURLConnectionClient @@ -98,7 +99,8 @@ class SampleApplication : Application() { logger.debug("onTrimMemory: $level") runOnlyInMainProcess { - components.sessionManager.onTrimMemory(level) + components.store.dispatch(SystemAction.LowMemoryAction(level)) + components.icons.onTrimMemory(level) } }