diff --git a/src/android/app/build.gradle b/src/android/app/build.gradle
index 22f2d4b800..a82d2706bf 100644
--- a/src/android/app/build.gradle
+++ b/src/android/app/build.gradle
@@ -140,10 +140,6 @@ dependencies {
implementation "io.coil-kt:coil:2.2.2"
implementation 'androidx.core:core-splashscreen:1.0.0'
implementation 'androidx.window:window:1.0.0'
-
- // Allows FRP-style asynchronous operations in Android.
- implementation 'io.reactivex:rxandroid:1.2.1'
- implementation 'com.nononsenseapps:filepicker:4.2.1'
implementation 'org.ini4j:ini4j:0.5.4'
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
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 a5c063d52d..18539af809 100644
--- a/src/android/app/src/main/AndroidManifest.xml
+++ b/src/android/app/src/main/AndroidManifest.xml
@@ -65,23 +65,6 @@
-
-
-
-
-
-
-
(),
+class GameAdapter(private val activity: AppCompatActivity, var games: ArrayList) :
+ 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 binding = CardGameBinding.inflate(LayoutInflater.from(parent.context))
@@ -60,114 +41,11 @@ class GameAdapter(private val activity: AppCompatActivity) : RecyclerView.Adapte
}
override fun onBindViewHolder(holder: GameViewHolder, position: Int) {
- if (isDatasetValid) {
- if (cursor!!.moveToPosition(position)) {
- // TODO These shouldn't be necessary once the move to a DB-based model is complete.
- val game = Game(
- cursor!!.getString(GameDatabase.GAME_COLUMN_TITLE),
- cursor!!.getString(GameDatabase.GAME_COLUMN_DESCRIPTION),
- cursor!!.getString(GameDatabase.GAME_COLUMN_REGIONS),
- cursor!!.getString(GameDatabase.GAME_COLUMN_PATH),
- cursor!!.getString(GameDatabase.GAME_COLUMN_GAME_ID),
- cursor!!.getString(GameDatabase.GAME_COLUMN_CAPTION)
- )
- holder.game = game
-
- holder.binding.imageGameScreen.scaleType = ImageView.ScaleType.CENTER_CROP
- activity.lifecycleScope.launch {
- val bitmap = decodeGameIcon(game.path)
- holder.binding.imageGameScreen.load(bitmap) {
- error(R.drawable.no_icon)
- crossfade(true)
- }
- }
-
- holder.binding.textGameTitle.text = game.title.replace("[\\t\\n\\r]+".toRegex(), " ")
- holder.binding.textGameCaption.text = game.company
-
- if (game.company.isEmpty()) {
- holder.binding.textGameCaption.visibility = View.GONE
- }
-
- val backgroundColorId =
- if (isValidGame(holder.game.path)) R.attr.colorSurface else R.attr.colorErrorContainer
- val itemView = holder.itemView
- itemView.setBackgroundColor(
- MaterialColors.getColor(
- itemView,
- 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.")
- }
+ holder.bind(games[position])
}
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()
+ return games.size
}
/**
@@ -180,11 +58,38 @@ class GameAdapter(private val activity: AppCompatActivity) : RecyclerView.Adapte
EmulationActivity.launch((view.context as AppCompatActivity), holder.game)
}
- private fun isValidGame(path: String): Boolean {
- return Stream.of(".rar", ".zip", ".7z", ".torrent", ".tar", ".gz")
- .noneMatch { suffix: String? ->
- path.lowercase(Locale.getDefault()).endsWith(suffix!!)
+ inner class GameViewHolder(val binding: CardGameBinding) :
+ RecyclerView.ViewHolder(binding.root) {
+ lateinit var game: Game
+
+ init {
+ itemView.tag = this
+ }
+
+ fun bind(game: Game) {
+ this.game = game
+
+ binding.imageGameScreen.scaleType = ImageView.ScaleType.CENTER_CROP
+ activity.lifecycleScope.launch {
+ val bitmap = decodeGameIcon(game.path)
+ binding.imageGameScreen.load(bitmap) {
+ error(R.drawable.no_icon)
+ crossfade(true)
+ }
}
+
+ binding.textGameTitle.text = game.title.replace("[\\t\\n\\r]+".toRegex(), " ")
+ binding.textGameCaption.text = game.company
+
+ if (game.company.isEmpty()) {
+ binding.textGameCaption.visibility = View.GONE
+ }
+ }
+ }
+
+ fun swapData(games: ArrayList) {
+ this.games = games
+ notifyDataSetChanged()
}
private fun decodeGameIcon(uri: String): Bitmap? {
@@ -196,18 +101,4 @@ class GameAdapter(private val activity: AppCompatActivity) : RecyclerView.Adapte
BitmapFactory.Options()
)
}
-
- private inner class GameDataSetObserver : DataSetObserver() {
- override fun onChanged() {
- super.onChanged()
- isDatasetValid = true
- notifyDataSetChanged()
- }
-
- override fun onInvalidated() {
- super.onInvalidated()
- isDatasetValid = false
- notifyDataSetChanged()
- }
- }
}
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 91f6c5d750..db494e40fe 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 @@
package org.yuzu.yuzu_emu.model
-import android.content.ContentValues
-import android.database.Cursor
import android.os.Parcelable
import kotlinx.parcelize.Parcelize
-import java.nio.file.Paths
import java.util.HashSet
@Parcelize
@@ -23,40 +20,5 @@ class Game(
val extensions: Set = HashSet(
listOf(".xci", ".nsp", ".nca", ".nro")
)
-
- @JvmStatic
- fun asContentValues(
- title: String?,
- description: String?,
- regions: String?,
- path: String?,
- gameId: String,
- company: String?
- ): ContentValues {
- var realGameId = gameId
- val values = ContentValues()
- if (realGameId.isEmpty()) {
- // Homebrew, etc. may not have a game ID, use filename as a unique identifier
- realGameId = Paths.get(path).fileName.toString()
- }
- values.put(GameDatabase.KEY_GAME_TITLE, title)
- values.put(GameDatabase.KEY_GAME_DESCRIPTION, description)
- values.put(GameDatabase.KEY_GAME_REGIONS, regions)
- values.put(GameDatabase.KEY_GAME_PATH, path)
- values.put(GameDatabase.KEY_GAME_ID, realGameId)
- values.put(GameDatabase.KEY_GAME_COMPANY, company)
- return values
- }
-
- fun fromCursor(cursor: Cursor): Game {
- return Game(
- cursor.getString(GameDatabase.GAME_COLUMN_TITLE),
- cursor.getString(GameDatabase.GAME_COLUMN_DESCRIPTION),
- cursor.getString(GameDatabase.GAME_COLUMN_REGIONS),
- cursor.getString(GameDatabase.GAME_COLUMN_PATH),
- cursor.getString(GameDatabase.GAME_COLUMN_GAME_ID),
- cursor.getString(GameDatabase.GAME_COLUMN_CAPTION)
- )
- }
}
}
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 c66183516b..0000000000
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/GameDatabase.kt
+++ /dev/null
@@ -1,263 +0,0 @@
-// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
-// SPDX-License-Identifier: GPL-2.0-or-later
-
-package org.yuzu.yuzu_emu.model
-
-import android.content.Context
-import android.database.Cursor
-import android.database.sqlite.SQLiteDatabase
-import android.database.sqlite.SQLiteOpenHelper
-import android.net.Uri
-import org.yuzu.yuzu_emu.NativeLibrary
-import org.yuzu.yuzu_emu.utils.FileUtil
-import org.yuzu.yuzu_emu.utils.Log
-import rx.Observable
-import rx.Subscriber
-import java.io.File
-import java.util.*
-
-/**
- * A helper class that provides several utilities simplifying interaction with
- * the SQLite database.
- */
-class GameDatabase(private val context: Context) :
- SQLiteOpenHelper(context, "games.db", null, DB_VERSION) {
- override fun onCreate(database: SQLiteDatabase) {
- Log.debug("[GameDatabase] GameDatabase - Creating database...")
- execSqlAndLog(database, SQL_CREATE_GAMES)
- execSqlAndLog(database, SQL_CREATE_FOLDERS)
- }
-
- override fun onDowngrade(database: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
- Log.verbose("[GameDatabase] Downgrades not supporting, clearing databases..")
- execSqlAndLog(database, SQL_DELETE_FOLDERS)
- execSqlAndLog(database, SQL_CREATE_FOLDERS)
- execSqlAndLog(database, SQL_DELETE_GAMES)
- execSqlAndLog(database, SQL_CREATE_GAMES)
- }
-
- override fun onUpgrade(database: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
- Log.info(
- "[GameDatabase] Upgrading database from schema version $oldVersion to $newVersion"
- )
-
- // Delete all the games
- execSqlAndLog(database, SQL_DELETE_GAMES)
- execSqlAndLog(database, SQL_CREATE_GAMES)
- }
-
- fun resetDatabase(database: SQLiteDatabase) {
- execSqlAndLog(database, SQL_DELETE_FOLDERS)
- execSqlAndLog(database, SQL_CREATE_FOLDERS)
- execSqlAndLog(database, SQL_DELETE_GAMES)
- execSqlAndLog(database, SQL_CREATE_GAMES)
- }
-
- fun scanLibrary(database: SQLiteDatabase) {
- // Before scanning known folders, go through the game table and remove any entries for which the file itself is missing.
- val fileCursor = database.query(
- TABLE_NAME_GAMES,
- null, // Get all columns.
- null, // Get all rows.
- null,
- null, // No grouping.
- null,
- null
- ) // Order of games is irrelevant.
-
- // Possibly overly defensive, but ensures that moveToNext() does not skip a row.
- fileCursor.moveToPosition(-1)
- while (fileCursor.moveToNext()) {
- val gamePath = fileCursor.getString(GAME_COLUMN_PATH)
- val game = File(gamePath)
- if (!game.exists()) {
- database.delete(
- TABLE_NAME_GAMES,
- "$KEY_DB_ID = ?",
- arrayOf(fileCursor.getLong(COLUMN_DB_ID).toString())
- )
- }
- }
-
- // Get a cursor listing all the folders the user has added to the library.
- val folderCursor = database.query(
- TABLE_NAME_FOLDERS,
- null, // Get all columns.
- null, // Get all rows.
- null,
- null, // No grouping.
- null,
- null
- ) // Order of folders is irrelevant.
-
-
- // Possibly overly defensive, but ensures that moveToNext() does not skip a row.
- folderCursor.moveToPosition(-1)
-
- // Iterate through all results of the DB query (i.e. all folders in the library.)
- while (folderCursor.moveToNext()) {
- val folderPath = folderCursor.getString(FOLDER_COLUMN_PATH)
- val folderUri = Uri.parse(folderPath)
- // If the folder is empty because it no longer exists, remove it from the library.
- if (FileUtil.listFiles(context, folderUri).isEmpty()) {
- Log.error(
- "[GameDatabase] Folder no longer exists. Removing from the library: $folderPath"
- )
- database.delete(
- TABLE_NAME_FOLDERS,
- "$KEY_DB_ID = ?",
- arrayOf(folderCursor.getLong(COLUMN_DB_ID).toString())
- )
- }
- addGamesRecursive(database, folderUri, Game.extensions, 3)
- }
- fileCursor.close()
- folderCursor.close()
- database.close()
- }
-
- private fun addGamesRecursive(
- database: SQLiteDatabase,
- parent: Uri,
- allowedExtensions: Set,
- depth: Int
- ) {
- if (depth <= 0)
- return
-
- // Ensure keys are loaded so that ROM metadata can be decrypted.
- NativeLibrary.ReloadKeys()
- val children = FileUtil.listFiles(context, parent)
- for (file in children) {
- if (file.isDirectory) {
- addGamesRecursive(database, file.uri, Game.extensions, depth - 1)
- } else {
- val filename = file.uri.toString()
- val extensionStart = filename.lastIndexOf('.')
- if (extensionStart > 0) {
- val fileExtension = filename.substring(extensionStart)
-
- // Check that the file has an extension we care about before trying to read out of it.
- if (allowedExtensions.contains(fileExtension.lowercase(Locale.getDefault()))) {
- attemptToAddGame(database, filename)
- }
- }
- }
- }
- }
- // Pass the result cursor to the consumer.
-
- // Tell the consumer we're done; it will unsubscribe implicitly.
- val games: Observable
- get() = Observable.create { subscriber: Subscriber ->
- Log.info("[GameDatabase] Reading games list...")
- val database = readableDatabase
- val resultCursor = database.query(
- TABLE_NAME_GAMES,
- null,
- null,
- null,
- null,
- null,
- "$KEY_GAME_TITLE ASC"
- )
-
- // Pass the result cursor to the consumer.
- subscriber.onNext(resultCursor)
-
- // Tell the consumer we're done; it will unsubscribe implicitly.
- subscriber.onCompleted()
- }
-
- private fun execSqlAndLog(database: SQLiteDatabase, sql: String) {
- Log.verbose("[GameDatabase] Executing SQL: $sql")
- database.execSQL(sql)
- }
-
- companion object {
- const val COLUMN_DB_ID = 0
- const val GAME_COLUMN_PATH = 1
- const val GAME_COLUMN_TITLE = 2
- const val GAME_COLUMN_DESCRIPTION = 3
- const val GAME_COLUMN_REGIONS = 4
- const val GAME_COLUMN_GAME_ID = 5
- const val GAME_COLUMN_CAPTION = 6
- const val FOLDER_COLUMN_PATH = 1
- const val KEY_DB_ID = "_id"
- const val KEY_GAME_PATH = "path"
- const val KEY_GAME_TITLE = "title"
- const val KEY_GAME_DESCRIPTION = "description"
- const val KEY_GAME_REGIONS = "regions"
- const val KEY_GAME_ID = "game_id"
- const val KEY_GAME_COMPANY = "company"
- const val KEY_FOLDER_PATH = "path"
- const val TABLE_NAME_FOLDERS = "folders"
- const val TABLE_NAME_GAMES = "games"
- private const val DB_VERSION = 2
- private const val TYPE_PRIMARY = " INTEGER PRIMARY KEY"
- private const val TYPE_INTEGER = " INTEGER"
- private const val TYPE_STRING = " TEXT"
- private const val CONSTRAINT_UNIQUE = " UNIQUE"
- private const val SEPARATOR = ", "
- private const val SQL_CREATE_GAMES = ("CREATE TABLE " + TABLE_NAME_GAMES + "("
- + KEY_DB_ID + TYPE_PRIMARY + SEPARATOR
- + KEY_GAME_PATH + TYPE_STRING + SEPARATOR
- + KEY_GAME_TITLE + TYPE_STRING + SEPARATOR
- + KEY_GAME_DESCRIPTION + TYPE_STRING + SEPARATOR
- + KEY_GAME_REGIONS + TYPE_STRING + SEPARATOR
- + KEY_GAME_ID + TYPE_STRING + SEPARATOR
- + KEY_GAME_COMPANY + TYPE_STRING + ")")
- private const val SQL_CREATE_FOLDERS = ("CREATE TABLE " + TABLE_NAME_FOLDERS + "("
- + KEY_DB_ID + TYPE_PRIMARY + SEPARATOR
- + KEY_FOLDER_PATH + TYPE_STRING + CONSTRAINT_UNIQUE + ")")
- private const val SQL_DELETE_FOLDERS = "DROP TABLE IF EXISTS $TABLE_NAME_FOLDERS"
- private const val SQL_DELETE_GAMES = "DROP TABLE IF EXISTS $TABLE_NAME_GAMES"
- private fun attemptToAddGame(database: SQLiteDatabase, filePath: String) {
- var name = NativeLibrary.GetTitle(filePath)
-
- // If the game's title field is empty, use the filename.
- if (name.isEmpty()) {
- name = filePath.substring(filePath.lastIndexOf("/") + 1)
- }
- var gameId = NativeLibrary.GetGameId(filePath)
-
- // If the game's ID field is empty, use the filename without extension.
- if (gameId.isEmpty()) {
- gameId = filePath.substring(
- filePath.lastIndexOf("/") + 1,
- filePath.lastIndexOf(".")
- )
- }
- val game = Game.asContentValues(
- name,
- NativeLibrary.GetDescription(filePath).replace("\n", " "),
- NativeLibrary.GetRegions(filePath),
- filePath,
- gameId,
- NativeLibrary.GetCompany(filePath)
- )
-
- // Try to update an existing game first.
- val rowsMatched = database.update(
- TABLE_NAME_GAMES, // Which table to update.
- game, // The values to fill the row with.
- "$KEY_GAME_ID = ?", arrayOf(
- game.getAsString(
- KEY_GAME_ID
- )
- )
- )
- // The ? in WHERE clause is replaced with this,
- // which is provided as an array because there
- // could potentially be more than one argument.
-
- // If update fails, insert a new game instead.
- if (rowsMatched == 0) {
- Log.verbose("[GameDatabase] Adding game: " + game.getAsString(KEY_GAME_TITLE))
- database.insert(TABLE_NAME_GAMES, null, game)
- } else {
- Log.verbose("[GameDatabase] Updated game: " + game.getAsString(KEY_GAME_TITLE))
- }
- }
- }
-}
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 5d8e5cc546..0000000000
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/GameProvider.kt
+++ /dev/null
@@ -1,130 +0,0 @@
-// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
-// SPDX-License-Identifier: GPL-2.0-or-later
-
-package org.yuzu.yuzu_emu.model
-
-import android.content.ContentProvider
-import android.content.ContentValues
-import android.database.Cursor
-import android.database.sqlite.SQLiteDatabase
-import android.net.Uri
-import org.yuzu.yuzu_emu.BuildConfig
-import org.yuzu.yuzu_emu.utils.Log
-
-/**
- * Provides an interface allowing Activities to interact with the SQLite database.
- * CRUD methods in this class can be called by Activities using getContentResolver().
- */
-class GameProvider : ContentProvider() {
- private var mDbHelper: GameDatabase? = null
- override fun onCreate(): Boolean {
- Log.info("[GameProvider] Creating Content Provider...")
- mDbHelper = GameDatabase(context!!)
- return true
- }
-
- override fun query(
- uri: Uri,
- projection: Array?,
- selection: String?,
- selectionArgs: Array?,
- sortOrder: String?
- ): Cursor? {
- Log.info("[GameProvider] Querying URI: $uri")
- val db = mDbHelper!!.readableDatabase
- val table = uri.lastPathSegment
- if (table == null) {
- Log.error("[GameProvider] Badly formatted URI: $uri")
- return null
- }
- val cursor = db.query(table, projection, selection, selectionArgs, null, null, sortOrder)
- cursor.setNotificationUri(context!!.contentResolver, uri)
- return cursor
- }
-
- override fun getType(uri: Uri): String? {
- Log.verbose("[GameProvider] Getting MIME type for URI: $uri")
- val lastSegment = uri.lastPathSegment
- if (lastSegment == null) {
- Log.error("[GameProvider] Badly formatted URI: $uri")
- return null
- }
- if (lastSegment == GameDatabase.TABLE_NAME_FOLDERS) {
- return MIME_TYPE_FOLDER
- } else if (lastSegment == GameDatabase.TABLE_NAME_GAMES) {
- return MIME_TYPE_GAME
- }
- Log.error("[GameProvider] Unknown MIME type for URI: $uri")
- return null
- }
-
- override fun insert(uri: Uri, values: ContentValues?): Uri {
- var realUri = uri
- Log.info("[GameProvider] Inserting row at URI: $realUri")
- val database = mDbHelper!!.writableDatabase
- val table = realUri.lastPathSegment
- if (table != null) {
- if (table == RESET_LIBRARY) {
- mDbHelper!!.resetDatabase(database)
- return realUri
- }
- if (table == REFRESH_LIBRARY) {
- Log.info(
- "[GameProvider] URI specified table REFRESH_LIBRARY. No insertion necessary; refreshing library contents..."
- )
- mDbHelper!!.scanLibrary(database)
- return realUri
- }
- val id =
- database.insertWithOnConflict(table, null, values, SQLiteDatabase.CONFLICT_IGNORE)
-
- // If insertion was successful...
- if (id > 0) {
- // If we just added a folder, add its contents to the game list.
- if (table == GameDatabase.TABLE_NAME_FOLDERS) {
- mDbHelper!!.scanLibrary(database)
- }
-
- // Notify the UI that its contents should be refreshed.
- context!!.contentResolver.notifyChange(realUri, null)
- realUri = Uri.withAppendedPath(realUri, id.toString())
- } else {
- Log.error("[GameProvider] Row already exists: $realUri id: $id")
- }
- } else {
- Log.error("[GameProvider] Badly formatted URI: $realUri")
- }
- database.close()
- return realUri
- }
-
- override fun delete(uri: Uri, selection: String?, selectionArgs: Array?): Int {
- Log.error("[GameProvider] Delete operations unsupported. URI: $uri")
- return 0
- }
-
- override fun update(
- uri: Uri, values: ContentValues?, selection: String?,
- selectionArgs: Array?
- ): Int {
- Log.error("[GameProvider] Update operations unsupported. URI: $uri")
- return 0
- }
-
- companion object {
- const val REFRESH_LIBRARY = "refresh"
- const val RESET_LIBRARY = "reset"
- private const val AUTHORITY = "content://${BuildConfig.APPLICATION_ID}.provider"
-
- @JvmField
- val URI_FOLDER: Uri = Uri.parse("$AUTHORITY/${GameDatabase.TABLE_NAME_FOLDERS}/")
-
- @JvmField
- val URI_REFRESH: Uri = Uri.parse("$AUTHORITY/$REFRESH_LIBRARY/")
-
- @JvmField
- val URI_RESET: Uri = Uri.parse("$AUTHORITY/$RESET_LIBRARY/")
- const val MIME_TYPE_FOLDER = "vnd.android.cursor.item/vnd.yuzu.folder"
- const val MIME_TYPE_GAME = "vnd.android.cursor.item/vnd.yuzu.game"
- }
-}
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 0000000000..fde99f1a27
--- /dev/null
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/GamesViewModel.kt
@@ -0,0 +1,18 @@
+package org.yuzu.yuzu_emu.model
+
+import androidx.lifecycle.LiveData
+import androidx.lifecycle.MutableLiveData
+import androidx.lifecycle.ViewModel
+
+class GamesViewModel : ViewModel() {
+ private val _games = MutableLiveData>()
+ val games: LiveData> get() = _games
+
+ init {
+ _games.value = ArrayList()
+ }
+
+ fun setGames(games: ArrayList) {
+ _games.value = games
+ }
+}
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 441c9da9c6..4885bc4bc7 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
import androidx.core.view.WindowInsetsCompat
import androidx.core.view.updatePadding
import androidx.lifecycle.lifecycleScope
+import androidx.preference.PreferenceManager
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
@@ -28,7 +29,6 @@ import org.yuzu.yuzu_emu.activities.EmulationActivity
import org.yuzu.yuzu_emu.databinding.ActivityMainBinding
import org.yuzu.yuzu_emu.databinding.DialogProgressBarBinding
import org.yuzu.yuzu_emu.features.settings.ui.SettingsActivity
-import org.yuzu.yuzu_emu.model.GameProvider
import org.yuzu.yuzu_emu.ui.platform.PlatformGamesFragment
import org.yuzu.yuzu_emu.utils.*
import java.io.IOException
@@ -82,11 +82,6 @@ class MainActivity : AppCompatActivity(), MainView {
)
}
- override fun onResume() {
- super.onResume()
- presenter.addDirIfNeeded(AddDirectoryHelper(this))
- }
-
override fun onCreateOptionsMenu(menu: Menu): Boolean {
menuInflater.inflate(R.menu.menu_game_grid, menu)
return true
@@ -99,11 +94,6 @@ class MainActivity : AppCompatActivity(), MainView {
binding.toolbarMain.subtitle = version
}
- override fun refresh() {
- contentResolver.insert(GameProvider.URI_REFRESH, null)
- refreshFragment()
- }
-
override fun launchSettingsActivity(menuTag: String) {
SettingsActivity.launch(this, menuTag, "")
}
@@ -185,10 +175,9 @@ class MainActivity : AppCompatActivity(), MainView {
// When a new directory is picked, we currently will reset the existing games
// database. This effectively means that only one game directory is supported.
- // TODO(bunnei): Consider fixing this in the future, or removing code for this.
- contentResolver.insert(GameProvider.URI_RESET, null)
- // Add the new directory
- presenter.onDirectorySelected(result.toString())
+ PreferenceManager.getDefaultSharedPreferences(applicationContext).edit()
+ .putString(GameHelper.KEY_GAME_PATH, result.toString())
+ .apply()
}
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 554542e05a..a7ddc333f9 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
import org.yuzu.yuzu_emu.BuildConfig
import org.yuzu.yuzu_emu.R
-import org.yuzu.yuzu_emu.YuzuApplication
import org.yuzu.yuzu_emu.features.settings.utils.SettingsFile
-import org.yuzu.yuzu_emu.utils.AddDirectoryHelper
class MainPresenter(private val view: MainView) {
- private var dirToAdd: String? = null
-
fun onCreate() {
val versionName = BuildConfig.VERSION_NAME
view.setVersionString(versionName)
- refreshGameList()
}
private fun launchFileListActivity(request: Int) {
@@ -48,23 +43,6 @@ class MainPresenter(private val view: MainView) {
return false
}
- fun addDirIfNeeded(helper: AddDirectoryHelper) {
- if (dirToAdd != null) {
- helper.addDirectory(dirToAdd) { view.refresh() }
- dirToAdd = null
- }
- }
-
- fun onDirectorySelected(dir: String?) {
- dirToAdd = dir
- }
-
- private fun refreshGameList() {
- val databaseHelper = YuzuApplication.databaseHelper
- databaseHelper!!.scanLibrary(databaseHelper.writableDatabase)
- view.refresh()
- }
-
companion object {
const val REQUEST_ADD_DIRECTORY = 1
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 dab3abe7ca..4dc9f07068 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 {
*/
fun setVersionString(version: String)
- /**
- * Tell the view to refresh its contents.
- */
- fun refresh()
fun launchSettingsActivity(menuTag: String)
+
fun launchFileListActivity(request: Int)
}
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 dcfac1b2a3..443a37cd2b 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 @@
package org.yuzu.yuzu_emu.ui.platform
-import android.database.Cursor
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
@@ -13,36 +12,40 @@ import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import androidx.core.view.updatePadding
import androidx.fragment.app.Fragment
+import androidx.lifecycle.ViewModelProvider
import com.google.android.material.color.MaterialColors
import org.yuzu.yuzu_emu.R
-import org.yuzu.yuzu_emu.YuzuApplication
import org.yuzu.yuzu_emu.adapters.GameAdapter
import org.yuzu.yuzu_emu.databinding.FragmentGridBinding
import org.yuzu.yuzu_emu.layout.AutofitGridLayoutManager
+import org.yuzu.yuzu_emu.model.GamesViewModel
+import org.yuzu.yuzu_emu.utils.GameHelper
-class PlatformGamesFragment : Fragment(), PlatformGamesView {
- private val presenter = PlatformGamesPresenter(this)
-
+class PlatformGamesFragment : Fragment() {
private var _binding: FragmentGridBinding? = null
private val binding get() = _binding!!
+ private lateinit var gamesViewModel: GamesViewModel
+
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
- presenter.onCreateView()
_binding = FragmentGridBinding.inflate(inflater)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ gamesViewModel = ViewModelProvider(requireActivity())[GamesViewModel::class.java]
+
binding.gridGames.apply {
layoutManager = AutofitGridLayoutManager(
requireContext(),
requireContext().resources.getDimensionPixelSize(R.dimen.card_width)
)
- adapter = GameAdapter(requireActivity() as AppCompatActivity)
+ adapter =
+ GameAdapter(requireActivity() as AppCompatActivity, gamesViewModel.games.value!!)
}
// Add swipe down to refresh gesture
@@ -59,7 +62,19 @@ class PlatformGamesFragment : Fragment(), PlatformGamesView {
MaterialColors.getColor(binding.swipeRefresh, R.attr.colorOnPrimary)
)
+ gamesViewModel.games.observe(viewLifecycleOwner) {
+ (binding.gridGames.adapter as GameAdapter).swapData(it)
+ updateTextView()
+ }
+
setInsets()
+
+ refresh()
+ }
+
+ override fun onResume() {
+ super.onResume()
+ refresh()
}
override fun onDestroyView() {
@@ -67,20 +82,8 @@ class PlatformGamesFragment : Fragment(), PlatformGamesView {
_binding = null
}
- override fun refresh() {
- val databaseHelper = YuzuApplication.databaseHelper
- databaseHelper!!.scanLibrary(databaseHelper.writableDatabase)
- presenter.refresh()
- updateTextView()
- }
-
- override fun showGames(games: Cursor) {
- if (_binding == null)
- return
-
- if (binding.gridGames.adapter != null) {
- (binding.gridGames.adapter as GameAdapter).swapCursor(games)
- }
+ fun refresh() {
+ gamesViewModel.setGames(GameHelper.getGames())
updateTextView()
}
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 0b9da5f579..0000000000
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/platform/PlatformGamesPresenter.kt
+++ /dev/null
@@ -1,33 +0,0 @@
-// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
-// SPDX-License-Identifier: GPL-2.0-or-later
-
-package org.yuzu.yuzu_emu.ui.platform
-
-import android.database.Cursor
-import org.yuzu.yuzu_emu.YuzuApplication
-import org.yuzu.yuzu_emu.utils.Log
-import rx.android.schedulers.AndroidSchedulers
-import rx.schedulers.Schedulers
-
-class PlatformGamesPresenter(private val view: PlatformGamesView) {
- fun onCreateView() {
- loadGames()
- }
-
- fun refresh() {
- Log.debug("[PlatformGamesPresenter] : Refreshing...")
- loadGames()
- }
-
- private fun loadGames() {
- Log.debug("[PlatformGamesPresenter] : Loading games...")
- val databaseHelper = YuzuApplication.databaseHelper
- databaseHelper!!.games
- .subscribeOn(Schedulers.io())
- .observeOn(AndroidSchedulers.mainThread())
- .subscribe { games: Cursor? ->
- Log.debug("[PlatformGamesPresenter] : Load finished, swapping cursor...")
- view.showGames(games!!)
- }
- }
-}
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 4132e560f1..0000000000
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/platform/PlatformGamesView.kt
+++ /dev/null
@@ -1,24 +0,0 @@
-// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
-// SPDX-License-Identifier: GPL-2.0-or-later
-
-package org.yuzu.yuzu_emu.ui.platform
-
-import android.database.Cursor
-
-/**
- * Abstraction for a screen representing a single platform's games.
- */
-interface PlatformGamesView {
- /**
- * Tell the view to refresh its contents.
- */
- fun refresh()
-
- /**
- * To be called when an asynchronous database read completes. Passes the
- * result, in this case a [Cursor], to the view.
- *
- * @param games A Cursor containing the games read from the database.
- */
- fun showGames(games: Cursor)
-}
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 9041a7bee7..0000000000
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/Action1.kt
+++ /dev/null
@@ -1,8 +0,0 @@
-// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
-// SPDX-License-Identifier: GPL-2.0-or-later
-
-package org.yuzu.yuzu_emu.utils
-
-interface Action1 {
- fun call(t: T?)
-}
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 acec7ba5e5..0000000000
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/AddDirectoryHelper.kt
+++ /dev/null
@@ -1,30 +0,0 @@
-// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
-// SPDX-License-Identifier: GPL-2.0-or-later
-
-package org.yuzu.yuzu_emu.utils
-
-import android.content.AsyncQueryHandler
-import android.content.ContentValues
-import android.content.Context
-import android.net.Uri
-import org.yuzu.yuzu_emu.model.GameDatabase
-import org.yuzu.yuzu_emu.model.GameProvider
-
-class AddDirectoryHelper(private val context: Context) {
- fun addDirectory(dir: String?, onAddUnit: () -> Unit) {
- val handler: AsyncQueryHandler = object : AsyncQueryHandler(context.contentResolver) {
- override fun onInsertComplete(token: Int, cookie: Any?, uri: Uri) {
- onAddUnit.invoke()
- }
- }
-
- val file = ContentValues()
- file.put(GameDatabase.KEY_FOLDER_PATH, dir)
- handler.startInsert(
- 0, // We don't need to identify this call to the handler
- null, // We don't need to pass additional data to the handler
- GameProvider.URI_FOLDER, // Tell the GameProvider we are adding a folder
- file
- )
- }
-}
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 0000000000..6dfd8b7f8b
--- /dev/null
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/GameHelper.kt
@@ -0,0 +1,72 @@
+// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+package org.yuzu.yuzu_emu.utils
+
+import android.net.Uri
+import androidx.preference.PreferenceManager
+import org.yuzu.yuzu_emu.NativeLibrary
+import org.yuzu.yuzu_emu.YuzuApplication
+import org.yuzu.yuzu_emu.model.Game
+import java.util.*
+import kotlin.collections.ArrayList
+
+object GameHelper {
+ const val KEY_GAME_PATH = "game_path"
+
+ fun getGames(): ArrayList {
+ val games = ArrayList()
+ val context = YuzuApplication.appContext
+ val gamesDir =
+ PreferenceManager.getDefaultSharedPreferences(context).getString(KEY_GAME_PATH, "")
+ val gamesUri = Uri.parse(gamesDir)
+
+ // Ensure keys are loaded so that ROM metadata can be decrypted.
+ NativeLibrary.ReloadKeys()
+
+ val children = FileUtil.listFiles(context, gamesUri)
+ for (file in children) {
+ if (!file.isDirectory) {
+ val filename = file.uri.toString()
+ val extensionStart = filename.lastIndexOf('.')
+ if (extensionStart > 0) {
+ val fileExtension = filename.substring(extensionStart)
+
+ // Check that the file has an extension we care about before trying to read out of it.
+ if (Game.extensions.contains(fileExtension.lowercase(Locale.getDefault()))) {
+ games.add(getGame(filename))
+ }
+ }
+ }
+ }
+
+ return games
+ }
+
+ private fun getGame(filePath: String): Game {
+ var name = NativeLibrary.GetTitle(filePath)
+
+ // If the game's title field is empty, use the filename.
+ if (name.isEmpty()) {
+ name = filePath.substring(filePath.lastIndexOf("/") + 1)
+ }
+ var gameId = NativeLibrary.GetGameId(filePath)
+
+ // If the game's ID field is empty, use the filename without extension.
+ if (gameId.isEmpty()) {
+ gameId = filePath.substring(
+ filePath.lastIndexOf("/") + 1,
+ filePath.lastIndexOf(".")
+ )
+ }
+
+ return Game(
+ name,
+ NativeLibrary.GetDescription(filePath).replace("\n", " "),
+ NativeLibrary.GetRegions(filePath),
+ filePath,
+ gameId,
+ NativeLibrary.GetCompany(filePath)
+ )
+ }
+}
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 51420448f6..0000000000
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/viewholders/GameViewHolder.kt
+++ /dev/null
@@ -1,16 +0,0 @@
-// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
-// SPDX-License-Identifier: GPL-2.0-or-later
-
-package org.yuzu.yuzu_emu.viewholders
-
-import androidx.recyclerview.widget.RecyclerView
-import org.yuzu.yuzu_emu.databinding.CardGameBinding
-import org.yuzu.yuzu_emu.model.Game
-
-class GameViewHolder(val binding: CardGameBinding) : RecyclerView.ViewHolder(binding.root) {
- lateinit var game: Game
-
- init {
- itemView.tag = this
- }
-}