diff --git a/demo/src/main/java/com/google/android/fhir/demo/FhirApplication.kt b/demo/src/main/java/com/google/android/fhir/demo/FhirApplication.kt index cfb30758c7..16d109a439 100644 --- a/demo/src/main/java/com/google/android/fhir/demo/FhirApplication.kt +++ b/demo/src/main/java/com/google/android/fhir/demo/FhirApplication.kt @@ -26,11 +26,8 @@ import com.google.android.fhir.NetworkConfiguration import com.google.android.fhir.ServerConfiguration import com.google.android.fhir.datacapture.DataCaptureConfig import com.google.android.fhir.datacapture.XFhirQueryResolver -import com.google.android.fhir.demo.data.FhirSyncWorker import com.google.android.fhir.search.search -import com.google.android.fhir.sync.Sync import com.google.android.fhir.sync.remote.HttpLogger -import org.hl7.fhir.r4.model.Patient import timber.log.Timber class FhirApplication : Application(), DataCaptureConfig.Provider { @@ -46,7 +43,6 @@ class FhirApplication : Application(), DataCaptureConfig.Provider { if (BuildConfig.DEBUG) { Timber.plant(Timber.DebugTree()) } - Patient.IDENTIFIER FhirEngineProvider.init( FhirEngineConfiguration( enableEncryptionIfSupported = true, @@ -63,7 +59,6 @@ class FhirApplication : Application(), DataCaptureConfig.Provider { ) ) ) - Sync.oneTimeSync(this) dataCaptureConfig = DataCaptureConfig().apply { diff --git a/demo/src/main/java/com/google/android/fhir/demo/MainActivityViewModel.kt b/demo/src/main/java/com/google/android/fhir/demo/MainActivityViewModel.kt index d5c27b91e6..c9fc9bbf95 100644 --- a/demo/src/main/java/com/google/android/fhir/demo/MainActivityViewModel.kt +++ b/demo/src/main/java/com/google/android/fhir/demo/MainActivityViewModel.kt @@ -23,7 +23,7 @@ import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.viewModelScope import androidx.work.Constraints -import com.google.android.fhir.demo.data.FhirSyncWorker +import com.google.android.fhir.demo.data.DemoFhirSyncWorker import com.google.android.fhir.sync.PeriodicSyncConfiguration import com.google.android.fhir.sync.RepeatInterval import com.google.android.fhir.sync.Sync @@ -50,12 +50,13 @@ class MainActivityViewModel(application: Application) : AndroidViewModel(applica init { viewModelScope.launch { - Sync.periodicSync( + Sync.periodicSync( application.applicationContext, - PeriodicSyncConfiguration( - syncConstraints = Constraints.Builder().build(), - repeat = RepeatInterval(interval = 15, timeUnit = TimeUnit.MINUTES) - ) + periodicSyncConfiguration = + PeriodicSyncConfiguration( + syncConstraints = Constraints.Builder().build(), + repeat = RepeatInterval(interval = 15, timeUnit = TimeUnit.MINUTES) + ) ) .shareIn(this, SharingStarted.Eagerly, 10) .collect { _pollState.emit(it) } @@ -64,7 +65,7 @@ class MainActivityViewModel(application: Application) : AndroidViewModel(applica fun triggerOneTimeSync() { viewModelScope.launch { - Sync.oneTimeSync(getApplication()) + Sync.oneTimeSync(getApplication()) .shareIn(this, SharingStarted.Eagerly, 10) .collect { _pollState.emit(it) } } diff --git a/demo/src/main/java/com/google/android/fhir/demo/data/FhirSyncWorker.kt b/demo/src/main/java/com/google/android/fhir/demo/data/DemoFhirSyncWorker.kt similarity index 94% rename from demo/src/main/java/com/google/android/fhir/demo/data/FhirSyncWorker.kt rename to demo/src/main/java/com/google/android/fhir/demo/data/DemoFhirSyncWorker.kt index df92512e20..11ea86af99 100644 --- a/demo/src/main/java/com/google/android/fhir/demo/data/FhirSyncWorker.kt +++ b/demo/src/main/java/com/google/android/fhir/demo/data/DemoFhirSyncWorker.kt @@ -23,7 +23,7 @@ import com.google.android.fhir.sync.AcceptLocalConflictResolver import com.google.android.fhir.sync.DownloadWorkManager import com.google.android.fhir.sync.FhirSyncWorker -class FhirSyncWorker(appContext: Context, workerParams: WorkerParameters) : +class DemoFhirSyncWorker(appContext: Context, workerParams: WorkerParameters) : FhirSyncWorker(appContext, workerParams) { override fun getDownloadWorkManager(): DownloadWorkManager { diff --git a/engine/src/androidTest/java/com/google/android/fhir/sync/SyncInstrumentedTest.kt b/engine/src/androidTest/java/com/google/android/fhir/sync/SyncInstrumentedTest.kt new file mode 100644 index 0000000000..b6bd930747 --- /dev/null +++ b/engine/src/androidTest/java/com/google/android/fhir/sync/SyncInstrumentedTest.kt @@ -0,0 +1,119 @@ +/* + * Copyright 2022 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.android.fhir.sync + +import android.content.Context +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.platform.app.InstrumentationRegistry +import androidx.work.Constraints +import androidx.work.WorkInfo +import androidx.work.WorkManager +import androidx.work.WorkerParameters +import androidx.work.testing.WorkManagerTestInitHelper +import com.google.android.fhir.FhirEngine +import com.google.android.fhir.testing.TestDataSourceImpl +import com.google.android.fhir.testing.TestDownloadManagerImpl +import com.google.android.fhir.testing.TestFhirEngineImpl +import com.google.common.truth.Truth.assertThat +import java.util.concurrent.TimeUnit +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.shareIn +import kotlinx.coroutines.flow.transformWhile +import kotlinx.coroutines.runBlocking +import org.junit.Test +import org.junit.runner.RunWith + +@OptIn(ExperimentalCoroutinesApi::class) +@RunWith(AndroidJUnit4::class) +class SyncInstrumentedTest { + + private val context: Context = InstrumentationRegistry.getInstrumentation().targetContext + + class TestSyncWorker(appContext: Context, workerParams: WorkerParameters) : + FhirSyncWorker(appContext, workerParams) { + + override fun getFhirEngine(): FhirEngine = TestFhirEngineImpl + override fun getDataSource(): DataSource = TestDataSourceImpl + override fun getDownloadWorkManager(): DownloadWorkManager = TestDownloadManagerImpl() + override fun getConflictResolver() = AcceptRemoteConflictResolver + } + + @Test + fun oneTime_worker_runs() { + WorkManagerTestInitHelper.initializeTestWorkManager(context) + val workManager = WorkManager.getInstance(context) + runBlocking { + Sync.oneTimeSync(context = context) + .transformWhile { + emit(it is SyncJobStatus.Finished) + it !is SyncJobStatus.Finished + } + .shareIn(this, SharingStarted.Eagerly, 5) + } + + assertThat(workManager.getWorkInfosByTag(TestSyncWorker::class.java.name).get().first().state) + .isEqualTo(WorkInfo.State.SUCCEEDED) + } + + @Test + fun periodic_worker_still_queued_to_run_after_oneTime_worker_started() { + WorkManagerTestInitHelper.initializeTestWorkManager(context) + val workManager = WorkManager.getInstance(context) + // run and wait for periodic worker to finish + runBlocking { + Sync.periodicSync( + context = context, + periodicSyncConfiguration = + PeriodicSyncConfiguration( + syncConstraints = Constraints.Builder().build(), + repeat = RepeatInterval(interval = 15, timeUnit = TimeUnit.MINUTES) + ), + ) + .transformWhile { + emit(it) + it !is SyncJobStatus.Finished + } + .shareIn(this, SharingStarted.Eagerly, 5) + } + + // Verify the periodic worker completed the run, and is queued to run again + val periodicWorkerId = + workManager.getWorkInfosByTag(TestSyncWorker::class.java.name).get().first().id + assertThat(workManager.getWorkInfoById(periodicWorkerId).get().state) + .isEqualTo(WorkInfo.State.ENQUEUED) + + // Start and complete a oneTime job, and verify it does not remove the periodic worker + runBlocking { + Sync.oneTimeSync(context = context) + .transformWhile { + emit(it) + it !is SyncJobStatus.Finished + } + .shareIn(this, SharingStarted.Eagerly, 5) + } + assertThat(workManager.getWorkInfosByTag(TestSyncWorker::class.java.name).get().size) + .isEqualTo(2) + + // Move forward to the next epoch to trigger the periodic sync + val testDriver = WorkManagerTestInitHelper.getTestDriver(context) + testDriver?.setPeriodDelayMet(periodicWorkerId) + + assertThat(workManager.getWorkInfoById(periodicWorkerId).get().state) + .isEqualTo(WorkInfo.State.RUNNING) + } +} diff --git a/engine/src/main/java/com/google/android/fhir/sync/Sync.kt b/engine/src/main/java/com/google/android/fhir/sync/Sync.kt index 77d007dc07..4ff7f5dbfd 100644 --- a/engine/src/main/java/com/google/android/fhir/sync/Sync.kt +++ b/engine/src/main/java/com/google/android/fhir/sync/Sync.kt @@ -55,10 +55,11 @@ object Sync { context: Context, retryConfiguration: RetryConfiguration? = defaultRetryConfiguration ): Flow { - val flow = getWorkerInfo(context) + val uniqueWorkName = "${W::class.java.name}-oneTimeSync" + val flow = getWorkerInfo(context, uniqueWorkName) WorkManager.getInstance(context) .enqueueUniqueWork( - W::class.java.name, + uniqueWorkName, ExistingWorkPolicy.KEEP, createOneTimeWorkRequest(retryConfiguration, W::class.java) ) @@ -80,10 +81,11 @@ object Sync { context: Context, periodicSyncConfiguration: PeriodicSyncConfiguration ): Flow { - val flow = getWorkerInfo(context) + val uniqueWorkName = "${W::class.java.name}-periodicSync" + val flow = getWorkerInfo(context, uniqueWorkName) WorkManager.getInstance(context) .enqueueUniquePeriodicWork( - W::class.java.name, + uniqueWorkName, ExistingPeriodicWorkPolicy.KEEP, createPeriodicWorkRequest(periodicSyncConfiguration, W::class.java) ) @@ -91,9 +93,9 @@ object Sync { } /** Gets the worker info for the [FhirSyncWorker] */ - inline fun getWorkerInfo(context: Context) = + fun getWorkerInfo(context: Context, workName: String) = WorkManager.getInstance(context) - .getWorkInfosForUniqueWorkLiveData(W::class.java.name) + .getWorkInfosForUniqueWorkLiveData(workName) .asFlow() .flatMapConcat { it.asFlow() } .mapNotNull { workInfo -> @@ -106,11 +108,6 @@ object Sync { } } - /** Gets the timestamp of the last sync job. */ - fun getLastSyncTimestamp(context: Context): OffsetDateTime? { - return DatastoreUtil(context).readLastSyncTimestamp() - } - @PublishedApi internal inline fun createOneTimeWorkRequest( retryConfiguration: RetryConfiguration?, @@ -155,4 +152,9 @@ object Sync { } return periodicWorkRequestBuilder.build() } + + /** Gets the timestamp of the last sync job. */ + fun getLastSyncTimestamp(context: Context): OffsetDateTime? { + return DatastoreUtil(context).readLastSyncTimestamp() + } }