Support loading custom game settings via intent

This commit introduces the ability to launch games with custom configurations supplied via an Android intent. This allows external applications to provide specific settings for a game at launch time.

Key changes include:

*   **`CustomSettingsHandler.kt`**: A new class responsible for:
    *   Processing incoming intents with custom settings.
    *   Finding the target game in the user's library by its title ID.
    *   Writing the custom settings to a per-game INI file (`config/custom/<title_id>.ini`).
    *   Handling potential conflicts if a custom configuration already exists, prompting the user to overwrite or cancel.
    *   Integrating with `DriverResolver` to check for and handle required GPU drivers specified in the custom settings.
    *   Initializing the native per-game configuration.
*   **`DriverResolver.kt`**: A new utility class for managing GPU drivers specified in custom settings:
    *   Extracts the driver path from the custom settings INI content.
    *   Checks if the required driver exists locally.
    *   If not found locally, searches for the driver in predefined GitHub repositories (Mr. Purple Turnip, GameHub Adreno 8xx, KIMCHI Turnip, Weab-Chan Freedreno).
    *   Prompts the user to download and install the missing driver if found online.
    *   Handles automatic download and installation of drivers using `DriverViewModel`.
    *   Notifies the user if a required driver cannot be found or installed.
*   **`AndroidManifest.xml`**:
    *   Added a new intent filter for the action `dev.eden.eden_emulator.LAUNCH_WITH_CUSTOM_CONFIG` to `EmulationActivity`. This allows the app to respond to custom settings intents.
*   **`EmulationFragment.kt`**:
    *   Modified `onCreate` to detect and handle the new custom settings intent.
    *   If a custom settings intent is received:
        *   It uses `CustomSettingsHandler.applyCustomSettingsWithDriverCheck` to process the settings asynchronously. This allows for driver checks and user interaction (e.g., overwrite confirmation, driver installation).
        *   Displays appropriate error messages via `Toast` if custom settings processing fails (e.g., game not found, driver issues).
        *   The game is then launched with the applied custom settings.
    *   If a regular file intent or navigation arguments are used, the existing logic for loading game configurations (including custom per-game configs) is retained.
    *   Ensures that per-game configurations are correctly loaded or unloaded based on how the game is launched.
This commit is contained in:
Producdevity 2025-07-12 13:02:48 +02:00
parent 3df06da02c
commit eb720ef074
4 changed files with 690 additions and 12 deletions

View file

@ -85,6 +85,10 @@ SPDX-License-Identifier: GPL-3.0-or-later
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="application/octet-stream" android:scheme="content"/>
</intent-filter>
<intent-filter>
<action android:name="dev.eden.eden_emulator.LAUNCH_WITH_CUSTOM_CONFIG" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
<meta-data android:name="android.nfc.action.TECH_DISCOVERED" android:resource="@xml/nfc_tech_filter" />
</activity>

View file

@ -44,6 +44,7 @@ import androidx.core.view.updateLayoutParams
import androidx.drawerlayout.widget.DrawerLayout
import androidx.drawerlayout.widget.DrawerLayout.DrawerListener
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentActivity
import androidx.fragment.app.activityViewModels
import androidx.navigation.findNavController
import androidx.navigation.fragment.navArgs
@ -81,6 +82,10 @@ import org.yuzu.yuzu_emu.utils.NativeConfig
import org.yuzu.yuzu_emu.utils.ViewUtils
import org.yuzu.yuzu_emu.utils.ViewUtils.setVisible
import org.yuzu.yuzu_emu.utils.collect
import org.yuzu.yuzu_emu.utils.CustomSettingsHandler
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import java.io.File
class EmulationFragment : Fragment(), SurfaceHolder.Callback {
@ -94,6 +99,7 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
private lateinit var gpuDriver: String
private var _binding: FragmentEmulationBinding? = null
private val binding get() = _binding!!
private val args by navArgs<EmulationFragmentArgs>()
@ -108,6 +114,9 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
private lateinit var gpuModel: String
private lateinit var fwVersion: String
private var intentGame: Game? = null
private var isCustomSettingsIntent = false
override fun onAttach(context: Context) {
super.onAttach(context)
if (context is EmulationActivity) {
@ -125,9 +134,70 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
super.onCreate(savedInstanceState)
updateOrientation()
val intentUri: Uri? = requireActivity().intent.data
var intentGame: Game? = null
if (intentUri != null) {
val intent = requireActivity().intent
val intentUri: Uri? = intent.data
intentGame = null
isCustomSettingsIntent = false
if (intent.action == CustomSettingsHandler.CUSTOM_CONFIG_ACTION) {
val titleId = intent.getStringExtra(CustomSettingsHandler.EXTRA_TITLE_ID)
val customSettings = intent.getStringExtra(CustomSettingsHandler.EXTRA_CUSTOM_SETTINGS)
if (titleId != null && customSettings != null) {
Log.info("[EmulationFragment] Received custom settings intent for title: $titleId")
// Handle custom settings asynchronously to allow for driver checking/installation
CoroutineScope(Dispatchers.Main).launch {
try {
intentGame = CustomSettingsHandler.applyCustomSettingsWithDriverCheck(
titleId,
customSettings,
requireContext(),
requireActivity() as? FragmentActivity,
driverViewModel
)
if (intentGame == null) {
Log.error("[EmulationFragment] Custom settings processing failed for title ID: $titleId")
Toast.makeText(
requireContext(),
"Failed to apply custom settings. This could be due to:\n• Game not found in library\n• User cancelled configuration overwrite\n• Driver installation failed\n• Missing required drivers",
Toast.LENGTH_LONG
).show()
requireActivity().finish()
return@launch
}
isCustomSettingsIntent = true
// Continue with game setup
finishGameSetup()
} catch (e: Exception) {
Log.error("[EmulationFragment] Error processing custom settings: ${e.message}")
Toast.makeText(
requireContext(),
"Error processing custom settings: ${e.message}",
Toast.LENGTH_LONG
).show()
requireActivity().finish()
}
}
// Return early to prevent synchronous continuation
return
} else {
Log.error("[EmulationFragment] Custom settings intent missing required extras")
Toast.makeText(
requireContext(),
"Invalid custom settings data",
Toast.LENGTH_SHORT
).show()
requireActivity().finish()
return
}
} else if (intentUri != null) {
// Handle regular file intent
intentGame = if (Game.extensions.contains(FileUtil.getExtension(intentUri))) {
GameHelper.getGame(requireActivity().intent.data!!, false)
} else {
@ -135,6 +205,15 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
}
}
if (!isCustomSettingsIntent) {
finishGameSetup()
}
}
/**
* Complete the game setup process (extracted for async custom settings handling)
*/
private fun finishGameSetup() {
try {
game = if (args.game != null) {
args.game!!
@ -151,8 +230,12 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
return
}
// Handle configuration loading
if (isCustomSettingsIntent) {
// Custom settings already applied by CustomSettingsHandler
Log.info("[EmulationFragment] Using custom settings from intent")
} else if (args.custom || intentGame != null) {
// Always load custom settings when launching a game from an intent
if (args.custom || intentGame != null) {
SettingsFile.loadCustomConfig(game)
NativeConfig.unloadPerGameConfig()
} else {
@ -162,13 +245,13 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
// Install the selected driver asynchronously as the game starts
driverViewModel.onLaunchGame()
// So this fragment doesn't restart on configuration changes; i.e. rotation.
retainInstance = true
// Initialize emulation state (ViewModels handle state retention now)
emulationState = EmulationState(game.path) {
return@EmulationState driverViewModel.isInteractionAllowed.value
}
}
/**
* Initialize the UI and start emulation in here.
*/
@ -634,7 +717,12 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
val batteryTemp = getBatteryTemperature()
when (IntSetting.BAT_TEMPERATURE_UNIT.getInt(needsGlobal)) {
0 -> sb.append(String.format("%.1f°C", batteryTemp))
1 -> sb.append(String.format("%.1f°F", celsiusToFahrenheit(batteryTemp)))
1 -> sb.append(
String.format(
"%.1f°F",
celsiusToFahrenheit(batteryTemp)
)
)
}
}
@ -643,8 +731,10 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
val battery: BatteryManager =
requireContext().getSystemService(Context.BATTERY_SERVICE) as BatteryManager
val batteryIntent = requireContext().registerReceiver(null,
IntentFilter(Intent.ACTION_BATTERY_CHANGED))
val batteryIntent = requireContext().registerReceiver(
null,
IntentFilter(Intent.ACTION_BATTERY_CHANGED)
)
val capacity = battery.getIntProperty(BATTERY_PROPERTY_CAPACITY)
val nowUAmps = battery.getIntProperty(BATTERY_PROPERTY_CURRENT_NOW)

View file

@ -0,0 +1,222 @@
// 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.content.Context
import androidx.fragment.app.FragmentActivity
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import org.yuzu.yuzu_emu.model.DriverViewModel
import org.yuzu.yuzu_emu.model.Game
import java.io.File
import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine
object CustomSettingsHandler {
const val CUSTOM_CONFIG_ACTION = "dev.eden.eden_emulator.LAUNCH_WITH_CUSTOM_CONFIG"
const val EXTRA_TITLE_ID = "title_id"
const val EXTRA_CUSTOM_SETTINGS = "custom_settings"
/**
* Apply custom settings from a string instead of loading from file
* @param titleId The game title ID (16-digit hex string)
* @param customSettings The complete INI file content as string
* @param context Application context
* @return Game object created from title ID, or null if not found
*/
fun applyCustomSettings(titleId: String, customSettings: String, context: Context): Game? {
// For synchronous calls without driver checking
Log.info("[CustomSettingsHandler] Applying custom settings for title ID: $titleId")
// Find the game by title ID
val game = findGameByTitleId(titleId, context)
if (game == null) {
Log.error("[CustomSettingsHandler] Game not found for title ID: $titleId")
return null
}
// Check if config already exists - this should be handled by the caller
val configFile = getConfigFile(titleId)
if (configFile.exists()) {
Log.warning("[CustomSettingsHandler] Config file already exists for title ID: $titleId")
// The caller should have already asked the user about overwriting
}
// Write the config file
if (!writeConfigFile(titleId, customSettings)) {
Log.error("[CustomSettingsHandler] Failed to write config file")
return null
}
// Initialize per-game config
try {
NativeConfig.initializePerGameConfig(game.programId, configFile.nameWithoutExtension)
Log.info("[CustomSettingsHandler] Successfully applied custom settings")
return game
} catch (e: Exception) {
Log.error("[CustomSettingsHandler] Failed to apply custom settings: ${e.message}")
return null
}
}
/**
* Apply custom settings with automatic driver checking and installation
* @param titleId The game title ID (16-digit hex string)
* @param customSettings The complete INI file content as string
* @param context Application context
* @param activity Fragment activity for driver installation dialogs (optional)
* @param driverViewModel DriverViewModel for driver management (optional)
* @return Game object created from title ID, or null if not found
*/
suspend fun applyCustomSettingsWithDriverCheck(
titleId: String,
customSettings: String,
context: Context,
activity: FragmentActivity?,
driverViewModel: DriverViewModel?
): Game? {
Log.info("[CustomSettingsHandler] Applying custom settings for title ID: $titleId")
// Find the game by title ID
val game = findGameByTitleId(titleId, context)
if (game == null) {
Log.error("[CustomSettingsHandler] Game not found for title ID: $titleId")
// This will be handled by the caller to show appropriate error message
return null
}
// Check if config already exists
val configFile = getConfigFile(titleId)
if (configFile.exists() && activity != null) {
Log.info("[CustomSettingsHandler] Config file already exists, asking user for confirmation")
val shouldOverwrite = askUserToOverwriteConfig(activity, game.title)
if (!shouldOverwrite) {
Log.info("[CustomSettingsHandler] User chose not to overwrite existing config")
return null
}
}
// Check for driver requirements if activity and driverViewModel are provided
if (activity != null && driverViewModel != null) {
val driverPath = DriverResolver.extractDriverPath(customSettings)
if (driverPath != null) {
Log.info("[CustomSettingsHandler] Custom settings specify driver: $driverPath")
val driverExists = DriverResolver.ensureDriverExists(driverPath, activity, driverViewModel)
if (!driverExists) {
Log.error("[CustomSettingsHandler] Required driver not available: $driverPath")
// Don't write config if driver installation failed
return null
}
}
}
// Only write the config file after all checks pass
if (!writeConfigFile(titleId, customSettings)) {
Log.error("[CustomSettingsHandler] Failed to write config file")
return null
}
// Initialize per-game config
try {
NativeConfig.initializePerGameConfig(game.programId, configFile.nameWithoutExtension)
Log.info("[CustomSettingsHandler] Successfully applied custom settings")
return game
} catch (e: Exception) {
Log.error("[CustomSettingsHandler] Failed to apply custom settings: ${e.message}")
return null
}
}
/**
* Find a game by its title ID in the user's game library
*/
private fun findGameByTitleId(titleId: String, context: Context): Game? {
Log.info("[CustomSettingsHandler] Searching for game with title ID: $titleId")
// Convert hex title ID to decimal for comparison with programId
val programIdDecimal = try {
titleId.toLong(16).toString()
} catch (e: NumberFormatException) {
Log.error("[CustomSettingsHandler] Invalid title ID format: $titleId")
return null
}
// Expected hex format with "0" prefix
val expectedHex = "0${titleId.uppercase()}"
// First check cached games for fast lookup
GameHelper.cachedGameList.find { game ->
game.programId == programIdDecimal ||
game.programIdHex.equals(expectedHex, ignoreCase = true)
}?.let { foundGame ->
Log.info("[CustomSettingsHandler] Found game in cache: ${foundGame.title}")
return foundGame
}
// If not in cache, perform full game library scan
Log.info("[CustomSettingsHandler] Game not in cache, scanning full library...")
val allGames = GameHelper.getGames()
val foundGame = allGames.find { game ->
game.programId == programIdDecimal ||
game.programIdHex.equals(expectedHex, ignoreCase = true)
}
if (foundGame != null) {
Log.info("[CustomSettingsHandler] Found game: ${foundGame.title} at ${foundGame.path}")
} else {
Log.warning("[CustomSettingsHandler] No game found for title ID: $titleId")
}
return foundGame
}
/**
* Get the config file path for a title ID
*/
private fun getConfigFile(titleId: String): File {
val configDir = File(DirectoryInitialization.userDirectory, "config/custom")
return File(configDir, "$titleId.ini")
}
/**
* Write the config file with the custom settings
*/
private fun writeConfigFile(titleId: String, customSettings: String): Boolean {
return try {
val configDir = File(DirectoryInitialization.userDirectory, "config/custom")
if (!configDir.exists()) {
configDir.mkdirs()
}
val configFile = File(configDir, "$titleId.ini")
configFile.writeText(customSettings)
Log.info("[CustomSettingsHandler] Wrote config file: ${configFile.absolutePath}")
true
} catch (e: Exception) {
Log.error("[CustomSettingsHandler] Failed to write config file: ${e.message}")
false
}
}
/**
* Ask user if they want to overwrite existing configuration
*/
private suspend fun askUserToOverwriteConfig(activity: FragmentActivity, gameTitle: String): Boolean {
return suspendCoroutine { continuation ->
activity.runOnUiThread {
MaterialAlertDialogBuilder(activity)
.setTitle("Configuration Already Exists")
.setMessage(
"Custom settings already exist for '$gameTitle'.\n\n" +
"Do you want to overwrite the existing configuration?\n\n" +
"This action cannot be undone."
)
.setPositiveButton("Overwrite") { _, _ ->
continuation.resume(true)
}
.setNegativeButton("Cancel") { _, _ ->
continuation.resume(false)
}
.setCancelable(false)
.show()
}
}
}
}

View file

@ -0,0 +1,362 @@
// 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 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.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("Missing GPU Driver")
.setMessage(
"The custom settings require the GPU driver '$driverName' which is not installed.\n\n" +
"Would you like to download and install it automatically?"
)
.setPositiveButton("Install") { _, _ ->
continuation.resume(true)
}
.setNegativeButton("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}")
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}")
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("Installation Failed")
.setMessage("Failed to download and install the driver: ${e.message}")
.setPositiveButton("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("Driver Not Available")
.setMessage(
"The required GPU driver '$driverName' is not available for automatic download.\n\n" +
"Please manually install the driver or launch the game with default settings."
)
.setPositiveButton("OK") { dialog, _ -> dialog.dismiss() }
.show()
}
}
}