android: Add import/export buttons for user data
This commit is contained in:
parent
df56ecc318
commit
1e740df9b8
|
@ -49,6 +49,7 @@ class HomeSettingAdapter(
|
||||||
holder.option.onClick.invoke()
|
holder.option.onClick.invoke()
|
||||||
} else {
|
} else {
|
||||||
MessageDialogFragment.newInstance(
|
MessageDialogFragment.newInstance(
|
||||||
|
activity,
|
||||||
titleId = holder.option.disabledTitleId,
|
titleId = holder.option.disabledTitleId,
|
||||||
descriptionId = holder.option.disabledMessageId
|
descriptionId = holder.option.disabledMessageId
|
||||||
).show(activity.supportFragmentManager, MessageDialogFragment.TAG)
|
).show(activity.supportFragmentManager, MessageDialogFragment.TAG)
|
||||||
|
|
|
@ -26,6 +26,7 @@ import org.yuzu.yuzu_emu.BuildConfig
|
||||||
import org.yuzu.yuzu_emu.R
|
import org.yuzu.yuzu_emu.R
|
||||||
import org.yuzu.yuzu_emu.databinding.FragmentAboutBinding
|
import org.yuzu.yuzu_emu.databinding.FragmentAboutBinding
|
||||||
import org.yuzu.yuzu_emu.model.HomeViewModel
|
import org.yuzu.yuzu_emu.model.HomeViewModel
|
||||||
|
import org.yuzu.yuzu_emu.ui.main.MainActivity
|
||||||
|
|
||||||
class AboutFragment : Fragment() {
|
class AboutFragment : Fragment() {
|
||||||
private var _binding: FragmentAboutBinding? = null
|
private var _binding: FragmentAboutBinding? = null
|
||||||
|
@ -92,6 +93,12 @@ class AboutFragment : Fragment() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val mainActivity = requireActivity() as MainActivity
|
||||||
|
binding.buttonExport.setOnClickListener { mainActivity.exportUserData.launch("export.zip") }
|
||||||
|
binding.buttonImport.setOnClickListener {
|
||||||
|
mainActivity.importUserData.launch(arrayOf("application/zip"))
|
||||||
|
}
|
||||||
|
|
||||||
binding.buttonDiscord.setOnClickListener { openLink(getString(R.string.support_link)) }
|
binding.buttonDiscord.setOnClickListener { openLink(getString(R.string.support_link)) }
|
||||||
binding.buttonWebsite.setOnClickListener { openLink(getString(R.string.website_link)) }
|
binding.buttonWebsite.setOnClickListener { openLink(getString(R.string.website_link)) }
|
||||||
binding.buttonGithub.setOnClickListener { openLink(getString(R.string.github_link)) }
|
binding.buttonGithub.setOnClickListener { openLink(getString(R.string.github_link)) }
|
||||||
|
|
|
@ -187,6 +187,7 @@ class ImportExportSavesFragment : DialogFragment() {
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
if (!validZip) {
|
if (!validZip) {
|
||||||
MessageDialogFragment.newInstance(
|
MessageDialogFragment.newInstance(
|
||||||
|
requireActivity(),
|
||||||
titleId = R.string.save_file_invalid_zip_structure,
|
titleId = R.string.save_file_invalid_zip_structure,
|
||||||
descriptionId = R.string.save_file_invalid_zip_structure_description
|
descriptionId = R.string.save_file_invalid_zip_structure_description
|
||||||
).show(activity.supportFragmentManager, MessageDialogFragment.TAG)
|
).show(activity.supportFragmentManager, MessageDialogFragment.TAG)
|
||||||
|
|
|
@ -4,6 +4,7 @@
|
||||||
package org.yuzu.yuzu_emu.fragments
|
package org.yuzu.yuzu_emu.fragments
|
||||||
|
|
||||||
import android.app.Dialog
|
import android.app.Dialog
|
||||||
|
import android.content.DialogInterface
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
|
@ -18,6 +19,7 @@ import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.lifecycle.repeatOnLifecycle
|
import androidx.lifecycle.repeatOnLifecycle
|
||||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import org.yuzu.yuzu_emu.R
|
||||||
import org.yuzu.yuzu_emu.databinding.DialogProgressBarBinding
|
import org.yuzu.yuzu_emu.databinding.DialogProgressBarBinding
|
||||||
import org.yuzu.yuzu_emu.model.TaskViewModel
|
import org.yuzu.yuzu_emu.model.TaskViewModel
|
||||||
|
|
||||||
|
@ -28,19 +30,27 @@ class IndeterminateProgressDialogFragment : DialogFragment() {
|
||||||
|
|
||||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||||
val titleId = requireArguments().getInt(TITLE)
|
val titleId = requireArguments().getInt(TITLE)
|
||||||
|
val cancellable = requireArguments().getBoolean(CANCELLABLE)
|
||||||
|
|
||||||
binding = DialogProgressBarBinding.inflate(layoutInflater)
|
binding = DialogProgressBarBinding.inflate(layoutInflater)
|
||||||
binding.progressBar.isIndeterminate = true
|
binding.progressBar.isIndeterminate = true
|
||||||
val dialog = MaterialAlertDialogBuilder(requireContext())
|
val dialog = MaterialAlertDialogBuilder(requireContext())
|
||||||
.setTitle(titleId)
|
.setTitle(titleId)
|
||||||
.setView(binding.root)
|
.setView(binding.root)
|
||||||
.create()
|
|
||||||
dialog.setCanceledOnTouchOutside(false)
|
if (cancellable) {
|
||||||
|
dialog.setNegativeButton(android.R.string.cancel) { _: DialogInterface, _: Int ->
|
||||||
|
taskViewModel.setCancelled(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val alertDialog = dialog.create()
|
||||||
|
alertDialog.setCanceledOnTouchOutside(false)
|
||||||
|
|
||||||
if (!taskViewModel.isRunning.value) {
|
if (!taskViewModel.isRunning.value) {
|
||||||
taskViewModel.runTask()
|
taskViewModel.runTask()
|
||||||
}
|
}
|
||||||
return dialog
|
return alertDialog
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreateView(
|
override fun onCreateView(
|
||||||
|
@ -53,21 +63,35 @@ class IndeterminateProgressDialogFragment : DialogFragment() {
|
||||||
|
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
super.onViewCreated(view, savedInstanceState)
|
super.onViewCreated(view, savedInstanceState)
|
||||||
viewLifecycleOwner.lifecycleScope.launch {
|
viewLifecycleOwner.lifecycleScope.apply {
|
||||||
repeatOnLifecycle(Lifecycle.State.CREATED) {
|
launch {
|
||||||
taskViewModel.isComplete.collect {
|
repeatOnLifecycle(Lifecycle.State.CREATED) {
|
||||||
if (it) {
|
taskViewModel.isComplete.collect {
|
||||||
dismiss()
|
if (it) {
|
||||||
when (val result = taskViewModel.result.value) {
|
dismiss()
|
||||||
is String -> Toast.makeText(requireContext(), result, Toast.LENGTH_LONG)
|
when (val result = taskViewModel.result.value) {
|
||||||
.show()
|
is String -> Toast.makeText(
|
||||||
|
requireContext(),
|
||||||
|
result,
|
||||||
|
Toast.LENGTH_LONG
|
||||||
|
).show()
|
||||||
|
|
||||||
is MessageDialogFragment -> result.show(
|
is MessageDialogFragment -> result.show(
|
||||||
requireActivity().supportFragmentManager,
|
requireActivity().supportFragmentManager,
|
||||||
MessageDialogFragment.TAG
|
MessageDialogFragment.TAG
|
||||||
)
|
)
|
||||||
|
}
|
||||||
|
taskViewModel.clear()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
launch {
|
||||||
|
repeatOnLifecycle(Lifecycle.State.CREATED) {
|
||||||
|
taskViewModel.cancelled.collect {
|
||||||
|
if (it) {
|
||||||
|
dialog?.setTitle(R.string.cancelling)
|
||||||
}
|
}
|
||||||
taskViewModel.clear()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -78,16 +102,19 @@ class IndeterminateProgressDialogFragment : DialogFragment() {
|
||||||
const val TAG = "IndeterminateProgressDialogFragment"
|
const val TAG = "IndeterminateProgressDialogFragment"
|
||||||
|
|
||||||
private const val TITLE = "Title"
|
private const val TITLE = "Title"
|
||||||
|
private const val CANCELLABLE = "Cancellable"
|
||||||
|
|
||||||
fun newInstance(
|
fun newInstance(
|
||||||
activity: AppCompatActivity,
|
activity: AppCompatActivity,
|
||||||
titleId: Int,
|
titleId: Int,
|
||||||
|
cancellable: Boolean = false,
|
||||||
task: () -> Any
|
task: () -> Any
|
||||||
): IndeterminateProgressDialogFragment {
|
): IndeterminateProgressDialogFragment {
|
||||||
val dialog = IndeterminateProgressDialogFragment()
|
val dialog = IndeterminateProgressDialogFragment()
|
||||||
val args = Bundle()
|
val args = Bundle()
|
||||||
ViewModelProvider(activity)[TaskViewModel::class.java].task = task
|
ViewModelProvider(activity)[TaskViewModel::class.java].task = task
|
||||||
args.putInt(TITLE, titleId)
|
args.putInt(TITLE, titleId)
|
||||||
|
args.putBoolean(CANCELLABLE, cancellable)
|
||||||
dialog.arguments = args
|
dialog.arguments = args
|
||||||
return dialog
|
return dialog
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,14 +4,21 @@
|
||||||
package org.yuzu.yuzu_emu.fragments
|
package org.yuzu.yuzu_emu.fragments
|
||||||
|
|
||||||
import android.app.Dialog
|
import android.app.Dialog
|
||||||
|
import android.content.DialogInterface
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import androidx.fragment.app.DialogFragment
|
import androidx.fragment.app.DialogFragment
|
||||||
|
import androidx.fragment.app.FragmentActivity
|
||||||
|
import androidx.fragment.app.activityViewModels
|
||||||
|
import androidx.lifecycle.ViewModelProvider
|
||||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||||
import org.yuzu.yuzu_emu.R
|
import org.yuzu.yuzu_emu.R
|
||||||
|
import org.yuzu.yuzu_emu.model.MessageDialogViewModel
|
||||||
|
|
||||||
class MessageDialogFragment : DialogFragment() {
|
class MessageDialogFragment : DialogFragment() {
|
||||||
|
private val messageDialogViewModel: MessageDialogViewModel by activityViewModels()
|
||||||
|
|
||||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||||
val titleId = requireArguments().getInt(TITLE_ID)
|
val titleId = requireArguments().getInt(TITLE_ID)
|
||||||
val titleString = requireArguments().getString(TITLE_STRING)!!
|
val titleString = requireArguments().getString(TITLE_STRING)!!
|
||||||
|
@ -37,6 +44,12 @@ class MessageDialogFragment : DialogFragment() {
|
||||||
return dialog.show()
|
return dialog.show()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onDismiss(dialog: DialogInterface) {
|
||||||
|
super.onDismiss(dialog)
|
||||||
|
messageDialogViewModel.dismissAction.invoke()
|
||||||
|
messageDialogViewModel.clear()
|
||||||
|
}
|
||||||
|
|
||||||
private fun openLink(link: String) {
|
private fun openLink(link: String) {
|
||||||
val intent = Intent(Intent.ACTION_VIEW, Uri.parse(link))
|
val intent = Intent(Intent.ACTION_VIEW, Uri.parse(link))
|
||||||
startActivity(intent)
|
startActivity(intent)
|
||||||
|
@ -52,11 +65,13 @@ class MessageDialogFragment : DialogFragment() {
|
||||||
private const val HELP_LINK = "Link"
|
private const val HELP_LINK = "Link"
|
||||||
|
|
||||||
fun newInstance(
|
fun newInstance(
|
||||||
|
activity: FragmentActivity,
|
||||||
titleId: Int = 0,
|
titleId: Int = 0,
|
||||||
titleString: String = "",
|
titleString: String = "",
|
||||||
descriptionId: Int = 0,
|
descriptionId: Int = 0,
|
||||||
descriptionString: String = "",
|
descriptionString: String = "",
|
||||||
helpLinkId: Int = 0
|
helpLinkId: Int = 0,
|
||||||
|
dismissAction: () -> Unit = {}
|
||||||
): MessageDialogFragment {
|
): MessageDialogFragment {
|
||||||
val dialog = MessageDialogFragment()
|
val dialog = MessageDialogFragment()
|
||||||
val bundle = Bundle()
|
val bundle = Bundle()
|
||||||
|
@ -67,6 +82,8 @@ class MessageDialogFragment : DialogFragment() {
|
||||||
putString(DESCRIPTION_STRING, descriptionString)
|
putString(DESCRIPTION_STRING, descriptionString)
|
||||||
putInt(HELP_LINK, helpLinkId)
|
putInt(HELP_LINK, helpLinkId)
|
||||||
}
|
}
|
||||||
|
ViewModelProvider(activity)[MessageDialogViewModel::class.java].dismissAction =
|
||||||
|
dismissAction
|
||||||
dialog.arguments = bundle
|
dialog.arguments = bundle
|
||||||
return dialog
|
return dialog
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,14 @@
|
||||||
|
// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
package org.yuzu.yuzu_emu.model
|
||||||
|
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
|
||||||
|
class MessageDialogViewModel : ViewModel() {
|
||||||
|
var dismissAction: () -> Unit = {}
|
||||||
|
|
||||||
|
fun clear() {
|
||||||
|
dismissAction = {}
|
||||||
|
}
|
||||||
|
}
|
|
@ -20,12 +20,20 @@ class TaskViewModel : ViewModel() {
|
||||||
val isRunning: StateFlow<Boolean> get() = _isRunning
|
val isRunning: StateFlow<Boolean> get() = _isRunning
|
||||||
private val _isRunning = MutableStateFlow(false)
|
private val _isRunning = MutableStateFlow(false)
|
||||||
|
|
||||||
|
val cancelled: StateFlow<Boolean> get() = _cancelled
|
||||||
|
private val _cancelled = MutableStateFlow(false)
|
||||||
|
|
||||||
lateinit var task: () -> Any
|
lateinit var task: () -> Any
|
||||||
|
|
||||||
fun clear() {
|
fun clear() {
|
||||||
_result.value = Any()
|
_result.value = Any()
|
||||||
_isComplete.value = false
|
_isComplete.value = false
|
||||||
_isRunning.value = false
|
_isRunning.value = false
|
||||||
|
_cancelled.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setCancelled(value: Boolean) {
|
||||||
|
_cancelled.value = value
|
||||||
}
|
}
|
||||||
|
|
||||||
fun runTask() {
|
fun runTask() {
|
||||||
|
|
|
@ -46,13 +46,21 @@ import org.yuzu.yuzu_emu.fragments.IndeterminateProgressDialogFragment
|
||||||
import org.yuzu.yuzu_emu.fragments.MessageDialogFragment
|
import org.yuzu.yuzu_emu.fragments.MessageDialogFragment
|
||||||
import org.yuzu.yuzu_emu.model.GamesViewModel
|
import org.yuzu.yuzu_emu.model.GamesViewModel
|
||||||
import org.yuzu.yuzu_emu.model.HomeViewModel
|
import org.yuzu.yuzu_emu.model.HomeViewModel
|
||||||
|
import org.yuzu.yuzu_emu.model.TaskViewModel
|
||||||
import org.yuzu.yuzu_emu.utils.*
|
import org.yuzu.yuzu_emu.utils.*
|
||||||
|
import java.io.BufferedInputStream
|
||||||
|
import java.io.BufferedOutputStream
|
||||||
|
import java.io.FileOutputStream
|
||||||
|
import java.util.zip.ZipEntry
|
||||||
|
import java.util.zip.ZipInputStream
|
||||||
|
import java.util.zip.ZipOutputStream
|
||||||
|
|
||||||
class MainActivity : AppCompatActivity(), ThemeProvider {
|
class MainActivity : AppCompatActivity(), ThemeProvider {
|
||||||
private lateinit var binding: ActivityMainBinding
|
private lateinit var binding: ActivityMainBinding
|
||||||
|
|
||||||
private val homeViewModel: HomeViewModel by viewModels()
|
private val homeViewModel: HomeViewModel by viewModels()
|
||||||
private val gamesViewModel: GamesViewModel by viewModels()
|
private val gamesViewModel: GamesViewModel by viewModels()
|
||||||
|
private val taskViewModel: TaskViewModel by viewModels()
|
||||||
|
|
||||||
override var themeId: Int = 0
|
override var themeId: Int = 0
|
||||||
|
|
||||||
|
@ -307,6 +315,7 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
|
||||||
fun processKey(result: Uri): Boolean {
|
fun processKey(result: Uri): Boolean {
|
||||||
if (FileUtil.getExtension(result) != "keys") {
|
if (FileUtil.getExtension(result) != "keys") {
|
||||||
MessageDialogFragment.newInstance(
|
MessageDialogFragment.newInstance(
|
||||||
|
this,
|
||||||
titleId = R.string.reading_keys_failure,
|
titleId = R.string.reading_keys_failure,
|
||||||
descriptionId = R.string.install_prod_keys_failure_extension_description
|
descriptionId = R.string.install_prod_keys_failure_extension_description
|
||||||
).show(supportFragmentManager, MessageDialogFragment.TAG)
|
).show(supportFragmentManager, MessageDialogFragment.TAG)
|
||||||
|
@ -336,6 +345,7 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
|
||||||
return true
|
return true
|
||||||
} else {
|
} else {
|
||||||
MessageDialogFragment.newInstance(
|
MessageDialogFragment.newInstance(
|
||||||
|
this,
|
||||||
titleId = R.string.invalid_keys_error,
|
titleId = R.string.invalid_keys_error,
|
||||||
descriptionId = R.string.install_keys_failure_description,
|
descriptionId = R.string.install_keys_failure_description,
|
||||||
helpLinkId = R.string.dumping_keys_quickstart_link
|
helpLinkId = R.string.dumping_keys_quickstart_link
|
||||||
|
@ -376,6 +386,7 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
|
||||||
val filteredNumOfFiles = cacheFirmwareDir.list(filterNCA)?.size ?: -2
|
val filteredNumOfFiles = cacheFirmwareDir.list(filterNCA)?.size ?: -2
|
||||||
messageToShow = if (unfilteredNumOfFiles != filteredNumOfFiles) {
|
messageToShow = if (unfilteredNumOfFiles != filteredNumOfFiles) {
|
||||||
MessageDialogFragment.newInstance(
|
MessageDialogFragment.newInstance(
|
||||||
|
this,
|
||||||
titleId = R.string.firmware_installed_failure,
|
titleId = R.string.firmware_installed_failure,
|
||||||
descriptionId = R.string.firmware_installed_failure_description
|
descriptionId = R.string.firmware_installed_failure_description
|
||||||
)
|
)
|
||||||
|
@ -395,7 +406,7 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
|
||||||
IndeterminateProgressDialogFragment.newInstance(
|
IndeterminateProgressDialogFragment.newInstance(
|
||||||
this,
|
this,
|
||||||
R.string.firmware_installing,
|
R.string.firmware_installing,
|
||||||
task
|
task = task
|
||||||
).show(supportFragmentManager, IndeterminateProgressDialogFragment.TAG)
|
).show(supportFragmentManager, IndeterminateProgressDialogFragment.TAG)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -407,6 +418,7 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
|
||||||
|
|
||||||
if (FileUtil.getExtension(result) != "bin") {
|
if (FileUtil.getExtension(result) != "bin") {
|
||||||
MessageDialogFragment.newInstance(
|
MessageDialogFragment.newInstance(
|
||||||
|
this,
|
||||||
titleId = R.string.reading_keys_failure,
|
titleId = R.string.reading_keys_failure,
|
||||||
descriptionId = R.string.install_amiibo_keys_failure_extension_description
|
descriptionId = R.string.install_amiibo_keys_failure_extension_description
|
||||||
).show(supportFragmentManager, MessageDialogFragment.TAG)
|
).show(supportFragmentManager, MessageDialogFragment.TAG)
|
||||||
|
@ -434,6 +446,7 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
|
||||||
).show()
|
).show()
|
||||||
} else {
|
} else {
|
||||||
MessageDialogFragment.newInstance(
|
MessageDialogFragment.newInstance(
|
||||||
|
this,
|
||||||
titleId = R.string.invalid_keys_error,
|
titleId = R.string.invalid_keys_error,
|
||||||
descriptionId = R.string.install_keys_failure_description,
|
descriptionId = R.string.install_keys_failure_description,
|
||||||
helpLinkId = R.string.dumping_keys_quickstart_link
|
helpLinkId = R.string.dumping_keys_quickstart_link
|
||||||
|
@ -583,12 +596,14 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
|
||||||
installResult.append(separator)
|
installResult.append(separator)
|
||||||
}
|
}
|
||||||
return@newInstance MessageDialogFragment.newInstance(
|
return@newInstance MessageDialogFragment.newInstance(
|
||||||
|
this,
|
||||||
titleId = R.string.install_game_content_failure,
|
titleId = R.string.install_game_content_failure,
|
||||||
descriptionString = installResult.toString().trim(),
|
descriptionString = installResult.toString().trim(),
|
||||||
helpLinkId = R.string.install_game_content_help_link
|
helpLinkId = R.string.install_game_content_help_link
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
return@newInstance MessageDialogFragment.newInstance(
|
return@newInstance MessageDialogFragment.newInstance(
|
||||||
|
this,
|
||||||
titleId = R.string.install_game_content_success,
|
titleId = R.string.install_game_content_success,
|
||||||
descriptionString = installResult.toString().trim()
|
descriptionString = installResult.toString().trim()
|
||||||
)
|
)
|
||||||
|
@ -596,4 +611,110 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
|
||||||
}.show(supportFragmentManager, IndeterminateProgressDialogFragment.TAG)
|
}.show(supportFragmentManager, IndeterminateProgressDialogFragment.TAG)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val exportUserData = registerForActivityResult(
|
||||||
|
ActivityResultContracts.CreateDocument("application/zip")
|
||||||
|
) { result ->
|
||||||
|
if (result == null) {
|
||||||
|
return@registerForActivityResult
|
||||||
|
}
|
||||||
|
|
||||||
|
IndeterminateProgressDialogFragment.newInstance(
|
||||||
|
this,
|
||||||
|
R.string.exporting_user_data,
|
||||||
|
true
|
||||||
|
) {
|
||||||
|
val zos = ZipOutputStream(
|
||||||
|
BufferedOutputStream(contentResolver.openOutputStream(result))
|
||||||
|
)
|
||||||
|
zos.use { stream ->
|
||||||
|
File(DirectoryInitialization.userDirectory!!).walkTopDown().forEach { file ->
|
||||||
|
if (taskViewModel.cancelled.value) {
|
||||||
|
return@newInstance R.string.user_data_export_cancelled
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!file.isDirectory) {
|
||||||
|
val newPath = file.path.substring(
|
||||||
|
DirectoryInitialization.userDirectory!!.length,
|
||||||
|
file.path.length
|
||||||
|
)
|
||||||
|
stream.putNextEntry(ZipEntry(newPath))
|
||||||
|
stream.write(file.readBytes())
|
||||||
|
stream.closeEntry()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return@newInstance getString(R.string.user_data_export_success)
|
||||||
|
}.show(supportFragmentManager, IndeterminateProgressDialogFragment.TAG)
|
||||||
|
}
|
||||||
|
|
||||||
|
val importUserData =
|
||||||
|
registerForActivityResult(ActivityResultContracts.OpenDocument()) { result ->
|
||||||
|
if (result == null) {
|
||||||
|
return@registerForActivityResult
|
||||||
|
}
|
||||||
|
|
||||||
|
IndeterminateProgressDialogFragment.newInstance(
|
||||||
|
this,
|
||||||
|
R.string.importing_user_data
|
||||||
|
) {
|
||||||
|
val checkStream =
|
||||||
|
ZipInputStream(BufferedInputStream(contentResolver.openInputStream(result)))
|
||||||
|
var isYuzuBackup = false
|
||||||
|
checkStream.use { stream ->
|
||||||
|
var ze: ZipEntry? = null
|
||||||
|
while (stream.nextEntry?.also { ze = it } != null) {
|
||||||
|
if (ze!!.name.trim() == "/config/config.ini") {
|
||||||
|
isYuzuBackup = true
|
||||||
|
return@use
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!isYuzuBackup) {
|
||||||
|
return@newInstance getString(R.string.invalid_yuzu_backup)
|
||||||
|
}
|
||||||
|
|
||||||
|
File(DirectoryInitialization.userDirectory!!).deleteRecursively()
|
||||||
|
|
||||||
|
val zis =
|
||||||
|
ZipInputStream(BufferedInputStream(contentResolver.openInputStream(result)))
|
||||||
|
val userDirectory = File(DirectoryInitialization.userDirectory!!)
|
||||||
|
val canonicalPath = userDirectory.canonicalPath + '/'
|
||||||
|
zis.use { stream ->
|
||||||
|
var ze: ZipEntry? = stream.nextEntry
|
||||||
|
while (ze != null) {
|
||||||
|
val newFile = File(userDirectory, ze!!.name)
|
||||||
|
val destinationDirectory =
|
||||||
|
if (ze!!.isDirectory) newFile else newFile.parentFile
|
||||||
|
|
||||||
|
if (!newFile.canonicalPath.startsWith(canonicalPath)) {
|
||||||
|
throw SecurityException(
|
||||||
|
"Zip file attempted path traversal! ${ze!!.name}"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!destinationDirectory.isDirectory && !destinationDirectory.mkdirs()) {
|
||||||
|
throw IOException("Failed to create directory $destinationDirectory")
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!ze!!.isDirectory) {
|
||||||
|
val buffer = ByteArray(8096)
|
||||||
|
var read: Int
|
||||||
|
BufferedOutputStream(FileOutputStream(newFile)).use { bos ->
|
||||||
|
while (zis.read(buffer).also { read = it } != -1) {
|
||||||
|
bos.write(buffer, 0, read)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ze = stream.nextEntry
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reinitialize relevant data
|
||||||
|
NativeLibrary.initializeEmulation()
|
||||||
|
gamesViewModel.reloadGames(false)
|
||||||
|
|
||||||
|
return@newInstance getString(R.string.user_data_import_success)
|
||||||
|
}.show(supportFragmentManager, IndeterminateProgressDialogFragment.TAG)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
9
src/android/app/src/main/res/drawable/ic_export.xml
Normal file
9
src/android/app/src/main/res/drawable/ic_export.xml
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24">
|
||||||
|
<path
|
||||||
|
android:fillColor="?attr/colorControlNormal"
|
||||||
|
android:pathData="M9,16h6v-6h4l-7,-7 -7,7h4zM5,18h14v2L5,20z" />
|
||||||
|
</vector>
|
9
src/android/app/src/main/res/drawable/ic_import.xml
Normal file
9
src/android/app/src/main/res/drawable/ic_import.xml
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24">
|
||||||
|
<path
|
||||||
|
android:fillColor="?attr/colorControlNormal"
|
||||||
|
android:pathData="M19,9h-4V3H9v6H5l7,7 7,-7zM5,18v2h14v-2H5z" />
|
||||||
|
</vector>
|
|
@ -1,24 +1,8 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
<com.google.android.material.progressindicator.LinearProgressIndicator xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="match_parent"
|
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
android:orientation="vertical">
|
android:id="@+id/progress_bar"
|
||||||
|
android:layout_width="match_parent"
|
||||||
<com.google.android.material.progressindicator.LinearProgressIndicator
|
android:layout_height="wrap_content"
|
||||||
android:id="@+id/progress_bar"
|
android:padding="24dp"
|
||||||
android:layout_width="match_parent"
|
app:trackCornerRadius="4dp" />
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_margin="24dp"
|
|
||||||
app:trackCornerRadius="4dp" />
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/progress_text"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_marginLeft="24dp"
|
|
||||||
android:layout_marginRight="24dp"
|
|
||||||
android:layout_marginBottom="24dp"
|
|
||||||
android:gravity="end" />
|
|
||||||
|
|
||||||
</LinearLayout>
|
|
||||||
|
|
|
@ -176,6 +176,67 @@
|
||||||
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
|
<com.google.android.material.divider.MaterialDivider
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginHorizontal="20dp" />
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="horizontal">
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:paddingVertical="16dp"
|
||||||
|
android:paddingHorizontal="16dp"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:layout_weight="1">
|
||||||
|
|
||||||
|
<com.google.android.material.textview.MaterialTextView
|
||||||
|
style="@style/TextAppearance.Material3.TitleMedium"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginHorizontal="24dp"
|
||||||
|
android:textAlignment="viewStart"
|
||||||
|
android:text="@string/user_data" />
|
||||||
|
|
||||||
|
<com.google.android.material.textview.MaterialTextView
|
||||||
|
style="@style/TextAppearance.Material3.BodyMedium"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginHorizontal="24dp"
|
||||||
|
android:layout_marginTop="6dp"
|
||||||
|
android:textAlignment="viewStart"
|
||||||
|
android:text="@string/user_data_description" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:id="@+id/button_import"
|
||||||
|
style="@style/Widget.Material3.Button.IconButton.Filled.Tonal"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="center_vertical"
|
||||||
|
android:contentDescription="@string/string_import"
|
||||||
|
android:tooltipText="@string/string_import"
|
||||||
|
app:icon="@drawable/ic_import" />
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:id="@+id/button_export"
|
||||||
|
style="@style/Widget.Material3.Button.IconButton.Filled.Tonal"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="12dp"
|
||||||
|
android:layout_marginEnd="24dp"
|
||||||
|
android:layout_gravity="center_vertical"
|
||||||
|
android:contentDescription="@string/export"
|
||||||
|
android:tooltipText="@string/export"
|
||||||
|
app:icon="@drawable/ic_export" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
<com.google.android.material.divider.MaterialDivider
|
<com.google.android.material.divider.MaterialDivider
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
|
|
|
@ -128,6 +128,15 @@
|
||||||
<string name="contributors_link">https://github.com/yuzu-emu/yuzu/graphs/contributors</string>
|
<string name="contributors_link">https://github.com/yuzu-emu/yuzu/graphs/contributors</string>
|
||||||
<string name="licenses_description">Projects that make yuzu for Android possible</string>
|
<string name="licenses_description">Projects that make yuzu for Android possible</string>
|
||||||
<string name="build">Build</string>
|
<string name="build">Build</string>
|
||||||
|
<string name="user_data">User data</string>
|
||||||
|
<string name="user_data_description">Import/export all app data.\n\nWhen importing user data, all existing user data will be deleted!</string>
|
||||||
|
<string name="exporting_user_data">Exporting user data…</string>
|
||||||
|
<string name="importing_user_data">Importing user data…</string>
|
||||||
|
<string name="import_user_data">Import user data</string>
|
||||||
|
<string name="invalid_yuzu_backup">Invalid yuzu backup</string>
|
||||||
|
<string name="user_data_export_success">User data exported successfully</string>
|
||||||
|
<string name="user_data_import_success">User data imported successfully</string>
|
||||||
|
<string name="user_data_export_cancelled">Export cancelled</string>
|
||||||
<string name="support_link">https://discord.gg/u77vRWY</string>
|
<string name="support_link">https://discord.gg/u77vRWY</string>
|
||||||
<string name="website_link">https://yuzu-emu.org/</string>
|
<string name="website_link">https://yuzu-emu.org/</string>
|
||||||
<string name="github_link">https://github.com/yuzu-emu</string>
|
<string name="github_link">https://github.com/yuzu-emu</string>
|
||||||
|
@ -215,6 +224,9 @@
|
||||||
<string name="auto">Auto</string>
|
<string name="auto">Auto</string>
|
||||||
<string name="submit">Submit</string>
|
<string name="submit">Submit</string>
|
||||||
<string name="string_null">Null</string>
|
<string name="string_null">Null</string>
|
||||||
|
<string name="string_import">Import</string>
|
||||||
|
<string name="export">Export</string>
|
||||||
|
<string name="cancelling">Cancelling</string>
|
||||||
|
|
||||||
<!-- GPU driver installation -->
|
<!-- GPU driver installation -->
|
||||||
<string name="select_gpu_driver">Select GPU driver</string>
|
<string name="select_gpu_driver">Select GPU driver</string>
|
||||||
|
|
Loading…
Reference in a new issue