summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/HomeSettingAdapter.kt1
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/AboutFragment.kt7
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/ImportExportSavesFragment.kt1
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/IndeterminateProgressDialogFragment.kt59
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/MessageDialogFragment.kt19
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/model/MessageDialogViewModel.kt14
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/model/TaskViewModel.kt8
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/main/MainActivity.kt123
-rw-r--r--src/android/app/src/main/res/drawable/ic_export.xml9
-rw-r--r--src/android/app/src/main/res/drawable/ic_import.xml9
-rw-r--r--src/android/app/src/main/res/layout/dialog_progress_bar.xml28
-rw-r--r--src/android/app/src/main/res/layout/fragment_about.xml61
-rw-r--r--src/android/app/src/main/res/values/strings.xml12
13 files changed, 311 insertions, 40 deletions
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/HomeSettingAdapter.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/HomeSettingAdapter.kt
index 1675627a1..58ce343f4 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/HomeSettingAdapter.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/HomeSettingAdapter.kt
@@ -49,6 +49,7 @@ class HomeSettingAdapter(
49 holder.option.onClick.invoke() 49 holder.option.onClick.invoke()
50 } else { 50 } else {
51 MessageDialogFragment.newInstance( 51 MessageDialogFragment.newInstance(
52 activity,
52 titleId = holder.option.disabledTitleId, 53 titleId = holder.option.disabledTitleId,
53 descriptionId = holder.option.disabledMessageId 54 descriptionId = holder.option.disabledMessageId
54 ).show(activity.supportFragmentManager, MessageDialogFragment.TAG) 55 ).show(activity.supportFragmentManager, MessageDialogFragment.TAG)
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/AboutFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/AboutFragment.kt
index 2ff827c6b..7b8f99872 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/AboutFragment.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/AboutFragment.kt
@@ -26,6 +26,7 @@ import org.yuzu.yuzu_emu.BuildConfig
26import org.yuzu.yuzu_emu.R 26import org.yuzu.yuzu_emu.R
27import org.yuzu.yuzu_emu.databinding.FragmentAboutBinding 27import org.yuzu.yuzu_emu.databinding.FragmentAboutBinding
28import org.yuzu.yuzu_emu.model.HomeViewModel 28import org.yuzu.yuzu_emu.model.HomeViewModel
29import org.yuzu.yuzu_emu.ui.main.MainActivity
29 30
30class AboutFragment : Fragment() { 31class AboutFragment : Fragment() {
31 private var _binding: FragmentAboutBinding? = null 32 private var _binding: FragmentAboutBinding? = null
@@ -92,6 +93,12 @@ class AboutFragment : Fragment() {
92 } 93 }
93 } 94 }
94 95
96 val mainActivity = requireActivity() as MainActivity
97 binding.buttonExport.setOnClickListener { mainActivity.exportUserData.launch("export.zip") }
98 binding.buttonImport.setOnClickListener {
99 mainActivity.importUserData.launch(arrayOf("application/zip"))
100 }
101
95 binding.buttonDiscord.setOnClickListener { openLink(getString(R.string.support_link)) } 102 binding.buttonDiscord.setOnClickListener { openLink(getString(R.string.support_link)) }
96 binding.buttonWebsite.setOnClickListener { openLink(getString(R.string.website_link)) } 103 binding.buttonWebsite.setOnClickListener { openLink(getString(R.string.website_link)) }
97 binding.buttonGithub.setOnClickListener { openLink(getString(R.string.github_link)) } 104 binding.buttonGithub.setOnClickListener { openLink(getString(R.string.github_link)) }
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/ImportExportSavesFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/ImportExportSavesFragment.kt
index f38aeea53..ee2d44718 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/ImportExportSavesFragment.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/ImportExportSavesFragment.kt
@@ -187,6 +187,7 @@ class ImportExportSavesFragment : DialogFragment() {
187 withContext(Dispatchers.Main) { 187 withContext(Dispatchers.Main) {
188 if (!validZip) { 188 if (!validZip) {
189 MessageDialogFragment.newInstance( 189 MessageDialogFragment.newInstance(
190 requireActivity(),
190 titleId = R.string.save_file_invalid_zip_structure, 191 titleId = R.string.save_file_invalid_zip_structure,
191 descriptionId = R.string.save_file_invalid_zip_structure_description 192 descriptionId = R.string.save_file_invalid_zip_structure_description
192 ).show(activity.supportFragmentManager, MessageDialogFragment.TAG) 193 ).show(activity.supportFragmentManager, MessageDialogFragment.TAG)
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/IndeterminateProgressDialogFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/IndeterminateProgressDialogFragment.kt
index 18bc34b9f..0d16a7d37 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/IndeterminateProgressDialogFragment.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/IndeterminateProgressDialogFragment.kt
@@ -4,6 +4,7 @@
4package org.yuzu.yuzu_emu.fragments 4package org.yuzu.yuzu_emu.fragments
5 5
6import android.app.Dialog 6import android.app.Dialog
7import android.content.DialogInterface
7import android.os.Bundle 8import android.os.Bundle
8import android.view.LayoutInflater 9import android.view.LayoutInflater
9import android.view.View 10import android.view.View
@@ -18,6 +19,7 @@ import androidx.lifecycle.lifecycleScope
18import androidx.lifecycle.repeatOnLifecycle 19import androidx.lifecycle.repeatOnLifecycle
19import com.google.android.material.dialog.MaterialAlertDialogBuilder 20import com.google.android.material.dialog.MaterialAlertDialogBuilder
20import kotlinx.coroutines.launch 21import kotlinx.coroutines.launch
22import org.yuzu.yuzu_emu.R
21import org.yuzu.yuzu_emu.databinding.DialogProgressBarBinding 23import org.yuzu.yuzu_emu.databinding.DialogProgressBarBinding
22import org.yuzu.yuzu_emu.model.TaskViewModel 24import org.yuzu.yuzu_emu.model.TaskViewModel
23 25
@@ -28,19 +30,27 @@ class IndeterminateProgressDialogFragment : DialogFragment() {
28 30
29 override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { 31 override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
30 val titleId = requireArguments().getInt(TITLE) 32 val titleId = requireArguments().getInt(TITLE)
33 val cancellable = requireArguments().getBoolean(CANCELLABLE)
31 34
32 binding = DialogProgressBarBinding.inflate(layoutInflater) 35 binding = DialogProgressBarBinding.inflate(layoutInflater)
33 binding.progressBar.isIndeterminate = true 36 binding.progressBar.isIndeterminate = true
34 val dialog = MaterialAlertDialogBuilder(requireContext()) 37 val dialog = MaterialAlertDialogBuilder(requireContext())
35 .setTitle(titleId) 38 .setTitle(titleId)
36 .setView(binding.root) 39 .setView(binding.root)
37 .create() 40
38 dialog.setCanceledOnTouchOutside(false) 41 if (cancellable) {
42 dialog.setNegativeButton(android.R.string.cancel) { _: DialogInterface, _: Int ->
43 taskViewModel.setCancelled(true)
44 }
45 }
46
47 val alertDialog = dialog.create()
48 alertDialog.setCanceledOnTouchOutside(false)
39 49
40 if (!taskViewModel.isRunning.value) { 50 if (!taskViewModel.isRunning.value) {
41 taskViewModel.runTask() 51 taskViewModel.runTask()
42 } 52 }
43 return dialog 53 return alertDialog
44 } 54 }
45 55
46 override fun onCreateView( 56 override fun onCreateView(
@@ -53,21 +63,35 @@ class IndeterminateProgressDialogFragment : DialogFragment() {
53 63
54 override fun onViewCreated(view: View, savedInstanceState: Bundle?) { 64 override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
55 super.onViewCreated(view, savedInstanceState) 65 super.onViewCreated(view, savedInstanceState)
56 viewLifecycleOwner.lifecycleScope.launch { 66 viewLifecycleOwner.lifecycleScope.apply {
57 repeatOnLifecycle(Lifecycle.State.CREATED) { 67 launch {
58 taskViewModel.isComplete.collect { 68 repeatOnLifecycle(Lifecycle.State.CREATED) {
59 if (it) { 69 taskViewModel.isComplete.collect {
60 dismiss() 70 if (it) {
61 when (val result = taskViewModel.result.value) { 71 dismiss()
62 is String -> Toast.makeText(requireContext(), result, Toast.LENGTH_LONG) 72 when (val result = taskViewModel.result.value) {
63 .show() 73 is String -> Toast.makeText(
74 requireContext(),
75 result,
76 Toast.LENGTH_LONG
77 ).show()
64 78
65 is MessageDialogFragment -> result.show( 79 is MessageDialogFragment -> result.show(
66 requireActivity().supportFragmentManager, 80 requireActivity().supportFragmentManager,
67 MessageDialogFragment.TAG 81 MessageDialogFragment.TAG
68 ) 82 )
83 }
84 taskViewModel.clear()
85 }
86 }
87 }
88 }
89 launch {
90 repeatOnLifecycle(Lifecycle.State.CREATED) {
91 taskViewModel.cancelled.collect {
92 if (it) {
93 dialog?.setTitle(R.string.cancelling)
69 } 94 }
70 taskViewModel.clear()
71 } 95 }
72 } 96 }
73 } 97 }
@@ -78,16 +102,19 @@ class IndeterminateProgressDialogFragment : DialogFragment() {
78 const val TAG = "IndeterminateProgressDialogFragment" 102 const val TAG = "IndeterminateProgressDialogFragment"
79 103
80 private const val TITLE = "Title" 104 private const val TITLE = "Title"
105 private const val CANCELLABLE = "Cancellable"
81 106
82 fun newInstance( 107 fun newInstance(
83 activity: AppCompatActivity, 108 activity: AppCompatActivity,
84 titleId: Int, 109 titleId: Int,
110 cancellable: Boolean = false,
85 task: () -> Any 111 task: () -> Any
86 ): IndeterminateProgressDialogFragment { 112 ): IndeterminateProgressDialogFragment {
87 val dialog = IndeterminateProgressDialogFragment() 113 val dialog = IndeterminateProgressDialogFragment()
88 val args = Bundle() 114 val args = Bundle()
89 ViewModelProvider(activity)[TaskViewModel::class.java].task = task 115 ViewModelProvider(activity)[TaskViewModel::class.java].task = task
90 args.putInt(TITLE, titleId) 116 args.putInt(TITLE, titleId)
117 args.putBoolean(CANCELLABLE, cancellable)
91 dialog.arguments = args 118 dialog.arguments = args
92 return dialog 119 return dialog
93 } 120 }
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/MessageDialogFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/MessageDialogFragment.kt
index 7d1c2c8dd..541b22f47 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/MessageDialogFragment.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/MessageDialogFragment.kt
@@ -4,14 +4,21 @@
4package org.yuzu.yuzu_emu.fragments 4package org.yuzu.yuzu_emu.fragments
5 5
6import android.app.Dialog 6import android.app.Dialog
7import android.content.DialogInterface
7import android.content.Intent 8import android.content.Intent
8import android.net.Uri 9import android.net.Uri
9import android.os.Bundle 10import android.os.Bundle
10import androidx.fragment.app.DialogFragment 11import androidx.fragment.app.DialogFragment
12import androidx.fragment.app.FragmentActivity
13import androidx.fragment.app.activityViewModels
14import androidx.lifecycle.ViewModelProvider
11import com.google.android.material.dialog.MaterialAlertDialogBuilder 15import com.google.android.material.dialog.MaterialAlertDialogBuilder
12import org.yuzu.yuzu_emu.R 16import org.yuzu.yuzu_emu.R
17import org.yuzu.yuzu_emu.model.MessageDialogViewModel
13 18
14class MessageDialogFragment : DialogFragment() { 19class MessageDialogFragment : DialogFragment() {
20 private val messageDialogViewModel: MessageDialogViewModel by activityViewModels()
21
15 override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { 22 override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
16 val titleId = requireArguments().getInt(TITLE_ID) 23 val titleId = requireArguments().getInt(TITLE_ID)
17 val titleString = requireArguments().getString(TITLE_STRING)!! 24 val titleString = requireArguments().getString(TITLE_STRING)!!
@@ -37,6 +44,12 @@ class MessageDialogFragment : DialogFragment() {
37 return dialog.show() 44 return dialog.show()
38 } 45 }
39 46
47 override fun onDismiss(dialog: DialogInterface) {
48 super.onDismiss(dialog)
49 messageDialogViewModel.dismissAction.invoke()
50 messageDialogViewModel.clear()
51 }
52
40 private fun openLink(link: String) { 53 private fun openLink(link: String) {
41 val intent = Intent(Intent.ACTION_VIEW, Uri.parse(link)) 54 val intent = Intent(Intent.ACTION_VIEW, Uri.parse(link))
42 startActivity(intent) 55 startActivity(intent)
@@ -52,11 +65,13 @@ class MessageDialogFragment : DialogFragment() {
52 private const val HELP_LINK = "Link" 65 private const val HELP_LINK = "Link"
53 66
54 fun newInstance( 67 fun newInstance(
68 activity: FragmentActivity,
55 titleId: Int = 0, 69 titleId: Int = 0,
56 titleString: String = "", 70 titleString: String = "",
57 descriptionId: Int = 0, 71 descriptionId: Int = 0,
58 descriptionString: String = "", 72 descriptionString: String = "",
59 helpLinkId: Int = 0 73 helpLinkId: Int = 0,
74 dismissAction: () -> Unit = {}
60 ): MessageDialogFragment { 75 ): MessageDialogFragment {
61 val dialog = MessageDialogFragment() 76 val dialog = MessageDialogFragment()
62 val bundle = Bundle() 77 val bundle = Bundle()
@@ -67,6 +82,8 @@ class MessageDialogFragment : DialogFragment() {
67 putString(DESCRIPTION_STRING, descriptionString) 82 putString(DESCRIPTION_STRING, descriptionString)
68 putInt(HELP_LINK, helpLinkId) 83 putInt(HELP_LINK, helpLinkId)
69 } 84 }
85 ViewModelProvider(activity)[MessageDialogViewModel::class.java].dismissAction =
86 dismissAction
70 dialog.arguments = bundle 87 dialog.arguments = bundle
71 return dialog 88 return dialog
72 } 89 }
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/MessageDialogViewModel.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/MessageDialogViewModel.kt
new file mode 100644
index 000000000..36ffd08d2
--- /dev/null
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/MessageDialogViewModel.kt
@@ -0,0 +1,14 @@
1// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
2// SPDX-License-Identifier: GPL-2.0-or-later
3
4package org.yuzu.yuzu_emu.model
5
6import androidx.lifecycle.ViewModel
7
8class MessageDialogViewModel : ViewModel() {
9 var dismissAction: () -> Unit = {}
10
11 fun clear() {
12 dismissAction = {}
13 }
14}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/TaskViewModel.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/TaskViewModel.kt
index 531c2aaf0..d6418a666 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/TaskViewModel.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/TaskViewModel.kt
@@ -20,12 +20,20 @@ class TaskViewModel : ViewModel() {
20 val isRunning: StateFlow<Boolean> get() = _isRunning 20 val isRunning: StateFlow<Boolean> get() = _isRunning
21 private val _isRunning = MutableStateFlow(false) 21 private val _isRunning = MutableStateFlow(false)
22 22
23 val cancelled: StateFlow<Boolean> get() = _cancelled
24 private val _cancelled = MutableStateFlow(false)
25
23 lateinit var task: () -> Any 26 lateinit var task: () -> Any
24 27
25 fun clear() { 28 fun clear() {
26 _result.value = Any() 29 _result.value = Any()
27 _isComplete.value = false 30 _isComplete.value = false
28 _isRunning.value = false 31 _isRunning.value = false
32 _cancelled.value = false
33 }
34
35 fun setCancelled(value: Boolean) {
36 _cancelled.value = value
29 } 37 }
30 38
31 fun runTask() { 39 fun runTask() {
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/main/MainActivity.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/main/MainActivity.kt
index b6b6c6c17..74941f934 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/main/MainActivity.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/main/MainActivity.kt
@@ -46,13 +46,21 @@ import org.yuzu.yuzu_emu.fragments.IndeterminateProgressDialogFragment
46import org.yuzu.yuzu_emu.fragments.MessageDialogFragment 46import org.yuzu.yuzu_emu.fragments.MessageDialogFragment
47import org.yuzu.yuzu_emu.model.GamesViewModel 47import org.yuzu.yuzu_emu.model.GamesViewModel
48import org.yuzu.yuzu_emu.model.HomeViewModel 48import org.yuzu.yuzu_emu.model.HomeViewModel
49import org.yuzu.yuzu_emu.model.TaskViewModel
49import org.yuzu.yuzu_emu.utils.* 50import org.yuzu.yuzu_emu.utils.*
51import java.io.BufferedInputStream
52import java.io.BufferedOutputStream
53import java.io.FileOutputStream
54import java.util.zip.ZipEntry
55import java.util.zip.ZipInputStream
56import java.util.zip.ZipOutputStream
50 57
51class MainActivity : AppCompatActivity(), ThemeProvider { 58class MainActivity : AppCompatActivity(), ThemeProvider {
52 private lateinit var binding: ActivityMainBinding 59 private lateinit var binding: ActivityMainBinding
53 60
54 private val homeViewModel: HomeViewModel by viewModels() 61 private val homeViewModel: HomeViewModel by viewModels()
55 private val gamesViewModel: GamesViewModel by viewModels() 62 private val gamesViewModel: GamesViewModel by viewModels()
63 private val taskViewModel: TaskViewModel by viewModels()
56 64
57 override var themeId: Int = 0 65 override var themeId: Int = 0
58 66
@@ -307,6 +315,7 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
307 fun processKey(result: Uri): Boolean { 315 fun processKey(result: Uri): Boolean {
308 if (FileUtil.getExtension(result) != "keys") { 316 if (FileUtil.getExtension(result) != "keys") {
309 MessageDialogFragment.newInstance( 317 MessageDialogFragment.newInstance(
318 this,
310 titleId = R.string.reading_keys_failure, 319 titleId = R.string.reading_keys_failure,
311 descriptionId = R.string.install_prod_keys_failure_extension_description 320 descriptionId = R.string.install_prod_keys_failure_extension_description
312 ).show(supportFragmentManager, MessageDialogFragment.TAG) 321 ).show(supportFragmentManager, MessageDialogFragment.TAG)
@@ -336,6 +345,7 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
336 return true 345 return true
337 } else { 346 } else {
338 MessageDialogFragment.newInstance( 347 MessageDialogFragment.newInstance(
348 this,
339 titleId = R.string.invalid_keys_error, 349 titleId = R.string.invalid_keys_error,
340 descriptionId = R.string.install_keys_failure_description, 350 descriptionId = R.string.install_keys_failure_description,
341 helpLinkId = R.string.dumping_keys_quickstart_link 351 helpLinkId = R.string.dumping_keys_quickstart_link
@@ -376,6 +386,7 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
376 val filteredNumOfFiles = cacheFirmwareDir.list(filterNCA)?.size ?: -2 386 val filteredNumOfFiles = cacheFirmwareDir.list(filterNCA)?.size ?: -2
377 messageToShow = if (unfilteredNumOfFiles != filteredNumOfFiles) { 387 messageToShow = if (unfilteredNumOfFiles != filteredNumOfFiles) {
378 MessageDialogFragment.newInstance( 388 MessageDialogFragment.newInstance(
389 this,
379 titleId = R.string.firmware_installed_failure, 390 titleId = R.string.firmware_installed_failure,
380 descriptionId = R.string.firmware_installed_failure_description 391 descriptionId = R.string.firmware_installed_failure_description
381 ) 392 )
@@ -395,7 +406,7 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
395 IndeterminateProgressDialogFragment.newInstance( 406 IndeterminateProgressDialogFragment.newInstance(
396 this, 407 this,
397 R.string.firmware_installing, 408 R.string.firmware_installing,
398 task 409 task = task
399 ).show(supportFragmentManager, IndeterminateProgressDialogFragment.TAG) 410 ).show(supportFragmentManager, IndeterminateProgressDialogFragment.TAG)
400 } 411 }
401 412
@@ -407,6 +418,7 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
407 418
408 if (FileUtil.getExtension(result) != "bin") { 419 if (FileUtil.getExtension(result) != "bin") {
409 MessageDialogFragment.newInstance( 420 MessageDialogFragment.newInstance(
421 this,
410 titleId = R.string.reading_keys_failure, 422 titleId = R.string.reading_keys_failure,
411 descriptionId = R.string.install_amiibo_keys_failure_extension_description 423 descriptionId = R.string.install_amiibo_keys_failure_extension_description
412 ).show(supportFragmentManager, MessageDialogFragment.TAG) 424 ).show(supportFragmentManager, MessageDialogFragment.TAG)
@@ -434,6 +446,7 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
434 ).show() 446 ).show()
435 } else { 447 } else {
436 MessageDialogFragment.newInstance( 448 MessageDialogFragment.newInstance(
449 this,
437 titleId = R.string.invalid_keys_error, 450 titleId = R.string.invalid_keys_error,
438 descriptionId = R.string.install_keys_failure_description, 451 descriptionId = R.string.install_keys_failure_description,
439 helpLinkId = R.string.dumping_keys_quickstart_link 452 helpLinkId = R.string.dumping_keys_quickstart_link
@@ -583,12 +596,14 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
583 installResult.append(separator) 596 installResult.append(separator)
584 } 597 }
585 return@newInstance MessageDialogFragment.newInstance( 598 return@newInstance MessageDialogFragment.newInstance(
599 this,
586 titleId = R.string.install_game_content_failure, 600 titleId = R.string.install_game_content_failure,
587 descriptionString = installResult.toString().trim(), 601 descriptionString = installResult.toString().trim(),
588 helpLinkId = R.string.install_game_content_help_link 602 helpLinkId = R.string.install_game_content_help_link
589 ) 603 )
590 } else { 604 } else {
591 return@newInstance MessageDialogFragment.newInstance( 605 return@newInstance MessageDialogFragment.newInstance(
606 this,
592 titleId = R.string.install_game_content_success, 607 titleId = R.string.install_game_content_success,
593 descriptionString = installResult.toString().trim() 608 descriptionString = installResult.toString().trim()
594 ) 609 )
@@ -596,4 +611,110 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
596 }.show(supportFragmentManager, IndeterminateProgressDialogFragment.TAG) 611 }.show(supportFragmentManager, IndeterminateProgressDialogFragment.TAG)
597 } 612 }
598 } 613 }
614
615 val exportUserData = registerForActivityResult(
616 ActivityResultContracts.CreateDocument("application/zip")
617 ) { result ->
618 if (result == null) {
619 return@registerForActivityResult
620 }
621
622 IndeterminateProgressDialogFragment.newInstance(
623 this,
624 R.string.exporting_user_data,
625 true
626 ) {
627 val zos = ZipOutputStream(
628 BufferedOutputStream(contentResolver.openOutputStream(result))
629 )
630 zos.use { stream ->
631 File(DirectoryInitialization.userDirectory!!).walkTopDown().forEach { file ->
632 if (taskViewModel.cancelled.value) {
633 return@newInstance R.string.user_data_export_cancelled
634 }
635
636 if (!file.isDirectory) {
637 val newPath = file.path.substring(
638 DirectoryInitialization.userDirectory!!.length,
639 file.path.length
640 )
641 stream.putNextEntry(ZipEntry(newPath))
642 stream.write(file.readBytes())
643 stream.closeEntry()
644 }
645 }
646 }
647 return@newInstance getString(R.string.user_data_export_success)
648 }.show(supportFragmentManager, IndeterminateProgressDialogFragment.TAG)
649 }
650
651 val importUserData =
652 registerForActivityResult(ActivityResultContracts.OpenDocument()) { result ->
653 if (result == null) {
654 return@registerForActivityResult
655 }
656
657 IndeterminateProgressDialogFragment.newInstance(
658 this,
659 R.string.importing_user_data
660 ) {
661 val checkStream =
662 ZipInputStream(BufferedInputStream(contentResolver.openInputStream(result)))
663 var isYuzuBackup = false
664 checkStream.use { stream ->
665 var ze: ZipEntry? = null
666 while (stream.nextEntry?.also { ze = it } != null) {
667 if (ze!!.name.trim() == "/config/config.ini") {
668 isYuzuBackup = true
669 return@use
670 }
671 }
672 }
673 if (!isYuzuBackup) {
674 return@newInstance getString(R.string.invalid_yuzu_backup)
675 }
676
677 File(DirectoryInitialization.userDirectory!!).deleteRecursively()
678
679 val zis =
680 ZipInputStream(BufferedInputStream(contentResolver.openInputStream(result)))
681 val userDirectory = File(DirectoryInitialization.userDirectory!!)
682 val canonicalPath = userDirectory.canonicalPath + '/'
683 zis.use { stream ->
684 var ze: ZipEntry? = stream.nextEntry
685 while (ze != null) {
686 val newFile = File(userDirectory, ze!!.name)
687 val destinationDirectory =
688 if (ze!!.isDirectory) newFile else newFile.parentFile
689
690 if (!newFile.canonicalPath.startsWith(canonicalPath)) {
691 throw SecurityException(
692 "Zip file attempted path traversal! ${ze!!.name}"
693 )
694 }
695
696 if (!destinationDirectory.isDirectory && !destinationDirectory.mkdirs()) {
697 throw IOException("Failed to create directory $destinationDirectory")
698 }
699
700 if (!ze!!.isDirectory) {
701 val buffer = ByteArray(8096)
702 var read: Int
703 BufferedOutputStream(FileOutputStream(newFile)).use { bos ->
704 while (zis.read(buffer).also { read = it } != -1) {
705 bos.write(buffer, 0, read)
706 }
707 }
708 }
709 ze = stream.nextEntry
710 }
711 }
712
713 // Reinitialize relevant data
714 NativeLibrary.initializeEmulation()
715 gamesViewModel.reloadGames(false)
716
717 return@newInstance getString(R.string.user_data_import_success)
718 }.show(supportFragmentManager, IndeterminateProgressDialogFragment.TAG)
719 }
599} 720}
diff --git a/src/android/app/src/main/res/drawable/ic_export.xml b/src/android/app/src/main/res/drawable/ic_export.xml
new file mode 100644
index 000000000..463d2f41c
--- /dev/null
+++ b/src/android/app/src/main/res/drawable/ic_export.xml
@@ -0,0 +1,9 @@
1<vector xmlns:android="http://schemas.android.com/apk/res/android"
2 android:width="24dp"
3 android:height="24dp"
4 android:viewportWidth="24"
5 android:viewportHeight="24">
6 <path
7 android:fillColor="?attr/colorControlNormal"
8 android:pathData="M9,16h6v-6h4l-7,-7 -7,7h4zM5,18h14v2L5,20z" />
9</vector>
diff --git a/src/android/app/src/main/res/drawable/ic_import.xml b/src/android/app/src/main/res/drawable/ic_import.xml
new file mode 100644
index 000000000..3a99dd5e6
--- /dev/null
+++ b/src/android/app/src/main/res/drawable/ic_import.xml
@@ -0,0 +1,9 @@
1<vector xmlns:android="http://schemas.android.com/apk/res/android"
2 android:width="24dp"
3 android:height="24dp"
4 android:viewportWidth="24"
5 android:viewportHeight="24">
6 <path
7 android:fillColor="?attr/colorControlNormal"
8 android:pathData="M19,9h-4V3H9v6H5l7,7 7,-7zM5,18v2h14v-2H5z" />
9</vector>
diff --git a/src/android/app/src/main/res/layout/dialog_progress_bar.xml b/src/android/app/src/main/res/layout/dialog_progress_bar.xml
index d17711a65..0209ea082 100644
--- a/src/android/app/src/main/res/layout/dialog_progress_bar.xml
+++ b/src/android/app/src/main/res/layout/dialog_progress_bar.xml
@@ -1,24 +1,8 @@
1<?xml version="1.0" encoding="utf-8"?> 1<?xml version="1.0" encoding="utf-8"?>
2<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" 2<com.google.android.material.progressindicator.LinearProgressIndicator xmlns:android="http://schemas.android.com/apk/res/android"
3 android:layout_width="match_parent"
4 android:layout_height="match_parent"
5 xmlns:app="http://schemas.android.com/apk/res-auto" 3 xmlns:app="http://schemas.android.com/apk/res-auto"
6 android:orientation="vertical"> 4 android:id="@+id/progress_bar"
7 5 android:layout_width="match_parent"
8 <com.google.android.material.progressindicator.LinearProgressIndicator 6 android:layout_height="wrap_content"
9 android:id="@+id/progress_bar" 7 android:padding="24dp"
10 android:layout_width="match_parent" 8 app:trackCornerRadius="4dp" />
11 android:layout_height="wrap_content"
12 android:layout_margin="24dp"
13 app:trackCornerRadius="4dp" />
14
15 <TextView
16 android:id="@+id/progress_text"
17 android:layout_width="match_parent"
18 android:layout_height="wrap_content"
19 android:layout_marginLeft="24dp"
20 android:layout_marginRight="24dp"
21 android:layout_marginBottom="24dp"
22 android:gravity="end" />
23
24</LinearLayout>
diff --git a/src/android/app/src/main/res/layout/fragment_about.xml b/src/android/app/src/main/res/layout/fragment_about.xml
index 3e1d98451..36b350338 100644
--- a/src/android/app/src/main/res/layout/fragment_about.xml
+++ b/src/android/app/src/main/res/layout/fragment_about.xml
@@ -184,6 +184,67 @@
184 <LinearLayout 184 <LinearLayout
185 android:layout_width="match_parent" 185 android:layout_width="match_parent"
186 android:layout_height="wrap_content" 186 android:layout_height="wrap_content"
187 android:orientation="horizontal">
188
189 <LinearLayout
190 android:layout_width="match_parent"
191 android:layout_height="wrap_content"
192 android:paddingVertical="16dp"
193 android:paddingHorizontal="16dp"
194 android:orientation="vertical"
195 android:layout_weight="1">
196
197 <com.google.android.material.textview.MaterialTextView
198 style="@style/TextAppearance.Material3.TitleMedium"
199 android:layout_width="match_parent"
200 android:layout_height="wrap_content"
201 android:layout_marginHorizontal="24dp"
202 android:textAlignment="viewStart"
203 android:text="@string/user_data" />
204
205 <com.google.android.material.textview.MaterialTextView
206 style="@style/TextAppearance.Material3.BodyMedium"
207 android:layout_width="match_parent"
208 android:layout_height="wrap_content"
209 android:layout_marginHorizontal="24dp"
210 android:layout_marginTop="6dp"
211 android:textAlignment="viewStart"
212 android:text="@string/user_data_description" />
213
214 </LinearLayout>
215
216 <Button
217 android:id="@+id/button_import"
218 style="@style/Widget.Material3.Button.IconButton.Filled.Tonal"
219 android:layout_width="wrap_content"
220 android:layout_height="wrap_content"
221 android:layout_gravity="center_vertical"
222 android:contentDescription="@string/string_import"
223 android:tooltipText="@string/string_import"
224 app:icon="@drawable/ic_import" />
225
226 <Button
227 android:id="@+id/button_export"
228 style="@style/Widget.Material3.Button.IconButton.Filled.Tonal"
229 android:layout_width="wrap_content"
230 android:layout_height="wrap_content"
231 android:layout_marginStart="12dp"
232 android:layout_marginEnd="24dp"
233 android:layout_gravity="center_vertical"
234 android:contentDescription="@string/export"
235 android:tooltipText="@string/export"
236 app:icon="@drawable/ic_export" />
237
238 </LinearLayout>
239
240 <com.google.android.material.divider.MaterialDivider
241 android:layout_width="match_parent"
242 android:layout_height="wrap_content"
243 android:layout_marginHorizontal="20dp" />
244
245 <LinearLayout
246 android:layout_width="match_parent"
247 android:layout_height="wrap_content"
187 android:orientation="horizontal" 248 android:orientation="horizontal"
188 android:gravity="center_horizontal" 249 android:gravity="center_horizontal"
189 android:layout_marginTop="12dp" 250 android:layout_marginTop="12dp"
diff --git a/src/android/app/src/main/res/values/strings.xml b/src/android/app/src/main/res/values/strings.xml
index b163e6fc1..0730143bd 100644
--- a/src/android/app/src/main/res/values/strings.xml
+++ b/src/android/app/src/main/res/values/strings.xml
@@ -128,6 +128,15 @@
128 <string name="contributors_link">https://github.com/yuzu-emu/yuzu/graphs/contributors</string> 128 <string name="contributors_link">https://github.com/yuzu-emu/yuzu/graphs/contributors</string>
129 <string name="licenses_description">Projects that make yuzu for Android possible</string> 129 <string name="licenses_description">Projects that make yuzu for Android possible</string>
130 <string name="build">Build</string> 130 <string name="build">Build</string>
131 <string name="user_data">User data</string>
132 <string name="user_data_description">Import/export all app data.\n\nWhen importing user data, all existing user data will be deleted!</string>
133 <string name="exporting_user_data">Exporting user data…</string>
134 <string name="importing_user_data">Importing user data…</string>
135 <string name="import_user_data">Import user data</string>
136 <string name="invalid_yuzu_backup">Invalid yuzu backup</string>
137 <string name="user_data_export_success">User data exported successfully</string>
138 <string name="user_data_import_success">User data imported successfully</string>
139 <string name="user_data_export_cancelled">Export cancelled</string>
131 <string name="support_link">https://discord.gg/u77vRWY</string> 140 <string name="support_link">https://discord.gg/u77vRWY</string>
132 <string name="website_link">https://yuzu-emu.org/</string> 141 <string name="website_link">https://yuzu-emu.org/</string>
133 <string name="github_link">https://github.com/yuzu-emu</string> 142 <string name="github_link">https://github.com/yuzu-emu</string>
@@ -215,6 +224,9 @@
215 <string name="auto">Auto</string> 224 <string name="auto">Auto</string>
216 <string name="submit">Submit</string> 225 <string name="submit">Submit</string>
217 <string name="string_null">Null</string> 226 <string name="string_null">Null</string>
227 <string name="string_import">Import</string>
228 <string name="export">Export</string>
229 <string name="cancelling">Cancelling</string>
218 230
219 <!-- GPU driver installation --> 231 <!-- GPU driver installation -->
220 <string name="select_gpu_driver">Select GPU driver</string> 232 <string name="select_gpu_driver">Select GPU driver</string>