diff --git a/app/src/main/java/app/gamenative/service/SteamService.kt b/app/src/main/java/app/gamenative/service/SteamService.kt index 26090e263..73d386d4d 100644 --- a/app/src/main/java/app/gamenative/service/SteamService.kt +++ b/app/src/main/java/app/gamenative/service/SteamService.kt @@ -103,6 +103,7 @@ import java.util.Collections import java.util.EnumSet import java.util.concurrent.CancellationException import java.util.concurrent.ConcurrentHashMap +import java.util.concurrent.atomic.AtomicBoolean import javax.inject.Inject import kotlin.io.path.pathString import kotlin.time.Duration.Companion.seconds @@ -317,7 +318,30 @@ class SteamService : Service(), IChallengeUrlChanged { return File(dirPath).exists() && !MarkerUtils.hasMarker(dirPath, Marker.DOWNLOAD_COMPLETE_MARKER) } - private var syncInProgress: Boolean = false + private val syncInProgressApps = ConcurrentHashMap() + + private fun getSyncFlag(appId: Int): AtomicBoolean { + val existing = syncInProgressApps[appId] + if (existing != null) { + return existing + } + val created = AtomicBoolean(false) + val prior = syncInProgressApps.putIfAbsent(appId, created) + return prior ?: created + } + + private fun tryAcquireSync(appId: Int): Boolean { + val flag = getSyncFlag(appId) + return flag.compareAndSet(false, true) + } + + private fun releaseSync(appId: Int) { + val flag = syncInProgressApps[appId] + flag?.set(false) + if (flag != null && !flag.get()) { + syncInProgressApps.remove(appId, flag) + } + } // Track whether a game is currently running to prevent premature service stop @JvmStatic @@ -1916,71 +1940,71 @@ class SteamService : Service(), IChallengeUrlChanged { if (isOffline || !isConnected) { return@async PostSyncInfo(SyncResult.UpToDate) } - if (syncInProgress) { - Timber.w("Cannot launch app when sync already in progress") + if (!tryAcquireSync(appId)) { + Timber.w("Cannot launch app when sync already in progress for appId=$appId") return@async PostSyncInfo(SyncResult.InProgress) } - syncInProgress = true - - var syncResult = PostSyncInfo(SyncResult.UnknownFail) - - PrefManager.clientId?.let { clientId -> - instance?.let { steamInstance -> - getAppInfoOf(appId)?.let { appInfo -> - steamInstance._steamCloud?.let { steamCloud -> - val postSyncInfo = SteamAutoCloud.syncUserFiles( - appInfo = appInfo, - clientId = clientId, - steamInstance = steamInstance, - steamCloud = steamCloud, - preferredSave = preferredSave, - parentScope = parentScope, - prefixToPath = prefixToPath, - onProgress = onProgress, - ).await() - - postSyncInfo?.let { info -> - syncResult = info + try { + var syncResult = PostSyncInfo(SyncResult.UnknownFail) - if (info.syncResult == SyncResult.Success || info.syncResult == SyncResult.UpToDate) { - Timber.i( - "Signaling app launch:\n\tappId: %d\n\tclientId: %s\n\tosType: %s", - appId, - PrefManager.clientId, - EOSType.AndroidUnknown, - ) + PrefManager.clientId?.let { clientId -> + instance?.let { steamInstance -> + getAppInfoOf(appId)?.let { appInfo -> + steamInstance._steamCloud?.let { steamCloud -> + val postSyncInfo = SteamAutoCloud.syncUserFiles( + appInfo = appInfo, + clientId = clientId, + steamInstance = steamInstance, + steamCloud = steamCloud, + preferredSave = preferredSave, + parentScope = parentScope, + prefixToPath = prefixToPath, + onProgress = onProgress, + ).await() - val pendingRemoteOperations = steamCloud.signalAppLaunchIntent( - appId = appId, - clientId = clientId, - machineName = SteamUtils.getMachineName(steamInstance), - ignorePendingOperations = ignorePendingOperations, - osType = EOSType.AndroidUnknown, - ).await() + postSyncInfo?.let { info -> + syncResult = info - if (pendingRemoteOperations.isNotEmpty() && !ignorePendingOperations) { - syncResult = PostSyncInfo( - syncResult = SyncResult.PendingOperations, - pendingRemoteOperations = pendingRemoteOperations, + if (info.syncResult == SyncResult.Success || info.syncResult == SyncResult.UpToDate) { + Timber.i( + "Signaling app launch:\n\tappId: %d\n\tclientId: %s\n\tosType: %s", + appId, + PrefManager.clientId, + EOSType.AndroidUnknown, ) - } else if (ignorePendingOperations && - pendingRemoteOperations.any { - it.operation == ECloudPendingRemoteOperation.k_ECloudPendingRemoteOperationAppSessionActive + + val pendingRemoteOperations = steamCloud.signalAppLaunchIntent( + appId = appId, + clientId = clientId, + machineName = SteamUtils.getMachineName(steamInstance), + ignorePendingOperations = ignorePendingOperations, + osType = EOSType.AndroidUnknown, + ).await() + + if (pendingRemoteOperations.isNotEmpty() && !ignorePendingOperations) { + syncResult = PostSyncInfo( + syncResult = SyncResult.PendingOperations, + pendingRemoteOperations = pendingRemoteOperations, + ) + } else if (ignorePendingOperations && + pendingRemoteOperations.any { + it.operation == ECloudPendingRemoteOperation.k_ECloudPendingRemoteOperationAppSessionActive + } + ) { + steamInstance._steamUser!!.kickPlayingSession() } - ) { - steamInstance._steamUser!!.kickPlayingSession() } } } } } } - } - syncInProgress = false - - return@async syncResult + return@async syncResult + } finally { + releaseSync(appId) + } } suspend fun forceSyncUserFiles( @@ -1990,82 +2014,82 @@ class SteamService : Service(), IChallengeUrlChanged { parentScope: CoroutineScope = CoroutineScope(Dispatchers.IO), overrideLocalChangeNumber: Long? = null, ): Deferred = parentScope.async { - if (syncInProgress) { - Timber.w("Cannot force sync when sync already in progress") + if (!tryAcquireSync(appId)) { + Timber.w("Cannot force sync when sync already in progress for appId=$appId") return@async PostSyncInfo(SyncResult.InProgress) } - syncInProgress = true - - var syncResult = PostSyncInfo(SyncResult.UnknownFail) + try { + var syncResult = PostSyncInfo(SyncResult.UnknownFail) - PrefManager.clientId?.let { clientId -> - instance?.let { steamInstance -> - getAppInfoOf(appId)?.let { appInfo -> - steamInstance._steamCloud?.let { steamCloud -> - val postSyncInfo = SteamAutoCloud.syncUserFiles( - appInfo = appInfo, - clientId = clientId, - steamInstance = steamInstance, - steamCloud = steamCloud, - preferredSave = preferredSave, - parentScope = parentScope, - prefixToPath = prefixToPath, - overrideLocalChangeNumber = overrideLocalChangeNumber, - ).await() + PrefManager.clientId?.let { clientId -> + instance?.let { steamInstance -> + getAppInfoOf(appId)?.let { appInfo -> + steamInstance._steamCloud?.let { steamCloud -> + val postSyncInfo = SteamAutoCloud.syncUserFiles( + appInfo = appInfo, + clientId = clientId, + steamInstance = steamInstance, + steamCloud = steamCloud, + preferredSave = preferredSave, + parentScope = parentScope, + prefixToPath = prefixToPath, + overrideLocalChangeNumber = overrideLocalChangeNumber, + ).await() - postSyncInfo?.let { info -> - syncResult = info - Timber.i("Force cloud sync completed for app $appId with result: ${info.syncResult}") + postSyncInfo?.let { info -> + syncResult = info + Timber.i("Force cloud sync completed for app $appId with result: ${info.syncResult}") + } } } } } - } - - syncInProgress = false - return@async syncResult + return@async syncResult + } finally { + releaseSync(appId) + } } suspend fun closeApp(appId: Int, isOffline: Boolean, prefixToPath: (String) -> String) = withContext(Dispatchers.IO) { async { - if (syncInProgress) { - Timber.w("Cannot close app when sync already in progress") + if (isOffline || !isConnected) { return@async } - if (isOffline || !isConnected) { + if (!tryAcquireSync(appId)) { + Timber.w("Cannot close app when sync already in progress for appId=$appId") return@async } - syncInProgress = true - - PrefManager.clientId?.let { clientId -> - instance?.let { steamInstance -> - getAppInfoOf(appId)?.let { appInfo -> - steamInstance._steamCloud?.let { steamCloud -> - val postSyncInfo = SteamAutoCloud.syncUserFiles( - appInfo = appInfo, - clientId = clientId, - steamInstance = steamInstance, - steamCloud = steamCloud, - parentScope = this, - prefixToPath = prefixToPath, - ).await() + try { + PrefManager.clientId?.let { clientId -> + instance?.let { steamInstance -> + getAppInfoOf(appId)?.let { appInfo -> + steamInstance._steamCloud?.let { steamCloud -> + val postSyncInfo = SteamAutoCloud.syncUserFiles( + appInfo = appInfo, + clientId = clientId, + steamInstance = steamInstance, + steamCloud = steamCloud, + parentScope = this, + prefixToPath = prefixToPath, + ).await() - steamCloud.signalAppExitSyncDone( - appId = appId, - clientId = clientId, - uploadsCompleted = postSyncInfo?.uploadsCompleted == true, - uploadsRequired = postSyncInfo?.uploadsRequired == false, - ) + steamCloud.signalAppExitSyncDone( + appId = appId, + clientId = clientId, + uploadsCompleted = postSyncInfo?.uploadsCompleted == true, + uploadsRequired = postSyncInfo?.uploadsRequired == false, + ) + } } } } + } finally { + releaseSync(appId) } - - syncInProgress = false } } @@ -2385,7 +2409,8 @@ class SteamService : Service(), IChallengeUrlChanged { // Add helper to detect if any downloads or cloud sync are in progress fun hasActiveOperations(): Boolean { - return syncInProgress || downloadJobs.values.any { it.getProgress() < 1f } + val anySyncInProgress = syncInProgressApps.values.any { it.get() } + return anySyncInProgress || downloadJobs.values.any { it.getProgress() < 1f } } // Should service auto-stop when idle (backgrounded)? diff --git a/app/src/main/java/app/gamenative/ui/PluviaMain.kt b/app/src/main/java/app/gamenative/ui/PluviaMain.kt index 0c4aefe02..fdf63ba45 100644 --- a/app/src/main/java/app/gamenative/ui/PluviaMain.kt +++ b/app/src/main/java/app/gamenative/ui/PluviaMain.kt @@ -576,6 +576,29 @@ fun PluviaMain( } } + DialogType.SYNC_IN_PROGRESS -> { + onConfirmClick = { + setMessageDialogState(MessageDialogState(false)) + preLaunchApp( + context = context, + appId = state.launchedAppId, + skipCloudSync = true, + setLoadingDialogVisible = viewModel::setLoadingDialogVisible, + setLoadingProgress = viewModel::setLoadingDialogProgress, + setLoadingMessage = viewModel::setLoadingDialogMessage, + setMessageDialogState = setMessageDialogState, + onSuccess = viewModel::launchApp, + isOffline = viewModel.isOffline.value, + ) + } + onDismissClick = { + setMessageDialogState(MessageDialogState(false)) + } + onDismissRequest = { + setMessageDialogState(MessageDialogState(false)) + } + } + DialogType.PENDING_UPLOAD_IN_PROGRESS -> { onDismissClick = { setMessageDialogState(MessageDialogState(false)) @@ -1008,6 +1031,7 @@ fun preLaunchApp( ignorePendingOperations: Boolean = false, preferredSave: SaveLocation = SaveLocation.None, useTemporaryOverride: Boolean = false, + skipCloudSync: Boolean = false, setLoadingDialogVisible: (Boolean) -> Unit, setLoadingProgress: (Float) -> Unit, setLoadingMessage: (String) -> Unit, @@ -1187,6 +1211,13 @@ fun preLaunchApp( return@launch } + if (skipCloudSync) { + Timber.tag("preLaunchApp").w("Skipping Steam Cloud sync for $appId by user request") + setLoadingDialogVisible(false) + onSuccess(context, appId) + return@launch + } + // For Steam games, sync save files and check no pending remote operations are running val prefixToPath: (String) -> String = { prefix -> PathType.from(prefix).toAbsPath(context, gameId, SteamService.userSteamId!!.accountID) @@ -1245,18 +1276,14 @@ fun preLaunchApp( retryCount = retryCount + 1, ) } else { - val message = if (useTemporaryOverride) { - context.getString(R.string.main_sync_in_progress_retry) - } else { - context.getString(R.string.main_sync_in_progress) - } setMessageDialogState( MessageDialogState( visible = true, - type = DialogType.SYNC_FAIL, + type = DialogType.SYNC_IN_PROGRESS, title = context.getString(R.string.sync_error_title), - message = message, - dismissBtnText = context.getString(R.string.ok), + message = context.getString(R.string.main_sync_in_progress_launch_anyway_message), + confirmBtnText = context.getString(R.string.main_launch_anyway), + dismissBtnText = context.getString(R.string.main_wait), ), ) } diff --git a/app/src/main/java/app/gamenative/ui/enums/DialogType.kt b/app/src/main/java/app/gamenative/ui/enums/DialogType.kt index 6d74cffd8..1b783b3da 100644 --- a/app/src/main/java/app/gamenative/ui/enums/DialogType.kt +++ b/app/src/main/java/app/gamenative/ui/enums/DialogType.kt @@ -12,6 +12,7 @@ enum class DialogType(val icon: ImageVector? = null) { DISCORD, SYNC_CONFLICT, SYNC_FAIL, + SYNC_IN_PROGRESS, MULTIPLE_PENDING_OPERATIONS, PENDING_OPERATION_NONE, PENDING_UPLOAD, diff --git a/app/src/main/res/values-da/strings.xml b/app/src/main/res/values-da/strings.xml index a468d7161..06f475077 100644 --- a/app/src/main/res/values-da/strings.xml +++ b/app/src/main/res/values-da/strings.xml @@ -575,6 +575,9 @@ Synkroniseringsoperationen tager for lang tid. Prøv venligst at starte spillet igen om et øjeblik. Synkronisering er i gang. Prøv venligst igen om et øjeblik. + Cloud-synkronisering for dette spil er allerede i gang. Du kan vente og prøve igen eller starte alligevel uden synkronisering (kan give konflikter i gemte filer). + Start alligevel + Vent Kunne ikke synkronisere gemfiler: %s. diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 8ca0355ed..6aa267ef2 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -690,6 +690,9 @@ Die Synchronisierung dauert zu lange. Bitte versuche, das Spiel später erneut zu starten. Synchronisierung läuft. Bitte versuche es später noch einmal. + Die Cloud-Synchronisierung für dieses Spiel läuft bereits. Du kannst warten und es später erneut versuchen oder trotzdem starten ohne zu synchronisieren (kann Speicherstand-Konflikte verursachen). + Trotzdem starten + Warten Speicherstände konnten nicht synchronisiert werden: %s. Upload läuft diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 08180c422..4c864f25a 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -742,6 +742,9 @@ L\'opération de synchronisation prend trop de temps. Veuillez réessayer de lancer le jeu dans un moment. La synchronisation est en cours. Veuillez réessayer dans un moment. + La synchronisation cloud des sauvegardes pour ce jeu est déjà en cours. Vous pouvez attendre et réessayer, ou lancer quand même sans synchroniser (peut causer des conflits de sauvegarde). + Lancer quand même + Attendre Échec de la synchronisation des fichiers de sauvegarde : %s. diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index 1a17b2ddb..e23a79ee3 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -746,6 +746,9 @@ L\'operazione di sincronizzazione sta impiegando troppo tempo. Prova ad avviare nuovamente il gioco tra un momento. La sincronizzazione è attualmente in corso. Riprova tra un momento. + La sincronizzazione dei salvataggi cloud per questo gioco è già in corso. Puoi attendere e riprovare, oppure avviare comunque senza sincronizzare (può causare conflitti di salvataggio). + Avvia comunque + Attendi Impossibile sincronizzare i file di salvataggio: %s. diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml index afa398a91..38278b708 100644 --- a/app/src/main/res/values-pt-rBR/strings.xml +++ b/app/src/main/res/values-pt-rBR/strings.xml @@ -575,6 +575,9 @@ A operação de sincronização está demorando muito. Por favor, tente iniciar o jogo novamente em um momento. A sincronização está em andamento. Por favor, tente novamente em um momento. + A sincronização na nuvem deste jogo já está em andamento. Você pode esperar e tentar novamente ou iniciar mesmo assim sem sincronizar (pode causar conflitos de salvamento). + Iniciar mesmo assim + Esperar Falha ao sincronizar arquivos de save: %s. diff --git a/app/src/main/res/values-ro/strings.xml b/app/src/main/res/values-ro/strings.xml index afcb7531b..f1711bd4a 100644 --- a/app/src/main/res/values-ro/strings.xml +++ b/app/src/main/res/values-ro/strings.xml @@ -746,6 +746,9 @@ Operația de sincronizare durează prea mult. Încearcă să lansezi jocul din nou peste puțin timp. Sincronizarea este în desfășurare. Încearcă din nou peste puțin timp. + Sincronizarea în cloud pentru acest joc este deja în curs. Poți aștepta și încerca din nou sau poți porni oricum fără sincronizare (poate cauza conflicte de salvare). + Pornește oricum + Așteaptă Sincronizarea fișierelor de salvare a eșuat: %s. diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml index b2b45ebb4..4f6187fba 100644 --- a/app/src/main/res/values-uk/strings.xml +++ b/app/src/main/res/values-uk/strings.xml @@ -746,6 +746,9 @@ Операція синхронізації триває занадто довго. Спробуйте запустити гру ще раз через деякий час. Наразі триває синхронізація. Спробуйте ще раз за мить. + Синхронізація хмарних збережень для цієї гри вже триває. Ви можете зачекати й спробувати знову або запустити без синхронізації (може спричинити конфлікти збережень). + Запустити попри це + Зачекати Помилка синхронізації файлів збереження: %s. diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index ac00393e7..a88afa196 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -736,6 +736,9 @@ 同步操作耗时过久,请稍后重新启动游戏 同步目前正在进行中,请稍后再试 + 该游戏的云存档同步正在进行。你可以等待后重试,或继续启动但不进行同步(可能导致存档冲突)。 + 仍然启动 + 等待 同步存档失败:%s diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml index fe7d8d344..d304528f0 100644 --- a/app/src/main/res/values-zh-rTW/strings.xml +++ b/app/src/main/res/values-zh-rTW/strings.xml @@ -740,6 +740,9 @@ 同步操作耗時過久, 請稍後重新啟動遊戲 同步目前正在進行中, 請稍後再試 + 此遊戲的雲端存檔同步正在進行。你可以稍後再試,或繼續啟動但不進行同步(可能造成存檔衝突)。 + 仍然啟動 + 等待 同步存檔案失敗:%s diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 0b6c03a96..6439b9072 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -746,6 +746,9 @@ Sync operation is taking too long. Please try launching the game again in a moment. Sync is currently in progress. Please try again in a moment. + Cloud save sync for this game is already running. You can wait and try again, or launch anyway without syncing (may cause save conflicts). + Launch anyway + Wait Failed to sync save files: %s.