summaryrefslogtreecommitdiffstats
path: root/src/android/app
diff options
context:
space:
mode:
authorCharles Lombardo <clombardo169@gmail.com>2023-04-20 04:42:18 +0200
committerbunnei <bunneidev@gmail.com>2023-06-03 09:05:52 +0200
commit59525ddbebc4c1a8add986eabd7802a62fedc71b (patch)
treec06ba77c1d13a33918052ab51b01f73c57b8faac /src/android/app
parentandroid: Prevent editing unsafe settings at runtime (diff)
downloadyuzu-59525ddbebc4c1a8add986eabd7802a62fedc71b.tar
yuzu-59525ddbebc4c1a8add986eabd7802a62fedc71b.tar.gz
yuzu-59525ddbebc4c1a8add986eabd7802a62fedc71b.tar.bz2
yuzu-59525ddbebc4c1a8add986eabd7802a62fedc71b.tar.lz
yuzu-59525ddbebc4c1a8add986eabd7802a62fedc71b.tar.xz
yuzu-59525ddbebc4c1a8add986eabd7802a62fedc71b.tar.zst
yuzu-59525ddbebc4c1a8add986eabd7802a62fedc71b.zip
Diffstat (limited to 'src/android/app')
-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 @@
+// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+package org.yuzu.yuzu_emu.adapters
+
+import android.text.Html
+import android.view.LayoutInflater
+import android.view.ViewGroup
+import androidx.appcompat.app.AppCompatActivity
+import androidx.core.content.res.ResourcesCompat
+import androidx.recyclerview.widget.RecyclerView
+import com.google.android.material.button.MaterialButton
+import org.yuzu.yuzu_emu.databinding.PageSetupBinding
+import org.yuzu.yuzu_emu.model.SetupPage
+
+class SetupAdapter(val activity: AppCompatActivity, val pages: List<SetupPage>) :
+ RecyclerView.Adapter<SetupAdapter.SetupPageViewHolder>() {
+ override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SetupPageViewHolder {
+ val binding = PageSetupBinding.inflate(LayoutInflater.from(parent.context), parent, false)
+ return SetupPageViewHolder(binding)
+ }
+
+ override fun getItemCount(): Int = pages.size
+
+ override fun onBindViewHolder(holder: SetupPageViewHolder, position: Int) =
+ holder.bind(pages[position])
+
+ inner class SetupPageViewHolder(val binding: PageSetupBinding) :
+ RecyclerView.ViewHolder(binding.root) {
+ lateinit var page: SetupPage
+
+ init {
+ itemView.tag = this
+ }
+
+ fun bind(page: SetupPage) {
+ this.page = page
+ binding.icon.setImageDrawable(
+ ResourcesCompat.getDrawable(
+ activity.resources,
+ page.iconId,
+ activity.theme
+ )
+ )
+ binding.textTitle.text = activity.resources.getString(page.titleId)
+ binding.textDescription.text =
+ Html.fromHtml(activity.resources.getString(page.descriptionId), 0)
+
+ binding.buttonAction.apply {
+ text = activity.resources.getString(page.buttonTextId)
+ if (page.buttonIconId != 0) {
+ icon = ResourcesCompat.getDrawable(
+ activity.resources,
+ page.buttonIconId,
+ activity.theme
+ )
+ }
+ iconGravity =
+ if (page.leftAlignedIcon) {
+ MaterialButton.ICON_GRAVITY_START
+ } else {
+ MaterialButton.ICON_GRAVITY_END
+ }
+ setOnClickListener {
+ page.buttonAction.invoke()
+ }
+ }
+ }
+ }
+}
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
import android.view.View
import android.view.ViewGroup
import android.widget.Toast
-import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import androidx.fragment.app.Fragment
-import androidx.fragment.app.activityViewModels
-import androidx.lifecycle.lifecycleScope
-import androidx.preference.PreferenceManager
import androidx.recyclerview.widget.LinearLayoutManager
import com.google.android.material.dialog.MaterialAlertDialogBuilder
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.launch
-import kotlinx.coroutines.withContext
-import org.yuzu.yuzu_emu.NativeLibrary
import org.yuzu.yuzu_emu.R
import org.yuzu.yuzu_emu.adapters.HomeOptionAdapter
-import org.yuzu.yuzu_emu.databinding.DialogProgressBarBinding
import org.yuzu.yuzu_emu.databinding.FragmentOptionsBinding
import org.yuzu.yuzu_emu.features.settings.ui.SettingsActivity
import org.yuzu.yuzu_emu.features.settings.utils.SettingsFile
-import org.yuzu.yuzu_emu.model.GamesViewModel
import org.yuzu.yuzu_emu.model.HomeOption
-import org.yuzu.yuzu_emu.utils.DirectoryInitialization
-import org.yuzu.yuzu_emu.utils.FileUtil
-import org.yuzu.yuzu_emu.utils.GameHelper
+import org.yuzu.yuzu_emu.ui.main.MainActivity
import org.yuzu.yuzu_emu.utils.GpuDriverHelper
-import java.io.IOException
class OptionsFragment : Fragment() {
private var _binding: FragmentOptionsBinding? = null
private val binding get() = _binding!!
- private val gamesViewModel: GamesViewModel by activityViewModels()
+ private lateinit var mainActivity: MainActivity
override fun onCreateView(
inflater: LayoutInflater,
@@ -54,22 +41,24 @@ class OptionsFragment : Fragment() {
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ mainActivity = requireActivity() as MainActivity
+
val optionsList: List<HomeOption> = listOf(
HomeOption(
R.string.add_games,
R.string.add_games_description,
R.drawable.ic_add
- ) { getGamesDirectory.launch(Intent(Intent.ACTION_OPEN_DOCUMENT_TREE).data) },
+ ) { mainActivity.getGamesDirectory.launch(Intent(Intent.ACTION_OPEN_DOCUMENT_TREE).data) },
HomeOption(
R.string.install_prod_keys,
R.string.install_prod_keys_description,
R.drawable.ic_unlock
- ) { getProdKey.launch(arrayOf("*/*")) },
+ ) { mainActivity.getProdKey.launch(arrayOf("*/*")) },
HomeOption(
R.string.install_amiibo_keys,
R.string.install_amiibo_keys_description,
R.drawable.ic_nfc
- ) { getAmiiboKey.launch(arrayOf("*/*")) },
+ ) { mainActivity.getAmiiboKey.launch(arrayOf("*/*")) },
HomeOption(
R.string.install_gpu_driver,
R.string.install_gpu_driver_description,
@@ -115,7 +104,7 @@ class OptionsFragment : Fragment() {
).show()
}
.setNeutralButton(R.string.select_gpu_driver_install) { _: DialogInterface?, _: Int ->
- getDriver.launch(arrayOf("application/zip"))
+ mainActivity.getDriver.launch(arrayOf("application/zip"))
}
.show()
}
@@ -131,144 +120,4 @@ class OptionsFragment : Fragment() {
)
windowInsets
}
-
- private val getGamesDirectory =
- registerForActivityResult(ActivityResultContracts.OpenDocumentTree()) { result ->
- if (result == null)
- return@registerForActivityResult
-
- val takeFlags =
- Intent.FLAG_GRANT_WRITE_URI_PERMISSION or Intent.FLAG_GRANT_READ_URI_PERMISSION
- requireActivity().contentResolver.takePersistableUriPermission(
- result,
- takeFlags
- )
-
- // When a new directory is picked, we currently will reset the existing games
- // database. This effectively means that only one game directory is supported.
- PreferenceManager.getDefaultSharedPreferences(requireContext()).edit()
- .putString(GameHelper.KEY_GAME_PATH, result.toString())
- .apply()
-
- gamesViewModel.reloadGames(true)
- }
-
- private val getProdKey =
- registerForActivityResult(ActivityResultContracts.OpenDocument()) { result ->
- if (result == null)
- return@registerForActivityResult
-
- val takeFlags =
- Intent.FLAG_GRANT_WRITE_URI_PERMISSION or Intent.FLAG_GRANT_READ_URI_PERMISSION
- requireActivity().contentResolver.takePersistableUriPermission(
- result,
- takeFlags
- )
-
- val dstPath = DirectoryInitialization.userDirectory + "/keys/"
- if (FileUtil.copyUriToInternalStorage(requireContext(), result, dstPath, "prod.keys")) {
- if (NativeLibrary.reloadKeys()) {
- Toast.makeText(
- requireContext(),
- R.string.install_keys_success,
- Toast.LENGTH_SHORT
- ).show()
- gamesViewModel.reloadGames(true)
- } else {
- Toast.makeText(
- requireContext(),
- R.string.install_keys_failure,
- Toast.LENGTH_LONG
- ).show()
- }
- }
- }
-
- private val getAmiiboKey =
- registerForActivityResult(ActivityResultContracts.OpenDocument()) { result ->
- if (result == null)
- return@registerForActivityResult
-
- val takeFlags =
- Intent.FLAG_GRANT_WRITE_URI_PERMISSION or Intent.FLAG_GRANT_READ_URI_PERMISSION
- requireActivity().contentResolver.takePersistableUriPermission(
- result,
- takeFlags
- )
-
- val dstPath = DirectoryInitialization.userDirectory + "/keys/"
- if (FileUtil.copyUriToInternalStorage(
- requireContext(),
- result,
- dstPath,
- "key_retail.bin"
- )
- ) {
- if (NativeLibrary.reloadKeys()) {
- Toast.makeText(
- requireContext(),
- R.string.install_keys_success,
- Toast.LENGTH_SHORT
- ).show()
- } else {
- Toast.makeText(
- requireContext(),
- R.string.install_amiibo_keys_failure,
- Toast.LENGTH_LONG
- ).show()
- }
- }
- }
-
- private val getDriver =
- registerForActivityResult(ActivityResultContracts.OpenDocument()) { result ->
- if (result == null)
- return@registerForActivityResult
-
- val takeFlags =
- Intent.FLAG_GRANT_WRITE_URI_PERMISSION or Intent.FLAG_GRANT_READ_URI_PERMISSION
- requireActivity().contentResolver.takePersistableUriPermission(
- result,
- takeFlags
- )
-
- val progressBinding = DialogProgressBarBinding.inflate(layoutInflater)
- progressBinding.progressBar.isIndeterminate = true
- val installationDialog = MaterialAlertDialogBuilder(requireContext())
- .setTitle(R.string.installing_driver)
- .setView(progressBinding.root)
- .show()
-
- lifecycleScope.launch {
- withContext(Dispatchers.IO) {
- // Ignore file exceptions when a user selects an invalid zip
- try {
- GpuDriverHelper.installCustomDriver(requireContext(), result)
- } catch (_: IOException) {
- }
-
- withContext(Dispatchers.Main) {
- installationDialog.dismiss()
-
- val driverName = GpuDriverHelper.customDriverName
- if (driverName != null) {
- Toast.makeText(
- requireContext(),
- getString(
- R.string.select_gpu_driver_install_success,
- driverName
- ),
- Toast.LENGTH_SHORT
- ).show()
- } else {
- Toast.makeText(
- requireContext(),
- R.string.select_gpu_driver_error,
- Toast.LENGTH_LONG
- ).show()
- }
- }
- }
- }
- }
}
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 @@
+// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+package org.yuzu.yuzu_emu.fragments
+
+import android.content.Intent
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.activity.OnBackPressedCallback
+import androidx.appcompat.app.AppCompatActivity
+import androidx.core.content.ContextCompat
+import androidx.core.view.ViewCompat
+import androidx.core.view.WindowInsetsCompat
+import androidx.fragment.app.Fragment
+import androidx.fragment.app.activityViewModels
+import androidx.navigation.findNavController
+import androidx.preference.PreferenceManager
+import androidx.viewpager2.widget.ViewPager2.OnPageChangeCallback
+import com.google.android.material.transition.MaterialFadeThrough
+import org.yuzu.yuzu_emu.R
+import org.yuzu.yuzu_emu.YuzuApplication
+import org.yuzu.yuzu_emu.adapters.SetupAdapter
+import org.yuzu.yuzu_emu.databinding.FragmentSetupBinding
+import org.yuzu.yuzu_emu.features.settings.model.Settings
+import org.yuzu.yuzu_emu.model.HomeViewModel
+import org.yuzu.yuzu_emu.model.SetupPage
+import org.yuzu.yuzu_emu.ui.main.MainActivity
+
+class SetupFragment : Fragment() {
+ private var _binding: FragmentSetupBinding? = null
+ private val binding get() = _binding!!
+
+ private val homeViewModel: HomeViewModel by activityViewModels()
+
+ private lateinit var mainActivity: MainActivity
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ exitTransition = MaterialFadeThrough()
+ }
+
+ override fun onCreateView(
+ inflater: LayoutInflater,
+ container: ViewGroup?,
+ savedInstanceState: Bundle?
+ ): View {
+ _binding = FragmentSetupBinding.inflate(layoutInflater)
+ return binding.root
+ }
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ mainActivity = requireActivity() as MainActivity
+
+ homeViewModel.setNavigationVisibility(false)
+
+ requireActivity().onBackPressedDispatcher.addCallback(
+ viewLifecycleOwner,
+ object : OnBackPressedCallback(true) {
+ override fun handleOnBackPressed() {
+ if (binding.viewPager2.currentItem > 0) {
+ pageBackward()
+ } else {
+ requireActivity().finish()
+ }
+ }
+ })
+
+ requireActivity().window.navigationBarColor =
+ ContextCompat.getColor(requireContext(), android.R.color.transparent)
+
+ val pages = listOf(
+ SetupPage(
+ R.drawable.ic_yuzu_title,
+ R.string.welcome,
+ R.string.welcome_description,
+ 0,
+ true,
+ R.string.get_started
+ ) { pageForward() },
+ SetupPage(
+ R.drawable.ic_key,
+ R.string.keys,
+ R.string.keys_description,
+ R.drawable.ic_add,
+ true,
+ R.string.select_keys
+ ) { mainActivity.getProdKey.launch(arrayOf("*/*")) },
+ SetupPage(
+ R.drawable.ic_controller,
+ R.string.games,
+ R.string.games_description,
+ R.drawable.ic_add,
+ true,
+ R.string.add_games
+ ) { mainActivity.getGamesDirectory.launch(Intent(Intent.ACTION_OPEN_DOCUMENT_TREE).data) },
+ SetupPage(
+ R.drawable.ic_check,
+ R.string.done,
+ R.string.done_description,
+ R.drawable.ic_arrow_forward,
+ false,
+ R.string.text_continue
+ ) { finishSetup() }
+ )
+ binding.viewPager2.apply {
+ adapter = SetupAdapter(requireActivity() as AppCompatActivity, pages)
+ offscreenPageLimit = 2
+ }
+
+ binding.viewPager2.registerOnPageChangeCallback(object : OnPageChangeCallback() {
+ override fun onPageScrolled(
+ position: Int,
+ positionOffset: Float,
+ positionOffsetPixels: Int
+ ) {
+ super.onPageScrolled(position, positionOffset, positionOffsetPixels)
+ if (position == 0) {
+ hideView(binding.buttonBack)
+ } else {
+ showView(binding.buttonBack)
+ }
+
+ if (position == pages.size - 1 || position == 0) {
+ hideView(binding.buttonNext)
+ } else {
+ showView(binding.buttonNext)
+ }
+ }
+ })
+
+ binding.buttonNext.setOnClickListener { pageForward() }
+ binding.buttonBack.setOnClickListener { pageBackward() }
+
+ if (binding.viewPager2.currentItem == 0) {
+ binding.buttonNext.visibility = View.INVISIBLE
+ binding.buttonBack.visibility = View.INVISIBLE
+ }
+
+ setInsets()
+ }
+
+ override fun onDestroyView() {
+ super.onDestroyView()
+ _binding = null
+ }
+
+ private fun finishSetup() {
+ PreferenceManager.getDefaultSharedPreferences(YuzuApplication.appContext).edit()
+ .putBoolean(Settings.PREF_FIRST_APP_LAUNCH, false)
+ .apply()
+ mainActivity.finishSetup(binding.root.findNavController())
+ }
+
+ private fun showView(view: View) {
+ if (view.visibility == View.VISIBLE) {
+ return
+ }
+
+ view.apply {
+ alpha = 0f
+ visibility = View.VISIBLE
+ isClickable = true
+ }.animate().apply {
+ duration = 300
+ alpha(1f)
+ }.start()
+ }
+
+ private fun hideView(view: View) {
+ if (view.visibility == View.GONE) {
+ return
+ }
+
+ view.apply {
+ alpha = 1f
+ isClickable = false
+ }.animate().apply {
+ duration = 300
+ alpha(0f)
+ }.withEndAction {
+ view.visibility = View.INVISIBLE
+ }
+ }
+
+ private fun pageForward() {
+ binding.viewPager2.currentItem = binding.viewPager2.currentItem + 1
+ }
+
+ private fun pageBackward() {
+ binding.viewPager2.currentItem = binding.viewPager2.currentItem - 1
+ }
+
+ private fun setInsets() =
+ ViewCompat.setOnApplyWindowInsetsListener(binding.setupRoot) { view: View, windowInsets: WindowInsetsCompat ->
+ val insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars())
+ view.setPadding(
+ insets.left,
+ insets.top,
+ insets.right,
+ insets.bottom
+ )
+ windowInsets
+ }
+}
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() {
private val _statusBarShadeVisible = MutableLiveData(true)
val statusBarShadeVisible: LiveData<Boolean> get() = _statusBarShadeVisible
+ var navigatedToSetup = false
+
fun setNavigationVisibility(visible: Boolean) {
if (_navigationVisible.value == visible) {
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 @@
+// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+package org.yuzu.yuzu_emu.model
+
+data class SetupPage(
+ val iconId: Int,
+ val titleId: Int,
+ val descriptionId: Int,
+ val buttonIconId: Int,
+ val leftAlignedIcon: Boolean,
+ val buttonTextId: Int,
+ val buttonAction: () -> Unit
+)
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
import com.google.android.material.color.MaterialColors
import com.google.android.material.search.SearchView
import com.google.android.material.search.SearchView.TransitionState
+import com.google.android.material.transition.MaterialFadeThrough
import info.debatty.java.stringsimilarity.Jaccard
import org.yuzu.yuzu_emu.R
import org.yuzu.yuzu_emu.adapters.GameAdapter
@@ -35,6 +36,11 @@ class GamesFragment : Fragment() {
private val gamesViewModel: GamesViewModel by activityViewModels()
private val homeViewModel: HomeViewModel by activityViewModels()
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ enterTransition = MaterialFadeThrough()
+ }
+
override fun onCreateView(
inflater: LayoutInflater,
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 @@
package org.yuzu.yuzu_emu.ui.main
+import android.content.Intent
import android.os.Bundle
import android.view.View
import android.view.ViewGroup.MarginLayoutParams
import android.view.animation.PathInterpolator
+import android.widget.Toast
+import androidx.activity.result.contract.ActivityResultContracts
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.ContextCompat
@@ -14,20 +17,33 @@ import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
import androidx.core.view.ViewCompat
import androidx.core.view.WindowCompat
import androidx.core.view.WindowInsetsCompat
+import androidx.lifecycle.lifecycleScope
+import androidx.navigation.NavController
import androidx.navigation.fragment.NavHostFragment
import androidx.navigation.ui.setupWithNavController
+import androidx.preference.PreferenceManager
import com.google.android.material.color.MaterialColors
+import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.elevation.ElevationOverlayProvider
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
+import org.yuzu.yuzu_emu.NativeLibrary
import org.yuzu.yuzu_emu.R
import org.yuzu.yuzu_emu.activities.EmulationActivity
import org.yuzu.yuzu_emu.databinding.ActivityMainBinding
+import org.yuzu.yuzu_emu.databinding.DialogProgressBarBinding
+import org.yuzu.yuzu_emu.features.settings.model.Settings
+import org.yuzu.yuzu_emu.model.GamesViewModel
import org.yuzu.yuzu_emu.model.HomeViewModel
import org.yuzu.yuzu_emu.utils.*
+import java.io.IOException
class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
private val homeViewModel: HomeViewModel by viewModels()
+ private val gamesViewModel: GamesViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
val splashScreen = installSplashScreen()
@@ -52,10 +68,9 @@ class MainActivity : AppCompatActivity() {
)
)
- // Set up a central host fragment that is controlled via bottom navigation with xml navigation
val navHostFragment =
supportFragmentManager.findFragmentById(R.id.fragment_container) as NavHostFragment
- binding.navigationBar.setupWithNavController(navHostFragment.navController)
+ setUpNavigation(navHostFragment.navController)
binding.statusBarShade.setBackgroundColor(
ThemeHelper.getColorWithOpacity(
@@ -85,6 +100,32 @@ class MainActivity : AppCompatActivity() {
setInsets()
}
+ fun finishSetup(navController: NavController) {
+ navController.navigate(R.id.action_firstTimeSetupFragment_to_gamesFragment)
+ binding.navigationBar.setupWithNavController(navController)
+ showNavigation(true)
+
+ ThemeHelper.setNavigationBarColor(
+ this,
+ ElevationOverlayProvider(binding.navigationBar.context).compositeOverlay(
+ MaterialColors.getColor(binding.navigationBar, R.attr.colorSurface),
+ binding.navigationBar.elevation
+ )
+ )
+ }
+
+ private fun setUpNavigation(navController: NavController) {
+ val firstTimeSetup = PreferenceManager.getDefaultSharedPreferences(applicationContext)
+ .getBoolean(Settings.PREF_FIRST_APP_LAUNCH, true)
+
+ if (firstTimeSetup && !homeViewModel.navigatedToSetup) {
+ navController.navigate(R.id.firstTimeSetupFragment)
+ homeViewModel.navigatedToSetup = true
+ } else {
+ binding.navigationBar.setupWithNavController(navController)
+ }
+ }
+
private fun showNavigation(visible: Boolean) {
binding.navigationBar.animate().apply {
if (visible) {
@@ -138,4 +179,150 @@ class MainActivity : AppCompatActivity() {
binding.statusBarShade.layoutParams = mlpShade
windowInsets
}
+
+ val getGamesDirectory =
+ registerForActivityResult(ActivityResultContracts.OpenDocumentTree()) { result ->
+ if (result == null)
+ return@registerForActivityResult
+
+ val takeFlags =
+ Intent.FLAG_GRANT_WRITE_URI_PERMISSION or Intent.FLAG_GRANT_READ_URI_PERMISSION
+ contentResolver.takePersistableUriPermission(
+ result,
+ takeFlags
+ )
+
+ // When a new directory is picked, we currently will reset the existing games
+ // database. This effectively means that only one game directory is supported.
+ PreferenceManager.getDefaultSharedPreferences(applicationContext).edit()
+ .putString(GameHelper.KEY_GAME_PATH, result.toString())
+ .apply()
+
+ Toast.makeText(
+ applicationContext,
+ R.string.games_dir_selected,
+ Toast.LENGTH_LONG
+ ).show()
+
+ gamesViewModel.reloadGames(true)
+ }
+
+ val getProdKey =
+ registerForActivityResult(ActivityResultContracts.OpenDocument()) { result ->
+ if (result == null)
+ return@registerForActivityResult
+
+ val takeFlags =
+ Intent.FLAG_GRANT_WRITE_URI_PERMISSION or Intent.FLAG_GRANT_READ_URI_PERMISSION
+ contentResolver.takePersistableUriPermission(
+ result,
+ takeFlags
+ )
+
+ val dstPath = DirectoryInitialization.userDirectory + "/keys/"
+ if (FileUtil.copyUriToInternalStorage(applicationContext, result, dstPath, "prod.keys")) {
+ if (NativeLibrary.reloadKeys()) {
+ Toast.makeText(
+ applicationContext,
+ R.string.install_keys_success,
+ Toast.LENGTH_SHORT
+ ).show()
+ gamesViewModel.reloadGames(true)
+ } else {
+ Toast.makeText(
+ applicationContext,
+ R.string.install_keys_failure,
+ Toast.LENGTH_LONG
+ ).show()
+ }
+ }
+ }
+
+ val getAmiiboKey =
+ registerForActivityResult(ActivityResultContracts.OpenDocument()) { result ->
+ if (result == null)
+ return@registerForActivityResult
+
+ val takeFlags =
+ Intent.FLAG_GRANT_WRITE_URI_PERMISSION or Intent.FLAG_GRANT_READ_URI_PERMISSION
+ contentResolver.takePersistableUriPermission(
+ result,
+ takeFlags
+ )
+
+ val dstPath = DirectoryInitialization.userDirectory + "/keys/"
+ if (FileUtil.copyUriToInternalStorage(
+ applicationContext,
+ result,
+ dstPath,
+ "key_retail.bin"
+ )
+ ) {
+ if (NativeLibrary.reloadKeys()) {
+ Toast.makeText(
+ applicationContext,
+ R.string.install_keys_success,
+ Toast.LENGTH_SHORT
+ ).show()
+ } else {
+ Toast.makeText(
+ applicationContext,
+ R.string.install_amiibo_keys_failure,
+ Toast.LENGTH_LONG
+ ).show()
+ }
+ }
+ }
+
+ val getDriver =
+ registerForActivityResult(ActivityResultContracts.OpenDocument()) { result ->
+ if (result == null)
+ return@registerForActivityResult
+
+ val takeFlags =
+ Intent.FLAG_GRANT_WRITE_URI_PERMISSION or Intent.FLAG_GRANT_READ_URI_PERMISSION
+ contentResolver.takePersistableUriPermission(
+ result,
+ takeFlags
+ )
+
+ val progressBinding = DialogProgressBarBinding.inflate(layoutInflater)
+ progressBinding.progressBar.isIndeterminate = true
+ val installationDialog = MaterialAlertDialogBuilder(this)
+ .setTitle(R.string.installing_driver)
+ .setView(progressBinding.root)
+ .show()
+
+ lifecycleScope.launch {
+ withContext(Dispatchers.IO) {
+ // Ignore file exceptions when a user selects an invalid zip
+ try {
+ GpuDriverHelper.installCustomDriver(applicationContext, result)
+ } catch (_: IOException) {
+ }
+
+ withContext(Dispatchers.Main) {
+ installationDialog.dismiss()
+
+ val driverName = GpuDriverHelper.customDriverName
+ if (driverName != null) {
+ Toast.makeText(
+ applicationContext,
+ getString(
+ R.string.select_gpu_driver_install_success,
+ driverName
+ ),
+ Toast.LENGTH_SHORT
+ ).show()
+ } else {
+ Toast.makeText(
+ applicationContext,
+ R.string.select_gpu_driver_error,
+ Toast.LENGTH_LONG
+ ).show()
+ }
+ }
+ }
+ }
+ }
}
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 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:autoMirrored="true"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
+ <path
+ android:fillColor="?attr/colorControlNormal"
+ android:pathData="M12,4l-1.41,1.41L16.17,11H4v2h12.17l-5.58,5.59L12,20l8,-8z" />
+</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 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
+ <path
+ android:fillColor="?attr/colorOnSurface"
+ android:pathData="M9,16.17L4.83,12l-1.42,1.41L9,19 21,7l-1.41,-1.41z" />
+</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 @@
android:viewportHeight="24"
android:viewportWidth="24">
<path
- android:fillColor="?attr/colorControlNormal"
+ android:fillColor="?attr/colorOnSurface"
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" />
</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 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
+ <path
+ android:fillColor="?attr/colorOnSurface"
+ 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" />
+</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 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="340.97dp"
+ android:height="389.85dp"
+ android:viewportWidth="340.97"
+ android:viewportHeight="389.85">
+ <path
+ android:fillColor="?attr/colorOnSurface"
+ 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" />
+ <path
+ android:fillColor="?attr/colorOnSurface"
+ 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" />
+ <path
+ android:fillColor="?attr/colorOnSurface"
+ 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" />
+ <path
+ android:fillColor="?attr/colorOnSurface"
+ 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" />
+ <path
+ android:fillColor="#ff3c28"
+ 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" />
+ <path
+ android:fillColor="#0ab9e6"
+ 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" />
+</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 @@
+<?xml version="1.0" encoding="utf-8"?>
+<androidx.constraintlayout.widget.ConstraintLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ android:id="@+id/setup_root"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+
+ <androidx.viewpager2.widget.ViewPager2
+ android:id="@+id/viewPager2"
+ android:layout_width="0dp"
+ android:layout_height="0dp"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toTopOf="parent" />
+
+ <com.google.android.material.button.MaterialButton
+ style="@style/Widget.Material3.Button.TextButton"
+ android:id="@+id/button_next"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_margin="16dp"
+ android:text="@string/next"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintEnd_toEndOf="parent" />
+
+ <com.google.android.material.button.MaterialButton
+ android:id="@+id/button_back"
+ style="@style/Widget.Material3.Button.TextButton"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_margin="16dp"
+ android:text="@string/back"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintStart_toStartOf="parent" />
+
+</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 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="vertical"
+ android:layout_weight="1"
+ android:gravity="center">
+
+ <ImageView
+ android:id="@+id/icon"
+ android:layout_width="260dp"
+ android:layout_height="260dp"
+ android:layout_gravity="center" />
+
+ </LinearLayout>
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:layout_weight="1"
+ android:orientation="vertical"
+ android:gravity="center">
+
+ <com.google.android.material.textview.MaterialTextView
+ style="@style/TextAppearance.Material3.DisplaySmall"
+ android:id="@+id/text_title"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:textAlignment="center"
+ android:textColor="?attr/colorOnSurface"
+ android:textStyle="bold"
+ tools:text="@string/welcome" />
+
+ <com.google.android.material.textview.MaterialTextView
+ style="@style/TextAppearance.Material3.TitleLarge"
+ android:id="@+id/text_description"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="16dp"
+ android:paddingHorizontal="32dp"
+ android:textAlignment="center"
+ android:textSize="26sp"
+ app:lineHeight="40sp"
+ tools:text="@string/welcome_description" />
+
+ <com.google.android.material.button.MaterialButton
+ android:id="@+id/button_action"
+ android:layout_width="wrap_content"
+ android:layout_height="56dp"
+ android:layout_marginTop="32dp"
+ android:textSize="20sp"
+ app:iconSize="24sp"
+ app:iconGravity="end"
+ tools:text="Get started" />
+
+ </LinearLayout>
+
+</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 @@
android:id="@+id/navigation_bar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
+ android:visibility="invisible"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
- app:menu="@menu/menu_navigation" />
+ app:menu="@menu/menu_navigation"
+ tools:visibility="visible" />
<View
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 @@
+<?xml version="1.0" encoding="utf-8"?>
+<androidx.constraintlayout.widget.ConstraintLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ android:id="@+id/setup_root"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+
+ <androidx.viewpager2.widget.ViewPager2
+ android:id="@+id/viewPager2"
+ android:layout_width="0dp"
+ android:layout_height="0dp"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toTopOf="parent" />
+
+ <com.google.android.material.button.MaterialButton
+ style="@style/Widget.Material3.Button.TextButton"
+ android:id="@+id/button_next"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_margin="16dp"
+ android:text="@string/next"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintEnd_toEndOf="parent" />
+
+ <com.google.android.material.button.MaterialButton
+ style="@style/Widget.Material3.Button.TextButton"
+ android:id="@+id/button_back"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_margin="16dp"
+ android:text="@string/back"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintStart_toStartOf="parent" />
+
+</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 @@
+<?xml version="1.0" encoding="utf-8"?>
+<androidx.appcompat.widget.LinearLayoutCompat
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="vertical"
+ android:paddingBottom="64dp">
+
+ <ImageView
+ android:id="@+id/icon"
+ android:layout_width="220dp"
+ android:layout_height="220dp"
+ android:layout_marginTop="64dp"
+ android:layout_gravity="center" />
+
+ <com.google.android.material.textview.MaterialTextView
+ style="@style/TextAppearance.Material3.DisplayMedium"
+ android:id="@+id/text_title"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="64dp"
+ android:textAlignment="center"
+ android:textColor="?attr/colorOnSurface"
+ android:textStyle="bold"
+ tools:text="@string/welcome" />
+
+ <com.google.android.material.textview.MaterialTextView
+ style="@style/TextAppearance.Material3.TitleLarge"
+ android:id="@+id/text_description"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="24dp"
+ android:paddingHorizontal="32dp"
+ android:textAlignment="center"
+ android:textSize="26sp"
+ app:lineHeight="40sp"
+ tools:text="@string/welcome_description" />
+
+ <com.google.android.material.button.MaterialButton
+ android:id="@+id/button_action"
+ android:layout_width="wrap_content"
+ android:layout_height="56dp"
+ android:layout_marginTop="96dp"
+ android:layout_gravity="center"
+ android:textSize="20sp"
+ app:iconSize="24sp"
+ app:iconGravity="end"
+ tools:text="Get started" />
+
+</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 @@
android:name="org.yuzu.yuzu_emu.fragments.OptionsFragment"
android:label="OptionsFragment" />
+ <fragment
+ android:id="@+id/firstTimeSetupFragment"
+ android:name="org.yuzu.yuzu_emu.fragments.SetupFragment"
+ android:label="FirstTimeSetupFragment" >
+ <action
+ android:id="@+id/action_firstTimeSetupFragment_to_gamesFragment"
+ app:destination="@id/gamesFragment" />
+ </fragment>
+
</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 @@
<string name="app_notification_channel_description">yuzu Switch emulator notifications</string>
<string name="app_notification_running">yuzu is running</string>
+ <!-- Setup strings -->
+ <string name="welcome">Welcome!</string>
+ <string name="welcome_description">Learn how to setup &lt;b>yuzu&lt;/b> and jump into emulation.</string>
+ <string name="get_started">Get started</string>
+ <string name="keys">Keys</string>
+ <string name="keys_description">Select your &lt;b>prod.keys&lt;/b> file with the button below.</string>
+ <string name="select_keys">Select Keys</string>
+ <string name="games">Games</string>
+ <string name="games_description">Select your &lt;b>Games&lt;/b> folder with the button below.</string>
+ <string name="done">Done</string>
+ <string name="done_description">You\'re all set.\nEnjoy your games!</string>
+ <string name="text_continue">Continue</string>
+ <string name="next">Next</string>
+ <string name="back">Back</string>
+
<!-- Home strings -->
<string name="home_games">Games</string>
<string name="home_options">Options</string>
<string name="add_games">Add Games</string>
<string name="add_games_description">Select your games folder</string>
<string name="home_search_games">Search Games</string>
+ <string name="games_dir_selected">Games directory selected</string>
<string name="install_prod_keys">Install Prod.keys</string>
<string name="install_prod_keys_description">Required to decrypt retail games</string>
<string name="install_amiibo_keys">Install Amiibo Keys</string>