Android: Implement a UI for Adrenotools

This commit is contained in:
Robin Kertels 2023-06-01 00:05:38 +02:00
parent 23bebc5270
commit 2da7d16b7c
No known key found for this signature in database
GPG Key ID: 3824904F14D40757
11 changed files with 517 additions and 19 deletions

View File

@ -83,13 +83,6 @@
android:theme="@style/Theme.Dolphin.Main"
android:label="@string/settings"/>
<activity
android:name=".features.settings.ui.GpuDriverActivity"
android:exported="false"
android:configChanges="orientation|screenSize"
android:theme="@style/Theme.Dolphin.Main"
android:label="@string/settings"/>
<activity
android:name=".features.cheats.ui.CheatsActivity"
android:exported="false"

View File

@ -49,7 +49,8 @@ enum class MenuTag {
WIIMOTE_MOTION_INPUT_1("wiimote_motion_input", 0),
WIIMOTE_MOTION_INPUT_2("wiimote_motion_input", 1),
WIIMOTE_MOTION_INPUT_3("wiimote_motion_input", 2),
WIIMOTE_MOTION_INPUT_4("wiimote_motion_input", 3);
WIIMOTE_MOTION_INPUT_4("wiimote_motion_input", 3),
GPU_DRIVERS("gpu_drivers");
var tag: String
private set

View File

@ -1,5 +1,9 @@
// SPDX-License-Identifier: GPL-2.0-or-later
// GPU driver implementation partially based on:
// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
package org.dolphinemu.dolphinemu.features.settings.ui
import android.content.Context
@ -20,6 +24,7 @@ import androidx.lifecycle.ViewModelProvider
import com.google.android.material.appbar.CollapsingToolbarLayout
import com.google.android.material.color.MaterialColors
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.snackbar.Snackbar
import org.dolphinemu.dolphinemu.NativeLibrary
import org.dolphinemu.dolphinemu.R
import org.dolphinemu.dolphinemu.databinding.ActivitySettingsBinding
@ -27,6 +32,7 @@ import org.dolphinemu.dolphinemu.features.settings.model.Settings
import org.dolphinemu.dolphinemu.features.settings.ui.SettingsFragment.Companion.newInstance
import org.dolphinemu.dolphinemu.ui.main.MainPresenter
import org.dolphinemu.dolphinemu.utils.FileBrowserHelper
import org.dolphinemu.dolphinemu.utils.GpuDriverInstallResult
import org.dolphinemu.dolphinemu.utils.InsetsHelper
import org.dolphinemu.dolphinemu.utils.SerializableHelper.serializable
import org.dolphinemu.dolphinemu.utils.ThemeHelper.enableScrollTint
@ -165,8 +171,21 @@ class SettingsActivity : AppCompatActivity(), SettingsActivityView {
super.onActivityResult(requestCode, resultCode, result)
// If the user picked a file, as opposed to just backing out.
if (resultCode == RESULT_OK) {
if (requestCode != MainPresenter.REQUEST_DIRECTORY) {
if (resultCode != RESULT_OK) {
return
}
when (requestCode) {
MainPresenter.REQUEST_DIRECTORY -> {
val path = FileBrowserHelper.getSelectedPath(result)
fragment!!.adapter!!.onFilePickerConfirmation(path!!)
}
MainPresenter.REQUEST_GAME_FILE
or MainPresenter.REQUEST_SD_FILE
or MainPresenter.REQUEST_WAD_FILE
or MainPresenter.REQUEST_WII_SAVE_FILE
or MainPresenter.REQUEST_NAND_BIN_FILE -> {
val uri = canonicalizeIfPossible(result!!.data!!)
val validExtensions: Set<String> =
if (requestCode == MainPresenter.REQUEST_GAME_FILE) FileBrowserHelper.GAME_EXTENSIONS else FileBrowserHelper.RAW_EXTENSION
@ -178,9 +197,6 @@ class SettingsActivity : AppCompatActivity(), SettingsActivityView {
contentResolver.takePersistableUriPermission(uri, takeFlags)
fragment!!.adapter!!.onFilePickerConfirmation(uri.toString())
}
} else {
val path = FileBrowserHelper.getSelectedPath(result)
fragment!!.adapter!!.onFilePickerConfirmation(path!!)
}
}
}

View File

@ -3,10 +3,15 @@
package org.dolphinemu.dolphinemu.features.settings.ui
import android.content.Context
import android.content.DialogInterface
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import androidx.core.view.updatePadding
@ -14,10 +19,15 @@ import androidx.fragment.app.DialogFragment
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentActivity
import androidx.recyclerview.widget.LinearLayoutManager
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.snackbar.Snackbar
import org.dolphinemu.dolphinemu.R
import org.dolphinemu.dolphinemu.databinding.FragmentSettingsBinding
import org.dolphinemu.dolphinemu.features.settings.model.Settings
import org.dolphinemu.dolphinemu.features.settings.model.view.SettingsItem
import org.dolphinemu.dolphinemu.ui.main.MainActivity
import org.dolphinemu.dolphinemu.ui.main.MainPresenter
import org.dolphinemu.dolphinemu.utils.GpuDriverInstallResult
import org.dolphinemu.dolphinemu.utils.SerializableHelper.serializable
import java.util.*
import kotlin.collections.ArrayList
@ -111,6 +121,11 @@ class SettingsFragment : Fragment(), SettingsFragmentView {
}
override fun loadSubMenu(menuKey: MenuTag) {
if (menuKey == MenuTag.GPU_DRIVERS) {
showGpuDriverDialog()
return
}
activityView!!.showSettingsFragment(
menuKey,
null,
@ -170,6 +185,74 @@ class SettingsFragment : Fragment(), SettingsFragmentView {
}
}
override fun showGpuDriverDialog() {
if (presenter.gpuDriver == null) {
return
}
val msg = "${presenter!!.gpuDriver!!.name} ${presenter!!.gpuDriver!!.driverVersion}"
MaterialAlertDialogBuilder(requireContext())
.setTitle(getString(R.string.gpu_driver_dialog_title))
.setMessage(msg)
.setNegativeButton(android.R.string.cancel, null)
.setNeutralButton(R.string.gpu_driver_dialog_system) { _: DialogInterface?, _: Int ->
presenter.useSystemDriver()
}
.setPositiveButton(R.string.gpu_driver_dialog_install) { _: DialogInterface?, _: Int ->
askForDriverFile()
}
.show()
}
private fun askForDriverFile() {
val intent = Intent(Intent.ACTION_GET_CONTENT).apply {
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
type = "application/zip"
}
startActivityForResult(intent, MainPresenter.REQUEST_GPU_DRIVER)
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
// If the user picked a file, as opposed to just backing out.
if (resultCode != AppCompatActivity.RESULT_OK) {
return
}
if (requestCode != MainPresenter.REQUEST_GPU_DRIVER) {
return
}
val uri = data?.data ?: return
presenter.installDriver(uri)
}
override fun onDriverInstallDone(result: GpuDriverInstallResult) {
val view = binding?.root ?: return
Snackbar
.make(view, resolveInstallResultString(result), Snackbar.LENGTH_LONG)
.show()
}
override fun onDriverUninstallDone() {
Toast.makeText(
requireContext(),
R.string.gpu_driver_dialog_uninstall_done,
Toast.LENGTH_SHORT
).show()
}
private fun resolveInstallResultString(result: GpuDriverInstallResult) = when (result) {
GpuDriverInstallResult.Success -> getString(R.string.gpu_driver_install_success)
GpuDriverInstallResult.InvalidArchive -> getString(R.string.gpu_driver_install_invalid_archive)
GpuDriverInstallResult.MissingMetadata -> getString(R.string.gpu_driver_install_missing_metadata)
GpuDriverInstallResult.InvalidMetadata -> getString(R.string.gpu_driver_install_invalid_metadata)
GpuDriverInstallResult.UnsupportedAndroidVersion -> getString(R.string.gpu_driver_install_unsupported_android_version)
GpuDriverInstallResult.AlreadyInstalled -> getString(R.string.gpu_driver_install_already_installed)
GpuDriverInstallResult.FileNotFound -> getString(R.string.gpu_driver_install_file_not_found)
}
companion object {
private const val ARGUMENT_MENU_TAG = "menu_tag"
private const val ARGUMENT_GAME_ID = "game_id"

View File

@ -4,11 +4,16 @@ package org.dolphinemu.dolphinemu.features.settings.ui
import android.content.Context
import android.content.pm.PackageManager
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.text.TextUtils
import androidx.appcompat.app.AppCompatActivity
import androidx.collection.ArraySet
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.dolphinemu.dolphinemu.NativeLibrary
import org.dolphinemu.dolphinemu.R
import org.dolphinemu.dolphinemu.activities.UserDataActivity
@ -25,6 +30,7 @@ import org.dolphinemu.dolphinemu.features.input.ui.ProfileDialog
import org.dolphinemu.dolphinemu.features.input.ui.ProfileDialogPresenter
import org.dolphinemu.dolphinemu.features.settings.model.*
import org.dolphinemu.dolphinemu.features.settings.model.view.*
import org.dolphinemu.dolphinemu.model.GpuDriverMetadata
import org.dolphinemu.dolphinemu.ui.main.MainPresenter
import org.dolphinemu.dolphinemu.utils.*
import java.util.*
@ -45,6 +51,9 @@ class SettingsFragmentPresenter(
private var controllerNumber = 0
private var controllerType = 0
var gpuDriver: GpuDriverMetadata? = null
private val libNameSetting: StringSetting = StringSetting.GFX_DRIVER_LIB_NAME
fun onCreate(menuTag: MenuTag, gameId: String?, extras: Bundle) {
this.gameId = gameId
this.menuTag = menuTag
@ -56,6 +65,11 @@ class SettingsFragmentPresenter(
controllerNumber = menuTag.subType
} else if (menuTag.isSerialPort1Menu) {
serialPort1Type = extras.getInt(ARG_SERIALPORT1_TYPE)
} else if (menuTag == MenuTag.GRAPHICS) {
this.gpuDriver =
GpuDriverHelper.getInstalledDriverMetadata() ?: GpuDriverHelper.getSystemDriverMetadata(
context.applicationContext
)
}
}
@ -1250,6 +1264,15 @@ class SettingsFragmentPresenter(
MenuTag.ADVANCED_GRAPHICS
)
)
if (GpuDriverHelper.supportsCustomDriverLoading() && this.gpuDriver != null) {
sl.add(
SubmenuSetting(
context,
R.string.gpu_driver_submenu, MenuTag.GPU_DRIVERS
)
)
}
}
private fun addEnhanceSettings(sl: ArrayList<SettingsItem>) {
@ -2113,7 +2136,7 @@ class SettingsFragmentPresenter(
profileString: String,
controllerNumber: Int
) {
val profiles = ProfileDialogPresenter(menuTag).getProfileNames(false)
val profiles = ProfileDialogPresenter(menuTag!!).getProfileNames(false)
val profileKey = profileString + "Profile" + (controllerNumber + 1)
sl.add(
StringSingleChoiceSetting(
@ -2324,6 +2347,45 @@ class SettingsFragmentPresenter(
)
}
fun installDriver(uri: Uri) {
val context = this.context.applicationContext
CoroutineScope(Dispatchers.IO).launch {
val stream = context.contentResolver.openInputStream(uri)
if (stream == null) {
GpuDriverHelper.uninstallDriver()
withContext(Dispatchers.Main) {
fragmentView.onDriverInstallDone(GpuDriverInstallResult.FileNotFound)
}
return@launch
}
val result = GpuDriverHelper.installDriver(stream)
withContext(Dispatchers.Main) {
with(this@SettingsFragmentPresenter) {
this.gpuDriver = GpuDriverHelper.getInstalledDriverMetadata()
?: GpuDriverHelper.getSystemDriverMetadata(context) ?: return@withContext
this.libNameSetting.setString(this.settings!!, this.gpuDriver!!.libraryName)
}
fragmentView.onDriverInstallDone(result)
}
}
}
fun useSystemDriver() {
CoroutineScope(Dispatchers.IO).launch {
GpuDriverHelper.uninstallDriver()
withContext(Dispatchers.Main) {
with(this@SettingsFragmentPresenter) {
this.gpuDriver =
GpuDriverHelper.getInstalledDriverMetadata()
?: GpuDriverHelper.getSystemDriverMetadata(context.applicationContext)
this.libNameSetting.setString(this.settings!!, "")
}
fragmentView.onDriverUninstallDone()
}
}
}
companion object {
private val LOG_TYPE_NAMES = NativeLibrary.GetLogTypeNames()
const val ARG_CONTROLLER_TYPE = "controller_type"

View File

@ -6,6 +6,7 @@ import androidx.fragment.app.DialogFragment
import androidx.fragment.app.FragmentActivity
import org.dolphinemu.dolphinemu.features.settings.model.Settings
import org.dolphinemu.dolphinemu.features.settings.model.view.SettingsItem
import org.dolphinemu.dolphinemu.utils.GpuDriverInstallResult
/**
* Abstraction for a screen showing a list of settings. Instances of
@ -111,4 +112,21 @@ interface SettingsFragmentView {
* @param visible Whether the warning should be visible.
*/
fun setOldControllerSettingsWarningVisibility(visible: Boolean)
/**
* Called when the driver installation is finished
*
* @param result The result of the driver installation
*/
fun onDriverInstallDone(result: GpuDriverInstallResult)
/**
* Called when the driver uninstall process is finished
*/
fun onDriverUninstallDone()
/**
* Shows a dialog asking the user to install or uninstall a GPU driver
*/
fun showGpuDriverDialog()
}

View File

@ -0,0 +1,61 @@
/*
* SPDX-License-Identifier: MPL-2.0
* Copyright © 2022 Skyline Team and Contributors (https://github.com/skyline-emu/)
*/
package org.dolphinemu.dolphinemu.model
import kotlinx.serialization.Serializable
import kotlinx.serialization.SerializationException
import kotlinx.serialization.json.*
import java.io.File
data class GpuDriverMetadata(
val name : String,
val author : String,
val packageVersion : String,
val vendor : String,
val driverVersion : String,
val minApi : Int,
val description : String,
val libraryName : String,
) {
private constructor(metadataV1 : GpuDriverMetadataV1) : this(
name = metadataV1.name,
author = metadataV1.author,
packageVersion = metadataV1.packageVersion,
vendor = metadataV1.vendor,
driverVersion = metadataV1.driverVersion,
minApi = metadataV1.minApi,
description = metadataV1.description,
libraryName = metadataV1.libraryName,
)
val label get() = "${name}-v${packageVersion}"
companion object {
private const val SCHEMA_VERSION_V1 = 1
fun deserialize(metadataFile : File) : GpuDriverMetadata {
val metadataJson = Json.parseToJsonElement(metadataFile.readText())
return when (metadataJson.jsonObject["schemaVersion"]?.jsonPrimitive?.intOrNull) {
SCHEMA_VERSION_V1 -> GpuDriverMetadata(Json.decodeFromJsonElement<GpuDriverMetadataV1>(metadataJson))
else -> throw SerializationException("Unsupported metadata version")
}
}
}
}
@Serializable
private data class GpuDriverMetadataV1(
val schemaVersion : Int,
val name : String,
val author : String,
val packageVersion : String,
val vendor : String,
val driverVersion : String,
val minApi : Int,
val description : String,
val libraryName : String,
)

View File

@ -286,6 +286,7 @@ class MainPresenter(private val mainView: MainView, private val activity: Fragme
const val REQUEST_WAD_FILE = 4
const val REQUEST_WII_SAVE_FILE = 5
const val REQUEST_NAND_BIN_FILE = 6
const val REQUEST_GPU_DRIVER = 7
private var shouldRescanLibrary = true

View File

@ -0,0 +1,148 @@
// Copyright 2023 Dolphin Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
// Partially based on:
// SPDX-License-Identifier: MPL-2.0
// Copyright © 2022 Skyline Team and Contributors (https://github.com/skyline-emu/)
// Partially based on:
// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
package org.dolphinemu.dolphinemu.utils
import android.content.Context
import android.os.Build
import kotlinx.serialization.SerializationException
import org.dolphinemu.dolphinemu.R
import org.dolphinemu.dolphinemu.model.GpuDriverMetadata
import java.io.File
import java.io.InputStream
private const val GPU_DRIVER_META_FILE = "meta.json"
interface GpuDriverHelper {
companion object {
/**
* Returns information about the system GPU driver.
* @return An array containing the driver vendor and the driver version, in this order, or `null` if an error occurred
*/
private external fun getSystemDriverInfo(): Array<String>?
/**
* Queries the driver for custom driver loading support.
* @return `true` if the device supports loading custom drivers, `false` otherwise
*/
external fun supportsCustomDriverLoading(): Boolean
/**
* Queries the driver for manual max clock forcing support
*/
external fun supportsForceMaxGpuClocks(): Boolean
/**
* Calls into the driver to force the GPU to run at the maximum possible clock speed
* @param force Whether to enable or disable the forced clocks
*/
external fun forceMaxGpuClocks(enable: Boolean)
/**
* Uninstalls the currently installed custom driver
*/
fun uninstallDriver() {
File(DirectoryInitialization.getExtractedDriverDirectory())
.deleteRecursively()
File(DirectoryInitialization.getExtractedDriverDirectory()).mkdir()
}
fun getInstalledDriverMetadata(): GpuDriverMetadata? {
val metadataFile = File(
DirectoryInitialization.getExtractedDriverDirectory(),
GPU_DRIVER_META_FILE
)
if (!metadataFile.exists()) {
return null;
}
return try {
GpuDriverMetadata.deserialize(metadataFile)
} catch (e: SerializationException) {
null
}
}
/**
* Fetches metadata about the system driver.
* @return A [GpuDriverMetadata] object containing data about the system driver
*/
fun getSystemDriverMetadata(context: Context): GpuDriverMetadata? {
val systemDriverInfo = getSystemDriverInfo()
if (systemDriverInfo.isNullOrEmpty()) {
return null;
}
return GpuDriverMetadata(
name = context.getString(R.string.system_driver),
author = "",
packageVersion = "",
vendor = systemDriverInfo[0],
driverVersion = systemDriverInfo[1],
minApi = 0,
description = context.getString(R.string.system_driver_desc),
libraryName = ""
)
}
/**
* Installs the given driver to the emulator's drivers directory.
* @param stream InputStream of a driver package
* @return The exit status of the installation process
*/
fun installDriver(stream: InputStream): GpuDriverInstallResult {
uninstallDriver()
val driverDir = File(DirectoryInitialization.getExtractedDriverDirectory())
try {
ZipUtils.unzip(stream, driverDir)
} catch (e: Exception) {
e.printStackTrace()
uninstallDriver()
return GpuDriverInstallResult.InvalidArchive
}
// Check that the metadata file exists
val metadataFile = File(driverDir, GPU_DRIVER_META_FILE)
if (!metadataFile.isFile) {
uninstallDriver()
return GpuDriverInstallResult.MissingMetadata
}
// Check that the driver metadata is valid
val driverMetadata = try {
GpuDriverMetadata.deserialize(metadataFile)
} catch (e: SerializationException) {
uninstallDriver()
return GpuDriverInstallResult.InvalidMetadata
}
// Check that the device satisfies the driver's minimum Android version requirements
if (Build.VERSION.SDK_INT < driverMetadata.minApi) {
uninstallDriver()
return GpuDriverInstallResult.UnsupportedAndroidVersion
}
return GpuDriverInstallResult.Success
}
}
}
enum class GpuDriverInstallResult {
Success,
InvalidArchive,
MissingMetadata,
InvalidMetadata,
UnsupportedAndroidVersion,
AlreadyInstalled,
FileNotFound
}

View File

@ -0,0 +1,110 @@
/*
* SPDX-License-Identifier: MPL-2.0
* Copyright © 2022 Skyline Team and Contributors (https://github.com/skyline-emu/)
*/
package org.dolphinemu.dolphinemu.utils
import java.io.*
import java.util.zip.ZipEntry
import java.util.zip.ZipFile
import java.util.zip.ZipInputStream
interface ZipUtils {
companion object {
/**
* Extracts a zip file to the given target directory.
* @exception IOException if unzipping fails for any reason
*/
@Throws(IOException::class)
fun unzip(file : File, targetDirectory : File) {
ZipFile(file).use { zipFile ->
for (zipEntry in zipFile.entries()) {
val destFile = createNewFile(targetDirectory, zipEntry)
// If the zip entry is a file, we need to create its parent directories
val destDirectory : File? = if (zipEntry.isDirectory) destFile else destFile.parentFile
// Create the destination directory
if (destDirectory == null || (!destDirectory.isDirectory && !destDirectory.mkdirs()))
throw FileNotFoundException("Failed to create destination directory: $destDirectory")
// If the entry is a directory we don't need to copy anything
if (zipEntry.isDirectory)
continue
// Copy bytes to destination
try {
zipFile.getInputStream(zipEntry).use { inputStream ->
destFile.outputStream().use { outputStream ->
inputStream.copyTo(outputStream)
}
}
} catch (e : IOException) {
if (destFile.exists())
destFile.delete()
throw e
}
}
}
}
/**
* Extracts a zip file from the given stream to the given target directory.
*
* This method is ~5x slower than [unzip], as [ZipInputStream] uses a fixed `512` bytes buffer for inflation,
* instead of `8192` bytes or more used by input streams returned by [ZipFile].
* This results in ~8x the amount of JNI calls, producing an increased number of array bounds checking, which kills performance.
* Usage of this method is discouraged when possible, [unzip] should be used instead.
* Nevertheless, it's the only option when extracting zips obtained from content URIs, as a [File] object cannot be obtained from them.
* @exception IOException if unzipping fails for any reason
*/
@Throws(IOException::class)
fun unzip(stream : InputStream, targetDirectory : File) {
ZipInputStream(BufferedInputStream(stream)).use { zis ->
do {
// Get the next entry, break if we've reached the end
val zipEntry = zis.nextEntry ?: break
val destFile = createNewFile(targetDirectory, zipEntry)
// If the zip entry is a file, we need to create its parent directories
val destDirectory : File? = if (zipEntry.isDirectory) destFile else destFile.parentFile
// Create the destination directory
if (destDirectory == null || (!destDirectory.isDirectory && !destDirectory.mkdirs()))
throw FileNotFoundException("Failed to create destination directory: $destDirectory")
// If the entry is a directory we don't need to copy anything
if (zipEntry.isDirectory)
continue
// Copy bytes to destination
try {
BufferedOutputStream(destFile.outputStream()).use { zis.copyTo(it) }
} catch (e : IOException) {
if (destFile.exists())
destFile.delete()
throw e
}
} while (true)
}
}
/**
* Safely creates a new destination file where the given zip entry will be extracted to.
*
* @exception IOException if the file was being created outside of the target directory
* **see:** [Zip Slip](https://github.com/snyk/zip-slip-vulnerability)
*/
@Throws(IOException::class)
private fun createNewFile(destinationDir : File, zipEntry : ZipEntry) : File {
val destFile = File(destinationDir, zipEntry.name)
val destDirPath = destinationDir.canonicalPath
val destFilePath = destFile.canonicalPath
if (!destFilePath.startsWith(destDirPath + File.separator))
throw IOException("Entry is outside of the target dir: " + zipEntry.name)
return destFile
}
}
}

View File

@ -845,14 +845,19 @@ It can efficiently compress both junk data and encrypted Wii data.
<string name="system_driver_desc">The GPU driver that is part of the OS.</string>
<!-- Custom GPU drivers -->
<string name="gpu_driver_submenu">GPU driver</string>
<string name="gpu_driver_dialog_title">Select the GPU driver for Dolphin</string>
<string name="gpu_driver_dialog_system">Default</string>
<string name="gpu_driver_dialog_install">Install driver</string>
<string name="gpu_driver_dialog_uninstall_done">Successfully switched to the system driver</string>
<string name="gpu_driver_submenu">GPU Driver</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>
<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 installed.</string>
<string name="gpu_driver_install_file_not_found">The selected file could not be found or accessed.</string>
<!-- Emulated USB Devices -->
<string name="emulated_usb_devices">Emulated USB Devices</string>