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
This commit is contained in:
lynxnb 2022-07-30 12:46:54 +02:00 committed by Mark Collins
parent e9f609b923
commit 48cf1263bc
11 changed files with 410 additions and 3 deletions

View File

@ -53,6 +53,14 @@
android:value="emu.skyline.SettingsActivity" />
</activity>
<activity
android:name="emu.skyline.preference.GpuDriverActivity"
android:exported="true">
<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value="emu.skyline.SettingsActivity" />
</activity>
<activity
android:name="emu.skyline.input.onscreen.OnScreenEditActivity"
android:exported="true"

View File

@ -0,0 +1,66 @@
/*
* SPDX-License-Identifier: MPL-2.0
* Copyright © 2022 Skyline Team and Contributors (https://github.com/skyline-emu/)
*/
package emu.skyline.adapter
import android.view.ViewGroup
import emu.skyline.data.GpuDriverMetadata
import emu.skyline.databinding.GpuDriverItemBinding
object GpuDriverBindingFactory : ViewBindingFactory {
override fun createBinding(parent : ViewGroup) = GpuDriverItemBinding.inflate(parent.inflater(), parent, false)
}
open class GpuDriverViewItem(
val driverMetadata : GpuDriverMetadata,
private val onDelete : ((wasChecked : Boolean) -> Unit)? = null,
private val onClick : (() -> Unit)? = null
) : SelectableGenericListItem<GpuDriverItemBinding>() {
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<GpuDriverItemBinding>) : Boolean = key() == other.key()
override fun areContentsTheSame(other : GenericListItem<GpuDriverItemBinding>) : Boolean = other is GpuDriverViewItem && driverMetadata == other.driverMetadata
}

View File

@ -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<GenericListItem<out ViewBinding>> = 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)
}
}

View File

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/white"
android:pathData="M19,13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z" />
</vector>

View File

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/white"
android:pathData="M6,19c0,1.1 0.9,2 2,2h8c1.1,0 2,-0.9 2,-2V7H6v12zM19,4h-3.5l-1,-1h-5l-1,1H5v2h14V4z" />
</vector>

View File

@ -0,0 +1,34 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/coordinator_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".preference.GpuDriverActivity">
<include
android:id="@+id/titlebar"
layout="@layout/titlebar" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/driver_list"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipToPadding="false"
android:padding="@dimen/grid_padding"
app:layout_behavior="@string/appbar_scrolling_view_behavior" />
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/add_driver_button"
style="@style/Widget.MaterialComponents.FloatingActionButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end|bottom"
android:layout_margin="16dp"
android:contentDescription="@string/add_gpu_driver"
android:src="@drawable/ic_add"
app:hoveredFocusedTranslationZ="0dp"
app:pressedTranslationZ="0dp" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View File

@ -0,0 +1,93 @@
<?xml version="1.0" encoding="utf-8"?>
<com.google.android.material.card.MaterialCardView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
style="@style/Widget.App.OutlinedCard"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="@dimen/card_content_padding">
<com.google.android.material.radiobutton.MaterialRadioButton
android:id="@+id/radio_button"
android:layout_width="32dp"
android:layout_height="32dp"
android:clickable="false"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:checked="true" />
<TextView
android:id="@+id/name"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginHorizontal="8dp"
android:includeFontPadding="false"
android:textAppearance="@style/TextAppearance.MaterialComponents.Subtitle1"
app:layout_constraintEnd_toStartOf="@+id/delete_button"
app:layout_constraintStart_toEndOf="@id/radio_button"
app:layout_constraintTop_toTopOf="parent"
tools:text="Mesa Turnip Driver" />
<TextView
android:id="@+id/author_package_version"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:fontFamily="sans-serif-medium"
android:includeFontPadding="false"
android:textAppearance="@style/TextAppearance.MaterialComponents.Subtitle1"
android:textColor="?android:attr/textColorSecondary"
android:textSize="14sp"
android:textStyle="italic"
app:layout_constraintEnd_toEndOf="@+id/name"
app:layout_constraintStart_toStartOf="@id/name"
app:layout_constraintTop_toBottomOf="@id/name"
tools:text="v1.0.2 by Skyline" />
<TextView
android:id="@+id/vendor_driver_version"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:fontFamily="sans-serif-medium"
android:includeFontPadding="false"
android:textAppearance="@style/TextAppearance.MaterialComponents.Subtitle1"
android:textColor="?android:attr/textColorSecondary"
android:textSize="14sp"
android:textStyle="italic"
app:layout_constraintEnd_toEndOf="@+id/name"
app:layout_constraintStart_toStartOf="@id/name"
app:layout_constraintTop_toBottomOf="@id/author_package_version"
tools:text="Driver: 512.615.0 • Mesa" />
<TextView
android:id="@+id/description"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="6dp"
android:includeFontPadding="false"
app:layout_constraintEnd_toEndOf="@+id/name"
app:layout_constraintStart_toStartOf="@+id/name"
app:layout_constraintTop_toBottomOf="@id/vendor_driver_version"
tools:text="Turnip driver for a6xx generation adreno" />
<ImageView
android:id="@+id/delete_button"
android:layout_width="30dp"
android:layout_height="30dp"
android:background="?attr/selectableItemBackgroundBorderless"
android:contentDescription="@string/delete_gpu_driver"
android:padding="3dp"
android:scaleType="fitCenter"
app:layout_constraintBottom_toBottomOf="@+id/radio_button"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="@+id/radio_button"
app:srcCompat="@drawable/ic_delete"
app:tint="?android:attr/textColorSecondary" />
</androidx.constraintlayout.widget.ConstraintLayout>
</com.google.android.material.card.MaterialCardView>

View File

@ -2,4 +2,5 @@
<resources>
<dimen name="grid_padding">8dp</dimen>
<dimen name="onScreenItemHorizontalMargin">10dp</dimen>
<dimen name="card_content_padding">12dp</dimen>
</resources>

View File

@ -36,6 +36,7 @@
<string name="perf_stats_desc_off">Performance Statistics will not be shown</string>
<string name="perf_stats_desc_on">Performance Statistics will be shown in the top-left corner</string>
<string name="log_level">Log Level</string>
<string name="gpu_driver_config">GPU Driver Configuration</string>
<!-- Settings - System -->
<string name="system">System</string>
<string name="use_docked">Use Docked Mode</string>
@ -66,6 +67,19 @@
<string name="respect_display_cutout">Respect Display Cutout</string>
<string name="respect_display_cutout_enabled">Do not draw UI elements in the cutout area</string>
<string name="respect_display_cutout_disabled">Allow UI elements to be drawn in the cutout area</string>
<!-- Gpu Driver Activity -->
<string name="gpu_driver">GPU Driver</string>
<string name="add_gpu_driver">Add a GPU driver</string>
<string name="delete_gpu_driver">Delete this GPU driver</string>
<string name="system_driver">System Driver</string>
<string name="system_driver_desc">The GPU driver provided by your system</string>
<string name="gpu_driver_install_inprogress">Installing the GPU driver…</string>
<string name="gpu_driver_install_success">GPU driver installed successfully</string>
<string name="gpu_driver_install_invalid_archive">Failed to unzip the provided driver package</string>
<string name="gpu_driver_install_missing_metadata">The supplied driver package is invalid due to missing metadata</string>
<string name="gpu_driver_install_invalid_metadata">The supplied driver package contains invalid metadata, it may be corrupted</string>
<string name="gpu_driver_install_unsupported_android_version">Your device doesn\'t meet the minimum Android version requirements for the supplied driver</string>
<string name="gpu_driver_install_already_installed">The supplied driver package is already installled</string>
<!-- Input -->
<string name="input">Input</string>
<string name="osc">On-Screen Controls</string>

View File

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<resources xmlns:tools="http://schemas.android.com/tools">
<style name="RoundedAppImage">
<item name="cornerFamily">rounded</item>
@ -18,4 +18,12 @@
<item name="materialThemeOverlay">@style/ChipChoice.Material</item>
<item name="shapeAppearance">@style/ShapeAppearance.MaterialComponents.LargeComponent</item>
</style>
<style name="Widget.App.OutlinedCard" parent="Widget.MaterialComponents.CardView">
<item name="cardBackgroundColor">@android:color/transparent</item>
<item name="cardElevation">0dp</item>
<item name="strokeColor" tools:ignore="PrivateResource">@color/mtrl_btn_stroke_color_selector</item>
<item name="strokeWidth">1dp</item>
<item name="android:clickable">true</item>
</style>
</resources>

View File

@ -3,9 +3,9 @@
<style name="BaseAppTheme" parent="Theme.MaterialComponents.DayNight.NoActionBar">
<item name="colorPrimary">@color/colorPrimary</item>
<item name="colorOnPrimary">@color/colorPrimary</item>
<item name="colorOnPrimary">@android:color/white</item>
<item name="colorSecondary">@color/colorPrimary</item>
<item name="colorOnSecondary">@color/colorPrimary</item>
<item name="colorOnSecondary">@android:color/white</item>
<item name="android:statusBarColor">@color/backgroundColor</item>
<item name="android:navigationBarColor">@color/backgroundColor</item>
<item name="android:colorBackground">@color/backgroundColor</item>