From 4ce86a526c038ddea0694b299338ec1e2699d38b Mon Sep 17 00:00:00 2001 From: Charles Lombardo Date: Wed, 8 Mar 2023 15:38:16 -0500 Subject: [PATCH] android: Convert GameDatabase to Kotlin --- .../org/yuzu/yuzu_emu/model/GameDatabase.java | 275 ------------------ .../org/yuzu/yuzu_emu/model/GameDatabase.kt | 260 +++++++++++++++++ 2 files changed, 260 insertions(+), 275 deletions(-) delete mode 100644 src/android/app/src/main/java/org/yuzu/yuzu_emu/model/GameDatabase.java create mode 100644 src/android/app/src/main/java/org/yuzu/yuzu_emu/model/GameDatabase.kt diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/GameDatabase.java b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/GameDatabase.java deleted file mode 100644 index a10ac6ff2..000000000 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/GameDatabase.java +++ /dev/null @@ -1,275 +0,0 @@ -package org.yuzu.yuzu_emu.model; - -import android.content.ContentValues; -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 java.io.File; -import java.util.Arrays; -import java.util.HashSet; -import java.util.Set; - -import rx.Observable; - -/** - * A helper class that provides several utilities simplifying interaction with - * the SQLite database. - */ -public final class GameDatabase extends SQLiteOpenHelper { - public static final int COLUMN_DB_ID = 0; - public static final int GAME_COLUMN_PATH = 1; - public static final int GAME_COLUMN_TITLE = 2; - public static final int GAME_COLUMN_DESCRIPTION = 3; - public static final int GAME_COLUMN_REGIONS = 4; - public static final int GAME_COLUMN_GAME_ID = 5; - public static final int GAME_COLUMN_CAPTION = 6; - public static final int FOLDER_COLUMN_PATH = 1; - public static final String KEY_DB_ID = "_id"; - public static final String KEY_GAME_PATH = "path"; - public static final String KEY_GAME_TITLE = "title"; - public static final String KEY_GAME_DESCRIPTION = "description"; - public static final String KEY_GAME_REGIONS = "regions"; - public static final String KEY_GAME_ID = "game_id"; - public static final String KEY_GAME_COMPANY = "company"; - public static final String KEY_FOLDER_PATH = "path"; - public static final String TABLE_NAME_FOLDERS = "folders"; - public static final String TABLE_NAME_GAMES = "games"; - private static final int DB_VERSION = 2; - private static final String TYPE_PRIMARY = " INTEGER PRIMARY KEY"; - private static final String TYPE_INTEGER = " INTEGER"; - private static final String TYPE_STRING = " TEXT"; - - private static final String CONSTRAINT_UNIQUE = " UNIQUE"; - - private static final String SEPARATOR = ", "; - - private static final String 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 static final String SQL_CREATE_FOLDERS = "CREATE TABLE " + TABLE_NAME_FOLDERS + "(" - + KEY_DB_ID + TYPE_PRIMARY + SEPARATOR - + KEY_FOLDER_PATH + TYPE_STRING + CONSTRAINT_UNIQUE + ")"; - - private static final String SQL_DELETE_FOLDERS = "DROP TABLE IF EXISTS " + TABLE_NAME_FOLDERS; - private static final String SQL_DELETE_GAMES = "DROP TABLE IF EXISTS " + TABLE_NAME_GAMES; - private final Context context; - - public GameDatabase(Context context) { - // Superclass constructor builds a database or uses an existing one. - super(context, "games.db", null, DB_VERSION); - this.context = context; - } - - @Override - public void onCreate(SQLiteDatabase database) { - Log.debug("[GameDatabase] GameDatabase - Creating database..."); - - execSqlAndLog(database, SQL_CREATE_GAMES); - execSqlAndLog(database, SQL_CREATE_FOLDERS); - } - - @Override - public void onDowngrade(SQLiteDatabase database, int oldVersion, int newVersion) { - 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 - public void onUpgrade(SQLiteDatabase database, int oldVersion, int newVersion) { - 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); - } - - public void resetDatabase(SQLiteDatabase database) { - execSqlAndLog(database, SQL_DELETE_FOLDERS); - execSqlAndLog(database, SQL_CREATE_FOLDERS); - - execSqlAndLog(database, SQL_DELETE_GAMES); - execSqlAndLog(database, SQL_CREATE_GAMES); - } - - public void scanLibrary(SQLiteDatabase database) { - // Before scanning known folders, go through the game table and remove any entries for which the file itself is missing. - Cursor 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()) { - String gamePath = fileCursor.getString(GAME_COLUMN_PATH); - File game = new File(gamePath); - - if (!game.exists()) { - database.delete(TABLE_NAME_GAMES, - KEY_DB_ID + " = ?", - new String[]{Long.toString(fileCursor.getLong(COLUMN_DB_ID))}); - } - } - - // Get a cursor listing all the folders the user has added to the library. - Cursor 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. - - Set allowedExtensions = new HashSet(Arrays.asList( - ".xci", ".nsp", ".nca", ".nro")); - - // 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()) { - String folderPath = folderCursor.getString(FOLDER_COLUMN_PATH); - - Uri folderUri = Uri.parse(folderPath); - // If the folder is empty because it no longer exists, remove it from the library. - if (FileUtil.listFiles(context, folderUri).length == 0) { - Log.error( - "[GameDatabase] Folder no longer exists. Removing from the library: " + folderPath); - database.delete(TABLE_NAME_FOLDERS, - KEY_DB_ID + " = ?", - new String[]{Long.toString(folderCursor.getLong(COLUMN_DB_ID))}); - } - - this.addGamesRecursive(database, folderUri, allowedExtensions, 3); - } - - fileCursor.close(); - folderCursor.close(); - - database.close(); - } - - private void addGamesRecursive(SQLiteDatabase database, Uri parent, Set allowedExtensions, int depth) { - if (depth <= 0) { - return; - } - - // Ensure keys are loaded so that ROM metadata can be decrypted. - NativeLibrary.ReloadKeys(); - - MinimalDocumentFile[] children = FileUtil.listFiles(context, parent); - for (MinimalDocumentFile file : children) { - if (file.isDirectory()) { - Set newExtensions = new HashSet<>(Arrays.asList( - ".xci", ".nsp", ".nca", ".nro")); - this.addGamesRecursive(database, file.getUri(), newExtensions, depth - 1); - } else { - String filename = file.getUri().toString(); - - int extensionStart = filename.lastIndexOf('.'); - if (extensionStart > 0) { - String 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.toLowerCase())) { - attemptToAddGame(database, filename); - } - } - } - } - } - - private static void attemptToAddGame(SQLiteDatabase database, String filePath) { - String name = NativeLibrary.GetTitle(filePath); - - // If the game's title field is empty, use the filename. - if (name.isEmpty()) { - name = filePath.substring(filePath.lastIndexOf("/") + 1); - } - - String 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(".")); - } - - ContentValues game = Game.asContentValues(name, - NativeLibrary.GetDescription(filePath).replace("\n", " "), - NativeLibrary.GetRegions(filePath), - filePath, - gameId, - NativeLibrary.GetCompany(filePath)); - - // Try to update an existing game first. - int rowsMatched = database.update(TABLE_NAME_GAMES, // Which table to update. - game, - // The values to fill the row with. - KEY_GAME_ID + " = ?", - // The WHERE clause used to find the right row. - new String[]{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)); - } - } - - public Observable getGames() { - return Observable.create(subscriber -> - { - Log.info("[GameDatabase] Reading games list..."); - - SQLiteDatabase database = getReadableDatabase(); - Cursor 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 void execSqlAndLog(SQLiteDatabase database, String sql) { - Log.verbose("[GameDatabase] Executing SQL: " + sql); - database.execSQL(sql); - } -} 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 new file mode 100644 index 000000000..52326ed0a --- /dev/null +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/GameDatabase.kt @@ -0,0 +1,260 @@ +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)) + } + } + } +}