diff options
Diffstat (limited to 'src/android/app')
50 files changed, 1135 insertions, 1155 deletions
| diff --git a/src/android/app/build.gradle.kts b/src/android/app/build.gradle.kts index 021b070e0..5721327e7 100644 --- a/src/android/app/build.gradle.kts +++ b/src/android/app/build.gradle.kts @@ -219,7 +219,6 @@ dependencies {      implementation("io.coil-kt:coil:2.2.2")      implementation("androidx.core:core-splashscreen:1.0.1")      implementation("androidx.window:window:1.2.0-beta03") -    implementation("org.ini4j:ini4j:0.5.4")      implementation("androidx.constraintlayout:constraintlayout:2.1.4")      implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.1.0")      implementation("androidx.navigation:navigation-fragment-ktx:2.7.4") diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/NativeLibrary.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/NativeLibrary.kt index 9ebd6c732..e0f01127c 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/NativeLibrary.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/NativeLibrary.kt @@ -230,8 +230,6 @@ object NativeLibrary {       */      external fun onTouchReleased(finger_id: Int) -    external fun reloadSettings() -      external fun initGameIni(gameID: String?)      external fun setAppDirectory(directory: String) @@ -302,6 +300,11 @@ object NativeLibrary {      external fun getPerfStats(): DoubleArray      /** +     * Returns the current CPU backend. +     */ +    external fun getCpuBackend(): String + +    /**       * Notifies the core emulation that the orientation has changed.       */      external fun notifyOrientationChange(layout_option: Int, rotation: Int) 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/features/settings/model/IntSetting.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/IntSetting.kt index 151362124..ef10b209f 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/IntSetting.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/IntSetting.kt @@ -10,6 +10,7 @@ enum class IntSetting(      override val category: Settings.Category,      override val androidDefault: Int? = null  ) : AbstractIntSetting { +    CPU_BACKEND("cpu_backend", Settings.Category.Cpu),      CPU_ACCURACY("cpu_accuracy", Settings.Category.Cpu),      REGION_INDEX("region_index", Settings.Category.System),      LANGUAGE_INDEX("language_index", Settings.Category.System), diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/Settings.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/Settings.kt index 2bf0e1b0d..e3cd66185 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/Settings.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/Settings.kt @@ -3,33 +3,9 @@  package org.yuzu.yuzu_emu.features.settings.model -import android.text.TextUtils -import android.widget.Toast  import org.yuzu.yuzu_emu.R -import org.yuzu.yuzu_emu.YuzuApplication -import org.yuzu.yuzu_emu.features.settings.utils.SettingsFile  object Settings { -    private val context get() = YuzuApplication.appContext - -    fun saveSettings(gameId: String = "") { -        if (TextUtils.isEmpty(gameId)) { -            Toast.makeText( -                context, -                context.getString(R.string.ini_saved), -                Toast.LENGTH_SHORT -            ).show() -            SettingsFile.saveFile(SettingsFile.FILE_NAME_CONFIG) -        } else { -            // TODO: Save custom game settings -            Toast.makeText( -                context, -                context.getString(R.string.gameid_saved, gameId), -                Toast.LENGTH_SHORT -            ).show() -        } -    } -      enum class Category {          Android,          Audio, diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/SettingsItem.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/SettingsItem.kt index b3b3fc209..e198b18a0 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/SettingsItem.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/SettingsItem.kt @@ -73,12 +73,21 @@ abstract class SettingsItem(                      R.string.frame_limit_slider,                      R.string.frame_limit_slider_description,                      1, -                    200, +                    400,                      "%"                  )              )              put(                  SingleChoiceSetting( +                    IntSetting.CPU_BACKEND, +                    R.string.cpu_backend, +                    0, +                    R.array.cpuBackendArm64Names, +                    R.array.cpuBackendArm64Values +                ) +            ) +            put( +                SingleChoiceSetting(                      IntSetting.CPU_ACCURACY,                      R.string.cpu_accuracy,                      0, 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 c73edd50e..64bfc6dd0 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 @@ -19,13 +19,13 @@ import androidx.lifecycle.repeatOnLifecycle  import androidx.navigation.fragment.NavHostFragment  import androidx.navigation.navArgs  import com.google.android.material.color.MaterialColors +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers  import kotlinx.coroutines.flow.collectLatest  import kotlinx.coroutines.launch -import org.yuzu.yuzu_emu.NativeLibrary  import java.io.IOException  import org.yuzu.yuzu_emu.R  import org.yuzu.yuzu_emu.databinding.ActivitySettingsBinding -import org.yuzu.yuzu_emu.features.settings.model.Settings  import org.yuzu.yuzu_emu.features.settings.utils.SettingsFile  import org.yuzu.yuzu_emu.fragments.ResetSettingsDialogFragment  import org.yuzu.yuzu_emu.model.SettingsViewModel @@ -54,10 +54,6 @@ class SettingsActivity : AppCompatActivity() {          WindowCompat.setDecorFitsSystemWindows(window, false) -        if (savedInstanceState != null) { -            settingsViewModel.shouldSave = savedInstanceState.getBoolean(KEY_SHOULD_SAVE) -        } -          if (InsetsHelper.getSystemGestureType(applicationContext) !=              InsetsHelper.GESTURE_NAVIGATION          ) { @@ -128,12 +124,6 @@ class SettingsActivity : AppCompatActivity() {          }      } -    override fun onSaveInstanceState(outState: Bundle) { -        // Critical: If super method is not called, rotations will be busted. -        super.onSaveInstanceState(outState) -        outState.putBoolean(KEY_SHOULD_SAVE, settingsViewModel.shouldSave) -    } -      override fun onStart() {          super.onStart()          // TODO: Load custom settings contextually @@ -142,16 +132,10 @@ class SettingsActivity : AppCompatActivity() {          }      } -    /** -     * If this is called, the user has left the settings screen (potentially through the -     * home button) and will expect their changes to be persisted. So we kick off an -     * IntentService which will do so on a background thread. -     */      override fun onStop() {          super.onStop() -        if (isFinishing && settingsViewModel.shouldSave) { -            Log.debug("[SettingsActivity] Settings activity stopping. Saving settings to INI...") -            Settings.saveSettings() +        CoroutineScope(Dispatchers.IO).launch { +            NativeConfig.saveSettings()          }      } @@ -161,15 +145,13 @@ class SettingsActivity : AppCompatActivity() {      }      fun onSettingsReset() { -        // Prevents saving to a non-existent settings file -        settingsViewModel.shouldSave = false -          // Delete settings file because the user may have changed values that do not exist in the UI +        NativeConfig.unloadConfig()          val settingsFile = SettingsFile.getSettingsFile(SettingsFile.FILE_NAME_CONFIG)          if (!settingsFile.delete()) {              throw IOException("Failed to delete $settingsFile")          } -        NativeLibrary.reloadSettings() +        NativeConfig.initializeConfig()          Toast.makeText(              applicationContext, @@ -194,8 +176,4 @@ class SettingsActivity : AppCompatActivity() {              windowInsets          }      } - -    companion object { -        private const val KEY_SHOULD_SAVE = "should_save" -    }  } diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsAdapter.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsAdapter.kt index a7a029fc1..af2c1e582 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsAdapter.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsAdapter.kt @@ -105,7 +105,6 @@ class SettingsAdapter(      fun onBooleanClick(item: SwitchSetting, checked: Boolean) {          item.checked = checked          settingsViewModel.setShouldReloadSettingsList(true) -        settingsViewModel.shouldSave = true      }      fun onSingleChoiceClick(item: SingleChoiceSetting, position: Int) { @@ -161,7 +160,6 @@ class SettingsAdapter(              epochTime += timePicker.hour.toLong() * 60 * 60              epochTime += timePicker.minute.toLong() * 60              if (item.value != epochTime) { -                settingsViewModel.shouldSave = true                  notifyItemChanged(position)                  item.value = epochTime              } diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsFragmentPresenter.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsFragmentPresenter.kt index 8b71e32f3..7425728c6 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsFragmentPresenter.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsFragmentPresenter.kt @@ -269,6 +269,7 @@ class SettingsFragmentPresenter(              add(BooleanSetting.RENDERER_DEBUG.key)              add(HeaderSetting(R.string.cpu)) +            add(IntSetting.CPU_BACKEND.key)              add(IntSetting.CPU_ACCURACY.key)              add(BooleanSetting.CPU_DEBUG_MODE.key)              add(SettingsItem.FASTMEM_COMBINED) diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/utils/SettingsFile.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/utils/SettingsFile.kt index 2b04d666a..3ae5b4653 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/utils/SettingsFile.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/utils/SettingsFile.kt @@ -3,15 +3,8 @@  package org.yuzu.yuzu_emu.features.settings.utils -import android.widget.Toast  import java.io.* -import org.ini4j.Wini -import org.yuzu.yuzu_emu.R -import org.yuzu.yuzu_emu.YuzuApplication -import org.yuzu.yuzu_emu.features.settings.model.*  import org.yuzu.yuzu_emu.utils.DirectoryInitialization -import org.yuzu.yuzu_emu.utils.Log -import org.yuzu.yuzu_emu.utils.NativeConfig  /**   * Contains static methods for interacting with .ini files in which settings are stored. @@ -19,41 +12,6 @@ import org.yuzu.yuzu_emu.utils.NativeConfig  object SettingsFile {      const val FILE_NAME_CONFIG = "config" -    /** -     * Saves a Settings HashMap to a given .ini file on disk. If unsuccessful, outputs an error -     * telling why it failed. -     * -     * @param fileName The target filename without a path or extension. -     */ -    fun saveFile(fileName: String) { -        val ini = getSettingsFile(fileName) -        try { -            val wini = Wini(ini) -            for (specificCategory in Settings.Category.values()) { -                val categoryHeader = NativeConfig.getConfigHeader(specificCategory.ordinal) -                for (setting in Settings.settingsList) { -                    if (setting.key!!.isEmpty()) continue - -                    val settingCategoryHeader = -                        NativeConfig.getConfigHeader(setting.category.ordinal) -                    val iniSetting: String? = wini.get(categoryHeader, setting.key) -                    if (iniSetting != null || settingCategoryHeader == categoryHeader) { -                        wini.put(settingCategoryHeader, setting.key, setting.valueAsString) -                    } -                } -            } -            wini.store() -        } catch (e: IOException) { -            Log.error("[SettingsFile] File not found: " + fileName + ".ini: " + e.message) -            val context = YuzuApplication.appContext -            Toast.makeText( -                context, -                context.getString(R.string.error_saving, fileName, e.message), -                Toast.LENGTH_SHORT -            ).show() -        } -    } -      fun getSettingsFile(fileName: String): File =          File(DirectoryInitialization.userDirectory + "/config/" + fileName + ".ini")  } 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/EmulationFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/EmulationFragment.kt index c32fa0d7e..734c1d5ca 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/EmulationFragment.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/EmulationFragment.kt @@ -414,8 +414,10 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {              perfStatsUpdater = {                  if (emulationViewModel.emulationStarted.value) {                      val perfStats = NativeLibrary.getPerfStats() +                    val cpuBackend = NativeLibrary.getCpuBackend()                      if (_binding != null) { -                        binding.showFpsText.text = String.format("FPS: %.1f", perfStats[FPS]) +                        binding.showFpsText.text = +                            String.format("FPS: %.1f\n%s", perfStats[FPS], cpuBackend)                      }                      perfStatsUpdateHandler.postDelayed(perfStatsUpdater!!, 800)                  } 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/InstallableFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/InstallableFragment.kt index ec116ab62..6940fc757 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 @@ -21,6 +21,8 @@ import org.yuzu.yuzu_emu.databinding.FragmentInstallablesBinding  import org.yuzu.yuzu_emu.model.HomeViewModel  import org.yuzu.yuzu_emu.model.Installable  import org.yuzu.yuzu_emu.ui.main.MainActivity +import java.time.LocalDateTime +import java.time.format.DateTimeFormatter  class InstallableFragment : Fragment() {      private var _binding: FragmentInstallablesBinding? = null @@ -78,7 +80,15 @@ class InstallableFragment : Fragment() {                      R.string.manage_save_data,                      R.string.import_export_saves_description,                      install = { mainActivity.importSaves.launch(arrayOf("application/zip")) }, -                    export = { mainActivity.exportSave() } +                    export = { +                        mainActivity.exportSaves.launch( +                            "yuzu saves - ${ +                            LocalDateTime.now().format( +                                DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm") +                            ) +                            }.zip" +                        ) +                    }                  )              } else {                  Installable( diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/SettingsDialogFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/SettingsDialogFragment.kt index d18ec6974..b88d2c038 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/SettingsDialogFragment.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/SettingsDialogFragment.kt @@ -52,7 +52,6 @@ class SettingsDialogFragment : DialogFragment(), DialogInterface.OnClickListener                      .setPositiveButton(android.R.string.ok) { _: DialogInterface, _: Int ->                          settingsViewModel.clickedItem!!.setting.reset()                          settingsViewModel.setAdapterItemChanged(position) -                        settingsViewModel.shouldSave = true                      }                      .setNegativeButton(android.R.string.cancel, null)                      .create() @@ -137,24 +136,17 @@ class SettingsDialogFragment : DialogFragment(), DialogInterface.OnClickListener              is SingleChoiceSetting -> {                  val scSetting = settingsViewModel.clickedItem as SingleChoiceSetting                  val value = getValueForSingleChoiceSelection(scSetting, which) -                if (scSetting.selectedValue != value) { -                    settingsViewModel.shouldSave = true -                }                  scSetting.selectedValue = value              }              is StringSingleChoiceSetting -> {                  val scSetting = settingsViewModel.clickedItem as StringSingleChoiceSetting                  val value = scSetting.getValueAt(which) -                if (scSetting.selectedValue != value) settingsViewModel.shouldSave = true                  scSetting.selectedValue = value              }              is SliderSetting -> {                  val sliderSetting = settingsViewModel.clickedItem as SliderSetting -                if (sliderSetting.selectedValue != settingsViewModel.sliderProgress.value) { -                    settingsViewModel.shouldSave = true -                }                  sliderSetting.selectedValue = settingsViewModel.sliderProgress.value              }          } 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/Game.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/Game.kt index de84b2adb..2fa3ab31b 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/Game.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/Game.kt @@ -18,8 +18,8 @@ class Game(      val version: String = "",      val isHomebrew: Boolean = false  ) : Parcelable { -    val keyAddedToLibraryTime get() = "${programId}_AddedToLibraryTime" -    val keyLastPlayedTime get() = "${programId}_LastPlayed" +    val keyAddedToLibraryTime get() = "${path}_AddedToLibraryTime" +    val keyLastPlayedTime get() = "${path}_LastPlayed"      override fun equals(other: Any?): Boolean {          if (other !is Game) { 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/model/SettingsViewModel.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/SettingsViewModel.kt index 6f947674e..ccc981e95 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/SettingsViewModel.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/SettingsViewModel.kt @@ -13,8 +13,6 @@ import org.yuzu.yuzu_emu.features.settings.model.view.SettingsItem  class SettingsViewModel : ViewModel() {      var game: Game? = null -    var shouldSave = false -      var clickedItem: SettingsItem? = null      val shouldRecreate: StateFlow<Boolean> get() = _shouldRecreate @@ -73,6 +71,5 @@ class SettingsViewModel : ViewModel() {      fun clear() {          game = null -        shouldSave = false      }  } 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 211b7cf69..16323a316 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 @@ -6,7 +6,6 @@ package org.yuzu.yuzu_emu.ui.main  import android.content.Intent  import android.net.Uri  import android.os.Bundle -import android.provider.DocumentsContract  import android.view.View  import android.view.ViewGroup.MarginLayoutParams  import android.view.WindowManager @@ -20,7 +19,6 @@ import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen  import androidx.core.view.ViewCompat  import androidx.core.view.WindowCompat  import androidx.core.view.WindowInsetsCompat -import androidx.documentfile.provider.DocumentFile  import androidx.lifecycle.Lifecycle  import androidx.lifecycle.lifecycleScope  import androidx.lifecycle.repeatOnLifecycle @@ -41,8 +39,8 @@ 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.features.DocumentProvider  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 @@ -53,9 +51,6 @@ import org.yuzu.yuzu_emu.model.TaskViewModel  import org.yuzu.yuzu_emu.utils.*  import java.io.BufferedInputStream  import java.io.BufferedOutputStream -import java.io.FileOutputStream -import java.time.LocalDateTime -import java.time.format.DateTimeFormatter  import java.util.zip.ZipEntry  import java.util.zip.ZipInputStream @@ -73,7 +68,6 @@ class MainActivity : AppCompatActivity(), ThemeProvider {      // Get first subfolder in saves folder (should be the user folder)      val savesFolderRoot get() = File(savesFolder).listFiles()?.firstOrNull()?.canonicalPath ?: "" -    private var lastZipCreated: File? = null      override fun onCreate(savedInstanceState: Bundle?) {          val splashScreen = installSplashScreen() @@ -259,6 +253,13 @@ class MainActivity : AppCompatActivity(), ThemeProvider {          super.onResume()      } +    override fun onStop() { +        super.onStop() +        CoroutineScope(Dispatchers.IO).launch { +            NativeConfig.saveSettings() +        } +    } +      override fun onDestroy() {          EmulationActivity.stopForegroundService(this)          super.onDestroy() @@ -300,20 +301,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 = @@ -632,6 +632,7 @@ class MainActivity : AppCompatActivity(), ThemeProvider {                  }                  // Clear existing user data +                NativeConfig.unloadConfig()                  File(DirectoryInitialization.userDirectory!!).deleteRecursively()                  // Copy archive to internal storage @@ -650,6 +651,7 @@ class MainActivity : AppCompatActivity(), ThemeProvider {                  // Reinitialize relevant data                  NativeLibrary.initializeSystem(true) +                NativeConfig.initializeConfig()                  gamesViewModel.reloadGames(false)                  return@newInstance getString(R.string.user_data_import_success) @@ -657,74 +659,30 @@ class MainActivity : AppCompatActivity(), ThemeProvider {          }      /** -     * Zips the save files located in the given folder path and creates a new zip file with the current date and time. -     * @return true if the zip file is successfully created, false otherwise. -     */ -    private fun zipSave(): Boolean { -        try { -            val tempFolder = File(getPublicFilesDir().canonicalPath, "temp") -            tempFolder.mkdirs() -            val saveFolder = File(savesFolderRoot) -            val outputZipFile = File( -                tempFolder, -                "yuzu saves - ${ -                LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm")) -                }.zip" -            ) -            outputZipFile.createNewFile() -            val result = FileUtil.zipFromInternalStorage( -                saveFolder, -                savesFolderRoot, -                BufferedOutputStream(FileOutputStream(outputZipFile)) -            ) -            if (result == TaskState.Failed) { -                return false -            } -            lastZipCreated = outputZipFile -        } catch (e: Exception) { -            return false -        } -        return true -    } - -    /**       * Exports the save file located in the given folder path by creating a zip file and sharing it via intent.       */ -    fun exportSave() { -        CoroutineScope(Dispatchers.IO).launch { -            val wasZipCreated = zipSave() -            val lastZipFile = lastZipCreated -            if (!wasZipCreated || lastZipFile == null) { -                withContext(Dispatchers.Main) { -                    Toast.makeText( -                        this@MainActivity, -                        getString(R.string.export_save_failed), -                        Toast.LENGTH_LONG -                    ).show() -                } -                return@launch -            } +    val exportSaves = registerForActivityResult( +        ActivityResultContracts.CreateDocument("application/zip") +    ) { result -> +        if (result == null) { +            return@registerForActivityResult +        } -            withContext(Dispatchers.Main) { -                val file = DocumentFile.fromSingleUri( -                    this@MainActivity, -                    DocumentsContract.buildDocumentUri( -                        DocumentProvider.AUTHORITY, -                        "${DocumentProvider.ROOT_ID}/temp/${lastZipFile.name}" -                    ) -                )!! -                val intent = Intent(Intent.ACTION_SEND) -                    .setDataAndType(file.uri, "application/zip") -                    .addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) -                    .putExtra(Intent.EXTRA_STREAM, file.uri) -                startForResultExportSave.launch( -                    Intent.createChooser( -                        intent, -                        getString(R.string.share_save_file) -                    ) -                ) +        IndeterminateProgressDialogFragment.newInstance( +            this, +            R.string.save_files_exporting, +            false +        ) { +            val zipResult = FileUtil.zipFromInternalStorage( +                File(savesFolderRoot), +                savesFolderRoot, +                BufferedOutputStream(contentResolver.openOutputStream(result)) +            ) +            return@newInstance when (zipResult) { +                TaskState.Completed -> getString(R.string.export_success) +                TaskState.Cancelled, TaskState.Failed -> getString(R.string.export_failed)              } -        } +        }.show(supportFragmentManager, IndeterminateProgressDialogFragment.TAG)      }      private val startForResultExportSave = diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/DirectoryInitialization.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/DirectoryInitialization.kt index 5e9a1176a..21270fc84 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/DirectoryInitialization.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/DirectoryInitialization.kt @@ -16,6 +16,7 @@ object DirectoryInitialization {          if (!areDirectoriesReady) {              initializeInternalStorage()              NativeLibrary.initializeSystem(false) +            NativeConfig.initializeConfig()              areDirectoriesReady = true          }      } 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/InputHandler.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/InputHandler.kt index 47bde5081..e63382e1d 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/InputHandler.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/InputHandler.kt @@ -27,6 +27,8 @@ object InputHandler {              0x054C -> getInputDS5ButtonKey(event.keyCode)              0x057E -> getInputJoyconButtonKey(event.keyCode)              0x1532 -> getInputRazerButtonKey(event.keyCode) +            0x3537 -> getInputRedmagicButtonKey(event.keyCode) +            0x358A -> getInputBackboneLabsButtonKey(event.keyCode)              else -> getInputGenericButtonKey(event.keyCode)          } @@ -227,6 +229,42 @@ object InputHandler {          }      } +    private fun getInputRedmagicButtonKey(key: Int): Int { +        return when (key) { +            KeyEvent.KEYCODE_BUTTON_A -> NativeLibrary.ButtonType.BUTTON_B +            KeyEvent.KEYCODE_BUTTON_B -> NativeLibrary.ButtonType.BUTTON_A +            KeyEvent.KEYCODE_BUTTON_X -> NativeLibrary.ButtonType.BUTTON_Y +            KeyEvent.KEYCODE_BUTTON_Y -> NativeLibrary.ButtonType.BUTTON_X +            KeyEvent.KEYCODE_BUTTON_L1 -> NativeLibrary.ButtonType.TRIGGER_L +            KeyEvent.KEYCODE_BUTTON_R1 -> NativeLibrary.ButtonType.TRIGGER_R +            KeyEvent.KEYCODE_BUTTON_L2 -> NativeLibrary.ButtonType.TRIGGER_ZL +            KeyEvent.KEYCODE_BUTTON_R2 -> NativeLibrary.ButtonType.TRIGGER_ZR +            KeyEvent.KEYCODE_BUTTON_THUMBL -> NativeLibrary.ButtonType.STICK_L +            KeyEvent.KEYCODE_BUTTON_THUMBR -> NativeLibrary.ButtonType.STICK_R +            KeyEvent.KEYCODE_BUTTON_START -> NativeLibrary.ButtonType.BUTTON_PLUS +            KeyEvent.KEYCODE_BUTTON_SELECT -> NativeLibrary.ButtonType.BUTTON_MINUS +            else -> -1 +        } +    } + +    private fun getInputBackboneLabsButtonKey(key: Int): Int { +        return when (key) { +            KeyEvent.KEYCODE_BUTTON_A -> NativeLibrary.ButtonType.BUTTON_B +            KeyEvent.KEYCODE_BUTTON_B -> NativeLibrary.ButtonType.BUTTON_A +            KeyEvent.KEYCODE_BUTTON_X -> NativeLibrary.ButtonType.BUTTON_Y +            KeyEvent.KEYCODE_BUTTON_Y -> NativeLibrary.ButtonType.BUTTON_X +            KeyEvent.KEYCODE_BUTTON_L1 -> NativeLibrary.ButtonType.TRIGGER_L +            KeyEvent.KEYCODE_BUTTON_R1 -> NativeLibrary.ButtonType.TRIGGER_R +            KeyEvent.KEYCODE_BUTTON_L2 -> NativeLibrary.ButtonType.TRIGGER_ZL +            KeyEvent.KEYCODE_BUTTON_R2 -> NativeLibrary.ButtonType.TRIGGER_ZR +            KeyEvent.KEYCODE_BUTTON_THUMBL -> NativeLibrary.ButtonType.STICK_L +            KeyEvent.KEYCODE_BUTTON_THUMBR -> NativeLibrary.ButtonType.STICK_R +            KeyEvent.KEYCODE_BUTTON_START -> NativeLibrary.ButtonType.BUTTON_PLUS +            KeyEvent.KEYCODE_BUTTON_SELECT -> NativeLibrary.ButtonType.BUTTON_MINUS +            else -> -1 +        } +    } +      private fun getInputGenericButtonKey(key: Int): Int {          return when (key) {              KeyEvent.KEYCODE_BUTTON_A -> NativeLibrary.ButtonType.BUTTON_A 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 9425f8b99..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,7 +3,33 @@  package org.yuzu.yuzu_emu.utils +import org.yuzu.yuzu_emu.model.GameDir +  object NativeConfig { +    /** +     * Creates a Config object and opens the emulation config. +     */ +    @Synchronized +    external fun initializeConfig() + +    /** +     * Destroys the stored config object. This automatically saves the existing config. +     */ +    @Synchronized +    external fun unloadConfig() + +    /** +     * Reads values saved to the config file and saves them. +     */ +    @Synchronized +    external fun reloadSettings() + +    /** +     * Saves settings values in memory to disk. +     */ +    @Synchronized +    external fun saveSettings() +      external fun getBoolean(key: String, getDefault: Boolean): Boolean      external fun setBoolean(key: String, value: Boolean) @@ -30,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)  } diff --git a/src/android/app/src/main/jni/CMakeLists.txt b/src/android/app/src/main/jni/CMakeLists.txt index 88a570f68..2acc93da8 100644 --- a/src/android/app/src/main/jni/CMakeLists.txt +++ b/src/android/app/src/main/jni/CMakeLists.txt @@ -6,9 +6,6 @@ add_library(yuzu-android SHARED      android_common/android_common.h      applets/software_keyboard.cpp      applets/software_keyboard.h -    config.cpp -    config.h -    default_ini.h      emu_window/emu_window.cpp      emu_window/emu_window.h      id_cache.cpp @@ -16,15 +13,17 @@ add_library(yuzu-android SHARED      native.cpp      native.h      native_config.cpp -    uisettings.cpp +    android_settings.cpp      game_metadata.cpp      native_log.cpp +    android_config.cpp +    android_config.h  )  set_property(TARGET yuzu-android PROPERTY IMPORTED_LOCATION ${FFmpeg_LIBRARY_DIR}) -target_link_libraries(yuzu-android PRIVATE audio_core common core input_common) -target_link_libraries(yuzu-android PRIVATE android camera2ndk EGL glad inih jnigraphics log) +target_link_libraries(yuzu-android PRIVATE audio_core common core input_common frontend_common) +target_link_libraries(yuzu-android PRIVATE android camera2ndk EGL glad jnigraphics log)  if (ARCHITECTURE_arm64)      target_link_libraries(yuzu-android PRIVATE adrenotools)  endif() diff --git a/src/android/app/src/main/jni/android_config.cpp b/src/android/app/src/main/jni/android_config.cpp new file mode 100644 index 000000000..767d8ea83 --- /dev/null +++ b/src/android/app/src/main/jni/android_config.cpp @@ -0,0 +1,120 @@ +// SPDX-FileCopyrightText: 2023 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "android_config.h" +#include "android_settings.h" +#include "common/settings_setting.h" + +AndroidConfig::AndroidConfig(const std::string& config_name, ConfigType config_type) +    : Config(config_type) { +    Initialize(config_name); +    if (config_type != ConfigType::InputProfile) { +        ReadAndroidValues(); +        SaveAndroidValues(); +    } +} + +AndroidConfig::~AndroidConfig() { +    if (global) { +        AndroidConfig::SaveAllValues(); +    } +} + +void AndroidConfig::ReloadAllValues() { +    Reload(); +    ReadAndroidValues(); +    SaveAndroidValues(); +} + +void AndroidConfig::SaveAllValues() { +    Save(); +    SaveAndroidValues(); +} + +void AndroidConfig::ReadAndroidValues() { +    if (global) { +        ReadAndroidUIValues(); +        ReadUIValues(); +    } +} + +void AndroidConfig::ReadAndroidUIValues() { +    BeginGroup(Settings::TranslateCategory(Settings::Category::Android)); + +    ReadCategory(Settings::Category::Android); + +    EndGroup(); +} + +void AndroidConfig::ReadUIValues() { +    BeginGroup(Settings::TranslateCategory(Settings::Category::Ui)); + +    ReadPathValues(); + +    EndGroup(); +} + +void AndroidConfig::ReadPathValues() { +    BeginGroup(Settings::TranslateCategory(Settings::Category::Paths)); + +    const int gamedirs_size = BeginArray(std::string("gamedirs")); +    for (int i = 0; i < gamedirs_size; ++i) { +        SetArrayIndex(i); +        AndroidSettings::GameDir game_dir; +        game_dir.path = ReadStringSetting(std::string("path")); +        game_dir.deep_scan = +            ReadBooleanSetting(std::string("deep_scan"), std::make_optional(false)); +        AndroidSettings::values.game_dirs.push_back(game_dir); +    } +    EndArray(); + +    EndGroup(); +} + +void AndroidConfig::SaveAndroidValues() { +    if (global) { +        SaveAndroidUIValues(); +        SaveUIValues(); +    } + +    WriteToIni(); +} + +void AndroidConfig::SaveAndroidUIValues() { +    BeginGroup(Settings::TranslateCategory(Settings::Category::Android)); + +    WriteCategory(Settings::Category::Android); + +    EndGroup(); +} + +void AndroidConfig::SaveUIValues() { +    BeginGroup(Settings::TranslateCategory(Settings::Category::Ui)); + +    SavePathValues(); + +    EndGroup(); +} + +void AndroidConfig::SavePathValues() { +    BeginGroup(Settings::TranslateCategory(Settings::Category::Paths)); + +    BeginArray(std::string("gamedirs")); +    for (size_t i = 0; i < AndroidSettings::values.game_dirs.size(); ++i) { +        SetArrayIndex(i); +        const auto& game_dir = AndroidSettings::values.game_dirs[i]; +        WriteSetting(std::string("path"), game_dir.path); +        WriteSetting(std::string("deep_scan"), game_dir.deep_scan, std::make_optional(false)); +    } +    EndArray(); + +    EndGroup(); +} + +std::vector<Settings::BasicSetting*>& AndroidConfig::FindRelevantList(Settings::Category category) { +    auto& map = Settings::values.linkage.by_category; +    if (map.contains(category)) { +        return Settings::values.linkage.by_category[category]; +    } +    return AndroidSettings::values.linkage.by_category[category]; +} diff --git a/src/android/app/src/main/jni/android_config.h b/src/android/app/src/main/jni/android_config.h new file mode 100644 index 000000000..f490be016 --- /dev/null +++ b/src/android/app/src/main/jni/android_config.h @@ -0,0 +1,41 @@ +// SPDX-FileCopyrightText: 2023 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include "frontend_common/config.h" + +class AndroidConfig final : public Config { +public: +    explicit AndroidConfig(const std::string& config_name = "config", +                           ConfigType config_type = ConfigType::GlobalConfig); +    ~AndroidConfig() override; + +    void ReloadAllValues() override; +    void SaveAllValues() override; + +protected: +    void ReadAndroidValues(); +    void ReadAndroidUIValues(); +    void ReadHidbusValues() override {} +    void ReadDebugControlValues() override {} +    void ReadPathValues() override; +    void ReadShortcutValues() override {} +    void ReadUIValues() override; +    void ReadUIGamelistValues() override {} +    void ReadUILayoutValues() override {} +    void ReadMultiplayerValues() override {} + +    void SaveAndroidValues(); +    void SaveAndroidUIValues(); +    void SaveHidbusValues() override {} +    void SaveDebugControlValues() override {} +    void SavePathValues() override; +    void SaveShortcutValues() override {} +    void SaveUIValues() override; +    void SaveUIGamelistValues() override {} +    void SaveUILayoutValues() override {} +    void SaveMultiplayerValues() override {} + +    std::vector<Settings::BasicSetting*>& FindRelevantList(Settings::Category category) override; +}; diff --git a/src/android/app/src/main/jni/uisettings.cpp b/src/android/app/src/main/jni/android_settings.cpp index f2f0bad50..16023a6b0 100644 --- a/src/android/app/src/main/jni/uisettings.cpp +++ b/src/android/app/src/main/jni/android_settings.cpp @@ -1,7 +1,7 @@  // SPDX-FileCopyrightText: 2023 yuzu Emulator Project  // SPDX-License-Identifier: GPL-2.0-or-later -#include "uisettings.h" +#include "android_settings.h"  namespace AndroidSettings { diff --git a/src/android/app/src/main/jni/uisettings.h b/src/android/app/src/main/jni/android_settings.h index 37bc33918..fc0523206 100644 --- a/src/android/app/src/main/jni/uisettings.h +++ b/src/android/app/src/main/jni/android_settings.h @@ -9,9 +9,17 @@  namespace AndroidSettings { +struct GameDir { +    std::string path; +    bool deep_scan = false; +}; +  struct Values {      Settings::Linkage linkage; +    // Path settings +    std::vector<GameDir> game_dirs; +      // Android      Settings::Setting<bool> picture_in_picture{linkage, false, "picture_in_picture",                                                 Settings::Category::Android}; diff --git a/src/android/app/src/main/jni/config.cpp b/src/android/app/src/main/jni/config.cpp deleted file mode 100644 index 81120ab0f..000000000 --- a/src/android/app/src/main/jni/config.cpp +++ /dev/null @@ -1,330 +0,0 @@ -// SPDX-FileCopyrightText: 2023 yuzu Emulator Project -// SPDX-License-Identifier: GPL-2.0-or-later - -#include <memory> -#include <optional> -#include <sstream> - -#include <INIReader.h> -#include "common/fs/file.h" -#include "common/fs/fs.h" -#include "common/fs/path_util.h" -#include "common/logging/log.h" -#include "common/settings.h" -#include "common/settings_enums.h" -#include "core/hle/service/acc/profile_manager.h" -#include "input_common/main.h" -#include "jni/config.h" -#include "jni/default_ini.h" -#include "uisettings.h" - -namespace FS = Common::FS; - -Config::Config(const std::string& config_name, ConfigType config_type) -    : type(config_type), global{config_type == ConfigType::GlobalConfig} { -    Initialize(config_name); -} - -Config::~Config() = default; - -bool Config::LoadINI(const std::string& default_contents, bool retry) { -    void(FS::CreateParentDir(config_loc)); -    config = std::make_unique<INIReader>(FS::PathToUTF8String(config_loc)); -    const auto config_loc_str = FS::PathToUTF8String(config_loc); -    if (config->ParseError() < 0) { -        if (retry) { -            LOG_WARNING(Config, "Failed to load {}. Creating file from defaults...", -                        config_loc_str); - -            void(FS::CreateParentDir(config_loc)); -            void(FS::WriteStringToFile(config_loc, FS::FileType::TextFile, default_contents)); - -            config = std::make_unique<INIReader>(config_loc_str); - -            return LoadINI(default_contents, false); -        } -        LOG_ERROR(Config, "Failed."); -        return false; -    } -    LOG_INFO(Config, "Successfully loaded {}", config_loc_str); -    return true; -} - -template <> -void Config::ReadSetting(const std::string& group, Settings::Setting<std::string>& setting) { -    std::string setting_value = config->Get(group, setting.GetLabel(), setting.GetDefault()); -    if (setting_value.empty()) { -        setting_value = setting.GetDefault(); -    } -    setting = std::move(setting_value); -} - -template <> -void Config::ReadSetting(const std::string& group, Settings::Setting<bool>& setting) { -    setting = config->GetBoolean(group, setting.GetLabel(), setting.GetDefault()); -} - -template <typename Type, bool ranged> -void Config::ReadSetting(const std::string& group, Settings::Setting<Type, ranged>& setting) { -    setting = static_cast<Type>( -        config->GetInteger(group, setting.GetLabel(), static_cast<long>(setting.GetDefault()))); -} - -void Config::ReadValues() { -    ReadSetting("ControlsGeneral", Settings::values.mouse_enabled); -    ReadSetting("ControlsGeneral", Settings::values.touch_device); -    ReadSetting("ControlsGeneral", Settings::values.keyboard_enabled); -    ReadSetting("ControlsGeneral", Settings::values.debug_pad_enabled); -    ReadSetting("ControlsGeneral", Settings::values.vibration_enabled); -    ReadSetting("ControlsGeneral", Settings::values.enable_accurate_vibrations); -    ReadSetting("ControlsGeneral", Settings::values.motion_enabled); -    Settings::values.touchscreen.enabled = -        config->GetBoolean("ControlsGeneral", "touch_enabled", true); -    Settings::values.touchscreen.rotation_angle = -        config->GetInteger("ControlsGeneral", "touch_angle", 0); -    Settings::values.touchscreen.diameter_x = -        config->GetInteger("ControlsGeneral", "touch_diameter_x", 15); -    Settings::values.touchscreen.diameter_y = -        config->GetInteger("ControlsGeneral", "touch_diameter_y", 15); - -    int num_touch_from_button_maps = -        config->GetInteger("ControlsGeneral", "touch_from_button_map", 0); -    if (num_touch_from_button_maps > 0) { -        for (int i = 0; i < num_touch_from_button_maps; ++i) { -            Settings::TouchFromButtonMap map; -            map.name = config->Get("ControlsGeneral", -                                   std::string("touch_from_button_maps_") + std::to_string(i) + -                                       std::string("_name"), -                                   "default"); -            const int num_touch_maps = config->GetInteger( -                "ControlsGeneral", -                std::string("touch_from_button_maps_") + std::to_string(i) + std::string("_count"), -                0); -            map.buttons.reserve(num_touch_maps); - -            for (int j = 0; j < num_touch_maps; ++j) { -                std::string touch_mapping = -                    config->Get("ControlsGeneral", -                                std::string("touch_from_button_maps_") + std::to_string(i) + -                                    std::string("_bind_") + std::to_string(j), -                                ""); -                map.buttons.emplace_back(std::move(touch_mapping)); -            } - -            Settings::values.touch_from_button_maps.emplace_back(std::move(map)); -        } -    } else { -        Settings::values.touch_from_button_maps.emplace_back( -            Settings::TouchFromButtonMap{"default", {}}); -        num_touch_from_button_maps = 1; -    } -    Settings::values.touch_from_button_map_index = std::clamp( -        Settings::values.touch_from_button_map_index.GetValue(), 0, num_touch_from_button_maps - 1); - -    ReadSetting("ControlsGeneral", Settings::values.udp_input_servers); - -    // Data Storage -    ReadSetting("Data Storage", Settings::values.use_virtual_sd); -    FS::SetYuzuPath(FS::YuzuPath::NANDDir, -                    config->Get("Data Storage", "nand_directory", -                                FS::GetYuzuPathString(FS::YuzuPath::NANDDir))); -    FS::SetYuzuPath(FS::YuzuPath::SDMCDir, -                    config->Get("Data Storage", "sdmc_directory", -                                FS::GetYuzuPathString(FS::YuzuPath::SDMCDir))); -    FS::SetYuzuPath(FS::YuzuPath::LoadDir, -                    config->Get("Data Storage", "load_directory", -                                FS::GetYuzuPathString(FS::YuzuPath::LoadDir))); -    FS::SetYuzuPath(FS::YuzuPath::DumpDir, -                    config->Get("Data Storage", "dump_directory", -                                FS::GetYuzuPathString(FS::YuzuPath::DumpDir))); -    ReadSetting("Data Storage", Settings::values.gamecard_inserted); -    ReadSetting("Data Storage", Settings::values.gamecard_current_game); -    ReadSetting("Data Storage", Settings::values.gamecard_path); - -    // System -    ReadSetting("System", Settings::values.current_user); -    Settings::values.current_user = std::clamp<int>(Settings::values.current_user.GetValue(), 0, -                                                    Service::Account::MAX_USERS - 1); - -    // Disable docked mode by default on Android -    Settings::values.use_docked_mode.SetValue(config->GetBoolean("System", "use_docked_mode", false) -                                                  ? Settings::ConsoleMode::Docked -                                                  : Settings::ConsoleMode::Handheld); - -    const auto rng_seed_enabled = config->GetBoolean("System", "rng_seed_enabled", false); -    if (rng_seed_enabled) { -        Settings::values.rng_seed.SetValue(config->GetInteger("System", "rng_seed", 0)); -    } else { -        Settings::values.rng_seed.SetValue(0); -    } -    Settings::values.rng_seed_enabled.SetValue(rng_seed_enabled); - -    const auto custom_rtc_enabled = config->GetBoolean("System", "custom_rtc_enabled", false); -    if (custom_rtc_enabled) { -        Settings::values.custom_rtc = config->GetInteger("System", "custom_rtc", 0); -    } else { -        Settings::values.custom_rtc = 0; -    } -    Settings::values.custom_rtc_enabled = custom_rtc_enabled; - -    ReadSetting("System", Settings::values.language_index); -    ReadSetting("System", Settings::values.region_index); -    ReadSetting("System", Settings::values.time_zone_index); -    ReadSetting("System", Settings::values.sound_index); - -    // Core -    ReadSetting("Core", Settings::values.use_multi_core); -    ReadSetting("Core", Settings::values.memory_layout_mode); - -    // Cpu -    ReadSetting("Cpu", Settings::values.cpu_accuracy); -    ReadSetting("Cpu", Settings::values.cpu_debug_mode); -    ReadSetting("Cpu", Settings::values.cpuopt_page_tables); -    ReadSetting("Cpu", Settings::values.cpuopt_block_linking); -    ReadSetting("Cpu", Settings::values.cpuopt_return_stack_buffer); -    ReadSetting("Cpu", Settings::values.cpuopt_fast_dispatcher); -    ReadSetting("Cpu", Settings::values.cpuopt_context_elimination); -    ReadSetting("Cpu", Settings::values.cpuopt_const_prop); -    ReadSetting("Cpu", Settings::values.cpuopt_misc_ir); -    ReadSetting("Cpu", Settings::values.cpuopt_reduce_misalign_checks); -    ReadSetting("Cpu", Settings::values.cpuopt_fastmem); -    ReadSetting("Cpu", Settings::values.cpuopt_fastmem_exclusives); -    ReadSetting("Cpu", Settings::values.cpuopt_recompile_exclusives); -    ReadSetting("Cpu", Settings::values.cpuopt_ignore_memory_aborts); -    ReadSetting("Cpu", Settings::values.cpuopt_unsafe_unfuse_fma); -    ReadSetting("Cpu", Settings::values.cpuopt_unsafe_reduce_fp_error); -    ReadSetting("Cpu", Settings::values.cpuopt_unsafe_ignore_standard_fpcr); -    ReadSetting("Cpu", Settings::values.cpuopt_unsafe_inaccurate_nan); -    ReadSetting("Cpu", Settings::values.cpuopt_unsafe_fastmem_check); -    ReadSetting("Cpu", Settings::values.cpuopt_unsafe_ignore_global_monitor); - -    // Renderer -    ReadSetting("Renderer", Settings::values.renderer_backend); -    ReadSetting("Renderer", Settings::values.renderer_debug); -    ReadSetting("Renderer", Settings::values.renderer_shader_feedback); -    ReadSetting("Renderer", Settings::values.enable_nsight_aftermath); -    ReadSetting("Renderer", Settings::values.disable_shader_loop_safety_checks); -    ReadSetting("Renderer", Settings::values.vulkan_device); - -    ReadSetting("Renderer", Settings::values.resolution_setup); -    ReadSetting("Renderer", Settings::values.scaling_filter); -    ReadSetting("Renderer", Settings::values.fsr_sharpening_slider); -    ReadSetting("Renderer", Settings::values.anti_aliasing); -    ReadSetting("Renderer", Settings::values.fullscreen_mode); -    ReadSetting("Renderer", Settings::values.aspect_ratio); -    ReadSetting("Renderer", Settings::values.max_anisotropy); -    ReadSetting("Renderer", Settings::values.use_speed_limit); -    ReadSetting("Renderer", Settings::values.speed_limit); -    ReadSetting("Renderer", Settings::values.use_disk_shader_cache); -    ReadSetting("Renderer", Settings::values.use_asynchronous_gpu_emulation); -    ReadSetting("Renderer", Settings::values.vsync_mode); -    ReadSetting("Renderer", Settings::values.shader_backend); -    ReadSetting("Renderer", Settings::values.use_asynchronous_shaders); -    ReadSetting("Renderer", Settings::values.nvdec_emulation); -    ReadSetting("Renderer", Settings::values.use_fast_gpu_time); -    ReadSetting("Renderer", Settings::values.use_vulkan_driver_pipeline_cache); - -    ReadSetting("Renderer", Settings::values.bg_red); -    ReadSetting("Renderer", Settings::values.bg_green); -    ReadSetting("Renderer", Settings::values.bg_blue); - -    // Use GPU accuracy normal by default on Android -    Settings::values.gpu_accuracy = static_cast<Settings::GpuAccuracy>(config->GetInteger( -        "Renderer", "gpu_accuracy", static_cast<u32>(Settings::GpuAccuracy::Normal))); - -    // Use GPU default anisotropic filtering on Android -    Settings::values.max_anisotropy = -        static_cast<Settings::AnisotropyMode>(config->GetInteger("Renderer", "max_anisotropy", 1)); - -    // Disable ASTC compute by default on Android -    Settings::values.accelerate_astc.SetValue( -        config->GetBoolean("Renderer", "accelerate_astc", false) ? Settings::AstcDecodeMode::Gpu -                                                                 : Settings::AstcDecodeMode::Cpu); - -    // Enable asynchronous presentation by default on Android -    Settings::values.async_presentation = -        config->GetBoolean("Renderer", "async_presentation", true); - -    // Disable force_max_clock by default on Android -    Settings::values.renderer_force_max_clock = -        config->GetBoolean("Renderer", "force_max_clock", false); - -    // Disable use_reactive_flushing by default on Android -    Settings::values.use_reactive_flushing = -        config->GetBoolean("Renderer", "use_reactive_flushing", false); - -    // Audio -    ReadSetting("Audio", Settings::values.sink_id); -    ReadSetting("Audio", Settings::values.audio_output_device_id); -    ReadSetting("Audio", Settings::values.volume); - -    // Miscellaneous -    // log_filter has a different default here than from common -    Settings::values.log_filter = "*:Info"; -    ReadSetting("Miscellaneous", Settings::values.use_dev_keys); - -    // Debugging -    Settings::values.record_frame_times = -        config->GetBoolean("Debugging", "record_frame_times", false); -    ReadSetting("Debugging", Settings::values.dump_exefs); -    ReadSetting("Debugging", Settings::values.dump_nso); -    ReadSetting("Debugging", Settings::values.enable_fs_access_log); -    ReadSetting("Debugging", Settings::values.reporting_services); -    ReadSetting("Debugging", Settings::values.quest_flag); -    ReadSetting("Debugging", Settings::values.use_debug_asserts); -    ReadSetting("Debugging", Settings::values.use_auto_stub); -    ReadSetting("Debugging", Settings::values.disable_macro_jit); -    ReadSetting("Debugging", Settings::values.disable_macro_hle); -    ReadSetting("Debugging", Settings::values.use_gdbstub); -    ReadSetting("Debugging", Settings::values.gdbstub_port); - -    const auto title_list = config->Get("AddOns", "title_ids", ""); -    std::stringstream ss(title_list); -    std::string line; -    while (std::getline(ss, line, '|')) { -        const auto title_id = std::strtoul(line.c_str(), nullptr, 16); -        const auto disabled_list = config->Get("AddOns", "disabled_" + line, ""); - -        std::stringstream inner_ss(disabled_list); -        std::string inner_line; -        std::vector<std::string> out; -        while (std::getline(inner_ss, inner_line, '|')) { -            out.push_back(inner_line); -        } - -        Settings::values.disabled_addons.insert_or_assign(title_id, out); -    } - -    // Web Service -    ReadSetting("WebService", Settings::values.enable_telemetry); -    ReadSetting("WebService", Settings::values.web_api_url); -    ReadSetting("WebService", Settings::values.yuzu_username); -    ReadSetting("WebService", Settings::values.yuzu_token); - -    // Network -    ReadSetting("Network", Settings::values.network_interface); - -    // Android -    ReadSetting("Android", AndroidSettings::values.picture_in_picture); -    ReadSetting("Android", AndroidSettings::values.screen_layout); -} - -void Config::Initialize(const std::string& config_name) { -    const auto fs_config_loc = FS::GetYuzuPath(FS::YuzuPath::ConfigDir); -    const auto config_file = fmt::format("{}.ini", config_name); - -    switch (type) { -    case ConfigType::GlobalConfig: -        config_loc = FS::PathToUTF8String(fs_config_loc / config_file); -        break; -    case ConfigType::PerGameConfig: -        config_loc = FS::PathToUTF8String(fs_config_loc / "custom" / FS::ToU8String(config_file)); -        break; -    case ConfigType::InputProfile: -        config_loc = FS::PathToUTF8String(fs_config_loc / "input" / config_file); -        LoadINI(DefaultINI::android_config_file); -        return; -    } -    LoadINI(DefaultINI::android_config_file); -    ReadValues(); -} diff --git a/src/android/app/src/main/jni/config.h b/src/android/app/src/main/jni/config.h deleted file mode 100644 index e1e8f47ed..000000000 --- a/src/android/app/src/main/jni/config.h +++ /dev/null @@ -1,47 +0,0 @@ -// SPDX-FileCopyrightText: 2023 yuzu Emulator Project -// SPDX-License-Identifier: GPL-2.0-or-later - -#pragma once - -#include <filesystem> -#include <memory> -#include <optional> -#include <string> - -#include "common/settings.h" - -class INIReader; - -class Config { -    bool LoadINI(const std::string& default_contents = "", bool retry = true); - -public: -    enum class ConfigType { -        GlobalConfig, -        PerGameConfig, -        InputProfile, -    }; - -    explicit Config(const std::string& config_name = "config", -                    ConfigType config_type = ConfigType::GlobalConfig); -    ~Config(); - -    void Initialize(const std::string& config_name); - -private: -    /** -     * Applies a value read from the config to a Setting. -     * -     * @param group The name of the INI group -     * @param setting The yuzu setting to modify -     */ -    template <typename Type, bool ranged> -    void ReadSetting(const std::string& group, Settings::Setting<Type, ranged>& setting); - -    void ReadValues(); - -    const ConfigType type; -    std::unique_ptr<INIReader> config; -    std::string config_loc; -    const bool global; -}; diff --git a/src/android/app/src/main/jni/default_ini.h b/src/android/app/src/main/jni/default_ini.h deleted file mode 100644 index d81422a74..000000000 --- a/src/android/app/src/main/jni/default_ini.h +++ /dev/null @@ -1,511 +0,0 @@ -// SPDX-FileCopyrightText: 2023 yuzu Emulator Project -// SPDX-License-Identifier: GPL-2.0-or-later - -#pragma once - -namespace DefaultINI { - -const char* android_config_file = R"( - -[ControlsP0] -# The input devices and parameters for each Switch native input -# The config section determines the player number where the config will be applied on. For example "ControlsP0", "ControlsP1", ... -# It should be in the format of "engine:[engine_name],[param1]:[value1],[param2]:[value2]..." -# Escape characters $0 (for ':'), $1 (for ',') and $2 (for '$') can be used in values - -# Indicates if this player should be connected at boot -connected= - -# for button input, the following devices are available: -#  - "keyboard" (default) for keyboard input. Required parameters: -#      - "code": the code of the key to bind -#  - "sdl" for joystick input using SDL. Required parameters: -#      - "guid": SDL identification GUID of the joystick -#      - "port": the index of the joystick to bind -#      - "button"(optional): the index of the button to bind -#      - "hat"(optional): the index of the hat to bind as direction buttons -#      - "axis"(optional): the index of the axis to bind -#      - "direction"(only used for hat): the direction name of the hat to bind. Can be "up", "down", "left" or "right" -#      - "threshold"(only used for axis): a float value in (-1.0, 1.0) which the button is -#          triggered if the axis value crosses -#      - "direction"(only used for axis): "+" means the button is triggered when the axis value -#          is greater than the threshold; "-" means the button is triggered when the axis value -#          is smaller than the threshold -button_a= -button_b= -button_x= -button_y= -button_lstick= -button_rstick= -button_l= -button_r= -button_zl= -button_zr= -button_plus= -button_minus= -button_dleft= -button_dup= -button_dright= -button_ddown= -button_lstick_left= -button_lstick_up= -button_lstick_right= -button_lstick_down= -button_sl= -button_sr= -button_home= -button_screenshot= - -# for analog input, the following devices are available: -#  - "analog_from_button" (default) for emulating analog input from direction buttons. Required parameters: -#      - "up", "down", "left", "right": sub-devices for each direction. -#          Should be in the format as a button input devices using escape characters, for example, "engine$0keyboard$1code$00" -#      - "modifier": sub-devices as a modifier. -#      - "modifier_scale": a float number representing the applied modifier scale to the analog input. -#          Must be in range of 0.0-1.0. Defaults to 0.5 -#  - "sdl" for joystick input using SDL. Required parameters: -#      - "guid": SDL identification GUID of the joystick -#      - "port": the index of the joystick to bind -#      - "axis_x": the index of the axis to bind as x-axis (default to 0) -#      - "axis_y": the index of the axis to bind as y-axis (default to 1) -lstick= -rstick= - -# for motion input, the following devices are available: -#  - "keyboard" (default) for emulating random motion input from buttons. Required parameters: -#      - "code": the code of the key to bind -#  - "sdl" for motion input using SDL. Required parameters: -#      - "guid": SDL identification GUID of the joystick -#      - "port": the index of the joystick to bind -#      - "motion": the index of the motion sensor to bind -#  - "cemuhookudp" for motion input using Cemu Hook protocol. Required parameters: -#      - "guid": the IP address of the cemu hook server encoded to a hex string. for example 192.168.0.1 = "c0a80001" -#      - "port": the port of the cemu hook server -#      - "pad": the index of the joystick -#      - "motion": the index of the motion sensor of the joystick to bind -motionleft= -motionright= - -[ControlsGeneral] -# To use the debug_pad, prepend `debug_pad_` before each button setting above. -# i.e. debug_pad_button_a= - -# Enable debug pad inputs to the guest -# 0 (default): Disabled, 1: Enabled -debug_pad_enabled = - -# Whether to enable or disable vibration -# 0: Disabled, 1 (default): Enabled -vibration_enabled= - -# Whether to enable or disable accurate vibrations -# 0 (default): Disabled, 1: Enabled -enable_accurate_vibrations= - -# Enables controller motion inputs -# 0: Disabled, 1 (default): Enabled -motion_enabled = - -# Defines the udp device's touch screen coordinate system for cemuhookudp devices -#  - "min_x", "min_y", "max_x", "max_y" -touch_device= - -# for mapping buttons to touch inputs. -#touch_from_button_map=1 -#touch_from_button_maps_0_name=default -#touch_from_button_maps_0_count=2 -#touch_from_button_maps_0_bind_0=foo -#touch_from_button_maps_0_bind_1=bar -# etc. - -# List of Cemuhook UDP servers, delimited by ','. -# Default: 127.0.0.1:26760 -# Example: 127.0.0.1:26760,123.4.5.67:26761 -udp_input_servers = - -# Enable controlling an axis via a mouse input. -# 0 (default): Off, 1: On -mouse_panning = - -# Set mouse sensitivity. -# Default: 1.0 -mouse_panning_sensitivity = - -# Emulate an analog control stick from keyboard inputs. -# 0 (default): Disabled, 1: Enabled -emulate_analog_keyboard = - -# Enable mouse inputs to the guest -# 0 (default): Disabled, 1: Enabled -mouse_enabled = - -# Enable keyboard inputs to the guest -# 0 (default): Disabled, 1: Enabled -keyboard_enabled = - -[Core] -# Whether to use multi-core for CPU emulation -# 0: Disabled, 1 (default): Enabled -use_multi_core = - -# Enable unsafe extended guest system memory layout (8GB DRAM) -# 0 (default): Disabled, 1: Enabled -use_unsafe_extended_memory_layout = - -[Cpu] -# Adjusts various optimizations. -# Auto-select mode enables choice unsafe optimizations. -# Accurate enables only safe optimizations. -# Unsafe allows any unsafe optimizations. -# 0 (default): Auto-select, 1: Accurate, 2: Enable unsafe optimizations -cpu_accuracy = - -# Allow disabling safe optimizations. -# 0 (default): Disabled, 1: Enabled -cpu_debug_mode = - -# Enable inline page tables optimization (faster guest memory access) -# 0: Disabled, 1 (default): Enabled -cpuopt_page_tables = - -# Enable block linking CPU optimization (reduce block dispatcher use during predictable jumps) -# 0: Disabled, 1 (default): Enabled -cpuopt_block_linking = - -# Enable return stack buffer CPU optimization (reduce block dispatcher use during predictable returns) -# 0: Disabled, 1 (default): Enabled -cpuopt_return_stack_buffer = - -# Enable fast dispatcher CPU optimization (use a two-tiered dispatcher architecture) -# 0: Disabled, 1 (default): Enabled -cpuopt_fast_dispatcher = - -# Enable context elimination CPU Optimization (reduce host memory use for guest context) -# 0: Disabled, 1 (default): Enabled -cpuopt_context_elimination = - -# Enable constant propagation CPU optimization (basic IR optimization) -# 0: Disabled, 1 (default): Enabled -cpuopt_const_prop = - -# Enable miscellaneous CPU optimizations (basic IR optimization) -# 0: Disabled, 1 (default): Enabled -cpuopt_misc_ir = - -# Enable reduction of memory misalignment checks (reduce memory fallbacks for misaligned access) -# 0: Disabled, 1 (default): Enabled -cpuopt_reduce_misalign_checks = - -# Enable Host MMU Emulation (faster guest memory access) -# 0: Disabled, 1 (default): Enabled -cpuopt_fastmem = - -# Enable Host MMU Emulation for exclusive memory instructions (faster guest memory access) -# 0: Disabled, 1 (default): Enabled -cpuopt_fastmem_exclusives = - -# Enable fallback on failure of fastmem of exclusive memory instructions (faster guest memory access) -# 0: Disabled, 1 (default): Enabled -cpuopt_recompile_exclusives = - -# Enable optimization to ignore invalid memory accesses (faster guest memory access) -# 0: Disabled, 1 (default): Enabled -cpuopt_ignore_memory_aborts = - -# Enable unfuse FMA (improve performance on CPUs without FMA) -# Only enabled if cpu_accuracy is set to Unsafe. Automatically chosen with cpu_accuracy = Auto-select. -# 0: Disabled, 1 (default): Enabled -cpuopt_unsafe_unfuse_fma = - -# Enable faster FRSQRTE and FRECPE -# Only enabled if cpu_accuracy is set to Unsafe. -# 0: Disabled, 1 (default): Enabled -cpuopt_unsafe_reduce_fp_error = - -# Enable faster ASIMD instructions (32 bits only) -# Only enabled if cpu_accuracy is set to Unsafe. Automatically chosen with cpu_accuracy = Auto-select. -# 0: Disabled, 1 (default): Enabled -cpuopt_unsafe_ignore_standard_fpcr = - -# Enable inaccurate NaN handling -# Only enabled if cpu_accuracy is set to Unsafe. Automatically chosen with cpu_accuracy = Auto-select. -# 0: Disabled, 1 (default): Enabled -cpuopt_unsafe_inaccurate_nan = - -# Disable address space checks (64 bits only) -# Only enabled if cpu_accuracy is set to Unsafe. Automatically chosen with cpu_accuracy = Auto-select. -# 0: Disabled, 1 (default): Enabled -cpuopt_unsafe_fastmem_check = - -# Enable faster exclusive instructions -# Only enabled if cpu_accuracy is set to Unsafe. Automatically chosen with cpu_accuracy = Auto-select. -# 0: Disabled, 1 (default): Enabled -cpuopt_unsafe_ignore_global_monitor = - -[Renderer] -# Which backend API to use. -# 0: OpenGL (unsupported), 1 (default): Vulkan, 2: Null -backend = - -# Whether to enable asynchronous presentation (Vulkan only) -# 0: Off, 1 (default): On -async_presentation = - -# Forces the GPU to run at the maximum possible clocks (thermal constraints will still be applied). -# 0 (default): Disabled, 1: Enabled -force_max_clock = - -# Enable graphics API debugging mode. -# 0 (default): Disabled, 1: Enabled -debug = - -# Enable shader feedback. -# 0 (default): Disabled, 1: Enabled -renderer_shader_feedback = - -# Enable Nsight Aftermath crash dumps -# 0 (default): Disabled, 1: Enabled -nsight_aftermath = - -# Disable shader loop safety checks, executing the shader without loop logic changes -# 0 (default): Disabled, 1: Enabled -disable_shader_loop_safety_checks = - -# Which Vulkan physical device to use (defaults to 0) -vulkan_device = - -# 0: 0.5x (360p/540p) [EXPERIMENTAL] -# 1: 0.75x (540p/810p) [EXPERIMENTAL] -# 2 (default): 1x (720p/1080p) -# 3: 2x (1440p/2160p) -# 4: 3x (2160p/3240p) -# 5: 4x (2880p/4320p) -# 6: 5x (3600p/5400p) -# 7: 6x (4320p/6480p) -resolution_setup = - -# Pixel filter to use when up- or down-sampling rendered frames. -# 0: Nearest Neighbor -# 1 (default): Bilinear -# 2: Bicubic -# 3: Gaussian -# 4: ScaleForce -# 5: AMD FidelityFX™️ Super Resolution [Vulkan Only] -scaling_filter = - -# Anti-Aliasing (AA) -# 0 (default): None, 1: FXAA -anti_aliasing = - -# Whether to use fullscreen or borderless window mode -# 0 (Windows default): Borderless window, 1 (All other default): Exclusive fullscreen -fullscreen_mode = - -# Aspect ratio -# 0: Default (16:9), 1: Force 4:3, 2: Force 21:9, 3: Force 16:10, 4: Stretch to Window -aspect_ratio = - -# Anisotropic filtering -# 0: Default, 1: 2x, 2: 4x, 3: 8x, 4: 16x -max_anisotropy = - -# Whether to enable VSync or not. -# OpenGL: Values other than 0 enable VSync -# Vulkan: FIFO is selected if the requested mode is not supported by the driver. -# FIFO (VSync) does not drop frames or exhibit tearing but is limited by the screen refresh rate. -# FIFO Relaxed is similar to FIFO but allows tearing as it recovers from a slow down. -# Mailbox can have lower latency than FIFO and does not tear but may drop frames. -# Immediate (no synchronization) just presents whatever is available and can exhibit tearing. -# 0: Immediate (Off), 1 (Default): Mailbox (On), 2: FIFO, 3: FIFO Relaxed -use_vsync = - -# Selects the OpenGL shader backend. NV_gpu_program5 is required for GLASM. If NV_gpu_program5 is -# not available and GLASM is selected, GLSL will be used. -# 0: GLSL, 1 (default): GLASM, 2: SPIR-V -shader_backend = - -# Whether to allow asynchronous shader building. -# 0 (default): Off, 1: On -use_asynchronous_shaders = - -# Uses reactive flushing instead of predictive flushing. Allowing a more accurate syncing of memory. -# 0 (default): Off, 1: On -use_reactive_flushing = - -# NVDEC emulation. -# 0: Disabled, 1: CPU Decoding, 2 (default): GPU Decoding -nvdec_emulation = - -# Accelerate ASTC texture decoding. -# 0 (default): Off, 1: On -accelerate_astc = - -# Turns on the speed limiter, which will limit the emulation speed to the desired speed limit value -# 0: Off, 1: On (default) -use_speed_limit = - -# Limits the speed of the game to run no faster than this value as a percentage of target speed -# 1 - 9999: Speed limit as a percentage of target game speed. 100 (default) -speed_limit = - -# Whether to use disk based shader cache -# 0: Off, 1 (default): On -use_disk_shader_cache = - -# Which gpu accuracy level to use -# 0 (default): Normal, 1: High, 2: Extreme (Very slow) -gpu_accuracy = - -# Whether to use asynchronous GPU emulation -# 0 : Off (slow), 1 (default): On (fast) -use_asynchronous_gpu_emulation = - -# Inform the guest that GPU operations completed more quickly than they did. -# 0: Off, 1 (default): On -use_fast_gpu_time = - -# Force unmodified buffers to be flushed, which can cost performance. -# 0: Off (default), 1: On -use_pessimistic_flushes = - -# Whether to use garbage collection or not for GPU caches. -# 0 (default): Off, 1: On -use_caches_gc = - -# The clear color for the renderer. What shows up on the sides of the bottom screen. -# Must be in range of 0-255. Defaults to 0 for all. -bg_red = -bg_blue = -bg_green = - -[Audio] -# Which audio output engine to use. -# auto (default): Auto-select -# cubeb: Cubeb audio engine (if available) -# sdl2: SDL2 audio engine (if available) -# null: No audio output -output_engine = - -# Which audio device to use. -# auto (default): Auto-select -output_device = - -# Output volume. -# 100 (default): 100%, 0; mute -volume = - -[Data Storage] -# Whether to create a virtual SD card. -# 1: Yes, 0 (default): No -use_virtual_sd = - -# Whether or not to enable gamecard emulation -# 1: Yes, 0 (default): No -gamecard_inserted = - -# Whether or not the gamecard should be emulated as the current game -# If 'gamecard_inserted' is 0 this setting is irrelevant -# 1: Yes, 0 (default): No -gamecard_current_game = - -# Path to an XCI file to use as the gamecard -# If 'gamecard_inserted' is 0 this setting is irrelevant -# If 'gamecard_current_game' is 1 this setting is irrelevant -gamecard_path = - -[System] -# Whether the system is docked -# 1 (default): Yes, 0: No -use_docked_mode = - -# Sets the seed for the RNG generator built into the switch -# rng_seed will be ignored and randomly generated if rng_seed_enabled is false -rng_seed_enabled = -rng_seed = - -# Sets the current time (in seconds since 12:00 AM Jan 1, 1970) that will be used by the time service -# This will auto-increment, with the time set being the time the game is started -# This override will only occur if custom_rtc_enabled is true, otherwise the current time is used -custom_rtc_enabled = -custom_rtc = - -# Sets the systems language index -# 0: Japanese, 1: English (default), 2: French, 3: German, 4: Italian, 5: Spanish, 6: Chinese, -# 7: Korean, 8: Dutch, 9: Portuguese, 10: Russian, 11: Taiwanese, 12: British English, 13: Canadian French, -# 14: Latin American Spanish, 15: Simplified Chinese, 16: Traditional Chinese, 17: Brazilian Portuguese -language_index = - -# The system region that yuzu will use during emulation -# -1: Auto-select (default), 0: Japan, 1: USA, 2: Europe, 3: Australia, 4: China, 5: Korea, 6: Taiwan -region_index = - -# The system time zone that yuzu will use during emulation -# 0: Auto-select (default), 1: Default (system archive value), Others: Index for specified time zone -time_zone_index = - -# Sets the sound output mode. -# 0: Mono, 1 (default): Stereo, 2: Surround -sound_index = - -[Miscellaneous] -# A filter which removes logs below a certain logging level. -# Examples: *:Debug Kernel.SVC:Trace Service.*:Critical -log_filter = *:Trace - -# Use developer keys -# 0 (default): Disabled, 1: Enabled -use_dev_keys = - -[Debugging] -# Record frame time data, can be found in the log directory. Boolean value -record_frame_times = -# Determines whether or not yuzu will dump the ExeFS of all games it attempts to load while loading them -dump_exefs=false -# Determines whether or not yuzu will dump all NSOs it attempts to load while loading them -dump_nso=false -# Determines whether or not yuzu will save the filesystem access log. -enable_fs_access_log=false -# Enables verbose reporting services -reporting_services = -# Determines whether or not yuzu will report to the game that the emulated console is in Kiosk Mode -# false: Retail/Normal Mode (default), true: Kiosk Mode -quest_flag = -# Determines whether debug asserts should be enabled, which will throw an exception on asserts. -# false: Disabled (default), true: Enabled -use_debug_asserts = -# Determines whether unimplemented HLE service calls should be automatically stubbed. -# false: Disabled (default), true: Enabled -use_auto_stub = -# Enables/Disables the macro JIT compiler -disable_macro_jit=false -# Determines whether to enable the GDB stub and wait for the debugger to attach before running. -# false: Disabled (default), true: Enabled -use_gdbstub=false -# The port to use for the GDB server, if it is enabled. -gdbstub_port=6543 - -[WebService] -# Whether or not to enable telemetry -# 0: No, 1 (default): Yes -enable_telemetry = -# URL for Web API -web_api_url = https://api.yuzu-emu.org -# Username and token for yuzu Web Service -# See https://profile.yuzu-emu.org/ for more info -yuzu_username = -yuzu_token = - -[Network] -# Name of the network interface device to use with yuzu LAN play. -# e.g. On *nix: 'enp7s0', 'wlp6s0u1u3u3', 'lo' -# e.g. On Windows: 'Ethernet', 'Wi-Fi' -network_interface = - -[AddOns] -# Used to disable add-ons -# List of title IDs of games that will have add-ons disabled (separated by '|'): -title_ids = -# For each title ID, have a key/value pair called `disabled_<title_id>` equal to the names of the add-ons to disable (sep. by '|') -# e.x. disabled_0100000000010000 = Update|DLC <- disables Updates and DLC on Super Mario Odyssey -)"; -} // namespace DefaultINI diff --git a/src/android/app/src/main/jni/id_cache.cpp b/src/android/app/src/main/jni/id_cache.cpp index 960abf95a..a56ed5662 100644 --- a/src/android/app/src/main/jni/id_cache.cpp +++ b/src/android/app/src/main/jni/id_cache.cpp @@ -13,6 +13,8 @@ static JavaVM* s_java_vm;  static jclass s_native_library_class;  static jclass s_disk_cache_progress_class;  static jclass s_load_callback_stage_class; +static jclass s_game_dir_class; +static jmethodID s_game_dir_constructor;  static jmethodID s_exit_emulation_activity;  static jmethodID s_disk_cache_load_progress;  static jmethodID s_on_emulation_started; @@ -53,6 +55,14 @@ jclass GetDiskCacheLoadCallbackStageClass() {      return s_load_callback_stage_class;  } +jclass GetGameDirClass() { +    return s_game_dir_class; +} + +jmethodID GetGameDirConstructor() { +    return s_game_dir_constructor; +} +  jmethodID GetExitEmulationActivity() {      return s_exit_emulation_activity;  } @@ -90,6 +100,11 @@ jint JNI_OnLoad(JavaVM* vm, void* reserved) {      s_load_callback_stage_class = reinterpret_cast<jclass>(env->NewGlobalRef(env->FindClass(          "org/yuzu/yuzu_emu/disk_shader_cache/DiskShaderCacheProgress$LoadCallbackStage"))); +    const jclass game_dir_class = env->FindClass("org/yuzu/yuzu_emu/model/GameDir"); +    s_game_dir_class = reinterpret_cast<jclass>(env->NewGlobalRef(game_dir_class)); +    s_game_dir_constructor = env->GetMethodID(game_dir_class, "<init>", "(Ljava/lang/String;Z)V"); +    env->DeleteLocalRef(game_dir_class); +      // Initialize methods      s_exit_emulation_activity =          env->GetStaticMethodID(s_native_library_class, "exitEmulationActivity", "(I)V"); @@ -120,6 +135,7 @@ void JNI_OnUnload(JavaVM* vm, void* reserved) {      env->DeleteGlobalRef(s_native_library_class);      env->DeleteGlobalRef(s_disk_cache_progress_class);      env->DeleteGlobalRef(s_load_callback_stage_class); +    env->DeleteGlobalRef(s_game_dir_class);      // UnInitialize applets      SoftwareKeyboard::CleanupJNI(env); diff --git a/src/android/app/src/main/jni/id_cache.h b/src/android/app/src/main/jni/id_cache.h index b76158928..855649efa 100644 --- a/src/android/app/src/main/jni/id_cache.h +++ b/src/android/app/src/main/jni/id_cache.h @@ -13,6 +13,8 @@ JNIEnv* GetEnvForThread();  jclass GetNativeLibraryClass();  jclass GetDiskCacheProgressClass();  jclass GetDiskCacheLoadCallbackStageClass(); +jclass GetGameDirClass(); +jmethodID GetGameDirConstructor();  jmethodID GetExitEmulationActivity();  jmethodID GetDiskCacheLoadProgress();  jmethodID GetOnEmulationStarted(); diff --git a/src/android/app/src/main/jni/native.cpp b/src/android/app/src/main/jni/native.cpp index 64663b084..3d795b57f 100644 --- a/src/android/app/src/main/jni/native.cpp +++ b/src/android/app/src/main/jni/native.cpp @@ -52,8 +52,8 @@  #include "core/hle/service/am/applets/applets.h"  #include "core/hle/service/filesystem/filesystem.h"  #include "core/loader/loader.h" +#include "frontend_common/config.h"  #include "jni/android_common/android_common.h" -#include "jni/config.h"  #include "jni/id_cache.h"  #include "jni/native.h"  #include "video_core/renderer_base.h" @@ -123,9 +123,6 @@ int EmulationSession::InstallFileToNand(std::string filename, std::string file_e          ErrorFilenameExtension = 4,      }; -    m_system.SetContentProvider(std::make_unique<FileSys::ContentProviderUnion>()); -    m_system.GetFileSystemController().CreateFactories(*m_vfs); -      [[maybe_unused]] std::shared_ptr<FileSys::NSP> nsp;      if (file_extension == "nsp") {          nsp = std::make_shared<FileSys::NSP>(m_vfs->OpenFile(filename, FileSys::Mode::Read)); @@ -664,8 +661,6 @@ void Java_org_yuzu_yuzu_1emu_NativeLibrary_onTouchReleased(JNIEnv* env, jclass c  void Java_org_yuzu_yuzu_1emu_NativeLibrary_initializeSystem(JNIEnv* env, jclass clazz,                                                              jboolean reload) { -    // Create the default config.ini. -    Config{};      // Initialize the emulated system.      if (!reload) {          EmulationSession::GetInstance().System().Initialize(); @@ -680,17 +675,6 @@ jint Java_org_yuzu_yuzu_1emu_NativeLibrary_defaultCPUCore(JNIEnv* env, jclass cl  void Java_org_yuzu_yuzu_1emu_NativeLibrary_run__Ljava_lang_String_2Ljava_lang_String_2Z(      JNIEnv* env, jclass clazz, jstring j_file, jstring j_savestate, jboolean j_delete_savestate) {} -void Java_org_yuzu_yuzu_1emu_NativeLibrary_reloadSettings(JNIEnv* env, jclass clazz) { -    Config{}; -} - -void Java_org_yuzu_yuzu_1emu_NativeLibrary_initGameIni(JNIEnv* env, jclass clazz, -                                                       jstring j_game_id) { -    std::string_view game_id = env->GetStringUTFChars(j_game_id, 0); - -    env->ReleaseStringUTFChars(j_game_id, game_id.data()); -} -  jdoubleArray Java_org_yuzu_yuzu_1emu_NativeLibrary_getPerfStats(JNIEnv* env, jclass clazz) {      jdoubleArray j_stats = env->NewDoubleArray(4); @@ -707,6 +691,14 @@ jdoubleArray Java_org_yuzu_yuzu_1emu_NativeLibrary_getPerfStats(JNIEnv* env, jcl      return j_stats;  } +jstring Java_org_yuzu_yuzu_1emu_NativeLibrary_getCpuBackend(JNIEnv* env, jclass clazz) { +    if (Settings::IsNceEnabled()) { +        return ToJString(env, "NCE"); +    } + +    return ToJString(env, "JIT"); +} +  void Java_org_yuzu_yuzu_1emu_utils_DirectoryInitialization_setSysDirectory(JNIEnv* env,                                                                             jclass clazz,                                                                             jstring j_path) {} diff --git a/src/android/app/src/main/jni/native_config.cpp b/src/android/app/src/main/jni/native_config.cpp index 8a704960c..763b2164c 100644 --- a/src/android/app/src/main/jni/native_config.cpp +++ b/src/android/app/src/main/jni/native_config.cpp @@ -5,11 +5,15 @@  #include <jni.h> +#include "android_config.h" +#include "android_settings.h"  #include "common/logging/log.h"  #include "common/settings.h" +#include "frontend_common/config.h"  #include "jni/android_common/android_common.h" -#include "jni/config.h" -#include "uisettings.h" +#include "jni/id_cache.h" + +std::unique_ptr<AndroidConfig> config;  template <typename T>  Settings::Setting<T>* getSetting(JNIEnv* env, jstring jkey) { @@ -28,6 +32,22 @@ Settings::Setting<T>* getSetting(JNIEnv* env, jstring jkey) {  extern "C" { +void Java_org_yuzu_yuzu_1emu_utils_NativeConfig_initializeConfig(JNIEnv* env, jobject obj) { +    config = std::make_unique<AndroidConfig>(); +} + +void Java_org_yuzu_yuzu_1emu_utils_NativeConfig_unloadConfig(JNIEnv* env, jobject obj) { +    config.reset(); +} + +void Java_org_yuzu_yuzu_1emu_utils_NativeConfig_reloadSettings(JNIEnv* env, jobject obj) { +    config->AndroidConfig::ReloadAllValues(); +} + +void Java_org_yuzu_yuzu_1emu_utils_NativeConfig_saveSettings(JNIEnv* env, jobject obj) { +    config->AndroidConfig::SaveAllValues(); +} +  jboolean Java_org_yuzu_yuzu_1emu_utils_NativeConfig_getBoolean(JNIEnv* env, jobject obj,                                                                 jstring jkey, jboolean getDefault) {      auto setting = getSetting<bool>(env, jkey); @@ -234,4 +254,55 @@ jstring Java_org_yuzu_yuzu_1emu_utils_NativeConfig_getPairedSettingKey(JNIEnv* e      return ToJString(env, setting->PairedSetting()->GetLabel());  } +jobjectArray Java_org_yuzu_yuzu_1emu_utils_NativeConfig_getGameDirs(JNIEnv* env, jobject obj) { +    jclass gameDirClass = IDCache::GetGameDirClass(); +    jmethodID gameDirConstructor = IDCache::GetGameDirConstructor(); +    jobjectArray jgameDirArray = +        env->NewObjectArray(AndroidSettings::values.game_dirs.size(), gameDirClass, nullptr); +    for (size_t i = 0; i < AndroidSettings::values.game_dirs.size(); ++i) { +        jobject jgameDir = +            env->NewObject(gameDirClass, gameDirConstructor, +                           ToJString(env, AndroidSettings::values.game_dirs[i].path), +                           static_cast<jboolean>(AndroidSettings::values.game_dirs[i].deep_scan)); +        env->SetObjectArrayElement(jgameDirArray, i, jgameDir); +    } +    return jgameDirArray; +} + +void Java_org_yuzu_yuzu_1emu_utils_NativeConfig_setGameDirs(JNIEnv* env, jobject obj, +                                                            jobjectArray gameDirs) { +    AndroidSettings::values.game_dirs.clear(); +    int size = env->GetArrayLength(gameDirs); + +    if (size == 0) { +        return; +    } + +    jobject dir = env->GetObjectArrayElement(gameDirs, 0); +    jclass gameDirClass = IDCache::GetGameDirClass(); +    jfieldID uriStringField = env->GetFieldID(gameDirClass, "uriString", "Ljava/lang/String;"); +    jfieldID deepScanBooleanField = env->GetFieldID(gameDirClass, "deepScan", "Z"); +    for (int i = 0; i < size; ++i) { +        dir = env->GetObjectArrayElement(gameDirs, i); +        jstring juriString = static_cast<jstring>(env->GetObjectField(dir, uriStringField)); +        jboolean jdeepScanBoolean = env->GetBooleanField(dir, deepScanBooleanField); +        std::string uriString = GetJString(env, juriString); +        AndroidSettings::values.game_dirs.push_back( +            AndroidSettings::GameDir{uriString, static_cast<bool>(jdeepScanBoolean)}); +    } +} + +void Java_org_yuzu_yuzu_1emu_utils_NativeConfig_addGameDir(JNIEnv* env, jobject obj, +                                                           jobject gameDir) { +    jclass gameDirClass = IDCache::GetGameDirClass(); +    jfieldID uriStringField = env->GetFieldID(gameDirClass, "uriString", "Ljava/lang/String;"); +    jfieldID deepScanBooleanField = env->GetFieldID(gameDirClass, "deepScan", "Z"); + +    jstring juriString = static_cast<jstring>(env->GetObjectField(gameDir, uriStringField)); +    jboolean jdeepScanBoolean = env->GetBooleanField(gameDir, deepScanBooleanField); +    std::string uriString = GetJString(env, juriString); +    AndroidSettings::values.game_dirs.push_back( +        AndroidSettings::GameDir{uriString, static_cast<bool>(jdeepScanBoolean)}); +} +  } // extern "C" diff --git a/src/android/app/src/main/res/layout/card_folder.xml b/src/android/app/src/main/res/layout/card_folder.xml new file mode 100644 index 000000000..4e0c04b6b --- /dev/null +++ b/src/android/app/src/main/res/layout/card_folder.xml @@ -0,0 +1,70 @@ +<?xml version="1.0" encoding="utf-8"?> +<com.google.android.material.card.MaterialCardView 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" +    style="?attr/materialCardViewOutlinedStyle" +    android:layout_width="match_parent" +    android:layout_height="wrap_content" +    android:layout_marginHorizontal="16dp" +    android:layout_marginVertical="12dp" +    android:focusable="true"> + +    <androidx.constraintlayout.widget.ConstraintLayout +        android:layout_width="match_parent" +        android:layout_height="wrap_content" +        android:orientation="horizontal" +        android:padding="16dp" +        android:layout_gravity="center_vertical" +        android:animateLayoutChanges="true"> + +        <com.google.android.material.textview.MaterialTextView +            android:id="@+id/path" +            style="@style/TextAppearance.Material3.BodyLarge" +            android:layout_width="0dp" +            android:layout_height="wrap_content" +            android:layout_gravity="center_vertical|start" +            android:ellipsize="none" +            android:marqueeRepeatLimit="marquee_forever" +            android:requiresFadingEdge="horizontal" +            android:singleLine="true" +            android:textAlignment="viewStart" +            app:layout_constraintBottom_toBottomOf="parent" +            app:layout_constraintEnd_toStartOf="@+id/button_layout" +            app:layout_constraintStart_toStartOf="parent" +            app:layout_constraintTop_toTopOf="parent" +            tools:text="@string/select_gpu_driver_default" /> + +        <LinearLayout +            android:id="@+id/button_layout" +            android:layout_width="wrap_content" +            android:layout_height="wrap_content" +            android:orientation="horizontal" +            app:layout_constraintBottom_toBottomOf="parent" +            app:layout_constraintEnd_toEndOf="parent" +            app:layout_constraintTop_toTopOf="parent"> + +            <Button +                android:id="@+id/button_edit" +                style="@style/Widget.Material3.Button.IconButton" +                android:layout_width="wrap_content" +                android:layout_height="wrap_content" +                android:contentDescription="@string/delete" +                android:tooltipText="@string/edit" +                app:icon="@drawable/ic_edit" +                app:iconTint="?attr/colorControlNormal" /> + +            <Button +                android:id="@+id/button_delete" +                style="@style/Widget.Material3.Button.IconButton" +                android:layout_width="wrap_content" +                android:layout_height="wrap_content" +                android:contentDescription="@string/delete" +                android:tooltipText="@string/delete" +                app:icon="@drawable/ic_delete" +                app:iconTint="?attr/colorControlNormal" /> + +        </LinearLayout> + +    </androidx.constraintlayout.widget.ConstraintLayout> + +</com.google.android.material.card.MaterialCardView> diff --git a/src/android/app/src/main/res/layout/dialog_add_folder.xml b/src/android/app/src/main/res/layout/dialog_add_folder.xml new file mode 100644 index 000000000..01f95e868 --- /dev/null +++ b/src/android/app/src/main/res/layout/dialog_add_folder.xml @@ -0,0 +1,45 @@ +<?xml version="1.0" encoding="utf-8"?> +<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" +    xmlns:tools="http://schemas.android.com/tools" +    android:layout_width="match_parent" +    android:layout_height="wrap_content" +    android:padding="24dp" +    android:orientation="vertical"> + +    <com.google.android.material.textview.MaterialTextView +        android:id="@+id/path" +        style="@style/TextAppearance.Material3.BodyLarge" +        android:layout_width="match_parent" +        android:layout_height="0dp" +        android:layout_gravity="center_vertical|start" +        android:layout_weight="1" +        android:ellipsize="marquee" +        android:marqueeRepeatLimit="marquee_forever" +        android:requiresFadingEdge="horizontal" +        android:singleLine="true" +        android:textAlignment="viewStart" +        tools:text="folder/folder/folder/folder" /> + +    <LinearLayout +        android:layout_width="match_parent" +        android:layout_height="wrap_content" +        android:orientation="horizontal" +        android:paddingTop="8dp"> + +        <com.google.android.material.textview.MaterialTextView +            style="@style/TextAppearance.Material3.BodyMedium" +            android:layout_width="0dp" +            android:layout_height="wrap_content" +            android:layout_gravity="center_vertical|start" +            android:layout_weight="1" +            android:text="@string/deep_scan" +            android:textAlignment="viewStart" /> + +        <com.google.android.material.checkbox.MaterialCheckBox +            android:id="@+id/deep_scan_switch" +            android:layout_width="wrap_content" +            android:layout_height="wrap_content" /> + +    </LinearLayout> + +</LinearLayout> diff --git a/src/android/app/src/main/res/layout/dialog_folder_properties.xml b/src/android/app/src/main/res/layout/dialog_folder_properties.xml new file mode 100644 index 000000000..248d048cb --- /dev/null +++ b/src/android/app/src/main/res/layout/dialog_folder_properties.xml @@ -0,0 +1,30 @@ +<?xml version="1.0" encoding="utf-8"?> +<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" +    android:layout_width="match_parent" +    android:layout_height="wrap_content" +    android:padding="24dp" +    android:orientation="vertical"> + +    <LinearLayout +        android:id="@+id/deep_scan_layout" +        android:layout_width="match_parent" +        android:layout_height="wrap_content" +        android:orientation="horizontal"> + +        <com.google.android.material.textview.MaterialTextView +            style="@style/TextAppearance.Material3.BodyMedium" +            android:layout_width="0dp" +            android:layout_height="wrap_content" +            android:layout_gravity="center_vertical|start" +            android:layout_weight="1" +            android:text="@string/deep_scan" +            android:textAlignment="viewStart" /> + +        <com.google.android.material.checkbox.MaterialCheckBox +            android:id="@+id/deep_scan_switch" +            android:layout_width="wrap_content" +            android:layout_height="wrap_content" /> + +    </LinearLayout> + +</LinearLayout> diff --git a/src/android/app/src/main/res/layout/fragment_folders.xml b/src/android/app/src/main/res/layout/fragment_folders.xml new file mode 100644 index 000000000..74f2f3754 --- /dev/null +++ b/src/android/app/src/main/res/layout/fragment_folders.xml @@ -0,0 +1,48 @@ +<?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/coordinator_folders" +    android:layout_width="match_parent" +    android:layout_height="match_parent" +    android:background="?attr/colorSurface"> + +    <androidx.coordinatorlayout.widget.CoordinatorLayout +        android:layout_width="match_parent" +        android:layout_height="match_parent"> + +        <com.google.android.material.appbar.AppBarLayout +            android:id="@+id/appbar_folders" +            android:layout_width="match_parent" +            android:layout_height="wrap_content" +            android:fitsSystemWindows="true" +            app:liftOnScrollTargetViewId="@id/list_folders"> + +            <com.google.android.material.appbar.MaterialToolbar +                android:id="@+id/toolbar_folders" +                android:layout_width="match_parent" +                android:layout_height="?attr/actionBarSize" +                app:navigationIcon="@drawable/ic_back" +                app:title="@string/game_folders" /> + +        </com.google.android.material.appbar.AppBarLayout> + +        <androidx.recyclerview.widget.RecyclerView +            android:id="@+id/list_folders" +            android:layout_width="match_parent" +            android:layout_height="wrap_content" +            android:clipToPadding="false" +            app:layout_behavior="@string/appbar_scrolling_view_behavior" /> + +    </androidx.coordinatorlayout.widget.CoordinatorLayout> + +    <com.google.android.material.floatingactionbutton.FloatingActionButton +        android:id="@+id/button_add" +        android:layout_width="wrap_content" +        android:layout_height="wrap_content" +        android:layout_gravity="bottom|end" +        android:contentDescription="@string/add_games" +        app:srcCompat="@drawable/ic_add" +        app:layout_constraintBottom_toBottomOf="parent" +        app:layout_constraintEnd_toEndOf="parent" /> + +</androidx.constraintlayout.widget.ConstraintLayout> diff --git a/src/android/app/src/main/res/layout/fragment_search.xml b/src/android/app/src/main/res/layout/fragment_search.xml index b8d54d947..efdfd7047 100644 --- a/src/android/app/src/main/res/layout/fragment_search.xml +++ b/src/android/app/src/main/res/layout/fragment_search.xml @@ -127,6 +127,7 @@              android:layout_height="wrap_content"              android:clipToPadding="false"              android:paddingVertical="4dp" +            app:checkedChip="@id/chip_recently_played"              app:chipSpacingHorizontal="12dp"              app:singleLine="true"              app:singleSelection="true"> 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 6d4c1f86d..cf70b4bc4 100644 --- a/src/android/app/src/main/res/navigation/home_navigation.xml +++ b/src/android/app/src/main/res/navigation/home_navigation.xml @@ -28,6 +28,9 @@          <action              android:id="@+id/action_homeSettingsFragment_to_appletLauncherFragment"              app:destination="@id/appletLauncherFragment" /> +        <action +            android:id="@+id/action_homeSettingsFragment_to_gameFoldersFragment" +            app:destination="@id/gameFoldersFragment" />      </fragment>      <fragment @@ -117,5 +120,9 @@          android:id="@+id/cabinetLauncherDialogFragment"          android:name="org.yuzu.yuzu_emu.fragments.CabinetLauncherDialogFragment"          android:label="CabinetLauncherDialogFragment" /> +    <fragment +        android:id="@+id/gameFoldersFragment" +        android:name="org.yuzu.yuzu_emu.fragments.GameFoldersFragment" +        android:label="GameFoldersFragment" />  </navigation> diff --git a/src/android/app/src/main/res/values/arrays.xml b/src/android/app/src/main/res/values/arrays.xml index 51bcc49a3..ab435dce9 100644 --- a/src/android/app/src/main/res/values/arrays.xml +++ b/src/android/app/src/main/res/values/arrays.xml @@ -175,6 +175,24 @@          <item>2</item>      </integer-array> +    <string-array name="cpuBackendArm64Names"> +        <item>@string/cpu_backend_dynarmic</item> +        <item>@string/cpu_backend_nce</item> +    </string-array> + +    <integer-array name="cpuBackendArm64Values"> +        <item>0</item> +        <item>1</item> +    </integer-array> + +    <string-array name="cpuBackendX86Names"> +        <item>@string/cpu_backend_dynarmic</item> +    </string-array> + +    <integer-array name="cpuBackendX86Values"> +        <item>0</item> +    </integer-array> +      <string-array name="cpuAccuracyNames">          <item>@string/auto</item>          <item>@string/cpu_accuracy_accurate</item> diff --git a/src/android/app/src/main/res/values/dimens.xml b/src/android/app/src/main/res/values/dimens.xml index ef855ea6f..380d14213 100644 --- a/src/android/app/src/main/res/values/dimens.xml +++ b/src/android/app/src/main/res/values/dimens.xml @@ -13,7 +13,7 @@      <dimen name="menu_width">256dp</dimen>      <dimen name="card_width">165dp</dimen>      <dimen name="icon_inset">24dp</dimen> -    <dimen name="spacing_bottom_list_fab">72dp</dimen> +    <dimen name="spacing_bottom_list_fab">76dp</dimen>      <dimen name="spacing_fab">24dp</dimen>      <dimen name="dialog_margin">20dp</dimen> diff --git a/src/android/app/src/main/res/values/strings.xml b/src/android/app/src/main/res/values/strings.xml index 98c3f20f8..a6ccef8a1 100644 --- a/src/android/app/src/main/res/values/strings.xml +++ b/src/android/app/src/main/res/values/strings.xml @@ -38,6 +38,7 @@      <string name="empty_gamelist">No files were found or no game directory has been selected yet.</string>      <string name="search_and_filter_games">Search and filter games</string>      <string name="select_games_folder">Select games folder</string> +    <string name="manage_game_folders">Manage game folders</string>      <string name="select_games_folder_description">Allows yuzu to populate the games list</string>      <string name="add_games_warning">Skip selecting games folder?</string>      <string name="add_games_warning_description">Games won\'t be displayed in the Games list if a folder isn\'t selected.</string> @@ -91,6 +92,7 @@      <string name="manage_save_data">Manage save data</string>      <string name="manage_save_data_description">Save data found. Please select an option below.</string>      <string name="import_export_saves_description">Import or export save files</string> +    <string name="save_files_exporting">Exporting save files…</string>      <string name="save_file_imported_success">Imported successfully</string>      <string name="save_file_invalid_zip_structure">Invalid save directory structure</string>      <string name="save_file_invalid_zip_structure_description">The first subfolder name must be the title ID of the game.</string> @@ -123,6 +125,11 @@      <string name="manage_yuzu_data_description">Import/export firmware, keys, user data, and more!</string>      <string name="share_save_file">Share save file</string>      <string name="export_save_failed">Failed to export save</string> +    <string name="game_folders">Game folders</string> +    <string name="deep_scan">Deep scan</string> +    <string name="add_game_folder">Add game folder</string> +    <string name="folder_already_added">This folder was already added!</string> +    <string name="game_folder_properties">Game folder properties</string>      <!-- Applet launcher strings -->      <string name="applets">Applet launcher</string> @@ -184,6 +191,7 @@      <string name="frame_limit_enable_description">Limits emulation speed to a specified percentage of normal speed.</string>      <string name="frame_limit_slider">Limit speed percent</string>      <string name="frame_limit_slider_description">Specifies the percentage to limit emulation speed. 100% is the normal speed. Values higher or lower will increase or decrease the speed limit.</string> +    <string name="cpu_backend">CPU backend</string>      <string name="cpu_accuracy">CPU accuracy</string>      <string name="value_with_units">%1$s%2$s</string> @@ -256,6 +264,8 @@      <string name="cancelling">Cancelling</string>      <string name="install">Install</string>      <string name="delete">Delete</string> +    <string name="edit">Edit</string> +    <string name="export_success">Exported successfully</string>      <!-- GPU driver installation -->      <string name="select_gpu_driver">Select GPU driver</string> @@ -414,6 +424,10 @@      <string name="ratio_force_sixteen_ten">Force 16:10</string>      <string name="ratio_stretch">Stretch to window</string> +    <!-- CPU Backend --> +    <string name="cpu_backend_dynarmic">Dynarmic (Slow)</string> +    <string name="cpu_backend_nce">Native code execution (NCE)</string> +      <!-- CPU Accuracy -->      <string name="cpu_accuracy_accurate">Accurate</string>      <string name="cpu_accuracy_unsafe">Unsafe</string> | 
