diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index ba0b74e3..cb1cbb37 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -53,6 +53,14 @@
android:value="emu.skyline.SettingsActivity" />
+
+
+
+
Unit)? = null,
+ private val onClick : (() -> Unit)? = null
+) : SelectableGenericListItem() {
+ private var position = -1
+
+ override fun getViewBindingFactory() = GpuDriverBindingFactory
+
+ override fun bind(binding : GpuDriverItemBinding, position : Int) {
+ this.position = position
+ binding.name.text = driverMetadata.name
+
+ if (driverMetadata.packageVersion.isNotBlank() || driverMetadata.packageVersion.isNotBlank()) {
+ binding.authorPackageVersion.text = "v${driverMetadata.packageVersion} by ${driverMetadata.author}"
+ binding.authorPackageVersion.visibility = ViewGroup.VISIBLE
+ } else {
+ binding.authorPackageVersion.visibility = ViewGroup.GONE
+ }
+
+ binding.vendorDriverVersion.text = "Driver: v${driverMetadata.driverVersion} • ${driverMetadata.vendor}"
+ binding.description.text = driverMetadata.description
+ binding.radioButton.isChecked = position == selectableAdapter?.selectedPosition
+
+ binding.root.setOnClickListener {
+ selectableAdapter?.selectAndNotify(position)
+ onClick?.invoke()
+ }
+
+ if (onDelete != null) {
+ binding.deleteButton.visibility = ViewGroup.VISIBLE
+ binding.deleteButton.setOnClickListener {
+ val wasChecked = position == selectableAdapter?.selectedPosition
+ selectableAdapter?.removeItemAt(position)
+
+ onDelete.invoke(wasChecked)
+ }
+ } else {
+ binding.deleteButton.visibility = ViewGroup.GONE
+ }
+ }
+
+ /**
+ * The label of the driver is used as key as it's effectively unique
+ */
+ override fun key() = driverMetadata.label
+
+ override fun areItemsTheSame(other : GenericListItem) : Boolean = key() == other.key()
+
+ override fun areContentsTheSame(other : GenericListItem) : Boolean = other is GpuDriverViewItem && driverMetadata == other.driverMetadata
+}
diff --git a/app/src/main/java/emu/skyline/preference/GpuDriverActivity.kt b/app/src/main/java/emu/skyline/preference/GpuDriverActivity.kt
new file mode 100644
index 00000000..992f38cd
--- /dev/null
+++ b/app/src/main/java/emu/skyline/preference/GpuDriverActivity.kt
@@ -0,0 +1,165 @@
+/*
+ * SPDX-License-Identifier: MPL-2.0
+ * Copyright © 2022 Skyline Team and Contributors (https://github.com/skyline-emu/)
+ */
+
+package emu.skyline.preference
+
+import android.content.Intent
+import android.content.Intent.FLAG_GRANT_READ_URI_PERMISSION
+import android.os.Bundle
+import android.view.ViewTreeObserver
+import androidx.activity.result.contract.ActivityResultContracts
+import androidx.appcompat.app.AppCompatActivity
+import androidx.coordinatorlayout.widget.CoordinatorLayout
+import androidx.recyclerview.widget.LinearLayoutManager
+import androidx.viewbinding.ViewBinding
+import com.google.android.material.snackbar.Snackbar
+import dagger.hilt.android.AndroidEntryPoint
+import emu.skyline.R
+import emu.skyline.adapter.GenericListItem
+import emu.skyline.adapter.GpuDriverViewItem
+import emu.skyline.adapter.SelectableGenericAdapter
+import emu.skyline.adapter.SpacingItemDecoration
+import emu.skyline.databinding.GpuDriverActivityBinding
+import emu.skyline.utils.GpuDriverHelper
+import emu.skyline.utils.GpuDriverInstallResult
+import emu.skyline.utils.PreferenceSettings
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
+import javax.inject.Inject
+
+/**
+ * This activity is used to manage the installed gpu drivers and select one to use.
+ */
+@AndroidEntryPoint
+class GpuDriverActivity : AppCompatActivity() {
+ private val binding by lazy { GpuDriverActivityBinding.inflate(layoutInflater) }
+
+ private val adapter = SelectableGenericAdapter(0)
+
+ @Inject
+ lateinit var preferenceSettings : PreferenceSettings
+
+ /**
+ * The callback called after a user picked a driver to install.
+ */
+ private val installCallback = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
+ if (it.resultCode == RESULT_OK) {
+ it.data?.data?.let { uri ->
+ val fileStream = contentResolver.openInputStream(uri) ?: return@let
+
+ Snackbar.make(binding.root, getString(R.string.gpu_driver_install_inprogress), Snackbar.LENGTH_INDEFINITE).show()
+ CoroutineScope(Dispatchers.IO).launch {
+ val result = GpuDriverHelper.installDriver(this@GpuDriverActivity, fileStream)
+ runOnUiThread {
+ Snackbar.make(binding.root, resolveInstallResultString(result), Snackbar.LENGTH_LONG).show()
+ if (result == GpuDriverInstallResult.SUCCESS)
+ populateAdapter()
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * Updates the [adapter] with the current list of installed drivers.
+ */
+ private fun populateAdapter() {
+ val items : MutableList> = ArrayList()
+
+ // Insert the system driver entry at the top of the list.
+ items.add(GpuDriverViewItem(GpuDriverHelper.getSystemDriverMetadata(this)) {
+ preferenceSettings.gpuDriver = PreferenceSettings.SYSTEM_GPU_DRIVER
+ })
+
+ if (preferenceSettings.gpuDriver == PreferenceSettings.SYSTEM_GPU_DRIVER) {
+ adapter.selectedPosition = 0
+ }
+
+ GpuDriverHelper.getInstalledDrivers(this).onEachIndexed { index, (file, metadata) ->
+ items.add(GpuDriverViewItem(metadata, { wasChecked ->
+ if (wasChecked) {
+ // If the deleted driver was the selected one, select the system driver
+ preferenceSettings.gpuDriver = PreferenceSettings.SYSTEM_GPU_DRIVER
+ }
+ if (!file.deleteRecursively()) {
+ Snackbar.make(binding.root, getString(R.string.gpu_driver_delete_failed), Snackbar.LENGTH_LONG).show()
+ }
+ }, {
+ preferenceSettings.gpuDriver = metadata.label
+ }))
+
+ if (preferenceSettings.gpuDriver == metadata.label) {
+ adapter.selectedPosition = index + 1 // Add 1 to account for the system driver entry
+ }
+ }
+
+ adapter.setItems(items)
+ }
+
+ override fun onCreate(savedInstanceState : Bundle?) {
+ super.onCreate(savedInstanceState)
+
+ setContentView(binding.root)
+
+ setSupportActionBar(binding.titlebar.toolbar)
+ supportActionBar?.setDisplayHomeAsUpEnabled(true)
+ supportActionBar?.title = getString(R.string.gpu_driver_config)
+
+ val layoutManager = LinearLayoutManager(this)
+ binding.driverList.layoutManager = layoutManager
+ binding.driverList.adapter = adapter
+
+ var layoutDone = false // Tracks if the layout is complete to avoid retrieving invalid attributes
+ binding.coordinatorLayout.viewTreeObserver.addOnTouchModeChangeListener { isTouchMode ->
+ val layoutUpdate = {
+ val params = binding.driverList.layoutParams as CoordinatorLayout.LayoutParams
+ if (!isTouchMode) {
+ binding.titlebar.appBarLayout.setExpanded(true)
+ params.height = binding.coordinatorLayout.height - binding.titlebar.toolbar.height
+ } else {
+ params.height = CoordinatorLayout.LayoutParams.MATCH_PARENT
+ }
+
+ binding.driverList.layoutParams = params
+ binding.driverList.requestLayout()
+ }
+
+ if (!layoutDone) {
+ binding.coordinatorLayout.viewTreeObserver.addOnGlobalLayoutListener(object : ViewTreeObserver.OnGlobalLayoutListener {
+ override fun onGlobalLayout() {
+ // We need to wait till the layout is done to get the correct height of the toolbar
+ binding.coordinatorLayout.viewTreeObserver.removeOnGlobalLayoutListener(this)
+ layoutUpdate()
+ layoutDone = true
+ }
+ })
+ } else {
+ layoutUpdate()
+ }
+ }
+
+ binding.driverList.addItemDecoration(SpacingItemDecoration(resources.getDimensionPixelSize(R.dimen.grid_padding)))
+
+ binding.addDriverButton.setOnClickListener {
+ val intent = Intent(Intent.ACTION_GET_CONTENT).apply {
+ addFlags(FLAG_GRANT_READ_URI_PERMISSION)
+ type = "application/zip"
+ }
+ installCallback.launch(intent)
+ }
+
+ populateAdapter()
+ }
+
+ private fun resolveInstallResultString(result : GpuDriverInstallResult) = when (result) {
+ GpuDriverInstallResult.SUCCESS -> getString(R.string.gpu_driver_install_success)
+ GpuDriverInstallResult.INVALID_ARCHIVE -> getString(R.string.gpu_driver_install_invalid_archive)
+ GpuDriverInstallResult.MISSING_METADATA -> getString(R.string.gpu_driver_install_missing_metadata)
+ GpuDriverInstallResult.INVALID_METADATA -> getString(R.string.gpu_driver_install_invalid_metadata)
+ GpuDriverInstallResult.UNSUPPORTED_ANDROID_VERSION -> getString(R.string.gpu_driver_install_unsupported_android_version)
+ GpuDriverInstallResult.ALREADY_INSTALLED -> getString(R.string.gpu_driver_install_already_installed)
+ }
+}
diff --git a/app/src/main/res/drawable/ic_add.xml b/app/src/main/res/drawable/ic_add.xml
new file mode 100644
index 00000000..d3c2150c
--- /dev/null
+++ b/app/src/main/res/drawable/ic_add.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_delete.xml b/app/src/main/res/drawable/ic_delete.xml
new file mode 100644
index 00000000..6c6f704a
--- /dev/null
+++ b/app/src/main/res/drawable/ic_delete.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/app/src/main/res/layout/gpu_driver_activity.xml b/app/src/main/res/layout/gpu_driver_activity.xml
new file mode 100644
index 00000000..308c5cdd
--- /dev/null
+++ b/app/src/main/res/layout/gpu_driver_activity.xml
@@ -0,0 +1,34 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/gpu_driver_item.xml b/app/src/main/res/layout/gpu_driver_item.xml
new file mode 100644
index 00000000..b04a7a91
--- /dev/null
+++ b/app/src/main/res/layout/gpu_driver_item.xml
@@ -0,0 +1,93 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml
index 090dcbfc..14064691 100644
--- a/app/src/main/res/values/dimens.xml
+++ b/app/src/main/res/values/dimens.xml
@@ -2,4 +2,5 @@
8dp
10dp
+ 12dp
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 1e273aaf..68049d8b 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -36,6 +36,7 @@
Performance Statistics will not be shown
Performance Statistics will be shown in the top-left corner
Log Level
+ GPU Driver Configuration
System
Use Docked Mode
@@ -66,6 +67,19 @@
Respect Display Cutout
Do not draw UI elements in the cutout area
Allow UI elements to be drawn in the cutout area
+
+ GPU Driver
+ Add a GPU driver
+ Delete this GPU driver
+ System Driver
+ The GPU driver provided by your system
+ Installing the GPU driver…
+ GPU driver installed successfully
+ Failed to unzip the provided driver package
+ The supplied driver package is invalid due to missing metadata
+ The supplied driver package contains invalid metadata, it may be corrupted
+ Your device doesn\'t meet the minimum Android version requirements for the supplied driver
+ The supplied driver package is already installled
Input
On-Screen Controls
diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml
index e9818aa1..417dd9e9 100644
--- a/app/src/main/res/values/styles.xml
+++ b/app/src/main/res/values/styles.xml
@@ -1,5 +1,5 @@
-
+
+
+
diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml
index ce853a11..cce27949 100644
--- a/app/src/main/res/values/themes.xml
+++ b/app/src/main/res/values/themes.xml
@@ -3,9 +3,9 @@