mirror of
https://github.com/yairm210/Unciv.git
synced 2025-07-21 05:09:25 +07:00
Refactor: Extract all cross-platform code from CustomSaveLocationHelpers into core module (#6962)
* Also fixes the GameInfo.customSaveLocation to work for Android
This commit is contained in:
@ -15,13 +15,13 @@ import com.unciv.utils.Log
|
||||
import java.io.File
|
||||
|
||||
open class AndroidLauncher : AndroidApplication() {
|
||||
private var customSaveLocationHelper: CustomSaveLocationHelperAndroid? = null
|
||||
private var customFileLocationHelper: CustomFileLocationHelperAndroid? = null
|
||||
private var game: UncivGame? = null
|
||||
private var deepLinkedMultiplayerGame: String? = null
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
Log.backend = AndroidLogBackend()
|
||||
customSaveLocationHelper = CustomSaveLocationHelperAndroid(this)
|
||||
customFileLocationHelper = CustomFileLocationHelperAndroid(this)
|
||||
MultiplayerTurnCheckWorker.createNotificationChannels(applicationContext)
|
||||
|
||||
copyMods()
|
||||
@ -41,7 +41,7 @@ open class AndroidLauncher : AndroidApplication() {
|
||||
version = BuildConfig.VERSION_NAME,
|
||||
crashReportSysInfo = CrashReportSysInfoAndroid,
|
||||
fontImplementation = NativeFontAndroid(Fonts.ORIGINAL_FONT_SIZE.toInt(), fontFamily),
|
||||
customSaveLocationHelper = customSaveLocationHelper,
|
||||
customFileLocationHelper = customFileLocationHelper,
|
||||
platformSpecificHelper = platformSpecificHelper
|
||||
)
|
||||
|
||||
@ -72,9 +72,12 @@ open class AndroidLauncher : AndroidApplication() {
|
||||
if (UncivGame.isCurrentInitialized()
|
||||
&& UncivGame.Current.isGameInfoInitialized()
|
||||
&& UncivGame.Current.settings.multiplayer.turnCheckerEnabled
|
||||
&& UncivGame.Current.gameSaver.getMultiplayerSaves().any()) {
|
||||
MultiplayerTurnCheckWorker.startTurnChecker(applicationContext, UncivGame.Current.gameSaver,
|
||||
UncivGame.Current.gameInfo, UncivGame.Current.settings.multiplayer)
|
||||
&& UncivGame.Current.gameSaver.getMultiplayerSaves().any()
|
||||
) {
|
||||
MultiplayerTurnCheckWorker.startTurnChecker(
|
||||
applicationContext, UncivGame.Current.gameSaver,
|
||||
UncivGame.Current.gameInfo, UncivGame.Current.settings.multiplayer
|
||||
)
|
||||
}
|
||||
super.onPause()
|
||||
}
|
||||
@ -115,7 +118,7 @@ open class AndroidLauncher : AndroidApplication() {
|
||||
}
|
||||
|
||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||
customSaveLocationHelper?.handleIntentData(requestCode, data?.data)
|
||||
customFileLocationHelper?.onActivityResult(requestCode, data)
|
||||
super.onActivityResult(requestCode, resultCode, data)
|
||||
}
|
||||
}
|
||||
|
98
android/src/com/unciv/app/CustomFileLocationHelperAndroid.kt
Normal file
98
android/src/com/unciv/app/CustomFileLocationHelperAndroid.kt
Normal file
@ -0,0 +1,98 @@
|
||||
package com.unciv.app
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.provider.DocumentsContract
|
||||
import android.provider.OpenableColumns
|
||||
import androidx.annotation.GuardedBy
|
||||
import com.unciv.logic.CustomFileLocationHelper
|
||||
import com.unciv.ui.crashhandling.postCrashHandlingRunnable
|
||||
import java.io.InputStream
|
||||
import java.io.OutputStream
|
||||
|
||||
class CustomFileLocationHelperAndroid(private val activity: Activity) : CustomFileLocationHelper() {
|
||||
|
||||
@GuardedBy("this")
|
||||
private val callbacks = mutableListOf<ActivityCallback>()
|
||||
@GuardedBy("this")
|
||||
private var curActivityRequestCode = 100
|
||||
|
||||
override fun createOutputStream(suggestedLocation: String, callback: (String?, OutputStream?, Exception?) -> Unit) {
|
||||
val requestCode = createActivityCallback(callback) { activity.contentResolver.openOutputStream(it, "rwt") }
|
||||
|
||||
// When we loaded, we returned a "content://" URI as file location.
|
||||
val uri = Uri.parse(suggestedLocation)
|
||||
val fileName = if (uri.scheme == "content") {
|
||||
val cursor = activity.contentResolver.query(uri, null, null, null, null)
|
||||
cursor.use {
|
||||
// we should have a direct URI to a file, so first is enough
|
||||
if (it?.moveToFirst() == true) {
|
||||
it.getString(it.getColumnIndex(OpenableColumns.DISPLAY_NAME))
|
||||
} else {
|
||||
""
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// if we didn't load, this is some file name entered by the user
|
||||
suggestedLocation
|
||||
}
|
||||
|
||||
Intent(Intent.ACTION_CREATE_DOCUMENT).apply {
|
||||
type = "application/json"
|
||||
putExtra(Intent.EXTRA_TITLE, fileName)
|
||||
if (uri.scheme == "content") {
|
||||
putExtra(DocumentsContract.EXTRA_INITIAL_URI, uri)
|
||||
}
|
||||
activity.startActivityForResult(this, requestCode)
|
||||
}
|
||||
}
|
||||
|
||||
override fun createInputStream(callback: (String?, InputStream?, Exception?) -> Unit) {
|
||||
val callbackIndex = createActivityCallback(callback, activity.contentResolver::openInputStream)
|
||||
|
||||
Intent(Intent.ACTION_OPEN_DOCUMENT).apply {
|
||||
type = "*/*"
|
||||
// It is theoretically possible to use an initial URI here, however, the only Android URIs we have are obtained from here, so, no dice
|
||||
activity.startActivityForResult(this, callbackIndex)
|
||||
}
|
||||
}
|
||||
|
||||
private fun <T> createActivityCallback(callback: (String?, T?, Exception?) -> Unit,
|
||||
createValue: (Uri) -> T): Int {
|
||||
synchronized(this) {
|
||||
val requestCode = curActivityRequestCode++
|
||||
val activityCallback = ActivityCallback(requestCode) { uri ->
|
||||
if (uri == null) {
|
||||
callback(null, null, null)
|
||||
return@ActivityCallback
|
||||
}
|
||||
|
||||
try {
|
||||
val outputStream = createValue(uri)
|
||||
callback(uri.toString(), outputStream, null)
|
||||
} catch (ex: Exception) {
|
||||
callback(null, null, ex)
|
||||
}
|
||||
}
|
||||
callbacks.add(activityCallback)
|
||||
return requestCode
|
||||
}
|
||||
}
|
||||
|
||||
fun onActivityResult(requestCode: Int, data: Intent?) {
|
||||
val callback = synchronized(this) {
|
||||
val index = callbacks.indexOfFirst { it.requestCode == requestCode }
|
||||
if (index == -1) return
|
||||
callbacks.removeAt(index)
|
||||
}
|
||||
postCrashHandlingRunnable {
|
||||
callback.callback(data?.data)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class ActivityCallback(
|
||||
val requestCode: Int,
|
||||
val callback: (Uri?) -> Unit
|
||||
)
|
@ -1,124 +0,0 @@
|
||||
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
|
||||
|
||||
// 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(GameSaver.gameInfoToString(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