From 48cf1263bc5a22174ae9b606531d1853f1b2932e Mon Sep 17 00:00:00 2001 From: lynxnb Date: Sat, 30 Jul 2022 12:46:54 +0200 Subject: [PATCH] Add a custom GPU driver configuration activity The activity adds the following functionalities: * Lists installed drivers * Allows the user to install new drivers, or remove installed ones * Allows the user to select the driver that will be used by the emulator --- app/src/main/AndroidManifest.xml | 8 + .../emu/skyline/adapter/GpuDriverViewItem.kt | 66 +++++++ .../skyline/preference/GpuDriverActivity.kt | 165 ++++++++++++++++++ app/src/main/res/drawable/ic_add.xml | 9 + app/src/main/res/drawable/ic_delete.xml | 9 + .../main/res/layout/gpu_driver_activity.xml | 34 ++++ app/src/main/res/layout/gpu_driver_item.xml | 93 ++++++++++ app/src/main/res/values/dimens.xml | 1 + app/src/main/res/values/strings.xml | 14 ++ app/src/main/res/values/styles.xml | 10 +- app/src/main/res/values/themes.xml | 4 +- 11 files changed, 410 insertions(+), 3 deletions(-) create mode 100644 app/src/main/java/emu/skyline/adapter/GpuDriverViewItem.kt create mode 100644 app/src/main/java/emu/skyline/preference/GpuDriverActivity.kt create mode 100644 app/src/main/res/drawable/ic_add.xml create mode 100644 app/src/main/res/drawable/ic_delete.xml create mode 100644 app/src/main/res/layout/gpu_driver_activity.xml create mode 100644 app/src/main/res/layout/gpu_driver_item.xml 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 @@