diff --git a/src/android/app/src/main/java/org/citra/citra_emu/NativeLibrary.kt b/src/android/app/src/main/java/org/citra/citra_emu/NativeLibrary.kt index 554369a45..2fa77aaa4 100644 --- a/src/android/app/src/main/java/org/citra/citra_emu/NativeLibrary.kt +++ b/src/android/app/src/main/java/org/citra/citra_emu/NativeLibrary.kt @@ -220,14 +220,6 @@ object NativeLibrary { val title: String val message: String when (error) { - CoreError.ErrorSystemFiles -> { - title = emulationActivity.getString(R.string.system_archive_not_found) - message = emulationActivity.getString( - R.string.system_archive_not_found_message, - details.ifEmpty { emulationActivity.getString(R.string.system_archive_general) } - ) - } - CoreError.ErrorSavestate -> { title = emulationActivity.getString(R.string.save_load_error) message = details @@ -410,7 +402,6 @@ object NativeLibrary { const val ErrorLoader = 4 const val ErrorLoader_ErrorEncrypted = 5 const val ErrorLoader_ErrorInvalidFormat = 6 - const val ErrorSystemFiles = 7 const val ShutdownRequested = 11 const val ErrorUnknown = 12 @@ -629,7 +620,6 @@ object NativeLibrary { } enum class CoreError { - ErrorSystemFiles, ErrorSavestate, ErrorUnknown } diff --git a/src/android/app/src/main/java/org/citra/citra_emu/fragments/DownloadSystemFilesDialogFragment.kt b/src/android/app/src/main/java/org/citra/citra_emu/fragments/DownloadSystemFilesDialogFragment.kt deleted file mode 100644 index 3f5abfd14..000000000 --- a/src/android/app/src/main/java/org/citra/citra_emu/fragments/DownloadSystemFilesDialogFragment.kt +++ /dev/null @@ -1,152 +0,0 @@ -// Copyright 2023 Citra Emulator Project -// Licensed under GPLv2 or any later version -// Refer to the license.txt file included. - -package org.citra.citra_emu.fragments - -import android.app.Dialog -import android.os.Bundle -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import androidx.appcompat.app.AlertDialog -import androidx.fragment.app.DialogFragment -import androidx.fragment.app.activityViewModels -import androidx.lifecycle.Lifecycle -import androidx.lifecycle.lifecycleScope -import androidx.lifecycle.repeatOnLifecycle -import com.google.android.material.dialog.MaterialAlertDialogBuilder -import kotlinx.coroutines.flow.collect -import kotlinx.coroutines.flow.collectLatest -import kotlinx.coroutines.launch -import org.citra.citra_emu.NativeLibrary.InstallStatus -import org.citra.citra_emu.R -import org.citra.citra_emu.databinding.DialogProgressBarBinding -import org.citra.citra_emu.viewmodel.GamesViewModel -import org.citra.citra_emu.viewmodel.SystemFilesViewModel - -class DownloadSystemFilesDialogFragment : DialogFragment() { - private var _binding: DialogProgressBarBinding? = null - private val binding get() = _binding!! - - private val downloadViewModel: SystemFilesViewModel by activityViewModels() - private val gamesViewModel: GamesViewModel by activityViewModels() - - private lateinit var titles: LongArray - - override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { - _binding = DialogProgressBarBinding.inflate(layoutInflater) - - titles = requireArguments().getLongArray(TITLES)!! - - binding.progressText.visibility = View.GONE - - binding.progressBar.min = 0 - binding.progressBar.max = titles.size - if (downloadViewModel.isDownloading.value != true) { - binding.progressBar.progress = 0 - } - - isCancelable = false - return MaterialAlertDialogBuilder(requireContext()) - .setView(binding.root) - .setTitle(R.string.downloading_files) - .setMessage(R.string.downloading_files_description) - .setNegativeButton(android.R.string.cancel, null) - .create() - } - - override fun onCreateView( - inflater: LayoutInflater, - container: ViewGroup?, - savedInstanceState: Bundle? - ): View { - return binding.root - } - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - viewLifecycleOwner.lifecycleScope.apply { - launch { - repeatOnLifecycle(Lifecycle.State.CREATED) { - downloadViewModel.progress.collectLatest { binding.progressBar.progress = it } - } - } - launch { - repeatOnLifecycle(Lifecycle.State.CREATED) { - downloadViewModel.result.collect { - when (it) { - InstallStatus.Success -> { - downloadViewModel.clear() - dismiss() - MessageDialogFragment.newInstance(R.string.download_success, 0) - .show(requireActivity().supportFragmentManager, MessageDialogFragment.TAG) - gamesViewModel.setShouldSwapData(true) - } - - InstallStatus.ErrorFailedToOpenFile, - InstallStatus.ErrorEncrypted, - InstallStatus.ErrorFileNotFound, - InstallStatus.ErrorInvalid, - InstallStatus.ErrorAborted -> { - downloadViewModel.clear() - dismiss() - MessageDialogFragment.newInstance( - R.string.download_failed, - R.string.download_failed_description - ).show(requireActivity().supportFragmentManager, MessageDialogFragment.TAG) - gamesViewModel.setShouldSwapData(true) - } - - InstallStatus.Cancelled -> { - downloadViewModel.clear() - dismiss() - MessageDialogFragment.newInstance( - R.string.download_cancelled, - R.string.download_cancelled_description - ).show(requireActivity().supportFragmentManager, MessageDialogFragment.TAG) - } - - // Do nothing on null - else -> {} - } - } - } - } - } - - // Consider using WorkManager here. While the home menu can only really amount to - // about 150MBs, this could be a problem on inconsistent networks - downloadViewModel.download(titles) - } - - override fun onResume() { - super.onResume() - val alertDialog = dialog as AlertDialog - val negativeButton = alertDialog.getButton(Dialog.BUTTON_NEGATIVE) - negativeButton.setOnClickListener { - downloadViewModel.cancel() - dialog?.setTitle(R.string.cancelling) - binding.progressBar.isIndeterminate = true - } - } - - override fun onDestroyView() { - super.onDestroyView() - _binding = null - } - - companion object { - const val TAG = "DownloadSystemFilesDialogFragment" - - const val TITLES = "Titles" - - fun newInstance(titles: LongArray): DownloadSystemFilesDialogFragment { - val dialog = DownloadSystemFilesDialogFragment() - val args = Bundle() - args.putLongArray(TITLES, titles) - dialog.arguments = args - return dialog - } - } -} diff --git a/src/android/app/src/main/java/org/citra/citra_emu/fragments/HomeSettingsFragment.kt b/src/android/app/src/main/java/org/citra/citra_emu/fragments/HomeSettingsFragment.kt index 32f6a56b1..34d8ef4ca 100644 --- a/src/android/app/src/main/java/org/citra/citra_emu/fragments/HomeSettingsFragment.kt +++ b/src/android/app/src/main/java/org/citra/citra_emu/fragments/HomeSettingsFragment.kt @@ -76,16 +76,6 @@ class HomeSettingsFragment : Fragment() { R.drawable.ic_settings, { SettingsActivity.launch(requireContext(), SettingsFile.FILE_NAME_CONFIG, "") } ), - /*HomeSetting( - R.string.system_files, - R.string.system_files_description, - R.drawable.ic_system_update, - { - exitTransition = MaterialSharedAxis(MaterialSharedAxis.X, true) - parentFragmentManager.primaryNavigationFragment?.findNavController() - ?.navigate(R.id.action_homeSettingsFragment_to_systemFilesFragment) - } - ),*/ HomeSetting( R.string.install_game_content, R.string.install_game_content_description, diff --git a/src/android/app/src/main/java/org/citra/citra_emu/fragments/SystemFilesFragment.kt b/src/android/app/src/main/java/org/citra/citra_emu/fragments/SystemFilesFragment.kt deleted file mode 100644 index 812218375..000000000 --- a/src/android/app/src/main/java/org/citra/citra_emu/fragments/SystemFilesFragment.kt +++ /dev/null @@ -1,311 +0,0 @@ -// Copyright 2023 Citra Emulator Project -// Licensed under GPLv2 or any later version -// Refer to the license.txt file included. - -package org.citra.citra_emu.fragments - -import android.content.res.Resources -import android.os.Bundle -import android.text.Html -import android.text.method.LinkMovementMethod -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import android.widget.AdapterView -import android.widget.ArrayAdapter -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.preference.PreferenceManager -import com.google.android.material.textfield.MaterialAutoCompleteTextView -import com.google.android.material.transition.MaterialSharedAxis -import kotlinx.coroutines.launch -import org.citra.citra_emu.CitraApplication -import org.citra.citra_emu.HomeNavigationDirections -import org.citra.citra_emu.NativeLibrary -import org.citra.citra_emu.R -import org.citra.citra_emu.activities.EmulationActivity -import org.citra.citra_emu.databinding.FragmentSystemFilesBinding -import org.citra.citra_emu.features.settings.model.Settings -import org.citra.citra_emu.model.Game -import org.citra.citra_emu.utils.SystemSaveGame -import org.citra.citra_emu.viewmodel.GamesViewModel -import org.citra.citra_emu.viewmodel.HomeViewModel -import org.citra.citra_emu.viewmodel.SystemFilesViewModel -import org.citra.citra_emu.vr.VrActivity - -class SystemFilesFragment : Fragment() { - private var _binding: FragmentSystemFilesBinding? = null - private val binding get() = _binding!! - - private val homeViewModel: HomeViewModel by activityViewModels() - private val systemFilesViewModel: SystemFilesViewModel by activityViewModels() - private val gamesViewModel: GamesViewModel by activityViewModels() - - private lateinit var regionValues: IntArray - - private val systemTypeDropdown = DropdownItem(R.array.systemFileTypeValues) - private val systemRegionDropdown = DropdownItem(R.array.systemFileRegionValues) - - private val SYS_TYPE = "SysType" - private val REGION = "Region" - private val REGION_START = "RegionStart" - - private val homeMenuMap: MutableMap = mutableMapOf() - - private val WARNING_SHOWN = "SystemFilesWarningShown" - - private class DropdownItem(val valuesId: Int) : AdapterView.OnItemClickListener { - var position = 0 - - fun getValue(resources: Resources): Int { - return resources.getIntArray(valuesId)[position] - } - - override fun onItemClick(p0: AdapterView<*>?, view: View?, position: Int, id: Long) { - this.position = position - } - } - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - enterTransition = MaterialSharedAxis(MaterialSharedAxis.X, true) - returnTransition = MaterialSharedAxis(MaterialSharedAxis.X, false) - SystemSaveGame.load() - } - - override fun onCreateView( - inflater: LayoutInflater, - container: ViewGroup?, - savedInstanceState: Bundle? - ): View { - _binding = FragmentSystemFilesBinding.inflate(layoutInflater) - return binding.root - } - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - homeViewModel.setNavigationVisibility(visible = false, animated = true) - homeViewModel.setStatusBarShadeVisibility(visible = false) - - val preferences = PreferenceManager.getDefaultSharedPreferences(CitraApplication.appContext) - if (!preferences.getBoolean(WARNING_SHOWN, false)) { - MessageDialogFragment.newInstance( - R.string.home_menu_warning, - R.string.home_menu_warning_description - ).show(childFragmentManager, MessageDialogFragment.TAG) - preferences.edit() - .putBoolean(WARNING_SHOWN, true) - .apply() - } - - binding.toolbarSystemFiles.setNavigationOnClickListener { - binding.root.findNavController().popBackStack() - } - - // TODO: Remove workaround for text filtering issue in material components when fixed - // https://github.com/material-components/material-components-android/issues/1464 - binding.dropdownSystemType.isSaveEnabled = false - binding.dropdownSystemRegion.isSaveEnabled = false - binding.dropdownSystemRegionStart.isSaveEnabled = false - - viewLifecycleOwner.lifecycleScope.launch { - repeatOnLifecycle(Lifecycle.State.CREATED) { - systemFilesViewModel.shouldRefresh.collect { - if (it) { - reloadUi() - systemFilesViewModel.setShouldRefresh(false) - } - } - } - } - - reloadUi() - if (savedInstanceState != null) { - setDropdownSelection( - binding.dropdownSystemType, - systemTypeDropdown, - savedInstanceState.getInt(SYS_TYPE) - ) - setDropdownSelection( - binding.dropdownSystemRegion, - systemRegionDropdown, - savedInstanceState.getInt(REGION) - ) - binding.dropdownSystemRegionStart - .setText(savedInstanceState.getString(REGION_START), false) - } - - setInsets() - } - - override fun onSaveInstanceState(outState: Bundle) { - outState.putInt(SYS_TYPE, systemTypeDropdown.position) - outState.putInt(REGION, systemRegionDropdown.position) - outState.putString(REGION_START, binding.dropdownSystemRegionStart.text.toString()) - } - - override fun onPause() { - super.onPause() - SystemSaveGame.save() - } - - private fun reloadUi() { - val preferences = PreferenceManager.getDefaultSharedPreferences(CitraApplication.appContext) - - binding.switchRunSystemSetup.isChecked = SystemSaveGame.getIsSystemSetupNeeded() - binding.switchRunSystemSetup.setOnCheckedChangeListener { _, isChecked -> - SystemSaveGame.setSystemSetupNeeded(isChecked) - } - - val showHomeApps = preferences.getBoolean(Settings.PREF_SHOW_HOME_APPS, false) - binding.switchShowApps.isChecked = showHomeApps - binding.switchShowApps.setOnCheckedChangeListener { _, isChecked -> - preferences.edit() - .putBoolean(Settings.PREF_SHOW_HOME_APPS, isChecked) - .apply() - gamesViewModel.setShouldSwapData(true) - } - - if (!NativeLibrary.areKeysAvailable()) { - binding.apply { - systemType.isEnabled = false - systemRegion.isEnabled = false - buttonDownloadHomeMenu.isEnabled = false - textKeysMissing.visibility = View.VISIBLE - textKeysMissingHelp.visibility = View.VISIBLE - textKeysMissingHelp.text = - Html.fromHtml(getString(R.string.how_to_get_keys), Html.FROM_HTML_MODE_LEGACY) - textKeysMissingHelp.movementMethod = LinkMovementMethod.getInstance() - } - } else { - populateDownloadOptions() - } - - binding.buttonDownloadHomeMenu.setOnClickListener { - val titleIds = NativeLibrary.getSystemTitleIds( - systemTypeDropdown.getValue(resources), - systemRegionDropdown.getValue(resources) - ) - - DownloadSystemFilesDialogFragment.newInstance(titleIds).show( - childFragmentManager, - DownloadSystemFilesDialogFragment.TAG - ) - } - - populateHomeMenuOptions() - binding.buttonStartHomeMenu.setOnClickListener { - val menuPath = homeMenuMap[binding.dropdownSystemRegionStart.text.toString()]!! - val menu = Game( - title = getString(R.string.home_menu), - path = menuPath, - filename = "" - ) - // val action = HomeNavigationDirections.actionGlobalEmulationActivity(menu) - // binding.root.findNavController().navigate(action) - VrActivity.launch(CitraApplication.appContext, menu.path, menu.title) - } - } - - private fun populateDropdown( - dropdown: MaterialAutoCompleteTextView, - valuesId: Int, - dropdownItem: DropdownItem - ) { - val valuesAdapter = ArrayAdapter.createFromResource( - requireContext(), - valuesId, - R.layout.support_simple_spinner_dropdown_item - ) - dropdown.setAdapter(valuesAdapter) - dropdown.onItemClickListener = dropdownItem - } - - private fun setDropdownSelection( - dropdown: MaterialAutoCompleteTextView, - dropdownItem: DropdownItem, - selection: Int - ) { - if (dropdown.adapter != null) { - dropdown.setText(dropdown.adapter.getItem(selection).toString(), false) - } - dropdownItem.position = selection - } - - private fun populateDownloadOptions() { - populateDropdown(binding.dropdownSystemType, R.array.systemFileTypes, systemTypeDropdown) - populateDropdown( - binding.dropdownSystemRegion, - R.array.systemFileRegions, - systemRegionDropdown - ) - - setDropdownSelection( - binding.dropdownSystemType, - systemTypeDropdown, - systemTypeDropdown.position - ) - setDropdownSelection( - binding.dropdownSystemRegion, - systemRegionDropdown, - systemRegionDropdown.position - ) - } - - private fun populateHomeMenuOptions() { - regionValues = resources.getIntArray(R.array.systemFileRegionValues) - val regionEntries = resources.getStringArray(R.array.systemFileRegions) - regionValues.forEachIndexed { i: Int, region: Int -> - val regionString = regionEntries[i] - val regionPath = NativeLibrary.getHomeMenuPath(region) - homeMenuMap[regionString] = regionPath - } - - val availableMenus = homeMenuMap.filter { it.value != "" } - if (availableMenus.isNotEmpty()) { - binding.systemRegionStart.isEnabled = true - binding.buttonStartHomeMenu.isEnabled = true - - binding.dropdownSystemRegionStart.setAdapter( - ArrayAdapter( - requireContext(), - R.layout.support_simple_spinner_dropdown_item, - availableMenus.keys.toList() - ) - ) - binding.dropdownSystemRegionStart.setText(availableMenus.keys.first(), false) - } - } - - 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 mlpAppBar = binding.toolbarSystemFiles.layoutParams as ViewGroup.MarginLayoutParams - mlpAppBar.leftMargin = leftInsets - mlpAppBar.rightMargin = rightInsets - binding.toolbarSystemFiles.layoutParams = mlpAppBar - - val mlpScrollSystemFiles = - binding.scrollSystemFiles.layoutParams as ViewGroup.MarginLayoutParams - mlpScrollSystemFiles.leftMargin = leftInsets - mlpScrollSystemFiles.rightMargin = rightInsets - binding.scrollSystemFiles.layoutParams = mlpScrollSystemFiles - - binding.scrollSystemFiles.updatePadding(bottom = barInsets.bottom) - - windowInsets - } -} diff --git a/src/android/app/src/main/java/org/citra/citra_emu/viewmodel/SystemFilesViewModel.kt b/src/android/app/src/main/java/org/citra/citra_emu/viewmodel/SystemFilesViewModel.kt deleted file mode 100644 index d4f654d5c..000000000 --- a/src/android/app/src/main/java/org/citra/citra_emu/viewmodel/SystemFilesViewModel.kt +++ /dev/null @@ -1,139 +0,0 @@ -// Copyright 2023 Citra Emulator Project -// Licensed under GPLv2 or any later version -// Refer to the license.txt file included. - -package org.citra.citra_emu.viewmodel - -import androidx.lifecycle.ViewModel -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.Job -import kotlinx.coroutines.cancelChildren -import kotlinx.coroutines.delay -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.launch -import kotlinx.coroutines.yield -import org.citra.citra_emu.NativeLibrary -import org.citra.citra_emu.NativeLibrary.InstallStatus -import org.citra.citra_emu.utils.Log -import java.util.concurrent.atomic.AtomicInteger -import kotlin.coroutines.CoroutineContext -import kotlin.math.min - -class SystemFilesViewModel : ViewModel() { - private var job: Job - private val coroutineContext: CoroutineContext - get() = Dispatchers.IO + job - - val isDownloading get() = _isDownloading.asStateFlow() - private val _isDownloading = MutableStateFlow(false) - - val progress get() = _progress.asStateFlow() - private val _progress = MutableStateFlow(0) - - val result get() = _result.asStateFlow() - private val _result = MutableStateFlow(null) - - val shouldRefresh get() = _shouldRefresh.asStateFlow() - private val _shouldRefresh = MutableStateFlow(false) - - private var cancelled = false - - private val RETRY_AMOUNT = 3 - - init { - job = Job() - clear() - } - - fun setShouldRefresh(refresh: Boolean) { - _shouldRefresh.value = refresh - } - - fun setProgress(progress: Int) { - _progress.value = progress - } - - fun download(titles: LongArray) { - if (isDownloading.value) { - return - } - clear() - _isDownloading.value = true - Log.debug("System menu download started.") - - val minExecutors = min(Runtime.getRuntime().availableProcessors(), titles.size) - val segment = (titles.size / minExecutors) - val atomicProgress = AtomicInteger(0) - for (i in 0 until minExecutors) { - val titlesSegment = if (i < minExecutors - 1) { - titles.copyOfRange(i * segment, (i + 1) * segment) - } else { - titles.copyOfRange(i * segment, titles.size) - } - - CoroutineScope(coroutineContext).launch { - titlesSegment.forEach { title: Long -> - // Notify UI of cancellation before ending coroutine - if (cancelled) { - _result.value = InstallStatus.ErrorAborted - cancelled = false - } - - // Takes a moment to see if the coroutine was cancelled - yield() - - // Retry downloading a title repeatedly - for (j in 0 until RETRY_AMOUNT) { - val result = tryDownloadTitle(title) - if (result == InstallStatus.Success) { - break - } else if (j == RETRY_AMOUNT - 1) { - _result.value = result - return@launch - } - Log.warning("Download for title{$title} failed, retrying in 3s...") - delay(3000L) - } - - Log.debug("Successfully installed title - $title") - setProgress(atomicProgress.incrementAndGet()) - - Log.debug("System File Progress - ${atomicProgress.get()} / ${titles.size}") - if (atomicProgress.get() == titles.size) { - _result.value = InstallStatus.Success - setShouldRefresh(true) - } - } - } - } - } - - private fun tryDownloadTitle(title: Long): InstallStatus { - val result = NativeLibrary.downloadTitleFromNus(title) - if (result != InstallStatus.Success) { - Log.error("Failed to install title $title with error - $result") - } - return result - } - - fun clear() { - Log.debug("Clearing") - job.cancelChildren() - job = Job() - _progress.value = 0 - _result.value = null - _isDownloading.value = false - cancelled = false - } - - fun cancel() { - Log.debug("Canceling system file download.") - cancelled = true - job.cancelChildren() - job = Job() - _progress.value = 0 - _result.value = InstallStatus.Cancelled - } -} diff --git a/src/android/app/src/main/res/layout/fragment_system_files.xml b/src/android/app/src/main/res/layout/fragment_system_files.xml deleted file mode 100644 index 6c833f876..000000000 --- a/src/android/app/src/main/res/layout/fragment_system_files.xml +++ /dev/null @@ -1,219 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -