From f13b29196d25ba8c36f40903ad5d025b09be9ba0 Mon Sep 17 00:00:00 2001 From: Charles Lombardo Date: Wed, 20 Sep 2023 14:12:22 -0400 Subject: [PATCH] Android: Use custom image loader for game covers This fixes a bug where custom cover loading was initiated but would finish by the time another image view would be in the place of the previous one. --- .../dolphinemu/adapters/GameAdapter.kt | 17 +-- .../dolphinemu/adapters/GameRowPresenter.kt | 17 +-- .../dolphinemu/ui/main/TvMainActivity.kt | 2 +- .../ui/platform/PlatformGamesFragment.kt | 2 +- .../dolphinemu/dolphinemu/utils/CoilUtils.kt | 113 ++++++++++++------ 5 files changed, 82 insertions(+), 69 deletions(-) diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/adapters/GameAdapter.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/adapters/GameAdapter.kt index 520c96d812..c1ef9c2b8f 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/adapters/GameAdapter.kt +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/adapters/GameAdapter.kt @@ -25,7 +25,7 @@ import org.dolphinemu.dolphinemu.features.settings.model.BooleanSetting import org.dolphinemu.dolphinemu.utils.CoilUtils import java.util.ArrayList -class GameAdapter(private val mActivity: FragmentActivity) : RecyclerView.Adapter(), +class GameAdapter : RecyclerView.Adapter(), View.OnClickListener, OnLongClickListener { private var mGameFiles: List = ArrayList() @@ -72,20 +72,7 @@ class GameAdapter(private val mActivity: FragmentActivity) : RecyclerView.Adapte binding.textGameCaption.visibility = View.GONE } } - - mActivity.lifecycleScope.launch { - withContext(Dispatchers.IO) { - val customCoverUri = CoilUtils.findCustomCover(gameFile) - withContext(Dispatchers.Main) { - CoilUtils.loadGameCover( - holder, - holder.binding.imageGameScreen, - gameFile, - customCoverUri - ) - } - } - } + CoilUtils.loadGameCover(holder, holder.binding.imageGameScreen, gameFile) val animateIn = AnimationUtils.loadAnimation(context, R.anim.anim_card_game_in) animateIn.fillAfter = true diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/adapters/GameRowPresenter.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/adapters/GameRowPresenter.kt index b355dc8f6d..754c089876 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/adapters/GameRowPresenter.kt +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/adapters/GameRowPresenter.kt @@ -24,7 +24,7 @@ import org.dolphinemu.dolphinemu.utils.CoilUtils * The Leanback library / docs call this a Presenter, but it works very * similarly to a RecyclerView.Adapter. */ -class GameRowPresenter(private val mActivity: FragmentActivity) : Presenter() { +class GameRowPresenter : Presenter() { override fun onCreateViewHolder(parent: ViewGroup): ViewHolder { // Create a new view. @@ -69,20 +69,7 @@ class GameRowPresenter(private val mActivity: FragmentActivity) : Presenter() { holder.cardParent.contentText = gameFile.getCompany() } } - - mActivity.lifecycleScope.launch { - withContext(Dispatchers.IO) { - val customCoverUri = CoilUtils.findCustomCover(gameFile) - withContext(Dispatchers.Main) { - CoilUtils.loadGameCover( - null, - holder.imageScreenshot, - gameFile, - customCoverUri - ) - } - } - } + CoilUtils.loadGameCover(null, holder.imageScreenshot, gameFile) } override fun onUnbindViewHolder(viewHolder: ViewHolder) { diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/main/TvMainActivity.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/main/TvMainActivity.kt index 79ec2f2943..9beec74fc3 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/main/TvMainActivity.kt +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/main/TvMainActivity.kt @@ -268,7 +268,7 @@ class TvMainActivity : FragmentActivity(), MainView, OnRefreshListener { } // Create an adapter for this row. - val row = ArrayObjectAdapter(GameRowPresenter(this)) + val row = ArrayObjectAdapter(GameRowPresenter()) row.addAll(0, gameFiles) // Keep a reference to the row in case we need to refresh it. diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/platform/PlatformGamesFragment.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/platform/PlatformGamesFragment.kt index 5f1139da8a..c23eeb4fe0 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/platform/PlatformGamesFragment.kt +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/platform/PlatformGamesFragment.kt @@ -37,7 +37,7 @@ class PlatformGamesFragment : Fragment(), PlatformGamesView { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { swipeRefresh = binding.swipeRefresh - val gameAdapter = GameAdapter(requireActivity()) + val gameAdapter = GameAdapter() gameAdapter.stateRestorationPolicy = RecyclerView.Adapter.StateRestorationPolicy.PREVENT_WHEN_EMPTY diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/CoilUtils.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/CoilUtils.kt index dd2e2e68fb..350306b572 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/CoilUtils.kt +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/CoilUtils.kt @@ -2,11 +2,22 @@ package org.dolphinemu.dolphinemu.utils +import android.graphics.drawable.Drawable import android.net.Uri import android.view.View import android.widget.ImageView -import coil.load -import coil.target.ImageViewTarget +import coil.ImageLoader +import coil.decode.DataSource +import coil.executeBlocking +import coil.fetch.DrawableResult +import coil.fetch.FetchResult +import coil.fetch.Fetcher +import coil.imageLoader +import coil.key.Keyer +import coil.memory.MemoryCache +import coil.request.ImageRequest +import coil.request.Options +import org.dolphinemu.dolphinemu.DolphinApplication import org.dolphinemu.dolphinemu.R import org.dolphinemu.dolphinemu.adapters.GameAdapter.GameViewHolder import org.dolphinemu.dolphinemu.features.settings.model.BooleanSetting @@ -14,47 +25,75 @@ import org.dolphinemu.dolphinemu.model.GameFile import java.io.File import java.io.FileNotFoundException +class GameCoverFetcher( + private val game: GameFile, + private val options: Options +) : Fetcher { + override suspend fun fetch(): FetchResult { + val customCoverUri = CoilUtils.findCustomCover(game) + val builder = ImageRequest.Builder(DolphinApplication.getAppContext()) + var dataSource = DataSource.DISK + val drawable: Drawable? = if (customCoverUri != null) { + val request = builder.data(customCoverUri).error(R.drawable.no_banner).build() + DolphinApplication.getAppContext().imageLoader.executeBlocking(request).drawable + } else if (BooleanSetting.MAIN_USE_GAME_COVERS.boolean) { + val request = builder.data( + CoverHelper.buildGameTDBUrl(game, CoverHelper.getRegion(game)) + ).error(R.drawable.no_banner).build() + dataSource = DataSource.NETWORK + DolphinApplication.getAppContext().imageLoader.executeBlocking(request).drawable + } else { + null + } + + return DrawableResult( + // In the case where the drawable is null, intentionally throw an NPE. This tells Coil + // to load the error drawable. + drawable = drawable!!, + isSampled = false, + dataSource = dataSource + ) + } + + class Factory : Fetcher.Factory { + override fun create(data: GameFile, options: Options, imageLoader: ImageLoader): Fetcher = + GameCoverFetcher(data, options) + } +} + +class GameCoverKeyer : Keyer { + override fun key(data: GameFile, options: Options): String = data.getGameId() + data.getPath() +} + object CoilUtils { + private val imageLoader = ImageLoader.Builder(DolphinApplication.getAppContext()) + .components { + add(GameCoverKeyer()) + add(GameCoverFetcher.Factory()) + } + .memoryCache { + MemoryCache.Builder(DolphinApplication.getAppContext()) + .maxSizePercent(0.25) + .build() + } + .build() + fun loadGameCover( gameViewHolder: GameViewHolder?, imageView: ImageView, - gameFile: GameFile, - customCoverUri: Uri? + gameFile: GameFile ) { imageView.scaleType = ImageView.ScaleType.FIT_CENTER - val imageTarget = ImageViewTarget(imageView) - if (customCoverUri != null) { - imageView.load(customCoverUri) { - error(R.drawable.no_banner) - target( - onSuccess = { success -> - disableInnerTitle(gameViewHolder) - imageTarget.drawable = success - }, - onError = { error -> - enableInnerTitle(gameViewHolder) - imageTarget.drawable = error - } - ) - } - } else if (BooleanSetting.MAIN_USE_GAME_COVERS.boolean) { - imageView.load(CoverHelper.buildGameTDBUrl(gameFile, CoverHelper.getRegion(gameFile))) { - error(R.drawable.no_banner) - target( - onSuccess = { success -> - disableInnerTitle(gameViewHolder) - imageTarget.drawable = success - }, - onError = { error -> - enableInnerTitle(gameViewHolder) - imageTarget.drawable = error - } - ) - } - } else { - imageView.load(R.drawable.no_banner) - enableInnerTitle(gameViewHolder) - } + val imageRequest = ImageRequest.Builder(imageView.context) + .data(gameFile) + .error(R.drawable.no_banner) + .target(imageView) + .listener( + onSuccess = { _, _ -> disableInnerTitle(gameViewHolder) }, + onError = { _, _ -> enableInnerTitle(gameViewHolder) } + ) + .build() + imageLoader.enqueue(imageRequest) } private fun enableInnerTitle(gameViewHolder: GameViewHolder?) {