summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
author Andrey Epin <ayepin@google.com> 2024-06-25 12:48:32 -0700
committer Andrey Yepin <ayepin@google.com> 2024-07-15 13:33:23 -0700
commite48daa217dc397cce855a5357ee11a87b0c7bce4 (patch)
treeaea79b4a9f640e60f4422bb996ca7ec125310a46
parent1936da00dab547490ac4f0a349063fee3591d9d2 (diff)
Update ImageLoader interface to receive preview sizes along with the URI
The UI calculates and provides view sizes but they are not used by the existing ImageLoader implementations thus no functional change is expected. Bug: 348665058 Test: atest IntentResolver-tests-unit Test: atest IntentResolver-tests-activity Test: inject debug logging and verify preview values being requested for app preview types Flag: EXEMPT refactor Change-Id: I0e282f773c424b6fe81587a71e1b8630452ac63c
-rw-r--r--java/src/com/android/intentresolver/contentpreview/CachingImagePreviewImageLoader.kt7
-rw-r--r--java/src/com/android/intentresolver/contentpreview/FilesPlusTextContentPreviewUi.java5
-rw-r--r--java/src/com/android/intentresolver/contentpreview/ImageLoader.kt14
-rw-r--r--java/src/com/android/intentresolver/contentpreview/ImagePreviewImageLoader.kt7
-rw-r--r--java/src/com/android/intentresolver/contentpreview/TextContentPreviewUi.java9
-rw-r--r--java/src/com/android/intentresolver/contentpreview/UnifiedContentPreviewUi.java17
-rw-r--r--java/src/com/android/intentresolver/contentpreview/payloadtoggle/ui/composable/ShareouselComposable.kt13
-rw-r--r--java/src/com/android/intentresolver/contentpreview/payloadtoggle/ui/viewmodel/ShareouselViewModel.kt21
-rw-r--r--java/src/com/android/intentresolver/widget/ScrollableImagePreviewView.kt42
-rw-r--r--tests/shared/src/com/android/intentresolver/FakeImageLoader.kt12
-rw-r--r--tests/unit/src/com/android/intentresolver/contentpreview/CachingImagePreviewImageLoaderTest.kt36
-rw-r--r--tests/unit/src/com/android/intentresolver/contentpreview/ImagePreviewImageLoaderTest.kt48
-rw-r--r--tests/unit/src/com/android/intentresolver/contentpreview/payloadtoggle/ui/viewmodel/ShareouselViewModelTest.kt75
-rw-r--r--tests/unit/src/com/android/intentresolver/widget/BatchPreviewLoaderTest.kt28
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()