Restore features

This commit is contained in:
AzaharPlus
2025-03-31 12:39:28 +02:00
parent 5ade69f5f4
commit 3e64dc0c8c
51 changed files with 2462 additions and 765 deletions

View File

@@ -63,6 +63,7 @@ android {
// The application ID refers to Lime3DS to allow for
// the Play Store listing, which was originally set up for Lime3DS, to still be used.
applicationId = "io.github.lime3ds.android"
minSdk = 28
targetSdk = 35
versionCode = autoVersion

View File

@@ -186,6 +186,8 @@ object NativeLibrary {
external fun unlinkConsole()
external fun downloadTitleFromNus(title: Long): InstallStatus
private var coreErrorAlertResult = false
private val coreErrorAlertLock = Object()

View File

@@ -0,0 +1,152 @@
// 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

@@ -45,7 +45,7 @@ class GamesFragment : Fragment() {
private val gamesViewModel: GamesViewModel by activityViewModels()
private val homeViewModel: HomeViewModel by activityViewModels()
private var show3DSFileWarning: Boolean = true
private var show3DSFileWarning: Boolean = false
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)

View File

@@ -1,4 +1,4 @@
// Copyright Citra Emulator Project / Azahar Emulator Project
// Copyright 2023 Citra Emulator Project
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.
@@ -121,14 +121,8 @@ class HomeSettingsFragment : Fragment() {
}
),
HomeSetting(
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.string.system_files,
R.string.system_files_description,
R.drawable.ic_system_update,
{
exitTransition = MaterialSharedAxis(MaterialSharedAxis.X, true)
@@ -136,6 +130,12 @@ 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,

View File

@@ -1,61 +1,76 @@
// Copyright Citra Emulator Project / Azahar Emulator Project
// 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.DialogInterface
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 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.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.dialog.MaterialAlertDialogBuilder
import com.google.android.material.progressindicator.CircularProgressIndicator
import com.google.android.material.textview.MaterialTextView
import com.google.android.material.textfield.MaterialAutoCompleteTextView
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.databinding.DialogSoftwareKeyboardBinding
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
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<String, String> = mutableMapOf()
private var setupStateCached: BooleanArray? = null
private lateinit var regionValues: IntArray
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)
@@ -77,15 +92,61 @@ 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 onResume() {
@@ -98,41 +159,6 @@ 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)
@@ -150,167 +176,31 @@ class SystemFilesFragment : Fragment() {
gamesViewModel.setShouldSwapData(true)
}
binding.setupSystemFilesDescription?.apply {
text = HtmlCompat.fromHtml(
context.getString(R.string.setup_system_files_preamble),
HtmlCompat.FROM_HTML_MODE_COMPACT
)
movementMethod = LinkMovementMethod.getInstance()
}
binding.buttonUnlinkConsoleData.isEnabled = NativeLibrary.isFullConsoleLinked()
binding.buttonUnlinkConsoleData.setOnClickListener {
MaterialAlertDialogBuilder(requireContext())
.setTitle(R.string.delete_system_files)
.setMessage(HtmlCompat.fromHtml(
requireContext().getString(R.string.delete_system_files_description),
HtmlCompat.FROM_HTML_MODE_COMPACT
))
.setPositiveButton(android.R.string.ok) { _: DialogInterface, _: Int ->
NativeLibrary.unlinkConsole()
binding.buttonUnlinkConsoleData.isEnabled = NativeLibrary.isFullConsoleLinked()
}
.setNegativeButton(android.R.string.cancel, null)
.show()
}
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)
)
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()
}
}
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()
@@ -326,6 +216,51 @@ 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)
@@ -350,4 +285,30 @@ 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
}
}

View File

@@ -63,7 +63,7 @@ class Game(
val allExtensions: Set<String> get() = extensions + badExtensions
val extensions: Set<String> = HashSet(
listOf("3dsx", "elf", "axf", "cci", "cxi", "app")
listOf("3ds", "3dsx", "elf", "axf", "cci", "cxi", "app")
)
val badExtensions: Set<String> = HashSet(

View File

@@ -0,0 +1,139 @@
// 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

@@ -464,6 +464,17 @@ void Java_org_citra_citra_1emu_NativeLibrary_uninstallSystemFiles(JNIEnv* env,
: Core::SystemTitleSet::New3ds);
}
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);
Service::AM::InstallStatus status = Service::AM::InstallFromNus(title_id);
if (status != Service::AM::InstallStatus::Success) {
return IDCache::GetJavaCiaInstallStatus(status);
}
return IDCache::GetJavaCiaInstallStatus(Service::AM::InstallStatus::Success);
}
[[maybe_unused]] static bool CheckKgslPresent() {
constexpr auto KgslPath{"/dev/kgsl-3d0"};

View File

@@ -0,0 +1,219 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/coordinator_about"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="?attr/colorSurface">
<com.google.android.material.appbar.AppBarLayout
android:id="@+id/appbar_about"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:fitsSystemWindows="true">
<com.google.android.material.appbar.MaterialToolbar
android:id="@+id/toolbar_system_files"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
app:navigationIcon="@drawable/ic_back"
app:title="@string/system_files" />
</com.google.android.material.appbar.AppBarLayout>
<androidx.core.widget.NestedScrollView
android:id="@+id/scroll_system_files"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fadeScrollbars="false"
android:scrollbars="vertical"
android:clipToPadding="false"
app:layout_behavior="@string/appbar_scrolling_view_behavior">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginHorizontal="24dp"
android:orientation="vertical"
android:paddingBottom="16dp"
android:layout_weight="1">
<com.google.android.material.textview.MaterialTextView
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"
style="@style/Widget.Material3.Button.UnelevatedButton"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="@string/download" />
<com.google.android.material.textview.MaterialTextView
android:id="@+id/text_keys_missing"
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?" />
</LinearLayout>
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginHorizontal="24dp"
android:orientation="vertical"
android:paddingBottom="16dp"
android:layout_weight="1">
<com.google.android.material.textview.MaterialTextView
style="@style/TextAppearance.Material3.TitleSmall"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="@string/boot_home_menu"
android:textAlignment="viewStart" />
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/system_region_start"
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"
android:enabled="false"
app:layout_constraintTop_toTopOf="parent">
<com.google.android.material.textfield.MaterialAutoCompleteTextView
android:id="@+id/dropdown_system_region_start"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="none" />
</com.google.android.material.textfield.TextInputLayout>
<Button
android:id="@+id/button_start_home_menu"
style="@style/Widget.Material3.Button.UnelevatedButton"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:enabled="false"
android:text="@string/start" />
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp">
<com.google.android.material.textview.MaterialTextView
android:id="@+id/text_run_system_setup"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:text="@string/run_system_setup"
android:textAlignment="viewStart"
android:layout_marginEnd="16dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/switch_run_system_setup"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<com.google.android.material.materialswitch.MaterialSwitch
android:id="@+id/switch_run_system_setup"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp">
<com.google.android.material.textview.MaterialTextView
android:id="@+id/text_show_apps"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:text="@string/show_home_apps"
android:textAlignment="viewStart"
android:layout_marginEnd="16dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/switch_show_apps"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<com.google.android.material.materialswitch.MaterialSwitch
android:id="@+id/switch_show_apps"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
</LinearLayout>
</LinearLayout>
</androidx.core.widget.NestedScrollView>
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View File

@@ -18,7 +18,7 @@
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
app:navigationIcon="@drawable/ic_back"
app:title="@string/setup_system_files" />
app:title="@string/system_files" />
</com.google.android.material.appbar.AppBarLayout>
@@ -39,48 +39,77 @@
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_set_up_system_files"
android:id="@+id/button_download_home_menu"
style="@style/Widget.Material3.Button.UnelevatedButton"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/setup_tool_connect" />
<Button
android:id="@+id/button_unlink_console_data"
style="@style/Widget.Material3.Button.UnelevatedButton"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/delete_system_files" />
<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" />
android:layout_marginTop="16dp"
android:text="@string/download" />
<com.google.android.material.textview.MaterialTextView
style="@style/TextAppearance.Material3.TitleMedium"
android:id="@+id/text_keys_missing"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="24dp"
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:text="@string/boot_home_menu"
android:textAlignment="viewStart" />

View File

@@ -297,6 +297,17 @@
<item>6</item>
</integer-array>
<string-array name="systemFileTypes">
<item>@string/system_type_minimal</item>
<item>@string/system_type_old_3ds</item>
<item>@string/system_type_new_3ds</item>
</string-array>
<integer-array name="systemFileTypeValues">
<item>1</item>
<item>2</item>
<item>4</item>
</integer-array>
<string-array name="soundOutputModes">
<item>@string/mono</item>
<item>@string/stereo</item>

View File

@@ -2,7 +2,7 @@
<resources>
<!-- General application strings -->
<string name="app_name" translatable="false">Azahar</string>
<string name="app_name" translatable="false">AzaharPlus</string>
<string name="app_disclaimer">This software will run applications for the Nintendo 3DS handheld game console. No game titles are included.\n\nBefore you can begin with emulating, please select a folder to store Azahar\'s user data in.\n\nWhat\'s this:\n<a href='https://web.archive.org/web/20240304193549/https://github.com/citra-emu/citra/wiki/Citra-Android-user-data-and-storage'>Wiki - Citra Android user data and storage</a></string>
<string name="app_notification_channel_name" translatable="false">Azahar</string>
<string name="app_notification_channel_id" translatable="false">Azahar</string>
@@ -148,12 +148,15 @@
<!-- System files strings -->
<string name="setup_system_files">System Files</string>
<string name="system_files">System Files</string>
<string name="setup_system_files_description">Perform system file operations such as installing system files or booting the Home Menu</string>
<string name="system_files_description">Download system files to get Mii files, boot the HOME menu, and more</string>
<string name="setup_tool_connect">Connect to Artic Setup Tool</string>
<string name="setup_system_files_preamble"><![CDATA[Azahar needs console unique data and firmware files from a real console to be able to use some of its features. Such files and data can be set up 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 data to Azahar, do not share your user or nand folders after performing the setup process!</b></li><li>While doing the setup process, Azahar will link to the console running the setup tool. You can unlink the console later from the System Files tab in the emulator options menu.</li><li>Do not go online with both Azahar and your 3DS console at the same time after setting up system files, as this could cause issues.</li><li>Old 3DS setup is needed for the New 3DS setup to work (setting up both is recommended).</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="delete_system_files">Unlink Console Unique Data</string>
<string name="delete_system_files_description"><![CDATA[This action will unlink your real console from Azahar, with the following consequences:<br><ul><li>Your OTP, SecureInfo and LocalFriendCodeSeed will be removed from Azahar.</li><li>Your friend list will reset and you will be logged out of your NNID/PNID account.</li><li>System files and eshop titles obtained through Azahar will become inaccessible until the same console is linked again using the setup tool (save data will not be lost).</li></ul><br>Continue?]]></string>
<string name="download_system_files">Download System Files</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>
@@ -162,9 +165,25 @@
<string name="setup_system_files_enter_address">Enter Artic Setup Tool address</string>
<string name="setup_system_files_preparing">Preparing setup, please wait...</string>
<string name="boot_home_menu">Boot the HOME Menu</string>
<string name="system_type">System Type</string>
<string name="download">Download</string>
<string name="keys_missing">Azahar is missing keys to download system files.</string>
<string name="how_to_get_keys"><![CDATA[<a href="https://web.archive.org/web/20240304203412/https://citra-emu.org/wiki/aes-keys/">How to get keys?</a>]]></string>
<string name="show_home_apps">Show HOME menu apps in Applications list</string>
<string name="run_system_setup">Run System Setup when the HOME Menu is launched</string>
<string name="system_type_minimal">Minimal</string>
<string name="system_type_old_3ds">Old 3DS</string>
<string name="system_type_new_3ds">New 3DS</string>
<string name="downloading_files">Downloading Files…</string>
<string name="downloading_files_description">Please do not close the app.</string>
<string name="download_failed">Download Failed</string>
<string name="download_failed_description">Please make sure you are connected to the internet and try again.</string>
<string name="download_success">Download Complete!</string>
<string name="download_cancelled">Download Cancelled</string>
<string name="download_cancelled_description">Please restart the download to prevent issues with having incomplete system files.</string>
<string name="home_menu">HOME Menu</string>
<string name="home_menu_warning">System Files Warning</string>
<string name="home_menu_warning_description">Due to how slow Android\'s storage access framework is for accessing Azahar\'s files, downloading multiple versions of system files can dramatically slow down loading for applications, save states, and the Applications list. Only download the files that you require to avoid any issues with loading speeds.</string>
<!-- Generic buttons (Shared with lots of stuff) -->
<string name="generic_buttons">Buttons</string>