Skip to content
This repository was archived by the owner on Nov 1, 2022. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import mozilla.components.browser.tabstray.thumbnail.TabThumbnailView
import mozilla.components.concept.tabstray.Tab
import mozilla.components.concept.tabstray.TabsTray
import mozilla.components.support.base.observer.Observable
import mozilla.components.support.images.ImageRequest
import mozilla.components.support.images.loader.ImageLoader
import mozilla.components.support.ktx.kotlin.tryGetHostFromUrl

Expand Down Expand Up @@ -83,7 +84,7 @@ class DefaultTabViewHolder(

// In the final else case, we have no cache or fresh screenshot; do nothing instead of clearing the image.
if (thumbnailLoader != null && tab.thumbnail == null) {
thumbnailLoader.loadIntoView(thumbnailView, tab.id)
thumbnailLoader.loadIntoView(thumbnailView, ImageRequest(tab.id))
} else if (tab.thumbnail != null) {
thumbnailView.setImageBitmap(tab.thumbnail)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import androidx.test.ext.junit.runners.AndroidJUnit4
import mozilla.components.concept.tabstray.Tab
import mozilla.components.concept.tabstray.TabsTray
import mozilla.components.support.base.observer.ObserverRegistry
import mozilla.components.support.images.ImageRequest
import mozilla.components.support.images.loader.ImageLoader
import mozilla.components.support.test.any
import mozilla.components.support.test.eq
Expand Down Expand Up @@ -161,11 +162,11 @@ class DefaultTabViewHolderTest {

viewHolder.bind(tabWithThumbnail, false, mock())

verify(loader, never()).loadIntoView(any(), eq("123"), nullable(), nullable())
verify(loader, never()).loadIntoView(any(), eq(ImageRequest("123")), nullable(), nullable())

viewHolder.bind(tab, false, mock())

verify(loader).loadIntoView(any(), eq("123"), nullable(), nullable())
verify(loader).loadIntoView(any(), eq(ImageRequest("123")), nullable(), nullable())
}

@Test
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ 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
import mozilla.components.support.images.ImageRequest

/**
* [Middleware] implementation for handling [ContentAction.UpdateThumbnailAction] and storing
Expand All @@ -28,7 +29,7 @@ class ThumbnailsMiddleware(
is ContentAction.UpdateThumbnailAction -> {
// Store the captured tab screenshot from the EngineView when the session's
// thumbnail is updated.
thumbnailStorage.saveThumbnail(action.sessionId, action.thumbnail)
thumbnailStorage.saveThumbnail(ImageRequest(action.sessionId), action.thumbnail)
}
is TabListAction.RemoveAllNormalTabsAction -> {
store.state.tabs.filterNot { it.content.private }.forEach { tab ->
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
/* 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.ext

import android.widget.ImageView
import mozilla.components.browser.thumbnails.loader.ThumbnailLoader
import mozilla.components.support.images.ImageRequest

/**
* Loads an image asynchronously and then displays it in the [ImageView].
* If the view is detached from the window before loading is completed, then loading is cancelled.
*
* @param view [ImageView] to load the image into.
* @param id Load image for this given ID.
*/
fun ThumbnailLoader.loadIntoView(view: ImageView, id: String) = loadIntoView(view, ImageRequest(id))
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import kotlinx.coroutines.launch
import mozilla.components.browser.thumbnails.R
import mozilla.components.browser.thumbnails.storage.ThumbnailStorage
import mozilla.components.support.images.CancelOnDetach
import mozilla.components.support.images.ImageRequest
import mozilla.components.support.images.loader.ImageLoader
import java.lang.ref.WeakReference

Expand All @@ -25,19 +26,19 @@ class ThumbnailLoader(private val storage: ThumbnailStorage) : ImageLoader {

override fun loadIntoView(
view: ImageView,
id: String,
request: ImageRequest,
placeholder: Drawable?,
error: Drawable?
) {
CoroutineScope(Dispatchers.Main).launch {
loadIntoViewInternal(WeakReference(view), id, placeholder, error)
loadIntoViewInternal(WeakReference(view), request, placeholder, error)
}
}

@MainThread
private suspend fun loadIntoViewInternal(
view: WeakReference<ImageView>,
id: String,
request: ImageRequest,
placeholder: Drawable?,
error: Drawable?
) {
Expand All @@ -48,7 +49,7 @@ class ThumbnailLoader(private val storage: ThumbnailStorage) : ImageLoader {
view.get()?.setImageDrawable(placeholder)

// Create a loading job
val deferredThumbnail = storage.loadThumbnail(id)
val deferredThumbnail = storage.loadThumbnail(request)

view.get()?.setTag(R.id.mozac_browser_thumbnails_tag_job, deferredThumbnail)
val onAttachStateChangeListener = CancelOnDetach(deferredThumbnail).also {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ 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.ImageRequest
import mozilla.components.support.images.decoder.AndroidImageDecoder
import java.util.concurrent.Executors

Expand Down Expand Up @@ -62,30 +63,30 @@ class ThumbnailStorage(
}

/**
* Asynchronously loads a thumbnail [Bitmap] for the given session ID or url.
* Asynchronously loads a thumbnail [Bitmap] for the given [ImageRequest].
*/
fun loadThumbnail(sessionIdOrUrl: String): Deferred<Bitmap?> = scope.async {
loadThumbnailInternal(sessionIdOrUrl).also { loadedThumbnail ->
fun loadThumbnail(request: ImageRequest): Deferred<Bitmap?> = scope.async {
loadThumbnailInternal(request).also { loadedThumbnail ->
if (loadedThumbnail != null) {
logger.debug(
"Loaded thumbnail from disk (sessionIdOrUrl = $sessionIdOrUrl, " +
"Loaded thumbnail from disk (id = ${request.id}, " +
"generationId = ${loadedThumbnail.generationId})"
)
} else {
logger.debug("No thumbnail loaded (sessionIdOrUrl = $sessionIdOrUrl)")
logger.debug("No thumbnail loaded (id = ${request.id})")
}
}
}

@WorkerThread
private fun loadThumbnailInternal(sessionIdOrUrl: String): Bitmap? {
private fun loadThumbnailInternal(request: ImageRequest): Bitmap? {
val desiredSize = DesiredSize(
targetSize = context.resources.getDimensionPixelSize(R.dimen.mozac_browser_thumbnails_size_default),
targetSize = context.resources.getDimensionPixelSize(request.size.dimen),
maxSize = maximumSize,
maxScaleFactor = MAXIMUM_SCALE_FACTOR
)

val data = sharedDiskCache.getThumbnailData(context, sessionIdOrUrl)
val data = sharedDiskCache.getThumbnailData(context, request)

if (data != null) {
return decoders.decode(data, desiredSize)
Expand All @@ -95,15 +96,15 @@ class ThumbnailStorage(
}

/**
* Stores the given thumbnail [Bitmap] into the disk cache with the provided session ID or url
* Stores the given thumbnail [Bitmap] into the disk cache with the provided [ImageRequest]
* as its key.
*/
fun saveThumbnail(sessionIdOrUrl: String, bitmap: Bitmap): Job =
fun saveThumbnail(request: ImageRequest, bitmap: Bitmap): Job =
scope.launch {
logger.debug(
"Saved thumbnail to disk (sessionIdOrUrl = $sessionIdOrUrl, " +
"Saved thumbnail to disk (id = ${request.id}, " +
"generationId = ${bitmap.generationId})"
)
sharedDiskCache.putThumbnailBitmap(context, sessionIdOrUrl, bitmap)
sharedDiskCache.putThumbnailBitmap(context, request, bitmap)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import android.content.Context
import android.graphics.Bitmap
import com.jakewharton.disklrucache.DiskLruCache
import mozilla.components.support.base.log.logger.Logger
import mozilla.components.support.images.ImageRequest
import java.io.File
import java.io.IOException

Expand All @@ -34,11 +35,11 @@ class ThumbnailDiskCache {
* Retrieves the thumbnail data from the disk cache for the given session ID or URL.
*
* @param context the application [Context].
* @param sessionIdOrUrl the session ID or URL of the thumbnail to retrieve.
* @param request [ImageRequest] providing the session ID or URL of the thumbnail to retrieve.
* @return the [ByteArray] of the thumbnail or null if the snapshot of the entry does not exist.
*/
internal fun getThumbnailData(context: Context, sessionIdOrUrl: String): ByteArray? {
val snapshot = getThumbnailCache(context).get(sessionIdOrUrl) ?: return null
internal fun getThumbnailData(context: Context, request: ImageRequest): ByteArray? {
val snapshot = getThumbnailCache(context).get(request.id) ?: return null

return try {
snapshot.getInputStream(0).use {
Expand All @@ -54,14 +55,14 @@ class ThumbnailDiskCache {
* Stores the given session ID or URL's thumbnail [Bitmap] into the disk cache.
*
* @param context the application [Context].
* @param sessionIdOrUrl the session ID or URL.
* @param request [ImageRequest] providing the session ID or URL of the thumbnail to retrieve.
* @param bitmap the thumbnail [Bitmap] to store.
*/
internal fun putThumbnailBitmap(context: Context, sessionIdOrUrl: String, bitmap: Bitmap) {
internal fun putThumbnailBitmap(context: Context, request: ImageRequest, bitmap: Bitmap) {
try {
synchronized(thumbnailCacheWriteLock) {
val editor = getThumbnailCache(context)
.edit(sessionIdOrUrl) ?: return
.edit(request.id) ?: return

editor.newOutputStream(0).use { stream ->
bitmap.compress(Bitmap.CompressFormat.WEBP, WEBP_QUALITY, stream)
Expand Down
2 changes: 0 additions & 2 deletions components/browser/thumbnails/src/main/res/values/dimens.xml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,5 @@
- 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
Expand Up @@ -12,6 +12,7 @@ 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.images.ImageRequest
import mozilla.components.support.test.ext.joinBlocking
import mozilla.components.support.test.mock
import org.junit.Test
Expand All @@ -23,7 +24,7 @@ class ThumbnailsMiddlewareTest {

@Test
fun `thumbnail storage stores the provided thumbnail on update thumbnail action`() {
val sessionIdOrUrl = "test-tab1"
val request = ImageRequest("test-tab1")
val tab = createTab("https://www.mozilla.org", id = "test-tab1")
val thumbnailStorage: ThumbnailStorage = mock()
val store = BrowserStore(
Expand All @@ -32,8 +33,8 @@ class ThumbnailsMiddlewareTest {
)

val bitmap: Bitmap = mock()
store.dispatch(ContentAction.UpdateThumbnailAction(sessionIdOrUrl, bitmap)).joinBlocking()
verify(thumbnailStorage).saveThumbnail(sessionIdOrUrl, bitmap)
store.dispatch(ContentAction.UpdateThumbnailAction(request.id, bitmap)).joinBlocking()
verify(thumbnailStorage).saveThumbnail(request, bitmap)
}

@Test
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ import kotlinx.coroutines.test.resetMain
import kotlinx.coroutines.test.setMain
import mozilla.components.browser.thumbnails.R
import mozilla.components.browser.thumbnails.storage.ThumbnailStorage
import mozilla.components.support.images.ImageRequest
import mozilla.components.support.images.ImageRequest.Size
import mozilla.components.support.test.any
import mozilla.components.support.test.eq
import mozilla.components.support.test.mock
Expand Down Expand Up @@ -55,10 +57,11 @@ class ThumbnailLoaderTest {
val view: ImageView = mock()
val storage: ThumbnailStorage = mock()
val loader = spy(ThumbnailLoader(storage))
val request = ImageRequest("123", Size.DEFAULT)

doReturn(result).`when`(storage).loadThumbnail("123")
doReturn(result).`when`(storage).loadThumbnail(request)

loader.loadIntoView(view, "123")
loader.loadIntoView(view, request)

verify(view).setImageDrawable(null)
verify(view).addOnAttachStateChangeListener(any())
Expand All @@ -80,10 +83,11 @@ class ThumbnailLoaderTest {
val error: Drawable = mock()
val storage: ThumbnailStorage = mock()
val loader = spy(ThumbnailLoader(storage))
val request = ImageRequest("123", Size.DEFAULT)

doReturn(result).`when`(storage).loadThumbnail("123")
doReturn(result).`when`(storage).loadThumbnail(request)

loader.loadIntoView(view, "123", placeholder = placeholder, error = error)
loader.loadIntoView(view, request, placeholder = placeholder, error = error)

verify(view).setImageDrawable(placeholder)

Expand All @@ -101,11 +105,12 @@ class ThumbnailLoaderTest {
val previousJob: Job = mock()
val storage: ThumbnailStorage = mock()
val loader = spy(ThumbnailLoader(storage))
val request = ImageRequest("123")

doReturn(previousJob).`when`(view).getTag(R.id.mozac_browser_thumbnails_tag_job)
doReturn(result).`when`(storage).loadThumbnail("123")
doReturn(result).`when`(storage).loadThumbnail(request)

loader.loadIntoView(view, "123")
loader.loadIntoView(view, request)

verify(previousJob).cancel()

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import android.graphics.Bitmap
import androidx.test.ext.junit.runners.AndroidJUnit4
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.runBlocking
import mozilla.components.support.images.ImageRequest
import mozilla.components.support.test.ext.joinBlocking
import mozilla.components.support.test.mock
import mozilla.components.support.test.robolectric.testContext
Expand Down Expand Up @@ -38,59 +39,60 @@ class ThumbnailStorageTest {
val bitmap: Bitmap = mock()
val thumbnailStorage = spy(ThumbnailStorage(testContext, testDispatcher))

thumbnailStorage.saveThumbnail("test-tab1", bitmap).joinBlocking()
thumbnailStorage.saveThumbnail("test-tab2", bitmap).joinBlocking()
var thumbnail1 = thumbnailStorage.loadThumbnail("test-tab1").await()
var thumbnail2 = thumbnailStorage.loadThumbnail("test-tab2").await()
thumbnailStorage.saveThumbnail(ImageRequest("test-tab1"), bitmap).joinBlocking()
thumbnailStorage.saveThumbnail(ImageRequest("test-tab2"), bitmap).joinBlocking()
var thumbnail1 = thumbnailStorage.loadThumbnail(ImageRequest("test-tab1")).await()
var thumbnail2 = thumbnailStorage.loadThumbnail(ImageRequest("test-tab2")).await()
assertNotNull(thumbnail1)
assertNotNull(thumbnail2)

thumbnailStorage.clearThumbnails()
thumbnail1 = thumbnailStorage.loadThumbnail("test-tab1").await()
thumbnail2 = thumbnailStorage.loadThumbnail("test-tab2").await()
thumbnail1 = thumbnailStorage.loadThumbnail(ImageRequest("test-tab1")).await()
thumbnail2 = thumbnailStorage.loadThumbnail(ImageRequest("test-tab2")).await()
assertNull(thumbnail1)
assertNull(thumbnail2)
}

@Test
fun `deleteThumbnail`() = runBlocking {
val sessionIdOrUrl = "test-tab1"
val id = "test-tab1"
val request = ImageRequest(id)
val bitmap: Bitmap = mock()
val thumbnailStorage = spy(ThumbnailStorage(testContext, testDispatcher))

thumbnailStorage.saveThumbnail(sessionIdOrUrl, bitmap).joinBlocking()
var thumbnail = thumbnailStorage.loadThumbnail(sessionIdOrUrl).await()
thumbnailStorage.saveThumbnail(request, bitmap).joinBlocking()
var thumbnail = thumbnailStorage.loadThumbnail(request).await()
assertNotNull(thumbnail)

thumbnailStorage.deleteThumbnail(sessionIdOrUrl).joinBlocking()
thumbnail = thumbnailStorage.loadThumbnail(sessionIdOrUrl).await()
thumbnailStorage.deleteThumbnail(id).joinBlocking()
thumbnail = thumbnailStorage.loadThumbnail(request).await()
assertNull(thumbnail)
}

@Test
fun `saveThumbnail`() = runBlocking {
val sessionIdOrUrl = "test-tab1"
val request = ImageRequest("test-tab1")
val bitmap: Bitmap = mock()
val thumbnailStorage = spy(ThumbnailStorage(testContext, testDispatcher))
var thumbnail = thumbnailStorage.loadThumbnail(sessionIdOrUrl).await()
val thumbnailStorage = spy(ThumbnailStorage(testContext))
var thumbnail = thumbnailStorage.loadThumbnail(request).await()

assertNull(thumbnail)

thumbnailStorage.saveThumbnail(sessionIdOrUrl, bitmap).joinBlocking()
thumbnail = thumbnailStorage.loadThumbnail(sessionIdOrUrl).await()
thumbnailStorage.saveThumbnail(request, bitmap).joinBlocking()
thumbnail = thumbnailStorage.loadThumbnail(request).await()
assertNotNull(thumbnail)
}

@Test
fun `loadThumbnail`() = runBlocking {
val sessionIdOrUrl = "test-tab1"
val request = ImageRequest("test-tab1")
val bitmap: Bitmap = mock()
val thumbnailStorage = spy(ThumbnailStorage(testContext, testDispatcher))

thumbnailStorage.saveThumbnail(sessionIdOrUrl, bitmap)
`when`(thumbnailStorage.loadThumbnail(sessionIdOrUrl)).thenReturn(CompletableDeferred(bitmap))
thumbnailStorage.saveThumbnail(request, bitmap)
`when`(thumbnailStorage.loadThumbnail(request)).thenReturn(CompletableDeferred(bitmap))

val thumbnail = thumbnailStorage.loadThumbnail(sessionIdOrUrl).await()
val thumbnail = thumbnailStorage.loadThumbnail(request).await()
assertEquals(bitmap, thumbnail)
}
}
Loading