diff options
14 files changed, 219 insertions, 115 deletions
diff --git a/java/src/com/android/intentresolver/contentpreview/CachingImagePreviewImageLoader.kt b/java/src/com/android/intentresolver/contentpreview/CachingImagePreviewImageLoader.kt index c9deec1b..f60f550e 100644 --- a/java/src/com/android/intentresolver/contentpreview/CachingImagePreviewImageLoader.kt +++ b/java/src/com/android/intentresolver/contentpreview/CachingImagePreviewImageLoader.kt @@ -19,6 +19,7 @@ package com.android.intentresolver.contentpreview import android.graphics.Bitmap import android.net.Uri import android.util.Log +import android.util.Size import androidx.core.util.lruCache import com.android.intentresolver.inject.Background import com.android.intentresolver.inject.ViewModelOwned @@ -72,11 +73,11 @@ constructor( } ) - override fun prePopulate(uris: List<Uri>) { - uris.take(cache.maxSize()).map { cache[it] } + override fun prePopulate(uriSizePairs: List<Pair<Uri, Size>>) { + uriSizePairs.take(cache.maxSize()).map { cache[it.first] } } - override suspend fun invoke(uri: Uri, caching: Boolean): Bitmap? { + override suspend fun invoke(uri: Uri, size: Size, caching: Boolean): Bitmap? { return if (caching) { loadCachedImage(uri) } else { diff --git a/java/src/com/android/intentresolver/contentpreview/FilesPlusTextContentPreviewUi.java b/java/src/com/android/intentresolver/contentpreview/FilesPlusTextContentPreviewUi.java index b50f5bc8..30161cfb 100644 --- a/java/src/com/android/intentresolver/contentpreview/FilesPlusTextContentPreviewUi.java +++ b/java/src/com/android/intentresolver/contentpreview/FilesPlusTextContentPreviewUi.java @@ -23,6 +23,7 @@ import android.content.res.Resources; import android.net.Uri; import android.text.util.Linkify; import android.util.PluralsMessageFormatter; +import android.util.Size; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; @@ -68,6 +69,7 @@ class FilesPlusTextContentPreviewUi extends ContentPreviewUi { private Uri mFirstFilePreviewUri; private boolean mAllImages; private boolean mAllVideos; + private int mPreviewSize; // TODO(b/285309527): make this a flag private static final boolean SHOW_TOGGLE_CHECKMARK = false; @@ -109,6 +111,7 @@ class FilesPlusTextContentPreviewUi extends ContentPreviewUi { LayoutInflater layoutInflater, ViewGroup parent, View headlineViewParent) { + mPreviewSize = resources.getDimensionPixelSize(R.dimen.width_text_image_preview_size); return displayInternal(layoutInflater, parent, headlineViewParent); } @@ -164,12 +167,12 @@ class FilesPlusTextContentPreviewUi extends ContentPreviewUi { private void updateUiWithMetadata(ViewGroup contentPreviewView, View headlineView) { prepareTextPreview(contentPreviewView, headlineView, mActionFactory); updateHeadline(headlineView, mFileCount, mAllImages, mAllVideos); - ImageView imagePreview = mContentPreviewView.requireViewById(R.id.image_view); if (mIsSingleImage && mFirstFilePreviewUri != null) { mImageLoader.loadImage( mScope, mFirstFilePreviewUri, + new Size(mPreviewSize, mPreviewSize), bitmap -> { if (bitmap == null) { imagePreview.setVisibility(View.GONE); diff --git a/java/src/com/android/intentresolver/contentpreview/ImageLoader.kt b/java/src/com/android/intentresolver/contentpreview/ImageLoader.kt index b4d03ac9..ac34f552 100644 --- a/java/src/com/android/intentresolver/contentpreview/ImageLoader.kt +++ b/java/src/com/android/intentresolver/contentpreview/ImageLoader.kt @@ -18,23 +18,25 @@ package com.android.intentresolver.contentpreview import android.graphics.Bitmap import android.net.Uri +import android.util.Size import java.util.function.Consumer import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.isActive import kotlinx.coroutines.launch /** A content preview image loader. */ -interface ImageLoader : suspend (Uri) -> Bitmap?, suspend (Uri, Boolean) -> Bitmap? { +interface ImageLoader : suspend (Uri, Size) -> Bitmap?, suspend (Uri, Size, Boolean) -> Bitmap? { /** * Load preview image asynchronously; caching is allowed. * * @param uri content URI + * @param size target bitmap size * @param callback a callback that will be invoked with the loaded image or null if loading has * failed. */ - fun loadImage(callerScope: CoroutineScope, uri: Uri, callback: Consumer<Bitmap?>) { + fun loadImage(callerScope: CoroutineScope, uri: Uri, size: Size, callback: Consumer<Bitmap?>) { callerScope.launch { - val bitmap = invoke(uri) + val bitmap = invoke(uri, size) if (isActive) { callback.accept(bitmap) } @@ -42,13 +44,13 @@ interface ImageLoader : suspend (Uri) -> Bitmap?, suspend (Uri, Boolean) -> Bitm } /** Prepopulate the image loader cache. */ - fun prePopulate(uris: List<Uri>) + fun prePopulate(uriSizePairs: List<Pair<Uri, Size>>) /** Returns a bitmap for the given URI if it's already cached, otherwise null */ fun getCachedBitmap(uri: Uri): Bitmap? = null /** Load preview image; caching is allowed. */ - override suspend fun invoke(uri: Uri) = invoke(uri, true) + override suspend fun invoke(uri: Uri, size: Size) = invoke(uri, size, true) /** * Load preview image. @@ -56,5 +58,5 @@ interface ImageLoader : suspend (Uri) -> Bitmap?, suspend (Uri, Boolean) -> Bitm * @param uri content URI * @param caching indicates if the loaded image could be cached. */ - override suspend fun invoke(uri: Uri, caching: Boolean): Bitmap? + override suspend fun invoke(uri: Uri, size: Size, caching: Boolean): Bitmap? } diff --git a/java/src/com/android/intentresolver/contentpreview/ImagePreviewImageLoader.kt b/java/src/com/android/intentresolver/contentpreview/ImagePreviewImageLoader.kt index 7cf9a8c9..379bdb37 100644 --- a/java/src/com/android/intentresolver/contentpreview/ImagePreviewImageLoader.kt +++ b/java/src/com/android/intentresolver/contentpreview/ImagePreviewImageLoader.kt @@ -98,10 +98,11 @@ constructor( @GuardedBy("lock") private val cache = LruCache<Uri, RequestRecord>(cacheSize) @GuardedBy("lock") private val runningRequests = HashMap<Uri, RequestRecord>() - override suspend fun invoke(uri: Uri, caching: Boolean): Bitmap? = loadImageAsync(uri, caching) + override suspend fun invoke(uri: Uri, size: Size, caching: Boolean): Bitmap? = + loadImageAsync(uri, caching) - override fun prePopulate(uris: List<Uri>) { - uris.asSequence().take(cache.maxSize()).forEach { uri -> + override fun prePopulate(uriSizePairs: List<Pair<Uri, Size>>) { + uriSizePairs.asSequence().take(cache.maxSize()).forEach { (uri, _) -> scope.launch { loadImageAsync(uri, caching = true) } } } diff --git a/java/src/com/android/intentresolver/contentpreview/TextContentPreviewUi.java b/java/src/com/android/intentresolver/contentpreview/TextContentPreviewUi.java index ae7ddcd9..b12eb8cf 100644 --- a/java/src/com/android/intentresolver/contentpreview/TextContentPreviewUi.java +++ b/java/src/com/android/intentresolver/contentpreview/TextContentPreviewUi.java @@ -22,6 +22,7 @@ import android.content.res.Resources; import android.net.Uri; import android.text.SpannableStringBuilder; import android.text.TextUtils; +import android.util.Size; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; @@ -50,6 +51,7 @@ class TextContentPreviewUi extends ContentPreviewUi { private final ChooserContentPreviewUi.ActionFactory mActionFactory; private final HeadlineGenerator mHeadlineGenerator; private final ContentTypeHint mContentTypeHint; + private int mPreviewSize; TextContentPreviewUi( CoroutineScope scope, @@ -83,6 +85,7 @@ class TextContentPreviewUi extends ContentPreviewUi { LayoutInflater layoutInflater, ViewGroup parent, View headlineViewParent) { + mPreviewSize = resources.getDimensionPixelSize(R.dimen.width_text_image_preview_size); return displayInternal(layoutInflater, parent, headlineViewParent); } @@ -119,7 +122,7 @@ class TextContentPreviewUi extends ContentPreviewUi { previewTitleView.setText(mPreviewTitle); } - ImageView previewThumbnailView = contentPreviewLayout.findViewById( + final ImageView previewThumbnailView = contentPreviewLayout.requireViewById( com.android.internal.R.id.content_preview_thumbnail); if (!isOwnedByCurrentUser(mPreviewThumbnail)) { previewThumbnailView.setVisibility(View.GONE); @@ -127,9 +130,9 @@ class TextContentPreviewUi extends ContentPreviewUi { mImageLoader.loadImage( mScope, mPreviewThumbnail, + new Size(mPreviewSize, mPreviewSize), (bitmap) -> updateViewWithImage( - contentPreviewLayout.findViewById( - com.android.internal.R.id.content_preview_thumbnail), + previewThumbnailView, bitmap)); } diff --git a/java/src/com/android/intentresolver/contentpreview/UnifiedContentPreviewUi.java b/java/src/com/android/intentresolver/contentpreview/UnifiedContentPreviewUi.java index 88311016..7de988c4 100644 --- a/java/src/com/android/intentresolver/contentpreview/UnifiedContentPreviewUi.java +++ b/java/src/com/android/intentresolver/contentpreview/UnifiedContentPreviewUi.java @@ -20,6 +20,7 @@ import static com.android.intentresolver.contentpreview.ContentPreviewType.CONTE import android.content.res.Resources; import android.util.Log; +import android.util.Size; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; @@ -31,6 +32,8 @@ import com.android.intentresolver.widget.ActionRow; import com.android.intentresolver.widget.ImagePreviewView.TransitionElementStatusCallback; import com.android.intentresolver.widget.ScrollableImagePreviewView; +import kotlin.Pair; + import kotlinx.coroutines.CoroutineScope; import kotlinx.coroutines.flow.Flow; @@ -55,6 +58,7 @@ class UnifiedContentPreviewUi extends ContentPreviewUi { @Nullable private ViewGroup mContentPreviewView; private View mHeadlineView; + private int mPreviewSize; UnifiedContentPreviewUi( CoroutineScope scope, @@ -93,14 +97,18 @@ class UnifiedContentPreviewUi extends ContentPreviewUi { LayoutInflater layoutInflater, ViewGroup parent, View headlineViewParent) { + mPreviewSize = resources.getDimensionPixelSize(R.dimen.chooser_preview_image_max_dimen); return displayInternal(layoutInflater, parent, headlineViewParent); } private void setFiles(List<FileInfo> files) { - mImageLoader.prePopulate(files.stream() - .map(FileInfo::getPreviewUri) - .filter(Objects::nonNull) - .toList()); + Size previewSize = new Size(mPreviewSize, mPreviewSize); + mImageLoader.prePopulate( + files.stream() + .map(FileInfo::getPreviewUri) + .filter(Objects::nonNull) + .map((uri -> new Pair<>(uri, previewSize))) + .toList()); mFiles = files; if (mContentPreviewView != null) { updatePreviewWithFiles(mContentPreviewView, mHeadlineView, files); @@ -121,6 +129,7 @@ class UnifiedContentPreviewUi extends ContentPreviewUi { ScrollableImagePreviewView imagePreview = mContentPreviewView.requireViewById(R.id.scrollable_image_preview); + imagePreview.setPreviewHeight(mPreviewSize); imagePreview.setImageLoader(mImageLoader); imagePreview.setOnNoPreviewCallback(() -> imagePreview.setVisibility(View.GONE)); imagePreview.setTransitionElementStatusCallback(mTransitionElementStatusCallback); diff --git a/java/src/com/android/intentresolver/contentpreview/payloadtoggle/ui/composable/ShareouselComposable.kt b/java/src/com/android/intentresolver/contentpreview/payloadtoggle/ui/composable/ShareouselComposable.kt index 3c3381a2..9ac36a87 100644 --- a/java/src/com/android/intentresolver/contentpreview/payloadtoggle/ui/composable/ShareouselComposable.kt +++ b/java/src/com/android/intentresolver/contentpreview/payloadtoggle/ui/composable/ShareouselComposable.kt @@ -111,6 +111,7 @@ private fun PreviewCarousel( prefetchStrategy = remember { ShareouselLazyListPrefetchStrategy() } ) var maxAspectRatio by remember { mutableStateOf(0f) } + var viewportHeight by remember { mutableStateOf(0) } val horizontalPadding = 16.dp Box( @@ -130,9 +131,8 @@ private fun PreviewCarousel( MAX_ASPECT_RATIO ) } - if (maxAspectRatio != aspectRatio) { - maxAspectRatio = aspectRatio - } + maxAspectRatio = aspectRatio + viewportHeight = placeable.height layout(placeable.width, placeable.height) { placeable.place(0, 0) } }, ) { @@ -170,7 +170,12 @@ private fun PreviewCarousel( } ShareouselCard( - viewModel.preview(model, previewIndex, rememberCoroutineScope()), + viewModel.preview( + model, + viewportHeight, + previewIndex, + rememberCoroutineScope() + ), maxAspectRatio, ) } diff --git a/java/src/com/android/intentresolver/contentpreview/payloadtoggle/ui/viewmodel/ShareouselViewModel.kt b/java/src/com/android/intentresolver/contentpreview/payloadtoggle/ui/viewmodel/ShareouselViewModel.kt index d0b89860..f1e65f73 100644 --- a/java/src/com/android/intentresolver/contentpreview/payloadtoggle/ui/viewmodel/ShareouselViewModel.kt +++ b/java/src/com/android/intentresolver/contentpreview/payloadtoggle/ui/viewmodel/ShareouselViewModel.kt @@ -15,6 +15,7 @@ */ package com.android.intentresolver.contentpreview.payloadtoggle.ui.viewmodel +import android.util.Size import com.android.intentresolver.contentpreview.CachingImagePreviewImageLoader import com.android.intentresolver.contentpreview.HeadlineGenerator import com.android.intentresolver.contentpreview.ImageLoader @@ -57,7 +58,9 @@ data class ShareouselViewModel( val actions: Flow<List<ActionChipViewModel>>, /** Creates a [ShareouselPreviewViewModel] for a [PreviewModel] present in [previews]. */ val preview: - (key: PreviewModel, index: Int?, scope: CoroutineScope) -> ShareouselPreviewViewModel, + ( + key: PreviewModel, previewHeight: Int, index: Int?, scope: CoroutineScope + ) -> ShareouselPreviewViewModel, ) @Module @@ -114,7 +117,7 @@ interface ShareouselViewModelModule { } } }, - preview = { key, index, previewScope -> + preview = { key, previewHeight, index, previewScope -> keySet.value?.maybeLoad(index) val previewInteractor = interactor.preview(key) val contentType = @@ -130,9 +133,19 @@ interface ShareouselViewModelModule { ShareouselPreviewViewModel( bitmapLoadState = flow { + val previewWidth = + if (key.aspectRatio > 0) { + previewHeight.toFloat() / key.aspectRatio + } else { + previewHeight + } + .toInt() emit( - key.previewUri?.let { ValueUpdate.Value(imageLoader(it)) } - ?: ValueUpdate.Absent + key.previewUri?.let { + ValueUpdate.Value( + imageLoader(it, Size(previewWidth, previewHeight)) + ) + } ?: ValueUpdate.Absent ) } .stateIn(previewScope, SharingStarted.Eagerly, initialBitmapValue), diff --git a/java/src/com/android/intentresolver/widget/ScrollableImagePreviewView.kt b/java/src/com/android/intentresolver/widget/ScrollableImagePreviewView.kt index 7fe16091..c706e3ee 100644 --- a/java/src/com/android/intentresolver/widget/ScrollableImagePreviewView.kt +++ b/java/src/com/android/intentresolver/widget/ScrollableImagePreviewView.kt @@ -22,6 +22,7 @@ import android.graphics.Rect import android.net.Uri import android.util.AttributeSet import android.util.PluralsMessageFormatter +import android.util.Size import android.util.TypedValue import android.view.LayoutInflater import android.view.View @@ -60,11 +61,13 @@ private const val MIN_ASPECT_RATIO_STRING = "2:5" private const val MAX_ASPECT_RATIO = 2.5f private const val MAX_ASPECT_RATIO_STRING = "5:2" -private typealias CachingImageLoader = suspend (Uri, Boolean) -> Bitmap? +private typealias CachingImageLoader = suspend (Uri, Size, Boolean) -> Bitmap? class ScrollableImagePreviewView : RecyclerView, ImagePreviewView { constructor(context: Context) : this(context, null) + constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, 0) + constructor( context: Context, attrs: AttributeSet?, @@ -121,12 +124,19 @@ class ScrollableImagePreviewView : RecyclerView, ImagePreviewView { * A hint about the maximum width this view can grow to, this helps to optimize preview loading. */ var maxWidthHint: Int = -1 + private var requestedHeight: Int = 0 private var isMeasured = false private var maxAspectRatio = MAX_ASPECT_RATIO private var maxAspectRatioString = MAX_ASPECT_RATIO_STRING private var outerSpacing: Int = 0 + var previewHeight: Int + get() = previewAdapter.previewHeight + set(value) { + previewAdapter.previewHeight = value + } + override fun onMeasure(widthSpec: Int, heightSpec: Int) { super.onMeasure(widthSpec, heightSpec) if (!isMeasured) { @@ -198,6 +208,7 @@ class ScrollableImagePreviewView : RecyclerView, ImagePreviewView { BatchPreviewLoader( previewAdapter.imageLoader ?: error("Image loader is not set"), previews, + Size(previewHeight, previewHeight), totalItemCount, onUpdate = previewAdapter::addPreviews, onCompletion = { @@ -303,11 +314,19 @@ class ScrollableImagePreviewView : RecyclerView, ImagePreviewView { private var isLoading = false private val hasOtherItem get() = previews.size < totalItemCount + val hasPreviews: Boolean get() = previews.isNotEmpty() var transitionStatusElementCallback: TransitionElementStatusCallback? = null + private var previewSize: Size = Size(0, 0) + var previewHeight: Int + get() = previewSize.height + set(value) { + previewSize = Size(value, value) + } + fun reset(totalItemCount: Int) { firstImagePos = -1 previews.clear() @@ -387,6 +406,7 @@ class ScrollableImagePreviewView : RecyclerView, ImagePreviewView { vh.bind( previews[position], imageLoader ?: error("ImageLoader is missing"), + previewSize, fadeInDurationMs, isSharedTransitionElement = position == firstImagePos, previewReadyCallback = @@ -438,6 +458,7 @@ class ScrollableImagePreviewView : RecyclerView, ImagePreviewView { fun bind( preview: Preview, imageLoader: CachingImageLoader, + previewSize: Size, fadeInDurationMs: Long, isSharedTransitionElement: Boolean, previewReadyCallback: ((String) -> Unit)? @@ -477,7 +498,7 @@ class ScrollableImagePreviewView : RecyclerView, ImagePreviewView { } } resetScope().launch { - loadImage(preview, imageLoader) + loadImage(preview, previewSize, imageLoader) if (preview.type == PreviewType.Image && previewReadyCallback != null) { image.waitForPreDraw() previewReadyCallback(TRANSITION_NAME) @@ -487,12 +508,16 @@ class ScrollableImagePreviewView : RecyclerView, ImagePreviewView { } } - private suspend fun loadImage(preview: Preview, imageLoader: CachingImageLoader) { + private suspend fun loadImage( + preview: Preview, + previewSize: Size, + imageLoader: CachingImageLoader, + ) { val bitmap = runCatching { // it's expected for all loading/caching optimizations to be implemented by // the loader - imageLoader(preview.uri, true) + imageLoader(preview.uri, previewSize, true) } .getOrNull() image.setImageBitmap(bitmap) @@ -507,6 +532,7 @@ class ScrollableImagePreviewView : RecyclerView, ImagePreviewView { setAnimationListener( object : AnimationListener { override fun onAnimationStart(animation: Animation?) = Unit + override fun onAnimationRepeat(animation: Animation?) = Unit override fun onAnimationEnd(animation: Animation?) { @@ -551,6 +577,7 @@ class ScrollableImagePreviewView : RecyclerView, ImagePreviewView { private class LoadingItemViewHolder(view: View) : ViewHolder(view) { fun bind() = Unit + override fun unbind() = Unit } @@ -638,6 +665,7 @@ class ScrollableImagePreviewView : RecyclerView, ImagePreviewView { class BatchPreviewLoader( private val imageLoader: CachingImageLoader, private val previews: Flow<Preview>, + private val previewSize: Size, val totalItemCount: Int, private val onUpdate: (List<Preview>) -> Unit, private val onCompletion: () -> Unit, @@ -701,10 +729,10 @@ class ScrollableImagePreviewView : RecyclerView, ImagePreviewView { // imagine is one of the first images never loads so we never // fill the initial viewport and does not show the previews at // all. - imageLoader(preview.uri, isFirstBlock)?.let { bitmap -> + imageLoader(preview.uri, previewSize, isFirstBlock)?.let { + bitmap -> previewSizeUpdater(preview, bitmap.width, bitmap.height) - } - ?: 0 + } ?: 0 } .getOrDefault(0) diff --git a/tests/shared/src/com/android/intentresolver/FakeImageLoader.kt b/tests/shared/src/com/android/intentresolver/FakeImageLoader.kt index c57ea78b..76eb5e0d 100644 --- a/tests/shared/src/com/android/intentresolver/FakeImageLoader.kt +++ b/tests/shared/src/com/android/intentresolver/FakeImageLoader.kt @@ -18,6 +18,7 @@ package com.android.intentresolver import android.graphics.Bitmap import android.net.Uri +import android.util.Size import com.android.intentresolver.contentpreview.ImageLoader import java.util.function.Consumer import kotlinx.coroutines.CoroutineScope @@ -25,13 +26,18 @@ import kotlinx.coroutines.CoroutineScope class FakeImageLoader(initialBitmaps: Map<Uri, Bitmap> = emptyMap()) : ImageLoader { private val bitmaps = HashMap<Uri, Bitmap>().apply { putAll(initialBitmaps) } - override fun loadImage(callerScope: CoroutineScope, uri: Uri, callback: Consumer<Bitmap?>) { + override fun loadImage( + callerScope: CoroutineScope, + uri: Uri, + size: Size, + callback: Consumer<Bitmap?>, + ) { callback.accept(bitmaps[uri]) } - override suspend fun invoke(uri: Uri, caching: Boolean): Bitmap? = bitmaps[uri] + override suspend fun invoke(uri: Uri, size: Size, caching: Boolean): Bitmap? = bitmaps[uri] - override fun prePopulate(uris: List<Uri>) = Unit + override fun prePopulate(uriSizePairs: List<Pair<Uri, Size>>) = Unit fun setBitmap(uri: Uri, bitmap: Bitmap) { bitmaps[uri] = bitmap diff --git a/tests/unit/src/com/android/intentresolver/contentpreview/CachingImagePreviewImageLoaderTest.kt b/tests/unit/src/com/android/intentresolver/contentpreview/CachingImagePreviewImageLoaderTest.kt index 331f9f64..d5a569aa 100644 --- a/tests/unit/src/com/android/intentresolver/contentpreview/CachingImagePreviewImageLoaderTest.kt +++ b/tests/unit/src/com/android/intentresolver/contentpreview/CachingImagePreviewImageLoaderTest.kt @@ -18,6 +18,7 @@ package com.android.intentresolver.contentpreview import android.graphics.Bitmap import android.net.Uri +import android.util.Size import com.google.common.truth.Truth.assertThat import kotlin.math.ceil import kotlin.math.roundToInt @@ -43,6 +44,7 @@ class CachingImagePreviewImageLoaderTest { testJobTime * ceil((testCacheSize).toFloat() / testMaxConcurrency.toFloat()).roundToInt() private val testUris = List(5) { Uri.fromParts("TestScheme$it", "TestSsp$it", "TestFragment$it") } + private val previewSize = Size(500, 500) private val testTimeToLoadAllUris = testJobTime * ceil((testUris.size).toFloat() / testMaxConcurrency.toFloat()).roundToInt() private val testBitmap = Bitmap.createBitmap(10, 10, Bitmap.Config.ALPHA_8) @@ -72,7 +74,7 @@ class CachingImagePreviewImageLoaderTest { var result: Bitmap? = null // Act - imageLoader.loadImage(testScope, testUris[0]) { result = it } + imageLoader.loadImage(testScope, testUris[0], previewSize) { result = it } advanceTimeBy(testJobTime) runCurrent() @@ -85,14 +87,14 @@ class CachingImagePreviewImageLoaderTest { fun loadImage_cached_usesCachedValue() = testScope.runTest { // Arrange - imageLoader.loadImage(testScope, testUris[0]) {} + imageLoader.loadImage(testScope, testUris[0], previewSize) {} advanceTimeBy(testJobTime) runCurrent() fakeThumbnailLoader.invokeCalls.clear() var result: Bitmap? = null // Act - imageLoader.loadImage(testScope, testUris[0]) { result = it } + imageLoader.loadImage(testScope, testUris[0], previewSize) { result = it } advanceTimeBy(testJobTime) runCurrent() @@ -112,7 +114,7 @@ class CachingImagePreviewImageLoaderTest { var result: Bitmap? = testBitmap // Act - imageLoader.loadImage(testScope, testUris[0]) { result = it } + imageLoader.loadImage(testScope, testUris[0], previewSize) { result = it } advanceTimeBy(testJobTime) runCurrent() @@ -130,7 +132,7 @@ class CachingImagePreviewImageLoaderTest { // Act testUris.take(testMaxConcurrency + 1).forEach { uri -> - imageLoader.loadImage(testScope, uri) { results.add(it) } + imageLoader.loadImage(testScope, uri, previewSize) { results.add(it) } } // Assert @@ -153,10 +155,10 @@ class CachingImagePreviewImageLoaderTest { assertThat(testUris.size).isGreaterThan(testCacheSize) // Act - imageLoader.loadImage(testScope, testUris[0]) { results[0] = it } + imageLoader.loadImage(testScope, testUris[0], previewSize) { results[0] = it } runCurrent() testUris.indices.drop(1).take(testCacheSize).forEach { i -> - imageLoader.loadImage(testScope, testUris[i]) { results[i] = it } + imageLoader.loadImage(testScope, testUris[i], previewSize) { results[i] = it } } advanceTimeBy(testTimeToFillCache) runCurrent() @@ -179,7 +181,7 @@ class CachingImagePreviewImageLoaderTest { assertThat(fullCacheUris).hasSize(testCacheSize) // Act - imageLoader.prePopulate(fullCacheUris) + imageLoader.prePopulate(fullCacheUris.map { it to previewSize }) advanceTimeBy(testTimeToFillCache) runCurrent() @@ -188,7 +190,7 @@ class CachingImagePreviewImageLoaderTest { // Act fakeThumbnailLoader.invokeCalls.clear() - imageLoader.prePopulate(fullCacheUris) + imageLoader.prePopulate(fullCacheUris.map { it to previewSize }) advanceTimeBy(testTimeToFillCache) runCurrent() @@ -203,7 +205,7 @@ class CachingImagePreviewImageLoaderTest { assertThat(testUris.size).isGreaterThan(testCacheSize) // Act - imageLoader.prePopulate(testUris) + imageLoader.prePopulate(testUris.map { it to previewSize }) advanceTimeBy(testTimeToLoadAllUris) runCurrent() @@ -213,7 +215,7 @@ class CachingImagePreviewImageLoaderTest { // Act fakeThumbnailLoader.invokeCalls.clear() - imageLoader.prePopulate(testUris) + imageLoader.prePopulate(testUris.map { it to previewSize }) advanceTimeBy(testTimeToLoadAllUris) runCurrent() @@ -229,7 +231,7 @@ class CachingImagePreviewImageLoaderTest { assertThat(unfilledCacheUris.size).isLessThan(testCacheSize) // Act - imageLoader.prePopulate(unfilledCacheUris) + imageLoader.prePopulate(unfilledCacheUris.map { it to previewSize }) advanceTimeBy(testJobTime) runCurrent() @@ -238,7 +240,7 @@ class CachingImagePreviewImageLoaderTest { // Act fakeThumbnailLoader.invokeCalls.clear() - imageLoader.prePopulate(unfilledCacheUris) + imageLoader.prePopulate(unfilledCacheUris.map { it to previewSize }) advanceTimeBy(testJobTime) runCurrent() @@ -252,8 +254,8 @@ class CachingImagePreviewImageLoaderTest { // Arrange // Act - imageLoader.invoke(testUris[0], caching = false) - imageLoader.invoke(testUris[0], caching = false) + imageLoader.invoke(testUris[0], previewSize, caching = false) + imageLoader.invoke(testUris[0], previewSize, caching = false) advanceTimeBy(testJobTime) runCurrent() @@ -267,8 +269,8 @@ class CachingImagePreviewImageLoaderTest { // Arrange // Act - imageLoader.invoke(testUris[0], caching = true) - imageLoader.invoke(testUris[0], caching = true) + imageLoader.invoke(testUris[0], previewSize, caching = true) + imageLoader.invoke(testUris[0], previewSize, caching = true) advanceTimeBy(testJobTime) runCurrent() diff --git a/tests/unit/src/com/android/intentresolver/contentpreview/ImagePreviewImageLoaderTest.kt b/tests/unit/src/com/android/intentresolver/contentpreview/ImagePreviewImageLoaderTest.kt index 3a45e2f6..d78e6665 100644 --- a/tests/unit/src/com/android/intentresolver/contentpreview/ImagePreviewImageLoaderTest.kt +++ b/tests/unit/src/com/android/intentresolver/contentpreview/ImagePreviewImageLoaderTest.kt @@ -77,24 +77,25 @@ class ImagePreviewImageLoaderTest { contentResolver, cacheSize = 1, ) + private val previewSize = Size(500, 500) @Test fun prePopulate_cachesImagesUpToTheCacheSize() = scope.runTest { - testSubject.prePopulate(listOf(uriOne, uriTwo)) + testSubject.prePopulate(listOf(uriOne to previewSize, uriTwo to previewSize)) verify(contentResolver, times(1)).loadThumbnail(uriOne, imageSize, null) verify(contentResolver, never()).loadThumbnail(uriTwo, imageSize, null) - testSubject(uriOne) + testSubject(uriOne, previewSize) verify(contentResolver, times(1)).loadThumbnail(uriOne, imageSize, null) } @Test fun invoke_returnCachedImageWhenCalledTwice() = scope.runTest { - testSubject(uriOne) - testSubject(uriOne) + testSubject(uriOne, previewSize) + testSubject(uriOne, previewSize) verify(contentResolver, times(1)).loadThumbnail(any(), any(), anyOrNull()) } @@ -102,8 +103,8 @@ class ImagePreviewImageLoaderTest { @Test fun invoke_whenInstructed_doesNotCache() = scope.runTest { - testSubject(uriOne, false) - testSubject(uriOne, false) + testSubject(uriOne, previewSize, false) + testSubject(uriOne, previewSize, false) verify(contentResolver, times(2)).loadThumbnail(any(), any(), anyOrNull()) } @@ -120,8 +121,8 @@ class ImagePreviewImageLoaderTest { cacheSize = 1, ) coroutineScope { - launch(start = UNDISPATCHED) { testSubject(uriOne, false) } - launch(start = UNDISPATCHED) { testSubject(uriOne, false) } + launch(start = UNDISPATCHED) { testSubject(uriOne, previewSize, false) } + launch(start = UNDISPATCHED) { testSubject(uriOne, previewSize, false) } scheduler.advanceUntilIdle() } @@ -131,10 +132,10 @@ class ImagePreviewImageLoaderTest { @Test fun invoke_oldRecordsEvictedFromTheCache() = scope.runTest { - testSubject(uriOne) - testSubject(uriTwo) - testSubject(uriTwo) - testSubject(uriOne) + testSubject(uriOne, previewSize) + testSubject(uriTwo, previewSize) + testSubject(uriTwo, previewSize) + testSubject(uriOne, previewSize) verify(contentResolver, times(2)).loadThumbnail(uriOne, imageSize, null) verify(contentResolver, times(1)).loadThumbnail(uriTwo, imageSize, null) @@ -144,8 +145,8 @@ class ImagePreviewImageLoaderTest { fun invoke_doNotCacheNulls() = scope.runTest { whenever(contentResolver.loadThumbnail(any(), any(), anyOrNull())).thenReturn(null) - testSubject(uriOne) - testSubject(uriOne) + testSubject(uriOne, previewSize) + testSubject(uriOne, previewSize) verify(contentResolver, times(2)).loadThumbnail(uriOne, imageSize, null) } @@ -162,7 +163,7 @@ class ImagePreviewImageLoaderTest { cacheSize = 1, ) imageLoaderScope.cancel() - testSubject(uriOne) + testSubject(uriOne, previewSize) } @Test(expected = CancellationException::class) @@ -178,7 +179,8 @@ class ImagePreviewImageLoaderTest { cacheSize = 1, ) coroutineScope { - val deferred = async(start = UNDISPATCHED) { testSubject(uriOne, false) } + val deferred = + async(start = UNDISPATCHED) { testSubject(uriOne, previewSize, false) } imageLoaderScope.cancel() scheduler.advanceUntilIdle() deferred.await() @@ -198,11 +200,11 @@ class ImagePreviewImageLoaderTest { cacheSize = 1, ) coroutineScope { - launch(start = UNDISPATCHED) { testSubject(uriOne, false) } - launch(start = UNDISPATCHED) { testSubject(uriOne, true) } + launch(start = UNDISPATCHED) { testSubject(uriOne, previewSize, false) } + launch(start = UNDISPATCHED) { testSubject(uriOne, previewSize, true) } scheduler.advanceUntilIdle() } - testSubject(uriOne, true) + testSubject(uriOne, previewSize, true) verify(contentResolver, times(1)).loadThumbnail(uriOne, imageSize, null) } @@ -243,7 +245,7 @@ class ImagePreviewImageLoaderTest { cacheSize = 1, testSemaphore, ) - testSubject(uriOne, false) + testSubject(uriOne, previewSize, false) verify(contentResolver, times(1)).loadThumbnail(uriOne, imageSize, null) assertThat(acquireCount.get()).isEqualTo(1) @@ -281,7 +283,7 @@ class ImagePreviewImageLoaderTest { cacheSize = 1, testSemaphore, ) - launch(start = UNDISPATCHED) { testSubject(uriOne, false) } + launch(start = UNDISPATCHED) { testSubject(uriOne, previewSize, false) } verify(contentResolver, never()).loadThumbnail(any(), any(), anyOrNull()) @@ -324,7 +326,9 @@ class ImagePreviewImageLoaderTest { ) coroutineScope { repeat(requestCount) { - launch { testSubject(Uri.parse("content://org.pkg.app/image-$it.png")) } + launch { + testSubject(Uri.parse("content://org.pkg.app/image-$it.png"), previewSize) + } } yield() // wait for all requests to be dispatched diff --git a/tests/unit/src/com/android/intentresolver/contentpreview/payloadtoggle/ui/viewmodel/ShareouselViewModelTest.kt b/tests/unit/src/com/android/intentresolver/contentpreview/payloadtoggle/ui/viewmodel/ShareouselViewModelTest.kt index bb67e084..1047d145 100644 --- a/tests/unit/src/com/android/intentresolver/contentpreview/payloadtoggle/ui/viewmodel/ShareouselViewModelTest.kt +++ b/tests/unit/src/com/android/intentresolver/contentpreview/payloadtoggle/ui/viewmodel/ShareouselViewModelTest.kt @@ -76,23 +76,25 @@ class ShareouselViewModelTest { scope = viewModelScope, ) } + private val previewHeight = 500 @Test fun headline_images() = runTest { assertThat(shareouselViewModel.headline.first()).isEqualTo("FILES: 1") previewSelectionsRepository.selections.value = listOf( - PreviewModel( - uri = Uri.fromParts("scheme", "ssp", "fragment"), - mimeType = "image/png", - order = 0, - ), - PreviewModel( - uri = Uri.fromParts("scheme1", "ssp1", "fragment1"), - mimeType = "image/jpeg", - order = 1, + PreviewModel( + uri = Uri.fromParts("scheme", "ssp", "fragment"), + mimeType = "image/png", + order = 0, + ), + PreviewModel( + uri = Uri.fromParts("scheme1", "ssp1", "fragment1"), + mimeType = "image/jpeg", + order = 1, + ) ) - ).associateBy { it.uri } + .associateBy { it.uri } runCurrent() assertThat(shareouselViewModel.headline.first()).isEqualTo("IMAGES: 2") } @@ -101,17 +103,18 @@ class ShareouselViewModelTest { fun headline_videos() = runTest { previewSelectionsRepository.selections.value = listOf( - PreviewModel( - uri = Uri.fromParts("scheme", "ssp", "fragment"), - mimeType = "video/mpeg", - order = 0, - ), - PreviewModel( - uri = Uri.fromParts("scheme1", "ssp1", "fragment1"), - mimeType = "video/mpeg", - order = 1, + PreviewModel( + uri = Uri.fromParts("scheme", "ssp", "fragment"), + mimeType = "video/mpeg", + order = 0, + ), + PreviewModel( + uri = Uri.fromParts("scheme1", "ssp1", "fragment1"), + mimeType = "video/mpeg", + order = 1, + ) ) - ).associateBy { it.uri } + .associateBy { it.uri } runCurrent() assertThat(shareouselViewModel.headline.first()).isEqualTo("VIDEOS: 2") } @@ -120,17 +123,18 @@ class ShareouselViewModelTest { fun headline_mixed() = runTest { previewSelectionsRepository.selections.value = listOf( - PreviewModel( - uri = Uri.fromParts("scheme", "ssp", "fragment"), - mimeType = "image/jpeg", - order = 0, - ), - PreviewModel( - uri = Uri.fromParts("scheme1", "ssp1", "fragment1"), - mimeType = "video/mpeg", - order = 1, + PreviewModel( + uri = Uri.fromParts("scheme", "ssp", "fragment"), + mimeType = "image/jpeg", + order = 0, + ), + PreviewModel( + uri = Uri.fromParts("scheme1", "ssp1", "fragment1"), + mimeType = "video/mpeg", + order = 1, + ) ) - ).associateBy { it.uri } + .associateBy { it.uri } runCurrent() assertThat(shareouselViewModel.headline.first()).isEqualTo("FILES: 2") } @@ -194,6 +198,7 @@ class ShareouselViewModelTest { mimeType = "video/mpeg", order = 0, ), + previewHeight, /* index = */ 1, viewModelScope, ) @@ -245,6 +250,7 @@ class ShareouselViewModelTest { mimeType = "video/mpeg", order = 1, ), + previewHeight, /* index = */ 1, viewModelScope, ) @@ -308,10 +314,11 @@ class ShareouselViewModelTest { this.targetIntentModifier = targetIntentModifier previewSelectionsRepository.selections.value = PreviewModel( - uri = Uri.fromParts("scheme", "ssp", "fragment"), - mimeType = null, - order = 0, - ).let { mapOf(it.uri to it) } + uri = Uri.fromParts("scheme", "ssp", "fragment"), + mimeType = null, + order = 0, + ) + .let { mapOf(it.uri to it) } payloadToggleImageLoader = FakeImageLoader( initialBitmaps = diff --git a/tests/unit/src/com/android/intentresolver/widget/BatchPreviewLoaderTest.kt b/tests/unit/src/com/android/intentresolver/widget/BatchPreviewLoaderTest.kt index 4f4223c0..b1e8593d 100644 --- a/tests/unit/src/com/android/intentresolver/widget/BatchPreviewLoaderTest.kt +++ b/tests/unit/src/com/android/intentresolver/widget/BatchPreviewLoaderTest.kt @@ -18,6 +18,7 @@ package com.android.intentresolver.widget import android.graphics.Bitmap import android.net.Uri +import android.util.Size import com.android.intentresolver.captureMany import com.android.intentresolver.mock import com.android.intentresolver.widget.ScrollableImagePreviewView.BatchPreviewLoader @@ -49,6 +50,7 @@ class BatchPreviewLoaderTest { private val testScope = CoroutineScope(dispatcher) private val onCompletion = mock<() -> Unit>() private val onUpdate = mock<(List<Preview>) -> Unit>() + private val previewSize = Size(500, 500) @Before fun setup() { @@ -71,6 +73,7 @@ class BatchPreviewLoaderTest { BatchPreviewLoader( imageLoader, previews(uriOne, uriTwo), + previewSize, totalItemCount = 2, onUpdate, onCompletion @@ -94,6 +97,7 @@ class BatchPreviewLoaderTest { BatchPreviewLoader( imageLoader, previews(uriOne, uriTwo, uriThree), + previewSize, totalItemCount = 3, onUpdate, onCompletion @@ -122,7 +126,14 @@ class BatchPreviewLoaderTest { } imageLoader.setUriLoadingOrder(*loadingOrder) val testSubject = - BatchPreviewLoader(imageLoader, previews(*uris), uris.size, onUpdate, onCompletion) + BatchPreviewLoader( + imageLoader, + previews(*uris), + previewSize, + uris.size, + onUpdate, + onCompletion + ) testSubject.loadAspectRatios(200) { _, _, _ -> 100 } dispatcher.scheduler.advanceUntilIdle() @@ -151,7 +162,14 @@ class BatchPreviewLoaderTest { val expectedUris = Array(uris.size / 2) { createUri(it * 2 + 1) } imageLoader.setUriLoadingOrder(*loadingOrder) val testSubject = - BatchPreviewLoader(imageLoader, previews(*uris), uris.size, onUpdate, onCompletion) + BatchPreviewLoader( + imageLoader, + previews(*uris), + previewSize, + uris.size, + onUpdate, + onCompletion + ) testSubject.loadAspectRatios(200) { _, _, _ -> 100 } dispatcher.scheduler.advanceUntilIdle() @@ -166,7 +184,9 @@ class BatchPreviewLoaderTest { private fun createUri(idx: Int): Uri = Uri.parse("content://org.pkg.app/image-$idx.png") private fun fail(uri: Uri) = uri to false + private fun succeed(uri: Uri) = uri to true + private fun previews(vararg uris: Uri) = uris .fold(ArrayList<Preview>(uris.size)) { acc, uri -> @@ -175,7 +195,7 @@ class BatchPreviewLoaderTest { .asFlow() } -private class TestImageLoader(scope: CoroutineScope) : suspend (Uri, Boolean) -> Bitmap? { +private class TestImageLoader(scope: CoroutineScope) : suspend (Uri, Size, Boolean) -> Bitmap? { private val loadingOrder = ArrayDeque<Pair<Uri, Boolean>>() private val pendingRequests = LinkedHashMap<Uri, CompletableDeferred<Bitmap?>>() private val flow = MutableSharedFlow<Unit>(replay = 1) @@ -203,7 +223,7 @@ private class TestImageLoader(scope: CoroutineScope) : suspend (Uri, Boolean) -> loadingOrder.addAll(uris) } - override suspend fun invoke(uri: Uri, cache: Boolean): Bitmap? { + override suspend fun invoke(uri: Uri, size: Size, cache: Boolean): Bitmap? { val deferred = pendingRequests.getOrPut(uri) { CompletableDeferred() } flow.tryEmit(Unit) return deferred.await() |