diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/fetcher/ReleaseAdapter.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/fetcher/ReleaseAdapter.kt
index e35ef741fd..23b980d302 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/fetcher/ReleaseAdapter.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/fetcher/ReleaseAdapter.kt
@@ -143,8 +143,21 @@ class ReleaseAdapter(
binding.containerDownloads.removeAllViews()
release.artifacts.forEach { artifact ->
+ val alreadyInstalled = try {
+ // Prefer fast check via ViewModel list; fallback to helper if needed
+ driverViewModel.driverData.any {
+ File(it.first).name.equals(artifact.name, ignoreCase = true)
+ } || GpuDriverHelper.isDriverZipInstalledByName(artifact.name)
+ } catch (_: Exception) {
+ false
+ }
+
val button = MaterialButton(binding.root.context).apply {
- text = artifact.name
+ text = if (alreadyInstalled) {
+ context.getString(R.string.installed_label, artifact.name)
+ } else {
+ artifact.name
+ }
setTextAppearance(
com.google.android.material.R.style.TextAppearance_Material3_LabelLarge
)
@@ -154,7 +167,7 @@ class ReleaseAdapter(
com.google.android.material.R.color.m3_button_background_color_selector
)
)
- setIconResource(R.drawable.ic_import)
+ setIconResource(if (alreadyInstalled) R.drawable.ic_check else R.drawable.ic_import)
iconTint = ColorStateList.valueOf(
MaterialColors.getColor(
this,
@@ -167,7 +180,22 @@ class ReleaseAdapter(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.WRAP_CONTENT
)
+ isEnabled = !alreadyInstalled
setOnClickListener {
+ // Double-check just before starting (race-proof)
+ if (GpuDriverHelper.isDriverZipInstalledByName(artifact.name)) {
+ Toast.makeText(
+ context,
+ context.getString(R.string.driver_already_installed),
+ Toast.LENGTH_SHORT
+ ).show()
+ // Update UI to reflect installed state
+ this.isEnabled = false
+ this.text = context.getString(R.string.installed_label, artifact.name)
+ this.setIconResource(R.drawable.ic_check)
+ return@setOnClickListener
+ }
+
val dialogBinding =
DialogProgressBinding.inflate(LayoutInflater.from(context))
dialogBinding.progressBar.isIndeterminate = true
@@ -233,6 +261,10 @@ class ReleaseAdapter(
driverViewModel.onDriverAdded(Pair(driverPath, driverData))
progressDialog.dismiss()
+ // Update button to installed state
+ this@apply.isEnabled = false
+ this@apply.text = context.getString(R.string.installed_label, artifact.name)
+ this@apply.setIconResource(R.drawable.ic_check)
Toast.makeText(
context,
context.getString(
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/GpuDriverHelper.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/GpuDriverHelper.kt
index 99f7fd81fe..e5832d660e 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/GpuDriverHelper.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/GpuDriverHelper.kt
@@ -238,4 +238,18 @@ object GpuDriverHelper {
driverStorageDirectory.mkdirs()
}
}
+
+ /**
+ * Checks if a driver zip with the given filename is already present and valid in the
+ * internal driver storage directory. Validation requires a readable meta.json with a name.
+ */
+ fun isDriverZipInstalledByName(fileName: String): Boolean {
+ // Normalize separators in case upstream sent a path
+ val baseName = fileName.substringAfterLast('/')
+ .substringAfterLast('\\')
+ val candidate = File("$driverStoragePath$baseName")
+ if (!candidate.exists() || candidate.length() == 0L) return false
+ val metadata = getMetadataFromZip(candidate)
+ return metadata.name != null
+ }
}
diff --git a/src/android/app/src/main/res/values/strings.xml b/src/android/app/src/main/res/values/strings.xml
index f73fc1d9aa..19cb17ea3a 100644
--- a/src/android/app/src/main/res/values/strings.xml
+++ b/src/android/app/src/main/res/values/strings.xml
@@ -678,6 +678,7 @@
Using default GPU driver
Invalid driver selected
Driver already installed
+ %1$s (Installed)
System GPU driver
Installing driver…