Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
231 changes: 128 additions & 103 deletions app/src/main/java/app/gamenative/service/SteamService.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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<Int, AtomicBoolean>()

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
Expand Down Expand Up @@ -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(
Expand All @@ -1990,82 +2014,82 @@ class SteamService : Service(), IChallengeUrlChanged {
parentScope: CoroutineScope = CoroutineScope(Dispatchers.IO),
overrideLocalChangeNumber: Long? = null,
): Deferred<PostSyncInfo> = 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
}
}

Expand Down Expand Up @@ -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)?
Expand Down
43 changes: 35 additions & 8 deletions app/src/main/java/app/gamenative/ui/PluviaMain.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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),
),
)
}
Expand Down
1 change: 1 addition & 0 deletions app/src/main/java/app/gamenative/ui/enums/DialogType.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
3 changes: 3 additions & 0 deletions app/src/main/res/values-da/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -575,6 +575,9 @@
<!-- TODO: Manual review - PluviaMain: Sync Errors -->
<string name="main_sync_in_progress_retry">Synkroniseringsoperationen tager for lang tid. Prøv venligst at starte spillet igen om et øjeblik.</string>
<string name="main_sync_in_progress">Synkronisering er i gang. Prøv venligst igen om et øjeblik.</string>
<string name="main_sync_in_progress_launch_anyway_message">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).</string>
<string name="main_launch_anyway">Start alligevel</string>
<string name="main_wait">Vent</string>
Comment on lines +578 to +580
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Align terminology with existing “Sky‑synkronisering”.

Other Danish strings use “Sky‑synkronisering” (e.g., library_cloud_sync_*), but this new line says “Cloud‑synkronisering”. Consider aligning for consistency.

♻️ Suggested tweak
-    <string name="main_sync_in_progress_launch_anyway_message">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).</string>
+    <string name="main_sync_in_progress_launch_anyway_message">Sky-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).</string>
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<string name="main_sync_in_progress_launch_anyway_message">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).</string>
<string name="main_launch_anyway">Start alligevel</string>
<string name="main_wait">Vent</string>
<string name="main_sync_in_progress_launch_anyway_message">Sky-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).</string>
<string name="main_launch_anyway">Start alligevel</string>
<string name="main_wait">Vent</string>
🤖 Prompt for AI Agents
In `@app/src/main/res/values-da/strings.xml` around lines 578 - 580, The Danish
string main_sync_in_progress_launch_anyway_message uses "Cloud-synkronisering"
but other strings use "Sky-synkronisering"; update the message text to replace
"Cloud-synkronisering" with "Sky-synkronisering" so terminology is consistent
with existing keys like library_cloud_sync_*, keeping the rest of the sentence
intact and leaving string name main_sync_in_progress_launch_anyway_message,
main_launch_anyway and main_wait unchanged.

<string name="main_sync_failed">Kunne ikke synkronisere gemfiler: %s.</string>

<!-- TODO: Manual review - PluviaMain: Pending Operations -->
Expand Down
Loading