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/NativeLibrary.kt9
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/InstallableFragment.kt219
-rw-r--r--src/android/app/src/main/jni/native.cpp16
-rw-r--r--src/android/app/src/main/res/values/strings.xml10
-rw-r--r--src/core/file_sys/savedata_factory.cpp9
-rw-r--r--src/core/file_sys/savedata_factory.h1
6 files changed, 264 insertions, 0 deletions
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/NativeLibrary.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/NativeLibrary.kt
index 010c44951..b7556e353 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/NativeLibrary.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/NativeLibrary.kt
@@ -548,6 +548,15 @@ object NativeLibrary {
548 external fun getSavePath(programId: String): String 548 external fun getSavePath(programId: String): String
549 549
550 /** 550 /**
551 * Gets the root save directory for the default profile as either
552 * /user/save/account/<user id raw string> or /user/save/000...000/<user id>
553 *
554 * @param future If true, returns the /user/save/account/... directory
555 * @return Save data path that may not exist yet
556 */
557 external fun getDefaultProfileSaveDataRoot(future: Boolean): String
558
559 /**
551 * Adds a file to the manual filesystem provider in our EmulationSession instance 560 * Adds a file to the manual filesystem provider in our EmulationSession instance
552 * @param path Path to the file we're adding. Can be a string representation of a [Uri] or 561 * @param path Path to the file we're adding. Can be a string representation of a [Uri] or
553 * a normal path 562 * a normal path
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/InstallableFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/InstallableFragment.kt
index 569727b90..5b4bf2c9f 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/InstallableFragment.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/InstallableFragment.kt
@@ -7,20 +7,39 @@ import android.os.Bundle
7import android.view.LayoutInflater 7import android.view.LayoutInflater
8import android.view.View 8import android.view.View
9import android.view.ViewGroup 9import android.view.ViewGroup
10import android.widget.Toast
11import androidx.activity.result.contract.ActivityResultContracts
10import androidx.core.view.ViewCompat 12import androidx.core.view.ViewCompat
11import androidx.core.view.WindowInsetsCompat 13import androidx.core.view.WindowInsetsCompat
12import androidx.core.view.updatePadding 14import androidx.core.view.updatePadding
13import androidx.fragment.app.Fragment 15import androidx.fragment.app.Fragment
14import androidx.fragment.app.activityViewModels 16import androidx.fragment.app.activityViewModels
17import androidx.lifecycle.Lifecycle
18import androidx.lifecycle.lifecycleScope
19import androidx.lifecycle.repeatOnLifecycle
15import androidx.navigation.findNavController 20import androidx.navigation.findNavController
16import androidx.recyclerview.widget.GridLayoutManager 21import androidx.recyclerview.widget.GridLayoutManager
17import com.google.android.material.transition.MaterialSharedAxis 22import com.google.android.material.transition.MaterialSharedAxis
23import kotlinx.coroutines.Dispatchers
24import kotlinx.coroutines.launch
25import kotlinx.coroutines.withContext
26import org.yuzu.yuzu_emu.NativeLibrary
18import org.yuzu.yuzu_emu.R 27import org.yuzu.yuzu_emu.R
28import org.yuzu.yuzu_emu.YuzuApplication
19import org.yuzu.yuzu_emu.adapters.InstallableAdapter 29import org.yuzu.yuzu_emu.adapters.InstallableAdapter
20import org.yuzu.yuzu_emu.databinding.FragmentInstallablesBinding 30import org.yuzu.yuzu_emu.databinding.FragmentInstallablesBinding
21import org.yuzu.yuzu_emu.model.HomeViewModel 31import org.yuzu.yuzu_emu.model.HomeViewModel
22import org.yuzu.yuzu_emu.model.Installable 32import org.yuzu.yuzu_emu.model.Installable
33import org.yuzu.yuzu_emu.model.TaskState
23import org.yuzu.yuzu_emu.ui.main.MainActivity 34import org.yuzu.yuzu_emu.ui.main.MainActivity
35import org.yuzu.yuzu_emu.utils.DirectoryInitialization
36import org.yuzu.yuzu_emu.utils.FileUtil
37import java.io.BufferedInputStream
38import java.io.BufferedOutputStream
39import java.io.File
40import java.math.BigInteger
41import java.time.LocalDateTime
42import java.time.format.DateTimeFormatter
24 43
25class InstallableFragment : Fragment() { 44class InstallableFragment : Fragment() {
26 private var _binding: FragmentInstallablesBinding? = null 45 private var _binding: FragmentInstallablesBinding? = null
@@ -56,6 +75,17 @@ class InstallableFragment : Fragment() {
56 binding.root.findNavController().popBackStack() 75 binding.root.findNavController().popBackStack()
57 } 76 }
58 77
78 viewLifecycleOwner.lifecycleScope.launch {
79 repeatOnLifecycle(Lifecycle.State.CREATED) {
80 homeViewModel.openImportSaves.collect {
81 if (it) {
82 importSaves.launch(arrayOf("application/zip"))
83 homeViewModel.setOpenImportSaves(false)
84 }
85 }
86 }
87 }
88
59 val installables = listOf( 89 val installables = listOf(
60 Installable( 90 Installable(
61 R.string.user_data, 91 R.string.user_data,
@@ -64,6 +94,43 @@ class InstallableFragment : Fragment() {
64 export = { mainActivity.exportUserData.launch("export.zip") } 94 export = { mainActivity.exportUserData.launch("export.zip") }
65 ), 95 ),
66 Installable( 96 Installable(
97 R.string.manage_save_data,
98 R.string.manage_save_data_description,
99 install = {
100 MessageDialogFragment.newInstance(
101 requireActivity(),
102 titleId = R.string.import_save_warning,
103 descriptionId = R.string.import_save_warning_description,
104 positiveAction = { homeViewModel.setOpenImportSaves(true) }
105 ).show(parentFragmentManager, MessageDialogFragment.TAG)
106 },
107 export = {
108 val oldSaveDataFolder = File(
109 "${DirectoryInitialization.userDirectory}/nand" +
110 NativeLibrary.getDefaultProfileSaveDataRoot(false)
111 )
112 val futureSaveDataFolder = File(
113 "${DirectoryInitialization.userDirectory}/nand" +
114 NativeLibrary.getDefaultProfileSaveDataRoot(true)
115 )
116 if (!oldSaveDataFolder.exists() && !futureSaveDataFolder.exists()) {
117 Toast.makeText(
118 YuzuApplication.appContext,
119 R.string.no_save_data_found,
120 Toast.LENGTH_SHORT
121 ).show()
122 return@Installable
123 } else {
124 exportSaves.launch(
125 "${getString(R.string.save_data)} " +
126 LocalDateTime.now().format(
127 DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm")
128 )
129 )
130 }
131 }
132 ),
133 Installable(
67 R.string.install_game_content, 134 R.string.install_game_content,
68 R.string.install_game_content_description, 135 R.string.install_game_content_description,
69 install = { mainActivity.installGameUpdate.launch(arrayOf("*/*")) } 136 install = { mainActivity.installGameUpdate.launch(arrayOf("*/*")) }
@@ -121,4 +188,156 @@ class InstallableFragment : Fragment() {
121 188
122 windowInsets 189 windowInsets
123 } 190 }
191
192 private val importSaves =
193 registerForActivityResult(ActivityResultContracts.OpenDocument()) { result ->
194 if (result == null) {
195 return@registerForActivityResult
196 }
197
198 val inputZip = requireContext().contentResolver.openInputStream(result)
199 val cacheSaveDir = File("${requireContext().cacheDir.path}/saves/")
200 cacheSaveDir.mkdir()
201
202 if (inputZip == null) {
203 Toast.makeText(
204 YuzuApplication.appContext,
205 getString(R.string.fatal_error),
206 Toast.LENGTH_LONG
207 ).show()
208 return@registerForActivityResult
209 }
210
211 IndeterminateProgressDialogFragment.newInstance(
212 requireActivity(),
213 R.string.save_files_importing,
214 false
215 ) {
216 try {
217 FileUtil.unzipToInternalStorage(BufferedInputStream(inputZip), cacheSaveDir)
218 val files = cacheSaveDir.listFiles()
219 var successfulImports = 0
220 var failedImports = 0
221 if (files != null) {
222 for (file in files) {
223 if (file.isDirectory) {
224 val baseSaveDir =
225 NativeLibrary.getSavePath(BigInteger(file.name, 16).toString())
226 if (baseSaveDir.isEmpty()) {
227 failedImports++
228 continue
229 }
230
231 val internalSaveFolder = File(
232 "${DirectoryInitialization.userDirectory}/nand$baseSaveDir"
233 )
234 internalSaveFolder.deleteRecursively()
235 internalSaveFolder.mkdir()
236 file.copyRecursively(target = internalSaveFolder, overwrite = true)
237 successfulImports++
238 }
239 }
240 }
241
242 withContext(Dispatchers.Main) {
243 if (successfulImports == 0) {
244 MessageDialogFragment.newInstance(
245 requireActivity(),
246 titleId = R.string.save_file_invalid_zip_structure,
247 descriptionId = R.string.save_file_invalid_zip_structure_description
248 ).show(parentFragmentManager, MessageDialogFragment.TAG)
249 return@withContext
250 }
251 val successString = if (failedImports > 0) {
252 """
253 ${
254 requireContext().resources.getQuantityString(
255 R.plurals.saves_import_success,
256 successfulImports,
257 successfulImports
258 )
259 }
260 ${
261 requireContext().resources.getQuantityString(
262 R.plurals.saves_import_failed,
263 failedImports,
264 failedImports
265 )
266 }
267 """
268 } else {
269 requireContext().resources.getQuantityString(
270 R.plurals.saves_import_success,
271 successfulImports,
272 successfulImports
273 )
274 }
275 MessageDialogFragment.newInstance(
276 requireActivity(),
277 titleId = R.string.import_complete,
278 descriptionString = successString
279 ).show(parentFragmentManager, MessageDialogFragment.TAG)
280 }
281
282 cacheSaveDir.deleteRecursively()
283 } catch (e: Exception) {
284 Toast.makeText(
285 YuzuApplication.appContext,
286 getString(R.string.fatal_error),
287 Toast.LENGTH_LONG
288 ).show()
289 }
290 }.show(parentFragmentManager, IndeterminateProgressDialogFragment.TAG)
291 }
292
293 private val exportSaves = registerForActivityResult(
294 ActivityResultContracts.CreateDocument("application/zip")
295 ) { result ->
296 if (result == null) {
297 return@registerForActivityResult
298 }
299
300 IndeterminateProgressDialogFragment.newInstance(
301 requireActivity(),
302 R.string.save_files_exporting,
303 false
304 ) {
305 val cacheSaveDir = File("${requireContext().cacheDir.path}/saves/")
306 cacheSaveDir.mkdir()
307
308 val oldSaveDataFolder = File(
309 "${DirectoryInitialization.userDirectory}/nand" +
310 NativeLibrary.getDefaultProfileSaveDataRoot(false)
311 )
312 if (oldSaveDataFolder.exists()) {
313 oldSaveDataFolder.copyRecursively(cacheSaveDir)
314 }
315
316 val futureSaveDataFolder = File(
317 "${DirectoryInitialization.userDirectory}/nand" +
318 NativeLibrary.getDefaultProfileSaveDataRoot(true)
319 )
320 if (futureSaveDataFolder.exists()) {
321 futureSaveDataFolder.copyRecursively(cacheSaveDir)
322 }
323
324 val saveFilesTotal = cacheSaveDir.listFiles()?.size ?: 0
325 if (saveFilesTotal == 0) {
326 cacheSaveDir.deleteRecursively()
327 return@newInstance getString(R.string.no_save_data_found)
328 }
329
330 val zipResult = FileUtil.zipFromInternalStorage(
331 cacheSaveDir,
332 cacheSaveDir.path,
333 BufferedOutputStream(requireContext().contentResolver.openOutputStream(result))
334 )
335 cacheSaveDir.deleteRecursively()
336
337 return@newInstance when (zipResult) {
338 TaskState.Completed -> getString(R.string.export_success)
339 TaskState.Cancelled, TaskState.Failed -> getString(R.string.export_failed)
340 }
341 }.show(parentFragmentManager, IndeterminateProgressDialogFragment.TAG)
342 }
124} 343}
diff --git a/src/android/app/src/main/jni/native.cpp b/src/android/app/src/main/jni/native.cpp
index 056920a4a..136c8dee6 100644
--- a/src/android/app/src/main/jni/native.cpp
+++ b/src/android/app/src/main/jni/native.cpp
@@ -862,6 +862,9 @@ jobjectArray Java_org_yuzu_yuzu_1emu_NativeLibrary_getAddonsForFile(JNIEnv* env,
862jstring Java_org_yuzu_yuzu_1emu_NativeLibrary_getSavePath(JNIEnv* env, jobject jobj, 862jstring Java_org_yuzu_yuzu_1emu_NativeLibrary_getSavePath(JNIEnv* env, jobject jobj,
863 jstring jprogramId) { 863 jstring jprogramId) {
864 auto program_id = EmulationSession::GetProgramId(env, jprogramId); 864 auto program_id = EmulationSession::GetProgramId(env, jprogramId);
865 if (program_id == 0) {
866 return ToJString(env, "");
867 }
865 868
866 auto& system = EmulationSession::GetInstance().System(); 869 auto& system = EmulationSession::GetInstance().System();
867 870
@@ -880,6 +883,19 @@ jstring Java_org_yuzu_yuzu_1emu_NativeLibrary_getSavePath(JNIEnv* env, jobject j
880 return ToJString(env, user_save_data_path); 883 return ToJString(env, user_save_data_path);
881} 884}
882 885
886jstring Java_org_yuzu_yuzu_1emu_NativeLibrary_getDefaultProfileSaveDataRoot(JNIEnv* env,
887 jobject jobj,
888 jboolean jfuture) {
889 Service::Account::ProfileManager manager;
890 // TODO: Pass in a selected user once we get the relevant UI working
891 const auto user_id = manager.GetUser(static_cast<std::size_t>(0));
892 ASSERT(user_id);
893
894 const auto user_save_data_root =
895 FileSys::SaveDataFactory::GetUserGameSaveDataRoot(user_id->AsU128(), jfuture);
896 return ToJString(env, user_save_data_root);
897}
898
883void Java_org_yuzu_yuzu_1emu_NativeLibrary_addFileToFilesystemProvider(JNIEnv* env, jobject jobj, 899void Java_org_yuzu_yuzu_1emu_NativeLibrary_addFileToFilesystemProvider(JNIEnv* env, jobject jobj,
884 jstring jpath) { 900 jstring jpath) {
885 EmulationSession::GetInstance().ConfigureFilesystemProvider(GetJString(env, jpath)); 901 EmulationSession::GetInstance().ConfigureFilesystemProvider(GetJString(env, jpath));
diff --git a/src/android/app/src/main/res/values/strings.xml b/src/android/app/src/main/res/values/strings.xml
index 83aa1b781..3bb92ad67 100644
--- a/src/android/app/src/main/res/values/strings.xml
+++ b/src/android/app/src/main/res/values/strings.xml
@@ -133,6 +133,15 @@
133 <string name="add_game_folder">Add game folder</string> 133 <string name="add_game_folder">Add game folder</string>
134 <string name="folder_already_added">This folder was already added!</string> 134 <string name="folder_already_added">This folder was already added!</string>
135 <string name="game_folder_properties">Game folder properties</string> 135 <string name="game_folder_properties">Game folder properties</string>
136 <plurals name="saves_import_failed">
137 <item quantity="one">Failed to import %d save</item>
138 <item quantity="other">Failed to import %d saves</item>
139 </plurals>
140 <plurals name="saves_import_success">
141 <item quantity="one">Successfully imported %d save</item>
142 <item quantity="other">Successfully imported %d saves</item>
143 </plurals>
144 <string name="no_save_data_found">No save data found</string>
136 145
137 <!-- Applet launcher strings --> 146 <!-- Applet launcher strings -->
138 <string name="applets">Applet launcher</string> 147 <string name="applets">Applet launcher</string>
@@ -276,6 +285,7 @@
276 <string name="global">Global</string> 285 <string name="global">Global</string>
277 <string name="custom">Custom</string> 286 <string name="custom">Custom</string>
278 <string name="notice">Notice</string> 287 <string name="notice">Notice</string>
288 <string name="import_complete">Import complete</string>
279 289
280 <!-- GPU driver installation --> 290 <!-- GPU driver installation -->
281 <string name="select_gpu_driver">Select GPU driver</string> 291 <string name="select_gpu_driver">Select GPU driver</string>
diff --git a/src/core/file_sys/savedata_factory.cpp b/src/core/file_sys/savedata_factory.cpp
index 8d5d593e8..12b3bd797 100644
--- a/src/core/file_sys/savedata_factory.cpp
+++ b/src/core/file_sys/savedata_factory.cpp
@@ -189,6 +189,15 @@ std::string SaveDataFactory::GetFullPath(Core::System& system, VirtualDir dir,
189 } 189 }
190} 190}
191 191
192std::string SaveDataFactory::GetUserGameSaveDataRoot(u128 user_id, bool future) {
193 if (future) {
194 Common::UUID uuid;
195 std::memcpy(uuid.uuid.data(), user_id.data(), sizeof(Common::UUID));
196 return fmt::format("/user/save/account/{}", uuid.RawString());
197 }
198 return fmt::format("/user/save/{:016X}/{:016X}{:016X}", 0, user_id[1], user_id[0]);
199}
200
192SaveDataSize SaveDataFactory::ReadSaveDataSize(SaveDataType type, u64 title_id, 201SaveDataSize SaveDataFactory::ReadSaveDataSize(SaveDataType type, u64 title_id,
193 u128 user_id) const { 202 u128 user_id) const {
194 const auto path = 203 const auto path =
diff --git a/src/core/file_sys/savedata_factory.h b/src/core/file_sys/savedata_factory.h
index e3a0f8cef..fd4887e99 100644
--- a/src/core/file_sys/savedata_factory.h
+++ b/src/core/file_sys/savedata_factory.h
@@ -101,6 +101,7 @@ public:
101 static std::string GetSaveDataSpaceIdPath(SaveDataSpaceId space); 101 static std::string GetSaveDataSpaceIdPath(SaveDataSpaceId space);
102 static std::string GetFullPath(Core::System& system, VirtualDir dir, SaveDataSpaceId space, 102 static std::string GetFullPath(Core::System& system, VirtualDir dir, SaveDataSpaceId space,
103 SaveDataType type, u64 title_id, u128 user_id, u64 save_id); 103 SaveDataType type, u64 title_id, u128 user_id, u64 save_id);
104 static std::string GetUserGameSaveDataRoot(u128 user_id, bool future);
104 105
105 SaveDataSize ReadSaveDataSize(SaveDataType type, u64 title_id, u128 user_id) const; 106 SaveDataSize ReadSaveDataSize(SaveDataType type, u64 title_id, u128 user_id) const;
106 void WriteSaveDataSize(SaveDataType type, u64 title_id, u128 user_id, 107 void WriteSaveDataSize(SaveDataType type, u64 title_id, u128 user_id,