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:
parent
3e3e35f558
commit
e99b129cc2
3 changed files with 122 additions and 409 deletions
|
@ -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()
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
Loading…
Add table
Add a link
Reference in a new issue