mirror of
https://github.com/kichikuou/system3-sdl2.git
synced 2026-09-22 22:58:12 +03:00
Android: Keep launcher install state in Launcher
Move ZIP install jobs from GlobalScope to a Launcher-owned coroutine scope and store progress, success, and failure in InstallState. This lets a recreated LauncherActivity render the current install state and consume completed results once, even when observer callbacks happened while the activity was absent.
This commit is contained in:
@@ -19,10 +19,12 @@ package io.github.kichikuou.system3
|
||||
|
||||
import android.os.Build
|
||||
import android.util.Log
|
||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.io.*
|
||||
import java.nio.charset.Charset
|
||||
@@ -40,6 +42,13 @@ interface LauncherObserver {
|
||||
fun onInstallFailure(msgId: Int)
|
||||
}
|
||||
|
||||
sealed class InstallState {
|
||||
object Idle : InstallState()
|
||||
data class Installing(val progress: String?) : InstallState()
|
||||
data class Succeeded(val path: File) : InstallState()
|
||||
data class Failed(val msgId: Int) : InstallState()
|
||||
}
|
||||
|
||||
private const val SAVE_DIR = "save"
|
||||
|
||||
class Launcher private constructor(private val rootDir: File) {
|
||||
@@ -64,7 +73,9 @@ class Launcher private constructor(private val rootDir: File) {
|
||||
val titles: List<String>
|
||||
get() = games.map(Entry::title)
|
||||
var observer: LauncherObserver? = null
|
||||
var isInstalling = false
|
||||
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Main)
|
||||
private var installJob: Job? = null
|
||||
var installState: InstallState = InstallState.Idle
|
||||
private set
|
||||
val saveDir: File
|
||||
get() = File(rootDir, SAVE_DIR)
|
||||
@@ -73,30 +84,54 @@ class Launcher private constructor(private val rootDir: File) {
|
||||
updateGameList()
|
||||
}
|
||||
|
||||
@OptIn(DelicateCoroutinesApi::class)
|
||||
fun install(input: InputStream) {
|
||||
val dir = createDirForGame()
|
||||
isInstalling = true
|
||||
GlobalScope.launch(Dispatchers.Main) {
|
||||
if (installJob?.isActive == true) {
|
||||
input.close()
|
||||
return
|
||||
}
|
||||
installState = InstallState.Installing(null)
|
||||
installJob = scope.launch {
|
||||
try {
|
||||
withContext(Dispatchers.IO) {
|
||||
extractFiles(input, dir) { msg ->
|
||||
GlobalScope.launch(Dispatchers.Main) {
|
||||
observer?.onInstallProgress(msg)
|
||||
val dir = withContext(Dispatchers.IO) {
|
||||
input.use {
|
||||
extractFilesTransactionally(it) { msg ->
|
||||
withContext(Dispatchers.Main) {
|
||||
setInstallProgress(msg)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
observer?.onInstallSuccess(dir)
|
||||
setInstallSucceeded(dir)
|
||||
} catch (e: InstallFailureException) {
|
||||
observer?.onInstallFailure(e.msgId)
|
||||
setInstallFailed(e.msgId)
|
||||
} catch (e: Exception) {
|
||||
Log.e("launcher", "Failed to extract ZIP", e)
|
||||
observer?.onInstallFailure(R.string.zip_extraction_error)
|
||||
setInstallFailed(R.string.zip_extraction_error)
|
||||
}
|
||||
isInstalling = false
|
||||
}
|
||||
}
|
||||
|
||||
fun consumeInstallResult() {
|
||||
if (installState is InstallState.Succeeded || installState is InstallState.Failed) {
|
||||
installState = InstallState.Idle
|
||||
}
|
||||
}
|
||||
|
||||
private fun setInstallProgress(progress: String) {
|
||||
installState = InstallState.Installing(progress)
|
||||
observer?.onInstallProgress(progress)
|
||||
}
|
||||
|
||||
private fun setInstallSucceeded(path: File) {
|
||||
installState = InstallState.Succeeded(path)
|
||||
observer?.onInstallSuccess(path)
|
||||
}
|
||||
|
||||
private fun setInstallFailed(msgId: Int) {
|
||||
installState = InstallState.Failed(msgId)
|
||||
observer?.onInstallFailure(msgId)
|
||||
}
|
||||
|
||||
fun uninstall(id: Int) {
|
||||
games[id].path.deleteRecursively()
|
||||
games.removeAt(id)
|
||||
@@ -180,13 +215,34 @@ class Launcher private constructor(private val rootDir: File) {
|
||||
}
|
||||
}
|
||||
|
||||
private fun extractFiles(input: InputStream, outDir: File, progressCallback: (String) -> Unit) {
|
||||
private suspend fun extractFilesTransactionally(
|
||||
input: InputStream,
|
||||
progressCallback: suspend (String) -> Unit
|
||||
): File {
|
||||
val dir = createDirForGame()
|
||||
var committed = false
|
||||
try {
|
||||
extractFiles(input, dir, progressCallback)
|
||||
committed = true
|
||||
return dir
|
||||
} finally {
|
||||
if (!committed && !dir.deleteRecursively()) {
|
||||
Log.w("launcher", "Failed to delete incomplete install directory: $dir")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun extractFiles(
|
||||
input: InputStream,
|
||||
outDir: File,
|
||||
progressCallback: suspend (String) -> Unit
|
||||
) {
|
||||
val configWriter = GameConfigWriter()
|
||||
forEachZipEntry(input) { zipEntry, zip ->
|
||||
forEachZipEntrySuspending(input) { zipEntry, zip ->
|
||||
Log.i("extractFiles", zipEntry.name)
|
||||
val entryName = File(zipEntry.name).name
|
||||
if (zipEntry.isDirectory)
|
||||
return@forEachZipEntry
|
||||
return@forEachZipEntrySuspending
|
||||
progressCallback(entryName)
|
||||
FileOutputStream(resolveOutputPath(outDir, entryName)).buffered().use {
|
||||
zip.copyTo(it)
|
||||
@@ -236,7 +292,17 @@ private fun resolveOutputPath(baseDir: File, relativePath: String): File {
|
||||
return file
|
||||
}
|
||||
|
||||
private fun forEachZipEntry(input: InputStream, action: (ZipEntry, ZipInputStream) -> Unit) {
|
||||
private fun forEachZipEntry(input: InputStream, action: (ZipEntry, ZipInputStream) -> Unit) =
|
||||
runBlocking {
|
||||
forEachZipEntrySuspending(input) { zipEntry, zip ->
|
||||
action(zipEntry, zip)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun forEachZipEntrySuspending(
|
||||
input: InputStream,
|
||||
action: suspend (ZipEntry, ZipInputStream) -> Unit
|
||||
) {
|
||||
val zip = if (Build.VERSION.SDK_INT >= 24) {
|
||||
ZipInputStream(input.buffered(), Charset.forName("Shift_JIS"))
|
||||
} else {
|
||||
|
||||
@@ -34,7 +34,6 @@ 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
|
||||
private const val STATE_PROGRESS_TEXT = "progressText"
|
||||
|
||||
class LauncherActivity : Activity(), LauncherObserver {
|
||||
private lateinit var launcher: Launcher
|
||||
@@ -46,9 +45,7 @@ class LauncherActivity : Activity(), LauncherObserver {
|
||||
|
||||
launcher = Launcher.getInstance(filesDir)
|
||||
launcher.observer = this
|
||||
if (launcher.isInstalling) {
|
||||
showProgressDialog(savedInstanceState)
|
||||
}
|
||||
renderInstallState(launcher.installState)
|
||||
|
||||
onGameListChange()
|
||||
val listView = findViewById<ListView>(R.id.list)
|
||||
@@ -68,13 +65,6 @@ class LauncherActivity : Activity(), LauncherObserver {
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
override fun onSaveInstanceState(outState: Bundle) {
|
||||
progressDialog?.let {
|
||||
outState.putCharSequence(STATE_PROGRESS_TEXT, it.findViewById<TextView>(R.id.text).text)
|
||||
}
|
||||
super.onSaveInstanceState(outState)
|
||||
}
|
||||
|
||||
private fun onListItemClick(position: Int) {
|
||||
startGame(launcher.games[position].path)
|
||||
}
|
||||
@@ -137,8 +127,8 @@ class LauncherActivity : Activity(), LauncherObserver {
|
||||
when (requestCode) {
|
||||
INSTALL_REQUEST -> {
|
||||
val input = contentResolver.openInputStream(uri) ?: return
|
||||
showProgressDialog()
|
||||
launcher.install(input)
|
||||
renderInstallState(launcher.installState)
|
||||
}
|
||||
SAVEDATA_EXPORT_REQUEST -> try {
|
||||
launcher.exportSaveData(contentResolver.openOutputStream(uri)!!)
|
||||
@@ -167,17 +157,15 @@ class LauncherActivity : Activity(), LauncherObserver {
|
||||
}
|
||||
|
||||
override fun onInstallProgress(path: String) {
|
||||
progressDialog?.findViewById<TextView>(R.id.text)?.text = getString(R.string.install_progress, path)
|
||||
renderInstallState(launcher.installState)
|
||||
}
|
||||
|
||||
override fun onInstallSuccess(path: File) {
|
||||
dismissProgressDialog()
|
||||
startGame(path)
|
||||
renderInstallState(launcher.installState)
|
||||
}
|
||||
|
||||
override fun onInstallFailure(msgId: Int) {
|
||||
dismissProgressDialog()
|
||||
errorDialog(msgId)
|
||||
renderInstallState(launcher.installState)
|
||||
}
|
||||
|
||||
private fun startGame(path: File) {
|
||||
@@ -192,16 +180,35 @@ class LauncherActivity : Activity(), LauncherObserver {
|
||||
launcher.uninstall(id)
|
||||
}
|
||||
|
||||
private fun showProgressDialog(savedInstanceState: Bundle? = null) {
|
||||
progressDialog = Dialog(this)
|
||||
progressDialog!!.apply {
|
||||
setTitle(R.string.install_dialog_title)
|
||||
setCancelable(false)
|
||||
setContentView(R.layout.progress_dialog)
|
||||
savedInstanceState?.let {
|
||||
findViewById<TextView>(R.id.text)?.text = it.getCharSequence(STATE_PROGRESS_TEXT)
|
||||
private fun renderInstallState(state: InstallState) {
|
||||
when (state) {
|
||||
InstallState.Idle -> dismissProgressDialog()
|
||||
is InstallState.Installing -> showProgressDialog(state.progress)
|
||||
is InstallState.Succeeded -> {
|
||||
dismissProgressDialog()
|
||||
startGame(state.path)
|
||||
launcher.consumeInstallResult()
|
||||
}
|
||||
show()
|
||||
is InstallState.Failed -> {
|
||||
dismissProgressDialog()
|
||||
errorDialog(state.msgId)
|
||||
launcher.consumeInstallResult()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun showProgressDialog(progress: String? = null) {
|
||||
if (progressDialog == null) {
|
||||
progressDialog = Dialog(this)
|
||||
progressDialog!!.apply {
|
||||
setTitle(R.string.install_dialog_title)
|
||||
setCancelable(false)
|
||||
setContentView(R.layout.progress_dialog)
|
||||
show()
|
||||
}
|
||||
}
|
||||
progress?.let {
|
||||
progressDialog?.findViewById<TextView>(R.id.text)?.text = getString(R.string.install_progress, it)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user