Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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,
Expand All @@ -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()
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
}
}
}

Expand All @@ -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() }
Expand All @@ -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() }
Expand All @@ -112,15 +132,19 @@ 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 =
dao
.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,
Expand All @@ -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"
Expand All @@ -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,
Expand All @@ -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"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -75,21 +75,27 @@ class TreeUploader(
onHandleUpload: suspend (List<Long>) -> 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")
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.*

Expand Down Expand Up @@ -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) =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand Down
Loading
Loading