Unciv/android/src/com/unciv/app/CustomSaveLocationHelperAndroid.kt
Billy Brawner 205b5ccfea
Implement custom save locations for Android and Desktop (#3160)
* Implement custom save locations for Android and Desktop

* Request write permission to save to external storage

* Fix race condition for custom saves/loads caused by autosaves

* Remove unnecessary WRITE_EXTERNAL_STORAGE permission for saving files

* Fix padding for custom save/load location buttons

* Use nullability checks as defined in coding style guide

* Use nullability checks as defined in coding style guide

* Use early return for readability

* Rename save/load completion callbacks for custom locations and implement error handling
2020-09-20 23:22:07 +03:00

126 lines
4.9 KiB
Kotlin

package com.unciv.app
import android.app.Activity
import android.content.Intent
import android.net.Uri
import android.os.Build
import androidx.annotation.GuardedBy
import androidx.annotation.RequiresApi
import com.unciv.logic.CustomSaveLocationHelper
import com.unciv.logic.GameInfo
import com.unciv.logic.GameSaver
import com.unciv.logic.GameSaver.json
// The Storage Access Framework is available from API 19 and up:
// https://developer.android.com/guide/topics/providers/document-provider
@RequiresApi(Build.VERSION_CODES.KITKAT)
class CustomSaveLocationHelperAndroid(private val activity: Activity) : CustomSaveLocationHelper {
// This looks a little scary but it's really not so bad. Whenever a load or save operation is
// attempted, the game automatically autosaves as well (but on a separate thread), so we end up
// with a race condition when trying to handle both operations in parallel. In order to work
// around that, the callbacks are given an arbitrary index beginning at 100 and incrementing
// each time, and this index is used as the requestCode for the call to startActivityForResult()
// so that we can map it back to the corresponding callback when onActivityResult is called
@GuardedBy("this")
@Volatile
private var callbackIndex = 100
@GuardedBy("this")
private val callbacks = ArrayList<IndexedCallback>()
override fun saveGame(gameInfo: GameInfo, gameName: String, forcePrompt: Boolean, saveCompleteCallback: ((Exception?) -> Unit)?) {
val callbackIndex = synchronized(this) {
val index = callbackIndex++
callbacks.add(IndexedCallback(
index,
{ uri ->
if (uri != null) {
saveGame(gameInfo, uri)
saveCompleteCallback?.invoke(null)
} else {
saveCompleteCallback?.invoke(RuntimeException("Uri was null"))
}
}
))
index
}
if (!forcePrompt && gameInfo.customSaveLocation != null) {
handleIntentData(callbackIndex, Uri.parse(gameInfo.customSaveLocation))
return
}
Intent(Intent.ACTION_CREATE_DOCUMENT).apply {
type = "application/json"
putExtra(Intent.EXTRA_TITLE, gameName)
activity.startActivityForResult(this, callbackIndex)
}
}
// This will be called on the main thread
fun handleIntentData(requestCode: Int, uri: Uri?) {
val callback = synchronized(this) {
val index = callbacks.indexOfFirst { it.index == requestCode }
if (index == -1) return
callbacks.removeAt(index)
}
callback.thread.run {
callback.callback(uri)
}
}
private fun saveGame(gameInfo: GameInfo, uri: Uri) {
gameInfo.customSaveLocation = uri.toString()
activity.contentResolver.openOutputStream(uri, "rwt")
?.writer()
?.use {
it.write(json().toJson(gameInfo))
}
}
override fun loadGame(loadCompleteCallback: (GameInfo?, Exception?) -> Unit) {
val callbackIndex = synchronized(this) {
val index = callbackIndex++
callbacks.add(IndexedCallback(
index,
callback@{ uri ->
if (uri == null) return@callback
var exception: Exception? = null
val game = try {
activity.contentResolver.openInputStream(uri)
?.reader()
?.readText()
?.run {
GameSaver.gameInfoFromString(this)
}
} catch (e: Exception) {
exception = e
null
}
if (game != null) {
// If the user has saved the game from another platform (like Android),
// then the save location might not be right so we have to correct for that
// here
game.customSaveLocation = uri.toString()
loadCompleteCallback(game, null)
} else {
loadCompleteCallback(null, RuntimeException("Failed to load save game", exception))
}
}
))
index
}
Intent(Intent.ACTION_OPEN_DOCUMENT).apply {
type = "*/*"
activity.startActivityForResult(this, callbackIndex)
}
}
}
data class IndexedCallback(
val index: Int,
val callback: (Uri?) -> Unit,
val thread: Thread = Thread.currentThread()
)