diff options
author | Charles Lombardo <clombardo169@gmail.com> | 2023-09-29 07:58:03 +0200 |
---|---|---|
committer | Charles Lombardo <clombardo169@gmail.com> | 2023-10-12 23:17:21 +0200 |
commit | a5fb9de6faf4c75d231f414949306459de8b0926 (patch) | |
tree | 8c37d35773467e9f40e386d0edd1fbb16e15dec3 /src | |
parent | android: Use application context for all FileUtil functions (diff) | |
download | yuzu-a5fb9de6faf4c75d231f414949306459de8b0926.tar yuzu-a5fb9de6faf4c75d231f414949306459de8b0926.tar.gz yuzu-a5fb9de6faf4c75d231f414949306459de8b0926.tar.bz2 yuzu-a5fb9de6faf4c75d231f414949306459de8b0926.tar.lz yuzu-a5fb9de6faf4c75d231f414949306459de8b0926.tar.xz yuzu-a5fb9de6faf4c75d231f414949306459de8b0926.tar.zst yuzu-a5fb9de6faf4c75d231f414949306459de8b0926.zip |
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> |