Implement "Set Up System Files" on Android

This commit is contained in:
PabloMK7 2025-03-10 15:40:27 +01:00 committed by OpenSauce04
parent 57b5f7da17
commit 5e06d5d5e7
11 changed files with 310 additions and 435 deletions

View File

@ -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()

View File

@ -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
}
}
}

View File

@ -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)

View File

@ -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<InstallStatus?>(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
}
}

View File

@ -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<u64>(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,

View File

@ -87,7 +87,7 @@
</com.google.android.material.textfield.TextInputLayout>
<Button
android:id="@+id/button_download_home_menu"
android:id="@+id/button_set_up_system_files"
style="@style/Widget.Material3.Button.UnelevatedButton"
android:layout_width="match_parent"
android:layout_height="wrap_content"

View File

@ -39,77 +39,41 @@
android:paddingBottom="16dp">
<com.google.android.material.textview.MaterialTextView
style="@style/TextAppearance.Material3.TitleMedium"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="@string/setup_system_files"
android:textAlignment="viewStart" />
<com.google.android.material.textview.MaterialTextView
android:id="@+id/setupSystemFilesDescription"
style="@style/TextAppearance.Material3.TitleSmall"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="@string/download_system_files"
android:textAlignment="viewStart" />
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/system_type"
style="@style/Widget.Material3.TextInputLayout.OutlinedBox.ExposedDropdownMenu"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:hint="@string/system_type"
app:layout_constraintTop_toTopOf="parent">
<com.google.android.material.textfield.MaterialAutoCompleteTextView
android:id="@+id/dropdown_system_type"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="none" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/system_region"
style="@style/Widget.Material3.TextInputLayout.OutlinedBox.ExposedDropdownMenu"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:hint="@string/emulated_region"
app:layout_constraintTop_toTopOf="parent">
<com.google.android.material.textfield.MaterialAutoCompleteTextView
android:id="@+id/dropdown_system_region"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="none" />
</com.google.android.material.textfield.TextInputLayout>
<Button
android:id="@+id/button_download_home_menu"
android:id="@+id/button_set_up_system_files"
style="@style/Widget.Material3.Button.UnelevatedButton"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="@string/download" />
android:text="@string/setup_tool_connect" />
<View
android:id="@+id/divider2"
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_marginTop="24dp"
android:background="?android:attr/listDivider"
android:padding="40px" />
<com.google.android.material.textview.MaterialTextView
android:id="@+id/text_keys_missing"
style="@style/TextAppearance.Material3.TitleMedium"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="@string/keys_missing"
android:textAlignment="viewStart"
android:visibility="gone" />
<com.google.android.material.textview.MaterialTextView
android:id="@+id/text_keys_missing_help"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textAlignment="viewStart"
android:visibility="gone"
tools:text="How to get keys?" />
<com.google.android.material.textview.MaterialTextView
style="@style/TextAppearance.Material3.TitleSmall"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="48dp"
android:layout_marginTop="24dp"
android:text="@string/boot_home_menu"
android:textAlignment="viewStart" />

View File

@ -785,5 +785,16 @@
<string name="delay_start_lle_modules_description">Delays the start of the app when LLE modules are enabled.</string>
<string name="deterministic_async_operations">Deterministic Async Operations</string>
<string name="deterministic_async_operations_description">Makes async operations deterministic for debugging. Enabling this may cause freezes.</string>
<string name="setup_system_files">Set Up System Files</string>
<string name="setup_tool_connect">Connect to Artic Setup Tool</string>
<string name="setup_system_files_description"><![CDATA[Azahar needs files from a real console to be able to use some of its features. You can get such files with the <a href=https://github.com/azahar-emu/ArticSetupTool>Azahar Artic Setup Tool</a>.<br> Notes:<ul><li><b>This operation will install console unique files to Azahar, do not share your user or nand folders<br>after performing the setup process!</b></li><li>Old 3DS setup is needed for the New 3DS setup to work.</li><li>Both setup modes will work regardless of the model of the console running the setup tool.</li></ul>]]></string>
<string name="setup_system_files_detect">Fetching current system files status, please wait...</string>
<string name="setup_system_files_o3ds">Old 3DS Setup</string>
<string name="setup_system_files_n3ds">New 3DS Setup</string>
<string name="setup_system_files_possible">Setup is possible.</string>
<string name="setup_system_files_o3ds_needed">Old 3DS setup is required first.</string>
<string name="setup_system_files_completed">Setup already completed.</string>
<string name="setup_system_files_enter_address">Enter Artic Setup Tool address</string>
<string name="setup_system_files_preparing">Preparing setup, please wait...</string>
</resources>

View File

@ -2139,6 +2139,8 @@ void GMainWindow::OnMenuSetUpSystemFiles() {
QRadioButton radio1(&dialog);
QRadioButton radio2(&dialog);
if (!install_state.first) {
radio1.setChecked(true);
radio1.setText(tr("(\u2139\uFE0F) Old 3DS setup"));
radio1.setToolTip(tr("Setup is possible."));
@ -2150,14 +2152,17 @@ void GMainWindow::OnMenuSetUpSystemFiles() {
radio1.setToolTip(tr("Setup completed."));
if (!install_state.second) {
radio2.setChecked(true);
radio2.setText(tr("(\u2139\uFE0F) New 3DS setup"));
radio2.setToolTip(tr("Setup is possible."));
} else {
radio1.setChecked(true);
radio2.setText(tr("(\u2705) New 3DS setup"));
radio2.setToolTip(tr("Setup completed."));
}
}
radio1.setChecked(true);
layout.addWidget(&radio1);
layout.addWidget(&radio2);

View File

@ -1184,6 +1184,8 @@ std::string GetMediaTitlePath(Service::FS::MediaType media_type) {
void Module::ScanForTickets() {
am_ticket_list.clear();
LOG_DEBUG(Service_AM, "Starting ticket scan");
std::string ticket_path = GetTicketDirectory();
FileUtil::FSTEntry entries;
@ -1204,11 +1206,14 @@ void Module::ScanForTickets() {
}
}
}
LOG_DEBUG(Service_AM, "Finished ticket scan");
}
void Module::ScanForTitles(Service::FS::MediaType media_type) {
am_title_list[static_cast<u32>(media_type)].clear();
LOG_DEBUG(Service_AM, "Starting title scan for media_type={}", static_cast<int>(media_type));
std::string title_path = GetMediaTitlePath(media_type);
FileUtil::FSTEntry entries;
@ -1236,6 +1241,7 @@ void Module::ScanForTitles(Service::FS::MediaType media_type) {
}
}
}
LOG_DEBUG(Service_AM, "Finished title scan for media_type={}", static_cast<int>(media_type));
}
void Module::ScanForAllTitles() {
@ -2272,7 +2278,7 @@ void Module::Interface::DeleteTicket(Kernel::HLERequestContext& ctx) {
FileUtil::Delete(path);
}
am->ScanForTickets();
am->am_ticket_list.erase(range.first, range.second);
rb.Push(ResultSuccess);
}
@ -3279,7 +3285,8 @@ void Module::Interface::EndImportTicket(Kernel::HLERequestContext& ctx) {
auto ticket_file = GetFileBackendFromSession<TicketFile>(ticket);
if (ticket_file.Succeeded()) {
rb.Push(ticket_file.Unwrap()->Commit());
am->ScanForTickets();
am->am_ticket_list.insert(std::make_pair(ticket_file.Unwrap()->GetTitleID(),
ticket_file.Unwrap()->GetTicketID()));
} else {
rb.Push(ticket_file.Code());
}
@ -3416,7 +3423,6 @@ void Module::Interface::EndImportTitle(Kernel::HLERequestContext& ctx) {
}
am->importing_title->cia_file.SetDone();
am->ScanForTitles(am->importing_title->media_type);
am->importing_title.reset();
IPC::RequestBuilder rb = rp.MakeBuilder(1, 0);
@ -3825,7 +3831,7 @@ void Module::Interface::DeleteTicketId(Kernel::HLERequestContext& ctx) {
auto path = GetTicketPath(title_id, ticket_id);
FileUtil::Delete(path);
am->ScanForTickets();
am->am_ticket_list.erase(it);
rb.Push(ResultSuccess);
}

View File

@ -1,4 +1,4 @@
// Copyright 2014 Citra Emulator Project
// Copyright Citra Emulator Project / Azahar Emulator Project
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.
@ -740,8 +740,8 @@ void HTTP_C::CreateContext(Kernel::HLERequestContext& ctx) {
Kernel::MappedBuffer& buffer = rp.PopMappedBuffer();
// Copy the buffer into a string without the \0 at the end of the buffer
std::string url(url_size, '\0');
buffer.Read(&url[0], 0, url_size - 1);
std::string url(url_size - 1, '\0');
buffer.Read(url.data(), 0, url_size - 1);
LOG_DEBUG(Service_HTTP, "called, url_size={}, url={}, method={}", url_size, url, method);