mirror of
https://github.com/kichikuou/system3-sdl2.git
synced 2026-09-30 02:48:10 +03:00
Android: Copy the new launcher code from xsystem35-sdl2 70c6480
This commit is contained in:
@@ -1,35 +0,0 @@
|
||||
/* Copyright (C) 2019 <KichikuouChrome@gmail.com>
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 2 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program; if not, write to the Free Software
|
||||
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||
*
|
||||
*/
|
||||
package io.github.kichikuou.system3;
|
||||
|
||||
import org.libsdl.app.SDLActivity;
|
||||
|
||||
public class GameActivity extends SDLActivity {
|
||||
@Override
|
||||
protected String[] getLibraries() {
|
||||
return new String[] {
|
||||
"hidapi",
|
||||
"SDL2",
|
||||
// "SDL2_image",
|
||||
// "SDL2_mixer",
|
||||
// "SDL2_net",
|
||||
// "SDL2_ttf",
|
||||
"system3"
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,192 @@
|
||||
/* Copyright (C) 2019 <KichikuouChrome@gmail.com>
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 2 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program; if not, write to the Free Software
|
||||
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||
*
|
||||
*/
|
||||
package io.github.kichikuou.system3
|
||||
|
||||
import android.media.MediaPlayer
|
||||
import android.os.Bundle
|
||||
import android.util.Base64
|
||||
import android.util.Log
|
||||
import org.libsdl.app.SDLActivity
|
||||
import java.io.File
|
||||
import java.io.IOException
|
||||
|
||||
// Intent for this activity must have the following extra:
|
||||
// - EXTRA_GAME_ROOT (string): A path to the game installation.
|
||||
class GameActivity : SDLActivity() {
|
||||
companion object {
|
||||
const val EXTRA_GAME_ROOT = "GAME_ROOT"
|
||||
}
|
||||
|
||||
private lateinit var gameRoot: File
|
||||
private lateinit var cdda: CddaPlayer
|
||||
private val midi = MidiPlayer()
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
gameRoot = File(intent.getStringExtra(EXTRA_GAME_ROOT))
|
||||
cdda = CddaPlayer(File(gameRoot, Launcher.PLAYLIST_FILE))
|
||||
}
|
||||
|
||||
override fun onStop() {
|
||||
super.onStop()
|
||||
cdda.onActivityStop()
|
||||
midi.onActivityStop()
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
cdda.onActivityResume()
|
||||
midi.onActivityResume()
|
||||
}
|
||||
|
||||
override fun getLibraries(): Array<String> {
|
||||
return arrayOf("hidapi", "SDL2", "system3")
|
||||
}
|
||||
|
||||
override fun getArguments(): Array<String> {
|
||||
return arrayOf("-gamedir", intent.getStringExtra(EXTRA_GAME_ROOT))
|
||||
}
|
||||
|
||||
override fun setTitle(title: CharSequence?) {
|
||||
super.setTitle(title)
|
||||
val str = title?.toString()?.substringAfter(':', "")
|
||||
if (str.isNullOrEmpty())
|
||||
return
|
||||
File(gameRoot, Launcher.TITLE_FILE).writeText(str)
|
||||
Launcher.updateGameList()
|
||||
}
|
||||
|
||||
// These functions are called in the SDL thread by JNI.
|
||||
@Suppress("unused") fun cddaStart(track: Int, loop: Int) = cdda.start(track, loop)
|
||||
@Suppress("unused") fun cddaStop() = cdda.stop()
|
||||
@Suppress("unused") fun cddaCurrentPosition() = cdda.currentPosition()
|
||||
@Suppress("unused") fun midiStart(buf: ByteArray, loop: Int) = midi.start(buf, loop)
|
||||
@Suppress("unused") fun midiStop() = midi.stop()
|
||||
@Suppress("unused") fun midiCurrentPosition() = midi.currentPosition()
|
||||
}
|
||||
|
||||
private class CddaPlayer(private val playlistPath: File) {
|
||||
private val playlist =
|
||||
try {
|
||||
playlistPath.readLines()
|
||||
} catch (e: IOException) {
|
||||
Log.e("loadPlaylist", "Cannot load $playlistPath", e)
|
||||
emptyList<String>()
|
||||
}
|
||||
private var currentTrack = 0
|
||||
private val player = MediaPlayer()
|
||||
private var playerPaused = false
|
||||
|
||||
fun start(track: Int, loop: Int) {
|
||||
val f = playlist.elementAtOrNull(track)
|
||||
if (f.isNullOrEmpty()) {
|
||||
Log.w("cddaStart", "No playlist entry for track $track")
|
||||
return
|
||||
}
|
||||
Log.v("cddaStart", f)
|
||||
try {
|
||||
player.apply {
|
||||
reset()
|
||||
setDataSource(File(playlistPath.parent, f).path)
|
||||
isLooping = loop == 0
|
||||
prepare()
|
||||
start()
|
||||
}
|
||||
currentTrack = track
|
||||
} catch (e: IOException) {
|
||||
Log.e("cddaStart", "Cannot play $f", e)
|
||||
player.reset()
|
||||
}
|
||||
}
|
||||
|
||||
fun stop() {
|
||||
if (currentTrack > 0 && player.isPlaying) {
|
||||
player.stop()
|
||||
currentTrack = 0
|
||||
}
|
||||
}
|
||||
|
||||
fun currentPosition(): Int {
|
||||
if (currentTrack == 0)
|
||||
return 0
|
||||
val frames = player.currentPosition * 75 / 1000
|
||||
return currentTrack or (frames shl 8)
|
||||
}
|
||||
|
||||
fun onActivityStop() {
|
||||
if (currentTrack > 0 && player.isPlaying) {
|
||||
player.pause()
|
||||
playerPaused = true
|
||||
}
|
||||
}
|
||||
|
||||
fun onActivityResume() {
|
||||
if (playerPaused) {
|
||||
player.start()
|
||||
playerPaused = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class MidiPlayer {
|
||||
private val player = MediaPlayer()
|
||||
private var playing = false
|
||||
private var playerPaused = false
|
||||
|
||||
fun start(buf: ByteArray, loop: Int) {
|
||||
val url = "data:audio/midi;base64," + Base64.encodeToString(buf, Base64.DEFAULT)
|
||||
try {
|
||||
player.apply {
|
||||
reset()
|
||||
setDataSource(url)
|
||||
isLooping = loop == 0
|
||||
prepare()
|
||||
start()
|
||||
}
|
||||
playing = true
|
||||
} catch (e: IOException) {
|
||||
Log.e("midiStart", "Cannot play midi", e)
|
||||
player.reset()
|
||||
}
|
||||
}
|
||||
|
||||
fun stop() {
|
||||
if (playing && player.isPlaying) {
|
||||
player.stop()
|
||||
playing = false
|
||||
}
|
||||
}
|
||||
|
||||
fun currentPosition(): Int {
|
||||
return if (playing) player.currentPosition else 0
|
||||
}
|
||||
|
||||
fun onActivityStop() {
|
||||
if (playing && player.isPlaying) {
|
||||
player.pause()
|
||||
playerPaused = true
|
||||
}
|
||||
}
|
||||
|
||||
fun onActivityResume() {
|
||||
if (playerPaused) {
|
||||
player.start()
|
||||
playerPaused = false
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,280 @@
|
||||
/* Copyright (C) 2020 <KichikuouChrome@gmail.com>
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 2 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program; if not, write to the Free Software
|
||||
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||
*
|
||||
*/
|
||||
package io.github.kichikuou.system3
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.os.Build
|
||||
import android.os.Handler
|
||||
import android.os.Message
|
||||
import android.util.Log
|
||||
import java.io.*
|
||||
import java.lang.StringBuilder
|
||||
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
|
||||
|
||||
interface LauncherObserver {
|
||||
fun onGameListChange()
|
||||
fun onInstallProgress(path: String)
|
||||
fun onInstallSuccess(path: File)
|
||||
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
|
||||
|
||||
class Launcher private constructor(private val rootDir: File) {
|
||||
companion object {
|
||||
const val TITLE_FILE = "title.txt"
|
||||
const val PLAYLIST_FILE = "playlist.txt"
|
||||
|
||||
fun getInstance(rootDir: File): Launcher {
|
||||
if (gLauncher == null) {
|
||||
gLauncher = Launcher(rootDir)
|
||||
}
|
||||
return gLauncher!!
|
||||
}
|
||||
|
||||
fun updateGameList() {
|
||||
gLauncher?.updateGameList()
|
||||
}
|
||||
}
|
||||
|
||||
data class Entry(val path: File, val title: String)
|
||||
val games = arrayListOf<Entry>()
|
||||
val titles: List<String>
|
||||
get() = games.map(Entry::title)
|
||||
var observer: LauncherObserver? = null
|
||||
var isInstalling = false
|
||||
private set
|
||||
|
||||
init {
|
||||
updateGameList()
|
||||
}
|
||||
|
||||
fun install(input: InputStream) {
|
||||
val dir = createDirForGame()
|
||||
@SuppressLint("HandlerLeak")
|
||||
val handler = object : Handler() {
|
||||
override fun handleMessage(msg: Message) {
|
||||
when (msg.what) {
|
||||
PROGRESS -> {
|
||||
observer?.onInstallProgress(msg.obj as String)
|
||||
}
|
||||
SUCCESS -> {
|
||||
isInstalling = false
|
||||
observer?.onInstallSuccess(msg.obj as File)
|
||||
}
|
||||
FAILURE -> {
|
||||
isInstalling = false
|
||||
observer?.onInstallFailure(msg.obj as Int)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
val t = Thread {
|
||||
extractFiles(input, dir, handler)
|
||||
}
|
||||
t.start()
|
||||
isInstalling = true
|
||||
}
|
||||
|
||||
fun uninstall(id: Int) {
|
||||
games[id].path.deleteRecursively()
|
||||
games.removeAt(id)
|
||||
observer?.onGameListChange()
|
||||
}
|
||||
|
||||
private fun updateGameList() {
|
||||
var saveDirFound = false
|
||||
for (path in rootDir.listFiles()) {
|
||||
if (!path.isDirectory)
|
||||
continue
|
||||
if (path.name == SAVE_DIR) {
|
||||
saveDirFound = true
|
||||
continue
|
||||
}
|
||||
try {
|
||||
val title = File(path, TITLE_FILE).readText()
|
||||
games.add(Entry(path, title))
|
||||
} catch (e: IOException) {
|
||||
// Incomplete game installation. Delete it.
|
||||
path.deleteRecursively()
|
||||
}
|
||||
}
|
||||
if (!saveDirFound) {
|
||||
File(rootDir, SAVE_DIR).mkdir()
|
||||
}
|
||||
}
|
||||
|
||||
private fun createDirForGame(): File {
|
||||
var i = 0
|
||||
while (true) {
|
||||
val f = File(rootDir, i++.toString())
|
||||
if (!f.exists() && f.mkdir()) {
|
||||
return f
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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()
|
||||
forEachZipEntry(input) { zipEntry, zip ->
|
||||
Log.i("extractFiles", zipEntry.name)
|
||||
val path = File(outDir, zipEntry.name)
|
||||
if (zipEntry.isDirectory)
|
||||
return@forEachZipEntry
|
||||
path.parentFile.mkdirs()
|
||||
handler.sendMessage(handler.obtainMessage(PROGRESS, zipEntry.name))
|
||||
FileOutputStream(path).buffered().use {
|
||||
zip.copyTo(it)
|
||||
}
|
||||
configWriter.maybeAdd(zipEntry.name)
|
||||
}
|
||||
configWriter.write(outDir)
|
||||
handler.sendMessage(handler.obtainMessage(SUCCESS, outDir))
|
||||
} catch (e: InstallFailureException) {
|
||||
handler.sendMessage(handler.obtainMessage(FAILURE, e.msgId))
|
||||
} catch (e: UTFDataFormatException) {
|
||||
// Attempted to read Shift_JIS zip in Android < 7
|
||||
handler.sendMessage(handler.obtainMessage(FAILURE, R.string.unsupported_zip))
|
||||
} catch (e: IOException) {
|
||||
Log.e("launcher", "Failed to extract ZIP", e)
|
||||
handler.sendMessage(handler.obtainMessage(FAILURE, R.string.zip_extraction_error))
|
||||
}
|
||||
}
|
||||
|
||||
class InstallFailureException(val msgId: Int) : Exception()
|
||||
|
||||
// A helper class which generates xsystem35.gr and playlist.txt in the game root directory.
|
||||
private class GameConfigWriter {
|
||||
private val grb = StringBuilder()
|
||||
private var basename: String? = null
|
||||
private val aldRegex = """(.*?)([a-z])([a-z])\.ald""".toRegex(RegexOption.IGNORE_CASE)
|
||||
private val resourceType = mapOf(
|
||||
"d" to "Data",
|
||||
"g" to "Graphics",
|
||||
"m" to "Midi",
|
||||
"r" to "Resource",
|
||||
"s" to "Scenario",
|
||||
"w" to "Wave")
|
||||
|
||||
private val audioRegex = """.*?(\d+)\.(wav|mp3|ogg)""".toRegex(RegexOption.IGNORE_CASE)
|
||||
private val audioFiles: Array<String?> = arrayOfNulls(100)
|
||||
|
||||
fun maybeAdd(path: String) {
|
||||
val name = File(path).name
|
||||
|
||||
if (name.toLowerCase(Locale.US) == "system39.ain") {
|
||||
grb.appendln("Ain $path")
|
||||
return
|
||||
}
|
||||
aldRegex.matchEntire(name)?.let {
|
||||
val type = resourceType[it.groupValues[2].toLowerCase(Locale.US)]
|
||||
val id = it.groupValues[3].toUpperCase(Locale.US)
|
||||
if (type != null) {
|
||||
grb.appendln("$type$id $path")
|
||||
basename = it.groupValues[1]
|
||||
}
|
||||
}
|
||||
audioRegex.matchEntire(name)?.let {
|
||||
val track = it.groupValues[1].toInt()
|
||||
if (track < audioFiles.size)
|
||||
audioFiles[track] = path
|
||||
}
|
||||
}
|
||||
|
||||
fun write(outDir: File) {
|
||||
basename ?: throw InstallFailureException(R.string.cannot_find_adisk)
|
||||
for (id in 'A' .. 'Z') {
|
||||
grb.appendln("Save$id ../save/${basename}s${id.toLowerCase()}.asd")
|
||||
}
|
||||
val gr = grb.toString()
|
||||
Log.i("xsystem35.gr", gr)
|
||||
File(outDir, "xsystem35.gr").writeText(gr)
|
||||
|
||||
val playlist = audioFiles.joinToString("\n") { it ?: "" }.trimEnd('\n')
|
||||
File(outDir, PLAYLIST_FILE).writeText(playlist)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,91 +0,0 @@
|
||||
/* Copyright (C) 2019 <KichikuouChrome@gmail.com>
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 2 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program; if not, write to the Free Software
|
||||
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||
*
|
||||
*/
|
||||
package io.github.kichikuou.system3;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.util.Log;
|
||||
|
||||
import java.io.BufferedInputStream;
|
||||
import java.io.BufferedOutputStream;
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.nio.charset.Charset;
|
||||
import java.util.zip.ZipEntry;
|
||||
import java.util.zip.ZipInputStream;
|
||||
|
||||
public class LauncherActivity extends Activity {
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
Intent i = new Intent(Intent.ACTION_GET_CONTENT);
|
||||
i.setType("application/zip");
|
||||
startActivityForResult(Intent.createChooser(i, "Choose a file"), 0);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActivityResult(int requestCode, int resultCode, Intent data) {
|
||||
if (resultCode == RESULT_OK)
|
||||
extractFiles(data.getData(), getApplicationContext().getFilesDir());
|
||||
startGame();
|
||||
}
|
||||
|
||||
boolean extractFiles(Uri zipUri, File outDir) {
|
||||
try {
|
||||
InputStream input = getContentResolver().openInputStream(zipUri);
|
||||
if (input == null)
|
||||
return false;
|
||||
ZipInputStream zip;
|
||||
if (Build.VERSION.SDK_INT >= 24)
|
||||
zip = new ZipInputStream(new BufferedInputStream(input), Charset.forName("Shift_JIS"));
|
||||
else
|
||||
zip = new ZipInputStream(new BufferedInputStream(input));
|
||||
byte[] buffer = new byte[4096];
|
||||
ZipEntry zipEntry;
|
||||
while ((zipEntry = zip.getNextEntry()) != null) {
|
||||
File entryName = new File(zipEntry.getName());
|
||||
if (zipEntry.isDirectory())
|
||||
continue;
|
||||
Log.d("Extracting", entryName.toString());
|
||||
File outFile = new File(outDir, entryName.getName());
|
||||
BufferedOutputStream output = new BufferedOutputStream(new FileOutputStream(outFile));
|
||||
int size;
|
||||
while ((size = zip.read(buffer)) > 0) {
|
||||
output.write(buffer, 0, size);
|
||||
}
|
||||
output.close();
|
||||
}
|
||||
zip.close();
|
||||
return true;
|
||||
} catch (IOException e) {
|
||||
Log.e("launcher", "Error while extracting zip", e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
void startGame() {
|
||||
Intent i = new Intent();
|
||||
i.setClass(getApplicationContext(), GameActivity.class);
|
||||
startActivity(i);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,199 @@
|
||||
/* Copyright (C) 2019 <KichikuouChrome@gmail.com>
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 2 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program; if not, write to the Free Software
|
||||
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||
*
|
||||
*/
|
||||
package io.github.kichikuou.system3
|
||||
|
||||
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
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
launcher = Launcher.getInstance(filesDir)
|
||||
launcher.observer = this
|
||||
if (launcher.isInstalling) {
|
||||
showProgressDialog()
|
||||
}
|
||||
|
||||
onGameListChange()
|
||||
listView.onItemLongClickListener = this
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
launcher.observer = null
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
override fun onListItemClick(l: ListView?, v: View?, position: Int, id: Long) {
|
||||
super.onListItemClick(l, v, position, id)
|
||||
if (position < launcher.games.size) {
|
||||
startGame(launcher.games[position].path)
|
||||
} else {
|
||||
val i = Intent(Intent.ACTION_GET_CONTENT)
|
||||
i.type = CONTENT_TYPE_ZIP
|
||||
startActivityForResult(Intent.createChooser(i, getString(R.string.choose_a_file)), INSTALL_REQUEST)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onItemLongClick(a: AdapterView<*>?, v: View?, position: Int, id: Long): Boolean {
|
||||
if (position < launcher.games.size) {
|
||||
AlertDialog.Builder(this).setTitle(R.string.uninstall_dialog_title)
|
||||
.setMessage(getString(R.string.uninstall_dialog_message, launcher.games[position].title))
|
||||
.setPositiveButton(R.string.ok) {_, _ -> uninstall(position)}
|
||||
.setNegativeButton(R.string.cancel) {_, _ -> }
|
||||
.show()
|
||||
}
|
||||
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
|
||||
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() {
|
||||
val items = launcher.titles.toMutableList()
|
||||
items.add(getString(R.string.install_from_zip))
|
||||
listAdapter = ArrayAdapter(this, android.R.layout.simple_list_item_1, items)
|
||||
}
|
||||
|
||||
override fun onInstallProgress(path: String) {
|
||||
progressDialog?.setProgress(getString(R.string.install_progress, path))
|
||||
}
|
||||
|
||||
override fun onInstallSuccess(path: File) {
|
||||
dismissProgressDialog()
|
||||
startGame(path)
|
||||
}
|
||||
|
||||
override fun onInstallFailure(msgId: Int) {
|
||||
dismissProgressDialog()
|
||||
errorDialog(msgId)
|
||||
}
|
||||
|
||||
private fun startGame(path: File) {
|
||||
val i = Intent()
|
||||
i.setClass(applicationContext, GameActivity::class.java)
|
||||
i.putExtra(GameActivity.EXTRA_GAME_ROOT, path.path)
|
||||
startActivity(i)
|
||||
}
|
||||
|
||||
private fun uninstall(id: Int) {
|
||||
launcher.uninstall(id)
|
||||
}
|
||||
|
||||
private fun showProgressDialog() {
|
||||
progressDialog = ProgressDialogFragment()
|
||||
progressDialog!!.show(fragmentManager, "progress_dialog")
|
||||
}
|
||||
|
||||
private fun dismissProgressDialog() {
|
||||
progressDialog?.dismiss()
|
||||
progressDialog = null
|
||||
}
|
||||
|
||||
private fun errorDialog(msgId: Int) {
|
||||
AlertDialog.Builder(this).setTitle(R.string.error_dialog_title)
|
||||
.setMessage(msgId)
|
||||
.setPositiveButton(R.string.ok) {_, _ -> }
|
||||
.show()
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("DEPRECATION") // for ProgressDialog
|
||||
class ProgressDialogFragment : DialogFragment() {
|
||||
private lateinit var dialog: ProgressDialog
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||
dialog = ProgressDialog(activity)
|
||||
return dialog.apply {
|
||||
setTitle(R.string.install_dialog_title)
|
||||
setCancelable(true)
|
||||
}
|
||||
}
|
||||
fun setProgress(msg: String) {
|
||||
dialog.setMessage(msg)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user