diff options
| -rw-r--r-- | src/android/app/src/main/java/org/yuzu/yuzu_emu/model/GameDatabase.java | 275 | ||||
| -rw-r--r-- | src/android/app/src/main/java/org/yuzu/yuzu_emu/model/GameDatabase.kt | 260 | 
2 files changed, 260 insertions, 275 deletions
| 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<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 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<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)) +            } +        } +    } +} | 
