diff options
author | Charles Lombardo <clombardo169@gmail.com> | 2023-09-29 01:58:03 -0400 |
---|---|---|
committer | Charles Lombardo <clombardo169@gmail.com> | 2023-10-12 17:17:21 -0400 |
commit | a5fb9de6faf4c75d231f414949306459de8b0926 (patch) | |
tree | 8c37d35773467e9f40e386d0edd1fbb16e15dec3 /src | |
parent | 26f9d1f1223ad88d42383de75abfd3226cf03a2e (diff) |
android: Add GPU driver management fragment
Implements a GPU driver manager that saves all drivers to the user data directory and asynchronously installs drivers when they're needed.
Diffstat (limited to 'src')
32 files changed, 1010 insertions, 263 deletions
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/DriverAdapter.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/DriverAdapter.kt new file mode 100644 index 000000000..0e818cab9 --- /dev/null +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/DriverAdapter.kt @@ -0,0 +1,117 @@ +// SPDX-FileCopyrightText: 2023 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +package org.yuzu.yuzu_emu.adapters + +import android.text.TextUtils +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.recyclerview.widget.AsyncDifferConfig +import androidx.recyclerview.widget.DiffUtil +import androidx.recyclerview.widget.ListAdapter +import androidx.recyclerview.widget.RecyclerView +import org.yuzu.yuzu_emu.R +import org.yuzu.yuzu_emu.databinding.CardDriverOptionBinding +import org.yuzu.yuzu_emu.model.DriverViewModel +import org.yuzu.yuzu_emu.utils.GpuDriverHelper +import org.yuzu.yuzu_emu.utils.GpuDriverMetadata + +class DriverAdapter(private val driverViewModel: DriverViewModel) : + ListAdapter<Pair<String, GpuDriverMetadata>, DriverAdapter.DriverViewHolder>( + AsyncDifferConfig.Builder(DiffCallback()).build() + ) { + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): DriverViewHolder { + val binding = + CardDriverOptionBinding.inflate(LayoutInflater.from(parent.context), parent, false) + return DriverViewHolder(binding) + } + + override fun getItemCount(): Int = currentList.size + + override fun onBindViewHolder(holder: DriverViewHolder, position: Int) = + holder.bind(currentList[position]) + + private fun onSelectDriver(position: Int) { + driverViewModel.setSelectedDriverIndex(position) + notifyItemChanged(driverViewModel.previouslySelectedDriver) + notifyItemChanged(driverViewModel.selectedDriver) + } + + private fun onDeleteDriver(driverData: Pair<String, GpuDriverMetadata>, position: Int) { + if (driverViewModel.selectedDriver > position) { + driverViewModel.setSelectedDriverIndex(driverViewModel.selectedDriver - 1) + } + if (GpuDriverHelper.customDriverData == driverData.second) { + driverViewModel.setSelectedDriverIndex(0) + } + driverViewModel.driversToDelete.add(driverData.first) + driverViewModel.removeDriver(driverData) + notifyItemRemoved(position) + notifyItemChanged(driverViewModel.selectedDriver) + } + + inner class DriverViewHolder(val binding: CardDriverOptionBinding) : + RecyclerView.ViewHolder(binding.root) { + private lateinit var driverData: Pair<String, GpuDriverMetadata> + + fun bind(driverData: Pair<String, GpuDriverMetadata>) { + this.driverData = driverData + val driver = driverData.second + + binding.apply { + radioButton.isChecked = driverViewModel.selectedDriver == bindingAdapterPosition + root.setOnClickListener { + onSelectDriver(bindingAdapterPosition) + } + buttonDelete.setOnClickListener { + onDeleteDriver(driverData, bindingAdapterPosition) + } + + // Delay marquee by 3s + title.postDelayed( + { + title.isSelected = true + title.ellipsize = TextUtils.TruncateAt.MARQUEE + version.isSelected = true + version.ellipsize = TextUtils.TruncateAt.MARQUEE + description.isSelected = true + description.ellipsize = TextUtils.TruncateAt.MARQUEE + }, + 3000 + ) + if (driver.name == null) { + title.setText(R.string.system_gpu_driver) + description.text = "" + version.text = "" + version.visibility = View.GONE + description.visibility = View.GONE + buttonDelete.visibility = View.GONE + } else { + title.text = driver.name + version.text = driver.version + description.text = driver.description + version.visibility = View.VISIBLE + description.visibility = View.VISIBLE + buttonDelete.visibility = View.VISIBLE + } + } + } + } + + private class DiffCallback : DiffUtil.ItemCallback<Pair<String, GpuDriverMetadata>>() { + override fun areItemsTheSame( + oldItem: Pair<String, GpuDriverMetadata>, + newItem: Pair<String, GpuDriverMetadata> + ): Boolean { + return oldItem.first == newItem.first + } + + override fun areContentsTheSame( + oldItem: Pair<String, GpuDriverMetadata>, + newItem: Pair<String, GpuDriverMetadata> + ): Boolean { + return oldItem.second == newItem.second + } + } +} diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/DriverManagerFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/DriverManagerFragment.kt new file mode 100644 index 000000000..10b1d3547 --- /dev/null +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/DriverManagerFragment.kt @@ -0,0 +1,185 @@ +// SPDX-FileCopyrightText: 2023 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +package org.yuzu.yuzu_emu.fragments + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.activity.result.contract.ActivityResultContracts +import androidx.core.view.ViewCompat +import androidx.core.view.WindowInsetsCompat +import androidx.core.view.updatePadding +import androidx.fragment.app.Fragment +import androidx.fragment.app.activityViewModels +import androidx.lifecycle.lifecycleScope +import androidx.navigation.findNavController +import androidx.recyclerview.widget.GridLayoutManager +import com.google.android.material.transition.MaterialSharedAxis +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.launch +import org.yuzu.yuzu_emu.R +import org.yuzu.yuzu_emu.adapters.DriverAdapter +import org.yuzu.yuzu_emu.databinding.FragmentDriverManagerBinding +import org.yuzu.yuzu_emu.model.DriverViewModel +import org.yuzu.yuzu_emu.model.HomeViewModel +import org.yuzu.yuzu_emu.utils.FileUtil +import org.yuzu.yuzu_emu.utils.GpuDriverHelper +import java.io.IOException + +class DriverManagerFragment : Fragment() { + private var _binding: FragmentDriverManagerBinding? = null + private val binding get() = _binding!! + + private val homeViewModel: HomeViewModel by activityViewModels() + private val driverViewModel: DriverViewModel by activityViewModels() + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + enterTransition = MaterialSharedAxis(MaterialSharedAxis.X, true) + returnTransition = MaterialSharedAxis(MaterialSharedAxis.X, false) + reenterTransition = MaterialSharedAxis(MaterialSharedAxis.X, false) + } + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + _binding = FragmentDriverManagerBinding.inflate(inflater) + return binding.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + homeViewModel.setNavigationVisibility(visible = false, animated = true) + homeViewModel.setStatusBarShadeVisibility(visible = false) + + if (!driverViewModel.isInteractionAllowed) { + DriversLoadingDialogFragment().show( + childFragmentManager, + DriversLoadingDialogFragment.TAG + ) + } + + binding.toolbarDrivers.setNavigationOnClickListener { + binding.root.findNavController().popBackStack() + } + + binding.buttonInstall.setOnClickListener { + getDriver.launch(arrayOf("application/zip")) + } + + binding.listDrivers.apply { + layoutManager = GridLayoutManager( + requireContext(), + resources.getInteger(R.integer.grid_columns) + ) + adapter = DriverAdapter(driverViewModel) + } + + viewLifecycleOwner.lifecycleScope.apply { + launch { + driverViewModel.driverList.collectLatest { + (binding.listDrivers.adapter as DriverAdapter).submitList(it) + } + } + launch { + driverViewModel.newDriverInstalled.collect { + if (_binding != null && it) { + (binding.listDrivers.adapter as DriverAdapter).apply { + notifyItemChanged(driverViewModel.previouslySelectedDriver) + notifyItemChanged(driverViewModel.selectedDriver) + driverViewModel.setNewDriverInstalled(false) + } + } + } + } + } + + setInsets() + } + + // Start installing requested driver + override fun onStop() { + super.onStop() + driverViewModel.onCloseDriverManager() + } + + private fun setInsets() = + ViewCompat.setOnApplyWindowInsetsListener( + binding.root + ) { _: View, windowInsets: WindowInsetsCompat -> + val barInsets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars()) + val cutoutInsets = windowInsets.getInsets(WindowInsetsCompat.Type.displayCutout()) + + val leftInsets = barInsets.left + cutoutInsets.left + val rightInsets = barInsets.right + cutoutInsets.right + + val mlpAppBar = binding.toolbarDrivers.layoutParams as ViewGroup.MarginLayoutParams + mlpAppBar.leftMargin = leftInsets + mlpAppBar.rightMargin = rightInsets + binding.toolbarDrivers.layoutParams = mlpAppBar + + val mlplistDrivers = binding.listDrivers.layoutParams as ViewGroup.MarginLayoutParams + mlplistDrivers.leftMargin = leftInsets + mlplistDrivers.rightMargin = rightInsets + binding.listDrivers.layoutParams = mlplistDrivers + + val fabSpacing = resources.getDimensionPixelSize(R.dimen.spacing_fab) + val mlpFab = + binding.buttonInstall.layoutParams as ViewGroup.MarginLayoutParams + mlpFab.leftMargin = leftInsets + fabSpacing + mlpFab.rightMargin = rightInsets + fabSpacing + mlpFab.bottomMargin = barInsets.bottom + fabSpacing + binding.buttonInstall.layoutParams = mlpFab + + binding.listDrivers.updatePadding( + bottom = barInsets.bottom + + resources.getDimensionPixelSize(R.dimen.spacing_bottom_list_fab) + ) + + windowInsets + } + + private val getDriver = + registerForActivityResult(ActivityResultContracts.OpenDocument()) { result -> + if (result == null) { + return@registerForActivityResult + } + + IndeterminateProgressDialogFragment.newInstance( + requireActivity(), + R.string.installing_driver, + false + ) { + // Ignore file exceptions when a user selects an invalid zip + try { + GpuDriverHelper.copyDriverToInternalStorage(result) + } catch (_: IOException) { + return@newInstance getString(R.string.select_gpu_driver_error) + } + + val driverData = GpuDriverHelper.customDriverData + if (driverData.name == null) { + return@newInstance getString(R.string.select_gpu_driver_error) + } + + val driverInList = + driverViewModel.driverList.value.firstOrNull { it.second == driverData } + if (driverInList != null) { + return@newInstance getString(R.string.driver_already_installed) + } else { + driverViewModel.addDriver( + Pair( + "${GpuDriverHelper.driverStoragePath}/${FileUtil.getFilename(result)}", + driverData + ) + ) + driverViewModel.setNewDriverInstalled(true) + } + return@newInstance Any() + }.show(childFragmentManager, IndeterminateProgressDialogFragment.TAG) + } +} diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/DriversLoadingDialogFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/DriversLoadingDialogFragment.kt new file mode 100644 index 000000000..f8c34346a --- /dev/null +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/DriversLoadingDialogFragment.kt @@ -0,0 +1,75 @@ +// SPDX-FileCopyrightText: 2023 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +package org.yuzu.yuzu_emu.fragments + +import android.app.Dialog +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.fragment.app.DialogFragment +import androidx.fragment.app.activityViewModels +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.lifecycleScope +import androidx.lifecycle.repeatOnLifecycle +import com.google.android.material.dialog.MaterialAlertDialogBuilder +import kotlinx.coroutines.launch +import org.yuzu.yuzu_emu.R +import org.yuzu.yuzu_emu.databinding.DialogProgressBarBinding +import org.yuzu.yuzu_emu.model.DriverViewModel + +class DriversLoadingDialogFragment : DialogFragment() { + private val driverViewModel: DriverViewModel by activityViewModels() + + private lateinit var binding: DialogProgressBarBinding + + override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { + binding = DialogProgressBarBinding.inflate(layoutInflater) + binding.progressBar.isIndeterminate = true + + isCancelable = false + + return MaterialAlertDialogBuilder(requireContext()) + .setTitle(R.string.loading) + .setView(binding.root) + .create() + } + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View = binding.root + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + viewLifecycleOwner.lifecycleScope.apply { + launch { + repeatOnLifecycle(Lifecycle.State.RESUMED) { + driverViewModel.areDriversLoading.collect { checkForDismiss() } + } + } + launch { + repeatOnLifecycle(Lifecycle.State.RESUMED) { + driverViewModel.isDriverReady.collect { checkForDismiss() } + } + } + launch { + repeatOnLifecycle(Lifecycle.State.RESUMED) { + driverViewModel.isDeletingDrivers.collect { checkForDismiss() } + } + } + } + } + + private fun checkForDismiss() { + if (driverViewModel.isInteractionAllowed) { + dismiss() + } + } + + companion object { + const val TAG = "DriversLoadingDialogFragment" + } +} diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/EmulationFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/EmulationFragment.kt index e6ad2aa77..598a9d42b 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/EmulationFragment.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/EmulationFragment.kt @@ -39,6 +39,7 @@ import androidx.window.layout.WindowLayoutInfo import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.slider.Slider import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.launch import org.yuzu.yuzu_emu.HomeNavigationDirections @@ -50,6 +51,7 @@ import org.yuzu.yuzu_emu.databinding.DialogOverlayAdjustBinding import org.yuzu.yuzu_emu.databinding.FragmentEmulationBinding import org.yuzu.yuzu_emu.features.settings.model.IntSetting import org.yuzu.yuzu_emu.features.settings.model.Settings +import org.yuzu.yuzu_emu.model.DriverViewModel import org.yuzu.yuzu_emu.model.Game import org.yuzu.yuzu_emu.model.EmulationViewModel import org.yuzu.yuzu_emu.overlay.InputOverlay @@ -70,6 +72,7 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback { private lateinit var game: Game private val emulationViewModel: EmulationViewModel by activityViewModels() + private val driverViewModel: DriverViewModel by activityViewModels() private var isInFoldableLayout = false @@ -299,6 +302,21 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback { } } } + launch { + repeatOnLifecycle(Lifecycle.State.RESUMED) { + driverViewModel.isDriverReady.collect { + if (it && !emulationState.isRunning) { + if (!DirectoryInitialization.areDirectoriesReady) { + DirectoryInitialization.start() + } + + updateScreenLayout() + + emulationState.run(emulationActivity!!.isActivityRecreated) + } + } + } + } } } @@ -332,17 +350,6 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback { } } - override fun onResume() { - super.onResume() - if (!DirectoryInitialization.areDirectoriesReady) { - DirectoryInitialization.start() - } - - updateScreenLayout() - - emulationState.run(emulationActivity!!.isActivityRecreated) - } - override fun onPause() { if (emulationState.isRunning && emulationActivity?.isInPictureInPictureMode != true) { emulationState.pause() diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/HomeSettingsFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/HomeSettingsFragment.kt index 18857db2d..fd9785075 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/HomeSettingsFragment.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/HomeSettingsFragment.kt @@ -5,7 +5,6 @@ package org.yuzu.yuzu_emu.fragments import android.Manifest import android.content.ActivityNotFoundException -import android.content.DialogInterface import android.content.Intent import android.content.pm.PackageManager import android.os.Bundle @@ -28,7 +27,6 @@ import androidx.fragment.app.activityViewModels import androidx.navigation.findNavController import androidx.navigation.fragment.findNavController import androidx.recyclerview.widget.LinearLayoutManager -import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.transition.MaterialSharedAxis import org.yuzu.yuzu_emu.BuildConfig import org.yuzu.yuzu_emu.HomeNavigationDirections @@ -37,6 +35,7 @@ import org.yuzu.yuzu_emu.adapters.HomeSettingAdapter import org.yuzu.yuzu_emu.databinding.FragmentHomeSettingsBinding import org.yuzu.yuzu_emu.features.DocumentProvider import org.yuzu.yuzu_emu.features.settings.model.Settings +import org.yuzu.yuzu_emu.model.DriverViewModel import org.yuzu.yuzu_emu.model.HomeSetting import org.yuzu.yuzu_emu.model.HomeViewModel import org.yuzu.yuzu_emu.ui.main.MainActivity @@ -50,6 +49,7 @@ class HomeSettingsFragment : Fragment() { private lateinit var mainActivity: MainActivity private val homeViewModel: HomeViewModel by activityViewModels() + private val driverViewModel: DriverViewModel by activityViewModels() override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -107,13 +107,17 @@ class HomeSettingsFragment : Fragment() { ) add( HomeSetting( - R.string.install_gpu_driver, + R.string.gpu_driver_manager, R.string.install_gpu_driver_description, - R.drawable.ic_exit, - { driverInstaller() }, + R.drawable.ic_build, + { + binding.root.findNavController() + .navigate(R.id.action_homeSettingsFragment_to_driverManagerFragment) + }, { GpuDriverHelper.supportsCustomDriverLoading() }, R.string.custom_driver_not_supported, - R.string.custom_driver_not_supported_description + R.string.custom_driver_not_supported_description, + driverViewModel.selectedDriverMetadata ) ) add( @@ -292,31 +296,6 @@ class HomeSettingsFragment : Fragment() { } } - private fun driverInstaller() { - // Get the driver name for the dialog message. - var driverName = GpuDriverHelper.customDriverName - if (driverName == null) { - driverName = getString(R.string.system_gpu_driver) - } - - MaterialAlertDialogBuilder(requireContext()) - .setTitle(getString(R.string.select_gpu_driver_title)) - .setMessage(driverName) - .setNegativeButton(android.R.string.cancel, null) - .setNeutralButton(R.string.select_gpu_driver_default) { _: DialogInterface?, _: Int -> - GpuDriverHelper.installDefaultDriver() - Toast.makeText( - requireContext(), - R.string.select_gpu_driver_use_default, - Toast.LENGTH_SHORT - ).show() - } - .setPositiveButton(R.string.select_gpu_driver_install) { _: DialogInterface?, _: Int -> - mainActivity.getDriver.launch(arrayOf("application/zip")) - } - .show() - } - private fun shareLog() { val file = DocumentFile.fromSingleUri( mainActivity, diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/IndeterminateProgressDialogFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/IndeterminateProgressDialogFragment.kt index f128deda8..7e467814d 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/IndeterminateProgressDialogFragment.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/IndeterminateProgressDialogFragment.kt @@ -10,8 +10,8 @@ import android.view.View import android.view.ViewGroup import android.widget.Toast import androidx.appcompat.app.AlertDialog -import androidx.appcompat.app.AppCompatActivity import androidx.fragment.app.DialogFragment +import androidx.fragment.app.FragmentActivity import androidx.fragment.app.activityViewModels import androidx.lifecycle.Lifecycle import androidx.lifecycle.ViewModelProvider @@ -78,6 +78,10 @@ class IndeterminateProgressDialogFragment : DialogFragment() { requireActivity().supportFragmentManager, MessageDialogFragment.TAG ) + + else -> { + // Do nothing + } } taskViewModel.clear() } @@ -115,7 +119,7 @@ class IndeterminateProgressDialogFragment : DialogFragment() { private const val CANCELLABLE = "Cancellable" fun newInstance( - activity: AppCompatActivity, + activity: FragmentActivity, titleId: Int, cancellable: Boolean = false, task: () -> Any diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/DriverViewModel.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/DriverViewModel.kt new file mode 100644 index 000000000..62945ad65 --- /dev/null +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/DriverViewModel.kt @@ -0,0 +1,158 @@ +// SPDX-FileCopyrightText: 2023 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +package org.yuzu.yuzu_emu.model + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import org.yuzu.yuzu_emu.R +import org.yuzu.yuzu_emu.YuzuApplication +import org.yuzu.yuzu_emu.utils.FileUtil +import org.yuzu.yuzu_emu.utils.GpuDriverHelper +import org.yuzu.yuzu_emu.utils.GpuDriverMetadata +import java.io.BufferedOutputStream +import java.io.File + +class DriverViewModel : ViewModel() { + private val _areDriversLoading = MutableStateFlow(false) + val areDriversLoading: StateFlow<Boolean> get() = _areDriversLoading + + private val _isDriverReady = MutableStateFlow(true) + val isDriverReady: StateFlow<Boolean> get() = _isDriverReady + + private val _isDeletingDrivers = MutableStateFlow(false) + val isDeletingDrivers: StateFlow<Boolean> get() = _isDeletingDrivers + + private val _driverList = MutableStateFlow(mutableListOf<Pair<String, GpuDriverMetadata>>()) + val driverList: StateFlow<MutableList<Pair<String, GpuDriverMetadata>>> get() = _driverList + + var previouslySelectedDriver = 0 + var selectedDriver = -1 + + private val _selectedDriverMetadata = + MutableStateFlow( + GpuDriverHelper.customDriverData.name + ?: YuzuApplication.appContext.getString(R.string.system_gpu_driver) + ) + val selectedDriverMetadata: StateFlow<String> get() = _selectedDriverMetadata + + private val _newDriverInstalled = MutableStateFlow(false) + val newDriverInstalled: StateFlow<Boolean> get() = _newDriverInstalled + + val driversToDelete = mutableListOf<String>() + + val isInteractionAllowed + get() = !areDriversLoading.value && isDriverReady.value && !isDeletingDrivers.value + + init { + _areDriversLoading.value = true + viewModelScope.launch { + withContext(Dispatchers.IO) { + val drivers = GpuDriverHelper.getDrivers() + val currentDriverMetadata = GpuDriverHelper.customDriverData + for (i in drivers.indices) { + if (drivers[i].second == currentDriverMetadata) { + setSelectedDriverIndex(i) + break + } + } + + // If a user had installed a driver before the manager was implemented, this zips + // the installed driver to UserData/gpu_drivers/CustomDriver.zip so that it can + // be indexed and exported as expected. + if (selectedDriver == -1) { + val driverToSave = + File(GpuDriverHelper.driverStoragePath, "CustomDriver.zip") + driverToSave.createNewFile() + FileUtil.zipFromInternalStorage( + File(GpuDriverHelper.driverInstallationPath!!), + GpuDriverHelper.driverInstallationPath!!, + BufferedOutputStream(driverToSave.outputStream()) + ) + drivers.add(Pair(driverToSave.path, currentDriverMetadata)) + setSelectedDriverIndex(drivers.size - 1) + } + + _driverList.value = drivers + _areDriversLoading.value = false + } + } + } + + fun setSelectedDriverIndex(value: Int) { + if (selectedDriver != -1) { + previouslySelectedDriver = selectedDriver + } + selectedDriver = value + } + + fun setNewDriverInstalled(value: Boolean) { + _newDriverInstalled.value = value + } + + fun addDriver(driverData: Pair<String, GpuDriverMetadata>) { + val driverIndex = _driverList.value.indexOfFirst { it == driverData } + if (driverIndex == -1) { + setSelectedDriverIndex(_driverList.value.size) + _driverList.value.add(driverData) + _selectedDriverMetadata.value = driverData.second.name + ?: YuzuApplication.appContext.getString(R.string.system_gpu_driver) + } else { + setSelectedDriverIndex(driverIndex) + } + } + + fun removeDriver(driverData: Pair<String, GpuDriverMetadata>) { + _driverList.value.remove(driverData) + } + + fun onCloseDriverManager() { + _isDeletingDrivers.value = true + viewModelScope.launch { + withContext(Dispatchers.IO) { + driversToDelete.forEach { + val driver = File(it) + if (driver.exists()) { + driver.delete() + } + } + driversToDelete.clear() + _isDeletingDrivers.value = false + } + } + + if (GpuDriverHelper.customDriverData == driverList.value[selectedDriver].second) { + return + } + + _isDriverReady.value = false + viewModelScope.launch { + withContext(Dispatchers.IO) { + if (selectedDriver == 0) { + GpuDriverHelper.installDefaultDriver() + setDriverReady() + return@withContext + } + + val driverToInstall = File(driverList.value[selectedDriver].first) + if (driverToInstall.exists()) { + GpuDriverHelper.installCustomDriver(driverToInstall) + } else { + GpuDriverHelper.installDefaultDriver() + } + setDriverReady() + } + } + } + + private fun setDriverReady() { + _isDriverReady.value = true + _selectedDriverMetadata.value = GpuDriverHelper.customDriverData.name + ?: YuzuApplication.appContext.getString(R.string.system_gpu_driver) + } +} diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/main/MainActivity.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/main/MainActivity.kt index ac96c8207..233aa4101 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/main/MainActivity.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/main/MainActivity.kt @@ -29,12 +29,10 @@ import androidx.navigation.fragment.NavHostFragment import androidx.navigation.ui.setupWithNavController import androidx.preference.PreferenceManager import com.google.android.material.color.MaterialColors -import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.navigation.NavigationBarView import kotlinx.coroutines.CoroutineScope import java.io.File import java.io.FilenameFilter -import java.io.IOException import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext @@ -43,7 +41,6 @@ import org.yuzu.yuzu_emu.NativeLibrary import org.yuzu.yuzu_emu.R import org.yuzu.yuzu_emu.activities.EmulationActivity import org.yuzu.yuzu_emu.databinding.ActivityMainBinding -import org.yuzu.yuzu_emu.databinding.DialogProgressBarBinding import org.yuzu.yuzu_emu.features.DocumentProvider import org.yuzu.yuzu_emu.features.settings.model.Settings import org.yuzu.yuzu_emu.fragments.IndeterminateProgressDialogFragment @@ -346,7 +343,7 @@ class MainActivity : AppCompatActivity(), ThemeProvider { result, dstPath, "prod.keys" - ) + ) != null ) { if (NativeLibrary.reloadKeys()) { Toast.makeText( @@ -448,7 +445,7 @@ class MainActivity : AppCompatActivity(), ThemeProvider { result, dstPath, "key_retail.bin" - ) + ) != null ) { if (NativeLibrary.reloadKeys()) { Toast.makeText( @@ -467,59 +464,6 @@ class MainActivity : AppCompatActivity(), ThemeProvider { } } - val getDriver = - registerForActivityResult(ActivityResultContracts.OpenDocument()) { result -> - if (result == null) { - return@registerForActivityResult - } - - val takeFlags = - Intent.FLAG_GRANT_WRITE_URI_PERMISSION or Intent.FLAG_GRANT_READ_URI_PERMISSION - contentResolver.takePersistableUriPermission( - result, - takeFlags - ) - - val progressBinding = DialogProgressBarBinding.inflate(layoutInflater) - progressBinding.progressBar.isIndeterminate = true - val installationDialog = MaterialAlertDialogBuilder(this) - .setTitle(R.string.installing_driver) - .setView(progressBinding.root) - .show() - - lifecycleScope.launch { - withContext(Dispatchers.IO) { - // Ignore file exceptions when a user selects an invalid zip - try { - GpuDriverHelper.installCustomDriver(result) - } catch (_: IOException) { - } - - withContext(Dispatchers.Main) { - installationDialog.dismiss() - - val driverData = GpuDriverHelper.customDriverData - if (driverData.name != null) { - Toast.makeText( - applicationContext, - getString( - R.string.select_gpu_driver_install_success, - driverData.name - ), - Toast.LENGTH_SHORT - ).show() - } else { - Toast.makeText( - applicationContext, - R.string.select_gpu_driver_error, - Toast.LENGTH_LONG - ).show() - } - } - } - } - } - val installGameUpdate = registerForActivityResult( ActivityResultContracts.OpenMultipleDocuments() ) { documents: List<Uri> -> diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/FileUtil.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/FileUtil.kt index a5f89bba6..5ee74a52c 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/FileUtil.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/FileUtil.kt @@ -10,7 +10,6 @@ import androidx.documentfile.provider.DocumentFile import kotlinx.coroutines.flow.StateFlow import java.io.BufferedInputStream import java.io.File -import java.io.FileOutputStream import java.io.IOException import java.io.InputStream import java.net.URLDecoder @@ -20,6 +19,8 @@ import org.yuzu.yuzu_emu.YuzuApplication import org.yuzu.yuzu_emu.model.MinimalDocumentFile import org.yuzu.yuzu_emu.model.TaskState import java.io.BufferedOutputStream +import java.lang.NullPointerException +import java.nio.charset.StandardCharsets import java.util.zip.ZipOutputStream object FileUtil { @@ -243,43 +244,38 @@ object FileUtil { return size } + /** + * Creates an input stream with a given [Uri] and copies its data to the given path. This will + * overwrite any pre-existing files. + * + * @param sourceUri The [Uri] to copy data from + * @param destinationParentPath Destination directory + * @param destinationFilename Optionally renames the file once copied + */ fun copyUriToInternalStorage( - sourceUri: Uri?, + sourceUri: Uri, destinationParentPath: String, - destinationFilename: String - ): Boolean { - var input: InputStream? = null - var output: FileOutputStream? = null + destinationFilename: String = "" + ): File? = try { - input = context.contentResolver.openInputStream(sourceUri!!) - output = FileOutputStream("$destinationParentPath/$destinationFilename") - val buffer = ByteArray(1024) - var len: Int - while (input!!.read(buffer).also { len = it } != -1) { - output.write(buffer, 0, len) - } - output.flush() - return true - } catch (e: Exception) { - Log.error("[FileUtil]: Cannot copy file, error: " + e.message) - } finally { - if (input != null) { - try { - input.close() - } catch (e: IOException) { - Log.error("[FileUtil]: Cannot close input file, error: " + e.message) - } + val fileName = + if (destinationFilename == "") getFilename(sourceUri) else "/$destinationFilename" + val inputStream = context.contentResolver.openInputStream(sourceUri)!! + + val destinationFile = File("$destinationParentPath$fileName") + if (destinationFile.exists()) { + destinationFile.delete() } - if (output != null) { - try { - output.close() - } catch (e: IOException) { - Log.error("[FileUtil]: Cannot close output file, error: " + e.message) - } + + destinationFile.outputStream().use { fos -> + inputStream.use { it.copyTo(fos) } } + destinationFile + } catch (e: IOException) { + null + } catch (e: NullPointerException) { + null } - return false - } /** * Extracts the given zip file into the given directory. @@ -365,4 +361,12 @@ object FileUtil { return fileName.substring(fileName.lastIndexOf(".") + 1) .lowercase() } + + @Throws(IOException::class) + fun getStringFromFile(file: File): String = + String(file.readBytes(), StandardCharsets.UTF_8) + + @Throws(IOException::class) + fun getStringFromInputStream(stream: InputStream): String = + String(stream.readBytes(), StandardCharsets.UTF_8) } 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 296a8f1cf..f6882ce6c 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 @@ -3,65 +3,32 @@ package org.yuzu.yuzu_emu.utils -import android.content.Context import android.net.Uri +import android.os.Build import java.io.BufferedInputStream import java.io.File -import java.io.FileInputStream -import java.io.FileOutputStream import java.io.IOException -import java.util.zip.ZipInputStream import org.yuzu.yuzu_emu.NativeLibrary -import org.yuzu.yuzu_emu.utils.FileUtil.copyUriToInternalStorage import org.yuzu.yuzu_emu.YuzuApplication +import java.util.zip.ZipException +import java.util.zip.ZipFile object GpuDriverHelper { private const val META_JSON_FILENAME = "meta.json" - private const val DRIVER_INTERNAL_FILENAME = "gpu_driver.zip" private var fileRedirectionPath: String? = null - private var driverInstallationPath: String? = null + var driverInstallationPath: String? = null private var hookLibPath: String? = null - @Throws(IOException::class) - private fun unzip(zipFilePath: String, destDir: String) { - val dir = File(destDir) - - // Create output directory if it doesn't exist - if (!dir.exists()) dir.mkdirs() - - // Unpack the files. - val inputStream = FileInputStream(zipFilePath) - val zis = ZipInputStream(BufferedInputStream(inputStream)) - val buffer = ByteArray(1024) - var ze = zis.nextEntry - while (ze != null) { - val newFile = File(destDir, ze.name) - val canonicalPath = newFile.canonicalPath - if (!canonicalPath.startsWith(destDir + ze.name)) { - throw SecurityException("Zip file attempted path traversal! " + ze.name) - } - - newFile.parentFile!!.mkdirs() - val fos = FileOutputStream(newFile) - var len: Int - while (zis.read(buffer).also { len = it } > 0) { - fos.write(buffer, 0, len) - } - fos.close() - zis.closeEntry() - ze = zis.nextEntry - } - zis.closeEntry() - } + val driverStoragePath get() = DirectoryInitialization.userDirectory!! + "/gpu_drivers/" - fun initializeDriverParameters(context: Context) { + fun initializeDriverParameters() { try { // Initialize the file redirection directory. - fileRedirectionPath = - context.getExternalFilesDir(null)!!.canonicalPath + "/gpu/vk_file_redirect/" + fileRedirectionPath = YuzuApplication.appContext + .getExternalFilesDir(null)!!.canonicalPath + "/gpu/vk_file_redirect/" // Initialize the driver installation directory. - driverInstallationPath = context.filesDir.canonicalPath + "/gpu_driver/" + driverInstallationPath = YuzuApplication.appContext .filesDir.canonicalPath + "/gpu_driver/" } catch (e: IOException) { throw RuntimeException(e) @@ -71,69 +38,169 @@ object GpuDriverHelper { initializeDirectories() // Initialize hook libraries directory. - hookLibPath = context.applicationInfo.nativeLibraryDir + "/" hookLibPath = YuzuApplication.appContext.applicationInfo.nativeLibraryDir + "/" // Initialize GPU driver. NativeLibrary.initializeGpuDriver( hookLibPath, driverInstallationPath, - customDriverLibraryName, + customDriverData.libraryName, fileRedirectionPath ) } - fun installDefaultDriver(context: Context) { + fun getDrivers(): MutableList<Pair<String, GpuDriverMetadata>> { + val driverZips = File(driverStoragePath).listFiles() + val drivers: MutableList<Pair<String, GpuDriverMetadata>> = + driverZips + ?.mapNotNull { + val metadata = getMetadataFromZip(it) + metadata.name?.let { _ -> Pair(it.path, metadata) } + } + ?.sortedByDescending { it: Pair<String, GpuDriverMetadata> -> it.second.name } + ?.distinct() + ?.toMutableList() ?: mutableListOf() + + // TODO: Get system driver information + drivers.add(0, Pair("", GpuDriverMetadata())) + return drivers + } + fun installDefaultDriver() { // Removing the installed driver will result in the backend using the default system driver. - val driverInstallationDir = File(driverInstallationPath!!) - deleteRecursive(driverInstallationDir) + File(driverInstallationPath!!).deleteRecursively() + initializeDriverParameters() + } + + fun copyDriverToInternalStorage(driverUri: Uri): Boolean { + // Ensure we have directories. + initializeDirectories() + + // Copy the zip file URI to user data + val copiedFile = + FileUtil.copyUriToInternalStorage(driverUri, driverStoragePath) ?: return false + + // Validate driver + val metadata = getMetadataFromZip(copiedFile) + if (metadata.name == null) { + copiedFile.delete() + return false + } + + if (metadata.minApi > Build.VERSION.SDK_INT) { + copiedFile.delete() + return false + } + return true } - fun installCustomDriver(context: Context, driverPathUri: Uri?) { + /** + * Copies driver zip into user data directory so that it can be exported along with + * other user data and also unzipped into the installation directory + */ + fun installCustomDriver(driverUri: Uri): Boolean { // Revert to system default in the event the specified driver is bad. installDefaultDriver() // Ensure we have directories. initializeDirectories() - // Copy the zip file URI into our private storage. - copyUriToInternalStorage( - context, - driverPathUri, - driverInstallationPath!!, - DRIVER_INTERNAL_FILENAME - ) + // Copy the zip file URI to user data + val copiedFile = + FileUtil.copyUriToInternalStorage(driverUri, driverStoragePath) ?: return false + + // Validate driver + val metadata = getMetadataFromZip(copiedFile) + if (metadata.name == null) { + copiedFile.delete() + return false + } + + if (metadata.minApi > Build.VERSION.SDK_INT) { + copiedFile.delete() + return false + } // Unzip the driver. try { - unzip(driverInstallationPath + DRIVER_INTERNAL_FILENAME, driverInstallationPath!!) + FileUtil.unzipToInternalStorage( + BufferedInputStream(copiedFile.inputStream()), + File(driverInstallationPath!!) + ) } catch (e: SecurityException) { - return + return false } // Initialize the driver parameters. - initializeDriverParameters(context) + initializeDriverParameters() + + return true } - external fun supportsCustomDriverLoading(): Boolean + /** + * Unzips driver into installation directory + */ + fun installCustomDriver(driver: File): Boolean { + // Revert to system default in the event the specified driver is bad. + installDefaultDriver() - // Parse the custom driver metadata to retrieve the name. - val customDriverName: String? - get() { - val metadata = GpuDriverMetadata(driverInstallationPath + META_JSON_FILENAME) - return metadata.name + // Ensure we have directories. + initializeDirectories() + + // Validate driver + val metadata = getMetadataFromZip(driver) + if (metadata.name == null) { + driver.delete() + return false } - // Parse the custom driver metadata to retrieve the library name. - private val customDriverLibraryName: String? - get() { - // Parse the custom driver metadata to retrieve the library name. - val metadata = GpuDriverMetadata(driverInstallationPath + META_JSON_FILENAME) - return metadata.libraryName + // Unzip the driver to the private installation directory + try { + FileUtil.unzipToInternalStorage( + BufferedInputStream(driver.inputStream()), + File(driverInstallationPath!!) + ) + } catch (e: SecurityException) { + return false + } + + // Initialize the driver parameters. + initializeDriverParameters() + + return true + } + + /** + * Takes in a zip file and reads the meta.json file for presentation to the UI + * + * @param driver Zip containing driver and meta.json file + * @return A non-null [GpuDriverMetadata] instance that may have null members + */ + fun getMetadataFromZip(driver: File): GpuDriverMetadata { + try { + ZipFile(driver).use { zf -> + val entries = zf.entries() + while (entries.hasMoreElements()) { + val entry = entries.nextElement() + if (!entry.isDirectory && entry.name.lowercase().contains(".json")) { + zf.getInputStream(entry).use { + return GpuDriverMetadata(it, entry.size) + } + } + } + } + } catch (_: ZipException) { } + return GpuDriverMetadata() + } + + external fun supportsCustomDriverLoading(): Boolean + + // Parse the custom driver metadata to retrieve the name. + val customDriverData: GpuDriverMetadata + get() = GpuDriverMetadata(File(driverInstallationPath + META_JSON_FILENAME)) - private fun initializeDirectories() { + fun initializeDirectories() { // Ensure the file redirection directory exists. val fileRedirectionDir = File(fileRedirectionPath!!) if (!fileRedirectionDir.exists()) { @@ -144,14 +211,10 @@ object GpuDriverHelper { if (!driverInstallationDir.exists()) { driverInstallationDir.mkdirs() } - } - - private fun deleteRecursive(fileOrDirectory: File) { - if (fileOrDirectory.isDirectory) { - for (child in fileOrDirectory.listFiles()!!) { - deleteRecursive(child) - } + // Ensure the driver storage directory exists + val driverStorageDirectory = File(driverStoragePath) + if (!driverStorageDirectory.exists()) { + driverStorageDirectory.mkdirs() } - fileOrDirectory.delete() } } diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/GpuDriverMetadata.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/GpuDriverMetadata.kt index a4e64070a..511a4171a 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/GpuDriverMetadata.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/GpuDriverMetadata.kt @@ -4,29 +4,29 @@ package org.yuzu.yuzu_emu.utils import java.io.IOException -import java.nio.charset.StandardCharsets -import java.nio.file.Files -import java.nio.file.Paths import org.json.JSONException import org.json.JSONObject +import java.io.File +import java.io.InputStream -class GpuDriverMetadata(metadataFilePath: String) { - var name: String? = null - var description: String? = null - var author: String? = null - var vendor: String? = null - var driverVersion: String? = null - var minApi = 0 - var libraryName: String? = null +class GpuDriverMetadata { + /** + * Tries to get driver metadata information from a meta.json [File] + * + * @param metadataFile meta.json file provided with a GPU driver + */ + constructor(metadataFile: File) { + if (metadataFile.length() > MAX_META_SIZE_BYTES) { + return + } - init { try { - val json = JSONObject(getStringFromFile(metadataFilePath)) + val json = JSONObject(FileUtil.getStringFromFile(metadataFile)) name = json.getString("name") description = json.getString("description") author = json.getString("author") vendor = json.getString("vendor") - driverVersion = json.getString("driverVersion") + version = json.getString("driverVersion") minApi = json.getInt("minApi") libraryName = json.getString("libraryName") } catch (e: JSONException) { @@ -36,12 +36,84 @@ class GpuDriverMetadata(metadataFilePath: String) { } } - companion object { - @Throws(IOException::class) - private fun getStringFromFile(filePath: String): String { - val path = Paths.get(filePath) - val bytes = Files.readAllBytes(path) - return String(bytes, StandardCharsets.UTF_8) + /** + * Tries to get driver metadata information from an input stream that's intended to be + * from a zip file + * + * @param metadataStream ZipEntry input stream + * @param size Size of the file in bytes + */ + constructor(metadataStream: InputStream, size: Long) { + if (size > MAX_META_SIZE_BYTES) { + return } + + try { + val json = JSONObject(FileUtil.getStringFromInputStream(metadataStream)) + name = json.getString("name") + description = json.getString("description") + author = json.getString("author") + vendor = json.getString("vendor") + version = json.getString("driverVersion") + minApi = json.getInt("minApi") + libraryName = json.getString("libraryName") + } catch (e: JSONException) { + // JSON is malformed, ignore and treat as unsupported metadata. + } catch (e: IOException) { + // File is inaccessible, ignore and treat as unsupported metadata. + } + } + + /** + * Creates an empty metadata instance + */ + constructor() + + override fun equals(other: Any?): Boolean { + if (other !is GpuDriverMetadata) { + return false + } + + return other.name == name && + other.description == description && + other.author == author && + other.vendor == vendor && + other.version == version && + other.minApi == minApi && + other.libraryName == libraryName + } + + override fun hashCode(): Int { + var result = name?.hashCode() ?: 0 + result = 31 * result + (description?.hashCode() ?: 0) + result = 31 * result + (author?.hashCode() ?: 0) + result = 31 * result + (vendor?.hashCode() ?: 0) + result = 31 * result + (version?.hashCode() ?: 0) + result = 31 * result + minApi + result = 31 * result + (libraryName?.hashCode() ?: 0) + return result + } + + override fun toString(): String = + """ + Name - $name + Description - $description + Author - $author + Vendor - $vendor + Version - $version + Min API - $minApi + Library Name - $libraryName + """.trimMargin().trimIndent() + + var name: String? = null + var description: String? = null + var author: String? = null + var vendor: String? = null + var version: String? = null + var minApi = 0 + var libraryName: String? = null + + companion object { + private const val MAX_META_SIZE_BYTES = 500000 } } diff --git a/src/android/app/src/main/res/drawable/ic_build.xml b/src/android/app/src/main/res/drawable/ic_build.xml new file mode 100644 index 000000000..91d52f1b8 --- /dev/null +++ b/src/android/app/src/main/res/drawable/ic_build.xml @@ -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="?attr/colorControlNormal" + android:pathData="M22.7,19l-9.1,-9.1c0.9,-2.3 0.4,-5 -1.5,-6.9 -2,-2 -5,-2.4 -7.4,-1.3L9,6 6,9 1.6,4.7C0.4,7.1 0.9,10.1 2.9,12.1c1.9,1.9 4.6,2.4 6.9,1.5l9.1,9.1c0.4,0.4 1,0.4 1.4,0l2.3,-2.3c0.5,-0.4 0.5,-1.1 0.1,-1.4z" /> +</vector> diff --git a/src/android/app/src/main/res/drawable/ic_delete.xml b/src/android/app/src/main/res/drawable/ic_delete.xml new file mode 100644 index 000000000..d26a79711 --- /dev/null +++ b/src/android/app/src/main/res/drawable/ic_delete.xml @@ -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="?attr/colorControlNormal" + 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> diff --git a/src/android/app/src/main/res/layout/card_driver_option.xml b/src/android/app/src/main/res/layout/card_driver_option.xml new file mode 100644 index 000000000..1dd9a6d7d --- /dev/null +++ b/src/android/app/src/main/res/layout/card_driver_option.xml @@ -0,0 +1,89 @@ +<?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="?attr/materialCardViewOutlinedStyle" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginHorizontal="16dp" + android:layout_marginVertical="12dp" + android:background="?attr/selectableItemBackground" + android:clickable="true" + android:focusable="true"> + + <LinearLayout + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:orientation="horizontal" + android:layout_gravity="center" + android:padding="16dp"> + + <RadioButton + android:id="@+id/radio_button" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_gravity="center_vertical" + android:clickable="false" + android:checked="false" /> + + <LinearLayout + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_weight="1" + android:orientation="vertical" + android:layout_gravity="center_vertical"> + + <com.google.android.material.textview.MaterialTextView + android:id="@+id/title" + style="@style/TextAppearance.Material3.TitleMedium" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:ellipsize="none" + android:marqueeRepeatLimit="marquee_forever" + android:requiresFadingEdge="horizontal" + android:singleLine="true" + android:textAlignment="viewStart" + tools:text="@string/select_gpu_driver_default" /> + + <com.google.android.material.textview.MaterialTextView + android:id="@+id/version" + style="@style/TextAppearance.Material3.BodyMedium" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginTop="6dp" + android:ellipsize="none" + android:marqueeRepeatLimit="marquee_forever" + android:requiresFadingEdge="horizontal" + android:singleLine="true" + android:textAlignment="viewStart" + tools:text="@string/install_gpu_driver_description" /> + + <com.google.android.material.textview.MaterialTextView + android:id="@+id/description" + style="@style/TextAppearance.Material3.BodyMedium" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginTop="6dp" + android:ellipsize="none" + android:marqueeRepeatLimit="marquee_forever" + android:requiresFadingEdge="horizontal" + android:singleLine="true" + android:textAlignment="viewStart" + tools:text="@string/install_gpu_driver_description" /> + + </LinearLayout> + + <Button + android:id="@+id/button_delete" + style="@style/Widget.Material3.Button.IconButton" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_gravity="center_vertical" + android:contentDescription="@string/delete" + android:tooltipText="@string/delete" + app:icon="@drawable/ic_delete" + app:iconTint="?attr/colorControlNormal" /> + + </LinearLayout> + +</com.google.android.material.card.MaterialCardView> diff --git a/src/android/app/src/main/res/layout/fragment_driver_manager.xml b/src/android/app/src/main/res/layout/fragment_driver_manager.xml new file mode 100644 index 000000000..6cea2d164 --- /dev/null +++ b/src/android/app/src/main/res/layout/fragment_driver_manager.xml @@ -0,0 +1,48 @@ +<?xml version="1.0" encoding="utf-8"?> +<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto" + android:id="@+id/coordinator_licenses" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:background="?attr/colorSurface"> + + <androidx.coordinatorlayout.widget.CoordinatorLayout + android:layout_width="match_parent" + android:layout_height="match_parent"> + + <com.google.android.material.appbar.AppBarLayout + android:id="@+id/appbar_drivers" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:fitsSystemWindows="true" + app:liftOnScrollTargetViewId="@id/list_drivers"> + + <com.google.android.material.appbar.MaterialToolbar + android:id="@+id/toolbar_drivers" + android:layout_width="match_parent" + android:layout_height="?attr/actionBarSize" + app:navigationIcon="@drawable/ic_back" + app:title="@string/gpu_driver_manager" /> + + </com.google.android.material.appbar.AppBarLayout> + + <androidx.recyclerview.widget.RecyclerView + android:id="@+id/list_drivers" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:clipToPadding="false" + app:layout_behavior="@string/appbar_scrolling_view_behavior" /> + + </androidx.coordinatorlayout.widget.CoordinatorLayout> + + <com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton + android:id="@+id/button_install" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_gravity="bottom|end" + android:text="@string/install" + app:icon="@drawable/ic_add" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintEnd_toEndOf="parent" /> + +</androidx.constraintlayout.widget.ConstraintLayout> diff --git a/src/android/app/src/main/res/navigation/home_navigation.xml b/src/android/app/src/main/res/navigation/home_navigation.xml index 2356b802b..82749359d 100644 --- a/src/android/app/src/main/res/navigation/home_navigation.xml +++ b/src/android/app/src/main/res/navigation/home_navigation.xml @@ -22,6 +22,9 @@ <action android:id="@+id/action_homeSettingsFragment_to_installableFragment" app:destination="@id/installableFragment" /> + <action + android:id="@+id/action_homeSettingsFragment_to_driverManagerFragment" + app:destination="@id/driverManagerFragment" /> </fragment> <fragment @@ -95,5 +98,9 @@ android:id="@+id/installableFragment" android:name="org.yuzu.yuzu_emu.fragments.InstallableFragment" android:label="InstallableFragment" /> + <fragment + android:id="@+id/driverManagerFragment" + android:name="org.yuzu.yuzu_emu.fragments.DriverManagerFragment" + android:label="DriverManagerFragment" /> </navigation> diff --git a/src/android/app/src/main/res/values-de/strings.xml b/src/android/app/src/main/res/values-de/strings.xml index dd0f36392..72a47fbdb 100644 --- a/src/android/app/src/main/res/values-de/strings.xml +++ b/src/android/app/src/main/res/values-de/strings.xml @@ -168,9 +168,7 @@ <string name="select_gpu_driver_title">Möchtest du deinen aktuellen GPU-Treiber ersetzen?</string> <string name="select_gpu_driver_install">Installieren</string> <string name="select_gpu_driver_default">Standard</string> - <string name="select_gpu_driver_install_success">%s wurde installiert</string> <string name="select_gpu_driver_use_default">Standard GPU-Treiber wird verwendet</string> - <string name="select_gpu_driver_error">Ungültiger Treiber ausgewählt, Standard-Treiber wird verwendet!</string> <string name="system_gpu_driver">System GPU-Treiber</string> <string name="installing_driver">Treiber wird installiert...</string> diff --git a/src/android/app/src/main/res/values-es/strings.xml b/src/android/app/src/main/res/values-es/strings.xml index d398f862f..e5bdd5889 100644 --- a/src/android/app/src/main/res/values-es/strings.xml +++ b/src/android/app/src/main/res/values-es/strings.xml @@ -171,9 +171,7 @@ <string name="select_gpu_driver_title">¿Quiere reemplazar el driver de GPU actual?</string> <string name="select_gpu_driver_install">Instalar</string> <string name="select_gpu_driver_default">Predeterminado</string> - <string name="select_gpu_driver_install_success">Instalado %s</string> <string name="select_gpu_driver_use_default">Usando el driver de GPU por defecto </string> - <string name="select_gpu_driver_error">¡Driver no válido, utilizando el predeterminado del sistema!</string> <string name="system_gpu_driver">Driver GPU del sistema</string> <string name="installing_driver">Instalando driver...</string> diff --git a/src/android/app/src/main/res/values-fr/strings.xml b/src/android/app/src/main/res/values-fr/strings.xml index a7abd9077..1e02828aa 100644 --- a/src/android/app/src/main/res/values-fr/strings.xml +++ b/src/android/app/src/main/res/values-fr/strings.xml @@ -171,9 +171,7 @@ <string name="select_gpu_driver_title">Souhaitez vous remplacer votre pilote actuel ?</string> <string name="select_gpu_driver_install">Installer</string> <string name="select_gpu_driver_default">Défaut</string> - <string name="select_gpu_driver_install_success">%s Installé</string> <string name="select_gpu_driver_use_default">Utilisation du pilote de GPU par défaut</string> - <string name="select_gpu_driver_error">Pilote non valide sélectionné, utilisation du paramètre par défaut du système !</string> <string name="system_gpu_driver">Pilote du GPU du système</string> <string name="installing_driver">Installation du pilote...</string> diff --git a/src/android/app/src/main/res/values-it/strings.xml b/src/android/app/src/main/res/values-it/strings.xml index b18161801..09c9345b0 100644 --- a/src/android/app/src/main/res/values-it/strings.xml +++ b/src/android/app/src/main/res/values-it/strings.xml @@ -171,9 +171,7 @@ <string name="select_gpu_driver_title">Vuoi sostituire il driver della tua GPU attuale?</string> <string name="select_gpu_driver_install">Installa</string> <string name="select_gpu_driver_default">Predefinito</string> - <string name="select_gpu_driver_install_success">Installato%s</string> <string name="select_gpu_driver_use_default">Utilizza il driver predefinito della GPU.</string> - <string name="select_gpu_driver_error">Il driver selezionato è invalido, è in utilizzo quello predefinito di sistema!</string> <string name="system_gpu_driver">Driver GPU del sistema</string> <string name="installing_driver">Installando i driver...</string> diff --git a/src/android/app/src/main/res/values-ja/strings.xml b/src/android/app/src/main/res/values-ja/strings.xml index 88fa5a0bb..a0ea78bef 100644 --- a/src/android/app/src/main/res/values-ja/strings.xml +++ b/src/android/app/src/main/res/values-ja/strings.xml @@ -170,9 +170,7 @@ <string name="select_gpu_driver_title">現在のGPUドライバーを置き換えますか?</string> <string name="select_gpu_driver_install">インストール</string> <string name="select_gpu_driver_default">デフォルト</string> - <string name="select_gpu_driver_install_success">%s をインストールしました</string> <string name="select_gpu_driver_use_default">デフォルトのGPUドライバーを使用します</string> - <string name="select_gpu_driver_error">選択されたドライバが無効なため、システムのデフォルトを使用します!</string> <string name="system_gpu_driver">システムのGPUドライバ</string> <string name="installing_driver">インストール中…</string> diff --git a/src/android/app/src/main/res/values-ko/strings.xml b/src/android/app/src/main/res/values-ko/strings.xml index 4b658255c..214f95706 100644 --- a/src/android/app/src/main/res/values-ko/strings.xml +++ b/src/android/app/src/main/res/values-ko/strings.xml @@ -171,9 +171,7 @@ <string name="select_gpu_driver_title">현재 사용 중인 GPU 드라이버를 교체하겠습니까?</string> <string name="select_gpu_driver_install">설치</string> <string name="select_gpu_driver_default">기본값</string> - <string name="select_gpu_driver_install_success">설치된 %s</string> <string name="select_gpu_driver_use_default">기본 GPU 드라이버 사용</string> - <string name="select_gpu_driver_error">시스템 기본값을 사용하여 잘못된 드라이버를 선택했습니다!</string> <string name="system_gpu_driver">시스템 GPU 드라이버</string> <string name="installing_driver">드라이버 설치 중...</string> diff --git a/src/android/app/src/main/res/values-nb/strings.xml b/src/android/app/src/main/res/values-nb/strings.xml index dd602a389..5443cef42 100644 --- a/src/android/app/src/main/res/values-nb/strings.xml +++ b/src/android/app/src/main/res/values-nb/strings.xml @@ -171,9 +171,7 @@ <string name="select_gpu_driver_title">Ønsker du å bytte ut din nåværende GPU-driver?</string> <string name="select_gpu_driver_install">Installer</string> <string name="select_gpu_driver_default">Standard</string> - <string name="select_gpu_driver_install_success">Installert %s</string> <string name="select_gpu_driver_use_default">Bruk av standard GPU-driver</string> - <string name="select_gpu_driver_error">Ugyldig driver valgt, bruker systemstandard!</string> <string name="system_gpu_driver">Systemets GPU-driver</string> <string name="installing_driver">Installerer driver...</string> diff --git a/src/android/app/src/main/res/values-pl/strings.xml b/src/android/app/src/main/res/values-pl/strings.xml index 2fdd1f952..899e233d0 100644 --- a/src/android/app/src/main/res/values-pl/strings.xml +++ b/src/android/app/src/main/res/values-pl/strings.xml @@ -171,9 +171,7 @@ <string name="select_gpu_driver_title">Chcesz zastąpić obecny sterownik układu graficznego?</string> <string name="select_gpu_driver_install">Zainstaluj</string> <string name="select_gpu_driver_default">Domyślne</string> - <string name="select_gpu_driver_install_success">Zainstalowano %s</string> <string name="select_gpu_driver_use_default">Aktywny domyślny sterownik GPU</string> - <string name="select_gpu_driver_error">Wybrano błędny sterownik, powrót do domyślnego. </string> <string name="system_gpu_driver">Systemowy sterownik GPU</string> <string name="installing_driver">Instalowanie sterownika...</string> diff --git a/src/android/app/src/main/res/values-pt-rBR/strings.xml b/src/android/app/src/main/res/values-pt-rBR/strings.xml index 2f26367fe..caa095364 100644 --- a/src/android/app/src/main/res/values-pt-rBR/strings.xml +++ b/src/android/app/src/main/res/values-pt-rBR/strings.xml @@ -171,9 +171,7 @@ <string name="select_gpu_driver_title">Queres substituir o driver do GPU atual? </string> <string name="select_gpu_driver_install">Instalar</string> <string name="select_gpu_driver_default">Padrão</string> - <string name="select_gpu_driver_install_success">Instalado%s</string> <string name="select_gpu_driver_use_default">Usar o driver padrão do GPU</string> - <string name="select_gpu_driver_error">Driver selecionado inválido, a usar o padrão do sistema!</string> <string name="system_gpu_driver">Driver do GPU padrão</string> <string name="installing_driver">A instalar o Driver...</string> diff --git a/src/android/app/src/main/res/values-pt-rPT/strings.xml b/src/android/app/src/main/res/values-pt-rPT/strings.xml index 4e1eb4cd7..0a1a47fbb 100644 --- a/src/android/app/src/main/res/values-pt-rPT/strings.xml +++ b/src/android/app/src/main/res/values-pt-rPT/strings.xml @@ -171,9 +171,7 @@ <string name="select_gpu_driver_title">Queres substituir o driver do GPU atual? </string> <string name="select_gpu_driver_install">Instalar</string> <string name="select_gpu_driver_default">Padrão</string> - <string name="select_gpu_driver_install_success">Instalado%s</string> <string name="select_gpu_driver_use_default">Usar o driver padrão do GPU</string> - <string name="select_gpu_driver_error">Driver selecionado inválido, a usar o padrão do sistema!</string> <string name="system_gpu_driver">Driver do GPU padrão</string> <string name="installing_driver">A instalar o Driver...</string> diff --git a/src/android/app/src/main/res/values-ru/strings.xml b/src/android/app/src/main/res/values-ru/strings.xml index f5695dc93..0bef035d6 100644 --- a/src/android/app/src/main/res/values-ru/strings.xml +++ b/src/android/app/src/main/res/values-ru/strings.xml @@ -171,9 +171,7 @@ <string name="select_gpu_driver_title">Хотите заменить текущий драйвер ГП?</string> <string name="select_gpu_driver_install">Установить</string> <string name="select_gpu_driver_default">По умолчанию</string> - <string name="select_gpu_driver_install_success">Установлено %s</string> <string name="select_gpu_driver_use_default">Используется стандартный драйвер ГП </string> - <string name="select_gpu_driver_error">Выбран неверный драйвер, используется стандартный системный!</string> <string name="system_gpu_driver">Системный драйвер ГП</string> <string name="installing_driver">Установка драйвера...</string> diff --git a/src/android/app/src/main/res/values-uk/strings.xml b/src/android/app/src/main/res/values-uk/strings.xml index 061bc6f04..5b789ee98 100644 --- a/src/android/app/src/main/res/values-uk/strings.xml +++ b/src/android/app/src/main/res/values-uk/strings.xml @@ -171,9 +171,7 @@ <string name="select_gpu_driver_title">Хочете замінити поточний драйвер ГП?</string> <string name="select_gpu_driver_install">Встановити</string> <string name="select_gpu_driver_default">За замовчуванням</string> - <string name="select_gpu_driver_install_success">Встановлено %s</string> <string name="select_gpu_driver_use_default">Використовується стандартний драйвер ГП</string> - <string name="select_gpu_driver_error">Обрано неправильний драйвер, використовується стандартний системний!</string> <string name="system_gpu_driver">Системний драйвер ГП</string> <string name="installing_driver">Встановлення драйвера...</string> diff --git a/src/android/app/src/main/res/values-zh-rCN/strings.xml b/src/android/app/src/main/res/values-zh-rCN/strings.xml index fe6dd5eaa..c0e885751 100644 --- a/src/android/app/src/main/res/values-zh-rCN/strings.xml +++ b/src/android/app/src/main/res/values-zh-rCN/strings.xml @@ -171,9 +171,7 @@ <string name="select_gpu_driver_title">要取代您当前的 GPU 驱动程序吗?</string> <string name="select_gpu_driver_install">安装</string> <string name="select_gpu_driver_default">系统默认</string> - <string name="select_gpu_driver_install_success">已安装 %s</string> <string name="select_gpu_driver_use_default">使用默认 GPU 驱动程序</string> - <string name="select_gpu_driver_error">选择的驱动程序无效,将使用系统默认的驱动程序!</string> <string name="system_gpu_driver">系统 GPU 驱动程序</string> <string name="installing_driver">正在安装驱动程序…</string> diff --git a/src/android/app/src/main/res/values-zh-rTW/strings.xml b/src/android/app/src/main/res/values-zh-rTW/strings.xml index 9b3e54224..4a21bf893 100644 --- a/src/android/app/src/main/res/values-zh-rTW/strings.xml +++ b/src/android/app/src/main/res/values-zh-rTW/strings.xml @@ -171,9 +171,7 @@ <string name="select_gpu_driver_title">要取代您目前的 GPU 驅動程式嗎?</string> <string name="select_gpu_driver_install">安裝</string> <string name="select_gpu_driver_default">預設</string> - <string name="select_gpu_driver_install_success">已安裝 %s</string> <string name="select_gpu_driver_use_default">使用預設 GPU 驅動程式</string> - <string name="select_gpu_driver_error">選取的驅動程式無效,將使用系統預設驅動程式!</string> <string name="system_gpu_driver">系統 GPU 驅動程式</string> <string name="installing_driver">正在安裝驅動程式…</string> diff --git a/src/android/app/src/main/res/values/dimens.xml b/src/android/app/src/main/res/values/dimens.xml index 7b2296d95..ef855ea6f 100644 --- a/src/android/app/src/main/res/values/dimens.xml +++ b/src/android/app/src/main/res/values/dimens.xml @@ -13,6 +13,8 @@ <dimen name="menu_width">256dp</dimen> <dimen name="card_width">165dp</dimen> <dimen name="icon_inset">24dp</dimen> + <dimen name="spacing_bottom_list_fab">72dp</dimen> + <dimen name="spacing_fab">24dp</dimen> <dimen name="dialog_margin">20dp</dimen> <dimen name="elevated_app_bar">3dp</dimen> diff --git a/src/android/app/src/main/res/values/strings.xml b/src/android/app/src/main/res/values/strings.xml index e51edf872..9e4854221 100644 --- a/src/android/app/src/main/res/values/strings.xml +++ b/src/android/app/src/main/res/values/strings.xml @@ -72,6 +72,7 @@ <string name="invalid_keys_error">Invalid encryption keys</string> <string name="dumping_keys_quickstart_link">https://yuzu-emu.org/help/quickstart/#dumping-decryption-keys</string> <string name="install_keys_failure_description">The selected file is incorrect or corrupt. Please redump your keys.</string> + <string name="gpu_driver_manager">GPU Driver Manager</string> <string name="install_gpu_driver">Install GPU driver</string> <string name="install_gpu_driver_description">Install alternative drivers for potentially better performance or accuracy</string> <string name="advanced_settings">Advanced settings</string> @@ -234,15 +235,17 @@ <string name="export_failed">Export failed</string> <string name="import_failed">Import failed</string> <string name="cancelling">Cancelling</string> + <string name="install">Install</string> + <string name="delete">Delete</string> <!-- GPU driver installation --> <string name="select_gpu_driver">Select GPU driver</string> <string name="select_gpu_driver_title">Would you like to replace your current GPU driver?</string> <string name="select_gpu_driver_install">Install</string> <string name="select_gpu_driver_default">Default</string> - <string name="select_gpu_driver_install_success">Installed %s</string> <string name="select_gpu_driver_use_default">Using default GPU driver</string> - <string name="select_gpu_driver_error">Invalid driver selected, using system default!</string> + <string name="select_gpu_driver_error">Invalid driver selected</string> + <string name="driver_already_installed">Driver already installed</string> <string name="system_gpu_driver">System GPU driver</string> <string name="installing_driver">Installing driver…</string> |