diff --git a/src/android/app/src/main/AndroidManifest.xml b/src/android/app/src/main/AndroidManifest.xml
index c642dbdcda..b899ca07fa 100644
--- a/src/android/app/src/main/AndroidManifest.xml
+++ b/src/android/app/src/main/AndroidManifest.xml
@@ -73,6 +73,11 @@ SPDX-License-Identifier: GPL-3.0-or-later
android:theme="@style/Theme.Yuzu.Main"
android:label="@string/preferences_settings"/>
+
+
) :
title = YuzuApplication.appContext.getString(applet.titleId),
path = appletPath
)
- val action = HomeNavigationDirections.actionGlobalEmulationActivity(appletGame)
- binding.root.findNavController().navigate(action)
+ binding.root.findNavController().navigate(
+ R.id.action_global_emulationActivity,
+ bundleOf(
+ "game" to appletGame,
+ "custom" to false
+ )
+ )
}
}
}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsActivity.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsActivity.kt
index 11be703536..d33bbc3d7d 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsActivity.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsActivity.kt
@@ -111,18 +111,10 @@ class SettingsActivity : AppCompatActivity() {
if (navHostFragment.childFragmentManager.backStackEntryCount > 0) {
navHostFragment.navController.popBackStack()
} else {
- finishWithFragmentLikeAnimation()
+ finish()
}
}
- private fun finishWithFragmentLikeAnimation() {
- finish()
- overridePendingTransition(
- androidx.navigation.ui.R.anim.nav_default_pop_enter_anim,
- androidx.navigation.ui.R.anim.nav_default_pop_exit_anim
- )
- }
-
override fun onStart() {
super.onStart()
if (!DirectoryInitialization.areDirectoriesReady) {
@@ -178,7 +170,7 @@ class SettingsActivity : AppCompatActivity() {
getString(R.string.settings_reset),
Toast.LENGTH_LONG
).show()
- finishWithFragmentLikeAnimation()
+ finish()
}
private fun setInsets() {
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsSubscreenActivity.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsSubscreenActivity.kt
new file mode 100644
index 0000000000..11ecd355fb
--- /dev/null
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsSubscreenActivity.kt
@@ -0,0 +1,152 @@
+// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+package org.yuzu.yuzu_emu.features.settings.ui
+
+import android.content.Context
+import android.os.Bundle
+import android.view.View
+import android.view.ViewGroup.MarginLayoutParams
+import androidx.activity.OnBackPressedCallback
+import androidx.appcompat.app.AppCompatActivity
+import androidx.core.os.bundleOf
+import androidx.core.view.ViewCompat
+import androidx.core.view.WindowCompat
+import androidx.core.view.WindowInsetsCompat
+import androidx.navigation.fragment.NavHostFragment
+import androidx.navigation.navArgs
+import com.google.android.material.color.MaterialColors
+import org.yuzu.yuzu_emu.R
+import org.yuzu.yuzu_emu.YuzuApplication
+import org.yuzu.yuzu_emu.databinding.ActivitySettingsBinding
+import org.yuzu.yuzu_emu.utils.DirectoryInitialization
+import org.yuzu.yuzu_emu.utils.InsetsHelper
+import org.yuzu.yuzu_emu.utils.ThemeHelper
+
+enum class SettingsSubscreen {
+ PROFILE_MANAGER,
+ DRIVER_MANAGER,
+ DRIVER_FETCHER,
+ FREEDRENO_SETTINGS,
+ APPLET_LAUNCHER,
+ INSTALLABLE,
+ GAME_FOLDERS,
+ ABOUT,
+ LICENSES,
+ GAME_INFO,
+ ADDONS,
+}
+
+class SettingsSubscreenActivity : AppCompatActivity() {
+ private lateinit var binding: ActivitySettingsBinding
+
+ private val args by navArgs()
+
+ override fun attachBaseContext(base: Context) {
+ super.attachBaseContext(YuzuApplication.applyLanguage(base))
+ }
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ ThemeHelper.setTheme(this)
+
+ super.onCreate(savedInstanceState)
+
+ binding = ActivitySettingsBinding.inflate(layoutInflater)
+ setContentView(binding.root)
+
+ val navHostFragment =
+ supportFragmentManager.findFragmentById(R.id.fragment_container) as NavHostFragment
+ if (savedInstanceState == null) {
+ val navController = navHostFragment.navController
+ val navGraph = navController.navInflater.inflate(
+ R.navigation.settings_subscreen_navigation
+ )
+ navGraph.setStartDestination(resolveStartDestination())
+ navController.setGraph(navGraph, createStartDestinationArgs())
+ }
+
+ WindowCompat.setDecorFitsSystemWindows(window, false)
+
+ if (InsetsHelper.getSystemGestureType(applicationContext) !=
+ InsetsHelper.GESTURE_NAVIGATION
+ ) {
+ binding.navigationBarShade.setBackgroundColor(
+ ThemeHelper.getColorWithOpacity(
+ MaterialColors.getColor(
+ binding.navigationBarShade,
+ com.google.android.material.R.attr.colorSurface
+ ),
+ ThemeHelper.SYSTEM_BAR_ALPHA
+ )
+ )
+ }
+
+ onBackPressedDispatcher.addCallback(
+ this,
+ object : OnBackPressedCallback(true) {
+ override fun handleOnBackPressed() = navigateBack()
+ }
+ )
+
+ setInsets()
+ }
+
+ override fun onStart() {
+ super.onStart()
+ if (!DirectoryInitialization.areDirectoriesReady) {
+ DirectoryInitialization.start()
+ }
+ }
+
+ fun navigateBack() {
+ val navHostFragment =
+ supportFragmentManager.findFragmentById(R.id.fragment_container) as NavHostFragment
+ if (!navHostFragment.navController.popBackStack()) {
+ finish()
+ }
+ }
+
+ private fun resolveStartDestination(): Int =
+ when (args.destination) {
+ SettingsSubscreen.PROFILE_MANAGER -> R.id.profileManagerFragment
+ SettingsSubscreen.DRIVER_MANAGER -> R.id.driverManagerFragment
+ SettingsSubscreen.DRIVER_FETCHER -> R.id.driverFetcherFragment
+ SettingsSubscreen.FREEDRENO_SETTINGS -> R.id.freedrenoSettingsFragment
+ SettingsSubscreen.APPLET_LAUNCHER -> R.id.appletLauncherFragment
+ SettingsSubscreen.INSTALLABLE -> R.id.installableFragment
+ SettingsSubscreen.GAME_FOLDERS -> R.id.gameFoldersFragment
+ SettingsSubscreen.ABOUT -> R.id.aboutFragment
+ SettingsSubscreen.LICENSES -> R.id.licensesFragment
+ SettingsSubscreen.GAME_INFO -> R.id.gameInfoFragment
+ SettingsSubscreen.ADDONS -> R.id.addonsFragment
+ }
+
+ private fun createStartDestinationArgs(): Bundle =
+ when (args.destination) {
+ SettingsSubscreen.DRIVER_MANAGER,
+ SettingsSubscreen.FREEDRENO_SETTINGS -> bundleOf("game" to args.game)
+
+ SettingsSubscreen.GAME_INFO,
+ SettingsSubscreen.ADDONS -> bundleOf(
+ "game" to requireNotNull(args.game) {
+ "Game is required for ${args.destination}"
+ }
+ )
+
+ else -> Bundle()
+ }
+
+ private fun setInsets() {
+ ViewCompat.setOnApplyWindowInsetsListener(
+ binding.navigationBarShade
+ ) { _: View, windowInsets: WindowInsetsCompat ->
+ val barInsets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars())
+
+ val mlpNavShade = binding.navigationBarShade.layoutParams as MarginLayoutParams
+ mlpNavShade.height = barInsets.bottom
+ binding.navigationBarShade.layoutParams = mlpNavShade
+
+ windowInsets
+ }
+ }
+}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/AboutFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/AboutFragment.kt
index 7fec413b66..aa2b3c7df2 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/AboutFragment.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/AboutFragment.kt
@@ -21,9 +21,10 @@ import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
import androidx.navigation.findNavController
import com.google.android.material.transition.MaterialSharedAxis
-import org.yuzu.yuzu_emu.BuildConfig
+import org.yuzu.yuzu_emu.HomeNavigationDirections
import org.yuzu.yuzu_emu.R
import org.yuzu.yuzu_emu.databinding.FragmentAboutBinding
+import org.yuzu.yuzu_emu.features.settings.ui.SettingsSubscreen
import org.yuzu.yuzu_emu.model.HomeViewModel
import org.yuzu.yuzu_emu.utils.ViewUtils.updateMargins
import org.yuzu.yuzu_emu.NativeLibrary
@@ -54,7 +55,7 @@ class AboutFragment : Fragment() {
super.onViewCreated(view, savedInstanceState)
homeViewModel.setStatusBarShadeVisibility(visible = false)
binding.toolbarAbout.setNavigationOnClickListener {
- binding.root.findNavController().popBackStack()
+ requireActivity().onBackPressedDispatcher.onBackPressed()
}
binding.imageLogo.setOnLongClickListener {
@@ -72,8 +73,11 @@ class AboutFragment : Fragment() {
)
}
binding.buttonLicenses.setOnClickListener {
- exitTransition = MaterialSharedAxis(MaterialSharedAxis.X, true)
- binding.root.findNavController().navigate(R.id.action_aboutFragment_to_licensesFragment)
+ val action = HomeNavigationDirections.actionGlobalSettingsSubscreenActivity(
+ SettingsSubscreen.LICENSES,
+ null
+ )
+ binding.root.findNavController().navigate(action)
}
val buildName = getString(R.string.app_name_suffixed)
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/AddonsFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/AddonsFragment.kt
index 96b7a8cce2..b20d75ef0a 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/AddonsFragment.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/AddonsFragment.kt
@@ -15,7 +15,6 @@ import androidx.core.view.updatePadding
import androidx.documentfile.provider.DocumentFile
import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
-import androidx.navigation.findNavController
import androidx.navigation.fragment.navArgs
import androidx.recyclerview.widget.LinearLayoutManager
import com.google.android.material.transition.MaterialSharedAxis
@@ -61,7 +60,7 @@ class AddonsFragment : Fragment() {
homeViewModel.setStatusBarShadeVisibility(false)
binding.toolbarAddons.setNavigationOnClickListener {
- binding.root.findNavController().popBackStack()
+ requireActivity().onBackPressedDispatcher.onBackPressed()
}
binding.toolbarAddons.title = getString(R.string.addons_game, args.game.title)
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/AppletLauncherFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/AppletLauncherFragment.kt
index 3ab171a8d4..49237cb756 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/AppletLauncherFragment.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/AppletLauncherFragment.kt
@@ -1,4 +1,4 @@
-// SPDX-FileCopyrightText: 2025 Eden Emulator Project
+// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
package org.yuzu.yuzu_emu.fragments
@@ -12,7 +12,6 @@ import androidx.core.view.WindowInsetsCompat
import androidx.core.view.updatePadding
import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
-import androidx.navigation.findNavController
import androidx.recyclerview.widget.GridLayoutManager
import com.google.android.material.transition.MaterialSharedAxis
import org.yuzu.yuzu_emu.R
@@ -50,7 +49,7 @@ class AppletLauncherFragment : Fragment() {
homeViewModel.setStatusBarShadeVisibility(visible = false)
binding.toolbarApplets.setNavigationOnClickListener {
- binding.root.findNavController().popBackStack()
+ requireActivity().onBackPressedDispatcher.onBackPressed()
}
val applets = listOf(
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/DriverFetcherFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/DriverFetcherFragment.kt
index e2b652dc60..eacc64a63e 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/DriverFetcherFragment.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/DriverFetcherFragment.kt
@@ -13,7 +13,6 @@ import androidx.core.view.WindowInsetsCompat
import androidx.core.view.isVisible
import androidx.core.view.updatePadding
import androidx.fragment.app.activityViewModels
-import androidx.navigation.findNavController
import androidx.recyclerview.widget.LinearLayoutManager
import com.fasterxml.jackson.databind.JsonNode
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
@@ -142,7 +141,7 @@ class DriverFetcherFragment : Fragment() {
super.onViewCreated(view, savedInstanceState)
homeViewModel.setStatusBarShadeVisibility(visible = false)
binding.toolbarDrivers.setNavigationOnClickListener {
- binding.root.findNavController().popBackStack()
+ requireActivity().onBackPressedDispatcher.onBackPressed()
}
binding.listDrivers.layoutManager = LinearLayoutManager(context)
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/DriverManagerFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/DriverManagerFragment.kt
index 23334f05eb..89a6362dc6 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/DriverManagerFragment.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/DriverManagerFragment.kt
@@ -1,4 +1,4 @@
-// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
+// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
package org.yuzu.yuzu_emu.fragments
@@ -19,6 +19,7 @@ import androidx.navigation.fragment.navArgs
import androidx.preference.PreferenceManager
import androidx.recyclerview.widget.GridLayoutManager
import com.google.android.material.transition.MaterialSharedAxis
+import org.yuzu.yuzu_emu.HomeNavigationDirections
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
@@ -27,6 +28,7 @@ import org.yuzu.yuzu_emu.adapters.DriverAdapter
import org.yuzu.yuzu_emu.databinding.FragmentDriverManagerBinding
import org.yuzu.yuzu_emu.features.settings.model.Settings
import org.yuzu.yuzu_emu.features.settings.model.StringSetting
+import org.yuzu.yuzu_emu.features.settings.ui.SettingsSubscreen
import org.yuzu.yuzu_emu.model.Driver.Companion.toDriver
import org.yuzu.yuzu_emu.model.DriverViewModel
import org.yuzu.yuzu_emu.model.HomeViewModel
@@ -105,7 +107,7 @@ class DriverManagerFragment : Fragment() {
}
binding.toolbarDrivers.setNavigationOnClickListener {
- binding.root.findNavController().popBackStack()
+ requireActivity().onBackPressedDispatcher.onBackPressed()
}
binding.buttonInstall.setOnClickListener {
@@ -113,9 +115,11 @@ class DriverManagerFragment : Fragment() {
}
binding.buttonFetch.setOnClickListener {
- binding.root.findNavController().navigate(
- R.id.action_driverManagerFragment_to_driverFetcherFragment
+ val action = HomeNavigationDirections.actionGlobalSettingsSubscreenActivity(
+ SettingsSubscreen.DRIVER_FETCHER,
+ null
)
+ binding.root.findNavController().navigate(action)
}
binding.listDrivers.apply {
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/GameFoldersFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/GameFoldersFragment.kt
index 9c43d2c6e1..6e05df799b 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/GameFoldersFragment.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/GameFoldersFragment.kt
@@ -4,20 +4,21 @@
package org.yuzu.yuzu_emu.fragments
import android.content.Intent
+import android.net.Uri
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
+import android.widget.Toast
+import androidx.activity.result.contract.ActivityResultContracts
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import androidx.core.view.updatePadding
import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
-import androidx.navigation.findNavController
import androidx.recyclerview.widget.GridLayoutManager
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.transition.MaterialSharedAxis
-import kotlinx.coroutines.launch
import org.yuzu.yuzu_emu.R
import org.yuzu.yuzu_emu.adapters.FolderAdapter
import org.yuzu.yuzu_emu.databinding.FragmentFoldersBinding
@@ -25,7 +26,6 @@ import org.yuzu.yuzu_emu.model.DirectoryType
import org.yuzu.yuzu_emu.model.GameDir
import org.yuzu.yuzu_emu.model.GamesViewModel
import org.yuzu.yuzu_emu.model.HomeViewModel
-import org.yuzu.yuzu_emu.ui.main.MainActivity
import org.yuzu.yuzu_emu.utils.ViewUtils.updateMargins
import org.yuzu.yuzu_emu.utils.collect
@@ -36,6 +36,20 @@ class GameFoldersFragment : Fragment() {
private val homeViewModel: HomeViewModel by activityViewModels()
private val gamesViewModel: GamesViewModel by activityViewModels()
+ private val getGamesDirectory =
+ registerForActivityResult(ActivityResultContracts.OpenDocumentTree()) { result ->
+ if (result != null) {
+ processGamesDir(result)
+ }
+ }
+
+ private val getExternalContentDirectory =
+ registerForActivityResult(ActivityResultContracts.OpenDocumentTree()) { result ->
+ if (result != null) {
+ processExternalContentDir(result)
+ }
+ }
+
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enterTransition = MaterialSharedAxis(MaterialSharedAxis.X, true)
@@ -59,7 +73,7 @@ class GameFoldersFragment : Fragment() {
homeViewModel.setStatusBarShadeVisibility(visible = false)
binding.toolbarFolders.setNavigationOnClickListener {
- binding.root.findNavController().popBackStack()
+ requireActivity().onBackPressedDispatcher.onBackPressed()
}
binding.listFolders.apply {
@@ -74,7 +88,6 @@ class GameFoldersFragment : Fragment() {
(binding.listFolders.adapter as FolderAdapter).submitList(it)
}
- val mainActivity = requireActivity() as MainActivity
binding.buttonAdd.setOnClickListener {
// Show a model to choose between Game and External Content
val options = arrayOf(
@@ -87,10 +100,10 @@ class GameFoldersFragment : Fragment() {
.setItems(options) { _, which ->
when (which) {
0 -> { // Game Folder
- mainActivity.getGamesDirectory.launch(Intent(Intent.ACTION_OPEN_DOCUMENT_TREE).data)
+ getGamesDirectory.launch(Intent(Intent.ACTION_OPEN_DOCUMENT_TREE).data)
}
1 -> { // External Content Folder
- mainActivity.getExternalContentDirectory.launch(null)
+ getExternalContentDirectory.launch(null)
}
}
}
@@ -105,6 +118,50 @@ class GameFoldersFragment : Fragment() {
gamesViewModel.onCloseGameFoldersFragment()
}
+ private fun processGamesDir(result: Uri) {
+ requireContext().contentResolver.takePersistableUriPermission(
+ result,
+ Intent.FLAG_GRANT_READ_URI_PERMISSION
+ )
+
+ val uriString = result.toString()
+ val folder = gamesViewModel.folders.value.firstOrNull { it.uriString == uriString }
+ if (folder != null) {
+ Toast.makeText(
+ requireContext().applicationContext,
+ R.string.folder_already_added,
+ Toast.LENGTH_SHORT
+ ).show()
+ return
+ }
+
+ AddGameFolderDialogFragment.newInstance(uriString, calledFromGameFragment = false)
+ .show(parentFragmentManager, AddGameFolderDialogFragment.TAG)
+ }
+
+ private fun processExternalContentDir(result: Uri) {
+ requireContext().contentResolver.takePersistableUriPermission(
+ result,
+ Intent.FLAG_GRANT_READ_URI_PERMISSION
+ )
+
+ val uriString = result.toString()
+ val folder = gamesViewModel.folders.value.firstOrNull {
+ it.uriString == uriString && it.type == DirectoryType.EXTERNAL_CONTENT
+ }
+ if (folder != null) {
+ Toast.makeText(
+ requireContext().applicationContext,
+ R.string.folder_already_added,
+ Toast.LENGTH_SHORT
+ ).show()
+ return
+ }
+
+ val externalContentDir = GameDir(uriString, deepScan = false, DirectoryType.EXTERNAL_CONTENT)
+ gamesViewModel.addFolder(externalContentDir, savedFromGameFragment = false)
+ }
+
private fun setInsets() =
ViewCompat.setOnApplyWindowInsetsListener(
binding.root
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/GameInfoFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/GameInfoFragment.kt
index 7863e40ff5..5d6238e5a1 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/GameInfoFragment.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/GameInfoFragment.kt
@@ -1,4 +1,4 @@
-// SPDX-FileCopyrightText: 2025 Eden Emulator Project
+// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
package org.yuzu.yuzu_emu.fragments
@@ -18,7 +18,6 @@ import androidx.core.view.WindowInsetsCompat
import androidx.core.view.updatePadding
import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
-import androidx.navigation.findNavController
import androidx.navigation.fragment.navArgs
import com.google.android.material.transition.MaterialSharedAxis
import org.yuzu.yuzu_emu.NativeLibrary
@@ -64,7 +63,7 @@ class GameInfoFragment : Fragment() {
binding.apply {
toolbarInfo.title = args.game.title
toolbarInfo.setNavigationOnClickListener {
- view.findNavController().popBackStack()
+ requireActivity().onBackPressedDispatcher.onBackPressed()
}
val pathString = Uri.parse(args.game.path).path ?: ""
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/GamePropertiesFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/GamePropertiesFragment.kt
index 9e55297846..46b75197d5 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/GamePropertiesFragment.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/GamePropertiesFragment.kt
@@ -35,6 +35,7 @@ import org.yuzu.yuzu_emu.adapters.GamePropertiesAdapter
import org.yuzu.yuzu_emu.databinding.FragmentGamePropertiesBinding
import org.yuzu.yuzu_emu.features.DocumentProvider
import org.yuzu.yuzu_emu.features.settings.model.Settings
+import org.yuzu.yuzu_emu.features.settings.ui.SettingsSubscreen
import org.yuzu.yuzu_emu.model.DriverViewModel
import org.yuzu.yuzu_emu.model.GameProperty
import org.yuzu.yuzu_emu.model.GamesViewModel
@@ -250,8 +251,10 @@ class GamePropertiesFragment : Fragment() {
R.string.info_description,
R.drawable.ic_info_outline,
action = {
- val action = GamePropertiesFragmentDirections
- .actionPerGamePropertiesFragmentToGameInfoFragment(args.game)
+ val action = HomeNavigationDirections.actionGlobalSettingsSubscreenActivity(
+ SettingsSubscreen.GAME_INFO,
+ args.game
+ )
binding.root.findNavController().navigate(action)
}
)
@@ -317,8 +320,11 @@ class GamePropertiesFragment : Fragment() {
R.string.add_ons_description,
R.drawable.ic_edit,
action = {
- val action = GamePropertiesFragmentDirections
- .actionPerGamePropertiesFragmentToAddonsFragment(args.game)
+ val action =
+ HomeNavigationDirections.actionGlobalSettingsSubscreenActivity(
+ SettingsSubscreen.ADDONS,
+ args.game
+ )
binding.root.findNavController().navigate(action)
}
)
@@ -333,8 +339,11 @@ class GamePropertiesFragment : Fragment() {
R.drawable.ic_build,
detailsFlow = driverViewModel.selectedDriverTitle,
action = {
- val action = GamePropertiesFragmentDirections
- .actionPerGamePropertiesFragmentToDriverManagerFragment(args.game)
+ val action =
+ HomeNavigationDirections.actionGlobalSettingsSubscreenActivity(
+ SettingsSubscreen.DRIVER_MANAGER,
+ args.game
+ )
binding.root.findNavController().navigate(action)
}
)
@@ -347,8 +356,11 @@ class GamePropertiesFragment : Fragment() {
R.string.freedreno_per_game_description,
R.drawable.ic_graphics,
action = {
- val action = GamePropertiesFragmentDirections
- .actionPerGamePropertiesFragmentToFreedrenoSettingsFragment(args.game)
+ val action =
+ HomeNavigationDirections.actionGlobalSettingsSubscreenActivity(
+ SettingsSubscreen.FREEDRENO_SETTINGS,
+ args.game
+ )
binding.root.findNavController().navigate(action)
}
)
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/HomeSettingsFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/HomeSettingsFragment.kt
index 6f4bf858ea..37eda22c69 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/HomeSettingsFragment.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/HomeSettingsFragment.kt
@@ -36,6 +36,7 @@ import org.yuzu.yuzu_emu.databinding.FragmentHomeSettingsBinding
import org.yuzu.yuzu_emu.features.DocumentProvider
import org.yuzu.yuzu_emu.features.fetcher.SpacingItemDecoration
import org.yuzu.yuzu_emu.features.settings.model.Settings
+import org.yuzu.yuzu_emu.features.settings.ui.SettingsSubscreen
import org.yuzu.yuzu_emu.model.DriverViewModel
import org.yuzu.yuzu_emu.model.HomeSetting
import org.yuzu.yuzu_emu.model.HomeViewModel
@@ -126,8 +127,11 @@ class HomeSettingsFragment : Fragment() {
R.string.profile_manager_description,
R.drawable.ic_account_circle,
{
- binding.root.findNavController()
- .navigate(R.id.action_homeSettingsFragment_to_profileManagerFragment)
+ val action = HomeNavigationDirections.actionGlobalSettingsSubscreenActivity(
+ SettingsSubscreen.PROFILE_MANAGER,
+ null
+ )
+ binding.root.findNavController().navigate(action)
}
)
)
@@ -137,8 +141,10 @@ class HomeSettingsFragment : Fragment() {
R.string.install_gpu_driver_description,
R.drawable.ic_build,
{
- val action = HomeSettingsFragmentDirections
- .actionHomeSettingsFragmentToDriverManagerFragment(null)
+ val action = HomeNavigationDirections.actionGlobalSettingsSubscreenActivity(
+ SettingsSubscreen.DRIVER_MANAGER,
+ null
+ )
binding.root.findNavController().navigate(action)
},
{ true },
@@ -154,7 +160,12 @@ class HomeSettingsFragment : Fragment() {
R.string.gpu_driver_settings,
R.drawable.ic_graphics,
{
- binding.root.findNavController().navigate(R.id.freedrenoSettingsFragment)
+ val action =
+ HomeNavigationDirections.actionGlobalSettingsSubscreenActivity(
+ SettingsSubscreen.FREEDRENO_SETTINGS,
+ null
+ )
+ binding.root.findNavController().navigate(action)
}
)
)
@@ -175,8 +186,11 @@ class HomeSettingsFragment : Fragment() {
R.string.applets_description,
R.drawable.ic_applet,
{
- binding.root.findNavController()
- .navigate(R.id.action_homeSettingsFragment_to_appletLauncherFragment)
+ val action = HomeNavigationDirections.actionGlobalSettingsSubscreenActivity(
+ SettingsSubscreen.APPLET_LAUNCHER,
+ null
+ )
+ binding.root.findNavController().navigate(action)
},
{ NativeLibrary.isFirmwareAvailable() },
R.string.applets_error_firmware,
@@ -189,8 +203,11 @@ class HomeSettingsFragment : Fragment() {
R.string.manage_yuzu_data_description,
R.drawable.ic_install,
{
- binding.root.findNavController()
- .navigate(R.id.action_homeSettingsFragment_to_installableFragment)
+ val action = HomeNavigationDirections.actionGlobalSettingsSubscreenActivity(
+ SettingsSubscreen.INSTALLABLE,
+ null
+ )
+ binding.root.findNavController().navigate(action)
}
)
)
@@ -200,8 +217,11 @@ class HomeSettingsFragment : Fragment() {
R.string.select_games_folder_description,
R.drawable.ic_add,
{
- binding.root.findNavController()
- .navigate(R.id.action_homeSettingsFragment_to_gameFoldersFragment)
+ val action = HomeNavigationDirections.actionGlobalSettingsSubscreenActivity(
+ SettingsSubscreen.GAME_FOLDERS,
+ null
+ )
+ binding.root.findNavController().navigate(action)
}
)
)
@@ -284,9 +304,11 @@ class HomeSettingsFragment : Fragment() {
R.string.about_description,
R.drawable.ic_info_outline,
{
- exitTransition = MaterialSharedAxis(MaterialSharedAxis.X, true)
- parentFragmentManager.primaryNavigationFragment?.findNavController()
- ?.navigate(R.id.action_homeSettingsFragment_to_aboutFragment)
+ val action = HomeNavigationDirections.actionGlobalSettingsSubscreenActivity(
+ SettingsSubscreen.ABOUT,
+ null
+ )
+ binding.root.findNavController().navigate(action)
}
)
)
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/InstallableFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/InstallableFragment.kt
index 1b94d5f1a6..10862c37b4 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/InstallableFragment.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/InstallableFragment.kt
@@ -1,4 +1,4 @@
-// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
+// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
package org.yuzu.yuzu_emu.fragments
@@ -14,23 +14,23 @@ import androidx.core.view.WindowInsetsCompat
import androidx.core.view.updatePadding
import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
-import androidx.navigation.findNavController
import androidx.recyclerview.widget.GridLayoutManager
import com.google.android.material.transition.MaterialSharedAxis
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.YuzuApplication
import org.yuzu.yuzu_emu.adapters.InstallableAdapter
import org.yuzu.yuzu_emu.databinding.FragmentInstallablesBinding
+import org.yuzu.yuzu_emu.model.AddonViewModel
+import org.yuzu.yuzu_emu.model.DriverViewModel
+import org.yuzu.yuzu_emu.model.GamesViewModel
import org.yuzu.yuzu_emu.model.HomeViewModel
import org.yuzu.yuzu_emu.model.Installable
import org.yuzu.yuzu_emu.model.TaskState
-import org.yuzu.yuzu_emu.ui.main.MainActivity
-import org.yuzu.yuzu_emu.utils.DirectoryInitialization
import org.yuzu.yuzu_emu.utils.FileUtil
+import org.yuzu.yuzu_emu.utils.InstallableActions
import org.yuzu.yuzu_emu.utils.NativeConfig
import org.yuzu.yuzu_emu.utils.ViewUtils.updateMargins
import org.yuzu.yuzu_emu.utils.collect
@@ -45,6 +45,9 @@ class InstallableFragment : Fragment() {
private val binding get() = _binding!!
private val homeViewModel: HomeViewModel by activityViewModels()
+ private val gamesViewModel: GamesViewModel by activityViewModels()
+ private val addonViewModel: AddonViewModel by activityViewModels()
+ private val driverViewModel: DriverViewModel by activityViewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@@ -65,12 +68,10 @@ class InstallableFragment : Fragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
- val mainActivity = requireActivity() as MainActivity
-
homeViewModel.setStatusBarShadeVisibility(visible = false)
binding.toolbarInstallables.setNavigationOnClickListener {
- binding.root.findNavController().popBackStack()
+ requireActivity().onBackPressedDispatcher.onBackPressed()
}
homeViewModel.openImportSaves.collect(viewLifecycleOwner) {
@@ -84,8 +85,8 @@ class InstallableFragment : Fragment() {
Installable(
R.string.user_data,
R.string.user_data_description,
- install = { mainActivity.importUserData.launch(arrayOf("application/zip")) },
- export = { mainActivity.exportUserData.launch("export.zip") }
+ install = { importUserDataLauncher.launch(arrayOf("application/zip")) },
+ export = { exportUserDataLauncher.launch("export.zip") }
),
Installable(
R.string.manage_save_data,
@@ -127,27 +128,33 @@ class InstallableFragment : Fragment() {
Installable(
R.string.install_game_content,
R.string.install_game_content_description,
- install = { mainActivity.installGameUpdate.launch(arrayOf("*/*")) }
+ install = { installGameUpdateLauncher.launch(arrayOf("*/*")) }
),
Installable(
R.string.install_firmware,
R.string.install_firmware_description,
- install = { mainActivity.getFirmware.launch(arrayOf("application/zip")) }
+ install = { getFirmwareLauncher.launch(arrayOf("application/zip")) }
),
Installable(
R.string.uninstall_firmware,
R.string.uninstall_firmware_description,
- install = { mainActivity.uninstallFirmware() }
+ install = {
+ InstallableActions.uninstallFirmware(
+ activity = requireActivity(),
+ fragmentManager = parentFragmentManager,
+ homeViewModel = homeViewModel
+ )
+ }
),
Installable(
R.string.install_prod_keys,
R.string.install_prod_keys_description,
- install = { mainActivity.getProdKey.launch(arrayOf("*/*")) }
+ install = { getProdKeyLauncher.launch(arrayOf("*/*")) }
),
Installable(
R.string.install_amiibo_keys,
R.string.install_amiibo_keys_description,
- install = { mainActivity.getAmiiboKey.launch(arrayOf("*/*")) }
+ install = { getAmiiboKeyLauncher.launch(arrayOf("*/*")) }
)
)
@@ -180,6 +187,132 @@ class InstallableFragment : Fragment() {
windowInsets
}
+ private val getProdKeyLauncher =
+ registerForActivityResult(ActivityResultContracts.OpenDocument()) { result ->
+ if (result != null) {
+ InstallableActions.processKey(
+ activity = requireActivity(),
+ fragmentManager = parentFragmentManager,
+ gamesViewModel = gamesViewModel,
+ result = result,
+ extension = "keys"
+ )
+ }
+ }
+
+ private val getAmiiboKeyLauncher =
+ registerForActivityResult(ActivityResultContracts.OpenDocument()) { result ->
+ if (result != null) {
+ InstallableActions.processKey(
+ activity = requireActivity(),
+ fragmentManager = parentFragmentManager,
+ gamesViewModel = gamesViewModel,
+ result = result,
+ extension = "bin"
+ )
+ }
+ }
+
+ private val getFirmwareLauncher =
+ registerForActivityResult(ActivityResultContracts.OpenDocument()) { result ->
+ if (result != null) {
+ InstallableActions.processFirmware(
+ activity = requireActivity(),
+ fragmentManager = parentFragmentManager,
+ homeViewModel = homeViewModel,
+ result = result
+ )
+ }
+ }
+
+ private val installGameUpdateLauncher =
+ registerForActivityResult(ActivityResultContracts.OpenMultipleDocuments()) { documents ->
+ if (documents.isEmpty()) {
+ return@registerForActivityResult
+ }
+
+ if (addonViewModel.game == null) {
+ InstallableActions.installContent(
+ activity = requireActivity(),
+ fragmentManager = parentFragmentManager,
+ addonViewModel = addonViewModel,
+ documents = documents
+ )
+ return@registerForActivityResult
+ }
+
+ ProgressDialogFragment.newInstance(
+ requireActivity(),
+ R.string.verifying_content,
+ false
+ ) { _, _ ->
+ var updatesMatchProgram = true
+ for (document in documents) {
+ val valid = NativeLibrary.doesUpdateMatchProgram(
+ addonViewModel.game!!.programId,
+ document.toString()
+ )
+ if (!valid) {
+ updatesMatchProgram = false
+ break
+ }
+ }
+
+ if (updatesMatchProgram) {
+ requireActivity().runOnUiThread {
+ InstallableActions.installContent(
+ activity = requireActivity(),
+ fragmentManager = parentFragmentManager,
+ addonViewModel = addonViewModel,
+ documents = documents
+ )
+ }
+ } else {
+ requireActivity().runOnUiThread {
+ MessageDialogFragment.newInstance(
+ requireActivity(),
+ titleId = R.string.content_install_notice,
+ descriptionId = R.string.content_install_notice_description,
+ positiveAction = {
+ InstallableActions.installContent(
+ activity = requireActivity(),
+ fragmentManager = parentFragmentManager,
+ addonViewModel = addonViewModel,
+ documents = documents
+ )
+ },
+ negativeAction = {}
+ ).show(parentFragmentManager, MessageDialogFragment.TAG)
+ }
+ }
+ return@newInstance Any()
+ }.show(parentFragmentManager, ProgressDialogFragment.TAG)
+ }
+
+ private val importUserDataLauncher =
+ registerForActivityResult(ActivityResultContracts.OpenDocument()) { result ->
+ if (result != null) {
+ InstallableActions.importUserData(
+ activity = requireActivity(),
+ fragmentManager = parentFragmentManager,
+ gamesViewModel = gamesViewModel,
+ driverViewModel = driverViewModel,
+ result = result
+ )
+ }
+ }
+
+ private val exportUserDataLauncher =
+ registerForActivityResult(ActivityResultContracts.CreateDocument("application/zip")) { result ->
+ if (result != null) {
+ InstallableActions.exportUserData(
+ activity = requireActivity(),
+ fragmentManager = parentFragmentManager,
+ result = result
+ )
+ }
+ }
+
private val importSaves =
registerForActivityResult(ActivityResultContracts.OpenDocument()) { result ->
if (result == null) {
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/LicensesFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/LicensesFragment.kt
index aa18aa2482..32b72fe38f 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/LicensesFragment.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/LicensesFragment.kt
@@ -1,4 +1,4 @@
-// SPDX-FileCopyrightText: 2025 Eden Emulator Project
+// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
package org.yuzu.yuzu_emu.fragments
@@ -13,7 +13,6 @@ import androidx.core.view.WindowInsetsCompat
import androidx.core.view.updatePadding
import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
-import androidx.navigation.findNavController
import androidx.recyclerview.widget.LinearLayoutManager
import com.google.android.material.transition.MaterialSharedAxis
import org.yuzu.yuzu_emu.R
@@ -48,7 +47,7 @@ class LicensesFragment : Fragment() {
homeViewModel.setStatusBarShadeVisibility(visible = false)
binding.toolbarLicenses.setNavigationOnClickListener {
- binding.root.findNavController().popBackStack()
+ requireActivity().onBackPressedDispatcher.onBackPressed()
}
val licenses = listOf(
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/ProfileManagerFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/ProfileManagerFragment.kt
index 6ee34105e7..2786906f6b 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/ProfileManagerFragment.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/ProfileManagerFragment.kt
@@ -51,7 +51,7 @@ class ProfileManagerFragment : Fragment() {
homeViewModel.setStatusBarShadeVisibility(visible = false)
binding.toolbarProfiles.setNavigationOnClickListener {
- findNavController().popBackStack()
+ requireActivity().onBackPressedDispatcher.onBackPressed()
}
setupRecyclerView()
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 74a171cf1f..f0806df786 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
@@ -26,7 +26,6 @@ import androidx.preference.PreferenceManager
import com.google.android.material.color.MaterialColors
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import java.io.File
-import java.io.FilenameFilter
import org.yuzu.yuzu_emu.NativeLibrary
import org.yuzu.yuzu_emu.R
import org.yuzu.yuzu_emu.databinding.ActivityMainBinding
@@ -39,16 +38,10 @@ import org.yuzu.yuzu_emu.model.AddonViewModel
import org.yuzu.yuzu_emu.model.DriverViewModel
import org.yuzu.yuzu_emu.model.GamesViewModel
import org.yuzu.yuzu_emu.model.HomeViewModel
-import org.yuzu.yuzu_emu.model.InstallResult
import android.os.Build
-import org.yuzu.yuzu_emu.model.TaskState
import org.yuzu.yuzu_emu.model.TaskViewModel
import org.yuzu.yuzu_emu.utils.*
import org.yuzu.yuzu_emu.utils.ViewUtils.setVisible
-import java.io.BufferedInputStream
-import java.io.BufferedOutputStream
-import java.util.zip.ZipEntry
-import java.util.zip.ZipInputStream
import androidx.core.content.edit
import org.yuzu.yuzu_emu.activities.EmulationActivity
import kotlin.text.compareTo
@@ -453,35 +446,13 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
}
fun processKey(result: Uri, extension: String = "keys") {
- contentResolver.takePersistableUriPermission(
- result,
- Intent.FLAG_GRANT_READ_URI_PERMISSION
+ InstallableActions.processKey(
+ activity = this,
+ fragmentManager = supportFragmentManager,
+ gamesViewModel = gamesViewModel,
+ result = result,
+ extension = extension
)
-
- val resultCode: Int = NativeLibrary.installKeys(result.toString(), extension)
-
- if (resultCode == 0) {
- // TODO(crueter): It may be worth it to switch some of these Toasts to snackbars,
- // since most of it is foreground-only anyways.
- Toast.makeText(
- applicationContext,
- R.string.keys_install_success,
- Toast.LENGTH_SHORT
- ).show()
-
- gamesViewModel.reloadGames(true)
-
- return
- }
-
- val resultString: String =
- resources.getStringArray(R.array.installKeysResults)[resultCode]
-
- MessageDialogFragment.newInstance(
- titleId = R.string.keys_failed,
- descriptionString = resultString,
- helpLinkId = R.string.keys_missing_help
- ).show(supportFragmentManager, MessageDialogFragment.TAG)
}
val getFirmware = registerForActivityResult(ActivityResultContracts.OpenDocument()) { result ->
@@ -491,75 +462,21 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
}
fun processFirmware(result: Uri, onComplete: (() -> Unit)? = null) {
- val filterNCA = FilenameFilter { _, dirName -> dirName.endsWith(".nca") }
-
- val firmwarePath =
- File(NativeConfig.getNandDir() + "/system/Contents/registered/")
- val cacheFirmwareDir = File("${cacheDir.path}/registered/")
-
- ProgressDialogFragment.newInstance(
- this,
- R.string.firmware_installing
- ) { progressCallback, _ ->
- var messageToShow: Any
- try {
- FileUtil.unzipToInternalStorage(
- result.toString(),
- cacheFirmwareDir,
- progressCallback
- )
- val unfilteredNumOfFiles = cacheFirmwareDir.list()?.size ?: -1
- val filteredNumOfFiles = cacheFirmwareDir.list(filterNCA)?.size ?: -2
- messageToShow = if (unfilteredNumOfFiles != filteredNumOfFiles) {
- MessageDialogFragment.newInstance(
- this,
- titleId = R.string.firmware_installed_failure,
- descriptionId = R.string.firmware_installed_failure_description
- )
- } else {
- firmwarePath.deleteRecursively()
- cacheFirmwareDir.copyRecursively(firmwarePath, true)
- NativeLibrary.initializeSystem(true)
- homeViewModel.setCheckKeys(true)
- getString(R.string.save_file_imported_success)
- }
- } catch (e: Exception) {
- Log.error("[MainActivity] Firmware install failed - ${e.message}")
- messageToShow = getString(R.string.fatal_error)
- } finally {
- cacheFirmwareDir.deleteRecursively()
- }
- messageToShow
- }.apply {
- onDialogComplete = onComplete
- }.show(supportFragmentManager, ProgressDialogFragment.TAG)
+ InstallableActions.processFirmware(
+ activity = this,
+ fragmentManager = supportFragmentManager,
+ homeViewModel = homeViewModel,
+ result = result,
+ onComplete = onComplete
+ )
}
fun uninstallFirmware() {
- val firmwarePath =
- File(NativeConfig.getNandDir() + "/system/Contents/registered/")
- ProgressDialogFragment.newInstance(
- this,
- R.string.firmware_uninstalling
- ) { progressCallback, _ ->
- var messageToShow: Any
- try {
- // Ensure the firmware directory exists before attempting to delete
- if (firmwarePath.exists()) {
- firmwarePath.deleteRecursively()
- // Optionally reinitialize the system or perform other necessary steps
- NativeLibrary.initializeSystem(true)
- homeViewModel.setCheckKeys(true)
- messageToShow = getString(R.string.firmware_uninstalled_success)
- } else {
- messageToShow = getString(R.string.firmware_uninstalled_failure)
- }
- } catch (e: Exception) {
- Log.error("[MainActivity] Firmware uninstall failed - ${e.message}")
- messageToShow = getString(R.string.fatal_error)
- }
- messageToShow
- }.show(supportFragmentManager, ProgressDialogFragment.TAG)
+ InstallableActions.uninstallFirmware(
+ activity = this,
+ fragmentManager = supportFragmentManager,
+ homeViewModel = homeViewModel
+ )
}
val installGameUpdate = registerForActivityResult(
@@ -606,101 +523,12 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
}
private fun installContent(documents: List) {
- ProgressDialogFragment.newInstance(
- this@MainActivity,
- R.string.installing_game_content
- ) { progressCallback, messageCallback ->
- var installSuccess = 0
- var installOverwrite = 0
- var errorBaseGame = 0
- var error = 0
- documents.forEach {
- messageCallback.invoke(FileUtil.getFilename(it))
- when (
- InstallResult.from(
- NativeLibrary.installFileToNand(
- it.toString(),
- progressCallback
- )
- )
- ) {
- InstallResult.Success -> {
- installSuccess += 1
- }
-
- InstallResult.Overwrite -> {
- installOverwrite += 1
- }
-
- InstallResult.BaseInstallAttempted -> {
- errorBaseGame += 1
- }
-
- InstallResult.Failure -> {
- error += 1
- }
- }
- }
-
- addonViewModel.refreshAddons(force = true)
-
- val separator = System.lineSeparator() ?: "\n"
- val installResult = StringBuilder()
- if (installSuccess > 0) {
- installResult.append(
- getString(
- R.string.install_game_content_success_install,
- installSuccess
- )
- )
- installResult.append(separator)
- }
- if (installOverwrite > 0) {
- installResult.append(
- getString(
- R.string.install_game_content_success_overwrite,
- installOverwrite
- )
- )
- installResult.append(separator)
- }
- val errorTotal: Int = errorBaseGame + error
- if (errorTotal > 0) {
- installResult.append(separator)
- installResult.append(
- getString(
- R.string.install_game_content_failed_count,
- errorTotal
- )
- )
- installResult.append(separator)
- if (errorBaseGame > 0) {
- installResult.append(separator)
- installResult.append(
- getString(R.string.install_game_content_failure_base)
- )
- installResult.append(separator)
- }
- if (error > 0) {
- installResult.append(
- getString(R.string.install_game_content_failure_description)
- )
- installResult.append(separator)
- }
- return@newInstance MessageDialogFragment.newInstance(
- this,
- titleId = R.string.install_game_content_failure,
- descriptionString = installResult.toString().trim(),
- helpLinkId = R.string.install_game_content_help_link
- )
- } else {
- return@newInstance MessageDialogFragment.newInstance(
- this,
- titleId = R.string.install_game_content_success,
- descriptionString = installResult.toString().trim()
- )
- }
- }.show(supportFragmentManager, ProgressDialogFragment.TAG)
+ InstallableActions.installContent(
+ activity = this,
+ fragmentManager = supportFragmentManager,
+ addonViewModel = addonViewModel,
+ documents = documents
+ )
}
val exportUserData = registerForActivityResult(
@@ -709,25 +537,11 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
if (result == null) {
return@registerForActivityResult
}
-
- ProgressDialogFragment.newInstance(
- this,
- R.string.exporting_user_data,
- true
- ) { progressCallback, _ ->
- val zipResult = FileUtil.zipFromInternalStorage(
- File(DirectoryInitialization.userDirectory!!),
- DirectoryInitialization.userDirectory!!,
- BufferedOutputStream(contentResolver.openOutputStream(result)),
- progressCallback,
- compression = false
- )
- return@newInstance when (zipResult) {
- TaskState.Completed -> getString(R.string.user_data_export_success)
- TaskState.Failed -> R.string.export_failed
- TaskState.Cancelled -> R.string.user_data_export_cancelled
- }
- }.show(supportFragmentManager, ProgressDialogFragment.TAG)
+ InstallableActions.exportUserData(
+ activity = this,
+ fragmentManager = supportFragmentManager,
+ result = result
+ )
}
val importUserData =
@@ -735,58 +549,12 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
if (result == null) {
return@registerForActivityResult
}
-
- ProgressDialogFragment.newInstance(
- this,
- R.string.importing_user_data
- ) { progressCallback, _ ->
- val checkStream =
- ZipInputStream(BufferedInputStream(contentResolver.openInputStream(result)))
- var isYuzuBackup = false
- checkStream.use { stream ->
- var ze: ZipEntry? = null
- while (stream.nextEntry?.also { ze = it } != null) {
- val itemName = ze!!.name.trim()
- if (itemName == "/config/config.ini" || itemName == "config/config.ini") {
- isYuzuBackup = true
- return@use
- }
- }
- }
- if (!isYuzuBackup) {
- return@newInstance MessageDialogFragment.newInstance(
- this,
- titleId = R.string.invalid_yuzu_backup,
- descriptionId = R.string.user_data_import_failed_description
- )
- }
-
- // Clear existing user data
- NativeConfig.unloadGlobalConfig()
- File(DirectoryInitialization.userDirectory!!).deleteRecursively()
-
- // Copy archive to internal storage
- try {
- FileUtil.unzipToInternalStorage(
- result.toString(),
- File(DirectoryInitialization.userDirectory!!),
- progressCallback
- )
- } catch (e: Exception) {
- return@newInstance MessageDialogFragment.newInstance(
- this,
- titleId = R.string.import_failed,
- descriptionId = R.string.user_data_import_failed_description
- )
- }
-
- // Reinitialize relevant data
- NativeLibrary.initializeSystem(true)
- NativeConfig.initializeGlobalConfig()
- gamesViewModel.reloadGames(false)
- driverViewModel.reloadDriverData()
-
- return@newInstance getString(R.string.user_data_import_success)
- }.show(supportFragmentManager, ProgressDialogFragment.TAG)
+ InstallableActions.importUserData(
+ activity = this,
+ fragmentManager = supportFragmentManager,
+ gamesViewModel = gamesViewModel,
+ driverViewModel = driverViewModel,
+ result = result
+ )
}
}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/InstallableActions.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/InstallableActions.kt
new file mode 100644
index 0000000000..d385e2a095
--- /dev/null
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/InstallableActions.kt
@@ -0,0 +1,327 @@
+// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+package org.yuzu.yuzu_emu.utils
+
+import android.content.Intent
+import android.net.Uri
+import android.widget.Toast
+import androidx.fragment.app.FragmentActivity
+import androidx.fragment.app.FragmentManager
+import org.yuzu.yuzu_emu.NativeLibrary
+import org.yuzu.yuzu_emu.R
+import org.yuzu.yuzu_emu.fragments.MessageDialogFragment
+import org.yuzu.yuzu_emu.fragments.ProgressDialogFragment
+import org.yuzu.yuzu_emu.model.AddonViewModel
+import org.yuzu.yuzu_emu.model.DriverViewModel
+import org.yuzu.yuzu_emu.model.GamesViewModel
+import org.yuzu.yuzu_emu.model.HomeViewModel
+import org.yuzu.yuzu_emu.model.InstallResult
+import org.yuzu.yuzu_emu.model.TaskState
+import java.io.BufferedInputStream
+import java.io.BufferedOutputStream
+import java.io.File
+import java.io.FilenameFilter
+import java.util.zip.ZipEntry
+import java.util.zip.ZipInputStream
+
+object InstallableActions {
+ fun processKey(
+ activity: FragmentActivity,
+ fragmentManager: FragmentManager,
+ gamesViewModel: GamesViewModel,
+ result: Uri,
+ extension: String = "keys"
+ ) {
+ activity.contentResolver.takePersistableUriPermission(
+ result,
+ Intent.FLAG_GRANT_READ_URI_PERMISSION
+ )
+
+ val resultCode = NativeLibrary.installKeys(result.toString(), extension)
+ if (resultCode == 0) {
+ Toast.makeText(
+ activity.applicationContext,
+ R.string.keys_install_success,
+ Toast.LENGTH_SHORT
+ ).show()
+ gamesViewModel.reloadGames(true)
+ return
+ }
+
+ val resultString = activity.resources.getStringArray(R.array.installKeysResults)[resultCode]
+ MessageDialogFragment.newInstance(
+ titleId = R.string.keys_failed,
+ descriptionString = resultString,
+ helpLinkId = R.string.keys_missing_help
+ ).show(fragmentManager, MessageDialogFragment.TAG)
+ }
+
+ fun processFirmware(
+ activity: FragmentActivity,
+ fragmentManager: FragmentManager,
+ homeViewModel: HomeViewModel,
+ result: Uri,
+ onComplete: (() -> Unit)? = null
+ ) {
+ val filterNCA = FilenameFilter { _, dirName -> dirName.endsWith(".nca") }
+ val firmwarePath = File(NativeConfig.getNandDir() + "/system/Contents/registered/")
+ val cacheFirmwareDir = File("${activity.cacheDir.path}/registered/")
+
+ ProgressDialogFragment.newInstance(
+ activity,
+ R.string.firmware_installing
+ ) { progressCallback, _ ->
+ var messageToShow: Any
+ try {
+ FileUtil.unzipToInternalStorage(
+ result.toString(),
+ cacheFirmwareDir,
+ progressCallback
+ )
+ val unfilteredNumOfFiles = cacheFirmwareDir.list()?.size ?: -1
+ val filteredNumOfFiles = cacheFirmwareDir.list(filterNCA)?.size ?: -2
+ messageToShow = if (unfilteredNumOfFiles != filteredNumOfFiles) {
+ MessageDialogFragment.newInstance(
+ activity,
+ titleId = R.string.firmware_installed_failure,
+ descriptionId = R.string.firmware_installed_failure_description
+ )
+ } else {
+ firmwarePath.deleteRecursively()
+ cacheFirmwareDir.copyRecursively(firmwarePath, overwrite = true)
+ NativeLibrary.initializeSystem(true)
+ homeViewModel.setCheckKeys(true)
+ activity.getString(R.string.save_file_imported_success)
+ }
+ } catch (_: Exception) {
+ messageToShow = activity.getString(R.string.fatal_error)
+ } finally {
+ cacheFirmwareDir.deleteRecursively()
+ }
+ messageToShow
+ }.apply {
+ onDialogComplete = onComplete
+ }.show(fragmentManager, ProgressDialogFragment.TAG)
+ }
+
+ fun uninstallFirmware(
+ activity: FragmentActivity,
+ fragmentManager: FragmentManager,
+ homeViewModel: HomeViewModel
+ ) {
+ val firmwarePath = File(NativeConfig.getNandDir() + "/system/Contents/registered/")
+ ProgressDialogFragment.newInstance(
+ activity,
+ R.string.firmware_uninstalling
+ ) { _, _ ->
+ val messageToShow: Any = try {
+ if (firmwarePath.exists()) {
+ firmwarePath.deleteRecursively()
+ NativeLibrary.initializeSystem(true)
+ homeViewModel.setCheckKeys(true)
+ activity.getString(R.string.firmware_uninstalled_success)
+ } else {
+ activity.getString(R.string.firmware_uninstalled_failure)
+ }
+ } catch (_: Exception) {
+ activity.getString(R.string.fatal_error)
+ }
+ messageToShow
+ }.show(fragmentManager, ProgressDialogFragment.TAG)
+ }
+
+ fun installContent(
+ activity: FragmentActivity,
+ fragmentManager: FragmentManager,
+ addonViewModel: AddonViewModel,
+ documents: List
+ ) {
+ ProgressDialogFragment.newInstance(
+ activity,
+ R.string.installing_game_content
+ ) { progressCallback, messageCallback ->
+ var installSuccess = 0
+ var installOverwrite = 0
+ var errorBaseGame = 0
+ var error = 0
+ documents.forEach {
+ messageCallback.invoke(FileUtil.getFilename(it))
+ when (
+ InstallResult.from(
+ NativeLibrary.installFileToNand(
+ it.toString(),
+ progressCallback
+ )
+ )
+ ) {
+ InstallResult.Success -> installSuccess += 1
+ InstallResult.Overwrite -> installOverwrite += 1
+ InstallResult.BaseInstallAttempted -> errorBaseGame += 1
+ InstallResult.Failure -> error += 1
+ }
+ }
+
+ addonViewModel.refreshAddons(force = true)
+
+ val separator = System.lineSeparator() ?: "\n"
+ val installResult = StringBuilder()
+ if (installSuccess > 0) {
+ installResult.append(
+ activity.getString(
+ R.string.install_game_content_success_install,
+ installSuccess
+ )
+ )
+ installResult.append(separator)
+ }
+ if (installOverwrite > 0) {
+ installResult.append(
+ activity.getString(
+ R.string.install_game_content_success_overwrite,
+ installOverwrite
+ )
+ )
+ installResult.append(separator)
+ }
+ val errorTotal = errorBaseGame + error
+ if (errorTotal > 0) {
+ installResult.append(separator)
+ installResult.append(
+ activity.getString(
+ R.string.install_game_content_failed_count,
+ errorTotal
+ )
+ )
+ installResult.append(separator)
+ if (errorBaseGame > 0) {
+ installResult.append(separator)
+ installResult.append(activity.getString(R.string.install_game_content_failure_base))
+ installResult.append(separator)
+ }
+ if (error > 0) {
+ installResult.append(
+ activity.getString(R.string.install_game_content_failure_description)
+ )
+ installResult.append(separator)
+ }
+ return@newInstance MessageDialogFragment.newInstance(
+ activity,
+ titleId = R.string.install_game_content_failure,
+ descriptionString = installResult.toString().trim(),
+ helpLinkId = R.string.install_game_content_help_link
+ )
+ } else {
+ return@newInstance MessageDialogFragment.newInstance(
+ activity,
+ titleId = R.string.install_game_content_success,
+ descriptionString = installResult.toString().trim()
+ )
+ }
+ }.show(fragmentManager, ProgressDialogFragment.TAG)
+ }
+
+ fun exportUserData(
+ activity: FragmentActivity,
+ fragmentManager: FragmentManager,
+ result: Uri
+ ) {
+ val userDirectory = DirectoryInitialization.userDirectory
+ if (userDirectory == null) {
+ Toast.makeText(
+ activity.applicationContext,
+ R.string.fatal_error,
+ Toast.LENGTH_SHORT
+ ).show()
+ return
+ }
+
+ ProgressDialogFragment.newInstance(
+ activity,
+ R.string.exporting_user_data,
+ true
+ ) { progressCallback, _ ->
+ val zipResult = FileUtil.zipFromInternalStorage(
+ File(userDirectory),
+ userDirectory,
+ BufferedOutputStream(activity.contentResolver.openOutputStream(result)),
+ progressCallback,
+ compression = false
+ )
+ return@newInstance when (zipResult) {
+ TaskState.Completed -> activity.getString(R.string.user_data_export_success)
+ TaskState.Failed -> R.string.export_failed
+ TaskState.Cancelled -> R.string.user_data_export_cancelled
+ }
+ }.show(fragmentManager, ProgressDialogFragment.TAG)
+ }
+
+ fun importUserData(
+ activity: FragmentActivity,
+ fragmentManager: FragmentManager,
+ gamesViewModel: GamesViewModel,
+ driverViewModel: DriverViewModel,
+ result: Uri
+ ) {
+ val userDirectory = DirectoryInitialization.userDirectory
+ if (userDirectory == null) {
+ Toast.makeText(
+ activity.applicationContext,
+ R.string.fatal_error,
+ Toast.LENGTH_SHORT
+ ).show()
+ return
+ }
+
+ ProgressDialogFragment.newInstance(
+ activity,
+ R.string.importing_user_data
+ ) { progressCallback, _ ->
+ val checkStream = ZipInputStream(
+ BufferedInputStream(activity.contentResolver.openInputStream(result))
+ )
+ var isYuzuBackup = false
+ checkStream.use { stream ->
+ var ze: ZipEntry? = null
+ while (stream.nextEntry?.also { ze = it } != null) {
+ val itemName = ze!!.name.trim()
+ if (itemName == "/config/config.ini" || itemName == "config/config.ini") {
+ isYuzuBackup = true
+ return@use
+ }
+ }
+ }
+ if (!isYuzuBackup) {
+ return@newInstance MessageDialogFragment.newInstance(
+ activity,
+ titleId = R.string.invalid_yuzu_backup,
+ descriptionId = R.string.user_data_import_failed_description
+ )
+ }
+
+ NativeConfig.unloadGlobalConfig()
+ File(userDirectory).deleteRecursively()
+
+ try {
+ FileUtil.unzipToInternalStorage(
+ result.toString(),
+ File(userDirectory),
+ progressCallback
+ )
+ } catch (_: Exception) {
+ return@newInstance MessageDialogFragment.newInstance(
+ activity,
+ titleId = R.string.import_failed,
+ descriptionId = R.string.user_data_import_failed_description
+ )
+ }
+
+ NativeLibrary.initializeSystem(true)
+ NativeConfig.initializeGlobalConfig()
+ gamesViewModel.reloadGames(false)
+ driverViewModel.reloadDriverData()
+
+ return@newInstance activity.getString(R.string.user_data_import_success)
+ }.show(fragmentManager, ProgressDialogFragment.TAG)
+ }
+}
diff --git a/src/android/app/src/main/res/layout-w600dp/fragment_about.xml b/src/android/app/src/main/res/layout-w600dp/fragment_about.xml
index ae2b3e3637..cc8d26dd58 100644
--- a/src/android/app/src/main/res/layout-w600dp/fragment_about.xml
+++ b/src/android/app/src/main/res/layout-w600dp/fragment_about.xml
@@ -10,12 +10,11 @@
+ android:touchscreenBlocksFocus="false">
+ android:paddingBottom="24dp"
+ android:paddingStart="24dp"
+ android:paddingTop="0dp"
+ android:paddingEnd="24dp">
-
+ android:gravity="center_horizontal"
+ android:orientation="vertical">
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
diff --git a/src/android/app/src/main/res/layout/fragment_about.xml b/src/android/app/src/main/res/layout/fragment_about.xml
index da823bcc25..b1b896f169 100644
--- a/src/android/app/src/main/res/layout/fragment_about.xml
+++ b/src/android/app/src/main/res/layout/fragment_about.xml
@@ -10,12 +10,11 @@
+ android:touchscreenBlocksFocus="false">
-
-
-
-
+
+
+
+
+ android:text="@string/app_name"
+ android:textAlignment="center" />
-
+
-
-
-
-
+
@@ -220,7 +206,9 @@
app:icon="@drawable/ic_discord"
app:iconSize="24dp"
app:iconGravity="textStart"
- app:iconPadding="0dp" />
+ app:iconPadding="0dp"
+ app:strokeColor="?attr/colorOutline"
+ app:strokeWidth="1dp" />
+ app:iconPadding="0dp"
+ app:strokeColor="?attr/colorOutline"
+ app:strokeWidth="1dp" />
+ app:iconPadding="0dp"
+ app:strokeColor="?attr/colorOutline"
+ app:strokeWidth="1dp" />
+ app:iconPadding="0dp"
+ app:strokeColor="?attr/colorOutline"
+ app:strokeWidth="1dp" />
+ app:iconPadding="0dp"
+ app:strokeColor="?attr/colorOutline"
+ app:strokeWidth="1dp" />
diff --git a/src/android/app/src/main/res/navigation/emulation_navigation.xml b/src/android/app/src/main/res/navigation/emulation_navigation.xml
index 5e6a49501d..2adc60a47c 100644
--- a/src/android/app/src/main/res/navigation/emulation_navigation.xml
+++ b/src/android/app/src/main/res/navigation/emulation_navigation.xml
@@ -40,10 +40,6 @@
+ app:destination="@id/settingsActivity" />
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 7d04a19f36..dd567abc1a 100644
--- a/src/android/app/src/main/res/navigation/home_navigation.xml
+++ b/src/android/app/src/main/res/navigation/home_navigation.xml
@@ -20,26 +20,7 @@
-
-
-
-
-
-
-
+ android:label="HomeSettingsFragment" />
-
-
+ android:label="AboutFragment" />
+ app:destination="@id/settingsActivity" />
+
+
+
+
+
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/android/app/src/main/res/values/strings.xml b/src/android/app/src/main/res/values/strings.xml
index 7d094effcb..83c04ad40e 100644
--- a/src/android/app/src/main/res/values/strings.xml
+++ b/src/android/app/src/main/res/values/strings.xml
@@ -400,7 +400,7 @@
Copied to clipboard
An open-source Switch emulator
Contributors
- Contributors who made Eden for Android possible
+ People who made Eden for Android possible
https://git.eden-emu.dev/eden-emu/eden/activity/contributors
Projects that make Eden for Android possible
Build