android: Convert GameAdapter to Kotlin

This commit is contained in:
Charles Lombardo 2023-03-07 13:46:11 -05:00 committed by bunnei
parent 87f4c3f105
commit 0d044e9f2f
2 changed files with 178 additions and 244 deletions

View File

@ -1,244 +0,0 @@
package org.yuzu.yuzu_emu.adapters;
import android.database.Cursor;
import android.database.DataSetObserver;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.os.Build;
import android.os.SystemClock;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.annotation.RequiresApi;
import androidx.core.content.ContextCompat;
import androidx.fragment.app.FragmentActivity;
import androidx.recyclerview.widget.RecyclerView;
import org.yuzu.yuzu_emu.YuzuApplication;
import org.yuzu.yuzu_emu.R;
import org.yuzu.yuzu_emu.activities.EmulationActivity;
import org.yuzu.yuzu_emu.model.GameDatabase;
import org.yuzu.yuzu_emu.ui.DividerItemDecoration;
import org.yuzu.yuzu_emu.utils.FileUtil;
import org.yuzu.yuzu_emu.utils.Log;
import org.yuzu.yuzu_emu.utils.PicassoUtils;
import org.yuzu.yuzu_emu.viewholders.GameViewHolder;
import java.util.stream.Stream;
/**
* This adapter gets its information from a database Cursor. This fact, paired with the usage of
* ContentProviders and Loaders, allows for efficient display of a limited view into a (possibly)
* large dataset.
*/
public final class GameAdapter extends RecyclerView.Adapter<GameViewHolder> implements
View.OnClickListener {
private Cursor mCursor;
private GameDataSetObserver mObserver;
private boolean mDatasetValid;
private long mLastClickTime = 0;
/**
* Initializes the adapter's observer, which watches for changes to the dataset. The adapter will
* display no data until a Cursor is supplied by a CursorLoader.
*/
public GameAdapter() {
mDatasetValid = false;
mObserver = new GameDataSetObserver();
}
/**
* 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
public GameViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
// Create a new view.
View gameCard = LayoutInflater.from(parent.getContext())
.inflate(R.layout.card_game, parent, false);
gameCard.setOnClickListener(this);
// Use that view to create a ViewHolder.
return new GameViewHolder(gameCard);
}
/**
* 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.
*/
@RequiresApi(api = Build.VERSION_CODES.O)
@Override
public void onBindViewHolder(@NonNull GameViewHolder holder, int position) {
if (mDatasetValid) {
if (mCursor.moveToPosition(position)) {
PicassoUtils.loadGameIcon(holder.imageIcon,
mCursor.getString(GameDatabase.GAME_COLUMN_PATH));
holder.textGameTitle.setText(mCursor.getString(GameDatabase.GAME_COLUMN_TITLE).replaceAll("[\\t\\n\\r]+", " "));
holder.textGameCaption.setText(mCursor.getString(GameDatabase.GAME_COLUMN_CAPTION));
// TODO These shouldn't be necessary once the move to a DB-based model is complete.
holder.gameId = mCursor.getString(GameDatabase.GAME_COLUMN_GAME_ID);
holder.path = mCursor.getString(GameDatabase.GAME_COLUMN_PATH);
holder.title = mCursor.getString(GameDatabase.GAME_COLUMN_TITLE);
holder.description = mCursor.getString(GameDatabase.GAME_COLUMN_DESCRIPTION);
holder.regions = mCursor.getString(GameDatabase.GAME_COLUMN_REGIONS);
holder.company = mCursor.getString(GameDatabase.GAME_COLUMN_CAPTION);
final int backgroundColorId = isValidGame(holder.path) ? R.color.view_background : R.color.view_disabled;
View itemView = holder.getItemView();
itemView.setBackgroundColor(ContextCompat.getColor(itemView.getContext(), backgroundColorId));
} else {
Log.error("[GameAdapter] Can't bind view; Cursor is not valid.");
}
} else {
Log.error("[GameAdapter] Can't bind view; dataset is not valid.");
}
}
/**
* Called by the LayoutManager to find out how much data we have.
*
* @return Size of the dataset.
*/
@Override
public int getItemCount() {
if (mDatasetValid && mCursor != null) {
return mCursor.getCount();
}
Log.error("[GameAdapter] Dataset is not valid.");
return 0;
}
/**
* Return the contents of the _id column for a given row.
*
* @param position The row for which Android wants an ID.
* @return A valid ID from the database, or 0 if not available.
*/
@Override
public long getItemId(int position) {
if (mDatasetValid && mCursor != null) {
if (mCursor.moveToPosition(position)) {
return mCursor.getLong(GameDatabase.COLUMN_DB_ID);
}
}
Log.error("[GameAdapter] Dataset is not valid.");
return 0;
}
/**
* Tell Android whether or not each item in the dataset has a stable identifier.
* Which it does, because it's a database, so always tell Android 'true'.
*
* @param hasStableIds ignored.
*/
@Override
public void setHasStableIds(boolean hasStableIds) {
super.setHasStableIds(true);
}
/**
* When a load is finished, call this to replace the existing data with the newly-loaded
* data.
*
* @param cursor The newly-loaded Cursor.
*/
public void swapCursor(Cursor cursor) {
// Sanity check.
if (cursor == mCursor) {
return;
}
// Before getting rid of the old cursor, disassociate it from the Observer.
final Cursor oldCursor = mCursor;
if (oldCursor != null && mObserver != null) {
oldCursor.unregisterDataSetObserver(mObserver);
}
mCursor = cursor;
if (mCursor != null) {
// Attempt to associate the new Cursor with the Observer.
if (mObserver != null) {
mCursor.registerDataSetObserver(mObserver);
}
mDatasetValid = true;
} else {
mDatasetValid = false;
}
notifyDataSetChanged();
}
/**
* 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) {
// Double-click prevention, using threshold of 1000 ms
if (SystemClock.elapsedRealtime() - mLastClickTime < 1000) {
return;
}
mLastClickTime = SystemClock.elapsedRealtime();
GameViewHolder holder = (GameViewHolder) view.getTag();
EmulationActivity.launch((FragmentActivity) view.getContext(), holder.path, holder.title);
}
public static class SpacesItemDecoration extends DividerItemDecoration {
private int space;
public SpacesItemDecoration(Drawable divider, int space) {
super(divider);
this.space = space;
}
@Override
public void getItemOffsets(Rect outRect, @NonNull View view, @NonNull RecyclerView parent,
@NonNull RecyclerView.State state) {
outRect.left = 0;
outRect.right = 0;
outRect.bottom = space;
outRect.top = 0;
}
}
private boolean isValidGame(String path) {
return Stream.of(
".rar", ".zip", ".7z", ".torrent", ".tar", ".gz").noneMatch(suffix -> path.toLowerCase().endsWith(suffix));
}
private final class GameDataSetObserver extends DataSetObserver {
@Override
public void onChanged() {
super.onChanged();
mDatasetValid = true;
notifyDataSetChanged();
}
@Override
public void onInvalidated() {
super.onInvalidated();
mDatasetValid = false;
notifyDataSetChanged();
}
}
}

View File

@ -0,0 +1,178 @@
package org.yuzu.yuzu_emu.adapters
import android.database.Cursor
import android.database.DataSetObserver
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.core.content.ContextCompat
import androidx.fragment.app.FragmentActivity
import androidx.recyclerview.widget.RecyclerView
import org.yuzu.yuzu_emu.R
import org.yuzu.yuzu_emu.activities.EmulationActivity.Companion.launch
import org.yuzu.yuzu_emu.model.GameDatabase
import org.yuzu.yuzu_emu.utils.Log
import org.yuzu.yuzu_emu.utils.PicassoUtils
import org.yuzu.yuzu_emu.viewholders.GameViewHolder
import java.util.*
import java.util.stream.Stream
/**
* This adapter gets its information from a database Cursor. This fact, paired with the usage of
* ContentProviders and Loaders, allows for efficient display of a limited view into a (possibly)
* large dataset.
*/
class GameAdapter : RecyclerView.Adapter<GameViewHolder>(), View.OnClickListener {
private var cursor: Cursor? = null
private val observer: GameDataSetObserver?
private var isDatasetValid = false
/**
* Initializes the adapter's observer, which watches for changes to the dataset. The adapter will
* display no data until a Cursor is supplied by a CursorLoader.
*/
init {
observer = GameDataSetObserver()
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): GameViewHolder {
// Create a new view.
val gameCard = LayoutInflater.from(parent.context)
.inflate(R.layout.card_game, parent, false)
gameCard.setOnClickListener(this)
// Use that view to create a ViewHolder.
return GameViewHolder(gameCard)
}
override fun onBindViewHolder(holder: GameViewHolder, position: Int) {
if (isDatasetValid) {
if (cursor!!.moveToPosition(position)) {
PicassoUtils.loadGameIcon(
holder.imageIcon,
cursor!!.getString(GameDatabase.GAME_COLUMN_PATH)
)
holder.textGameTitle.text =
cursor!!.getString(GameDatabase.GAME_COLUMN_TITLE)
.replace("[\\t\\n\\r]+".toRegex(), " ")
holder.textGameCaption.text = cursor!!.getString(GameDatabase.GAME_COLUMN_CAPTION)
// TODO These shouldn't be necessary once the move to a DB-based model is complete.
holder.gameId = cursor!!.getString(GameDatabase.GAME_COLUMN_GAME_ID)
holder.path = cursor!!.getString(GameDatabase.GAME_COLUMN_PATH)
holder.title = cursor!!.getString(GameDatabase.GAME_COLUMN_TITLE)
holder.description = cursor!!.getString(GameDatabase.GAME_COLUMN_DESCRIPTION)
holder.regions = cursor!!.getString(GameDatabase.GAME_COLUMN_REGIONS)
holder.company = cursor!!.getString(GameDatabase.GAME_COLUMN_CAPTION)
val backgroundColorId =
if (isValidGame(holder.path!!)) R.color.view_background else R.color.view_disabled
val itemView = holder.itemView
itemView.setBackgroundColor(
ContextCompat.getColor(
itemView.context,
backgroundColorId
)
)
} else {
Log.error("[GameAdapter] Can't bind view; Cursor is not valid.")
}
} else {
Log.error("[GameAdapter] Can't bind view; dataset is not valid.")
}
}
override fun getItemCount(): Int {
if (isDatasetValid && cursor != null) {
return cursor!!.count
}
Log.error("[GameAdapter] Dataset is not valid.")
return 0
}
/**
* Return the contents of the _id column for a given row.
*
* @param position The row for which Android wants an ID.
* @return A valid ID from the database, or 0 if not available.
*/
override fun getItemId(position: Int): Long {
if (isDatasetValid && cursor != null) {
if (cursor!!.moveToPosition(position)) {
return cursor!!.getLong(GameDatabase.COLUMN_DB_ID)
}
}
Log.error("[GameAdapter] Dataset is not valid.")
return 0
}
/**
* Tell Android whether or not each item in the dataset has a stable identifier.
* Which it does, because it's a database, so always tell Android 'true'.
*
* @param hasStableIds ignored.
*/
override fun setHasStableIds(hasStableIds: Boolean) {
super.setHasStableIds(true)
}
/**
* When a load is finished, call this to replace the existing data with the newly-loaded
* data.
*
* @param cursor The newly-loaded Cursor.
*/
fun swapCursor(cursor: Cursor) {
// Sanity check.
if (cursor === this.cursor) {
return
}
// Before getting rid of the old cursor, disassociate it from the Observer.
val oldCursor = this.cursor
if (oldCursor != null && observer != null) {
oldCursor.unregisterDataSetObserver(observer)
}
this.cursor = cursor
isDatasetValid = if (this.cursor != null) {
// Attempt to associate the new Cursor with the Observer.
if (observer != null) {
this.cursor!!.registerDataSetObserver(observer)
}
true
} else {
false
}
notifyDataSetChanged()
}
/**
* 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
launch((view.context as FragmentActivity), holder.path, holder.title)
}
private fun isValidGame(path: String): Boolean {
return Stream.of(".rar", ".zip", ".7z", ".torrent", ".tar", ".gz")
.noneMatch { suffix: String? ->
path.lowercase(Locale.getDefault()).endsWith(suffix!!)
}
}
private inner class GameDataSetObserver : DataSetObserver() {
override fun onChanged() {
super.onChanged()
isDatasetValid = true
notifyDataSetChanged()
}
override fun onInvalidated() {
super.onInvalidated()
isDatasetValid = false
notifyDataSetChanged()
}
}
}