mirror of
https://github.com/skyline-emu/skyline.git
synced 2024-11-25 14:06:53 +01:00
Address some feedback
Replace 0-9 with \d Change zip import to use cache dir Move export and import logic to their own functions Delete zip after being shared
This commit is contained in:
parent
a6c2699395
commit
6e68786bd8
@ -10,6 +10,7 @@ import android.content.Intent
|
||||
import android.content.pm.ShortcutInfo
|
||||
import android.content.pm.ShortcutManager
|
||||
import android.graphics.drawable.Icon
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.provider.DocumentsContract
|
||||
import android.view.KeyEvent
|
||||
@ -34,14 +35,13 @@ import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.io.BufferedInputStream
|
||||
import java.io.BufferedOutputStream
|
||||
import java.io.File
|
||||
import java.io.FileOutputStream
|
||||
import java.io.FilenameFilter
|
||||
import java.time.LocalDateTime
|
||||
import java.time.format.DateTimeFormatter
|
||||
import java.util.zip.ZipEntry
|
||||
import java.util.zip.ZipInputStream
|
||||
import java.util.zip.ZipOutputStream
|
||||
|
||||
/**
|
||||
@ -69,35 +69,7 @@ class AppDialog : BottomSheetDialogFragment() {
|
||||
private val savesFolderRoot by lazy { "${requireContext().getPublicFilesDir().canonicalPath}/switch/nand/user/save/0000000000000000/00000000000000000000000000000001/" }
|
||||
private var lastZipCreated : File? = null
|
||||
private val documentPicker = registerForActivityResult(ActivityResultContracts.OpenDocument()) {
|
||||
it?.let { uri ->
|
||||
try {
|
||||
val savesFolder = File(savesFolderRoot)
|
||||
var inputZip = requireContext().contentResolver.openInputStream(uri)
|
||||
var validZip = false
|
||||
// A TitleID must be the first folder name inside the zip in order to be considered valid.
|
||||
if (inputZip != null) {
|
||||
ZipInputStream(BufferedInputStream(inputZip)).use { zis ->
|
||||
validZip = Regex("^0100[0-9A-Fa-f]{12}/").containsMatchIn(zis.nextEntry.name)
|
||||
}
|
||||
}
|
||||
inputZip = requireContext().contentResolver.openInputStream(uri)
|
||||
if (inputZip != null && validZip){
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
ZipUtils.unzip(inputZip, savesFolder)
|
||||
withContext(Dispatchers.Main){
|
||||
val isSaveFileOfThisGame = File("$savesFolderRoot${item.titleId}").exists()
|
||||
binding.deleteSave.isEnabled = isSaveFileOfThisGame
|
||||
binding.exportSave.isEnabled = isSaveFileOfThisGame
|
||||
Snackbar.make(binding.root, R.string.save_file_imported_ok, Snackbar.LENGTH_LONG).show()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Snackbar.make(binding.root, getString(R.string.save_file_invalid_zip_structure), Snackbar.LENGTH_LONG).show()
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Snackbar.make(binding.root, getString(R.string.error), Snackbar.LENGTH_LONG).show()
|
||||
}
|
||||
}
|
||||
it?.let { uri -> importSave(uri) }
|
||||
}
|
||||
|
||||
/**
|
||||
@ -178,40 +150,7 @@ class AppDialog : BottomSheetDialogFragment() {
|
||||
|
||||
binding.exportSave.isEnabled = saveExists
|
||||
binding.exportSave.setOnClickListener {
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
try {
|
||||
val saveFolder = File(saveFolderPath)
|
||||
val outputZipFile = File("$savesFolderRoot${item.title} (v${binding.gameVersion.text}) [${item.titleId}] - ${LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm"))}.zip")
|
||||
lastZipCreated?.delete()
|
||||
outputZipFile.createNewFile()
|
||||
ZipOutputStream(BufferedOutputStream(FileOutputStream(outputZipFile))).use { zos ->
|
||||
saveFolder.walkTopDown().forEach { file ->
|
||||
val zipFileName = file.absolutePath.removePrefix(savesFolderRoot).removePrefix("/")
|
||||
if (zipFileName == "") return@forEach
|
||||
val entry = ZipEntry("$zipFileName${(if (file.isDirectory) "/" else "")}")
|
||||
zos.putNextEntry(entry)
|
||||
if (file.isFile) {
|
||||
file.inputStream().use { fis -> fis.copyTo(zos) }
|
||||
}
|
||||
}
|
||||
}
|
||||
lastZipCreated = outputZipFile
|
||||
} catch (e: Exception) {
|
||||
withContext(Dispatchers.Main){
|
||||
Snackbar.make(binding.root, e.message as CharSequence, Snackbar.LENGTH_LONG).show()
|
||||
}
|
||||
return@launch
|
||||
}
|
||||
|
||||
withContext(Dispatchers.Main) {
|
||||
val file = DocumentFile.fromSingleUri(requireContext(), DocumentsContract.buildDocumentUri(DocumentsProvider.AUTHORITY, "${DocumentsProvider.ROOT_ID}/switch/nand/user/save/0000000000000000/00000000000000000000000000000001/${lastZipCreated!!.name}"))!!
|
||||
val intent = Intent(Intent.ACTION_SEND)
|
||||
.setDataAndType(file.uri, "application/zip")
|
||||
.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||
.putExtra(Intent.EXTRA_STREAM, file.uri)
|
||||
startActivity(Intent.createChooser(intent, "Share save file"))
|
||||
}
|
||||
}
|
||||
exportSave(saveFolderPath)
|
||||
}
|
||||
|
||||
binding.gameTitleId.setOnLongClickListener {
|
||||
@ -231,8 +170,107 @@ class AppDialog : BottomSheetDialogFragment() {
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroyView(){
|
||||
super.onDestroyView()
|
||||
/**
|
||||
* Zips the save file located in the given folder path and creates a new zip file with the name and version of the game, and the current date and time.
|
||||
* @param saveFolderPath The path to the folder containing the save file to zip.
|
||||
* @return true if the zip file is successfully created, false otherwise.
|
||||
*/
|
||||
private fun zipSave(saveFolderPath : String) : Boolean {
|
||||
try {
|
||||
val saveFolder = File(saveFolderPath)
|
||||
val outputZipFile = File("$savesFolderRoot${item.title} (v${binding.gameVersion.text}) [${item.titleId}] - ${LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm"))}.zip")
|
||||
lastZipCreated?.delete()
|
||||
outputZipFile.createNewFile()
|
||||
ZipOutputStream(BufferedOutputStream(FileOutputStream(outputZipFile))).use { zos ->
|
||||
saveFolder.walkTopDown().forEach { file ->
|
||||
val zipFileName = file.absolutePath.removePrefix(savesFolderRoot).removePrefix("/")
|
||||
if (zipFileName == "")
|
||||
return@forEach
|
||||
val entry = ZipEntry("$zipFileName${(if (file.isDirectory) "/" else "")}")
|
||||
zos.putNextEntry(entry)
|
||||
if (file.isFile) {
|
||||
file.inputStream().use { fis -> fis.copyTo(zos) }
|
||||
}
|
||||
}
|
||||
}
|
||||
lastZipCreated = outputZipFile
|
||||
} catch (e : Exception) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
private val startForResultExportSave = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { _ ->
|
||||
lastZipCreated?.delete()
|
||||
}
|
||||
|
||||
/**
|
||||
* Exports the save file located in the given folder path by creating a zip file and sharing it via intent.
|
||||
* @param saveFolderPath The path to the folder containing the save file(s) to export.
|
||||
*/
|
||||
private fun exportSave(saveFolderPath : String) {
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
if (!zipSave(saveFolderPath)) {
|
||||
withContext(Dispatchers.Main) {
|
||||
Snackbar.make(binding.root, R.string.error, Snackbar.LENGTH_LONG).show()
|
||||
}
|
||||
return@launch
|
||||
}
|
||||
|
||||
withContext(Dispatchers.Main) {
|
||||
val file = DocumentFile.fromSingleUri(requireContext(), DocumentsContract.buildDocumentUri(DocumentsProvider.AUTHORITY, "${DocumentsProvider.ROOT_ID}/switch/nand/user/save/0000000000000000/00000000000000000000000000000001/${lastZipCreated!!.name}"))!!
|
||||
val intent = Intent(Intent.ACTION_SEND)
|
||||
.setDataAndType(file.uri, "application/zip")
|
||||
.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||
.putExtra(Intent.EXTRA_STREAM, file.uri)
|
||||
startForResultExportSave.launch(Intent.createChooser(intent, "Share save file"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Imports the save files contained in the zip file, and replaces any existing ones with the new save file.
|
||||
* @param zipUri The Uri of the zip file containing the save file(s) to import.
|
||||
*/
|
||||
private fun importSave(zipUri : Uri) {
|
||||
val inputZip = requireContext().contentResolver.openInputStream(zipUri)
|
||||
// A zip needs to have at least one subfolder named after a TitleId in order to be considered valid.
|
||||
var validZip = false
|
||||
val savesFolder = File(savesFolderRoot)
|
||||
val cacheSaveDir = File("${requireContext().cacheDir.path}/saves/")
|
||||
cacheSaveDir.mkdir()
|
||||
|
||||
if (inputZip == null) {
|
||||
Snackbar.make(binding.root, getString(R.string.error), Snackbar.LENGTH_LONG).show()
|
||||
return
|
||||
}
|
||||
|
||||
val filterTitleId = FilenameFilter { _, dirName -> dirName.matches(Regex("^0100[\\dA-Fa-f]{12}$")) }
|
||||
|
||||
try {
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
ZipUtils.unzip(inputZip, cacheSaveDir)
|
||||
cacheSaveDir.list(filterTitleId)?.forEach { savePath ->
|
||||
File(savesFolder, savePath).deleteRecursively()
|
||||
File(cacheSaveDir, savePath).copyRecursively(File(savesFolder, savePath), true)
|
||||
validZip = true
|
||||
}
|
||||
|
||||
withContext(Dispatchers.Main) {
|
||||
if (validZip) {
|
||||
val isSaveFileOfThisGame = File("$savesFolderRoot${item.titleId}").exists()
|
||||
binding.deleteSave.isEnabled = isSaveFileOfThisGame
|
||||
binding.exportSave.isEnabled = isSaveFileOfThisGame
|
||||
Snackbar.make(binding.root, R.string.save_file_imported_ok, Snackbar.LENGTH_LONG).show()
|
||||
} else {
|
||||
Snackbar.make(binding.root, getString(R.string.save_file_invalid_zip_structure), Snackbar.LENGTH_LONG).show()
|
||||
}
|
||||
}
|
||||
|
||||
cacheSaveDir.deleteRecursively()
|
||||
}
|
||||
} catch (e : Exception) {
|
||||
Snackbar.make(binding.root, getString(R.string.error), Snackbar.LENGTH_LONG).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -284,5 +284,5 @@
|
||||
<string name="yes">Yes</string>
|
||||
<string name="no">No</string>
|
||||
<string name="save_file_imported_ok">The save file was imported successfully</string>
|
||||
<string name="save_file_invalid_zip_structure">Failed to unzip. First subfolder must be named after the TitleId of the game.</string>
|
||||
<string name="save_file_invalid_zip_structure">Invalid Zip directory structure: the first subfolder name must be the Title ID of the game.</string>
|
||||
</resources>
|
||||
|
Loading…
Reference in New Issue
Block a user