Skip to content

Commit

Permalink
feat(#133): distinguish between data synchronization and observation …
Browse files Browse the repository at this point in the history
…records synchronization
  • Loading branch information
sgrimault committed Mar 22, 2023
1 parent 8a24912 commit d0d9a70
Show file tree
Hide file tree
Showing 45 changed files with 1,557 additions and 890 deletions.
2 changes: 2 additions & 0 deletions occtax/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ dependencies {
implementation 'androidx.preference:preference-ktx:1.2.0'
implementation 'androidx.recyclerview:recyclerview:1.2.1'
implementation 'com.google.android.material:material:1.5.0'
implementation 'androidx.work:work-runtime-ktx:2.7.1'
implementation "io.github.l4digital:fastscroll:2.0.1"

// Logging
Expand All @@ -89,6 +90,7 @@ dependencies {
// Testing dependencies
testImplementation 'androidx.arch.core:core-testing:2.1.0'
testImplementation 'androidx.test.ext:junit-ktx:1.1.3'
testImplementation 'androidx.work:work-testing:2.7.1'
testImplementation("com.squareup.okhttp3:mockwebserver:4.10.0")
testImplementation 'io.mockk:mockk:1.12.3'
testImplementation 'io.mockk:mockk-agent-jvm:1.12.3'
Expand Down
4 changes: 2 additions & 2 deletions occtax/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@
<activity
android:name=".ui.home.HomeActivity"
android:exported="true"
android:launchMode="singleTask">
android:launchMode="singleTask"
android:theme="@style/AppTheme.NoActionBar">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
Expand Down Expand Up @@ -68,7 +69,6 @@
android:name="androidx.startup.InitializationProvider"
android:authorities="${applicationId}.androidx-startup"
tools:node="remove" />

<provider
android:name="fr.geonature.commons.data.MainContentProvider"
android:authorities="${applicationId}.provider"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ import fr.geonature.commons.features.taxon.data.ITaxonLocalDataSource
import fr.geonature.commons.settings.IAppSettingsManager
import fr.geonature.datasync.api.IGeoNatureAPIClient
import fr.geonature.datasync.auth.IAuthManager
import fr.geonature.datasync.packageinfo.ISynchronizeObservationRecordRepository
import fr.geonature.occtax.api.IOcctaxAPIClient
import fr.geonature.occtax.features.record.data.IMediaRecordLocalDataSource
import fr.geonature.occtax.features.record.data.IObservationRecordLocalDataSource
Expand All @@ -22,6 +21,7 @@ import fr.geonature.occtax.features.record.data.ObservationRecordLocalDataSource
import fr.geonature.occtax.features.record.data.ObservationRecordRemoteDataSourceImpl
import fr.geonature.occtax.features.record.repository.IMediaRecordRepository
import fr.geonature.occtax.features.record.repository.IObservationRecordRepository
import fr.geonature.occtax.features.record.repository.ISynchronizeObservationRecordRepository
import fr.geonature.occtax.features.record.repository.MediaRecordRepositoryImpl
import fr.geonature.occtax.features.record.repository.ObservationRecordRepositoryImpl
import fr.geonature.occtax.features.record.repository.SynchronizeObservationRecordRepositoryImpl
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,10 @@ data class ObservationRecord(

enum class Status {
DRAFT,
TO_SYNC
TO_SYNC,
SYNC_IN_PROGRESS,
SYNC_ERROR,
SYNC_SUCCESSFUL
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package fr.geonature.occtax.features.record.domain

import androidx.work.WorkInfo

/**
* Describes the current status of [ObservationRecord] synchronization.
*
* @author S. Grimault
*/
sealed class SynchronizationStatus(open val state: WorkInfo.State ) {

/**
* The current worker status.
*/
data class WorkerStatus(override val state: WorkInfo.State) : SynchronizationStatus(state)

/**
* The current [ObservationRecord] status.
*/
data class ObservationRecordStatus(
override val state: WorkInfo.State,
val internalId: Long,
val status: ObservationRecord.Status
) : SynchronizationStatus(state)
}
Original file line number Diff line number Diff line change
@@ -1,19 +1,29 @@
package fr.geonature.occtax.features.record.presentation

import android.app.Application
import androidx.lifecycle.LiveData
import androidx.lifecycle.MediatorLiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.Transformations.map
import androidx.lifecycle.viewModelScope
import androidx.work.WorkInfo
import androidx.work.WorkManager
import dagger.hilt.android.lifecycle.HiltViewModel
import fr.geonature.commons.interactor.BaseResultUseCase
import fr.geonature.commons.lifecycle.BaseViewModel
import fr.geonature.commons.lifecycle.BaseAndroidViewModel
import fr.geonature.occtax.features.record.domain.ObservationRecord
import fr.geonature.occtax.features.record.domain.SynchronizationStatus
import fr.geonature.occtax.features.record.usecase.DeleteObservationRecordUseCase
import fr.geonature.occtax.features.record.usecase.EditObservationRecordUseCase
import fr.geonature.occtax.features.record.usecase.ExportObservationRecordUseCase
import fr.geonature.occtax.features.record.usecase.GetAllObservationRecordsUseCase
import fr.geonature.occtax.features.record.usecase.SaveObservationRecordUseCase
import fr.geonature.occtax.features.record.worker.SynchronizeObservationRecordsWorker
import fr.geonature.occtax.settings.AppSettings
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import org.tinylog.kotlin.Logger
import java.util.UUID
import javax.inject.Inject

/**
Expand All @@ -23,19 +33,104 @@ import javax.inject.Inject
*/
@HiltViewModel
class ObservationRecordViewModel @Inject constructor(
application: Application,
private val getAllObservationRecordsUseCase: GetAllObservationRecordsUseCase,
private val saveObservationRecordUseCase: SaveObservationRecordUseCase,
private val editObservationRecordUseCase: EditObservationRecordUseCase,
private val deleteObservationRecordUseCase: DeleteObservationRecordUseCase,
private val exportObservationRecordUseCase: ExportObservationRecordUseCase
) : BaseViewModel() {
) : BaseAndroidViewModel(application) {

private val workManager: WorkManager = WorkManager.getInstance(getApplication())

private val _observationRecords = MutableLiveData<List<ObservationRecord>>()
val observationRecords: LiveData<List<ObservationRecord>> = _observationRecords

private val _observationRecord = MutableLiveData<ObservationRecord>()

/**
* The current [ObservationRecord] being edited.
*/
val observationRecord: LiveData<ObservationRecord> = _observationRecord

private var currentSyncWorkerId: UUID? = null
set(value) {
field = value
_isSyncRunning.postValue(field != null)
}

private val _isSyncRunning: MutableLiveData<Boolean> = MutableLiveData(false)
val isSyncRunning: LiveData<Boolean> = _isSyncRunning

private val _observeSynchronizationStatus: LiveData<SynchronizationStatus?> =
map(workManager.getWorkInfosByTagLiveData(SynchronizeObservationRecordsWorker.OBSERVATION_RECORDS_SYNC_WORKER_TAG)) { workInfoList ->
if (workInfoList == null || workInfoList.isEmpty()) {
currentSyncWorkerId = null
return@map null
}

val workInfo = workInfoList.firstOrNull { it.id == currentSyncWorkerId }
?: workInfoList.firstOrNull { it.state == WorkInfo.State.RUNNING }

// no work info is running: abort
if (workInfo == null) {
currentSyncWorkerId = null
return@map null
}

// this is a new work info: set the current worker
if (workInfo.id != currentSyncWorkerId) {
currentSyncWorkerId = workInfo.id
}

workInfoList.firstOrNull()
?.let {
SynchronizeObservationRecordsWorker.toSynchronizationStatus(it)
}
?.also {
if (it.state.isFinished) currentSyncWorkerId = null
}
?: return@map null
}

/**
* All [ObservationRecord]s loaded.
*/
val observationRecords: LiveData<List<ObservationRecord>> =
MediatorLiveData<List<ObservationRecord>>().apply {
addSource(_observationRecords) { value = it }
addSource(_observeSynchronizationStatus) { synchronizationStatus ->
if (synchronizationStatus == null) return@addSource

value = (value ?: emptyList()).map { observationRecord ->
synchronizationStatus.takeIf { it is SynchronizationStatus.ObservationRecordStatus }
?.let { it as SynchronizationStatus.ObservationRecordStatus }
?.let {
if (it.internalId == observationRecord.internalId) {
observationRecord.copy(status = it.status)
} else observationRecord
}
?: observationRecord
}
viewModelScope.launch {
if (synchronizationStatus is SynchronizationStatus.ObservationRecordStatus && synchronizationStatus.status == ObservationRecord.Status.SYNC_SUCCESSFUL) {
delay(500)
value = (value
?: emptyList()).filter { it.internalId != synchronizationStatus.internalId }
}
}
}
}

/**
* Whether some [ObservationRecord]s are ready to synchronize according to their current status.
*/
val hasObservationRecordsReadyToSynchronize =
map(_observationRecords) { observationRecords ->
observationRecords.any {
it.status == ObservationRecord.Status.TO_SYNC
}
}

/**
* Gets all [ObservationRecord]s.
*
Expand Down Expand Up @@ -159,4 +254,13 @@ class ObservationRecordViewModel @Inject constructor(
)
}
}

/**
* Synchronizes all eligible [ObservationRecord]s (i.e. with a valid status [ObservationRecord.Status.TO_SYNC]).
*/
fun synchronizeObservationRecords() {
currentSyncWorkerId = SynchronizeObservationRecordsWorker.enqueueUniqueWork(
getApplication()
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package fr.geonature.occtax.features.record.repository

import fr.geonature.occtax.features.record.domain.ObservationRecord
import fr.geonature.occtax.features.record.error.ObservationRecordException

/**
* Synchronize observation record.
*
* @author S. Grimault
*/
interface ISynchronizeObservationRecordRepository {

/**
* Performs synchronization of given [ObservationRecord].
* Returns [ObservationRecordException.InvalidStatusException] if this [ObservationRecord] has a
* wrong status.
*/
suspend fun synchronize(observationRecord: ObservationRecord): Result<ObservationRecord>
}
Loading

0 comments on commit d0d9a70

Please sign in to comment.