Android: Export / import save files

Now save files can be exported to ZIP / imported from ZIP by the
option menu of the launcher activity.
This commit is contained in:
kichikuou
2020-02-01 18:52:23 +09:00
parent b7bbbaee61
commit 6854d9bea2
5 changed files with 147 additions and 23 deletions
@@ -25,7 +25,9 @@ import android.util.Log
import java.io.*
import java.nio.charset.Charset
import java.util.*
import java.util.zip.ZipEntry
import java.util.zip.ZipInputStream
import java.util.zip.ZipOutputStream
private var gLauncher: Launcher? = null
@@ -36,6 +38,7 @@ interface LauncherObserver {
fun onInstallFailure(msgId: Int)
}
private const val SAVE_DIR = "save"
private const val PROGRESS = 0
private const val SUCCESS = 1
private const val FAILURE = 2
@@ -107,7 +110,7 @@ class Launcher private constructor(private val rootDir: File) {
for (path in rootDir.listFiles()) {
if (!path.isDirectory)
continue
if (path.name == "save") {
if (path.name == SAVE_DIR) {
saveDirFound = true
continue
}
@@ -120,7 +123,7 @@ class Launcher private constructor(private val rootDir: File) {
}
}
if (!saveDirFound) {
File(rootDir, "save").mkdir()
File(rootDir, SAVE_DIR).mkdir()
}
}
@@ -134,28 +137,65 @@ class Launcher private constructor(private val rootDir: File) {
}
}
// Throws IOException
fun exportSaveData(output: OutputStream) {
if (Build.VERSION.SDK_INT >= 24) {
ZipOutputStream(output.buffered(), Charset.forName("Shift_JIS"))
} else {
ZipOutputStream(output.buffered())
}.use { zip ->
for (path in File(rootDir, SAVE_DIR).listFiles()) {
if (path.isDirectory || path.name.endsWith(".asd."))
continue
val pathInZip = "${SAVE_DIR}/${path.name}"
Log.i("exportSaveData", pathInZip)
zip.putNextEntry(ZipEntry(pathInZip))
path.inputStream().buffered().use {
it.copyTo(zip)
}
}
}
}
fun importSaveData(input: InputStream): Int? {
try {
var imported = false
forEachZipEntry(input) { zipEntry, zip ->
// Process only files directly under save/
if (zipEntry.isDirectory || !zipEntry.name.startsWith("save/") ||
zipEntry.name.count{it == '/'} != 1)
return@forEachZipEntry
Log.i("importSaveData", zipEntry.name)
FileOutputStream(File(rootDir, zipEntry.name)).buffered().use {
zip.copyTo(it)
}
imported = true
}
return if (imported) null else R.string.no_data_to_import
} catch (e: UTFDataFormatException) {
// Attempted to read Shift_JIS zip in Android < 7
return R.string.unsupported_zip
} catch (e: IOException) {
Log.e("launcher", "Failed to extract ZIP", e)
return R.string.zip_extraction_error
}
}
private fun extractFiles(input: InputStream, outDir: File, handler: Handler) {
try {
val configWriter = GameConfigWriter()
val zip = if (Build.VERSION.SDK_INT >= 24) {
ZipInputStream(input.buffered(), Charset.forName("Shift_JIS"))
} else {
ZipInputStream(input.buffered())
}
while (true) {
val zipEntry = zip.nextEntry ?: break
Log.v("extractFiles", zipEntry.name)
forEachZipEntry(input) { zipEntry, zip ->
Log.i("extractFiles", zipEntry.name)
val path = File(outDir, zipEntry.name)
if (zipEntry.isDirectory)
continue
return@forEachZipEntry
path.parentFile.mkdirs()
handler.sendMessage(handler.obtainMessage(PROGRESS, zipEntry.name))
val output = FileOutputStream(path).buffered()
zip.copyTo(output)
output.close()
FileOutputStream(path).buffered().use {
zip.copyTo(it)
}
configWriter.maybeAdd(zipEntry.name)
}
zip.close()
configWriter.write(outDir)
handler.sendMessage(handler.obtainMessage(SUCCESS, outDir))
} catch (e: InstallFailureException) {
@@ -195,7 +235,7 @@ class Launcher private constructor(private val rootDir: File) {
return
}
aldRegex.matchEntire(name)?.let {
val type = resourceType[it.groupValues[2]]
val type = resourceType[it.groupValues[2].toLowerCase(Locale.US)]
val id = it.groupValues[3].toUpperCase(Locale.US)
if (type != null) {
grLines.add("$type$id $path")
@@ -216,7 +256,7 @@ class Launcher private constructor(private val rootDir: File) {
}
grLines.add("")
val gr = grLines.joinToString("\n")
Log.d("xsystem35.gr", gr)
Log.i("xsystem35.gr", gr)
File(outDir, "xsystem35.gr").writeText(gr)
val playlist = audioFiles.joinToString("\n") { it ?: "" }.trimEnd('\n')
@@ -224,3 +264,17 @@ class Launcher private constructor(private val rootDir: File) {
}
}
}
private fun forEachZipEntry(input: InputStream, action: (ZipEntry, ZipInputStream) -> Unit) {
val zip = if (Build.VERSION.SDK_INT >= 24) {
ZipInputStream(input.buffered(), Charset.forName("Shift_JIS"))
} else {
ZipInputStream(input.buffered())
}
zip.use {
while (true) {
val zipEntry = zip.nextEntry ?: break
action(zipEntry, zip)
}
}
}
@@ -20,12 +20,21 @@ package io.github.kichikuou.xsystem35
import android.app.*
import android.content.Intent
import android.os.Bundle
import android.util.Log
import android.view.Menu
import android.view.MenuItem
import android.view.View
import android.widget.AdapterView
import android.widget.ArrayAdapter
import android.widget.ListView
import android.widget.Toast
import java.io.*
private const val CONTENT_TYPE_ZIP = "application/zip"
private const val INSTALL_REQUEST = 1
private const val SAVEDATA_EXPORT_REQUEST = 2
private const val SAVEDATA_IMPORT_REQUEST = 3
class LauncherActivity : ListActivity(), AdapterView.OnItemLongClickListener, LauncherObserver {
private lateinit var launcher: Launcher
private var progressDialog: ProgressDialogFragment? = null
@@ -54,8 +63,8 @@ class LauncherActivity : ListActivity(), AdapterView.OnItemLongClickListener, La
startGame(launcher.games[position].path)
} else {
val i = Intent(Intent.ACTION_GET_CONTENT)
i.type = "application/zip"
startActivityForResult(Intent.createChooser(i, getString(R.string.choose_a_file)), 0)
i.type = CONTENT_TYPE_ZIP
startActivityForResult(Intent.createChooser(i, getString(R.string.choose_a_file)), INSTALL_REQUEST)
}
}
@@ -70,13 +79,59 @@ class LauncherActivity : ListActivity(), AdapterView.OnItemLongClickListener, La
return true
}
override fun onCreateOptionsMenu(menu: Menu): Boolean {
menuInflater.inflate(R.menu.launcher_menu, menu)
return true
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
return when (item.itemId) {
R.id.export_savedata -> {
val i = Intent(Intent.ACTION_CREATE_DOCUMENT)
i.type = CONTENT_TYPE_ZIP
i.putExtra(Intent.EXTRA_TITLE, "savedata.zip")
startActivityForResult(i, SAVEDATA_EXPORT_REQUEST)
true
}
R.id.import_savedata -> {
val i = Intent(Intent.ACTION_GET_CONTENT)
i.type = CONTENT_TYPE_ZIP
startActivityForResult(Intent.createChooser(i, getString(R.string.choose_a_file)),
SAVEDATA_IMPORT_REQUEST)
true
}
else -> super.onOptionsItemSelected(item)
}
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
if (resultCode != RESULT_OK)
return
val uri = data?.data ?: return
val input = contentResolver.openInputStream(uri) ?: return
showProgressDialog()
launcher.install(input)
when (requestCode) {
INSTALL_REQUEST -> {
val input = contentResolver.openInputStream(uri) ?: return
showProgressDialog()
launcher.install(input)
}
SAVEDATA_EXPORT_REQUEST -> try {
launcher.exportSaveData(contentResolver.openOutputStream(uri)!!)
Toast.makeText(this, R.string.save_data_export_success, Toast.LENGTH_SHORT).show()
} catch (e: IOException) {
Log.e("launcher", "Failed to export savedata", e)
errorDialog(R.string.save_data_export_error)
}
SAVEDATA_IMPORT_REQUEST -> {
val input = contentResolver.openInputStream(uri) ?: return
val errMsgId = launcher.importSaveData(input)
if (errMsgId == null) {
Toast.makeText(this, R.string.save_data_import_success, Toast.LENGTH_SHORT).show()
} else {
errorDialog(errMsgId)
}
}
}
}
override fun onGameListChange() {