diff options
| author | 2023-04-03 05:01:54 -0400 | |
|---|---|---|
| committer | 2023-06-03 00:05:49 -0700 | |
| commit | 9d7a60346f46c02dccc5f19702c7a997676faa2e (patch) | |
| tree | dd8e36bb1d53c46eb3401f6914aac8bd9d74f1a6 /src/android | |
| parent | android: Adjust game icon loading (diff) | |
| download | yuzu-9d7a60346f46c02dccc5f19702c7a997676faa2e.tar.gz yuzu-9d7a60346f46c02dccc5f19702c7a997676faa2e.tar.xz yuzu-9d7a60346f46c02dccc5f19702c7a997676faa2e.zip | |
android: Remove game database
The content provider + database solution was excessive and is now replaced with the simple file checks from before but turned into an array list held within a viewmodel.
Diffstat (limited to 'src/android')
18 files changed, 154 insertions, 773 deletions
diff --git a/src/android/app/build.gradle b/src/android/app/build.gradle index 22f2d4b80..a82d2706b 100644 --- a/src/android/app/build.gradle +++ b/src/android/app/build.gradle | |||
| @@ -140,10 +140,6 @@ dependencies { | |||
| 140 | implementation "io.coil-kt:coil:2.2.2" | 140 | implementation "io.coil-kt:coil:2.2.2" |
| 141 | implementation 'androidx.core:core-splashscreen:1.0.0' | 141 | implementation 'androidx.core:core-splashscreen:1.0.0' |
| 142 | implementation 'androidx.window:window:1.0.0' | 142 | implementation 'androidx.window:window:1.0.0' |
| 143 | |||
| 144 | // Allows FRP-style asynchronous operations in Android. | ||
| 145 | implementation 'io.reactivex:rxandroid:1.2.1' | ||
| 146 | implementation 'com.nononsenseapps:filepicker:4.2.1' | ||
| 147 | implementation 'org.ini4j:ini4j:0.5.4' | 143 | implementation 'org.ini4j:ini4j:0.5.4' |
| 148 | implementation 'androidx.constraintlayout:constraintlayout:2.1.4' | 144 | implementation 'androidx.constraintlayout:constraintlayout:2.1.4' |
| 149 | implementation 'androidx.localbroadcastmanager:localbroadcastmanager:1.1.0' | 145 | implementation 'androidx.localbroadcastmanager:localbroadcastmanager:1.1.0' |
diff --git a/src/android/app/src/main/AndroidManifest.xml b/src/android/app/src/main/AndroidManifest.xml index a5c063d52..18539af80 100644 --- a/src/android/app/src/main/AndroidManifest.xml +++ b/src/android/app/src/main/AndroidManifest.xml | |||
| @@ -66,23 +66,6 @@ | |||
| 66 | <service android:name="org.yuzu.yuzu_emu.utils.ForegroundService"/> | 66 | <service android:name="org.yuzu.yuzu_emu.utils.ForegroundService"/> |
| 67 | 67 | ||
| 68 | <provider | 68 | <provider |
| 69 | android:name="org.yuzu.yuzu_emu.model.GameProvider" | ||
| 70 | android:authorities="${applicationId}.provider" | ||
| 71 | android:enabled="true" | ||
| 72 | android:exported="false"> | ||
| 73 | </provider> | ||
| 74 | |||
| 75 | <provider | ||
| 76 | android:name="androidx.core.content.FileProvider" | ||
| 77 | android:authorities="${applicationId}.filesprovider" | ||
| 78 | android:exported="false" | ||
| 79 | android:grantUriPermissions="true"> | ||
| 80 | <meta-data | ||
| 81 | android:name="android.support.FILE_PROVIDER_PATHS" | ||
| 82 | android:resource="@xml/nnf_provider_paths" /> | ||
| 83 | </provider> | ||
| 84 | |||
| 85 | <provider | ||
| 86 | android:name=".features.DocumentProvider" | 69 | android:name=".features.DocumentProvider" |
| 87 | android:authorities="${applicationId}.user" | 70 | android:authorities="${applicationId}.user" |
| 88 | android:grantUriPermissions="true" | 71 | android:grantUriPermissions="true" |
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/YuzuApplication.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/YuzuApplication.kt index a0c5c5c25..f81b4da40 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/YuzuApplication.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/YuzuApplication.kt | |||
| @@ -7,7 +7,6 @@ import android.app.Application | |||
| 7 | import android.app.NotificationChannel | 7 | import android.app.NotificationChannel |
| 8 | import android.app.NotificationManager | 8 | import android.app.NotificationManager |
| 9 | import android.content.Context | 9 | import android.content.Context |
| 10 | import org.yuzu.yuzu_emu.model.GameDatabase | ||
| 11 | import org.yuzu.yuzu_emu.utils.DirectoryInitialization | 10 | import org.yuzu.yuzu_emu.utils.DirectoryInitialization |
| 12 | import org.yuzu.yuzu_emu.utils.DocumentsTree | 11 | import org.yuzu.yuzu_emu.utils.DocumentsTree |
| 13 | import org.yuzu.yuzu_emu.utils.GpuDriverHelper | 12 | import org.yuzu.yuzu_emu.utils.GpuDriverHelper |
| @@ -45,12 +44,9 @@ class YuzuApplication : Application() { | |||
| 45 | 44 | ||
| 46 | // TODO(bunnei): Disable notifications until we support app suspension. | 45 | // TODO(bunnei): Disable notifications until we support app suspension. |
| 47 | //createNotificationChannel(); | 46 | //createNotificationChannel(); |
| 48 | databaseHelper = GameDatabase(this) | ||
| 49 | } | 47 | } |
| 50 | 48 | ||
| 51 | companion object { | 49 | companion object { |
| 52 | var databaseHelper: GameDatabase? = null | ||
| 53 | |||
| 54 | @JvmField | 50 | @JvmField |
| 55 | var documentsTree: DocumentsTree? = null | 51 | var documentsTree: DocumentsTree? = null |
| 56 | lateinit var application: YuzuApplication | 52 | lateinit var application: YuzuApplication |
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 index 7a0969a55..024676185 100644 --- 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 | |||
| @@ -3,11 +3,8 @@ | |||
| 3 | 3 | ||
| 4 | package org.yuzu.yuzu_emu.adapters | 4 | package org.yuzu.yuzu_emu.adapters |
| 5 | 5 | ||
| 6 | import android.database.Cursor | ||
| 7 | import android.database.DataSetObserver | ||
| 8 | import android.graphics.Bitmap | 6 | import android.graphics.Bitmap |
| 9 | import android.graphics.BitmapFactory | 7 | import android.graphics.BitmapFactory |
| 10 | import android.net.Uri | ||
| 11 | import android.view.LayoutInflater | 8 | import android.view.LayoutInflater |
| 12 | import android.view.View | 9 | import android.view.View |
| 13 | import android.view.ViewGroup | 10 | import android.view.ViewGroup |
| @@ -16,7 +13,6 @@ import androidx.appcompat.app.AppCompatActivity | |||
| 16 | import androidx.lifecycle.lifecycleScope | 13 | import androidx.lifecycle.lifecycleScope |
| 17 | import androidx.recyclerview.widget.RecyclerView | 14 | import androidx.recyclerview.widget.RecyclerView |
| 18 | import coil.load | 15 | import coil.load |
| 19 | import com.google.android.material.color.MaterialColors | ||
| 20 | import kotlinx.coroutines.Dispatchers | 16 | import kotlinx.coroutines.Dispatchers |
| 21 | import kotlinx.coroutines.launch | 17 | import kotlinx.coroutines.launch |
| 22 | import kotlinx.coroutines.withContext | 18 | import kotlinx.coroutines.withContext |
| @@ -25,31 +21,16 @@ import org.yuzu.yuzu_emu.R | |||
| 25 | import org.yuzu.yuzu_emu.databinding.CardGameBinding | 21 | import org.yuzu.yuzu_emu.databinding.CardGameBinding |
| 26 | import org.yuzu.yuzu_emu.activities.EmulationActivity | 22 | import org.yuzu.yuzu_emu.activities.EmulationActivity |
| 27 | import org.yuzu.yuzu_emu.model.Game | 23 | import org.yuzu.yuzu_emu.model.Game |
| 28 | import org.yuzu.yuzu_emu.model.GameDatabase | 24 | import kotlin.collections.ArrayList |
| 29 | import org.yuzu.yuzu_emu.utils.Log | ||
| 30 | import org.yuzu.yuzu_emu.viewholders.GameViewHolder | ||
| 31 | import java.util.* | ||
| 32 | import java.util.stream.Stream | ||
| 33 | 25 | ||
| 34 | /** | 26 | /** |
| 35 | * This adapter gets its information from a database Cursor. This fact, paired with the usage of | 27 | * This adapter gets its information from a database Cursor. This fact, paired with the usage of |
| 36 | * ContentProviders and Loaders, allows for efficient display of a limited view into a (possibly) | 28 | * ContentProviders and Loaders, allows for efficient display of a limited view into a (possibly) |
| 37 | * large dataset. | 29 | * large dataset. |
| 38 | */ | 30 | */ |
| 39 | class GameAdapter(private val activity: AppCompatActivity) : RecyclerView.Adapter<GameViewHolder>(), | 31 | class GameAdapter(private val activity: AppCompatActivity, var games: ArrayList<Game>) : |
| 32 | RecyclerView.Adapter<GameAdapter.GameViewHolder>(), | ||
| 40 | View.OnClickListener { | 33 | View.OnClickListener { |
| 41 | private var cursor: Cursor? = null | ||
| 42 | private val observer: GameDataSetObserver? | ||
| 43 | private var isDatasetValid = false | ||
| 44 | |||
| 45 | /** | ||
| 46 | * Initializes the adapter's observer, which watches for changes to the dataset. The adapter will | ||
| 47 | * display no data until a Cursor is supplied by a CursorLoader. | ||
| 48 | */ | ||
| 49 | init { | ||
| 50 | observer = GameDataSetObserver() | ||
| 51 | } | ||
| 52 | |||
| 53 | override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): GameViewHolder { | 34 | override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): GameViewHolder { |
| 54 | // Create a new view. | 35 | // Create a new view. |
| 55 | val binding = CardGameBinding.inflate(LayoutInflater.from(parent.context)) | 36 | val binding = CardGameBinding.inflate(LayoutInflater.from(parent.context)) |
| @@ -60,131 +41,55 @@ class GameAdapter(private val activity: AppCompatActivity) : RecyclerView.Adapte | |||
| 60 | } | 41 | } |
| 61 | 42 | ||
| 62 | override fun onBindViewHolder(holder: GameViewHolder, position: Int) { | 43 | override fun onBindViewHolder(holder: GameViewHolder, position: Int) { |
| 63 | if (isDatasetValid) { | 44 | holder.bind(games[position]) |
| 64 | if (cursor!!.moveToPosition(position)) { | ||
| 65 | // TODO These shouldn't be necessary once the move to a DB-based model is complete. | ||
| 66 | val game = Game( | ||
| 67 | cursor!!.getString(GameDatabase.GAME_COLUMN_TITLE), | ||
| 68 | cursor!!.getString(GameDatabase.GAME_COLUMN_DESCRIPTION), | ||
| 69 | cursor!!.getString(GameDatabase.GAME_COLUMN_REGIONS), | ||
| 70 | cursor!!.getString(GameDatabase.GAME_COLUMN_PATH), | ||
| 71 | cursor!!.getString(GameDatabase.GAME_COLUMN_GAME_ID), | ||
| 72 | cursor!!.getString(GameDatabase.GAME_COLUMN_CAPTION) | ||
| 73 | ) | ||
| 74 | holder.game = game | ||
| 75 | |||
| 76 | holder.binding.imageGameScreen.scaleType = ImageView.ScaleType.CENTER_CROP | ||
| 77 | activity.lifecycleScope.launch { | ||
| 78 | val bitmap = decodeGameIcon(game.path) | ||
| 79 | holder.binding.imageGameScreen.load(bitmap) { | ||
| 80 | error(R.drawable.no_icon) | ||
| 81 | crossfade(true) | ||
| 82 | } | ||
| 83 | } | ||
| 84 | |||
| 85 | holder.binding.textGameTitle.text = game.title.replace("[\\t\\n\\r]+".toRegex(), " ") | ||
| 86 | holder.binding.textGameCaption.text = game.company | ||
| 87 | |||
| 88 | if (game.company.isEmpty()) { | ||
| 89 | holder.binding.textGameCaption.visibility = View.GONE | ||
| 90 | } | ||
| 91 | |||
| 92 | val backgroundColorId = | ||
| 93 | if (isValidGame(holder.game.path)) R.attr.colorSurface else R.attr.colorErrorContainer | ||
| 94 | val itemView = holder.itemView | ||
| 95 | itemView.setBackgroundColor( | ||
| 96 | MaterialColors.getColor( | ||
| 97 | itemView, | ||
| 98 | backgroundColorId | ||
| 99 | ) | ||
| 100 | ) | ||
| 101 | } else { | ||
| 102 | Log.error("[GameAdapter] Can't bind view; Cursor is not valid.") | ||
| 103 | } | ||
| 104 | } else { | ||
| 105 | Log.error("[GameAdapter] Can't bind view; dataset is not valid.") | ||
| 106 | } | ||
| 107 | } | 45 | } |
| 108 | 46 | ||
| 109 | override fun getItemCount(): Int { | 47 | override fun getItemCount(): Int { |
| 110 | if (isDatasetValid && cursor != null) { | 48 | return games.size |
| 111 | return cursor!!.count | ||
| 112 | } | ||
| 113 | Log.error("[GameAdapter] Dataset is not valid.") | ||
| 114 | return 0 | ||
| 115 | } | 49 | } |
| 116 | 50 | ||
| 117 | /** | 51 | /** |
| 118 | * Return the contents of the _id column for a given row. | 52 | * Launches the game that was clicked on. |
| 119 | * | 53 | * |
| 120 | * @param position The row for which Android wants an ID. | 54 | * @param view The card representing the game the user wants to play. |
| 121 | * @return A valid ID from the database, or 0 if not available. | ||
| 122 | */ | 55 | */ |
| 123 | override fun getItemId(position: Int): Long { | 56 | override fun onClick(view: View) { |
| 124 | if (isDatasetValid && cursor != null) { | 57 | val holder = view.tag as GameViewHolder |
| 125 | if (cursor!!.moveToPosition(position)) { | 58 | EmulationActivity.launch((view.context as AppCompatActivity), holder.game) |
| 126 | return cursor!!.getLong(GameDatabase.COLUMN_DB_ID) | ||
| 127 | } | ||
| 128 | } | ||
| 129 | Log.error("[GameAdapter] Dataset is not valid.") | ||
| 130 | return 0 | ||
| 131 | } | 59 | } |
| 132 | 60 | ||
| 133 | /** | 61 | inner class GameViewHolder(val binding: CardGameBinding) : |
| 134 | * Tell Android whether or not each item in the dataset has a stable identifier. | 62 | RecyclerView.ViewHolder(binding.root) { |
| 135 | * Which it does, because it's a database, so always tell Android 'true'. | 63 | lateinit var game: Game |
| 136 | * | ||
| 137 | * @param hasStableIds ignored. | ||
| 138 | */ | ||
| 139 | override fun setHasStableIds(hasStableIds: Boolean) { | ||
| 140 | super.setHasStableIds(true) | ||
| 141 | } | ||
| 142 | 64 | ||
| 143 | /** | 65 | init { |
| 144 | * When a load is finished, call this to replace the existing data with the newly-loaded | 66 | itemView.tag = this |
| 145 | * data. | ||
| 146 | * | ||
| 147 | * @param cursor The newly-loaded Cursor. | ||
| 148 | */ | ||
| 149 | fun swapCursor(cursor: Cursor) { | ||
| 150 | // Sanity check. | ||
| 151 | if (cursor === this.cursor) { | ||
| 152 | return | ||
| 153 | } | 67 | } |
| 154 | 68 | ||
| 155 | // Before getting rid of the old cursor, disassociate it from the Observer. | 69 | fun bind(game: Game) { |
| 156 | val oldCursor = this.cursor | 70 | this.game = game |
| 157 | if (oldCursor != null && observer != null) { | 71 | |
| 158 | oldCursor.unregisterDataSetObserver(observer) | 72 | binding.imageGameScreen.scaleType = ImageView.ScaleType.CENTER_CROP |
| 159 | } | 73 | activity.lifecycleScope.launch { |
| 160 | this.cursor = cursor | 74 | val bitmap = decodeGameIcon(game.path) |
| 161 | isDatasetValid = if (this.cursor != null) { | 75 | binding.imageGameScreen.load(bitmap) { |
| 162 | // Attempt to associate the new Cursor with the Observer. | 76 | error(R.drawable.no_icon) |
| 163 | if (observer != null) { | 77 | crossfade(true) |
| 164 | this.cursor!!.registerDataSetObserver(observer) | 78 | } |
| 165 | } | 79 | } |
| 166 | true | ||
| 167 | } else { | ||
| 168 | false | ||
| 169 | } | ||
| 170 | notifyDataSetChanged() | ||
| 171 | } | ||
| 172 | 80 | ||
| 173 | /** | 81 | binding.textGameTitle.text = game.title.replace("[\\t\\n\\r]+".toRegex(), " ") |
| 174 | * Launches the game that was clicked on. | 82 | binding.textGameCaption.text = game.company |
| 175 | * | ||
| 176 | * @param view The card representing the game the user wants to play. | ||
| 177 | */ | ||
| 178 | override fun onClick(view: View) { | ||
| 179 | val holder = view.tag as GameViewHolder | ||
| 180 | EmulationActivity.launch((view.context as AppCompatActivity), holder.game) | ||
| 181 | } | ||
| 182 | 83 | ||
| 183 | private fun isValidGame(path: String): Boolean { | 84 | if (game.company.isEmpty()) { |
| 184 | return Stream.of(".rar", ".zip", ".7z", ".torrent", ".tar", ".gz") | 85 | binding.textGameCaption.visibility = View.GONE |
| 185 | .noneMatch { suffix: String? -> | ||
| 186 | path.lowercase(Locale.getDefault()).endsWith(suffix!!) | ||
| 187 | } | 86 | } |
| 87 | } | ||
| 88 | } | ||
| 89 | |||
| 90 | fun swapData(games: ArrayList<Game>) { | ||
| 91 | this.games = games | ||
| 92 | notifyDataSetChanged() | ||
| 188 | } | 93 | } |
| 189 | 94 | ||
| 190 | private fun decodeGameIcon(uri: String): Bitmap? { | 95 | private fun decodeGameIcon(uri: String): Bitmap? { |
| @@ -196,18 +101,4 @@ class GameAdapter(private val activity: AppCompatActivity) : RecyclerView.Adapte | |||
| 196 | BitmapFactory.Options() | 101 | BitmapFactory.Options() |
| 197 | ) | 102 | ) |
| 198 | } | 103 | } |
| 199 | |||
| 200 | private inner class GameDataSetObserver : DataSetObserver() { | ||
| 201 | override fun onChanged() { | ||
| 202 | super.onChanged() | ||
| 203 | isDatasetValid = true | ||
| 204 | notifyDataSetChanged() | ||
| 205 | } | ||
| 206 | |||
| 207 | override fun onInvalidated() { | ||
| 208 | super.onInvalidated() | ||
| 209 | isDatasetValid = false | ||
| 210 | notifyDataSetChanged() | ||
| 211 | } | ||
| 212 | } | ||
| 213 | } | 104 | } |
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/Game.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/Game.kt index 91f6c5d75..db494e40f 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/Game.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/Game.kt | |||
| @@ -3,11 +3,8 @@ | |||
| 3 | 3 | ||
| 4 | package org.yuzu.yuzu_emu.model | 4 | package org.yuzu.yuzu_emu.model |
| 5 | 5 | ||
| 6 | import android.content.ContentValues | ||
| 7 | import android.database.Cursor | ||
| 8 | import android.os.Parcelable | 6 | import android.os.Parcelable |
| 9 | import kotlinx.parcelize.Parcelize | 7 | import kotlinx.parcelize.Parcelize |
| 10 | import java.nio.file.Paths | ||
| 11 | import java.util.HashSet | 8 | import java.util.HashSet |
| 12 | 9 | ||
| 13 | @Parcelize | 10 | @Parcelize |
| @@ -23,40 +20,5 @@ class Game( | |||
| 23 | val extensions: Set<String> = HashSet( | 20 | val extensions: Set<String> = HashSet( |
| 24 | listOf(".xci", ".nsp", ".nca", ".nro") | 21 | listOf(".xci", ".nsp", ".nca", ".nro") |
| 25 | ) | 22 | ) |
| 26 | |||
| 27 | @JvmStatic | ||
| 28 | fun asContentValues( | ||
| 29 | title: String?, | ||
| 30 | description: String?, | ||
| 31 | regions: String?, | ||
| 32 | path: String?, | ||
| 33 | gameId: String, | ||
| 34 | company: String? | ||
| 35 | ): ContentValues { | ||
| 36 | var realGameId = gameId | ||
| 37 | val values = ContentValues() | ||
| 38 | if (realGameId.isEmpty()) { | ||
| 39 | // Homebrew, etc. may not have a game ID, use filename as a unique identifier | ||
| 40 | realGameId = Paths.get(path).fileName.toString() | ||
| 41 | } | ||
| 42 | values.put(GameDatabase.KEY_GAME_TITLE, title) | ||
| 43 | values.put(GameDatabase.KEY_GAME_DESCRIPTION, description) | ||
| 44 | values.put(GameDatabase.KEY_GAME_REGIONS, regions) | ||
| 45 | values.put(GameDatabase.KEY_GAME_PATH, path) | ||
| 46 | values.put(GameDatabase.KEY_GAME_ID, realGameId) | ||
| 47 | values.put(GameDatabase.KEY_GAME_COMPANY, company) | ||
| 48 | return values | ||
| 49 | } | ||
| 50 | |||
| 51 | fun fromCursor(cursor: Cursor): Game { | ||
| 52 | return Game( | ||
| 53 | cursor.getString(GameDatabase.GAME_COLUMN_TITLE), | ||
| 54 | cursor.getString(GameDatabase.GAME_COLUMN_DESCRIPTION), | ||
| 55 | cursor.getString(GameDatabase.GAME_COLUMN_REGIONS), | ||
| 56 | cursor.getString(GameDatabase.GAME_COLUMN_PATH), | ||
| 57 | cursor.getString(GameDatabase.GAME_COLUMN_GAME_ID), | ||
| 58 | cursor.getString(GameDatabase.GAME_COLUMN_CAPTION) | ||
| 59 | ) | ||
| 60 | } | ||
| 61 | } | 23 | } |
| 62 | } | 24 | } |
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/GameDatabase.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/GameDatabase.kt deleted file mode 100644 index c66183516..000000000 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/GameDatabase.kt +++ /dev/null | |||
| @@ -1,263 +0,0 @@ | |||
| 1 | // SPDX-FileCopyrightText: 2023 yuzu Emulator Project | ||
| 2 | // SPDX-License-Identifier: GPL-2.0-or-later | ||
| 3 | |||
| 4 | package org.yuzu.yuzu_emu.model | ||
| 5 | |||
| 6 | import android.content.Context | ||
| 7 | import android.database.Cursor | ||
| 8 | import android.database.sqlite.SQLiteDatabase | ||
| 9 | import android.database.sqlite.SQLiteOpenHelper | ||
| 10 | import android.net.Uri | ||
| 11 | import org.yuzu.yuzu_emu.NativeLibrary | ||
| 12 | import org.yuzu.yuzu_emu.utils.FileUtil | ||
| 13 | import org.yuzu.yuzu_emu.utils.Log | ||
| 14 | import rx.Observable | ||
| 15 | import rx.Subscriber | ||
| 16 | import java.io.File | ||
| 17 | import java.util.* | ||
| 18 | |||
| 19 | /** | ||
| 20 | * A helper class that provides several utilities simplifying interaction with | ||
| 21 | * the SQLite database. | ||
| 22 | */ | ||
| 23 | class GameDatabase(private val context: Context) : | ||
| 24 | SQLiteOpenHelper(context, "games.db", null, DB_VERSION) { | ||
| 25 | override fun onCreate(database: SQLiteDatabase) { | ||
| 26 | Log.debug("[GameDatabase] GameDatabase - Creating database...") | ||
| 27 | execSqlAndLog(database, SQL_CREATE_GAMES) | ||
| 28 | execSqlAndLog(database, SQL_CREATE_FOLDERS) | ||
| 29 | } | ||
| 30 | |||
| 31 | override fun onDowngrade(database: SQLiteDatabase, oldVersion: Int, newVersion: Int) { | ||
| 32 | Log.verbose("[GameDatabase] Downgrades not supporting, clearing databases..") | ||
| 33 | execSqlAndLog(database, SQL_DELETE_FOLDERS) | ||
| 34 | execSqlAndLog(database, SQL_CREATE_FOLDERS) | ||
| 35 | execSqlAndLog(database, SQL_DELETE_GAMES) | ||
| 36 | execSqlAndLog(database, SQL_CREATE_GAMES) | ||
| 37 | } | ||
| 38 | |||
| 39 | override fun onUpgrade(database: SQLiteDatabase, oldVersion: Int, newVersion: Int) { | ||
| 40 | Log.info( | ||
| 41 | "[GameDatabase] Upgrading database from schema version $oldVersion to $newVersion" | ||
| 42 | ) | ||
| 43 | |||
| 44 | // Delete all the games | ||
| 45 | execSqlAndLog(database, SQL_DELETE_GAMES) | ||
| 46 | execSqlAndLog(database, SQL_CREATE_GAMES) | ||
| 47 | } | ||
| 48 | |||
| 49 | fun resetDatabase(database: SQLiteDatabase) { | ||
| 50 | execSqlAndLog(database, SQL_DELETE_FOLDERS) | ||
| 51 | execSqlAndLog(database, SQL_CREATE_FOLDERS) | ||
| 52 | execSqlAndLog(database, SQL_DELETE_GAMES) | ||
| 53 | execSqlAndLog(database, SQL_CREATE_GAMES) | ||
| 54 | } | ||
| 55 | |||
| 56 | fun scanLibrary(database: SQLiteDatabase) { | ||
| 57 | // Before scanning known folders, go through the game table and remove any entries for which the file itself is missing. | ||
| 58 | val fileCursor = database.query( | ||
| 59 | TABLE_NAME_GAMES, | ||
| 60 | null, // Get all columns. | ||
| 61 | null, // Get all rows. | ||
| 62 | null, | ||
| 63 | null, // No grouping. | ||
| 64 | null, | ||
| 65 | null | ||
| 66 | ) // Order of games is irrelevant. | ||
| 67 | |||
| 68 | // Possibly overly defensive, but ensures that moveToNext() does not skip a row. | ||
| 69 | fileCursor.moveToPosition(-1) | ||
| 70 | while (fileCursor.moveToNext()) { | ||
| 71 | val gamePath = fileCursor.getString(GAME_COLUMN_PATH) | ||
| 72 | val game = File(gamePath) | ||
| 73 | if (!game.exists()) { | ||
| 74 | database.delete( | ||
| 75 | TABLE_NAME_GAMES, | ||
| 76 | "$KEY_DB_ID = ?", | ||
| 77 | arrayOf(fileCursor.getLong(COLUMN_DB_ID).toString()) | ||
| 78 | ) | ||
| 79 | } | ||
| 80 | } | ||
| 81 | |||
| 82 | // Get a cursor listing all the folders the user has added to the library. | ||
| 83 | val folderCursor = database.query( | ||
| 84 | TABLE_NAME_FOLDERS, | ||
| 85 | null, // Get all columns. | ||
| 86 | null, // Get all rows. | ||
| 87 | null, | ||
| 88 | null, // No grouping. | ||
| 89 | null, | ||
| 90 | null | ||
| 91 | ) // Order of folders is irrelevant. | ||
| 92 | |||
| 93 | |||
| 94 | // Possibly overly defensive, but ensures that moveToNext() does not skip a row. | ||
| 95 | folderCursor.moveToPosition(-1) | ||
| 96 | |||
| 97 | // Iterate through all results of the DB query (i.e. all folders in the library.) | ||
| 98 | while (folderCursor.moveToNext()) { | ||
| 99 | val folderPath = folderCursor.getString(FOLDER_COLUMN_PATH) | ||
| 100 | val folderUri = Uri.parse(folderPath) | ||
| 101 | // If the folder is empty because it no longer exists, remove it from the library. | ||
| 102 | if (FileUtil.listFiles(context, folderUri).isEmpty()) { | ||
| 103 | Log.error( | ||
| 104 | "[GameDatabase] Folder no longer exists. Removing from the library: $folderPath" | ||
| 105 | ) | ||
| 106 | database.delete( | ||
| 107 | TABLE_NAME_FOLDERS, | ||
| 108 | "$KEY_DB_ID = ?", | ||
| 109 | arrayOf(folderCursor.getLong(COLUMN_DB_ID).toString()) | ||
| 110 | ) | ||
| 111 | } | ||
| 112 | addGamesRecursive(database, folderUri, Game.extensions, 3) | ||
| 113 | } | ||
| 114 | fileCursor.close() | ||
| 115 | folderCursor.close() | ||
| 116 | database.close() | ||
| 117 | } | ||
| 118 | |||
| 119 | private fun addGamesRecursive( | ||
| 120 | database: SQLiteDatabase, | ||
| 121 | parent: Uri, | ||
| 122 | allowedExtensions: Set<String>, | ||
| 123 | depth: Int | ||
| 124 | ) { | ||
| 125 | if (depth <= 0) | ||
| 126 | return | ||
| 127 | |||
| 128 | // Ensure keys are loaded so that ROM metadata can be decrypted. | ||
| 129 | NativeLibrary.ReloadKeys() | ||
| 130 | val children = FileUtil.listFiles(context, parent) | ||
| 131 | for (file in children) { | ||
| 132 | if (file.isDirectory) { | ||
| 133 | addGamesRecursive(database, file.uri, Game.extensions, depth - 1) | ||
| 134 | } else { | ||
| 135 | val filename = file.uri.toString() | ||
| 136 | val extensionStart = filename.lastIndexOf('.') | ||
| 137 | if (extensionStart > 0) { | ||
| 138 | val fileExtension = filename.substring(extensionStart) | ||
| 139 | |||
| 140 | // Check that the file has an extension we care about before trying to read out of it. | ||
| 141 | if (allowedExtensions.contains(fileExtension.lowercase(Locale.getDefault()))) { | ||
| 142 | attemptToAddGame(database, filename) | ||
| 143 | } | ||
| 144 | } | ||
| 145 | } | ||
| 146 | } | ||
| 147 | } | ||
| 148 | // Pass the result cursor to the consumer. | ||
| 149 | |||
| 150 | // Tell the consumer we're done; it will unsubscribe implicitly. | ||
| 151 | val games: Observable<Cursor?> | ||
| 152 | get() = Observable.create { subscriber: Subscriber<in Cursor?> -> | ||
| 153 | Log.info("[GameDatabase] Reading games list...") | ||
| 154 | val database = readableDatabase | ||
| 155 | val resultCursor = database.query( | ||
| 156 | TABLE_NAME_GAMES, | ||
| 157 | null, | ||
| 158 | null, | ||
| 159 | null, | ||
| 160 | null, | ||
| 161 | null, | ||
| 162 | "$KEY_GAME_TITLE ASC" | ||
| 163 | ) | ||
| 164 | |||
| 165 | // Pass the result cursor to the consumer. | ||
| 166 | subscriber.onNext(resultCursor) | ||
| 167 | |||
| 168 | // Tell the consumer we're done; it will unsubscribe implicitly. | ||
| 169 | subscriber.onCompleted() | ||
| 170 | } | ||
| 171 | |||
| 172 | private fun execSqlAndLog(database: SQLiteDatabase, sql: String) { | ||
| 173 | Log.verbose("[GameDatabase] Executing SQL: $sql") | ||
| 174 | database.execSQL(sql) | ||
| 175 | } | ||
| 176 | |||
| 177 | companion object { | ||
| 178 | const val COLUMN_DB_ID = 0 | ||
| 179 | const val GAME_COLUMN_PATH = 1 | ||
| 180 | const val GAME_COLUMN_TITLE = 2 | ||
| 181 | const val GAME_COLUMN_DESCRIPTION = 3 | ||
| 182 | const val GAME_COLUMN_REGIONS = 4 | ||
| 183 | const val GAME_COLUMN_GAME_ID = 5 | ||
| 184 | const val GAME_COLUMN_CAPTION = 6 | ||
| 185 | const val FOLDER_COLUMN_PATH = 1 | ||
| 186 | const val KEY_DB_ID = "_id" | ||
| 187 | const val KEY_GAME_PATH = "path" | ||
| 188 | const val KEY_GAME_TITLE = "title" | ||
| 189 | const val KEY_GAME_DESCRIPTION = "description" | ||
| 190 | const val KEY_GAME_REGIONS = "regions" | ||
| 191 | const val KEY_GAME_ID = "game_id" | ||
| 192 | const val KEY_GAME_COMPANY = "company" | ||
| 193 | const val KEY_FOLDER_PATH = "path" | ||
| 194 | const val TABLE_NAME_FOLDERS = "folders" | ||
| 195 | const val TABLE_NAME_GAMES = "games" | ||
| 196 | private const val DB_VERSION = 2 | ||
| 197 | private const val TYPE_PRIMARY = " INTEGER PRIMARY KEY" | ||
| 198 | private const val TYPE_INTEGER = " INTEGER" | ||
| 199 | private const val TYPE_STRING = " TEXT" | ||
| 200 | private const val CONSTRAINT_UNIQUE = " UNIQUE" | ||
| 201 | private const val SEPARATOR = ", " | ||
| 202 | private const val SQL_CREATE_GAMES = ("CREATE TABLE " + TABLE_NAME_GAMES + "(" | ||
| 203 | + KEY_DB_ID + TYPE_PRIMARY + SEPARATOR | ||
| 204 | + KEY_GAME_PATH + TYPE_STRING + SEPARATOR | ||
| 205 | + KEY_GAME_TITLE + TYPE_STRING + SEPARATOR | ||
| 206 | + KEY_GAME_DESCRIPTION + TYPE_STRING + SEPARATOR | ||
| 207 | + KEY_GAME_REGIONS + TYPE_STRING + SEPARATOR | ||
| 208 | + KEY_GAME_ID + TYPE_STRING + SEPARATOR | ||
| 209 | + KEY_GAME_COMPANY + TYPE_STRING + ")") | ||
| 210 | private const val SQL_CREATE_FOLDERS = ("CREATE TABLE " + TABLE_NAME_FOLDERS + "(" | ||
| 211 | + KEY_DB_ID + TYPE_PRIMARY + SEPARATOR | ||
| 212 | + KEY_FOLDER_PATH + TYPE_STRING + CONSTRAINT_UNIQUE + ")") | ||
| 213 | private const val SQL_DELETE_FOLDERS = "DROP TABLE IF EXISTS $TABLE_NAME_FOLDERS" | ||
| 214 | private const val SQL_DELETE_GAMES = "DROP TABLE IF EXISTS $TABLE_NAME_GAMES" | ||
| 215 | private fun attemptToAddGame(database: SQLiteDatabase, filePath: String) { | ||
| 216 | var name = NativeLibrary.GetTitle(filePath) | ||
| 217 | |||
| 218 | // If the game's title field is empty, use the filename. | ||
| 219 | if (name.isEmpty()) { | ||
| 220 | name = filePath.substring(filePath.lastIndexOf("/") + 1) | ||
| 221 | } | ||
| 222 | var gameId = NativeLibrary.GetGameId(filePath) | ||
| 223 | |||
| 224 | // If the game's ID field is empty, use the filename without extension. | ||
| 225 | if (gameId.isEmpty()) { | ||
| 226 | gameId = filePath.substring( | ||
| 227 | filePath.lastIndexOf("/") + 1, | ||
| 228 | filePath.lastIndexOf(".") | ||
| 229 | ) | ||
| 230 | } | ||
| 231 | val game = Game.asContentValues( | ||
| 232 | name, | ||
| 233 | NativeLibrary.GetDescription(filePath).replace("\n", " "), | ||
| 234 | NativeLibrary.GetRegions(filePath), | ||
| 235 | filePath, | ||
| 236 | gameId, | ||
| 237 | NativeLibrary.GetCompany(filePath) | ||
| 238 | ) | ||
| 239 | |||
| 240 | // Try to update an existing game first. | ||
| 241 | val rowsMatched = database.update( | ||
| 242 | TABLE_NAME_GAMES, // Which table to update. | ||
| 243 | game, // The values to fill the row with. | ||
| 244 | "$KEY_GAME_ID = ?", arrayOf( | ||
| 245 | game.getAsString( | ||
| 246 | KEY_GAME_ID | ||
| 247 | ) | ||
| 248 | ) | ||
| 249 | ) | ||
| 250 | // The ? in WHERE clause is replaced with this, | ||
| 251 | // which is provided as an array because there | ||
| 252 | // could potentially be more than one argument. | ||
| 253 | |||
| 254 | // If update fails, insert a new game instead. | ||
| 255 | if (rowsMatched == 0) { | ||
| 256 | Log.verbose("[GameDatabase] Adding game: " + game.getAsString(KEY_GAME_TITLE)) | ||
| 257 | database.insert(TABLE_NAME_GAMES, null, game) | ||
| 258 | } else { | ||
| 259 | Log.verbose("[GameDatabase] Updated game: " + game.getAsString(KEY_GAME_TITLE)) | ||
| 260 | } | ||
| 261 | } | ||
| 262 | } | ||
| 263 | } | ||
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/GameProvider.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/GameProvider.kt deleted file mode 100644 index 5d8e5cc54..000000000 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/GameProvider.kt +++ /dev/null | |||
| @@ -1,130 +0,0 @@ | |||
| 1 | // SPDX-FileCopyrightText: 2023 yuzu Emulator Project | ||
| 2 | // SPDX-License-Identifier: GPL-2.0-or-later | ||
| 3 | |||
| 4 | package org.yuzu.yuzu_emu.model | ||
| 5 | |||
| 6 | import android.content.ContentProvider | ||
| 7 | import android.content.ContentValues | ||
| 8 | import android.database.Cursor | ||
| 9 | import android.database.sqlite.SQLiteDatabase | ||
| 10 | import android.net.Uri | ||
| 11 | import org.yuzu.yuzu_emu.BuildConfig | ||
| 12 | import org.yuzu.yuzu_emu.utils.Log | ||
| 13 | |||
| 14 | /** | ||
| 15 | * Provides an interface allowing Activities to interact with the SQLite database. | ||
| 16 | * CRUD methods in this class can be called by Activities using getContentResolver(). | ||
| 17 | */ | ||
| 18 | class GameProvider : ContentProvider() { | ||
| 19 | private var mDbHelper: GameDatabase? = null | ||
| 20 | override fun onCreate(): Boolean { | ||
| 21 | Log.info("[GameProvider] Creating Content Provider...") | ||
| 22 | mDbHelper = GameDatabase(context!!) | ||
| 23 | return true | ||
| 24 | } | ||
| 25 | |||
| 26 | override fun query( | ||
| 27 | uri: Uri, | ||
| 28 | projection: Array<String>?, | ||
| 29 | selection: String?, | ||
| 30 | selectionArgs: Array<String>?, | ||
| 31 | sortOrder: String? | ||
| 32 | ): Cursor? { | ||
| 33 | Log.info("[GameProvider] Querying URI: $uri") | ||
| 34 | val db = mDbHelper!!.readableDatabase | ||
| 35 | val table = uri.lastPathSegment | ||
| 36 | if (table == null) { | ||
| 37 | Log.error("[GameProvider] Badly formatted URI: $uri") | ||
| 38 | return null | ||
| 39 | } | ||
| 40 | val cursor = db.query(table, projection, selection, selectionArgs, null, null, sortOrder) | ||
| 41 | cursor.setNotificationUri(context!!.contentResolver, uri) | ||
| 42 | return cursor | ||
| 43 | } | ||
| 44 | |||
| 45 | override fun getType(uri: Uri): String? { | ||
| 46 | Log.verbose("[GameProvider] Getting MIME type for URI: $uri") | ||
| 47 | val lastSegment = uri.lastPathSegment | ||
| 48 | if (lastSegment == null) { | ||
| 49 | Log.error("[GameProvider] Badly formatted URI: $uri") | ||
| 50 | return null | ||
| 51 | } | ||
| 52 | if (lastSegment == GameDatabase.TABLE_NAME_FOLDERS) { | ||
| 53 | return MIME_TYPE_FOLDER | ||
| 54 | } else if (lastSegment == GameDatabase.TABLE_NAME_GAMES) { | ||
| 55 | return MIME_TYPE_GAME | ||
| 56 | } | ||
| 57 | Log.error("[GameProvider] Unknown MIME type for URI: $uri") | ||
| 58 | return null | ||
| 59 | } | ||
| 60 | |||
| 61 | override fun insert(uri: Uri, values: ContentValues?): Uri { | ||
| 62 | var realUri = uri | ||
| 63 | Log.info("[GameProvider] Inserting row at URI: $realUri") | ||
| 64 | val database = mDbHelper!!.writableDatabase | ||
| 65 | val table = realUri.lastPathSegment | ||
| 66 | if (table != null) { | ||
| 67 | if (table == RESET_LIBRARY) { | ||
| 68 | mDbHelper!!.resetDatabase(database) | ||
| 69 | return realUri | ||
| 70 | } | ||
| 71 | if (table == REFRESH_LIBRARY) { | ||
| 72 | Log.info( | ||
| 73 | "[GameProvider] URI specified table REFRESH_LIBRARY. No insertion necessary; refreshing library contents..." | ||
| 74 | ) | ||
| 75 | mDbHelper!!.scanLibrary(database) | ||
| 76 | return realUri | ||
| 77 | } | ||
| 78 | val id = | ||
| 79 | database.insertWithOnConflict(table, null, values, SQLiteDatabase.CONFLICT_IGNORE) | ||
| 80 | |||
| 81 | // If insertion was successful... | ||
| 82 | if (id > 0) { | ||
| 83 | // If we just added a folder, add its contents to the game list. | ||
| 84 | if (table == GameDatabase.TABLE_NAME_FOLDERS) { | ||
| 85 | mDbHelper!!.scanLibrary(database) | ||
| 86 | } | ||
| 87 | |||
| 88 | // Notify the UI that its contents should be refreshed. | ||
| 89 | context!!.contentResolver.notifyChange(realUri, null) | ||
| 90 | realUri = Uri.withAppendedPath(realUri, id.toString()) | ||
| 91 | } else { | ||
| 92 | Log.error("[GameProvider] Row already exists: $realUri id: $id") | ||
| 93 | } | ||
| 94 | } else { | ||
| 95 | Log.error("[GameProvider] Badly formatted URI: $realUri") | ||
| 96 | } | ||
| 97 | database.close() | ||
| 98 | return realUri | ||
| 99 | } | ||
| 100 | |||
| 101 | override fun delete(uri: Uri, selection: String?, selectionArgs: Array<String>?): Int { | ||
| 102 | Log.error("[GameProvider] Delete operations unsupported. URI: $uri") | ||
| 103 | return 0 | ||
| 104 | } | ||
| 105 | |||
| 106 | override fun update( | ||
| 107 | uri: Uri, values: ContentValues?, selection: String?, | ||
| 108 | selectionArgs: Array<String>? | ||
| 109 | ): Int { | ||
| 110 | Log.error("[GameProvider] Update operations unsupported. URI: $uri") | ||
| 111 | return 0 | ||
| 112 | } | ||
| 113 | |||
| 114 | companion object { | ||
| 115 | const val REFRESH_LIBRARY = "refresh" | ||
| 116 | const val RESET_LIBRARY = "reset" | ||
| 117 | private const val AUTHORITY = "content://${BuildConfig.APPLICATION_ID}.provider" | ||
| 118 | |||
| 119 | @JvmField | ||
| 120 | val URI_FOLDER: Uri = Uri.parse("$AUTHORITY/${GameDatabase.TABLE_NAME_FOLDERS}/") | ||
| 121 | |||
| 122 | @JvmField | ||
| 123 | val URI_REFRESH: Uri = Uri.parse("$AUTHORITY/$REFRESH_LIBRARY/") | ||
| 124 | |||
| 125 | @JvmField | ||
| 126 | val URI_RESET: Uri = Uri.parse("$AUTHORITY/$RESET_LIBRARY/") | ||
| 127 | const val MIME_TYPE_FOLDER = "vnd.android.cursor.item/vnd.yuzu.folder" | ||
| 128 | const val MIME_TYPE_GAME = "vnd.android.cursor.item/vnd.yuzu.game" | ||
| 129 | } | ||
| 130 | } | ||
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/GamesViewModel.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/GamesViewModel.kt new file mode 100644 index 000000000..fde99f1a2 --- /dev/null +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/GamesViewModel.kt | |||
| @@ -0,0 +1,18 @@ | |||
| 1 | package org.yuzu.yuzu_emu.model | ||
| 2 | |||
| 3 | import androidx.lifecycle.LiveData | ||
| 4 | import androidx.lifecycle.MutableLiveData | ||
| 5 | import androidx.lifecycle.ViewModel | ||
| 6 | |||
| 7 | class GamesViewModel : ViewModel() { | ||
| 8 | private val _games = MutableLiveData<ArrayList<Game>>() | ||
| 9 | val games: LiveData<ArrayList<Game>> get() = _games | ||
| 10 | |||
| 11 | init { | ||
| 12 | _games.value = ArrayList() | ||
| 13 | } | ||
| 14 | |||
| 15 | fun setGames(games: ArrayList<Game>) { | ||
| 16 | _games.value = games | ||
| 17 | } | ||
| 18 | } | ||
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/main/MainActivity.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/main/MainActivity.kt index 441c9da9c..4885bc4bc 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/main/MainActivity.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/main/MainActivity.kt | |||
| @@ -18,6 +18,7 @@ import androidx.core.view.WindowCompat | |||
| 18 | import androidx.core.view.WindowInsetsCompat | 18 | import androidx.core.view.WindowInsetsCompat |
| 19 | import androidx.core.view.updatePadding | 19 | import androidx.core.view.updatePadding |
| 20 | import androidx.lifecycle.lifecycleScope | 20 | import androidx.lifecycle.lifecycleScope |
| 21 | import androidx.preference.PreferenceManager | ||
| 21 | import com.google.android.material.dialog.MaterialAlertDialogBuilder | 22 | import com.google.android.material.dialog.MaterialAlertDialogBuilder |
| 22 | import kotlinx.coroutines.Dispatchers | 23 | import kotlinx.coroutines.Dispatchers |
| 23 | import kotlinx.coroutines.launch | 24 | import kotlinx.coroutines.launch |
| @@ -28,7 +29,6 @@ import org.yuzu.yuzu_emu.activities.EmulationActivity | |||
| 28 | import org.yuzu.yuzu_emu.databinding.ActivityMainBinding | 29 | import org.yuzu.yuzu_emu.databinding.ActivityMainBinding |
| 29 | import org.yuzu.yuzu_emu.databinding.DialogProgressBarBinding | 30 | import org.yuzu.yuzu_emu.databinding.DialogProgressBarBinding |
| 30 | import org.yuzu.yuzu_emu.features.settings.ui.SettingsActivity | 31 | import org.yuzu.yuzu_emu.features.settings.ui.SettingsActivity |
| 31 | import org.yuzu.yuzu_emu.model.GameProvider | ||
| 32 | import org.yuzu.yuzu_emu.ui.platform.PlatformGamesFragment | 32 | import org.yuzu.yuzu_emu.ui.platform.PlatformGamesFragment |
| 33 | import org.yuzu.yuzu_emu.utils.* | 33 | import org.yuzu.yuzu_emu.utils.* |
| 34 | import java.io.IOException | 34 | import java.io.IOException |
| @@ -82,11 +82,6 @@ class MainActivity : AppCompatActivity(), MainView { | |||
| 82 | ) | 82 | ) |
| 83 | } | 83 | } |
| 84 | 84 | ||
| 85 | override fun onResume() { | ||
| 86 | super.onResume() | ||
| 87 | presenter.addDirIfNeeded(AddDirectoryHelper(this)) | ||
| 88 | } | ||
| 89 | |||
| 90 | override fun onCreateOptionsMenu(menu: Menu): Boolean { | 85 | override fun onCreateOptionsMenu(menu: Menu): Boolean { |
| 91 | menuInflater.inflate(R.menu.menu_game_grid, menu) | 86 | menuInflater.inflate(R.menu.menu_game_grid, menu) |
| 92 | return true | 87 | return true |
| @@ -99,11 +94,6 @@ class MainActivity : AppCompatActivity(), MainView { | |||
| 99 | binding.toolbarMain.subtitle = version | 94 | binding.toolbarMain.subtitle = version |
| 100 | } | 95 | } |
| 101 | 96 | ||
| 102 | override fun refresh() { | ||
| 103 | contentResolver.insert(GameProvider.URI_REFRESH, null) | ||
| 104 | refreshFragment() | ||
| 105 | } | ||
| 106 | |||
| 107 | override fun launchSettingsActivity(menuTag: String) { | 97 | override fun launchSettingsActivity(menuTag: String) { |
| 108 | SettingsActivity.launch(this, menuTag, "") | 98 | SettingsActivity.launch(this, menuTag, "") |
| 109 | } | 99 | } |
| @@ -185,10 +175,9 @@ class MainActivity : AppCompatActivity(), MainView { | |||
| 185 | 175 | ||
| 186 | // When a new directory is picked, we currently will reset the existing games | 176 | // When a new directory is picked, we currently will reset the existing games |
| 187 | // database. This effectively means that only one game directory is supported. | 177 | // database. This effectively means that only one game directory is supported. |
| 188 | // TODO(bunnei): Consider fixing this in the future, or removing code for this. | 178 | PreferenceManager.getDefaultSharedPreferences(applicationContext).edit() |
| 189 | contentResolver.insert(GameProvider.URI_RESET, null) | 179 | .putString(GameHelper.KEY_GAME_PATH, result.toString()) |
| 190 | // Add the new directory | 180 | .apply() |
| 191 | presenter.onDirectorySelected(result.toString()) | ||
| 192 | } | 181 | } |
| 193 | 182 | ||
| 194 | private val getProdKey = | 183 | private val getProdKey = |
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/main/MainPresenter.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/main/MainPresenter.kt index 554542e05..a7ddc333f 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/main/MainPresenter.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/main/MainPresenter.kt | |||
| @@ -5,17 +5,12 @@ package org.yuzu.yuzu_emu.ui.main | |||
| 5 | 5 | ||
| 6 | import org.yuzu.yuzu_emu.BuildConfig | 6 | import org.yuzu.yuzu_emu.BuildConfig |
| 7 | import org.yuzu.yuzu_emu.R | 7 | import org.yuzu.yuzu_emu.R |
| 8 | import org.yuzu.yuzu_emu.YuzuApplication | ||
| 9 | import org.yuzu.yuzu_emu.features.settings.utils.SettingsFile | 8 | import org.yuzu.yuzu_emu.features.settings.utils.SettingsFile |
| 10 | import org.yuzu.yuzu_emu.utils.AddDirectoryHelper | ||
| 11 | 9 | ||
| 12 | class MainPresenter(private val view: MainView) { | 10 | class MainPresenter(private val view: MainView) { |
| 13 | private var dirToAdd: String? = null | ||
| 14 | |||
| 15 | fun onCreate() { | 11 | fun onCreate() { |
| 16 | val versionName = BuildConfig.VERSION_NAME | 12 | val versionName = BuildConfig.VERSION_NAME |
| 17 | view.setVersionString(versionName) | 13 | view.setVersionString(versionName) |
| 18 | refreshGameList() | ||
| 19 | } | 14 | } |
| 20 | 15 | ||
| 21 | private fun launchFileListActivity(request: Int) { | 16 | private fun launchFileListActivity(request: Int) { |
| @@ -48,23 +43,6 @@ class MainPresenter(private val view: MainView) { | |||
| 48 | return false | 43 | return false |
| 49 | } | 44 | } |
| 50 | 45 | ||
| 51 | fun addDirIfNeeded(helper: AddDirectoryHelper) { | ||
| 52 | if (dirToAdd != null) { | ||
| 53 | helper.addDirectory(dirToAdd) { view.refresh() } | ||
| 54 | dirToAdd = null | ||
| 55 | } | ||
| 56 | } | ||
| 57 | |||
| 58 | fun onDirectorySelected(dir: String?) { | ||
| 59 | dirToAdd = dir | ||
| 60 | } | ||
| 61 | |||
| 62 | private fun refreshGameList() { | ||
| 63 | val databaseHelper = YuzuApplication.databaseHelper | ||
| 64 | databaseHelper!!.scanLibrary(databaseHelper.writableDatabase) | ||
| 65 | view.refresh() | ||
| 66 | } | ||
| 67 | |||
| 68 | companion object { | 46 | companion object { |
| 69 | const val REQUEST_ADD_DIRECTORY = 1 | 47 | const val REQUEST_ADD_DIRECTORY = 1 |
| 70 | const val REQUEST_INSTALL_KEYS = 2 | 48 | const val REQUEST_INSTALL_KEYS = 2 |
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/main/MainView.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/main/MainView.kt index dab3abe7c..4dc9f0706 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/main/MainView.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/main/MainView.kt | |||
| @@ -17,10 +17,7 @@ interface MainView { | |||
| 17 | */ | 17 | */ |
| 18 | fun setVersionString(version: String) | 18 | fun setVersionString(version: String) |
| 19 | 19 | ||
| 20 | /** | ||
| 21 | * Tell the view to refresh its contents. | ||
| 22 | */ | ||
| 23 | fun refresh() | ||
| 24 | fun launchSettingsActivity(menuTag: String) | 20 | fun launchSettingsActivity(menuTag: String) |
| 21 | |||
| 25 | fun launchFileListActivity(request: Int) | 22 | fun launchFileListActivity(request: Int) |
| 26 | } | 23 | } |
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/platform/PlatformGamesFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/platform/PlatformGamesFragment.kt index dcfac1b2a..443a37cd2 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/platform/PlatformGamesFragment.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/platform/PlatformGamesFragment.kt | |||
| @@ -3,7 +3,6 @@ | |||
| 3 | 3 | ||
| 4 | package org.yuzu.yuzu_emu.ui.platform | 4 | package org.yuzu.yuzu_emu.ui.platform |
| 5 | 5 | ||
| 6 | import android.database.Cursor | ||
| 7 | import android.os.Bundle | 6 | import android.os.Bundle |
| 8 | import android.view.LayoutInflater | 7 | import android.view.LayoutInflater |
| 9 | import android.view.View | 8 | import android.view.View |
| @@ -13,36 +12,40 @@ import androidx.core.view.ViewCompat | |||
| 13 | import androidx.core.view.WindowInsetsCompat | 12 | import androidx.core.view.WindowInsetsCompat |
| 14 | import androidx.core.view.updatePadding | 13 | import androidx.core.view.updatePadding |
| 15 | import androidx.fragment.app.Fragment | 14 | import androidx.fragment.app.Fragment |
| 15 | import androidx.lifecycle.ViewModelProvider | ||
| 16 | import com.google.android.material.color.MaterialColors | 16 | import com.google.android.material.color.MaterialColors |
| 17 | import org.yuzu.yuzu_emu.R | 17 | import org.yuzu.yuzu_emu.R |
| 18 | import org.yuzu.yuzu_emu.YuzuApplication | ||
| 19 | import org.yuzu.yuzu_emu.adapters.GameAdapter | 18 | import org.yuzu.yuzu_emu.adapters.GameAdapter |
| 20 | import org.yuzu.yuzu_emu.databinding.FragmentGridBinding | 19 | import org.yuzu.yuzu_emu.databinding.FragmentGridBinding |
| 21 | import org.yuzu.yuzu_emu.layout.AutofitGridLayoutManager | 20 | import org.yuzu.yuzu_emu.layout.AutofitGridLayoutManager |
| 21 | import org.yuzu.yuzu_emu.model.GamesViewModel | ||
| 22 | import org.yuzu.yuzu_emu.utils.GameHelper | ||
| 22 | 23 | ||
| 23 | class PlatformGamesFragment : Fragment(), PlatformGamesView { | 24 | class PlatformGamesFragment : Fragment() { |
| 24 | private val presenter = PlatformGamesPresenter(this) | ||
| 25 | |||
| 26 | private var _binding: FragmentGridBinding? = null | 25 | private var _binding: FragmentGridBinding? = null |
| 27 | private val binding get() = _binding!! | 26 | private val binding get() = _binding!! |
| 28 | 27 | ||
| 28 | private lateinit var gamesViewModel: GamesViewModel | ||
| 29 | |||
| 29 | override fun onCreateView( | 30 | override fun onCreateView( |
| 30 | inflater: LayoutInflater, | 31 | inflater: LayoutInflater, |
| 31 | container: ViewGroup?, | 32 | container: ViewGroup?, |
| 32 | savedInstanceState: Bundle? | 33 | savedInstanceState: Bundle? |
| 33 | ): View { | 34 | ): View { |
| 34 | presenter.onCreateView() | ||
| 35 | _binding = FragmentGridBinding.inflate(inflater) | 35 | _binding = FragmentGridBinding.inflate(inflater) |
| 36 | return binding.root | 36 | return binding.root |
| 37 | } | 37 | } |
| 38 | 38 | ||
| 39 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) { | 39 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) { |
| 40 | gamesViewModel = ViewModelProvider(requireActivity())[GamesViewModel::class.java] | ||
| 41 | |||
| 40 | binding.gridGames.apply { | 42 | binding.gridGames.apply { |
| 41 | layoutManager = AutofitGridLayoutManager( | 43 | layoutManager = AutofitGridLayoutManager( |
| 42 | requireContext(), | 44 | requireContext(), |
| 43 | requireContext().resources.getDimensionPixelSize(R.dimen.card_width) | 45 | requireContext().resources.getDimensionPixelSize(R.dimen.card_width) |
| 44 | ) | 46 | ) |
| 45 | adapter = GameAdapter(requireActivity() as AppCompatActivity) | 47 | adapter = |
| 48 | GameAdapter(requireActivity() as AppCompatActivity, gamesViewModel.games.value!!) | ||
| 46 | } | 49 | } |
| 47 | 50 | ||
| 48 | // Add swipe down to refresh gesture | 51 | // Add swipe down to refresh gesture |
| @@ -59,7 +62,19 @@ class PlatformGamesFragment : Fragment(), PlatformGamesView { | |||
| 59 | MaterialColors.getColor(binding.swipeRefresh, R.attr.colorOnPrimary) | 62 | MaterialColors.getColor(binding.swipeRefresh, R.attr.colorOnPrimary) |
| 60 | ) | 63 | ) |
| 61 | 64 | ||
| 65 | gamesViewModel.games.observe(viewLifecycleOwner) { | ||
| 66 | (binding.gridGames.adapter as GameAdapter).swapData(it) | ||
| 67 | updateTextView() | ||
| 68 | } | ||
| 69 | |||
| 62 | setInsets() | 70 | setInsets() |
| 71 | |||
| 72 | refresh() | ||
| 73 | } | ||
| 74 | |||
| 75 | override fun onResume() { | ||
| 76 | super.onResume() | ||
| 77 | refresh() | ||
| 63 | } | 78 | } |
| 64 | 79 | ||
| 65 | override fun onDestroyView() { | 80 | override fun onDestroyView() { |
| @@ -67,20 +82,8 @@ class PlatformGamesFragment : Fragment(), PlatformGamesView { | |||
| 67 | _binding = null | 82 | _binding = null |
| 68 | } | 83 | } |
| 69 | 84 | ||
| 70 | override fun refresh() { | 85 | fun refresh() { |
| 71 | val databaseHelper = YuzuApplication.databaseHelper | 86 | gamesViewModel.setGames(GameHelper.getGames()) |
| 72 | databaseHelper!!.scanLibrary(databaseHelper.writableDatabase) | ||
| 73 | presenter.refresh() | ||
| 74 | updateTextView() | ||
| 75 | } | ||
| 76 | |||
| 77 | override fun showGames(games: Cursor) { | ||
| 78 | if (_binding == null) | ||
| 79 | return | ||
| 80 | |||
| 81 | if (binding.gridGames.adapter != null) { | ||
| 82 | (binding.gridGames.adapter as GameAdapter).swapCursor(games) | ||
| 83 | } | ||
| 84 | updateTextView() | 87 | updateTextView() |
| 85 | } | 88 | } |
| 86 | 89 | ||
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/platform/PlatformGamesPresenter.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/platform/PlatformGamesPresenter.kt deleted file mode 100644 index 0b9da5f57..000000000 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/platform/PlatformGamesPresenter.kt +++ /dev/null | |||
| @@ -1,33 +0,0 @@ | |||
| 1 | // SPDX-FileCopyrightText: 2023 yuzu Emulator Project | ||
| 2 | // SPDX-License-Identifier: GPL-2.0-or-later | ||
| 3 | |||
| 4 | package org.yuzu.yuzu_emu.ui.platform | ||
| 5 | |||
| 6 | import android.database.Cursor | ||
| 7 | import org.yuzu.yuzu_emu.YuzuApplication | ||
| 8 | import org.yuzu.yuzu_emu.utils.Log | ||
| 9 | import rx.android.schedulers.AndroidSchedulers | ||
| 10 | import rx.schedulers.Schedulers | ||
| 11 | |||
| 12 | class PlatformGamesPresenter(private val view: PlatformGamesView) { | ||
| 13 | fun onCreateView() { | ||
| 14 | loadGames() | ||
| 15 | } | ||
| 16 | |||
| 17 | fun refresh() { | ||
| 18 | Log.debug("[PlatformGamesPresenter] : Refreshing...") | ||
| 19 | loadGames() | ||
| 20 | } | ||
| 21 | |||
| 22 | private fun loadGames() { | ||
| 23 | Log.debug("[PlatformGamesPresenter] : Loading games...") | ||
| 24 | val databaseHelper = YuzuApplication.databaseHelper | ||
| 25 | databaseHelper!!.games | ||
| 26 | .subscribeOn(Schedulers.io()) | ||
| 27 | .observeOn(AndroidSchedulers.mainThread()) | ||
| 28 | .subscribe { games: Cursor? -> | ||
| 29 | Log.debug("[PlatformGamesPresenter] : Load finished, swapping cursor...") | ||
| 30 | view.showGames(games!!) | ||
| 31 | } | ||
| 32 | } | ||
| 33 | } | ||
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/platform/PlatformGamesView.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/platform/PlatformGamesView.kt deleted file mode 100644 index 4132e560f..000000000 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/platform/PlatformGamesView.kt +++ /dev/null | |||
| @@ -1,24 +0,0 @@ | |||
| 1 | // SPDX-FileCopyrightText: 2023 yuzu Emulator Project | ||
| 2 | // SPDX-License-Identifier: GPL-2.0-or-later | ||
| 3 | |||
| 4 | package org.yuzu.yuzu_emu.ui.platform | ||
| 5 | |||
| 6 | import android.database.Cursor | ||
| 7 | |||
| 8 | /** | ||
| 9 | * Abstraction for a screen representing a single platform's games. | ||
| 10 | */ | ||
| 11 | interface PlatformGamesView { | ||
| 12 | /** | ||
| 13 | * Tell the view to refresh its contents. | ||
| 14 | */ | ||
| 15 | fun refresh() | ||
| 16 | |||
| 17 | /** | ||
| 18 | * To be called when an asynchronous database read completes. Passes the | ||
| 19 | * result, in this case a [Cursor], to the view. | ||
| 20 | * | ||
| 21 | * @param games A Cursor containing the games read from the database. | ||
| 22 | */ | ||
| 23 | fun showGames(games: Cursor) | ||
| 24 | } | ||
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/Action1.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/Action1.kt deleted file mode 100644 index 9041a7bee..000000000 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/Action1.kt +++ /dev/null | |||
| @@ -1,8 +0,0 @@ | |||
| 1 | // SPDX-FileCopyrightText: 2023 yuzu Emulator Project | ||
| 2 | // SPDX-License-Identifier: GPL-2.0-or-later | ||
| 3 | |||
| 4 | package org.yuzu.yuzu_emu.utils | ||
| 5 | |||
| 6 | interface Action1<T> { | ||
| 7 | fun call(t: T?) | ||
| 8 | } | ||
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/AddDirectoryHelper.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/AddDirectoryHelper.kt deleted file mode 100644 index acec7ba5e..000000000 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/AddDirectoryHelper.kt +++ /dev/null | |||
| @@ -1,30 +0,0 @@ | |||
| 1 | // SPDX-FileCopyrightText: 2023 yuzu Emulator Project | ||
| 2 | // SPDX-License-Identifier: GPL-2.0-or-later | ||
| 3 | |||
| 4 | package org.yuzu.yuzu_emu.utils | ||
| 5 | |||
| 6 | import android.content.AsyncQueryHandler | ||
| 7 | import android.content.ContentValues | ||
| 8 | import android.content.Context | ||
| 9 | import android.net.Uri | ||
| 10 | import org.yuzu.yuzu_emu.model.GameDatabase | ||
| 11 | import org.yuzu.yuzu_emu.model.GameProvider | ||
| 12 | |||
| 13 | class AddDirectoryHelper(private val context: Context) { | ||
| 14 | fun addDirectory(dir: String?, onAddUnit: () -> Unit) { | ||
| 15 | val handler: AsyncQueryHandler = object : AsyncQueryHandler(context.contentResolver) { | ||
| 16 | override fun onInsertComplete(token: Int, cookie: Any?, uri: Uri) { | ||
| 17 | onAddUnit.invoke() | ||
| 18 | } | ||
| 19 | } | ||
| 20 | |||
| 21 | val file = ContentValues() | ||
| 22 | file.put(GameDatabase.KEY_FOLDER_PATH, dir) | ||
| 23 | handler.startInsert( | ||
| 24 | 0, // We don't need to identify this call to the handler | ||
| 25 | null, // We don't need to pass additional data to the handler | ||
| 26 | GameProvider.URI_FOLDER, // Tell the GameProvider we are adding a folder | ||
| 27 | file | ||
| 28 | ) | ||
| 29 | } | ||
| 30 | } | ||
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/GameHelper.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/GameHelper.kt new file mode 100644 index 000000000..6dfd8b7f8 --- /dev/null +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/GameHelper.kt | |||
| @@ -0,0 +1,72 @@ | |||
| 1 | // SPDX-FileCopyrightText: 2023 yuzu Emulator Project | ||
| 2 | // SPDX-License-Identifier: GPL-2.0-or-later | ||
| 3 | |||
| 4 | package org.yuzu.yuzu_emu.utils | ||
| 5 | |||
| 6 | import android.net.Uri | ||
| 7 | import androidx.preference.PreferenceManager | ||
| 8 | import org.yuzu.yuzu_emu.NativeLibrary | ||
| 9 | import org.yuzu.yuzu_emu.YuzuApplication | ||
| 10 | import org.yuzu.yuzu_emu.model.Game | ||
| 11 | import java.util.* | ||
| 12 | import kotlin.collections.ArrayList | ||
| 13 | |||
| 14 | object GameHelper { | ||
| 15 | const val KEY_GAME_PATH = "game_path" | ||
| 16 | |||
| 17 | fun getGames(): ArrayList<Game> { | ||
| 18 | val games = ArrayList<Game>() | ||
| 19 | val context = YuzuApplication.appContext | ||
| 20 | val gamesDir = | ||
| 21 | PreferenceManager.getDefaultSharedPreferences(context).getString(KEY_GAME_PATH, "") | ||
| 22 | val gamesUri = Uri.parse(gamesDir) | ||
| 23 | |||
| 24 | // Ensure keys are loaded so that ROM metadata can be decrypted. | ||
| 25 | NativeLibrary.ReloadKeys() | ||
| 26 | |||
| 27 | val children = FileUtil.listFiles(context, gamesUri) | ||
| 28 | for (file in children) { | ||
| 29 | if (!file.isDirectory) { | ||
| 30 | val filename = file.uri.toString() | ||
| 31 | val extensionStart = filename.lastIndexOf('.') | ||
| 32 | if (extensionStart > 0) { | ||
| 33 | val fileExtension = filename.substring(extensionStart) | ||
| 34 | |||
| 35 | // Check that the file has an extension we care about before trying to read out of it. | ||
| 36 | if (Game.extensions.contains(fileExtension.lowercase(Locale.getDefault()))) { | ||
| 37 | games.add(getGame(filename)) | ||
| 38 | } | ||
| 39 | } | ||
| 40 | } | ||
| 41 | } | ||
| 42 | |||
| 43 | return games | ||
| 44 | } | ||
| 45 | |||
| 46 | private fun getGame(filePath: String): Game { | ||
| 47 | var name = NativeLibrary.GetTitle(filePath) | ||
| 48 | |||
| 49 | // If the game's title field is empty, use the filename. | ||
| 50 | if (name.isEmpty()) { | ||
| 51 | name = filePath.substring(filePath.lastIndexOf("/") + 1) | ||
| 52 | } | ||
| 53 | var gameId = NativeLibrary.GetGameId(filePath) | ||
| 54 | |||
| 55 | // If the game's ID field is empty, use the filename without extension. | ||
| 56 | if (gameId.isEmpty()) { | ||
| 57 | gameId = filePath.substring( | ||
| 58 | filePath.lastIndexOf("/") + 1, | ||
| 59 | filePath.lastIndexOf(".") | ||
| 60 | ) | ||
| 61 | } | ||
| 62 | |||
| 63 | return Game( | ||
| 64 | name, | ||
| 65 | NativeLibrary.GetDescription(filePath).replace("\n", " "), | ||
| 66 | NativeLibrary.GetRegions(filePath), | ||
| 67 | filePath, | ||
| 68 | gameId, | ||
| 69 | NativeLibrary.GetCompany(filePath) | ||
| 70 | ) | ||
| 71 | } | ||
| 72 | } | ||
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/viewholders/GameViewHolder.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/viewholders/GameViewHolder.kt deleted file mode 100644 index 51420448f..000000000 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/viewholders/GameViewHolder.kt +++ /dev/null | |||
| @@ -1,16 +0,0 @@ | |||
| 1 | // SPDX-FileCopyrightText: 2023 yuzu Emulator Project | ||
| 2 | // SPDX-License-Identifier: GPL-2.0-or-later | ||
| 3 | |||
| 4 | package org.yuzu.yuzu_emu.viewholders | ||
| 5 | |||
| 6 | import androidx.recyclerview.widget.RecyclerView | ||
| 7 | import org.yuzu.yuzu_emu.databinding.CardGameBinding | ||
| 8 | import org.yuzu.yuzu_emu.model.Game | ||
| 9 | |||
| 10 | class GameViewHolder(val binding: CardGameBinding) : RecyclerView.ViewHolder(binding.root) { | ||
| 11 | lateinit var game: Game | ||
| 12 | |||
| 13 | init { | ||
| 14 | itemView.tag = this | ||
| 15 | } | ||
| 16 | } | ||