committed by
bunnei
2 changed files with 178 additions and 244 deletions
-
244src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/GameAdapter.java
-
178src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/GameAdapter.kt
@ -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() |
||||
|
} |
||||
|
} |
||||
|
} |
||||
Write
Preview
Loading…
Cancel
Save
Reference in new issue