From 1d9b80bad8984a6b2c4d6277e162de07ded7bd41 Mon Sep 17 00:00:00 2001 From: Andrey Epin Date: Fri, 17 Feb 2023 15:25:15 -0800 Subject: Add image caching to ImagePreviewImageLoader ScrollableImagePreviewView being a RecyclerView may reattache its children multiple times and rely on the ImageLoader to implement any image retrival optimizations. Fix: 269797062 Test: manual test, unit tests Change-Id: I256f4a78a677e939f717fee5dd82492ec572bc65 --- .../intentresolver/ImagePreviewImageLoaderTest.kt | 101 +++++++++++++++++++++ .../android/intentresolver/MockitoKotlinHelpers.kt | 3 + .../intentresolver/TestPreviewImageLoader.kt | 1 + 3 files changed, 105 insertions(+) create mode 100644 java/tests/src/com/android/intentresolver/ImagePreviewImageLoaderTest.kt (limited to 'java/tests/src') diff --git a/java/tests/src/com/android/intentresolver/ImagePreviewImageLoaderTest.kt b/java/tests/src/com/android/intentresolver/ImagePreviewImageLoaderTest.kt new file mode 100644 index 00000000..f327e19e --- /dev/null +++ b/java/tests/src/com/android/intentresolver/ImagePreviewImageLoaderTest.kt @@ -0,0 +1,101 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.intentresolver + +import android.content.ContentResolver +import android.content.Context +import android.content.res.Resources +import android.net.Uri +import android.util.Size +import androidx.lifecycle.Lifecycle +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.TestCoroutineScheduler +import kotlinx.coroutines.test.UnconfinedTestDispatcher +import kotlinx.coroutines.test.resetMain +import kotlinx.coroutines.test.runTest +import kotlinx.coroutines.test.setMain +import org.junit.After +import org.junit.Before +import org.junit.Test +import org.mockito.Mockito.never +import org.mockito.Mockito.times +import org.mockito.Mockito.verify + +@OptIn(ExperimentalCoroutinesApi::class) +class ImagePreviewImageLoaderTest { + private val imageSize = Size(300, 300) + private val uriOne = Uri.parse("content://org.package.app/image-1.png") + private val uriTwo = Uri.parse("content://org.package.app/image-2.png") + private val contentResolver = mock() + private val resources = mock { + whenever(getDimensionPixelSize(R.dimen.chooser_preview_image_max_dimen)) + .thenReturn(imageSize.width) + } + private val context = mock { + whenever(this.resources).thenReturn(this@ImagePreviewImageLoaderTest.resources) + whenever(this.contentResolver).thenReturn(this@ImagePreviewImageLoaderTest.contentResolver) + } + private val scheduler = TestCoroutineScheduler() + private val lifecycleOwner = TestLifecycleOwner() + private val dispatcher = UnconfinedTestDispatcher(scheduler) + private val testSubject = ImagePreviewImageLoader( + context, lifecycleOwner.lifecycle, 1, dispatcher + ) + + @Before + fun setup() { + Dispatchers.setMain(dispatcher) + lifecycleOwner.state = Lifecycle.State.CREATED + } + + @After + fun cleanup() { + lifecycleOwner.state = Lifecycle.State.DESTROYED + Dispatchers.resetMain() + } + + @Test + fun test_prePopulate() = runTest { + testSubject.prePopulate(listOf(uriOne, uriTwo)) + + verify(contentResolver, times(1)).loadThumbnail(uriOne, imageSize, null) + verify(contentResolver, never()).loadThumbnail(uriTwo, imageSize, null) + + testSubject(uriOne) + verify(contentResolver, times(1)).loadThumbnail(uriOne, imageSize, null) + } + + @Test + fun test_invoke_return_cached_image() = runTest { + testSubject(uriOne) + testSubject(uriOne) + + verify(contentResolver, times(1)).loadThumbnail(any(), any(), anyOrNull()) + } + + @Test + fun test_invoke_old_records_evicted_from_the_cache() = runTest { + testSubject(uriOne) + testSubject(uriTwo) + testSubject(uriTwo) + testSubject(uriOne) + + verify(contentResolver, times(2)).loadThumbnail(uriOne, imageSize, null) + verify(contentResolver, times(1)).loadThumbnail(uriTwo, imageSize, null) + } +} diff --git a/java/tests/src/com/android/intentresolver/MockitoKotlinHelpers.kt b/java/tests/src/com/android/intentresolver/MockitoKotlinHelpers.kt index 159c6d6a..aaa7a282 100644 --- a/java/tests/src/com/android/intentresolver/MockitoKotlinHelpers.kt +++ b/java/tests/src/com/android/intentresolver/MockitoKotlinHelpers.kt @@ -26,6 +26,7 @@ package com.android.intentresolver import org.mockito.ArgumentCaptor import org.mockito.ArgumentMatcher +import org.mockito.ArgumentMatchers import org.mockito.Mockito import org.mockito.stubbing.OngoingStubbing @@ -144,3 +145,5 @@ inline fun withArgCaptor(block: KotlinArgumentCaptor.() -> */ inline fun captureMany(block: KotlinArgumentCaptor.() -> Unit): List = kotlinArgumentCaptor().apply{ block() }.allValues + +inline fun anyOrNull() = ArgumentMatchers.argThat(ArgumentMatcher { true }) diff --git a/java/tests/src/com/android/intentresolver/TestPreviewImageLoader.kt b/java/tests/src/com/android/intentresolver/TestPreviewImageLoader.kt index fd617fdd..cfe041dd 100644 --- a/java/tests/src/com/android/intentresolver/TestPreviewImageLoader.kt +++ b/java/tests/src/com/android/intentresolver/TestPreviewImageLoader.kt @@ -34,4 +34,5 @@ internal class TestPreviewImageLoader( } override suspend fun invoke(uri: Uri): Bitmap? = imageOverride() ?: imageLoader(uri) + override fun prePopulate(uris: List) = Unit } -- cgit v1.2.3-59-g8ed1b