diff options
Diffstat (limited to 'java/src')
9 files changed, 100 insertions, 35 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) |