Android: Convert image loading code to Kotlin

This commit is contained in:
Charles Lombardo 2023-01-06 14:58:31 -05:00
parent 72b22ef0a5
commit e6583f8bec
12 changed files with 683 additions and 809 deletions

View File

@ -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<GameAdapter.GameViewHolder> implements
View.OnClickListener,
View.OnLongClickListener
{
private List<GameFile> 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<GameFile> 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;
}
}

View File

@ -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<GameViewHolder>(),
View.OnClickListener, OnLongClickListener {
private var mGameFiles: List<GameFile>
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<GameFile>) {
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
}
}

View File

@ -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
}
}

View File

@ -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
}
}

View File

@ -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();
}
}

View File

@ -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
}
}
}

View File

@ -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)
{
}
}
}

View File

@ -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) {
}
}
}

View File

@ -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<Drawable>()
{
@Override public boolean onLoadFailed(@Nullable GlideException e, Object model,
Target<Drawable> target, boolean isFirstResource)
{
GlideUtils.enableInnerTitle(gameViewHolder);
return false;
}
@Override public boolean onResourceReady(Drawable resource, Object model,
Target<Drawable> 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<Drawable>()
{
@Override
public boolean onLoadFailed(@Nullable GlideException e, Object model,
Target<Drawable> target, boolean isFirstResource)
{
GlideUtils.enableInnerTitle(gameViewHolder);
return false;
}
@Override
public boolean onResourceReady(Drawable resource, Object model,
Target<Drawable> 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<Drawable>()
{
@Override
public boolean onLoadFailed(@Nullable GlideException e, Object model,
Target<Drawable> target, boolean isFirstResource)
{
GlideUtils.enableInnerTitle(gameViewHolder);
return false;
}
@Override
public boolean onResourceReady(Drawable resource, Object model,
Target<Drawable> target, DataSource dataSource, boolean isFirstResource)
{
GlideUtils.disableInnerTitle(gameViewHolder);
return false;
}
})
.into(new CustomTarget<Drawable>()
{
@Override
public void onResourceReady(@NonNull Drawable resource,
@Nullable Transition<? super Drawable> 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);
}
}
}

View File

@ -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<Drawable?> {
override fun onLoadFailed(
e: GlideException?,
model: Any,
target: Target<Drawable?>,
isFirstResource: Boolean
): Boolean {
enableInnerTitle(gameViewHolder)
return false
}
override fun onResourceReady(
resource: Drawable?,
model: Any,
target: Target<Drawable?>,
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<Drawable?> {
override fun onLoadFailed(
e: GlideException?,
model: Any,
target: Target<Drawable?>,
isFirstResource: Boolean
): Boolean {
enableInnerTitle(gameViewHolder)
return false
}
override fun onResourceReady(
resource: Drawable?,
model: Any,
target: Target<Drawable?>,
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<Drawable?> {
override fun onLoadFailed(
e: GlideException?,
model: Any,
target: Target<Drawable?>,
isFirstResource: Boolean
): Boolean {
enableInnerTitle(gameViewHolder)
return false
}
override fun onResourceReady(
resource: Drawable?,
model: Any,
target: Target<Drawable?>,
dataSource: DataSource,
isFirstResource: Boolean
): Boolean {
disableInnerTitle(gameViewHolder)
return false
}
})
.into(object : CustomTarget<Drawable?>() {
override fun onLoadCleared(placeholder: Drawable?) {}
override fun onResourceReady(
resource: Drawable,
transition: Transition<in Drawable?>?
) {
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
}
}
}

View File

@ -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();
}
}

View File

@ -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
}
}