Skip to content
This repository has been archived by the owner on Nov 1, 2022. It is now read-only.

Commit

Permalink
Merge #7127
Browse files Browse the repository at this point in the history
7127: Issue #7021: Integrate the ThumbnailDiskCache with BrowserThumbnails r=jonalmeida a=gabrielluong



Co-authored-by: Gabriel Luong <[email protected]>
  • Loading branch information
MozLando and gabrielluong committed May 28, 2020
2 parents 539d368 + 7c650be commit 521d633
Show file tree
Hide file tree
Showing 18 changed files with 475 additions and 55 deletions.
1 change: 1 addition & 0 deletions components/browser/thumbnails/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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<BrowserState, BrowserAction> {
override fun invoke(
store: MiddlewareStore<BrowserState, BrowserAction>,
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)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
/* 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 of 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, " +
"generationId = ${it.generationId})"
)
return@invoke it
}

return thumbnailStorage.loadThumbnail(sessionIdOrUrl).await()
}
}

val loadThumbnail: LoadThumbnailUseCase by lazy {
LoadThumbnailUseCase(
store,
thumbnailStorage
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
/* 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<Bitmap?> = scope.async {
loadThumbnailInternal(sessionIdOrUrl).also { loadedThumbnail ->
if (loadedThumbnail != null) {
logger.debug(
"Loaded thumbnail from disk (sessionIdOrUrl = $sessionIdOrUrl, " +
"generationId = ${loadedThumbnail.generationId})"
)
} 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, " +
"generationId = ${bitmap.generationId})"
)
sharedDiskCache.putThumbnailBitmap(context, sessionIdOrUrl, bitmap)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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.
*
Expand Down
9 changes: 9 additions & 0 deletions components/browser/thumbnails/src/main/res/values/dimens.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- 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/. -->
<resources xmlns:tools="http://schemas.android.com/tools">
<dimen name="mozac_browser_thumbnails_size_default">102dp</dimen>

<dimen name="mozac_browser_thumbnails_maximum_size" tools:ignore="PxUsage">2153px</dimen>
</resources>
Original file line number Diff line number Diff line change
@@ -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)
}
}
Original file line number Diff line number Diff line change
@@ -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)
}
}
Original file line number Diff line number Diff line change
@@ -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)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,4 +24,17 @@ data class Tab(
val icon: Bitmap? = null,
val thumbnail: Bitmap? = null,
val mediaState: Media.State? = null
)
) {
override fun equals(other: Any?): Boolean {
if (javaClass != other?.javaClass) return false

other as Tab

return id == other.id &&
url == other.url &&
title == other.title &&
icon?.generationId == other.icon?.generationId &&
thumbnail?.generationId == other.thumbnail?.generationId &&
mediaState == other.mediaState
}
}
Loading

0 comments on commit 521d633

Please sign in to comment.