Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
## Fixed
- Flow no longer crashes at launch in Android's Private Space (#1357)
- Fixed a crash while Flow loads covers and thumbnails for files on your phone (#1353)
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package io.github.aedev.flow.data.localmedia

import coil3.ImageLoader
import coil3.decode.DecodeResult
import coil3.decode.Decoder
import coil3.fetch.SourceFetchResult
import coil3.request.Options
import io.github.aedev.flow.utils.PerformanceDispatcher
import kotlinx.coroutines.withContext

/** Runs a `MediaMetadataRetriever`-backed Coil decoder on [PerformanceDispatcher.mediaRetriever]. */
class MediaRetrieverDecoderFactory(
private val delegate: Decoder.Factory,
) : Decoder.Factory {
override fun create(
result: SourceFetchResult,
options: Options,
imageLoader: ImageLoader,
): Decoder? {
val decoder = delegate.create(result, options, imageLoader) ?: return null
return object : Decoder {
override suspend fun decode(): DecodeResult? = withContext(PerformanceDispatcher.mediaRetriever) { decoder.decode() }
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import coil3.fetch.FetchResult
import coil3.fetch.Fetcher
import coil3.fetch.ImageFetchResult
import coil3.request.Options
import kotlinx.coroutines.Dispatchers
import io.github.aedev.flow.utils.PerformanceDispatcher
import kotlinx.coroutines.withContext
import java.io.IOException

Expand All @@ -24,7 +24,7 @@ class MediaStoreThumbnailFetcher(
private val uri: android.net.Uri,
) : Fetcher {
override suspend fun fetch(): FetchResult =
withContext(Dispatchers.IO) {
withContext(PerformanceDispatcher.mediaRetriever) {
val bitmap = MediaStoreThumbnails.load(context, uri, ARTWORK_SIZE_PX) ?: throw IOException("No artwork for $uri")
ImageFetchResult(image = bitmap.asImage(), isSampled = true, dataSource = DataSource.DISK)
}
Expand Down
3 changes: 2 additions & 1 deletion app/src/main/java/io/github/aedev/flow/di/AppModule.kt
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
import io.github.aedev.flow.BuildConfig
import io.github.aedev.flow.data.localmedia.MediaRetrieverDecoderFactory
import io.github.aedev.flow.data.localmedia.MediaStoreThumbnailFetcher
import io.github.aedev.flow.data.recommendation.FlowNeuroEngine
import io.github.aedev.flow.innertube.YouTube
Expand Down Expand Up @@ -51,7 +52,7 @@ object AppModule {
.Builder(context)
.components {
add(OkHttpNetworkFetcherFactory(callFactory = { okHttpClient }))
add(VideoFrameDecoder.Factory())
add(MediaRetrieverDecoderFactory(VideoFrameDecoder.Factory()))
add(MediaStoreThumbnailFetcher.Factory(context))
}.memoryCache {
MemoryCache
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,14 @@ object PerformanceDispatcher {
*/
val diskIO: CoroutineDispatcher = Dispatchers.IO

/**
* Fan-out work on `MediaMetadataRetriever`. Every retriever's `release()` and finalizer share one
* process-wide native lock, and a release waits on mediaserver while holding it; dozens of image
* loads at once kept the finalizer daemon queued past its 10 s watchdog, which kills the app (#1353).
*/
val mediaRetriever: CoroutineDispatcher = Dispatchers.IO.limitedParallelism(MEDIA_RETRIEVER_PARALLELISM)
private const val MEDIA_RETRIEVER_PARALLELISM = 2

/**
* Main thread dispatcher for UI updates
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,9 +80,16 @@ object FlowWidgets {
val stamp = "${BuildConfig.VERSION_CODE}|$themeSignature"
if (context.lastWidgetPreviewStamp() == stamp) return
val manager = GlanceAppWidgetManager(context)
// Some profiles, such as a Private Space, reject our own providers as invalid (#1357), and
// Glance passes that binder exception straight through.
val published =
catalog.all { entry ->
manager.setWidgetPreviews(entry.receiver) == GlanceAppWidgetManager.SET_WIDGET_PREVIEWS_RESULT_SUCCESS
try {
catalog.all { entry ->
manager.setWidgetPreviews(entry.receiver) == GlanceAppWidgetManager.SET_WIDGET_PREVIEWS_RESULT_SUCCESS
}
} catch (e: IllegalArgumentException) {
Log.w(TAG, "Widget previews rejected: ${e.message}")
false
}
Log.i(TAG, "Widget previews published: $published")
if (published) context.writeWidgetPreviewStamp(stamp)
Expand Down
Loading