diff --git a/components/browser/thumbnails/build.gradle b/components/browser/thumbnails/build.gradle index 3058085574c..18c19f72e73 100644 --- a/components/browser/thumbnails/build.gradle +++ b/components/browser/thumbnails/build.gradle @@ -35,6 +35,7 @@ tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).all { dependencies { implementation project(':browser-state') implementation project(':concept-engine') + implementation project(':support-images') implementation project(':support-ktx') implementation Dependencies.androidx_annotation diff --git a/components/browser/thumbnails/src/main/java/mozilla/components/browser/thumbnails/ThumbnailsMiddleware.kt b/components/browser/thumbnails/src/main/java/mozilla/components/browser/thumbnails/ThumbnailsMiddleware.kt new file mode 100644 index 00000000000..c89a1322a4b --- /dev/null +++ b/components/browser/thumbnails/src/main/java/mozilla/components/browser/thumbnails/ThumbnailsMiddleware.kt @@ -0,0 +1,36 @@ +/* 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.thumbnails + +import mozilla.components.browser.state.action.BrowserAction +import mozilla.components.browser.state.action.ContentAction +import mozilla.components.browser.state.state.BrowserState +import mozilla.components.browser.thumbnails.storage.ThumbnailStorage +import mozilla.components.lib.state.Middleware +import mozilla.components.lib.state.MiddlewareStore + +/** + * [Middleware] implementation for handling [ContentAction.UpdateThumbnailAction] and storing + * the thumbnail to the disk cache. + */ +class ThumbnailsMiddleware( + private val thumbnailStorage: ThumbnailStorage +) : Middleware { + override fun invoke( + store: MiddlewareStore, + next: (BrowserAction) -> Unit, + action: BrowserAction + ) { + when (action) { + is ContentAction.UpdateThumbnailAction -> { + // Store the captured tab screenshot from the EngineView when the session's + // thumbnail is updated. + thumbnailStorage.saveThumbnail(action.sessionId, action.thumbnail) + } + } + + next(action) + } +} diff --git a/components/browser/thumbnails/src/main/java/mozilla/components/browser/thumbnails/ThumbnailsUseCases.kt b/components/browser/thumbnails/src/main/java/mozilla/components/browser/thumbnails/ThumbnailsUseCases.kt new file mode 100644 index 00000000000..65f0bc5a7d7 --- /dev/null +++ b/components/browser/thumbnails/src/main/java/mozilla/components/browser/thumbnails/ThumbnailsUseCases.kt @@ -0,0 +1,45 @@ +/* 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.thumbnails + +import android.graphics.Bitmap +import mozilla.components.browser.state.selector.findTab +import mozilla.components.browser.state.store.BrowserStore +import mozilla.components.browser.thumbnails.storage.ThumbnailStorage +import mozilla.components.support.base.log.logger.Logger + +/** + * Contains use cases related to the thumbnails feature. + */ +class ThumbnailsUseCases( + store: BrowserStore, + thumbnailStorage: ThumbnailStorage +) { + /** + * Load thumbnail use case. + */ + class LoadThumbnailUseCase internal constructor( + private val store: BrowserStore, + private val thumbnailStorage: ThumbnailStorage + ) { + private val logger = Logger("ThumbnailsUseCases") + + /** + * Loads the thumbnail for a tab from its in-memory [ContentState] or from the disk cache + * of [ThumbnailStorage]. + */ + suspend operator fun invoke(sessionIdOrUrl: String): Bitmap? { + val tab = store.state.findTab(sessionIdOrUrl) + tab?.content?.thumbnail?.let { + logger.debug("Loaded thumbnail from memory (sessionIdOrUrl = $sessionIdOrUrl)") + return@invoke it + } + + return thumbnailStorage.loadThumbnail(sessionIdOrUrl).await() + } + } + + val loadThumbnail: LoadThumbnailUseCase by lazy { LoadThumbnailUseCase(store, thumbnailStorage) } +} diff --git a/components/browser/thumbnails/src/main/java/mozilla/components/browser/thumbnails/storage/ThumbnailStorage.kt b/components/browser/thumbnails/src/main/java/mozilla/components/browser/thumbnails/storage/ThumbnailStorage.kt new file mode 100644 index 00000000000..930c6aa78ac --- /dev/null +++ b/components/browser/thumbnails/src/main/java/mozilla/components/browser/thumbnails/storage/ThumbnailStorage.kt @@ -0,0 +1,79 @@ +/* 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.thumbnails.storage + +import android.content.Context +import android.graphics.Bitmap +import androidx.annotation.WorkerThread +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Deferred +import kotlinx.coroutines.asCoroutineDispatcher +import kotlinx.coroutines.async +import mozilla.components.browser.thumbnails.R +import mozilla.components.browser.thumbnails.utils.ThumbnailDiskCache +import mozilla.components.support.base.log.logger.Logger +import mozilla.components.support.images.DesiredSize +import mozilla.components.support.images.decoder.AndroidImageDecoder +import java.util.concurrent.Executors + +private const val MAXIMUM_SCALE_FACTOR = 2.0f + +// Number of worker threads we are using internally. +private const val THREADS = 3 + +internal val sharedDiskCache = ThumbnailDiskCache() + +/** + * Thumbnail storage layer which handles saving and loading the thumbnail from the disk cache. + */ +class ThumbnailStorage( + private val context: Context, + jobDispatcher: CoroutineDispatcher = Executors.newFixedThreadPool(THREADS).asCoroutineDispatcher() +) { + private val decoders = AndroidImageDecoder() + private val logger = Logger("ThumbnailStorage") + private val maximumSize = context.resources.getDimensionPixelSize(R.dimen.mozac_browser_thumbnails_maximum_size) + private val scope = CoroutineScope(jobDispatcher) + + /** + * Asynchronously loads a thumbnail [Bitmap] for the given session ID or url. + */ + fun loadThumbnail(sessionIdOrUrl: String): Deferred = scope.async { + loadThumbnailInternal(sessionIdOrUrl).also { loadedThumbnail -> + if (loadedThumbnail != null) { + logger.debug("Loaded thumbnail from disk (sessionIdOrUrl = $sessionIdOrUrl)") + } else { + logger.debug("No thumbnail loaded (sessionIdOrUrl = $sessionIdOrUrl)") + } + } + } + + @WorkerThread + private fun loadThumbnailInternal(sessionIdOrUrl: String): Bitmap? { + val desiredSize = DesiredSize( + targetSize = context.resources.getDimensionPixelSize(R.dimen.mozac_browser_thumbnails_size_default), + maxSize = maximumSize, + maxScaleFactor = MAXIMUM_SCALE_FACTOR + ) + + val data = sharedDiskCache.getThumbnailData(context, sessionIdOrUrl) + + if (data != null) { + return decoders.decode(data, desiredSize) + } + + return null + } + + /** + * Stores the given thumbnail [Bitmap] into the disk cache with the provided session ID or url + * as its key. + */ + fun saveThumbnail(sessionIdOrUrl: String, bitmap: Bitmap) { + logger.debug("Saved thumbnail to disk (sessionIdOrUrl = $sessionIdOrUrl)") + sharedDiskCache.putThumbnailBitmap(context, sessionIdOrUrl, bitmap) + } +} diff --git a/components/browser/thumbnails/src/main/java/mozilla/components/browser/thumbnails/utils/ThumbnailDiskCache.kt b/components/browser/thumbnails/src/main/java/mozilla/components/browser/thumbnails/utils/ThumbnailDiskCache.kt index e0daad7efaa..72fd7bec304 100644 --- a/components/browser/thumbnails/src/main/java/mozilla/components/browser/thumbnails/utils/ThumbnailDiskCache.kt +++ b/components/browser/thumbnails/src/main/java/mozilla/components/browser/thumbnails/utils/ThumbnailDiskCache.kt @@ -6,6 +6,7 @@ package mozilla.components.browser.thumbnails.utils import android.content.Context import android.graphics.Bitmap +import androidx.annotation.VisibleForTesting import com.jakewharton.disklrucache.DiskLruCache import mozilla.components.support.base.log.logger.Logger import java.io.File @@ -23,6 +24,12 @@ class ThumbnailDiskCache { private var thumbnailCache: DiskLruCache? = null private val thumbnailCacheWriteLock = Any() + @VisibleForTesting(otherwise = VisibleForTesting.NONE) + internal fun clear(context: Context) { + getThumbnailCache(context).delete() + thumbnailCache = null + } + /** * Retrieves the thumbnail data from the disk cache for the given session ID or URL. * diff --git a/components/browser/thumbnails/src/main/res/values/dimens.xml b/components/browser/thumbnails/src/main/res/values/dimens.xml new file mode 100644 index 00000000000..7b3bcf1ea57 --- /dev/null +++ b/components/browser/thumbnails/src/main/res/values/dimens.xml @@ -0,0 +1,9 @@ + + + + 102dp + + 2153px + diff --git a/components/browser/thumbnails/src/test/java/mozilla/components/browser/thumbnails/ThumbnailsMiddlewareTest.kt b/components/browser/thumbnails/src/test/java/mozilla/components/browser/thumbnails/ThumbnailsMiddlewareTest.kt new file mode 100644 index 00000000000..d9d0d29af27 --- /dev/null +++ b/components/browser/thumbnails/src/test/java/mozilla/components/browser/thumbnails/ThumbnailsMiddlewareTest.kt @@ -0,0 +1,37 @@ +/* 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.thumbnails + +import android.graphics.Bitmap +import androidx.test.ext.junit.runners.AndroidJUnit4 +import mozilla.components.browser.state.action.ContentAction +import mozilla.components.browser.state.state.BrowserState +import mozilla.components.browser.state.state.createTab +import mozilla.components.browser.state.store.BrowserStore +import mozilla.components.browser.thumbnails.storage.ThumbnailStorage +import mozilla.components.support.test.ext.joinBlocking +import mozilla.components.support.test.mock +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mockito.verify + +@RunWith(AndroidJUnit4::class) +class ThumbnailsMiddlewareTest { + + @Test + fun `thumbnail storage stores the provided thumbnail on update thumbnail action`() { + val sessionIdOrUrl = "test-tab1" + val tab = createTab("https://www.mozilla.org", id = "test-tab1") + val thumbnailStorage: ThumbnailStorage = mock() + val store = BrowserStore( + initialState = BrowserState(tabs = listOf(tab)), + middleware = listOf(ThumbnailsMiddleware(thumbnailStorage)) + ) + + val bitmap: Bitmap = mock() + store.dispatch(ContentAction.UpdateThumbnailAction(sessionIdOrUrl, bitmap)).joinBlocking() + verify(thumbnailStorage).saveThumbnail(sessionIdOrUrl, bitmap) + } +} diff --git a/components/browser/thumbnails/src/test/java/mozilla/components/browser/thumbnails/ThumbnailsUseCasesTest.kt b/components/browser/thumbnails/src/test/java/mozilla/components/browser/thumbnails/ThumbnailsUseCasesTest.kt new file mode 100644 index 00000000000..a5dc7487356 --- /dev/null +++ b/components/browser/thumbnails/src/test/java/mozilla/components/browser/thumbnails/ThumbnailsUseCasesTest.kt @@ -0,0 +1,60 @@ +/* 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.thumbnails + +import android.graphics.Bitmap +import androidx.test.ext.junit.runners.AndroidJUnit4 +import kotlinx.coroutines.CompletableDeferred +import kotlinx.coroutines.runBlocking +import mozilla.components.browser.state.state.BrowserState +import mozilla.components.browser.state.state.createTab +import mozilla.components.browser.state.store.BrowserStore +import mozilla.components.browser.thumbnails.storage.ThumbnailStorage +import mozilla.components.support.test.any +import mozilla.components.support.test.mock +import org.junit.Assert.assertEquals +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mockito.`when` +import org.mockito.Mockito.verify + +@RunWith(AndroidJUnit4::class) +class ThumbnailsUseCasesTest { + + @Test + fun `LoadThumbnailUseCase - loads the thumbnail from the in-memory ContentState if available`() = runBlocking { + val sessionIdOrUrl = "test-tab1" + val bitmap: Bitmap = mock() + val tab = createTab("https://www.mozilla.org", id = "test-tab1", thumbnail = bitmap) + val thumbnailStorage: ThumbnailStorage = mock() + val store = BrowserStore( + initialState = BrowserState(tabs = listOf(tab)), + middleware = listOf(ThumbnailsMiddleware(thumbnailStorage)) + ) + val useCases = ThumbnailsUseCases(store, thumbnailStorage) + + val thumbnail = useCases.loadThumbnail(sessionIdOrUrl) + assertEquals(bitmap, thumbnail) + } + + @Test + fun `LoadThumbnailUseCase - loads the thumbnail from the disk cache if in-memory thumbnail is unavailable`() = runBlocking { + val sessionIdOrUrl = "test-tab1" + val bitmap: Bitmap = mock() + val tab = createTab("https://www.mozilla.org", id = "test-tab1") + val thumbnailStorage: ThumbnailStorage = mock() + val store = BrowserStore( + initialState = BrowserState(tabs = listOf(tab)), + middleware = listOf(ThumbnailsMiddleware(thumbnailStorage)) + ) + val useCases = ThumbnailsUseCases(store, thumbnailStorage) + + `when`(thumbnailStorage.loadThumbnail(any())).thenReturn(CompletableDeferred(bitmap)) + + val thumbnail = useCases.loadThumbnail(sessionIdOrUrl) + verify(thumbnailStorage).loadThumbnail(sessionIdOrUrl) + assertEquals(bitmap, thumbnail) + } +} diff --git a/components/browser/thumbnails/src/test/java/mozilla/components/browser/thumbnails/storage/ThumbnailStorageTest.kt b/components/browser/thumbnails/src/test/java/mozilla/components/browser/thumbnails/storage/ThumbnailStorageTest.kt new file mode 100644 index 00000000000..e0568c0a15f --- /dev/null +++ b/components/browser/thumbnails/src/test/java/mozilla/components/browser/thumbnails/storage/ThumbnailStorageTest.kt @@ -0,0 +1,58 @@ +/* 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.thumbnails.storage + +import android.graphics.Bitmap +import androidx.test.ext.junit.runners.AndroidJUnit4 +import kotlinx.coroutines.CompletableDeferred +import kotlinx.coroutines.runBlocking +import mozilla.components.support.test.mock +import mozilla.components.support.test.robolectric.testContext +import org.junit.After +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNotNull +import org.junit.Assert.assertNull +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mockito.`when` +import org.mockito.Mockito.spy + +@RunWith(AndroidJUnit4::class) +class ThumbnailStorageTest { + + @Before + @After + fun cleanUp() { + sharedDiskCache.clear(testContext) + } + + @Test + fun `saveThumbnail`() = runBlocking { + val sessionIdOrUrl = "test-tab1" + val bitmap: Bitmap = mock() + val thumbnailStorage = spy(ThumbnailStorage(testContext)) + var thumbnail = thumbnailStorage.loadThumbnail(sessionIdOrUrl).await() + + assertNull(thumbnail) + + thumbnailStorage.saveThumbnail(sessionIdOrUrl, bitmap) + thumbnail = thumbnailStorage.loadThumbnail(sessionIdOrUrl).await() + assertNotNull(thumbnail) + } + + @Test + fun `loadThumbnail`() = runBlocking { + val sessionIdOrUrl = "test-tab1" + val bitmap: Bitmap = mock() + val thumbnailStorage = spy(ThumbnailStorage(testContext)) + + thumbnailStorage.saveThumbnail(sessionIdOrUrl, bitmap) + `when`(thumbnailStorage.loadThumbnail(sessionIdOrUrl)).thenReturn(CompletableDeferred(bitmap)) + + val thumbnail = thumbnailStorage.loadThumbnail(sessionIdOrUrl).await() + assertEquals(bitmap, thumbnail) + } +} diff --git a/components/feature/tabs/build.gradle b/components/feature/tabs/build.gradle index ba2108781bf..4cd7f3529ca 100644 --- a/components/feature/tabs/build.gradle +++ b/components/feature/tabs/build.gradle @@ -31,6 +31,7 @@ tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).all { dependencies { implementation project(':browser-session') + implementation project(':browser-thumbnails') api project(':feature-session') implementation project(':concept-engine') implementation project(':concept-tabstray') diff --git a/components/feature/tabs/src/main/java/mozilla/components/feature/tabs/tabstray/TabsFeature.kt b/components/feature/tabs/src/main/java/mozilla/components/feature/tabs/tabstray/TabsFeature.kt index 2baa335c071..618cf9c1472 100644 --- a/components/feature/tabs/src/main/java/mozilla/components/feature/tabs/tabstray/TabsFeature.kt +++ b/components/feature/tabs/src/main/java/mozilla/components/feature/tabs/tabstray/TabsFeature.kt @@ -7,6 +7,7 @@ package mozilla.components.feature.tabs.tabstray import androidx.annotation.VisibleForTesting import mozilla.components.browser.state.state.TabSessionState import mozilla.components.browser.state.store.BrowserStore +import mozilla.components.browser.thumbnails.ThumbnailsUseCases import mozilla.components.concept.tabstray.TabsTray import mozilla.components.feature.tabs.TabsUseCases import mozilla.components.feature.tabs.ext.toTabs @@ -18,10 +19,12 @@ import mozilla.components.support.base.feature.LifecycleAwareFeature * @param defaultTabsFilter A tab filter that is used for the initial presenting of tabs that will be used by * [TabsFeature.filterTabs] by default as well. */ +@Suppress("LongParameterList") class TabsFeature( tabsTray: TabsTray, private val store: BrowserStore, tabsUseCases: TabsUseCases, + thumbnailsUseCases: ThumbnailsUseCases, private val defaultTabsFilter: (TabSessionState) -> Boolean = { true }, closeTabsTray: () -> Unit ) : LifecycleAwareFeature { @@ -29,6 +32,7 @@ class TabsFeature( internal var presenter = TabsTrayPresenter( tabsTray, store, + thumbnailsUseCases, defaultTabsFilter, closeTabsTray ) diff --git a/components/feature/tabs/src/main/java/mozilla/components/feature/tabs/tabstray/TabsTrayPresenter.kt b/components/feature/tabs/src/main/java/mozilla/components/feature/tabs/tabstray/TabsTrayPresenter.kt index f10411918dc..2e29c1935b6 100644 --- a/components/feature/tabs/src/main/java/mozilla/components/feature/tabs/tabstray/TabsTrayPresenter.kt +++ b/components/feature/tabs/src/main/java/mozilla/components/feature/tabs/tabstray/TabsTrayPresenter.kt @@ -14,6 +14,7 @@ import kotlinx.coroutines.flow.map import mozilla.components.browser.state.state.BrowserState import mozilla.components.browser.state.state.TabSessionState import mozilla.components.browser.state.store.BrowserStore +import mozilla.components.browser.thumbnails.ThumbnailsUseCases import mozilla.components.concept.tabstray.Tabs import mozilla.components.concept.tabstray.TabsTray import mozilla.components.feature.tabs.ext.toTabs @@ -27,6 +28,7 @@ import mozilla.components.support.ktx.kotlinx.coroutines.flow.ifChanged class TabsTrayPresenter( private val tabsTray: TabsTray, private val store: BrowserStore, + private val thumbnailsUseCases: ThumbnailsUseCases, internal var tabsFilter: (TabSessionState) -> Boolean, private val closeTabsTray: () -> Unit ) { @@ -43,6 +45,12 @@ class TabsTrayPresenter( private suspend fun collect(flow: Flow) { flow.map { it.toTabs(tabsFilter) } + .map { tabs -> + // Load the tab thumbnail from the memory or disk caches. + tabs.copy(list = tabs.list.map { tab -> + tab.copy(thumbnail = thumbnailsUseCases.loadThumbnail(tab.id)) + }) + } .ifChanged() .collect { tabs -> // Do not invoke the callback on start if this is the initial state. diff --git a/components/feature/tabs/src/test/java/mozilla/components/feature/tabs/tabstray/TabsFeatureTest.kt b/components/feature/tabs/src/test/java/mozilla/components/feature/tabs/tabstray/TabsFeatureTest.kt index 438377f31a9..da06c454328 100644 --- a/components/feature/tabs/src/test/java/mozilla/components/feature/tabs/tabstray/TabsFeatureTest.kt +++ b/components/feature/tabs/src/test/java/mozilla/components/feature/tabs/tabstray/TabsFeatureTest.kt @@ -25,7 +25,7 @@ class TabsFeatureTest { val presenter: TabsTrayPresenter = mock() val interactor: TabsTrayInteractor = mock() val useCases = TabsUseCases(sessionManager) - val tabsFeature = spy(TabsFeature(mock(), store, useCases, mock(), mock())) + val tabsFeature = spy(TabsFeature(mock(), store, useCases, mock(), mock(), mock())) assertNotEquals(tabsFeature.interactor, interactor) assertNotEquals(tabsFeature.presenter, presenter) @@ -44,7 +44,7 @@ class TabsFeatureTest { val presenter: TabsTrayPresenter = mock() val interactor: TabsTrayInteractor = mock() val useCases = TabsUseCases(sessionManager) - val tabsFeature = spy(TabsFeature(mock(), store, useCases, mock(), mock())) + val tabsFeature = spy(TabsFeature(mock(), store, useCases, mock(), mock(), mock())) tabsFeature.presenter = presenter tabsFeature.interactor = interactor @@ -62,7 +62,7 @@ class TabsFeatureTest { val presenter: TabsTrayPresenter = mock() val interactor: TabsTrayInteractor = mock() val useCases = TabsUseCases(sessionManager) - val tabsFeature = spy(TabsFeature(mock(), store, useCases, mock(), mock())) + val tabsFeature = spy(TabsFeature(mock(), store, useCases, mock(), mock(), mock())) tabsFeature.presenter = presenter tabsFeature.interactor = interactor @@ -80,7 +80,7 @@ class TabsFeatureTest { val presenter: TabsTrayPresenter = mock() val interactor: TabsTrayInteractor = mock() val useCases = TabsUseCases(sessionManager) - val tabsFeature = spy(TabsFeature(mock(), store, useCases, mock(), mock())) + val tabsFeature = spy(TabsFeature(mock(), store, useCases, mock(), mock(), mock())) tabsFeature.presenter = presenter tabsFeature.interactor = interactor @@ -99,7 +99,7 @@ class TabsFeatureTest { val filter: (TabSessionState) -> Boolean = { false } val sessionManager = SessionManager(engine = mock()) val useCases = TabsUseCases(sessionManager) - val tabsFeature = spy(TabsFeature(mock(), store, useCases, filter, mock())) + val tabsFeature = spy(TabsFeature(mock(), store, useCases, mock(), filter, mock())) val presenter: TabsTrayPresenter = mock() val interactor: TabsTrayInteractor = mock() diff --git a/components/feature/tabs/src/test/java/mozilla/components/feature/tabs/tabstray/TabsTrayPresenterTest.kt b/components/feature/tabs/src/test/java/mozilla/components/feature/tabs/tabstray/TabsTrayPresenterTest.kt index 3af3e74436b..9f562d2476b 100644 --- a/components/feature/tabs/src/test/java/mozilla/components/feature/tabs/tabstray/TabsTrayPresenterTest.kt +++ b/components/feature/tabs/src/test/java/mozilla/components/feature/tabs/tabstray/TabsTrayPresenterTest.kt @@ -66,7 +66,7 @@ class TabsTrayPresenterTest { ) val tabsTray: MockedTabsTray = spy(MockedTabsTray()) - val presenter = TabsTrayPresenter(tabsTray, store, { true }, mock()) + val presenter = TabsTrayPresenter(tabsTray, store, mock(), { true }, mock()) verifyNoMoreInteractions(tabsTray) @@ -97,7 +97,7 @@ class TabsTrayPresenterTest { ) val tabsTray: MockedTabsTray = spy(MockedTabsTray()) - val presenter = TabsTrayPresenter(tabsTray, store, { true }, mock()) + val presenter = TabsTrayPresenter(tabsTray, store, mock(), { true }, mock()) presenter.start() @@ -131,7 +131,7 @@ class TabsTrayPresenterTest { ) val tabsTray: MockedTabsTray = spy(MockedTabsTray()) - val presenter = TabsTrayPresenter(tabsTray, store, { true }, mock()) + val presenter = TabsTrayPresenter(tabsTray, store, mock(), { true }, mock()) presenter.start() @@ -167,7 +167,7 @@ class TabsTrayPresenterTest { ) val tabsTray: MockedTabsTray = spy(MockedTabsTray()) - val presenter = TabsTrayPresenter(tabsTray, store, { true }, mock()) + val presenter = TabsTrayPresenter(tabsTray, store, mock(), { true }, mock()) presenter.start() @@ -201,7 +201,7 @@ class TabsTrayPresenterTest { ) val tabsTray: MockedTabsTray = spy(MockedTabsTray()) - val presenter = TabsTrayPresenter(tabsTray, store, { true }, mock()) + val presenter = TabsTrayPresenter(tabsTray, store, mock(), { true }, mock()) presenter.start() testDispatcher.advanceUntilIdle() @@ -224,26 +224,29 @@ class TabsTrayPresenterTest { val store = BrowserStore( BrowserState( tabs = listOf( - createTab("https://www.mozilla.org", id = "a"), - createTab("https://getpocket.com", id = "b"), - createTab("https://developer.mozilla.org", id = "c"), - createTab("https://www.firefox.com", id = "d"), - createTab("https://www.google.com", id = "e") - ), - selectedTabId = "a" - ) + createTab("https://www.mozilla.org", id = "a"), + createTab("https://getpocket.com", id = "b"), + createTab("https://developer.mozilla.org", id = "c"), + createTab("https://www.firefox.com", id = "d"), + createTab("https://www.google.com", id = "e") + ), + selectedTabId = "a" + ) ) val tabsTray: MockedTabsTray = spy(MockedTabsTray()) - val presenter = TabsTrayPresenter(tabsTray, store, { true }, mock()) + val presenter = TabsTrayPresenter(tabsTray, store, mock(), { true }, mock()) presenter.start() testDispatcher.advanceUntilIdle() store.dispatch( - MediaAction.UpdateMediaAggregateAction( - store.state.media.aggregate.copy(activeTabId = "a", state = MediaState.State.PLAYING) + MediaAction.UpdateMediaAggregateAction( + store.state.media.aggregate.copy( + activeTabId = "a", + state = MediaState.State.PLAYING ) + ) ).joinBlocking() testDispatcher.advanceUntilIdle() @@ -269,7 +272,12 @@ class TabsTrayPresenterTest { var closed = false val tabsTray: MockedTabsTray = spy(MockedTabsTray()) - val presenter = TabsTrayPresenter(tabsTray, store, tabsFilter = { true }, closeTabsTray = { closed = true }) + val presenter = TabsTrayPresenter( + tabsTray, + store, + mock(), + tabsFilter = { true }, + closeTabsTray = { closed = true }) presenter.start() testDispatcher.advanceUntilIdle() @@ -299,7 +307,12 @@ class TabsTrayPresenterTest { var closed = false val tabsTray: MockedTabsTray = spy(MockedTabsTray()) - val presenter = TabsTrayPresenter(tabsTray, store, tabsFilter = { true }, closeTabsTray = { closed = true }) + val presenter = TabsTrayPresenter( + tabsTray, + store, + mock(), + tabsFilter = { true }, + closeTabsTray = { closed = true }) presenter.start() testDispatcher.advanceUntilIdle() @@ -332,7 +345,7 @@ class TabsTrayPresenterTest { ) val tabsTray: MockedTabsTray = spy(MockedTabsTray()) - val presenter = TabsTrayPresenter(tabsTray, store, { true }, mock()) + val presenter = TabsTrayPresenter(tabsTray, store, mock(), { true }, mock()) presenter.start() testDispatcher.advanceUntilIdle() @@ -362,7 +375,7 @@ class TabsTrayPresenterTest { ) val tabsTray: MockedTabsTray = spy(MockedTabsTray()) - val presenter = TabsTrayPresenter(tabsTray, store, { it.content.private }, mock()) + val presenter = TabsTrayPresenter(tabsTray, store, mock(), { it.content.private }, mock()) presenter.start() testDispatcher.advanceUntilIdle() @@ -381,7 +394,8 @@ class TabsTrayPresenterTest { var invoked = false val tabsTray: MockedTabsTray = spy(MockedTabsTray()) - val presenter = TabsTrayPresenter(tabsTray, store, { it.content.private }, { invoked = true }) + val presenter = + TabsTrayPresenter(tabsTray, store, mock(), { it.content.private }, { invoked = true }) presenter.start() testDispatcher.advanceUntilIdle() @@ -419,7 +433,8 @@ private class MockedTabsTray : TabsTray { override fun notifyAtLeastOneObserver(block: TabsTray.Observer.() -> Unit) {} - override fun wrapConsumers(block: TabsTray.Observer.(R) -> Boolean): List<(R) -> Boolean> = emptyList() + override fun wrapConsumers(block: TabsTray.Observer.(R) -> Boolean): List<(R) -> Boolean> = + emptyList() override fun isObserved(): Boolean = false 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 7506cf61a7f..a0591e60ff1 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 @@ -30,6 +30,9 @@ import mozilla.components.browser.session.SessionManager import mozilla.components.browser.session.storage.SessionStorage import mozilla.components.browser.state.store.BrowserStore import mozilla.components.browser.storage.memory.InMemoryHistoryStorage +import mozilla.components.browser.thumbnails.ThumbnailsMiddleware +import mozilla.components.browser.thumbnails.ThumbnailsUseCases +import mozilla.components.browser.thumbnails.storage.ThumbnailStorage import mozilla.components.concept.engine.DefaultSettings import mozilla.components.concept.engine.Engine import mozilla.components.concept.fetch.Client @@ -109,10 +112,13 @@ open class DefaultComponents(private val applicationContext: Context) { private val sessionStorage by lazy { SessionStorage(applicationContext, engine) } + private val thumbnailStorage by lazy { ThumbnailStorage(applicationContext) } + val store by lazy { BrowserStore(middleware = listOf( MediaMiddleware(applicationContext, MediaService::class.java), - ReaderViewMiddleware() + ReaderViewMiddleware(), + ThumbnailsMiddleware(thumbnailStorage) )) } @@ -340,6 +346,7 @@ open class DefaultComponents(private val applicationContext: Context) { } val tabsUseCases: TabsUseCases by lazy { TabsUseCases(sessionManager) } + val thumbnailsUseCases: ThumbnailsUseCases by lazy { ThumbnailsUseCases(store, thumbnailStorage) } val downloadsUseCases: DownloadsUseCases by lazy { DownloadsUseCases(store) } val contextMenuUseCases: ContextMenuUseCases by lazy { ContextMenuUseCases(sessionManager, store) } } diff --git a/samples/browser/src/main/java/org/mozilla/samples/browser/TabsTrayFragment.kt b/samples/browser/src/main/java/org/mozilla/samples/browser/TabsTrayFragment.kt index 7d349e139fb..df19fcc78f0 100644 --- a/samples/browser/src/main/java/org/mozilla/samples/browser/TabsTrayFragment.kt +++ b/samples/browser/src/main/java/org/mozilla/samples/browser/TabsTrayFragment.kt @@ -47,6 +47,7 @@ class TabsTrayFragment : Fragment(), UserInteractionHandler { tabsTray, components.store, components.tabsUseCases, + components.thumbnailsUseCases, closeTabsTray = ::closeTabsTray ).also { lifecycle.addObserver(it) } }