From 1e7b978852e0e4ab8266f80ea6881eafdd74fdcc Mon Sep 17 00:00:00 2001 From: izzy2lost Date: Sat, 7 Feb 2026 21:54:55 -0500 Subject: [PATCH] add game folder for game list --- android/app/build.gradle.kts | 1 + android/app/src/main/AndroidManifest.xml | 5 + android/app/src/main/cpp/xemu_android.cpp | 62 ++++- .../app/src/main/cpp/xemu_settings_android.cc | 29 +++ .../izzy2lost/x1box/GameLibraryActivity.kt | 240 ++++++++++++++++++ .../com/izzy2lost/x1box/LauncherActivity.kt | 18 +- .../izzy2lost/x1box/SetupWizardActivity.kt | 84 +++--- .../main/res/layout/activity_game_library.xml | 126 +++++++++ .../main/res/layout/activity_setup_wizard.xml | 4 +- .../src/main/res/layout/item_game_entry.xml | 73 ++++++ android/app/src/main/res/values/strings.xml | 19 +- 11 files changed, 602 insertions(+), 59 deletions(-) create mode 100644 android/app/src/main/java/com/izzy2lost/x1box/GameLibraryActivity.kt create mode 100644 android/app/src/main/res/layout/activity_game_library.xml create mode 100644 android/app/src/main/res/layout/item_game_entry.xml diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts index 06fdeceb57..f6edfb41b1 100644 --- a/android/app/build.gradle.kts +++ b/android/app/build.gradle.kts @@ -104,6 +104,7 @@ dependencies { implementation("androidx.core:core-ktx:1.15.0") implementation("androidx.appcompat:appcompat:1.7.0") implementation("androidx.constraintlayout:constraintlayout:2.1.4") + implementation("androidx.documentfile:documentfile:1.0.1") implementation("com.google.android.material:material:1.14.0-alpha07") } diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 671c7a91f5..66540a7a79 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -35,5 +35,10 @@ android:screenOrientation="fullSensor" android:exported="false" /> + + diff --git a/android/app/src/main/cpp/xemu_android.cpp b/android/app/src/main/cpp/xemu_android.cpp index 7f0b840866..b25ab3ebbb 100644 --- a/android/app/src/main/cpp/xemu_android.cpp +++ b/android/app/src/main/cpp/xemu_android.cpp @@ -9,6 +9,8 @@ #include #include +#include +#include #include #include #include @@ -57,6 +59,42 @@ static bool FileExists(const std::string& path) { return stat(path.c_str(), &st) == 0; } +static bool IsTcgTuningEnabled() { + const char* value = SDL_getenv("XEMU_ANDROID_TCG_TUNING"); + return !(value && value[0] == '0'); +} + +static const char* GetTcgThreadFromEnv() { + const char* value = SDL_getenv("XEMU_ANDROID_TCG_THREAD"); + if (value && strcmp(value, "single") == 0) { + return "single"; + } + return "multi"; +} + +static int GetTcgTbSizeFromEnv() { + constexpr int kDefaultTbSize = 128; + constexpr int kMinTbSize = 32; + constexpr int kMaxTbSize = 512; + + const char* value = SDL_getenv("XEMU_ANDROID_TCG_TB_SIZE"); + if (!value || value[0] == '\0') { + return kDefaultTbSize; + } + + char* end = nullptr; + long parsed = strtol(value, &end, 10); + if (end == value || (end && *end != '\0')) { + return kDefaultTbSize; + } + if (parsed < kMinTbSize) { + parsed = kMinTbSize; + } else if (parsed > kMaxTbSize) { + parsed = kMaxTbSize; + } + return static_cast(parsed); +} + static JNIEnv* GetEnv() { return static_cast(SDL_AndroidGetJNIEnv()); } @@ -221,6 +259,15 @@ static bool WriteConfigToml(const std::string& config_path, if (!android->contains("force_cpu_blit")) { android->insert_or_assign("force_cpu_blit", false); } + if (!android->contains("tcg_tuning")) { + android->insert_or_assign("tcg_tuning", true); + } + if (!android->contains("tcg_thread")) { + android->insert_or_assign("tcg_thread", "multi"); + } + if (!android->contains("tcg_tb_size")) { + android->insert_or_assign("tcg_tb_size", 128); + } files->insert_or_assign("bootrom_path", mcpx); files->insert_or_assign("flashrom_path", flash); @@ -454,9 +501,18 @@ extern "C" int SDL_main(int argc, char* argv[]) { std::vector arg_storage; arg_storage.emplace_back("xemu"); - arg_storage.emplace_back("-accel"); - arg_storage.emplace_back("tcg,thread=multi,tb-size=256"); - LogInfo("SDL_main: forcing TCG accel thread=multi tb-size=256"); + if (IsTcgTuningEnabled()) { + const char* tcg_thread = GetTcgThreadFromEnv(); + int tcg_tb_size = GetTcgTbSizeFromEnv(); + char accel_opts[64]; + snprintf(accel_opts, sizeof(accel_opts), "tcg,thread=%s,tb-size=%d", + tcg_thread, tcg_tb_size); + arg_storage.emplace_back("-accel"); + arg_storage.emplace_back(accel_opts); + LogInfoFmt("SDL_main: using accel %s", accel_opts); + } else { + LogInfo("SDL_main: TCG tuning disabled"); + } std::vector xemu_argv; xemu_argv.reserve(arg_storage.size() + 1); diff --git a/android/app/src/main/cpp/xemu_settings_android.cc b/android/app/src/main/cpp/xemu_settings_android.cc index 7ba879bd2a..ead262ae43 100644 --- a/android/app/src/main/cpp/xemu_settings_android.cc +++ b/android/app/src/main/cpp/xemu_settings_android.cc @@ -198,6 +198,9 @@ bool xemu_settings_load(void) xemu_settings_apply_defaults(); error_msg.clear(); setenv("XEMU_ANDROID_FORCE_CPU_BLIT", "0", 1); + setenv("XEMU_ANDROID_TCG_TUNING", "1", 1); + setenv("XEMU_ANDROID_TCG_THREAD", "multi", 1); + setenv("XEMU_ANDROID_TCG_TB_SIZE", "128", 1); const char *path = xemu_settings_get_path(); if (!path || *path == '\0') { @@ -256,11 +259,37 @@ bool xemu_settings_load(void) } // Android-specific settings + if (auto force_cpu_blit = android_cfg["force_cpu_blit"].value()) { + setenv("XEMU_ANDROID_FORCE_CPU_BLIT", *force_cpu_blit ? "1" : "0", 1); + } if (auto egl_offscreen = android_cfg["egl_offscreen"].value()) { if (!*egl_offscreen) { setenv("XEMU_ANDROID_EGL_OFFSCREEN", "0", 1); } } + if (auto tcg_tuning = android_cfg["tcg_tuning"].value()) { + setenv("XEMU_ANDROID_TCG_TUNING", *tcg_tuning ? "1" : "0", 1); + } + if (auto tcg_thread = android_cfg["tcg_thread"].value()) { + if (*tcg_thread == "single" || *tcg_thread == "multi") { + setenv("XEMU_ANDROID_TCG_THREAD", tcg_thread->c_str(), 1); + } else { + __android_log_print(ANDROID_LOG_WARN, "xemu-android", + "Ignoring android.tcg_thread=%s (expected single|multi)", + tcg_thread->c_str()); + } + } + if (auto tcg_tb_size = android_cfg["tcg_tb_size"].value()) { + int tb_size = (int)*tcg_tb_size; + if (tb_size < 32) { + tb_size = 32; + } else if (tb_size > 512) { + tb_size = 512; + } + char tb_size_str[16]; + snprintf(tb_size_str, sizeof(tb_size_str), "%d", tb_size); + setenv("XEMU_ANDROID_TCG_TB_SIZE", tb_size_str, 1); + } // System file paths if (auto bootrom = sys_files["bootrom_path"].value()) { diff --git a/android/app/src/main/java/com/izzy2lost/x1box/GameLibraryActivity.kt b/android/app/src/main/java/com/izzy2lost/x1box/GameLibraryActivity.kt new file mode 100644 index 0000000000..87a5e1251a --- /dev/null +++ b/android/app/src/main/java/com/izzy2lost/x1box/GameLibraryActivity.kt @@ -0,0 +1,240 @@ +package com.izzy2lost.x1box + +import android.content.Intent +import android.net.Uri +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.widget.LinearLayout +import android.widget.ProgressBar +import android.widget.TextView +import android.widget.Toast +import androidx.activity.result.contract.ActivityResultContracts +import androidx.appcompat.app.AppCompatActivity +import androidx.documentfile.provider.DocumentFile +import com.google.android.material.button.MaterialButton +import java.util.ArrayDeque +import java.util.Locale + +class GameLibraryActivity : AppCompatActivity() { + private data class GameEntry( + val title: String, + val uri: Uri, + val relativePath: String, + val sizeBytes: Long + ) + + private val prefs by lazy { getSharedPreferences("x1box_prefs", MODE_PRIVATE) } + private val gameExts = setOf("iso", "xiso", "cso", "cci") + + private lateinit var folderText: TextView + private lateinit var loadingSpinner: ProgressBar + private lateinit var loadingText: TextView + private lateinit var emptyText: TextView + private lateinit var gamesContainer: LinearLayout + private lateinit var btnChangeFolder: MaterialButton + + private var gamesFolderUri: Uri? = null + private var scanGeneration = 0 + + private val pickGamesFolder = + registerForActivityResult(ActivityResultContracts.OpenDocumentTree()) { uri -> + if (uri != null) { + persistUriPermission(uri) + gamesFolderUri = uri + prefs.edit().putString("gamesFolderUri", uri.toString()).apply() + loadGames() + } + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_game_library) + + folderText = findViewById(R.id.library_folder_text) + loadingSpinner = findViewById(R.id.library_loading) + loadingText = findViewById(R.id.library_loading_text) + emptyText = findViewById(R.id.library_empty_text) + gamesContainer = findViewById(R.id.library_games_container) + btnChangeFolder = findViewById(R.id.btn_change_games_folder) + + gamesFolderUri = prefs.getString("gamesFolderUri", null)?.let(Uri::parse) + + btnChangeFolder.setOnClickListener { + pickGamesFolder.launch(gamesFolderUri) + } + + if (!isFolderReady(gamesFolderUri)) { + folderText.text = getString(R.string.library_no_folder) + Toast.makeText(this, getString(R.string.setup_pick_disc), Toast.LENGTH_SHORT).show() + pickGamesFolder.launch(gamesFolderUri) + return + } + + loadGames() + } + + private fun loadGames() { + val folderUri = gamesFolderUri + if (!isFolderReady(folderUri)) { + setLoading(false) + setGames(emptyList()) + folderText.text = getString(R.string.library_no_folder) + return + } + + folderText.text = getString(R.string.library_folder_value, formatTreeLabel(folderUri!!)) + setLoading(true) + + val generation = ++scanGeneration + Thread { + val games = scanFolderForGames(folderUri) + runOnUiThread { + if (generation != scanGeneration) { + return@runOnUiThread + } + setLoading(false) + setGames(games) + } + }.start() + } + + private fun setLoading(loading: Boolean) { + loadingSpinner.visibility = if (loading) View.VISIBLE else View.GONE + loadingText.visibility = if (loading) View.VISIBLE else View.GONE + } + + private fun setGames(games: List) { + gamesContainer.removeAllViews() + emptyText.visibility = if (games.isEmpty()) View.VISIBLE else View.GONE + if (games.isEmpty()) { + return + } + + val inflater = LayoutInflater.from(this) + for (game in games) { + val item = inflater.inflate(R.layout.item_game_entry, gamesContainer, false) + val nameText = item.findViewById(R.id.game_name_text) + val sizeText = item.findViewById(R.id.game_size_text) + val pathText = item.findViewById(R.id.game_path_text) + + nameText.text = game.title + sizeText.text = getString(R.string.library_game_size, formatSize(game.sizeBytes)) + pathText.text = getString(R.string.library_game_path, game.relativePath) + + item.setOnClickListener { launchGame(game) } + gamesContainer.addView(item) + } + } + + private fun launchGame(game: GameEntry) { + persistUriPermission(game.uri) + prefs.edit() + .putString("dvdUri", game.uri.toString()) + .remove("dvdPath") + .putBoolean("skip_game_picker", false) + .apply() + + startActivity(Intent(this, MainActivity::class.java)) + finish() + } + + private fun scanFolderForGames(folderUri: Uri): List { + val root = DocumentFile.fromTreeUri(this, folderUri) ?: return emptyList() + val stack = ArrayDeque>() + stack.add(root to "") + + val games = ArrayList() + while (stack.isNotEmpty()) { + val (node, prefix) = stack.removeLast() + val files = try { + node.listFiles() + } catch (_: Exception) { + emptyArray() + } + for (child in files) { + val name = child.name ?: continue + if (child.isDirectory) { + stack.add(child to (prefix + name + "/")) + continue + } + if (!child.isFile || !isSupportedGame(name)) { + continue + } + games.add( + GameEntry( + title = toGameTitle(name), + uri = child.uri, + relativePath = prefix + name, + sizeBytes = child.length() + ) + ) + } + } + + games.sortBy { it.title.lowercase(Locale.ROOT) } + return games + } + + private fun isSupportedGame(name: String): Boolean { + val lower = name.lowercase(Locale.ROOT) + if (lower.endsWith(".xiso.iso")) { + return true + } + val ext = lower.substringAfterLast('.', "") + return ext.isNotEmpty() && gameExts.contains(ext) + } + + private fun toGameTitle(fileName: String): String { + val lower = fileName.lowercase(Locale.ROOT) + return when { + lower.endsWith(".xiso.iso") -> fileName.dropLast(".xiso.iso".length) + fileName.contains('.') -> fileName.substringBeforeLast('.') + else -> fileName + } + } + + private fun formatSize(bytes: Long): String { + if (bytes <= 0L) { + return "Unknown" + } + val units = arrayOf("B", "KB", "MB", "GB", "TB") + var value = bytes.toDouble() + var unitIndex = 0 + while (value >= 1024.0 && unitIndex < units.lastIndex) { + value /= 1024.0 + unitIndex++ + } + return String.format(Locale.US, "%.1f %s", value, units[unitIndex]) + } + + private fun isFolderReady(uri: Uri?): Boolean { + if (uri == null || !hasPersistedReadPermission(uri)) { + return false + } + val root = DocumentFile.fromTreeUri(this, uri) ?: return false + return root.exists() && root.isDirectory + } + + private fun persistUriPermission(uri: Uri) { + val flags = Intent.FLAG_GRANT_READ_URI_PERMISSION + try { + contentResolver.takePersistableUriPermission(uri, flags) + } catch (_: SecurityException) { + } + } + + private fun hasPersistedReadPermission(uri: Uri): Boolean { + return contentResolver.persistedUriPermissions.any { perm -> + perm.uri == uri && perm.isReadPermission + } + } + + private fun formatTreeLabel(uri: Uri): String { + val name = DocumentFile.fromTreeUri(this, uri)?.name + if (!name.isNullOrBlank()) { + return name + } + return uri.toString() + } +} diff --git a/android/app/src/main/java/com/izzy2lost/x1box/LauncherActivity.kt b/android/app/src/main/java/com/izzy2lost/x1box/LauncherActivity.kt index 226b21244e..13c085a5f3 100644 --- a/android/app/src/main/java/com/izzy2lost/x1box/LauncherActivity.kt +++ b/android/app/src/main/java/com/izzy2lost/x1box/LauncherActivity.kt @@ -16,21 +16,23 @@ class LauncherActivity : Activity() { val flashUriStr = prefs.getString("flashUri", null) val hddUriStr = prefs.getString("hddUri", null) val dvdUriStr = prefs.getString("dvdUri", null) + val gamesFolderUriStr = prefs.getString("gamesFolderUri", null) val mcpxPath = prefs.getString("mcpxPath", null) val flashPath = prefs.getString("flashPath", null) val hddPath = prefs.getString("hddPath", null) val dvdPath = prefs.getString("dvdPath", null) - val skipGamePicker = prefs.getBoolean("skip_game_picker", false) val mcpxUri = mcpxUriStr?.let(Uri::parse) val flashUri = flashUriStr?.let(Uri::parse) val hddUri = hddUriStr?.let(Uri::parse) val dvdUri = dvdUriStr?.let(Uri::parse) + val gamesFolderUri = gamesFolderUriStr?.let(Uri::parse) val hasMcpx = hasLocalFile(mcpxPath) || (mcpxUri != null && hasPersistedReadPermission(mcpxUri)) val hasFlash = hasLocalFile(flashPath) || (flashUri != null && hasPersistedReadPermission(flashUri)) val hasHdd = hasLocalFile(hddPath) || (hddUri != null && hasPersistedReadPermission(hddUri)) val hasDvd = hasLocalFile(dvdPath) || (dvdUri != null && hasPersistedReadPermission(dvdUri)) + val hasGamesFolder = gamesFolderUri != null && hasPersistedReadPermission(gamesFolderUri) val editor = prefs.edit() var clearedCore = false @@ -67,6 +69,10 @@ class LauncherActivity : Activity() { editor.remove("dvdPath") clearedOptional = true } + if (!hasGamesFolder && gamesFolderUriStr != null) { + editor.remove("gamesFolderUri") + clearedCore = true + } if (clearedCore) { setupComplete = false editor.putBoolean("setup_complete", false) @@ -76,14 +82,8 @@ class LauncherActivity : Activity() { editor.apply() } - val needsSetup = !setupComplete || !hasMcpx || !hasFlash || !hasHdd - val needsGamePicker = !skipGamePicker && !hasDvd - val next = - if (needsSetup || needsGamePicker) { - SetupWizardActivity::class.java - } else { - MainActivity::class.java - } + val needsSetup = !setupComplete || !hasMcpx || !hasFlash || !hasHdd || !hasGamesFolder + val next = if (needsSetup) SetupWizardActivity::class.java else GameLibraryActivity::class.java startActivity(Intent(this, next)) finish() diff --git a/android/app/src/main/java/com/izzy2lost/x1box/SetupWizardActivity.kt b/android/app/src/main/java/com/izzy2lost/x1box/SetupWizardActivity.kt index c99ef843cb..ec1bc3cd7b 100644 --- a/android/app/src/main/java/com/izzy2lost/x1box/SetupWizardActivity.kt +++ b/android/app/src/main/java/com/izzy2lost/x1box/SetupWizardActivity.kt @@ -10,6 +10,7 @@ import android.widget.Toast import androidx.activity.OnBackPressedCallback import androidx.activity.result.contract.ActivityResultContracts import androidx.appcompat.app.AppCompatActivity +import androidx.documentfile.provider.DocumentFile import com.google.android.material.button.MaterialButton import java.io.File import java.io.FileOutputStream @@ -38,24 +39,16 @@ class SetupWizardActivity : AppCompatActivity() { private var mcpxUri: Uri? = null private var flashUri: Uri? = null private var hddUri: Uri? = null - private var dvdUri: Uri? = null + private var gamesFolderUri: Uri? = null private var mcpxPath: String? = null private var flashPath: String? = null private var hddPath: String? = null - private var dvdPath: String? = null private var currentStep = 0 private var isCopying = false private val mcpxExts = setOf("bin", "rom", "img") private val flashExts = setOf("bin", "rom", "img") private val hddExts = setOf("qcow2", "img") - private val discExts = setOf("iso", "xiso", "cso", "cci") - private val discMimes = setOf( - "application/x-iso9660-image", - "application/x-cd-image", - "application/x-iso9660", - "application/vnd.iso-image" - ) private val pickMcpx = registerForActivityResult(ActivityResultContracts.OpenDocument()) { uri -> @@ -126,26 +119,14 @@ class SetupWizardActivity : AppCompatActivity() { } } - private val pickDisc = - registerForActivityResult(ActivityResultContracts.OpenDocument()) { uri -> + private val pickGamesFolder = + registerForActivityResult(ActivityResultContracts.OpenDocumentTree()) { uri -> if (uri != null) { - if (!isAllowedFile(uri, discExts, discMimes)) { - showExtensionError(discExts) - return@registerForActivityResult - } persistUriPermission(uri) - dvdUri = uri - prefs.edit().putString("dvdUri", uri.toString()).apply() - copyUriAsync(uri, "dvd.iso") { path -> - if (path != null) { - dvdPath = path - prefs.edit().putString("dvdPath", path).apply() - } else { - Toast.makeText(this, "Failed to copy game disc image", Toast.LENGTH_LONG).show() - } - updateDiscSelection() - updateButtons() - } + gamesFolderUri = uri + prefs.edit().putString("gamesFolderUri", uri.toString()).apply() + updateDiscSelection() + updateButtons() } } @@ -155,16 +136,15 @@ class SetupWizardActivity : AppCompatActivity() { mcpxPath = loadLocalPath("mcpxPath") flashPath = loadLocalPath("flashPath") hddPath = loadLocalPath("hddPath") - dvdPath = loadLocalPath("dvdPath") mcpxUri = prefs.getString("mcpxUri", null)?.let(Uri::parse) flashUri = prefs.getString("flashUri", null)?.let(Uri::parse) hddUri = prefs.getString("hddUri", null)?.let(Uri::parse) - dvdUri = prefs.getString("dvdUri", null)?.let(Uri::parse) + gamesFolderUri = prefs.getString("gamesFolderUri", null)?.let(Uri::parse) val coreReady = isFileReady(mcpxPath) && isFileReady(flashPath) && isFileReady(hddPath) - val discReady = isFileReady(dvdPath) - if (prefs.getBoolean("setup_complete", false) && coreReady && discReady) { - goToMain() + val gamesFolderReady = hasGamesFolderReady() + if (prefs.getBoolean("setup_complete", false) && coreReady && gamesFolderReady) { + goToLibrary() return } @@ -204,7 +184,7 @@ class SetupWizardActivity : AppCompatActivity() { btnPickMcpx.setOnClickListener { pickMcpx.launch(arrayOf("application/octet-stream")) } btnPickFlash.setOnClickListener { pickFlash.launch(arrayOf("application/octet-stream")) } btnPickHdd.setOnClickListener { pickHdd.launch(arrayOf("application/x-qcow2", "application/octet-stream")) } - btnPickDisc.setOnClickListener { pickDisc.launch(arrayOf("*/*")) } + btnPickDisc.setOnClickListener { pickGamesFolder.launch(gamesFolderUri) } btnBack.setOnClickListener { showStep(currentStep - 1) } btnNext.setOnClickListener { @@ -233,7 +213,7 @@ class SetupWizardActivity : AppCompatActivity() { updateHddSelection() updateDiscSelection() - val startStep = if (coreReady && !discReady) pages.size - 1 else 0 + val startStep = if (coreReady && !gamesFolderReady) pages.size - 1 else 0 showStep(startStep) } @@ -271,7 +251,7 @@ class SetupWizardActivity : AppCompatActivity() { 0 -> isFileReady(mcpxPath) 1 -> isFileReady(flashPath) 2 -> isFileReady(hddPath) - else -> true + else -> hasGamesFolderReady() } } @@ -291,22 +271,21 @@ class SetupWizardActivity : AppCompatActivity() { } private fun updateDiscSelection() { - val value = dvdPath ?: dvdUri?.toString() ?: getString(R.string.setup_not_set) + val value = gamesFolderUri?.let { formatTreeLabel(it) } ?: getString(R.string.setup_not_set) discPathText.text = getString(R.string.setup_disc_value, value) } private fun finishSetup() { - val skipGamePicker = !isFileReady(dvdPath) prefs.edit() .putBoolean("setup_complete", true) - .putBoolean("skip_game_picker", skipGamePicker) + .putBoolean("skip_game_picker", false) .apply() - goToMain() + goToLibrary() } - private fun goToMain() { - startActivity(Intent(this, MainActivity::class.java)) + private fun goToLibrary() { + startActivity(Intent(this, GameLibraryActivity::class.java)) finish() } @@ -331,6 +310,29 @@ class SetupWizardActivity : AppCompatActivity() { return path != null && File(path).isFile } + private fun hasGamesFolderReady(): Boolean { + val uri = gamesFolderUri ?: return false + if (!hasPersistedReadPermission(uri)) { + return false + } + val root = DocumentFile.fromTreeUri(this, uri) ?: return false + return root.exists() && root.isDirectory + } + + private fun hasPersistedReadPermission(uri: Uri): Boolean { + return contentResolver.persistedUriPermissions.any { perm -> + perm.uri == uri && perm.isReadPermission + } + } + + private fun formatTreeLabel(uri: Uri): String { + val name = DocumentFile.fromTreeUri(this, uri)?.name + if (!name.isNullOrBlank()) { + return name + } + return uri.toString() + } + private fun copyUriAsync(uri: Uri, destName: String, onDone: (String?) -> Unit) { if (isCopying) return isCopying = true diff --git a/android/app/src/main/res/layout/activity_game_library.xml b/android/app/src/main/res/layout/activity_game_library.xml new file mode 100644 index 0000000000..47de8a15f2 --- /dev/null +++ b/android/app/src/main/res/layout/activity_game_library.xml @@ -0,0 +1,126 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/android/app/src/main/res/layout/activity_setup_wizard.xml b/android/app/src/main/res/layout/activity_setup_wizard.xml index daf036c788..88b6fab895 100644 --- a/android/app/src/main/res/layout/activity_setup_wizard.xml +++ b/android/app/src/main/res/layout/activity_setup_wizard.xml @@ -242,7 +242,7 @@ android:layout_height="40dp" android:layout_gravity="center_horizontal" android:contentDescription="@string/setup_disc_title" - android:src="@android:drawable/ic_menu_slideshow" + android:src="@android:drawable/ic_menu_agenda" android:tint="@color/xemu_green" /> diff --git a/android/app/src/main/res/layout/item_game_entry.xml b/android/app/src/main/res/layout/item_game_entry.xml new file mode 100644 index 0000000000..d6a599c2d6 --- /dev/null +++ b/android/app/src/main/res/layout/item_game_entry.xml @@ -0,0 +1,73 @@ + + + + + + + + + + + + + + + + + + + diff --git a/android/app/src/main/res/values/strings.xml b/android/app/src/main/res/values/strings.xml index 0a47ed9d4a..b25b52e6b0 100644 --- a/android/app/src/main/res/values/strings.xml +++ b/android/app/src/main/res/values/strings.xml @@ -22,8 +22,19 @@ Choose Hard Disk Hard Disk: %1$s - Game Disc (Optional) - Choose an Xbox game disc image to boot. - Choose Game Disc - Game Disc: %1$s + Games Folder + Pick your games folder using SAF. X1 BOX will scan it for playable images. + Choose Games Folder + Games Folder: %1$s + + Game Library + Pick a game to launch in xemu. + Change Folder + Folder: %1$s + No folder selected + Scanning games... + No supported games found in this folder. + Size: %1$s + Path: %1$s + Open