Browse Source
android: Remove game database
android: Remove game database
The content provider + database solution was excessive and is now replaced with the simple file checks from before but turned into an array list held within a viewmodel.nce_cpp
committed by
bunnei
18 changed files with 154 additions and 773 deletions
-
4src/android/app/build.gradle
-
17src/android/app/src/main/AndroidManifest.xml
-
4src/android/app/src/main/java/org/yuzu/yuzu_emu/YuzuApplication.kt
-
179src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/GameAdapter.kt
-
38src/android/app/src/main/java/org/yuzu/yuzu_emu/model/Game.kt
-
263src/android/app/src/main/java/org/yuzu/yuzu_emu/model/GameDatabase.kt
-
130src/android/app/src/main/java/org/yuzu/yuzu_emu/model/GameProvider.kt
-
18src/android/app/src/main/java/org/yuzu/yuzu_emu/model/GamesViewModel.kt
-
19src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/main/MainActivity.kt
-
22src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/main/MainPresenter.kt
-
5src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/main/MainView.kt
-
45src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/platform/PlatformGamesFragment.kt
-
33src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/platform/PlatformGamesPresenter.kt
-
24src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/platform/PlatformGamesView.kt
-
8src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/Action1.kt
-
30src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/AddDirectoryHelper.kt
-
72src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/GameHelper.kt
-
16src/android/app/src/main/java/org/yuzu/yuzu_emu/viewholders/GameViewHolder.kt
@ -1,263 +0,0 @@ |
|||||
// SPDX-FileCopyrightText: 2023 yuzu Emulator Project |
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later |
|
||||
|
|
||||
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)) |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -1,130 +0,0 @@ |
|||||
// SPDX-FileCopyrightText: 2023 yuzu Emulator Project |
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later |
|
||||
|
|
||||
package org.yuzu.yuzu_emu.model |
|
||||
|
|
||||
import android.content.ContentProvider |
|
||||
import android.content.ContentValues |
|
||||
import android.database.Cursor |
|
||||
import android.database.sqlite.SQLiteDatabase |
|
||||
import android.net.Uri |
|
||||
import org.yuzu.yuzu_emu.BuildConfig |
|
||||
import org.yuzu.yuzu_emu.utils.Log |
|
||||
|
|
||||
/** |
|
||||
* Provides an interface allowing Activities to interact with the SQLite database. |
|
||||
* CRUD methods in this class can be called by Activities using getContentResolver(). |
|
||||
*/ |
|
||||
class GameProvider : ContentProvider() { |
|
||||
private var mDbHelper: GameDatabase? = null |
|
||||
override fun onCreate(): Boolean { |
|
||||
Log.info("[GameProvider] Creating Content Provider...") |
|
||||
mDbHelper = GameDatabase(context!!) |
|
||||
return true |
|
||||
} |
|
||||
|
|
||||
override fun query( |
|
||||
uri: Uri, |
|
||||
projection: Array<String>?, |
|
||||
selection: String?, |
|
||||
selectionArgs: Array<String>?, |
|
||||
sortOrder: String? |
|
||||
): Cursor? { |
|
||||
Log.info("[GameProvider] Querying URI: $uri") |
|
||||
val db = mDbHelper!!.readableDatabase |
|
||||
val table = uri.lastPathSegment |
|
||||
if (table == null) { |
|
||||
Log.error("[GameProvider] Badly formatted URI: $uri") |
|
||||
return null |
|
||||
} |
|
||||
val cursor = db.query(table, projection, selection, selectionArgs, null, null, sortOrder) |
|
||||
cursor.setNotificationUri(context!!.contentResolver, uri) |
|
||||
return cursor |
|
||||
} |
|
||||
|
|
||||
override fun getType(uri: Uri): String? { |
|
||||
Log.verbose("[GameProvider] Getting MIME type for URI: $uri") |
|
||||
val lastSegment = uri.lastPathSegment |
|
||||
if (lastSegment == null) { |
|
||||
Log.error("[GameProvider] Badly formatted URI: $uri") |
|
||||
return null |
|
||||
} |
|
||||
if (lastSegment == GameDatabase.TABLE_NAME_FOLDERS) { |
|
||||
return MIME_TYPE_FOLDER |
|
||||
} else if (lastSegment == GameDatabase.TABLE_NAME_GAMES) { |
|
||||
return MIME_TYPE_GAME |
|
||||
} |
|
||||
Log.error("[GameProvider] Unknown MIME type for URI: $uri") |
|
||||
return null |
|
||||
} |
|
||||
|
|
||||
override fun insert(uri: Uri, values: ContentValues?): Uri { |
|
||||
var realUri = uri |
|
||||
Log.info("[GameProvider] Inserting row at URI: $realUri") |
|
||||
val database = mDbHelper!!.writableDatabase |
|
||||
val table = realUri.lastPathSegment |
|
||||
if (table != null) { |
|
||||
if (table == RESET_LIBRARY) { |
|
||||
mDbHelper!!.resetDatabase(database) |
|
||||
return realUri |
|
||||
} |
|
||||
if (table == REFRESH_LIBRARY) { |
|
||||
Log.info( |
|
||||
"[GameProvider] URI specified table REFRESH_LIBRARY. No insertion necessary; refreshing library contents..." |
|
||||
) |
|
||||
mDbHelper!!.scanLibrary(database) |
|
||||
return realUri |
|
||||
} |
|
||||
val id = |
|
||||
database.insertWithOnConflict(table, null, values, SQLiteDatabase.CONFLICT_IGNORE) |
|
||||
|
|
||||
// If insertion was successful... |
|
||||
if (id > 0) { |
|
||||
// If we just added a folder, add its contents to the game list. |
|
||||
if (table == GameDatabase.TABLE_NAME_FOLDERS) { |
|
||||
mDbHelper!!.scanLibrary(database) |
|
||||
} |
|
||||
|
|
||||
// Notify the UI that its contents should be refreshed. |
|
||||
context!!.contentResolver.notifyChange(realUri, null) |
|
||||
realUri = Uri.withAppendedPath(realUri, id.toString()) |
|
||||
} else { |
|
||||
Log.error("[GameProvider] Row already exists: $realUri id: $id") |
|
||||
} |
|
||||
} else { |
|
||||
Log.error("[GameProvider] Badly formatted URI: $realUri") |
|
||||
} |
|
||||
database.close() |
|
||||
return realUri |
|
||||
} |
|
||||
|
|
||||
override fun delete(uri: Uri, selection: String?, selectionArgs: Array<String>?): Int { |
|
||||
Log.error("[GameProvider] Delete operations unsupported. URI: $uri") |
|
||||
return 0 |
|
||||
} |
|
||||
|
|
||||
override fun update( |
|
||||
uri: Uri, values: ContentValues?, selection: String?, |
|
||||
selectionArgs: Array<String>? |
|
||||
): Int { |
|
||||
Log.error("[GameProvider] Update operations unsupported. URI: $uri") |
|
||||
return 0 |
|
||||
} |
|
||||
|
|
||||
companion object { |
|
||||
const val REFRESH_LIBRARY = "refresh" |
|
||||
const val RESET_LIBRARY = "reset" |
|
||||
private const val AUTHORITY = "content://${BuildConfig.APPLICATION_ID}.provider" |
|
||||
|
|
||||
@JvmField |
|
||||
val URI_FOLDER: Uri = Uri.parse("$AUTHORITY/${GameDatabase.TABLE_NAME_FOLDERS}/") |
|
||||
|
|
||||
@JvmField |
|
||||
val URI_REFRESH: Uri = Uri.parse("$AUTHORITY/$REFRESH_LIBRARY/") |
|
||||
|
|
||||
@JvmField |
|
||||
val URI_RESET: Uri = Uri.parse("$AUTHORITY/$RESET_LIBRARY/") |
|
||||
const val MIME_TYPE_FOLDER = "vnd.android.cursor.item/vnd.yuzu.folder" |
|
||||
const val MIME_TYPE_GAME = "vnd.android.cursor.item/vnd.yuzu.game" |
|
||||
} |
|
||||
} |
|
||||
@ -0,0 +1,18 @@ |
|||||
|
package org.yuzu.yuzu_emu.model |
||||
|
|
||||
|
import androidx.lifecycle.LiveData |
||||
|
import androidx.lifecycle.MutableLiveData |
||||
|
import androidx.lifecycle.ViewModel |
||||
|
|
||||
|
class GamesViewModel : ViewModel() { |
||||
|
private val _games = MutableLiveData<ArrayList<Game>>() |
||||
|
val games: LiveData<ArrayList<Game>> get() = _games |
||||
|
|
||||
|
init { |
||||
|
_games.value = ArrayList() |
||||
|
} |
||||
|
|
||||
|
fun setGames(games: ArrayList<Game>) { |
||||
|
_games.value = games |
||||
|
} |
||||
|
} |
||||
@ -1,33 +0,0 @@ |
|||||
// SPDX-FileCopyrightText: 2023 yuzu Emulator Project |
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later |
|
||||
|
|
||||
package org.yuzu.yuzu_emu.ui.platform |
|
||||
|
|
||||
import android.database.Cursor |
|
||||
import org.yuzu.yuzu_emu.YuzuApplication |
|
||||
import org.yuzu.yuzu_emu.utils.Log |
|
||||
import rx.android.schedulers.AndroidSchedulers |
|
||||
import rx.schedulers.Schedulers |
|
||||
|
|
||||
class PlatformGamesPresenter(private val view: PlatformGamesView) { |
|
||||
fun onCreateView() { |
|
||||
loadGames() |
|
||||
} |
|
||||
|
|
||||
fun refresh() { |
|
||||
Log.debug("[PlatformGamesPresenter] : Refreshing...") |
|
||||
loadGames() |
|
||||
} |
|
||||
|
|
||||
private fun loadGames() { |
|
||||
Log.debug("[PlatformGamesPresenter] : Loading games...") |
|
||||
val databaseHelper = YuzuApplication.databaseHelper |
|
||||
databaseHelper!!.games |
|
||||
.subscribeOn(Schedulers.io()) |
|
||||
.observeOn(AndroidSchedulers.mainThread()) |
|
||||
.subscribe { games: Cursor? -> |
|
||||
Log.debug("[PlatformGamesPresenter] : Load finished, swapping cursor...") |
|
||||
view.showGames(games!!) |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -1,24 +0,0 @@ |
|||||
// SPDX-FileCopyrightText: 2023 yuzu Emulator Project |
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later |
|
||||
|
|
||||
package org.yuzu.yuzu_emu.ui.platform |
|
||||
|
|
||||
import android.database.Cursor |
|
||||
|
|
||||
/** |
|
||||
* Abstraction for a screen representing a single platform's games. |
|
||||
*/ |
|
||||
interface PlatformGamesView { |
|
||||
/** |
|
||||
* Tell the view to refresh its contents. |
|
||||
*/ |
|
||||
fun refresh() |
|
||||
|
|
||||
/** |
|
||||
* To be called when an asynchronous database read completes. Passes the |
|
||||
* result, in this case a [Cursor], to the view. |
|
||||
* |
|
||||
* @param games A Cursor containing the games read from the database. |
|
||||
*/ |
|
||||
fun showGames(games: Cursor) |
|
||||
} |
|
||||
@ -1,8 +0,0 @@ |
|||||
// SPDX-FileCopyrightText: 2023 yuzu Emulator Project |
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later |
|
||||
|
|
||||
package org.yuzu.yuzu_emu.utils |
|
||||
|
|
||||
interface Action1<T> { |
|
||||
fun call(t: T?) |
|
||||
} |
|
||||
@ -1,30 +0,0 @@ |
|||||
// SPDX-FileCopyrightText: 2023 yuzu Emulator Project |
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later |
|
||||
|
|
||||
package org.yuzu.yuzu_emu.utils |
|
||||
|
|
||||
import android.content.AsyncQueryHandler |
|
||||
import android.content.ContentValues |
|
||||
import android.content.Context |
|
||||
import android.net.Uri |
|
||||
import org.yuzu.yuzu_emu.model.GameDatabase |
|
||||
import org.yuzu.yuzu_emu.model.GameProvider |
|
||||
|
|
||||
class AddDirectoryHelper(private val context: Context) { |
|
||||
fun addDirectory(dir: String?, onAddUnit: () -> Unit) { |
|
||||
val handler: AsyncQueryHandler = object : AsyncQueryHandler(context.contentResolver) { |
|
||||
override fun onInsertComplete(token: Int, cookie: Any?, uri: Uri) { |
|
||||
onAddUnit.invoke() |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
val file = ContentValues() |
|
||||
file.put(GameDatabase.KEY_FOLDER_PATH, dir) |
|
||||
handler.startInsert( |
|
||||
0, // We don't need to identify this call to the handler |
|
||||
null, // We don't need to pass additional data to the handler |
|
||||
GameProvider.URI_FOLDER, // Tell the GameProvider we are adding a folder |
|
||||
file |
|
||||
) |
|
||||
} |
|
||||
} |
|
||||
@ -0,0 +1,72 @@ |
|||||
|
// SPDX-FileCopyrightText: 2023 yuzu Emulator Project |
||||
|
// SPDX-License-Identifier: GPL-2.0-or-later |
||||
|
|
||||
|
package org.yuzu.yuzu_emu.utils |
||||
|
|
||||
|
import android.net.Uri |
||||
|
import androidx.preference.PreferenceManager |
||||
|
import org.yuzu.yuzu_emu.NativeLibrary |
||||
|
import org.yuzu.yuzu_emu.YuzuApplication |
||||
|
import org.yuzu.yuzu_emu.model.Game |
||||
|
import java.util.* |
||||
|
import kotlin.collections.ArrayList |
||||
|
|
||||
|
object GameHelper { |
||||
|
const val KEY_GAME_PATH = "game_path" |
||||
|
|
||||
|
fun getGames(): ArrayList<Game> { |
||||
|
val games = ArrayList<Game>() |
||||
|
val context = YuzuApplication.appContext |
||||
|
val gamesDir = |
||||
|
PreferenceManager.getDefaultSharedPreferences(context).getString(KEY_GAME_PATH, "") |
||||
|
val gamesUri = Uri.parse(gamesDir) |
||||
|
|
||||
|
// Ensure keys are loaded so that ROM metadata can be decrypted. |
||||
|
NativeLibrary.ReloadKeys() |
||||
|
|
||||
|
val children = FileUtil.listFiles(context, gamesUri) |
||||
|
for (file in children) { |
||||
|
if (!file.isDirectory) { |
||||
|
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 (Game.extensions.contains(fileExtension.lowercase(Locale.getDefault()))) { |
||||
|
games.add(getGame(filename)) |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return games |
||||
|
} |
||||
|
|
||||
|
private fun getGame(filePath: String): Game { |
||||
|
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(".") |
||||
|
) |
||||
|
} |
||||
|
|
||||
|
return Game( |
||||
|
name, |
||||
|
NativeLibrary.GetDescription(filePath).replace("\n", " "), |
||||
|
NativeLibrary.GetRegions(filePath), |
||||
|
filePath, |
||||
|
gameId, |
||||
|
NativeLibrary.GetCompany(filePath) |
||||
|
) |
||||
|
} |
||||
|
} |
||||
@ -1,16 +0,0 @@ |
|||||
// SPDX-FileCopyrightText: 2023 yuzu Emulator Project |
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later |
|
||||
|
|
||||
package org.yuzu.yuzu_emu.viewholders |
|
||||
|
|
||||
import androidx.recyclerview.widget.RecyclerView |
|
||||
import org.yuzu.yuzu_emu.databinding.CardGameBinding |
|
||||
import org.yuzu.yuzu_emu.model.Game |
|
||||
|
|
||||
class GameViewHolder(val binding: CardGameBinding) : RecyclerView.ViewHolder(binding.root) { |
|
||||
lateinit var game: Game |
|
||||
|
|
||||
init { |
|
||||
itemView.tag = this |
|
||||
} |
|
||||
} |
|
||||
Write
Preview
Loading…
Cancel
Save
Reference in new issue