yuzu-emu
/
yuzu
Archived
1
0
Fork 0

android: Convert GameDatabase to Kotlin

This commit is contained in:
Charles Lombardo 2023-03-08 15:38:16 -05:00 committed by bunnei
parent bbe5dee9f8
commit 4ce86a526c
2 changed files with 260 additions and 275 deletions

View File

@ -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);
}
}

View File

@ -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))
}
}
}
}