Skip to content

Commit

Permalink
Update Sync To Use New Api
Browse files Browse the repository at this point in the history
  • Loading branch information
j-schwar committed Sep 3, 2020
1 parent d4dd03f commit 46b7113
Show file tree
Hide file tree
Showing 8 changed files with 396 additions and 5 deletions.
46 changes: 46 additions & 0 deletions app/src/main/java/com/cradle/neptune/manager/PatientManager.kt
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import com.cradle.neptune.model.Patient
import com.cradle.neptune.model.PatientAndReadings
import com.cradle.neptune.net.NetworkResult
import com.cradle.neptune.net.RestApi
import com.cradle.neptune.net.Success
import java.util.ArrayList
import javax.inject.Inject
import kotlinx.coroutines.Dispatchers.IO
Expand Down Expand Up @@ -86,6 +87,51 @@ class PatientManager @Inject constructor(
suspend fun getEditedPatients(timeStamp: Long): List<Patient> =
withContext(IO) { daoAccess.getEditedPatients(timeStamp) }

/**
* Uploads a patient and associated readings to the server.
*
* Upon successful upload, the patient's `base` field will be updated to
* reflect the fact that any changes made on mobile have been received by
* the server.
*
* @param patientAndReadings the patient to upload
* @return whether the upload succeeded or not
*/
suspend fun uploadPatient(patientAndReadings: PatientAndReadings): NetworkResult<Unit> =
withContext(IO) {
val result = restApi.postPatient(patientAndReadings)
if (result is Success) {
// Update the patient's `base` field if successfully uploaded
val patient = patientAndReadings.patient
patient.base = patient.lastEdited
add(patient)
}

result.map { Unit }
}

/**
* Uploads an edited patient to the server.
*
* Upon successful upload, the patient's `base` field will be updated to
* reflect the fact that any changes made on mobile have been received by
* the server.
*
* @param patient the patient to upload
* @return whether the upload succeeded or not
*/
suspend fun updatePatientOnServer(patient: Patient): NetworkResult<Unit> =
withContext(IO) {
val result = restApi.putPatient(patient)
if (result is Success) {
// Update the patient's `base` field if successfully uploaded
patient.base = patient.lastEdited
add(patient)
}

result.map { Unit }
}

/**
* Downloads all the information for a patient from the server.
*
Expand Down
194 changes: 194 additions & 0 deletions app/src/main/java/com/cradle/neptune/manager/SyncManager.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,194 @@
package com.cradle.neptune.manager

import android.content.SharedPreferences
import android.util.Log
import com.cradle.neptune.net.NetworkResult
import com.cradle.neptune.net.RestApi
import com.cradle.neptune.net.Success
import com.cradle.neptune.net.monadicSequence
import com.cradle.neptune.sync.SyncStepperCallback
import com.cradle.neptune.sync.TotalRequestStatus
import com.cradle.neptune.utilitiles.UnixTimestamp
import javax.inject.Inject
import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.Dispatchers.Main
import kotlinx.coroutines.async
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext

class SyncManager @Inject constructor(
private val patientManager: PatientManager,
private val readingManager: ReadingManager,
private val sharedPreferences: SharedPreferences,
private val restApi: RestApi
) {
companion object {
const val LAST_SYNC = "lastSyncTime"
}

/**
* Invokes the mobile sync algorithm.
*/
suspend fun sync(callback: SyncStepperCallback): NetworkResult<Unit> =
withContext<NetworkResult<Unit>>(IO) context@{
// Get update lists from the server
val lastSyncTime = sharedPreferences.getLong(LAST_SYNC, 0)
val updates = restApi.getUpdates(lastSyncTime)
if (updates !is Success) {
withContext(Main) {
callback.onFetchDataCompleted(false)
}
return@context updates.cast()
}
withContext(Main) {
callback.onFetchDataCompleted(true)
}

// Upload new readings and patients from mobile to server
val npUpload = async(IO) {
val results = patientManager
.getUnUploadedPatients()
.filterNot { updates.value.newPatients.contains(it.patient.id) }
.map { patient -> patientManager.uploadPatient(patient) }
monadicSequence(Unit, results)
}

val epUpload = async(IO) {
val results = patientManager
.getEditedPatients(lastSyncTime)
.filterNot { updates.value.editedPatients.contains(it.id) }
.map { patient -> patientManager.updatePatientOnServer(patient) }
monadicSequence(Unit, results)
}

val rUpload = async(IO) {
val results = readingManager
.getUnUploadedReadingsForServerPatients()
.filterNot { updates.value.readings.contains(it.id) }
.map { reading -> restApi.postReading(reading).map { Unit } }
monadicSequence(Unit, results)
}

val uploadResult = npUpload.await() sequence epUpload.await() sequence rUpload.await()
// FIXME: UI is currently broken here as we've moved away from a
// sequential upload algorithm to a parallel one. We can either
// update each of the async blocks to increment a single status
// or just update the UI to something else.
val uploadStatus = if (uploadResult is Success) {
TotalRequestStatus(1, 0, 1)
} else {
TotalRequestStatus(1, 1, 0)
}
withContext(Main) {
callback.onNewPatientAndReadingUploadFinish(uploadStatus)
}

// Download new information from server using the update lists as a guide
val npDl = async(IO) {
val results = updates
.value
.newPatients
.map { id -> // For each new patient in updates...
// download patient then store it in the database if successful
restApi.getPatient(id).map { patientAndReadings ->
patientManager.add(patientAndReadings.patient)
patientAndReadings.readings.forEach {
it.isUploadedToServer = true
}
readingManager.addAllReadings(patientAndReadings.readings)
Unit
}
}
monadicSequence(Unit, results)
}

val epDl = async(IO) {
val results = updates
.value
.editedPatients
.map { id -> // For each edited patient in updates...
// download patient then store it in the database if successful
restApi.getPatientInfo(id).map { patient ->
patientManager.add(patient)
Unit
}
}
monadicSequence(Unit, results)
}

val rDl = async(IO) {
val results = updates
.value
.readings
.map { id -> // For each reading id in updates...
// download reading then store it in the database if successful
restApi.getReading(id).map { reading ->
reading.isUploadedToServer = true
readingManager.addReading(reading)
Unit
}
}
monadicSequence(Unit, results)
}

val fDl = async(IO) {
val results = updates
.value
.followups
.map { id -> // For each assessment id in updates...
// Download assessment
restApi.getAssessment(id).map { assessment ->
// Lookup associated reading
val reading = runBlocking(IO) {
readingManager.getReadingById(assessment.readingId)
}
// Add assessment to reading
reading?.followUp = assessment
reading?.referral?.isAssessed = true
// Update reading in database
if (reading != null) {
readingManager.addReading(reading)
} else {
Log.e(
this@SyncManager::class.simpleName,
"Got assessment for unknown reading $assessment"
)
}
Unit
}
}
monadicSequence(Unit, results)
}

val downloadResult = npDl.await() sequence epDl.await() sequence rDl.await() sequence fDl.await()
// FIXME: See above
val downloadStatus = if (downloadResult is Success) {
TotalRequestStatus(1, 0, 1)
} else {
TotalRequestStatus(1, 1, 0)
}
withContext(Main) {
callback.onNewPatientAndReadingDownloadFinish(downloadStatus)
}

// Finish up by updating the last sync timestamp in shared preferences

// FIXME: We really should be aborting after the first error. For
// example, if we failed to upload we should not download etc.
val result = uploadResult sequence downloadResult

if (result is Success) {
sharedPreferences.edit()
.putLong(LAST_SYNC, UnixTimestamp.now)
.apply()
// FIXME: Not tracking errors like this anymore, find a new way to
// show errors to the user.
withContext(Main) {
callback.onFinish(HashMap())
}
}

// Return overall result
result
}
}
2 changes: 1 addition & 1 deletion app/src/main/java/com/cradle/neptune/model/Referral.kt
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ data class Referral(
val userId: Int?,
val patientId: String,
val readingId: String,
val isAssessed: Boolean
var isAssessed: Boolean
) : Serializable,
Marshal<JSONObject> {

Expand Down
42 changes: 42 additions & 0 deletions app/src/main/java/com/cradle/neptune/model/SyncUpdate.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package com.cradle.neptune.model

import com.cradle.neptune.ext.Field
import com.cradle.neptune.ext.arrayField
import com.cradle.neptune.ext.toList
import org.json.JSONArray
import org.json.JSONObject

/**
* API response model for the `/sync/updates` endpoint. Contains lists of
* new/edited patients, readings, and assessments (aka. followups).
*
* @property newPatients List of ids of new patients that have been created
* since the last sync
* @property editedPatients List of ids of patients which have been edited
* since the last sync
* @property readings List of ids of new readings since the last sync
* @property followups List of ids of new followups since the last sync
*/
data class SyncUpdate(
val newPatients: Set<String>,
val editedPatients: Set<String>,
val readings: Set<String>,
val followups: Set<String>
) {
companion object : Unmarshal<SyncUpdate, JSONObject> {
override fun unmarshal(data: JSONObject): SyncUpdate =
SyncUpdate(
HashSet(data.arrayField(SyncUpdateField.NEW_PATIENTS).toList(JSONArray::getString)),
HashSet(data.arrayField(SyncUpdateField.EDITED_PATIENTS).toList(JSONArray::getString)),
HashSet(data.arrayField(SyncUpdateField.READINGS).toList(JSONArray::getString)),
HashSet(data.arrayField(SyncUpdateField.FOLLOWUPS).toList(JSONArray::getString))
)
}
}

private enum class SyncUpdateField(override val text: String) : Field {
NEW_PATIENTS("newPatients"),
EDITED_PATIENTS("editedPatients"),
READINGS("readings"),
FOLLOWUPS("followups"),
}
44 changes: 44 additions & 0 deletions app/src/main/java/com/cradle/neptune/net/NetworkResult.kt
Original file line number Diff line number Diff line change
@@ -1,11 +1,24 @@
package com.cradle.neptune.net

import com.cradle.neptune.model.Unmarshal
import com.cradle.neptune.utilitiles.functional.fold1

/**
* Sum type representing the result of a network request.
*/
sealed class NetworkResult<T> {

companion object {
/**
* Lifts [value] into the [NetworkResult] monad.
*
* @param value the value to lift
* @return A successful network result which contains [value]
*/
@Suppress("MagicNumber")
fun <T> pure(value: T) = Success(value, 200)
}

/**
* Unwraps this network result into an optional value.
*
Expand Down Expand Up @@ -54,6 +67,21 @@ sealed class NetworkResult<T> {
is Failure -> Failure(body, statusCode)
is NetworkException -> NetworkException(cause)
}

/**
* Sequences two [NetworkResult] monads into a single result which will
* be a [Success] variant iff both results are [Success] variants. If
* either are erroneous variants, the first erroneous result is returned.
*
* @return A sequenced [NetworkResult]
*/
infix fun <U> sequence(other: NetworkResult<U>) = when {
// Results in Success
this is Success && other is Success -> other
// Results if Failure or NetworkException
this is Success -> other
else -> this.cast()
}
}

/**
Expand Down Expand Up @@ -129,3 +157,19 @@ data class Failure<T>(val body: ByteArray, val statusCode: Int) : NetworkResult<
* @property cause the exception which caused the failure
*/
data class NetworkException<T>(val cause: Exception) : NetworkResult<T>()

/**
* Sequences a list of results into a single result.
*
* See [NetworkResult.sequence] for more information.
*/
fun <T> monadicSequence(results: List<NetworkResult<T>>) =
results.fold1(NetworkResult<T>::sequence)

/**
* Sequences a list of results into a single result.
*
* See [NetworkResult.sequence] for more information.
*/
fun <T> monadicSequence(initial: T, results: List<NetworkResult<T>>) =
results.fold<NetworkResult<T>, NetworkResult<T>>(NetworkResult.pure(initial), NetworkResult<T>::sequence)
Loading

0 comments on commit 46b7113

Please sign in to comment.