Android: New launcher

The new launcher activity is a ListActivity and allows:

- Installing games from ZIP
- Launching installed games
- Uninstalling games
This commit is contained in:
kichikuou
2020-01-13 18:55:52 +09:00
parent 127c21e032
commit 473e490686
8 changed files with 291 additions and 134 deletions
@@ -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.xsystem35;
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",
"xsystem35"
};
}
}
@@ -0,0 +1,47 @@
/* 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.xsystem35
import org.libsdl.app.SDLActivity
import java.io.File
// Intent for this activity must have two extras:
// - EXTRA_GAME_ROOT (string): A path to the game installation.
// - EXTRA_TITLE_FILE (string): A file to which the game title will be written.
class GameActivity : SDLActivity() {
companion object {
const val EXTRA_GAME_ROOT = "GAME_ROOT"
const val EXTRA_TITLE_FILE = "TITLE_FILE"
}
override fun getLibraries(): Array<String> {
return arrayOf("hidapi", "SDL2", "xsystem35")
}
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
intent.getStringExtra(EXTRA_TITLE_FILE)?.let { File(it).writeText(str) }
}
}
@@ -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.xsystem35;
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,232 @@
/* 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.xsystem35
import android.annotation.SuppressLint
import android.app.*
import android.content.Intent
import android.os.Build
import android.os.Bundle
import android.os.Handler
import android.os.Message
import android.util.Log
import android.view.View
import android.widget.AdapterView
import android.widget.ArrayAdapter
import android.widget.ListView
import java.io.*
import java.nio.charset.Charset
import java.util.Locale
import java.util.zip.ZipInputStream
import kotlin.collections.ArrayList
const val PROGRESS = 0
const val SUCCESS = 1
const val FAILURE = 2
class LauncherActivity : ListActivity(), AdapterView.OnItemLongClickListener {
private lateinit var gm: GameManager
private lateinit var adapter: ArrayAdapter<String>
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
gm = GameManager(filesDir)
val items = gm.titles.toMutableList()
items.add("Install from ZIP")
adapter = ArrayAdapter(this, android.R.layout.simple_list_item_1, items)
listAdapter = adapter
listView.onItemLongClickListener = this
}
override fun onListItemClick(l: ListView?, v: View?, position: Int, id: Long) {
super.onListItemClick(l, v, position, id)
if (position < gm.games.size) {
startGame(gm.games[position].path)
} else {
val i = Intent(Intent.ACTION_GET_CONTENT)
i.type = "application/zip"
startActivityForResult(Intent.createChooser(i, "Choose a file"), 0)
}
}
override fun onItemLongClick(a: AdapterView<*>?, v: View?, position: Int, id: Long): Boolean {
if (position < gm.games.size) {
AlertDialog.Builder(this).setTitle("Confirm")
.setMessage("Uninstall ${gm.games[position].title}?")
.setPositiveButton("OK") {_, _ -> uninstall(position)}
.setNegativeButton("Cancel") {_, _ -> }
.show()
}
return true
}
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
val dialog = ProgressDialogFragment()
dialog.show(fragmentManager, "progress_dialog")
@SuppressLint("HandlerLeak") // Should be okay as installer thread has a short lifetime
val handler = object : Handler() {
override fun handleMessage(msg: Message) {
when (msg.what) {
PROGRESS -> {
dialog.setProgress(msg.obj as String)
}
SUCCESS -> {
dialog.dismiss()
startGame(msg.obj as File)
}
FAILURE -> {
dialog.dismiss()
errorDialog(msg.obj as String)
}
}
}
}
gm.install(input, handler)
}
private fun startGame(path: File) {
val gameRoot = findGameRoot(path)
if (gameRoot == null) {
errorDialog("Cannot find game files (*.ald)")
return
}
val i = Intent()
i.setClass(applicationContext, GameActivity::class.java)
i.putExtra(GameActivity.EXTRA_GAME_ROOT, gameRoot.path)
i.putExtra(GameActivity.EXTRA_TITLE_FILE, File(path, GameManager.TITLE_FILE).path)
startActivity(i)
}
private fun uninstall(id: Int) {
gm.uninstall(id)
adapter.remove(adapter.getItem(id))
}
private fun errorDialog(msg: String) {
AlertDialog.Builder(this).setTitle("Error")
.setMessage(msg)
.setPositiveButton("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("Installing")
setCancelable(true)
}
}
fun setProgress(msg: String) {
dialog.setMessage(msg)
}
}
private class GameManager(private val rootDir: File) {
companion object {
const val TITLE_FILE = "title.txt"
}
data class Entry(val path: File, val title: String)
val games: ArrayList<Entry> = arrayListOf()
val titles: List<String>
get() = games.map(Entry::title)
init {
for (path in rootDir.listFiles()) {
if (!path.isDirectory)
continue
try {
val title = File(path, TITLE_FILE).readText()
games.add(Entry(path, title))
} catch (e: IOException) {
// Incomplete game installation. Delete it.
path.deleteRecursively()
}
}
}
fun install(input: InputStream, handler: Handler) {
var i = 0
while (true) {
val f = File(rootDir, i++.toString())
if (!f.exists() && f.mkdir()) {
val t = Thread {
extractFiles(input, f, handler)
}
t.start()
return
}
}
}
fun uninstall(id: Int) {
games[id].path.deleteRecursively()
games.removeAt(id)
}
private fun extractFiles(input: InputStream, outDir: File, handler: Handler) {
try {
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.d("extractFiles", zipEntry.name)
val path = File(outDir, zipEntry.name)
if (zipEntry.isDirectory)
continue
path.parentFile.mkdirs()
handler.sendMessage(handler.obtainMessage(PROGRESS, "Extracting ${zipEntry.name}..."))
val output = FileOutputStream(path).buffered()
zip.copyTo(output)
output.close()
}
zip.close()
handler.sendMessage(handler.obtainMessage(SUCCESS, outDir))
} catch (e: UTFDataFormatException) {
// Attempted to read Shift_JIS zip in Android < 7
handler.sendMessage(handler.obtainMessage(FAILURE, "This ZIP file is unsupported."))
} catch (e: IOException) {
Log.e("launcher", "Failed to extract ZIP", e)
handler.sendMessage(handler.obtainMessage(FAILURE, "Failed to extract ZIP."))
}
}
}
private fun findGameRoot(path: File): File? {
path.walkTopDown().forEach {
val name = it.name.toLowerCase(Locale.US)
if (name == "xsystem35.gr" || name.endsWith(".ald"))
return it.parentFile
}
return null
}