diff --git a/app/src/main/java/org/greenstand/android/TreeTracker/background/TreeSyncWorker.kt b/app/src/main/java/org/greenstand/android/TreeTracker/background/TreeSyncWorker.kt index 2e9aeae4b..5148699d7 100644 --- a/app/src/main/java/org/greenstand/android/TreeTracker/background/TreeSyncWorker.kt +++ b/app/src/main/java/org/greenstand/android/TreeTracker/background/TreeSyncWorker.kt @@ -26,6 +26,7 @@ import androidx.core.app.NotificationCompat import androidx.work.CoroutineWorker import androidx.work.ForegroundInfo import androidx.work.WorkerParameters +import kotlinx.coroutines.CancellationException import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.delay @@ -39,6 +40,7 @@ import org.greenstand.android.TreeTracker.database.TreeTrackerDAO import org.greenstand.android.TreeTracker.usecases.SyncDataUseCase import org.koin.core.component.KoinComponent import org.koin.core.component.inject +import timber.log.Timber class TreeSyncWorker( context: Context, @@ -59,23 +61,35 @@ class TreeSyncWorker( return coroutineScope { val progressJob = launch { - while (true) { - delay(750) - val remaining = - withContext(Dispatchers.IO) { - dao.getNonUploadedLegacyTreeCaptureImageCount() + dao.getNonUploadedTreeImageCount() - } - val uploaded = (totalTreesToSync - remaining).coerceAtLeast(0) - val contentText = applicationContext.getString(R.string.uploading_trees) + " ($uploaded/$totalTreesToSync)" - syncNotificationManager.updateProgress(uploaded, totalTreesToSync, contentText) + try { + while (true) { + delay(750) + val remaining = + withContext(Dispatchers.IO) { + dao.getNonUploadedLegacyTreeCaptureImageCount() + dao.getNonUploadedTreeImageCount() + } + val uploaded = (totalTreesToSync - remaining).coerceAtLeast(0) + val contentText = applicationContext.getString(R.string.uploading_trees) + " ($uploaded/$totalTreesToSync)" + syncNotificationManager.updateProgress(uploaded, totalTreesToSync, contentText) + } + } catch (e: CancellationException) { + // Expected on completion } } - exceptionDataCollector.set(ExceptionDataCollector.IS_SYNCING, true) - val result = syncDataBundleUseCase.execute(Unit) - exceptionDataCollector.set(ExceptionDataCollector.IS_SYNCING, false) - progressJob.cancel() - if (result) Result.success() else Result.failure() + try { + exceptionDataCollector.set(ExceptionDataCollector.IS_SYNCING, true) + val result = syncDataBundleUseCase.execute(Unit) + if (result) Result.success() else Result.failure() + } catch (e: CancellationException) { + throw e + } catch (e: Exception) { + Timber.e(e, "TreeSyncWorker failed unexpectedly") + Result.failure() + } finally { + exceptionDataCollector.set(ExceptionDataCollector.IS_SYNCING, false) + progressJob.cancel() + } } } diff --git a/app/src/main/java/org/greenstand/android/TreeTracker/models/PlanterUploader.kt b/app/src/main/java/org/greenstand/android/TreeTracker/models/PlanterUploader.kt index ddb89d498..7341a460d 100644 --- a/app/src/main/java/org/greenstand/android/TreeTracker/models/PlanterUploader.kt +++ b/app/src/main/java/org/greenstand/android/TreeTracker/models/PlanterUploader.kt @@ -15,6 +15,7 @@ */ package org.greenstand.android.TreeTracker.models +import kotlinx.coroutines.CancellationException import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.async import kotlinx.coroutines.coroutineScope @@ -44,11 +45,18 @@ class PlanterUploader( ) { suspend fun upload(instanceId: String) { withContext(Dispatchers.IO) { - uploadLegacyPlanterImages() - uploadUserImages() - uploadPlanterInfo(instanceId) - uploadUsers() - deleteLocalImagesThatWereUploaded() + try { + uploadLegacyPlanterImages() + uploadUserImages() + uploadPlanterInfo(instanceId) + uploadUsers() + deleteLocalImagesThatWereUploaded() + } catch (e: CancellationException) { + throw e + } catch (e: Exception) { + Timber.tag(TAG).e(e, "Failed to upload planter data") + throw e + } } } @@ -59,17 +67,23 @@ class PlanterUploader( .filter { it.photoUrl == null && it.localPhotoPath != null } .map { planterCheckIn -> async { - val imageUrl = - uploadImageUseCase.execute( - UploadImageParams( - imagePath = planterCheckIn.localPhotoPath!!, - lat = planterCheckIn.latitude, - long = planterCheckIn.longitude, - ), - ) - imageUrl?.let { - planterCheckIn.photoUrl = imageUrl - dao.updatePlanterCheckIn(planterCheckIn) + try { + val imageUrl = + uploadImageUseCase.execute( + UploadImageParams( + imagePath = planterCheckIn.localPhotoPath!!, + lat = planterCheckIn.latitude, + long = planterCheckIn.longitude, + ), + ) + imageUrl?.let { + planterCheckIn.photoUrl = imageUrl + dao.updatePlanterCheckIn(planterCheckIn) + } + } catch (e: CancellationException) { + throw e + } catch (e: Exception) { + Timber.tag(TAG).e(e, "Failed to upload legacy planter image: ${planterCheckIn.localPhotoPath}") } } }.forEach { it.await() } @@ -83,17 +97,23 @@ class PlanterUploader( .filter { it.photoUrl == null } .map { user -> async { - val imageUrl = - uploadImageUseCase.execute( - UploadImageParams( - imagePath = user.photoPath, - lat = user.latitude, - long = user.longitude, - ), - ) - imageUrl?.let { - user.photoUrl = imageUrl - dao.updateUser(user) + try { + val imageUrl = + uploadImageUseCase.execute( + UploadImageParams( + imagePath = user.photoPath, + lat = user.latitude, + long = user.longitude, + ), + ) + imageUrl?.let { + user.photoUrl = imageUrl + dao.updateUser(user) + } + } catch (e: CancellationException) { + throw e + } catch (e: Exception) { + Timber.tag(TAG).e(e, "Failed to upload user image: ${user.photoPath}") } } }.forEach { it.await() } @@ -112,7 +132,7 @@ class PlanterUploader( } val registrationRequests = planterInfoToUpload - .map { planterInfo -> + .mapNotNull { planterInfo -> // Find the image this user first took during registration // This image is the oldest image for PlanterCheckIn val registrationPhotoUrl = @@ -120,7 +140,11 @@ class PlanterUploader( .getAllPlanterCheckInsForPlanterInfoId(planterInfo.id) .minByOrNull { it.createdAt } ?.photoUrl - ?: "" + + if (registrationPhotoUrl == null) { + Timber.tag(TAG).w("Skipping planter info upload for ${planterInfo.id}: no photoUrl") + return@mapNotNull null + } RegistrationRequest( planterIdentifier = planterInfo.identifier, @@ -136,6 +160,8 @@ class PlanterUploader( ) } + if (registrationRequests.isEmpty()) return + val jsonBundle = json.encodeToString(UploadBundle.createV1(registrations = registrationRequests, instanceId = instanceId)) val bundleId = jsonBundle.md5() + "_registrations" @@ -162,7 +188,12 @@ class PlanterUploader( val walletRegistrations = usersToUpload - .map { user -> + .mapNotNull { user -> + val photoUrl = user.photoUrl + if (photoUrl == null) { + Timber.tag(TAG).w("Skipping user upload for ${user.uuid}: no photoUrl") + return@mapNotNull null + } WalletRegistrationRequest( registrationId = user.uuid, wallet = user.wallet, @@ -172,11 +203,13 @@ class PlanterUploader( email = user.email, lat = user.latitude, lon = user.longitude, - imageUrl = user.photoUrl!!, + imageUrl = photoUrl, createdAt = user.createdAt.toString(), ) } + if (walletRegistrations.isEmpty()) return + val jsonBundle = json.encodeToString(UploadBundle.createV2(walletRegistration = walletRegistrations)) val bundleId = jsonBundle.md5() + "_registrations" diff --git a/app/src/main/java/org/greenstand/android/TreeTracker/models/TreeCapturer.kt b/app/src/main/java/org/greenstand/android/TreeTracker/models/TreeCapturer.kt index 4db60afc9..966a786bf 100644 --- a/app/src/main/java/org/greenstand/android/TreeTracker/models/TreeCapturer.kt +++ b/app/src/main/java/org/greenstand/android/TreeTracker/models/TreeCapturer.kt @@ -15,6 +15,7 @@ */ package org.greenstand.android.TreeTracker.models +import kotlinx.coroutines.CancellationException import org.greenstand.android.TreeTracker.models.location.Convergence import org.greenstand.android.TreeTracker.models.location.LocationDataCapturer import org.greenstand.android.TreeTracker.usecases.CreateTreeUseCase @@ -96,6 +97,8 @@ class TreeCapturer( return try { createTreeUseCase.execute(tree) true + } catch (e: CancellationException) { + throw e } catch (e: Exception) { Timber.tag("TreeCapturer").e(e, "Failed to save tree") false diff --git a/app/src/main/java/org/greenstand/android/TreeTracker/models/TreeUploader.kt b/app/src/main/java/org/greenstand/android/TreeTracker/models/TreeUploader.kt index 527d20ad0..225b97cbb 100644 --- a/app/src/main/java/org/greenstand/android/TreeTracker/models/TreeUploader.kt +++ b/app/src/main/java/org/greenstand/android/TreeTracker/models/TreeUploader.kt @@ -15,10 +15,10 @@ */ package org.greenstand.android.TreeTracker.models +import kotlinx.coroutines.CancellationException import kotlinx.coroutines.async -import kotlinx.coroutines.cancel import kotlinx.coroutines.coroutineScope -import kotlinx.coroutines.isActive +import kotlinx.coroutines.ensureActive import kotlinx.serialization.encodeToString import kotlinx.serialization.json.Json import org.greenstand.android.TreeTracker.api.ObjectStorageClient @@ -75,21 +75,27 @@ class TreeUploader( onHandleUpload: suspend (List) -> Unit, ) { log("Uploading ${treeIds.size} trees") - treeIds.windowed(size = TREE_BUNDLE_SIZE, step = TREE_BUNDLE_SIZE, partialWindows = true).onEach { treeIdBundle -> + var firstError: Exception? = null + + treeIds.windowed(size = TREE_BUNDLE_SIZE, step = TREE_BUNDLE_SIZE, partialWindows = true).forEach { treeIdBundle -> try { - if (coroutineContext.isActive) { - coroutineScope { - log("Starting bulk upload for ${treeIdBundle.size} trees") - onHandleUpload(treeIdBundle) - log("Completed bulk upload for ${treeIdBundle.size} trees") - } - } else { - coroutineContext.cancel() + coroutineContext.ensureActive() + coroutineScope { + log("Starting bulk upload for ${treeIdBundle.size} trees") + onHandleUpload(treeIdBundle) + log("Completed bulk upload for ${treeIdBundle.size} trees") } + } catch (e: CancellationException) { + throw e } catch (e: Exception) { - Timber.e("NewTree upload failed") + Timber.e(e, "Bulk tree upload failed for bundle: $treeIdBundle") + if (firstError == null) { + firstError = e + } } } + + firstError?.let { throw it } log("Completed upload for ${treeIds.size} trees") } diff --git a/app/src/main/java/org/greenstand/android/TreeTracker/models/messages/MessagesRepo.kt b/app/src/main/java/org/greenstand/android/TreeTracker/models/messages/MessagesRepo.kt index 4c57ee14c..756ed2185 100644 --- a/app/src/main/java/org/greenstand/android/TreeTracker/models/messages/MessagesRepo.kt +++ b/app/src/main/java/org/greenstand/android/TreeTracker/models/messages/MessagesRepo.kt @@ -33,6 +33,7 @@ import org.greenstand.android.TreeTracker.models.messages.network.responses.Quer import org.greenstand.android.TreeTracker.utilities.Constants import org.greenstand.android.TreeTracker.utilities.TimeProvider import org.greenstand.android.TreeTracker.utils.runInParallel +import retrofit2.HttpException import timber.log.Timber import java.util.* @@ -145,17 +146,25 @@ class MessagesRepo( } catch (e: CancellationException) { // rethrow cancellation exception throw e + } catch (e: HttpException) { + if (e.code() != 404) { + Timber.e(e, "Failed to fetch messages for wallet: $wallet") + } } catch (e: Exception) { - if (e.localizedMessage == Constants.LOCAL_MSG_ERROR_HTTP404) { - // 404 indicates the user has never had messages before - continue - } else { - Timber.e(e) + if (e.localizedMessage != Constants.LOCAL_MSG_ERROR_HTTP404) { + Timber.e(e, "Failed to fetch messages for wallet: $wallet") } } } - messageUploader.uploadMessages() + try { + messageUploader.uploadMessages() + } catch (e: CancellationException) { + throw e + } catch (e: Exception) { + Timber.e(e, "Failed to upload messages") + throw e + } } private suspend fun fetchMessagesForWallet(wallet: String) = diff --git a/app/src/main/java/org/greenstand/android/TreeTracker/models/organization/OrgConfigProvider.kt b/app/src/main/java/org/greenstand/android/TreeTracker/models/organization/OrgConfigProvider.kt index e28ef92af..0f1ed3c39 100644 --- a/app/src/main/java/org/greenstand/android/TreeTracker/models/organization/OrgConfigProvider.kt +++ b/app/src/main/java/org/greenstand/android/TreeTracker/models/organization/OrgConfigProvider.kt @@ -16,6 +16,7 @@ package org.greenstand.android.TreeTracker.models.organization import com.google.firebase.remoteconfig.FirebaseRemoteConfig +import kotlinx.coroutines.CancellationException import kotlinx.coroutines.tasks.await import kotlinx.coroutines.withTimeoutOrNull import timber.log.Timber @@ -56,6 +57,8 @@ class OrgConfigProvider( Timber.tag(TAG).d("Remote Config value found for key: $key (${configValue.length} chars)") configValue } + } catch (e: CancellationException) { + throw e } catch (e: Exception) { val elapsed = System.currentTimeMillis() - startTime Timber.tag(TAG).e(e, "Remote Config fetch failed after ${elapsed}ms for org $orgId") diff --git a/app/src/main/java/org/greenstand/android/TreeTracker/models/organization/OrgRepo.kt b/app/src/main/java/org/greenstand/android/TreeTracker/models/organization/OrgRepo.kt index e9bcd8e1b..15055718e 100644 --- a/app/src/main/java/org/greenstand/android/TreeTracker/models/organization/OrgRepo.kt +++ b/app/src/main/java/org/greenstand/android/TreeTracker/models/organization/OrgRepo.kt @@ -15,6 +15,7 @@ */ package org.greenstand.android.TreeTracker.models.organization +import kotlinx.coroutines.CancellationException import kotlinx.serialization.encodeToString import kotlinx.serialization.json.Json import kotlinx.serialization.json.jsonArray @@ -130,14 +131,16 @@ class OrgRepo( version = configObj[OrgJsonKeys.V1.VERSION]?.jsonPrimitive?.content?.toIntOrNull() ?: 1, name = orgName, walletId = walletId, - captureSetupFlowJson = setupFlowJson.toString(), - captureFlowJson = captureFlowJson.toString(), + captureSetupFlowJson = setupFlowJson?.let { it.toString() } ?: "[]", + captureFlowJson = captureFlowJson?.let { it.toString() } ?: "[]", ) val validatedEntity = validateOrgRoutes(orgEntity) dao.insertOrg(validatedEntity) setOrg(validatedEntity.id) Timber.tag(ORG_LINK_TAG).i("Org '$orgName' ($orgId) loaded from Remote Config") true + } catch (e: CancellationException) { + throw e } catch (e: Exception) { Timber.tag(ORG_LINK_TAG).e(e, "Failed to parse Remote Config for org $orgId, falling back to minimal org") addMinimalOrg(orgId, orgName) @@ -173,6 +176,8 @@ class OrgRepo( setOrg(orgEntity.id) Timber.tag(ORG_LINK_TAG).i("Minimal org '$orgName' ($orgId) created with default flows") true + } catch (e: CancellationException) { + throw e } catch (e: Exception) { Timber.tag(ORG_LINK_TAG).e(e, "Failed to create minimal org for $orgId") false diff --git a/app/src/main/java/org/greenstand/android/TreeTracker/navigation/CaptureFlowNavigationController.kt b/app/src/main/java/org/greenstand/android/TreeTracker/navigation/CaptureFlowNavigationController.kt index 470139119..0472e582a 100644 --- a/app/src/main/java/org/greenstand/android/TreeTracker/navigation/CaptureFlowNavigationController.kt +++ b/app/src/main/java/org/greenstand/android/TreeTracker/navigation/CaptureFlowNavigationController.kt @@ -16,6 +16,7 @@ package org.greenstand.android.TreeTracker.navigation import androidx.navigation.NavHostController +import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.SupervisorJob @@ -51,11 +52,14 @@ class CaptureFlowNavigationController( // If navigating past last screen, save tree then loop back to capture if (currentNavPathIndex >= navPath.size) { - resetIndex() val saved = treeCapturer.saveTree() if (!saved) { - Timber.tag("CaptureFlowNav").w("Tree save failed or no tree to save") + Timber.tag("CaptureFlowNav").e("Tree save failed - data may be lost if screen is exited") + // Decrement index to allow retry on the same screen + decrementIndex() + return } + resetIndex() withContext(Dispatchers.Main) { navController.popBackStack(inclusive = false) } @@ -119,6 +123,8 @@ class CaptureFlowNavigationController( locationDataCapturer.stopGpsUpdates() locationDataCapturer.turnOffTreeCaptureMode() } + } catch (e: CancellationException) { + throw e } catch (e: Exception) { Timber.tag("CaptureFlowNav").e(e, "Error ending session") } diff --git a/app/src/main/java/org/greenstand/android/TreeTracker/usecases/CheckForInternetUseCase.kt b/app/src/main/java/org/greenstand/android/TreeTracker/usecases/CheckForInternetUseCase.kt index 5baa2d1eb..f96efc26e 100644 --- a/app/src/main/java/org/greenstand/android/TreeTracker/usecases/CheckForInternetUseCase.kt +++ b/app/src/main/java/org/greenstand/android/TreeTracker/usecases/CheckForInternetUseCase.kt @@ -15,9 +15,10 @@ */ package org.greenstand.android.TreeTracker.usecases +import kotlinx.coroutines.CancellationException import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext -import java.lang.Exception +import timber.log.Timber /** * checks if the internet is available on the user's device @@ -28,7 +29,10 @@ class CheckForInternetUseCase : UseCase() { try { val command = "ping -c 1 google.com" Runtime.getRuntime().exec(command).waitFor() == 0 + } catch (e: CancellationException) { + throw e } catch (e: Exception) { + Timber.tag("InternetCheck").e(e, "Failed to check for internet connection") false } } diff --git a/app/src/main/java/org/greenstand/android/TreeTracker/usecases/SyncDataUseCase.kt b/app/src/main/java/org/greenstand/android/TreeTracker/usecases/SyncDataUseCase.kt index 48dcabdd1..090621b94 100644 --- a/app/src/main/java/org/greenstand/android/TreeTracker/usecases/SyncDataUseCase.kt +++ b/app/src/main/java/org/greenstand/android/TreeTracker/usecases/SyncDataUseCase.kt @@ -16,9 +16,9 @@ package org.greenstand.android.TreeTracker.usecases import com.google.firebase.installations.FirebaseInstallations +import kotlinx.coroutines.CancellationException import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.cancel -import kotlinx.coroutines.isActive +import kotlinx.coroutines.ensureActive import kotlinx.coroutines.tasks.await import kotlinx.coroutines.withContext import org.greenstand.android.TreeTracker.database.TreeTrackerDAO @@ -46,61 +46,94 @@ class SyncDataUseCase( override suspend fun execute(params: Unit): Boolean { syncProgressTracker.startSync() + var overallSuccess = true + try { withContext(Dispatchers.IO) { val instanceId = try { FirebaseInstallations.getInstance().id.await() + } catch (e: CancellationException) { + throw e } catch (e: Exception) { + Timber.tag(TAG).e(e, "Failed to get Firebase instance ID") "" } - executeTrackedStep(SyncStep.MESSAGES, "Message Sync") { - messagesRepo.syncMessages() + if (!executeTrackedStep(SyncStep.MESSAGES) { + messagesRepo.syncMessages() + } + ) { + overallSuccess = false } - executeTrackedStep(SyncStep.DEVICE_CONFIG, "Device Config Upload") { - deviceConfigUploader.upload(instanceId) + if (!executeTrackedStep(SyncStep.DEVICE_CONFIG) { + deviceConfigUploader.upload(instanceId) + } + ) { + overallSuccess = false } - executeTrackedStep(SyncStep.USERS, "User Upload") { - planterUploader.upload(instanceId) + if (!executeTrackedStep(SyncStep.USERS) { + planterUploader.upload(instanceId) + } + ) { + overallSuccess = false } - executeTrackedStep(SyncStep.SESSIONS, "Session Upload") { - sessionUploader.upload() + if (!executeTrackedStep(SyncStep.SESSIONS) { + sessionUploader.upload() + } + ) { + overallSuccess = false } - treeUpload( - syncStep = SyncStep.LEGACY_TREES, - onGetTreeIds = { dao.getAllTreeCaptureIdsToUpload() }, - onUpload = { treeUploader.uploadLegacyTrees(it, instanceId) }, - ) + if (!treeUpload( + syncStep = SyncStep.LEGACY_TREES, + onGetTreeIds = { dao.getAllTreeCaptureIdsToUpload() }, + onUpload = { treeUploader.uploadLegacyTrees(it, instanceId) }, + ) + ) { + overallSuccess = false + } - treeUpload( - syncStep = SyncStep.TREES, - onGetTreeIds = { dao.getAllTreeIdsToUpload() }, - onUpload = { treeUploader.uploadTrees(it) }, - ) + if (!treeUpload( + syncStep = SyncStep.TREES, + onGetTreeIds = { dao.getAllTreeIdsToUpload() }, + onUpload = { treeUploader.uploadTrees(it) }, + ) + ) { + overallSuccess = false + } - executeTrackedStep(SyncStep.LOCATIONS, "Location Upload") { - uploadLocationDataUseCase.execute(Unit) + if (!executeTrackedStep(SyncStep.LOCATIONS) { + uploadLocationDataUseCase.execute(Unit) + } + ) { + overallSuccess = false } } + } catch (e: CancellationException) { + throw e } catch (e: Exception) { - Timber.e("Error occurred during syncing data. ${e.localizedMessage}") + Timber.tag(TAG).e(e, "Error occurred during syncing data") syncProgressTracker.endSync(error = e.localizedMessage) return false } - syncProgressTracker.endSync() - return true + + if (overallSuccess) { + syncProgressTracker.endSync() + } else { + syncProgressTracker.endSync(error = "One or more sync steps failed") + } + return overallSuccess } private suspend fun treeUpload( syncStep: SyncStep, onGetTreeIds: suspend () -> List, onUpload: suspend (List) -> Unit, - ) { + ): Boolean { syncProgressTracker.startStep(syncStep) try { var treeIds = onGetTreeIds() @@ -108,58 +141,65 @@ class SyncDataUseCase( var uploadedSoFar = 0 syncProgressTracker.updateStepProgress(syncStep, 0, totalTrees) - while (treeIds.isNotEmpty() && coroutineContext.isActive) { - executeIfContextActive("Tree Upload") { - onUpload(treeIds) + while (treeIds.isNotEmpty()) { + coroutineContext.ensureActive() + try { + executeIfContextActive { + onUpload(treeIds) + } + } catch (e: CancellationException) { + throw e + } catch (e: Exception) { + // Log and let the loop handle it or break + Timber.tag(TAG).e(e, "Tree upload batch failed") + // We don't throw here to allow progress tracker to mark failure and continue if needed, + // but since this is a while loop on IDs, we might get stuck if we don't break. + syncProgressTracker.failStep(syncStep, e.localizedMessage) + return false } + uploadedSoFar += treeIds.size syncProgressTracker.updateStepProgress(syncStep, uploadedSoFar, totalTrees) val remainingIds = onGetTreeIds() - if (!treeIds.containsAll(remainingIds)) { - treeIds = remainingIds - } else { - if (remainingIds.isNotEmpty()) { - Timber.tag(TAG).e("Remaining trees failed to upload, ending tree sync...") - } - break + if (remainingIds.isNotEmpty() && treeIds.containsAll(remainingIds)) { + Timber.tag(TAG).e("Remaining trees failed to upload, ending tree sync...") + syncProgressTracker.failStep(syncStep, "Remaining trees failed to upload") + return false } + treeIds = remainingIds } syncProgressTracker.completeStep(syncStep) + return true + } catch (e: CancellationException) { + throw e } catch (e: Exception) { syncProgressTracker.failStep(syncStep, e.localizedMessage) - throw e + return false } } private suspend fun executeTrackedStep( syncStep: SyncStep, - tag: String, action: suspend () -> Unit, - ) { + ): Boolean { syncProgressTracker.startStep(syncStep) - try { - executeIfContextActive(tag, action) + return try { + executeIfContextActive(action) syncProgressTracker.completeStep(syncStep) + true + } catch (e: CancellationException) { + throw e } catch (e: Exception) { syncProgressTracker.failStep(syncStep, e.localizedMessage) - throw e + false } } private suspend fun executeIfContextActive( - tag: String, action: suspend () -> Unit, ) { - try { - if (coroutineContext.isActive) { - action() - } else { - coroutineContext.cancel() - } - } catch (e: Exception) { - Timber.tag(TAG).e("$tag -> ${e.localizedMessage}") - throw e - } + coroutineContext.ensureActive() + action() } } \ No newline at end of file diff --git a/app/src/main/java/org/greenstand/android/TreeTracker/usecases/UploadLocationDataUseCase.kt b/app/src/main/java/org/greenstand/android/TreeTracker/usecases/UploadLocationDataUseCase.kt index c75bebb54..b3cf778c4 100644 --- a/app/src/main/java/org/greenstand/android/TreeTracker/usecases/UploadLocationDataUseCase.kt +++ b/app/src/main/java/org/greenstand/android/TreeTracker/usecases/UploadLocationDataUseCase.kt @@ -16,6 +16,7 @@ package org.greenstand.android.TreeTracker.usecases import com.amazonaws.AmazonClientException +import kotlinx.coroutines.CancellationException import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import kotlinx.serialization.encodeToString @@ -47,8 +48,14 @@ class UploadLocationDataUseCase( .map { (sessionId, entities) -> val locationRequests = entities - .map { json.decodeFromString(it.locationDataJson) } - .map { + .mapNotNull { + try { + json.decodeFromString(it.locationDataJson) + } catch (e: Exception) { + Timber.e(e, "Failed to decode location data for entity ${it.id}") + null + } + }.map { LocationRequest( accuracy = it.accuracy, latitude = it.latitude, @@ -59,15 +66,24 @@ class UploadLocationDataUseCase( return@map sessionId to locationRequests } - val sessionEntities = sessionIdToLocations.map { dao.getSessionById(it.key) } val trackRequests = - sessionIdToLocationRequests.map { (sessionId, locationList) -> + sessionIdToLocationRequests.mapNotNull { (sessionId, locationList) -> + val sessionEntity = dao.getSessionById(sessionId) + if (sessionEntity == null) { + Timber.w("Skipping location upload for session $sessionId: session not found in DB") + return@mapNotNull null + } TracksRequest( - sessionId = sessionEntities.find { it.id == sessionId }!!.uuid, + sessionId = sessionEntity.uuid, locations = locationList, ) } + if (trackRequests.isEmpty()) { + Timber.d("No valid track requests to upload") + return@withContext + } + val dataBundle = json.encodeToString( UploadBundle.createV2( @@ -84,18 +100,13 @@ class UploadLocationDataUseCase( Timber.tag("Location Upload").d("Completed uploading ${locationEntities.size} V2 GPS locations") } + } catch (e: CancellationException) { + throw e } catch (ace: AmazonClientException) { - Timber.e( - "Caught an AmazonClientException, which " + - "means the client encountered " + - "an internal error while trying to " + - "communicate with S3, " + - "such as not being able to access the network.", - ) - Timber.e("Error Message: ${ace.message}") + Timber.e(ace, "AmazonClientException encountered while communicating with S3") return false } catch (e: Exception) { - Timber.e("Location upload error: ${e.message}") + Timber.e(e, "Location upload error") return false } return true