From 5e06d5d5e7238760323d75e30245b64ff4c7e2fc Mon Sep 17 00:00:00 2001 From: PabloMK7 Date: Mon, 10 Mar 2025 15:40:27 +0100 Subject: [PATCH] Implement "Set Up System Files" on Android --- .../java/org/citra/citra_emu/NativeLibrary.kt | 6 +- .../DownloadSystemFilesDialogFragment.kt | 152 ---------- .../fragments/SystemFilesFragment.kt | 285 ++++++++++++++---- .../viewmodel/SystemFilesViewModel.kt | 139 --------- src/android/app/src/main/jni/native.cpp | 45 ++- .../layout-w600dp/fragment_system_files.xml | 2 +- .../main/res/layout/fragment_system_files.xml | 78 ++--- .../app/src/main/res/values/strings.xml | 11 + src/citra_qt/citra_qt.cpp | 7 +- src/core/hle/service/am/am.cpp | 14 +- src/core/hle/service/http/http_c.cpp | 6 +- 11 files changed, 310 insertions(+), 435 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 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/SystemFilesFragment.kt b/src/android/app/src/main/java/org/citra/citra_emu/fragments/SystemFilesFragment.kt index 586823424..931025d4c 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,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. @@ -8,14 +8,21 @@ 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 android.widget.FrameLayout +import android.widget.LinearLayout +import androidx.appcompat.app.AlertDialog +import androidx.core.content.ContextCompat +import androidx.core.text.HtmlCompat import androidx.core.view.ViewCompat import androidx.core.view.WindowInsetsCompat import androidx.core.view.updatePadding +import androidx.core.widget.doOnTextChanged import androidx.fragment.app.Fragment import androidx.fragment.app.activityViewModels import androidx.lifecycle.Lifecycle @@ -23,28 +30,34 @@ import androidx.lifecycle.lifecycleScope import androidx.lifecycle.repeatOnLifecycle import androidx.navigation.findNavController import androidx.preference.PreferenceManager +import com.google.android.material.dialog.MaterialAlertDialogBuilder +import com.google.android.material.progressindicator.CircularProgressIndicator +import com.google.android.material.switchmaterial.SwitchMaterial import com.google.android.material.textfield.MaterialAutoCompleteTextView +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 @@ -60,6 +73,8 @@ class SystemFilesFragment : Fragment() { private val WARNING_SHOWN = "SystemFilesWarningShown" + private var setupStateCached: BooleanArray? = null + private class DropdownItem(val valuesId: Int) : AdapterView.OnItemClickListener { var position = 0 @@ -109,33 +124,10 @@ class SystemFilesFragment : Fragment() { // 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) } @@ -154,6 +146,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 +198,175 @@ 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_description), + 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 switchOption1 = context?.let { it1 -> + SwitchMaterial(it1).apply { + text = context.getString(R.string.setup_system_files_o3ds) + isChecked = false + } + } + + val switchOption2 = context?.let { it1 -> + SwitchMaterial(it1).apply { + text = context.getString(R.string.setup_system_files_n3ds) + isChecked = false + } + } + + if (setupStateCached!![0] && !setupStateCached!![1]) { + switchOption2?.isChecked = true + } else { + switchOption1?.isChecked = true + } + + switchOption1?.setOnCheckedChangeListener { _, isChecked -> + if (isChecked) { + switchOption2!!.isChecked = false + } + } + + switchOption2?.setOnCheckedChangeListener { _, isChecked -> + if (isChecked) { + switchOption1!!.isChecked = false + } + } + + switchOption1?.setOnClickListener { + if (!switchOption1.isChecked && !switchOption2!!.isChecked) { + switchOption1.isChecked = true + } + } + + switchOption2?.setOnClickListener { + if (!switchOption2.isChecked && !switchOption1!!.isChecked) { + switchOption2.isChecked = true + } + } + + 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 + + switchOption2?.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 tooltipOption1 = context?.let { it1 -> + MaterialTextView(it1).apply { + text = textO3ds + textSize = 12f + setTextColor(ContextCompat.getColor(requireContext(), colorO3ds)) + } + } + + val tooltipOption2 = context?.let { it1 -> + MaterialTextView(it1).apply { + text = textN3ds + textSize = 12f + setTextColor(ContextCompat.getColor(requireContext(), colorN3ds)) + } + } + + inputBinding.root.apply { + addView(switchOption1) + addView(tooltipOption1) + addView(switchOption2) + addView(tooltipOption2) + } + + 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()) { + preferences.edit() + .putString("last_artic_base_addr", textInputValue) + .apply() + val menu = Game( + title = getString(R.string.artic_base), + path = if (switchOption1!!.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(switchOption1.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() @@ -236,26 +407,6 @@ class SystemFilesFragment : Fragment() { 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) 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 index 6c833f876..9ea3b194d 100644 --- 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 @@ -87,7 +87,7 @@