Skip to content

Commit ec13b1b

Browse files
committed
[mage-1437] (#84)
1) Refactor observation and attachment sync worker classes to make the sync logic clearer 2) Remove bitwise operators for the retry determination logic
1 parent c3c0f2c commit ec13b1b

File tree

3 files changed

+122
-131
lines changed

3 files changed

+122
-131
lines changed

mage/src/main/java/mil/nga/giat/mage/observation/sync/AttachmentSyncWorker.kt

Lines changed: 23 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@ import mil.nga.giat.mage.MageApplication
1313
import mil.nga.giat.mage.R
1414
import mil.nga.giat.mage.data.repository.observation.AttachmentRepository
1515
import mil.nga.giat.mage.data.datasource.observation.AttachmentLocalDataSource
16-
import java.net.HttpURLConnection
1716
import java.util.concurrent.TimeUnit
1817

1918
@HiltWorker
@@ -27,15 +26,34 @@ class AttachmentSyncWorker @AssistedInject constructor(
2726
override suspend fun doWork(): Result {
2827
// Lock to ensure previous running work will complete when cancelled before new work is started.
2928
return mutex.withLock {
30-
val result = try {
31-
syncAttachments()
29+
try {
30+
//submit observations to the server that are marked as "dirty" and track the overall response flag of all transactions
31+
val overallResponseFlag = syncAttachments()
32+
33+
//if any of the transactions returned RetryFlag, then Result.retry() will be returned to retry the work request per back off policy
34+
//this should not result in the successful transactions being resubmitted, as they will no longer be marked as "dirty"
35+
when (overallResponseFlag) {
36+
ResponseFlag.SuccessFlag -> Result.success()
37+
ResponseFlag.FailureFlag -> Result.failure()
38+
ResponseFlag.RetryFlag -> Result.retry()
39+
}
3240
} catch (e: Exception) {
3341
Log.e(LOG_NAME, "Failed to sync attachments", e)
34-
RESULT_RETRY_FLAG
42+
//any unhandled exception should result in a retry
43+
Result.retry()
3544
}
45+
}
46+
}
3647

37-
if (result.containsFlag(RESULT_RETRY_FLAG)) Result.retry() else Result.success()
48+
private suspend fun syncAttachments(): ResponseFlag {
49+
var overallResult: ResponseFlag = ResponseFlag.SuccessFlag
50+
51+
for (attachment in attachmentLocalDataSource.dirtyAttachments.filter { !it.observation.remoteId.isNullOrEmpty() && it.url.isNullOrEmpty() }) {
52+
val response = ResponseFlag.processResponse(attachmentRepository.syncAttachment(attachment))
53+
overallResult = ResponseFlag.combineResponseFlags(overallResult, response)
3854
}
55+
56+
return overallResult
3957
}
4058

4159
override suspend fun getForegroundInfo(): ForegroundInfo {
@@ -50,44 +68,8 @@ class AttachmentSyncWorker @AssistedInject constructor(
5068
return ForegroundInfo(ATTACHMENT_SYNC_NOTIFICATION_ID, notification)
5169
}
5270

53-
private suspend fun syncAttachments(): Int {
54-
var result = RESULT_SUCCESS_FLAG
55-
56-
for (attachment in attachmentLocalDataSource.dirtyAttachments.filter { !it.observation.remoteId.isNullOrEmpty() && it.url.isNullOrEmpty() }) {
57-
val response = attachmentRepository.syncAttachment(attachment)
58-
result = if (response.isSuccessful) {
59-
Result.success()
60-
} else {
61-
if (response.code() == HttpURLConnection.HTTP_UNAUTHORIZED) {
62-
Result.failure()
63-
} else {
64-
Result.retry()
65-
}
66-
}.withFlag(result)
67-
}
68-
69-
return result
70-
}
71-
72-
private fun Result.withFlag(flag: Int): Int {
73-
return when (this) {
74-
is Result.Success -> RESULT_FAILURE_FLAG or flag
75-
is Result.Retry -> RESULT_RETRY_FLAG or flag
76-
else -> RESULT_SUCCESS_FLAG or flag
77-
}
78-
}
79-
80-
private fun Int.containsFlag(flag: Int): Boolean {
81-
return (this or flag) == this
82-
}
83-
8471
companion object {
8572
private val LOG_NAME = AttachmentSyncWorker::class.java.simpleName
86-
87-
private const val RESULT_SUCCESS_FLAG = 0
88-
private const val RESULT_FAILURE_FLAG = 1
89-
private const val RESULT_RETRY_FLAG = 2
90-
9173
private const val ATTACHMENT_SYNC_WORK = "mil.nga.mage.ATTACHMENT_SYNC_WORK"
9274
private const val ATTACHMENT_SYNC_NOTIFICATION_ID = 200
9375

mage/src/main/java/mil/nga/giat/mage/observation/sync/ObservationSyncWorker.kt

Lines changed: 58 additions & 90 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@ import mil.nga.giat.mage.database.model.observation.Observation
1616
import mil.nga.giat.mage.database.model.observation.ObservationFavorite
1717
import mil.nga.giat.mage.data.datasource.observation.ObservationLocalDataSource
1818
import mil.nga.giat.mage.database.model.observation.State
19-
import java.net.HttpURLConnection
2019
import java.util.concurrent.TimeUnit
2120

2221
@HiltWorker
@@ -28,144 +27,117 @@ class ObservationSyncWorker @AssistedInject constructor(
2827
) : CoroutineWorker(context, params) {
2928

3029
override suspend fun doWork(): Result {
31-
// Lock to ensure previous running work will complete when cancelled before new work is started.
30+
//Lock to ensure previous running work will complete when cancelled before new work is started.
3231
return mutex.withLock {
33-
var result = RESULT_SUCCESS_FLAG
34-
3532
try {
36-
result = syncObservations().withFlag(result)
37-
result = syncObservationImportant().withFlag(result)
38-
result = syncObservationFavorites().withFlag(result)
33+
//submit observations to the server that are marked as "dirty" and track the overall response flag of all transactions
34+
val overallResponseFlag = ResponseFlag.combineResponseFlags(
35+
syncObservations(),
36+
syncObservationImportant(),
37+
syncObservationFavorites()
38+
)
39+
40+
//if any of the transactions returned RetryFlag, then Result.retry() will be returned to retry the work request per back off policy
41+
//this should not result in the successful transactions being resubmitted, as they will no longer be marked as "dirty"
42+
when (overallResponseFlag) {
43+
ResponseFlag.SuccessFlag -> Result.success()
44+
ResponseFlag.FailureFlag -> Result.failure()
45+
ResponseFlag.RetryFlag -> Result.retry()
46+
}
3947
} catch (e: Exception) {
40-
result = RESULT_RETRY_FLAG
4148
Log.e(LOG_NAME, "Error trying to sync observations with server", e)
49+
//any unhandled exception should result in a retry
50+
Result.retry()
4251
}
43-
44-
if (result.containsFlag(RESULT_RETRY_FLAG)) Result.retry() else Result.success()
4552
}
4653
}
4754

48-
override suspend fun getForegroundInfo(): ForegroundInfo {
49-
val notification = NotificationCompat.Builder(applicationContext, MageApplication.MAGE_NOTIFICATION_CHANNEL_ID)
50-
.setSmallIcon(R.drawable.ic_sync_preference_24dp)
51-
.setContentTitle("Sync Observations")
52-
.setContentText("Pushing observation updates to MAGE.")
53-
.setPriority(NotificationCompat.PRIORITY_MAX)
54-
.setAutoCancel(true)
55-
.build()
56-
57-
return ForegroundInfo(OBSERVATION_SYNC_NOTIFICATION_ID, notification)
58-
}
59-
60-
private suspend fun syncObservations(): Int {
61-
var result = RESULT_SUCCESS_FLAG
55+
private suspend fun syncObservations(): ResponseFlag {
56+
var overallResult: ResponseFlag = ResponseFlag.SuccessFlag
6257

6358
for (observation in observationLocalDataSource.dirty) {
64-
result = syncObservation(observation).withFlag(result)
59+
val syncResult = syncObservation(observation)
60+
overallResult = ResponseFlag.combineResponseFlags(overallResult, syncResult)
6561
}
6662

67-
return result
63+
return overallResult
6864
}
6965

70-
private suspend fun syncObservation(observation: Observation): Int {
71-
val result = RESULT_SUCCESS_FLAG
72-
66+
private suspend fun syncObservation(observation: Observation): ResponseFlag {
7367
return if (observation.state == State.ARCHIVE) {
74-
archive(observation).withFlag(result)
68+
archive(observation)
7569
} else {
76-
save(observation).withFlag(result)
70+
save(observation)
7771
}
7872
}
7973

80-
private suspend fun syncObservationImportant(): Int {
81-
var result = RESULT_SUCCESS_FLAG
74+
private suspend fun syncObservationImportant(): ResponseFlag {
75+
var overallResult: ResponseFlag = ResponseFlag.SuccessFlag
8276

8377
for (observation in observationLocalDataSource.dirtyImportant) {
84-
result = updateImportant(observation).withFlag(result)
78+
val updateResult = updateImportant(observation)
79+
overallResult = ResponseFlag.combineResponseFlags(overallResult, updateResult)
8580
}
8681

87-
return result
82+
return overallResult
8883
}
8984

90-
private suspend fun syncObservationFavorites(): Int {
91-
var result = RESULT_SUCCESS_FLAG
85+
private suspend fun syncObservationFavorites(): ResponseFlag {
86+
var overallResult: ResponseFlag = ResponseFlag.SuccessFlag
9287

9388
for (favorite in observationLocalDataSource.dirtyFavorites) {
94-
result = updateFavorite(favorite).withFlag(result)
89+
val updateResult = updateFavorite(favorite)
90+
overallResult = ResponseFlag.combineResponseFlags(overallResult, updateResult)
9591
}
9692

97-
return result
93+
return overallResult
9894
}
9995

100-
private suspend fun save(observation: Observation): Result {
96+
private suspend fun save(observation: Observation): ResponseFlag {
10197
return if (observation.remoteId.isNullOrEmpty()) {
98+
//observation doesn't exist on the server, so create it
10299
create(observation)
103100
} else {
101+
//observation should exist on the server, so update it
104102
update(observation)
105103
}
106104
}
107105

108-
private suspend fun create(observation: Observation): Result {
106+
private suspend fun create(observation: Observation): ResponseFlag {
109107
val response = observationRepository.create(observation)
110-
return if (response.isSuccessful) {
111-
Result.success()
112-
} else {
113-
if (response.code() == HttpURLConnection.HTTP_UNAUTHORIZED) Result.failure() else Result.retry()
114-
}
108+
return ResponseFlag.processResponse(response)
115109
}
116110

117-
private suspend fun update(observation: Observation): Result {
111+
private suspend fun update(observation: Observation): ResponseFlag {
118112
val response = observationRepository.update(observation)
119-
return if (response.isSuccessful) {
120-
Result.success()
121-
} else {
122-
if (response.code() == HttpURLConnection.HTTP_UNAUTHORIZED) Result.failure() else Result.retry()
123-
}
113+
return ResponseFlag.processResponse(response)
124114
}
125115

126-
private suspend fun archive(observation: Observation): Result {
127-
val response = observationRepository.archive(observation)
128-
return when {
129-
response.isSuccessful -> Result.success()
130-
response.code() == HttpURLConnection.HTTP_NOT_FOUND -> Result.success()
131-
response.code() == HttpURLConnection.HTTP_UNAUTHORIZED -> Result.failure()
132-
else -> Result.retry()
133-
}
134-
}
135-
136-
private suspend fun updateImportant(observation: Observation): Result {
116+
private suspend fun updateImportant(observation: Observation): ResponseFlag {
137117
val response = observationRepository.updateImportant(observation)
138-
139-
return if (response.isSuccessful) {
140-
Result.success()
141-
} else {
142-
if (response.code() == HttpURLConnection.HTTP_UNAUTHORIZED) Result.failure() else Result.retry()
143-
}
118+
return ResponseFlag.processResponse(response)
144119
}
145120

146-
private suspend fun updateFavorite(favorite: ObservationFavorite): Result {
121+
private suspend fun updateFavorite(favorite: ObservationFavorite): ResponseFlag {
147122
val response = observationRepository.updateFavorite(favorite)
148-
return if (response.isSuccessful) {
149-
Result.success()
150-
} else {
151-
if (response.code() == HttpURLConnection.HTTP_UNAUTHORIZED) Result.failure() else Result.retry()
152-
}
123+
return ResponseFlag.processResponse(response)
153124
}
154125

155-
private fun Result.withFlag(flag: Int): Int {
156-
return when(this) {
157-
is Result.Failure -> RESULT_FAILURE_FLAG or flag
158-
is Result.Retry -> RESULT_RETRY_FLAG or flag
159-
else -> RESULT_SUCCESS_FLAG or flag
160-
}
126+
private suspend fun archive(observation: Observation): ResponseFlag {
127+
val response = observationRepository.archive(observation)
128+
return ResponseFlag.processArchiveResponse(response)
161129
}
162130

163-
private fun Int.containsFlag(flag: Int): Boolean {
164-
return (this or flag) == this
165-
}
131+
override suspend fun getForegroundInfo(): ForegroundInfo {
132+
val notification = NotificationCompat.Builder(applicationContext, MageApplication.MAGE_NOTIFICATION_CHANNEL_ID)
133+
.setSmallIcon(R.drawable.ic_sync_preference_24dp)
134+
.setContentTitle("Sync Observations")
135+
.setContentText("Pushing observation updates to MAGE.")
136+
.setPriority(NotificationCompat.PRIORITY_MAX)
137+
.setAutoCancel(true)
138+
.build()
166139

167-
private fun Int.withFlag(flag: Int): Int {
168-
return this or flag
140+
return ForegroundInfo(OBSERVATION_SYNC_NOTIFICATION_ID, notification)
169141
}
170142

171143
companion object {
@@ -174,10 +146,6 @@ class ObservationSyncWorker @AssistedInject constructor(
174146
private const val OBSERVATION_SYNC_WORK = "mil.nga.mage.OBSERVATION_SYNC_WORK"
175147
private const val OBSERVATION_SYNC_NOTIFICATION_ID = 100
176148

177-
private const val RESULT_SUCCESS_FLAG = 0
178-
private const val RESULT_FAILURE_FLAG = 1
179-
private const val RESULT_RETRY_FLAG = 2
180-
181149
private val mutex = Mutex()
182150

183151
fun scheduleWork(context: Context) {
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
package mil.nga.giat.mage.observation.sync
2+
3+
import retrofit2.Response
4+
import java.net.HttpURLConnection
5+
6+
sealed class ResponseFlag {
7+
data object SuccessFlag : ResponseFlag()
8+
data object FailureFlag : ResponseFlag()
9+
data object RetryFlag : ResponseFlag()
10+
11+
companion object {
12+
//Combine multiple ResponseFlags into a single result. If any flag is marked as retry, the result will be retry
13+
//If any flag is marked as failure and no flags are marked as retry, the result will be failure
14+
fun combineResponseFlags(vararg flags: ResponseFlag): ResponseFlag {
15+
return when {
16+
flags.any { it is RetryFlag } -> RetryFlag
17+
flags.any { it is FailureFlag } -> FailureFlag
18+
else -> SuccessFlag
19+
}
20+
}
21+
22+
//map retrofit API response to ResultFlag
23+
fun <T> processResponse(response: Response<T>): ResponseFlag {
24+
return when {
25+
response.isSuccessful -> SuccessFlag
26+
response.code() == HttpURLConnection.HTTP_UNAUTHORIZED -> FailureFlag
27+
else -> RetryFlag
28+
}
29+
}
30+
31+
//map retrofit API response to ResultFlag for archive API
32+
fun <T> processArchiveResponse(response: Response<T>): ResponseFlag {
33+
return when {
34+
response.isSuccessful -> SuccessFlag
35+
response.code() == HttpURLConnection.HTTP_NOT_FOUND -> SuccessFlag //observation no longer exists on the server
36+
response.code() == HttpURLConnection.HTTP_UNAUTHORIZED -> FailureFlag
37+
else -> RetryFlag
38+
}
39+
}
40+
}
41+
}

0 commit comments

Comments
 (0)