summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/DriverAdapter.kt117
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/DriverManagerFragment.kt185
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/DriversLoadingDialogFragment.kt75
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/EmulationFragment.kt29
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/HomeSettingsFragment.kt41
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/IndeterminateProgressDialogFragment.kt8
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/model/DriverViewModel.kt158
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/main/MainActivity.kt60
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/FileUtil.kt68
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/GpuDriverHelper.kt221
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/GpuDriverMetadata.kt112
-rw-r--r--src/android/app/src/main/res/drawable/ic_build.xml9
-rw-r--r--src/android/app/src/main/res/drawable/ic_delete.xml9
-rw-r--r--src/android/app/src/main/res/layout/card_driver_option.xml89
-rw-r--r--src/android/app/src/main/res/layout/fragment_driver_manager.xml48
-rw-r--r--src/android/app/src/main/res/navigation/home_navigation.xml7
-rw-r--r--src/android/app/src/main/res/values-de/strings.xml2
-rw-r--r--src/android/app/src/main/res/values-es/strings.xml2
-rw-r--r--src/android/app/src/main/res/values-fr/strings.xml2
-rw-r--r--src/android/app/src/main/res/values-it/strings.xml2
-rw-r--r--src/android/app/src/main/res/values-ja/strings.xml2
-rw-r--r--src/android/app/src/main/res/values-ko/strings.xml2
-rw-r--r--src/android/app/src/main/res/values-nb/strings.xml2
-rw-r--r--src/android/app/src/main/res/values-pl/strings.xml2
-rw-r--r--src/android/app/src/main/res/values-pt-rBR/strings.xml2
-rw-r--r--src/android/app/src/main/res/values-pt-rPT/strings.xml2
-rw-r--r--src/android/app/src/main/res/values-ru/strings.xml2
-rw-r--r--src/android/app/src/main/res/values-uk/strings.xml2
-rw-r--r--src/android/app/src/main/res/values-zh-rCN/strings.xml2
-rw-r--r--src/android/app/src/main/res/values-zh-rTW/strings.xml2
-rw-r--r--src/android/app/src/main/res/values/dimens.xml2
-rw-r--r--src/android/app/src/main/res/values/strings.xml7
32 files changed, 1010 insertions, 263 deletions
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/DriverAdapter.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/DriverAdapter.kt
new file mode 100644
index 000000000..0e818cab9
--- /dev/null
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/DriverAdapter.kt
@@ -0,0 +1,117 @@
1// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
2// SPDX-License-Identifier: GPL-2.0-or-later
3
4package org.yuzu.yuzu_emu.adapters
5
6import android.text.TextUtils
7import android.view.LayoutInflater
8import android.view.View
9import android.view.ViewGroup
10import androidx.recyclerview.widget.AsyncDifferConfig
11import androidx.recyclerview.widget.DiffUtil
12import androidx.recyclerview.widget.ListAdapter
13import androidx.recyclerview.widget.RecyclerView
14import org.yuzu.yuzu_emu.R
15import org.yuzu.yuzu_emu.databinding.CardDriverOptionBinding
16import org.yuzu.yuzu_emu.model.DriverViewModel
17import org.yuzu.yuzu_emu.utils.GpuDriverHelper
18import org.yuzu.yuzu_emu.utils.GpuDriverMetadata
19
20class DriverAdapter(private val driverViewModel: DriverViewModel) :
21 ListAdapter<Pair<String, GpuDriverMetadata>, DriverAdapter.DriverViewHolder>(
22 AsyncDifferConfig.Builder(DiffCallback()).build()
23 ) {
24 override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): DriverViewHolder {
25 val binding =
26 CardDriverOptionBinding.inflate(LayoutInflater.from(parent.context), parent, false)
27 return DriverViewHolder(binding)
28 }
29
30 override fun getItemCount(): Int = currentList.size
31
32 override fun onBindViewHolder(holder: DriverViewHolder, position: Int) =
33 holder.bind(currentList[position])
34
35 private fun onSelectDriver(position: Int) {
36 driverViewModel.setSelectedDriverIndex(position)
37 notifyItemChanged(driverViewModel.previouslySelectedDriver)
38 notifyItemChanged(driverViewModel.selectedDriver)
39 }
40
41 private fun onDeleteDriver(driverData: Pair<String, GpuDriverMetadata>, position: Int) {
42 if (driverViewModel.selectedDriver > position) {
43 driverViewModel.setSelectedDriverIndex(driverViewModel.selectedDriver - 1)
44 }
45 if (GpuDriverHelper.customDriverData == driverData.second) {
46 driverViewModel.setSelectedDriverIndex(0)
47 }
48 driverViewModel.driversToDelete.add(driverData.first)
49 driverViewModel.removeDriver(driverData)
50 notifyItemRemoved(position)
51 notifyItemChanged(driverViewModel.selectedDriver)
52 }
53
54 inner class DriverViewHolder(val binding: CardDriverOptionBinding) :
55 RecyclerView.ViewHolder(binding.root) {
56 private lateinit var driverData: Pair<String, GpuDriverMetadata>
57
58 fun bind(driverData: Pair<String, GpuDriverMetadata>) {
59 this.driverData = driverData
60 val driver = driverData.second
61
62 binding.apply {
63 radioButton.isChecked = driverViewModel.selectedDriver == bindingAdapterPosition
64 root.setOnClickListener {
65 onSelectDriver(bindingAdapterPosition)
66 }
67 buttonDelete.setOnClickListener {
68 onDeleteDriver(driverData, bindingAdapterPosition)
69 }
70
71 // Delay marquee by 3s
72 title.postDelayed(
73 {
74 title.isSelected = true
75 title.ellipsize = TextUtils.TruncateAt.MARQUEE
76 version.isSelected = true
77 version.ellipsize = TextUtils.TruncateAt.MARQUEE
78 description.isSelected = true
79 description.ellipsize = TextUtils.TruncateAt.MARQUEE
80 },
81 3000
82 )
83 if (driver.name == null) {
84 title.setText(R.string.system_gpu_driver)
85 description.text = ""
86 version.text = ""
87 version.visibility = View.GONE
88 description.visibility = View.GONE
89 buttonDelete.visibility = View.GONE
90 } else {
91 title.text = driver.name
92 version.text = driver.version
93 description.text = driver.description
94 version.visibility = View.VISIBLE
95 description.visibility = View.VISIBLE
96 buttonDelete.visibility = View.VISIBLE
97 }
98 }
99 }
100 }
101
102 private class DiffCallback : DiffUtil.ItemCallback<Pair<String, GpuDriverMetadata>>() {
103 override fun areItemsTheSame(
104 oldItem: Pair<String, GpuDriverMetadata>,
105 newItem: Pair<String, GpuDriverMetadata>
106 ): Boolean {
107 return oldItem.first == newItem.first
108 }
109
110 override fun areContentsTheSame(
111 oldItem: Pair<String, GpuDriverMetadata>,
112 newItem: Pair<String, GpuDriverMetadata>
113 ): Boolean {
114 return oldItem.second == newItem.second
115 }
116 }
117}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/DriverManagerFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/DriverManagerFragment.kt
new file mode 100644
index 000000000..10b1d3547
--- /dev/null
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/DriverManagerFragment.kt
@@ -0,0 +1,185 @@
1// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
2// SPDX-License-Identifier: GPL-2.0-or-later
3
4package org.yuzu.yuzu_emu.fragments
5
6import android.os.Bundle
7import android.view.LayoutInflater
8import android.view.View
9import android.view.ViewGroup
10import androidx.activity.result.contract.ActivityResultContracts
11import androidx.core.view.ViewCompat
12import androidx.core.view.WindowInsetsCompat
13import androidx.core.view.updatePadding
14import androidx.fragment.app.Fragment
15import androidx.fragment.app.activityViewModels
16import androidx.lifecycle.lifecycleScope
17import androidx.navigation.findNavController
18import androidx.recyclerview.widget.GridLayoutManager
19import com.google.android.material.transition.MaterialSharedAxis
20import kotlinx.coroutines.flow.collectLatest
21import kotlinx.coroutines.launch
22import org.yuzu.yuzu_emu.R
23import org.yuzu.yuzu_emu.adapters.DriverAdapter
24import org.yuzu.yuzu_emu.databinding.FragmentDriverManagerBinding
25import org.yuzu.yuzu_emu.model.DriverViewModel
26import org.yuzu.yuzu_emu.model.HomeViewModel
27import org.yuzu.yuzu_emu.utils.FileUtil
28import org.yuzu.yuzu_emu.utils.GpuDriverHelper
29import java.io.IOException
30
31class DriverManagerFragment : Fragment() {
32 private var _binding: FragmentDriverManagerBinding? = null
33 private val binding get() = _binding!!
34
35 private val homeViewModel: HomeViewModel by activityViewModels()
36 private val driverViewModel: DriverViewModel by activityViewModels()
37
38 override fun onCreate(savedInstanceState: Bundle?) {
39 super.onCreate(savedInstanceState)
40 enterTransition = MaterialSharedAxis(MaterialSharedAxis.X, true)
41 returnTransition = MaterialSharedAxis(MaterialSharedAxis.X, false)
42 reenterTransition = MaterialSharedAxis(MaterialSharedAxis.X, false)
43 }
44
45 override fun onCreateView(
46 inflater: LayoutInflater,
47 container: ViewGroup?,
48 savedInstanceState: Bundle?
49 ): View {
50 _binding = FragmentDriverManagerBinding.inflate(inflater)
51 return binding.root
52 }
53
54 override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
55 super.onViewCreated(view, savedInstanceState)
56 homeViewModel.setNavigationVisibility(visible = false, animated = true)
57 homeViewModel.setStatusBarShadeVisibility(visible = false)
58
59 if (!driverViewModel.isInteractionAllowed) {
60 DriversLoadingDialogFragment().show(
61 childFragmentManager,
62 DriversLoadingDialogFragment.TAG
63 )
64 }
65
66 binding.toolbarDrivers.setNavigationOnClickListener {
67 binding.root.findNavController().popBackStack()
68 }
69
70 binding.buttonInstall.setOnClickListener {
71 getDriver.launch(arrayOf("application/zip"))
72 }
73
74 binding.listDrivers.apply {
75 layoutManager = GridLayoutManager(
76 requireContext(),
77 resources.getInteger(R.integer.grid_columns)
78 )
79 adapter = DriverAdapter(driverViewModel)
80 }
81
82 viewLifecycleOwner.lifecycleScope.apply {
83 launch {
84 driverViewModel.driverList.collectLatest {
85 (binding.listDrivers.adapter as DriverAdapter).submitList(it)
86 }
87 }
88 launch {
89 driverViewModel.newDriverInstalled.collect {
90 if (_binding != null && it) {
91 (binding.listDrivers.adapter as DriverAdapter).apply {
92 notifyItemChanged(driverViewModel.previouslySelectedDriver)
93 notifyItemChanged(driverViewModel.selectedDriver)
94 driverViewModel.setNewDriverInstalled(false)
95 }
96 }
97 }
98 }
99 }
100
101 setInsets()
102 }
103
104 // Start installing requested driver
105 override fun onStop() {
106 super.onStop()
107 driverViewModel.onCloseDriverManager()
108 }
109
110 private fun setInsets() =
111 ViewCompat.setOnApplyWindowInsetsListener(
112 binding.root
113 ) { _: View, windowInsets: WindowInsetsCompat ->
114 val barInsets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars())
115 val cutoutInsets = windowInsets.getInsets(WindowInsetsCompat.Type.displayCutout())
116
117 val leftInsets = barInsets.left + cutoutInsets.left
118 val rightInsets = barInsets.right + cutoutInsets.right
119
120 val mlpAppBar = binding.toolbarDrivers.layoutParams as ViewGroup.MarginLayoutParams
121 mlpAppBar.leftMargin = leftInsets
122 mlpAppBar.rightMargin = rightInsets
123 binding.toolbarDrivers.layoutParams = mlpAppBar
124
125 val mlplistDrivers = binding.listDrivers.layoutParams as ViewGroup.MarginLayoutParams
126 mlplistDrivers.leftMargin = leftInsets
127 mlplistDrivers.rightMargin = rightInsets
128 binding.listDrivers.layoutParams = mlplistDrivers
129
130 val fabSpacing = resources.getDimensionPixelSize(R.dimen.spacing_fab)
131 val mlpFab =
132 binding.buttonInstall.layoutParams as ViewGroup.MarginLayoutParams
133 mlpFab.leftMargin = leftInsets + fabSpacing
134 mlpFab.rightMargin = rightInsets + fabSpacing
135 mlpFab.bottomMargin = barInsets.bottom + fabSpacing
136 binding.buttonInstall.layoutParams = mlpFab
137
138 binding.listDrivers.updatePadding(
139 bottom = barInsets.bottom +
140 resources.getDimensionPixelSize(R.dimen.spacing_bottom_list_fab)
141 )
142
143 windowInsets
144 }
145
146 private val getDriver =
147 registerForActivityResult(ActivityResultContracts.OpenDocument()) { result ->
148 if (result == null) {
149 return@registerForActivityResult
150 }
151
152 IndeterminateProgressDialogFragment.newInstance(
153 requireActivity(),
154 R.string.installing_driver,
155 false
156 ) {
157 // Ignore file exceptions when a user selects an invalid zip
158 try {
159 GpuDriverHelper.copyDriverToInternalStorage(result)
160 } catch (_: IOException) {
161 return@newInstance getString(R.string.select_gpu_driver_error)
162 }
163
164 val driverData = GpuDriverHelper.customDriverData
165 if (driverData.name == null) {
166 return@newInstance getString(R.string.select_gpu_driver_error)
167 }
168
169 val driverInList =
170 driverViewModel.driverList.value.firstOrNull { it.second == driverData }
171 if (driverInList != null) {
172 return@newInstance getString(R.string.driver_already_installed)
173 } else {
174 driverViewModel.addDriver(
175 Pair(
176 "${GpuDriverHelper.driverStoragePath}/${FileUtil.getFilename(result)}",
177 driverData
178 )
179 )
180 driverViewModel.setNewDriverInstalled(true)
181 }
182 return@newInstance Any()
183 }.show(childFragmentManager, IndeterminateProgressDialogFragment.TAG)
184 }
185}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/DriversLoadingDialogFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/DriversLoadingDialogFragment.kt
new file mode 100644
index 000000000..f8c34346a
--- /dev/null
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/DriversLoadingDialogFragment.kt
@@ -0,0 +1,75 @@
1// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
2// SPDX-License-Identifier: GPL-2.0-or-later
3
4package org.yuzu.yuzu_emu.fragments
5
6import android.app.Dialog
7import android.os.Bundle
8import android.view.LayoutInflater
9import android.view.View
10import android.view.ViewGroup
11import androidx.fragment.app.DialogFragment
12import androidx.fragment.app.activityViewModels
13import androidx.lifecycle.Lifecycle
14import androidx.lifecycle.lifecycleScope
15import androidx.lifecycle.repeatOnLifecycle
16import com.google.android.material.dialog.MaterialAlertDialogBuilder
17import kotlinx.coroutines.launch
18import org.yuzu.yuzu_emu.R
19import org.yuzu.yuzu_emu.databinding.DialogProgressBarBinding
20import org.yuzu.yuzu_emu.model.DriverViewModel
21
22class DriversLoadingDialogFragment : DialogFragment() {
23 private val driverViewModel: DriverViewModel by activityViewModels()
24
25 private lateinit var binding: DialogProgressBarBinding
26
27 override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
28 binding = DialogProgressBarBinding.inflate(layoutInflater)
29 binding.progressBar.isIndeterminate = true
30
31 isCancelable = false
32
33 return MaterialAlertDialogBuilder(requireContext())
34 .setTitle(R.string.loading)
35 .setView(binding.root)
36 .create()
37 }
38
39 override fun onCreateView(
40 inflater: LayoutInflater,
41 container: ViewGroup?,
42 savedInstanceState: Bundle?
43 ): View = binding.root
44
45 override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
46 super.onViewCreated(view, savedInstanceState)
47 viewLifecycleOwner.lifecycleScope.apply {
48 launch {
49 repeatOnLifecycle(Lifecycle.State.RESUMED) {
50 driverViewModel.areDriversLoading.collect { checkForDismiss() }
51 }
52 }
53 launch {
54 repeatOnLifecycle(Lifecycle.State.RESUMED) {
55 driverViewModel.isDriverReady.collect { checkForDismiss() }
56 }
57 }
58 launch {
59 repeatOnLifecycle(Lifecycle.State.RESUMED) {
60 driverViewModel.isDeletingDrivers.collect { checkForDismiss() }
61 }
62 }
63 }
64 }
65
66 private fun checkForDismiss() {
67 if (driverViewModel.isInteractionAllowed) {
68 dismiss()
69 }
70 }
71
72 companion object {
73 const val TAG = "DriversLoadingDialogFragment"
74 }
75}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/EmulationFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/EmulationFragment.kt
index e6ad2aa77..598a9d42b 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/EmulationFragment.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/EmulationFragment.kt
@@ -39,6 +39,7 @@ import androidx.window.layout.WindowLayoutInfo
39import com.google.android.material.dialog.MaterialAlertDialogBuilder 39import com.google.android.material.dialog.MaterialAlertDialogBuilder
40import com.google.android.material.slider.Slider 40import com.google.android.material.slider.Slider
41import kotlinx.coroutines.Dispatchers 41import kotlinx.coroutines.Dispatchers
42import kotlinx.coroutines.flow.collect
42import kotlinx.coroutines.flow.collectLatest 43import kotlinx.coroutines.flow.collectLatest
43import kotlinx.coroutines.launch 44import kotlinx.coroutines.launch
44import org.yuzu.yuzu_emu.HomeNavigationDirections 45import org.yuzu.yuzu_emu.HomeNavigationDirections
@@ -50,6 +51,7 @@ import org.yuzu.yuzu_emu.databinding.DialogOverlayAdjustBinding
50import org.yuzu.yuzu_emu.databinding.FragmentEmulationBinding 51import org.yuzu.yuzu_emu.databinding.FragmentEmulationBinding
51import org.yuzu.yuzu_emu.features.settings.model.IntSetting 52import org.yuzu.yuzu_emu.features.settings.model.IntSetting
52import org.yuzu.yuzu_emu.features.settings.model.Settings 53import org.yuzu.yuzu_emu.features.settings.model.Settings
54import org.yuzu.yuzu_emu.model.DriverViewModel
53import org.yuzu.yuzu_emu.model.Game 55import org.yuzu.yuzu_emu.model.Game
54import org.yuzu.yuzu_emu.model.EmulationViewModel 56import org.yuzu.yuzu_emu.model.EmulationViewModel
55import org.yuzu.yuzu_emu.overlay.InputOverlay 57import org.yuzu.yuzu_emu.overlay.InputOverlay
@@ -70,6 +72,7 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
70 private lateinit var game: Game 72 private lateinit var game: Game
71 73
72 private val emulationViewModel: EmulationViewModel by activityViewModels() 74 private val emulationViewModel: EmulationViewModel by activityViewModels()
75 private val driverViewModel: DriverViewModel by activityViewModels()
73 76
74 private var isInFoldableLayout = false 77 private var isInFoldableLayout = false
75 78
@@ -299,6 +302,21 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
299 } 302 }
300 } 303 }
301 } 304 }
305 launch {
306 repeatOnLifecycle(Lifecycle.State.RESUMED) {
307 driverViewModel.isDriverReady.collect {
308 if (it && !emulationState.isRunning) {
309 if (!DirectoryInitialization.areDirectoriesReady) {
310 DirectoryInitialization.start()
311 }
312
313 updateScreenLayout()
314
315 emulationState.run(emulationActivity!!.isActivityRecreated)
316 }
317 }
318 }
319 }
302 } 320 }
303 } 321 }
304 322
@@ -332,17 +350,6 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
332 } 350 }
333 } 351 }
334 352
335 override fun onResume() {
336 super.onResume()
337 if (!DirectoryInitialization.areDirectoriesReady) {
338 DirectoryInitialization.start()
339 }
340
341 updateScreenLayout()
342
343 emulationState.run(emulationActivity!!.isActivityRecreated)
344 }
345
346 override fun onPause() { 353 override fun onPause() {
347 if (emulationState.isRunning && emulationActivity?.isInPictureInPictureMode != true) { 354 if (emulationState.isRunning && emulationActivity?.isInPictureInPictureMode != true) {
348 emulationState.pause() 355 emulationState.pause()
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/HomeSettingsFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/HomeSettingsFragment.kt
index 18857db2d..fd9785075 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/HomeSettingsFragment.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/HomeSettingsFragment.kt
@@ -5,7 +5,6 @@ package org.yuzu.yuzu_emu.fragments
5 5
6import android.Manifest 6import android.Manifest
7import android.content.ActivityNotFoundException 7import android.content.ActivityNotFoundException
8import android.content.DialogInterface
9import android.content.Intent 8import android.content.Intent
10import android.content.pm.PackageManager 9import android.content.pm.PackageManager
11import android.os.Bundle 10import android.os.Bundle
@@ -28,7 +27,6 @@ import androidx.fragment.app.activityViewModels
28import androidx.navigation.findNavController 27import androidx.navigation.findNavController
29import androidx.navigation.fragment.findNavController 28import androidx.navigation.fragment.findNavController
30import androidx.recyclerview.widget.LinearLayoutManager 29import androidx.recyclerview.widget.LinearLayoutManager
31import com.google.android.material.dialog.MaterialAlertDialogBuilder
32import com.google.android.material.transition.MaterialSharedAxis 30import com.google.android.material.transition.MaterialSharedAxis
33import org.yuzu.yuzu_emu.BuildConfig 31import org.yuzu.yuzu_emu.BuildConfig
34import org.yuzu.yuzu_emu.HomeNavigationDirections 32import org.yuzu.yuzu_emu.HomeNavigationDirections
@@ -37,6 +35,7 @@ import org.yuzu.yuzu_emu.adapters.HomeSettingAdapter
37import org.yuzu.yuzu_emu.databinding.FragmentHomeSettingsBinding 35import org.yuzu.yuzu_emu.databinding.FragmentHomeSettingsBinding
38import org.yuzu.yuzu_emu.features.DocumentProvider 36import org.yuzu.yuzu_emu.features.DocumentProvider
39import org.yuzu.yuzu_emu.features.settings.model.Settings 37import org.yuzu.yuzu_emu.features.settings.model.Settings
38import org.yuzu.yuzu_emu.model.DriverViewModel
40import org.yuzu.yuzu_emu.model.HomeSetting 39import org.yuzu.yuzu_emu.model.HomeSetting
41import org.yuzu.yuzu_emu.model.HomeViewModel 40import org.yuzu.yuzu_emu.model.HomeViewModel
42import org.yuzu.yuzu_emu.ui.main.MainActivity 41import org.yuzu.yuzu_emu.ui.main.MainActivity
@@ -50,6 +49,7 @@ class HomeSettingsFragment : Fragment() {
50 private lateinit var mainActivity: MainActivity 49 private lateinit var mainActivity: MainActivity
51 50
52 private val homeViewModel: HomeViewModel by activityViewModels() 51 private val homeViewModel: HomeViewModel by activityViewModels()
52 private val driverViewModel: DriverViewModel by activityViewModels()
53 53
54 override fun onCreate(savedInstanceState: Bundle?) { 54 override fun onCreate(savedInstanceState: Bundle?) {
55 super.onCreate(savedInstanceState) 55 super.onCreate(savedInstanceState)
@@ -107,13 +107,17 @@ class HomeSettingsFragment : Fragment() {
107 ) 107 )
108 add( 108 add(
109 HomeSetting( 109 HomeSetting(
110 R.string.install_gpu_driver, 110 R.string.gpu_driver_manager,
111 R.string.install_gpu_driver_description, 111 R.string.install_gpu_driver_description,
112 R.drawable.ic_exit, 112 R.drawable.ic_build,
113 { driverInstaller() }, 113 {
114 binding.root.findNavController()
115 .navigate(R.id.action_homeSettingsFragment_to_driverManagerFragment)
116 },
114 { GpuDriverHelper.supportsCustomDriverLoading() }, 117 { GpuDriverHelper.supportsCustomDriverLoading() },
115 R.string.custom_driver_not_supported, 118 R.string.custom_driver_not_supported,
116 R.string.custom_driver_not_supported_description 119 R.string.custom_driver_not_supported_description,
120 driverViewModel.selectedDriverMetadata
117 ) 121 )
118 ) 122 )
119 add( 123 add(
@@ -292,31 +296,6 @@ class HomeSettingsFragment : Fragment() {
292 } 296 }
293 } 297 }
294 298
295 private fun driverInstaller() {
296 // Get the driver name for the dialog message.
297 var driverName = GpuDriverHelper.customDriverName
298 if (driverName == null) {
299 driverName = getString(R.string.system_gpu_driver)
300 }
301
302 MaterialAlertDialogBuilder(requireContext())
303 .setTitle(getString(R.string.select_gpu_driver_title))
304 .setMessage(driverName)
305 .setNegativeButton(android.R.string.cancel, null)
306 .setNeutralButton(R.string.select_gpu_driver_default) { _: DialogInterface?, _: Int ->
307 GpuDriverHelper.installDefaultDriver()
308 Toast.makeText(
309 requireContext(),
310 R.string.select_gpu_driver_use_default,
311 Toast.LENGTH_SHORT
312 ).show()
313 }
314 .setPositiveButton(R.string.select_gpu_driver_install) { _: DialogInterface?, _: Int ->
315 mainActivity.getDriver.launch(arrayOf("application/zip"))
316 }
317 .show()
318 }
319
320 private fun shareLog() { 299 private fun shareLog() {
321 val file = DocumentFile.fromSingleUri( 300 val file = DocumentFile.fromSingleUri(
322 mainActivity, 301 mainActivity,
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/IndeterminateProgressDialogFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/IndeterminateProgressDialogFragment.kt
index f128deda8..7e467814d 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/IndeterminateProgressDialogFragment.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/IndeterminateProgressDialogFragment.kt
@@ -10,8 +10,8 @@ import android.view.View
10import android.view.ViewGroup 10import android.view.ViewGroup
11import android.widget.Toast 11import android.widget.Toast
12import androidx.appcompat.app.AlertDialog 12import androidx.appcompat.app.AlertDialog
13import androidx.appcompat.app.AppCompatActivity
14import androidx.fragment.app.DialogFragment 13import androidx.fragment.app.DialogFragment
14import androidx.fragment.app.FragmentActivity
15import androidx.fragment.app.activityViewModels 15import androidx.fragment.app.activityViewModels
16import androidx.lifecycle.Lifecycle 16import androidx.lifecycle.Lifecycle
17import androidx.lifecycle.ViewModelProvider 17import androidx.lifecycle.ViewModelProvider
@@ -78,6 +78,10 @@ class IndeterminateProgressDialogFragment : DialogFragment() {
78 requireActivity().supportFragmentManager, 78 requireActivity().supportFragmentManager,
79 MessageDialogFragment.TAG 79 MessageDialogFragment.TAG
80 ) 80 )
81
82 else -> {
83 // Do nothing
84 }
81 } 85 }
82 taskViewModel.clear() 86 taskViewModel.clear()
83 } 87 }
@@ -115,7 +119,7 @@ class IndeterminateProgressDialogFragment : DialogFragment() {
115 private const val CANCELLABLE = "Cancellable" 119 private const val CANCELLABLE = "Cancellable"
116 120
117 fun newInstance( 121 fun newInstance(
118 activity: AppCompatActivity, 122 activity: FragmentActivity,
119 titleId: Int, 123 titleId: Int,
120 cancellable: Boolean = false, 124 cancellable: Boolean = false,
121 task: () -> Any 125 task: () -> Any
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/DriverViewModel.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/DriverViewModel.kt
new file mode 100644
index 000000000..62945ad65
--- /dev/null
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/DriverViewModel.kt
@@ -0,0 +1,158 @@
1// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
2// SPDX-License-Identifier: GPL-2.0-or-later
3
4package org.yuzu.yuzu_emu.model
5
6import androidx.lifecycle.ViewModel
7import androidx.lifecycle.viewModelScope
8import kotlinx.coroutines.Dispatchers
9import kotlinx.coroutines.flow.MutableStateFlow
10import kotlinx.coroutines.flow.StateFlow
11import kotlinx.coroutines.launch
12import kotlinx.coroutines.withContext
13import org.yuzu.yuzu_emu.R
14import org.yuzu.yuzu_emu.YuzuApplication
15import org.yuzu.yuzu_emu.utils.FileUtil
16import org.yuzu.yuzu_emu.utils.GpuDriverHelper
17import org.yuzu.yuzu_emu.utils.GpuDriverMetadata
18import java.io.BufferedOutputStream
19import java.io.File
20
21class DriverViewModel : ViewModel() {
22 private val _areDriversLoading = MutableStateFlow(false)
23 val areDriversLoading: StateFlow<Boolean> get() = _areDriversLoading
24
25 private val _isDriverReady = MutableStateFlow(true)
26 val isDriverReady: StateFlow<Boolean> get() = _isDriverReady
27
28 private val _isDeletingDrivers = MutableStateFlow(false)
29 val isDeletingDrivers: StateFlow<Boolean> get() = _isDeletingDrivers
30
31 private val _driverList = MutableStateFlow(mutableListOf<Pair<String, GpuDriverMetadata>>())
32 val driverList: StateFlow<MutableList<Pair<String, GpuDriverMetadata>>> get() = _driverList
33
34 var previouslySelectedDriver = 0
35 var selectedDriver = -1
36
37 private val _selectedDriverMetadata =
38 MutableStateFlow(
39 GpuDriverHelper.customDriverData.name
40 ?: YuzuApplication.appContext.getString(R.string.system_gpu_driver)
41 )
42 val selectedDriverMetadata: StateFlow<String> get() = _selectedDriverMetadata
43
44 private val _newDriverInstalled = MutableStateFlow(false)
45 val newDriverInstalled: StateFlow<Boolean> get() = _newDriverInstalled
46
47 val driversToDelete = mutableListOf<String>()
48
49 val isInteractionAllowed
50 get() = !areDriversLoading.value && isDriverReady.value && !isDeletingDrivers.value
51
52 init {
53 _areDriversLoading.value = true
54 viewModelScope.launch {
55 withContext(Dispatchers.IO) {
56 val drivers = GpuDriverHelper.getDrivers()
57 val currentDriverMetadata = GpuDriverHelper.customDriverData
58 for (i in drivers.indices) {
59 if (drivers[i].second == currentDriverMetadata) {
60 setSelectedDriverIndex(i)
61 break
62 }
63 }
64
65 // If a user had installed a driver before the manager was implemented, this zips
66 // the installed driver to UserData/gpu_drivers/CustomDriver.zip so that it can
67 // be indexed and exported as expected.
68 if (selectedDriver == -1) {
69 val driverToSave =
70 File(GpuDriverHelper.driverStoragePath, "CustomDriver.zip")
71 driverToSave.createNewFile()
72 FileUtil.zipFromInternalStorage(
73 File(GpuDriverHelper.driverInstallationPath!!),
74 GpuDriverHelper.driverInstallationPath!!,
75 BufferedOutputStream(driverToSave.outputStream())
76 )
77 drivers.add(Pair(driverToSave.path, currentDriverMetadata))
78 setSelectedDriverIndex(drivers.size - 1)
79 }
80
81 _driverList.value = drivers
82 _areDriversLoading.value = false
83 }
84 }
85 }
86
87 fun setSelectedDriverIndex(value: Int) {
88 if (selectedDriver != -1) {
89 previouslySelectedDriver = selectedDriver
90 }
91 selectedDriver = value
92 }
93
94 fun setNewDriverInstalled(value: Boolean) {
95 _newDriverInstalled.value = value
96 }
97
98 fun addDriver(driverData: Pair<String, GpuDriverMetadata>) {
99 val driverIndex = _driverList.value.indexOfFirst { it == driverData }
100 if (driverIndex == -1) {
101 setSelectedDriverIndex(_driverList.value.size)
102 _driverList.value.add(driverData)
103 _selectedDriverMetadata.value = driverData.second.name
104 ?: YuzuApplication.appContext.getString(R.string.system_gpu_driver)
105 } else {
106 setSelectedDriverIndex(driverIndex)
107 }
108 }
109
110 fun removeDriver(driverData: Pair<String, GpuDriverMetadata>) {
111 _driverList.value.remove(driverData)
112 }
113
114 fun onCloseDriverManager() {
115 _isDeletingDrivers.value = true
116 viewModelScope.launch {
117 withContext(Dispatchers.IO) {
118 driversToDelete.forEach {
119 val driver = File(it)
120 if (driver.exists()) {
121 driver.delete()
122 }
123 }
124 driversToDelete.clear()
125 _isDeletingDrivers.value = false
126 }
127 }
128
129 if (GpuDriverHelper.customDriverData == driverList.value[selectedDriver].second) {
130 return
131 }
132
133 _isDriverReady.value = false
134 viewModelScope.launch {
135 withContext(Dispatchers.IO) {
136 if (selectedDriver == 0) {
137 GpuDriverHelper.installDefaultDriver()
138 setDriverReady()
139 return@withContext
140 }
141
142 val driverToInstall = File(driverList.value[selectedDriver].first)
143 if (driverToInstall.exists()) {
144 GpuDriverHelper.installCustomDriver(driverToInstall)
145 } else {
146 GpuDriverHelper.installDefaultDriver()
147 }
148 setDriverReady()
149 }
150 }
151 }
152
153 private fun setDriverReady() {
154 _isDriverReady.value = true
155 _selectedDriverMetadata.value = GpuDriverHelper.customDriverData.name
156 ?: YuzuApplication.appContext.getString(R.string.system_gpu_driver)
157 }
158}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/main/MainActivity.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/main/MainActivity.kt
index ac96c8207..233aa4101 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/main/MainActivity.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/main/MainActivity.kt
@@ -29,12 +29,10 @@ import androidx.navigation.fragment.NavHostFragment
29import androidx.navigation.ui.setupWithNavController 29import androidx.navigation.ui.setupWithNavController
30import androidx.preference.PreferenceManager 30import androidx.preference.PreferenceManager
31import com.google.android.material.color.MaterialColors 31import com.google.android.material.color.MaterialColors
32import com.google.android.material.dialog.MaterialAlertDialogBuilder
33import com.google.android.material.navigation.NavigationBarView 32import com.google.android.material.navigation.NavigationBarView
34import kotlinx.coroutines.CoroutineScope 33import kotlinx.coroutines.CoroutineScope
35import java.io.File 34import java.io.File
36import java.io.FilenameFilter 35import java.io.FilenameFilter
37import java.io.IOException
38import kotlinx.coroutines.Dispatchers 36import kotlinx.coroutines.Dispatchers
39import kotlinx.coroutines.launch 37import kotlinx.coroutines.launch
40import kotlinx.coroutines.withContext 38import kotlinx.coroutines.withContext
@@ -43,7 +41,6 @@ import org.yuzu.yuzu_emu.NativeLibrary
43import org.yuzu.yuzu_emu.R 41import org.yuzu.yuzu_emu.R
44import org.yuzu.yuzu_emu.activities.EmulationActivity 42import org.yuzu.yuzu_emu.activities.EmulationActivity
45import org.yuzu.yuzu_emu.databinding.ActivityMainBinding 43import org.yuzu.yuzu_emu.databinding.ActivityMainBinding
46import org.yuzu.yuzu_emu.databinding.DialogProgressBarBinding
47import org.yuzu.yuzu_emu.features.DocumentProvider 44import org.yuzu.yuzu_emu.features.DocumentProvider
48import org.yuzu.yuzu_emu.features.settings.model.Settings 45import org.yuzu.yuzu_emu.features.settings.model.Settings
49import org.yuzu.yuzu_emu.fragments.IndeterminateProgressDialogFragment 46import org.yuzu.yuzu_emu.fragments.IndeterminateProgressDialogFragment
@@ -346,7 +343,7 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
346 result, 343 result,
347 dstPath, 344 dstPath,
348 "prod.keys" 345 "prod.keys"
349 ) 346 ) != null
350 ) { 347 ) {
351 if (NativeLibrary.reloadKeys()) { 348 if (NativeLibrary.reloadKeys()) {
352 Toast.makeText( 349 Toast.makeText(
@@ -448,7 +445,7 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
448 result, 445 result,
449 dstPath, 446 dstPath,
450 "key_retail.bin" 447 "key_retail.bin"
451 ) 448 ) != null
452 ) { 449 ) {
453 if (NativeLibrary.reloadKeys()) { 450 if (NativeLibrary.reloadKeys()) {
454 Toast.makeText( 451 Toast.makeText(
@@ -467,59 +464,6 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
467 } 464 }
468 } 465 }
469 466
470 val getDriver =
471 registerForActivityResult(ActivityResultContracts.OpenDocument()) { result ->
472 if (result == null) {
473 return@registerForActivityResult
474 }
475
476 val takeFlags =
477 Intent.FLAG_GRANT_WRITE_URI_PERMISSION or Intent.FLAG_GRANT_READ_URI_PERMISSION
478 contentResolver.takePersistableUriPermission(
479 result,
480 takeFlags
481 )
482
483 val progressBinding = DialogProgressBarBinding.inflate(layoutInflater)
484 progressBinding.progressBar.isIndeterminate = true
485 val installationDialog = MaterialAlertDialogBuilder(this)
486 .setTitle(R.string.installing_driver)
487 .setView(progressBinding.root)
488 .show()
489
490 lifecycleScope.launch {
491 withContext(Dispatchers.IO) {
492 // Ignore file exceptions when a user selects an invalid zip
493 try {
494 GpuDriverHelper.installCustomDriver(result)
495 } catch (_: IOException) {
496 }
497
498 withContext(Dispatchers.Main) {
499 installationDialog.dismiss()
500
501 val driverData = GpuDriverHelper.customDriverData
502 if (driverData.name != null) {
503 Toast.makeText(
504 applicationContext,
505 getString(
506 R.string.select_gpu_driver_install_success,
507 driverData.name
508 ),
509 Toast.LENGTH_SHORT
510 ).show()
511 } else {
512 Toast.makeText(
513 applicationContext,
514 R.string.select_gpu_driver_error,
515 Toast.LENGTH_LONG
516 ).show()
517 }
518 }
519 }
520 }
521 }
522
523 val installGameUpdate = registerForActivityResult( 467 val installGameUpdate = registerForActivityResult(
524 ActivityResultContracts.OpenMultipleDocuments() 468 ActivityResultContracts.OpenMultipleDocuments()
525 ) { documents: List<Uri> -> 469 ) { documents: List<Uri> ->
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/FileUtil.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/FileUtil.kt
index a5f89bba6..5ee74a52c 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/FileUtil.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/FileUtil.kt
@@ -10,7 +10,6 @@ import androidx.documentfile.provider.DocumentFile
10import kotlinx.coroutines.flow.StateFlow 10import kotlinx.coroutines.flow.StateFlow
11import java.io.BufferedInputStream 11import java.io.BufferedInputStream
12import java.io.File 12import java.io.File
13import java.io.FileOutputStream
14import java.io.IOException 13import java.io.IOException
15import java.io.InputStream 14import java.io.InputStream
16import java.net.URLDecoder 15import java.net.URLDecoder
@@ -20,6 +19,8 @@ import org.yuzu.yuzu_emu.YuzuApplication
20import org.yuzu.yuzu_emu.model.MinimalDocumentFile 19import org.yuzu.yuzu_emu.model.MinimalDocumentFile
21import org.yuzu.yuzu_emu.model.TaskState 20import org.yuzu.yuzu_emu.model.TaskState
22import java.io.BufferedOutputStream 21import java.io.BufferedOutputStream
22import java.lang.NullPointerException
23import java.nio.charset.StandardCharsets
23import java.util.zip.ZipOutputStream 24import java.util.zip.ZipOutputStream
24 25
25object FileUtil { 26object FileUtil {
@@ -243,43 +244,38 @@ object FileUtil {
243 return size 244 return size
244 } 245 }
245 246
247 /**
248 * Creates an input stream with a given [Uri] and copies its data to the given path. This will
249 * overwrite any pre-existing files.
250 *
251 * @param sourceUri The [Uri] to copy data from
252 * @param destinationParentPath Destination directory
253 * @param destinationFilename Optionally renames the file once copied
254 */
246 fun copyUriToInternalStorage( 255 fun copyUriToInternalStorage(
247 sourceUri: Uri?, 256 sourceUri: Uri,
248 destinationParentPath: String, 257 destinationParentPath: String,
249 destinationFilename: String 258 destinationFilename: String = ""
250 ): Boolean { 259 ): File? =
251 var input: InputStream? = null
252 var output: FileOutputStream? = null
253 try { 260 try {
254 input = context.contentResolver.openInputStream(sourceUri!!) 261 val fileName =
255 output = FileOutputStream("$destinationParentPath/$destinationFilename") 262 if (destinationFilename == "") getFilename(sourceUri) else "/$destinationFilename"
256 val buffer = ByteArray(1024) 263 val inputStream = context.contentResolver.openInputStream(sourceUri)!!
257 var len: Int 264
258 while (input!!.read(buffer).also { len = it } != -1) { 265 val destinationFile = File("$destinationParentPath$fileName")
259 output.write(buffer, 0, len) 266 if (destinationFile.exists()) {
260 } 267 destinationFile.delete()
261 output.flush()
262 return true
263 } catch (e: Exception) {
264 Log.error("[FileUtil]: Cannot copy file, error: " + e.message)
265 } finally {
266 if (input != null) {
267 try {
268 input.close()
269 } catch (e: IOException) {
270 Log.error("[FileUtil]: Cannot close input file, error: " + e.message)
271 }
272 } 268 }
273 if (output != null) { 269
274 try { 270 destinationFile.outputStream().use { fos ->
275 output.close() 271 inputStream.use { it.copyTo(fos) }
276 } catch (e: IOException) {
277 Log.error("[FileUtil]: Cannot close output file, error: " + e.message)
278 }
279 } 272 }
273 destinationFile
274 } catch (e: IOException) {
275 null
276 } catch (e: NullPointerException) {
277 null
280 } 278 }
281 return false
282 }
283 279
284 /** 280 /**
285 * Extracts the given zip file into the given directory. 281 * Extracts the given zip file into the given directory.
@@ -365,4 +361,12 @@ object FileUtil {
365 return fileName.substring(fileName.lastIndexOf(".") + 1) 361 return fileName.substring(fileName.lastIndexOf(".") + 1)
366 .lowercase() 362 .lowercase()
367 } 363 }
364
365 @Throws(IOException::class)
366 fun getStringFromFile(file: File): String =
367 String(file.readBytes(), StandardCharsets.UTF_8)
368
369 @Throws(IOException::class)
370 fun getStringFromInputStream(stream: InputStream): String =
371 String(stream.readBytes(), StandardCharsets.UTF_8)
368} 372}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/GpuDriverHelper.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/GpuDriverHelper.kt
index 296a8f1cf..f6882ce6c 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/GpuDriverHelper.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/GpuDriverHelper.kt
@@ -3,65 +3,32 @@
3 3
4package org.yuzu.yuzu_emu.utils 4package org.yuzu.yuzu_emu.utils
5 5
6import android.content.Context
7import android.net.Uri 6import android.net.Uri
7import android.os.Build
8import java.io.BufferedInputStream 8import java.io.BufferedInputStream
9import java.io.File 9import java.io.File
10import java.io.FileInputStream
11import java.io.FileOutputStream
12import java.io.IOException 10import java.io.IOException
13import java.util.zip.ZipInputStream
14import org.yuzu.yuzu_emu.NativeLibrary 11import org.yuzu.yuzu_emu.NativeLibrary
15import org.yuzu.yuzu_emu.utils.FileUtil.copyUriToInternalStorage
16import org.yuzu.yuzu_emu.YuzuApplication 12import org.yuzu.yuzu_emu.YuzuApplication
13import java.util.zip.ZipException
14import java.util.zip.ZipFile
17 15
18object GpuDriverHelper { 16object GpuDriverHelper {
19 private const val META_JSON_FILENAME = "meta.json" 17 private const val META_JSON_FILENAME = "meta.json"
20 private const val DRIVER_INTERNAL_FILENAME = "gpu_driver.zip"
21 private var fileRedirectionPath: String? = null 18 private var fileRedirectionPath: String? = null
22 private var driverInstallationPath: String? = null 19 var driverInstallationPath: String? = null
23 private var hookLibPath: String? = null 20 private var hookLibPath: String? = null
24 21
25 @Throws(IOException::class) 22 val driverStoragePath get() = DirectoryInitialization.userDirectory!! + "/gpu_drivers/"
26 private fun unzip(zipFilePath: String, destDir: String) {
27 val dir = File(destDir)
28
29 // Create output directory if it doesn't exist
30 if (!dir.exists()) dir.mkdirs()
31
32 // Unpack the files.
33 val inputStream = FileInputStream(zipFilePath)
34 val zis = ZipInputStream(BufferedInputStream(inputStream))
35 val buffer = ByteArray(1024)
36 var ze = zis.nextEntry
37 while (ze != null) {
38 val newFile = File(destDir, ze.name)
39 val canonicalPath = newFile.canonicalPath
40 if (!canonicalPath.startsWith(destDir + ze.name)) {
41 throw SecurityException("Zip file attempted path traversal! " + ze.name)
42 }
43
44 newFile.parentFile!!.mkdirs()
45 val fos = FileOutputStream(newFile)
46 var len: Int
47 while (zis.read(buffer).also { len = it } > 0) {
48 fos.write(buffer, 0, len)
49 }
50 fos.close()
51 zis.closeEntry()
52 ze = zis.nextEntry
53 }
54 zis.closeEntry()
55 }
56 23
57 fun initializeDriverParameters(context: Context) { 24 fun initializeDriverParameters() {
58 try { 25 try {
59 // Initialize the file redirection directory. 26 // Initialize the file redirection directory.
60 fileRedirectionPath = 27 fileRedirectionPath = YuzuApplication.appContext
61 context.getExternalFilesDir(null)!!.canonicalPath + "/gpu/vk_file_redirect/" 28 .getExternalFilesDir(null)!!.canonicalPath + "/gpu/vk_file_redirect/"
62 29
63 // Initialize the driver installation directory. 30 // Initialize the driver installation directory.
64 driverInstallationPath = context.filesDir.canonicalPath + "/gpu_driver/" 31 driverInstallationPath = YuzuApplication.appContext
65 .filesDir.canonicalPath + "/gpu_driver/" 32 .filesDir.canonicalPath + "/gpu_driver/"
66 } catch (e: IOException) { 33 } catch (e: IOException) {
67 throw RuntimeException(e) 34 throw RuntimeException(e)
@@ -71,69 +38,169 @@ object GpuDriverHelper {
71 initializeDirectories() 38 initializeDirectories()
72 39
73 // Initialize hook libraries directory. 40 // Initialize hook libraries directory.
74 hookLibPath = context.applicationInfo.nativeLibraryDir + "/"
75 hookLibPath = YuzuApplication.appContext.applicationInfo.nativeLibraryDir + "/" 41 hookLibPath = YuzuApplication.appContext.applicationInfo.nativeLibraryDir + "/"
76 42
77 // Initialize GPU driver. 43 // Initialize GPU driver.
78 NativeLibrary.initializeGpuDriver( 44 NativeLibrary.initializeGpuDriver(
79 hookLibPath, 45 hookLibPath,
80 driverInstallationPath, 46 driverInstallationPath,
81 customDriverLibraryName, 47 customDriverData.libraryName,
82 fileRedirectionPath 48 fileRedirectionPath
83 ) 49 )
84 } 50 }
85 51
86 fun installDefaultDriver(context: Context) { 52 fun getDrivers(): MutableList<Pair<String, GpuDriverMetadata>> {
53 val driverZips = File(driverStoragePath).listFiles()
54 val drivers: MutableList<Pair<String, GpuDriverMetadata>> =
55 driverZips
56 ?.mapNotNull {
57 val metadata = getMetadataFromZip(it)
58 metadata.name?.let { _ -> Pair(it.path, metadata) }
59 }
60 ?.sortedByDescending { it: Pair<String, GpuDriverMetadata> -> it.second.name }
61 ?.distinct()
62 ?.toMutableList() ?: mutableListOf()
63
64 // TODO: Get system driver information
65 drivers.add(0, Pair("", GpuDriverMetadata()))
66 return drivers
67 }
68
87 fun installDefaultDriver() { 69 fun installDefaultDriver() {
88 // Removing the installed driver will result in the backend using the default system driver. 70 // Removing the installed driver will result in the backend using the default system driver.
89 val driverInstallationDir = File(driverInstallationPath!!) 71 File(driverInstallationPath!!).deleteRecursively()
90 deleteRecursive(driverInstallationDir) 72 initializeDriverParameters()
73 }
74
75 fun copyDriverToInternalStorage(driverUri: Uri): Boolean {
76 // Ensure we have directories.
77 initializeDirectories()
78
79 // Copy the zip file URI to user data
80 val copiedFile =
81 FileUtil.copyUriToInternalStorage(driverUri, driverStoragePath) ?: return false
82
83 // Validate driver
84 val metadata = getMetadataFromZip(copiedFile)
85 if (metadata.name == null) {
86 copiedFile.delete()
87 return false
88 }
89
90 if (metadata.minApi > Build.VERSION.SDK_INT) {
91 copiedFile.delete()
92 return false
93 }
94 return true
91 } 95 }
92 96
93 fun installCustomDriver(context: Context, driverPathUri: Uri?) { 97 /**
98 * Copies driver zip into user data directory so that it can be exported along with
99 * other user data and also unzipped into the installation directory
100 */
101 fun installCustomDriver(driverUri: Uri): Boolean {
94 // Revert to system default in the event the specified driver is bad. 102 // Revert to system default in the event the specified driver is bad.
95 installDefaultDriver() 103 installDefaultDriver()
96 104
97 // Ensure we have directories. 105 // Ensure we have directories.
98 initializeDirectories() 106 initializeDirectories()
99 107
100 // Copy the zip file URI into our private storage. 108 // Copy the zip file URI to user data
101 copyUriToInternalStorage( 109 val copiedFile =
102 context, 110 FileUtil.copyUriToInternalStorage(driverUri, driverStoragePath) ?: return false
103 driverPathUri, 111
104 driverInstallationPath!!, 112 // Validate driver
105 DRIVER_INTERNAL_FILENAME 113 val metadata = getMetadataFromZip(copiedFile)
106 ) 114 if (metadata.name == null) {
115 copiedFile.delete()
116 return false
117 }
118
119 if (metadata.minApi > Build.VERSION.SDK_INT) {
120 copiedFile.delete()
121 return false
122 }
107 123
108 // Unzip the driver. 124 // Unzip the driver.
109 try { 125 try {
110 unzip(driverInstallationPath + DRIVER_INTERNAL_FILENAME, driverInstallationPath!!) 126 FileUtil.unzipToInternalStorage(
127 BufferedInputStream(copiedFile.inputStream()),
128 File(driverInstallationPath!!)
129 )
111 } catch (e: SecurityException) { 130 } catch (e: SecurityException) {
112 return 131 return false
113 } 132 }
114 133
115 // Initialize the driver parameters. 134 // Initialize the driver parameters.
116 initializeDriverParameters(context) 135 initializeDriverParameters()
136
137 return true
117 } 138 }
118 139
119 external fun supportsCustomDriverLoading(): Boolean 140 /**
141 * Unzips driver into installation directory
142 */
143 fun installCustomDriver(driver: File): Boolean {
144 // Revert to system default in the event the specified driver is bad.
145 installDefaultDriver()
120 146
121 // Parse the custom driver metadata to retrieve the name. 147 // Ensure we have directories.
122 val customDriverName: String? 148 initializeDirectories()
123 get() { 149
124 val metadata = GpuDriverMetadata(driverInstallationPath + META_JSON_FILENAME) 150 // Validate driver
125 return metadata.name 151 val metadata = getMetadataFromZip(driver)
152 if (metadata.name == null) {
153 driver.delete()
154 return false
126 } 155 }
127 156
128 // Parse the custom driver metadata to retrieve the library name. 157 // Unzip the driver to the private installation directory
129 private val customDriverLibraryName: String? 158 try {
130 get() { 159 FileUtil.unzipToInternalStorage(
131 // Parse the custom driver metadata to retrieve the library name. 160 BufferedInputStream(driver.inputStream()),
132 val metadata = GpuDriverMetadata(driverInstallationPath + META_JSON_FILENAME) 161 File(driverInstallationPath!!)
133 return metadata.libraryName 162 )
163 } catch (e: SecurityException) {
164 return false
165 }
166
167 // Initialize the driver parameters.
168 initializeDriverParameters()
169
170 return true
171 }
172
173 /**
174 * Takes in a zip file and reads the meta.json file for presentation to the UI
175 *
176 * @param driver Zip containing driver and meta.json file
177 * @return A non-null [GpuDriverMetadata] instance that may have null members
178 */
179 fun getMetadataFromZip(driver: File): GpuDriverMetadata {
180 try {
181 ZipFile(driver).use { zf ->
182 val entries = zf.entries()
183 while (entries.hasMoreElements()) {
184 val entry = entries.nextElement()
185 if (!entry.isDirectory && entry.name.lowercase().contains(".json")) {
186 zf.getInputStream(entry).use {
187 return GpuDriverMetadata(it, entry.size)
188 }
189 }
190 }
191 }
192 } catch (_: ZipException) {
134 } 193 }
194 return GpuDriverMetadata()
195 }
196
197 external fun supportsCustomDriverLoading(): Boolean
198
199 // Parse the custom driver metadata to retrieve the name.
200 val customDriverData: GpuDriverMetadata
201 get() = GpuDriverMetadata(File(driverInstallationPath + META_JSON_FILENAME))
135 202
136 private fun initializeDirectories() { 203 fun initializeDirectories() {
137 // Ensure the file redirection directory exists. 204 // Ensure the file redirection directory exists.
138 val fileRedirectionDir = File(fileRedirectionPath!!) 205 val fileRedirectionDir = File(fileRedirectionPath!!)
139 if (!fileRedirectionDir.exists()) { 206 if (!fileRedirectionDir.exists()) {
@@ -144,14 +211,10 @@ object GpuDriverHelper {
144 if (!driverInstallationDir.exists()) { 211 if (!driverInstallationDir.exists()) {
145 driverInstallationDir.mkdirs() 212 driverInstallationDir.mkdirs()
146 } 213 }
147 } 214 // Ensure the driver storage directory exists
148 215 val driverStorageDirectory = File(driverStoragePath)
149 private fun deleteRecursive(fileOrDirectory: File) { 216 if (!driverStorageDirectory.exists()) {
150 if (fileOrDirectory.isDirectory) { 217 driverStorageDirectory.mkdirs()
151 for (child in fileOrDirectory.listFiles()!!) {
152 deleteRecursive(child)
153 }
154 } 218 }
155 fileOrDirectory.delete()
156 } 219 }
157} 220}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/GpuDriverMetadata.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/GpuDriverMetadata.kt
index a4e64070a..511a4171a 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/GpuDriverMetadata.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/GpuDriverMetadata.kt
@@ -4,29 +4,29 @@
4package org.yuzu.yuzu_emu.utils 4package org.yuzu.yuzu_emu.utils
5 5
6import java.io.IOException 6import java.io.IOException
7import java.nio.charset.StandardCharsets
8import java.nio.file.Files
9import java.nio.file.Paths
10import org.json.JSONException 7import org.json.JSONException
11import org.json.JSONObject 8import org.json.JSONObject
9import java.io.File
10import java.io.InputStream
12 11
13class GpuDriverMetadata(metadataFilePath: String) { 12class GpuDriverMetadata {
14 var name: String? = null 13 /**
15 var description: String? = null 14 * Tries to get driver metadata information from a meta.json [File]
16 var author: String? = null 15 *
17 var vendor: String? = null 16 * @param metadataFile meta.json file provided with a GPU driver
18 var driverVersion: String? = null 17 */
19 var minApi = 0 18 constructor(metadataFile: File) {
20 var libraryName: String? = null 19 if (metadataFile.length() > MAX_META_SIZE_BYTES) {
20 return
21 }
21 22
22 init {
23 try { 23 try {
24 val json = JSONObject(getStringFromFile(metadataFilePath)) 24 val json = JSONObject(FileUtil.getStringFromFile(metadataFile))
25 name = json.getString("name") 25 name = json.getString("name")
26 description = json.getString("description") 26 description = json.getString("description")
27 author = json.getString("author") 27 author = json.getString("author")
28 vendor = json.getString("vendor") 28 vendor = json.getString("vendor")
29 driverVersion = json.getString("driverVersion") 29 version = json.getString("driverVersion")
30 minApi = json.getInt("minApi") 30 minApi = json.getInt("minApi")
31 libraryName = json.getString("libraryName") 31 libraryName = json.getString("libraryName")
32 } catch (e: JSONException) { 32 } catch (e: JSONException) {
@@ -36,12 +36,84 @@ class GpuDriverMetadata(metadataFilePath: String) {
36 } 36 }
37 } 37 }
38 38
39 companion object { 39 /**
40 @Throws(IOException::class) 40 * Tries to get driver metadata information from an input stream that's intended to be
41 private fun getStringFromFile(filePath: String): String { 41 * from a zip file
42 val path = Paths.get(filePath) 42 *
43 val bytes = Files.readAllBytes(path) 43 * @param metadataStream ZipEntry input stream
44 return String(bytes, StandardCharsets.UTF_8) 44 * @param size Size of the file in bytes
45 */
46 constructor(metadataStream: InputStream, size: Long) {
47 if (size > MAX_META_SIZE_BYTES) {
48 return
45 } 49 }
50
51 try {
52 val json = JSONObject(FileUtil.getStringFromInputStream(metadataStream))
53 name = json.getString("name")
54 description = json.getString("description")
55 author = json.getString("author")
56 vendor = json.getString("vendor")
57 version = json.getString("driverVersion")
58 minApi = json.getInt("minApi")
59 libraryName = json.getString("libraryName")
60 } catch (e: JSONException) {
61 // JSON is malformed, ignore and treat as unsupported metadata.
62 } catch (e: IOException) {
63 // File is inaccessible, ignore and treat as unsupported metadata.
64 }
65 }
66
67 /**
68 * Creates an empty metadata instance
69 */
70 constructor()
71
72 override fun equals(other: Any?): Boolean {
73 if (other !is GpuDriverMetadata) {
74 return false
75 }
76
77 return other.name == name &&
78 other.description == description &&
79 other.author == author &&
80 other.vendor == vendor &&
81 other.version == version &&
82 other.minApi == minApi &&
83 other.libraryName == libraryName
84 }
85
86 override fun hashCode(): Int {
87 var result = name?.hashCode() ?: 0
88 result = 31 * result + (description?.hashCode() ?: 0)
89 result = 31 * result + (author?.hashCode() ?: 0)
90 result = 31 * result + (vendor?.hashCode() ?: 0)
91 result = 31 * result + (version?.hashCode() ?: 0)
92 result = 31 * result + minApi
93 result = 31 * result + (libraryName?.hashCode() ?: 0)
94 return result
95 }
96
97 override fun toString(): String =
98 """
99 Name - $name
100 Description - $description
101 Author - $author
102 Vendor - $vendor
103 Version - $version
104 Min API - $minApi
105 Library Name - $libraryName
106 """.trimMargin().trimIndent()
107
108 var name: String? = null
109 var description: String? = null
110 var author: String? = null
111 var vendor: String? = null
112 var version: String? = null
113 var minApi = 0
114 var libraryName: String? = null
115
116 companion object {
117 private const val MAX_META_SIZE_BYTES = 500000
46 } 118 }
47} 119}
diff --git a/src/android/app/src/main/res/drawable/ic_build.xml b/src/android/app/src/main/res/drawable/ic_build.xml
new file mode 100644
index 000000000..91d52f1b8
--- /dev/null
+++ b/src/android/app/src/main/res/drawable/ic_build.xml
@@ -0,0 +1,9 @@
1<vector xmlns:android="http://schemas.android.com/apk/res/android"
2 android:width="24dp"
3 android:height="24dp"
4 android:viewportWidth="24"
5 android:viewportHeight="24">
6 <path
7 android:fillColor="?attr/colorControlNormal"
8 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" />
9</vector>
diff --git a/src/android/app/src/main/res/drawable/ic_delete.xml b/src/android/app/src/main/res/drawable/ic_delete.xml
new file mode 100644
index 000000000..d26a79711
--- /dev/null
+++ b/src/android/app/src/main/res/drawable/ic_delete.xml
@@ -0,0 +1,9 @@
1<vector xmlns:android="http://schemas.android.com/apk/res/android"
2 android:width="24dp"
3 android:height="24dp"
4 android:viewportWidth="24"
5 android:viewportHeight="24">
6 <path
7 android:fillColor="?attr/colorControlNormal"
8 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" />
9</vector>
diff --git a/src/android/app/src/main/res/layout/card_driver_option.xml b/src/android/app/src/main/res/layout/card_driver_option.xml
new file mode 100644
index 000000000..1dd9a6d7d
--- /dev/null
+++ b/src/android/app/src/main/res/layout/card_driver_option.xml
@@ -0,0 +1,89 @@
1<?xml version="1.0" encoding="utf-8"?>
2<com.google.android.material.card.MaterialCardView xmlns:android="http://schemas.android.com/apk/res/android"
3 xmlns:app="http://schemas.android.com/apk/res-auto"
4 xmlns:tools="http://schemas.android.com/tools"
5 style="?attr/materialCardViewOutlinedStyle"
6 android:layout_width="match_parent"
7 android:layout_height="wrap_content"
8 android:layout_marginHorizontal="16dp"
9 android:layout_marginVertical="12dp"
10 android:background="?attr/selectableItemBackground"
11 android:clickable="true"
12 android:focusable="true">
13
14 <LinearLayout
15 android:layout_width="match_parent"
16 android:layout_height="wrap_content"
17 android:orientation="horizontal"
18 android:layout_gravity="center"
19 android:padding="16dp">
20
21 <RadioButton
22 android:id="@+id/radio_button"
23 android:layout_width="wrap_content"
24 android:layout_height="wrap_content"
25 android:layout_gravity="center_vertical"
26 android:clickable="false"
27 android:checked="false" />
28
29 <LinearLayout
30 android:layout_width="0dp"
31 android:layout_height="wrap_content"
32 android:layout_weight="1"
33 android:orientation="vertical"
34 android:layout_gravity="center_vertical">
35
36 <com.google.android.material.textview.MaterialTextView
37 android:id="@+id/title"
38 style="@style/TextAppearance.Material3.TitleMedium"
39 android:layout_width="match_parent"
40 android:layout_height="wrap_content"
41 android:ellipsize="none"
42 android:marqueeRepeatLimit="marquee_forever"
43 android:requiresFadingEdge="horizontal"
44 android:singleLine="true"
45 android:textAlignment="viewStart"
46 tools:text="@string/select_gpu_driver_default" />
47
48 <com.google.android.material.textview.MaterialTextView
49 android:id="@+id/version"
50 style="@style/TextAppearance.Material3.BodyMedium"
51 android:layout_width="match_parent"
52 android:layout_height="wrap_content"
53 android:layout_marginTop="6dp"
54 android:ellipsize="none"
55 android:marqueeRepeatLimit="marquee_forever"
56 android:requiresFadingEdge="horizontal"
57 android:singleLine="true"
58 android:textAlignment="viewStart"
59 tools:text="@string/install_gpu_driver_description" />
60
61 <com.google.android.material.textview.MaterialTextView
62 android:id="@+id/description"
63 style="@style/TextAppearance.Material3.BodyMedium"
64 android:layout_width="match_parent"
65 android:layout_height="wrap_content"
66 android:layout_marginTop="6dp"
67 android:ellipsize="none"
68 android:marqueeRepeatLimit="marquee_forever"
69 android:requiresFadingEdge="horizontal"
70 android:singleLine="true"
71 android:textAlignment="viewStart"
72 tools:text="@string/install_gpu_driver_description" />
73
74 </LinearLayout>
75
76 <Button
77 android:id="@+id/button_delete"
78 style="@style/Widget.Material3.Button.IconButton"
79 android:layout_width="wrap_content"
80 android:layout_height="wrap_content"
81 android:layout_gravity="center_vertical"
82 android:contentDescription="@string/delete"
83 android:tooltipText="@string/delete"
84 app:icon="@drawable/ic_delete"
85 app:iconTint="?attr/colorControlNormal" />
86
87 </LinearLayout>
88
89</com.google.android.material.card.MaterialCardView>
diff --git a/src/android/app/src/main/res/layout/fragment_driver_manager.xml b/src/android/app/src/main/res/layout/fragment_driver_manager.xml
new file mode 100644
index 000000000..6cea2d164
--- /dev/null
+++ b/src/android/app/src/main/res/layout/fragment_driver_manager.xml
@@ -0,0 +1,48 @@
1<?xml version="1.0" encoding="utf-8"?>
2<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
3 xmlns:app="http://schemas.android.com/apk/res-auto"
4 android:id="@+id/coordinator_licenses"
5 android:layout_width="match_parent"
6 android:layout_height="match_parent"
7 android:background="?attr/colorSurface">
8
9 <androidx.coordinatorlayout.widget.CoordinatorLayout
10 android:layout_width="match_parent"
11 android:layout_height="match_parent">
12
13 <com.google.android.material.appbar.AppBarLayout
14 android:id="@+id/appbar_drivers"
15 android:layout_width="match_parent"
16 android:layout_height="wrap_content"
17 android:fitsSystemWindows="true"
18 app:liftOnScrollTargetViewId="@id/list_drivers">
19
20 <com.google.android.material.appbar.MaterialToolbar
21 android:id="@+id/toolbar_drivers"
22 android:layout_width="match_parent"
23 android:layout_height="?attr/actionBarSize"
24 app:navigationIcon="@drawable/ic_back"
25 app:title="@string/gpu_driver_manager" />
26
27 </com.google.android.material.appbar.AppBarLayout>
28
29 <androidx.recyclerview.widget.RecyclerView
30 android:id="@+id/list_drivers"
31 android:layout_width="match_parent"
32 android:layout_height="match_parent"
33 android:clipToPadding="false"
34 app:layout_behavior="@string/appbar_scrolling_view_behavior" />
35
36 </androidx.coordinatorlayout.widget.CoordinatorLayout>
37
38 <com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
39 android:id="@+id/button_install"
40 android:layout_width="wrap_content"
41 android:layout_height="wrap_content"
42 android:layout_gravity="bottom|end"
43 android:text="@string/install"
44 app:icon="@drawable/ic_add"
45 app:layout_constraintBottom_toBottomOf="parent"
46 app:layout_constraintEnd_toEndOf="parent" />
47
48</androidx.constraintlayout.widget.ConstraintLayout>
diff --git a/src/android/app/src/main/res/navigation/home_navigation.xml b/src/android/app/src/main/res/navigation/home_navigation.xml
index 2356b802b..82749359d 100644
--- a/src/android/app/src/main/res/navigation/home_navigation.xml
+++ b/src/android/app/src/main/res/navigation/home_navigation.xml
@@ -22,6 +22,9 @@
22 <action 22 <action
23 android:id="@+id/action_homeSettingsFragment_to_installableFragment" 23 android:id="@+id/action_homeSettingsFragment_to_installableFragment"
24 app:destination="@id/installableFragment" /> 24 app:destination="@id/installableFragment" />
25 <action
26 android:id="@+id/action_homeSettingsFragment_to_driverManagerFragment"
27 app:destination="@id/driverManagerFragment" />
25 </fragment> 28 </fragment>
26 29
27 <fragment 30 <fragment
@@ -95,5 +98,9 @@
95 android:id="@+id/installableFragment" 98 android:id="@+id/installableFragment"
96 android:name="org.yuzu.yuzu_emu.fragments.InstallableFragment" 99 android:name="org.yuzu.yuzu_emu.fragments.InstallableFragment"
97 android:label="InstallableFragment" /> 100 android:label="InstallableFragment" />
101 <fragment
102 android:id="@+id/driverManagerFragment"
103 android:name="org.yuzu.yuzu_emu.fragments.DriverManagerFragment"
104 android:label="DriverManagerFragment" />
98 105
99</navigation> 106</navigation>
diff --git a/src/android/app/src/main/res/values-de/strings.xml b/src/android/app/src/main/res/values-de/strings.xml
index dd0f36392..72a47fbdb 100644
--- a/src/android/app/src/main/res/values-de/strings.xml
+++ b/src/android/app/src/main/res/values-de/strings.xml
@@ -168,9 +168,7 @@
168 <string name="select_gpu_driver_title">Möchtest du deinen aktuellen GPU-Treiber ersetzen?</string> 168 <string name="select_gpu_driver_title">Möchtest du deinen aktuellen GPU-Treiber ersetzen?</string>
169 <string name="select_gpu_driver_install">Installieren</string> 169 <string name="select_gpu_driver_install">Installieren</string>
170 <string name="select_gpu_driver_default">Standard</string> 170 <string name="select_gpu_driver_default">Standard</string>
171 <string name="select_gpu_driver_install_success">%s wurde installiert</string>
172 <string name="select_gpu_driver_use_default">Standard GPU-Treiber wird verwendet</string> 171 <string name="select_gpu_driver_use_default">Standard GPU-Treiber wird verwendet</string>
173 <string name="select_gpu_driver_error">Ungültiger Treiber ausgewählt, Standard-Treiber wird verwendet!</string>
174 <string name="system_gpu_driver">System GPU-Treiber</string> 172 <string name="system_gpu_driver">System GPU-Treiber</string>
175 <string name="installing_driver">Treiber wird installiert...</string> 173 <string name="installing_driver">Treiber wird installiert...</string>
176 174
diff --git a/src/android/app/src/main/res/values-es/strings.xml b/src/android/app/src/main/res/values-es/strings.xml
index d398f862f..e5bdd5889 100644
--- a/src/android/app/src/main/res/values-es/strings.xml
+++ b/src/android/app/src/main/res/values-es/strings.xml
@@ -171,9 +171,7 @@
171 <string name="select_gpu_driver_title">¿Quiere reemplazar el driver de GPU actual?</string> 171 <string name="select_gpu_driver_title">¿Quiere reemplazar el driver de GPU actual?</string>
172 <string name="select_gpu_driver_install">Instalar</string> 172 <string name="select_gpu_driver_install">Instalar</string>
173 <string name="select_gpu_driver_default">Predeterminado</string> 173 <string name="select_gpu_driver_default">Predeterminado</string>
174 <string name="select_gpu_driver_install_success">Instalado %s</string>
175 <string name="select_gpu_driver_use_default">Usando el driver de GPU por defecto </string> 174 <string name="select_gpu_driver_use_default">Usando el driver de GPU por defecto </string>
176 <string name="select_gpu_driver_error">¡Driver no válido, utilizando el predeterminado del sistema!</string>
177 <string name="system_gpu_driver">Driver GPU del sistema</string> 175 <string name="system_gpu_driver">Driver GPU del sistema</string>
178 <string name="installing_driver">Instalando driver...</string> 176 <string name="installing_driver">Instalando driver...</string>
179 177
diff --git a/src/android/app/src/main/res/values-fr/strings.xml b/src/android/app/src/main/res/values-fr/strings.xml
index a7abd9077..1e02828aa 100644
--- a/src/android/app/src/main/res/values-fr/strings.xml
+++ b/src/android/app/src/main/res/values-fr/strings.xml
@@ -171,9 +171,7 @@
171 <string name="select_gpu_driver_title">Souhaitez vous remplacer votre pilote actuel ?</string> 171 <string name="select_gpu_driver_title">Souhaitez vous remplacer votre pilote actuel ?</string>
172 <string name="select_gpu_driver_install">Installer</string> 172 <string name="select_gpu_driver_install">Installer</string>
173 <string name="select_gpu_driver_default">Défaut</string> 173 <string name="select_gpu_driver_default">Défaut</string>
174 <string name="select_gpu_driver_install_success">%s Installé</string>
175 <string name="select_gpu_driver_use_default">Utilisation du pilote de GPU par défaut</string> 174 <string name="select_gpu_driver_use_default">Utilisation du pilote de GPU par défaut</string>
176 <string name="select_gpu_driver_error">Pilote non valide sélectionné, utilisation du paramètre par défaut du système !</string>
177 <string name="system_gpu_driver">Pilote du GPU du système</string> 175 <string name="system_gpu_driver">Pilote du GPU du système</string>
178 <string name="installing_driver">Installation du pilote...</string> 176 <string name="installing_driver">Installation du pilote...</string>
179 177
diff --git a/src/android/app/src/main/res/values-it/strings.xml b/src/android/app/src/main/res/values-it/strings.xml
index b18161801..09c9345b0 100644
--- a/src/android/app/src/main/res/values-it/strings.xml
+++ b/src/android/app/src/main/res/values-it/strings.xml
@@ -171,9 +171,7 @@
171 <string name="select_gpu_driver_title">Vuoi sostituire il driver della tua GPU attuale?</string> 171 <string name="select_gpu_driver_title">Vuoi sostituire il driver della tua GPU attuale?</string>
172 <string name="select_gpu_driver_install">Installa</string> 172 <string name="select_gpu_driver_install">Installa</string>
173 <string name="select_gpu_driver_default">Predefinito</string> 173 <string name="select_gpu_driver_default">Predefinito</string>
174 <string name="select_gpu_driver_install_success">Installato%s</string>
175 <string name="select_gpu_driver_use_default">Utilizza il driver predefinito della GPU.</string> 174 <string name="select_gpu_driver_use_default">Utilizza il driver predefinito della GPU.</string>
176 <string name="select_gpu_driver_error">Il driver selezionato è invalido, è in utilizzo quello predefinito di sistema!</string>
177 <string name="system_gpu_driver">Driver GPU del sistema</string> 175 <string name="system_gpu_driver">Driver GPU del sistema</string>
178 <string name="installing_driver">Installando i driver...</string> 176 <string name="installing_driver">Installando i driver...</string>
179 177
diff --git a/src/android/app/src/main/res/values-ja/strings.xml b/src/android/app/src/main/res/values-ja/strings.xml
index 88fa5a0bb..a0ea78bef 100644
--- a/src/android/app/src/main/res/values-ja/strings.xml
+++ b/src/android/app/src/main/res/values-ja/strings.xml
@@ -170,9 +170,7 @@
170 <string name="select_gpu_driver_title">現在のGPUドライバーを置き換えますか?</string> 170 <string name="select_gpu_driver_title">現在のGPUドライバーを置き換えますか?</string>
171 <string name="select_gpu_driver_install">インストール</string> 171 <string name="select_gpu_driver_install">インストール</string>
172 <string name="select_gpu_driver_default">デフォルト</string> 172 <string name="select_gpu_driver_default">デフォルト</string>
173 <string name="select_gpu_driver_install_success">%s をインストールしました</string>
174 <string name="select_gpu_driver_use_default">デフォルトのGPUドライバーを使用します</string> 173 <string name="select_gpu_driver_use_default">デフォルトのGPUドライバーを使用します</string>
175 <string name="select_gpu_driver_error">選択されたドライバが無効なため、システムのデフォルトを使用します!</string>
176 <string name="system_gpu_driver">システムのGPUドライバ</string> 174 <string name="system_gpu_driver">システムのGPUドライバ</string>
177 <string name="installing_driver">インストール中…</string> 175 <string name="installing_driver">インストール中…</string>
178 176
diff --git a/src/android/app/src/main/res/values-ko/strings.xml b/src/android/app/src/main/res/values-ko/strings.xml
index 4b658255c..214f95706 100644
--- a/src/android/app/src/main/res/values-ko/strings.xml
+++ b/src/android/app/src/main/res/values-ko/strings.xml
@@ -171,9 +171,7 @@
171 <string name="select_gpu_driver_title">현재 사용 중인 GPU 드라이버를 교체하겠습니까?</string> 171 <string name="select_gpu_driver_title">현재 사용 중인 GPU 드라이버를 교체하겠습니까?</string>
172 <string name="select_gpu_driver_install">설치</string> 172 <string name="select_gpu_driver_install">설치</string>
173 <string name="select_gpu_driver_default">기본값</string> 173 <string name="select_gpu_driver_default">기본값</string>
174 <string name="select_gpu_driver_install_success">설치된 %s</string>
175 <string name="select_gpu_driver_use_default">기본 GPU 드라이버 사용</string> 174 <string name="select_gpu_driver_use_default">기본 GPU 드라이버 사용</string>
176 <string name="select_gpu_driver_error">시스템 기본값을 사용하여 잘못된 드라이버를 선택했습니다!</string>
177 <string name="system_gpu_driver">시스템 GPU 드라이버</string> 175 <string name="system_gpu_driver">시스템 GPU 드라이버</string>
178 <string name="installing_driver">드라이버 설치 중...</string> 176 <string name="installing_driver">드라이버 설치 중...</string>
179 177
diff --git a/src/android/app/src/main/res/values-nb/strings.xml b/src/android/app/src/main/res/values-nb/strings.xml
index dd602a389..5443cef42 100644
--- a/src/android/app/src/main/res/values-nb/strings.xml
+++ b/src/android/app/src/main/res/values-nb/strings.xml
@@ -171,9 +171,7 @@
171 <string name="select_gpu_driver_title">Ønsker du å bytte ut din nåværende GPU-driver?</string> 171 <string name="select_gpu_driver_title">Ønsker du å bytte ut din nåværende GPU-driver?</string>
172 <string name="select_gpu_driver_install">Installer</string> 172 <string name="select_gpu_driver_install">Installer</string>
173 <string name="select_gpu_driver_default">Standard</string> 173 <string name="select_gpu_driver_default">Standard</string>
174 <string name="select_gpu_driver_install_success">Installert %s</string>
175 <string name="select_gpu_driver_use_default">Bruk av standard GPU-driver</string> 174 <string name="select_gpu_driver_use_default">Bruk av standard GPU-driver</string>
176 <string name="select_gpu_driver_error">Ugyldig driver valgt, bruker systemstandard!</string>
177 <string name="system_gpu_driver">Systemets GPU-driver</string> 175 <string name="system_gpu_driver">Systemets GPU-driver</string>
178 <string name="installing_driver">Installerer driver...</string> 176 <string name="installing_driver">Installerer driver...</string>
179 177
diff --git a/src/android/app/src/main/res/values-pl/strings.xml b/src/android/app/src/main/res/values-pl/strings.xml
index 2fdd1f952..899e233d0 100644
--- a/src/android/app/src/main/res/values-pl/strings.xml
+++ b/src/android/app/src/main/res/values-pl/strings.xml
@@ -171,9 +171,7 @@
171 <string name="select_gpu_driver_title">Chcesz zastąpić obecny sterownik układu graficznego?</string> 171 <string name="select_gpu_driver_title">Chcesz zastąpić obecny sterownik układu graficznego?</string>
172 <string name="select_gpu_driver_install">Zainstaluj</string> 172 <string name="select_gpu_driver_install">Zainstaluj</string>
173 <string name="select_gpu_driver_default">Domyślne</string> 173 <string name="select_gpu_driver_default">Domyślne</string>
174 <string name="select_gpu_driver_install_success">Zainstalowano %s</string>
175 <string name="select_gpu_driver_use_default">Aktywny domyślny sterownik GPU</string> 174 <string name="select_gpu_driver_use_default">Aktywny domyślny sterownik GPU</string>
176 <string name="select_gpu_driver_error">Wybrano błędny sterownik, powrót do domyślnego. </string>
177 <string name="system_gpu_driver">Systemowy sterownik GPU</string> 175 <string name="system_gpu_driver">Systemowy sterownik GPU</string>
178 <string name="installing_driver">Instalowanie sterownika...</string> 176 <string name="installing_driver">Instalowanie sterownika...</string>
179 177
diff --git a/src/android/app/src/main/res/values-pt-rBR/strings.xml b/src/android/app/src/main/res/values-pt-rBR/strings.xml
index 2f26367fe..caa095364 100644
--- a/src/android/app/src/main/res/values-pt-rBR/strings.xml
+++ b/src/android/app/src/main/res/values-pt-rBR/strings.xml
@@ -171,9 +171,7 @@
171 <string name="select_gpu_driver_title">Queres substituir o driver do GPU atual? </string> 171 <string name="select_gpu_driver_title">Queres substituir o driver do GPU atual? </string>
172 <string name="select_gpu_driver_install">Instalar</string> 172 <string name="select_gpu_driver_install">Instalar</string>
173 <string name="select_gpu_driver_default">Padrão</string> 173 <string name="select_gpu_driver_default">Padrão</string>
174 <string name="select_gpu_driver_install_success">Instalado%s</string>
175 <string name="select_gpu_driver_use_default">Usar o driver padrão do GPU</string> 174 <string name="select_gpu_driver_use_default">Usar o driver padrão do GPU</string>
176 <string name="select_gpu_driver_error">Driver selecionado inválido, a usar o padrão do sistema!</string>
177 <string name="system_gpu_driver">Driver do GPU padrão</string> 175 <string name="system_gpu_driver">Driver do GPU padrão</string>
178 <string name="installing_driver">A instalar o Driver...</string> 176 <string name="installing_driver">A instalar o Driver...</string>
179 177
diff --git a/src/android/app/src/main/res/values-pt-rPT/strings.xml b/src/android/app/src/main/res/values-pt-rPT/strings.xml
index 4e1eb4cd7..0a1a47fbb 100644
--- a/src/android/app/src/main/res/values-pt-rPT/strings.xml
+++ b/src/android/app/src/main/res/values-pt-rPT/strings.xml
@@ -171,9 +171,7 @@
171 <string name="select_gpu_driver_title">Queres substituir o driver do GPU atual? </string> 171 <string name="select_gpu_driver_title">Queres substituir o driver do GPU atual? </string>
172 <string name="select_gpu_driver_install">Instalar</string> 172 <string name="select_gpu_driver_install">Instalar</string>
173 <string name="select_gpu_driver_default">Padrão</string> 173 <string name="select_gpu_driver_default">Padrão</string>
174 <string name="select_gpu_driver_install_success">Instalado%s</string>
175 <string name="select_gpu_driver_use_default">Usar o driver padrão do GPU</string> 174 <string name="select_gpu_driver_use_default">Usar o driver padrão do GPU</string>
176 <string name="select_gpu_driver_error">Driver selecionado inválido, a usar o padrão do sistema!</string>
177 <string name="system_gpu_driver">Driver do GPU padrão</string> 175 <string name="system_gpu_driver">Driver do GPU padrão</string>
178 <string name="installing_driver">A instalar o Driver...</string> 176 <string name="installing_driver">A instalar o Driver...</string>
179 177
diff --git a/src/android/app/src/main/res/values-ru/strings.xml b/src/android/app/src/main/res/values-ru/strings.xml
index f5695dc93..0bef035d6 100644
--- a/src/android/app/src/main/res/values-ru/strings.xml
+++ b/src/android/app/src/main/res/values-ru/strings.xml
@@ -171,9 +171,7 @@
171 <string name="select_gpu_driver_title">Хотите заменить текущий драйвер ГП?</string> 171 <string name="select_gpu_driver_title">Хотите заменить текущий драйвер ГП?</string>
172 <string name="select_gpu_driver_install">Установить</string> 172 <string name="select_gpu_driver_install">Установить</string>
173 <string name="select_gpu_driver_default">По умолчанию</string> 173 <string name="select_gpu_driver_default">По умолчанию</string>
174 <string name="select_gpu_driver_install_success">Установлено %s</string>
175 <string name="select_gpu_driver_use_default">Используется стандартный драйвер ГП </string> 174 <string name="select_gpu_driver_use_default">Используется стандартный драйвер ГП </string>
176 <string name="select_gpu_driver_error">Выбран неверный драйвер, используется стандартный системный!</string>
177 <string name="system_gpu_driver">Системный драйвер ГП</string> 175 <string name="system_gpu_driver">Системный драйвер ГП</string>
178 <string name="installing_driver">Установка драйвера...</string> 176 <string name="installing_driver">Установка драйвера...</string>
179 177
diff --git a/src/android/app/src/main/res/values-uk/strings.xml b/src/android/app/src/main/res/values-uk/strings.xml
index 061bc6f04..5b789ee98 100644
--- a/src/android/app/src/main/res/values-uk/strings.xml
+++ b/src/android/app/src/main/res/values-uk/strings.xml
@@ -171,9 +171,7 @@
171 <string name="select_gpu_driver_title">Хочете замінити поточний драйвер ГП?</string> 171 <string name="select_gpu_driver_title">Хочете замінити поточний драйвер ГП?</string>
172 <string name="select_gpu_driver_install">Встановити</string> 172 <string name="select_gpu_driver_install">Встановити</string>
173 <string name="select_gpu_driver_default">За замовчуванням</string> 173 <string name="select_gpu_driver_default">За замовчуванням</string>
174 <string name="select_gpu_driver_install_success">Встановлено %s</string>
175 <string name="select_gpu_driver_use_default">Використовується стандартний драйвер ГП</string> 174 <string name="select_gpu_driver_use_default">Використовується стандартний драйвер ГП</string>
176 <string name="select_gpu_driver_error">Обрано неправильний драйвер, використовується стандартний системний!</string>
177 <string name="system_gpu_driver">Системний драйвер ГП</string> 175 <string name="system_gpu_driver">Системний драйвер ГП</string>
178 <string name="installing_driver">Встановлення драйвера...</string> 176 <string name="installing_driver">Встановлення драйвера...</string>
179 177
diff --git a/src/android/app/src/main/res/values-zh-rCN/strings.xml b/src/android/app/src/main/res/values-zh-rCN/strings.xml
index fe6dd5eaa..c0e885751 100644
--- a/src/android/app/src/main/res/values-zh-rCN/strings.xml
+++ b/src/android/app/src/main/res/values-zh-rCN/strings.xml
@@ -171,9 +171,7 @@
171 <string name="select_gpu_driver_title">要取代您当前的 GPU 驱动程序吗?</string> 171 <string name="select_gpu_driver_title">要取代您当前的 GPU 驱动程序吗?</string>
172 <string name="select_gpu_driver_install">安装</string> 172 <string name="select_gpu_driver_install">安装</string>
173 <string name="select_gpu_driver_default">系统默认</string> 173 <string name="select_gpu_driver_default">系统默认</string>
174 <string name="select_gpu_driver_install_success">已安装 %s</string>
175 <string name="select_gpu_driver_use_default">使用默认 GPU 驱动程序</string> 174 <string name="select_gpu_driver_use_default">使用默认 GPU 驱动程序</string>
176 <string name="select_gpu_driver_error">选择的驱动程序无效,将使用系统默认的驱动程序!</string>
177 <string name="system_gpu_driver">系统 GPU 驱动程序</string> 175 <string name="system_gpu_driver">系统 GPU 驱动程序</string>
178 <string name="installing_driver">正在安装驱动程序…</string> 176 <string name="installing_driver">正在安装驱动程序…</string>
179 177
diff --git a/src/android/app/src/main/res/values-zh-rTW/strings.xml b/src/android/app/src/main/res/values-zh-rTW/strings.xml
index 9b3e54224..4a21bf893 100644
--- a/src/android/app/src/main/res/values-zh-rTW/strings.xml
+++ b/src/android/app/src/main/res/values-zh-rTW/strings.xml
@@ -171,9 +171,7 @@
171 <string name="select_gpu_driver_title">要取代您目前的 GPU 驅動程式嗎?</string> 171 <string name="select_gpu_driver_title">要取代您目前的 GPU 驅動程式嗎?</string>
172 <string name="select_gpu_driver_install">安裝</string> 172 <string name="select_gpu_driver_install">安裝</string>
173 <string name="select_gpu_driver_default">預設</string> 173 <string name="select_gpu_driver_default">預設</string>
174 <string name="select_gpu_driver_install_success">已安裝 %s</string>
175 <string name="select_gpu_driver_use_default">使用預設 GPU 驅動程式</string> 174 <string name="select_gpu_driver_use_default">使用預設 GPU 驅動程式</string>
176 <string name="select_gpu_driver_error">選取的驅動程式無效,將使用系統預設驅動程式!</string>
177 <string name="system_gpu_driver">系統 GPU 驅動程式</string> 175 <string name="system_gpu_driver">系統 GPU 驅動程式</string>
178 <string name="installing_driver">正在安裝驅動程式…</string> 176 <string name="installing_driver">正在安裝驅動程式…</string>
179 177
diff --git a/src/android/app/src/main/res/values/dimens.xml b/src/android/app/src/main/res/values/dimens.xml
index 7b2296d95..ef855ea6f 100644
--- a/src/android/app/src/main/res/values/dimens.xml
+++ b/src/android/app/src/main/res/values/dimens.xml
@@ -13,6 +13,8 @@
13 <dimen name="menu_width">256dp</dimen> 13 <dimen name="menu_width">256dp</dimen>
14 <dimen name="card_width">165dp</dimen> 14 <dimen name="card_width">165dp</dimen>
15 <dimen name="icon_inset">24dp</dimen> 15 <dimen name="icon_inset">24dp</dimen>
16 <dimen name="spacing_bottom_list_fab">72dp</dimen>
17 <dimen name="spacing_fab">24dp</dimen>
16 18
17 <dimen name="dialog_margin">20dp</dimen> 19 <dimen name="dialog_margin">20dp</dimen>
18 <dimen name="elevated_app_bar">3dp</dimen> 20 <dimen name="elevated_app_bar">3dp</dimen>
diff --git a/src/android/app/src/main/res/values/strings.xml b/src/android/app/src/main/res/values/strings.xml
index e51edf872..9e4854221 100644
--- a/src/android/app/src/main/res/values/strings.xml
+++ b/src/android/app/src/main/res/values/strings.xml
@@ -72,6 +72,7 @@
72 <string name="invalid_keys_error">Invalid encryption keys</string> 72 <string name="invalid_keys_error">Invalid encryption keys</string>
73 <string name="dumping_keys_quickstart_link">https://yuzu-emu.org/help/quickstart/#dumping-decryption-keys</string> 73 <string name="dumping_keys_quickstart_link">https://yuzu-emu.org/help/quickstart/#dumping-decryption-keys</string>
74 <string name="install_keys_failure_description">The selected file is incorrect or corrupt. Please redump your keys.</string> 74 <string name="install_keys_failure_description">The selected file is incorrect or corrupt. Please redump your keys.</string>
75 <string name="gpu_driver_manager">GPU Driver Manager</string>
75 <string name="install_gpu_driver">Install GPU driver</string> 76 <string name="install_gpu_driver">Install GPU driver</string>
76 <string name="install_gpu_driver_description">Install alternative drivers for potentially better performance or accuracy</string> 77 <string name="install_gpu_driver_description">Install alternative drivers for potentially better performance or accuracy</string>
77 <string name="advanced_settings">Advanced settings</string> 78 <string name="advanced_settings">Advanced settings</string>
@@ -234,15 +235,17 @@
234 <string name="export_failed">Export failed</string> 235 <string name="export_failed">Export failed</string>
235 <string name="import_failed">Import failed</string> 236 <string name="import_failed">Import failed</string>
236 <string name="cancelling">Cancelling</string> 237 <string name="cancelling">Cancelling</string>
238 <string name="install">Install</string>
239 <string name="delete">Delete</string>
237 240
238 <!-- GPU driver installation --> 241 <!-- GPU driver installation -->
239 <string name="select_gpu_driver">Select GPU driver</string> 242 <string name="select_gpu_driver">Select GPU driver</string>
240 <string name="select_gpu_driver_title">Would you like to replace your current GPU driver?</string> 243 <string name="select_gpu_driver_title">Would you like to replace your current GPU driver?</string>
241 <string name="select_gpu_driver_install">Install</string> 244 <string name="select_gpu_driver_install">Install</string>
242 <string name="select_gpu_driver_default">Default</string> 245 <string name="select_gpu_driver_default">Default</string>
243 <string name="select_gpu_driver_install_success">Installed %s</string>
244 <string name="select_gpu_driver_use_default">Using default GPU driver</string> 246 <string name="select_gpu_driver_use_default">Using default GPU driver</string>
245 <string name="select_gpu_driver_error">Invalid driver selected, using system default!</string> 247 <string name="select_gpu_driver_error">Invalid driver selected</string>
248 <string name="driver_already_installed">Driver already installed</string>
246 <string name="system_gpu_driver">System GPU driver</string> 249 <string name="system_gpu_driver">System GPU driver</string>
247 <string name="installing_driver">Installing driver…</string> 250 <string name="installing_driver">Installing driver…</string>
248 251