summaryrefslogtreecommitdiff
path: root/src/android
diff options
context:
space:
mode:
authorGravatar Charles Lombardo2023-03-07 13:46:11 -0500
committerGravatar bunnei2023-06-03 00:05:35 -0700
commit0d044e9f2f8883416e50210b0205b76e1e99111a (patch)
tree9bfedc5ae290cd7e1e16195047996271a5702be7 /src/android
parentandroid: Convert SettingsAdapter to Kotlin (diff)
downloadyuzu-0d044e9f2f8883416e50210b0205b76e1e99111a.tar.gz
yuzu-0d044e9f2f8883416e50210b0205b76e1e99111a.tar.xz
yuzu-0d044e9f2f8883416e50210b0205b76e1e99111a.zip
android: Convert GameAdapter to Kotlin
Diffstat (limited to 'src/android')
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/GameAdapter.java244
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/GameAdapter.kt178
2 files changed, 178 insertions, 244 deletions
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 ed1a000c7..000000000
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/GameAdapter.java
+++ /dev/null
@@ -1,244 +0,0 @@
1package org.yuzu.yuzu_emu.adapters;
2
3import android.database.Cursor;
4import android.database.DataSetObserver;
5import android.graphics.Rect;
6import android.graphics.drawable.Drawable;
7import android.os.Build;
8import android.os.SystemClock;
9import android.view.LayoutInflater;
10import android.view.View;
11import android.view.ViewGroup;
12
13import androidx.annotation.NonNull;
14import androidx.annotation.RequiresApi;
15import androidx.core.content.ContextCompat;
16import androidx.fragment.app.FragmentActivity;
17import androidx.recyclerview.widget.RecyclerView;
18
19import org.yuzu.yuzu_emu.YuzuApplication;
20import org.yuzu.yuzu_emu.R;
21import org.yuzu.yuzu_emu.activities.EmulationActivity;
22import org.yuzu.yuzu_emu.model.GameDatabase;
23import org.yuzu.yuzu_emu.ui.DividerItemDecoration;
24import org.yuzu.yuzu_emu.utils.FileUtil;
25import org.yuzu.yuzu_emu.utils.Log;
26import org.yuzu.yuzu_emu.utils.PicassoUtils;
27import org.yuzu.yuzu_emu.viewholders.GameViewHolder;
28
29import java.util.stream.Stream;
30
31/**
32 * This adapter gets its information from a database Cursor. This fact, paired with the usage of
33 * ContentProviders and Loaders, allows for efficient display of a limited view into a (possibly)
34 * large dataset.
35 */
36public final class GameAdapter extends RecyclerView.Adapter<GameViewHolder> implements
37 View.OnClickListener {
38 private Cursor mCursor;
39 private GameDataSetObserver mObserver;
40
41 private boolean mDatasetValid;
42 private long mLastClickTime = 0;
43
44 /**
45 * Initializes the adapter's observer, which watches for changes to the dataset. The adapter will
46 * display no data until a Cursor is supplied by a CursorLoader.
47 */
48 public GameAdapter() {
49 mDatasetValid = false;
50 mObserver = new GameDataSetObserver();
51 }
52
53 /**
54 * Called by the LayoutManager when it is necessary to create a new view.
55 *
56 * @param parent The RecyclerView (I think?) the created view will be thrown into.
57 * @param viewType Not used here, but useful when more than one type of child will be used in the RecyclerView.
58 * @return The created ViewHolder with references to all the child view's members.
59 */
60 @Override
61 public GameViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
62 // Create a new view.
63 View gameCard = LayoutInflater.from(parent.getContext())
64 .inflate(R.layout.card_game, parent, false);
65
66 gameCard.setOnClickListener(this);
67
68 // Use that view to create a ViewHolder.
69 return new GameViewHolder(gameCard);
70 }
71
72 /**
73 * Called by the LayoutManager when a new view is not necessary because we can recycle
74 * an existing one (for example, if a view just scrolled onto the screen from the bottom, we
75 * can use the view that just scrolled off the top instead of inflating a new one.)
76 *
77 * @param holder A ViewHolder representing the view we're recycling.
78 * @param position The position of the 'new' view in the dataset.
79 */
80 @RequiresApi(api = Build.VERSION_CODES.O)
81 @Override
82 public void onBindViewHolder(@NonNull GameViewHolder holder, int position) {
83 if (mDatasetValid) {
84 if (mCursor.moveToPosition(position)) {
85 PicassoUtils.loadGameIcon(holder.imageIcon,
86 mCursor.getString(GameDatabase.GAME_COLUMN_PATH));
87
88 holder.textGameTitle.setText(mCursor.getString(GameDatabase.GAME_COLUMN_TITLE).replaceAll("[\\t\\n\\r]+", " "));
89 holder.textGameCaption.setText(mCursor.getString(GameDatabase.GAME_COLUMN_CAPTION));
90
91 // TODO These shouldn't be necessary once the move to a DB-based model is complete.
92 holder.gameId = mCursor.getString(GameDatabase.GAME_COLUMN_GAME_ID);
93 holder.path = mCursor.getString(GameDatabase.GAME_COLUMN_PATH);
94 holder.title = mCursor.getString(GameDatabase.GAME_COLUMN_TITLE);
95 holder.description = mCursor.getString(GameDatabase.GAME_COLUMN_DESCRIPTION);
96 holder.regions = mCursor.getString(GameDatabase.GAME_COLUMN_REGIONS);
97 holder.company = mCursor.getString(GameDatabase.GAME_COLUMN_CAPTION);
98
99 final int backgroundColorId = isValidGame(holder.path) ? R.color.view_background : R.color.view_disabled;
100 View itemView = holder.getItemView();
101 itemView.setBackgroundColor(ContextCompat.getColor(itemView.getContext(), backgroundColorId));
102 } else {
103 Log.error("[GameAdapter] Can't bind view; Cursor is not valid.");
104 }
105 } else {
106 Log.error("[GameAdapter] Can't bind view; dataset is not valid.");
107 }
108 }
109
110 /**
111 * Called by the LayoutManager to find out how much data we have.
112 *
113 * @return Size of the dataset.
114 */
115 @Override
116 public int getItemCount() {
117 if (mDatasetValid && mCursor != null) {
118 return mCursor.getCount();
119 }
120 Log.error("[GameAdapter] Dataset is not valid.");
121 return 0;
122 }
123
124 /**
125 * Return the contents of the _id column for a given row.
126 *
127 * @param position The row for which Android wants an ID.
128 * @return A valid ID from the database, or 0 if not available.
129 */
130 @Override
131 public long getItemId(int position) {
132 if (mDatasetValid && mCursor != null) {
133 if (mCursor.moveToPosition(position)) {
134 return mCursor.getLong(GameDatabase.COLUMN_DB_ID);
135 }
136 }
137
138 Log.error("[GameAdapter] Dataset is not valid.");
139 return 0;
140 }
141
142 /**
143 * Tell Android whether or not each item in the dataset has a stable identifier.
144 * Which it does, because it's a database, so always tell Android 'true'.
145 *
146 * @param hasStableIds ignored.
147 */
148 @Override
149 public void setHasStableIds(boolean hasStableIds) {
150 super.setHasStableIds(true);
151 }
152
153 /**
154 * When a load is finished, call this to replace the existing data with the newly-loaded
155 * data.
156 *
157 * @param cursor The newly-loaded Cursor.
158 */
159 public void swapCursor(Cursor cursor) {
160 // Sanity check.
161 if (cursor == mCursor) {
162 return;
163 }
164
165 // Before getting rid of the old cursor, disassociate it from the Observer.
166 final Cursor oldCursor = mCursor;
167 if (oldCursor != null && mObserver != null) {
168 oldCursor.unregisterDataSetObserver(mObserver);
169 }
170
171 mCursor = cursor;
172 if (mCursor != null) {
173 // Attempt to associate the new Cursor with the Observer.
174 if (mObserver != null) {
175 mCursor.registerDataSetObserver(mObserver);
176 }
177
178 mDatasetValid = true;
179 } else {
180 mDatasetValid = false;
181 }
182
183 notifyDataSetChanged();
184 }
185
186 /**
187 * Launches the game that was clicked on.
188 *
189 * @param view The card representing the game the user wants to play.
190 */
191 @Override
192 public void onClick(View view) {
193 // Double-click prevention, using threshold of 1000 ms
194 if (SystemClock.elapsedRealtime() - mLastClickTime < 1000) {
195 return;
196 }
197 mLastClickTime = SystemClock.elapsedRealtime();
198
199 GameViewHolder holder = (GameViewHolder) view.getTag();
200
201 EmulationActivity.launch((FragmentActivity) view.getContext(), holder.path, holder.title);
202 }
203
204 public static class SpacesItemDecoration extends DividerItemDecoration {
205 private int space;
206
207 public SpacesItemDecoration(Drawable divider, int space) {
208 super(divider);
209 this.space = space;
210 }
211
212 @Override
213 public void getItemOffsets(Rect outRect, @NonNull View view, @NonNull RecyclerView parent,
214 @NonNull RecyclerView.State state) {
215 outRect.left = 0;
216 outRect.right = 0;
217 outRect.bottom = space;
218 outRect.top = 0;
219 }
220 }
221
222 private boolean isValidGame(String path) {
223 return Stream.of(
224 ".rar", ".zip", ".7z", ".torrent", ".tar", ".gz").noneMatch(suffix -> path.toLowerCase().endsWith(suffix));
225 }
226
227 private final class GameDataSetObserver extends DataSetObserver {
228 @Override
229 public void onChanged() {
230 super.onChanged();
231
232 mDatasetValid = true;
233 notifyDataSetChanged();
234 }
235
236 @Override
237 public void onInvalidated() {
238 super.onInvalidated();
239
240 mDatasetValid = false;
241 notifyDataSetChanged();
242 }
243 }
244}
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 000000000..b3761166d
--- /dev/null
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/GameAdapter.kt
@@ -0,0 +1,178 @@
1package org.yuzu.yuzu_emu.adapters
2
3import android.database.Cursor
4import android.database.DataSetObserver
5import android.view.LayoutInflater
6import android.view.View
7import android.view.ViewGroup
8import androidx.core.content.ContextCompat
9import androidx.fragment.app.FragmentActivity
10import androidx.recyclerview.widget.RecyclerView
11import org.yuzu.yuzu_emu.R
12import org.yuzu.yuzu_emu.activities.EmulationActivity.Companion.launch
13import org.yuzu.yuzu_emu.model.GameDatabase
14import org.yuzu.yuzu_emu.utils.Log
15import org.yuzu.yuzu_emu.utils.PicassoUtils
16import org.yuzu.yuzu_emu.viewholders.GameViewHolder
17import java.util.*
18import java.util.stream.Stream
19
20/**
21 * This adapter gets its information from a database Cursor. This fact, paired with the usage of
22 * ContentProviders and Loaders, allows for efficient display of a limited view into a (possibly)
23 * large dataset.
24 */
25class GameAdapter : RecyclerView.Adapter<GameViewHolder>(), View.OnClickListener {
26 private var cursor: Cursor? = null
27 private val observer: GameDataSetObserver?
28 private var isDatasetValid = false
29
30 /**
31 * Initializes the adapter's observer, which watches for changes to the dataset. The adapter will
32 * display no data until a Cursor is supplied by a CursorLoader.
33 */
34 init {
35 observer = GameDataSetObserver()
36 }
37
38 override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): GameViewHolder {
39 // Create a new view.
40 val gameCard = LayoutInflater.from(parent.context)
41 .inflate(R.layout.card_game, parent, false)
42 gameCard.setOnClickListener(this)
43
44 // Use that view to create a ViewHolder.
45 return GameViewHolder(gameCard)
46 }
47
48 override fun onBindViewHolder(holder: GameViewHolder, position: Int) {
49 if (isDatasetValid) {
50 if (cursor!!.moveToPosition(position)) {
51 PicassoUtils.loadGameIcon(
52 holder.imageIcon,
53 cursor!!.getString(GameDatabase.GAME_COLUMN_PATH)
54 )
55 holder.textGameTitle.text =
56 cursor!!.getString(GameDatabase.GAME_COLUMN_TITLE)
57 .replace("[\\t\\n\\r]+".toRegex(), " ")
58 holder.textGameCaption.text = cursor!!.getString(GameDatabase.GAME_COLUMN_CAPTION)
59
60 // TODO These shouldn't be necessary once the move to a DB-based model is complete.
61 holder.gameId = cursor!!.getString(GameDatabase.GAME_COLUMN_GAME_ID)
62 holder.path = cursor!!.getString(GameDatabase.GAME_COLUMN_PATH)
63 holder.title = cursor!!.getString(GameDatabase.GAME_COLUMN_TITLE)
64 holder.description = cursor!!.getString(GameDatabase.GAME_COLUMN_DESCRIPTION)
65 holder.regions = cursor!!.getString(GameDatabase.GAME_COLUMN_REGIONS)
66 holder.company = cursor!!.getString(GameDatabase.GAME_COLUMN_CAPTION)
67 val backgroundColorId =
68 if (isValidGame(holder.path!!)) R.color.view_background else R.color.view_disabled
69 val itemView = holder.itemView
70 itemView.setBackgroundColor(
71 ContextCompat.getColor(
72 itemView.context,
73 backgroundColorId
74 )
75 )
76 } else {
77 Log.error("[GameAdapter] Can't bind view; Cursor is not valid.")
78 }
79 } else {
80 Log.error("[GameAdapter] Can't bind view; dataset is not valid.")
81 }
82 }
83
84 override fun getItemCount(): Int {
85 if (isDatasetValid && cursor != null) {
86 return cursor!!.count
87 }
88 Log.error("[GameAdapter] Dataset is not valid.")
89 return 0
90 }
91
92 /**
93 * Return the contents of the _id column for a given row.
94 *
95 * @param position The row for which Android wants an ID.
96 * @return A valid ID from the database, or 0 if not available.
97 */
98 override fun getItemId(position: Int): Long {
99 if (isDatasetValid && cursor != null) {
100 if (cursor!!.moveToPosition(position)) {
101 return cursor!!.getLong(GameDatabase.COLUMN_DB_ID)
102 }
103 }
104 Log.error("[GameAdapter] Dataset is not valid.")
105 return 0
106 }
107
108 /**
109 * Tell Android whether or not each item in the dataset has a stable identifier.
110 * Which it does, because it's a database, so always tell Android 'true'.
111 *
112 * @param hasStableIds ignored.
113 */
114 override fun setHasStableIds(hasStableIds: Boolean) {
115 super.setHasStableIds(true)
116 }
117
118 /**
119 * When a load is finished, call this to replace the existing data with the newly-loaded
120 * data.
121 *
122 * @param cursor The newly-loaded Cursor.
123 */
124 fun swapCursor(cursor: Cursor) {
125 // Sanity check.
126 if (cursor === this.cursor) {
127 return
128 }
129
130 // Before getting rid of the old cursor, disassociate it from the Observer.
131 val oldCursor = this.cursor
132 if (oldCursor != null && observer != null) {
133 oldCursor.unregisterDataSetObserver(observer)
134 }
135 this.cursor = cursor
136 isDatasetValid = if (this.cursor != null) {
137 // Attempt to associate the new Cursor with the Observer.
138 if (observer != null) {
139 this.cursor!!.registerDataSetObserver(observer)
140 }
141 true
142 } else {
143 false
144 }
145 notifyDataSetChanged()
146 }
147
148 /**
149 * Launches the game that was clicked on.
150 *
151 * @param view The card representing the game the user wants to play.
152 */
153 override fun onClick(view: View) {
154 val holder = view.tag as GameViewHolder
155 launch((view.context as FragmentActivity), holder.path, holder.title)
156 }
157
158 private fun isValidGame(path: String): Boolean {
159 return Stream.of(".rar", ".zip", ".7z", ".torrent", ".tar", ".gz")
160 .noneMatch { suffix: String? ->
161 path.lowercase(Locale.getDefault()).endsWith(suffix!!)
162 }
163 }
164
165 private inner class GameDataSetObserver : DataSetObserver() {
166 override fun onChanged() {
167 super.onChanged()
168 isDatasetValid = true
169 notifyDataSetChanged()
170 }
171
172 override fun onInvalidated() {
173 super.onInvalidated()
174 isDatasetValid = false
175 notifyDataSetChanged()
176 }
177 }
178}