diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml
index b7e0b72..039b416 100644
--- a/android/app/src/main/AndroidManifest.xml
+++ b/android/app/src/main/AndroidManifest.xml
@@ -26,7 +26,7 @@
android:required="false" />
-
+
diff --git a/android/app/src/main/java/io/github/kichikuou/xsystem35/Launcher.kt b/android/app/src/main/java/io/github/kichikuou/xsystem35/Launcher.kt
index fe18af2..907acca 100644
--- a/android/app/src/main/java/io/github/kichikuou/xsystem35/Launcher.kt
+++ b/android/app/src/main/java/io/github/kichikuou/xsystem35/Launcher.kt
@@ -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)
+ }
+ }
+}
diff --git a/android/app/src/main/java/io/github/kichikuou/xsystem35/LauncherActivity.kt b/android/app/src/main/java/io/github/kichikuou/xsystem35/LauncherActivity.kt
index ffc59ea..ee824a6 100644
--- a/android/app/src/main/java/io/github/kichikuou/xsystem35/LauncherActivity.kt
+++ b/android/app/src/main/java/io/github/kichikuou/xsystem35/LauncherActivity.kt
@@ -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() {
diff --git a/android/app/src/main/res/menu/launcher_menu.xml b/android/app/src/main/res/menu/launcher_menu.xml
new file mode 100644
index 0000000..d77b28b
--- /dev/null
+++ b/android/app/src/main/res/menu/launcher_menu.xml
@@ -0,0 +1,9 @@
+
+
\ No newline at end of file
diff --git a/android/app/src/main/res/values/strings.xml b/android/app/src/main/res/values/strings.xml
index e5836f2..8fe969f 100644
--- a/android/app/src/main/res/values/strings.xml
+++ b/android/app/src/main/res/values/strings.xml
@@ -1,5 +1,7 @@
xsystem35
+ Export Save Files
+ Import Save Files
Cancel
Cannot find System 3.x game files (*.ald)
Choose a file
@@ -7,7 +9,11 @@
Installing...
Install from ZIP
Extracting %s
+ No save/ folder in the ZIP archive.
OK
+ Failed to export save files.
+ Imported successfully.
+ Exported successfully.
Uninstall \"%s\"?
Confirm
This type of ZIP is not supported.