summaryrefslogtreecommitdiff
path: root/src/android
diff options
context:
space:
mode:
authorGravatar Charles Lombardo2023-03-08 15:38:16 -0500
committerGravatar bunnei2023-06-03 00:05:38 -0700
commit4ce86a526c038ddea0694b299338ec1e2699d38b (patch)
treeb2d95f1dd9ed301423e71b51d8d87446c6c8ae78 /src/android
parentandroid: Convert Game to Kotlin (diff)
downloadyuzu-4ce86a526c038ddea0694b299338ec1e2699d38b.tar.gz
yuzu-4ce86a526c038ddea0694b299338ec1e2699d38b.tar.xz
yuzu-4ce86a526c038ddea0694b299338ec1e2699d38b.zip
android: Convert GameDatabase to Kotlin
Diffstat (limited to 'src/android')
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/model/GameDatabase.java275
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/model/GameDatabase.kt260
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 @@
1package org.yuzu.yuzu_emu.model;
2
3import android.content.ContentValues;
4import android.content.Context;
5import android.database.Cursor;
6import android.database.sqlite.SQLiteDatabase;
7import android.database.sqlite.SQLiteOpenHelper;
8import android.net.Uri;
9
10import org.yuzu.yuzu_emu.NativeLibrary;
11import org.yuzu.yuzu_emu.utils.FileUtil;
12import org.yuzu.yuzu_emu.utils.Log;
13
14import java.io.File;
15import java.util.Arrays;
16import java.util.HashSet;
17import java.util.Set;
18
19import rx.Observable;
20
21/**
22 * A helper class that provides several utilities simplifying interaction with
23 * the SQLite database.
24 */
25public final class GameDatabase extends SQLiteOpenHelper {
26 public static final int COLUMN_DB_ID = 0;
27 public static final int GAME_COLUMN_PATH = 1;
28 public static final int GAME_COLUMN_TITLE = 2;
29 public static final int GAME_COLUMN_DESCRIPTION = 3;
30 public static final int GAME_COLUMN_REGIONS = 4;
31 public static final int GAME_COLUMN_GAME_ID = 5;
32 public static final int GAME_COLUMN_CAPTION = 6;
33 public static final int FOLDER_COLUMN_PATH = 1;
34 public static final String KEY_DB_ID = "_id";
35 public static final String KEY_GAME_PATH = "path";
36 public static final String KEY_GAME_TITLE = "title";
37 public static final String KEY_GAME_DESCRIPTION = "description";
38 public static final String KEY_GAME_REGIONS = "regions";
39 public static final String KEY_GAME_ID = "game_id";
40 public static final String KEY_GAME_COMPANY = "company";
41 public static final String KEY_FOLDER_PATH = "path";
42 public static final String TABLE_NAME_FOLDERS = "folders";
43 public static final String TABLE_NAME_GAMES = "games";
44 private static final int DB_VERSION = 2;
45 private static final String TYPE_PRIMARY = " INTEGER PRIMARY KEY";
46 private static final String TYPE_INTEGER = " INTEGER";
47 private static final String TYPE_STRING = " TEXT";
48
49 private static final String CONSTRAINT_UNIQUE = " UNIQUE";
50
51 private static final String SEPARATOR = ", ";
52
53 private static final String SQL_CREATE_GAMES = "CREATE TABLE " + TABLE_NAME_GAMES + "("
54 + KEY_DB_ID + TYPE_PRIMARY + SEPARATOR
55 + KEY_GAME_PATH + TYPE_STRING + SEPARATOR
56 + KEY_GAME_TITLE + TYPE_STRING + SEPARATOR
57 + KEY_GAME_DESCRIPTION + TYPE_STRING + SEPARATOR
58 + KEY_GAME_REGIONS + TYPE_STRING + SEPARATOR
59 + KEY_GAME_ID + TYPE_STRING + SEPARATOR
60 + KEY_GAME_COMPANY + TYPE_STRING + ")";
61
62 private static final String SQL_CREATE_FOLDERS = "CREATE TABLE " + TABLE_NAME_FOLDERS + "("
63 + KEY_DB_ID + TYPE_PRIMARY + SEPARATOR
64 + KEY_FOLDER_PATH + TYPE_STRING + CONSTRAINT_UNIQUE + ")";
65
66 private static final String SQL_DELETE_FOLDERS = "DROP TABLE IF EXISTS " + TABLE_NAME_FOLDERS;
67 private static final String SQL_DELETE_GAMES = "DROP TABLE IF EXISTS " + TABLE_NAME_GAMES;
68 private final Context context;
69
70 public GameDatabase(Context context) {
71 // Superclass constructor builds a database or uses an existing one.
72 super(context, "games.db", null, DB_VERSION);
73 this.context = context;
74 }
75
76 @Override
77 public void onCreate(SQLiteDatabase database) {
78 Log.debug("[GameDatabase] GameDatabase - Creating database...");
79
80 execSqlAndLog(database, SQL_CREATE_GAMES);
81 execSqlAndLog(database, SQL_CREATE_FOLDERS);
82 }
83
84 @Override
85 public void onDowngrade(SQLiteDatabase database, int oldVersion, int newVersion) {
86 Log.verbose("[GameDatabase] Downgrades not supporting, clearing databases..");
87 execSqlAndLog(database, SQL_DELETE_FOLDERS);
88 execSqlAndLog(database, SQL_CREATE_FOLDERS);
89
90 execSqlAndLog(database, SQL_DELETE_GAMES);
91 execSqlAndLog(database, SQL_CREATE_GAMES);
92 }
93
94 @Override
95 public void onUpgrade(SQLiteDatabase database, int oldVersion, int newVersion) {
96 Log.info("[GameDatabase] Upgrading database from schema version " + oldVersion + " to " +
97 newVersion);
98
99 // Delete all the games
100 execSqlAndLog(database, SQL_DELETE_GAMES);
101 execSqlAndLog(database, SQL_CREATE_GAMES);
102 }
103
104 public void resetDatabase(SQLiteDatabase database) {
105 execSqlAndLog(database, SQL_DELETE_FOLDERS);
106 execSqlAndLog(database, SQL_CREATE_FOLDERS);
107
108 execSqlAndLog(database, SQL_DELETE_GAMES);
109 execSqlAndLog(database, SQL_CREATE_GAMES);
110 }
111
112 public void scanLibrary(SQLiteDatabase database) {
113 // Before scanning known folders, go through the game table and remove any entries for which the file itself is missing.
114 Cursor fileCursor = database.query(TABLE_NAME_GAMES,
115 null, // Get all columns.
116 null, // Get all rows.
117 null,
118 null, // No grouping.
119 null,
120 null); // Order of games is irrelevant.
121
122 // Possibly overly defensive, but ensures that moveToNext() does not skip a row.
123 fileCursor.moveToPosition(-1);
124
125 while (fileCursor.moveToNext()) {
126 String gamePath = fileCursor.getString(GAME_COLUMN_PATH);
127 File game = new File(gamePath);
128
129 if (!game.exists()) {
130 database.delete(TABLE_NAME_GAMES,
131 KEY_DB_ID + " = ?",
132 new String[]{Long.toString(fileCursor.getLong(COLUMN_DB_ID))});
133 }
134 }
135
136 // Get a cursor listing all the folders the user has added to the library.
137 Cursor folderCursor = database.query(TABLE_NAME_FOLDERS,
138 null, // Get all columns.
139 null, // Get all rows.
140 null,
141 null, // No grouping.
142 null,
143 null); // Order of folders is irrelevant.
144
145 Set<String> allowedExtensions = new HashSet<String>(Arrays.asList(
146 ".xci", ".nsp", ".nca", ".nro"));
147
148 // Possibly overly defensive, but ensures that moveToNext() does not skip a row.
149 folderCursor.moveToPosition(-1);
150
151 // Iterate through all results of the DB query (i.e. all folders in the library.)
152 while (folderCursor.moveToNext()) {
153 String folderPath = folderCursor.getString(FOLDER_COLUMN_PATH);
154
155 Uri folderUri = Uri.parse(folderPath);
156 // If the folder is empty because it no longer exists, remove it from the library.
157 if (FileUtil.listFiles(context, folderUri).length == 0) {
158 Log.error(
159 "[GameDatabase] Folder no longer exists. Removing from the library: " + folderPath);
160 database.delete(TABLE_NAME_FOLDERS,
161 KEY_DB_ID + " = ?",
162 new String[]{Long.toString(folderCursor.getLong(COLUMN_DB_ID))});
163 }
164
165 this.addGamesRecursive(database, folderUri, allowedExtensions, 3);
166 }
167
168 fileCursor.close();
169 folderCursor.close();
170
171 database.close();
172 }
173
174 private void addGamesRecursive(SQLiteDatabase database, Uri parent, Set<String> allowedExtensions, int depth) {
175 if (depth <= 0) {
176 return;
177 }
178
179 // Ensure keys are loaded so that ROM metadata can be decrypted.
180 NativeLibrary.ReloadKeys();
181
182 MinimalDocumentFile[] children = FileUtil.listFiles(context, parent);
183 for (MinimalDocumentFile file : children) {
184 if (file.isDirectory()) {
185 Set<String> newExtensions = new HashSet<>(Arrays.asList(
186 ".xci", ".nsp", ".nca", ".nro"));
187 this.addGamesRecursive(database, file.getUri(), newExtensions, depth - 1);
188 } else {
189 String filename = file.getUri().toString();
190
191 int extensionStart = filename.lastIndexOf('.');
192 if (extensionStart > 0) {
193 String fileExtension = filename.substring(extensionStart);
194
195 // Check that the file has an extension we care about before trying to read out of it.
196 if (allowedExtensions.contains(fileExtension.toLowerCase())) {
197 attemptToAddGame(database, filename);
198 }
199 }
200 }
201 }
202 }
203
204 private static void attemptToAddGame(SQLiteDatabase database, String filePath) {
205 String name = NativeLibrary.GetTitle(filePath);
206
207 // If the game's title field is empty, use the filename.
208 if (name.isEmpty()) {
209 name = filePath.substring(filePath.lastIndexOf("/") + 1);
210 }
211
212 String gameId = NativeLibrary.GetGameId(filePath);
213
214 // If the game's ID field is empty, use the filename without extension.
215 if (gameId.isEmpty()) {
216 gameId = filePath.substring(filePath.lastIndexOf("/") + 1,
217 filePath.lastIndexOf("."));
218 }
219
220 ContentValues game = Game.asContentValues(name,
221 NativeLibrary.GetDescription(filePath).replace("\n", " "),
222 NativeLibrary.GetRegions(filePath),
223 filePath,
224 gameId,
225 NativeLibrary.GetCompany(filePath));
226
227 // Try to update an existing game first.
228 int rowsMatched = database.update(TABLE_NAME_GAMES, // Which table to update.
229 game,
230 // The values to fill the row with.
231 KEY_GAME_ID + " = ?",
232 // The WHERE clause used to find the right row.
233 new String[]{game.getAsString(
234 KEY_GAME_ID)}); // The ? in WHERE clause is replaced with this,
235 // which is provided as an array because there
236 // could potentially be more than one argument.
237
238 // If update fails, insert a new game instead.
239 if (rowsMatched == 0) {
240 Log.verbose("[GameDatabase] Adding game: " + game.getAsString(KEY_GAME_TITLE));
241 database.insert(TABLE_NAME_GAMES, null, game);
242 } else {
243 Log.verbose("[GameDatabase] Updated game: " + game.getAsString(KEY_GAME_TITLE));
244 }
245 }
246
247 public Observable<Cursor> getGames() {
248 return Observable.create(subscriber ->
249 {
250 Log.info("[GameDatabase] Reading games list...");
251
252 SQLiteDatabase database = getReadableDatabase();
253 Cursor resultCursor = database.query(
254 TABLE_NAME_GAMES,
255 null,
256 null,
257 null,
258 null,
259 null,
260 KEY_GAME_TITLE + " ASC"
261 );
262
263 // Pass the result cursor to the consumer.
264 subscriber.onNext(resultCursor);
265
266 // Tell the consumer we're done; it will unsubscribe implicitly.
267 subscriber.onCompleted();
268 });
269 }
270
271 private void execSqlAndLog(SQLiteDatabase database, String sql) {
272 Log.verbose("[GameDatabase] Executing SQL: " + sql);
273 database.execSQL(sql);
274 }
275}
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 @@
1package org.yuzu.yuzu_emu.model
2
3import android.content.Context
4import android.database.Cursor
5import android.database.sqlite.SQLiteDatabase
6import android.database.sqlite.SQLiteOpenHelper
7import android.net.Uri
8import org.yuzu.yuzu_emu.NativeLibrary
9import org.yuzu.yuzu_emu.utils.FileUtil
10import org.yuzu.yuzu_emu.utils.Log
11import rx.Observable
12import rx.Subscriber
13import java.io.File
14import java.util.*
15
16/**
17 * A helper class that provides several utilities simplifying interaction with
18 * the SQLite database.
19 */
20class GameDatabase(private val context: Context) :
21 SQLiteOpenHelper(context, "games.db", null, DB_VERSION) {
22 override fun onCreate(database: SQLiteDatabase) {
23 Log.debug("[GameDatabase] GameDatabase - Creating database...")
24 execSqlAndLog(database, SQL_CREATE_GAMES)
25 execSqlAndLog(database, SQL_CREATE_FOLDERS)
26 }
27
28 override fun onDowngrade(database: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
29 Log.verbose("[GameDatabase] Downgrades not supporting, clearing databases..")
30 execSqlAndLog(database, SQL_DELETE_FOLDERS)
31 execSqlAndLog(database, SQL_CREATE_FOLDERS)
32 execSqlAndLog(database, SQL_DELETE_GAMES)
33 execSqlAndLog(database, SQL_CREATE_GAMES)
34 }
35
36 override fun onUpgrade(database: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
37 Log.info(
38 "[GameDatabase] Upgrading database from schema version $oldVersion to $newVersion"
39 )
40
41 // Delete all the games
42 execSqlAndLog(database, SQL_DELETE_GAMES)
43 execSqlAndLog(database, SQL_CREATE_GAMES)
44 }
45
46 fun resetDatabase(database: SQLiteDatabase) {
47 execSqlAndLog(database, SQL_DELETE_FOLDERS)
48 execSqlAndLog(database, SQL_CREATE_FOLDERS)
49 execSqlAndLog(database, SQL_DELETE_GAMES)
50 execSqlAndLog(database, SQL_CREATE_GAMES)
51 }
52
53 fun scanLibrary(database: SQLiteDatabase) {
54 // Before scanning known folders, go through the game table and remove any entries for which the file itself is missing.
55 val fileCursor = database.query(
56 TABLE_NAME_GAMES,
57 null, // Get all columns.
58 null, // Get all rows.
59 null,
60 null, // No grouping.
61 null,
62 null
63 ) // Order of games is irrelevant.
64
65 // Possibly overly defensive, but ensures that moveToNext() does not skip a row.
66 fileCursor.moveToPosition(-1)
67 while (fileCursor.moveToNext()) {
68 val gamePath = fileCursor.getString(GAME_COLUMN_PATH)
69 val game = File(gamePath)
70 if (!game.exists()) {
71 database.delete(
72 TABLE_NAME_GAMES,
73 "$KEY_DB_ID = ?",
74 arrayOf(fileCursor.getLong(COLUMN_DB_ID).toString())
75 )
76 }
77 }
78
79 // Get a cursor listing all the folders the user has added to the library.
80 val folderCursor = database.query(
81 TABLE_NAME_FOLDERS,
82 null, // Get all columns.
83 null, // Get all rows.
84 null,
85 null, // No grouping.
86 null,
87 null
88 ) // Order of folders is irrelevant.
89
90
91 // Possibly overly defensive, but ensures that moveToNext() does not skip a row.
92 folderCursor.moveToPosition(-1)
93
94 // Iterate through all results of the DB query (i.e. all folders in the library.)
95 while (folderCursor.moveToNext()) {
96 val folderPath = folderCursor.getString(FOLDER_COLUMN_PATH)
97 val folderUri = Uri.parse(folderPath)
98 // If the folder is empty because it no longer exists, remove it from the library.
99 if (FileUtil.listFiles(context, folderUri).isEmpty()) {
100 Log.error(
101 "[GameDatabase] Folder no longer exists. Removing from the library: $folderPath"
102 )
103 database.delete(
104 TABLE_NAME_FOLDERS,
105 "$KEY_DB_ID = ?",
106 arrayOf(folderCursor.getLong(COLUMN_DB_ID).toString())
107 )
108 }
109 addGamesRecursive(database, folderUri, Game.extensions, 3)
110 }
111 fileCursor.close()
112 folderCursor.close()
113 database.close()
114 }
115
116 private fun addGamesRecursive(
117 database: SQLiteDatabase,
118 parent: Uri,
119 allowedExtensions: Set<String>,
120 depth: Int
121 ) {
122 if (depth <= 0)
123 return
124
125 // Ensure keys are loaded so that ROM metadata can be decrypted.
126 NativeLibrary.ReloadKeys()
127 val children = FileUtil.listFiles(context, parent)
128 for (file in children) {
129 if (file.isDirectory) {
130 addGamesRecursive(database, file.uri, Game.extensions, depth - 1)
131 } else {
132 val filename = file.uri.toString()
133 val extensionStart = filename.lastIndexOf('.')
134 if (extensionStart > 0) {
135 val fileExtension = filename.substring(extensionStart)
136
137 // Check that the file has an extension we care about before trying to read out of it.
138 if (allowedExtensions.contains(fileExtension.lowercase(Locale.getDefault()))) {
139 attemptToAddGame(database, filename)
140 }
141 }
142 }
143 }
144 }
145 // Pass the result cursor to the consumer.
146
147 // Tell the consumer we're done; it will unsubscribe implicitly.
148 val games: Observable<Cursor?>
149 get() = Observable.create { subscriber: Subscriber<in Cursor?> ->
150 Log.info("[GameDatabase] Reading games list...")
151 val database = readableDatabase
152 val resultCursor = database.query(
153 TABLE_NAME_GAMES,
154 null,
155 null,
156 null,
157 null,
158 null,
159 "$KEY_GAME_TITLE ASC"
160 )
161
162 // Pass the result cursor to the consumer.
163 subscriber.onNext(resultCursor)
164
165 // Tell the consumer we're done; it will unsubscribe implicitly.
166 subscriber.onCompleted()
167 }
168
169 private fun execSqlAndLog(database: SQLiteDatabase, sql: String) {
170 Log.verbose("[GameDatabase] Executing SQL: $sql")
171 database.execSQL(sql)
172 }
173
174 companion object {
175 const val COLUMN_DB_ID = 0
176 const val GAME_COLUMN_PATH = 1
177 const val GAME_COLUMN_TITLE = 2
178 const val GAME_COLUMN_DESCRIPTION = 3
179 const val GAME_COLUMN_REGIONS = 4
180 const val GAME_COLUMN_GAME_ID = 5
181 const val GAME_COLUMN_CAPTION = 6
182 const val FOLDER_COLUMN_PATH = 1
183 const val KEY_DB_ID = "_id"
184 const val KEY_GAME_PATH = "path"
185 const val KEY_GAME_TITLE = "title"
186 const val KEY_GAME_DESCRIPTION = "description"
187 const val KEY_GAME_REGIONS = "regions"
188 const val KEY_GAME_ID = "game_id"
189 const val KEY_GAME_COMPANY = "company"
190 const val KEY_FOLDER_PATH = "path"
191 const val TABLE_NAME_FOLDERS = "folders"
192 const val TABLE_NAME_GAMES = "games"
193 private const val DB_VERSION = 2
194 private const val TYPE_PRIMARY = " INTEGER PRIMARY KEY"
195 private const val TYPE_INTEGER = " INTEGER"
196 private const val TYPE_STRING = " TEXT"
197 private const val CONSTRAINT_UNIQUE = " UNIQUE"
198 private const val SEPARATOR = ", "
199 private const val SQL_CREATE_GAMES = ("CREATE TABLE " + TABLE_NAME_GAMES + "("
200 + KEY_DB_ID + TYPE_PRIMARY + SEPARATOR
201 + KEY_GAME_PATH + TYPE_STRING + SEPARATOR
202 + KEY_GAME_TITLE + TYPE_STRING + SEPARATOR
203 + KEY_GAME_DESCRIPTION + TYPE_STRING + SEPARATOR
204 + KEY_GAME_REGIONS + TYPE_STRING + SEPARATOR
205 + KEY_GAME_ID + TYPE_STRING + SEPARATOR
206 + KEY_GAME_COMPANY + TYPE_STRING + ")")
207 private const val SQL_CREATE_FOLDERS = ("CREATE TABLE " + TABLE_NAME_FOLDERS + "("
208 + KEY_DB_ID + TYPE_PRIMARY + SEPARATOR
209 + KEY_FOLDER_PATH + TYPE_STRING + CONSTRAINT_UNIQUE + ")")
210 private const val SQL_DELETE_FOLDERS = "DROP TABLE IF EXISTS $TABLE_NAME_FOLDERS"
211 private const val SQL_DELETE_GAMES = "DROP TABLE IF EXISTS $TABLE_NAME_GAMES"
212 private fun attemptToAddGame(database: SQLiteDatabase, filePath: String) {
213 var name = NativeLibrary.GetTitle(filePath)
214
215 // If the game's title field is empty, use the filename.
216 if (name.isEmpty()) {
217 name = filePath.substring(filePath.lastIndexOf("/") + 1)
218 }
219 var gameId = NativeLibrary.GetGameId(filePath)
220
221 // If the game's ID field is empty, use the filename without extension.
222 if (gameId.isEmpty()) {
223 gameId = filePath.substring(
224 filePath.lastIndexOf("/") + 1,
225 filePath.lastIndexOf(".")
226 )
227 }
228 val game = Game.asContentValues(
229 name,
230 NativeLibrary.GetDescription(filePath).replace("\n", " "),
231 NativeLibrary.GetRegions(filePath),
232 filePath,
233 gameId,
234 NativeLibrary.GetCompany(filePath)
235 )
236
237 // Try to update an existing game first.
238 val rowsMatched = database.update(
239 TABLE_NAME_GAMES, // Which table to update.
240 game, // The values to fill the row with.
241 "$KEY_GAME_ID = ?", arrayOf(
242 game.getAsString(
243 KEY_GAME_ID
244 )
245 )
246 )
247 // The ? in WHERE clause is replaced with this,
248 // which is provided as an array because there
249 // could potentially be more than one argument.
250
251 // If update fails, insert a new game instead.
252 if (rowsMatched == 0) {
253 Log.verbose("[GameDatabase] Adding game: " + game.getAsString(KEY_GAME_TITLE))
254 database.insert(TABLE_NAME_GAMES, null, game)
255 } else {
256 Log.verbose("[GameDatabase] Updated game: " + game.getAsString(KEY_GAME_TITLE))
257 }
258 }
259 }
260}