From 4ce86a526c038ddea0694b299338ec1e2699d38b Mon Sep 17 00:00:00 2001
From: Charles Lombardo <clombardo169@gmail.com>
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 a10ac6ff2c..0000000000
--- 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<String> allowedExtensions = new HashSet<String>(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<String> 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<String> 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<Cursor> 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 0000000000..52326ed0ae
--- /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<String>,
+        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<Cursor?>
+        get() = Observable.create { subscriber: Subscriber<in Cursor?> ->
+            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))
+            }
+        }
+    }
+}