mirror of
https://github.com/Lime3DS/Lime3DS.git
synced 2025-03-13 09:12:27 +01:00
Implement "Set Up System Files" on Android
This commit is contained in:
parent
57b5f7da17
commit
5e06d5d5e7
@ -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()
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
@ -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)
|
||||
|
@ -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
|
||||
}
|
||||
}
|
@ -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,
|
||||
|
@ -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"
|
||||
|
@ -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" />
|
||||
|
||||
|
@ -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>
|
||||
|
@ -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);
|
||||
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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);
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user