android: Convert GameAdapter to Kotlin
parent
87f4c3f105
commit
0d044e9f2f
@ -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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue