From e6583f8bec814d8f3748f1d7738457600ce0de56 Mon Sep 17 00:00:00 2001 From: Charles Lombardo Date: Fri, 6 Jan 2023 14:58:31 -0500 Subject: [PATCH 1/2] Android: Convert image loading code to Kotlin --- .../dolphinemu/adapters/GameAdapter.java | 184 ------------- .../dolphinemu/adapters/GameAdapter.kt | 160 +++++++++++ .../dolphinemu/adapters/GameRowPresenter.java | 88 ------- .../dolphinemu/adapters/GameRowPresenter.kt | 72 +++++ .../dolphinemu/dialogs/GameDetailsDialog.java | 172 ------------ .../dolphinemu/dialogs/GameDetailsDialog.kt | 145 ++++++++++ .../dolphinemu/utils/CoverHelper.java | 83 ------ .../dolphinemu/utils/CoverHelper.kt | 46 ++++ .../dolphinemu/utils/GlideUtils.java | 248 ------------------ .../dolphinemu/dolphinemu/utils/GlideUtils.kt | 233 ++++++++++++++++ .../viewholders/TvGameViewHolder.java | 34 --- .../viewholders/TvGameViewHolder.kt | 27 ++ 12 files changed, 683 insertions(+), 809 deletions(-) delete mode 100644 Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/adapters/GameAdapter.java create mode 100644 Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/adapters/GameAdapter.kt delete mode 100644 Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/adapters/GameRowPresenter.java create mode 100644 Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/adapters/GameRowPresenter.kt delete mode 100644 Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/dialogs/GameDetailsDialog.java create mode 100644 Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/dialogs/GameDetailsDialog.kt delete mode 100644 Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/CoverHelper.java create mode 100644 Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/CoverHelper.kt delete mode 100644 Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/GlideUtils.java create mode 100644 Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/GlideUtils.kt delete mode 100644 Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/viewholders/TvGameViewHolder.java create mode 100644 Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/viewholders/TvGameViewHolder.kt diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/adapters/GameAdapter.java b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/adapters/GameAdapter.java deleted file mode 100644 index fb9fd356da..0000000000 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/adapters/GameAdapter.java +++ /dev/null @@ -1,184 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0-or-later - -package org.dolphinemu.dolphinemu.adapters; - -import android.app.Activity; -import android.content.Context; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.view.animation.Animation; -import android.view.animation.AnimationUtils; - -import androidx.annotation.NonNull; -import androidx.fragment.app.FragmentActivity; -import androidx.recyclerview.widget.RecyclerView; - -import org.dolphinemu.dolphinemu.R; -import org.dolphinemu.dolphinemu.activities.EmulationActivity; -import org.dolphinemu.dolphinemu.databinding.CardGameBinding; -import org.dolphinemu.dolphinemu.dialogs.GamePropertiesDialog; -import org.dolphinemu.dolphinemu.model.GameFile; -import org.dolphinemu.dolphinemu.services.GameFileCacheManager; -import org.dolphinemu.dolphinemu.utils.GlideUtils; - -import java.util.ArrayList; -import java.util.List; - -public final class GameAdapter extends RecyclerView.Adapter implements - View.OnClickListener, - View.OnLongClickListener -{ - private List mGameFiles; - private Activity mActivity; - - /** - * Initializes the adapter's observer, which watches for changes to the dataset. The adapter will - * display no data until swapDataSet is called. - */ - public GameAdapter(Activity activity) - { - mGameFiles = new ArrayList<>(); - mActivity = activity; - } - - /** - * Called by the LayoutManager when it is necessary to create a new view. - * - * @param parent The RecyclerView (I think?) the created view will be thrown into. - * @param viewType Not used here, but useful when more than one type of child will be used in the RecyclerView. - * @return The created ViewHolder with references to all the child view's members. - */ - @NonNull - @Override - public GameViewHolder onCreateViewHolder(ViewGroup parent, int viewType) - { - CardGameBinding binding = CardGameBinding.inflate(LayoutInflater.from(parent.getContext())); - - binding.getRoot().setOnClickListener(this); - binding.getRoot().setOnLongClickListener(this); - - // Use that view to create a ViewHolder. - return new GameViewHolder(binding); - } - - /** - * Called by the LayoutManager when a new view is not necessary because we can recycle - * an existing one (for example, if a view just scrolled onto the screen from the bottom, we - * can use the view that just scrolled off the top instead of inflating a new one.) - * - * @param holder A ViewHolder representing the view we're recycling. - * @param position The position of the 'new' view in the dataset. - */ - @Override - public void onBindViewHolder(GameViewHolder holder, int position) - { - Context context = holder.itemView.getContext(); - GameFile gameFile = mGameFiles.get(position); - GlideUtils.loadGameCover(holder, holder.binding.imageGameScreen, gameFile, mActivity); - - if (GameFileCacheManager.findSecondDisc(gameFile) != null) - { - holder.binding.textGameCaption - .setText(context.getString(R.string.disc_number, gameFile.getDiscNumber() + 1)); - } - else - { - holder.binding.textGameCaption.setText(gameFile.getCompany()); - } - - holder.gameFile = gameFile; - - Animation animateIn = AnimationUtils.loadAnimation(context, R.anim.anim_card_game_in); - animateIn.setFillAfter(true); - Animation animateOut = AnimationUtils.loadAnimation(context, R.anim.anim_card_game_out); - animateOut.setFillAfter(true); - holder.binding.getRoot().setOnFocusChangeListener((v, hasFocus) -> - holder.binding.cardGameArt.startAnimation(hasFocus ? animateIn : animateOut)); - } - - public static class GameViewHolder extends RecyclerView.ViewHolder - { - public GameFile gameFile; - public CardGameBinding binding; - - public GameViewHolder(@NonNull CardGameBinding binding) - { - super(binding.getRoot()); - binding.getRoot().setTag(this); - this.binding = binding; - } - } - - /** - * Called by the LayoutManager to find out how much data we have. - * - * @return Size of the dataset. - */ - @Override - public int getItemCount() - { - return mGameFiles.size(); - } - - /** - * Tell Android whether or not each item in the dataset has a stable identifier. - * - * @param hasStableIds ignored. - */ - @Override - public void setHasStableIds(boolean hasStableIds) - { - super.setHasStableIds(false); - } - - /** - * When a load is finished, call this to replace the existing data - * with the newly-loaded data. - */ - public void swapDataSet(List gameFiles) - { - mGameFiles = gameFiles; - notifyDataSetChanged(); - } - - /** - * Re-fetches game metadata from the game file cache. - */ - public void refetchMetadata() - { - notifyItemRangeChanged(0, getItemCount()); - } - - /** - * Launches the game that was clicked on. - * - * @param view The card representing the game the user wants to play. - */ - @Override - public void onClick(View view) - { - GameViewHolder holder = (GameViewHolder) view.getTag(); - - String[] paths = GameFileCacheManager.findSecondDiscAndGetPaths(holder.gameFile); - EmulationActivity.launch((FragmentActivity) view.getContext(), paths, false); - } - - /** - * Launches the details activity for this Game, using an ID stored in the - * details button's Tag. - * - * @param view The Card button that was long-clicked. - */ - @Override - public boolean onLongClick(View view) - { - GameViewHolder holder = (GameViewHolder) view.getTag(); - - GamePropertiesDialog fragment = GamePropertiesDialog.newInstance(holder.gameFile); - ((FragmentActivity) view.getContext()).getSupportFragmentManager().beginTransaction() - .add(fragment, GamePropertiesDialog.TAG).commit(); - - return true; - } -} 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 new file mode 100644 index 0000000000..e3fa287967 --- /dev/null +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/adapters/GameAdapter.kt @@ -0,0 +1,160 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +package org.dolphinemu.dolphinemu.adapters + +import android.annotation.SuppressLint +import android.app.Activity +import androidx.recyclerview.widget.RecyclerView +import org.dolphinemu.dolphinemu.adapters.GameAdapter.GameViewHolder +import android.view.View.OnLongClickListener +import org.dolphinemu.dolphinemu.model.GameFile +import android.view.ViewGroup +import android.view.LayoutInflater +import android.view.View +import org.dolphinemu.dolphinemu.utils.GlideUtils +import org.dolphinemu.dolphinemu.services.GameFileCacheManager +import org.dolphinemu.dolphinemu.R +import android.view.animation.AnimationUtils +import org.dolphinemu.dolphinemu.activities.EmulationActivity +import androidx.fragment.app.FragmentActivity +import org.dolphinemu.dolphinemu.databinding.CardGameBinding +import org.dolphinemu.dolphinemu.dialogs.GamePropertiesDialog +import java.util.ArrayList + +class GameAdapter(activity: Activity) : RecyclerView.Adapter(), + View.OnClickListener, OnLongClickListener { + private var mGameFiles: List + private val mActivity: Activity + + /** + * Initializes the adapter's observer, which watches for changes to the dataset. The adapter will + * display no data until swapDataSet is called. + */ + init { + mGameFiles = ArrayList() + mActivity = activity + } + + /** + * Called by the LayoutManager when it is necessary to create a new view. + * + * @param parent The RecyclerView (I think?) the created view will be thrown into. + * @param viewType Not used here, but useful when more than one type of child will be used in the RecyclerView. + * @return The created ViewHolder with references to all the child view's members. + */ + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): GameViewHolder { + val binding = CardGameBinding.inflate(LayoutInflater.from(parent.context)) + binding.root.apply { + setOnClickListener(this@GameAdapter) + setOnLongClickListener(this@GameAdapter) + } + + // Use that view to create a ViewHolder. + return GameViewHolder(binding) + } + + /** + * Called by the LayoutManager when a new view is not necessary because we can recycle + * an existing one (for example, if a view just scrolled onto the screen from the bottom, we + * can use the view that just scrolled off the top instead of inflating a new one.) + * + * @param holder A ViewHolder representing the view we're recycling. + * @param position The position of the 'new' view in the dataset. + */ + override fun onBindViewHolder(holder: GameViewHolder, position: Int) { + val context = holder.itemView.context + val gameFile = mGameFiles[position] + + GlideUtils.loadGameCover(holder, holder.binding.imageGameScreen, gameFile, mActivity) + + val animateIn = AnimationUtils.loadAnimation(context, R.anim.anim_card_game_in) + animateIn.fillAfter = true + val animateOut = AnimationUtils.loadAnimation(context, R.anim.anim_card_game_out) + animateOut.fillAfter = true + holder.apply { + if (GameFileCacheManager.findSecondDisc(gameFile) != null) { + binding.textGameCaption.text = + context.getString(R.string.disc_number, gameFile.discNumber + 1) + } else { + binding.textGameCaption.text = gameFile.company + } + holder.gameFile = gameFile + binding.root.onFocusChangeListener = + View.OnFocusChangeListener { _: View?, hasFocus: Boolean -> + binding.cardGameArt.startAnimation(if (hasFocus) animateIn else animateOut) + } + } + } + + class GameViewHolder(binding: CardGameBinding) : RecyclerView.ViewHolder(binding.root) { + var gameFile: GameFile? = null + + @JvmField + var binding: CardGameBinding + + init { + binding.root.tag = this + this.binding = binding + } + } + + /** + * Called by the LayoutManager to find out how much data we have. + * + * @return Size of the dataset. + */ + override fun getItemCount(): Int { + return mGameFiles.size + } + + /** + * Tell Android whether or not each item in the dataset has a stable identifier. + * + * @param hasStableIds ignored. + */ + override fun setHasStableIds(hasStableIds: Boolean) { + super.setHasStableIds(false) + } + + /** + * When a load is finished, call this to replace the existing data + * with the newly-loaded data. + */ + @SuppressLint("NotifyDataSetChanged") + fun swapDataSet(gameFiles: List) { + mGameFiles = gameFiles + notifyDataSetChanged() + } + + /** + * Re-fetches game metadata from the game file cache. + */ + fun refetchMetadata() { + notifyItemRangeChanged(0, itemCount) + } + + /** + * Launches the game that was clicked on. + * + * @param view The card representing the game the user wants to play. + */ + override fun onClick(view: View) { + val holder = view.tag as GameViewHolder + val paths = GameFileCacheManager.findSecondDiscAndGetPaths(holder.gameFile) + EmulationActivity.launch(view.context as FragmentActivity, paths, false) + } + + /** + * Launches the details activity for this Game, using an ID stored in the + * details button's Tag. + * + * @param view The Card button that was long-clicked. + */ + override fun onLongClick(view: View): Boolean { + val holder = view.tag as GameViewHolder + val fragment = GamePropertiesDialog.newInstance(holder.gameFile) + (view.context as FragmentActivity).supportFragmentManager.beginTransaction() + .add(fragment, GamePropertiesDialog.TAG).commit() + return true + } +} diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/adapters/GameRowPresenter.java b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/adapters/GameRowPresenter.java deleted file mode 100644 index 53b0e56861..0000000000 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/adapters/GameRowPresenter.java +++ /dev/null @@ -1,88 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0-or-later - -package org.dolphinemu.dolphinemu.adapters; - -import android.content.Context; -import android.graphics.drawable.Drawable; -import android.view.ViewGroup; -import android.widget.ImageView; - -import androidx.core.content.ContextCompat; -import androidx.fragment.app.FragmentActivity; -import androidx.leanback.widget.ImageCardView; -import androidx.leanback.widget.Presenter; - -import org.dolphinemu.dolphinemu.R; -import org.dolphinemu.dolphinemu.dialogs.GamePropertiesDialog; -import org.dolphinemu.dolphinemu.model.GameFile; -import org.dolphinemu.dolphinemu.services.GameFileCacheManager; -import org.dolphinemu.dolphinemu.utils.GlideUtils; -import org.dolphinemu.dolphinemu.viewholders.TvGameViewHolder; - -/** - * The Leanback library / docs call this a Presenter, but it works very - * similarly to a RecyclerView.Adapter. - */ -public final class GameRowPresenter extends Presenter -{ - @Override - public ViewHolder onCreateViewHolder(ViewGroup parent) - { - // Create a new view. - ImageCardView gameCard = new ImageCardView(parent.getContext()); - - gameCard.setMainImageAdjustViewBounds(true); - gameCard.setMainImageDimensions(240, 336); - gameCard.setMainImageScaleType(ImageView.ScaleType.CENTER_CROP); - - gameCard.setFocusable(true); - gameCard.setFocusableInTouchMode(true); - - // Use that view to create a ViewHolder. - return new TvGameViewHolder(gameCard); - } - - @Override - public void onBindViewHolder(ViewHolder viewHolder, Object item) - { - TvGameViewHolder holder = (TvGameViewHolder) viewHolder; - Context context = holder.cardParent.getContext(); - GameFile gameFile = (GameFile) item; - - holder.imageScreenshot.setImageDrawable(null); - GlideUtils.loadGameCover(null, holder.imageScreenshot, gameFile, null); - - holder.cardParent.setTitleText(gameFile.getTitle()); - - if (GameFileCacheManager.findSecondDisc(gameFile) != null) - { - holder.cardParent.setContentText( - context.getString(R.string.disc_number, gameFile.getDiscNumber() + 1)); - } - else - { - holder.cardParent.setContentText(gameFile.getCompany()); - } - - holder.gameFile = gameFile; - - // Set the background color of the card - Drawable background = ContextCompat.getDrawable(context, R.drawable.tv_card_background); - holder.cardParent.setInfoAreaBackground(background); - holder.cardParent.setOnLongClickListener((view) -> - { - FragmentActivity activity = (FragmentActivity) view.getContext(); - GamePropertiesDialog fragment = GamePropertiesDialog.newInstance(holder.gameFile); - activity.getSupportFragmentManager().beginTransaction() - .add(fragment, GamePropertiesDialog.TAG).commit(); - - return true; - }); - } - - @Override - public void onUnbindViewHolder(ViewHolder viewHolder) - { - // no op - } -} 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 new file mode 100644 index 0000000000..66a249e50d --- /dev/null +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/adapters/GameRowPresenter.kt @@ -0,0 +1,72 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +package org.dolphinemu.dolphinemu.adapters + +import androidx.leanback.widget.Presenter +import android.view.ViewGroup +import androidx.leanback.widget.ImageCardView +import org.dolphinemu.dolphinemu.viewholders.TvGameViewHolder +import org.dolphinemu.dolphinemu.model.GameFile +import org.dolphinemu.dolphinemu.utils.GlideUtils +import org.dolphinemu.dolphinemu.services.GameFileCacheManager +import org.dolphinemu.dolphinemu.R +import android.view.View +import androidx.core.content.ContextCompat +import android.widget.ImageView +import androidx.fragment.app.FragmentActivity +import org.dolphinemu.dolphinemu.dialogs.GamePropertiesDialog + +/** + * The Leanback library / docs call this a Presenter, but it works very + * similarly to a RecyclerView.Adapter. + */ +class GameRowPresenter : Presenter() { + override fun onCreateViewHolder(parent: ViewGroup): ViewHolder { + // Create a new view. + val gameCard = ImageCardView(parent.context) + gameCard.apply { + setMainImageAdjustViewBounds(true) + setMainImageDimensions(240, 336) + setMainImageScaleType(ImageView.ScaleType.CENTER_CROP) + isFocusable = true + isFocusableInTouchMode = true + } + + // Use that view to create a ViewHolder. + return TvGameViewHolder(gameCard) + } + + override fun onBindViewHolder(viewHolder: ViewHolder, item: Any) { + val holder = viewHolder as TvGameViewHolder + val context = holder.cardParent.context + val gameFile = item as GameFile + + holder.apply { + imageScreenshot.setImageDrawable(null) + cardParent.titleText = gameFile.title + holder.gameFile = gameFile + + // Set the background color of the card + val background = ContextCompat.getDrawable(context, R.drawable.tv_card_background) + cardParent.infoAreaBackground = background + cardParent.setOnClickListener { view: View -> + val activity = view.context as FragmentActivity + val fragment = GamePropertiesDialog.newInstance(holder.gameFile) + activity.supportFragmentManager.beginTransaction() + .add(fragment, GamePropertiesDialog.TAG).commit() + } + + if (GameFileCacheManager.findSecondDisc(gameFile) != null) { + holder.cardParent.contentText = + context.getString(R.string.disc_number, gameFile.discNumber + 1) + } else { + holder.cardParent.contentText = gameFile.company + } + } + GlideUtils.loadGameCover(null, holder.imageScreenshot, gameFile, null) + } + + override fun onUnbindViewHolder(viewHolder: ViewHolder) { + // no op + } +} diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/dialogs/GameDetailsDialog.java b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/dialogs/GameDetailsDialog.java deleted file mode 100644 index 176f5b7610..0000000000 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/dialogs/GameDetailsDialog.java +++ /dev/null @@ -1,172 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0-or-later - -package org.dolphinemu.dolphinemu.dialogs; - -import android.app.Dialog; -import android.os.Bundle; -import android.view.View; - -import androidx.annotation.NonNull; -import androidx.appcompat.app.AppCompatActivity; -import androidx.fragment.app.DialogFragment; - -import com.google.android.material.dialog.MaterialAlertDialogBuilder; - -import org.dolphinemu.dolphinemu.NativeLibrary; -import org.dolphinemu.dolphinemu.R; -import org.dolphinemu.dolphinemu.databinding.DialogGameDetailsBinding; -import org.dolphinemu.dolphinemu.databinding.DialogGameDetailsTvBinding; -import org.dolphinemu.dolphinemu.model.GameFile; -import org.dolphinemu.dolphinemu.services.GameFileCacheManager; -import org.dolphinemu.dolphinemu.utils.GlideUtils; - -public final class GameDetailsDialog extends DialogFragment -{ - private static final String ARG_GAME_PATH = "game_path"; - - public static GameDetailsDialog newInstance(String gamePath) - { - GameDetailsDialog fragment = new GameDetailsDialog(); - - Bundle arguments = new Bundle(); - arguments.putString(ARG_GAME_PATH, gamePath); - fragment.setArguments(arguments); - - return fragment; - } - - @NonNull - @Override - public Dialog onCreateDialog(Bundle savedInstanceState) - { - GameFile gameFile = GameFileCacheManager.addOrGet(getArguments().getString(ARG_GAME_PATH)); - - String country = getResources().getStringArray(R.array.countryNames)[gameFile.getCountry()]; - String description = gameFile.getDescription(); - String fileSize = NativeLibrary.FormatSize(gameFile.getFileSize(), 2); - - // TODO: Remove dialog_game_details_tv if we switch to an AppCompatActivity for leanback - DialogGameDetailsBinding binding; - DialogGameDetailsTvBinding tvBinding; - MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(requireContext()); - if (requireActivity() instanceof AppCompatActivity) - { - binding = DialogGameDetailsBinding.inflate(getLayoutInflater()); - - binding.textGameTitle.setText(gameFile.getTitle()); - binding.textDescription.setText(gameFile.getDescription()); - if (description.isEmpty()) - { - binding.textDescription.setVisibility(View.GONE); - } - - binding.textCountry.setText(country); - binding.textCompany.setText(gameFile.getCompany()); - binding.textGameId.setText(gameFile.getGameId()); - binding.textRevision.setText(String.valueOf(gameFile.getRevision())); - - if (!gameFile.shouldShowFileFormatDetails()) - { - binding.labelFileFormat.setText(R.string.game_details_file_size); - binding.textFileFormat.setText(fileSize); - - binding.labelCompression.setVisibility(View.GONE); - binding.textCompression.setVisibility(View.GONE); - binding.labelBlockSize.setVisibility(View.GONE); - binding.textBlockSize.setVisibility(View.GONE); - } - else - { - long blockSize = gameFile.getBlockSize(); - String compression = gameFile.getCompressionMethod(); - - binding.textFileFormat.setText( - getResources().getString(R.string.game_details_size_and_format, - gameFile.getFileFormatName(), fileSize)); - - if (compression.isEmpty()) - { - binding.textCompression.setText(R.string.game_details_no_compression); - } - else - { - binding.textCompression.setText(gameFile.getCompressionMethod()); - } - - if (blockSize > 0) - { - binding.textBlockSize.setText(NativeLibrary.FormatSize(blockSize, 0)); - } - else - { - binding.labelBlockSize.setVisibility(View.GONE); - binding.textBlockSize.setVisibility(View.GONE); - } - } - - GlideUtils.loadGameBanner(binding.banner, gameFile); - - builder.setView(binding.getRoot()); - } - else - { - tvBinding = DialogGameDetailsTvBinding.inflate(getLayoutInflater()); - - tvBinding.textGameTitle.setText(gameFile.getTitle()); - tvBinding.textDescription.setText(gameFile.getDescription()); - if (description.isEmpty()) - { - tvBinding.textDescription.setVisibility(View.GONE); - } - - tvBinding.textCountry.setText(country); - tvBinding.textCompany.setText(gameFile.getCompany()); - tvBinding.textGameId.setText(gameFile.getGameId()); - tvBinding.textRevision.setText(String.valueOf(gameFile.getRevision())); - - if (!gameFile.shouldShowFileFormatDetails()) - { - tvBinding.labelFileFormat.setText(R.string.game_details_file_size); - tvBinding.textFileFormat.setText(fileSize); - - tvBinding.labelCompression.setVisibility(View.GONE); - tvBinding.textCompression.setVisibility(View.GONE); - tvBinding.labelBlockSize.setVisibility(View.GONE); - tvBinding.textBlockSize.setVisibility(View.GONE); - } - else - { - long blockSize = gameFile.getBlockSize(); - String compression = gameFile.getCompressionMethod(); - - tvBinding.textFileFormat.setText( - getResources().getString(R.string.game_details_size_and_format, - gameFile.getFileFormatName(), fileSize)); - - if (compression.isEmpty()) - { - tvBinding.textCompression.setText(R.string.game_details_no_compression); - } - else - { - tvBinding.textCompression.setText(gameFile.getCompressionMethod()); - } - - if (blockSize > 0) - { - tvBinding.textBlockSize.setText(NativeLibrary.FormatSize(blockSize, 0)); - } - else - { - tvBinding.labelBlockSize.setVisibility(View.GONE); - tvBinding.textBlockSize.setVisibility(View.GONE); - } - } - - GlideUtils.loadGameBanner(tvBinding.banner, gameFile); - - builder.setView(tvBinding.getRoot()); - } - return builder.create(); - } -} diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/dialogs/GameDetailsDialog.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/dialogs/GameDetailsDialog.kt new file mode 100644 index 0000000000..5c7cf2ed6a --- /dev/null +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/dialogs/GameDetailsDialog.kt @@ -0,0 +1,145 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +package org.dolphinemu.dolphinemu.dialogs + +import android.app.Dialog +import android.os.Bundle +import android.view.View +import org.dolphinemu.dolphinemu.services.GameFileCacheManager +import org.dolphinemu.dolphinemu.R +import com.google.android.material.dialog.MaterialAlertDialogBuilder +import androidx.appcompat.app.AppCompatActivity +import androidx.fragment.app.DialogFragment +import org.dolphinemu.dolphinemu.NativeLibrary +import org.dolphinemu.dolphinemu.databinding.DialogGameDetailsBinding +import org.dolphinemu.dolphinemu.databinding.DialogGameDetailsTvBinding +import org.dolphinemu.dolphinemu.utils.GlideUtils + +class GameDetailsDialog : DialogFragment() { + override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { + val gameFile = GameFileCacheManager.addOrGet(requireArguments().getString(ARG_GAME_PATH)) + + val country = resources.getStringArray(R.array.countryNames)[gameFile.country] + val fileSize = NativeLibrary.FormatSize(gameFile.fileSize, 2) + + // TODO: Remove dialog_game_details_tv if we switch to an AppCompatActivity for leanback + val binding: DialogGameDetailsBinding + val tvBinding: DialogGameDetailsTvBinding + val builder = MaterialAlertDialogBuilder(requireContext()) + if (requireActivity() is AppCompatActivity) { + binding = DialogGameDetailsBinding.inflate(layoutInflater) + binding.apply { + textGameTitle.text = gameFile.title + textDescription.text = gameFile.description + if (gameFile.description.isEmpty()) { + textDescription.visibility = View.GONE + } + + textCountry.text = country + textCompany.text = gameFile.company + textGameId.text = gameFile.gameId + textRevision.text = gameFile.revision.toString() + + if (!gameFile.shouldShowFileFormatDetails()) { + labelFileFormat.setText(R.string.game_details_file_size) + textFileFormat.text = fileSize + + labelCompression.visibility = View.GONE + textCompression.visibility = View.GONE + labelBlockSize.visibility = View.GONE + textBlockSize.visibility = View.GONE + } else { + val blockSize = gameFile.blockSize + val compression = gameFile.compressionMethod + + textFileFormat.text = resources.getString( + R.string.game_details_size_and_format, + gameFile.fileFormatName, + fileSize + ) + + if (compression.isEmpty()) { + textCompression.setText(R.string.game_details_no_compression) + } else { + textCompression.text = gameFile.compressionMethod + } + + if (blockSize > 0) { + textBlockSize.text = NativeLibrary.FormatSize(blockSize, 0) + } else { + labelBlockSize.visibility = View.GONE + textBlockSize.visibility = View.GONE + } + } + } + + GlideUtils.loadGameBanner(binding.banner, gameFile) + + builder.setView(binding.root) + } else { + tvBinding = DialogGameDetailsTvBinding.inflate(layoutInflater) + tvBinding.apply { + textGameTitle.text = gameFile.title + textDescription.text = gameFile.description + if (gameFile.description.isEmpty()) { + tvBinding.textDescription.visibility = View.GONE + } + + textCountry.text = country + textCompany.text = gameFile.company + textGameId.text = gameFile.gameId + textRevision.text = gameFile.revision.toString() + + if (!gameFile.shouldShowFileFormatDetails()) { + labelFileFormat.setText(R.string.game_details_file_size) + textFileFormat.text = fileSize + + labelCompression.visibility = View.GONE + textCompression.visibility = View.GONE + labelBlockSize.visibility = View.GONE + textBlockSize.visibility = View.GONE + } else { + val blockSize = gameFile.blockSize + val compression = gameFile.compressionMethod + + textFileFormat.text = resources.getString( + R.string.game_details_size_and_format, + gameFile.fileFormatName, + fileSize + ) + + if (compression.isEmpty()) { + textCompression.setText(R.string.game_details_no_compression) + } else { + textCompression.text = gameFile.compressionMethod + } + + if (blockSize > 0) { + textBlockSize.text = NativeLibrary.FormatSize(blockSize, 0) + } else { + labelBlockSize.visibility = View.GONE + textBlockSize.visibility = View.GONE + } + } + } + + GlideUtils.loadGameBanner(tvBinding.banner, gameFile) + + builder.setView(tvBinding.root) + } + return builder.create() + } + + companion object { + private const val ARG_GAME_PATH = "game_path" + + @JvmStatic + fun newInstance(gamePath: String?): GameDetailsDialog { + val fragment = GameDetailsDialog() + val arguments = Bundle() + arguments.putString(ARG_GAME_PATH, gamePath) + fragment.arguments = arguments + return fragment + } + } +} diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/CoverHelper.java b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/CoverHelper.java deleted file mode 100644 index 84ed1be6c5..0000000000 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/CoverHelper.java +++ /dev/null @@ -1,83 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0-or-later - -package org.dolphinemu.dolphinemu.utils; - -import android.graphics.Bitmap; - -import org.dolphinemu.dolphinemu.model.GameFile; - -import java.io.FileOutputStream; - -public final class CoverHelper -{ - public static String buildGameTDBUrl(GameFile game, String region) - { - String baseUrl = "https://art.gametdb.com/wii/cover/%s/%s.png"; - return String.format(baseUrl, region, game.getGameTdbId()); - } - - public static String getRegion(GameFile game) - { - String region; - switch (game.getRegion()) - { - case 0: // NTSC_J - region = "JA"; - break; - case 1: // NTSC_U - region = "US"; - break; - case 4: // NTSC_K - region = "KO"; - break; - case 2: // PAL - switch (game.getCountry()) - { - case 3: // Australia - region = "AU"; - break; - case 4: // France - region = "FR"; - break; - case 5: // Germany - region = "DE"; - break; - case 6: // Italy - region = "IT"; - break; - case 8: // Netherlands - region = "NL"; - break; - case 9: // Russia - region = "RU"; - break; - case 10: // Spain - region = "ES"; - break; - case 0: // Europe - default: - region = "EN"; - break; - } - break; - case 3: // Unknown - default: - region = "EN"; - break; - } - return region; - } - - public static void saveCover(Bitmap cover, String path) - { - try - { - FileOutputStream out = new FileOutputStream(path); - cover.compress(Bitmap.CompressFormat.PNG, 100, out); - out.close(); - } - catch (Exception ignored) - { - } - } -} diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/CoverHelper.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/CoverHelper.kt new file mode 100644 index 0000000000..f1deeb3730 --- /dev/null +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/CoverHelper.kt @@ -0,0 +1,46 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +package org.dolphinemu.dolphinemu.utils + +import org.dolphinemu.dolphinemu.model.GameFile +import android.graphics.Bitmap +import java.io.FileOutputStream +import java.lang.Exception + +object CoverHelper { + fun buildGameTDBUrl(game: GameFile, region: String?): String { + val baseUrl = "https://art.gametdb.com/wii/cover/%s/%s.png" + return String.format(baseUrl, region, game.gameTdbId) + } + + fun getRegion(game: GameFile): String { + val region: String = when (game.region) { + 0 -> "JA" + 1 -> "US" + 4 -> "KO" + 2 -> when (game.country) { + 3 -> "AU" + 4 -> "FR" + 5 -> "DE" + 6 -> "IT" + 8 -> "NL" + 9 -> "RU" + 10 -> "ES" + 0 -> "EN" + else -> "EN" + } + 3 -> "EN" + else -> "EN" + } + return region + } + + fun saveCover(cover: Bitmap, path: String?) { + try { + val out = FileOutputStream(path) + cover.compress(Bitmap.CompressFormat.PNG, 100, out) + out.close() + } catch (ignored: Exception) { + } + } +} diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/GlideUtils.java b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/GlideUtils.java deleted file mode 100644 index 766d9684dc..0000000000 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/GlideUtils.java +++ /dev/null @@ -1,248 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0-or-later - -package org.dolphinemu.dolphinemu.utils; - -import android.app.Activity; -import android.content.Context; -import android.graphics.Bitmap; -import android.graphics.drawable.BitmapDrawable; -import android.graphics.drawable.Drawable; -import android.net.Uri; -import android.os.Handler; -import android.os.Looper; -import android.view.View; -import android.widget.ImageView; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - -import com.bumptech.glide.Glide; -import com.bumptech.glide.load.DataSource; -import com.bumptech.glide.load.engine.DiskCacheStrategy; -import com.bumptech.glide.load.engine.GlideException; -import com.bumptech.glide.request.RequestListener; -import com.bumptech.glide.request.target.CustomTarget; -import com.bumptech.glide.request.target.Target; -import com.bumptech.glide.request.transition.Transition; - -import org.dolphinemu.dolphinemu.R; -import org.dolphinemu.dolphinemu.adapters.GameAdapter; -import org.dolphinemu.dolphinemu.features.settings.model.BooleanSetting; -import org.dolphinemu.dolphinemu.model.GameFile; - -import java.io.File; -import java.io.FileNotFoundException; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; - -public class GlideUtils -{ - private static final ExecutorService saveCoverExecutor = Executors.newSingleThreadExecutor(); - private static final ExecutorService unmangleExecutor = Executors.newSingleThreadExecutor(); - private static final Handler unmangleHandler = new Handler(Looper.getMainLooper()); - - public static void loadGameBanner(ImageView imageView, GameFile gameFile) - { - Context context = imageView.getContext(); - int[] vector = gameFile.getBanner(); - int width = gameFile.getBannerWidth(); - int height = gameFile.getBannerHeight(); - if (width > 0 && height > 0) - { - Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); - bitmap.setPixels(vector, 0, width, 0, 0, width, height); - Glide.with(context) - .load(bitmap) - .diskCacheStrategy(DiskCacheStrategy.NONE) - .centerCrop() - .into(imageView); - } - else - { - Glide.with(context) - .load(R.drawable.no_banner) - .into(imageView); - } - } - - public static void loadGameCover(GameAdapter.GameViewHolder gameViewHolder, ImageView imageView, - GameFile gameFile, Activity activity) - { - if (BooleanSetting.MAIN_SHOW_GAME_TITLES.getBooleanGlobal() && gameViewHolder != null) - { - gameViewHolder.binding.textGameTitle.setText(gameFile.getTitle()); - gameViewHolder.binding.textGameTitle.setVisibility(View.VISIBLE); - gameViewHolder.binding.textGameTitleInner.setVisibility(View.GONE); - gameViewHolder.binding.textGameCaption.setVisibility(View.VISIBLE); - } - else if (gameViewHolder != null) - { - gameViewHolder.binding.textGameTitleInner.setText(gameFile.getTitle()); - gameViewHolder.binding.textGameTitle.setVisibility(View.GONE); - gameViewHolder.binding.textGameCaption.setVisibility(View.GONE); - } - - unmangleExecutor.execute(() -> - { - String customCoverPath = gameFile.getCustomCoverPath(); - Uri customCoverUri = null; - boolean customCoverExists = false; - if (ContentHandler.isContentUri(customCoverPath)) - { - try - { - customCoverUri = ContentHandler.unmangle(customCoverPath); - customCoverExists = true; - } - catch (FileNotFoundException | SecurityException ignored) - { - // Let customCoverExists remain false - } - } - else - { - customCoverUri = Uri.parse(customCoverPath); - customCoverExists = new File(customCoverPath).exists(); - } - - Context context = imageView.getContext(); - boolean finalCustomCoverExists = customCoverExists; - Uri finalCustomCoverUri = customCoverUri; - - File cover = new File(gameFile.getCoverPath(context)); - boolean cachedCoverExists = cover.exists(); - unmangleHandler.post(() -> - { - // We can't get a reference to the current activity in the TV version. - // Luckily it won't attempt to start loads on destroyed activities. - if (activity != null) - { - // We can't start an image load on a destroyed activity - if (activity.isDestroyed()) - { - return; - } - } - - if (finalCustomCoverExists) - { - Glide.with(imageView) - .load(finalCustomCoverUri) - .diskCacheStrategy(DiskCacheStrategy.NONE) - .centerCrop() - .error(R.drawable.no_banner) - .listener(new RequestListener() - { - @Override public boolean onLoadFailed(@Nullable GlideException e, Object model, - Target target, boolean isFirstResource) - { - GlideUtils.enableInnerTitle(gameViewHolder); - return false; - } - - @Override public boolean onResourceReady(Drawable resource, Object model, - Target target, DataSource dataSource, boolean isFirstResource) - { - GlideUtils.disableInnerTitle(gameViewHolder); - return false; - } - }) - .into(imageView); - } - else if (cachedCoverExists) - { - Glide.with(imageView) - .load(cover) - .diskCacheStrategy(DiskCacheStrategy.NONE) - .centerCrop() - .error(R.drawable.no_banner) - .listener(new RequestListener() - { - @Override - public boolean onLoadFailed(@Nullable GlideException e, Object model, - Target target, boolean isFirstResource) - { - GlideUtils.enableInnerTitle(gameViewHolder); - return false; - } - - @Override - public boolean onResourceReady(Drawable resource, Object model, - Target target, DataSource dataSource, boolean isFirstResource) - { - GlideUtils.disableInnerTitle(gameViewHolder); - return false; - } - }) - .into(imageView); - } - else if (BooleanSetting.MAIN_USE_GAME_COVERS.getBooleanGlobal()) - { - Glide.with(context) - .load(CoverHelper.buildGameTDBUrl(gameFile, CoverHelper.getRegion(gameFile))) - .diskCacheStrategy(DiskCacheStrategy.NONE) - .centerCrop() - .error(R.drawable.no_banner) - .listener(new RequestListener() - { - @Override - public boolean onLoadFailed(@Nullable GlideException e, Object model, - Target target, boolean isFirstResource) - { - GlideUtils.enableInnerTitle(gameViewHolder); - return false; - } - - @Override - public boolean onResourceReady(Drawable resource, Object model, - Target target, DataSource dataSource, boolean isFirstResource) - { - GlideUtils.disableInnerTitle(gameViewHolder); - return false; - } - }) - .into(new CustomTarget() - { - @Override - public void onResourceReady(@NonNull Drawable resource, - @Nullable Transition transition) - { - Bitmap cover = ((BitmapDrawable) resource).getBitmap(); - saveCoverExecutor.execute( - () -> CoverHelper.saveCover(cover, gameFile.getCoverPath(context))); - imageView.setImageBitmap(cover); - } - - @Override - public void onLoadCleared(@Nullable Drawable placeholder) - { - } - }); - } - else - { - Glide.with(imageView.getContext()) - .load(R.drawable.no_banner) - .into(imageView); - enableInnerTitle(gameViewHolder); - } - }); - }); - } - - private static void enableInnerTitle(GameAdapter.GameViewHolder gameViewHolder) - { - if (gameViewHolder != null && !BooleanSetting.MAIN_SHOW_GAME_TITLES.getBooleanGlobal()) - { - gameViewHolder.binding.textGameTitleInner.setVisibility(View.VISIBLE); - } - } - - private static void disableInnerTitle(GameAdapter.GameViewHolder gameViewHolder) - { - if (gameViewHolder != null && !BooleanSetting.MAIN_SHOW_GAME_TITLES.getBooleanGlobal()) - { - gameViewHolder.binding.textGameTitleInner.setVisibility(View.GONE); - } - } -} diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/GlideUtils.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/GlideUtils.kt new file mode 100644 index 0000000000..ba2b9e344f --- /dev/null +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/GlideUtils.kt @@ -0,0 +1,233 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +package org.dolphinemu.dolphinemu.utils + +import org.dolphinemu.dolphinemu.utils.CoverHelper.buildGameTDBUrl +import org.dolphinemu.dolphinemu.utils.CoverHelper.getRegion +import org.dolphinemu.dolphinemu.utils.CoverHelper.saveCover +import android.os.Looper +import org.dolphinemu.dolphinemu.model.GameFile +import com.bumptech.glide.Glide +import com.bumptech.glide.load.engine.DiskCacheStrategy +import org.dolphinemu.dolphinemu.R +import org.dolphinemu.dolphinemu.adapters.GameAdapter.GameViewHolder +import android.app.Activity +import android.graphics.Bitmap +import android.graphics.drawable.BitmapDrawable +import org.dolphinemu.dolphinemu.features.settings.model.BooleanSetting +import com.bumptech.glide.request.RequestListener +import android.graphics.drawable.Drawable +import android.net.Uri +import android.os.Handler +import android.view.View +import android.widget.ImageView +import com.bumptech.glide.load.DataSource +import com.bumptech.glide.load.engine.GlideException +import com.bumptech.glide.request.target.CustomTarget +import com.bumptech.glide.request.target.Target +import com.bumptech.glide.request.transition.Transition +import java.io.File +import java.io.FileNotFoundException +import java.util.concurrent.Executors + +object GlideUtils { + private val saveCoverExecutor = Executors.newSingleThreadExecutor() + private val unmangleExecutor = Executors.newSingleThreadExecutor() + private val unmangleHandler = Handler(Looper.getMainLooper()) + + fun loadGameBanner(imageView: ImageView, gameFile: GameFile) { + val context = imageView.context + val vector = gameFile.banner + val width = gameFile.bannerWidth + val height = gameFile.bannerHeight + if (width > 0 && height > 0) { + val bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888) + bitmap.setPixels(vector, 0, width, 0, 0, width, height) + Glide.with(context) + .load(bitmap) + .diskCacheStrategy(DiskCacheStrategy.NONE) + .centerCrop() + .into(imageView) + } else { + Glide.with(context) + .load(R.drawable.no_banner) + .into(imageView) + } + } + + fun loadGameCover( + gameViewHolder: GameViewHolder?, + imageView: ImageView, + gameFile: GameFile, + activity: Activity? + ) { + gameViewHolder?.apply { + if (BooleanSetting.MAIN_SHOW_GAME_TITLES.booleanGlobal) { + binding.textGameTitle.text = gameFile.title + binding.textGameTitle.visibility = View.VISIBLE + binding.textGameTitleInner.visibility = View.GONE + binding.textGameCaption.visibility = View.VISIBLE + } else { + binding.textGameTitleInner.text = gameFile.title + binding.textGameTitle.visibility = View.GONE + binding.textGameCaption.visibility = View.GONE + } + } + + unmangleExecutor.execute { + val customCoverPath = gameFile.customCoverPath + var customCoverUri: Uri? = null + var customCoverExists = false + if (ContentHandler.isContentUri(customCoverPath)) { + try { + customCoverUri = ContentHandler.unmangle(customCoverPath) + customCoverExists = true + } catch (ignored: FileNotFoundException) { + // Let customCoverExists remain false + } catch (ignored: SecurityException) { + } + } else { + customCoverUri = Uri.parse(customCoverPath) + customCoverExists = File(customCoverPath).exists() + } + + val context = imageView.context + val finalCustomCoverExists = customCoverExists + val finalCustomCoverUri = customCoverUri + + val cover = File(gameFile.getCoverPath(context)) + val cachedCoverExists = cover.exists() + unmangleHandler.post { + // We can't get a reference to the current activity in the TV version. + // Luckily it won't attempt to start loads on destroyed activities. + if (activity != null) { + // We can't start an image load on a destroyed activity + if (activity.isDestroyed) { + return@post + } + } + + if (finalCustomCoverExists) { + Glide.with(imageView) + .load(finalCustomCoverUri) + .diskCacheStrategy(DiskCacheStrategy.NONE) + .centerCrop() + .error(R.drawable.no_banner) + .listener(object : RequestListener { + override fun onLoadFailed( + e: GlideException?, + model: Any, + target: Target, + isFirstResource: Boolean + ): Boolean { + enableInnerTitle(gameViewHolder) + return false + } + + override fun onResourceReady( + resource: Drawable?, + model: Any, + target: Target, + dataSource: DataSource, + isFirstResource: Boolean + ): Boolean { + disableInnerTitle(gameViewHolder) + return false + } + }) + .into(imageView) + } else if (cachedCoverExists) { + Glide.with(imageView) + .load(cover) + .diskCacheStrategy(DiskCacheStrategy.NONE) + .centerCrop() + .error(R.drawable.no_banner) + .listener(object : RequestListener { + override fun onLoadFailed( + e: GlideException?, + model: Any, + target: Target, + isFirstResource: Boolean + ): Boolean { + enableInnerTitle(gameViewHolder) + return false + } + + override fun onResourceReady( + resource: Drawable?, + model: Any, + target: Target, + dataSource: DataSource, + isFirstResource: Boolean + ): Boolean { + disableInnerTitle(gameViewHolder) + return false + } + }) + .into(imageView) + } else if (BooleanSetting.MAIN_USE_GAME_COVERS.booleanGlobal) { + Glide.with(context) + .load(buildGameTDBUrl(gameFile, getRegion(gameFile))) + .diskCacheStrategy(DiskCacheStrategy.NONE) + .centerCrop() + .error(R.drawable.no_banner) + .listener(object : RequestListener { + override fun onLoadFailed( + e: GlideException?, + model: Any, + target: Target, + isFirstResource: Boolean + ): Boolean { + enableInnerTitle(gameViewHolder) + return false + } + + override fun onResourceReady( + resource: Drawable?, + model: Any, + target: Target, + dataSource: DataSource, + isFirstResource: Boolean + ): Boolean { + disableInnerTitle(gameViewHolder) + return false + } + }) + .into(object : CustomTarget() { + override fun onLoadCleared(placeholder: Drawable?) {} + override fun onResourceReady( + resource: Drawable, + transition: Transition? + ) { + val savedCover = (resource as BitmapDrawable).bitmap + saveCoverExecutor.execute { + saveCover( + savedCover, + gameFile.getCoverPath(context) + ) + } + imageView.setImageBitmap(savedCover) + } + }) + } else { + Glide.with(imageView.context) + .load(R.drawable.no_banner) + .into(imageView) + enableInnerTitle(gameViewHolder) + } + } + } + } + + private fun enableInnerTitle(gameViewHolder: GameViewHolder?) { + if (gameViewHolder != null && !BooleanSetting.MAIN_SHOW_GAME_TITLES.booleanGlobal) { + gameViewHolder.binding.textGameTitleInner.visibility = View.VISIBLE + } + } + + private fun disableInnerTitle(gameViewHolder: GameViewHolder?) { + if (gameViewHolder != null && !BooleanSetting.MAIN_SHOW_GAME_TITLES.booleanGlobal) { + gameViewHolder.binding.textGameTitleInner.visibility = View.GONE + } + } +} diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/viewholders/TvGameViewHolder.java b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/viewholders/TvGameViewHolder.java deleted file mode 100644 index b51caa7707..0000000000 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/viewholders/TvGameViewHolder.java +++ /dev/null @@ -1,34 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0-or-later - -package org.dolphinemu.dolphinemu.viewholders; - -import android.view.View; -import android.widget.ImageView; - -import androidx.leanback.widget.ImageCardView; -import androidx.leanback.widget.Presenter; - -import org.dolphinemu.dolphinemu.model.GameFile; - -/** - * A simple class that stores references to views so that the GameAdapter doesn't need to - * keep calling findViewById(), which is expensive. - */ -public final class TvGameViewHolder extends Presenter.ViewHolder -{ - public ImageCardView cardParent; - - public ImageView imageScreenshot; - - public GameFile gameFile; - - public TvGameViewHolder(View itemView) - { - super(itemView); - - itemView.setTag(this); - - cardParent = (ImageCardView) itemView; - imageScreenshot = cardParent.getMainImageView(); - } -} diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/viewholders/TvGameViewHolder.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/viewholders/TvGameViewHolder.kt new file mode 100644 index 0000000000..6fc5adbfa7 --- /dev/null +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/viewholders/TvGameViewHolder.kt @@ -0,0 +1,27 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +package org.dolphinemu.dolphinemu.viewholders + +import android.view.View +import android.widget.ImageView +import androidx.leanback.widget.Presenter +import androidx.leanback.widget.ImageCardView +import org.dolphinemu.dolphinemu.model.GameFile + +/** + * A simple class that stores references to views so that the GameAdapter doesn't need to + * keep calling findViewById(), which is expensive. + */ +class TvGameViewHolder(itemView: View) : Presenter.ViewHolder(itemView) { + var cardParent: ImageCardView + var imageScreenshot: ImageView + + @JvmField + var gameFile: GameFile? = null + + init { + itemView.tag = this + cardParent = itemView as ImageCardView + imageScreenshot = cardParent.mainImageView + } +} From 28faca63a634cd3c8de947143ee029b78a718690 Mon Sep 17 00:00:00 2001 From: Charles Lombardo Date: Fri, 6 Jan 2023 15:15:30 -0500 Subject: [PATCH 2/2] Android: Replace Glide with Coil image loading --- Source/Android/app/build.gradle | 2 +- .../dolphinemu/adapters/GameAdapter.kt | 55 +++-- .../dolphinemu/adapters/GameRowPresenter.kt | 23 +- .../dolphinemu/dialogs/GameDetailsDialog.kt | 36 ++- .../fragments/GridOptionDialogFragment.kt | 5 +- .../dolphinemu/dolphinemu/model/GameFile.java | 12 +- .../dolphinemu/ui/main/TvMainActivity.java | 2 +- .../dolphinemu/dolphinemu/utils/CoilUtils.kt | 95 +++++++ .../dolphinemu/utils/CoverHelper.kt | 40 ++- .../dolphinemu/dolphinemu/utils/GlideUtils.kt | 233 ------------------ .../dolphinemu/dolphinemu/utils/TvUtil.java | 4 +- 11 files changed, 210 insertions(+), 297 deletions(-) create mode 100644 Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/CoilUtils.kt delete mode 100644 Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/GlideUtils.kt diff --git a/Source/Android/app/build.gradle b/Source/Android/app/build.gradle index 3b1f8c3ca4..11349147e8 100644 --- a/Source/Android/app/build.gradle +++ b/Source/Android/app/build.gradle @@ -155,7 +155,7 @@ dependencies { implementation 'com.android.volley:volley:1.2.1' // For loading game covers from disk and GameTDB - implementation 'com.github.bumptech.glide:glide:4.13.2' + implementation 'io.coil-kt:coil:2.2.2' implementation 'com.nononsenseapps:filepicker:4.2.1' } 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 e3fa287967..919ded6fef 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 @@ -3,7 +3,6 @@ package org.dolphinemu.dolphinemu.adapters import android.annotation.SuppressLint -import android.app.Activity import androidx.recyclerview.widget.RecyclerView import org.dolphinemu.dolphinemu.adapters.GameAdapter.GameViewHolder import android.view.View.OnLongClickListener @@ -11,29 +10,23 @@ import org.dolphinemu.dolphinemu.model.GameFile import android.view.ViewGroup import android.view.LayoutInflater import android.view.View -import org.dolphinemu.dolphinemu.utils.GlideUtils import org.dolphinemu.dolphinemu.services.GameFileCacheManager import org.dolphinemu.dolphinemu.R import android.view.animation.AnimationUtils import org.dolphinemu.dolphinemu.activities.EmulationActivity import androidx.fragment.app.FragmentActivity +import androidx.lifecycle.lifecycleScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext import org.dolphinemu.dolphinemu.databinding.CardGameBinding import org.dolphinemu.dolphinemu.dialogs.GamePropertiesDialog +import org.dolphinemu.dolphinemu.features.settings.model.BooleanSetting +import org.dolphinemu.dolphinemu.utils.CoilUtils import java.util.ArrayList -class GameAdapter(activity: Activity) : RecyclerView.Adapter(), +class GameAdapter(private val mActivity: FragmentActivity) : RecyclerView.Adapter(), View.OnClickListener, OnLongClickListener { - private var mGameFiles: List - private val mActivity: Activity - - /** - * Initializes the adapter's observer, which watches for changes to the dataset. The adapter will - * display no data until swapDataSet is called. - */ - init { - mGameFiles = ArrayList() - mActivity = activity - } + private var mGameFiles: List = ArrayList() /** * Called by the LayoutManager when it is necessary to create a new view. @@ -65,7 +58,33 @@ class GameAdapter(activity: Activity) : RecyclerView.Adapter(), val context = holder.itemView.context val gameFile = mGameFiles[position] - GlideUtils.loadGameCover(holder, holder.binding.imageGameScreen, gameFile, mActivity) + holder.apply { + if (BooleanSetting.MAIN_SHOW_GAME_TITLES.booleanGlobal) { + binding.textGameTitle.text = gameFile.title + binding.textGameTitle.visibility = View.VISIBLE + binding.textGameTitleInner.visibility = View.GONE + binding.textGameCaption.visibility = View.VISIBLE + } else { + binding.textGameTitleInner.text = gameFile.title + binding.textGameTitleInner.visibility = View.VISIBLE + binding.textGameTitle.visibility = View.GONE + binding.textGameCaption.visibility = View.GONE + } + } + + mActivity.lifecycleScope.launchWhenStarted { + withContext(Dispatchers.IO) { + val customCoverUri = CoilUtils.findCustomCover(gameFile) + withContext(Dispatchers.Main) { + CoilUtils.loadGameCover( + holder, + holder.binding.imageGameScreen, + gameFile, + customCoverUri + ) + } + } + } val animateIn = AnimationUtils.loadAnimation(context, R.anim.anim_card_game_in) animateIn.fillAfter = true @@ -86,15 +105,11 @@ class GameAdapter(activity: Activity) : RecyclerView.Adapter(), } } - class GameViewHolder(binding: CardGameBinding) : RecyclerView.ViewHolder(binding.root) { + class GameViewHolder(var binding: CardGameBinding) : RecyclerView.ViewHolder(binding.root) { var gameFile: GameFile? = null - @JvmField - var binding: CardGameBinding - init { binding.root.tag = this - this.binding = binding } } 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 66a249e50d..4f062538f9 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 @@ -7,20 +7,24 @@ import android.view.ViewGroup import androidx.leanback.widget.ImageCardView import org.dolphinemu.dolphinemu.viewholders.TvGameViewHolder import org.dolphinemu.dolphinemu.model.GameFile -import org.dolphinemu.dolphinemu.utils.GlideUtils import org.dolphinemu.dolphinemu.services.GameFileCacheManager import org.dolphinemu.dolphinemu.R import android.view.View import androidx.core.content.ContextCompat import android.widget.ImageView import androidx.fragment.app.FragmentActivity +import androidx.lifecycle.lifecycleScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext import org.dolphinemu.dolphinemu.dialogs.GamePropertiesDialog +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 : Presenter() { +class GameRowPresenter(private val mActivity: FragmentActivity) : Presenter() { + override fun onCreateViewHolder(parent: ViewGroup): ViewHolder { // Create a new view. val gameCard = ImageCardView(parent.context) @@ -63,7 +67,20 @@ class GameRowPresenter : Presenter() { holder.cardParent.contentText = gameFile.company } } - GlideUtils.loadGameCover(null, holder.imageScreenshot, gameFile, null) + + mActivity.lifecycleScope.launchWhenStarted { + withContext(Dispatchers.IO) { + val customCoverUri = CoilUtils.findCustomCover(gameFile) + withContext(Dispatchers.Main) { + CoilUtils.loadGameCover( + null, + holder.imageScreenshot, + gameFile, + customCoverUri + ) + } + } + } } override fun onUnbindViewHolder(viewHolder: ViewHolder) { diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/dialogs/GameDetailsDialog.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/dialogs/GameDetailsDialog.kt index 5c7cf2ed6a..83d69ebbb4 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/dialogs/GameDetailsDialog.kt +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/dialogs/GameDetailsDialog.kt @@ -3,17 +3,23 @@ package org.dolphinemu.dolphinemu.dialogs import android.app.Dialog +import android.graphics.Bitmap import android.os.Bundle import android.view.View +import android.widget.ImageView import org.dolphinemu.dolphinemu.services.GameFileCacheManager import org.dolphinemu.dolphinemu.R import com.google.android.material.dialog.MaterialAlertDialogBuilder import androidx.appcompat.app.AppCompatActivity import androidx.fragment.app.DialogFragment +import androidx.lifecycle.lifecycleScope +import coil.imageLoader +import coil.request.ImageRequest +import kotlinx.coroutines.launch import org.dolphinemu.dolphinemu.NativeLibrary import org.dolphinemu.dolphinemu.databinding.DialogGameDetailsBinding import org.dolphinemu.dolphinemu.databinding.DialogGameDetailsTvBinding -import org.dolphinemu.dolphinemu.utils.GlideUtils +import org.dolphinemu.dolphinemu.model.GameFile class GameDetailsDialog : DialogFragment() { override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { @@ -73,7 +79,9 @@ class GameDetailsDialog : DialogFragment() { } } - GlideUtils.loadGameBanner(binding.banner, gameFile) + this.lifecycleScope.launch { + loadGameBanner(binding.banner, gameFile) + } builder.setView(binding.root) } else { @@ -123,13 +131,35 @@ class GameDetailsDialog : DialogFragment() { } } - GlideUtils.loadGameBanner(tvBinding.banner, gameFile) + this.lifecycleScope.launch { + loadGameBanner(tvBinding.banner, gameFile) + } builder.setView(tvBinding.root) } return builder.create() } + private suspend fun loadGameBanner(imageView: ImageView, gameFile: GameFile) { + val vector = gameFile.banner + val width = gameFile.bannerWidth + val height = gameFile.bannerHeight + + imageView.scaleType = ImageView.ScaleType.FIT_CENTER + val request = ImageRequest.Builder(imageView.context) + .target(imageView) + .error(R.drawable.no_banner) + + if (width > 0 && height > 0) { + val bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888) + bitmap.setPixels(vector, 0, width, 0, 0, width, height) + request.data(bitmap) + } else { + request.data(R.drawable.no_banner) + } + imageView.context.imageLoader.execute(request.build()) + } + companion object { private const val ARG_GAME_PATH = "game_path" diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/fragments/GridOptionDialogFragment.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/fragments/GridOptionDialogFragment.kt index 94dda3f579..5f51038462 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/fragments/GridOptionDialogFragment.kt +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/fragments/GridOptionDialogFragment.kt @@ -1,5 +1,6 @@ package org.dolphinemu.dolphinemu.fragments +import android.app.Activity import com.google.android.material.bottomsheet.BottomSheetDialogFragment import android.view.LayoutInflater import android.view.ViewGroup @@ -88,7 +89,7 @@ class GridOptionDialogFragment : BottomSheetDialogFragment() { NativeConfig.LAYER_BASE, mBindingMobile.switchDownloadCovers.isChecked ) - mView.reloadGrid() + (mView as Activity).recreate() } } @@ -118,7 +119,7 @@ class GridOptionDialogFragment : BottomSheetDialogFragment() { NativeConfig.LAYER_BASE, mBindingTv.switchDownloadCovers.isChecked ) - mView.reloadGrid() + (mView as Activity).recreate() } } } diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/model/GameFile.java b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/model/GameFile.java index b8de619e31..26f4c169ce 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/model/GameFile.java +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/model/GameFile.java @@ -2,12 +2,15 @@ package org.dolphinemu.dolphinemu.model; -import android.content.Context; - import androidx.annotation.Keep; public class GameFile { + public static int REGION_NTSC_J = 0; + public static int REGION_NTSC_U = 1; + public static int REGION_PAL = 2; + public static int REGION_NTSC_K = 4; + @Keep private long mPointer; @@ -68,11 +71,6 @@ public class GameFile public native int getBannerHeight(); - public String getCoverPath(Context context) - { - return context.getExternalCacheDir().getPath() + "/GameCovers/" + getGameTdbId() + ".png"; - } - public String getCustomCoverPath() { return getPath().substring(0, getPath().lastIndexOf(".")) + ".cover.png"; diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/main/TvMainActivity.java b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/main/TvMainActivity.java index 0e1f55154b..2da1b8591d 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/main/TvMainActivity.java +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/main/TvMainActivity.java @@ -349,7 +349,7 @@ public final class TvMainActivity extends FragmentActivity } // Create an adapter for this row. - ArrayObjectAdapter row = new ArrayObjectAdapter(new GameRowPresenter()); + ArrayObjectAdapter row = new ArrayObjectAdapter(new GameRowPresenter(this)); 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/utils/CoilUtils.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/CoilUtils.kt new file mode 100644 index 0000000000..233f620d23 --- /dev/null +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/CoilUtils.kt @@ -0,0 +1,95 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +package org.dolphinemu.dolphinemu.utils + +import android.net.Uri +import android.view.View +import android.widget.ImageView +import coil.load +import coil.target.ImageViewTarget +import org.dolphinemu.dolphinemu.R +import org.dolphinemu.dolphinemu.adapters.GameAdapter.GameViewHolder +import org.dolphinemu.dolphinemu.features.settings.model.BooleanSetting +import org.dolphinemu.dolphinemu.model.GameFile +import java.io.File +import java.io.FileNotFoundException + +object CoilUtils { + fun loadGameCover( + gameViewHolder: GameViewHolder?, + imageView: ImageView, + gameFile: GameFile, + customCoverUri: Uri? + ) { + 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.booleanGlobal) { + 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) + } + } + + private fun enableInnerTitle(gameViewHolder: GameViewHolder?) { + if (gameViewHolder != null && !BooleanSetting.MAIN_SHOW_GAME_TITLES.booleanGlobal) { + gameViewHolder.binding.textGameTitleInner.visibility = View.VISIBLE + } + } + + private fun disableInnerTitle(gameViewHolder: GameViewHolder?) { + if (gameViewHolder != null && !BooleanSetting.MAIN_SHOW_GAME_TITLES.booleanGlobal) { + gameViewHolder.binding.textGameTitleInner.visibility = View.GONE + } + } + + fun findCustomCover(gameFile: GameFile): Uri? { + val customCoverPath = gameFile.customCoverPath + var customCoverUri: Uri? = null + var customCoverExists = false + if (ContentHandler.isContentUri(customCoverPath)) { + try { + customCoverUri = ContentHandler.unmangle(customCoverPath) + customCoverExists = true + } catch (ignored: FileNotFoundException) { + } catch (ignored: SecurityException) { + // Let customCoverExists remain false + } + } else { + customCoverUri = Uri.parse(customCoverPath) + customCoverExists = File(customCoverPath).exists() + } + + return if (customCoverExists) { + customCoverUri + } else { + null + } + } +} diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/CoverHelper.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/CoverHelper.kt index f1deeb3730..9359d38eb3 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/CoverHelper.kt +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/CoverHelper.kt @@ -3,44 +3,34 @@ package org.dolphinemu.dolphinemu.utils import org.dolphinemu.dolphinemu.model.GameFile -import android.graphics.Bitmap -import java.io.FileOutputStream -import java.lang.Exception object CoverHelper { + @JvmStatic fun buildGameTDBUrl(game: GameFile, region: String?): String { val baseUrl = "https://art.gametdb.com/wii/cover/%s/%s.png" return String.format(baseUrl, region, game.gameTdbId) } + @JvmStatic fun getRegion(game: GameFile): String { val region: String = when (game.region) { - 0 -> "JA" - 1 -> "US" - 4 -> "KO" - 2 -> when (game.country) { - 3 -> "AU" - 4 -> "FR" - 5 -> "DE" - 6 -> "IT" - 8 -> "NL" - 9 -> "RU" - 10 -> "ES" - 0 -> "EN" + GameFile.REGION_NTSC_J -> "JA" + GameFile.REGION_NTSC_U -> "US" + GameFile.REGION_NTSC_K -> "KO" + GameFile.REGION_PAL -> when (game.country) { + 3 -> "AU" // Australia + 4 -> "FR" // France + 5 -> "DE" // Germany + 6 -> "IT" // Italy + 8 -> "NL" // Netherlands + 9 -> "RU" // Russia + 10 -> "ES" // Spain + 0 -> "EN" // Europe else -> "EN" } - 3 -> "EN" + 3 -> "EN" // Unknown else -> "EN" } return region } - - fun saveCover(cover: Bitmap, path: String?) { - try { - val out = FileOutputStream(path) - cover.compress(Bitmap.CompressFormat.PNG, 100, out) - out.close() - } catch (ignored: Exception) { - } - } } diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/GlideUtils.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/GlideUtils.kt deleted file mode 100644 index ba2b9e344f..0000000000 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/GlideUtils.kt +++ /dev/null @@ -1,233 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0-or-later - -package org.dolphinemu.dolphinemu.utils - -import org.dolphinemu.dolphinemu.utils.CoverHelper.buildGameTDBUrl -import org.dolphinemu.dolphinemu.utils.CoverHelper.getRegion -import org.dolphinemu.dolphinemu.utils.CoverHelper.saveCover -import android.os.Looper -import org.dolphinemu.dolphinemu.model.GameFile -import com.bumptech.glide.Glide -import com.bumptech.glide.load.engine.DiskCacheStrategy -import org.dolphinemu.dolphinemu.R -import org.dolphinemu.dolphinemu.adapters.GameAdapter.GameViewHolder -import android.app.Activity -import android.graphics.Bitmap -import android.graphics.drawable.BitmapDrawable -import org.dolphinemu.dolphinemu.features.settings.model.BooleanSetting -import com.bumptech.glide.request.RequestListener -import android.graphics.drawable.Drawable -import android.net.Uri -import android.os.Handler -import android.view.View -import android.widget.ImageView -import com.bumptech.glide.load.DataSource -import com.bumptech.glide.load.engine.GlideException -import com.bumptech.glide.request.target.CustomTarget -import com.bumptech.glide.request.target.Target -import com.bumptech.glide.request.transition.Transition -import java.io.File -import java.io.FileNotFoundException -import java.util.concurrent.Executors - -object GlideUtils { - private val saveCoverExecutor = Executors.newSingleThreadExecutor() - private val unmangleExecutor = Executors.newSingleThreadExecutor() - private val unmangleHandler = Handler(Looper.getMainLooper()) - - fun loadGameBanner(imageView: ImageView, gameFile: GameFile) { - val context = imageView.context - val vector = gameFile.banner - val width = gameFile.bannerWidth - val height = gameFile.bannerHeight - if (width > 0 && height > 0) { - val bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888) - bitmap.setPixels(vector, 0, width, 0, 0, width, height) - Glide.with(context) - .load(bitmap) - .diskCacheStrategy(DiskCacheStrategy.NONE) - .centerCrop() - .into(imageView) - } else { - Glide.with(context) - .load(R.drawable.no_banner) - .into(imageView) - } - } - - fun loadGameCover( - gameViewHolder: GameViewHolder?, - imageView: ImageView, - gameFile: GameFile, - activity: Activity? - ) { - gameViewHolder?.apply { - if (BooleanSetting.MAIN_SHOW_GAME_TITLES.booleanGlobal) { - binding.textGameTitle.text = gameFile.title - binding.textGameTitle.visibility = View.VISIBLE - binding.textGameTitleInner.visibility = View.GONE - binding.textGameCaption.visibility = View.VISIBLE - } else { - binding.textGameTitleInner.text = gameFile.title - binding.textGameTitle.visibility = View.GONE - binding.textGameCaption.visibility = View.GONE - } - } - - unmangleExecutor.execute { - val customCoverPath = gameFile.customCoverPath - var customCoverUri: Uri? = null - var customCoverExists = false - if (ContentHandler.isContentUri(customCoverPath)) { - try { - customCoverUri = ContentHandler.unmangle(customCoverPath) - customCoverExists = true - } catch (ignored: FileNotFoundException) { - // Let customCoverExists remain false - } catch (ignored: SecurityException) { - } - } else { - customCoverUri = Uri.parse(customCoverPath) - customCoverExists = File(customCoverPath).exists() - } - - val context = imageView.context - val finalCustomCoverExists = customCoverExists - val finalCustomCoverUri = customCoverUri - - val cover = File(gameFile.getCoverPath(context)) - val cachedCoverExists = cover.exists() - unmangleHandler.post { - // We can't get a reference to the current activity in the TV version. - // Luckily it won't attempt to start loads on destroyed activities. - if (activity != null) { - // We can't start an image load on a destroyed activity - if (activity.isDestroyed) { - return@post - } - } - - if (finalCustomCoverExists) { - Glide.with(imageView) - .load(finalCustomCoverUri) - .diskCacheStrategy(DiskCacheStrategy.NONE) - .centerCrop() - .error(R.drawable.no_banner) - .listener(object : RequestListener { - override fun onLoadFailed( - e: GlideException?, - model: Any, - target: Target, - isFirstResource: Boolean - ): Boolean { - enableInnerTitle(gameViewHolder) - return false - } - - override fun onResourceReady( - resource: Drawable?, - model: Any, - target: Target, - dataSource: DataSource, - isFirstResource: Boolean - ): Boolean { - disableInnerTitle(gameViewHolder) - return false - } - }) - .into(imageView) - } else if (cachedCoverExists) { - Glide.with(imageView) - .load(cover) - .diskCacheStrategy(DiskCacheStrategy.NONE) - .centerCrop() - .error(R.drawable.no_banner) - .listener(object : RequestListener { - override fun onLoadFailed( - e: GlideException?, - model: Any, - target: Target, - isFirstResource: Boolean - ): Boolean { - enableInnerTitle(gameViewHolder) - return false - } - - override fun onResourceReady( - resource: Drawable?, - model: Any, - target: Target, - dataSource: DataSource, - isFirstResource: Boolean - ): Boolean { - disableInnerTitle(gameViewHolder) - return false - } - }) - .into(imageView) - } else if (BooleanSetting.MAIN_USE_GAME_COVERS.booleanGlobal) { - Glide.with(context) - .load(buildGameTDBUrl(gameFile, getRegion(gameFile))) - .diskCacheStrategy(DiskCacheStrategy.NONE) - .centerCrop() - .error(R.drawable.no_banner) - .listener(object : RequestListener { - override fun onLoadFailed( - e: GlideException?, - model: Any, - target: Target, - isFirstResource: Boolean - ): Boolean { - enableInnerTitle(gameViewHolder) - return false - } - - override fun onResourceReady( - resource: Drawable?, - model: Any, - target: Target, - dataSource: DataSource, - isFirstResource: Boolean - ): Boolean { - disableInnerTitle(gameViewHolder) - return false - } - }) - .into(object : CustomTarget() { - override fun onLoadCleared(placeholder: Drawable?) {} - override fun onResourceReady( - resource: Drawable, - transition: Transition? - ) { - val savedCover = (resource as BitmapDrawable).bitmap - saveCoverExecutor.execute { - saveCover( - savedCover, - gameFile.getCoverPath(context) - ) - } - imageView.setImageBitmap(savedCover) - } - }) - } else { - Glide.with(imageView.context) - .load(R.drawable.no_banner) - .into(imageView) - enableInnerTitle(gameViewHolder) - } - } - } - } - - private fun enableInnerTitle(gameViewHolder: GameViewHolder?) { - if (gameViewHolder != null && !BooleanSetting.MAIN_SHOW_GAME_TITLES.booleanGlobal) { - gameViewHolder.binding.textGameTitleInner.visibility = View.VISIBLE - } - } - - private fun disableInnerTitle(gameViewHolder: GameViewHolder?) { - if (gameViewHolder != null && !BooleanSetting.MAIN_SHOW_GAME_TITLES.booleanGlobal) { - gameViewHolder.binding.textGameTitleInner.visibility = View.GONE - } - } -} diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/TvUtil.java b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/TvUtil.java index 940542ef49..57c567715c 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/TvUtil.java +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/TvUtil.java @@ -186,9 +186,9 @@ public class TvUtil } } - if (contentUri == null && (cover = new File(game.getCoverPath(context))).exists()) + if (contentUri == null) { - contentUri = getUriForFile(context, getFileProvider(context), cover); + contentUri = Uri.parse(CoverHelper.buildGameTDBUrl(game, CoverHelper.getRegion(game))); } context.grantUriPermission(LEANBACK_PACKAGE, contentUri, FLAG_GRANT_READ_URI_PERMISSION);