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.dialog.MaterialAlertDialogBuilder | ||||||
| import com.google.android.material.slider.Slider | import com.google.android.material.slider.Slider | ||||||
| import kotlinx.coroutines.Dispatchers | import kotlinx.coroutines.Dispatchers | ||||||
|  | import kotlinx.coroutines.flow.collect | ||||||
| import kotlinx.coroutines.flow.collectLatest | import kotlinx.coroutines.flow.collectLatest | ||||||
| import kotlinx.coroutines.launch | import kotlinx.coroutines.launch | ||||||
| import org.yuzu.yuzu_emu.HomeNavigationDirections | 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.databinding.FragmentEmulationBinding | ||||||
| import org.yuzu.yuzu_emu.features.settings.model.IntSetting | import org.yuzu.yuzu_emu.features.settings.model.IntSetting | ||||||
| import org.yuzu.yuzu_emu.features.settings.model.Settings | 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.Game | ||||||
| import org.yuzu.yuzu_emu.model.EmulationViewModel | import org.yuzu.yuzu_emu.model.EmulationViewModel | ||||||
| import org.yuzu.yuzu_emu.overlay.InputOverlay | import org.yuzu.yuzu_emu.overlay.InputOverlay | ||||||
| @@ -70,6 +72,7 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback { | |||||||
|     private lateinit var game: Game |     private lateinit var game: Game | ||||||
|  |  | ||||||
|     private val emulationViewModel: EmulationViewModel by activityViewModels() |     private val emulationViewModel: EmulationViewModel by activityViewModels() | ||||||
|  |     private val driverViewModel: DriverViewModel by activityViewModels() | ||||||
|  |  | ||||||
|     private var isInFoldableLayout = false |     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() { |     override fun onPause() { | ||||||
|         if (emulationState.isRunning && emulationActivity?.isInPictureInPictureMode != true) { |         if (emulationState.isRunning && emulationActivity?.isInPictureInPictureMode != true) { | ||||||
|             emulationState.pause() |             emulationState.pause() | ||||||
|   | |||||||
| @@ -5,7 +5,6 @@ package org.yuzu.yuzu_emu.fragments | |||||||
|  |  | ||||||
| import android.Manifest | import android.Manifest | ||||||
| import android.content.ActivityNotFoundException | import android.content.ActivityNotFoundException | ||||||
| import android.content.DialogInterface |  | ||||||
| import android.content.Intent | import android.content.Intent | ||||||
| import android.content.pm.PackageManager | import android.content.pm.PackageManager | ||||||
| import android.os.Bundle | import android.os.Bundle | ||||||
| @@ -28,7 +27,6 @@ import androidx.fragment.app.activityViewModels | |||||||
| import androidx.navigation.findNavController | import androidx.navigation.findNavController | ||||||
| import androidx.navigation.fragment.findNavController | import androidx.navigation.fragment.findNavController | ||||||
| import androidx.recyclerview.widget.LinearLayoutManager | import androidx.recyclerview.widget.LinearLayoutManager | ||||||
| import com.google.android.material.dialog.MaterialAlertDialogBuilder |  | ||||||
| import com.google.android.material.transition.MaterialSharedAxis | import com.google.android.material.transition.MaterialSharedAxis | ||||||
| import org.yuzu.yuzu_emu.BuildConfig | import org.yuzu.yuzu_emu.BuildConfig | ||||||
| import org.yuzu.yuzu_emu.HomeNavigationDirections | 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.databinding.FragmentHomeSettingsBinding | ||||||
| import org.yuzu.yuzu_emu.features.DocumentProvider | import org.yuzu.yuzu_emu.features.DocumentProvider | ||||||
| import org.yuzu.yuzu_emu.features.settings.model.Settings | 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.HomeSetting | ||||||
| import org.yuzu.yuzu_emu.model.HomeViewModel | import org.yuzu.yuzu_emu.model.HomeViewModel | ||||||
| import org.yuzu.yuzu_emu.ui.main.MainActivity | import org.yuzu.yuzu_emu.ui.main.MainActivity | ||||||
| @@ -50,6 +49,7 @@ class HomeSettingsFragment : Fragment() { | |||||||
|     private lateinit var mainActivity: MainActivity |     private lateinit var mainActivity: MainActivity | ||||||
|  |  | ||||||
|     private val homeViewModel: HomeViewModel by activityViewModels() |     private val homeViewModel: HomeViewModel by activityViewModels() | ||||||
|  |     private val driverViewModel: DriverViewModel by activityViewModels() | ||||||
|  |  | ||||||
|     override fun onCreate(savedInstanceState: Bundle?) { |     override fun onCreate(savedInstanceState: Bundle?) { | ||||||
|         super.onCreate(savedInstanceState) |         super.onCreate(savedInstanceState) | ||||||
| @@ -107,13 +107,17 @@ class HomeSettingsFragment : Fragment() { | |||||||
|             ) |             ) | ||||||
|             add( |             add( | ||||||
|                 HomeSetting( |                 HomeSetting( | ||||||
|                     R.string.install_gpu_driver, |                     R.string.gpu_driver_manager, | ||||||
|                     R.string.install_gpu_driver_description, |                     R.string.install_gpu_driver_description, | ||||||
|                     R.drawable.ic_exit, |                     R.drawable.ic_build, | ||||||
|                     { driverInstaller() }, |                     { | ||||||
|  |                         binding.root.findNavController() | ||||||
|  |                             .navigate(R.id.action_homeSettingsFragment_to_driverManagerFragment) | ||||||
|  |                     }, | ||||||
|                     { GpuDriverHelper.supportsCustomDriverLoading() }, |                     { GpuDriverHelper.supportsCustomDriverLoading() }, | ||||||
|                     R.string.custom_driver_not_supported, |                     R.string.custom_driver_not_supported, | ||||||
|                     R.string.custom_driver_not_supported_description |                     R.string.custom_driver_not_supported_description, | ||||||
|  |                     driverViewModel.selectedDriverMetadata | ||||||
|                 ) |                 ) | ||||||
|             ) |             ) | ||||||
|             add( |             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() { |     private fun shareLog() { | ||||||
|         val file = DocumentFile.fromSingleUri( |         val file = DocumentFile.fromSingleUri( | ||||||
|             mainActivity, |             mainActivity, | ||||||
|   | |||||||
| @@ -10,8 +10,8 @@ import android.view.View | |||||||
| import android.view.ViewGroup | import android.view.ViewGroup | ||||||
| import android.widget.Toast | import android.widget.Toast | ||||||
| import androidx.appcompat.app.AlertDialog | import androidx.appcompat.app.AlertDialog | ||||||
| import androidx.appcompat.app.AppCompatActivity |  | ||||||
| import androidx.fragment.app.DialogFragment | import androidx.fragment.app.DialogFragment | ||||||
|  | import androidx.fragment.app.FragmentActivity | ||||||
| import androidx.fragment.app.activityViewModels | import androidx.fragment.app.activityViewModels | ||||||
| import androidx.lifecycle.Lifecycle | import androidx.lifecycle.Lifecycle | ||||||
| import androidx.lifecycle.ViewModelProvider | import androidx.lifecycle.ViewModelProvider | ||||||
| @@ -78,6 +78,10 @@ class IndeterminateProgressDialogFragment : DialogFragment() { | |||||||
|                                     requireActivity().supportFragmentManager, |                                     requireActivity().supportFragmentManager, | ||||||
|                                     MessageDialogFragment.TAG |                                     MessageDialogFragment.TAG | ||||||
|                                 ) |                                 ) | ||||||
|  |  | ||||||
|  |                                 else -> { | ||||||
|  |                                     // Do nothing | ||||||
|  |                                 } | ||||||
|                             } |                             } | ||||||
|                             taskViewModel.clear() |                             taskViewModel.clear() | ||||||
|                         } |                         } | ||||||
| @@ -115,7 +119,7 @@ class IndeterminateProgressDialogFragment : DialogFragment() { | |||||||
|         private const val CANCELLABLE = "Cancellable" |         private const val CANCELLABLE = "Cancellable" | ||||||
|  |  | ||||||
|         fun newInstance( |         fun newInstance( | ||||||
|             activity: AppCompatActivity, |             activity: FragmentActivity, | ||||||
|             titleId: Int, |             titleId: Int, | ||||||
|             cancellable: Boolean = false, |             cancellable: Boolean = false, | ||||||
|             task: () -> Any |             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.navigation.ui.setupWithNavController | ||||||
| import androidx.preference.PreferenceManager | import androidx.preference.PreferenceManager | ||||||
| import com.google.android.material.color.MaterialColors | import com.google.android.material.color.MaterialColors | ||||||
| import com.google.android.material.dialog.MaterialAlertDialogBuilder |  | ||||||
| import com.google.android.material.navigation.NavigationBarView | import com.google.android.material.navigation.NavigationBarView | ||||||
| import kotlinx.coroutines.CoroutineScope | import kotlinx.coroutines.CoroutineScope | ||||||
| import java.io.File | import java.io.File | ||||||
| import java.io.FilenameFilter | import java.io.FilenameFilter | ||||||
| import java.io.IOException |  | ||||||
| import kotlinx.coroutines.Dispatchers | import kotlinx.coroutines.Dispatchers | ||||||
| import kotlinx.coroutines.launch | import kotlinx.coroutines.launch | ||||||
| import kotlinx.coroutines.withContext | 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.R | ||||||
| import org.yuzu.yuzu_emu.activities.EmulationActivity | import org.yuzu.yuzu_emu.activities.EmulationActivity | ||||||
| import org.yuzu.yuzu_emu.databinding.ActivityMainBinding | 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.DocumentProvider | ||||||
| import org.yuzu.yuzu_emu.features.settings.model.Settings | import org.yuzu.yuzu_emu.features.settings.model.Settings | ||||||
| import org.yuzu.yuzu_emu.fragments.IndeterminateProgressDialogFragment | import org.yuzu.yuzu_emu.fragments.IndeterminateProgressDialogFragment | ||||||
| @@ -346,7 +343,7 @@ class MainActivity : AppCompatActivity(), ThemeProvider { | |||||||
|                 result, |                 result, | ||||||
|                 dstPath, |                 dstPath, | ||||||
|                 "prod.keys" |                 "prod.keys" | ||||||
|             ) |             ) != null | ||||||
|         ) { |         ) { | ||||||
|             if (NativeLibrary.reloadKeys()) { |             if (NativeLibrary.reloadKeys()) { | ||||||
|                 Toast.makeText( |                 Toast.makeText( | ||||||
| @@ -448,7 +445,7 @@ class MainActivity : AppCompatActivity(), ThemeProvider { | |||||||
|                     result, |                     result, | ||||||
|                     dstPath, |                     dstPath, | ||||||
|                     "key_retail.bin" |                     "key_retail.bin" | ||||||
|                 ) |                 ) != null | ||||||
|             ) { |             ) { | ||||||
|                 if (NativeLibrary.reloadKeys()) { |                 if (NativeLibrary.reloadKeys()) { | ||||||
|                     Toast.makeText( |                     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( |     val installGameUpdate = registerForActivityResult( | ||||||
|         ActivityResultContracts.OpenMultipleDocuments() |         ActivityResultContracts.OpenMultipleDocuments() | ||||||
|     ) { documents: List<Uri> -> |     ) { documents: List<Uri> -> | ||||||
|   | |||||||
| @@ -10,7 +10,6 @@ import androidx.documentfile.provider.DocumentFile | |||||||
| import kotlinx.coroutines.flow.StateFlow | import kotlinx.coroutines.flow.StateFlow | ||||||
| import java.io.BufferedInputStream | import java.io.BufferedInputStream | ||||||
| import java.io.File | import java.io.File | ||||||
| import java.io.FileOutputStream |  | ||||||
| import java.io.IOException | import java.io.IOException | ||||||
| import java.io.InputStream | import java.io.InputStream | ||||||
| import java.net.URLDecoder | 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.MinimalDocumentFile | ||||||
| import org.yuzu.yuzu_emu.model.TaskState | import org.yuzu.yuzu_emu.model.TaskState | ||||||
| import java.io.BufferedOutputStream | import java.io.BufferedOutputStream | ||||||
|  | import java.lang.NullPointerException | ||||||
|  | import java.nio.charset.StandardCharsets | ||||||
| import java.util.zip.ZipOutputStream | import java.util.zip.ZipOutputStream | ||||||
|  |  | ||||||
| object FileUtil { | object FileUtil { | ||||||
| @@ -243,42 +244,37 @@ object FileUtil { | |||||||
|         return size |         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( |     fun copyUriToInternalStorage( | ||||||
|         sourceUri: Uri?, |         sourceUri: Uri, | ||||||
|         destinationParentPath: String, |         destinationParentPath: String, | ||||||
|         destinationFilename: String |         destinationFilename: String = "" | ||||||
|     ): Boolean { |     ): File? = | ||||||
|         var input: InputStream? = null |  | ||||||
|         var output: FileOutputStream? = null |  | ||||||
|         try { |         try { | ||||||
|             input = context.contentResolver.openInputStream(sourceUri!!) |             val fileName = | ||||||
|             output = FileOutputStream("$destinationParentPath/$destinationFilename") |                 if (destinationFilename == "") getFilename(sourceUri) else "/$destinationFilename" | ||||||
|             val buffer = ByteArray(1024) |             val inputStream = context.contentResolver.openInputStream(sourceUri)!! | ||||||
|             var len: Int |  | ||||||
|             while (input!!.read(buffer).also { len = it } != -1) { |             val destinationFile = File("$destinationParentPath$fileName") | ||||||
|                 output.write(buffer, 0, len) |             if (destinationFile.exists()) { | ||||||
|  |                 destinationFile.delete() | ||||||
|             } |             } | ||||||
|             output.flush() |  | ||||||
|             return true |             destinationFile.outputStream().use { fos -> | ||||||
|         } catch (e: Exception) { |                 inputStream.use { it.copyTo(fos) } | ||||||
|             Log.error("[FileUtil]: Cannot copy file, error: " + e.message) |             } | ||||||
|         } finally { |             destinationFile | ||||||
|             if (input != null) { |  | ||||||
|                 try { |  | ||||||
|                     input.close() |  | ||||||
|         } catch (e: IOException) { |         } catch (e: IOException) { | ||||||
|                     Log.error("[FileUtil]: Cannot close input file, error: " + e.message) |             null | ||||||
|                 } |         } catch (e: NullPointerException) { | ||||||
|             } |             null | ||||||
|             if (output != null) { |  | ||||||
|                 try { |  | ||||||
|                     output.close() |  | ||||||
|                 } catch (e: IOException) { |  | ||||||
|                     Log.error("[FileUtil]: Cannot close output file, error: " + e.message) |  | ||||||
|                 } |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
|         return false |  | ||||||
|         } |         } | ||||||
|  |  | ||||||
|     /** |     /** | ||||||
| @@ -365,4 +361,12 @@ object FileUtil { | |||||||
|         return fileName.substring(fileName.lastIndexOf(".") + 1) |         return fileName.substring(fileName.lastIndexOf(".") + 1) | ||||||
|             .lowercase() |             .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 | package org.yuzu.yuzu_emu.utils | ||||||
|  |  | ||||||
| import android.content.Context |  | ||||||
| import android.net.Uri | import android.net.Uri | ||||||
|  | import android.os.Build | ||||||
| import java.io.BufferedInputStream | import java.io.BufferedInputStream | ||||||
| import java.io.File | import java.io.File | ||||||
| import java.io.FileInputStream |  | ||||||
| import java.io.FileOutputStream |  | ||||||
| import java.io.IOException | import java.io.IOException | ||||||
| import java.util.zip.ZipInputStream |  | ||||||
| import org.yuzu.yuzu_emu.NativeLibrary | import org.yuzu.yuzu_emu.NativeLibrary | ||||||
| import org.yuzu.yuzu_emu.utils.FileUtil.copyUriToInternalStorage |  | ||||||
| import org.yuzu.yuzu_emu.YuzuApplication | import org.yuzu.yuzu_emu.YuzuApplication | ||||||
|  | import java.util.zip.ZipException | ||||||
|  | import java.util.zip.ZipFile | ||||||
|  |  | ||||||
| object GpuDriverHelper { | object GpuDriverHelper { | ||||||
|     private const val META_JSON_FILENAME = "meta.json" |     private const val META_JSON_FILENAME = "meta.json" | ||||||
|     private const val DRIVER_INTERNAL_FILENAME = "gpu_driver.zip" |  | ||||||
|     private var fileRedirectionPath: String? = null |     private var fileRedirectionPath: String? = null | ||||||
|     private var driverInstallationPath: String? = null |     var driverInstallationPath: String? = null | ||||||
|     private var hookLibPath: String? = null |     private var hookLibPath: String? = null | ||||||
|  |  | ||||||
|     @Throws(IOException::class) |     val driverStoragePath get() = DirectoryInitialization.userDirectory!! + "/gpu_drivers/" | ||||||
|     private fun unzip(zipFilePath: String, destDir: String) { |  | ||||||
|         val dir = File(destDir) |  | ||||||
|  |  | ||||||
|         // Create output directory if it doesn't exist |     fun initializeDriverParameters() { | ||||||
|         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) { |  | ||||||
|         try { |         try { | ||||||
|             // Initialize the file redirection directory. |             // Initialize the file redirection directory. | ||||||
|             fileRedirectionPath = |             fileRedirectionPath = YuzuApplication.appContext | ||||||
|                 context.getExternalFilesDir(null)!!.canonicalPath + "/gpu/vk_file_redirect/" |                 .getExternalFilesDir(null)!!.canonicalPath + "/gpu/vk_file_redirect/" | ||||||
|  |  | ||||||
|             // Initialize the driver installation directory. |             // Initialize the driver installation directory. | ||||||
|             driverInstallationPath = context.filesDir.canonicalPath + "/gpu_driver/" |             driverInstallationPath = YuzuApplication.appContext | ||||||
|                 .filesDir.canonicalPath + "/gpu_driver/" |                 .filesDir.canonicalPath + "/gpu_driver/" | ||||||
|         } catch (e: IOException) { |         } catch (e: IOException) { | ||||||
|             throw RuntimeException(e) |             throw RuntimeException(e) | ||||||
| @@ -71,69 +38,169 @@ object GpuDriverHelper { | |||||||
|         initializeDirectories() |         initializeDirectories() | ||||||
|  |  | ||||||
|         // Initialize hook libraries directory. |         // Initialize hook libraries directory. | ||||||
|         hookLibPath = context.applicationInfo.nativeLibraryDir + "/" |  | ||||||
|         hookLibPath = YuzuApplication.appContext.applicationInfo.nativeLibraryDir + "/" |         hookLibPath = YuzuApplication.appContext.applicationInfo.nativeLibraryDir + "/" | ||||||
|  |  | ||||||
|         // Initialize GPU driver. |         // Initialize GPU driver. | ||||||
|         NativeLibrary.initializeGpuDriver( |         NativeLibrary.initializeGpuDriver( | ||||||
|             hookLibPath, |             hookLibPath, | ||||||
|             driverInstallationPath, |             driverInstallationPath, | ||||||
|             customDriverLibraryName, |             customDriverData.libraryName, | ||||||
|             fileRedirectionPath |             fileRedirectionPath | ||||||
|         ) |         ) | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     fun installDefaultDriver(context: Context) { |     fun getDrivers(): MutableList<Pair<String, GpuDriverMetadata>> { | ||||||
|     fun installDefaultDriver() { |         val driverZips = File(driverStoragePath).listFiles() | ||||||
|         // Removing the installed driver will result in the backend using the default system driver. |         val drivers: MutableList<Pair<String, GpuDriverMetadata>> = | ||||||
|         val driverInstallationDir = File(driverInstallationPath!!) |             driverZips | ||||||
|         deleteRecursive(driverInstallationDir) |                 ?.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. |         // Revert to system default in the event the specified driver is bad. | ||||||
|         installDefaultDriver() |         installDefaultDriver() | ||||||
|  |  | ||||||
|         // Ensure we have directories. |         // Ensure we have directories. | ||||||
|         initializeDirectories() |         initializeDirectories() | ||||||
|  |  | ||||||
|         // Copy the zip file URI into our private storage. |         // Copy the zip file URI to user data | ||||||
|         copyUriToInternalStorage( |         val copiedFile = | ||||||
|             context, |             FileUtil.copyUriToInternalStorage(driverUri, driverStoragePath) ?: return false | ||||||
|             driverPathUri, |  | ||||||
|             driverInstallationPath!!, |         // Validate driver | ||||||
|             DRIVER_INTERNAL_FILENAME |         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. |         // Unzip the driver. | ||||||
|         try { |         try { | ||||||
|             unzip(driverInstallationPath + DRIVER_INTERNAL_FILENAME, driverInstallationPath!!) |             FileUtil.unzipToInternalStorage( | ||||||
|  |                 BufferedInputStream(copiedFile.inputStream()), | ||||||
|  |                 File(driverInstallationPath!!) | ||||||
|  |             ) | ||||||
|         } catch (e: SecurityException) { |         } catch (e: SecurityException) { | ||||||
|             return |             return false | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         // Initialize the driver parameters. |         // 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 |     external fun supportsCustomDriverLoading(): Boolean | ||||||
|  |  | ||||||
|     // Parse the custom driver metadata to retrieve the name. |     // Parse the custom driver metadata to retrieve the name. | ||||||
|     val customDriverName: String? |     val customDriverData: GpuDriverMetadata | ||||||
|         get() { |         get() = GpuDriverMetadata(File(driverInstallationPath + META_JSON_FILENAME)) | ||||||
|             val metadata = GpuDriverMetadata(driverInstallationPath + META_JSON_FILENAME) |  | ||||||
|             return metadata.name |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|     // Parse the custom driver metadata to retrieve the library name. |     fun initializeDirectories() { | ||||||
|     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() { |  | ||||||
|         // Ensure the file redirection directory exists. |         // Ensure the file redirection directory exists. | ||||||
|         val fileRedirectionDir = File(fileRedirectionPath!!) |         val fileRedirectionDir = File(fileRedirectionPath!!) | ||||||
|         if (!fileRedirectionDir.exists()) { |         if (!fileRedirectionDir.exists()) { | ||||||
| @@ -144,14 +211,10 @@ object GpuDriverHelper { | |||||||
|         if (!driverInstallationDir.exists()) { |         if (!driverInstallationDir.exists()) { | ||||||
|             driverInstallationDir.mkdirs() |             driverInstallationDir.mkdirs() | ||||||
|         } |         } | ||||||
|     } |         // Ensure the driver storage directory exists | ||||||
|  |         val driverStorageDirectory = File(driverStoragePath) | ||||||
|     private fun deleteRecursive(fileOrDirectory: File) { |         if (!driverStorageDirectory.exists()) { | ||||||
|         if (fileOrDirectory.isDirectory) { |             driverStorageDirectory.mkdirs() | ||||||
|             for (child in fileOrDirectory.listFiles()!!) { |  | ||||||
|                 deleteRecursive(child) |  | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|         fileOrDirectory.delete() |  | ||||||
|     } |  | ||||||
| } | } | ||||||
|   | |||||||
| @@ -4,29 +4,29 @@ | |||||||
| package org.yuzu.yuzu_emu.utils | package org.yuzu.yuzu_emu.utils | ||||||
|  |  | ||||||
| import java.io.IOException | 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.JSONException | ||||||
| import org.json.JSONObject | import org.json.JSONObject | ||||||
|  | import java.io.File | ||||||
|  | import java.io.InputStream | ||||||
|  |  | ||||||
| class GpuDriverMetadata(metadataFilePath: String) { | class GpuDriverMetadata { | ||||||
|     var name: String? = null |     /** | ||||||
|     var description: String? = null |      * Tries to get driver metadata information from a meta.json [File] | ||||||
|     var author: String? = null |      * | ||||||
|     var vendor: String? = null |      * @param metadataFile meta.json file provided with a GPU driver | ||||||
|     var driverVersion: String? = null |      */ | ||||||
|     var minApi = 0 |     constructor(metadataFile: File) { | ||||||
|     var libraryName: String? = null |         if (metadataFile.length() > MAX_META_SIZE_BYTES) { | ||||||
|  |             return | ||||||
|  |         } | ||||||
|  |  | ||||||
|     init { |  | ||||||
|         try { |         try { | ||||||
|             val json = JSONObject(getStringFromFile(metadataFilePath)) |             val json = JSONObject(FileUtil.getStringFromFile(metadataFile)) | ||||||
|             name = json.getString("name") |             name = json.getString("name") | ||||||
|             description = json.getString("description") |             description = json.getString("description") | ||||||
|             author = json.getString("author") |             author = json.getString("author") | ||||||
|             vendor = json.getString("vendor") |             vendor = json.getString("vendor") | ||||||
|             driverVersion = json.getString("driverVersion") |             version = json.getString("driverVersion") | ||||||
|             minApi = json.getInt("minApi") |             minApi = json.getInt("minApi") | ||||||
|             libraryName = json.getString("libraryName") |             libraryName = json.getString("libraryName") | ||||||
|         } catch (e: JSONException) { |         } catch (e: JSONException) { | ||||||
| @@ -36,12 +36,84 @@ class GpuDriverMetadata(metadataFilePath: String) { | |||||||
|         } |         } | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * 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 { |     companion object { | ||||||
|         @Throws(IOException::class) |         private const val MAX_META_SIZE_BYTES = 500000 | ||||||
|         private fun getStringFromFile(filePath: String): String { |  | ||||||
|             val path = Paths.get(filePath) |  | ||||||
|             val bytes = Files.readAllBytes(path) |  | ||||||
|             return String(bytes, StandardCharsets.UTF_8) |  | ||||||
|         } |  | ||||||
|     } |     } | ||||||
| } | } | ||||||
|   | |||||||
							
								
								
									
										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 |         <action | ||||||
|             android:id="@+id/action_homeSettingsFragment_to_installableFragment" |             android:id="@+id/action_homeSettingsFragment_to_installableFragment" | ||||||
|             app:destination="@id/installableFragment" /> |             app:destination="@id/installableFragment" /> | ||||||
|  |         <action | ||||||
|  |             android:id="@+id/action_homeSettingsFragment_to_driverManagerFragment" | ||||||
|  |             app:destination="@id/driverManagerFragment" /> | ||||||
|     </fragment> |     </fragment> | ||||||
|  |  | ||||||
|     <fragment |     <fragment | ||||||
| @@ -95,5 +98,9 @@ | |||||||
|         android:id="@+id/installableFragment" |         android:id="@+id/installableFragment" | ||||||
|         android:name="org.yuzu.yuzu_emu.fragments.InstallableFragment" |         android:name="org.yuzu.yuzu_emu.fragments.InstallableFragment" | ||||||
|         android:label="InstallableFragment" /> |         android:label="InstallableFragment" /> | ||||||
|  |     <fragment | ||||||
|  |         android:id="@+id/driverManagerFragment" | ||||||
|  |         android:name="org.yuzu.yuzu_emu.fragments.DriverManagerFragment" | ||||||
|  |         android:label="DriverManagerFragment" /> | ||||||
|  |  | ||||||
| </navigation> | </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_title">Möchtest du deinen aktuellen GPU-Treiber ersetzen?</string> | ||||||
|     <string name="select_gpu_driver_install">Installieren</string> |     <string name="select_gpu_driver_install">Installieren</string> | ||||||
|     <string name="select_gpu_driver_default">Standard</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_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="system_gpu_driver">System GPU-Treiber</string> | ||||||
|     <string name="installing_driver">Treiber wird installiert...</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_title">¿Quiere reemplazar el driver de GPU actual?</string> | ||||||
|     <string name="select_gpu_driver_install">Instalar</string> |     <string name="select_gpu_driver_install">Instalar</string> | ||||||
|     <string name="select_gpu_driver_default">Predeterminado</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_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="system_gpu_driver">Driver GPU del sistema</string> | ||||||
|     <string name="installing_driver">Instalando driver...</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_title">Souhaitez vous remplacer votre pilote actuel ?</string> | ||||||
|     <string name="select_gpu_driver_install">Installer</string> |     <string name="select_gpu_driver_install">Installer</string> | ||||||
|     <string name="select_gpu_driver_default">Défaut</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_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="system_gpu_driver">Pilote du GPU du système</string> | ||||||
|     <string name="installing_driver">Installation du pilote...</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_title">Vuoi sostituire il driver della tua GPU attuale?</string> | ||||||
|     <string name="select_gpu_driver_install">Installa</string> |     <string name="select_gpu_driver_install">Installa</string> | ||||||
|     <string name="select_gpu_driver_default">Predefinito</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_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="system_gpu_driver">Driver GPU del sistema</string> | ||||||
|     <string name="installing_driver">Installando i driver...</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_title">現在のGPUドライバーを置き換えますか?</string> | ||||||
|     <string name="select_gpu_driver_install">インストール</string> |     <string name="select_gpu_driver_install">インストール</string> | ||||||
|     <string name="select_gpu_driver_default">デフォルト</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_use_default">デフォルトのGPUドライバーを使用します</string> | ||||||
|     <string name="select_gpu_driver_error">選択されたドライバが無効なため、システムのデフォルトを使用します!</string> |  | ||||||
|     <string name="system_gpu_driver">システムのGPUドライバ</string> |     <string name="system_gpu_driver">システムのGPUドライバ</string> | ||||||
|     <string name="installing_driver">インストール中…</string> |     <string name="installing_driver">インストール中…</string> | ||||||
|  |  | ||||||
|   | |||||||
| @@ -171,9 +171,7 @@ | |||||||
|     <string name="select_gpu_driver_title">현재 사용 중인 GPU 드라이버를 교체하겠습니까?</string> |     <string name="select_gpu_driver_title">현재 사용 중인 GPU 드라이버를 교체하겠습니까?</string> | ||||||
|     <string name="select_gpu_driver_install">설치</string> |     <string name="select_gpu_driver_install">설치</string> | ||||||
|     <string name="select_gpu_driver_default">기본값</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_use_default">기본 GPU 드라이버 사용</string> | ||||||
|     <string name="select_gpu_driver_error">시스템 기본값을 사용하여 잘못된 드라이버를 선택했습니다!</string> |  | ||||||
|     <string name="system_gpu_driver">시스템 GPU 드라이버</string> |     <string name="system_gpu_driver">시스템 GPU 드라이버</string> | ||||||
|     <string name="installing_driver">드라이버 설치 중...</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_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_install">Installer</string> | ||||||
|     <string name="select_gpu_driver_default">Standard</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_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="system_gpu_driver">Systemets GPU-driver</string> | ||||||
|     <string name="installing_driver">Installerer 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_title">Chcesz zastąpić obecny sterownik układu graficznego?</string> | ||||||
|     <string name="select_gpu_driver_install">Zainstaluj</string> |     <string name="select_gpu_driver_install">Zainstaluj</string> | ||||||
|     <string name="select_gpu_driver_default">Domyślne</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_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="system_gpu_driver">Systemowy sterownik GPU</string> | ||||||
|     <string name="installing_driver">Instalowanie sterownika...</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_title">Queres substituir o driver do GPU atual? </string> | ||||||
|     <string name="select_gpu_driver_install">Instalar</string> |     <string name="select_gpu_driver_install">Instalar</string> | ||||||
|     <string name="select_gpu_driver_default">Padrão</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_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="system_gpu_driver">Driver do GPU padrão</string> | ||||||
|     <string name="installing_driver">A instalar o Driver...</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_title">Queres substituir o driver do GPU atual? </string> | ||||||
|     <string name="select_gpu_driver_install">Instalar</string> |     <string name="select_gpu_driver_install">Instalar</string> | ||||||
|     <string name="select_gpu_driver_default">Padrão</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_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="system_gpu_driver">Driver do GPU padrão</string> | ||||||
|     <string name="installing_driver">A instalar o Driver...</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_title">Хотите заменить текущий драйвер ГП?</string> | ||||||
|     <string name="select_gpu_driver_install">Установить</string> |     <string name="select_gpu_driver_install">Установить</string> | ||||||
|     <string name="select_gpu_driver_default">По умолчанию</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_use_default">Используется стандартный драйвер ГП </string> | ||||||
|     <string name="select_gpu_driver_error">Выбран неверный драйвер, используется стандартный системный!</string> |  | ||||||
|     <string name="system_gpu_driver">Системный драйвер ГП</string> |     <string name="system_gpu_driver">Системный драйвер ГП</string> | ||||||
|     <string name="installing_driver">Установка драйвера...</string> |     <string name="installing_driver">Установка драйвера...</string> | ||||||
|  |  | ||||||
|   | |||||||
| @@ -171,9 +171,7 @@ | |||||||
|     <string name="select_gpu_driver_title">Хочете замінити поточний драйвер ГП?</string> |     <string name="select_gpu_driver_title">Хочете замінити поточний драйвер ГП?</string> | ||||||
|     <string name="select_gpu_driver_install">Встановити</string> |     <string name="select_gpu_driver_install">Встановити</string> | ||||||
|     <string name="select_gpu_driver_default">За замовчуванням</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_use_default">Використовується стандартний драйвер ГП</string> | ||||||
|     <string name="select_gpu_driver_error">Обрано неправильний драйвер, використовується стандартний системний!</string> |  | ||||||
|     <string name="system_gpu_driver">Системний драйвер ГП</string> |     <string name="system_gpu_driver">Системний драйвер ГП</string> | ||||||
|     <string name="installing_driver">Встановлення драйвера...</string> |     <string name="installing_driver">Встановлення драйвера...</string> | ||||||
|  |  | ||||||
|   | |||||||
| @@ -171,9 +171,7 @@ | |||||||
|     <string name="select_gpu_driver_title">要取代您当前的 GPU 驱动程序吗?</string> |     <string name="select_gpu_driver_title">要取代您当前的 GPU 驱动程序吗?</string> | ||||||
|     <string name="select_gpu_driver_install">安装</string> |     <string name="select_gpu_driver_install">安装</string> | ||||||
|     <string name="select_gpu_driver_default">系统默认</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_use_default">使用默认 GPU 驱动程序</string> | ||||||
|     <string name="select_gpu_driver_error">选择的驱动程序无效,将使用系统默认的驱动程序!</string> |  | ||||||
|     <string name="system_gpu_driver">系统 GPU 驱动程序</string> |     <string name="system_gpu_driver">系统 GPU 驱动程序</string> | ||||||
|     <string name="installing_driver">正在安装驱动程序…</string> |     <string name="installing_driver">正在安装驱动程序…</string> | ||||||
|  |  | ||||||
|   | |||||||
| @@ -171,9 +171,7 @@ | |||||||
|     <string name="select_gpu_driver_title">要取代您目前的 GPU 驅動程式嗎?</string> |     <string name="select_gpu_driver_title">要取代您目前的 GPU 驅動程式嗎?</string> | ||||||
|     <string name="select_gpu_driver_install">安裝</string> |     <string name="select_gpu_driver_install">安裝</string> | ||||||
|     <string name="select_gpu_driver_default">預設</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_use_default">使用預設 GPU 驅動程式</string> | ||||||
|     <string name="select_gpu_driver_error">選取的驅動程式無效,將使用系統預設驅動程式!</string> |  | ||||||
|     <string name="system_gpu_driver">系統 GPU 驅動程式</string> |     <string name="system_gpu_driver">系統 GPU 驅動程式</string> | ||||||
|     <string name="installing_driver">正在安裝驅動程式…</string> |     <string name="installing_driver">正在安裝驅動程式…</string> | ||||||
|  |  | ||||||
|   | |||||||
| @@ -13,6 +13,8 @@ | |||||||
|     <dimen name="menu_width">256dp</dimen> |     <dimen name="menu_width">256dp</dimen> | ||||||
|     <dimen name="card_width">165dp</dimen> |     <dimen name="card_width">165dp</dimen> | ||||||
|     <dimen name="icon_inset">24dp</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="dialog_margin">20dp</dimen> | ||||||
|     <dimen name="elevated_app_bar">3dp</dimen> |     <dimen name="elevated_app_bar">3dp</dimen> | ||||||
|   | |||||||
| @@ -72,6 +72,7 @@ | |||||||
|     <string name="invalid_keys_error">Invalid encryption keys</string> |     <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="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="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">Install GPU driver</string> | ||||||
|     <string name="install_gpu_driver_description">Install alternative drivers for potentially better performance or accuracy</string> |     <string name="install_gpu_driver_description">Install alternative drivers for potentially better performance or accuracy</string> | ||||||
|     <string name="advanced_settings">Advanced settings</string> |     <string name="advanced_settings">Advanced settings</string> | ||||||
| @@ -234,15 +235,17 @@ | |||||||
|     <string name="export_failed">Export failed</string> |     <string name="export_failed">Export failed</string> | ||||||
|     <string name="import_failed">Import failed</string> |     <string name="import_failed">Import failed</string> | ||||||
|     <string name="cancelling">Cancelling</string> |     <string name="cancelling">Cancelling</string> | ||||||
|  |     <string name="install">Install</string> | ||||||
|  |     <string name="delete">Delete</string> | ||||||
|  |  | ||||||
|     <!-- GPU driver installation --> |     <!-- GPU driver installation --> | ||||||
|     <string name="select_gpu_driver">Select GPU driver</string> |     <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_title">Would you like to replace your current GPU driver?</string> | ||||||
|     <string name="select_gpu_driver_install">Install</string> |     <string name="select_gpu_driver_install">Install</string> | ||||||
|     <string name="select_gpu_driver_default">Default</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_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="system_gpu_driver">System GPU driver</string> | ||||||
|     <string name="installing_driver">Installing driver…</string> |     <string name="installing_driver">Installing driver…</string> | ||||||
|  |  | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user