summaryrefslogtreecommitdiff
path: root/src/android
diff options
context:
space:
mode:
authorGravatar Charles Lombardo2023-04-19 22:42:18 -0400
committerGravatar bunnei2023-06-03 00:05:52 -0700
commit59525ddbebc4c1a8add986eabd7802a62fedc71b (patch)
treec06ba77c1d13a33918052ab51b01f73c57b8faac /src/android
parentandroid: Prevent editing unsafe settings at runtime (diff)
downloadyuzu-59525ddbebc4c1a8add986eabd7802a62fedc71b.tar.gz
yuzu-59525ddbebc4c1a8add986eabd7802a62fedc71b.tar.xz
yuzu-59525ddbebc4c1a8add986eabd7802a62fedc71b.zip
android: First time setup screen
Diffstat (limited to 'src/android')
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/SetupAdapter.kt70
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/OptionsFragment.kt167
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/SetupFragment.kt206
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/model/HomeViewModel.kt2
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/model/SetupPage.kt14
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/GamesFragment.kt6
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/main/MainActivity.kt191
-rw-r--r--src/android/app/src/main/res/drawable/ic_arrow_forward.xml10
-rw-r--r--src/android/app/src/main/res/drawable/ic_check.xml9
-rw-r--r--src/android/app/src/main/res/drawable/ic_controller.xml2
-rw-r--r--src/android/app/src/main/res/drawable/ic_key.xml9
-rw-r--r--src/android/app/src/main/res/drawable/ic_yuzu_title.xml24
-rw-r--r--src/android/app/src/main/res/layout-w600dp/fragment_setup.xml38
-rw-r--r--src/android/app/src/main/res/layout-w600dp/page_setup.xml65
-rw-r--r--src/android/app/src/main/res/layout/activity_main.xml4
-rw-r--r--src/android/app/src/main/res/layout/fragment_setup.xml38
-rw-r--r--src/android/app/src/main/res/layout/page_setup.xml52
-rw-r--r--src/android/app/src/main/res/navigation/home_navigation.xml9
-rw-r--r--src/android/app/src/main/res/values/strings.xml16
19 files changed, 769 insertions, 163 deletions
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/SetupAdapter.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/SetupAdapter.kt
new file mode 100644
index 000000000..481ddd5a5
--- /dev/null
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/SetupAdapter.kt
@@ -0,0 +1,70 @@
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.Html
7import android.view.LayoutInflater
8import android.view.ViewGroup
9import androidx.appcompat.app.AppCompatActivity
10import androidx.core.content.res.ResourcesCompat
11import androidx.recyclerview.widget.RecyclerView
12import com.google.android.material.button.MaterialButton
13import org.yuzu.yuzu_emu.databinding.PageSetupBinding
14import org.yuzu.yuzu_emu.model.SetupPage
15
16class SetupAdapter(val activity: AppCompatActivity, val pages: List<SetupPage>) :
17 RecyclerView.Adapter<SetupAdapter.SetupPageViewHolder>() {
18 override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SetupPageViewHolder {
19 val binding = PageSetupBinding.inflate(LayoutInflater.from(parent.context), parent, false)
20 return SetupPageViewHolder(binding)
21 }
22
23 override fun getItemCount(): Int = pages.size
24
25 override fun onBindViewHolder(holder: SetupPageViewHolder, position: Int) =
26 holder.bind(pages[position])
27
28 inner class SetupPageViewHolder(val binding: PageSetupBinding) :
29 RecyclerView.ViewHolder(binding.root) {
30 lateinit var page: SetupPage
31
32 init {
33 itemView.tag = this
34 }
35
36 fun bind(page: SetupPage) {
37 this.page = page
38 binding.icon.setImageDrawable(
39 ResourcesCompat.getDrawable(
40 activity.resources,
41 page.iconId,
42 activity.theme
43 )
44 )
45 binding.textTitle.text = activity.resources.getString(page.titleId)
46 binding.textDescription.text =
47 Html.fromHtml(activity.resources.getString(page.descriptionId), 0)
48
49 binding.buttonAction.apply {
50 text = activity.resources.getString(page.buttonTextId)
51 if (page.buttonIconId != 0) {
52 icon = ResourcesCompat.getDrawable(
53 activity.resources,
54 page.buttonIconId,
55 activity.theme
56 )
57 }
58 iconGravity =
59 if (page.leftAlignedIcon) {
60 MaterialButton.ICON_GRAVITY_START
61 } else {
62 MaterialButton.ICON_GRAVITY_END
63 }
64 setOnClickListener {
65 page.buttonAction.invoke()
66 }
67 }
68 }
69 }
70}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/OptionsFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/OptionsFragment.kt
index 954e52dc6..1cf0d0f52 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/OptionsFragment.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/OptionsFragment.kt
@@ -10,39 +10,26 @@ import android.view.LayoutInflater
10import android.view.View 10import android.view.View
11import android.view.ViewGroup 11import android.view.ViewGroup
12import android.widget.Toast 12import android.widget.Toast
13import androidx.activity.result.contract.ActivityResultContracts
14import androidx.appcompat.app.AppCompatActivity 13import androidx.appcompat.app.AppCompatActivity
15import androidx.core.view.ViewCompat 14import androidx.core.view.ViewCompat
16import androidx.core.view.WindowInsetsCompat 15import androidx.core.view.WindowInsetsCompat
17import androidx.fragment.app.Fragment 16import androidx.fragment.app.Fragment
18import androidx.fragment.app.activityViewModels
19import androidx.lifecycle.lifecycleScope
20import androidx.preference.PreferenceManager
21import androidx.recyclerview.widget.LinearLayoutManager 17import androidx.recyclerview.widget.LinearLayoutManager
22import com.google.android.material.dialog.MaterialAlertDialogBuilder 18import com.google.android.material.dialog.MaterialAlertDialogBuilder
23import kotlinx.coroutines.Dispatchers
24import kotlinx.coroutines.launch
25import kotlinx.coroutines.withContext
26import org.yuzu.yuzu_emu.NativeLibrary
27import org.yuzu.yuzu_emu.R 19import org.yuzu.yuzu_emu.R
28import org.yuzu.yuzu_emu.adapters.HomeOptionAdapter 20import org.yuzu.yuzu_emu.adapters.HomeOptionAdapter
29import org.yuzu.yuzu_emu.databinding.DialogProgressBarBinding
30import org.yuzu.yuzu_emu.databinding.FragmentOptionsBinding 21import org.yuzu.yuzu_emu.databinding.FragmentOptionsBinding
31import org.yuzu.yuzu_emu.features.settings.ui.SettingsActivity 22import org.yuzu.yuzu_emu.features.settings.ui.SettingsActivity
32import org.yuzu.yuzu_emu.features.settings.utils.SettingsFile 23import org.yuzu.yuzu_emu.features.settings.utils.SettingsFile
33import org.yuzu.yuzu_emu.model.GamesViewModel
34import org.yuzu.yuzu_emu.model.HomeOption 24import org.yuzu.yuzu_emu.model.HomeOption
35import org.yuzu.yuzu_emu.utils.DirectoryInitialization 25import org.yuzu.yuzu_emu.ui.main.MainActivity
36import org.yuzu.yuzu_emu.utils.FileUtil
37import org.yuzu.yuzu_emu.utils.GameHelper
38import org.yuzu.yuzu_emu.utils.GpuDriverHelper 26import org.yuzu.yuzu_emu.utils.GpuDriverHelper
39import java.io.IOException
40 27
41class OptionsFragment : Fragment() { 28class OptionsFragment : Fragment() {
42 private var _binding: FragmentOptionsBinding? = null 29 private var _binding: FragmentOptionsBinding? = null
43 private val binding get() = _binding!! 30 private val binding get() = _binding!!
44 31
45 private val gamesViewModel: GamesViewModel by activityViewModels() 32 private lateinit var mainActivity: MainActivity
46 33
47 override fun onCreateView( 34 override fun onCreateView(
48 inflater: LayoutInflater, 35 inflater: LayoutInflater,
@@ -54,22 +41,24 @@ class OptionsFragment : Fragment() {
54 } 41 }
55 42
56 override fun onViewCreated(view: View, savedInstanceState: Bundle?) { 43 override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
44 mainActivity = requireActivity() as MainActivity
45
57 val optionsList: List<HomeOption> = listOf( 46 val optionsList: List<HomeOption> = listOf(
58 HomeOption( 47 HomeOption(
59 R.string.add_games, 48 R.string.add_games,
60 R.string.add_games_description, 49 R.string.add_games_description,
61 R.drawable.ic_add 50 R.drawable.ic_add
62 ) { getGamesDirectory.launch(Intent(Intent.ACTION_OPEN_DOCUMENT_TREE).data) }, 51 ) { mainActivity.getGamesDirectory.launch(Intent(Intent.ACTION_OPEN_DOCUMENT_TREE).data) },
63 HomeOption( 52 HomeOption(
64 R.string.install_prod_keys, 53 R.string.install_prod_keys,
65 R.string.install_prod_keys_description, 54 R.string.install_prod_keys_description,
66 R.drawable.ic_unlock 55 R.drawable.ic_unlock
67 ) { getProdKey.launch(arrayOf("*/*")) }, 56 ) { mainActivity.getProdKey.launch(arrayOf("*/*")) },
68 HomeOption( 57 HomeOption(
69 R.string.install_amiibo_keys, 58 R.string.install_amiibo_keys,
70 R.string.install_amiibo_keys_description, 59 R.string.install_amiibo_keys_description,
71 R.drawable.ic_nfc 60 R.drawable.ic_nfc
72 ) { getAmiiboKey.launch(arrayOf("*/*")) }, 61 ) { mainActivity.getAmiiboKey.launch(arrayOf("*/*")) },
73 HomeOption( 62 HomeOption(
74 R.string.install_gpu_driver, 63 R.string.install_gpu_driver,
75 R.string.install_gpu_driver_description, 64 R.string.install_gpu_driver_description,
@@ -115,7 +104,7 @@ class OptionsFragment : Fragment() {
115 ).show() 104 ).show()
116 } 105 }
117 .setNeutralButton(R.string.select_gpu_driver_install) { _: DialogInterface?, _: Int -> 106 .setNeutralButton(R.string.select_gpu_driver_install) { _: DialogInterface?, _: Int ->
118 getDriver.launch(arrayOf("application/zip")) 107 mainActivity.getDriver.launch(arrayOf("application/zip"))
119 } 108 }
120 .show() 109 .show()
121 } 110 }
@@ -131,144 +120,4 @@ class OptionsFragment : Fragment() {
131 ) 120 )
132 windowInsets 121 windowInsets
133 } 122 }
134
135 private val getGamesDirectory =
136 registerForActivityResult(ActivityResultContracts.OpenDocumentTree()) { result ->
137 if (result == null)
138 return@registerForActivityResult
139
140 val takeFlags =
141 Intent.FLAG_GRANT_WRITE_URI_PERMISSION or Intent.FLAG_GRANT_READ_URI_PERMISSION
142 requireActivity().contentResolver.takePersistableUriPermission(
143 result,
144 takeFlags
145 )
146
147 // When a new directory is picked, we currently will reset the existing games
148 // database. This effectively means that only one game directory is supported.
149 PreferenceManager.getDefaultSharedPreferences(requireContext()).edit()
150 .putString(GameHelper.KEY_GAME_PATH, result.toString())
151 .apply()
152
153 gamesViewModel.reloadGames(true)
154 }
155
156 private val getProdKey =
157 registerForActivityResult(ActivityResultContracts.OpenDocument()) { result ->
158 if (result == null)
159 return@registerForActivityResult
160
161 val takeFlags =
162 Intent.FLAG_GRANT_WRITE_URI_PERMISSION or Intent.FLAG_GRANT_READ_URI_PERMISSION
163 requireActivity().contentResolver.takePersistableUriPermission(
164 result,
165 takeFlags
166 )
167
168 val dstPath = DirectoryInitialization.userDirectory + "/keys/"
169 if (FileUtil.copyUriToInternalStorage(requireContext(), result, dstPath, "prod.keys")) {
170 if (NativeLibrary.reloadKeys()) {
171 Toast.makeText(
172 requireContext(),
173 R.string.install_keys_success,
174 Toast.LENGTH_SHORT
175 ).show()
176 gamesViewModel.reloadGames(true)
177 } else {
178 Toast.makeText(
179 requireContext(),
180 R.string.install_keys_failure,
181 Toast.LENGTH_LONG
182 ).show()
183 }
184 }
185 }
186
187 private val getAmiiboKey =
188 registerForActivityResult(ActivityResultContracts.OpenDocument()) { result ->
189 if (result == null)
190 return@registerForActivityResult
191
192 val takeFlags =
193 Intent.FLAG_GRANT_WRITE_URI_PERMISSION or Intent.FLAG_GRANT_READ_URI_PERMISSION
194 requireActivity().contentResolver.takePersistableUriPermission(
195 result,
196 takeFlags
197 )
198
199 val dstPath = DirectoryInitialization.userDirectory + "/keys/"
200 if (FileUtil.copyUriToInternalStorage(
201 requireContext(),
202 result,
203 dstPath,
204 "key_retail.bin"
205 )
206 ) {
207 if (NativeLibrary.reloadKeys()) {
208 Toast.makeText(
209 requireContext(),
210 R.string.install_keys_success,
211 Toast.LENGTH_SHORT
212 ).show()
213 } else {
214 Toast.makeText(
215 requireContext(),
216 R.string.install_amiibo_keys_failure,
217 Toast.LENGTH_LONG
218 ).show()
219 }
220 }
221 }
222
223 private val getDriver =
224 registerForActivityResult(ActivityResultContracts.OpenDocument()) { result ->
225 if (result == null)
226 return@registerForActivityResult
227
228 val takeFlags =
229 Intent.FLAG_GRANT_WRITE_URI_PERMISSION or Intent.FLAG_GRANT_READ_URI_PERMISSION
230 requireActivity().contentResolver.takePersistableUriPermission(
231 result,
232 takeFlags
233 )
234
235 val progressBinding = DialogProgressBarBinding.inflate(layoutInflater)
236 progressBinding.progressBar.isIndeterminate = true
237 val installationDialog = MaterialAlertDialogBuilder(requireContext())
238 .setTitle(R.string.installing_driver)
239 .setView(progressBinding.root)
240 .show()
241
242 lifecycleScope.launch {
243 withContext(Dispatchers.IO) {
244 // Ignore file exceptions when a user selects an invalid zip
245 try {
246 GpuDriverHelper.installCustomDriver(requireContext(), result)
247 } catch (_: IOException) {
248 }
249
250 withContext(Dispatchers.Main) {
251 installationDialog.dismiss()
252
253 val driverName = GpuDriverHelper.customDriverName
254 if (driverName != null) {
255 Toast.makeText(
256 requireContext(),
257 getString(
258 R.string.select_gpu_driver_install_success,
259 driverName
260 ),
261 Toast.LENGTH_SHORT
262 ).show()
263 } else {
264 Toast.makeText(
265 requireContext(),
266 R.string.select_gpu_driver_error,
267 Toast.LENGTH_LONG
268 ).show()
269 }
270 }
271 }
272 }
273 }
274} 123}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/SetupFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/SetupFragment.kt
new file mode 100644
index 000000000..e7d102aad
--- /dev/null
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/SetupFragment.kt
@@ -0,0 +1,206 @@
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.content.Intent
7import android.os.Bundle
8import android.view.LayoutInflater
9import android.view.View
10import android.view.ViewGroup
11import androidx.activity.OnBackPressedCallback
12import androidx.appcompat.app.AppCompatActivity
13import androidx.core.content.ContextCompat
14import androidx.core.view.ViewCompat
15import androidx.core.view.WindowInsetsCompat
16import androidx.fragment.app.Fragment
17import androidx.fragment.app.activityViewModels
18import androidx.navigation.findNavController
19import androidx.preference.PreferenceManager
20import androidx.viewpager2.widget.ViewPager2.OnPageChangeCallback
21import com.google.android.material.transition.MaterialFadeThrough
22import org.yuzu.yuzu_emu.R
23import org.yuzu.yuzu_emu.YuzuApplication
24import org.yuzu.yuzu_emu.adapters.SetupAdapter
25import org.yuzu.yuzu_emu.databinding.FragmentSetupBinding
26import org.yuzu.yuzu_emu.features.settings.model.Settings
27import org.yuzu.yuzu_emu.model.HomeViewModel
28import org.yuzu.yuzu_emu.model.SetupPage
29import org.yuzu.yuzu_emu.ui.main.MainActivity
30
31class SetupFragment : Fragment() {
32 private var _binding: FragmentSetupBinding? = null
33 private val binding get() = _binding!!
34
35 private val homeViewModel: HomeViewModel by activityViewModels()
36
37 private lateinit var mainActivity: MainActivity
38
39 override fun onCreate(savedInstanceState: Bundle?) {
40 super.onCreate(savedInstanceState)
41 exitTransition = MaterialFadeThrough()
42 }
43
44 override fun onCreateView(
45 inflater: LayoutInflater,
46 container: ViewGroup?,
47 savedInstanceState: Bundle?
48 ): View {
49 _binding = FragmentSetupBinding.inflate(layoutInflater)
50 return binding.root
51 }
52
53 override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
54 mainActivity = requireActivity() as MainActivity
55
56 homeViewModel.setNavigationVisibility(false)
57
58 requireActivity().onBackPressedDispatcher.addCallback(
59 viewLifecycleOwner,
60 object : OnBackPressedCallback(true) {
61 override fun handleOnBackPressed() {
62 if (binding.viewPager2.currentItem > 0) {
63 pageBackward()
64 } else {
65 requireActivity().finish()
66 }
67 }
68 })
69
70 requireActivity().window.navigationBarColor =
71 ContextCompat.getColor(requireContext(), android.R.color.transparent)
72
73 val pages = listOf(
74 SetupPage(
75 R.drawable.ic_yuzu_title,
76 R.string.welcome,
77 R.string.welcome_description,
78 0,
79 true,
80 R.string.get_started
81 ) { pageForward() },
82 SetupPage(
83 R.drawable.ic_key,
84 R.string.keys,
85 R.string.keys_description,
86 R.drawable.ic_add,
87 true,
88 R.string.select_keys
89 ) { mainActivity.getProdKey.launch(arrayOf("*/*")) },
90 SetupPage(
91 R.drawable.ic_controller,
92 R.string.games,
93 R.string.games_description,
94 R.drawable.ic_add,
95 true,
96 R.string.add_games
97 ) { mainActivity.getGamesDirectory.launch(Intent(Intent.ACTION_OPEN_DOCUMENT_TREE).data) },
98 SetupPage(
99 R.drawable.ic_check,
100 R.string.done,
101 R.string.done_description,
102 R.drawable.ic_arrow_forward,
103 false,
104 R.string.text_continue
105 ) { finishSetup() }
106 )
107 binding.viewPager2.apply {
108 adapter = SetupAdapter(requireActivity() as AppCompatActivity, pages)
109 offscreenPageLimit = 2
110 }
111
112 binding.viewPager2.registerOnPageChangeCallback(object : OnPageChangeCallback() {
113 override fun onPageScrolled(
114 position: Int,
115 positionOffset: Float,
116 positionOffsetPixels: Int
117 ) {
118 super.onPageScrolled(position, positionOffset, positionOffsetPixels)
119 if (position == 0) {
120 hideView(binding.buttonBack)
121 } else {
122 showView(binding.buttonBack)
123 }
124
125 if (position == pages.size - 1 || position == 0) {
126 hideView(binding.buttonNext)
127 } else {
128 showView(binding.buttonNext)
129 }
130 }
131 })
132
133 binding.buttonNext.setOnClickListener { pageForward() }
134 binding.buttonBack.setOnClickListener { pageBackward() }
135
136 if (binding.viewPager2.currentItem == 0) {
137 binding.buttonNext.visibility = View.INVISIBLE
138 binding.buttonBack.visibility = View.INVISIBLE
139 }
140
141 setInsets()
142 }
143
144 override fun onDestroyView() {
145 super.onDestroyView()
146 _binding = null
147 }
148
149 private fun finishSetup() {
150 PreferenceManager.getDefaultSharedPreferences(YuzuApplication.appContext).edit()
151 .putBoolean(Settings.PREF_FIRST_APP_LAUNCH, false)
152 .apply()
153 mainActivity.finishSetup(binding.root.findNavController())
154 }
155
156 private fun showView(view: View) {
157 if (view.visibility == View.VISIBLE) {
158 return
159 }
160
161 view.apply {
162 alpha = 0f
163 visibility = View.VISIBLE
164 isClickable = true
165 }.animate().apply {
166 duration = 300
167 alpha(1f)
168 }.start()
169 }
170
171 private fun hideView(view: View) {
172 if (view.visibility == View.GONE) {
173 return
174 }
175
176 view.apply {
177 alpha = 1f
178 isClickable = false
179 }.animate().apply {
180 duration = 300
181 alpha(0f)
182 }.withEndAction {
183 view.visibility = View.INVISIBLE
184 }
185 }
186
187 private fun pageForward() {
188 binding.viewPager2.currentItem = binding.viewPager2.currentItem + 1
189 }
190
191 private fun pageBackward() {
192 binding.viewPager2.currentItem = binding.viewPager2.currentItem - 1
193 }
194
195 private fun setInsets() =
196 ViewCompat.setOnApplyWindowInsetsListener(binding.setupRoot) { view: View, windowInsets: WindowInsetsCompat ->
197 val insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars())
198 view.setPadding(
199 insets.left,
200 insets.top,
201 insets.right,
202 insets.bottom
203 )
204 windowInsets
205 }
206}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/HomeViewModel.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/HomeViewModel.kt
index b3f4188cd..acda8663a 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/HomeViewModel.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/HomeViewModel.kt
@@ -11,6 +11,8 @@ class HomeViewModel : ViewModel() {
11 private val _statusBarShadeVisible = MutableLiveData(true) 11 private val _statusBarShadeVisible = MutableLiveData(true)
12 val statusBarShadeVisible: LiveData<Boolean> get() = _statusBarShadeVisible 12 val statusBarShadeVisible: LiveData<Boolean> get() = _statusBarShadeVisible
13 13
14 var navigatedToSetup = false
15
14 fun setNavigationVisibility(visible: Boolean) { 16 fun setNavigationVisibility(visible: Boolean) {
15 if (_navigationVisible.value == visible) { 17 if (_navigationVisible.value == visible) {
16 return 18 return
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/SetupPage.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/SetupPage.kt
new file mode 100644
index 000000000..a8a934552
--- /dev/null
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/SetupPage.kt
@@ -0,0 +1,14 @@
1// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
2// SPDX-License-Identifier: GPL-2.0-or-later
3
4package org.yuzu.yuzu_emu.model
5
6data class SetupPage(
7 val iconId: Int,
8 val titleId: Int,
9 val descriptionId: Int,
10 val buttonIconId: Int,
11 val leftAlignedIcon: Boolean,
12 val buttonTextId: Int,
13 val buttonAction: () -> Unit
14)
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/GamesFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/GamesFragment.kt
index c6bbc3c65..759ff18fc 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/GamesFragment.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/GamesFragment.kt
@@ -18,6 +18,7 @@ import androidx.fragment.app.activityViewModels
18import com.google.android.material.color.MaterialColors 18import com.google.android.material.color.MaterialColors
19import com.google.android.material.search.SearchView 19import com.google.android.material.search.SearchView
20import com.google.android.material.search.SearchView.TransitionState 20import com.google.android.material.search.SearchView.TransitionState
21import com.google.android.material.transition.MaterialFadeThrough
21import info.debatty.java.stringsimilarity.Jaccard 22import info.debatty.java.stringsimilarity.Jaccard
22import org.yuzu.yuzu_emu.R 23import org.yuzu.yuzu_emu.R
23import org.yuzu.yuzu_emu.adapters.GameAdapter 24import org.yuzu.yuzu_emu.adapters.GameAdapter
@@ -35,6 +36,11 @@ class GamesFragment : Fragment() {
35 private val gamesViewModel: GamesViewModel by activityViewModels() 36 private val gamesViewModel: GamesViewModel by activityViewModels()
36 private val homeViewModel: HomeViewModel by activityViewModels() 37 private val homeViewModel: HomeViewModel by activityViewModels()
37 38
39 override fun onCreate(savedInstanceState: Bundle?) {
40 super.onCreate(savedInstanceState)
41 enterTransition = MaterialFadeThrough()
42 }
43
38 override fun onCreateView( 44 override fun onCreateView(
39 inflater: LayoutInflater, 45 inflater: LayoutInflater,
40 container: ViewGroup?, 46 container: ViewGroup?,
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 e47866030..b455b7d35 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
@@ -3,10 +3,13 @@
3 3
4package org.yuzu.yuzu_emu.ui.main 4package org.yuzu.yuzu_emu.ui.main
5 5
6import android.content.Intent
6import android.os.Bundle 7import android.os.Bundle
7import android.view.View 8import android.view.View
8import android.view.ViewGroup.MarginLayoutParams 9import android.view.ViewGroup.MarginLayoutParams
9import android.view.animation.PathInterpolator 10import android.view.animation.PathInterpolator
11import android.widget.Toast
12import androidx.activity.result.contract.ActivityResultContracts
10import androidx.activity.viewModels 13import androidx.activity.viewModels
11import androidx.appcompat.app.AppCompatActivity 14import androidx.appcompat.app.AppCompatActivity
12import androidx.core.content.ContextCompat 15import androidx.core.content.ContextCompat
@@ -14,20 +17,33 @@ import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
14import androidx.core.view.ViewCompat 17import androidx.core.view.ViewCompat
15import androidx.core.view.WindowCompat 18import androidx.core.view.WindowCompat
16import androidx.core.view.WindowInsetsCompat 19import androidx.core.view.WindowInsetsCompat
20import androidx.lifecycle.lifecycleScope
21import androidx.navigation.NavController
17import androidx.navigation.fragment.NavHostFragment 22import androidx.navigation.fragment.NavHostFragment
18import androidx.navigation.ui.setupWithNavController 23import androidx.navigation.ui.setupWithNavController
24import androidx.preference.PreferenceManager
19import com.google.android.material.color.MaterialColors 25import com.google.android.material.color.MaterialColors
26import com.google.android.material.dialog.MaterialAlertDialogBuilder
20import com.google.android.material.elevation.ElevationOverlayProvider 27import com.google.android.material.elevation.ElevationOverlayProvider
28import kotlinx.coroutines.Dispatchers
29import kotlinx.coroutines.launch
30import kotlinx.coroutines.withContext
31import org.yuzu.yuzu_emu.NativeLibrary
21import org.yuzu.yuzu_emu.R 32import org.yuzu.yuzu_emu.R
22import org.yuzu.yuzu_emu.activities.EmulationActivity 33import org.yuzu.yuzu_emu.activities.EmulationActivity
23import org.yuzu.yuzu_emu.databinding.ActivityMainBinding 34import org.yuzu.yuzu_emu.databinding.ActivityMainBinding
35import org.yuzu.yuzu_emu.databinding.DialogProgressBarBinding
36import org.yuzu.yuzu_emu.features.settings.model.Settings
37import org.yuzu.yuzu_emu.model.GamesViewModel
24import org.yuzu.yuzu_emu.model.HomeViewModel 38import org.yuzu.yuzu_emu.model.HomeViewModel
25import org.yuzu.yuzu_emu.utils.* 39import org.yuzu.yuzu_emu.utils.*
40import java.io.IOException
26 41
27class MainActivity : AppCompatActivity() { 42class MainActivity : AppCompatActivity() {
28 private lateinit var binding: ActivityMainBinding 43 private lateinit var binding: ActivityMainBinding
29 44
30 private val homeViewModel: HomeViewModel by viewModels() 45 private val homeViewModel: HomeViewModel by viewModels()
46 private val gamesViewModel: GamesViewModel by viewModels()
31 47
32 override fun onCreate(savedInstanceState: Bundle?) { 48 override fun onCreate(savedInstanceState: Bundle?) {
33 val splashScreen = installSplashScreen() 49 val splashScreen = installSplashScreen()
@@ -52,10 +68,9 @@ class MainActivity : AppCompatActivity() {
52 ) 68 )
53 ) 69 )
54 70
55 // Set up a central host fragment that is controlled via bottom navigation with xml navigation
56 val navHostFragment = 71 val navHostFragment =
57 supportFragmentManager.findFragmentById(R.id.fragment_container) as NavHostFragment 72 supportFragmentManager.findFragmentById(R.id.fragment_container) as NavHostFragment
58 binding.navigationBar.setupWithNavController(navHostFragment.navController) 73 setUpNavigation(navHostFragment.navController)
59 74
60 binding.statusBarShade.setBackgroundColor( 75 binding.statusBarShade.setBackgroundColor(
61 ThemeHelper.getColorWithOpacity( 76 ThemeHelper.getColorWithOpacity(
@@ -85,6 +100,32 @@ class MainActivity : AppCompatActivity() {
85 setInsets() 100 setInsets()
86 } 101 }
87 102
103 fun finishSetup(navController: NavController) {
104 navController.navigate(R.id.action_firstTimeSetupFragment_to_gamesFragment)
105 binding.navigationBar.setupWithNavController(navController)
106 showNavigation(true)
107
108 ThemeHelper.setNavigationBarColor(
109 this,
110 ElevationOverlayProvider(binding.navigationBar.context).compositeOverlay(
111 MaterialColors.getColor(binding.navigationBar, R.attr.colorSurface),
112 binding.navigationBar.elevation
113 )
114 )
115 }
116
117 private fun setUpNavigation(navController: NavController) {
118 val firstTimeSetup = PreferenceManager.getDefaultSharedPreferences(applicationContext)
119 .getBoolean(Settings.PREF_FIRST_APP_LAUNCH, true)
120
121 if (firstTimeSetup && !homeViewModel.navigatedToSetup) {
122 navController.navigate(R.id.firstTimeSetupFragment)
123 homeViewModel.navigatedToSetup = true
124 } else {
125 binding.navigationBar.setupWithNavController(navController)
126 }
127 }
128
88 private fun showNavigation(visible: Boolean) { 129 private fun showNavigation(visible: Boolean) {
89 binding.navigationBar.animate().apply { 130 binding.navigationBar.animate().apply {
90 if (visible) { 131 if (visible) {
@@ -138,4 +179,150 @@ class MainActivity : AppCompatActivity() {
138 binding.statusBarShade.layoutParams = mlpShade 179 binding.statusBarShade.layoutParams = mlpShade
139 windowInsets 180 windowInsets
140 } 181 }
182
183 val getGamesDirectory =
184 registerForActivityResult(ActivityResultContracts.OpenDocumentTree()) { result ->
185 if (result == null)
186 return@registerForActivityResult
187
188 val takeFlags =
189 Intent.FLAG_GRANT_WRITE_URI_PERMISSION or Intent.FLAG_GRANT_READ_URI_PERMISSION
190 contentResolver.takePersistableUriPermission(
191 result,
192 takeFlags
193 )
194
195 // When a new directory is picked, we currently will reset the existing games
196 // database. This effectively means that only one game directory is supported.
197 PreferenceManager.getDefaultSharedPreferences(applicationContext).edit()
198 .putString(GameHelper.KEY_GAME_PATH, result.toString())
199 .apply()
200
201 Toast.makeText(
202 applicationContext,
203 R.string.games_dir_selected,
204 Toast.LENGTH_LONG
205 ).show()
206
207 gamesViewModel.reloadGames(true)
208 }
209
210 val getProdKey =
211 registerForActivityResult(ActivityResultContracts.OpenDocument()) { result ->
212 if (result == null)
213 return@registerForActivityResult
214
215 val takeFlags =
216 Intent.FLAG_GRANT_WRITE_URI_PERMISSION or Intent.FLAG_GRANT_READ_URI_PERMISSION
217 contentResolver.takePersistableUriPermission(
218 result,
219 takeFlags
220 )
221
222 val dstPath = DirectoryInitialization.userDirectory + "/keys/"
223 if (FileUtil.copyUriToInternalStorage(applicationContext, result, dstPath, "prod.keys")) {
224 if (NativeLibrary.reloadKeys()) {
225 Toast.makeText(
226 applicationContext,
227 R.string.install_keys_success,
228 Toast.LENGTH_SHORT
229 ).show()
230 gamesViewModel.reloadGames(true)
231 } else {
232 Toast.makeText(
233 applicationContext,
234 R.string.install_keys_failure,
235 Toast.LENGTH_LONG
236 ).show()
237 }
238 }
239 }
240
241 val getAmiiboKey =
242 registerForActivityResult(ActivityResultContracts.OpenDocument()) { result ->
243 if (result == null)
244 return@registerForActivityResult
245
246 val takeFlags =
247 Intent.FLAG_GRANT_WRITE_URI_PERMISSION or Intent.FLAG_GRANT_READ_URI_PERMISSION
248 contentResolver.takePersistableUriPermission(
249 result,
250 takeFlags
251 )
252
253 val dstPath = DirectoryInitialization.userDirectory + "/keys/"
254 if (FileUtil.copyUriToInternalStorage(
255 applicationContext,
256 result,
257 dstPath,
258 "key_retail.bin"
259 )
260 ) {
261 if (NativeLibrary.reloadKeys()) {
262 Toast.makeText(
263 applicationContext,
264 R.string.install_keys_success,
265 Toast.LENGTH_SHORT
266 ).show()
267 } else {
268 Toast.makeText(
269 applicationContext,
270 R.string.install_amiibo_keys_failure,
271 Toast.LENGTH_LONG
272 ).show()
273 }
274 }
275 }
276
277 val getDriver =
278 registerForActivityResult(ActivityResultContracts.OpenDocument()) { result ->
279 if (result == null)
280 return@registerForActivityResult
281
282 val takeFlags =
283 Intent.FLAG_GRANT_WRITE_URI_PERMISSION or Intent.FLAG_GRANT_READ_URI_PERMISSION
284 contentResolver.takePersistableUriPermission(
285 result,
286 takeFlags
287 )
288
289 val progressBinding = DialogProgressBarBinding.inflate(layoutInflater)
290 progressBinding.progressBar.isIndeterminate = true
291 val installationDialog = MaterialAlertDialogBuilder(this)
292 .setTitle(R.string.installing_driver)
293 .setView(progressBinding.root)
294 .show()
295
296 lifecycleScope.launch {
297 withContext(Dispatchers.IO) {
298 // Ignore file exceptions when a user selects an invalid zip
299 try {
300 GpuDriverHelper.installCustomDriver(applicationContext, result)
301 } catch (_: IOException) {
302 }
303
304 withContext(Dispatchers.Main) {
305 installationDialog.dismiss()
306
307 val driverName = GpuDriverHelper.customDriverName
308 if (driverName != null) {
309 Toast.makeText(
310 applicationContext,
311 getString(
312 R.string.select_gpu_driver_install_success,
313 driverName
314 ),
315 Toast.LENGTH_SHORT
316 ).show()
317 } else {
318 Toast.makeText(
319 applicationContext,
320 R.string.select_gpu_driver_error,
321 Toast.LENGTH_LONG
322 ).show()
323 }
324 }
325 }
326 }
327 }
141} 328}
diff --git a/src/android/app/src/main/res/drawable/ic_arrow_forward.xml b/src/android/app/src/main/res/drawable/ic_arrow_forward.xml
new file mode 100644
index 000000000..3b85a3e2c
--- /dev/null
+++ b/src/android/app/src/main/res/drawable/ic_arrow_forward.xml
@@ -0,0 +1,10 @@
1<vector xmlns:android="http://schemas.android.com/apk/res/android"
2 android:width="24dp"
3 android:height="24dp"
4 android:autoMirrored="true"
5 android:viewportWidth="24"
6 android:viewportHeight="24">
7 <path
8 android:fillColor="?attr/colorControlNormal"
9 android:pathData="M12,4l-1.41,1.41L16.17,11H4v2h12.17l-5.58,5.59L12,20l8,-8z" />
10</vector>
diff --git a/src/android/app/src/main/res/drawable/ic_check.xml b/src/android/app/src/main/res/drawable/ic_check.xml
new file mode 100644
index 000000000..04b89abf2
--- /dev/null
+++ b/src/android/app/src/main/res/drawable/ic_check.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/colorOnSurface"
8 android:pathData="M9,16.17L4.83,12l-1.42,1.41L9,19 21,7l-1.41,-1.41z" />
9</vector>
diff --git a/src/android/app/src/main/res/drawable/ic_controller.xml b/src/android/app/src/main/res/drawable/ic_controller.xml
index 2359c35be..060cd9ae2 100644
--- a/src/android/app/src/main/res/drawable/ic_controller.xml
+++ b/src/android/app/src/main/res/drawable/ic_controller.xml
@@ -4,6 +4,6 @@
4 android:viewportHeight="24" 4 android:viewportHeight="24"
5 android:viewportWidth="24"> 5 android:viewportWidth="24">
6 <path 6 <path
7 android:fillColor="?attr/colorControlNormal" 7 android:fillColor="?attr/colorOnSurface"
8 android:pathData="M21,6L3,6c-1.1,0 -2,0.9 -2,2v8c0,1.1 0.9,2 2,2h18c1.1,0 2,-0.9 2,-2L23,8c0,-1.1 -0.9,-2 -2,-2zM11,13L8,13v3L6,16v-3L3,13v-2h3L6,8h2v3h3v2zM15.5,15c-0.83,0 -1.5,-0.67 -1.5,-1.5s0.67,-1.5 1.5,-1.5 1.5,0.67 1.5,1.5 -0.67,1.5 -1.5,1.5zM19.5,12c-0.83,0 -1.5,-0.67 -1.5,-1.5S18.67,9 19.5,9s1.5,0.67 1.5,1.5 -0.67,1.5 -1.5,1.5z" /> 8 android:pathData="M21,6L3,6c-1.1,0 -2,0.9 -2,2v8c0,1.1 0.9,2 2,2h18c1.1,0 2,-0.9 2,-2L23,8c0,-1.1 -0.9,-2 -2,-2zM11,13L8,13v3L6,16v-3L3,13v-2h3L6,8h2v3h3v2zM15.5,15c-0.83,0 -1.5,-0.67 -1.5,-1.5s0.67,-1.5 1.5,-1.5 1.5,0.67 1.5,1.5 -0.67,1.5 -1.5,1.5zM19.5,12c-0.83,0 -1.5,-0.67 -1.5,-1.5S18.67,9 19.5,9s1.5,0.67 1.5,1.5 -0.67,1.5 -1.5,1.5z" />
9</vector> 9</vector>
diff --git a/src/android/app/src/main/res/drawable/ic_key.xml b/src/android/app/src/main/res/drawable/ic_key.xml
new file mode 100644
index 000000000..a3943634f
--- /dev/null
+++ b/src/android/app/src/main/res/drawable/ic_key.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/colorOnSurface"
8 android:pathData="M21,10h-8.35C11.83,7.67 9.61,6 7,6c-3.31,0 -6,2.69 -6,6s2.69,6 6,6c2.61,0 4.83,-1.67 5.65,-4H13l2,2l2,-2l2,2l4,-4.04L21,10zM7,15c-1.65,0 -3,-1.35 -3,-3c0,-1.65 1.35,-3 3,-3s3,1.35 3,3C10,13.65 8.65,15 7,15z" />
9</vector>
diff --git a/src/android/app/src/main/res/drawable/ic_yuzu_title.xml b/src/android/app/src/main/res/drawable/ic_yuzu_title.xml
new file mode 100644
index 000000000..b733e5248
--- /dev/null
+++ b/src/android/app/src/main/res/drawable/ic_yuzu_title.xml
@@ -0,0 +1,24 @@
1<vector xmlns:android="http://schemas.android.com/apk/res/android"
2 android:width="340.97dp"
3 android:height="389.85dp"
4 android:viewportWidth="340.97"
5 android:viewportHeight="389.85">
6 <path
7 android:fillColor="?attr/colorOnSurface"
8 android:pathData="M341,268.68v73c0,14.5 -2.24,25.24 -6.83,32.82 -5.92,10.15 -16.21,15.32 -30.54,15.32S279,384.61 273,374.27c-4.56,-7.64 -6.8,-18.42 -6.8,-32.92V268.68a4.52,4.52 0,0 1,4.51 -4.51H273a4.5,4.5 0,0 1,4.5 4.51v72.5c0,33.53 14.88,37.4 26.07,37.4 12.14,0 26.08,-4.17 26.08,-36.71V268.68a4.52,4.52 0,0 1,4.52 -4.51h2.27A4.5,4.5 0,0 1,341 268.68Z" />
9 <path
10 android:fillColor="?attr/colorOnSurface"
11 android:pathData="M246.49,389.85H178.6c-2.35,0 -4.72,-1.88 -4.72,-6.08a8.28,8.28 0,0 1,1.33 -4.48l60.33,-104.47H186a4.51,4.51 0,0 1,-4.51 -4.51v-1.58a4.51,4.51 0,0 1,4.48 -4.51h0.8c58.69,-0.11 59.12,0 59.67,0.07a5.19,5.19 0,0 1,4 5.8,8.69 8.69,0 0,1 -1.33,3.76l-60.6,104.77h58a4.51,4.51 0,0 1,4.51 4.51v2.21A4.51,4.51 0,0 1,246.49 389.85Z" />
12 <path
13 android:fillColor="?attr/colorOnSurface"
14 android:pathData="M73.6,268.68v82.06c0,26 -11.8,38.44 -37.12,39.09h-0.12a4.51,4.51 0,0 1,-4.51 -4.51V383a4.51,4.51 0,0 1,4.48 -4.5c18.49,-0.15 26,-8.23 26,-27.9v-2.37A32.34,32.34 0,0 1,59 351.46c-6.39,5.5 -14.5,8.29 -24.07,8.29C12.09,359.75 0,347.34 0,323.86V268.68a4.52,4.52 0,0 1,4.51 -4.51H6.73a4.52,4.52 0,0 1,4.5 4.51v55c0,7.6 1.82,14.22 5,18.18 3.57,4.56 9.17,6.49 18.75,6.49 10.13,0 17.32,-3.76 22,-11.5 3.61,-5.92 5.43,-13.66 5.43,-23V268.68a4.52,4.52 0,0 1,4.51 -4.51h2.22A4.52,4.52 0,0 1,73.6 268.68Z" />
15 <path
16 android:fillColor="?attr/colorOnSurface"
17 android:pathData="M163.27,268.68v73c0,14.5 -2.24,25.24 -6.84,32.82 -5.92,10.15 -16.2,15.32 -30.53,15.32s-24.62,-5.23 -30.58,-15.57c-4.56,-7.64 -6.79,-18.42 -6.79,-32.92V268.68A4.51,4.51 0,0 1,93 264.17h2.28a4.51,4.51 0,0 1,4.51 4.51v72.5c0,33.53 14.88,37.4 26.07,37.4 12.14,0 26.08,-4.17 26.08,-36.71V268.68a4.51,4.51 0,0 1,4.51 -4.51h2.27A4.51,4.51 0,0 1,163.27 268.68Z" />
18 <path
19 android:fillColor="#ff3c28"
20 android:pathData="M181.2,42.83V214.17a85.67,85.67 0,0 0,0 -171.34M197.93,61.6a69,69 0,0 1,0 133.8V61.6" />
21 <path
22 android:fillColor="#0ab9e6"
23 android:pathData="M159.78,0a85.67,85.67 0,1 0,0 171.33ZM143.05,18.77v133.8A69,69 0,0 1,111 36.92a68.47,68.47 0,0 1,32 -18.15" />
24</vector>
diff --git a/src/android/app/src/main/res/layout-w600dp/fragment_setup.xml b/src/android/app/src/main/res/layout-w600dp/fragment_setup.xml
new file mode 100644
index 000000000..e05af9bdd
--- /dev/null
+++ b/src/android/app/src/main/res/layout-w600dp/fragment_setup.xml
@@ -0,0 +1,38 @@
1<?xml version="1.0" encoding="utf-8"?>
2<androidx.constraintlayout.widget.ConstraintLayout
3 xmlns:android="http://schemas.android.com/apk/res/android"
4 xmlns:app="http://schemas.android.com/apk/res-auto"
5 android:id="@+id/setup_root"
6 android:layout_width="match_parent"
7 android:layout_height="match_parent">
8
9 <androidx.viewpager2.widget.ViewPager2
10 android:id="@+id/viewPager2"
11 android:layout_width="0dp"
12 android:layout_height="0dp"
13 app:layout_constraintBottom_toBottomOf="parent"
14 app:layout_constraintEnd_toEndOf="parent"
15 app:layout_constraintStart_toStartOf="parent"
16 app:layout_constraintTop_toTopOf="parent" />
17
18 <com.google.android.material.button.MaterialButton
19 style="@style/Widget.Material3.Button.TextButton"
20 android:id="@+id/button_next"
21 android:layout_width="wrap_content"
22 android:layout_height="wrap_content"
23 android:layout_margin="16dp"
24 android:text="@string/next"
25 app:layout_constraintBottom_toBottomOf="parent"
26 app:layout_constraintEnd_toEndOf="parent" />
27
28 <com.google.android.material.button.MaterialButton
29 android:id="@+id/button_back"
30 style="@style/Widget.Material3.Button.TextButton"
31 android:layout_width="wrap_content"
32 android:layout_height="wrap_content"
33 android:layout_margin="16dp"
34 android:text="@string/back"
35 app:layout_constraintBottom_toBottomOf="parent"
36 app:layout_constraintStart_toStartOf="parent" />
37
38</androidx.constraintlayout.widget.ConstraintLayout>
diff --git a/src/android/app/src/main/res/layout-w600dp/page_setup.xml b/src/android/app/src/main/res/layout-w600dp/page_setup.xml
new file mode 100644
index 000000000..e1c26b2f8
--- /dev/null
+++ b/src/android/app/src/main/res/layout-w600dp/page_setup.xml
@@ -0,0 +1,65 @@
1<?xml version="1.0" encoding="utf-8"?>
2<LinearLayout
3 xmlns:android="http://schemas.android.com/apk/res/android"
4 xmlns:app="http://schemas.android.com/apk/res-auto"
5 xmlns:tools="http://schemas.android.com/tools"
6 android:layout_width="match_parent"
7 android:layout_height="match_parent">
8
9 <LinearLayout
10 android:layout_width="match_parent"
11 android:layout_height="match_parent"
12 android:orientation="vertical"
13 android:layout_weight="1"
14 android:gravity="center">
15
16 <ImageView
17 android:id="@+id/icon"
18 android:layout_width="260dp"
19 android:layout_height="260dp"
20 android:layout_gravity="center" />
21
22 </LinearLayout>
23
24 <LinearLayout
25 android:layout_width="match_parent"
26 android:layout_height="match_parent"
27 android:layout_weight="1"
28 android:orientation="vertical"
29 android:gravity="center">
30
31 <com.google.android.material.textview.MaterialTextView
32 style="@style/TextAppearance.Material3.DisplaySmall"
33 android:id="@+id/text_title"
34 android:layout_width="match_parent"
35 android:layout_height="wrap_content"
36 android:textAlignment="center"
37 android:textColor="?attr/colorOnSurface"
38 android:textStyle="bold"
39 tools:text="@string/welcome" />
40
41 <com.google.android.material.textview.MaterialTextView
42 style="@style/TextAppearance.Material3.TitleLarge"
43 android:id="@+id/text_description"
44 android:layout_width="match_parent"
45 android:layout_height="wrap_content"
46 android:layout_marginTop="16dp"
47 android:paddingHorizontal="32dp"
48 android:textAlignment="center"
49 android:textSize="26sp"
50 app:lineHeight="40sp"
51 tools:text="@string/welcome_description" />
52
53 <com.google.android.material.button.MaterialButton
54 android:id="@+id/button_action"
55 android:layout_width="wrap_content"
56 android:layout_height="56dp"
57 android:layout_marginTop="32dp"
58 android:textSize="20sp"
59 app:iconSize="24sp"
60 app:iconGravity="end"
61 tools:text="Get started" />
62
63 </LinearLayout>
64
65</LinearLayout>
diff --git a/src/android/app/src/main/res/layout/activity_main.xml b/src/android/app/src/main/res/layout/activity_main.xml
index 68a3eae46..59812ab8e 100644
--- a/src/android/app/src/main/res/layout/activity_main.xml
+++ b/src/android/app/src/main/res/layout/activity_main.xml
@@ -24,10 +24,12 @@
24 android:id="@+id/navigation_bar" 24 android:id="@+id/navigation_bar"
25 android:layout_width="match_parent" 25 android:layout_width="match_parent"
26 android:layout_height="wrap_content" 26 android:layout_height="wrap_content"
27 android:visibility="invisible"
27 app:layout_constraintBottom_toBottomOf="parent" 28 app:layout_constraintBottom_toBottomOf="parent"
28 app:layout_constraintLeft_toLeftOf="parent" 29 app:layout_constraintLeft_toLeftOf="parent"
29 app:layout_constraintRight_toRightOf="parent" 30 app:layout_constraintRight_toRightOf="parent"
30 app:menu="@menu/menu_navigation" /> 31 app:menu="@menu/menu_navigation"
32 tools:visibility="visible" />
31 33
32 <View 34 <View
33 android:id="@+id/status_bar_shade" 35 android:id="@+id/status_bar_shade"
diff --git a/src/android/app/src/main/res/layout/fragment_setup.xml b/src/android/app/src/main/res/layout/fragment_setup.xml
new file mode 100644
index 000000000..6f8993152
--- /dev/null
+++ b/src/android/app/src/main/res/layout/fragment_setup.xml
@@ -0,0 +1,38 @@
1<?xml version="1.0" encoding="utf-8"?>
2<androidx.constraintlayout.widget.ConstraintLayout
3 xmlns:android="http://schemas.android.com/apk/res/android"
4 xmlns:app="http://schemas.android.com/apk/res-auto"
5 android:id="@+id/setup_root"
6 android:layout_width="match_parent"
7 android:layout_height="match_parent">
8
9 <androidx.viewpager2.widget.ViewPager2
10 android:id="@+id/viewPager2"
11 android:layout_width="0dp"
12 android:layout_height="0dp"
13 app:layout_constraintBottom_toBottomOf="parent"
14 app:layout_constraintEnd_toEndOf="parent"
15 app:layout_constraintStart_toStartOf="parent"
16 app:layout_constraintTop_toTopOf="parent" />
17
18 <com.google.android.material.button.MaterialButton
19 style="@style/Widget.Material3.Button.TextButton"
20 android:id="@+id/button_next"
21 android:layout_width="wrap_content"
22 android:layout_height="wrap_content"
23 android:layout_margin="16dp"
24 android:text="@string/next"
25 app:layout_constraintBottom_toBottomOf="parent"
26 app:layout_constraintEnd_toEndOf="parent" />
27
28 <com.google.android.material.button.MaterialButton
29 style="@style/Widget.Material3.Button.TextButton"
30 android:id="@+id/button_back"
31 android:layout_width="wrap_content"
32 android:layout_height="wrap_content"
33 android:layout_margin="16dp"
34 android:text="@string/back"
35 app:layout_constraintBottom_toBottomOf="parent"
36 app:layout_constraintStart_toStartOf="parent" />
37
38</androidx.constraintlayout.widget.ConstraintLayout>
diff --git a/src/android/app/src/main/res/layout/page_setup.xml b/src/android/app/src/main/res/layout/page_setup.xml
new file mode 100644
index 000000000..965019cdb
--- /dev/null
+++ b/src/android/app/src/main/res/layout/page_setup.xml
@@ -0,0 +1,52 @@
1<?xml version="1.0" encoding="utf-8"?>
2<androidx.appcompat.widget.LinearLayoutCompat
3 xmlns:android="http://schemas.android.com/apk/res/android"
4 xmlns:app="http://schemas.android.com/apk/res-auto"
5 xmlns:tools="http://schemas.android.com/tools"
6 android:layout_width="match_parent"
7 android:layout_height="match_parent"
8 android:orientation="vertical"
9 android:paddingBottom="64dp">
10
11 <ImageView
12 android:id="@+id/icon"
13 android:layout_width="220dp"
14 android:layout_height="220dp"
15 android:layout_marginTop="64dp"
16 android:layout_gravity="center" />
17
18 <com.google.android.material.textview.MaterialTextView
19 style="@style/TextAppearance.Material3.DisplayMedium"
20 android:id="@+id/text_title"
21 android:layout_width="match_parent"
22 android:layout_height="wrap_content"
23 android:layout_marginTop="64dp"
24 android:textAlignment="center"
25 android:textColor="?attr/colorOnSurface"
26 android:textStyle="bold"
27 tools:text="@string/welcome" />
28
29 <com.google.android.material.textview.MaterialTextView
30 style="@style/TextAppearance.Material3.TitleLarge"
31 android:id="@+id/text_description"
32 android:layout_width="match_parent"
33 android:layout_height="wrap_content"
34 android:layout_marginTop="24dp"
35 android:paddingHorizontal="32dp"
36 android:textAlignment="center"
37 android:textSize="26sp"
38 app:lineHeight="40sp"
39 tools:text="@string/welcome_description" />
40
41 <com.google.android.material.button.MaterialButton
42 android:id="@+id/button_action"
43 android:layout_width="wrap_content"
44 android:layout_height="56dp"
45 android:layout_marginTop="96dp"
46 android:layout_gravity="center"
47 android:textSize="20sp"
48 app:iconSize="24sp"
49 app:iconGravity="end"
50 tools:text="Get started" />
51
52</androidx.appcompat.widget.LinearLayoutCompat>
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 e85e24a85..5afa901c2 100644
--- a/src/android/app/src/main/res/navigation/home_navigation.xml
+++ b/src/android/app/src/main/res/navigation/home_navigation.xml
@@ -14,4 +14,13 @@
14 android:name="org.yuzu.yuzu_emu.fragments.OptionsFragment" 14 android:name="org.yuzu.yuzu_emu.fragments.OptionsFragment"
15 android:label="OptionsFragment" /> 15 android:label="OptionsFragment" />
16 16
17 <fragment
18 android:id="@+id/firstTimeSetupFragment"
19 android:name="org.yuzu.yuzu_emu.fragments.SetupFragment"
20 android:label="FirstTimeSetupFragment" >
21 <action
22 android:id="@+id/action_firstTimeSetupFragment_to_gamesFragment"
23 app:destination="@id/gamesFragment" />
24 </fragment>
25
17</navigation> 26</navigation>
diff --git a/src/android/app/src/main/res/values/strings.xml b/src/android/app/src/main/res/values/strings.xml
index 564bad081..916f516c0 100644
--- a/src/android/app/src/main/res/values/strings.xml
+++ b/src/android/app/src/main/res/values/strings.xml
@@ -9,12 +9,28 @@
9 <string name="app_notification_channel_description">yuzu Switch emulator notifications</string> 9 <string name="app_notification_channel_description">yuzu Switch emulator notifications</string>
10 <string name="app_notification_running">yuzu is running</string> 10 <string name="app_notification_running">yuzu is running</string>
11 11
12 <!-- Setup strings -->
13 <string name="welcome">Welcome!</string>
14 <string name="welcome_description">Learn how to setup &lt;b>yuzu&lt;/b> and jump into emulation.</string>
15 <string name="get_started">Get started</string>
16 <string name="keys">Keys</string>
17 <string name="keys_description">Select your &lt;b>prod.keys&lt;/b> file with the button below.</string>
18 <string name="select_keys">Select Keys</string>
19 <string name="games">Games</string>
20 <string name="games_description">Select your &lt;b>Games&lt;/b> folder with the button below.</string>
21 <string name="done">Done</string>
22 <string name="done_description">You\'re all set.\nEnjoy your games!</string>
23 <string name="text_continue">Continue</string>
24 <string name="next">Next</string>
25 <string name="back">Back</string>
26
12 <!-- Home strings --> 27 <!-- Home strings -->
13 <string name="home_games">Games</string> 28 <string name="home_games">Games</string>
14 <string name="home_options">Options</string> 29 <string name="home_options">Options</string>
15 <string name="add_games">Add Games</string> 30 <string name="add_games">Add Games</string>
16 <string name="add_games_description">Select your games folder</string> 31 <string name="add_games_description">Select your games folder</string>
17 <string name="home_search_games">Search Games</string> 32 <string name="home_search_games">Search Games</string>
33 <string name="games_dir_selected">Games directory selected</string>
18 <string name="install_prod_keys">Install Prod.keys</string> 34 <string name="install_prod_keys">Install Prod.keys</string>
19 <string name="install_prod_keys_description">Required to decrypt retail games</string> 35 <string name="install_prod_keys_description">Required to decrypt retail games</string>
20 <string name="install_amiibo_keys">Install Amiibo Keys</string> 36 <string name="install_amiibo_keys">Install Amiibo Keys</string>