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