mirror of
https://github.com/yairm210/Unciv.git
synced 2025-02-11 19:38:50 +07:00
![Billy Brawner](/assets/img/avatar_default.png)
* 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
126 lines
4.9 KiB
Kotlin
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()
|
|
)
|