From 42d77cd720eb42845c2afb77c6d7157e02c8c325 Mon Sep 17 00:00:00 2001 From: PabloMK7 Date: Wed, 12 Mar 2025 22:41:45 +0100 Subject: [PATCH] Implement "Set Up System Files" on Android (#653) * Implement "Set Up System Files" on Android * Use correct strings + Remove chunks of unused code * Updated license header * SystemFilesFragment.kt: Use radio buttons for selecting O3DS/N3DS * HomeSettingsFragment.kt: Moved `Install CIA` above `Set Up System Files` * strings.xml: Updated system file setup button strings * android: Remove System Files Warning This warning is no longer relevant due to changes in how system files are installed --------- Co-authored-by: OpenSauce04 --- .../java/org/citra/citra_emu/NativeLibrary.kt | 6 +- .../DownloadSystemFilesDialogFragment.kt | 152 -------- .../fragments/HomeSettingsFragment.kt | 18 +- .../fragments/SystemFilesFragment.kt | 368 ++++++++++-------- .../viewmodel/SystemFilesViewModel.kt | 139 ------- src/android/app/src/main/jni/native.cpp | 45 ++- .../layout-w600dp/fragment_system_files.xml | 219 ----------- .../main/res/layout/fragment_system_files.xml | 80 ++-- .../app/src/main/res/values/arrays.xml | 11 - .../app/src/main/res/values/strings.xml | 35 +- src/citra_qt/citra_qt.cpp | 7 +- src/core/hle/service/am/am.cpp | 14 +- src/core/hle/service/http/http_c.cpp | 6 +- 13 files changed, 299 insertions(+), 801 deletions(-) delete mode 100644 src/android/app/src/main/java/org/citra/citra_emu/fragments/DownloadSystemFilesDialogFragment.kt delete mode 100644 src/android/app/src/main/java/org/citra/citra_emu/viewmodel/SystemFilesViewModel.kt delete mode 100644 src/android/app/src/main/res/layout-w600dp/fragment_system_files.xml 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 40dbc94c9..f176f6b87 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 @@ -1,4 +1,4 @@ -// Copyright 2023 Citra Emulator Project +// Copyright Citra Emulator Project / Azahar Emulator Project // Licensed under GPLv2 or any later version // Refer to the license.txt file included. @@ -178,7 +178,9 @@ object NativeLibrary { external fun getSystemTitleIds(systemType: Int, region: Int): LongArray - external fun downloadTitleFromNus(title: Long): InstallStatus + external fun areSystemTitlesInstalled(): BooleanArray + + external fun uninstallSystemFiles(old3DS: Boolean) private var coreErrorAlertResult = false private val coreErrorAlertLock = Object() 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 e7ff2ba34..432a06aa0 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 @@ -1,4 +1,4 @@ -// Copyright 2023 Citra Emulator Project +// Copyright Citra Emulator Project / Azahar Emulator Project // Licensed under GPLv2 or any later version // Refer to the license.txt file included. @@ -121,8 +121,14 @@ class HomeSettingsFragment : Fragment() { } ), HomeSetting( - R.string.system_files, - R.string.system_files_description, + R.string.install_game_content, + R.string.install_game_content_description, + R.drawable.ic_install, + { mainActivity.ciaFileInstaller.launch(true) } + ), + HomeSetting( + R.string.setup_system_files, + R.string.setup_system_files_description, R.drawable.ic_system_update, { exitTransition = MaterialSharedAxis(MaterialSharedAxis.X, true) @@ -130,12 +136,6 @@ class HomeSettingsFragment : Fragment() { ?.navigate(R.id.action_homeSettingsFragment_to_systemFilesFragment) } ), - HomeSetting( - R.string.install_game_content, - R.string.install_game_content_description, - R.drawable.ic_install, - { mainActivity.ciaFileInstaller.launch(true) } - ), HomeSetting( R.string.share_log, R.string.share_log_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 index 586823424..649273f2c 100644 --- 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 @@ -1,76 +1,60 @@ -// Copyright 2023 Citra Emulator Project +// Copyright Citra Emulator Project / Azahar 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.Gravity 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 android.widget.FrameLayout +import android.widget.LinearLayout +import android.widget.RadioButton +import android.widget.RadioGroup +import androidx.appcompat.app.AlertDialog +import androidx.core.content.ContextCompat +import androidx.core.text.HtmlCompat +import androidx.core.widget.doOnTextChanged 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.dialog.MaterialAlertDialogBuilder +import com.google.android.material.progressindicator.CircularProgressIndicator +import com.google.android.material.textview.MaterialTextView import com.google.android.material.transition.MaterialSharedAxis +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext 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.DialogSoftwareKeyboardBinding 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 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 - } - } + private var setupStateCached: BooleanArray? = null + private lateinit var regionValues: IntArray override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -92,61 +76,15 @@ class SystemFilesFragment : Fragment() { 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() { @@ -154,6 +92,41 @@ class SystemFilesFragment : Fragment() { SystemSaveGame.save() } + private fun showProgressDialog( + main_title: CharSequence, + main_text: CharSequence + ): AlertDialog? { + val context = requireContext() + val progressIndicator = CircularProgressIndicator(context).apply { + isIndeterminate = true + layoutParams = FrameLayout.LayoutParams( + FrameLayout.LayoutParams.WRAP_CONTENT, + FrameLayout.LayoutParams.WRAP_CONTENT, + Gravity.CENTER // Center the progress indicator + ).apply { + setMargins(50, 50, 50, 50) // Add margins (left, top, right, bottom) + } + } + + val pleaseWaitText = MaterialTextView(context).apply { + text = main_text + } + + val container = LinearLayout(context).apply { + orientation = LinearLayout.VERTICAL + gravity = Gravity.CENTER + setPadding(40, 40, 40, 40) // Optional: Add padding to the entire layout + addView(pleaseWaitText) + addView(progressIndicator) + } + + return MaterialAlertDialogBuilder(context) + .setTitle(main_title) + .setView(container) + .setCancelable(false) + .show() + } + private fun reloadUi() { val preferences = PreferenceManager.getDefaultSharedPreferences(CitraApplication.appContext) @@ -171,31 +144,151 @@ class SystemFilesFragment : Fragment() { 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.setupSystemFilesDescription?.apply { + text = HtmlCompat.fromHtml( + context.getString(R.string.setup_system_files_preamble), + HtmlCompat.FROM_HTML_MODE_COMPACT + ) + movementMethod = LinkMovementMethod.getInstance() } - binding.buttonDownloadHomeMenu.setOnClickListener { - val titleIds = NativeLibrary.getSystemTitleIds( - systemTypeDropdown.getValue(resources), - systemRegionDropdown.getValue(resources) + binding.buttonSetUpSystemFiles.setOnClickListener { + val inflater = LayoutInflater.from(context) + val inputBinding = DialogSoftwareKeyboardBinding.inflate(inflater) + var textInputValue: String = preferences.getString("last_artic_base_addr", "")!! + + val progressDialog = showProgressDialog( + getText(R.string.setup_system_files), + getString(R.string.setup_system_files_detect) ) - DownloadSystemFilesDialogFragment.newInstance(titleIds).show( - childFragmentManager, - DownloadSystemFilesDialogFragment.TAG - ) + CoroutineScope(Dispatchers.IO).launch { + val setupState = setupStateCached ?: NativeLibrary.areSystemTitlesInstalled().also { + setupStateCached = it + } + + withContext(Dispatchers.Main) { + progressDialog?.dismiss() + + inputBinding.editTextInput.setText(textInputValue) + inputBinding.editTextInput.doOnTextChanged { text, _, _, _ -> + textInputValue = text.toString() + } + + val buttonGroup = context?.let { it1 -> RadioGroup(it1) }!! + + val buttonO3ds = context?.let { it1 -> + RadioButton(it1).apply { + text = context.getString(R.string.setup_system_files_o3ds) + isChecked = false + } + }!! + + val buttonN3ds = context?.let { it1 -> + RadioButton(it1).apply { + text = context.getString(R.string.setup_system_files_n3ds) + isChecked = false + } + }!! + + val textO3ds: String + val textN3ds: String + + val colorO3ds: Int + val colorN3ds: Int + + if (!setupStateCached!![0]) { + textO3ds = getString(R.string.setup_system_files_possible) + colorO3ds = R.color.citra_primary_blue + + textN3ds = getString(R.string.setup_system_files_o3ds_needed) + colorN3ds = R.color.citra_primary_yellow + + buttonN3ds.isEnabled = false + } else { + textO3ds = getString(R.string.setup_system_files_completed) + colorO3ds = R.color.citra_primary_green + + if (!setupStateCached!![1]) { + textN3ds = getString(R.string.setup_system_files_possible) + colorN3ds = R.color.citra_primary_blue + } else { + textN3ds = getString(R.string.setup_system_files_completed) + colorN3ds = R.color.citra_primary_green + } + } + + val tooltipO3ds = context?.let { it1 -> + MaterialTextView(it1).apply { + text = textO3ds + textSize = 12f + setTextColor(ContextCompat.getColor(requireContext(), colorO3ds)) + } + } + + val tooltipN3ds = context?.let { it1 -> + MaterialTextView(it1).apply { + text = textN3ds + textSize = 12f + setTextColor(ContextCompat.getColor(requireContext(), colorN3ds)) + } + } + + buttonGroup.apply { + addView(buttonO3ds) + addView(tooltipO3ds) + addView(buttonN3ds) + addView(tooltipN3ds) + } + + inputBinding.root.apply { + addView(buttonGroup) + } + + val dialog = context?.let { + MaterialAlertDialogBuilder(it) + .setView(inputBinding.root) + .setTitle(getString(R.string.setup_system_files_enter_address)) + .setPositiveButton(android.R.string.ok) { diag, _ -> + if (textInputValue.isNotEmpty() && !(!buttonO3ds.isChecked && !buttonN3ds.isChecked)) { + preferences.edit() + .putString("last_artic_base_addr", textInputValue) + .apply() + val menu = Game( + title = getString(R.string.artic_base), + path = if (buttonO3ds.isChecked) { + "articinio://$textInputValue" + } else { + "articinin://$textInputValue" + }, + filename = "" + ) + val progressDialog2 = showProgressDialog( + getText(R.string.setup_system_files), + getString( + R.string.setup_system_files_preparing + ) + ) + + CoroutineScope(Dispatchers.IO).launch { + NativeLibrary.uninstallSystemFiles(buttonO3ds.isChecked) + withContext(Dispatchers.Main) { + setupStateCached = null + progressDialog2?.dismiss() + val action = + HomeNavigationDirections.actionGlobalEmulationActivity( + menu + ) + binding.root.findNavController().navigate(action) + } + } + } + } + .setNegativeButton(android.R.string.cancel) { _, _ -> } + .show() + } + } + } } populateHomeMenuOptions() @@ -211,51 +304,6 @@ class SystemFilesFragment : Fragment() { } } - 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) @@ -280,30 +328,4 @@ class SystemFilesFragment : Fragment() { 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/jni/native.cpp b/src/android/app/src/main/jni/native.cpp index d4263c18a..a88a4036c 100644 --- a/src/android/app/src/main/jni/native.cpp +++ b/src/android/app/src/main/jni/native.cpp @@ -38,6 +38,7 @@ #include "core/hle/service/nfc/nfc.h" #include "core/loader/loader.h" #include "core/savestate.h" +#include "core/system_titles.h" #include "jni/android_common/android_common.h" #include "jni/applets/mii_selector.h" #include "jni/applets/swkbd.h" @@ -229,7 +230,10 @@ static Core::System::ResultStatus RunCitra(const std::string& filepath) { if (result == Core::System::ResultStatus::ShutdownRequested) { return result; // This also exits the emulation activity } else { - InputManager::NDKMotionHandler()->DisableSensors(); + auto* handler = InputManager::NDKMotionHandler(); + if (handler) { + handler->DisableSensors(); + } if (!HandleCoreError(result, system.GetStatusDetails())) { // Frontend requests us to abort // If the error was an Artic disconnect, return shutdown request. @@ -238,7 +242,10 @@ static Core::System::ResultStatus RunCitra(const std::string& filepath) { } return result; } - InputManager::NDKMotionHandler()->EnableSensors(); + handler = InputManager::NDKMotionHandler(); + if (handler) { + handler->EnableSensors(); + } } } else { // Ensure no audio bleeds out while game is paused @@ -435,11 +442,25 @@ jlongArray Java_org_citra_citra_1emu_NativeLibrary_getSystemTitleIds(JNIEnv* env return jTitles; } -jobject Java_org_citra_citra_1emu_NativeLibrary_downloadTitleFromNus([[maybe_unused]] JNIEnv* env, - [[maybe_unused]] jobject obj, - jlong title) { - [[maybe_unused]] const auto title_id = static_cast(title); - return IDCache::GetJavaCiaInstallStatus(Service::AM::InstallStatus::ErrorAborted); +jbooleanArray Java_org_citra_citra_1emu_NativeLibrary_areSystemTitlesInstalled( + JNIEnv* env, [[maybe_unused]] jobject obj) { + const auto installed = Core::AreSystemTitlesInstalled(); + jbooleanArray jInstalled = env->NewBooleanArray(2); + jboolean* elements = env->GetBooleanArrayElements(jInstalled, nullptr); + + elements[0] = installed.first ? JNI_TRUE : JNI_FALSE; + elements[1] = installed.second ? JNI_TRUE : JNI_FALSE; + + env->ReleaseBooleanArrayElements(jInstalled, elements, 0); + + return jInstalled; +} + +void Java_org_citra_citra_1emu_NativeLibrary_uninstallSystemFiles(JNIEnv* env, + [[maybe_unused]] jobject obj, + jboolean old3ds) { + Core::UninstallSystemFiles(old3ds ? Core::SystemTitleSet::Old3ds + : Core::SystemTitleSet::New3ds); } [[maybe_unused]] static bool CheckKgslPresent() { @@ -467,13 +488,19 @@ void Java_org_citra_citra_1emu_NativeLibrary_unPauseEmulation([[maybe_unused]] J [[maybe_unused]] jobject obj) { pause_emulation = false; running_cv.notify_all(); - InputManager::NDKMotionHandler()->EnableSensors(); + auto* handler = InputManager::NDKMotionHandler(); + if (handler) { + handler->EnableSensors(); + } } void Java_org_citra_citra_1emu_NativeLibrary_pauseEmulation([[maybe_unused]] JNIEnv* env, [[maybe_unused]] jobject obj) { pause_emulation = true; - InputManager::NDKMotionHandler()->DisableSensors(); + auto* handler = InputManager::NDKMotionHandler(); + if (handler) { + handler->DisableSensors(); + } } void Java_org_citra_citra_1emu_NativeLibrary_stopEmulation([[maybe_unused]] JNIEnv* env, diff --git a/src/android/app/src/main/res/layout-w600dp/fragment_system_files.xml b/src/android/app/src/main/res/layout-w600dp/fragment_system_files.xml deleted file mode 100644 index 6c833f876..000000000 --- a/src/android/app/src/main/res/layout-w600dp/fragment_system_files.xml +++ /dev/null @@ -1,219 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -