[iOS & Android] iOS 1.2.4 & Android 1.7.3

This commit is contained in:
2025-10-06 11:54:36 -06:00
parent acf487db93
commit c10fa48bf5
10 changed files with 245 additions and 54 deletions

View File

@@ -16,8 +16,8 @@ android {
applicationId = "com.atridad.openclimb"
minSdk = 31
targetSdk = 36
versionCode = 31
versionName = "1.7.2"
versionCode = 32
versionName = "1.7.3"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}

View File

@@ -79,12 +79,18 @@ class ClimbRepository(database: OpenClimbDatabase, private val context: Context)
suspend fun insertSession(session: ClimbSession) {
sessionDao.insertSession(session)
dataStateManager.updateDataState()
triggerAutoSync()
// Only trigger sync for completed sessions
if (session.status != SessionStatus.ACTIVE) {
triggerAutoSync()
}
}
suspend fun updateSession(session: ClimbSession) {
sessionDao.updateSession(session)
dataStateManager.updateDataState()
triggerAutoSync()
// Only trigger sync for completed sessions
if (session.status != SessionStatus.ACTIVE) {
triggerAutoSync()
}
}
suspend fun deleteSession(session: ClimbSession) {
sessionDao.deleteSession(session)
@@ -109,17 +115,14 @@ class ClimbRepository(database: OpenClimbDatabase, private val context: Context)
suspend fun insertAttempt(attempt: Attempt) {
attemptDao.insertAttempt(attempt)
dataStateManager.updateDataState()
triggerAutoSync()
}
suspend fun updateAttempt(attempt: Attempt) {
attemptDao.updateAttempt(attempt)
dataStateManager.updateDataState()
triggerAutoSync()
}
suspend fun deleteAttempt(attempt: Attempt) {
attemptDao.deleteAttempt(attempt)
dataStateManager.updateDataState()
triggerAutoSync()
}
suspend fun exportAllDataToZipUri(context: Context, uri: android.net.Uri) {

View File

@@ -10,6 +10,7 @@ import com.atridad.openclimb.data.format.BackupGym
import com.atridad.openclimb.data.format.BackupProblem
import com.atridad.openclimb.data.format.ClimbDataBackup
import com.atridad.openclimb.data.migration.ImageMigrationService
import com.atridad.openclimb.data.model.SessionStatus
import com.atridad.openclimb.data.repository.ClimbRepository
import com.atridad.openclimb.data.state.DataStateManager
import com.atridad.openclimb.utils.DateFormatUtils
@@ -564,14 +565,25 @@ class SyncService(private val context: Context, private val repository: ClimbRep
val allSessions = repository.getAllSessions().first()
val allAttempts = repository.getAllAttempts().first()
// Filter out active sessions and their attempts from sync
val completedSessions = allSessions.filter { it.status != SessionStatus.ACTIVE }
val activeSessionIds =
allSessions.filter { it.status == SessionStatus.ACTIVE }.map { it.id }.toSet()
val completedAttempts = allAttempts.filter { !activeSessionIds.contains(it.sessionId) }
Log.d(
TAG,
"Sync exclusions: ${allSessions.size - completedSessions.size} active sessions, ${allAttempts.size - completedAttempts.size} active session attempts"
)
return ClimbDataBackup(
exportedAt = dataStateManager.getLastModified(),
version = "2.0",
formatVersion = "2.0",
gyms = allGyms.map { BackupGym.fromGym(it) },
problems = allProblems.map { BackupProblem.fromProblem(it) },
sessions = allSessions.map { BackupClimbSession.fromClimbSession(it) },
attempts = allAttempts.map { BackupAttempt.fromAttempt(it) }
sessions = completedSessions.map { BackupClimbSession.fromClimbSession(it) },
attempts = completedAttempts.map { BackupAttempt.fromAttempt(it) }
)
}
@@ -579,6 +591,20 @@ class SyncService(private val context: Context, private val repository: ClimbRep
backup: ClimbDataBackup,
imagePathMapping: Map<String, String> = emptyMap()
) {
// Store active sessions and their attempts before reset
val activeSessions =
repository.getAllSessions().first().filter { it.status == SessionStatus.ACTIVE }
val activeSessionIds = activeSessions.map { it.id }.toSet()
val activeAttempts =
repository.getAllAttempts().first().filter {
activeSessionIds.contains(it.sessionId)
}
Log.d(
TAG,
"Preserving ${activeSessions.size} active sessions and ${activeAttempts.size} active attempts during import"
)
repository.resetAllData()
backup.gyms.forEach { backupGym ->
@@ -646,6 +672,24 @@ class SyncService(private val context: Context, private val repository: ClimbRep
}
}
// Restore active sessions and their attempts after import
activeSessions.forEach { session ->
try {
Log.d(TAG, "Restoring active session: ${session.id}")
repository.insertSessionWithoutSync(session)
} catch (e: Exception) {
Log.e(TAG, "Failed to restore active session '${session.id}': ${e.message}")
}
}
activeAttempts.forEach { attempt ->
try {
repository.insertAttemptWithoutSync(attempt)
} catch (e: Exception) {
Log.e(TAG, "Failed to restore active attempt '${attempt.id}': ${e.message}")
}
}
dataStateManager.setLastModified(backup.exportedAt)
Log.d(TAG, "Data state synchronized to imported timestamp: ${backup.exportedAt}")
}

View File

@@ -454,4 +454,61 @@ class SyncMergeLogicTest {
dateString1 > dateString2
}
}
@Test
fun `test active sessions excluded from sync`() {
// Test scenario: Active sessions should not be included in sync data
// This tests the new behavior where active sessions are excluded from sync
// until they are completed
val allLocalSessions =
listOf(
BackupClimbSession(
id = "active_session_1",
gymId = "gym1",
date = "2024-01-01",
startTime = "2024-01-01T10:00:00",
endTime = null,
duration = null,
status = SessionStatus.ACTIVE,
notes = null,
createdAt = "2024-01-01T10:00:00",
updatedAt = "2024-01-01T10:00:00"
),
BackupClimbSession(
id = "completed_session_1",
gymId = "gym1",
date = "2023-12-31",
startTime = "2023-12-31T15:00:00",
endTime = "2023-12-31T17:00:00",
duration = 7200000,
status = SessionStatus.COMPLETED,
notes = "Previous session",
createdAt = "2023-12-31T15:00:00",
updatedAt = "2023-12-31T17:00:00"
)
)
// Simulate filtering that would happen in createBackupFromRepository
val sessionsForSync = allLocalSessions.filter { it.status != SessionStatus.ACTIVE }
// Only completed sessions should be included in sync
assertEquals("Should only include completed sessions in sync", 1, sessionsForSync.size)
// Active session should be excluded
assertFalse(
"Should not contain active session in sync",
sessionsForSync.any {
it.id == "active_session_1" && it.status == SessionStatus.ACTIVE
}
)
// Completed session should be included
assertTrue(
"Should contain completed session in sync",
sessionsForSync.any {
it.id == "completed_session_1" && it.status == SessionStatus.COMPLETED
}
)
}
}