From 6d67ae6d8147349c163404d5933ee036139e6d02 Mon Sep 17 00:00:00 2001 From: Atridad Lahiji Date: Tue, 18 Nov 2025 12:58:45 -0700 Subject: [PATCH] Logging overhaul --- .../ascently/data/health/HealthConnectStub.kt | 193 ++++---- .../data/repository/ClimbRepository.kt | 123 ++--- .../ascently/data/state/DataStateManager.kt | 10 +- .../atridad/ascently/data/sync/SyncService.kt | 430 +++++++++--------- .../service/SessionTrackingService.kt | 223 ++++----- .../com/atridad/ascently/ui/AscentlyApp.kt | 321 +++++++------ .../ascently/ui/viewmodel/ClimbViewModel.kt | 222 +++++---- .../com/atridad/ascently/utils/AppLogger.kt | 51 +++ .../com/atridad/ascently/utils/ImageUtils.kt | 78 ++-- .../ascently/utils/MigrationManager.kt | 47 +- .../ascently/utils/ZipExportImportUtils.kt | 169 ++++--- .../LiveActivityManager.swift | 2 +- .../UserInterfaceState.xcuserstate | Bin 254970 -> 263252 bytes ios/Ascently/ContentView.swift | 14 +- ios/Ascently/Services/HealthKitService.swift | 39 +- ios/Ascently/Services/SyncService.swift | 91 ++-- ios/Ascently/Utils/AppLogger.swift | 46 ++ ios/Ascently/Utils/DataStateManager.swift | 22 +- ios/Ascently/Utils/ImageManager.swift | 164 ++++--- ios/Ascently/Utils/ZipUtils.swift | 52 ++- .../ViewModels/ClimbingDataManager.swift | 210 ++++++--- .../ViewModels/LiveActivityManager.swift | 74 +-- .../Views/LiveActivityDebugView.swift | 2 +- ios/Ascently/Views/ProblemsView.swift | 1 - ios/Ascently/Views/SettingsView.swift | 49 +- 25 files changed, 1428 insertions(+), 1205 deletions(-) create mode 100644 android/app/src/main/java/com/atridad/ascently/utils/AppLogger.kt create mode 100644 ios/Ascently/Utils/AppLogger.swift diff --git a/android/app/src/main/java/com/atridad/ascently/data/health/HealthConnectStub.kt b/android/app/src/main/java/com/atridad/ascently/data/health/HealthConnectStub.kt index 305b1dd..c6e54b5 100644 --- a/android/app/src/main/java/com/atridad/ascently/data/health/HealthConnectStub.kt +++ b/android/app/src/main/java/com/atridad/ascently/data/health/HealthConnectStub.kt @@ -3,7 +3,7 @@ package com.atridad.ascently.data.health import android.annotation.SuppressLint import android.content.Context import android.content.SharedPreferences -import android.util.Log +import com.atridad.ascently.utils.AppLogger import androidx.activity.result.contract.ActivityResultContract import androidx.health.connect.client.HealthConnectClient import androidx.health.connect.client.PermissionController @@ -31,7 +31,7 @@ import kotlinx.coroutines.flow.flow class HealthConnectManager(private val context: Context) { private val preferences: SharedPreferences = - context.getSharedPreferences("health_connect_prefs", Context.MODE_PRIVATE) + context.getSharedPreferences("health_connect_prefs", Context.MODE_PRIVATE) private val _isEnabled = MutableStateFlow(preferences.getBoolean("enabled", false)) private val _hasPermissions = MutableStateFlow(preferences.getBoolean("permissions", false)) @@ -46,21 +46,21 @@ class HealthConnectManager(private val context: Context) { private const val TAG = "HealthConnectManager" val REQUIRED_PERMISSIONS = - setOf( - HealthPermission.getReadPermission(ExerciseSessionRecord::class), - HealthPermission.getWritePermission(ExerciseSessionRecord::class), - HealthPermission.getReadPermission(HeartRateRecord::class), - HealthPermission.getWritePermission(HeartRateRecord::class), - HealthPermission.getReadPermission(TotalCaloriesBurnedRecord::class), - HealthPermission.getWritePermission(TotalCaloriesBurnedRecord::class) - ) + setOf( + HealthPermission.getReadPermission(ExerciseSessionRecord::class), + HealthPermission.getWritePermission(ExerciseSessionRecord::class), + HealthPermission.getReadPermission(HeartRateRecord::class), + HealthPermission.getWritePermission(HeartRateRecord::class), + HealthPermission.getReadPermission(TotalCaloriesBurnedRecord::class), + HealthPermission.getWritePermission(TotalCaloriesBurnedRecord::class) + ) } private val healthConnectClient by lazy { try { HealthConnectClient.getOrCreate(context) } catch (e: Exception) { - Log.e(TAG, "Failed to create Health Connect client", e) + AppLogger.e(TAG, e) { "Failed to create Health Connect client" } _isCompatible.value = false null } @@ -75,7 +75,7 @@ class HealthConnectManager(private val context: Context) { val status = HealthConnectClient.getSdkStatus(context) emit(status == HealthConnectClient.SDK_AVAILABLE) } catch (e: Exception) { - Log.e(TAG, "Error checking Health Connect availability", e) + AppLogger.e(TAG, e) { "Error checking Health Connect availability" } _isCompatible.value = false emit(false) } @@ -90,10 +90,10 @@ class HealthConnectManager(private val context: Context) { try { val alreadyHasPermissions = hasAllPermissions() if (!alreadyHasPermissions) { - Log.d(TAG, "Health Connect enabled - permissions will be requested by UI") + AppLogger.d(TAG) { "Health Connect enabled - permissions will be requested by UI" } } } catch (e: Exception) { - Log.w(TAG, "Error checking permissions when enabling Health Connect", e) + AppLogger.w(TAG, e) { "Error checking permissions when enabling Health Connect" } } } else if (!enabled) { setPermissionsGranted(false) @@ -111,15 +111,15 @@ class HealthConnectManager(private val context: Context) { return false } val grantedPermissions = - healthConnectClient!!.permissionController.getGrantedPermissions() + healthConnectClient!!.permissionController.getGrantedPermissions() val hasAll = - REQUIRED_PERMISSIONS.all { permission -> - grantedPermissions.contains(permission) - } + REQUIRED_PERMISSIONS.all { permission -> + grantedPermissions.contains(permission) + } setPermissionsGranted(hasAll) hasAll } catch (e: Exception) { - Log.e(TAG, "Error checking permissions", e) + AppLogger.e(TAG, e) { "Error checking permissions" } setPermissionsGranted(false) false } @@ -128,14 +128,14 @@ class HealthConnectManager(private val context: Context) { suspend fun isReady(): Boolean { return try { if (!_isEnabled.value || !_isCompatible.value || healthConnectClient == null) - return false + return false val isAvailable = - HealthConnectClient.getSdkStatus(context) == HealthConnectClient.SDK_AVAILABLE + HealthConnectClient.getSdkStatus(context) == HealthConnectClient.SDK_AVAILABLE val hasPerms = if (isAvailable) hasAllPermissions() else false isAvailable && hasPerms } catch (e: Exception) { - Log.e(TAG, "Error checking Health Connect readiness", e) + AppLogger.e(TAG, e) { "Error checking Health Connect readiness" } false } } @@ -148,27 +148,27 @@ class HealthConnectManager(private val context: Context) { return try { REQUIRED_PERMISSIONS.map { it }.toSet() } catch (e: Exception) { - Log.e(TAG, "Error getting required permissions", e) + AppLogger.e(TAG, e) { "Error getting required permissions" } emptySet() } } @SuppressLint("RestrictedApi") suspend fun syncCompletedSession( - session: ClimbSession, - gymName: String, - attemptCount: Int = 0 + session: ClimbSession, + gymName: String, + attemptCount: Int = 0 ): Result { return try { if (!isReady() || !_autoSync.value) { return Result.failure( - IllegalStateException("Health Connect not ready or auto-sync disabled") + IllegalStateException("Health Connect not ready or auto-sync disabled") ) } if (session.status != SessionStatus.COMPLETED) { return Result.failure( - IllegalArgumentException("Only completed sessions can be synced") + IllegalArgumentException("Only completed sessions can be synced") ) } @@ -177,29 +177,29 @@ class HealthConnectManager(private val context: Context) { if (startTime == null || endTime == null) { return Result.failure( - IllegalArgumentException("Session must have valid start and end times") + IllegalArgumentException("Session must have valid start and end times") ) } - Log.d(TAG, "Attempting to sync session '${session.id}' to Health Connect...") + AppLogger.d(TAG) { "Attempting to sync session '${session.id}' to Health Connect..." } val records = mutableListOf() try { val exerciseSession = - ExerciseSessionRecord( - startTime = startTime, - startZoneOffset = - ZoneOffset.systemDefault().rules.getOffset(startTime), - endTime = endTime, - endZoneOffset = ZoneOffset.systemDefault().rules.getOffset(endTime), - exerciseType = - ExerciseSessionRecord.EXERCISE_TYPE_STRENGTH_TRAINING, - title = "Rock Climbing at $gymName" - ) + ExerciseSessionRecord( + startTime = startTime, + startZoneOffset = + ZoneOffset.systemDefault().rules.getOffset(startTime), + endTime = endTime, + endZoneOffset = ZoneOffset.systemDefault().rules.getOffset(endTime), + exerciseType = + ExerciseSessionRecord.EXERCISE_TYPE_STRENGTH_TRAINING, + title = "Rock Climbing at $gymName" + ) records.add(exerciseSession) } catch (e: Exception) { - Log.w(TAG, "Failed to create exercise session record", e) + AppLogger.w(TAG, e) { "Failed to create exercise session record" } } try { @@ -208,75 +208,74 @@ class HealthConnectManager(private val context: Context) { if (estimatedCalories > 0) { val caloriesRecord = - TotalCaloriesBurnedRecord( - startTime = startTime, - startZoneOffset = - ZoneOffset.systemDefault().rules.getOffset(startTime), - endTime = endTime, - endZoneOffset = - ZoneOffset.systemDefault().rules.getOffset(endTime), - energy = Energy.calories(estimatedCalories) - ) + TotalCaloriesBurnedRecord( + startTime = startTime, + startZoneOffset = + ZoneOffset.systemDefault().rules.getOffset(startTime), + endTime = endTime, + endZoneOffset = + ZoneOffset.systemDefault().rules.getOffset(endTime), + energy = Energy.calories(estimatedCalories) + ) records.add(caloriesRecord) } } catch (e: Exception) { - Log.w(TAG, "Failed to create calories record", e) + AppLogger.w(TAG, e) { "Failed to create calories record" } } try { val heartRateRecord = createHeartRateRecord(startTime, endTime, attemptCount) heartRateRecord?.let { records.add(it) } } catch (e: Exception) { - Log.w(TAG, "Failed to create heart rate record", e) + AppLogger.w(TAG, e) { "Failed to create heart rate record" } } if (records.isNotEmpty() && healthConnectClient != null) { - Log.d(TAG, "Writing ${records.size} records to Health Connect...") + AppLogger.d(TAG) { "Writing ${records.size} records to Health Connect..." } healthConnectClient!!.insertRecords(records) - Log.i( - TAG, - "Successfully synced ${records.size} records for session '${session.id}' to Health Connect" - ) + AppLogger.i(TAG) { + "Successfully synced ${records.size} records for session '${session.id}' to Health Connect" + } preferences - .edit() - .putString("last_sync_success", DateFormatUtils.nowISO8601()) - .apply() + .edit() + .putString("last_sync_success", DateFormatUtils.nowISO8601()) + .apply() } else { val reason = - when { - records.isEmpty() -> "No records created" - healthConnectClient == null -> "Health Connect client unavailable" - else -> "Unknown reason" - } - Log.w(TAG, "Sync failed for session '${session.id}': $reason") + when { + records.isEmpty() -> "No records created" + healthConnectClient == null -> "Health Connect client unavailable" + else -> "Unknown reason" + } + AppLogger.w(TAG) { "Sync failed for session '${session.id}': $reason" } return Result.failure(Exception("Sync failed: $reason")) } Result.success(Unit) } catch (e: Exception) { - Log.e(TAG, "Error syncing climbing session to Health Connect", e) + AppLogger.e(TAG, e) { "Error syncing climbing session to Health Connect" } Result.failure(e) } } suspend fun autoSyncCompletedSession( - session: ClimbSession, - gymName: String, - attemptCount: Int = 0 + session: ClimbSession, + gymName: String, + attemptCount: Int = 0 ): Result { return if (_autoSync.value && isReady() && session.status == SessionStatus.COMPLETED) { - Log.d(TAG, "Auto-syncing completed session '${session.id}' to Health Connect...") + AppLogger.d(TAG) { "Auto-syncing completed session '${session.id}' to Health Connect..." } syncCompletedSession(session, gymName, attemptCount) } else { val reason = - when { - session.status != SessionStatus.COMPLETED -> "session not completed" - !_autoSync.value -> "auto-sync disabled" - !isReady() -> "Health Connect not ready" - else -> "unknown reason" - } - Log.d(TAG, "Auto-sync skipped for session '${session.id}': $reason") + when { + session.status != SessionStatus.COMPLETED -> "session not completed" + !_autoSync.value -> "auto-sync disabled" + !isReady() -> "Health Connect not ready" + else -> "unknown reason" + } + AppLogger.d(TAG) { "Auto-sync skipped for session '${session.id}': $reason" } Result.success(Unit) } } @@ -284,30 +283,30 @@ class HealthConnectManager(private val context: Context) { private fun estimateCaloriesForClimbing(durationMinutes: Long, attemptCount: Int): Double { val baseCaloriesPerMinute = 8.0 val intensityMultiplier = - when { - attemptCount >= 20 -> 1.3 - attemptCount >= 10 -> 1.1 - else -> 0.9 - } + when { + attemptCount >= 20 -> 1.3 + attemptCount >= 10 -> 1.1 + else -> 0.9 + } return durationMinutes * baseCaloriesPerMinute * intensityMultiplier } @SuppressLint("RestrictedApi") private fun createHeartRateRecord( - startTime: Instant, - endTime: Instant, - attemptCount: Int + startTime: Instant, + endTime: Instant, + attemptCount: Int ): HeartRateRecord? { return try { val samples = mutableListOf() val intervalMinutes = 5L val baseHeartRate = - when { - attemptCount >= 20 -> 155L - attemptCount >= 10 -> 145L - else -> 135L - } + when { + attemptCount >= 20 -> 155L + attemptCount >= 10 -> 145L + else -> 135L + } var currentTime = startTime while (currentTime.isBefore(endTime)) { @@ -321,14 +320,14 @@ class HealthConnectManager(private val context: Context) { if (samples.isEmpty()) return null HeartRateRecord( - startTime = startTime, - startZoneOffset = ZoneOffset.systemDefault().rules.getOffset(startTime), - endTime = endTime, - endZoneOffset = ZoneOffset.systemDefault().rules.getOffset(endTime), - samples = samples + startTime = startTime, + startZoneOffset = ZoneOffset.systemDefault().rules.getOffset(startTime), + endTime = endTime, + endZoneOffset = ZoneOffset.systemDefault().rules.getOffset(endTime), + samples = samples ) } catch (e: Exception) { - Log.e(TAG, "Error creating heart rate record", e) + AppLogger.e(TAG, e) { "Error creating heart rate record" } null } } diff --git a/android/app/src/main/java/com/atridad/ascently/data/repository/ClimbRepository.kt b/android/app/src/main/java/com/atridad/ascently/data/repository/ClimbRepository.kt index 7668c36..5d40f92 100644 --- a/android/app/src/main/java/com/atridad/ascently/data/repository/ClimbRepository.kt +++ b/android/app/src/main/java/com/atridad/ascently/data/repository/ClimbRepository.kt @@ -13,6 +13,7 @@ import com.atridad.ascently.data.format.DeletedItem import com.atridad.ascently.data.model.* import com.atridad.ascently.data.state.DataStateManager import com.atridad.ascently.utils.DateFormatUtils +import com.atridad.ascently.utils.AppLogger import com.atridad.ascently.utils.ZipExportImportUtils import java.io.File import kotlinx.coroutines.flow.Flow @@ -26,7 +27,7 @@ class ClimbRepository(database: AscentlyDatabase, private val context: Context) private val attemptDao = database.attemptDao() private val dataStateManager = DataStateManager(context) private val deletionPreferences: SharedPreferences = - context.getSharedPreferences("deleted_items", Context.MODE_PRIVATE) + context.getSharedPreferences("deleted_items", Context.MODE_PRIVATE) private var autoSyncCallback: (() -> Unit)? = null @@ -43,11 +44,13 @@ class ClimbRepository(database: AscentlyDatabase, private val context: Context) dataStateManager.updateDataState() triggerAutoSync() } + suspend fun updateGym(gym: Gym) { gymDao.updateGym(gym) dataStateManager.updateDataState() triggerAutoSync() } + suspend fun deleteGym(gym: Gym) { gymDao.deleteGym(gym) trackDeletion(gym.id, "gym") @@ -63,10 +66,12 @@ class ClimbRepository(database: AscentlyDatabase, private val context: Context) problemDao.insertProblem(problem) dataStateManager.updateDataState() } + suspend fun updateProblem(problem: Problem) { problemDao.updateProblem(problem) dataStateManager.updateDataState() } + suspend fun deleteProblem(problem: Problem) { problemDao.deleteProblem(problem) trackDeletion(problem.id, "problem") @@ -77,7 +82,8 @@ class ClimbRepository(database: AscentlyDatabase, private val context: Context) fun getAllSessions(): Flow> = sessionDao.getAllSessions() suspend fun getSessionById(id: String): ClimbSession? = sessionDao.getSessionById(id) fun getSessionsByGym(gymId: String): Flow> = - sessionDao.getSessionsByGym(gymId) + sessionDao.getSessionsByGym(gymId) + suspend fun getActiveSession(): ClimbSession? = sessionDao.getActiveSession() fun getActiveSessionFlow(): Flow = sessionDao.getActiveSessionFlow() suspend fun insertSession(session: ClimbSession) { @@ -88,6 +94,7 @@ class ClimbRepository(database: AscentlyDatabase, private val context: Context) triggerAutoSync() } } + suspend fun updateSession(session: ClimbSession) { sessionDao.updateSession(session) dataStateManager.updateDataState() @@ -96,12 +103,14 @@ class ClimbRepository(database: AscentlyDatabase, private val context: Context) triggerAutoSync() } } + suspend fun deleteSession(session: ClimbSession) { sessionDao.deleteSession(session) trackDeletion(session.id, "session") dataStateManager.updateDataState() triggerAutoSync() } + suspend fun getLastUsedGym(): Gym? { val recentSessions = sessionDao.getRecentSessions(1).first() return if (recentSessions.isNotEmpty()) { @@ -114,17 +123,21 @@ class ClimbRepository(database: AscentlyDatabase, private val context: Context) // Attempt operations fun getAllAttempts(): Flow> = attemptDao.getAllAttempts() fun getAttemptsBySession(sessionId: String): Flow> = - attemptDao.getAttemptsBySession(sessionId) + attemptDao.getAttemptsBySession(sessionId) + fun getAttemptsByProblem(problemId: String): Flow> = - attemptDao.getAttemptsByProblem(problemId) + attemptDao.getAttemptsByProblem(problemId) + suspend fun insertAttempt(attempt: Attempt) { attemptDao.insertAttempt(attempt) dataStateManager.updateDataState() } + suspend fun updateAttempt(attempt: Attempt) { attemptDao.updateAttempt(attempt) dataStateManager.updateDataState() } + suspend fun deleteAttempt(attempt: Attempt) { attemptDao.deleteAttempt(attempt) trackDeletion(attempt.id, "attempt") @@ -141,38 +154,38 @@ class ClimbRepository(database: AscentlyDatabase, private val context: Context) validateDataIntegrity(allGyms, allProblems, allSessions, allAttempts) val backupData = - ClimbDataBackup( - exportedAt = DateFormatUtils.nowISO8601(), - 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) } - ) + ClimbDataBackup( + exportedAt = DateFormatUtils.nowISO8601(), + 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) } + ) val referencedImagePaths = allProblems.flatMap { it.imagePaths }.toSet() val validImagePaths = - referencedImagePaths - .filter { imagePath -> - try { - val imageFile = - com.atridad.ascently.utils.ImageUtils.getImageFile( - context, - imagePath - ) - imageFile.exists() && imageFile.length() > 0 - } catch (_: Exception) { - false - } - } - .toSet() + referencedImagePaths + .filter { imagePath -> + try { + val imageFile = + com.atridad.ascently.utils.ImageUtils.getImageFile( + context, + imagePath + ) + imageFile.exists() && imageFile.length() > 0 + } catch (_: Exception) { + false + } + } + .toSet() ZipExportImportUtils.createExportZipToUri( - context = context, - uri = uri, - exportData = backupData, - referencedImagePaths = validImagePaths + context = context, + uri = uri, + exportData = backupData, + referencedImagePaths = validImagePaths ) } catch (e: Exception) { throw Exception("Export failed: ${e.message}") @@ -192,11 +205,11 @@ class ClimbRepository(database: AscentlyDatabase, private val context: Context) } val importData = - try { - json.decodeFromString(importResult.jsonContent) - } catch (e: Exception) { - throw Exception("Invalid data format: ${e.message}") - } + try { + json.decodeFromString(importResult.jsonContent) + } catch (e: Exception) { + throw Exception("Invalid data format: ${e.message}") + } validateImportData(importData) @@ -214,17 +227,17 @@ class ClimbRepository(database: AscentlyDatabase, private val context: Context) } val updatedBackupProblems = - ZipExportImportUtils.updateProblemImagePaths( - importData.problems, - importResult.importedImagePaths - ) + ZipExportImportUtils.updateProblemImagePaths( + importData.problems, + importResult.importedImagePaths + ) updatedBackupProblems.forEach { backupProblem -> try { problemDao.insertProblem(backupProblem.toProblem()) } catch (e: Exception) { throw Exception( - "Failed to import problem '${backupProblem.name}': ${e.message}" + "Failed to import problem '${backupProblem.name}': ${e.message}" ) } } @@ -262,7 +275,7 @@ class ClimbRepository(database: AscentlyDatabase, private val context: Context) fun trackDeletion(itemId: String, itemType: String) { val currentDeletions = getDeletedItems().toMutableList() val newDeletion = - DeletedItem(id = itemId, type = itemType, deletedAt = DateFormatUtils.nowISO8601()) + DeletedItem(id = itemId, type = itemType, deletedAt = DateFormatUtils.nowISO8601()) currentDeletions.add(newDeletion) val json = json.encodeToString(newDeletion) @@ -292,23 +305,23 @@ class ClimbRepository(database: AscentlyDatabase, private val context: Context) } private fun validateDataIntegrity( - gyms: List, - problems: List, - sessions: List, - attempts: List + gyms: List, + problems: List, + sessions: List, + attempts: List ) { val gymIds = gyms.map { it.id }.toSet() val invalidProblems = problems.filter { it.gymId !in gymIds } if (invalidProblems.isNotEmpty()) { throw Exception( - "Data integrity error: ${invalidProblems.size} problems reference non-existent gyms" + "Data integrity error: ${invalidProblems.size} problems reference non-existent gyms" ) } val invalidSessions = sessions.filter { it.gymId !in gymIds } if (invalidSessions.isNotEmpty()) { throw Exception( - "Data integrity error: ${invalidSessions.size} sessions reference non-existent gyms" + "Data integrity error: ${invalidSessions.size} sessions reference non-existent gyms" ) } @@ -316,10 +329,10 @@ class ClimbRepository(database: AscentlyDatabase, private val context: Context) val sessionIds = sessions.map { it.id }.toSet() val invalidAttempts = - attempts.filter { it.problemId !in problemIds || it.sessionId !in sessionIds } + attempts.filter { it.problemId !in problemIds || it.sessionId !in sessionIds } if (invalidAttempts.isNotEmpty()) { throw Exception( - "Data integrity error: ${invalidAttempts.size} attempts reference non-existent problems or sessions" + "Data integrity error: ${invalidAttempts.size} attempts reference non-existent problems or sessions" ) } } @@ -334,9 +347,9 @@ class ClimbRepository(database: AscentlyDatabase, private val context: Context) } if (importData.gyms.size > 1000 || - importData.problems.size > 10000 || - importData.sessions.size > 10000 || - importData.attempts.size > 100000 + importData.problems.size > 10000 || + importData.sessions.size > 10000 || + importData.attempts.size > 100000 ) { throw Exception("Import data is too large: possible corruption or malicious file") } @@ -386,10 +399,10 @@ class ClimbRepository(database: AscentlyDatabase, private val context: Context) if (imagesDir.exists() && imagesDir.isDirectory) { val deletedCount = imagesDir.listFiles()?.size ?: 0 imagesDir.deleteRecursively() - android.util.Log.i("ClimbRepository", "Cleared $deletedCount image files") + AppLogger.i("ClimbRepository") { "Cleared $deletedCount image files" } } } catch (e: Exception) { - android.util.Log.w("ClimbRepository", "Failed to clear some images: ${e.message}") + AppLogger.w("ClimbRepository", e) { "Failed to clear some images: ${e.message}" } } } } diff --git a/android/app/src/main/java/com/atridad/ascently/data/state/DataStateManager.kt b/android/app/src/main/java/com/atridad/ascently/data/state/DataStateManager.kt index 32ae8fb..bb573a1 100644 --- a/android/app/src/main/java/com/atridad/ascently/data/state/DataStateManager.kt +++ b/android/app/src/main/java/com/atridad/ascently/data/state/DataStateManager.kt @@ -2,8 +2,8 @@ package com.atridad.ascently.data.state import android.content.Context import android.content.SharedPreferences -import android.util.Log import androidx.core.content.edit +import com.atridad.ascently.utils.AppLogger import com.atridad.ascently.utils.DateFormatUtils /** @@ -20,13 +20,13 @@ class DataStateManager(context: Context) { } private val prefs: SharedPreferences = - context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE) + context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE) init { if (!isInitialized()) { updateDataState() markAsInitialized() - Log.d(TAG, "DataStateManager initialized with timestamp: ${getLastModified()}") + AppLogger.d(TAG) { "DataStateManager initialized with timestamp: ${getLastModified()}" } } } @@ -37,7 +37,7 @@ class DataStateManager(context: Context) { fun updateDataState() { val now = DateFormatUtils.nowISO8601() prefs.edit { putString(KEY_LAST_MODIFIED, now) } - Log.d(TAG, "Data state updated to: $now") + AppLogger.d(TAG) { "Data state updated to: $now" } } /** @@ -46,7 +46,7 @@ class DataStateManager(context: Context) { */ fun getLastModified(): String { return prefs.getString(KEY_LAST_MODIFIED, DateFormatUtils.nowISO8601()) - ?: DateFormatUtils.nowISO8601() + ?: DateFormatUtils.nowISO8601() } /** Checks if the data state has been initialized. */ diff --git a/android/app/src/main/java/com/atridad/ascently/data/sync/SyncService.kt b/android/app/src/main/java/com/atridad/ascently/data/sync/SyncService.kt index 21121af..c4be4f0 100644 --- a/android/app/src/main/java/com/atridad/ascently/data/sync/SyncService.kt +++ b/android/app/src/main/java/com/atridad/ascently/data/sync/SyncService.kt @@ -4,10 +4,10 @@ import android.content.Context import android.content.SharedPreferences import android.net.ConnectivityManager import android.net.NetworkCapabilities -import android.util.Log import androidx.annotation.RequiresPermission import androidx.core.content.edit import com.atridad.ascently.data.format.BackupAttempt +import com.atridad.ascently.utils.AppLogger import com.atridad.ascently.data.format.BackupClimbSession import com.atridad.ascently.data.format.BackupGym import com.atridad.ascently.data.format.BackupProblem @@ -53,14 +53,14 @@ class SyncService(private val context: Context, private val repository: ClimbRep } private val sharedPreferences: SharedPreferences = - context.getSharedPreferences("sync_preferences", Context.MODE_PRIVATE) + context.getSharedPreferences("sync_preferences", Context.MODE_PRIVATE) private val httpClient = - OkHttpClient.Builder() - .connectTimeout(45, TimeUnit.SECONDS) - .readTimeout(90, TimeUnit.SECONDS) - .writeTimeout(90, TimeUnit.SECONDS) - .build() + OkHttpClient.Builder() + .connectTimeout(45, TimeUnit.SECONDS) + .readTimeout(90, TimeUnit.SECONDS) + .writeTimeout(90, TimeUnit.SECONDS) + .build() private val json = Json { prettyPrint = true @@ -151,7 +151,7 @@ class SyncService(private val context: Context, private val repository: ClimbRep @RequiresPermission(android.Manifest.permission.ACCESS_NETWORK_STATE) private fun isNetworkAvailable(): Boolean { val connectivityManager = - context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager + context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager val network = connectivityManager.activeNetwork ?: return false val activeNetwork = connectivityManager.getNetworkCapabilities(network) ?: return false return when { @@ -164,12 +164,12 @@ class SyncService(private val context: Context, private val repository: ClimbRep @RequiresPermission(android.Manifest.permission.ACCESS_NETWORK_STATE) suspend fun syncWithServer() { if (isOfflineMode) { - Log.d(TAG, "Sync skipped: Offline mode is enabled.") + AppLogger.d(TAG) { "Sync skipped: Offline mode is enabled." } return } if (!isNetworkAvailable()) { _syncError.value = "No internet connection." - Log.d(TAG, "Sync skipped: No internet connection.") + AppLogger.d(TAG) { "Sync skipped: No internet connection." } return } if (!_isConfigured.value) { @@ -188,43 +188,46 @@ class SyncService(private val context: Context, private val repository: ClimbRep val serverBackup = downloadData() val hasLocalData = - localBackup.gyms.isNotEmpty() || - localBackup.problems.isNotEmpty() || - localBackup.sessions.isNotEmpty() || - localBackup.attempts.isNotEmpty() + localBackup.gyms.isNotEmpty() || + localBackup.problems.isNotEmpty() || + localBackup.sessions.isNotEmpty() || + localBackup.attempts.isNotEmpty() val hasServerData = - serverBackup.gyms.isNotEmpty() || - serverBackup.problems.isNotEmpty() || - serverBackup.sessions.isNotEmpty() || - serverBackup.attempts.isNotEmpty() + serverBackup.gyms.isNotEmpty() || + serverBackup.problems.isNotEmpty() || + serverBackup.sessions.isNotEmpty() || + serverBackup.attempts.isNotEmpty() // If both client and server have been synced before, use delta sync val lastSyncTimeStr = sharedPreferences.getString(Keys.LAST_SYNC_TIME, null) if (hasLocalData && hasServerData && lastSyncTimeStr != null) { - Log.d(TAG, "Using delta sync for incremental updates") + AppLogger.d(TAG) { "Using delta sync for incremental updates" } performDeltaSync(lastSyncTimeStr) } else { when { !hasLocalData && hasServerData -> { - Log.d(TAG, "No local data found, performing full restore from server") + AppLogger.d(TAG) { "No local data found, performing full restore from server" } val imagePathMapping = syncImagesFromServer(serverBackup) importBackupToRepository(serverBackup, imagePathMapping) - Log.d(TAG, "Full restore completed") + AppLogger.d(TAG) { "Full restore completed" } } + hasLocalData && !hasServerData -> { - Log.d(TAG, "No server data found, uploading local data to server") + AppLogger.d(TAG) { "No server data found, uploading local data to server" } uploadData(localBackup) syncImagesForBackup(localBackup) - Log.d(TAG, "Initial upload completed") + AppLogger.d(TAG) { "Initial upload completed" } } + hasLocalData && hasServerData -> { - Log.d(TAG, "Both local and server data exist, merging (server wins)") + AppLogger.d(TAG) { "Both local and server data exist, merging (server wins)" } mergeDataSafely(serverBackup) - Log.d(TAG, "Merge completed") + AppLogger.d(TAG) { "Merge completed" } } + else -> { - Log.d(TAG, "No data to sync") + AppLogger.d(TAG) { "No data to sync" } } } } @@ -242,7 +245,7 @@ class SyncService(private val context: Context, private val repository: ClimbRep } private suspend fun performDeltaSync(lastSyncTimeStr: String) { - Log.d(TAG, "Starting delta sync with lastSyncTime=$lastSyncTimeStr") + AppLogger.d(TAG) { "Starting delta sync with lastSyncTime=$lastSyncTimeStr" } // Parse last sync time to filter modified items val lastSyncDate = parseISO8601(lastSyncTimeStr) ?: Date(0) @@ -250,102 +253,100 @@ class SyncService(private val context: Context, private val repository: ClimbRep // Collect items modified since last sync val allGyms = repository.getAllGyms().first() val modifiedGyms = - allGyms - .filter { gym -> parseISO8601(gym.updatedAt)?.after(lastSyncDate) == true } - .map { BackupGym.fromGym(it) } + allGyms + .filter { gym -> parseISO8601(gym.updatedAt)?.after(lastSyncDate) == true } + .map { BackupGym.fromGym(it) } val allProblems = repository.getAllProblems().first() val modifiedProblems = - allProblems - .filter { problem -> - parseISO8601(problem.updatedAt)?.after(lastSyncDate) == true - } - .map { problem -> - val backupProblem = BackupProblem.fromProblem(problem) - val normalizedImagePaths = - problem.imagePaths.mapIndexed { index, _ -> - ImageNamingUtils.generateImageFilename(problem.id, index) - } - if (normalizedImagePaths.isNotEmpty()) { - backupProblem.copy(imagePaths = normalizedImagePaths) - } else { - backupProblem - } - } - - val allSessions = repository.getAllSessions().first() - val modifiedSessions = - allSessions - .filter { session -> - parseISO8601(session.updatedAt)?.after(lastSyncDate) == true - } - .map { BackupClimbSession.fromClimbSession(it) } - - val allAttempts = repository.getAllAttempts().first() - val modifiedAttempts = - allAttempts - .filter { attempt -> - parseISO8601(attempt.createdAt)?.after(lastSyncDate) == true - } - .map { BackupAttempt.fromAttempt(it) } - - val allDeletions = repository.getDeletedItems() - val modifiedDeletions = - allDeletions.filter { item -> - parseISO8601(item.deletedAt)?.after(lastSyncDate) == true + allProblems + .filter { problem -> + parseISO8601(problem.updatedAt)?.after(lastSyncDate) == true } - - Log.d( - TAG, - "Delta sync sending: gyms=${modifiedGyms.size}, problems=${modifiedProblems.size}, sessions=${modifiedSessions.size}, attempts=${modifiedAttempts.size}, deletions=${modifiedDeletions.size}" - ) - - // Create delta request - val deltaRequest = - DeltaSyncRequest( - lastSyncTime = lastSyncTimeStr, - gyms = modifiedGyms, - problems = modifiedProblems, - sessions = modifiedSessions, - attempts = modifiedAttempts, - deletedItems = modifiedDeletions - ) - - val requestBody = - json.encodeToString(DeltaSyncRequest.serializer(), deltaRequest) - .toRequestBody("application/json".toMediaType()) - - val request = - Request.Builder() - .url("$serverUrl/sync/delta") - .header("Authorization", "Bearer $authToken") - .post(requestBody) - .build() - - val deltaResponse = - withContext(Dispatchers.IO) { - try { - httpClient.newCall(request).execute().use { response -> - if (response.isSuccessful) { - val body = response.body?.string() - if (!body.isNullOrEmpty()) { - json.decodeFromString(DeltaSyncResponse.serializer(), body) - } else { - throw SyncException.InvalidResponse("Empty response body") - } - } else { - handleHttpError(response.code) - } + .map { problem -> + val backupProblem = BackupProblem.fromProblem(problem) + val normalizedImagePaths = + problem.imagePaths.mapIndexed { index, _ -> + ImageNamingUtils.generateImageFilename(problem.id, index) } - } catch (e: IOException) { - throw SyncException.NetworkError(e.message ?: "Network error") + if (normalizedImagePaths.isNotEmpty()) { + backupProblem.copy(imagePaths = normalizedImagePaths) + } else { + backupProblem } } - Log.d( - TAG, - "Delta sync received: gyms=${deltaResponse.gyms.size}, problems=${deltaResponse.problems.size}, sessions=${deltaResponse.sessions.size}, attempts=${deltaResponse.attempts.size}, deletions=${deltaResponse.deletedItems.size}" - ) + val allSessions = repository.getAllSessions().first() + val modifiedSessions = + allSessions + .filter { session -> + parseISO8601(session.updatedAt)?.after(lastSyncDate) == true + } + .map { BackupClimbSession.fromClimbSession(it) } + + val allAttempts = repository.getAllAttempts().first() + val modifiedAttempts = + allAttempts + .filter { attempt -> + parseISO8601(attempt.createdAt)?.after(lastSyncDate) == true + } + .map { BackupAttempt.fromAttempt(it) } + + val allDeletions = repository.getDeletedItems() + val modifiedDeletions = + allDeletions.filter { item -> + parseISO8601(item.deletedAt)?.after(lastSyncDate) == true + } + + AppLogger.d(TAG) { + "Delta sync sending: gyms=${modifiedGyms.size}, problems=${modifiedProblems.size}, sessions=${modifiedSessions.size}, attempts=${modifiedAttempts.size}, deletions=${modifiedDeletions.size}" + } + + // Create delta request + val deltaRequest = + DeltaSyncRequest( + lastSyncTime = lastSyncTimeStr, + gyms = modifiedGyms, + problems = modifiedProblems, + sessions = modifiedSessions, + attempts = modifiedAttempts, + deletedItems = modifiedDeletions + ) + + val requestBody = + json.encodeToString(DeltaSyncRequest.serializer(), deltaRequest) + .toRequestBody("application/json".toMediaType()) + + val request = + Request.Builder() + .url("$serverUrl/sync/delta") + .header("Authorization", "Bearer $authToken") + .post(requestBody) + .build() + + val deltaResponse = + withContext(Dispatchers.IO) { + try { + httpClient.newCall(request).execute().use { response -> + if (response.isSuccessful) { + val body = response.body?.string() + if (!body.isNullOrEmpty()) { + json.decodeFromString(DeltaSyncResponse.serializer(), body) + } else { + throw SyncException.InvalidResponse("Empty response body") + } + } else { + handleHttpError(response.code) + } + } + } catch (e: IOException) { + throw SyncException.NetworkError(e.message ?: "Network error") + } + } + + AppLogger.d(TAG) { + "Delta sync received: gyms=${deltaResponse.gyms.size}, problems=${deltaResponse.problems.size}, sessions=${deltaResponse.sessions.size}, attempts=${deltaResponse.attempts.size}, deletions=${deltaResponse.deletedItems.size}" + } // Apply server changes to local data applyDeltaResponse(deltaResponse) @@ -372,7 +373,7 @@ class SyncService(private val context: Context, private val repository: ClimbRep val allDeletions = repository.getDeletedItems() + response.deletedItems val uniqueDeletions = allDeletions.distinctBy { "${it.type}:${it.id}" } - Log.d(TAG, "Applying ${uniqueDeletions.size} deletion records before merging data") + AppLogger.d(TAG) { "Applying ${uniqueDeletions.size} deletion records before merging data" } applyDeletions(uniqueDeletions) // Build deleted item lookup set @@ -392,7 +393,7 @@ class SyncService(private val context: Context, private val repository: ClimbRep imagePathMapping[imagePath] = localImagePath } } catch (e: Exception) { - Log.w(TAG, "Failed to download image $imagePath: ${e.message}") + AppLogger.w(TAG) { "Failed to download image $imagePath: ${e.message}" } } } } @@ -421,9 +422,9 @@ class SyncService(private val context: Context, private val repository: ClimbRep continue } val updatedImagePaths = - backupProblem.imagePaths?.map { oldPath -> - imagePathMapping[oldPath] ?: oldPath - } + backupProblem.imagePaths?.map { oldPath -> + imagePathMapping[oldPath] ?: oldPath + } val problemToMerge = backupProblem.copy(imagePaths = updatedImagePaths) val problem = problemToMerge.toProblem() @@ -484,7 +485,7 @@ class SyncService(private val context: Context, private val repository: ClimbRep } private suspend fun applyDeletions( - deletions: List + deletions: List ) { val existingGyms = repository.getAllGyms().first() val existingProblems = repository.getAllProblems().first() @@ -496,12 +497,15 @@ class SyncService(private val context: Context, private val repository: ClimbRep "gym" -> { existingGyms.find { it.id == item.id }?.let { repository.deleteGym(it) } } + "problem" -> { existingProblems.find { it.id == item.id }?.let { repository.deleteProblem(it) } } + "session" -> { existingSessions.find { it.id == item.id }?.let { repository.deleteSession(it) } } + "attempt" -> { existingAttempts.find { it.id == item.id }?.let { repository.deleteAttempt(it) } } @@ -512,7 +516,7 @@ class SyncService(private val context: Context, private val repository: ClimbRep private suspend fun syncModifiedImages(modifiedProblems: List) { if (modifiedProblems.isEmpty()) return - Log.d(TAG, "Syncing images for ${modifiedProblems.size} modified problems") + AppLogger.d(TAG) { "Syncing images for ${modifiedProblems.size} modified problems" } for (backupProblem in modifiedProblems) { backupProblem.imagePaths?.forEach { imagePath -> @@ -524,11 +528,11 @@ class SyncService(private val context: Context, private val repository: ClimbRep private suspend fun downloadData(): ClimbDataBackup { val request = - Request.Builder() - .url("$serverUrl/sync") - .header("Authorization", "Bearer $authToken") - .get() - .build() + Request.Builder() + .url("$serverUrl/sync") + .header("Authorization", "Bearer $authToken") + .get() + .build() return withContext(Dispatchers.IO) { try { @@ -539,11 +543,11 @@ class SyncService(private val context: Context, private val repository: ClimbRep json.decodeFromString(body) } else { ClimbDataBackup( - exportedAt = DateFormatUtils.nowISO8601(), - gyms = emptyList(), - problems = emptyList(), - sessions = emptyList(), - attempts = emptyList() + exportedAt = DateFormatUtils.nowISO8601(), + gyms = emptyList(), + problems = emptyList(), + sessions = emptyList(), + attempts = emptyList() ) } } else { @@ -558,14 +562,14 @@ class SyncService(private val context: Context, private val repository: ClimbRep private suspend fun uploadData(backup: ClimbDataBackup) { val requestBody = - json.encodeToString(backup).toRequestBody("application/json".toMediaType()) + json.encodeToString(backup).toRequestBody("application/json".toMediaType()) val request = - Request.Builder() - .url("$serverUrl/sync") - .header("Authorization", "Bearer $authToken") - .put(requestBody) - .build() + Request.Builder() + .url("$serverUrl/sync") + .header("Authorization", "Bearer $authToken") + .put(requestBody) + .build() withContext(Dispatchers.IO) { try { @@ -583,7 +587,7 @@ class SyncService(private val context: Context, private val repository: ClimbRep private suspend fun syncImagesFromServer(backup: ClimbDataBackup): Map { val imagePathMapping = mutableMapOf() val totalImages = backup.problems.sumOf { it.imagePaths?.size ?: 0 } - Log.d(TAG, "Starting image download from server for $totalImages images") + AppLogger.d(TAG) { "Starting image download from server for $totalImages images" } withContext(Dispatchers.IO) { backup.problems.forEach { problem -> @@ -595,9 +599,9 @@ class SyncService(private val context: Context, private val repository: ClimbRep imagePathMapping[imagePath] = localImagePath } } catch (_: SyncException.ImageNotFound) { - Log.w(TAG, "Image not found on server: $imagePath") + AppLogger.w(TAG) { "Image not found on server: $imagePath" } } catch (e: Exception) { - Log.w(TAG, "Failed to download image $imagePath: ${e.message}") + AppLogger.w(TAG) { "Failed to download image $imagePath: ${e.message}" } } } } @@ -607,10 +611,10 @@ class SyncService(private val context: Context, private val repository: ClimbRep private suspend fun downloadImage(serverFilename: String): String? { val request = - Request.Builder() - .url("$serverUrl/images/download?filename=$serverFilename") - .header("Authorization", "Bearer $authToken") - .build() + Request.Builder() + .url("$serverUrl/images/download?filename=$serverFilename") + .header("Authorization", "Bearer $authToken") + .build() return withContext(Dispatchers.IO) { try { @@ -625,14 +629,14 @@ class SyncService(private val context: Context, private val repository: ClimbRep } } } catch (e: IOException) { - Log.e(TAG, "Network error downloading image $serverFilename", e) + AppLogger.e(TAG, e) { "Network error downloading image $serverFilename" } null } } } private suspend fun syncImagesForBackup(backup: ClimbDataBackup) { - Log.d(TAG, "Starting image sync for backup with ${backup.problems.size} problems") + AppLogger.d(TAG) { "Starting image sync for backup with ${backup.problems.size} problems" } withContext(Dispatchers.IO) { backup.problems.forEach { problem -> problem.imagePaths?.forEach { localPath -> @@ -646,33 +650,32 @@ class SyncService(private val context: Context, private val repository: ClimbRep private suspend fun uploadImage(localPath: String, filename: String) { val file = ImageUtils.getImageFile(context, localPath) if (!file.exists()) { - Log.w(TAG, "Local image file not found, cannot upload: $localPath") + AppLogger.w(TAG) { "Local image file not found, cannot upload: $localPath" } return } val requestBody = file.readBytes().toRequestBody("application/octet-stream".toMediaType()) val request = - Request.Builder() - .url("$serverUrl/images/upload?filename=$filename") - .header("Authorization", "Bearer $authToken") - .post(requestBody) - .build() + Request.Builder() + .url("$serverUrl/images/upload?filename=$filename") + .header("Authorization", "Bearer $authToken") + .post(requestBody) + .build() withContext(Dispatchers.IO) { try { httpClient.newCall(request).execute().use { response -> if (response.isSuccessful) { - Log.d(TAG, "Successfully uploaded image: $filename") + AppLogger.d(TAG) { "Successfully uploaded image: $filename" } } else { - Log.w( - TAG, - "Failed to upload image $filename. Server responded with ${response.code}" - ) + AppLogger.w(TAG) { + "Failed to upload image $filename. Server responded with ${response.code}" + } } } } catch (e: IOException) { - Log.e(TAG, "Network error uploading image $filename", e) + AppLogger.e(TAG, e) { "Network error uploading image $filename" } } } } @@ -680,49 +683,49 @@ class SyncService(private val context: Context, private val repository: ClimbRep private suspend fun createBackupFromRepository(): ClimbDataBackup { return withContext(Dispatchers.Default) { ClimbDataBackup( - exportedAt = dataStateManager.getLastModified(), - gyms = repository.getAllGyms().first().map { BackupGym.fromGym(it) }, - problems = - repository.getAllProblems().first().map { problem -> - val backupProblem = BackupProblem.fromProblem(problem) - val normalizedImagePaths = - problem.imagePaths.mapIndexed { index, _ -> - ImageNamingUtils.generateImageFilename( - problem.id, - index - ) - } - if (normalizedImagePaths.isNotEmpty()) { - backupProblem.copy(imagePaths = normalizedImagePaths) - } else { - backupProblem - } - }, - sessions = - repository.getAllSessions().first().map { - BackupClimbSession.fromClimbSession(it) - }, - attempts = - repository.getAllAttempts().first().map { - BackupAttempt.fromAttempt(it) - }, - deletedItems = repository.getDeletedItems() + exportedAt = dataStateManager.getLastModified(), + gyms = repository.getAllGyms().first().map { BackupGym.fromGym(it) }, + problems = + repository.getAllProblems().first().map { problem -> + val backupProblem = BackupProblem.fromProblem(problem) + val normalizedImagePaths = + problem.imagePaths.mapIndexed { index, _ -> + ImageNamingUtils.generateImageFilename( + problem.id, + index + ) + } + if (normalizedImagePaths.isNotEmpty()) { + backupProblem.copy(imagePaths = normalizedImagePaths) + } else { + backupProblem + } + }, + sessions = + repository.getAllSessions().first().map { + BackupClimbSession.fromClimbSession(it) + }, + attempts = + repository.getAllAttempts().first().map { + BackupAttempt.fromAttempt(it) + }, + deletedItems = repository.getDeletedItems() ) } } private suspend fun importBackupToRepository( - backup: ClimbDataBackup, - imagePathMapping: Map + backup: ClimbDataBackup, + imagePathMapping: Map ) { val gyms = backup.gyms.map { it.toGym() } val problems = - backup.problems.map { backupProblem -> - val imagePaths = backupProblem.imagePaths - val updatedImagePaths = - imagePaths?.map { oldPath -> imagePathMapping[oldPath] ?: oldPath } - backupProblem.copy(imagePaths = updatedImagePaths).toProblem() - } + backup.problems.map { backupProblem -> + val imagePaths = backupProblem.imagePaths + val updatedImagePaths = + imagePaths?.map { oldPath -> imagePathMapping[oldPath] ?: oldPath } + backupProblem.copy(imagePaths = updatedImagePaths).toProblem() + } val sessions = backup.sessions.map { it.toClimbSession() } val attempts = backup.attempts.map { it.toAttempt() } @@ -737,7 +740,7 @@ class SyncService(private val context: Context, private val repository: ClimbRep } private suspend fun mergeDataSafely(serverBackup: ClimbDataBackup) { - Log.d(TAG, "Server data will overwrite local data. Performing full restore.") + AppLogger.d(TAG) { "Server data will overwrite local data. Performing full restore." } val imagePathMapping = syncImagesFromServer(serverBackup) importBackupToRepository(serverBackup, imagePathMapping) } @@ -760,11 +763,11 @@ class SyncService(private val context: Context, private val repository: ClimbRep _syncError.value = null val request = - Request.Builder() - .url("$serverUrl/sync") - .header("Authorization", "Bearer $authToken") - .head() - .build() + Request.Builder() + .url("$serverUrl/sync") + .header("Authorization", "Bearer $authToken") + .head() + .build() try { withContext(Dispatchers.IO) { httpClient.newCall(request).execute().use { response -> @@ -793,18 +796,18 @@ class SyncService(private val context: Context, private val repository: ClimbRep } syncJob?.cancel() syncJob = - serviceScope.launch { - delay(syncDebounceDelay) - try { - syncWithServer() - } catch (e: Exception) { - Log.e(TAG, "Auto-sync failed", e) - } - if (pendingChanges) { - pendingChanges = false - triggerAutoSync() - } + serviceScope.launch { + delay(syncDebounceDelay) + try { + syncWithServer() + } catch (e: Exception) { + AppLogger.e(TAG, e) { "Auto-sync failed" } } + if (pendingChanges) { + pendingChanges = false + triggerAutoSync() + } + } } fun clearConfiguration() { @@ -822,7 +825,7 @@ class SyncService(private val context: Context, private val repository: ClimbRep sealed class SyncException(message: String) : IOException(message), Serializable { object NotConfigured : - SyncException("Sync is not configured. Please set server URL and auth token.") + SyncException("Sync is not configured. Please set server URL and auth token.") object NotConnected : SyncException("Not connected to server. Please test connection first.") @@ -832,6 +835,7 @@ sealed class SyncException(message: String) : IOException(message), Serializable data class ServerError(val code: Int) : SyncException("Server error: HTTP $code") data class InvalidResponse(val details: String) : - SyncException("Invalid server response: $details") + SyncException("Invalid server response: $details") + data class NetworkError(val details: String) : SyncException("Network error: $details") } diff --git a/android/app/src/main/java/com/atridad/ascently/service/SessionTrackingService.kt b/android/app/src/main/java/com/atridad/ascently/service/SessionTrackingService.kt index 6af2e5e..77e27e3 100644 --- a/android/app/src/main/java/com/atridad/ascently/service/SessionTrackingService.kt +++ b/android/app/src/main/java/com/atridad/ascently/service/SessionTrackingService.kt @@ -12,6 +12,7 @@ import com.atridad.ascently.MainActivity import com.atridad.ascently.R import com.atridad.ascently.data.database.AscentlyDatabase import com.atridad.ascently.data.repository.ClimbRepository +import com.atridad.ascently.utils.AppLogger import com.atridad.ascently.widget.ClimbStatsWidgetProvider import java.time.LocalDateTime import java.time.temporal.ChronoUnit @@ -29,6 +30,7 @@ class SessionTrackingService : Service() { private lateinit var notificationManager: NotificationManager companion object { + private const val LOG_TAG = "SessionTrackingService" const val NOTIFICATION_ID = 1001 const val CHANNEL_ID = "session_tracking_channel" const val ACTION_START_SESSION = "start_session" @@ -68,23 +70,24 @@ class SessionTrackingService : Service() { startSessionTracking(sessionId) } } + ACTION_STOP_SESSION -> { val sessionId = intent.getStringExtra(EXTRA_SESSION_ID) serviceScope.launch { try { val targetSession = - when { - sessionId != null -> repository.getSessionById(sessionId) - else -> repository.getActiveSession() - } + when { + sessionId != null -> repository.getSessionById(sessionId) + else -> repository.getActiveSession() + } if (targetSession != null && - targetSession.status == - com.atridad.ascently.data.model.SessionStatus.ACTIVE + targetSession.status == + com.atridad.ascently.data.model.SessionStatus.ACTIVE ) { val completed = - with(com.atridad.ascently.data.model.ClimbSession) { - targetSession.complete() - } + with(com.atridad.ascently.data.model.ClimbSession) { + targetSession.complete() + } repository.updateSession(completed) } } finally { @@ -108,50 +111,50 @@ class SessionTrackingService : Service() { // Update widget when session tracking starts ClimbStatsWidgetProvider.updateAllWidgets(this) } catch (e: Exception) { - e.printStackTrace() + AppLogger.e(LOG_TAG, e) { "Failed to initialize session tracking notification" } } notificationJob = - serviceScope.launch { - try { - if (!isNotificationActive()) { - delay(1000L) - createAndShowNotification(sessionId) - } - - while (isActive) { - delay(5000L) - updateNotification(sessionId) - } - } catch (e: Exception) { - e.printStackTrace() + serviceScope.launch { + try { + if (!isNotificationActive()) { + delay(1000L) + createAndShowNotification(sessionId) } + + while (isActive) { + delay(5000L) + updateNotification(sessionId) + } + } catch (e: Exception) { + AppLogger.e(LOG_TAG, e) { "Notification updater loop crashed" } } + } monitoringJob = - serviceScope.launch { - try { - while (isActive) { - delay(10000L) + serviceScope.launch { + try { + while (isActive) { + delay(10000L) - if (!isNotificationActive()) { - updateNotification(sessionId) - } - - val session = repository.getSessionById(sessionId) - if (session == null || - session.status != - com.atridad.ascently.data.model.SessionStatus - .ACTIVE - ) { - stopSessionTracking() - break - } + if (!isNotificationActive()) { + updateNotification(sessionId) + } + + val session = repository.getSessionById(sessionId) + if (session == null || + session.status != + com.atridad.ascently.data.model.SessionStatus + .ACTIVE + ) { + stopSessionTracking() + break } - } catch (e: Exception) { - e.printStackTrace() } + } catch (e: Exception) { + AppLogger.e(LOG_TAG, e) { "Session monitoring loop crashed" } } + } } private fun stopSessionTracking() { @@ -178,13 +181,13 @@ class SessionTrackingService : Service() { // Update widget when notification updates ClimbStatsWidgetProvider.updateAllWidgets(this) } catch (e: Exception) { - e.printStackTrace() + AppLogger.e(LOG_TAG, e) { "Failed to update notification; retrying in 10s" } try { delay(10000L) createAndShowNotification(sessionId) } catch (retryException: Exception) { - retryException.printStackTrace() + AppLogger.e(LOG_TAG, retryException) { "Retrying notification update failed" } stopSessionTracking() } } @@ -194,7 +197,7 @@ class SessionTrackingService : Service() { try { val session = runBlocking { repository.getSessionById(sessionId) } if (session == null || - session.status != com.atridad.ascently.data.model.SessionStatus.ACTIVE + session.status != com.atridad.ascently.data.model.SessionStatus.ACTIVE ) { stopSessionTracking() return @@ -207,99 +210,99 @@ class SessionTrackingService : Service() { } val duration = - session.startTime?.let { startTime -> - try { - val start = LocalDateTime.parse(startTime) - val now = LocalDateTime.now() - val totalSeconds = ChronoUnit.SECONDS.between(start, now) - val hours = totalSeconds / 3600 - val minutes = (totalSeconds % 3600) / 60 - val seconds = totalSeconds % 60 + session.startTime?.let { startTime -> + try { + val start = LocalDateTime.parse(startTime) + val now = LocalDateTime.now() + val totalSeconds = ChronoUnit.SECONDS.between(start, now) + val hours = totalSeconds / 3600 + val minutes = (totalSeconds % 3600) / 60 + val seconds = totalSeconds % 60 - when { - hours > 0 -> "${hours}h ${minutes}m ${seconds}s" - minutes > 0 -> "${minutes}m ${seconds}s" - else -> "${totalSeconds}s" - } - } catch (_: Exception) { - "Active" + when { + hours > 0 -> "${hours}h ${minutes}m ${seconds}s" + minutes > 0 -> "${minutes}m ${seconds}s" + else -> "${totalSeconds}s" } + } catch (_: Exception) { + "Active" } - ?: "Active" + } + ?: "Active" val notification = - NotificationCompat.Builder(this, CHANNEL_ID) - .setContentTitle("Climbing Session Active") - .setContentText( - "${gym?.name ?: "Gym"} • $duration • ${attempts.size} attempts" - ) - .setSmallIcon(R.drawable.ic_mountains) - .setOngoing(true) - .setAutoCancel(false) - .setPriority(NotificationCompat.PRIORITY_DEFAULT) - .setCategory(NotificationCompat.CATEGORY_SERVICE) - .setVisibility(NotificationCompat.VISIBILITY_PUBLIC) - .setContentIntent(createOpenAppIntent()) - .addAction( - R.drawable.ic_mountains, - "Open Session", - createOpenAppIntent() - ) - .addAction( - android.R.drawable.ic_menu_close_clear_cancel, - "End Session", - createStopPendingIntent(sessionId) - ) - .build() + NotificationCompat.Builder(this, CHANNEL_ID) + .setContentTitle("Climbing Session Active") + .setContentText( + "${gym?.name ?: "Gym"} • $duration • ${attempts.size} attempts" + ) + .setSmallIcon(R.drawable.ic_mountains) + .setOngoing(true) + .setAutoCancel(false) + .setPriority(NotificationCompat.PRIORITY_DEFAULT) + .setCategory(NotificationCompat.CATEGORY_SERVICE) + .setVisibility(NotificationCompat.VISIBILITY_PUBLIC) + .setContentIntent(createOpenAppIntent()) + .addAction( + R.drawable.ic_mountains, + "Open Session", + createOpenAppIntent() + ) + .addAction( + android.R.drawable.ic_menu_close_clear_cancel, + "End Session", + createStopPendingIntent(sessionId) + ) + .build() startForeground(NOTIFICATION_ID, notification) notificationManager.notify(NOTIFICATION_ID, notification) } catch (e: Exception) { - e.printStackTrace() + AppLogger.e(LOG_TAG, e) { "Failed to build session tracking notification" } throw e } } private fun createOpenAppIntent(): PendingIntent { val intent = - Intent(this, MainActivity::class.java).apply { - flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP - action = "OPEN_SESSION" - } + Intent(this, MainActivity::class.java).apply { + flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP + action = "OPEN_SESSION" + } return PendingIntent.getActivity( - this, - 0, - intent, - PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE + this, + 0, + intent, + PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE ) } private fun createStopPendingIntent(sessionId: String): PendingIntent { val intent = createStopIntent(this, sessionId) return PendingIntent.getService( - this, - 1, - intent, - PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE + this, + 1, + intent, + PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE ) } private fun createNotificationChannel() { val channel = - NotificationChannel( - CHANNEL_ID, - "Session Tracking", - NotificationManager.IMPORTANCE_DEFAULT - ) - .apply { - description = "Shows active climbing session information" - setShowBadge(false) - lockscreenVisibility = NotificationCompat.VISIBILITY_PUBLIC - enableLights(false) - enableVibration(false) - setSound(null, null) - } + NotificationChannel( + CHANNEL_ID, + "Session Tracking", + NotificationManager.IMPORTANCE_DEFAULT + ) + .apply { + description = "Shows active climbing session information" + setShowBadge(false) + lockscreenVisibility = NotificationCompat.VISIBILITY_PUBLIC + enableLights(false) + enableVibration(false) + setSound(null, null) + } notificationManager.createNotificationChannel(channel) } diff --git a/android/app/src/main/java/com/atridad/ascently/ui/AscentlyApp.kt b/android/app/src/main/java/com/atridad/ascently/ui/AscentlyApp.kt index a26a88e..4b05778 100644 --- a/android/app/src/main/java/com/atridad/ascently/ui/AscentlyApp.kt +++ b/android/app/src/main/java/com/atridad/ascently/ui/AscentlyApp.kt @@ -26,15 +26,16 @@ import com.atridad.ascently.ui.components.NotificationPermissionDialog import com.atridad.ascently.ui.screens.* import com.atridad.ascently.ui.viewmodel.ClimbViewModel import com.atridad.ascently.ui.viewmodel.ClimbViewModelFactory +import com.atridad.ascently.utils.AppLogger import com.atridad.ascently.utils.AppShortcutManager import com.atridad.ascently.utils.NotificationPermissionUtils @OptIn(ExperimentalMaterial3Api::class) @Composable fun AscentlyApp( - shortcutAction: String? = null, - lastUsedGymId: String? = null, - onShortcutActionProcessed: () -> Unit = {} + shortcutAction: String? = null, + lastUsedGymId: String? = null, + onShortcutActionProcessed: () -> Unit = {} ) { val navController = rememberNavController() val context = LocalContext.current @@ -45,26 +46,26 @@ fun AscentlyApp( val repository = remember { ClimbRepository(database, context) } val syncService = remember { SyncService(context, repository) } val viewModel: ClimbViewModel = - viewModel(factory = ClimbViewModelFactory(repository, syncService, context)) + viewModel(factory = ClimbViewModelFactory(repository, syncService, context)) var showNotificationPermissionDialog by remember { mutableStateOf(false) } var hasCheckedNotificationPermission by remember { mutableStateOf(false) } val permissionLauncher = - rememberLauncherForActivityResult( - contract = ActivityResultContracts.RequestPermission() - ) { isGranted: Boolean -> - if (!isGranted) { - showNotificationPermissionDialog = false - } + rememberLauncherForActivityResult( + contract = ActivityResultContracts.RequestPermission() + ) { isGranted: Boolean -> + if (!isGranted) { + showNotificationPermissionDialog = false } + } LaunchedEffect(Unit) { if (!hasCheckedNotificationPermission) { hasCheckedNotificationPermission = true if (NotificationPermissionUtils.shouldRequestNotificationPermission() && - !NotificationPermissionUtils.isNotificationPermissionGranted(context) + !NotificationPermissionUtils.isNotificationPermissionGranted(context) ) { showNotificationPermissionDialog = true } @@ -86,10 +87,10 @@ fun AscentlyApp( LaunchedEffect(activeSession, gyms, lastUsedGym) { AppShortcutManager.updateShortcuts( - context = context, - hasActiveSession = activeSession != null, - hasGyms = gyms.isNotEmpty(), - lastUsedGym = if (activeSession == null && gyms.size > 1) lastUsedGym else null + context = context, + hasActiveSession = activeSession != null, + hasGyms = gyms.isNotEmpty(), + lastUsedGym = if (activeSession == null && gyms.size > 1) lastUsedGym else null ) } @@ -101,6 +102,7 @@ fun AscentlyApp( launchSingleTop = true } } + AppShortcutManager.ACTION_END_SESSION -> { navController.navigate(Screen.Sessions) { popUpTo(0) { inclusive = true } @@ -114,51 +116,36 @@ fun AscentlyApp( LaunchedEffect(shortcutAction, activeSession, gyms, lastUsedGym) { if (shortcutAction == AppShortcutManager.ACTION_START_SESSION && gyms.isNotEmpty()) { - android.util.Log.d( - "AscentlyApp", - "Processing shortcut action: activeSession=$activeSession, gyms.size=${gyms.size}, lastUsedGymId=$lastUsedGymId, lastUsedGym=${lastUsedGym?.name}" - ) + AppLogger.d("AscentlyApp") { "Processing shortcut action: activeSession=$activeSession, gyms.size=${gyms.size}, lastUsedGymId=$lastUsedGymId, lastUsedGym=${lastUsedGym?.name}" } if (activeSession == null) { if (NotificationPermissionUtils.shouldRequestNotificationPermission() && - !NotificationPermissionUtils.isNotificationPermissionGranted( - context - ) + !NotificationPermissionUtils.isNotificationPermissionGranted( + context + ) ) { - android.util.Log.d("AscentlyApp", "Showing notification permission dialog") + AppLogger.d("AscentlyApp") { "Showing notification permission dialog" } showNotificationPermissionDialog = true } else { if (gyms.size == 1) { - android.util.Log.d( - "AscentlyApp", - "Starting session with single gym: ${gyms.first().name}" - ) + AppLogger.d("AscentlyApp") { "Starting session with single gym: ${gyms.first().name}" } viewModel.startSession(context, gyms.first().id) } else { val targetGym = - lastUsedGymId?.let { gymId -> gyms.find { it.id == gymId } } - ?: lastUsedGym + lastUsedGymId?.let { gymId -> gyms.find { it.id == gymId } } + ?: lastUsedGym if (targetGym != null) { - android.util.Log.d( - "AscentlyApp", - "Starting session with target gym: ${targetGym.name}" - ) + AppLogger.d("AscentlyApp") { "Starting session with target gym: ${targetGym.name}" } viewModel.startSession(context, targetGym.id) } else { - android.util.Log.d( - "AscentlyApp", - "No target gym found, navigating to selection" - ) + AppLogger.d("AscentlyApp") { "No target gym found, navigating to selection" } navController.navigate(Screen.AddEditSession()) } } } } else { - android.util.Log.d( - "AscentlyApp", - "Active session already exists: ${activeSession?.id}" - ) + AppLogger.d("AscentlyApp") { "Active session already exists: ${activeSession?.id}" } } onShortcutActionProcessed() @@ -168,79 +155,79 @@ fun AscentlyApp( var fabConfig by remember { mutableStateOf(null) } Scaffold( - bottomBar = { AscentlyBottomNavigation(navController = navController) }, - floatingActionButton = { - fabConfig?.let { config -> - FloatingActionButton( - onClick = config.onClick, - containerColor = MaterialTheme.colorScheme.primary - ) { - Icon( - imageVector = config.icon, - contentDescription = config.contentDescription - ) - } + bottomBar = { AscentlyBottomNavigation(navController = navController) }, + floatingActionButton = { + fabConfig?.let { config -> + FloatingActionButton( + onClick = config.onClick, + containerColor = MaterialTheme.colorScheme.primary + ) { + Icon( + imageVector = config.icon, + contentDescription = config.contentDescription + ) } } + } ) { innerPadding -> NavHost( - navController = navController, - startDestination = Screen.Sessions, - modifier = Modifier.padding(innerPadding) + navController = navController, + startDestination = Screen.Sessions, + modifier = Modifier.padding(innerPadding) ) { composable { LaunchedEffect(gyms, activeSession) { fabConfig = - if (gyms.isNotEmpty() && activeSession == null) { - FabConfig( - icon = Icons.Default.PlayArrow, - contentDescription = "Start Session", - onClick = { - if (NotificationPermissionUtils - .shouldRequestNotificationPermission() && - !NotificationPermissionUtils - .isNotificationPermissionGranted( - context - ) - ) { - showNotificationPermissionDialog = true - } else { - navController.navigate(Screen.AddEditSession()) - } - } - ) - } else { - null - } + if (gyms.isNotEmpty() && activeSession == null) { + FabConfig( + icon = Icons.Default.PlayArrow, + contentDescription = "Start Session", + onClick = { + if (NotificationPermissionUtils + .shouldRequestNotificationPermission() && + !NotificationPermissionUtils + .isNotificationPermissionGranted( + context + ) + ) { + showNotificationPermissionDialog = true + } else { + navController.navigate(Screen.AddEditSession()) + } + } + ) + } else { + null + } } SessionsScreen( - viewModel = viewModel, - onNavigateToSessionDetail = { sessionId -> - navController.navigate(Screen.SessionDetail(sessionId)) - } + viewModel = viewModel, + onNavigateToSessionDetail = { sessionId -> + navController.navigate(Screen.SessionDetail(sessionId)) + } ) } composable { LaunchedEffect(gyms) { fabConfig = - if (gyms.isNotEmpty()) { - FabConfig( - icon = Icons.Default.Add, - contentDescription = "Add Problem", - onClick = { - navController.navigate(Screen.AddEditProblem()) - } - ) - } else { - null - } + if (gyms.isNotEmpty()) { + FabConfig( + icon = Icons.Default.Add, + contentDescription = "Add Problem", + onClick = { + navController.navigate(Screen.AddEditProblem()) + } + ) + } else { + null + } } ProblemsScreen( - viewModel = viewModel, - onNavigateToProblemDetail = { problemId -> - navController.navigate(Screen.ProblemDetail(problemId)) - } + viewModel = viewModel, + onNavigateToProblemDetail = { problemId -> + navController.navigate(Screen.ProblemDetail(problemId)) + } ) } @@ -252,17 +239,17 @@ fun AscentlyApp( composable { LaunchedEffect(Unit) { fabConfig = - FabConfig( - icon = Icons.Default.Add, - contentDescription = "Add Gym", - onClick = { navController.navigate(Screen.AddEditGym()) } - ) + FabConfig( + icon = Icons.Default.Add, + contentDescription = "Add Gym", + onClick = { navController.navigate(Screen.AddEditGym()) } + ) } GymsScreen( - viewModel = viewModel, - onNavigateToGymDetail = { gymId -> - navController.navigate(Screen.GymDetail(gymId)) - } + viewModel = viewModel, + onNavigateToGymDetail = { gymId -> + navController.navigate(Screen.GymDetail(gymId)) + } ) } @@ -275,12 +262,12 @@ fun AscentlyApp( val args = backStackEntry.toRoute() LaunchedEffect(Unit) { fabConfig = null } SessionDetailScreen( - sessionId = args.sessionId, - viewModel = viewModel, - onNavigateBack = { navController.popBackStack() }, - onNavigateToProblemDetail = { problemId -> - navController.navigate(Screen.ProblemDetail(problemId)) - } + sessionId = args.sessionId, + viewModel = viewModel, + onNavigateBack = { navController.popBackStack() }, + onNavigateToProblemDetail = { problemId -> + navController.navigate(Screen.ProblemDetail(problemId)) + } ) } @@ -288,12 +275,12 @@ fun AscentlyApp( val args = backStackEntry.toRoute() LaunchedEffect(Unit) { fabConfig = null } ProblemDetailScreen( - problemId = args.problemId, - viewModel = viewModel, - onNavigateBack = { navController.popBackStack() }, - onNavigateToEdit = { problemId -> - navController.navigate(Screen.AddEditProblem(problemId = problemId)) - } + problemId = args.problemId, + viewModel = viewModel, + onNavigateBack = { navController.popBackStack() }, + onNavigateToEdit = { problemId -> + navController.navigate(Screen.AddEditProblem(problemId = problemId)) + } ) } @@ -301,18 +288,18 @@ fun AscentlyApp( val args = backStackEntry.toRoute() LaunchedEffect(Unit) { fabConfig = null } GymDetailScreen( - gymId = args.gymId, - viewModel = viewModel, - onNavigateBack = { navController.popBackStack() }, - onNavigateToEdit = { gymId -> - navController.navigate(Screen.AddEditGym(gymId = gymId)) - }, - onNavigateToSessionDetail = { sessionId -> - navController.navigate(Screen.SessionDetail(sessionId)) - }, - onNavigateToProblemDetail = { problemId -> - navController.navigate(Screen.ProblemDetail(problemId)) - } + gymId = args.gymId, + viewModel = viewModel, + onNavigateBack = { navController.popBackStack() }, + onNavigateToEdit = { gymId -> + navController.navigate(Screen.AddEditGym(gymId = gymId)) + }, + onNavigateToSessionDetail = { sessionId -> + navController.navigate(Screen.SessionDetail(sessionId)) + }, + onNavigateToProblemDetail = { problemId -> + navController.navigate(Screen.ProblemDetail(problemId)) + } ) } @@ -320,9 +307,9 @@ fun AscentlyApp( val args = backStackEntry.toRoute() LaunchedEffect(Unit) { fabConfig = null } AddEditGymScreen( - gymId = args.gymId, - viewModel = viewModel, - onNavigateBack = { navController.popBackStack() } + gymId = args.gymId, + viewModel = viewModel, + onNavigateBack = { navController.popBackStack() } ) } @@ -330,10 +317,10 @@ fun AscentlyApp( val args = backStackEntry.toRoute() LaunchedEffect(Unit) { fabConfig = null } AddEditProblemScreen( - problemId = args.problemId, - gymId = args.gymId, - viewModel = viewModel, - onNavigateBack = { navController.popBackStack() } + problemId = args.problemId, + gymId = args.gymId, + viewModel = viewModel, + onNavigateBack = { navController.popBackStack() } ) } @@ -341,22 +328,22 @@ fun AscentlyApp( val args = backStackEntry.toRoute() LaunchedEffect(Unit) { fabConfig = null } AddEditSessionScreen( - sessionId = args.sessionId, - gymId = args.gymId, - viewModel = viewModel, - onNavigateBack = { navController.popBackStack() } + sessionId = args.sessionId, + gymId = args.gymId, + viewModel = viewModel, + onNavigateBack = { navController.popBackStack() } ) } } if (showNotificationPermissionDialog) { NotificationPermissionDialog( - onDismiss = { showNotificationPermissionDialog = false }, - onRequestPermission = { - permissionLauncher.launch( - NotificationPermissionUtils.getNotificationPermissionString() - ) - } + onDismiss = { showNotificationPermissionDialog = false }, + onRequestPermission = { + permissionLauncher.launch( + NotificationPermissionUtils.getNotificationPermissionString() + ) + } ) } } @@ -370,34 +357,34 @@ fun AscentlyBottomNavigation(navController: NavHostController) { NavigationBar { bottomNavigationItems.forEach { item -> val isSelected = - when (item.screen) { - is Screen.Sessions -> currentRoute?.contains("Session") == true - is Screen.Problems -> currentRoute?.contains("Problem") == true - is Screen.Gyms -> currentRoute?.contains("Gym") == true - is Screen.Analytics -> currentRoute?.contains("Analytics") == true - is Screen.Settings -> currentRoute?.contains("Settings") == true - else -> currentRoute?.contains(item.screen::class.simpleName ?: "") == true - } + when (item.screen) { + is Screen.Sessions -> currentRoute?.contains("Session") == true + is Screen.Problems -> currentRoute?.contains("Problem") == true + is Screen.Gyms -> currentRoute?.contains("Gym") == true + is Screen.Analytics -> currentRoute?.contains("Analytics") == true + is Screen.Settings -> currentRoute?.contains("Settings") == true + else -> currentRoute?.contains(item.screen::class.simpleName ?: "") == true + } NavigationBarItem( - icon = { Icon(item.icon, contentDescription = item.label) }, - label = { Text(item.label) }, - selected = isSelected, - onClick = { - navController.navigate(item.screen) { - popUpTo(0) { inclusive = true } - launchSingleTop = true - // Don't restore state - always start fresh when switching tabs - restoreState = false - } + icon = { Icon(item.icon, contentDescription = item.label) }, + label = { Text(item.label) }, + selected = isSelected, + onClick = { + navController.navigate(item.screen) { + popUpTo(0) { inclusive = true } + launchSingleTop = true + // Don't restore state - always start fresh when switching tabs + restoreState = false } + } ) } } } data class FabConfig( - val icon: androidx.compose.ui.graphics.vector.ImageVector, - val contentDescription: String, - val onClick: () -> Unit + val icon: androidx.compose.ui.graphics.vector.ImageVector, + val contentDescription: String, + val onClick: () -> Unit ) diff --git a/android/app/src/main/java/com/atridad/ascently/ui/viewmodel/ClimbViewModel.kt b/android/app/src/main/java/com/atridad/ascently/ui/viewmodel/ClimbViewModel.kt index ea18c13..c88b01f 100644 --- a/android/app/src/main/java/com/atridad/ascently/ui/viewmodel/ClimbViewModel.kt +++ b/android/app/src/main/java/com/atridad/ascently/ui/viewmodel/ClimbViewModel.kt @@ -8,6 +8,7 @@ import com.atridad.ascently.data.model.* import com.atridad.ascently.data.repository.ClimbRepository import com.atridad.ascently.data.sync.SyncService import com.atridad.ascently.service.SessionTrackingService +import com.atridad.ascently.utils.AppLogger import com.atridad.ascently.utils.ImageUtils import com.atridad.ascently.widget.ClimbStatsWidgetProvider import java.io.File @@ -15,9 +16,9 @@ import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch class ClimbViewModel( - private val repository: ClimbRepository, - val syncService: SyncService, - private val context: Context + private val repository: ClimbRepository, + val syncService: SyncService, + private val context: Context ) : ViewModel() { // Health Connect manager @@ -29,49 +30,49 @@ class ClimbViewModel( // Data flows val gyms = - repository - .getAllGyms() - .stateIn( - scope = viewModelScope, - started = SharingStarted.WhileSubscribed(), - initialValue = emptyList() - ) + repository + .getAllGyms() + .stateIn( + scope = viewModelScope, + started = SharingStarted.WhileSubscribed(), + initialValue = emptyList() + ) val problems = - repository - .getAllProblems() - .stateIn( - scope = viewModelScope, - started = SharingStarted.WhileSubscribed(), - initialValue = emptyList() - ) + repository + .getAllProblems() + .stateIn( + scope = viewModelScope, + started = SharingStarted.WhileSubscribed(), + initialValue = emptyList() + ) val sessions = - repository - .getAllSessions() - .stateIn( - scope = viewModelScope, - started = SharingStarted.WhileSubscribed(), - initialValue = emptyList() - ) + repository + .getAllSessions() + .stateIn( + scope = viewModelScope, + started = SharingStarted.WhileSubscribed(), + initialValue = emptyList() + ) val activeSession = - repository - .getActiveSessionFlow() - .stateIn( - scope = viewModelScope, - started = SharingStarted.WhileSubscribed(), - initialValue = null - ) + repository + .getActiveSessionFlow() + .stateIn( + scope = viewModelScope, + started = SharingStarted.WhileSubscribed(), + initialValue = null + ) val attempts = - repository - .getAllAttempts() - .stateIn( - scope = viewModelScope, - started = SharingStarted.WhileSubscribed(), - initialValue = emptyList() - ) + repository + .getAllAttempts() + .stateIn( + scope = viewModelScope, + started = SharingStarted.WhileSubscribed(), + initialValue = emptyList() + ) // Gym operations fun addGym(gym: Gym, updateWidgets: Boolean = true) { @@ -124,7 +125,7 @@ class ClimbViewModel( problem.imagePaths.forEachIndexed { index, tempPath -> if (tempPath.startsWith("temp_")) { val finalPath = - ImageUtils.renameTemporaryImage(context, tempPath, problem.id, index) + ImageUtils.renameTemporaryImage(context, tempPath, problem.id, index) finalImagePaths.add(finalPath ?: tempPath) } else { finalImagePaths.add(tempPath) @@ -176,23 +177,23 @@ class ClimbViewModel( val allProblems = repository.getAllProblems().first() val updatedProblems = - allProblems.map { problem -> - if (problem.imagePaths.isNotEmpty()) { - problem.copy(imagePaths = emptyList()) - } else { - problem - } + allProblems.map { problem -> + if (problem.imagePaths.isNotEmpty()) { + problem.copy(imagePaths = emptyList()) + } else { + problem } + } for (updatedProblem in updatedProblems) { if (updatedProblem.imagePaths != - allProblems.find { it.id == updatedProblem.id }?.imagePaths + allProblems.find { it.id == updatedProblem.id }?.imagePaths ) { repository.insertProblemWithoutSync(updatedProblem) } } - println("Deleted $deletedCount image files and cleared image references") + AppLogger.i("ClimbViewModel") { "Deleted $deletedCount image files and cleared image references" } } } @@ -225,7 +226,7 @@ class ClimbViewModel( } fun getSessionsByGym(gymId: String): Flow> = - repository.getSessionsByGym(gymId) + repository.getSessionsByGym(gymId) // Get last used gym for shortcut functionality suspend fun getLastUsedGym(): Gym? = repository.getLastUsedGym() @@ -233,41 +234,35 @@ class ClimbViewModel( // Active session management fun startSession(context: Context, gymId: String, notes: String? = null) { viewModelScope.launch { - android.util.Log.d("ClimbViewModel", "startSession called with gymId: $gymId") + AppLogger.d("ClimbViewModel") { "startSession called with gymId: $gymId" } if (!com.atridad.ascently.utils.NotificationPermissionUtils - .isNotificationPermissionGranted(context) + .isNotificationPermissionGranted(context) ) { - android.util.Log.d("ClimbViewModel", "Notification permission not granted") + AppLogger.d("ClimbViewModel") { "Notification permission not granted" } _uiState.value = - _uiState.value.copy( - error = - "Notification permission is required to track your climbing session. Please enable notifications in settings." - ) + _uiState.value.copy( + error = + "Notification permission is required to track your climbing session. Please enable notifications in settings." + ) return@launch } val existingActive = repository.getActiveSession() if (existingActive != null) { - android.util.Log.d( - "ClimbViewModel", - "Active session already exists: ${existingActive.id}" - ) + AppLogger.d("ClimbViewModel") { "Active session already exists: ${existingActive.id}" } _uiState.value = - _uiState.value.copy( - error = "There's already an active session. Please end it first." - ) + _uiState.value.copy( + error = "There's already an active session. Please end it first." + ) return@launch } - android.util.Log.d("ClimbViewModel", "Creating new session") + AppLogger.d("ClimbViewModel") { "Creating new session" } val newSession = ClimbSession.create(gymId = gymId, notes = notes) repository.insertSession(newSession) - android.util.Log.d( - "ClimbViewModel", - "Starting tracking service for session: ${newSession.id}" - ) + AppLogger.d("ClimbViewModel") { "Starting tracking service for session: ${newSession.id}" } // Start the tracking service val serviceIntent = SessionTrackingService.createStartIntent(context, newSession.id) context.startForegroundService(serviceIntent) @@ -281,13 +276,13 @@ class ClimbViewModel( fun endSession(context: Context, sessionId: String) { viewModelScope.launch { if (!com.atridad.ascently.utils.NotificationPermissionUtils - .isNotificationPermissionGranted(context) + .isNotificationPermissionGranted(context) ) { _uiState.value = - _uiState.value.copy( - error = - "Notification permission is required to manage your climbing session. Please enable notifications in settings." - ) + _uiState.value.copy( + error = + "Notification permission is required to manage your climbing session. Please enable notifications in settings." + ) return@launch } @@ -313,7 +308,7 @@ class ClimbViewModel( val activeSession = repository.getActiveSession() if (activeSession != null && activeSession.status == SessionStatus.ACTIVE) { val serviceIntent = - SessionTrackingService.createStartIntent(context, activeSession.id) + SessionTrackingService.createStartIntent(context, activeSession.id) context.startForegroundService(serviceIntent) } } @@ -348,32 +343,32 @@ class ClimbViewModel( } fun getAttemptsBySession(sessionId: String): Flow> = - repository.getAttemptsBySession(sessionId) + repository.getAttemptsBySession(sessionId) fun getAttemptsByProblem(problemId: String): Flow> = - repository.getAttemptsByProblem(problemId) + repository.getAttemptsByProblem(problemId) fun exportDataToZipUri(context: Context, uri: android.net.Uri) { viewModelScope.launch { try { _uiState.value = - _uiState.value.copy( - isLoading = true, - message = "Creating ZIP file with images..." - ) + _uiState.value.copy( + isLoading = true, + message = "Creating ZIP file with images..." + ) repository.exportAllDataToZipUri(context, uri) _uiState.value = - _uiState.value.copy( - isLoading = false, - message = - "Export complete! Your climbing data and images have been saved." - ) + _uiState.value.copy( + isLoading = false, + message = + "Export complete! Your climbing data and images have been saved." + ) } catch (e: Exception) { _uiState.value = - _uiState.value.copy( - isLoading = false, - error = "Export failed: ${e.message}" - ) + _uiState.value.copy( + isLoading = false, + error = "Export failed: ${e.message}" + ) } } } @@ -385,23 +380,23 @@ class ClimbViewModel( if (!file.name.lowercase().endsWith(".zip")) { throw Exception( - "Only ZIP files are supported for import. Please use a ZIP file exported from Ascently." + "Only ZIP files are supported for import. Please use a ZIP file exported from Ascently." ) } repository.importDataFromZip(file) _uiState.value = - _uiState.value.copy( - isLoading = false, - message = "Data imported successfully from ${file.name}" - ) + _uiState.value.copy( + isLoading = false, + message = "Data imported successfully from ${file.name}" + ) } catch (e: Exception) { _uiState.value = - _uiState.value.copy( - isLoading = false, - error = "Import failed: ${e.message}" - ) + _uiState.value.copy( + isLoading = false, + error = "Import failed: ${e.message}" + ) } } } @@ -448,13 +443,13 @@ class ClimbViewModel( repository.resetAllData() _uiState.value = - _uiState.value.copy( - isLoading = false, - message = "All data has been reset successfully" - ) + _uiState.value.copy( + isLoading = false, + message = "All data has been reset successfully" + ) } catch (e: Exception) { _uiState.value = - _uiState.value.copy(isLoading = false, error = "Reset failed: ${e.message}") + _uiState.value.copy(isLoading = false, error = "Reset failed: ${e.message}") } } } @@ -469,23 +464,20 @@ class ClimbViewModel( val attemptCount = attempts.size val result = - healthConnectManager.autoSyncCompletedSession( - session, - gymName, - attemptCount - ) + healthConnectManager.autoSyncCompletedSession( + session, + gymName, + attemptCount + ) result.onFailure { error -> if (healthConnectManager.isReadySync()) { - android.util.Log.w( - "ClimbViewModel", - "Health Connect sync failed: ${error.message}" - ) + AppLogger.w("ClimbViewModel") { "Health Connect sync failed: ${error.message}" } } } } catch (e: Exception) { if (healthConnectManager.isReadySync()) { - android.util.Log.w("ClimbViewModel", "Health Connect sync error: ${e.message}") + AppLogger.w("ClimbViewModel") { "Health Connect sync error: ${e.message}" } } } } @@ -493,7 +485,7 @@ class ClimbViewModel( } data class ClimbUiState( - val isLoading: Boolean = false, - val message: String? = null, - val error: String? = null + val isLoading: Boolean = false, + val message: String? = null, + val error: String? = null ) diff --git a/android/app/src/main/java/com/atridad/ascently/utils/AppLogger.kt b/android/app/src/main/java/com/atridad/ascently/utils/AppLogger.kt new file mode 100644 index 0000000..d2a1964 --- /dev/null +++ b/android/app/src/main/java/com/atridad/ascently/utils/AppLogger.kt @@ -0,0 +1,51 @@ +package com.atridad.ascently.utils + +import android.util.Log +import com.atridad.ascently.BuildConfig + +/** + * Centralized logging utility to ensure all mobile logging happens only in debug builds. + */ +object AppLogger { + + enum class Level(val androidLevel: Int) { + DEBUG(Log.DEBUG), + INFO(Log.INFO), + WARN(Log.WARN), + ERROR(Log.ERROR) + } + + fun d(tag: String = DEFAULT_TAG, messageProvider: () -> String) { + log(Level.DEBUG, tag, messageProvider) + } + + fun i(tag: String = DEFAULT_TAG, messageProvider: () -> String) { + log(Level.INFO, tag, messageProvider) + } + + fun w(tag: String = DEFAULT_TAG, throwable: Throwable? = null, messageProvider: () -> String) { + log(Level.WARN, tag, messageProvider, throwable) + } + + fun e(tag: String = DEFAULT_TAG, throwable: Throwable? = null, messageProvider: () -> String) { + log(Level.ERROR, tag, messageProvider, throwable) + } + + private fun log( + level: Level, + tag: String, + messageProvider: () -> String, + throwable: Throwable? = null + ) { + if (!BuildConfig.DEBUG) return + + val message = messageProvider() + if (throwable != null) { + Log.println(level.androidLevel, tag, "$message\n${Log.getStackTraceString(throwable)}") + } else { + Log.println(level.androidLevel, tag, message) + } + } + + private const val DEFAULT_TAG = "Ascently" +} diff --git a/android/app/src/main/java/com/atridad/ascently/utils/ImageUtils.kt b/android/app/src/main/java/com/atridad/ascently/utils/ImageUtils.kt index 57ec226..10c3b91 100644 --- a/android/app/src/main/java/com/atridad/ascently/utils/ImageUtils.kt +++ b/android/app/src/main/java/com/atridad/ascently/utils/ImageUtils.kt @@ -6,7 +6,6 @@ import android.graphics.Bitmap import android.graphics.BitmapFactory import android.graphics.ImageDecoder import android.net.Uri -import android.util.Log import androidx.core.graphics.scale import androidx.exifinterface.media.ExifInterface import java.io.File @@ -30,17 +29,17 @@ object ImageUtils { /** Saves an image from a URI while preserving EXIF orientation data */ private fun saveImageWithExif( - context: Context, - imageUri: Uri, - originalBitmap: Bitmap, - outputFile: File + context: Context, + imageUri: Uri, + originalBitmap: Bitmap, + outputFile: File ): Boolean { return try { // Get EXIF data from original image val originalExif = - context.contentResolver.openInputStream(imageUri)?.use { input -> - ExifInterface(input) - } + context.contentResolver.openInputStream(imageUri)?.use { input -> + ExifInterface(input) + } // Compress and save the bitmap val compressedBitmap = compressImage(originalBitmap) @@ -73,7 +72,7 @@ object ImageUtils { compressedBitmap.recycle() true } catch (e: Exception) { - e.printStackTrace() + AppLogger.e("ImageUtils", e) { "Error saving image with EXIF data" } false } } @@ -86,11 +85,11 @@ object ImageUtils { // Calculate the scaling factor val scaleFactor = - if (width > height) { - if (width > MAX_IMAGE_SIZE) MAX_IMAGE_SIZE.toFloat() / width else 1f - } else { - if (height > MAX_IMAGE_SIZE) MAX_IMAGE_SIZE.toFloat() / height else 1f - } + if (width > height) { + if (width > MAX_IMAGE_SIZE) MAX_IMAGE_SIZE.toFloat() / width else 1f + } else { + if (height > MAX_IMAGE_SIZE) MAX_IMAGE_SIZE.toFloat() / height else 1f + } return if (scaleFactor < 1f) { val newWidth = (width * scaleFactor).toInt() @@ -119,7 +118,7 @@ object ImageUtils { val file = getImageFile(context, relativePath) file.delete() } catch (e: Exception) { - e.printStackTrace() + AppLogger.e("ImageUtils", e) { "Failed to delete image: $relativePath" } false } } @@ -137,7 +136,7 @@ object ImageUtils { sourceFile.copyTo(destFile, overwrite = true) "$IMAGES_DIR/$filename" } catch (e: Exception) { - e.printStackTrace() + AppLogger.e("ImageUtils", e) { "Failed to import image from source: ${sourceFile.name}" } null } } @@ -148,16 +147,16 @@ object ImageUtils { val imagesDir = getImagesDirectory(context) imagesDir.listFiles()?.mapNotNull { file -> if (file.isFile && - (file.extension == "jpg" || - file.extension == "jpeg" || - file.extension == "png") + (file.extension == "jpg" || + file.extension == "jpeg" || + file.extension == "png") ) { "$IMAGES_DIR/${file.name}" } else null } - ?: emptyList() + ?: emptyList() } catch (e: Exception) { - e.printStackTrace() + AppLogger.e("ImageUtils", e) { "Failed to enumerate images directory" } emptyList() } } @@ -178,50 +177,47 @@ object ImageUtils { tempFilename } catch (e: Exception) { - Log.e("ImageUtils", "Error saving temporary image from URI", e) + AppLogger.e("ImageUtils", e) { "Error saving temporary image from URI" } null } } /** Renames a temporary image */ fun renameTemporaryImage( - context: Context, - tempFilename: String, - problemId: String, - imageIndex: Int + context: Context, + tempFilename: String, + problemId: String, + imageIndex: Int ): String? { return try { val tempFile = File(getImagesDirectory(context), tempFilename) if (!tempFile.exists()) { - Log.e("ImageUtils", "Temporary file does not exist: $tempFilename") + AppLogger.e("ImageUtils") { "Temporary file does not exist: $tempFilename" } return null } val deterministicFilename = - ImageNamingUtils.generateImageFilename(problemId, imageIndex) + ImageNamingUtils.generateImageFilename(problemId, imageIndex) val finalFile = File(getImagesDirectory(context), deterministicFilename) if (tempFile.renameTo(finalFile)) { - Log.d( - "ImageUtils", - "Renamed temporary image: $tempFilename -> $deterministicFilename" - ) + AppLogger.d("ImageUtils") { "Renamed temporary image: $tempFilename -> $deterministicFilename" } deterministicFilename } else { - Log.e("ImageUtils", "Failed to rename temporary image: $tempFilename") + AppLogger.e("ImageUtils") { "Failed to rename temporary image: $tempFilename" } null } } catch (e: Exception) { - Log.e("ImageUtils", "Error renaming temporary image", e) + AppLogger.e("ImageUtils", e) { "Error renaming temporary image" } null } } /** Saves image data with a specific filename */ fun saveImageFromBytesWithFilename( - context: Context, - imageData: ByteArray, - filename: String + context: Context, + imageData: ByteArray, + filename: String ): String? { return try { val imageFile = File(getImagesDirectory(context), filename) @@ -230,7 +226,7 @@ object ImageUtils { if (imageData.size > 5 * 1024 * 1024) { // 5MB threshold // For large images, decode, compress, and try to preserve EXIF val bitmap = - BitmapFactory.decodeByteArray(imageData, 0, imageData.size) ?: return null + BitmapFactory.decodeByteArray(imageData, 0, imageData.size) ?: return null val compressedBitmap = compressImage(bitmap) // Save compressed image @@ -249,7 +245,7 @@ object ImageUtils { destExif.saveAttributes() } catch (e: Exception) { // If EXIF preservation fails, continue without it - Log.w("ImageUtils", "Failed to preserve EXIF data: ${e.message}") + AppLogger.w("ImageUtils") { "Failed to preserve EXIF data: ${e.message}" } } bitmap.recycle() @@ -262,7 +258,7 @@ object ImageUtils { // Return relative path "$IMAGES_DIR/$filename" } catch (e: Exception) { - e.printStackTrace() + AppLogger.e("ImageUtils", e) { "Failed to save image from bytes: $filename" } null } } @@ -275,7 +271,7 @@ object ImageUtils { orphanedImages.forEach { path -> deleteImage(context, path) } } catch (e: Exception) { - e.printStackTrace() + AppLogger.e("ImageUtils", e) { "Failed to clean up orphaned images" } } } } diff --git a/android/app/src/main/java/com/atridad/ascently/utils/MigrationManager.kt b/android/app/src/main/java/com/atridad/ascently/utils/MigrationManager.kt index 6532668..5bc3496 100644 --- a/android/app/src/main/java/com/atridad/ascently/utils/MigrationManager.kt +++ b/android/app/src/main/java/com/atridad/ascently/utils/MigrationManager.kt @@ -2,7 +2,6 @@ package com.atridad.ascently.utils import android.content.Context import android.content.SharedPreferences -import android.util.Log import androidx.core.content.edit class MigrationManager(private val context: Context) { @@ -14,7 +13,7 @@ class MigrationManager(private val context: Context) { } private val migrationPrefs: SharedPreferences = - context.getSharedPreferences(MIGRATION_PREFS, Context.MODE_PRIVATE) + context.getSharedPreferences(MIGRATION_PREFS, Context.MODE_PRIVATE) /** * Perform migration from OpenClimb to Ascently if needed This should be called early in app @@ -22,11 +21,11 @@ class MigrationManager(private val context: Context) { */ fun migrateIfNeeded() { if (migrationPrefs.getBoolean(MIGRATION_COMPLETED_KEY, false)) { - Log.d(TAG, "Migration already completed, skipping") + AppLogger.d(TAG) { "Migration already completed, skipping" } return } - Log.i(TAG, "🔄 Starting migration from OpenClimb to Ascently...") + AppLogger.i(TAG) { "🔄 Starting migration from OpenClimb to Ascently..." } var migrationCount = 0 // Migrate SharedPreferences @@ -36,12 +35,9 @@ class MigrationManager(private val context: Context) { migrationPrefs.edit { putBoolean(MIGRATION_COMPLETED_KEY, true) } if (migrationCount > 0) { - Log.i( - TAG, - "🎉 Migration completed! Migrated $migrationCount items from OpenClimb to Ascently" - ) + AppLogger.i(TAG) { "🎉 Migration completed! Migrated $migrationCount items from OpenClimb to Ascently" } } else { - Log.i(TAG, "ℹ️ No OpenClimb data found to migrate") + AppLogger.i(TAG) { "ℹ️ No OpenClimb data found to migrate" } } } @@ -51,12 +47,12 @@ class MigrationManager(private val context: Context) { // Define preference file migrations val preferenceFileMigrations = - listOf( - "openclimb_data_state" to "ascently_data_state", - "health_connect_prefs" to "health_connect_prefs", // Keep same name - "deleted_items" to "deleted_items", // Keep same name - "sync_preferences" to "sync_preferences" // Keep same name - ) + listOf( + "openclimb_data_state" to "ascently_data_state", + "health_connect_prefs" to "health_connect_prefs", // Keep same name + "deleted_items" to "deleted_items", // Keep same name + "sync_preferences" to "sync_preferences" // Keep same name + ) for ((oldFileName, newFileName) in preferenceFileMigrations) { if (oldFileName != newFileName) { @@ -95,10 +91,7 @@ class MigrationManager(private val context: Context) { // Clear old preferences oldPrefs.edit { clear() } - Log.d( - TAG, - "✅ Migrated preference file: $oldFileName → $newFileName (${oldPrefs.all.size} keys)" - ) + AppLogger.d(TAG) { "Migrated preference file: $oldFileName → $newFileName (${oldPrefs.all.size} keys)" } return oldPrefs.all.size } @@ -111,12 +104,12 @@ class MigrationManager(private val context: Context) { // Check for any openclimb-prefixed keys across all preference files val preferencesToCheck = - listOf( - "ascently_data_state", - "health_connect_prefs", - "deleted_items", - "sync_preferences" - ) + listOf( + "ascently_data_state", + "health_connect_prefs", + "deleted_items", + "sync_preferences" + ) for (prefFileName in preferencesToCheck) { val prefs = context.getSharedPreferences(prefFileName, Context.MODE_PRIVATE) @@ -150,7 +143,7 @@ class MigrationManager(private val context: Context) { } } - Log.d(TAG, "✅ Migrated ${keysToMigrate.size} keys in $prefFileName") + AppLogger.d(TAG) { "Migrated ${keysToMigrate.size} keys in $prefFileName" } count += keysToMigrate.size } } @@ -166,6 +159,6 @@ class MigrationManager(private val context: Context) { /** Reset migration state (for testing purposes) */ fun resetMigrationState() { migrationPrefs.edit { putBoolean(MIGRATION_COMPLETED_KEY, false) } - Log.d(TAG, "Migration state reset") + AppLogger.d(TAG) { "Migration state reset" } } } diff --git a/android/app/src/main/java/com/atridad/ascently/utils/ZipExportImportUtils.kt b/android/app/src/main/java/com/atridad/ascently/utils/ZipExportImportUtils.kt index 9db6c8b..3a4d7a6 100644 --- a/android/app/src/main/java/com/atridad/ascently/utils/ZipExportImportUtils.kt +++ b/android/app/src/main/java/com/atridad/ascently/utils/ZipExportImportUtils.kt @@ -22,19 +22,19 @@ object ZipExportImportUtils { /** Creates a ZIP file containing the JSON data and all referenced images */ fun createExportZip( - context: Context, - exportData: ClimbDataBackup, - referencedImagePaths: Set, - directory: File? = null + context: Context, + exportData: ClimbDataBackup, + referencedImagePaths: Set, + directory: File? = null ): File { val exportDir = - directory - ?: File( - context.getExternalFilesDir( - android.os.Environment.DIRECTORY_DOCUMENTS - ), - "Ascently" - ) + directory + ?: File( + context.getExternalFilesDir( + android.os.Environment.DIRECTORY_DOCUMENTS + ), + "Ascently" + ) if (!exportDir.exists()) { exportDir.mkdirs() } @@ -52,10 +52,11 @@ object ZipExportImportUtils { zipOut.closeEntry() // Add JSON data file - val json = Json { - prettyPrint = true - ignoreUnknownKeys = true - } + val json = + Json { + prettyPrint = true + ignoreUnknownKeys = true + } val jsonString = json.encodeToString(exportData) val jsonEntry = ZipEntry(DATA_JSON_FILENAME) @@ -78,24 +79,21 @@ object ZipExportImportUtils { zipOut.closeEntry() successfulImages++ } else { - android.util.Log.w( - "ZipExportImportUtils", - "Image file not found or empty: $imagePath" - ) + AppLogger.w("ZipExportImportUtils") { + "Image file not found or empty: $imagePath" + } } } catch (e: Exception) { - android.util.Log.e( - "ZipExportImportUtils", - "Failed to add image $imagePath: ${e.message}" - ) + AppLogger.e("ZipExportImportUtils", e) { + "Failed to add image $imagePath: ${e.message}" + } } } // Log export summary - android.util.Log.i( - "ZipExportImportUtils", - "Export completed: ${successfulImages}/${referencedImagePaths.size} images included" - ) + AppLogger.i("ZipExportImportUtils") { + "Export completed: ${successfulImages}/${referencedImagePaths.size} images included" + } } // Validate the created ZIP file @@ -115,10 +113,10 @@ object ZipExportImportUtils { /** Creates a ZIP file and writes it to a provided URI */ fun createExportZipToUri( - context: Context, - uri: android.net.Uri, - exportData: ClimbDataBackup, - referencedImagePaths: Set + context: Context, + uri: android.net.Uri, + exportData: ClimbDataBackup, + referencedImagePaths: Set ) { try { context.contentResolver.openOutputStream(uri)?.use { outputStream -> @@ -131,10 +129,11 @@ object ZipExportImportUtils { zipOut.closeEntry() // Add JSON data file - val json = Json { - prettyPrint = true - ignoreUnknownKeys = true - } + val json = + Json { + prettyPrint = true + ignoreUnknownKeys = true + } val jsonString = json.encodeToString(exportData) val jsonEntry = ZipEntry(DATA_JSON_FILENAME) @@ -158,28 +157,26 @@ object ZipExportImportUtils { successfulImages++ } } catch (e: Exception) { - android.util.Log.e( - "ZipExportImportUtils", - "Failed to add image $imagePath: ${e.message}" - ) + AppLogger.e("ZipExportImportUtils", e) { + "Failed to add image $imagePath: ${e.message}" + } } } - android.util.Log.i( - "ZipExportImportUtils", - "Export to URI completed: ${successfulImages}/${referencedImagePaths.size} images included" - ) + AppLogger.i("ZipExportImportUtils") { + "Export to URI completed: ${successfulImages}/${referencedImagePaths.size} images included" + } } } - ?: throw IOException("Could not open output stream") + ?: throw IOException("Could not open output stream") } catch (e: Exception) { throw IOException("Failed to create export ZIP to URI: ${e.message}") } } private fun createMetadata( - exportData: ClimbDataBackup, - referencedImagePaths: Set + exportData: ClimbDataBackup, + referencedImagePaths: Set ): String { return buildString { appendLine("Ascently Export Metadata") @@ -197,8 +194,8 @@ object ZipExportImportUtils { /** Data class to hold extraction results */ data class ImportResult( - val jsonContent: String, - val importedImagePaths: Map // original filename -> new relative path + val jsonContent: String, + val importedImagePaths: Map // original filename -> new relative path ) /** Extracts a ZIP file and returns the JSON content and imported image paths */ @@ -217,16 +214,17 @@ object ZipExportImportUtils { // Read metadata for validation val metadataContent = zipIn.readBytes().toString(Charsets.UTF_8) foundRequiredFiles.add("metadata") - android.util.Log.i( - "ZipExportImportUtils", - "Found metadata: ${metadataContent.lines().take(3).joinToString()}" - ) + AppLogger.i("ZipExportImportUtils") { + "Found metadata: ${metadataContent.lines().take(3).joinToString()}" + } } + entry.name == DATA_JSON_FILENAME -> { // Read JSON data jsonContent = zipIn.readBytes().toString(Charsets.UTF_8) foundRequiredFiles.add("data") } + entry.name.startsWith("$IMAGES_DIR_NAME/") && !entry.isDirectory -> { // Extract image file val originalFilename = entry.name.substringAfter("$IMAGES_DIR_NAME/") @@ -234,11 +232,11 @@ object ZipExportImportUtils { try { // Create temporary file to hold the extracted image val tempFile = - File.createTempFile( - "import_image_", - "_$originalFilename", - context.cacheDir - ) + File.createTempFile( + "import_image_", + "_$originalFilename", + context.cacheDir + ) FileOutputStream(tempFile).use { output -> zipIn.copyTo(output) } @@ -248,37 +246,33 @@ object ZipExportImportUtils { val newPath = ImageUtils.importImageFile(context, tempFile) if (newPath != null) { importedImagePaths[originalFilename] = newPath - android.util.Log.d( - "ZipExportImportUtils", - "Successfully imported image: $originalFilename -> $newPath" - ) + AppLogger.d("ZipExportImportUtils") { + "Successfully imported image: $originalFilename -> $newPath" + } } else { - android.util.Log.w( - "ZipExportImportUtils", - "Failed to import image: $originalFilename" - ) + AppLogger.w("ZipExportImportUtils") { + "Failed to import image: $originalFilename" + } } } else { - android.util.Log.w( - "ZipExportImportUtils", - "Extracted image is empty: $originalFilename" - ) + AppLogger.w("ZipExportImportUtils") { + "Extracted image is empty: $originalFilename" + } } // Clean up temp file tempFile.delete() } catch (e: Exception) { - android.util.Log.e( - "ZipExportImportUtils", - "Failed to process image $originalFilename: ${e.message}" - ) + AppLogger.e("ZipExportImportUtils", e) { + "Failed to process image $originalFilename: ${e.message}" + } } } + else -> { - android.util.Log.d( - "ZipExportImportUtils", - "Skipping ZIP entry: ${entry.name}" - ) + AppLogger.d("ZipExportImportUtils") { + "Skipping ZIP entry: ${entry.name}" + } } } @@ -296,10 +290,9 @@ object ZipExportImportUtils { throw IOException("Invalid ZIP file: data.json is empty") } - android.util.Log.i( - "ZipExportImportUtils", - "Import extraction completed: ${importedImagePaths.size} images processed" - ) + AppLogger.i("ZipExportImportUtils") { + "Import extraction completed: ${importedImagePaths.size} images processed" + } return ImportResult(jsonContent, importedImagePaths) } catch (e: Exception) { @@ -312,16 +305,16 @@ object ZipExportImportUtils { * the new ones after import */ fun updateProblemImagePaths( - problems: List, - imagePathMapping: Map + problems: List, + imagePathMapping: Map ): List { return problems.map { problem -> val updatedImagePaths = - (problem.imagePaths ?: emptyList()).mapNotNull { oldPath -> - // Extract filename from the old path - val filename = oldPath.substringAfterLast("/") - imagePathMapping[filename] - } + (problem.imagePaths ?: emptyList()).mapNotNull { oldPath -> + // Extract filename from the old path + val filename = oldPath.substringAfterLast("/") + imagePathMapping[filename] + } problem.withUpdatedImagePaths(updatedImagePaths) } } diff --git a/ios/Ascently.xcodeproj/LiveActivityManager.swift b/ios/Ascently.xcodeproj/LiveActivityManager.swift index d148bc8..da23ee2 100644 --- a/ios/Ascently.xcodeproj/LiveActivityManager.swift +++ b/ios/Ascently.xcodeproj/LiveActivityManager.swift @@ -25,7 +25,7 @@ final class LiveActivityManager { pushType: nil ) } catch { - print("Failed to start live activity: \(error)") + AppLogger.error("Failed to start live activity: \(error)", tag: "LegacyLiveActivityManager") } } diff --git a/ios/Ascently.xcodeproj/project.xcworkspace/xcuserdata/atridad.xcuserdatad/UserInterfaceState.xcuserstate b/ios/Ascently.xcodeproj/project.xcworkspace/xcuserdata/atridad.xcuserdatad/UserInterfaceState.xcuserstate index 9d0efa40c7b5c2b6c91271d55d4e2706822bdba5..f9a425f744c15b886f05caf5fc1834651396aafe 100644 GIT binary patch literal 263252 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 z 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%0DPX@V{sTV(@o;4Ts-Xr# zKwMn;Bv7|2soRy*?Mmu))o92A7Zk!cz{giT2>A6X>UGrQ-~X)qA*#ntG0 zHTqtC1KbRMD#|i^whW&w!xzhrfF?lSW$3$1gC#&5ETewQ@cpvy0Nt-Cg((mObibw+ zo(J^2<{fws$ak$B@ZGh=Py*z;b~~WwwNJs*Knz~jAE@7T8IT1IxE;2@J#a7FrzqF= zgftie`1<;ha3Nd^*8{$|{uV{KVK1OgHyjQ}0yeo}F_89#i{VnZ0)AAK8%cZPKCmAU z$2ZP^7MKh3VIfeT8^45q!PkHtZlYc{RRA%0(?pm8)a9l(;ca*q{;nuDXF>rK0Y1AK zJKTIX5Q{fI0<_1Q@%JrU3)lI0lY~IdB%V z195iid4RsR{sh0kuRvVhHW6k*9h?Eg=xxu!8}Jt3yW8GZl;s)V0^)2r^<3_OP4ED` z1g`>lmcI?yW;woI{xN(8{{ZZ?oOoHWH=yr|1K=Q_jw|rX3Sw==QE)U+=M~g>1$ADL z4d}Rnx~y0Z#K{Wkv62{FnFi>w65Ure1O8dL02acza0y%vR{^$KiLF-N2sgv6fV`C( zfS6jj4R*kT@CZB(_+aH1in3}SAZ}J23+T1#WH=T2!yq8`Rt*PYZxudYMVYHM!+k(Z zufmS2-i1%$dqr7|yw&>y^<7O}SDy$yp%3(f0f5}q)O~dh6hkTaf%sZI2`FdvG?)(f zdNuZ0O-!u57H$CQxcW9&3Ae*qSO**7Za}Zq&%kr=S9lR#hSvZ;u14q8=)C%U_yB%a zl-sHM?PGy5Z$BT_!{6W=MOlMC)(iw}uqGX{zz#0(!gvTkB}|0LFcr>#CTM|qun4Hj z8v2?w_;Af~z!z&MXAQcoc@y3NbXxNfdan7X3}VBb4>LNDkG zrvduhf&4pG12J&N{eT_rpqx9X`yHQw$ooxE))K30so&be;7B+Yj)#)~xoh$NTI#wM znQN)zTI#r#IYo|dC%z!$mhelWe7r=#ZF-vyQs2TMfutcR&0EsNcF5ff!x)2D}CD0&>@V1=MvtGS^eb_0(}a zbzF~+)}I8YKyT;=w5|1NkPF1-`a*C+F?azVuE&S#D_{al0{plh|E;eB{I|XV=0iIW zL+j6n3*iz#|MmEB{VlK(u+4hf-geyZ1@zy9{+rN$6Z&tW{cP$Fqrna6x(OR@nhtYe0h|Yv zz3Ec80eKyjsAE49X^52;Y;`izK0)xa<&kw zThbvPu;mtPxdpwp;PWl`e9Huw1gFDnSO^;6<1H5eHr{e2EQ9L+`))zkE$Fia`CGBg zR&26$Fi@YZl(V%2CIa%d5;t3ko2`rCLbwc&w-tF?iJ7f80dcc+Ic$Ol02^(69o~X> zfjVye2tI``;GclcwtffyQIvZQfb?DL z*a}a>=ZdmpZ|DouV+V1(1KB%}y<-ua1=w!~W$d7g9h9--GQi(E)&jD2p!1H$;VF0q z@WGCk;8no?JMjOG&lKgpz2I=bH}~Pg`>5M}17HXYgOM;Au;qQ&@4lIEI?RS9z=rps z*L}p{eZ=8?XTv#g9^44Gz-_PsR>2y$6V}5SNBf>>Ulr* zy8j9ISW$K=Z~z<(DjWv*WM?nv3;kgbp#M(v---S^(SK(i5DPnt!3+3oX9Y|E%Grrc zcVfSt*l#EP*trGJX(#cx^I>=lo&@}}^I7<}qCC(C3gG`Jy2~h`>a7pJ=g{5Vol2*4 zqhMi>N{LFSNGPQkgn)n`A*rC0K^Ryx0usZ}4BZ2hFf$A(aG(3(`QjSR-ut)rf7W`} zx-UFS58h!2qxlTq+pw4)Sc^F~tmkj+XTuhJcf&#KWy5hU6GJ@Lxydc=az6+*X2JX0 zXvU4zsYP8H(1@nApf&Ayik|f0HD2dU2J$xVGL-ii!6@co=8f{+sE>{M*qDr7Ha-Y~ zO&PF*P1(pv9`aFuLda!P6&|Ms`rD+xP5Rs9&1~w%2TW!-&^*f z-!1yxa)Q&GBlfWMn7Bivvnc% zymcwdSji8pW*xtxudVvpx*vINJ;YJuxmBK9<+=4dcE9y<5Nr$4+qTxcj{DlSmLsGD z!FE}0uTBFR;jL_M&XaWFCA!cZ{crC@AKb)t{cnGtQH8hdfp*}9Y11!JAUUcHnN$2If#6A9K(I>kkbzP+hML9X4xU19rD?6oBwze z1Uu7^j*8gnPMPhrpPk)#mEOF`K;GtEhBAgp=yRt&cdq0&^s#d?Q-aczqcT;gMh%+L5&i6XjW-y;TMR}|yWV3YdfPRYS_^yw^RxyJsugIlxh(IE9_^Bi{)5M#wkf1ztkF5xtOU#Os(LLZ%UJ zIN}37;!`Fd+X&f4$Tq_Jh*-=L*&>dR#KR!ipPo#}b-!Hq>wUl8_v?MX z-uIWHGHzi1lXOIH``zdM;f!EBYVV(lT=&mrF7sK)53FV_-rfFf?BHJ_h~yCZ+JA`~ zK@h30NPR`>D^g#PxyeUCiclQAMV6%jjc9^wBV`-ens(?pQnr!LAlpdy5ZRqq>5Xh7 z2Vs^-vqX;OLq0~fk!FjW$SmX;8Y$Pv6}Z<(*+!ZnG9JHA4g{nn1DVN1UJ6hc z*&c8Y2Wq0v1NuCmw*#**n4#G10ksd<>wzhJ!I#MPfNT#eXBF%Dn@wzGCvrWo7ug;- zO$-l%;9y8PGLnVt=}RFy2SbhurO){QY!;KzJ;wHEG54j!*F~bo(AJOxX zGRXAEV^l}qNA!C{zek$U61RKgX}Y1WBl8MQTEt z>UY3VxgPCGANuho19=<$AC>FTj~LHqOyLW@WDZ}U|D#d3fur&~dYxoak>}AzL2xV$ z>B&egicft`duEqx2Ld z+o)T}HcGZ9a+44JohU>R?EQp%Pn5yko~VdkPgJE2_0j8z#v$n=CvPmDpPCqBj;C$_Sm!yH4VC-i^fJeRn_O>PIl$#i5T2l_slkD`>IGP`jW`lX z3W8G^$c+9@Wyj7>DNHf+dP=sZn$UxfQ1jGI;)CFHF7$u;83rP^)9*8iF^p## zGtmEO{h$7d`7C4=t1-jrU-^>_Y)00n-Nc!Y9O&(gou8568GW58PbI2SjT&@i3P0lA zp4rAxqPRd}5S;bBvvzYfHw7^3*HWdpC@qRXFFm)XYJ=~XVf{{gI9SA z^PZiK-y>&dW5%=gbapXI_>L9$y>iy=pVimd9qeK^d)d!Hj$n^xPjHGe+zWzp`aEY3 z=X&xfvO5=nJ)F;qp3m2$4thV|kd}1d8J_1w^nYIe=k@BPQ4Ah?)~+~lJmc6PA@_IA;CFV>Xnz;=dT}j3^EdynlYiO6KJ4Va=BES=CtD}p5{69e@XwB^nXeJm-=8&mqsuJy@NL|>@Mv>?Msmy z;y7}=begjyaDxXya5)X>$wYQ?k_XvdmgVIdw4ps6cn15rEZfU^yR5g%db_N*%d)+! zugmV{@&}CNW5zKN`CgXqW$)wi4CH%xAu_$Z6f<0w>E-qO$p$vF4cT6n?d8L`yUQ}Y zY?jM1y)4tqG062w4yw|CX0$~ASM-17Svv6&UC{d#Jzp8jr%XU^SM+pc4fc9P{VQr; zk?9pXy%Nbmj&O`CT*EG}*ris2Xn97mS?oyqF+az(cWS7M|^@jqvaVb&*-V>IeI3uaIewwj9$tzRuGQ7s&P60v6-lU;CCH`HesLn@w!RT-W4!Z5Qt2 z+6hi0*K3!#id?UmEhZde=qt7aRnb?hzGC$itFPFOJWD6M zp;&JyR&TMdGL-iifox-C8#|Uy&~vP8V`UpV1veWzhj}bSwy`TPORQO9*YhVEkZr7a zV$BnKm}8vcEEl*G1V#$t%2I>6G@udskJEo#8`|+Q@{D_(w-}7R@oQX zn$Qe6B->%K9VXjhawqhhESuyv7=T?S4`v9%kXN$ZCCevy3e)(KIm}}Le{c$WN>1Pg zDY&!b`}}`~M?sL1gWPxnDdnj|RrHs2XS@d^Pwl^yy+nd#?iEMAy zL$5bwd$Tj|F&{N=o)3aldrGyFRQ;#E$#~?JI)hov;cHg%1Nu+Zf9lV;jnwtHhtz*D zL+XAGbBq(nI`v@?+$u^X^meNjbk>l-)T;UqA#G~iiNu*$hx9#wb9o|Vtb_!6KVw9u|<*0zH?#Sql zz1=a-9rN6g(H$Ax>A=$rU@G=`$BynSMQ(S#$DZz(;m$g?U`KZjaGaBzLEm?-qTf3S z+(5>6WPDe~cV&FH1a9kY9oq2}?&xk0deVnCkn3G{e^<75KVUL?x~r$Vdb+z3_j7kO zdb;a&?z;cGf3cB697TV3WqbD&vb}qO%S01H9D2Qbi@V76?!zFsmyulLMXvYcdQYzR z^nb4uvb|RwbKI-X6EveGa=mA^djpurEEcesC47h8@9F)X-tX!C-g-8(k8@l^Z};v5 z!Tqe{Kz{etzVD6Rx6}JosYVTI;dk8q*0kjXy3n0haj*CL@h1AZKavUP>%PA3>+8P0 z?yul`?EC&&enD^d|71T0IgD)Y%l5wA-?#hwc7I>b_pfjj+5VS-@;u2vrsMwqJAt_# z6hwXxn$Vn9$nL>2bfz1x&MgC_ z(&{bk0Q8mi6UH$Cdrv!sX~;IMY}3j%t^KE+&+q6ntsK%7qdE4JZZfO*7d@v-BniEz zyM=p5pNXtwCl`4zLwYl$FH0q=QjOZwqalqjL;BI^FTLHT{{p?G*H`)l*m?S|SkHo$SWlXNu$yM{sYMqS1e*c#^n@ zyUTPp2s0PJE;6^J1J9tZ%zDb)mF~R4Al_jNpQ4w{llUC_$~=d8EJU`M51>{S*<_JP zmKw+*i#l1%p2c3W_+A!!$>LVCxYH~Xac^0sGXq&<@!c%TFl&}w>>&c*&tgYej&Xw1 zoWmQ>a)o<5zZip63PZDVy8L=036w;C;p*(`?>6=Z5oUCp3Q7y2mj*kv+YN3+4Po8 zZ`t&g?F|3#D-}1B?JoCu82E=INQYdr%Qd^Vkv%(A&}VizWVg%gzhFn%?ITAqWST?I zIePONde6}xw~%8rAM!Ed(0>m7=a|A&d@sjBmhc@b_>ncZksQBbh8#D8FsJ@<>Mf_< za@u*$+~lJGg{X*LbJoX>bGD#0?eGqA$~fnXbfzov%<20%k8+kc=U4bmUp=DUUap=PmSzVM_+klnn$L2Hn5qk z$TW{k^XN5?Uh{Ylc@A(0JI`~R6Wj{Iy!mNJcgEsddHrl&bLGoRE%cVJ3w|cwtLQOb zf82S#VZ4Xk=NrWsCNYf}%tF8U^qWt=`IfK=+2uum-+27e|lt?UxxY1 zVR!lME`Lqx(1@nApfzoI2j9=X4RhyrH~HgGC%+vPu%iOLSD*+LaT5h98QvyuyYDp-J`l%OyYUnxWu$+*rXm z*ipgd{KQ)HUQq7^H?oE8=(nJoD(Gz%)L+3QZt@@q3uU1=rFa~7Q>ZQtkZYl4xcNeM zRpwsMq zb4SJc@do`F#5=spP~O8Xi;ZF~>p6*<#Y@qa0qDQDT^8TLe(a+75sq<=Yv{kYOp1HA z#m!J$F2&_hLKY=5l7*b)AwLCqg3i3gK;A}gC5GX?N_>d>Dlv`;tl>fsmXvWxnUs`C z$*NSNKCSV+lHD+KN%NL8Z^6j3raJh5eV5SxNJj(p#y56rmXIvy?1Lm8Ale(Q_$1m$J)JwP-|BTF{D)yhIne z^D4cu&rjaQ_4K0KHw9^^BI#dN2xs==QMUy>LU6t6@xvMO5{P{|7<2Z`OtT1 zeU~myIVw^GcUQVP4QPYDO6#liYuH!maZKb3X7V-Pp^wtvvj%yV_O?p9vC{6S^da<9 zIu?1BmS<^smX>E}d6t%EX?d2BXPNZqtBk(Nl%zEBEF;e{@+>3IGI}f{&oc5XQ->xr zN1kQ$T;>@%(~Vc?g*?mjL!M>iSw@~^K0uyj#v{)%llh4~$g|8@E^rzBmyu(cByMsW zy_eN<*_@Q2Ja%2yj?3zy?DN=XS+&c)PJiBJFy3U@;mEV>4CXM8g)HG)mg6m!{hfa} zg`UdlsjQyL>ZzgopLfQR}7h!(`z}sma9Y+ zL+!)if*W413I8i#m>Bj?^Sdc6({0573VUaMSO#uRP?QiX0B-Fie|3(Hyhc) zHvZuRcY?5z-+h&8<9%0pmk;@r2~0xImGoRmtx9w7{Yo;eExb5%W89n27>vzkMw`B)+B=rQ|v>|?%RBXWCeH~ZMnan7Uv$1W3% z86Jz}Hg4fDZ|dL>8c z>i2@MhRkZntVVX+Kn?dHTevZ~V<@3Bq`^is!7*J;mF z*mIqi>59JUct>^K!mZVjR~>oP(OaDnjOR1tRYzWRd33kN%USv z?{)NENAGnKxgUgebC8=NR79V3?X<3*>OM;+~JC!fc zOWoP5;8*licPDmOcOQ|+wXR(2%C+uQ?5}P-`l_2m3b(k!y&$ZYmJDPjEB0HjII^x+ znsQV?-u0@|fJVr>p1kY1&3f{#*B?37(|f&-_>>9gy`J9d>Al_@zG4Zh*~&l2wBAV) zxWR)Ute=MTWFk8`@eb?juYOtdRKGg4sEb>u-<($Hr+#~$=S4bW|MlHt{dX9~`;1~N zvaUZ4{nlTI-PX6$`j?1ChV^AwUxp3z)j%%|++zb7H7G-Q%p+e2e_(m0t%LD)ogP1565n#iz;xti3V3GTB=J7n3!oK4(A6PY&YhB=$`MsiUc-)mYMc{Oc=`)b;O=a5}f*){durgqizCw#BzX3W%d2YZlB(*ww+ zsX9&nU*{&bxyJ*{(<~bW&~r22Y}NyPHtU1D{9A%yv$t>y%|2!tGnmC(=41EG?5f!r zV!4jInx&#vGqsur$gX)l?4!9Eo13wDJsQ%OX0*i2&1KhIKh3?R=G}P}chcOQH1CJr znvY{8=520IEwb|%Zm)%$TF9w|8*9-Gf3C$K-exdE8P5CoPK$|5W-4DWli!eOi{0$Q zt+kM63pdunt+Y7DMdaI3?Uwp)smGT6d5=ka&TN+AdoA5>OFL?*=av!JQOlzo=On({ zG7j^!s*5~Ywc{zC#$2sl!d$J))yhn*M&hnoO~HOz*-tC`X=OjH=Cg<&S;IPhVLf_j zp zTX&`_eQ;N;KVdw2Z@qxk=(Dvq-g-Mbv4ht7YOSxC`-rL%D zTle2~ARl6ewt8=Czisu|_9yH2Yc9un{9iL^CXare%k7%?Smj}7oeAR=_yD# z^wh2%4e>79HKQeMXpjB1b9?Q2ps#kl>C5Z9$pD7(9wQlz%-hMj-E?N)KHJH=-B-xG zoy^<)fZex~ce`IW#wB9Wd%GK?a1Xt=*L!=tx7U0792CVZw6BRw+q=8=`fUFSdTQ_1 z+Pi`F!x_aG#v;%5ZlL{_EaYpJ@*T2m|5p%p@ZUT5?;ZU24*Kn&-wyiipx+Mq?chE- z{LcS>b|YKZ&Q9D&2fOHC7aiP6hoeN{W;&eZ0`8@QTkc@i4rc9O)(&Rva0_p(!+$&q z!j5S$Z^ukz#oQfplMnmrScKw~qAYH#V`UzrI<=@v0~*nk7PLlQ9iQT9++N2Qco}cC zV-I@Lhkn>|NB7wAZQf-V?<3caAMg>MGJ#2aj@@^h$!zSs;{q151X*`n!S~3#<63@U zJ%6%+&1_=_|FW0;9OMYcImsE$bBSnTi03-Vq;iM*JPg98L(-9vEMzAac_~0)icyj> zl&2C^sYXreP@gAgLUUTtmM7`RvvlGmy3n0h>CJ1r!2sT3FhhBdk&Iz1pD>=!n8Gw> zFpIg&XA$48`;8kcCwp&L~@9u zL~)9q;QM7{Kumpd^Qc~$wXFikehrIqzJ_+MOi9Pna8M3E$Y&MMl_`b zt!c+oJk4{wz{_-{2R-RSKi*^@Z}Tq0c%M;xz(;(_1Sau0)0xR^zG4B3S;BX$;Cp^z zEx)jyKiR-$wy}eM*~@+oa)jfY&o&Nu`@mwdFRPJz}he7y4NIEi-h3w=aF9j$}F-lT~@>HTK)u>4w>hlCmXih8I z@+2L3mQK7x7rOH*y?Ko{7{FT$W+?A5k}-_s6UOryQ<%mKW-*ufEaDr!WjU+(kv06x zZ~VdEY+@__u!}uJaDc-c;{>NU$3?DijW`lX;wHDb$AchzF(54&$V@hJl85{hq9`RO zO*tx3g~zEuZR*jG#x$cPZD>yip5b|3q%+-kg^eA0oT1>U347>r5_Vj;`u-Rj2EnAnf)Ool&QoI^FE5o4f3`5p}w$(@k#O z-AMPcsMB4Y?p3g_?vqfbyE@&!Kz7})qfU2qy59=I9_@Jvb$Y1NqX(<8n;z=)P^ZVX zAbcek#VJW?%HcL%`4so?%0wn(Pp`yaH?JgcBM4t@$+JAq3%txqe&cukWJ3`4%t2Aq z>8Vc7G7RHm)aj{CPj}bzBv(GdLRuh%cA(@ULRe+6N0chI{q z>hxBpcPWN27Ik{7(|ZC@TtS`Q>hz8e!ahyufI5BD>C=htScf`&)amm_5cbVNLDcE1 zPTvv?<^$B}t4`l>9ODw|^i`*CY!JTIm?u%^HFaKlj-{+Yo!8WPZG8~-%S3+E>8DP= zVhrGYMlu?A)^8stIK>&x2jT0rXhsWK(Uz}R#tK&PBM*b{jjUuRCwX{{cNxlXMzDwD zsPl$8Z=4OnH*3%Yb>39x&DPB1YnJdW_VcE9+dm{N=`mM-_tC#E{dfa&^>-irce9rW z4g}!<_c5S4HK|QK+{b{~%w--6aUTQj^MFS|I4}+FV_lU4)9L^R_x~ zSK}k5qR!juygiF(Qc&k@b>6-ggzvQHB|6iU9<1hXHnN#*K{z-cr725!Dlv+QOkxVt za36ycxK0u`gYey!Jj?UEfc?Dd?%!R{<{G ze}ZychtM6GfJIN-tQ>ychqsz8KurB?{~C2AKe6X z{JV$YXzzFQ9KJ=J(dvx;p8tYyOlH&>qt2LIyvjh{;vI(Y4+lBSQKEwIgDTXgA&qFt z=geaPi?E*$l6e$_ABLnQ16}CH8}w%oo7hVPksJ!bvE`^qZR*m134F_VZCEdeWP|*w06Qu!CLfW?v9~T!N}RPIYQAjv1))u{s}rMKmd-a+`ZW_(?~) z(w$f6#m{VJE8E!_gr63oJQbUIrZSzGTqX&1K2_(_J3%<^DLSLhICaLo%33y} z&Ny|(+0S@)KE5pKj8|v8{fu|#<0qlccy-2qfjb|69d*X5GyYZ(PH4^ZyueFz!F^2l zoj>^-`P%Hs54ESY3qYM;ziD>da7QMsyI)Y(N{-nW@gqjx6Q})S0Qy%%21Q@0DaHC%MVT>kMT$ z?=y;hoZu8^I3I+wYSD}qw4yCvv5Xb0;zu3^;q0uaGh3b6d3cR?QD?R~vq!Lp@7}e?d4mGwRG$XKpU~@DA$CRcG#d{L4|)nXAs+ z(?R%EH5#GLSL%G#l36T4ov+mSY9;rAa9&2#nWxUY9Q5KX)S0KwykYF*FzU=xXWq#m zoc|aNQD?q7^P4l1uTf{dI`fxvCkPj$N1X-gEXc;I3`Cs;>MR(-KO97z1?nt_3c`g| zsE;}e)mhk-FIa>+3)NZp9k+sTQCielq|Tx&^q@cLEK+CDV73v7I*Zg*uZY~62XBW{H6@msYz|>@fow3 z%RCm6$bBC0CMT`fX&ol;CF(3yXX$+6 zxr;hW)%iBy8M^ZdJ?X=*Y-Kw;_%{f@D?&voQg!dDL01&T@CQ{6nUo&T@5@yR+q&NkX0F>MVC>E1se=>a0*_ zg*#iZmW`;hLY)=vY-It;qRvWnR=Ts5W0-_GE7e))&Q@OJI_j)cXXULRT-BbJP-m4o zt9r1SzfotEI;*w?;rIC{jXK|}^L-`W<5R{nk;$AUhBy+q5rjXqDh;@W(6^q%cJ(!C*dMEFUwDV_f12SBVY6pBnQd9eA4OSjrmI`AMCh)(7G0OyozM z)#|J+#@mcWoz?2B{)8i3K%Lda0;`jXPWO4L_mI8gi#lu7 zS?kW$4q_zgtW{^NJ6n5*bEvacowe?4T?5*n&N_A0xwCbP`2ls-sk6?V{Tz^;oaDxS zetw;y4Cj4Dv5ym+;tc15@RwRNqXn&K%U3L81*`ZGcm8WeGLw}Y^x`ev;a!HYlfxY4 zI46Vfx5sEmBbv}0cmCVgEa6+0O+6MKmul0!l0-#ZHZ8%N=v>intB zpADG6m&|4^^NHs!_jwS6e+6`+C%x&*>-@nEcCnj%LHKtODpHxMRO2J2GMyRBBAOIZ zxy`*G+|ZG(bmtX%@iUv*%64`J;l@IgN1cu8YA`CLMx8C{Y}posTiyBA(x|glovnTkZ5_o#)Y+=e)@ht40d=;jv-M^W zZfna6sIyI-ZQb~hKT&6!I@`7c;r2X~M4j#GY_EVj-~KV5GM>*k$yH*ABQXg7Y0lF; z%k#X*a(>}Ae&??s+>wpK6s0(&7{XZ8*`dyk2}E%Pb#|z;BR&XsHl+jV>{MrGC%$7H z>g-f!r#su__vx;JsIyC*T_qUANJjGkA909tT;LMXLHKV2+R%BwS!;3wAba}e%M zM{e?xpF#}aebm{l&h8I6z!}upt^JYtFwP4_ku7oBkDw|6PbfvyoEZE>O{J; z$ekQUok(>e-PwW1Xoxxo)H&eJ4$S0h)H$He0e5!bP7oeUk2(j{Ihc)C8HhRu)j2qX ze>jLb2h}+k6@-VXP#<*;sdK0)pEHjIEaDrIc@%_)L(-CgF7)FK`ZI`4>?MLo4h7+n za@3?Yb!ot4=AzCKb&f3N1`kl@s5(c}@iKi;=cqbI2e5(NsB=`EqX&ZUSQ)CLj=z6C zJm$`hd9TN2qs}pPj=8gA-s`dZsN?U6509t8dp+J8b&jiZ{7t;qG%2KVn|nccsy#2!nXdF;HGi{_&1?(8)A=Y(S;|w1QA}hKQ<%ni61Ywh zH-qp@TV6n&GwPh_#*h4oI%m{5vn2@6=Ak6&oK@#+1x7F)b%ti=)nYb`GHQpw1O_uDG)+-|!P_SjVqH7@dK<RkH}@AcXl&Z5>ewXVtQ z+I8&aS~8Dpj9mVK7M&B{Ak9i4uj&V;h z{dk=>=#RUJ(PzvEMllh4jhVz0zG5Ep@p~j@6>c&{1~D>-vB#LdaWgTS*vozna)e_< z5yu_wa*zLlFg7z;$VzsK;1*(wQG%*G#^Y3{F->SnbDriIp5=L7r6;}U!#fP-U4~-M zu^;mZ+)V6Mt`W;^%p7ax*!w{k zmkBe+nK>>Sg)wuSnd97lToue5XXdzSG{VerW{zuy`-?McTqo=_t}|V+&$#{!!pw2z zjWciD1k4sU3-iUz#cXlQSk6k!7Pp??`IDXO!hCUiIF9|ro!}H^i;Ka0aq-;6F5}#7 zd>Yb`fxHx;F!mX5pYf%rjorn&)A+VLNeApQz8l@?fnCNA#7yyTVXpWwe87j;W&Bj8 zF&(>%U(DBhgI&h2VJ++Ug)MByjPbkKO9bahAd%}N@eng51ehx!C*ER0Zp@ZY3U4u? z4CSbax0ql)e}8e9&=NByw8oqXFW@aE*js|VCA@|=m@okICA`DCm@Q!}AMpv(`GOgI z$u}%vDc`Y%;2kEKFL5YlOY{yCKVckZOPq=M5@)j% z?=aDPiOVrtq8m+gpNSjT#1;;7o(q^S@iIx6F);;mUQa_>(vg9@H(K zqdpC>yX);}&r{gl^)7V9tzLhV{@CMnw|adfqZo}_y*`;Ke2!bazJP@+!mVEak)K$N zTfM#!^IhM}R`wIg0S<8vvtGZ#HDZb9F7|o-J`aNMMi%VzMmBO#loFK2%s0$?!@M^d zVYVCI;f<&0h}mv*qdTuKfPoC+ZALSO4;ahmOvesy*x!x0%wst#SjqSN&L8~A-~5Yr zcw-OyIKfFyafVpph$oTzJPg955c^EZKql-m$?uw^f)vJVNfoI?73$M~hBTr*=1qEv zj&#MmN!>AT(g4hxGzjx1jmEr5=1nqh(&tQN8s28oA{O&COIXbs>@(?SHnR=yGRe$I zd)SA0lgyiR3iBq#V&0^9%$szd|9HToAWY6ic5;x5;@D|&N$fSb8r7*mEt=7s7WiG1 z{2bn=zu!7c_PZ##H+|?!KZY=rVZ6tujAJ|#nT7o(&*3YUVaLgKoV<$l*m1HQC;!DR z%$sb-$$N>y%*kd>K1~c}PBwFL0{1X;vYC_ZIVCG*PBC*zPKsgX6f>uk;&IHJV&;^Z zG{wv*W=?6zvzR%>%qcI>3p1yfIpsCp#mp&YP8rT8m^sDFDHHe-GpCq2WiH=g<`grh ztmHS$oMPsbKiP?yQ_P%V$0^4#bBdW$>^S8bW=*l%lq6C}4Z@pgNJ~00ke7Vqry%7h zPX#JbmwMEvA#G_#d!C{*UFb@8-lRVR7{o|MF`5sU%oILn8Vgv+BEIHFequFi*~lg~ zvz7fsa)3je<2)C*#0|`QGntz~m>Oc{R5PchCl6*$HFIhK%3|hJGpAOh4rWd@b7}+H zVCGaar#{Kcm^szVsoi)3GpCw4bs!@!bE=tB$1n*qr7F5sK}A5iM5Lu8q*ElNK^kclNf87DQ3OOr>R0!s!@YRH0EWR(t(b2q6_`8&-aHQ z^L=^mk3!!2^4^#C{=3L~U*7wRSc1Iw<-Pv}>)46e-v5>#_>ujb=LR>q#UCW{kR(!q zhzFU-Oe9$-KtT#ogjh=RG|y6v>d5z?7BABT`5rV!wg=wX2l74W#sFk|FbFvxOyDgh zGMPEdWgheSgil$)O1@+ZTiMQD1DxVCXE?|2$oxR&2e(N;<_9uANDd<6Ga_qz zR&wwdxv{_T_Sg5TN5q$+9QHTN)H(uU&d3{fN zM7+H5^2W;>FK@iO3GybGae^5qM3EmePB7zy!aRi;Czx@6fq5pH zWuo6pzV|&Mu@C)_EpZg18OuAo%M50+m?eC~GS;$=^=xD}GA4e5oQX#`#!1XG@f>md zMKUR*1`!VvOVleKi*(8V;IYL zX7C;}naxKmWf{v^&j#dsxS4PG7Wp3jz%R)7@HlclyvjAM^BZ@$$9>|1h@`ZnBR!9j zi`?WPA0;Wp<2*?PN4)aWs zIZ5WESu8=;B=2d`XRP9Lwy~WZ>|{R&ILJ?&#XOVFbAemj<`4cPiDXiOh~$7sqR2uv z3Sq{{W}IA%r!nK?XEEpG8q}l~wP}joO>TwE$?fQXyvgz=_oY9hkT-b@@+QBFyvgq& zZ}Jk%IC&`_vko&(-oPgIV8+R2ocujUG2>)2PCmgE%sAPMlYix3%r-dznUhn3h?D?% zQ{+v_LN*E^Z%Pr0@ig+LJd3<3HIO%@7V@Svr5Vk6g)VfZ8$B4rV1_V^iA-WLQ<=y6 z%x3{BFyEAwtY#}_?C%0Zq19ND|4U24RG}5%NYvkrjC(q0==OCw%H{vYkxq-|PGW(ZmVMHP_N67324#P~y9LgMKp#U<6 zGKWQoMdncE@L8%Mb0~9Iiz{-$dzU|BN@$f>~5MF%nHIsauY)dO7SEckm-><$n(e# zL727yt#Mb{c68)A|8S3Z%r31Q=>l>QjdRmI&Qr*ePL6ah(hO&$>q2*W(wlw^U<~6i zi*#m@ZW`)Nr|xt!Si#+7VxtX5gY2LyNGR@-y)SF2!GA&0hGOc6> z-v3PQ$aIGDT;vMZ`JG!Nl7zj<9FT^zWI(>mnaN8biW0-)s3o&n{Oy4-vl(R`fO%$~ z#8lp925QW##>{HWyaaV-Hml5A*v}zOpsvjKh!4U@_eZ)rQg0*mHu5pdC{lkT3-BbR zsZ0%CpbiaaL=)^`r2UKR#~_9>0yRdC#|}kKMs1OwvKh09)VIi8?BP51qRz zj8t#rHGbm;dKsl>QTh_qn1ReXI9=uL(=A!;A?#kk>EXhHbH7j;Ct3GCZ9Oq@N zPD7evmRVcTjt=NyR_A7wC+iUQBTH7j$SOzH%lyg>ZgVFHvjv!4Hg{wzOfia6iYF<} zvzT4BTGXZv^=N?pWs@(P8D{H97rN6Exw7?TCX4xm&+u+#`vP;!W{%m+F`GGN+s!^s z@H@A-8-&>-$cesYk3lc8n_YH0oxLpOsfZoRUY|zjb#}eZt~c5BCi_&(IQv}Yvw#(> zVh!uq$Yu_3inG|E?3cL8brOOwM>@wCo zhkA01Lr-$p+ZM{~pa=wec=A47~BIk!JVJULt z+=o8qyoR01d6Pf*i+{O~J0HtHMl$2x$K3mvY>&D7u{`ACdD=3XW$fW1NkN$FQG7O6 z7P9jg=9EkBTrXiiay6v|t&lyJJX+$C{dZhg#c zzPVqf8L#jvW}MsZ<#w*`_z!ar;zy2ihV!U5w_fD_4ZX;Ho0K5Tqu+Vlk*6RDa>dU9Td}En_8uO_!-*nWOPo4Qza)950Fuy+Je+uX3cW(aXv_$Uwa_5&j|5WC& zki{(JQ=F4u&ir4nn-l1B{=d1)1N1t7a^Qb9lLqf%fyc1R1@e-gLKGny?|A{43zX$~ zDp8dh)WRMV7|00ZEFfnAISWifz5?$to4Kg9z-o4}2m4XrBsaK?o)^&Xf)PBzquASm zQP_=wg&Bl<3c9DDdkX4jL31s*l^r;{pzH+?aTs$hs1F5Ca~3l$m>l@O3&>wc{zC34 ze7JL_#IKm913-!E9PCOC%sTlp%r|GT!k~>+``T+{4DzD z?+t{7onP4bg`HoxHX|{^!W)sLuq=gT@izp*!auMNJ5u;O|BxDlMf9aeHgb}id=#Vz zW>=&P?kiHB3RI>lHE?ec_ZCrikx6{UL2d?NQT-@dg6HtrqWV!(y+zeqv?kpcz;M)E zbPVH}%G=oKqO+L8PVNL@u`J}FIBF@TmSXNN))eOzdyT=^qhg~N%S0wK4d)hH$YTD+ zoaEP5#NMZd`u zrt>Z{kt=#0+c2AGyB2NNqU~C=U5hrO=!;z8I=^A>qLYI#CM!88z~fXwUt=1g7cnhp zMLXhp4Hz zccHlZi|a>mvnc)=dQ!YEZ=fH=-CNwf#m%I+eiYY_;`&kC`(1oDW?Z5mjnKOiv-ynm zY(kwS^sB^v^s9t^l~8Ai<3U)mC{N(MC|M2rS<-%%d=Yh(Y>eKN)SHqm(T9@rS;}&} zZzWgqCEk~kW>Rt=dQtLcj`H6<`cU#Jzw$eJQSuMWrsO|CSSmZEc?EZrTE{A??%1E$-t@!n#13W{_9k{T<1nk($=KuAchIxg+05euK4b~*i~R(Bi(SnZ z=wGb<#p++IImeoFtbWJJ87pV3oUwAo${8zXteml@F!$IvE+KQQ%&|AP&7a5{dyjY? zk`jcaBY1@LWF(TT*w@mz$V&kVQ;g!2;z{(a^s|)397|WCDrQ;w1?o@_^DO-`&Cuu4 zt!YaKI%EGzzeX?m@&@*=^iW1Him^;!5>t7b8O&lXcBAw{7Gp=Cy3=4o})Y!sX}#X@gj9;Kx3NHf>yjrdpgmT9=uK;`ZJIr3}+-`7|%qeFr9aq z$sFEi0gL#EkNK39e9l@nu$iswU>AG%j=k*X5Qq7N6P)H87r4wde&Z&8@E8Abp9GRf z4Z>$a(vpEpM3If0B?=hQse87h+VHuzB8LRn%^=x7b+u6x(zU2q@agd)m%5hF{ zmN+hPm0!8RZT{pR?h(&JQiAZ=2p%Cl8Hpq-Imks`3Q(A06sHtVQkrKe%kxyCDm8e4 zI@F^fFVl=yXiZx>(3x($MlbsE27?&N2u3lM2~1)tZ!?2g%w;|cS3)y*$JmjYk zMTwy#PY}y9JV$vdQibZ&;zjDxfW|bX1+93M_H?2vJ$Riy^k*PL7|uw>FrJA_VLIeGlOG^ZtPXh%o7(4C(2 zrXK?s%rM?$G~;-S$xP!N-eWfN_<#>t!ZJSLGgk8j>)FH>wzHGne9I5);~+nCl;fP@ zEOA`oD!+1r+x*Et+#{Zcqy%Bv2p%Cl8Hpq-Imks`3Q(A$Jb`|eZA>flr>q{7mA&jJ z-sdCaDy!bI>MZ*c=2eQqM{gJzZtQA&bb`?%@8NIKVpBU_5#pbl515=rWtQEZ{6}=x7^{7%B zG82V-m1QZ@I-??gwGDg6Kyz^RK3cYHFyahH4|3$YhqYhP50;5)!#&p>UvQ9Q=C!VyHs6<>X$Ir>d8S^L(ghtK$aS^)R3iyGir3_ zHO!~RLKg8oKcSWy`cOk3YU)ExIch%5v%E@IeBR#!2y2>cO}kigCqM9G5Z1CswQ`~M zT57H39^d&N*3$1+p}ehX~7%gaA=BnX?>@g`=_rDq4pLNIK&A~1>q~XDMB&y^c8izGMLTmVmE4QskWACYxyGfx25`8 zy0hgnmSg5E)zwmuTdA#;+FGft)qM27mHxM~8?8Lo>aQSd?Y(ZTuGZe`)+10?>xt-X zYrSoKBnaC)N_KMMtTyJ^<`t$hhj|=E<~C=8@YTYU;t6DUbu?o+$Z<{vVOwXlEr@5^ z_QrEW2!+D>N(8msYaDNB&cDTVm+zrBxk5LG7>R6jb*xin5?XMF%)Iz;odeEN%%*HNvSFT>?UJ38N2CiH@)p9XE*nBQ)4$7yPH|} zD6&$Ps#M44y7%J^mhc&?I8GcFgRqCb^eBLN^k|OzdURkGX5M2FX4vBweC{=Oz2>gh z-1V9}UaL!e#xaez+0M6o&wWyZuxA91QwBBm?8NJsL(e(5r>7cw%GFb@o^ti{{=Ke$ zuZQTx>*X=S*W1z!b9j9+@1fq;ce9TJ+~L2^dw+Z7MNfOhpw?b;_VV8K8pudSvzD!> zy_df9`kk9W*gHQy*SiD_@wwiu7>v*L9?QqrpWgPTx83RO^Sv(zVV_LoBo~#bO&!dm z&j{Sz$G-MigZuj&;SA@{w?3&s*jL~Bmf~5S!+iRhYhU}&*Y5TGkj3m`FY@>OhleBw zVZY*(<{8x9&ushYeLsEcr*HlAt=}eg@-;rwKOGr(i6%5-JkxoHZG6LbT;Udf1mPRG zDS}?VQJ1FZ^&7(&#{~4~jV(Cijoh4FZ9xVa4X&J4fG^&lKkhRRgM&qg?7#C$HIXCrO~ z;hTQ`rk#AV4(@q#5i-B|C-Ec(;mC%(f}I~J??`z^$~*Ew5RM86an~qkjdIs0^B&cX z4mfj^GeLF(f(qunvuevE#bciF{W_Mw(B5v0LgW85{yU1NGP zn4zd`jM~PiZOnP}V2u45>m3}MlU&rqePd-B+ZUf3JDiXC9G@TiGpFz#j!h22ao&Y- z<~r_aDxmM<+R}|4%s^kqEno*`=({|_ae6%NZ~hI!@$Md9n4;980gV{In~dTB$1vv! z>YboB6MSZZK2CTIzbhx`m+KL>+wVkGLAm zU0Kc=)^deg{1Jpx9>*-Fl%*GgQ2P|wrfg(0W;o>ze+A*xVmwJKZBWZpGnlHLsp^@k zo~b8M%hbz3IL&)DEidYt){gGz<22__bN)2vPrFZQ5KfQ4Y^Uqr^cU#QaLi`<26nI$ zbxcp-VGzDuoYHtt-&V`pz3IzpHt{8YlSon!zEgvGG++|;=AD^*&rcij$6k1?#v=GWKbKXBK#7foB%@`~vkXP|pHqEKEy!n$U)}n9D-XEmYG&@5e%CEwq~- z*22Dg*oc*Ez-}$dL{4&{&x`bVkv=c_?`J#tItUlLXR%%{)~ChJSSE~FoB625RLV_u+FkvxUDg+vR%yNi76w+DS$PJv zth^S4tDLjSIjfwr$~~(-#h$E63c}TCNzbcvr8|DU+P#TNXFm^UNk{bQb3gxl zE8Bx`O(t@Z3qM<-2w}GuHjVeY}V3oxk4s>z%)T7T&}4a<4zZ1i)vf2B* zSsk05v048&-{rsOQiJfzO1!{}Ohw&a+KVsuag<|0xW%4sDM2Y-#k{t3M-R4qj6QBT z%T=xi;no;pc^Z4TbvpKN>ml@FtE}7dQMh=j?LMuEZexIt^)QKnq&35@&twxvxF5 z+cUd8vs=x(-M3rKyO*;D*>)!c;hqq8?s3MR_PBG;=WJ$65Pp*lnZ7B&Kt?i}Bb?z} z5PoZKzb%Ws{dOv|n8P*f?zevi;dfPdk(YR%k64B}zgOq?>ioVgO=-?t7UQ|^FY-G# zgYXA4|G_;!RA3n6n7|(Pb1(?^%ClDw_v+!^TIkWfS$yQH)^| zX1?D$wO=0&=)(bhI8c>3=*I!iAMpGE&mZvo0nZ=s`~lA&^!!23AFM|+TJQ;9ur3G> zWk8LGa?qQ>4CNT-xe$auJx3L)q5hwip#Gno{c~ob=s|x5u#clS|jeHc~4Mt$MjynIS^L_VU_)8foqaVNc*)J2B%t4NG zlDk28ECPFcOwGsi<5+fbkds_Q^8~RxgUrV&;oM_2c!3wupJU50>tiRdAIHuS#~tK4 z_TPOENC?8?K6BjskGG{0UFc3P`l4sY2QZv>`51FKzMc(iVh{HAxI2#@<`>+3+-#0t z#yfQUH|+lL+d+6jzfLsZO;++Pr?|vbe&u)6cS8S9qz2(h{W+P2%;cpIMTy~Y?B~hS z=*`IrRHqH*e)2VXBGbuXjA1-xank#Aat5=Q%X}8F0=1o7jhUZJ3BpsEh$IW<Y%sFoJC;xDdcpjpMXU*~KBRobP@>2-2 zJZqM|k1;&^1hMGJ*?Oq;tht??gj&v;$=Pju!}sXJ+5Om`vo~=6S@)l{>*q31igJ|a zc`8x|XP;}x%QT|}`h2cGdU0+D!?7dh^!?mC-bW4RK4lBr*~xC~@wp$^hn}3%k8}3n z+!e0#JGZ#Qf3@A^0SQ5P-n)Lj1T|65`7R7*0&nvUGq9iM-G6=|JCOan`_9XM-ks-9 zV+QB##QDqEiSxe_AB1rcgrp@J?@HXWl%Xu{j;qZ})Ta?@iL)nh`VyxvarzRct~mQ1 z_ZE|xiW=k87dMjyEW*CT*_Sx)Puym=*3Ag@3Sr7gB=oVtTTo-xrHek|%hIr?Dd! z?Z?F`R7X!Ps^#Jc%;Mri*0Bv)E}H8_^Sr3`OV3ahy}P7$mzvO=mb9TAuhR#;yrh?x z?AWE@j6|kOD^dSt^ Bool { diff --git a/ios/Ascently/Services/SyncService.swift b/ios/Ascently/Services/SyncService.swift index 07e416c..1f71a01 100644 --- a/ios/Ascently/Services/SyncService.swift +++ b/ios/Ascently/Services/SyncService.swift @@ -12,10 +12,27 @@ class SyncService: ObservableObject { @Published var isOfflineMode = false private let userDefaults = UserDefaults.standard + private let logTag = "SyncService" private var syncTask: Task? private var pendingChanges = false private let syncDebounceDelay: TimeInterval = 2.0 + private func logDebug(_ message: @autoclosure () -> String) { + AppLogger.debug(message(), tag: logTag) + } + + private func logInfo(_ message: @autoclosure () -> String) { + AppLogger.info(message(), tag: logTag) + } + + private func logWarning(_ message: @autoclosure () -> String) { + AppLogger.warning(message(), tag: logTag) + } + + private func logError(_ message: @autoclosure () -> String) { + AppLogger.error(message(), tag: logTag) + } + private enum Keys { static let serverURL = "sync_server_url" static let authToken = "sync_auth_token" @@ -201,7 +218,7 @@ class SyncService: ObservableObject { return false } - print( + logInfo( "iOS DELTA SYNC: Sending gyms=\(modifiedGyms.count), problems=\(modifiedProblems.count), sessions=\(modifiedSessions.count), attempts=\(modifiedAttempts.count), deletions=\(modifiedDeletions.count)" ) @@ -244,7 +261,7 @@ class SyncService: ObservableObject { let decoder = JSONDecoder() let deltaResponse = try decoder.decode(DeltaSyncResponse.self, from: data) - print( + logInfo( "iOS DELTA SYNC: Received gyms=\(deltaResponse.gyms.count), problems=\(deltaResponse.problems.count), sessions=\(deltaResponse.sessions.count), attempts=\(deltaResponse.attempts.count), deletions=\(deltaResponse.deletedItems.count)" ) @@ -270,7 +287,7 @@ class SyncService: ObservableObject { let allDeletions = dataManager.getDeletedItems() + response.deletedItems let uniqueDeletions = Array(Set(allDeletions)) - print( + logInfo( "iOS DELTA SYNC: Applying \(uniqueDeletions.count) deletion records before merging data" ) applyDeletionsToDataManager(deletions: uniqueDeletions, dataManager: dataManager) @@ -298,10 +315,10 @@ class SyncService: ObservableObject { _ = try imageManager.saveImportedImage(imageData, filename: consistentFilename) imagePathMapping[serverFilename] = consistentFilename } catch SyncError.imageNotFound { - print("Image not found on server: \(serverFilename)") + logInfo("Image not found on server: \(serverFilename)") continue } catch { - print("Failed to download image \(serverFilename): \(error)") + logInfo("Failed to download image \(serverFilename): \(error)") continue } } @@ -436,7 +453,7 @@ class SyncService: ObservableObject { ) async throws { guard !modifiedProblems.isEmpty else { return } - print("iOS DELTA SYNC: Syncing images for \(modifiedProblems.count) modified problems") + logInfo("iOS DELTA SYNC: Syncing images for \(modifiedProblems.count) modified problems") for backupProblem in modifiedProblems { guard @@ -465,9 +482,9 @@ class SyncService: ObservableObject { } try await uploadImage(filename: consistentFilename, imageData: imageData) - print("Uploaded modified problem image: \(consistentFilename)") + logInfo("Uploaded modified problem image: \(consistentFilename)") } catch { - print("Failed to upload image \(consistentFilename): \(error)") + logInfo("Failed to upload image \(consistentFilename): \(error)") } } } @@ -549,7 +566,7 @@ class SyncService: ObservableObject { func syncWithServer(dataManager: ClimbingDataManager) async throws { if isOfflineMode { - print("Sync skipped: Offline mode is enabled.") + logInfo("Sync skipped: Offline mode is enabled.") return } @@ -586,7 +603,7 @@ class SyncService: ObservableObject { // If both client and server have been synced before, use delta sync if hasLocalData && hasServerData && lastSyncTime != nil { - print("iOS SYNC: Using delta sync for incremental updates") + logInfo("iOS SYNC: Using delta sync for incremental updates") try await performDeltaSync(dataManager: dataManager) // Update last sync time @@ -597,32 +614,32 @@ class SyncService: ObservableObject { if !hasLocalData && hasServerData { // Case 1: No local data - do full restore from server - print("iOS SYNC: Case 1 - No local data, performing full restore from server") - print("Syncing images from server first...") + logInfo("iOS SYNC: Case 1 - No local data, performing full restore from server") + logInfo("Syncing images from server first...") let imagePathMapping = try await syncImagesFromServer( backup: serverBackup, dataManager: dataManager) - print("Importing data after images...") + logInfo("Importing data after images...") try importBackupToDataManager( serverBackup, dataManager: dataManager, imagePathMapping: imagePathMapping) - print("Full restore completed") + logInfo("Full restore completed") } else if hasLocalData && !hasServerData { // Case 2: No server data - upload local data to server - print("iOS SYNC: Case 2 - No server data, uploading local data to server") + logInfo("iOS SYNC: Case 2 - No server data, uploading local data to server") let currentBackup = createBackupFromDataManager(dataManager) _ = try await uploadData(currentBackup) - print("Uploading local images to server...") + logInfo("Uploading local images to server...") try await syncImagesToServer(dataManager: dataManager) - print("Initial upload completed") + logInfo("Initial upload completed") } else if hasLocalData && hasServerData { // Case 3: Both have data - use safe merge strategy - print("iOS SYNC: Case 3 - Merging local and server data safely") + logInfo("iOS SYNC: Case 3 - Merging local and server data safely") try await mergeDataSafely( localBackup: localBackup, serverBackup: serverBackup, dataManager: dataManager) - print("Safe merge completed") + logInfo("Safe merge completed") } else { - print("No data to sync") + logInfo("No data to sync") } // Update last sync time @@ -640,7 +657,7 @@ class SyncService: ObservableObject { if let date = formatter.date(from: timestamp) { return Int64(date.timeIntervalSince1970 * 1000) } - print("Failed to parse timestamp: \(timestamp), using 0") + logInfo("Failed to parse timestamp: \(timestamp), using 0") return 0 } @@ -666,12 +683,12 @@ class SyncService: ObservableObject { imageData, filename: consistentFilename) imagePathMapping[serverFilename] = consistentFilename - print("Downloaded and mapped image: \(serverFilename) -> \(consistentFilename)") + logInfo("Downloaded and mapped image: \(serverFilename) -> \(consistentFilename)") } catch SyncError.imageNotFound { - print("Image not found on server: \(serverFilename)") + logInfo("Image not found on server: \(serverFilename)") continue } catch { - print("Failed to download image \(serverFilename): \(error)") + logInfo("Failed to download image \(serverFilename): \(error)") continue } } @@ -704,18 +721,18 @@ class SyncService: ObservableObject { ).path do { try FileManager.default.moveItem(atPath: fullPath, toPath: newPath) - print("Renamed local image: \(filename) -> \(consistentFilename)") + logInfo("Renamed local image: \(filename) -> \(consistentFilename)") // Update problem's image path in memory for consistency } catch { - print("Failed to rename local image, using original: \(error)") + logInfo("Failed to rename local image, using original: \(error)") } } try await uploadImage(filename: consistentFilename, imageData: imageData) - print("Successfully uploaded image: \(consistentFilename)") + logInfo("Successfully uploaded image: \(consistentFilename)") } catch { - print("Failed to upload image \(consistentFilename): \(error)") + logInfo("Failed to upload image \(consistentFilename): \(error)") // Continue with other images even if one fails } } @@ -733,7 +750,7 @@ class SyncService: ObservableObject { !activeSessionIds.contains($0.sessionId) } - print( + logInfo( "iOS SYNC: Excluding \(dataManager.sessions.count - completedSessions.count) active sessions and \(dataManager.attempts.count - completedAttempts.count) active session attempts from sync" ) @@ -808,26 +825,26 @@ class SyncService: ObservableObject { let allDeletions = localDeletions + serverBackup.deletedItems let uniqueDeletions = Array(Set(allDeletions)) - print("Merging gyms...") + logInfo("Merging gyms...") let mergedGyms = mergeGyms( local: dataManager.gyms, server: serverBackup.gyms, deletedItems: uniqueDeletions) - print("Merging problems...") + logInfo("Merging problems...") let mergedProblems = try mergeProblems( local: dataManager.problems, server: serverBackup.problems, imagePathMapping: imagePathMapping, deletedItems: uniqueDeletions) - print("Merging sessions...") + logInfo("Merging sessions...") let mergedSessions = try mergeSessions( local: dataManager.sessions, server: serverBackup.sessions, deletedItems: uniqueDeletions) - print("Merging attempts...") + logInfo("Merging attempts...") let mergedAttempts = try mergeAttempts( local: dataManager.attempts, server: serverBackup.attempts, @@ -887,7 +904,7 @@ class SyncService: ObservableObject { && !allDeletedAttemptIds.contains($0.id.uuidString) } - print( + logInfo( "iOS IMPORT: Preserving \(activeSessions.count) active sessions and \(activeAttempts.count) active attempts during import" ) @@ -977,7 +994,7 @@ class SyncService: ObservableObject { // Restore active sessions and their attempts after import for session in activeSessions { - print("iOS IMPORT: Restoring active session: \(session.id)") + logInfo("iOS IMPORT: Restoring active session: \(session.id)") dataManager.sessions.append(session) if session.id == dataManager.activeSession?.id { dataManager.activeSession = session @@ -997,12 +1014,12 @@ class SyncService: ObservableObject { dataManager.clearDeletedItems() if let data = try? JSONEncoder().encode(backup.deletedItems) { UserDefaults.standard.set(data, forKey: "ascently_deleted_items") - print("iOS IMPORT: Imported \(backup.deletedItems.count) deletion records") + logInfo("iOS IMPORT: Imported \(backup.deletedItems.count) deletion records") } // Update local data state to match imported data timestamp DataStateManager.shared.setLastModified(backup.exportedAt) - print("Data state synchronized to imported timestamp: \(backup.exportedAt)") + logInfo("Data state synchronized to imported timestamp: \(backup.exportedAt)") } catch { throw SyncError.importFailed(error) diff --git a/ios/Ascently/Utils/AppLogger.swift b/ios/Ascently/Utils/AppLogger.swift new file mode 100644 index 0000000..1ac791c --- /dev/null +++ b/ios/Ascently/Utils/AppLogger.swift @@ -0,0 +1,46 @@ +import Foundation + +/// Centralized logging utility for the iOS app. +/// +/// All log output is automatically compiled out in non-debug builds to avoid leaking +/// sensitive information. Use this instead of calling `print` directly. +enum AppLogger { + + enum LogLevel: String { + case debug = "DEBUG" + case info = "INFO" + case warning = "WARN" + case error = "ERROR" + } + + static func debug(_ message: @autoclosure () -> String, tag: String = #fileID) { + log(level: .debug, tag: tag, message: message()) + } + + static func info(_ message: @autoclosure () -> String, tag: String = #fileID) { + log(level: .info, tag: tag, message: message()) + } + + static func warning(_ message: @autoclosure () -> String, tag: String = #fileID) { + log(level: .warning, tag: tag, message: message()) + } + + static func error(_ message: @autoclosure () -> String, tag: String = #fileID) { + log(level: .error, tag: tag, message: message()) + } + + static func log(level: LogLevel, tag: String, message: @autoclosure () -> String) { + #if DEBUG + let lastPath = (tag as NSString).lastPathComponent + let resolvedTag = lastPath.isEmpty ? tag : lastPath + Swift.print("[\(level.rawValue)][\(resolvedTag)] \(message())") + #endif + } +} + +enum LogTag { + static let climbingData = "ClimbingData" + static let dataManagement = "DataManagementSection" + static let exportData = "ExportDataView" + static let syncSection = "SyncSection" +} diff --git a/ios/Ascently/Utils/DataStateManager.swift b/ios/Ascently/Utils/DataStateManager.swift index 307d33d..53c84be 100644 --- a/ios/Ascently/Utils/DataStateManager.swift +++ b/ios/Ascently/Utils/DataStateManager.swift @@ -18,14 +18,17 @@ class DataStateManager { private init() { // Initialize with current timestamp if this is the first time if !isInitialized() { - print("DataStateManager: First time initialization") + AppLogger.info("DataStateManager: First time initialization", tag: "DataState") // Set initial timestamp to a very old date so server data will be considered newer let epochTime = "1970-01-01T00:00:00.000Z" userDefaults.set(epochTime, forKey: Keys.lastModified) markAsInitialized() - print("DataStateManager initialized with epoch timestamp: \(epochTime)") + AppLogger.info( + "DataStateManager initialized with epoch timestamp: \(epochTime)", tag: "DataState") } else { - print("DataStateManager: Already initialized, current timestamp: \(getLastModified())") + AppLogger.info( + "DataStateManager: Already initialized, current timestamp: \(getLastModified())", + tag: "DataState") } } @@ -34,29 +37,32 @@ class DataStateManager { func updateDataState() { let now = ISO8601DateFormatter().string(from: Date()) userDefaults.set(now, forKey: Keys.lastModified) - print("iOS Data state updated to: \(now)") + AppLogger.info("iOS Data state updated to: \(now)", tag: "DataState") } func getLastModified() -> String { if let storedTimestamp = userDefaults.string(forKey: Keys.lastModified) { - print("iOS DataStateManager returning stored timestamp: \(storedTimestamp)") + AppLogger.debug( + "iOS DataStateManager returning stored timestamp: \(storedTimestamp)", + tag: "DataState") return storedTimestamp } let epochTime = "1970-01-01T00:00:00.000Z" - print("No data state timestamp found - returning epoch time: \(epochTime)") + AppLogger.warning( + "No data state timestamp found - returning epoch time: \(epochTime)", tag: "DataState") return epochTime } func setLastModified(_ timestamp: String) { userDefaults.set(timestamp, forKey: Keys.lastModified) - print("Data state set to: \(timestamp)") + AppLogger.info("Data state set to: \(timestamp)", tag: "DataState") } func reset() { userDefaults.removeObject(forKey: Keys.lastModified) userDefaults.removeObject(forKey: Keys.initialized) - print("Data state reset") + AppLogger.info("Data state reset", tag: "DataState") } private func isInitialized() -> Bool { diff --git a/ios/Ascently/Utils/ImageManager.swift b/ios/Ascently/Utils/ImageManager.swift index 22fde9e..5e20ee7 100644 --- a/ios/Ascently/Utils/ImageManager.swift +++ b/ios/Ascently/Utils/ImageManager.swift @@ -5,6 +5,7 @@ import UIKit class ImageManager { static let shared = ImageManager() + private let logTag = "ImageManager" private let thumbnailCache = NSCache() private let fileManager = FileManager.default @@ -30,7 +31,7 @@ class ImageManager { // Final integrity check if !validateStorageIntegrity() { - print("CRITICAL: Storage integrity compromised - attempting emergency recovery") + logError("CRITICAL: Storage integrity compromised - attempting emergency recovery") emergencyImageRestore() } @@ -83,7 +84,7 @@ class ImageManager { return } - print("🔄 Migrating images from OpenClimb to Ascently directory...") + logInfo("🔄 Migrating images from OpenClimb to Ascently directory...") do { // Create parent directory if needed @@ -94,16 +95,16 @@ class ImageManager { // Move the entire directory try fileManager.moveItem(at: legacyDir, to: appSupportDirectory) - print("Successfully migrated image directory from OpenClimb to Ascently") + logInfo("Successfully migrated image directory from OpenClimb to Ascently") } catch { - print("❌ Failed to migrate image directory: \(error)") + logError("Failed to migrate image directory: \(error)") // If move fails, try to copy instead do { try fileManager.copyItem(at: legacyDir, to: appSupportDirectory) - print("Successfully copied image directory from OpenClimb to Ascently") + logInfo("Successfully copied image directory from OpenClimb to Ascently") // Don't remove the old directory in case of issues } catch { - print("❌ Failed to copy image directory: \(error)") + logError("Failed to copy image directory: \(error)") } } } @@ -122,9 +123,9 @@ class ImageManager { attributes: [ .protectionKey: FileProtectionType.completeUntilFirstUserAuthentication ]) - print("Created directory: \(directory.path)") + logInfo("Created directory: \(directory.path)") } catch { - print("ERROR: Failed to create directory \(directory.path): \(error)") + logError("ERROR: Failed to create directory \(directory.path): \(error)") } } } @@ -141,9 +142,9 @@ class ImageManager { var backupURL = backupDirectory try imagesURL.setResourceValues(resourceValues) try backupURL.setResourceValues(resourceValues) - print("Excluded image directories from iCloud backup") + logInfo("Excluded image directories from iCloud backup") } catch { - print("WARNING: Failed to exclude from iCloud backup: \(error)") + logWarning("WARNING: Failed to exclude from iCloud backup: \(error)") } } @@ -167,11 +168,11 @@ class ImageManager { } private func performRobustMigration() { - print("Starting robust image migration system...") + logInfo("Starting robust image migration system...") // Check for interrupted migration if let incompleteState = loadMigrationState() { - print("Detected interrupted migration, resuming...") + logInfo("Detected interrupted migration, resuming...") resumeMigration(from: incompleteState) } else { // Start fresh migration @@ -188,7 +189,7 @@ class ImageManager { private func startNewMigration() { // First check for images in previous Application Support directories if let previousAppSupportImages = findPreviousAppSupportImages() { - print("Found images in previous Application Support directory") + logInfo("Found images in previous Application Support directory") migratePreviousAppSupportImages(from: previousAppSupportImages) return } @@ -198,7 +199,7 @@ class ImageManager { let hasLegacyImportImages = fileManager.fileExists(atPath: legacyImportImagesDirectory.path) guard hasLegacyImages || hasLegacyImportImages else { - print("No legacy images to migrate") + logInfo("No legacy images to migrate") return } @@ -213,7 +214,7 @@ class ImageManager { let legacyFiles = try fileManager.contentsOfDirectory( atPath: legacyImagesDirectory.path) allLegacyFiles.append(contentsOf: legacyFiles) - print("Found \(legacyFiles.count) images in OpenClimbImages") + logInfo("Found \(legacyFiles.count) images in OpenClimbImages") } // Collect files from Documents/images directory @@ -221,10 +222,10 @@ class ImageManager { let importFiles = try fileManager.contentsOfDirectory( atPath: legacyImportImagesDirectory.path) allLegacyFiles.append(contentsOf: importFiles) - print("Found \(importFiles.count) images in Documents/images") + logInfo("Found \(importFiles.count) images in Documents/images") } - print("Total legacy images to migrate: \(allLegacyFiles.count)") + logInfo("Total legacy images to migrate: \(allLegacyFiles.count)") let initialState = MigrationState( version: MigrationState.currentVersion, @@ -239,24 +240,24 @@ class ImageManager { performMigrationWithCheckpoints(files: allLegacyFiles, currentState: initialState) } catch { - print("ERROR: Failed to start migration: \(error)") + logError("ERROR: Failed to start migration: \(error)") } } private func resumeMigration(from state: MigrationState) { - print("Resuming migration from checkpoint...") - print("Progress: \(state.completedFiles.count)/\(state.totalFiles)") + logInfo("Resuming migration from checkpoint...") + logInfo("Progress: \(state.completedFiles.count)/\(state.totalFiles)") do { let legacyFiles = try fileManager.contentsOfDirectory( atPath: legacyImagesDirectory.path) let remainingFiles = legacyFiles.filter { !state.completedFiles.contains($0) } - print("Resuming with \(remainingFiles.count) remaining files") + logInfo("Resuming with \(remainingFiles.count) remaining files") performMigrationWithCheckpoints(files: remainingFiles, currentState: state) } catch { - print("ERROR: Failed to resume migration: \(error)") + logError("ERROR: Failed to resume migration: \(error)") // Fallback: start fresh removeMigrationState() startNewMigration() @@ -323,11 +324,11 @@ class ImageManager { completedFiles.append(fileName) migratedCount += 1 - print("Migrated: \(fileName) (\(migratedCount)/\(currentState.totalFiles))") + logInfo("Migrated: \(fileName) (\(migratedCount)/\(currentState.totalFiles))") } catch { failedCount += 1 - print("ERROR: Failed to migrate \(fileName): \(error)") + logError("ERROR: Failed to migrate \(fileName): \(error)") } // Save checkpoint every 5 files or if interrupted @@ -341,7 +342,7 @@ class ImageManager { lastCheckpoint: Date() ) saveMigrationState(checkpointState) - print("Checkpoint saved: \(completedFiles.count)/\(currentState.totalFiles)") + logInfo("Checkpoint saved: \(completedFiles.count)/\(currentState.totalFiles)") } } } @@ -357,7 +358,7 @@ class ImageManager { ) saveMigrationState(finalState) - print("Migration complete: \(migratedCount) migrated, \(failedCount) failed") + logInfo("Migration complete: \(migratedCount) migrated, \(failedCount) failed") // Clean up legacy directory if no failures if failedCount == 0 { @@ -366,7 +367,7 @@ class ImageManager { } private func verifyMigrationIntegrity() { - print("Verifying migration integrity...") + logInfo("Verifying migration integrity...") var allLegacyFiles = Set() @@ -384,12 +385,12 @@ class ImageManager { allLegacyFiles.formUnion(importFiles) } } catch { - print("ERROR: Failed to read legacy directories: \(error)") + logError("ERROR: Failed to read legacy directories: \(error)") return } guard !allLegacyFiles.isEmpty else { - print("No legacy directories to verify against") + logInfo("No legacy directories to verify against") return } @@ -400,10 +401,10 @@ class ImageManager { let missingFiles = allLegacyFiles.subtracting(migratedFiles) if missingFiles.isEmpty { - print("Migration integrity verified - all files present") + logInfo("Migration integrity verified - all files present") cleanupLegacyDirectory() } else { - print("WARNING: Missing \(missingFiles.count) files, re-triggering migration") + logWarning("WARNING: Missing \(missingFiles.count) files, re-triggering migration") // Re-trigger migration for missing files performMigrationWithCheckpoints( files: Array(missingFiles), @@ -417,16 +418,16 @@ class ImageManager { )) } } catch { - print("ERROR: Failed to verify migration integrity: \(error)") + logError("ERROR: Failed to verify migration integrity: \(error)") } } private func cleanupLegacyDirectory() { do { try fileManager.removeItem(at: legacyImagesDirectory) - print("Cleaned up legacy directory") + logInfo("Cleaned up legacy directory") } catch { - print("WARNING: Failed to clean up legacy directory: \(error)") + logWarning("WARNING: Failed to clean up legacy directory: \(error)") } } @@ -446,16 +447,16 @@ class ImageManager { let data = try Data(contentsOf: migrationStateURL) let state = try JSONDecoder().decode(MigrationState.self, from: data) - // Check if state is too old (more than 1 hour) + // Check if state is too old if Date().timeIntervalSince(state.lastCheckpoint) > 3600 { - print("WARNING: Migration state is stale, starting fresh") + logWarning("WARNING: Migration state is stale, starting fresh") removeMigrationState() return nil } return state.isComplete ? nil : state } catch { - print("ERROR: Failed to load migration state: \(error)") + logError("ERROR: Failed to load migration state: \(error)") removeMigrationState() return nil } @@ -466,7 +467,7 @@ class ImageManager { let data = try JSONEncoder().encode(state) try data.write(to: migrationStateURL) } catch { - print("ERROR: Failed to save migration state: \(error)") + logError("ERROR: Failed to save migration state: \(error)") } } @@ -482,7 +483,7 @@ class ImageManager { private func cleanupMigrationState() { try? fileManager.removeItem(at: migrationStateURL) try? fileManager.removeItem(at: migrationLockURL) - print("Cleaned up migration state files") + logInfo("Cleaned up migration state files") } func saveImageData(_ data: Data, withName name: String? = nil) -> String? { @@ -497,10 +498,10 @@ class ImageManager { // Create backup copy try data.write(to: backupPath) - print("Saved image with backup: \(fileName)") + logInfo("Saved image with backup: \(fileName)") return fileName } catch { - print("ERROR: Failed to save image \(fileName): \(error)") + logError("ERROR: Failed to save image \(fileName): \(error)") return nil } } @@ -520,7 +521,7 @@ class ImageManager { if fileManager.fileExists(atPath: backupPath.path), let data = try? Data(contentsOf: backupPath) { - print("Restored image from backup: \(path)") + logInfo("Restored image from backup: \(path)") // Restore to primary location try? data.write(to: URL(fileURLWithPath: primaryPath)) @@ -595,7 +596,7 @@ class ImageManager { do { try fileManager.removeItem(atPath: primaryPath) } catch { - print("ERROR: Failed to delete primary image at \(primaryPath): \(error)") + logError("ERROR: Failed to delete primary image at \(primaryPath): \(error)") success = false } } @@ -605,7 +606,7 @@ class ImageManager { do { try fileManager.removeItem(at: backupPath) } catch { - print("ERROR: Failed to delete backup image at \(backupPath.path): \(error)") + logError("ERROR: Failed to delete backup image at \(backupPath.path): \(error)") success = false } } @@ -642,7 +643,7 @@ class ImageManager { } func performMaintenance() { - print("Starting image maintenance...") + logInfo("Starting image maintenance...") syncBackups() validateImageIntegrity() @@ -660,11 +661,11 @@ class ImageManager { let backupPath = backupDirectory.appendingPathComponent(fileName) try? fileManager.copyItem(at: primaryPath, to: backupPath) - print("Created missing backup for: \(fileName)") + logInfo("Created missing backup for: \(fileName)") } } } catch { - print("ERROR: Failed to sync backups: \(error)") + logError("ERROR: Failed to sync backups: \(error)") } } @@ -683,14 +684,14 @@ class ImageManager { } } - print("Validated \(validFiles) of \(files.count) image files") + logInfo("Validated \(validFiles) of \(files.count) image files") } catch { - print("ERROR: Failed to validate images: \(error)") + logError("ERROR: Failed to validate images: \(error)") } } private func cleanupOrphanedFiles() { - print("Cleanup would require coordination with data manager") + logInfo("Cleanup would require coordination with data manager") } func getStorageInfo() -> (primaryCount: Int, backupCount: Int, totalSize: Int64) { @@ -718,7 +719,7 @@ class ImageManager { private func logDirectoryInfo() { let info = getStorageInfo() let previousDir = findPreviousAppSupportImages() - print( + logInfo( """ Ascently Image Storage: - App Support: \(appSupportDirectory.path) @@ -732,7 +733,7 @@ class ImageManager { } func forceRecoveryMigration() { - print("FORCE RECOVERY: Starting manual migration recovery...") + logInfo("FORCE RECOVERY: Starting manual migration recovery...") // Remove any stale state removeMigrationState() @@ -741,7 +742,7 @@ class ImageManager { // Force fresh migration startNewMigration() - print("FORCE RECOVERY: Migration recovery completed") + logInfo("FORCE RECOVERY: Migration recovery completed") } func saveImportedImage(_ imageData: Data, filename: String) throws -> String { @@ -754,12 +755,12 @@ class ImageManager { // Create backup try? imageData.write(to: backupPath) - print("Imported image: \(filename)") + logInfo("Imported image: \(filename)") return filename } func emergencyImageRestore() { - print("EMERGENCY: Attempting image restoration...") + logError("EMERGENCY: Attempting image restoration...") // Try to restore from backup directory do { @@ -777,14 +778,14 @@ class ImageManager { } } - print("EMERGENCY: Restored \(restoredCount) images from backup") + logError("EMERGENCY: Restored \(restoredCount) images from backup") } catch { - print("EMERGENCY: Failed to restore from backup: \(error)") + logError("EMERGENCY: Failed to restore from backup: \(error)") } // Try previous Application Support directories first if let previousAppSupportImages = findPreviousAppSupportImages() { - print("EMERGENCY: Found previous Application Support images, migrating...") + logError("EMERGENCY: Found previous Application Support images, migrating...") migratePreviousAppSupportImages(from: previousAppSupportImages) return } @@ -793,23 +794,21 @@ class ImageManager { if fileManager.fileExists(atPath: legacyImagesDirectory.path) || fileManager.fileExists(atPath: legacyImportImagesDirectory.path) { - print("EMERGENCY: Attempting legacy migration as fallback...") + logError("EMERGENCY: Attempting legacy migration as fallback...") forceRecoveryMigration() } } func debugSafeInitialization() -> Bool { - print("DEBUG SAFE: Performing debug-safe initialization check...") + logDebug("DEBUG SAFE: Performing debug-safe initialization check...") // Check if we're in a debug environment #if DEBUG - print("DEBUG SAFE: Debug environment detected") + logDebug("DEBUG SAFE: Debug environment detected") - // Check for interrupted migration more aggressively if fileManager.fileExists(atPath: migrationLockURL.path) { - print("DEBUG SAFE: Found migration lock - likely debug interruption") + logDebug("DEBUG SAFE: Found migration lock - likely debug interruption") - // Give extra time for file system to stabilize Thread.sleep(forTimeInterval: 1.0) // Try emergency recovery @@ -829,14 +828,14 @@ class ImageManager { ((try? fileManager.contentsOfDirectory(atPath: backupDirectory.path)) ?? []).count > 0 if primaryEmpty && backupHasFiles { - print("DEBUG SAFE: Primary empty but backup exists - restoring") + logDebug("DEBUG SAFE: Primary empty but backup exists - restoring") emergencyImageRestore() return true } // Check if primary storage is empty but previous Application Support images exist if primaryEmpty, let previousAppSupportImages = findPreviousAppSupportImages() { - print("DEBUG SAFE: Primary empty but found previous Application Support images") + logDebug("DEBUG SAFE: Primary empty but found previous Application Support images") migratePreviousAppSupportImages(from: previousAppSupportImages) return true } @@ -852,7 +851,7 @@ class ImageManager { // Check if we have more backups than primary files (sign of corruption) if backupFiles.count > primaryFiles.count + 5 { - print( + logInfo( "WARNING INTEGRITY: Backup count significantly exceeds primary - potential corruption" ) return false @@ -860,7 +859,7 @@ class ImageManager { // Check if primary is completely empty but we have data elsewhere if primaryFiles.isEmpty && !backupFiles.isEmpty { - print("WARNING INTEGRITY: Primary storage empty but backups exist") + logWarning("WARNING INTEGRITY: Primary storage empty but backups exist") return false } @@ -874,7 +873,7 @@ class ImageManager { for: .applicationSupportDirectory, in: .userDomainMask ).first else { - print("ERROR: Could not access Application Support directory") + logError("ERROR: Could not access Application Support directory") return nil } @@ -908,13 +907,13 @@ class ImageManager { } } } catch { - print("ERROR: Error scanning for previous Application Support directories: \(error)") + logError("ERROR: Error scanning for previous Application Support directories: \(error)") } return nil } private func migratePreviousAppSupportImages(from sourceDirectory: URL) { - print("Migrating images from previous Application Support directory") + logInfo("Migrating images from previous Application Support directory") do { let imageFiles = try fileManager.contentsOfDirectory(atPath: sourceDirectory.path) @@ -937,18 +936,33 @@ class ImageManager { // Create backup try? fileManager.copyItem(at: sourcePath, to: backupPath) - print("Migrated: \(fileName)") + logInfo("Migrated: \(fileName)") } catch { - print("ERROR: Failed to migrate \(fileName): \(error)") + logError("ERROR: Failed to migrate \(fileName): \(error)") } } } - print("Completed migration from previous Application Support directory") + logInfo("Completed migration from previous Application Support directory") } catch { - print("ERROR: Failed to migrate from previous Application Support: \(error)") + logError("ERROR: Failed to migrate from previous Application Support: \(error)") } } + private func logInfo(_ message: String) { + AppLogger.info(message, tag: logTag) + } + + private func logWarning(_ message: String) { + AppLogger.warning(message, tag: logTag) + } + + private func logError(_ message: String) { + AppLogger.error(message, tag: logTag) + } + + private func logDebug(_ message: String) { + AppLogger.debug(message, tag: logTag) + } } diff --git a/ios/Ascently/Utils/ZipUtils.swift b/ios/Ascently/Utils/ZipUtils.swift index d47b1bf..9ca275f 100644 --- a/ios/Ascently/Utils/ZipUtils.swift +++ b/ios/Ascently/Utils/ZipUtils.swift @@ -4,6 +4,8 @@ import zlib struct ZipUtils { + private static let logTag = "ZipUtils" + private static let DATA_JSON_FILENAME = "data.json" private static let IMAGES_DIR_NAME = "images" private static let METADATA_FILENAME = "metadata.txt" @@ -49,7 +51,7 @@ struct ZipUtils { ) // Process images in batches for better performance - print("Processing \(referencedImagePaths.count) images for export") + logInfo("Processing \(referencedImagePaths.count) images for export") var successfulImages = 0 let batchSize = 10 let sortedPaths = Array(referencedImagePaths).sorted() @@ -59,7 +61,7 @@ struct ZipUtils { for (index, imagePath) in sortedPaths.enumerated() { if index % batchSize == 0 { - print("Processing images \(index)/\(sortedPaths.count)") + logInfo("Processing images \(index)/\(sortedPaths.count)") } let imageURL = URL(fileURLWithPath: imagePath) @@ -83,11 +85,11 @@ struct ZipUtils { successfulImages += 1 } } catch { - print("Failed to read image: \(imageName)") + logWarning("Failed to read image: \(imageName)") } } - print("Export: included \(successfulImages)/\(referencedImagePaths.count) images") + logInfo("Export: included \(successfulImages)/\(referencedImagePaths.count) images") // Build central directory centralDirectory.reserveCapacity(fileEntries.count * 100) // Estimate 100 bytes per entry @@ -114,7 +116,7 @@ struct ZipUtils { } static func extractImportZip(data: Data) throws -> ImportResult { - print("Starting ZIP extraction - data size: \(data.count) bytes") + logInfo("Starting ZIP extraction - data size: \(data.count) bytes") return try extractUsingCustomParser(data: data) } @@ -127,10 +129,10 @@ struct ZipUtils { let zipEntries: [ZipEntry] do { zipEntries = try parseZipFile(data: data) - print("Successfully parsed ZIP file with \(zipEntries.count) entries") + logInfo("Successfully parsed ZIP file with \(zipEntries.count) entries") } catch { - print("Failed to parse ZIP file: \(error)") - print( + logError("Failed to parse ZIP file: \(error)") + logError( "ZIP data header: \(data.prefix(20).map { String(format: "%02X", $0) }.joined(separator: " "))" ) throw NSError( @@ -142,24 +144,24 @@ struct ZipUtils { ) } - print("Found \(zipEntries.count) entries in ZIP file:") + logInfo("Found \(zipEntries.count) entries in ZIP file:") for entry in zipEntries { - print(" - \(entry.filename) (size: \(entry.data.count) bytes)") + logInfo(" - \(entry.filename) (size: \(entry.data.count) bytes)") } for entry in zipEntries { switch entry.filename { case METADATA_FILENAME: metadataContent = String(data: entry.data, encoding: .utf8) ?? "" - print("Found metadata: \(metadataContent.prefix(100))...") + logInfo("Found metadata: \(metadataContent.prefix(100))...") case DATA_JSON_FILENAME: jsonContent = String(data: entry.data, encoding: .utf8) ?? "" - print("Found data.json with \(jsonContent.count) characters") + logInfo("Found data.json with \(jsonContent.count) characters") if jsonContent.isEmpty { - print("WARNING: data.json is empty!") + logWarning("WARNING: data.json is empty!") } else { - print("data.json preview: \(jsonContent.prefix(200))...") + logInfo("data.json preview: \(jsonContent.prefix(200))...") } default: @@ -173,17 +175,17 @@ struct ZipUtils { entry.data, filename: originalFilename) importedImagePaths[originalFilename] = filename } catch { - print("Failed to import image \(originalFilename): \(error)") + logError("Failed to import image \(originalFilename): \(error)") } } } } guard !jsonContent.isEmpty else { - print("ERROR: data.json not found or empty") - print("Available files in ZIP:") + logError("ERROR: data.json not found or empty") + logInfo("Available files in ZIP:") for entry in zipEntries { - print(" - \(entry.filename)") + logInfo(" - \(entry.filename)") } throw NSError( domain: "ImportError", code: 1, @@ -194,13 +196,25 @@ struct ZipUtils { ) } - print("Import extraction completed: \(importedImagePaths.count) images processed") + logInfo("Import extraction completed: \(importedImagePaths.count) images processed") return ImportResult( jsonData: jsonContent.data(using: .utf8) ?? Data(), imagePathMapping: importedImagePaths ) } + private static func logInfo(_ message: String) { + AppLogger.info(message, tag: logTag) + } + + private static func logWarning(_ message: String) { + AppLogger.warning(message, tag: logTag) + } + + private static func logError(_ message: String) { + AppLogger.error(message, tag: logTag) + } + private static func createMetadata( exportData: ClimbDataBackup, referencedImagePaths: Set diff --git a/ios/Ascently/ViewModels/ClimbingDataManager.swift b/ios/Ascently/ViewModels/ClimbingDataManager.swift index 67d3f96..1779540 100644 --- a/ios/Ascently/ViewModels/ClimbingDataManager.swift +++ b/ios/Ascently/ViewModels/ClimbingDataManager.swift @@ -38,7 +38,6 @@ class ClimbingDataManager: ObservableObject { let healthKitService = HealthKitService.shared @Published var isSyncing = false - private enum Keys { static let gyms = "ascently_gyms" static let problems = "ascently_problems" @@ -115,7 +114,8 @@ class ClimbingDataManager: ObservableObject { return } - print("Starting migration from OpenClimb to Ascently keys...") + AppLogger.info( + "Starting migration from OpenClimb to Ascently keys...", tag: LogTag.climbingData) var migrationCount = 0 // Migrate each data type if it exists in old format but not in new format @@ -135,7 +135,7 @@ class ClimbingDataManager: ObservableObject { userDefaults.set(oldData, forKey: newKey) userDefaults.removeObject(forKey: oldKey) migrationCount += 1 - print("✅ Migrated: \(oldKey) → \(newKey)") + AppLogger.info("Migrated: \(oldKey) → \(newKey)", tag: LogTag.climbingData) } } @@ -147,7 +147,8 @@ class ClimbingDataManager: ObservableObject { { sharedDefaults.set(oldData, forKey: newKey) sharedDefaults.removeObject(forKey: oldKey) - print("✅ Migrated shared: \(oldKey) → \(newKey)") + AppLogger.info( + "Migrated shared: \(oldKey) → \(newKey)", tag: LogTag.climbingData) } } } @@ -161,18 +162,19 @@ class ClimbingDataManager: ObservableObject { userDefaults.set(lastModified, forKey: newDataStateKey) userDefaults.removeObject(forKey: legacyDataStateKey) migrationCount += 1 - print("✅ Migrated data state timestamp") + AppLogger.info("Migrated data state timestamp", tag: LogTag.climbingData) } // Mark migration as completed userDefaults.set(true, forKey: migrationKey) if migrationCount > 0 { - print( - "Migration completed! Migrated \(migrationCount) data items from OpenClimb to Ascently" + AppLogger.info( + "Migration completed! Migrated \(migrationCount) data items from OpenClimb to Ascently", + tag: LogTag.climbingData ) } else { - print("No OpenClimb data found to migrate") + AppLogger.info("No OpenClimb data found to migrate", tag: LogTag.climbingData) } } @@ -441,7 +443,9 @@ class ClimbingDataManager: ObservableObject { startDate: newSession.startTime ?? Date(), sessionId: newSession.id) } catch { - print("Failed to start HealthKit workout: \(error.localizedDescription)") + AppLogger.error( + "Failed to start HealthKit workout: \(error.localizedDescription)", + tag: LogTag.climbingData) } } } @@ -477,7 +481,9 @@ class ClimbingDataManager: ObservableObject { try await healthKitService.endWorkout( endDate: completedSession.endTime ?? Date()) } catch { - print("Failed to end HealthKit workout: \(error.localizedDescription)") + AppLogger.error( + "Failed to end HealthKit workout: \(error.localizedDescription)", + tag: LogTag.climbingData) } } } @@ -667,7 +673,9 @@ class ClimbingDataManager: ObservableObject { } if !orphanedAttempts.isEmpty { - print("🧹 Cleaning up \(orphanedAttempts.count) orphaned attempts") + AppLogger.info( + "🧹 Cleaning up \(orphanedAttempts.count) orphaned attempts", + tag: LogTag.climbingData) // Track these as deleted to prevent sync from re-introducing them for attempt in orphanedAttempts { @@ -693,14 +701,15 @@ class ClimbingDataManager: ObservableObject { if seenAttempts.contains(key) { duplicateIds.append(attempt.id) - print("🧹 Found duplicate attempt: \(attempt.id)") + AppLogger.info("🧹 Found duplicate attempt: \(attempt.id)", tag: LogTag.climbingData) } else { seenAttempts.insert(key) } } if !duplicateIds.isEmpty { - print("🧹 Removing \(duplicateIds.count) duplicate attempts") + AppLogger.info( + "🧹 Removing \(duplicateIds.count) duplicate attempts", tag: LogTag.climbingData) // Track duplicates as deleted for attemptId in duplicateIds { @@ -714,8 +723,9 @@ class ClimbingDataManager: ObservableObject { if initialAttemptCount != attempts.count { saveAttempts() let removedCount = initialAttemptCount - attempts.count - print( - "Cleanup complete. Removed \(removedCount) attempts. Remaining: \(attempts.count)" + AppLogger.info( + "Cleanup complete. Removed \(removedCount) attempts. Remaining: \(attempts.count)", + tag: LogTag.climbingData ) } @@ -725,7 +735,9 @@ class ClimbingDataManager: ObservableObject { } if !orphanedProblems.isEmpty { - print("🧹 Cleaning up \(orphanedProblems.count) orphaned problems") + AppLogger.info( + "🧹 Cleaning up \(orphanedProblems.count) orphaned problems", + tag: LogTag.climbingData) for problem in orphanedProblems { trackDeletion(itemId: problem.id.uuidString, itemType: "problem") @@ -744,7 +756,9 @@ class ClimbingDataManager: ObservableObject { } if !orphanedSessions.isEmpty { - print("🧹 Cleaning up \(orphanedSessions.count) orphaned sessions") + AppLogger.info( + "🧹 Cleaning up \(orphanedSessions.count) orphaned sessions", + tag: LogTag.climbingData) for session in orphanedSessions { trackDeletion(itemId: session.id.uuidString, itemType: "session") @@ -844,19 +858,29 @@ class ClimbingDataManager: ObservableObject { let problemsForImages = problems // Move heavy I/O operations to background thread + let logTag = LogTag.climbingData let zipData = try await Task.detached(priority: .userInitiated) { // Collect actual image paths from disk for the ZIP - let referencedImagePaths = await Self.collectReferencedImagePathsStatic( + let imageSummary = Self.collectReferencedImagePathsStatic( problems: problemsForImages, imagesDirectory: imagesDirectory) - print("Starting export with \(referencedImagePaths.count) images") + let referencedImagePaths = imageSummary.paths + + await MainActor.run { + AppLogger.info( + "Starting export with \(referencedImagePaths.count) images (\(imageSummary.missingCount) missing)", + tag: logTag) + } let zipData = try await ZipUtils.createExportZip( exportData: exportData, referencedImagePaths: referencedImagePaths ) - print("Export completed successfully") + await MainActor.run { + AppLogger.info("Export completed successfully", tag: logTag) + } + return (zipData, referencedImagePaths.count) }.value @@ -865,7 +889,7 @@ class ClimbingDataManager: ObservableObject { return zipData.0 } catch { let errorMessage = "Export failed: \(error.localizedDescription)" - print("ERROR: \(errorMessage)") + AppLogger.error("ERROR: \(errorMessage)", tag: LogTag.climbingData) setError(errorMessage) return nil } @@ -894,16 +918,24 @@ class ClimbingDataManager: ObservableObject { return Date() } - print("Raw JSON content preview:") - print(String(decoding: importResult.jsonData.prefix(500), as: UTF8.self) + "...") + AppLogger.debug("Raw JSON content preview:", tag: LogTag.climbingData) + AppLogger.debug( + String(decoding: importResult.jsonData.prefix(500), as: UTF8.self) + "...", + tag: LogTag.climbingData + ) let importData = try decoder.decode(ClimbDataBackup.self, from: importResult.jsonData) - print("Successfully decoded import data:") - print("- Gyms: \(importData.gyms.count)") - print("- Problems: \(importData.problems.count)") - print("- Sessions: \(importData.sessions.count)") - print("- Attempts: \(importData.attempts.count)") + AppLogger.info( + """ + Successfully decoded import data: + - Gyms: \(importData.gyms.count) + - Problems: \(importData.problems.count) + - Sessions: \(importData.sessions.count) + - Attempts: \(importData.attempts.count) + """, + tag: LogTag.climbingData + ) try validateImportData(importData) @@ -960,14 +992,20 @@ class ClimbingDataManager: ObservableObject { extension ClimbingDataManager { private func collectReferencedImagePaths() -> Set { let imagesDirectory = ImageManager.shared.imagesDirectory.path - return Self.collectReferencedImagePathsStatic( + let result = Self.collectReferencedImagePathsStatic( problems: problems, imagesDirectory: imagesDirectory) + + AppLogger.info( + "Export: Collected \(result.paths.count) images (\(result.missingCount) missing)", + tag: LogTag.climbingData) + + return result.paths } - private static func collectReferencedImagePathsStatic( + nonisolated private static func collectReferencedImagePathsStatic( problems: [Problem], imagesDirectory: String - ) -> Set { + ) -> (paths: Set, missingCount: Int) { var imagePaths = Set() var missingCount = 0 @@ -988,8 +1026,7 @@ extension ClimbingDataManager { } } - print("Export: Collected \(imagePaths.count) images (\(missingCount) missing)") - return imagePaths + return (imagePaths, missingCount) } private func updateProblemImagePaths( @@ -1030,11 +1067,14 @@ extension ClimbingDataManager { } deterministicImagePaths.append(deterministicName) - print("Renamed imported image: \(tempFileName) → \(deterministicName)") + AppLogger.debug( + "Renamed imported image: \(tempFileName) → \(deterministicName)", + tag: LogTag.climbingData) } } catch { - print( - "Failed to rename imported image \(tempFileName) to \(deterministicName): \(error)" + AppLogger.error( + "Failed to rename imported image \(tempFileName) to \(deterministicName): \(error)", + tag: LogTag.climbingData ) deterministicImagePaths.append(tempFileName) } @@ -1078,7 +1118,8 @@ extension ClimbingDataManager { if needsUpdate { problems = updatedProblems saveProblems() - print("Migrated image paths for \(problems.count) problems") + AppLogger.info( + "Migrated image paths for \(problems.count) problems", tag: LogTag.climbingData) } } @@ -1089,8 +1130,9 @@ extension ClimbingDataManager { // Log storage information for debugging let info = await ImageManager.shared.getStorageInfo() - print( - "Image Storage: \(info.primaryCount) primary, \(info.backupCount) backup, \(info.totalSize / 1024)KB total" + await AppLogger.debug( + "Image Storage: \(info.primaryCount) primary, \(info.backupCount) backup, \(info.totalSize / 1024)KB total", + tag: LogTag.climbingData ) }.value } @@ -1128,7 +1170,9 @@ extension ClimbingDataManager { } if !orphanedFiles.isEmpty { - print("Cleaned up \(orphanedFiles.count) orphaned image files") + AppLogger.info( + "Cleaned up \(orphanedFiles.count) orphaned image files", + tag: LogTag.climbingData) } } } @@ -1145,7 +1189,7 @@ extension ClimbingDataManager { } func forceImageRecovery() { - print("User initiated force image recovery") + AppLogger.info("User initiated force image recovery", tag: LogTag.climbingData) ImageManager.shared.forceRecoveryMigration() // Refresh the UI after recovery @@ -1153,7 +1197,7 @@ extension ClimbingDataManager { } func emergencyImageRestore() { - print("User initiated emergency image restore") + AppLogger.info("User initiated emergency image restore", tag: LogTag.climbingData) ImageManager.shared.emergencyImageRestore() // Refresh the UI after restore @@ -1179,15 +1223,15 @@ extension ClimbingDataManager { } func testLiveActivity() { - print("🧪 Testing Live Activity functionality...") + AppLogger.info("Testing Live Activity functionality...", tag: LogTag.climbingData) // Check Live Activity availability let status = LiveActivityManager.shared.checkLiveActivityAvailability() - print(status) + AppLogger.info(status, tag: LogTag.climbingData) // Test with dummy data if we have a gym guard let testGym = gyms.first else { - print("ERROR: No gyms available for testing") + AppLogger.error("No gyms available for testing", tag: LogTag.climbingData) return } @@ -1218,15 +1262,18 @@ extension ClimbingDataManager { // Only restart if session is actually active guard activeSession.status == .active else { - print( - "WARNING: Session exists but is not active (status: \(activeSession.status)), ending Live Activity" + AppLogger.warning( + "Session exists but is not active (status: \(activeSession.status)), ending Live Activity", + tag: LogTag.climbingData ) await LiveActivityManager.shared.endLiveActivity() return } if let gym = gym(withId: activeSession.gymId) { - print("Checking Live Activity for active session at \(gym.name)") + AppLogger.info( + "Checking Live Activity for active session at \(gym.name)", tag: LogTag.climbingData + ) // First cleanup any dismissed activities await LiveActivityManager.shared.cleanupDismissedActivities() @@ -1241,7 +1288,9 @@ extension ClimbingDataManager { /// Call this when app becomes active to check for Live Activity restart func onAppBecomeActive() { - print("App became active - checking Live Activity status") + let logTag = "ClimbingData" + AppLogger.info( + "App became active - checking Live Activity status", tag: logTag) Task { await checkAndRestartLiveActivity() } @@ -1249,35 +1298,46 @@ extension ClimbingDataManager { /// Call this when app enters background to update Live Activity func onAppEnterBackground() { - print("App entering background - updating Live Activity if needed") + let logTag = "ClimbingData" + AppLogger.info( + "App entering background - updating Live Activity if needed", tag: logTag) Task { await updateLiveActivityData() } } /// Setup notifications for Live Activity events - private func setupLiveActivityNotifications() { + nonisolated private func setupLiveActivityNotifications() { + let notificationName = Notification.Name("liveActivityDismissed") + let logTag = "ClimbingData" + liveActivityObserver = NotificationCenter.default.addObserver( - forName: .liveActivityDismissed, + forName: notificationName, object: nil, queue: .main ) { [weak self] _ in - print("🔔 Received Live Activity dismissed notification - attempting restart") Task { @MainActor in + AppLogger.info( + "Received Live Activity dismissed notification - attempting restart", + tag: logTag) await self?.handleLiveActivityDismissed() } } } - private func setupMigrationNotifications() { + nonisolated private func setupMigrationNotifications() { + let logTag = "ClimbingData" + migrationObserver = NotificationCenter.default.addObserver( forName: NSNotification.Name("ImageMigrationCompleted"), object: nil, queue: .main ) { [weak self] notification in if let updateCount = notification.userInfo?["updateCount"] as? Int { - print("🔔 Image migration completed with \(updateCount) updates - reloading data") Task { @MainActor in + AppLogger.info( + "Image migration completed with \(updateCount) updates - reloading data", + tag: logTag) self?.loadProblems() } } @@ -1293,7 +1353,9 @@ extension ClimbingDataManager { return } - print("Attempting to restart dismissed Live Activity for \(gym.name)") + AppLogger.info( + "Attempting to restart dismissed Live Activity for \(gym.name)", + tag: LogTag.climbingData) // Wait a bit before restarting to avoid frequency limits try? await Task.sleep(nanoseconds: 2_000_000_000) // 2 seconds @@ -1333,11 +1395,20 @@ extension ClimbingDataManager { activeSession.status == .active, let gym = gym(withId: activeSession.gymId) else { - print("WARNING: Live Activity update skipped - no active session or gym") + AppLogger.warning( + "Live Activity update skipped - no active session or gym", + tag: LogTag.climbingData + ) if let session = activeSession { - print(" Session ID: \(session.id)") - print(" Session Status: \(session.status)") - print(" Gym ID: \(session.gymId)") + AppLogger.debug( + """ + Skipped session details: + Session ID: \(session.id) + Session Status: \(session.status) + Gym ID: \(session.gymId) + """, + tag: LogTag.climbingData + ) } return } @@ -1357,14 +1428,17 @@ extension ClimbingDataManager { elapsedInterval = 0 } - print("Live Activity Update Debug:") - print(" Session ID: \(activeSession.id)") - print(" Gym: \(gym.name)") - print(" Total attempts in session: \(totalAttempts)") - print(" Completed problems: \(completedProblems)") - print(" Elapsed time: \(elapsedInterval) seconds") - print( - " All attempts for session: \(attemptsForSession.map { "\($0.result) - Problem: \($0.problemId)" })" + AppLogger.debug( + """ + Live Activity Update Debug: + Session ID: \(activeSession.id) + Gym: \(gym.name) + Total attempts in session: \(totalAttempts) + Completed problems: \(completedProblems) + Elapsed time: \(elapsedInterval) seconds + All attempts for session: \(attemptsForSession.map { "\($0.result) - Problem: \($0.problemId)" }) + """, + tag: LogTag.climbingData ) Task { diff --git a/ios/Ascently/ViewModels/LiveActivityManager.swift b/ios/Ascently/ViewModels/LiveActivityManager.swift index 60ad654..3bce362 100644 --- a/ios/Ascently/ViewModels/LiveActivityManager.swift +++ b/ios/Ascently/ViewModels/LiveActivityManager.swift @@ -8,6 +8,7 @@ extension Notification.Name { @MainActor final class LiveActivityManager { static let shared = LiveActivityManager() + private static let logTag = "LiveActivity" private init() {} nonisolated(unsafe) private var currentActivity: Activity? @@ -30,11 +31,12 @@ final class LiveActivityManager { let isStillActive = activities.contains { $0.id == currentActivity.id } if isStillActive { - print("Live Activity still running: \(currentActivity.id)") + AppLogger.debug("Live Activity still running: \(currentActivity.id)", tag: Self.logTag) return } else { - print( - "WARNING: Tracked Live Activity \(currentActivity.id) was dismissed, clearing reference" + AppLogger.warning( + "Tracked Live Activity \(currentActivity.id) was dismissed, clearing reference", + tag: Self.logTag ) self.currentActivity = nil } @@ -43,18 +45,18 @@ final class LiveActivityManager { // Check if there are ANY active Live Activities for this session let existingActivities = Activity.activities if let existingActivity = existingActivities.first { - print("Found existing Live Activity: \(existingActivity.id), using it") + AppLogger.info("Found existing Live Activity: \(existingActivity.id), using it", tag: Self.logTag) self.currentActivity = existingActivity return } - print("No Live Activity found, restarting for existing session") + AppLogger.info("No Live Activity found, restarting for existing session", tag: Self.logTag) await startLiveActivity(for: activeSession, gymName: gymName) } /// Call this when a ClimbSession starts to begin a Live Activity func startLiveActivity(for session: ClimbSession, gymName: String) async { - print("Starting Live Activity for gym: \(gymName)") + AppLogger.info("Starting Live Activity for gym: \(gymName)", tag: Self.logTag) await endLiveActivity() @@ -80,18 +82,26 @@ final class LiveActivityManager { pushType: nil ) self.currentActivity = activity - print("Live Activity started successfully: \(activity.id)") + AppLogger.info("Live Activity started successfully: \(activity.id)", tag: Self.logTag) } catch { - print("ERROR: Failed to start live activity: \(error)") - print("Error details: \(error.localizedDescription)") + AppLogger.error( + """ + Failed to start live activity: \(error) + Details: \(error.localizedDescription) + """, + tag: Self.logTag + ) // Check specific error types if error.localizedDescription.contains("authorization") { - print("Authorization error - check Live Activity permissions in Settings") + AppLogger.warning( + "Authorization error - check Live Activity permissions in Settings", + tag: Self.logTag + ) } else if error.localizedDescription.contains("content") { - print("Content error - check ActivityAttributes structure") + AppLogger.warning("Content error - check ActivityAttributes structure", tag: Self.logTag) } else if error.localizedDescription.contains("frequencyLimited") { - print("Frequency limited - too many Live Activities started recently") + AppLogger.warning("Frequency limited - too many Live Activities started recently", tag: Self.logTag) } } } @@ -100,7 +110,7 @@ final class LiveActivityManager { func updateLiveActivity(elapsed: TimeInterval, totalAttempts: Int, completedProblems: Int) async { guard let currentActivity = currentActivity else { - print("WARNING: No current activity to update") + AppLogger.warning("No current activity to update", tag: Self.logTag) return } @@ -109,15 +119,17 @@ final class LiveActivityManager { let isStillActive = activities.contains { $0.id == currentActivity.id } if !isStillActive { - print( - "WARNING: Tracked Live Activity \(currentActivity.id) is no longer active, clearing reference" + AppLogger.warning( + "Tracked Live Activity \(currentActivity.id) is no longer active, clearing reference", + tag: Self.logTag ) self.currentActivity = nil return } - print( - "Updating Live Activity - Attempts: \(totalAttempts), Completed: \(completedProblems)" + AppLogger.debug( + "Updating Live Activity - Attempts: \(totalAttempts), Completed: \(completedProblems)", + tag: Self.logTag ) let updatedContentState = SessionActivityAttributes.ContentState( @@ -137,26 +149,26 @@ final class LiveActivityManager { // First end the tracked activity if it exists if let currentActivity { - print("Ending tracked Live Activity: \(currentActivity.id)") + AppLogger.info("Ending tracked Live Activity: \(currentActivity.id)", tag: Self.logTag) nonisolated(unsafe) let activity = currentActivity await activity.end(nil, dismissalPolicy: .immediate) self.currentActivity = nil - print("Tracked Live Activity ended successfully") + AppLogger.info("Tracked Live Activity ended successfully", tag: Self.logTag) } // Force end ALL active activities of our type to ensure cleanup - print("Checking for any remaining active activities...") + AppLogger.debug("Checking for any remaining active activities...", tag: Self.logTag) let activities = Activity.activities if activities.isEmpty { - print("No additional activities found") + AppLogger.debug("No additional activities found", tag: Self.logTag) } else { - print("Found \(activities.count) additional active activities, ending them...") + AppLogger.info("Found \(activities.count) additional active activities, ending them...", tag: Self.logTag) for activity in activities { - print("Force ending activity: \(activity.id)") + AppLogger.debug("Force ending activity: \(activity.id)", tag: Self.logTag) await activity.end(nil, dismissalPolicy: .immediate) } - print("All Live Activities ended successfully") + AppLogger.info("All Live Activities ended successfully", tag: Self.logTag) } } @@ -174,7 +186,7 @@ final class LiveActivityManager { • All Active Activities: \(allActivities.count) """ - print(message) + AppLogger.info(message, tag: Self.logTag) return message } @@ -185,7 +197,7 @@ final class LiveActivityManager { if let currentActivity = currentActivity { let isStillActive = activities.contains { $0.id == currentActivity.id } if !isStillActive { - print("Cleaning up dismissed Live Activity: \(currentActivity.id)") + AppLogger.info("Cleaning up dismissed Live Activity: \(currentActivity.id)", tag: Self.logTag) self.currentActivity = nil } } @@ -195,7 +207,7 @@ final class LiveActivityManager { func startHealthChecks() { stopHealthChecks() // Stop any existing timer - print("🩺 Starting Live Activity health checks") + AppLogger.debug("🩺 Starting Live Activity health checks", tag: Self.logTag) healthCheckTimer = Timer.scheduledTimer(withTimeInterval: 30.0, repeats: true) { [weak self] _ in Task { @MainActor [weak self] in @@ -208,7 +220,7 @@ final class LiveActivityManager { func stopHealthChecks() { healthCheckTimer?.invalidate() healthCheckTimer = nil - print("Stopped Live Activity health checks") + AppLogger.debug("Stopped Live Activity health checks", tag: Self.logTag) } /// Perform a health check on the current Live Activity @@ -221,14 +233,14 @@ final class LiveActivityManager { // Only perform health check if it's been at least 25 seconds guard timeSinceLastCheck >= 25 else { return } - print("🩺 Performing Live Activity health check") + AppLogger.debug("🩺 Performing Live Activity health check", tag: Self.logTag) lastHealthCheck = now let activities = Activity.activities let isStillActive = activities.contains { $0.id == currentActivity.id } if !isStillActive { - print("Health check failed - Live Activity was dismissed") + AppLogger.warning("Health check failed - Live Activity was dismissed", tag: Self.logTag) self.currentActivity = nil // Notify that we need to restart @@ -237,7 +249,7 @@ final class LiveActivityManager { object: nil ) } else { - print("Live Activity health check passed") + AppLogger.debug("Live Activity health check passed", tag: Self.logTag) } } diff --git a/ios/Ascently/Views/LiveActivityDebugView.swift b/ios/Ascently/Views/LiveActivityDebugView.swift index cd7c8fd..86e79e1 100644 --- a/ios/Ascently/Views/LiveActivityDebugView.swift +++ b/ios/Ascently/Views/LiveActivityDebugView.swift @@ -196,7 +196,7 @@ struct LiveActivityDebugView: View { } isTestRunning = true - appendDebugOutput("🧪 Starting Live Activity test...") + appendDebugOutput("Starting Live Activity test...") Task { defer { diff --git a/ios/Ascently/Views/ProblemsView.swift b/ios/Ascently/Views/ProblemsView.swift index 5d59bde..a36e3c7 100644 --- a/ios/Ascently/Views/ProblemsView.swift +++ b/ios/Ascently/Views/ProblemsView.swift @@ -317,7 +317,6 @@ struct ProblemsList: View { } Button { - // Use a spring animation for more natural movement withAnimation(.spring(response: 0.5, dampingFraction: 0.8, blendDuration: 0.1)) { let updatedProblem = problem.updated(isActive: !problem.isActive) diff --git a/ios/Ascently/Views/SettingsView.swift b/ios/Ascently/Views/SettingsView.swift index e7ee043..6ec5e76 100644 --- a/ios/Ascently/Views/SettingsView.swift +++ b/ios/Ascently/Views/SettingsView.swift @@ -84,6 +84,8 @@ struct DataManagementSection: View { @State private var isDeletingImages = false @State private var showingDeleteImagesAlert = false + private static let logTag = "DataManagementSection" + var body: some View { Section("Data Management") { // Export Data @@ -217,13 +219,14 @@ struct DataManagementSection: View { try fileManager.removeItem(at: imageFile) deletedCount += 1 } catch { - print("Failed to delete image: \(imageFile.lastPathComponent)") + AppLogger.error( + "Failed to delete image: \(imageFile.lastPathComponent)", tag: Self.logTag) } } - print("Deleted \(deletedCount) image files") + AppLogger.info("Deleted \(deletedCount) image files", tag: Self.logTag) } catch { - print("Failed to access images directory: \(error)") + AppLogger.error("Failed to access images directory: \(error)", tag: Self.logTag) } // Delete all images from backup directory @@ -235,7 +238,7 @@ struct DataManagementSection: View { try? fileManager.removeItem(at: backupFile) } } catch { - print("Failed to access backup directory: \(error)") + AppLogger.error("Failed to access backup directory: \(error)", tag: Self.logTag) } // Clear image paths from all problems @@ -260,20 +263,6 @@ struct AppInfoSection: View { var body: some View { Section("App Information") { - HStack { - Image("AppLogo") - .resizable() - .frame(width: 24, height: 24) - VStack(alignment: .leading) { - Text("Ascently") - .font(.headline) - Text("Track your climbing progress") - .font(.caption) - .foregroundColor(.secondary) - } - Spacer() - } - HStack { Image(systemName: "info.circle") .foregroundColor(.blue) @@ -292,11 +281,13 @@ struct ExportDataView: View { @State private var tempFileURL: URL? @State private var isCreatingFile = true + private static let logTag = "ExportDataView" + var body: some View { NavigationStack { VStack(spacing: 30) { if isCreatingFile { - // Loading state - more prominent + // Loading state VStack(spacing: 20) { ProgressView() .scaleEffect(1.5) @@ -380,6 +371,7 @@ struct ExportDataView: View { } private func createTempFile() { + let logTag = Self.logTag // Capture before entering background queue DispatchQueue.global(qos: .userInitiated).async { do { let formatter = ISO8601DateFormatter() @@ -394,7 +386,9 @@ struct ExportDataView: View { for: .documentDirectory, in: .userDomainMask ).first else { - print("Could not access Documents directory") + Task { @MainActor in + AppLogger.error("Could not access Documents directory", tag: logTag) + } DispatchQueue.main.async { self.isCreatingFile = false } @@ -410,7 +404,9 @@ struct ExportDataView: View { self.isCreatingFile = false } } catch { - print("Failed to create export file: \(error)") + Task { @MainActor in + AppLogger.error("Failed to create export file: \(error)", tag: logTag) + } DispatchQueue.main.async { self.isCreatingFile = false } @@ -420,10 +416,12 @@ struct ExportDataView: View { private func cleanupTempFile() { if let fileURL = tempFileURL { + let logTag = Self.logTag // Capture before entering async closure // Clean up after a delay to ensure sharing is complete DispatchQueue.main.asyncAfter(deadline: .now() + 5.0) { try? FileManager.default.removeItem(at: fileURL) - print("Cleaned up export file: \(fileURL.lastPathComponent)") + AppLogger.debug( + "Cleaned up export file: \(fileURL.lastPathComponent)", tag: logTag) } } } @@ -435,6 +433,8 @@ struct SyncSection: View { @State private var showingSyncSettings = false @State private var showingDisconnectAlert = false + private static let logTag = "SyncSection" + var body: some View { Section("Sync") { // Sync Status @@ -579,11 +579,14 @@ struct SyncSection: View { } private func performSync() { + let logTag = Self.logTag // Capture before entering async context Task { do { try await syncService.syncWithServer(dataManager: dataManager) } catch { - print("Sync failed: \(error)") + await MainActor.run { + AppLogger.error("Sync failed: \(error)", tag: logTag) + } } } }