diff options
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> | 
