Compare commits
2 Commits
ANDROID_1.
...
ANDROID_1.
| Author | SHA1 | Date | |
|---|---|---|---|
|
c10fa48bf5
|
|||
|
acf487db93
|
@@ -16,8 +16,8 @@ android {
|
|||||||
applicationId = "com.atridad.openclimb"
|
applicationId = "com.atridad.openclimb"
|
||||||
minSdk = 31
|
minSdk = 31
|
||||||
targetSdk = 36
|
targetSdk = 36
|
||||||
versionCode = 30
|
versionCode = 32
|
||||||
versionName = "1.7.2"
|
versionName = "1.7.3"
|
||||||
|
|
||||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -79,13 +79,19 @@ class ClimbRepository(database: OpenClimbDatabase, private val context: Context)
|
|||||||
suspend fun insertSession(session: ClimbSession) {
|
suspend fun insertSession(session: ClimbSession) {
|
||||||
sessionDao.insertSession(session)
|
sessionDao.insertSession(session)
|
||||||
dataStateManager.updateDataState()
|
dataStateManager.updateDataState()
|
||||||
|
// Only trigger sync for completed sessions
|
||||||
|
if (session.status != SessionStatus.ACTIVE) {
|
||||||
triggerAutoSync()
|
triggerAutoSync()
|
||||||
}
|
}
|
||||||
|
}
|
||||||
suspend fun updateSession(session: ClimbSession) {
|
suspend fun updateSession(session: ClimbSession) {
|
||||||
sessionDao.updateSession(session)
|
sessionDao.updateSession(session)
|
||||||
dataStateManager.updateDataState()
|
dataStateManager.updateDataState()
|
||||||
|
// Only trigger sync for completed sessions
|
||||||
|
if (session.status != SessionStatus.ACTIVE) {
|
||||||
triggerAutoSync()
|
triggerAutoSync()
|
||||||
}
|
}
|
||||||
|
}
|
||||||
suspend fun deleteSession(session: ClimbSession) {
|
suspend fun deleteSession(session: ClimbSession) {
|
||||||
sessionDao.deleteSession(session)
|
sessionDao.deleteSession(session)
|
||||||
dataStateManager.updateDataState()
|
dataStateManager.updateDataState()
|
||||||
@@ -109,17 +115,14 @@ class ClimbRepository(database: OpenClimbDatabase, private val context: Context)
|
|||||||
suspend fun insertAttempt(attempt: Attempt) {
|
suspend fun insertAttempt(attempt: Attempt) {
|
||||||
attemptDao.insertAttempt(attempt)
|
attemptDao.insertAttempt(attempt)
|
||||||
dataStateManager.updateDataState()
|
dataStateManager.updateDataState()
|
||||||
triggerAutoSync()
|
|
||||||
}
|
}
|
||||||
suspend fun updateAttempt(attempt: Attempt) {
|
suspend fun updateAttempt(attempt: Attempt) {
|
||||||
attemptDao.updateAttempt(attempt)
|
attemptDao.updateAttempt(attempt)
|
||||||
dataStateManager.updateDataState()
|
dataStateManager.updateDataState()
|
||||||
triggerAutoSync()
|
|
||||||
}
|
}
|
||||||
suspend fun deleteAttempt(attempt: Attempt) {
|
suspend fun deleteAttempt(attempt: Attempt) {
|
||||||
attemptDao.deleteAttempt(attempt)
|
attemptDao.deleteAttempt(attempt)
|
||||||
dataStateManager.updateDataState()
|
dataStateManager.updateDataState()
|
||||||
triggerAutoSync()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun exportAllDataToZipUri(context: Context, uri: android.net.Uri) {
|
suspend fun exportAllDataToZipUri(context: Context, uri: android.net.Uri) {
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import com.atridad.openclimb.data.format.BackupGym
|
|||||||
import com.atridad.openclimb.data.format.BackupProblem
|
import com.atridad.openclimb.data.format.BackupProblem
|
||||||
import com.atridad.openclimb.data.format.ClimbDataBackup
|
import com.atridad.openclimb.data.format.ClimbDataBackup
|
||||||
import com.atridad.openclimb.data.migration.ImageMigrationService
|
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.repository.ClimbRepository
|
||||||
import com.atridad.openclimb.data.state.DataStateManager
|
import com.atridad.openclimb.data.state.DataStateManager
|
||||||
import com.atridad.openclimb.utils.DateFormatUtils
|
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 allSessions = repository.getAllSessions().first()
|
||||||
val allAttempts = repository.getAllAttempts().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(
|
return ClimbDataBackup(
|
||||||
exportedAt = dataStateManager.getLastModified(),
|
exportedAt = dataStateManager.getLastModified(),
|
||||||
version = "2.0",
|
version = "2.0",
|
||||||
formatVersion = "2.0",
|
formatVersion = "2.0",
|
||||||
gyms = allGyms.map { BackupGym.fromGym(it) },
|
gyms = allGyms.map { BackupGym.fromGym(it) },
|
||||||
problems = allProblems.map { BackupProblem.fromProblem(it) },
|
problems = allProblems.map { BackupProblem.fromProblem(it) },
|
||||||
sessions = allSessions.map { BackupClimbSession.fromClimbSession(it) },
|
sessions = completedSessions.map { BackupClimbSession.fromClimbSession(it) },
|
||||||
attempts = allAttempts.map { BackupAttempt.fromAttempt(it) }
|
attempts = completedAttempts.map { BackupAttempt.fromAttempt(it) }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -579,6 +591,20 @@ class SyncService(private val context: Context, private val repository: ClimbRep
|
|||||||
backup: ClimbDataBackup,
|
backup: ClimbDataBackup,
|
||||||
imagePathMapping: Map<String, String> = emptyMap()
|
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()
|
repository.resetAllData()
|
||||||
|
|
||||||
backup.gyms.forEach { backupGym ->
|
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)
|
dataStateManager.setLastModified(backup.exportedAt)
|
||||||
Log.d(TAG, "Data state synchronized to imported timestamp: ${backup.exportedAt}")
|
Log.d(TAG, "Data state synchronized to imported timestamp: ${backup.exportedAt}")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -454,4 +454,61 @@ class SyncMergeLogicTest {
|
|||||||
dateString1 > dateString2
|
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
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -465,7 +465,7 @@
|
|||||||
CODE_SIGN_ENTITLEMENTS = OpenClimb/OpenClimb.entitlements;
|
CODE_SIGN_ENTITLEMENTS = OpenClimb/OpenClimb.entitlements;
|
||||||
CODE_SIGN_IDENTITY = "Apple Development";
|
CODE_SIGN_IDENTITY = "Apple Development";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 14;
|
CURRENT_PROJECT_VERSION = 15;
|
||||||
DEVELOPMENT_TEAM = 4BC9Y2LL4B;
|
DEVELOPMENT_TEAM = 4BC9Y2LL4B;
|
||||||
ENABLE_PREVIEWS = YES;
|
ENABLE_PREVIEWS = YES;
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
@@ -485,7 +485,7 @@
|
|||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
);
|
);
|
||||||
MARKETING_VERSION = 1.2.3;
|
MARKETING_VERSION = 1.2.4;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.atridad.OpenClimb;
|
PRODUCT_BUNDLE_IDENTIFIER = com.atridad.OpenClimb;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||||
@@ -508,7 +508,7 @@
|
|||||||
CODE_SIGN_ENTITLEMENTS = OpenClimb/OpenClimb.entitlements;
|
CODE_SIGN_ENTITLEMENTS = OpenClimb/OpenClimb.entitlements;
|
||||||
CODE_SIGN_IDENTITY = "Apple Development";
|
CODE_SIGN_IDENTITY = "Apple Development";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 14;
|
CURRENT_PROJECT_VERSION = 15;
|
||||||
DEVELOPMENT_TEAM = 4BC9Y2LL4B;
|
DEVELOPMENT_TEAM = 4BC9Y2LL4B;
|
||||||
ENABLE_PREVIEWS = YES;
|
ENABLE_PREVIEWS = YES;
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
@@ -528,7 +528,7 @@
|
|||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
);
|
);
|
||||||
MARKETING_VERSION = 1.2.3;
|
MARKETING_VERSION = 1.2.4;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.atridad.OpenClimb;
|
PRODUCT_BUNDLE_IDENTIFIER = com.atridad.OpenClimb;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||||
@@ -592,7 +592,7 @@
|
|||||||
ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground;
|
ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground;
|
||||||
CODE_SIGN_ENTITLEMENTS = SessionStatusLiveExtension.entitlements;
|
CODE_SIGN_ENTITLEMENTS = SessionStatusLiveExtension.entitlements;
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 14;
|
CURRENT_PROJECT_VERSION = 15;
|
||||||
DEVELOPMENT_TEAM = 4BC9Y2LL4B;
|
DEVELOPMENT_TEAM = 4BC9Y2LL4B;
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
INFOPLIST_FILE = SessionStatusLive/Info.plist;
|
INFOPLIST_FILE = SessionStatusLive/Info.plist;
|
||||||
@@ -603,7 +603,7 @@
|
|||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
"@executable_path/../../Frameworks",
|
"@executable_path/../../Frameworks",
|
||||||
);
|
);
|
||||||
MARKETING_VERSION = 1.2.3;
|
MARKETING_VERSION = 1.2.4;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.atridad.OpenClimb.SessionStatusLive;
|
PRODUCT_BUNDLE_IDENTIFIER = com.atridad.OpenClimb.SessionStatusLive;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
SKIP_INSTALL = YES;
|
SKIP_INSTALL = YES;
|
||||||
@@ -622,7 +622,7 @@
|
|||||||
ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground;
|
ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground;
|
||||||
CODE_SIGN_ENTITLEMENTS = SessionStatusLiveExtension.entitlements;
|
CODE_SIGN_ENTITLEMENTS = SessionStatusLiveExtension.entitlements;
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 14;
|
CURRENT_PROJECT_VERSION = 15;
|
||||||
DEVELOPMENT_TEAM = 4BC9Y2LL4B;
|
DEVELOPMENT_TEAM = 4BC9Y2LL4B;
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
INFOPLIST_FILE = SessionStatusLive/Info.plist;
|
INFOPLIST_FILE = SessionStatusLive/Info.plist;
|
||||||
@@ -633,7 +633,7 @@
|
|||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
"@executable_path/../../Frameworks",
|
"@executable_path/../../Frameworks",
|
||||||
);
|
);
|
||||||
MARKETING_VERSION = 1.2.3;
|
MARKETING_VERSION = 1.2.4;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.atridad.OpenClimb.SessionStatusLive;
|
PRODUCT_BUNDLE_IDENTIFIER = com.atridad.OpenClimb.SessionStatusLive;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
SKIP_INSTALL = YES;
|
SKIP_INSTALL = YES;
|
||||||
|
|||||||
Binary file not shown.
@@ -378,7 +378,6 @@ class SyncService: ObservableObject {
|
|||||||
print("Renamed local image: \(filename) -> \(consistentFilename)")
|
print("Renamed local image: \(filename) -> \(consistentFilename)")
|
||||||
|
|
||||||
// Update problem's image path in memory for consistency
|
// Update problem's image path in memory for consistency
|
||||||
// Note: This would require updating the problem in the data manager
|
|
||||||
} catch {
|
} catch {
|
||||||
print("Failed to rename local image, using original: \(error)")
|
print("Failed to rename local image, using original: \(error)")
|
||||||
}
|
}
|
||||||
@@ -397,12 +396,24 @@ class SyncService: ObservableObject {
|
|||||||
|
|
||||||
private func createBackupFromDataManager(_ dataManager: ClimbingDataManager) -> ClimbDataBackup
|
private func createBackupFromDataManager(_ dataManager: ClimbingDataManager) -> ClimbDataBackup
|
||||||
{
|
{
|
||||||
|
// Filter out active sessions and their attempts from sync
|
||||||
|
let completedSessions = dataManager.sessions.filter { $0.status != .active }
|
||||||
|
let activeSessionIds = Set(
|
||||||
|
dataManager.sessions.filter { $0.status == .active }.map { $0.id })
|
||||||
|
let completedAttempts = dataManager.attempts.filter {
|
||||||
|
!activeSessionIds.contains($0.sessionId)
|
||||||
|
}
|
||||||
|
|
||||||
|
print(
|
||||||
|
"iOS SYNC: Excluding \(dataManager.sessions.count - completedSessions.count) active sessions and \(dataManager.attempts.count - completedAttempts.count) active session attempts from sync"
|
||||||
|
)
|
||||||
|
|
||||||
return ClimbDataBackup(
|
return ClimbDataBackup(
|
||||||
exportedAt: DataStateManager.shared.getLastModified(),
|
exportedAt: DataStateManager.shared.getLastModified(),
|
||||||
gyms: dataManager.gyms.map { BackupGym(from: $0) },
|
gyms: dataManager.gyms.map { BackupGym(from: $0) },
|
||||||
problems: dataManager.problems.map { BackupProblem(from: $0) },
|
problems: dataManager.problems.map { BackupProblem(from: $0) },
|
||||||
sessions: dataManager.sessions.map { BackupClimbSession(from: $0) },
|
sessions: completedSessions.map { BackupClimbSession(from: $0) },
|
||||||
attempts: dataManager.attempts.map { BackupAttempt(from: $0) }
|
attempts: completedAttempts.map { BackupAttempt(from: $0) }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -412,6 +423,17 @@ class SyncService: ObservableObject {
|
|||||||
) throws {
|
) throws {
|
||||||
do {
|
do {
|
||||||
|
|
||||||
|
// Store active sessions and their attempts before import
|
||||||
|
let activeSessions = dataManager.sessions.filter { $0.status == .active }
|
||||||
|
let activeSessionIds = Set(activeSessions.map { $0.id })
|
||||||
|
let activeAttempts = dataManager.attempts.filter {
|
||||||
|
activeSessionIds.contains($0.sessionId)
|
||||||
|
}
|
||||||
|
|
||||||
|
print(
|
||||||
|
"iOS IMPORT: Preserving \(activeSessions.count) active sessions and \(activeAttempts.count) active attempts during import"
|
||||||
|
)
|
||||||
|
|
||||||
// Update problem image paths to point to downloaded images
|
// Update problem image paths to point to downloaded images
|
||||||
let updatedBackup: ClimbDataBackup
|
let updatedBackup: ClimbDataBackup
|
||||||
if !imagePathMapping.isEmpty {
|
if !imagePathMapping.isEmpty {
|
||||||
@@ -456,6 +478,24 @@ class SyncService: ObservableObject {
|
|||||||
// Use existing import method which properly handles data restoration
|
// Use existing import method which properly handles data restoration
|
||||||
try dataManager.importData(from: zipData, showSuccessMessage: false)
|
try dataManager.importData(from: zipData, showSuccessMessage: false)
|
||||||
|
|
||||||
|
// Restore active sessions and their attempts after import
|
||||||
|
for session in activeSessions {
|
||||||
|
print("iOS IMPORT: Restoring active session: \(session.id)")
|
||||||
|
dataManager.sessions.append(session)
|
||||||
|
if session.id == dataManager.activeSession?.id {
|
||||||
|
dataManager.activeSession = session
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for attempt in activeAttempts {
|
||||||
|
dataManager.attempts.append(attempt)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save restored data
|
||||||
|
dataManager.saveSessions()
|
||||||
|
dataManager.saveAttempts()
|
||||||
|
dataManager.saveActiveSession()
|
||||||
|
|
||||||
// Update local data state to match imported data timestamp
|
// Update local data state to match imported data timestamp
|
||||||
DataStateManager.shared.setLastModified(backup.exportedAt)
|
DataStateManager.shared.setLastModified(backup.exportedAt)
|
||||||
print("Data state synchronized to imported timestamp: \(backup.exportedAt)")
|
print("Data state synchronized to imported timestamp: \(backup.exportedAt)")
|
||||||
|
|||||||
@@ -158,7 +158,7 @@ class ClimbingDataManager: ObservableObject {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func saveSessions() {
|
internal func saveSessions() {
|
||||||
if let data = try? encoder.encode(sessions) {
|
if let data = try? encoder.encode(sessions) {
|
||||||
userDefaults.set(data, forKey: Keys.sessions)
|
userDefaults.set(data, forKey: Keys.sessions)
|
||||||
// Share with widget - convert to widget format
|
// Share with widget - convert to widget format
|
||||||
@@ -176,7 +176,7 @@ class ClimbingDataManager: ObservableObject {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func saveAttempts() {
|
internal func saveAttempts() {
|
||||||
if let data = try? encoder.encode(attempts) {
|
if let data = try? encoder.encode(attempts) {
|
||||||
userDefaults.set(data, forKey: Keys.attempts)
|
userDefaults.set(data, forKey: Keys.attempts)
|
||||||
// Share with widget - convert to widget format
|
// Share with widget - convert to widget format
|
||||||
@@ -197,7 +197,7 @@ class ClimbingDataManager: ObservableObject {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func saveActiveSession() {
|
internal func saveActiveSession() {
|
||||||
if let activeSession = activeSession,
|
if let activeSession = activeSession,
|
||||||
let data = try? encoder.encode(activeSession)
|
let data = try? encoder.encode(activeSession)
|
||||||
{
|
{
|
||||||
@@ -326,9 +326,6 @@ class ClimbingDataManager: ObservableObject {
|
|||||||
saveSessions()
|
saveSessions()
|
||||||
DataStateManager.shared.updateDataState()
|
DataStateManager.shared.updateDataState()
|
||||||
|
|
||||||
successMessage = "Session started successfully"
|
|
||||||
clearMessageAfterDelay()
|
|
||||||
|
|
||||||
// MARK: - Start Live Activity for new session
|
// MARK: - Start Live Activity for new session
|
||||||
if let gym = gym(withId: gymId) {
|
if let gym = gym(withId: gymId) {
|
||||||
Task {
|
Task {
|
||||||
@@ -336,9 +333,6 @@ class ClimbingDataManager: ObservableObject {
|
|||||||
for: newSession, gymName: gym.name)
|
for: newSession, gymName: gym.name)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Trigger auto-sync if enabled
|
|
||||||
syncService.triggerAutoSync(dataManager: self)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func endSession(_ sessionId: UUID) {
|
func endSession(_ sessionId: UUID) {
|
||||||
@@ -356,8 +350,6 @@ class ClimbingDataManager: ObservableObject {
|
|||||||
saveActiveSession()
|
saveActiveSession()
|
||||||
saveSessions()
|
saveSessions()
|
||||||
DataStateManager.shared.updateDataState()
|
DataStateManager.shared.updateDataState()
|
||||||
successMessage = "Session completed successfully"
|
|
||||||
clearMessageAfterDelay()
|
|
||||||
|
|
||||||
// Trigger auto-sync if enabled
|
// Trigger auto-sync if enabled
|
||||||
syncService.triggerAutoSync(dataManager: self)
|
syncService.triggerAutoSync(dataManager: self)
|
||||||
@@ -380,16 +372,16 @@ class ClimbingDataManager: ObservableObject {
|
|||||||
|
|
||||||
saveSessions()
|
saveSessions()
|
||||||
DataStateManager.shared.updateDataState()
|
DataStateManager.shared.updateDataState()
|
||||||
successMessage = "Session updated successfully"
|
|
||||||
clearMessageAfterDelay()
|
|
||||||
|
|
||||||
// Update Live Activity when session is updated
|
// Update Live Activity when session is updated
|
||||||
updateLiveActivityForActiveSession()
|
updateLiveActivityForActiveSession()
|
||||||
|
|
||||||
// Trigger auto-sync if enabled
|
// Only trigger sync if session is completed
|
||||||
|
if session.status != .active {
|
||||||
syncService.triggerAutoSync(dataManager: self)
|
syncService.triggerAutoSync(dataManager: self)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func deleteSession(_ session: ClimbSession) {
|
func deleteSession(_ session: ClimbSession) {
|
||||||
// Delete associated attempts first
|
// Delete associated attempts first
|
||||||
@@ -406,8 +398,6 @@ class ClimbingDataManager: ObservableObject {
|
|||||||
sessions.removeAll { $0.id == session.id }
|
sessions.removeAll { $0.id == session.id }
|
||||||
saveSessions()
|
saveSessions()
|
||||||
DataStateManager.shared.updateDataState()
|
DataStateManager.shared.updateDataState()
|
||||||
successMessage = "Session deleted successfully"
|
|
||||||
clearMessageAfterDelay()
|
|
||||||
|
|
||||||
// Update Live Activity when session is deleted
|
// Update Live Activity when session is deleted
|
||||||
updateLiveActivityForActiveSession()
|
updateLiveActivityForActiveSession()
|
||||||
@@ -435,12 +425,6 @@ class ClimbingDataManager: ObservableObject {
|
|||||||
saveAttempts()
|
saveAttempts()
|
||||||
DataStateManager.shared.updateDataState()
|
DataStateManager.shared.updateDataState()
|
||||||
|
|
||||||
successMessage = "Attempt logged successfully"
|
|
||||||
|
|
||||||
// Trigger auto-sync if enabled
|
|
||||||
syncService.triggerAutoSync(dataManager: self)
|
|
||||||
clearMessageAfterDelay()
|
|
||||||
|
|
||||||
// Update Live Activity when new attempt is added
|
// Update Live Activity when new attempt is added
|
||||||
updateLiveActivityForActiveSession()
|
updateLiveActivityForActiveSession()
|
||||||
}
|
}
|
||||||
@@ -450,14 +434,9 @@ class ClimbingDataManager: ObservableObject {
|
|||||||
attempts[index] = attempt
|
attempts[index] = attempt
|
||||||
saveAttempts()
|
saveAttempts()
|
||||||
DataStateManager.shared.updateDataState()
|
DataStateManager.shared.updateDataState()
|
||||||
successMessage = "Attempt updated successfully"
|
|
||||||
clearMessageAfterDelay()
|
|
||||||
|
|
||||||
// Update Live Activity when attempt is updated
|
// Update Live Activity when attempt is updated
|
||||||
updateLiveActivityForActiveSession()
|
updateLiveActivityForActiveSession()
|
||||||
|
|
||||||
// Trigger auto-sync if enabled
|
|
||||||
syncService.triggerAutoSync(dataManager: self)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -465,14 +444,9 @@ class ClimbingDataManager: ObservableObject {
|
|||||||
attempts.removeAll { $0.id == attempt.id }
|
attempts.removeAll { $0.id == attempt.id }
|
||||||
saveAttempts()
|
saveAttempts()
|
||||||
DataStateManager.shared.updateDataState()
|
DataStateManager.shared.updateDataState()
|
||||||
successMessage = "Attempt deleted successfully"
|
|
||||||
clearMessageAfterDelay()
|
|
||||||
|
|
||||||
// Update Live Activity when attempt is deleted
|
// Update Live Activity when attempt is deleted
|
||||||
updateLiveActivityForActiveSession()
|
updateLiveActivityForActiveSession()
|
||||||
|
|
||||||
// Trigger auto-sync if enabled
|
|
||||||
syncService.triggerAutoSync(dataManager: self)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func attempts(forSession sessionId: UUID) -> [Attempt] {
|
func attempts(forSession sessionId: UUID) -> [Attempt] {
|
||||||
|
|||||||
@@ -131,7 +131,6 @@ final class LiveActivityManager {
|
|||||||
)
|
)
|
||||||
|
|
||||||
await currentActivity.update(.init(state: updatedContentState, staleDate: nil))
|
await currentActivity.update(.init(state: updatedContentState, staleDate: nil))
|
||||||
print("Live Activity updated successfully")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Call this when a ClimbSession ends to end the Live Activity
|
/// Call this when a ClimbSession ends to end the Live Activity
|
||||||
|
|||||||
@@ -252,4 +252,78 @@ final class OpenClimbTests: XCTestCase {
|
|||||||
XCTAssertNotNil(parsedDate)
|
XCTAssertNotNil(parsedDate)
|
||||||
XCTAssertEqual(date.timeIntervalSince1970, parsedDate!.timeIntervalSince1970, accuracy: 1.0)
|
XCTAssertEqual(date.timeIntervalSince1970, parsedDate!.timeIntervalSince1970, accuracy: 1.0)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: - Active Session Preservation Tests
|
||||||
|
|
||||||
|
func testActiveSessionPreservationDuringImport() throws {
|
||||||
|
// Test that active sessions are preserved during import operations
|
||||||
|
// This tests the fix for the bug where active sessions disappear after sync
|
||||||
|
|
||||||
|
// Simulate an active session that exists locally but not in import data
|
||||||
|
let activeSessionId = UUID()
|
||||||
|
let gymId = UUID()
|
||||||
|
|
||||||
|
// Test data structure representing local active session
|
||||||
|
let localActiveSession: [String: Any] = [
|
||||||
|
"id": activeSessionId.uuidString,
|
||||||
|
"gymId": gymId.uuidString,
|
||||||
|
"status": "active",
|
||||||
|
"date": "2024-01-01",
|
||||||
|
"startTime": "2024-01-01T10:00:00Z",
|
||||||
|
]
|
||||||
|
|
||||||
|
// Test data structure representing server sessions (without the active one)
|
||||||
|
let serverSessions: [[String: Any]] = [
|
||||||
|
[
|
||||||
|
"id": UUID().uuidString,
|
||||||
|
"gymId": gymId.uuidString,
|
||||||
|
"status": "completed",
|
||||||
|
"date": "2023-12-31",
|
||||||
|
"startTime": "2023-12-31T15:00:00Z",
|
||||||
|
"endTime": "2023-12-31T17:00:00Z",
|
||||||
|
]
|
||||||
|
]
|
||||||
|
|
||||||
|
// Verify test setup
|
||||||
|
XCTAssertEqual(localActiveSession["status"] as? String, "active")
|
||||||
|
XCTAssertEqual(serverSessions.count, 1)
|
||||||
|
XCTAssertEqual(serverSessions[0]["status"] as? String, "completed")
|
||||||
|
|
||||||
|
// Verify that the active session ID is not in the server sessions
|
||||||
|
let serverSessionIds = serverSessions.compactMap { $0["id"] as? String }
|
||||||
|
XCTAssertFalse(serverSessionIds.contains(activeSessionId.uuidString))
|
||||||
|
|
||||||
|
// Test that we can identify an active session
|
||||||
|
if let status = localActiveSession["status"] as? String {
|
||||||
|
XCTAssertTrue(status == "active")
|
||||||
|
} else {
|
||||||
|
XCTFail("Failed to extract session status")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test session ID validation
|
||||||
|
if let sessionIdString = localActiveSession["id"] as? String,
|
||||||
|
let sessionId = UUID(uuidString: sessionIdString)
|
||||||
|
{
|
||||||
|
XCTAssertEqual(sessionId, activeSessionId)
|
||||||
|
} else {
|
||||||
|
XCTFail("Failed to parse session ID")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test that combining sessions preserves both local active and server completed
|
||||||
|
var combinedSessions = serverSessions
|
||||||
|
combinedSessions.append(localActiveSession)
|
||||||
|
|
||||||
|
XCTAssertEqual(combinedSessions.count, 2)
|
||||||
|
|
||||||
|
// Verify both session types are present
|
||||||
|
let hasActiveSession = combinedSessions.contains { session in
|
||||||
|
(session["status"] as? String) == "active"
|
||||||
|
}
|
||||||
|
let hasCompletedSession = combinedSessions.contains { session in
|
||||||
|
(session["status"] as? String) == "completed"
|
||||||
|
}
|
||||||
|
|
||||||
|
XCTAssertTrue(hasActiveSession, "Combined sessions should contain active session")
|
||||||
|
XCTAssertTrue(hasCompletedSession, "Combined sessions should contain completed session")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user