android: Add GPU driver management fragment
Implements a GPU driver manager that saves all drivers to the user data directory and asynchronously installs drivers when they're needed.
This commit is contained in:
		@@ -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
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -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)
 | 
			
		||||
        }
 | 
			
		||||
}
 | 
			
		||||
@@ -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"
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -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()
 | 
			
		||||
 
 | 
			
		||||
@@ -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,
 | 
			
		||||
 
 | 
			
		||||
@@ -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
 | 
			
		||||
 
 | 
			
		||||
@@ -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)
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -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> ->
 | 
			
		||||
 
 | 
			
		||||
@@ -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)
 | 
			
		||||
            val fileName =
 | 
			
		||||
                if (destinationFilename == "") getFilename(sourceUri) else "/$destinationFilename"
 | 
			
		||||
            val inputStream = context.contentResolver.openInputStream(sourceUri)!!
 | 
			
		||||
 | 
			
		||||
            val destinationFile = File("$destinationParentPath$fileName")
 | 
			
		||||
            if (destinationFile.exists()) {
 | 
			
		||||
                destinationFile.delete()
 | 
			
		||||
            }
 | 
			
		||||
            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)
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            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)
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -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)
 | 
			
		||||
    val driverStoragePath get() = DirectoryInitialization.userDirectory!! + "/gpu_drivers/"
 | 
			
		||||
 | 
			
		||||
        // 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()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    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 installDefaultDriver() {
 | 
			
		||||
        // Removing the installed driver will result in the backend using the default system driver.
 | 
			
		||||
        val driverInstallationDir = File(driverInstallationPath!!)
 | 
			
		||||
        deleteRecursive(driverInstallationDir)
 | 
			
		||||
    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 installCustomDriver(context: Context, driverPathUri: Uri?) {
 | 
			
		||||
    fun installDefaultDriver() {
 | 
			
		||||
        // Removing the installed driver will result in the backend using the default system driver.
 | 
			
		||||
        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
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 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
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Unzips driver into installation directory
 | 
			
		||||
     */
 | 
			
		||||
    fun installCustomDriver(driver: File): Boolean {
 | 
			
		||||
        // Revert to system default in the event the specified driver is bad.
 | 
			
		||||
        installDefaultDriver()
 | 
			
		||||
 | 
			
		||||
        // Ensure we have directories.
 | 
			
		||||
        initializeDirectories()
 | 
			
		||||
 | 
			
		||||
        // Validate driver
 | 
			
		||||
        val metadata = getMetadataFromZip(driver)
 | 
			
		||||
        if (metadata.name == null) {
 | 
			
		||||
            driver.delete()
 | 
			
		||||
            return false
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // 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 customDriverName: String?
 | 
			
		||||
        get() {
 | 
			
		||||
            val metadata = GpuDriverMetadata(driverInstallationPath + META_JSON_FILENAME)
 | 
			
		||||
            return metadata.name
 | 
			
		||||
        }
 | 
			
		||||
    val customDriverData: GpuDriverMetadata
 | 
			
		||||
        get() = GpuDriverMetadata(File(driverInstallationPath + META_JSON_FILENAME))
 | 
			
		||||
 | 
			
		||||
    // 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
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
    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()
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -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
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										9
									
								
								src/android/app/src/main/res/drawable/ic_build.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								src/android/app/src/main/res/drawable/ic_build.xml
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,9 @@
 | 
			
		||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
 | 
			
		||||
    android:width="24dp"
 | 
			
		||||
    android:height="24dp"
 | 
			
		||||
    android:viewportWidth="24"
 | 
			
		||||
    android:viewportHeight="24">
 | 
			
		||||
    <path
 | 
			
		||||
        android:fillColor="?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>
 | 
			
		||||
							
								
								
									
										9
									
								
								src/android/app/src/main/res/drawable/ic_delete.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								src/android/app/src/main/res/drawable/ic_delete.xml
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,9 @@
 | 
			
		||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
 | 
			
		||||
    android:width="24dp"
 | 
			
		||||
    android:height="24dp"
 | 
			
		||||
    android:viewportWidth="24"
 | 
			
		||||
    android:viewportHeight="24">
 | 
			
		||||
    <path
 | 
			
		||||
        android:fillColor="?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>
 | 
			
		||||
							
								
								
									
										89
									
								
								src/android/app/src/main/res/layout/card_driver_option.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										89
									
								
								src/android/app/src/main/res/layout/card_driver_option.xml
									
									
									
									
									
										Normal file
									
								
							@@ -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>
 | 
			
		||||
@@ -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>
 | 
			
		||||
@@ -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>
 | 
			
		||||
 
 | 
			
		||||
@@ -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>
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -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>
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -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>
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -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>
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -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>
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -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>
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -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>
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -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>
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -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>
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -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>
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -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>
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -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>
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -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>
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -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>
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -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>
 | 
			
		||||
 
 | 
			
		||||
@@ -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>
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user