style: remove unused DriverResolver and refactor driver handling in CustomSettingsHandler

- Deleted `DriverResolver`
- Moved and streamlined driver path extraction logic into `CustomSettingsHandler`.
- Improved string resource usage and ensured consistent formatting across dialogs.
This commit is contained in:
Producdevity 2025-07-27 13:35:55 +02:00
parent 3e3e35f558
commit e99b129cc2
3 changed files with 122 additions and 409 deletions

View file

@ -34,6 +34,7 @@ import org.yuzu.yuzu_emu.YuzuApplication
import org.yuzu.yuzu_emu.adapters.HomeSettingAdapter import org.yuzu.yuzu_emu.adapters.HomeSettingAdapter
import org.yuzu.yuzu_emu.databinding.FragmentHomeSettingsBinding import org.yuzu.yuzu_emu.databinding.FragmentHomeSettingsBinding
import org.yuzu.yuzu_emu.features.DocumentProvider import org.yuzu.yuzu_emu.features.DocumentProvider
import org.yuzu.yuzu_emu.features.fetcher.SpacingItemDecoration
import org.yuzu.yuzu_emu.features.settings.model.Settings import org.yuzu.yuzu_emu.features.settings.model.Settings
import org.yuzu.yuzu_emu.model.DriverViewModel import org.yuzu.yuzu_emu.model.DriverViewModel
import org.yuzu.yuzu_emu.model.HomeSetting import org.yuzu.yuzu_emu.model.HomeSetting
@ -251,6 +252,8 @@ class HomeSettingsFragment : Fragment() {
viewLifecycleOwner, viewLifecycleOwner,
optionsList optionsList
) )
val spacing = resources.getDimensionPixelSize(R.dimen.spacing_small)
addItemDecoration(SpacingItemDecoration(spacing))
} }
setInsets() setInsets()

View file

@ -10,11 +10,14 @@ import android.content.Context
import android.widget.Toast import android.widget.Toast
import androidx.fragment.app.FragmentActivity import androidx.fragment.app.FragmentActivity
import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.dialog.MaterialAlertDialogBuilder
import org.yuzu.yuzu_emu.R
import org.yuzu.yuzu_emu.model.DriverViewModel import org.yuzu.yuzu_emu.model.DriverViewModel
import org.yuzu.yuzu_emu.model.Game import org.yuzu.yuzu_emu.model.Game
import java.io.File import java.io.File
import kotlin.coroutines.resume import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine import kotlin.coroutines.suspendCoroutine
import android.net.Uri
import org.yuzu.yuzu_emu.features.settings.utils.SettingsFile
object CustomSettingsHandler { object CustomSettingsHandler {
const val CUSTOM_CONFIG_ACTION = "dev.eden.eden_emulator.LAUNCH_WITH_CUSTOM_CONFIG" const val CUSTOM_CONFIG_ACTION = "dev.eden.eden_emulator.LAUNCH_WITH_CUSTOM_CONFIG"
@ -39,21 +42,21 @@ object CustomSettingsHandler {
} }
// Check if config already exists - this should be handled by the caller // Check if config already exists - this should be handled by the caller
val configFile = getConfigFile(titleId) val configFile = getConfigFile(game)
if (configFile.exists()) { if (configFile.exists()) {
Log.warning("[CustomSettingsHandler] Config file already exists for title ID: $titleId") Log.warning("[CustomSettingsHandler] Config file already exists for game: ${game.title}")
// The caller should have already asked the user about overwriting
} }
// Write the config file // Write the config file
if (!writeConfigFile(titleId, customSettings)) { if (!writeConfigFile(game, customSettings)) {
Log.error("[CustomSettingsHandler] Failed to write config file") Log.error("[CustomSettingsHandler] Failed to write config file")
return null return null
} }
// Initialize per-game config // Initialize per-game config
try { try {
NativeConfig.initializePerGameConfig(game.programId, configFile.nameWithoutExtension) val fileName = FileUtil.getFilename(Uri.parse(game.path))
NativeConfig.initializePerGameConfig(game.programId, fileName)
Log.info("[CustomSettingsHandler] Successfully applied custom settings") Log.info("[CustomSettingsHandler] Successfully applied custom settings")
return game return game
} catch (e: Exception) { } catch (e: Exception) {
@ -88,50 +91,104 @@ object CustomSettingsHandler {
} }
// Check if config already exists // Check if config already exists
val configFile = getConfigFile(titleId) val configFile = getConfigFile(game)
if (configFile.exists() && activity != null) { if (configFile.exists() && activity != null) {
Log.info("[CustomSettingsHandler] Config file already exists, asking user for confirmation") Log.info(
Toast.makeText(activity, "Config exists, asking to overwrite", Toast.LENGTH_SHORT).show() "[CustomSettingsHandler] Config file already exists, asking user for confirmation"
)
Toast.makeText(
activity,
activity.getString(R.string.config_exists_prompt),
Toast.LENGTH_SHORT
).show()
val shouldOverwrite = askUserToOverwriteConfig(activity, game.title) val shouldOverwrite = askUserToOverwriteConfig(activity, game.title)
if (!shouldOverwrite) { if (!shouldOverwrite) {
Log.info("[CustomSettingsHandler] User chose not to overwrite existing config") Log.info("[CustomSettingsHandler] User chose not to overwrite existing config")
Toast.makeText(activity, "Overwrite cancelled", Toast.LENGTH_SHORT).show() Toast.makeText(
activity,
activity.getString(R.string.overwrite_cancelled),
Toast.LENGTH_SHORT
).show()
return null return null
} }
} }
// Check for driver requirements if activity and driverViewModel are provided // Check for driver requirements if activity and driverViewModel are provided
if (activity != null && driverViewModel != null) { if (activity != null && driverViewModel != null) {
val driverPath = DriverResolver.extractDriverPath(customSettings) val driverPath = extractDriverPath(customSettings)
if (driverPath != null) { if (driverPath != null) {
Log.info("[CustomSettingsHandler] Custom settings specify driver: $driverPath") Log.info("[CustomSettingsHandler] Custom settings specify driver: $driverPath")
Toast.makeText(activity, "Checking driver: ${driverPath.split("/").lastOrNull()?.take(20) ?: "driver"}", Toast.LENGTH_SHORT).show() // Check if driver exists in the driver storage
val driverExists = DriverResolver.ensureDriverExists(driverPath, activity, driverViewModel) val driverFile = File(driverPath)
if (!driverExists) { if (!driverFile.exists()) {
Log.error("[CustomSettingsHandler] Required driver not available: $driverPath") Log.error("[CustomSettingsHandler] Required driver not found: $driverPath")
Toast.makeText(activity, "Driver unavailable", Toast.LENGTH_SHORT).show() Toast.makeText(
// Don't write config if driver installation failed activity,
activity.getString(
R.string.custom_settings_failed_message,
game.title,
activity.getString(R.string.driver_not_found, driverFile.name)
),
Toast.LENGTH_LONG
).show()
// Don't write config if driver is missing
return null return null
} }
// Verify it's a valid driver
val metadata = GpuDriverHelper.getMetadataFromZip(driverFile)
if (metadata.name == null) {
Log.error("[CustomSettingsHandler] Invalid driver file: $driverPath")
Toast.makeText(
activity,
activity.getString(
R.string.custom_settings_failed_message,
game.title,
activity.getString(R.string.invalid_driver_file, driverFile.name)
),
Toast.LENGTH_LONG
).show()
return null
}
Log.info("[CustomSettingsHandler] Driver verified: ${metadata.name}")
} }
} }
// Only write the config file after all checks pass // Only write the config file after all checks pass
if (!writeConfigFile(titleId, customSettings)) { if (!writeConfigFile(game, customSettings)) {
Log.error("[CustomSettingsHandler] Failed to write config file") Log.error("[CustomSettingsHandler] Failed to write config file")
Toast.makeText(activity, "Config write failed", Toast.LENGTH_SHORT).show() activity?.let {
Toast.makeText(
it,
it.getString(R.string.config_write_failed),
Toast.LENGTH_SHORT
).show()
}
return null return null
} }
// Initialize per-game config // Initialize per-game config
try { try {
NativeConfig.initializePerGameConfig(game.programId, configFile.nameWithoutExtension) SettingsFile.loadCustomConfig(game)
Log.info("[CustomSettingsHandler] Successfully applied custom settings") Log.info("[CustomSettingsHandler] Successfully applied custom settings")
Toast.makeText(activity, "Custom settings applied", Toast.LENGTH_SHORT).show() activity?.let {
Toast.makeText(
it,
it.getString(R.string.custom_settings_applied),
Toast.LENGTH_SHORT
).show()
}
return game return game
} catch (e: Exception) { } catch (e: Exception) {
Log.error("[CustomSettingsHandler] Failed to apply custom settings: ${e.message}") Log.error("[CustomSettingsHandler] Failed to apply custom settings: ${e.message}")
Toast.makeText(activity, "Config apply failed", Toast.LENGTH_SHORT).show() activity?.let {
Toast.makeText(
it,
it.getString(R.string.config_apply_failed),
Toast.LENGTH_SHORT
).show()
}
return null return null
} }
} }
@ -154,7 +211,7 @@ object CustomSettingsHandler {
// First check cached games for fast lookup // First check cached games for fast lookup
GameHelper.cachedGameList.find { game -> GameHelper.cachedGameList.find { game ->
game.programId == programIdDecimal || game.programId == programIdDecimal ||
game.programIdHex.equals(expectedHex, ignoreCase = true) game.programIdHex.equals(expectedHex, ignoreCase = true)
}?.let { foundGame -> }?.let { foundGame ->
Log.info("[CustomSettingsHandler] Found game in cache: ${foundGame.title}") Log.info("[CustomSettingsHandler] Found game in cache: ${foundGame.title}")
return foundGame return foundGame
@ -164,22 +221,27 @@ object CustomSettingsHandler {
val allGames = GameHelper.getGames() val allGames = GameHelper.getGames()
val foundGame = allGames.find { game -> val foundGame = allGames.find { game ->
game.programId == programIdDecimal || game.programId == programIdDecimal ||
game.programIdHex.equals(expectedHex, ignoreCase = true) game.programIdHex.equals(expectedHex, ignoreCase = true)
} }
if (foundGame != null) { if (foundGame != null) {
Log.info("[CustomSettingsHandler] Found game: ${foundGame.title} at ${foundGame.path}") Log.info("[CustomSettingsHandler] Found game: ${foundGame.title} at ${foundGame.path}")
Toast.makeText(context, "Found: ${foundGame.title}", Toast.LENGTH_SHORT).show()
} else { } else {
Log.warning("[CustomSettingsHandler] No game found for title ID: $titleId") Log.warning("[CustomSettingsHandler] No game found for title ID: $titleId")
Toast.makeText(context, "Game not found: $titleId", Toast.LENGTH_SHORT).show()
} }
return foundGame return foundGame
} }
/** /**
* Get the config file path for a title ID * Get the config file path for a game
*/ */
private fun getConfigFile(titleId: String): File { private fun getConfigFile(game: Game): File {
return SettingsFile.getCustomSettingsFile(game)
}
/**
* Get the title ID config file path
*/
private fun getTitleIdConfigFile(titleId: String): File {
val configDir = File(DirectoryInitialization.userDirectory, "config/custom") val configDir = File(DirectoryInitialization.userDirectory, "config/custom")
return File(configDir, "$titleId.ini") return File(configDir, "$titleId.ini")
} }
@ -187,14 +249,14 @@ object CustomSettingsHandler {
/** /**
* Write the config file with the custom settings * Write the config file with the custom settings
*/ */
private fun writeConfigFile(titleId: String, customSettings: String): Boolean { private fun writeConfigFile(game: Game, customSettings: String): Boolean {
return try { return try {
val configDir = File(DirectoryInitialization.userDirectory, "config/custom") val configFile = getConfigFile(game)
if (!configDir.exists()) { val configDir = configFile.parentFile
if (configDir != null && !configDir.exists()) {
configDir.mkdirs() configDir.mkdirs()
} }
val configFile = File(configDir, "$titleId.ini")
configFile.writeText(customSettings) configFile.writeText(customSettings)
Log.info("[CustomSettingsHandler] Wrote config file: ${configFile.absolutePath}") Log.info("[CustomSettingsHandler] Wrote config file: ${configFile.absolutePath}")
@ -212,16 +274,14 @@ object CustomSettingsHandler {
return suspendCoroutine { continuation -> return suspendCoroutine { continuation ->
activity.runOnUiThread { activity.runOnUiThread {
MaterialAlertDialogBuilder(activity) MaterialAlertDialogBuilder(activity)
.setTitle("Configuration Already Exists") .setTitle(activity.getString(R.string.config_already_exists_title))
.setMessage( .setMessage(
"Custom settings already exist for '$gameTitle'.\n\n" + activity.getString(R.string.config_already_exists_message, gameTitle)
"Do you want to overwrite the existing configuration?\n\n" +
"This action cannot be undone."
) )
.setPositiveButton("Overwrite") { _, _ -> .setPositiveButton(activity.getString(R.string.overwrite)) { _, _ ->
continuation.resume(true) continuation.resume(true)
} }
.setNegativeButton("Cancel") { _, _ -> .setNegativeButton(activity.getString(R.string.cancel)) { _, _ ->
continuation.resume(false) continuation.resume(false)
} }
.setCancelable(false) .setCancelable(false)
@ -229,4 +289,26 @@ object CustomSettingsHandler {
} }
} }
} }
/**
* Extract driver path from custom settings INI content
*/
private fun extractDriverPath(customSettings: String): String? {
val lines = customSettings.lines()
var inGpuDriverSection = false
for (line in lines) {
val trimmed = line.trim()
if (trimmed.startsWith("[") && trimmed.endsWith("]")) {
inGpuDriverSection = trimmed == "[GpuDriver]"
continue
}
if (inGpuDriverSection && trimmed.startsWith("driver_path=")) {
return trimmed.substringAfter("driver_path=")
}
}
return null
}
} }

View file

@ -1,372 +0,0 @@
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
package org.yuzu.yuzu_emu.utils
import android.widget.Toast
import androidx.core.net.toUri
import androidx.fragment.app.FragmentActivity
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import okhttp3.OkHttpClient
import okhttp3.Request
import org.yuzu.yuzu_emu.R
import org.yuzu.yuzu_emu.fragments.DriverFetcherFragment
import org.yuzu.yuzu_emu.fragments.DriverFetcherFragment.SortMode
import org.yuzu.yuzu_emu.model.DriverViewModel
import java.io.File
import java.io.FileOutputStream
import java.io.IOException
import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine
object DriverResolver {
private val client = OkHttpClient()
// Mirror of the repositories from DriverFetcherFragment
private val repoList = listOf(
DriverRepo("Mr. Purple Turnip", "MrPurple666/purple-turnip", 0),
DriverRepo("GameHub Adreno 8xx", "crueter/GameHub-8Elite-Drivers", 1),
DriverRepo("KIMCHI Turnip", "K11MCH1/AdrenoToolsDrivers", 2, true),
DriverRepo("Weab-Chan Freedreno", "Weab-chan/freedreno_turnip-CI", 3)
)
private data class DriverRepo(
val name: String,
val path: String,
val sort: Int,
val useTagName: Boolean = false
)
/**
* Extract driver path from custom settings INI content
*/
fun extractDriverPath(customSettings: String): String? {
val lines = customSettings.lines()
var inGpuDriverSection = false
for (line in lines) {
val trimmed = line.trim()
if (trimmed.startsWith("[") && trimmed.endsWith("]")) {
inGpuDriverSection = trimmed == "[GpuDriver]"
continue
}
if (inGpuDriverSection && trimmed.startsWith("driver_path=")) {
return trimmed.substringAfter("driver_path=")
}
}
return null
}
/**
* Check if a driver exists and handle missing drivers
*/
suspend fun ensureDriverExists(
driverPath: String,
activity: FragmentActivity,
driverViewModel: DriverViewModel
): Boolean {
Log.info("[DriverResolver] Checking driver path: $driverPath")
val driverFile = File(driverPath)
if (driverFile.exists()) {
Log.info("[DriverResolver] Driver exists at: $driverPath")
return true
}
Log.warning("[DriverResolver] Driver not found: $driverPath")
// Extract driver name from path
val driverName = extractDriverNameFromPath(driverPath)
if (driverName == null) {
Log.error("[DriverResolver] Could not extract driver name from path")
return false
}
Log.info("[DriverResolver] Searching for downloadable driver: $driverName")
// Check if driver exists locally with different path
val localDriver = findLocalDriver(driverName)
if (localDriver != null) {
Log.info("[DriverResolver] Found local driver: ${localDriver.first}")
// The game can use this local driver, no need to download
return true
}
// Search for downloadable driver
val downloadableDriver = findDownloadableDriver(driverName)
if (downloadableDriver != null) {
Log.info("[DriverResolver] Found downloadable driver: ${downloadableDriver.name}")
val shouldInstall = askUserToInstallDriver(activity, downloadableDriver.name)
if (shouldInstall) {
return downloadAndInstallDriver(activity, downloadableDriver, driverViewModel)
}
} else {
Log.warning("[DriverResolver] No downloadable driver found for: $driverName")
showDriverNotFoundDialog(activity, driverName)
}
return false
}
/**
* Extract driver name from full path
*/
private fun extractDriverNameFromPath(driverPath: String): String? {
val file = File(driverPath)
val fileName = file.name
// Remove .zip extension and extract meaningful name
if (fileName.endsWith(".zip")) {
return fileName.substring(0, fileName.length - 4)
}
return fileName
}
/**
* Find driver in local storage by name matching
*/
private fun findLocalDriver(driverName: String): Pair<String, GpuDriverMetadata>? {
val availableDrivers = GpuDriverHelper.getDrivers()
// Try exact match first
availableDrivers.find { (_, metadata) ->
metadata.name?.contains(driverName, ignoreCase = true) == true
}?.let { return it }
// Try partial match
availableDrivers.find { (path, metadata) ->
path.contains(driverName, ignoreCase = true) ||
metadata.name?.contains(
extractKeywords(driverName).first(),
ignoreCase = true
) == true
}?.let { return it }
return null
}
/**
* Extract keywords from driver name for matching
*/
private fun extractKeywords(driverName: String): List<String> {
val keywords = mutableListOf<String>()
// Common driver patterns
when {
driverName.contains("turnip", ignoreCase = true) -> keywords.add("turnip")
driverName.contains("purple", ignoreCase = true) -> keywords.add("purple")
driverName.contains("kimchi", ignoreCase = true) -> keywords.add("kimchi")
driverName.contains("freedreno", ignoreCase = true) -> keywords.add("freedreno")
driverName.contains("gamehub", ignoreCase = true) -> keywords.add("gamehub")
}
// Version patterns
Regex("v?\\d+\\.\\d+\\.\\d+").find(driverName)?.value?.let { keywords.add(it) }
if (keywords.isEmpty()) {
keywords.add(driverName)
}
return keywords
}
/**
* Find downloadable driver that matches the required driver
*/
private suspend fun findDownloadableDriver(driverName: String): DriverFetcherFragment.Artifact? {
val keywords = extractKeywords(driverName)
for (repo in repoList) {
// Check if this repo is relevant based on driver name
val isRelevant = keywords.any { keyword ->
repo.name.contains(keyword, ignoreCase = true) ||
keyword.contains(repo.name.split(" ").first(), ignoreCase = true)
}
if (!isRelevant) continue
try {
val releases = fetchReleases(repo)
val latestRelease = releases.firstOrNull { !it.prerelease }
latestRelease?.artifacts?.forEach { artifact ->
if (matchesDriverName(artifact.name, driverName, keywords)) {
return artifact
}
}
} catch (e: Exception) {
Log.error(
"[DriverResolver] Failed to fetch releases for ${repo.name}: ${e.message}"
)
}
}
return null
}
/**
* Check if artifact name matches the required driver
*/
private fun matchesDriverName(
artifactName: String,
requiredName: String,
keywords: List<String>
): Boolean {
// Exact match
if (artifactName.equals(requiredName, ignoreCase = true)) return true
// Keyword matching
return keywords.any { keyword ->
artifactName.contains(keyword, ignoreCase = true)
}
}
/**
* Fetch releases from GitHub repo
*/
private suspend fun fetchReleases(repo: DriverRepo): List<DriverFetcherFragment.Release> {
return withContext(Dispatchers.IO) {
val request = Request.Builder()
.url("https://api.github.com/repos/${repo.path}/releases")
.build()
client.newCall(request).execute().use { response ->
if (!response.isSuccessful) {
throw IOException("Failed to fetch releases: ${response.code}")
}
val body = response.body?.string() ?: throw IOException("Empty response")
DriverFetcherFragment.Release.fromJsonArray(body, repo.useTagName, SortMode.Default)
}
}
}
/**
* Ask user if they want to install the missing driver
*/
private suspend fun askUserToInstallDriver(
activity: FragmentActivity,
driverName: String
): Boolean {
return suspendCoroutine { continuation ->
activity.runOnUiThread {
MaterialAlertDialogBuilder(activity)
.setTitle(activity.getString(R.string.missing_gpu_driver_title))
.setMessage(activity.getString(R.string.missing_gpu_driver_message, driverName))
.setPositiveButton(activity.getString(R.string.install)) { _, _ ->
continuation.resume(true)
}
.setNegativeButton(activity.getString(R.string.cancel)) { _, _ ->
continuation.resume(false)
}
.setCancelable(false)
.show()
}
}
}
/**
* Download and install driver automatically
*/
private suspend fun downloadAndInstallDriver(
activity: FragmentActivity,
artifact: DriverFetcherFragment.Artifact,
driverViewModel: DriverViewModel
): Boolean {
return try {
Log.info("[DriverResolver] Downloading driver: ${artifact.name}")
Toast.makeText(
activity,
activity.getString(R.string.downloading_driver),
Toast.LENGTH_SHORT
).show()
val cacheDir =
activity.externalCacheDir ?: throw IOException("Cache directory not available")
cacheDir.mkdirs()
val file = File(cacheDir, artifact.name)
// Download the driver
withContext(Dispatchers.IO) {
val request = Request.Builder()
.url(artifact.url)
.header("Accept", "application/octet-stream")
.build()
client.newBuilder()
.followRedirects(true)
.followSslRedirects(true)
.build()
.newCall(request).execute().use { response ->
if (!response.isSuccessful) {
throw IOException("Download failed: ${response.code}")
}
response.body?.byteStream()?.use { input ->
FileOutputStream(file).use { output ->
input.copyTo(output)
}
} ?: throw IOException("Empty response body")
}
}
if (file.length() == 0L) {
throw IOException("Downloaded file is empty")
}
// Install the driver on main thread
withContext(Dispatchers.Main) {
val driverData = GpuDriverHelper.getMetadataFromZip(file)
val driverPath = "${GpuDriverHelper.driverStoragePath}${file.name}"
if (GpuDriverHelper.copyDriverToInternalStorage(file.toUri())) {
driverViewModel.onDriverAdded(Pair(driverPath, driverData))
Log.info("[DriverResolver] Successfully installed driver: ${driverData.name}")
Toast.makeText(
activity,
activity.getString(R.string.driver_installed),
Toast.LENGTH_SHORT
).show()
true
} else {
throw IOException("Failed to install driver")
}
}
} catch (e: Exception) {
Log.error("[DriverResolver] Failed to download/install driver: ${e.message}")
withContext(Dispatchers.Main) {
MaterialAlertDialogBuilder(activity)
.setTitle(activity.getString(R.string.driver_installation_failed_title))
.setMessage(
activity.getString(R.string.driver_installation_failed_message, e.message)
)
.setPositiveButton(activity.getString(R.string.ok)) { dialog, _ -> dialog.dismiss() }
.show()
}
false
}
}
/**
* Show dialog when driver cannot be found
*/
private fun showDriverNotFoundDialog(activity: FragmentActivity, driverName: String) {
activity.runOnUiThread {
MaterialAlertDialogBuilder(activity)
.setTitle(activity.getString(R.string.driver_not_available_title))
.setMessage(activity.getString(R.string.driver_not_available_message, driverName))
.setPositiveButton(activity.getString(R.string.ok)) { dialog, _ -> dialog.dismiss() }
.show()
}
}
}