mirror of
https://github.com/yairm210/Unciv.git
synced 2025-07-21 13:18:56 +07:00
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
This commit is contained in:
@ -1,5 +1,6 @@
|
||||
package com.unciv.app
|
||||
|
||||
import android.content.Intent
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import androidx.core.app.NotificationManagerCompat
|
||||
@ -13,8 +14,12 @@ import com.unciv.ui.utils.ORIGINAL_FONT_SIZE
|
||||
import java.io.File
|
||||
|
||||
class AndroidLauncher : AndroidApplication() {
|
||||
private var customSaveLocationHelper: CustomSaveLocationHelperAndroid? = null
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
|
||||
customSaveLocationHelper = CustomSaveLocationHelperAndroid(this)
|
||||
}
|
||||
MultiplayerTurnCheckWorker.createNotificationChannels(applicationContext)
|
||||
|
||||
// Only allow mods on KK+, to avoid READ_EXTERNAL_STORAGE permission earlier versions need
|
||||
@ -29,7 +34,8 @@ class AndroidLauncher : AndroidApplication() {
|
||||
version = BuildConfig.VERSION_NAME,
|
||||
crashReportSender = CrashReportSenderAndroid(this),
|
||||
exitEvent = this::finish,
|
||||
fontImplementation = NativeFontAndroid(ORIGINAL_FONT_SIZE.toInt())
|
||||
fontImplementation = NativeFontAndroid(ORIGINAL_FONT_SIZE.toInt()),
|
||||
customSaveLocationHelper = customSaveLocationHelper
|
||||
)
|
||||
val game = UncivGame ( androidParameters )
|
||||
initialize(game, config)
|
||||
@ -77,4 +83,13 @@ class AndroidLauncher : AndroidApplication() {
|
||||
catch (ex:Exception){}
|
||||
super.onResume()
|
||||
}
|
||||
|
||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
|
||||
// This should only happen on API 19+ but it's wrapped in the if check to keep the
|
||||
// compiler happy
|
||||
customSaveLocationHelper?.handleIntentData(requestCode, data?.data)
|
||||
}
|
||||
super.onActivityResult(requestCode, resultCode, data)
|
||||
}
|
||||
}
|
125
android/src/com/unciv/app/CustomSaveLocationHelperAndroid.kt
Normal file
125
android/src/com/unciv/app/CustomSaveLocationHelperAndroid.kt
Normal file
@ -0,0 +1,125 @@
|
||||
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()
|
||||
)
|
Reference in New Issue
Block a user