diff options
| author | 2023-09-20 10:17:24 -0400 | |
|---|---|---|
| committer | 2023-09-20 10:17:24 -0400 | |
| commit | 1fae4a01a820f2a83936c806537573c11126029c (patch) | |
| tree | cf63ed186f26755bac24c122bc6b6b81e2a14338 /src | |
| parent | Merge pull request #11542 from t895/touch-offset-fix (diff) | |
| parent | android: Add import/export buttons for user data (diff) | |
| download | yuzu-1fae4a01a820f2a83936c806537573c11126029c.tar.gz yuzu-1fae4a01a820f2a83936c806537573c11126029c.tar.xz yuzu-1fae4a01a820f2a83936c806537573c11126029c.zip | |
Merge pull request #11543 from t895/import-export-user-data
android: Add import/export buttons for user data
Diffstat (limited to 'src')
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 | |||
| 26 | import org.yuzu.yuzu_emu.R | 26 | import org.yuzu.yuzu_emu.R |
| 27 | import org.yuzu.yuzu_emu.databinding.FragmentAboutBinding | 27 | import org.yuzu.yuzu_emu.databinding.FragmentAboutBinding |
| 28 | import org.yuzu.yuzu_emu.model.HomeViewModel | 28 | import org.yuzu.yuzu_emu.model.HomeViewModel |
| 29 | import org.yuzu.yuzu_emu.ui.main.MainActivity | ||
| 29 | 30 | ||
| 30 | class AboutFragment : Fragment() { | 31 | class 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 @@ | |||
| 4 | package org.yuzu.yuzu_emu.fragments | 4 | package org.yuzu.yuzu_emu.fragments |
| 5 | 5 | ||
| 6 | import android.app.Dialog | 6 | import android.app.Dialog |
| 7 | import android.content.DialogInterface | ||
| 7 | import android.os.Bundle | 8 | import android.os.Bundle |
| 8 | import android.view.LayoutInflater | 9 | import android.view.LayoutInflater |
| 9 | import android.view.View | 10 | import android.view.View |
| @@ -18,6 +19,7 @@ import androidx.lifecycle.lifecycleScope | |||
| 18 | import androidx.lifecycle.repeatOnLifecycle | 19 | import androidx.lifecycle.repeatOnLifecycle |
| 19 | import com.google.android.material.dialog.MaterialAlertDialogBuilder | 20 | import com.google.android.material.dialog.MaterialAlertDialogBuilder |
| 20 | import kotlinx.coroutines.launch | 21 | import kotlinx.coroutines.launch |
| 22 | import org.yuzu.yuzu_emu.R | ||
| 21 | import org.yuzu.yuzu_emu.databinding.DialogProgressBarBinding | 23 | import org.yuzu.yuzu_emu.databinding.DialogProgressBarBinding |
| 22 | import org.yuzu.yuzu_emu.model.TaskViewModel | 24 | import 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 @@ | |||
| 4 | package org.yuzu.yuzu_emu.fragments | 4 | package org.yuzu.yuzu_emu.fragments |
| 5 | 5 | ||
| 6 | import android.app.Dialog | 6 | import android.app.Dialog |
| 7 | import android.content.DialogInterface | ||
| 7 | import android.content.Intent | 8 | import android.content.Intent |
| 8 | import android.net.Uri | 9 | import android.net.Uri |
| 9 | import android.os.Bundle | 10 | import android.os.Bundle |
| 10 | import androidx.fragment.app.DialogFragment | 11 | import androidx.fragment.app.DialogFragment |
| 12 | import androidx.fragment.app.FragmentActivity | ||
| 13 | import androidx.fragment.app.activityViewModels | ||
| 14 | import androidx.lifecycle.ViewModelProvider | ||
| 11 | import com.google.android.material.dialog.MaterialAlertDialogBuilder | 15 | import com.google.android.material.dialog.MaterialAlertDialogBuilder |
| 12 | import org.yuzu.yuzu_emu.R | 16 | import org.yuzu.yuzu_emu.R |
| 17 | import org.yuzu.yuzu_emu.model.MessageDialogViewModel | ||
| 13 | 18 | ||
| 14 | class MessageDialogFragment : DialogFragment() { | 19 | class 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 | |||
| 4 | package org.yuzu.yuzu_emu.model | ||
| 5 | |||
| 6 | import androidx.lifecycle.ViewModel | ||
| 7 | |||
| 8 | class 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 | |||
| 46 | import org.yuzu.yuzu_emu.fragments.MessageDialogFragment | 46 | import org.yuzu.yuzu_emu.fragments.MessageDialogFragment |
| 47 | import org.yuzu.yuzu_emu.model.GamesViewModel | 47 | import org.yuzu.yuzu_emu.model.GamesViewModel |
| 48 | import org.yuzu.yuzu_emu.model.HomeViewModel | 48 | import org.yuzu.yuzu_emu.model.HomeViewModel |
| 49 | import org.yuzu.yuzu_emu.model.TaskViewModel | ||
| 49 | import org.yuzu.yuzu_emu.utils.* | 50 | import org.yuzu.yuzu_emu.utils.* |
| 51 | import java.io.BufferedInputStream | ||
| 52 | import java.io.BufferedOutputStream | ||
| 53 | import java.io.FileOutputStream | ||
| 54 | import java.util.zip.ZipEntry | ||
| 55 | import java.util.zip.ZipInputStream | ||
| 56 | import java.util.zip.ZipOutputStream | ||
| 50 | 57 | ||
| 51 | class MainActivity : AppCompatActivity(), ThemeProvider { | 58 | class 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> |