From 0d044e9f2f8883416e50210b0205b76e1e99111a Mon Sep 17 00:00:00 2001 From: Charles Lombardo Date: Tue, 7 Mar 2023 13:46:11 -0500 Subject: [PATCH] android: Convert GameAdapter to Kotlin --- .../yuzu/yuzu_emu/adapters/GameAdapter.java | 244 ------------------ .../org/yuzu/yuzu_emu/adapters/GameAdapter.kt | 178 +++++++++++++ 2 files changed, 178 insertions(+), 244 deletions(-) delete mode 100644 src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/GameAdapter.java create mode 100644 src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/GameAdapter.kt diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/GameAdapter.java b/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/GameAdapter.java deleted file mode 100644 index ed1a000c7b..0000000000 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/GameAdapter.java +++ /dev/null @@ -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 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(); - } - } -} diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/GameAdapter.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/GameAdapter.kt new file mode 100644 index 0000000000..b3761166de --- /dev/null +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/GameAdapter.kt @@ -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(), 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() + } + } +}