[Android] 2.0.1 - Refactoring & Minor Optimizations
This commit is contained in:
@@ -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<String, String> {
|
||||
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")
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
@@ -45,7 +45,7 @@ fun OrientationAwareImage(
|
||||
?: return@withContext null
|
||||
val correctedBitmap = correctImageOrientation(imageFile, originalBitmap)
|
||||
correctedBitmap.asImageBitmap()
|
||||
} catch (e: Exception) {
|
||||
} catch (_: Exception) {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -201,14 +201,6 @@ class ClimbViewModel(
|
||||
fun getProblemsByGym(gymId: String): Flow<List<Problem>> = 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(
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<String>
|
||||
): Map<String, String> {
|
||||
val migrationMap = mutableMapOf<String, String>()
|
||||
|
||||
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<String, List<String>>
|
||||
): Map<String, String> {
|
||||
val allMigrations = mutableMapOf<String, String>()
|
||||
|
||||
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<String>) {
|
||||
try {
|
||||
|
||||
@@ -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")
|
||||
|
||||
Reference in New Issue
Block a user