From ef1cf3583a57c59d296ff626bb6ffbf0cdbd23bd Mon Sep 17 00:00:00 2001 From: Atridad Lahiji Date: Tue, 14 Oct 2025 23:57:46 -0600 Subject: [PATCH] [Android] 2.0.1 - Refactoring & Minor Optimizations --- .../ascently/data/health/HealthConnectStub.kt | 19 -- .../atridad/ascently/data/sync/SyncService.kt | 26 +-- .../ui/components/OrientationAwareImage.kt | 2 +- .../ascently/ui/screens/AddEditScreens.kt | 18 +- .../ascently/ui/viewmodel/ClimbViewModel.kt | 10 -- .../atridad/ascently/utils/DateFormatUtils.kt | 8 +- .../com/atridad/ascently/utils/ImageUtils.kt | 163 ------------------ .../widget/ClimbStatsWidgetProvider.kt | 3 +- 8 files changed, 22 insertions(+), 227 deletions(-) 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 03a3227..d3fb0d6 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 @@ -354,14 +354,6 @@ class HealthConnectManager(private val context: Context) { } } - /** Reset all preferences */ - fun reset() { - preferences.edit().clear().apply() - _isEnabled.value = false - _hasPermissions.value = false - _autoSync.value = true - } - /** Check if ready for use */ fun isReadySync(): Boolean { return _isEnabled.value && _hasPermissions.value @@ -371,15 +363,4 @@ class HealthConnectManager(private val context: Context) { fun getLastSyncSuccess(): String? { return preferences.getString("last_sync_success", null) } - - /** Get detailed status */ - fun getDetailedStatus(): Map { - return mapOf( - "enabled" to _isEnabled.value.toString(), - "hasPermissions" to _hasPermissions.value.toString(), - "autoSync" to _autoSync.value.toString(), - "compatible" to _isCompatible.value.toString(), - "lastSyncSuccess" to (getLastSyncSuccess() ?: "never") - ) - } } 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 1d831f9..d016f8d 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 @@ -33,6 +33,7 @@ import kotlinx.coroutines.launch import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.withContext +import kotlinx.serialization.encodeToString import kotlinx.serialization.json.Json import okhttp3.MediaType.Companion.toMediaType import okhttp3.OkHttpClient @@ -266,8 +267,7 @@ class SyncService(private val context: Context, private val repository: ClimbRep private suspend fun uploadData(backup: ClimbDataBackup) { val requestBody = - json.encodeToString(ClimbDataBackup.serializer(), backup) - .toRequestBody("application/json".toMediaType()) + json.encodeToString(backup).toRequestBody("application/json".toMediaType()) val request = Request.Builder() @@ -429,9 +429,7 @@ class SyncService(private val context: Context, private val repository: ClimbRep backup.problems.map { backupProblem -> val imagePaths = backupProblem.imagePaths val updatedImagePaths = - imagePaths?.map { oldPath -> - imagePathMapping[oldPath] ?: oldPath - } + imagePaths?.map { oldPath -> imagePathMapping[oldPath] ?: oldPath } backupProblem.copy(imagePaths = updatedImagePaths).toProblem() } val sessions = backup.sessions.map { it.toClimbSession() } @@ -533,26 +531,16 @@ 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.") { - @JvmStatic private fun readResolve(): Any = NotConfigured - } + SyncException("Sync is not configured. Please set server URL and auth token.") - object NotConnected : SyncException("Not connected to server. Please test connection first.") { - @JvmStatic private fun readResolve(): Any = NotConnected - } + object NotConnected : SyncException("Not connected to server. Please test connection first.") - object Unauthorized : SyncException("Unauthorized. Please check your auth token.") { - @JvmStatic private fun readResolve(): Any = Unauthorized - } + object Unauthorized : SyncException("Unauthorized. Please check your auth token.") - object ImageNotFound : SyncException("Image not found on server") { - @JvmStatic private fun readResolve(): Any = ImageNotFound - } + object ImageNotFound : SyncException("Image not found on server") data class ServerError(val code: Int) : SyncException("Server error: HTTP $code") data class InvalidResponse(val details: String) : SyncException("Invalid server response: $details") - data class DecodingError(val details: String) : - SyncException("Failed to decode server response: $details") data class NetworkError(val details: String) : SyncException("Network error: $details") } diff --git a/android/app/src/main/java/com/atridad/ascently/ui/components/OrientationAwareImage.kt b/android/app/src/main/java/com/atridad/ascently/ui/components/OrientationAwareImage.kt index be95029..85c6a8b 100644 --- a/android/app/src/main/java/com/atridad/ascently/ui/components/OrientationAwareImage.kt +++ b/android/app/src/main/java/com/atridad/ascently/ui/components/OrientationAwareImage.kt @@ -45,7 +45,7 @@ fun OrientationAwareImage( ?: return@withContext null val correctedBitmap = correctImageOrientation(imageFile, originalBitmap) correctedBitmap.asImageBitmap() - } catch (e: Exception) { + } catch (_: Exception) { null } } diff --git a/android/app/src/main/java/com/atridad/ascently/ui/screens/AddEditScreens.kt b/android/app/src/main/java/com/atridad/ascently/ui/screens/AddEditScreens.kt index fcece32..622a845 100644 --- a/android/app/src/main/java/com/atridad/ascently/ui/screens/AddEditScreens.kt +++ b/android/app/src/main/java/com/atridad/ascently/ui/screens/AddEditScreens.kt @@ -88,8 +88,8 @@ fun AddEditGymScreen(gymId: String?, viewModel: ClimbViewModel, onNavigateBack: notes = notes ) - if (isEditing) { - viewModel.updateGym(gym.copy(id = gymId!!)) + if (isEditing && gymId != null) { + viewModel.updateGym(gym.copy(id = gymId)) } else { viewModel.addGym(gym) } @@ -385,10 +385,10 @@ fun AddEditProblemScreen( notes = notes.ifBlank { null } ) - if (isEditing && problemId != null) { - viewModel.updateProblem( - problem.copy(id = problemId) - ) + if (isEditing) { + problemId?.let { id -> + viewModel.updateProblem(problem.copy(id = id)) + } } else { viewModel.addProblem(problem) } @@ -762,9 +762,9 @@ fun AddEditSessionScreen( null } ) - viewModel.updateSession( - session.copy(id = sessionId!!) - ) + sessionId?.let { id -> + viewModel.updateSession(session.copy(id = id)) + } } else { viewModel.startSession( context, 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 feb3462..8c34209 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 @@ -201,14 +201,6 @@ class ClimbViewModel( fun getProblemsByGym(gymId: String): Flow> = repository.getProblemsByGym(gymId) // Session operations - fun addSession(session: ClimbSession, updateWidgets: Boolean = true) { - viewModelScope.launch { - repository.insertSession(session) - if (updateWidgets) { - ClimbStatsWidgetProvider.updateAllWidgets(context) - } - } - } fun updateSession(session: ClimbSession, updateWidgets: Boolean = true) { viewModelScope.launch { @@ -498,8 +490,6 @@ class ClimbViewModel( } } } - - fun getHealthConnectManager(): HealthConnectManager = healthConnectManager } data class ClimbUiState( diff --git a/android/app/src/main/java/com/atridad/ascently/utils/DateFormatUtils.kt b/android/app/src/main/java/com/atridad/ascently/utils/DateFormatUtils.kt index cfa73ab..915990c 100644 --- a/android/app/src/main/java/com/atridad/ascently/utils/DateFormatUtils.kt +++ b/android/app/src/main/java/com/atridad/ascently/utils/DateFormatUtils.kt @@ -21,11 +21,11 @@ object DateFormatUtils { fun parseISO8601(dateString: String): Instant? { return try { Instant.from(ISO_FORMATTER.parse(dateString)) - } catch (e: Exception) { + } catch (_: Exception) { try { Instant.parse(dateString) - } catch (e2: Exception) { + } catch (_: Exception) { null } } @@ -46,7 +46,7 @@ object DateFormatUtils { // Fallback for malformed dates dateString.take(10) } - } catch (e: Exception) { + } catch (_: Exception) { dateString.take(10) } } @@ -56,7 +56,7 @@ object DateFormatUtils { return try { val instant = parseISO8601(dateString) instant?.let { LocalDateTime.ofInstant(it, ZoneId.systemDefault()) } - } catch (e: Exception) { + } catch (_: Exception) { null } } 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 a481278..57ec226 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,8 +6,6 @@ import android.graphics.Bitmap import android.graphics.BitmapFactory import android.graphics.ImageDecoder import android.net.Uri -import android.os.Build -import android.provider.MediaStore import android.util.Log import androidx.core.graphics.scale import androidx.exifinterface.media.ExifInterface @@ -80,93 +78,6 @@ object ImageUtils { } } - /** Saves an image from a URI with compression */ - fun saveImageFromUri( - context: Context, - imageUri: Uri, - problemId: String? = null, - imageIndex: Int? = null - ): String? { - return try { - - val originalBitmap = - context.contentResolver.openInputStream(imageUri)?.use { input -> - BitmapFactory.decodeStream(input) - } - ?: return null - - // Always require deterministic naming - require(problemId != null && imageIndex != null) { - "Problem ID and image index are required for deterministic image naming" - } - - val filename = ImageNamingUtils.generateImageFilename(problemId, imageIndex) - val imageFile = File(getImagesDirectory(context), filename) - - val success = saveImageWithExif(context, imageUri, originalBitmap, imageFile) - originalBitmap.recycle() - - if (!success) return null - - "$IMAGES_DIR/$filename" - } catch (e: Exception) { - e.printStackTrace() - null - } - } - - /** Corrects image orientation based on EXIF data */ - private fun correctImageOrientation(context: Context, imageUri: Uri, bitmap: Bitmap): Bitmap { - return try { - val inputStream = context.contentResolver.openInputStream(imageUri) - inputStream?.use { input -> - val exif = ExifInterface(input) - val orientation = - exif.getAttributeInt( - ExifInterface.TAG_ORIENTATION, - ExifInterface.ORIENTATION_NORMAL - ) - - val matrix = android.graphics.Matrix() - when (orientation) { - ExifInterface.ORIENTATION_ROTATE_90 -> { - matrix.postRotate(90f) - } - ExifInterface.ORIENTATION_ROTATE_180 -> { - matrix.postRotate(180f) - } - ExifInterface.ORIENTATION_ROTATE_270 -> { - matrix.postRotate(270f) - } - ExifInterface.ORIENTATION_FLIP_HORIZONTAL -> { - matrix.postScale(-1f, 1f) - } - ExifInterface.ORIENTATION_FLIP_VERTICAL -> { - matrix.postScale(1f, -1f) - } - ExifInterface.ORIENTATION_TRANSPOSE -> { - matrix.postRotate(90f) - matrix.postScale(-1f, 1f) - } - ExifInterface.ORIENTATION_TRANSVERSE -> { - matrix.postRotate(-90f) - matrix.postScale(-1f, 1f) - } - } - - if (matrix.isIdentity) { - bitmap - } else { - Bitmap.createBitmap(bitmap, 0, 0, bitmap.width, bitmap.height, matrix, true) - } - } - ?: bitmap - } catch (e: Exception) { - e.printStackTrace() - bitmap - } - } - /** Compresses and resizes an image bitmap */ @SuppressLint("UseKtx") private fun compressImage(original: Bitmap): Bitmap { @@ -306,34 +217,6 @@ object ImageUtils { } } - /** Saves an image from byte array to app's private storage */ - fun saveImageFromBytes(context: Context, imageData: ByteArray): String? { - return try { - val bitmap = BitmapFactory.decodeByteArray(imageData, 0, imageData.size) ?: return null - - val compressedBitmap = compressImage(bitmap) - - // Generate unique filename - val filename = "${UUID.randomUUID()}.jpg" - val imageFile = File(getImagesDirectory(context), filename) - - // Save compressed image - FileOutputStream(imageFile).use { output -> - compressedBitmap.compress(Bitmap.CompressFormat.JPEG, IMAGE_QUALITY, output) - } - - // Clean up bitmaps - bitmap.recycle() - compressedBitmap.recycle() - - // Return relative path - "$IMAGES_DIR/$filename" - } catch (e: Exception) { - e.printStackTrace() - null - } - } - /** Saves image data with a specific filename */ fun saveImageFromBytesWithFilename( context: Context, @@ -384,52 +267,6 @@ object ImageUtils { } } - /** Migrates existing images to use consistent naming convention */ - fun migrateImageNaming( - context: Context, - problemId: String, - currentImagePaths: List - ): Map { - val migrationMap = mutableMapOf() - - currentImagePaths.forEachIndexed { index, oldPath -> - val oldFilename = oldPath.substringAfterLast('/') - val newFilename = ImageNamingUtils.generateImageFilename(problemId, index) - - if (oldFilename != newFilename) { - try { - val oldFile = getImageFile(context, oldPath) - val newFile = File(getImagesDirectory(context), newFilename) - - if (oldFile.exists() && oldFile.renameTo(newFile)) { - val newPath = "$IMAGES_DIR/$newFilename" - migrationMap[oldPath] = newPath - } - } catch (e: Exception) { - // Log error but continue with other images - e.printStackTrace() - } - } - } - - return migrationMap - } - - /** Batch migrates all images in the system to use consistent naming */ - fun batchMigrateAllImages( - context: Context, - problemImageMap: Map> - ): Map { - val allMigrations = mutableMapOf() - - problemImageMap.forEach { (problemId, imagePaths) -> - val migrations = migrateImageNaming(context, problemId, imagePaths) - allMigrations.putAll(migrations) - } - - return allMigrations - } - /** Cleans up orphaned images that are not referenced by any problems */ fun cleanupOrphanedImages(context: Context, referencedPaths: Set) { try { diff --git a/android/app/src/main/java/com/atridad/ascently/widget/ClimbStatsWidgetProvider.kt b/android/app/src/main/java/com/atridad/ascently/widget/ClimbStatsWidgetProvider.kt index 885a16d..dd3daca 100644 --- a/android/app/src/main/java/com/atridad/ascently/widget/ClimbStatsWidgetProvider.kt +++ b/android/app/src/main/java/com/atridad/ascently/widget/ClimbStatsWidgetProvider.kt @@ -53,7 +53,6 @@ class ClimbStatsWidgetProvider : AppWidgetProvider() { val problems = repository.getAllProblems().first() val attempts = repository.getAllAttempts().first() val gyms = repository.getAllGyms().first() - repository.getActiveSession() // Calculate stats val completedSessions = sessions.filter { it.endTime != null } @@ -108,7 +107,7 @@ class ClimbStatsWidgetProvider : AppWidgetProvider() { appWidgetManager.updateAppWidget(appWidgetId, views) } - } catch (e: Exception) { + } catch (_: Exception) { launch(Dispatchers.Main) { val views = RemoteViews(context.packageName, R.layout.widget_climb_stats) views.setTextViewText(R.id.widget_total_sessions, "0")