summaryrefslogtreecommitdiffstats
path: root/src/android/app/src/main/java/org
diff options
context:
space:
mode:
Diffstat (limited to 'src/android/app/src/main/java/org')
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/FolderAdapter.kt76
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/AddGameFolderDialogFragment.kt53
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/GameFolderPropertiesDialogFragment.kt72
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/GameFoldersFragment.kt128
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/HomeSettingsFragment.kt13
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/SetupFragment.kt8
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/model/GameDir.kt13
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/model/GamesViewModel.kt61
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/model/HomeViewModel.kt19
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/main/MainActivity.kt26
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/FileUtil.kt21
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/GameHelper.kt39
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/NativeConfig.kt20
13 files changed, 495 insertions, 54 deletions
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/FolderAdapter.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/FolderAdapter.kt
new file mode 100644
index 000000000..ab657a7b9
--- /dev/null
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/FolderAdapter.kt
@@ -0,0 +1,76 @@
+// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+package org.yuzu.yuzu_emu.adapters
+
+import android.net.Uri
+import android.text.TextUtils
+import android.view.LayoutInflater
+import android.view.ViewGroup
+import androidx.fragment.app.FragmentActivity
+import androidx.recyclerview.widget.AsyncDifferConfig
+import androidx.recyclerview.widget.DiffUtil
+import androidx.recyclerview.widget.ListAdapter
+import androidx.recyclerview.widget.RecyclerView
+import org.yuzu.yuzu_emu.databinding.CardFolderBinding
+import org.yuzu.yuzu_emu.fragments.GameFolderPropertiesDialogFragment
+import org.yuzu.yuzu_emu.model.GameDir
+import org.yuzu.yuzu_emu.model.GamesViewModel
+
+class FolderAdapter(val activity: FragmentActivity, val gamesViewModel: GamesViewModel) :
+ ListAdapter<GameDir, FolderAdapter.FolderViewHolder>(
+ AsyncDifferConfig.Builder(DiffCallback()).build()
+ ) {
+ override fun onCreateViewHolder(
+ parent: ViewGroup,
+ viewType: Int
+ ): FolderAdapter.FolderViewHolder {
+ CardFolderBinding.inflate(LayoutInflater.from(parent.context), parent, false)
+ .also { return FolderViewHolder(it) }
+ }
+
+ override fun onBindViewHolder(holder: FolderAdapter.FolderViewHolder, position: Int) =
+ holder.bind(currentList[position])
+
+ inner class FolderViewHolder(val binding: CardFolderBinding) :
+ RecyclerView.ViewHolder(binding.root) {
+ private lateinit var gameDir: GameDir
+
+ fun bind(gameDir: GameDir) {
+ this.gameDir = gameDir
+
+ binding.apply {
+ path.text = Uri.parse(gameDir.uriString).path
+ path.postDelayed(
+ {
+ path.isSelected = true
+ path.ellipsize = TextUtils.TruncateAt.MARQUEE
+ },
+ 3000
+ )
+
+ buttonEdit.setOnClickListener {
+ GameFolderPropertiesDialogFragment.newInstance(this@FolderViewHolder.gameDir)
+ .show(
+ activity.supportFragmentManager,
+ GameFolderPropertiesDialogFragment.TAG
+ )
+ }
+
+ buttonDelete.setOnClickListener {
+ gamesViewModel.removeFolder(this@FolderViewHolder.gameDir)
+ }
+ }
+ }
+ }
+
+ private class DiffCallback : DiffUtil.ItemCallback<GameDir>() {
+ override fun areItemsTheSame(oldItem: GameDir, newItem: GameDir): Boolean {
+ return oldItem == newItem
+ }
+
+ override fun areContentsTheSame(oldItem: GameDir, newItem: GameDir): Boolean {
+ return oldItem == newItem
+ }
+ }
+}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/AddGameFolderDialogFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/AddGameFolderDialogFragment.kt
new file mode 100644
index 000000000..dec2b7cf1
--- /dev/null
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/AddGameFolderDialogFragment.kt
@@ -0,0 +1,53 @@
+// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+package org.yuzu.yuzu_emu.fragments
+
+import android.app.Dialog
+import android.content.DialogInterface
+import android.net.Uri
+import android.os.Bundle
+import androidx.fragment.app.DialogFragment
+import androidx.fragment.app.activityViewModels
+import com.google.android.material.dialog.MaterialAlertDialogBuilder
+import org.yuzu.yuzu_emu.R
+import org.yuzu.yuzu_emu.databinding.DialogAddFolderBinding
+import org.yuzu.yuzu_emu.model.GameDir
+import org.yuzu.yuzu_emu.model.GamesViewModel
+
+class AddGameFolderDialogFragment : DialogFragment() {
+ private val gamesViewModel: GamesViewModel by activityViewModels()
+
+ override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
+ val binding = DialogAddFolderBinding.inflate(layoutInflater)
+ val folderUriString = requireArguments().getString(FOLDER_URI_STRING)
+ if (folderUriString == null) {
+ dismiss()
+ }
+ binding.path.text = Uri.parse(folderUriString).path
+
+ return MaterialAlertDialogBuilder(requireContext())
+ .setTitle(R.string.add_game_folder)
+ .setPositiveButton(android.R.string.ok) { _: DialogInterface, _: Int ->
+ val newGameDir = GameDir(folderUriString!!, binding.deepScanSwitch.isChecked)
+ gamesViewModel.addFolder(newGameDir)
+ }
+ .setNegativeButton(android.R.string.cancel, null)
+ .setView(binding.root)
+ .show()
+ }
+
+ companion object {
+ const val TAG = "AddGameFolderDialogFragment"
+
+ private const val FOLDER_URI_STRING = "FolderUriString"
+
+ fun newInstance(folderUriString: String): AddGameFolderDialogFragment {
+ val args = Bundle()
+ args.putString(FOLDER_URI_STRING, folderUriString)
+ val fragment = AddGameFolderDialogFragment()
+ fragment.arguments = args
+ return fragment
+ }
+ }
+}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/GameFolderPropertiesDialogFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/GameFolderPropertiesDialogFragment.kt
new file mode 100644
index 000000000..b6c2e4635
--- /dev/null
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/GameFolderPropertiesDialogFragment.kt
@@ -0,0 +1,72 @@
+// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+package org.yuzu.yuzu_emu.fragments
+
+import android.app.Dialog
+import android.content.DialogInterface
+import android.os.Bundle
+import androidx.fragment.app.DialogFragment
+import androidx.fragment.app.activityViewModels
+import com.google.android.material.dialog.MaterialAlertDialogBuilder
+import org.yuzu.yuzu_emu.R
+import org.yuzu.yuzu_emu.databinding.DialogFolderPropertiesBinding
+import org.yuzu.yuzu_emu.model.GameDir
+import org.yuzu.yuzu_emu.model.GamesViewModel
+import org.yuzu.yuzu_emu.utils.SerializableHelper.parcelable
+
+class GameFolderPropertiesDialogFragment : DialogFragment() {
+ private val gamesViewModel: GamesViewModel by activityViewModels()
+
+ private var deepScan = false
+
+ override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
+ val binding = DialogFolderPropertiesBinding.inflate(layoutInflater)
+ val gameDir = requireArguments().parcelable<GameDir>(GAME_DIR)!!
+
+ // Restore checkbox state
+ binding.deepScanSwitch.isChecked =
+ savedInstanceState?.getBoolean(DEEP_SCAN) ?: gameDir.deepScan
+
+ // Ensure that we can get the checkbox state even if the view is destroyed
+ deepScan = binding.deepScanSwitch.isChecked
+ binding.deepScanSwitch.setOnClickListener {
+ deepScan = binding.deepScanSwitch.isChecked
+ }
+
+ return MaterialAlertDialogBuilder(requireContext())
+ .setView(binding.root)
+ .setTitle(R.string.game_folder_properties)
+ .setPositiveButton(android.R.string.ok) { _: DialogInterface, _: Int ->
+ val folderIndex = gamesViewModel.folders.value.indexOf(gameDir)
+ if (folderIndex != -1) {
+ gamesViewModel.folders.value[folderIndex].deepScan =
+ binding.deepScanSwitch.isChecked
+ gamesViewModel.updateGameDirs()
+ }
+ }
+ .setNegativeButton(android.R.string.cancel, null)
+ .show()
+ }
+
+ override fun onSaveInstanceState(outState: Bundle) {
+ super.onSaveInstanceState(outState)
+ outState.putBoolean(DEEP_SCAN, deepScan)
+ }
+
+ companion object {
+ const val TAG = "GameFolderPropertiesDialogFragment"
+
+ private const val GAME_DIR = "GameDir"
+
+ private const val DEEP_SCAN = "DeepScan"
+
+ fun newInstance(gameDir: GameDir): GameFolderPropertiesDialogFragment {
+ val args = Bundle()
+ args.putParcelable(GAME_DIR, gameDir)
+ val fragment = GameFolderPropertiesDialogFragment()
+ fragment.arguments = args
+ return fragment
+ }
+ }
+}
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
new file mode 100644
index 000000000..341a37fdb
--- /dev/null
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/GameFoldersFragment.kt
@@ -0,0 +1,128 @@
+// 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.core.view.ViewCompat
+import androidx.core.view.WindowInsetsCompat
+import androidx.core.view.updatePadding
+import androidx.fragment.app.Fragment
+import androidx.fragment.app.activityViewModels
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.lifecycleScope
+import androidx.lifecycle.repeatOnLifecycle
+import androidx.navigation.findNavController
+import androidx.recyclerview.widget.GridLayoutManager
+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
+import org.yuzu.yuzu_emu.model.GamesViewModel
+import org.yuzu.yuzu_emu.model.HomeViewModel
+import org.yuzu.yuzu_emu.ui.main.MainActivity
+
+class GameFoldersFragment : Fragment() {
+ private var _binding: FragmentFoldersBinding? = null
+ private val binding get() = _binding!!
+
+ private val homeViewModel: HomeViewModel by activityViewModels()
+ private val gamesViewModel: GamesViewModel by activityViewModels()
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ enterTransition = MaterialSharedAxis(MaterialSharedAxis.X, true)
+ returnTransition = MaterialSharedAxis(MaterialSharedAxis.X, false)
+ reenterTransition = MaterialSharedAxis(MaterialSharedAxis.X, false)
+
+ gamesViewModel.onOpenGameFoldersFragment()
+ }
+
+ override fun onCreateView(
+ inflater: LayoutInflater,
+ container: ViewGroup?,
+ savedInstanceState: Bundle?
+ ): View {
+ _binding = FragmentFoldersBinding.inflate(inflater)
+ return binding.root
+ }
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ super.onViewCreated(view, savedInstanceState)
+ homeViewModel.setNavigationVisibility(visible = false, animated = true)
+ homeViewModel.setStatusBarShadeVisibility(visible = false)
+
+ binding.toolbarFolders.setNavigationOnClickListener {
+ binding.root.findNavController().popBackStack()
+ }
+
+ binding.listFolders.apply {
+ layoutManager = GridLayoutManager(
+ requireContext(),
+ resources.getInteger(R.integer.grid_columns)
+ )
+ adapter = FolderAdapter(requireActivity(), gamesViewModel)
+ }
+
+ viewLifecycleOwner.lifecycleScope.launch {
+ repeatOnLifecycle(Lifecycle.State.CREATED) {
+ gamesViewModel.folders.collect {
+ (binding.listFolders.adapter as FolderAdapter).submitList(it)
+ }
+ }
+ }
+
+ val mainActivity = requireActivity() as MainActivity
+ binding.buttonAdd.setOnClickListener {
+ mainActivity.getGamesDirectory.launch(Intent(Intent.ACTION_OPEN_DOCUMENT_TREE).data)
+ }
+
+ setInsets()
+ }
+
+ override fun onStop() {
+ super.onStop()
+ gamesViewModel.onCloseGameFoldersFragment()
+ }
+
+ private fun setInsets() =
+ ViewCompat.setOnApplyWindowInsetsListener(
+ binding.root
+ ) { _: View, windowInsets: WindowInsetsCompat ->
+ val barInsets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars())
+ val cutoutInsets = windowInsets.getInsets(WindowInsetsCompat.Type.displayCutout())
+
+ val leftInsets = barInsets.left + cutoutInsets.left
+ val rightInsets = barInsets.right + cutoutInsets.right
+
+ val mlpToolbar = binding.toolbarFolders.layoutParams as ViewGroup.MarginLayoutParams
+ mlpToolbar.leftMargin = leftInsets
+ mlpToolbar.rightMargin = rightInsets
+ binding.toolbarFolders.layoutParams = mlpToolbar
+
+ val fabSpacing = resources.getDimensionPixelSize(R.dimen.spacing_fab)
+ val mlpFab =
+ binding.buttonAdd.layoutParams as ViewGroup.MarginLayoutParams
+ mlpFab.leftMargin = leftInsets + fabSpacing
+ mlpFab.rightMargin = rightInsets + fabSpacing
+ mlpFab.bottomMargin = barInsets.bottom + fabSpacing
+ binding.buttonAdd.layoutParams = mlpFab
+
+ val mlpListFolders = binding.listFolders.layoutParams as ViewGroup.MarginLayoutParams
+ mlpListFolders.leftMargin = leftInsets
+ mlpListFolders.rightMargin = rightInsets
+ binding.listFolders.layoutParams = mlpListFolders
+
+ binding.listFolders.updatePadding(
+ bottom = barInsets.bottom +
+ resources.getDimensionPixelSize(R.dimen.spacing_bottom_list_fab)
+ )
+
+ windowInsets
+ }
+}
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 4720daec4..3addc2e63 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
@@ -127,18 +127,13 @@ class HomeSettingsFragment : Fragment() {
)
add(
HomeSetting(
- R.string.select_games_folder,
+ R.string.manage_game_folders,
R.string.select_games_folder_description,
R.drawable.ic_add,
{
- mainActivity.getGamesDirectory.launch(
- Intent(Intent.ACTION_OPEN_DOCUMENT_TREE).data
- )
- },
- { true },
- 0,
- 0,
- homeViewModel.gamesDir
+ binding.root.findNavController()
+ .navigate(R.id.action_homeSettingsFragment_to_gameFoldersFragment)
+ }
)
)
add(
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
index c66bb635a..c4277735d 100644
--- 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
@@ -42,7 +42,7 @@ import org.yuzu.yuzu_emu.model.SetupPage
import org.yuzu.yuzu_emu.model.StepState
import org.yuzu.yuzu_emu.ui.main.MainActivity
import org.yuzu.yuzu_emu.utils.DirectoryInitialization
-import org.yuzu.yuzu_emu.utils.GameHelper
+import org.yuzu.yuzu_emu.utils.NativeConfig
import org.yuzu.yuzu_emu.utils.ViewUtils
class SetupFragment : Fragment() {
@@ -184,11 +184,7 @@ class SetupFragment : Fragment() {
R.string.add_games_warning_description,
R.string.add_games_warning_help,
{
- val preferences =
- PreferenceManager.getDefaultSharedPreferences(
- YuzuApplication.appContext
- )
- if (preferences.getString(GameHelper.KEY_GAME_PATH, "")!!.isNotEmpty()) {
+ if (NativeConfig.getGameDirs().isNotEmpty()) {
StepState.COMPLETE
} else {
StepState.INCOMPLETE
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/GameDir.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/GameDir.kt
new file mode 100644
index 000000000..274bc1c7b
--- /dev/null
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/GameDir.kt
@@ -0,0 +1,13 @@
+// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+package org.yuzu.yuzu_emu.model
+
+import android.os.Parcelable
+import kotlinx.parcelize.Parcelize
+
+@Parcelize
+data class GameDir(
+ val uriString: String,
+ var deepScan: Boolean
+) : Parcelable
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/GamesViewModel.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/GamesViewModel.kt
index 8512ed17c..752d98c10 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/GamesViewModel.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/GamesViewModel.kt
@@ -12,6 +12,7 @@ import java.util.Locale
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import kotlinx.serialization.decodeFromString
@@ -20,6 +21,7 @@ import org.yuzu.yuzu_emu.NativeLibrary
import org.yuzu.yuzu_emu.YuzuApplication
import org.yuzu.yuzu_emu.utils.GameHelper
import org.yuzu.yuzu_emu.utils.GameMetadata
+import org.yuzu.yuzu_emu.utils.NativeConfig
class GamesViewModel : ViewModel() {
val games: StateFlow<List<Game>> get() = _games
@@ -40,6 +42,9 @@ class GamesViewModel : ViewModel() {
val searchFocused: StateFlow<Boolean> get() = _searchFocused
private val _searchFocused = MutableStateFlow(false)
+ private val _folders = MutableStateFlow(mutableListOf<GameDir>())
+ val folders = _folders.asStateFlow()
+
init {
// Ensure keys are loaded so that ROM metadata can be decrypted.
NativeLibrary.reloadKeys()
@@ -50,6 +55,7 @@ class GamesViewModel : ViewModel() {
viewModelScope.launch {
withContext(Dispatchers.IO) {
+ getGameDirs()
if (storedGames!!.isNotEmpty()) {
val deserializedGames = mutableSetOf<Game>()
storedGames.forEach {
@@ -104,7 +110,7 @@ class GamesViewModel : ViewModel() {
_searchFocused.value = searchFocused
}
- fun reloadGames(directoryChanged: Boolean) {
+ fun reloadGames(directoriesChanged: Boolean) {
if (isReloading.value) {
return
}
@@ -116,10 +122,61 @@ class GamesViewModel : ViewModel() {
setGames(GameHelper.getGames())
_isReloading.value = false
- if (directoryChanged) {
+ if (directoriesChanged) {
setShouldSwapData(true)
}
}
}
}
+
+ fun addFolder(gameDir: GameDir) =
+ viewModelScope.launch {
+ withContext(Dispatchers.IO) {
+ NativeConfig.addGameDir(gameDir)
+ getGameDirs()
+ }
+ }
+
+ fun removeFolder(gameDir: GameDir) =
+ viewModelScope.launch {
+ withContext(Dispatchers.IO) {
+ val gameDirs = _folders.value.toMutableList()
+ val removedDirIndex = gameDirs.indexOf(gameDir)
+ if (removedDirIndex != -1) {
+ gameDirs.removeAt(removedDirIndex)
+ NativeConfig.setGameDirs(gameDirs.toTypedArray())
+ getGameDirs()
+ }
+ }
+ }
+
+ fun updateGameDirs() =
+ viewModelScope.launch {
+ withContext(Dispatchers.IO) {
+ NativeConfig.setGameDirs(_folders.value.toTypedArray())
+ getGameDirs()
+ }
+ }
+
+ fun onOpenGameFoldersFragment() =
+ viewModelScope.launch {
+ withContext(Dispatchers.IO) {
+ getGameDirs()
+ }
+ }
+
+ fun onCloseGameFoldersFragment() =
+ viewModelScope.launch {
+ withContext(Dispatchers.IO) {
+ getGameDirs(true)
+ }
+ }
+
+ private fun getGameDirs(reloadList: Boolean = false) {
+ val gameDirs = NativeConfig.getGameDirs()
+ _folders.value = gameDirs.toMutableList()
+ if (reloadList) {
+ reloadGames(true)
+ }
+ }
}
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 756f76721..251b5a667 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
@@ -3,15 +3,9 @@
package org.yuzu.yuzu_emu.model
-import android.net.Uri
-import androidx.fragment.app.FragmentActivity
import androidx.lifecycle.ViewModel
-import androidx.lifecycle.ViewModelProvider
-import androidx.preference.PreferenceManager
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
-import org.yuzu.yuzu_emu.YuzuApplication
-import org.yuzu.yuzu_emu.utils.GameHelper
class HomeViewModel : ViewModel() {
val navigationVisible: StateFlow<Pair<Boolean, Boolean>> get() = _navigationVisible
@@ -23,14 +17,6 @@ class HomeViewModel : ViewModel() {
val shouldPageForward: StateFlow<Boolean> get() = _shouldPageForward
private val _shouldPageForward = MutableStateFlow(false)
- val gamesDir: StateFlow<String> get() = _gamesDir
- private val _gamesDir = MutableStateFlow(
- Uri.parse(
- PreferenceManager.getDefaultSharedPreferences(YuzuApplication.appContext)
- .getString(GameHelper.KEY_GAME_PATH, "")
- ).path ?: ""
- )
-
var navigatedToSetup = false
fun setNavigationVisibility(visible: Boolean, animated: Boolean) {
@@ -50,9 +36,4 @@ class HomeViewModel : ViewModel() {
fun setShouldPageForward(pageForward: Boolean) {
_shouldPageForward.value = pageForward
}
-
- fun setGamesDir(activity: FragmentActivity, dir: String) {
- ViewModelProvider(activity)[GamesViewModel::class.java].reloadGames(true)
- _gamesDir.value = dir
- }
}
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 bd2f4cd25..745901e19 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
@@ -40,6 +40,7 @@ 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.features.settings.model.Settings
+import org.yuzu.yuzu_emu.fragments.AddGameFolderDialogFragment
import org.yuzu.yuzu_emu.fragments.IndeterminateProgressDialogFragment
import org.yuzu.yuzu_emu.fragments.MessageDialogFragment
import org.yuzu.yuzu_emu.getPublicFilesDir
@@ -293,20 +294,19 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
Intent.FLAG_GRANT_READ_URI_PERMISSION
)
- // 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()
+ val uriString = result.toString()
+ val folder = gamesViewModel.folders.value.firstOrNull { it.uriString == uriString }
+ if (folder != null) {
+ Toast.makeText(
+ applicationContext,
+ R.string.folder_already_added,
+ Toast.LENGTH_SHORT
+ ).show()
+ return
+ }
- gamesViewModel.reloadGames(true)
- homeViewModel.setGamesDir(this, result.path!!)
+ AddGameFolderDialogFragment.newInstance(uriString)
+ .show(supportFragmentManager, AddGameFolderDialogFragment.TAG)
}
val getProdKey =
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/FileUtil.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/FileUtil.kt
index 8c3268e9c..bbe7bfa92 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/FileUtil.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/FileUtil.kt
@@ -364,6 +364,27 @@ object FileUtil {
.lowercase()
}
+ fun isTreeUriValid(uri: Uri): Boolean {
+ val resolver = context.contentResolver
+ val columns = arrayOf(
+ DocumentsContract.Document.COLUMN_DOCUMENT_ID,
+ DocumentsContract.Document.COLUMN_DISPLAY_NAME,
+ DocumentsContract.Document.COLUMN_MIME_TYPE
+ )
+ return try {
+ val docId: String = if (isRootTreeUri(uri)) {
+ DocumentsContract.getTreeDocumentId(uri)
+ } else {
+ DocumentsContract.getDocumentId(uri)
+ }
+ val childrenUri = DocumentsContract.buildChildDocumentsUriUsingTree(uri, docId)
+ resolver.query(childrenUri, columns, null, null, null)
+ true
+ } catch (_: Exception) {
+ false
+ }
+ }
+
@Throws(IOException::class)
fun getStringFromFile(file: File): String =
String(file.readBytes(), StandardCharsets.UTF_8)
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/GameHelper.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/GameHelper.kt
index e6aca6b44..55010dc59 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/GameHelper.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/GameHelper.kt
@@ -11,10 +11,11 @@ import kotlinx.serialization.json.Json
import org.yuzu.yuzu_emu.NativeLibrary
import org.yuzu.yuzu_emu.YuzuApplication
import org.yuzu.yuzu_emu.model.Game
+import org.yuzu.yuzu_emu.model.GameDir
import org.yuzu.yuzu_emu.model.MinimalDocumentFile
object GameHelper {
- const val KEY_GAME_PATH = "game_path"
+ private const val KEY_OLD_GAME_PATH = "game_path"
const val KEY_GAMES = "Games"
private lateinit var preferences: SharedPreferences
@@ -22,15 +23,43 @@ object GameHelper {
fun getGames(): List<Game> {
val games = mutableListOf<Game>()
val context = YuzuApplication.appContext
- val gamesDir =
- PreferenceManager.getDefaultSharedPreferences(context).getString(KEY_GAME_PATH, "")
- val gamesUri = Uri.parse(gamesDir)
preferences = PreferenceManager.getDefaultSharedPreferences(context)
+ val gameDirs = mutableListOf<GameDir>()
+ val oldGamesDir = preferences.getString(KEY_OLD_GAME_PATH, "") ?: ""
+ if (oldGamesDir.isNotEmpty()) {
+ gameDirs.add(GameDir(oldGamesDir, true))
+ preferences.edit().remove(KEY_OLD_GAME_PATH).apply()
+ }
+ gameDirs.addAll(NativeConfig.getGameDirs())
+
// Ensure keys are loaded so that ROM metadata can be decrypted.
NativeLibrary.reloadKeys()
- addGamesRecursive(games, FileUtil.listFiles(gamesUri), 3)
+ val badDirs = mutableListOf<Int>()
+ gameDirs.forEachIndexed { index: Int, gameDir: GameDir ->
+ val gameDirUri = Uri.parse(gameDir.uriString)
+ val isValid = FileUtil.isTreeUriValid(gameDirUri)
+ if (isValid) {
+ addGamesRecursive(
+ games,
+ FileUtil.listFiles(gameDirUri),
+ if (gameDir.deepScan) 3 else 1
+ )
+ } else {
+ badDirs.add(index)
+ }
+ }
+
+ // Remove all game dirs with insufficient permissions from config
+ if (badDirs.isNotEmpty()) {
+ var offset = 0
+ badDirs.forEach {
+ gameDirs.removeAt(it - offset)
+ offset++
+ }
+ }
+ NativeConfig.setGameDirs(gameDirs.toTypedArray())
// Cache list of games found on disk
val serializedGames = mutableSetOf<String>()
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/NativeConfig.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/NativeConfig.kt
index 87e579fa7..f4e1bb13f 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/NativeConfig.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/NativeConfig.kt
@@ -3,6 +3,8 @@
package org.yuzu.yuzu_emu.utils
+import org.yuzu.yuzu_emu.model.GameDir
+
object NativeConfig {
/**
* Creates a Config object and opens the emulation config.
@@ -54,4 +56,22 @@ object NativeConfig {
external fun getConfigHeader(category: Int): String
external fun getPairedSettingKey(key: String): String
+
+ /**
+ * Gets every [GameDir] in AndroidSettings::values.game_dirs
+ */
+ @Synchronized
+ external fun getGameDirs(): Array<GameDir>
+
+ /**
+ * Clears the AndroidSettings::values.game_dirs array and replaces them with the provided array
+ */
+ @Synchronized
+ external fun setGameDirs(dirs: Array<GameDir>)
+
+ /**
+ * Adds a single [GameDir] to the AndroidSettings::values.game_dirs array
+ */
+ @Synchronized
+ external fun addGameDir(dir: GameDir)
}