From 6854d9bea255153fc3a135c7778f629ed589e853 Mon Sep 17 00:00:00 2001 From: kichikuou Date: Sat, 1 Feb 2020 17:29:20 +0900 Subject: [PATCH] Android: Export / import save files Now save files can be exported to ZIP / imported from ZIP by the option menu of the launcher activity. --- android/app/src/main/AndroidManifest.xml | 2 +- .../io/github/kichikuou/xsystem35/Launcher.kt | 88 +++++++++++++++---- .../kichikuou/xsystem35/LauncherActivity.kt | 65 ++++++++++++-- .../app/src/main/res/menu/launcher_menu.xml | 9 ++ android/app/src/main/res/values/strings.xml | 6 ++ 5 files changed, 147 insertions(+), 23 deletions(-) create mode 100644 android/app/src/main/res/menu/launcher_menu.xml 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.