diff options
Diffstat (limited to 'java/src')
7 files changed, 239 insertions, 169 deletions
diff --git a/java/src/com/android/intentresolver/contentpreview/ChooserContentPreviewUi.java b/java/src/com/android/intentresolver/contentpreview/ChooserContentPreviewUi.java index d4874cac..d279f11f 100644 --- a/java/src/com/android/intentresolver/contentpreview/ChooserContentPreviewUi.java +++ b/java/src/com/android/intentresolver/contentpreview/ChooserContentPreviewUi.java @@ -16,6 +16,8 @@ package com.android.intentresolver.contentpreview; +import static androidx.lifecycle.LifecycleKt.getCoroutineScope; + import static com.android.intentresolver.contentpreview.ContentPreviewType.CONTENT_PREVIEW_FILE; import static com.android.intentresolver.contentpreview.ContentPreviewType.CONTENT_PREVIEW_IMAGE; import static com.android.intentresolver.contentpreview.ContentPreviewType.CONTENT_PREVIEW_TEXT; @@ -156,23 +158,25 @@ public final class ChooserContentPreviewUi { typeClassifier, headlineGenerator); if (previewData.getUriCount() > 0) { - previewData.getFileMetadataForImagePreview( - mLifecycle, previewUi::updatePreviewMetadata); + JavaFlowHelper.collectToList( + getCoroutineScope(mLifecycle), + previewData.getImagePreviewFileInfoFlow(), + previewUi::updatePreviewMetadata); } return previewUi; } - UnifiedContentPreviewUi unifiedContentPreviewUi = new UnifiedContentPreviewUi( + return new UnifiedContentPreviewUi( + getCoroutineScope(mLifecycle), isSingleImageShare, targetIntent.getType(), actionFactory, imageLoader, typeClassifier, transitionElementStatusCallback, + previewData.getImagePreviewFileInfoFlow(), previewData.getUriCount(), headlineGenerator); - previewData.getFileMetadataForImagePreview(mLifecycle, unifiedContentPreviewUi::setFiles); - return unifiedContentPreviewUi; } public int getPreferredContentPreview() { diff --git a/java/src/com/android/intentresolver/contentpreview/ContentPreviewUi.java b/java/src/com/android/intentresolver/contentpreview/ContentPreviewUi.java index 07071236..2d81794e 100644 --- a/java/src/com/android/intentresolver/contentpreview/ContentPreviewUi.java +++ b/java/src/com/android/intentresolver/contentpreview/ContentPreviewUi.java @@ -85,7 +85,7 @@ abstract class ContentPreviewUi { } } - protected static ScrollableImagePreviewView.PreviewType getPreviewType( + static ScrollableImagePreviewView.PreviewType getPreviewType( MimeTypeClassifier typeClassifier, String mimeType) { if (mimeType == null) { return ScrollableImagePreviewView.PreviewType.File; diff --git a/java/src/com/android/intentresolver/contentpreview/JavaFlowHelper.kt b/java/src/com/android/intentresolver/contentpreview/JavaFlowHelper.kt new file mode 100644 index 00000000..b29c5774 --- /dev/null +++ b/java/src/com/android/intentresolver/contentpreview/JavaFlowHelper.kt @@ -0,0 +1,51 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +@file:JvmName("JavaFlowHelper") + +package com.android.intentresolver.contentpreview + +import com.android.intentresolver.widget.ScrollableImagePreviewView.Preview +import java.util.function.Consumer +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.filter +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.toList +import kotlinx.coroutines.launch + +internal fun mapFileIntoToPreview( + flow: Flow<FileInfo>, + typeClassifier: MimeTypeClassifier, + editAction: Runnable? +): Flow<Preview> = + flow + .filter { it.previewUri != null } + .map { fileInfo -> + Preview( + ContentPreviewUi.getPreviewType(typeClassifier, fileInfo.mimeType), + requireNotNull(fileInfo.previewUri), + editAction + ) + } + +internal fun <T> collectToList( + clientScope: CoroutineScope, + flow: Flow<T>, + callback: Consumer<List<T>> +) { + clientScope.launch { callback.accept(flow.toList()) } +} diff --git a/java/src/com/android/intentresolver/contentpreview/PreviewDataProvider.kt b/java/src/com/android/intentresolver/contentpreview/PreviewDataProvider.kt index 8ab3a272..fd5ce3f8 100644 --- a/java/src/com/android/intentresolver/contentpreview/PreviewDataProvider.kt +++ b/java/src/com/android/intentresolver/contentpreview/PreviewDataProvider.kt @@ -38,14 +38,18 @@ import com.android.intentresolver.measurements.runTracing import com.android.intentresolver.util.ownedByCurrentUser import java.util.concurrent.atomic.AtomicInteger import java.util.function.Consumer +import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CompletableDeferred -import kotlinx.coroutines.CoroutineDispatcher -import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.async import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.SharedFlow +import kotlinx.coroutines.flow.take import kotlinx.coroutines.isActive import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking -import kotlinx.coroutines.withContext import kotlinx.coroutines.withTimeoutOrNull /** @@ -69,30 +73,45 @@ private const val TIMEOUT_MS = 1_000L @OpenForTesting open class PreviewDataProvider @VisibleForTesting +@JvmOverloads constructor( + private val scope: CoroutineScope, private val targetIntent: Intent, private val contentResolver: ContentInterface, - private val typeClassifier: MimeTypeClassifier, - private val dispatcher: CoroutineDispatcher, + private val typeClassifier: MimeTypeClassifier = DefaultMimeTypeClassifier, ) { - constructor( - targetIntent: Intent, - contentResolver: ContentInterface, - ) : this( - targetIntent, - contentResolver, - DefaultMimeTypeClassifier, - Dispatchers.IO, - ) private val records = targetIntent.contentUris.map { UriRecord(it) } + private val fileInfoSharedFlow: SharedFlow<FileInfo> by lazy { + // Alternatively, we could just use [shareIn()] on a [flow] -- and it would be, arguably, + // cleaner -- but we'd lost the ability to trace the traverse as [runTracing] does not + // generally work over suspend function invocations. + MutableSharedFlow<FileInfo>(replay = records.size).apply { + scope.launch { + runTracing("image-preview-metadata") { + for (record in records) { + tryEmit(FileInfo.Builder(record.uri).readFromRecord(record).build()) + } + } + } + } + } + /** returns number of shared URIs, see [Intent.EXTRA_STREAM] */ @get:OpenForTesting open val uriCount: Int get() = records.size /** + * Returns a [Flow] of [FileInfo], for each shared URI in order, with [FileInfo.mimeType] and + * [FileInfo.previewUri] set (a data projection tailored for the image preview UI). + */ + @get:OpenForTesting + open val imagePreviewFileInfoFlow: Flow<FileInfo> + get() = fileInfoSharedFlow.take(records.size) + + /** * Preview type to use. The type is determined asynchronously with a timeout; the fall-back * values is [ContentPreviewType.CONTENT_PREVIEW_FILE] */ @@ -107,10 +126,17 @@ constructor( if (!targetIntent.isSend || records.isEmpty()) { CONTENT_PREVIEW_TEXT } else { - runBlocking(dispatcher) { - withTimeoutOrNull(TIMEOUT_MS) { - loadPreviewType() - } ?: CONTENT_PREVIEW_FILE + try { + runBlocking(scope.coroutineContext) { + withTimeoutOrNull(TIMEOUT_MS) { loadPreviewType() } ?: CONTENT_PREVIEW_FILE + } + } catch (e: CancellationException) { + Log.w( + ContentPreviewUi.TAG, + "An attempt to read preview type from a cancelled scope", + e + ) + CONTENT_PREVIEW_FILE } } } @@ -123,46 +149,22 @@ constructor( open val firstFileInfo: FileInfo? by lazy { runTracing("first-uri-metadata") { records.firstOrNull()?.let { record -> - runBlocking(dispatcher) { - val builder = FileInfo.Builder(record.uri) - withTimeoutOrNull(TIMEOUT_MS) { - builder.readFromRecord(record) + val builder = FileInfo.Builder(record.uri) + try { + runBlocking(scope.coroutineContext) { + withTimeoutOrNull(TIMEOUT_MS) { builder.readFromRecord(record) } } - builder.build() - } - } - } - } - - /** - * Returns a collection of [FileInfo], for each shared URI in order, with [FileInfo.mimeType] - * and [FileInfo.previewUri] set (a data projection tailored for the image preview UI). - */ - @OpenForTesting - open fun getFileMetadataForImagePreview( - callerLifecycle: Lifecycle, - callback: Consumer<List<FileInfo>>, - ) { - callerLifecycle.coroutineScope.launch { - val result = withContext(dispatcher) { - getFileMetadataForImagePreview() - } - callback.accept(result) - } - } - - private fun getFileMetadataForImagePreview(): List<FileInfo> = - runTracing("image-preview-metadata") { - ArrayList<FileInfo>(records.size).also { result -> - for (record in records) { - result.add( - FileInfo.Builder(record.uri) - .readFromRecord(record) - .build() + } catch (e: CancellationException) { + Log.w( + ContentPreviewUi.TAG, + "An attempt to read first file info from a cancelled scope", + e ) } + builder.build() } } + } private fun FileInfo.Builder.readFromRecord(record: UriRecord): FileInfo.Builder { withMimeType(record.mimeType) @@ -186,9 +188,7 @@ constructor( throw IndexOutOfBoundsException("There are no shared URIs") } callerLifecycle.coroutineScope.launch { - val result = withContext(dispatcher) { - getFirstFileName() - } + val result = scope.async { getFirstFileName() }.await() callback.accept(result) } } @@ -237,8 +237,7 @@ constructor( } resultDeferred.complete(CONTENT_PREVIEW_FILE) } - resultDeferred.await() - .also { job.cancel() } + resultDeferred.await().also { job.cancel() } } } @@ -251,8 +250,8 @@ constructor( val isImageType: Boolean get() = typeClassifier.isImageType(mimeType) val supportsImageType: Boolean by lazy { - contentResolver.getStreamTypesSafe(uri) - ?.firstOrNull(typeClassifier::isImageType) != null + contentResolver.getStreamTypesSafe(uri)?.firstOrNull(typeClassifier::isImageType) != + null } val supportsThumbnail: Boolean get() = query.supportsThumbnail @@ -264,9 +263,8 @@ constructor( private val query by lazy { readQueryResult() } private fun readQueryResult(): QueryResult { - val cursor = contentResolver.querySafe(uri) - ?.takeIf { it.moveToFirst() } - ?: return QueryResult() + val cursor = + contentResolver.querySafe(uri)?.takeIf { it.moveToFirst() } ?: return QueryResult() var flagColIdx = -1 var displayIconUriColIdx = -1 diff --git a/java/src/com/android/intentresolver/contentpreview/PreviewViewModel.kt b/java/src/com/android/intentresolver/contentpreview/PreviewViewModel.kt index 331b0cb6..6013f5a0 100644 --- a/java/src/com/android/intentresolver/contentpreview/PreviewViewModel.kt +++ b/java/src/com/android/intentresolver/contentpreview/PreviewViewModel.kt @@ -25,11 +25,15 @@ import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewmodel.CreationExtras import com.android.intentresolver.ChooserRequestParameters import com.android.intentresolver.R +import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.plus /** A trivial view model to keep a [PreviewDataProvider] instance over a configuration change */ -class PreviewViewModel(private val application: Application) : BasePreviewViewModel() { +class PreviewViewModel( + private val application: Application, + private val dispatcher: CoroutineDispatcher = Dispatchers.IO, +) : BasePreviewViewModel() { private var previewDataProvider: PreviewDataProvider? = null private var imageLoader: ImagePreviewImageLoader? = null @@ -38,15 +42,18 @@ class PreviewViewModel(private val application: Application) : BasePreviewViewMo chooserRequest: ChooserRequestParameters ): PreviewDataProvider = previewDataProvider - ?: PreviewDataProvider(chooserRequest.targetIntent, application.contentResolver).also { - previewDataProvider = it - } + ?: PreviewDataProvider( + viewModelScope + dispatcher, + chooserRequest.targetIntent, + application.contentResolver + ) + .also { previewDataProvider = it } @MainThread override fun createOrReuseImageLoader(): ImageLoader = imageLoader ?: ImagePreviewImageLoader( - viewModelScope + Dispatchers.IO, + viewModelScope + dispatcher, thumbnailSize = application.resources.getDimensionPixelSize( R.dimen.chooser_preview_image_max_dimen diff --git a/java/src/com/android/intentresolver/contentpreview/UnifiedContentPreviewUi.java b/java/src/com/android/intentresolver/contentpreview/UnifiedContentPreviewUi.java index 5db5020e..8e635aba 100644 --- a/java/src/com/android/intentresolver/contentpreview/UnifiedContentPreviewUi.java +++ b/java/src/com/android/intentresolver/contentpreview/UnifiedContentPreviewUi.java @@ -31,10 +31,12 @@ import com.android.intentresolver.widget.ActionRow; import com.android.intentresolver.widget.ImagePreviewView.TransitionElementStatusCallback; import com.android.intentresolver.widget.ScrollableImagePreviewView; -import java.util.ArrayList; import java.util.List; import java.util.Objects; +import kotlinx.coroutines.CoroutineScope; +import kotlinx.coroutines.flow.Flow; + class UnifiedContentPreviewUi extends ContentPreviewUi { private final boolean mShowEditAction; @Nullable @@ -44,6 +46,7 @@ class UnifiedContentPreviewUi extends ContentPreviewUi { private final MimeTypeClassifier mTypeClassifier; private final TransitionElementStatusCallback mTransitionElementStatusCallback; private final HeadlineGenerator mHeadlineGenerator; + private final Flow<FileInfo> mFileInfoFlow; private final int mItemCount; @Nullable private List<FileInfo> mFiles; @@ -51,12 +54,14 @@ class UnifiedContentPreviewUi extends ContentPreviewUi { private ViewGroup mContentPreviewView; UnifiedContentPreviewUi( + CoroutineScope scope, boolean isSingleImage, @Nullable String intentMimeType, ChooserContentPreviewUi.ActionFactory actionFactory, ImageLoader imageLoader, MimeTypeClassifier typeClassifier, TransitionElementStatusCallback transitionElementStatusCallback, + Flow<FileInfo> fileInfoFlow, int itemCount, HeadlineGenerator headlineGenerator) { mShowEditAction = isSingleImage; @@ -65,8 +70,11 @@ class UnifiedContentPreviewUi extends ContentPreviewUi { mImageLoader = imageLoader; mTypeClassifier = typeClassifier; mTransitionElementStatusCallback = transitionElementStatusCallback; + mFileInfoFlow = fileInfoFlow; mItemCount = itemCount; mHeadlineGenerator = headlineGenerator; + + JavaFlowHelper.collectToList(scope, fileInfoFlow, this::setFiles); } @Override @@ -81,7 +89,7 @@ class UnifiedContentPreviewUi extends ContentPreviewUi { return layout; } - public void setFiles(List<FileInfo> files) { + private void setFiles(List<FileInfo> files) { mImageLoader.prePopulate(files.stream() .map(FileInfo::getPreviewUri) .filter(Objects::nonNull) @@ -106,6 +114,12 @@ class UnifiedContentPreviewUi extends ContentPreviewUi { imagePreview.setImageLoader(mImageLoader); imagePreview.setOnNoPreviewCallback(() -> imagePreview.setVisibility(View.GONE)); imagePreview.setTransitionElementStatusCallback(mTransitionElementStatusCallback); + imagePreview.setPreviews( + JavaFlowHelper.mapFileIntoToPreview( + mFileInfoFlow, + mTypeClassifier, + mShowEditAction ? mActionFactory.getEditButtonRunnable() : null), + mItemCount); if (mFiles != null) { updatePreviewWithFiles(mContentPreviewView, mFiles); @@ -135,7 +149,6 @@ class UnifiedContentPreviewUi extends ContentPreviewUi { return; } - List<ScrollableImagePreviewView.Preview> previews = new ArrayList<>(); boolean allImages = true; boolean allVideos = true; for (FileInfo fileInfo : files) { @@ -143,17 +156,8 @@ class UnifiedContentPreviewUi extends ContentPreviewUi { getPreviewType(mTypeClassifier, fileInfo.getMimeType()); allImages = allImages && previewType == ScrollableImagePreviewView.PreviewType.Image; allVideos = allVideos && previewType == ScrollableImagePreviewView.PreviewType.Video; - - if (fileInfo.getPreviewUri() != null) { - Runnable editAction = - mShowEditAction ? mActionFactory.getEditButtonRunnable() : null; - previews.add( - new ScrollableImagePreviewView.Preview( - previewType, fileInfo.getPreviewUri(), editAction)); - } } - imagePreview.setPreviews(previews, count - previews.size()); displayHeadline(contentPreviewView, count, allImages, allVideos); } diff --git a/java/src/com/android/intentresolver/widget/ScrollableImagePreviewView.kt b/java/src/com/android/intentresolver/widget/ScrollableImagePreviewView.kt index d9844d7b..3bbafc40 100644 --- a/java/src/com/android/intentresolver/widget/ScrollableImagePreviewView.kt +++ b/java/src/com/android/intentresolver/widget/ScrollableImagePreviewView.kt @@ -39,14 +39,12 @@ import com.android.intentresolver.widget.ImagePreviewView.TransitionElementStatu import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job -import kotlinx.coroutines.MainScope import kotlinx.coroutines.cancel +import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableSharedFlow -import kotlinx.coroutines.flow.onCompletion import kotlinx.coroutines.flow.takeWhile import kotlinx.coroutines.joinAll import kotlinx.coroutines.launch -import kotlinx.coroutines.plus private const val TRANSITION_NAME = "screenshot_preview_image" private const val PLURALS_COUNT = "count" @@ -127,7 +125,7 @@ class ScrollableImagePreviewView : RecyclerView, ImagePreviewView { isMeasured = true updateMaxWidthHint(widthSpec) updateMaxAspectRatio() - batchLoader?.loadAspectRatios(getMaxWidth(), this::updatePreviewSize) + maybeLoadAspectRatios() } } @@ -145,6 +143,17 @@ class ScrollableImagePreviewView : RecyclerView, ImagePreviewView { ) } + override fun onAttachedToWindow() { + super.onAttachedToWindow() + batchLoader?.totalItemCount?.let(previewAdapter::reset) + maybeLoadAspectRatios() + } + + override fun onDetachedFromWindow() { + batchLoader?.cancel() + super.onDetachedFromWindow() + } + override fun setTransitionElementStatusCallback(callback: TransitionElementStatusCallback?) { previewAdapter.transitionStatusElementCallback = callback } @@ -166,30 +175,30 @@ class ScrollableImagePreviewView : RecyclerView, ImagePreviewView { previewAdapter.reset(totalItemCount) } - fun setPreviews(previews: List<Preview>, otherItemCount: Int) { - previewAdapter.reset(previews.size + otherItemCount) + fun setPreviews(previews: Flow<Preview>, totalItemCount: Int) { + previewAdapter.reset(totalItemCount) batchLoader?.cancel() batchLoader = BatchPreviewLoader( - previewAdapter.imageLoader ?: error("Image loader is not set"), - previews, - otherItemCount, - onUpdate = previewAdapter::addPreviews, - onCompletion = { - if (!previewAdapter.hasPreviews) { - onNoPreviewCallback?.run() - } - previewAdapter.markLoaded() - } - ) - .apply { - if (isMeasured) { - loadAspectRatios( - getMaxWidth(), - this@ScrollableImagePreviewView::updatePreviewSize - ) + previewAdapter.imageLoader ?: error("Image loader is not set"), + previews, + totalItemCount, + onUpdate = previewAdapter::addPreviews, + onCompletion = { + batchLoader = null + if (!previewAdapter.hasPreviews) { + onNoPreviewCallback?.run() } + previewAdapter.markLoaded() } + ) + maybeLoadAspectRatios() + } + + private fun maybeLoadAspectRatios() { + if (isMeasured && isAttachedToWindow()) { + batchLoader?.let { it.loadAspectRatios(getMaxWidth(), this::updatePreviewSize) } + } } var onNoPreviewCallback: Runnable? = null @@ -320,6 +329,7 @@ class ScrollableImagePreviewView : RecyclerView, ImagePreviewView { !hadOtherItem && hasOtherItem -> { notifyItemInserted(previews.size) } + else -> notifyItemChanged(previews.size) } } } @@ -464,7 +474,7 @@ class ScrollableImagePreviewView : RecyclerView, ImagePreviewView { } private fun resetScope(): CoroutineScope = - (MainScope() + Dispatchers.Main.immediate).also { + CoroutineScope(Dispatchers.Main.immediate).also { scope?.cancel() scope = it } @@ -514,26 +524,22 @@ class ScrollableImagePreviewView : RecyclerView, ImagePreviewView { @VisibleForTesting class BatchPreviewLoader( private val imageLoader: CachingImageLoader, - previews: List<Preview>, - otherItemCount: Int, + private val previews: Flow<Preview>, + val totalItemCount: Int, private val onUpdate: (List<Preview>) -> Unit, private val onCompletion: () -> Unit, ) { - private val previews: List<Preview> = - if (previews is RandomAccess) previews else ArrayList(previews) - private val totalItemCount = previews.size + otherItemCount - private var scope: CoroutineScope? = MainScope() + Dispatchers.Main.immediate + private var scope: CoroutineScope = createScope() + + private fun createScope() = CoroutineScope(Dispatchers.Main.immediate) fun cancel() { - scope?.cancel() - scope = null + scope.cancel() + scope = createScope() } fun loadAspectRatios(maxWidth: Int, previewSizeUpdater: (Preview, Int, Int) -> Int) { - val scope = this.scope ?: return - // -1 encodes that the preview has not been processed, - // 0 means failed, > 0 is a preview width - val previewWidths = IntArray(previews.size) { -1 } + val previewInfos = ArrayList<PreviewWidthInfo>(totalItemCount) var blockStart = 0 // inclusive var blockEnd = 0 // exclusive @@ -542,23 +548,16 @@ class ScrollableImagePreviewView : RecyclerView, ImagePreviewView { val updateEvent = Any() val completedEvent = Any() - // throttle adapter updates using flow; the flow first emits when enough preview - // elements is loaded to fill the viewport and then each time a subsequent block of - // previews is loaded + // collects updates from [reportFlow] throttling adapter updates; scope.launch(Dispatchers.Main) { reportFlow .takeWhile { it !== completedEvent } .throttle(ADAPTER_UPDATE_INTERVAL_MS) - .onCompletion { cause -> - if (cause == null) { - onCompletion() - } - } .collect { val updates = ArrayList<Preview>(blockEnd - blockStart) while (blockStart < blockEnd) { - if (previewWidths[blockStart] > 0) { - updates.add(previews[blockStart]) + if (previewInfos[blockStart].width > 0) { + updates.add(previewInfos[blockStart].preview) } blockStart++ } @@ -566,57 +565,64 @@ class ScrollableImagePreviewView : RecyclerView, ImagePreviewView { onUpdate(updates) } } + onCompletion() } + // Collects [previews] flow and loads aspect ratios, emits updates into [reportFlow] + // when a next sequential block of preview aspect ratios is loaded: initially emits when + // enough preview elements is loaded to fill the viewport. scope.launch { var blockWidth = 0 var isFirstBlock = true - var nextIdx = 0 - List<Job>(4) { - launch { - while (true) { - val i = nextIdx++ - if (i >= previews.size) break - val preview = previews[i] - - previewWidths[i] = - runCatching { - // TODO: decide on adding a timeout - imageLoader(preview.uri, isFirstBlock)?.let { bitmap -> - previewSizeUpdater( - preview, - bitmap.width, - bitmap.height - ) - } - ?: 0 - } - .getOrDefault(0) - - if (blockEnd != i) continue - while ( - blockEnd < previewWidths.size && previewWidths[blockEnd] >= 0 - ) { - blockWidth += previewWidths[blockEnd] - blockEnd++ - } - if (isFirstBlock) { - if (blockWidth >= maxWidth) { - isFirstBlock = false - // notify that the preview now can be displayed - reportFlow.emit(updateEvent) + + val jobs = ArrayList<Job>() + previews.collect { preview -> + val i = previewInfos.size + val pair = PreviewWidthInfo(preview) + previewInfos.add(pair) + + val job = launch { + pair.width = + runCatching { + // TODO: decide on adding a timeout. The worst case I can + // 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 -> + previewSizeUpdater(preview, bitmap.width, bitmap.height) } - } else { - reportFlow.emit(updateEvent) + ?: 0 } + .getOrDefault(0) + + if (i == blockEnd) { + while ( + blockEnd < previewInfos.size && previewInfos[blockEnd].width >= 0 + ) { + blockWidth += previewInfos[blockEnd].width + blockEnd++ + } + if (isFirstBlock && blockWidth >= maxWidth) { + isFirstBlock = false + } + if (!isFirstBlock) { + reportFlow.emit(updateEvent) } } } - .joinAll() + jobs.add(job) + } + jobs.joinAll() // in case all previews have failed to load reportFlow.emit(updateEvent) reportFlow.emit(completedEvent) } } } + + private class PreviewWidthInfo(val preview: Preview) { + // -1 encodes that the preview has not been processed, + // 0 means failed, > 0 is a preview width + var width: Int = -1 + } } |