diff --git a/analytics/src/main/java/org/odk/collect/analytics/Analytics.kt b/analytics/src/main/java/org/odk/collect/analytics/Analytics.kt index 6bfefeb6080..033392ea33d 100644 --- a/analytics/src/main/java/org/odk/collect/analytics/Analytics.kt +++ b/analytics/src/main/java/org/odk/collect/analytics/Analytics.kt @@ -41,5 +41,9 @@ interface Analytics { fun setParam(key: String, value: String) { params[key] = value } + + fun setUserProperty(name: String, value: String) { + instance.setUserProperty(name, value) + } } } diff --git a/androidshared/src/main/java/org/odk/collect/androidshared/network/ConnectivityProvider.kt b/androidshared/src/main/java/org/odk/collect/androidshared/network/ConnectivityProvider.kt deleted file mode 100644 index 39faf56766e..00000000000 --- a/androidshared/src/main/java/org/odk/collect/androidshared/network/ConnectivityProvider.kt +++ /dev/null @@ -1,19 +0,0 @@ -package org.odk.collect.androidshared.network - -import android.content.Context -import android.net.ConnectivityManager -import android.net.NetworkInfo - -class ConnectivityProvider(private val context: Context) : NetworkStateProvider { - override val isDeviceOnline: Boolean - get() { - val networkInfo = networkInfo - return networkInfo != null && networkInfo.isConnected - } - - override val networkInfo: NetworkInfo? - get() = connectivityManager.activeNetworkInfo - - private val connectivityManager: ConnectivityManager - get() = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager -} diff --git a/androidshared/src/main/java/org/odk/collect/androidshared/network/NetworkStateProvider.kt b/androidshared/src/main/java/org/odk/collect/androidshared/network/NetworkStateProvider.kt deleted file mode 100644 index d3b9e983c58..00000000000 --- a/androidshared/src/main/java/org/odk/collect/androidshared/network/NetworkStateProvider.kt +++ /dev/null @@ -1,8 +0,0 @@ -package org.odk.collect.androidshared.network - -import android.net.NetworkInfo - -interface NetworkStateProvider { - val isDeviceOnline: Boolean - val networkInfo: NetworkInfo? -} diff --git a/async/build.gradle.kts b/async/build.gradle.kts index 9c958ba7e33..d6eae2c48cb 100644 --- a/async/build.gradle.kts +++ b/async/build.gradle.kts @@ -42,6 +42,9 @@ dependencies { implementation(Dependencies.androidx_core_ktx) implementation(Dependencies.kotlinx_coroutines_android) implementation(Dependencies.androidx_work_runtime) + implementation(project(":analytics")) { + exclude("com.google.firebase") + } testImplementation(Dependencies.hamcrest) testImplementation(Dependencies.robolectric) diff --git a/async/src/main/AndroidManifest.xml b/async/src/main/AndroidManifest.xml index c7e7078dcda..d7546021b58 100644 --- a/async/src/main/AndroidManifest.xml +++ b/async/src/main/AndroidManifest.xml @@ -1,2 +1,3 @@ - \ No newline at end of file + + diff --git a/async/src/main/java/org/odk/collect/async/CoroutineAndWorkManagerScheduler.kt b/async/src/main/java/org/odk/collect/async/CoroutineAndWorkManagerScheduler.kt index d07aa1aad6c..d07a2448abd 100644 --- a/async/src/main/java/org/odk/collect/async/CoroutineAndWorkManagerScheduler.kt +++ b/async/src/main/java/org/odk/collect/async/CoroutineAndWorkManagerScheduler.kt @@ -13,25 +13,44 @@ import kotlinx.coroutines.Dispatchers import java.util.concurrent.TimeUnit import kotlin.coroutines.CoroutineContext -class CoroutineAndWorkManagerScheduler(foregroundContext: CoroutineContext, backgroundContext: CoroutineContext, private val workManager: WorkManager) : CoroutineScheduler(foregroundContext, backgroundContext) { - - constructor(workManager: WorkManager) : this(Dispatchers.Main, Dispatchers.IO, workManager) // Needed for Java construction - - override fun networkDeferred(tag: String, spec: TaskSpec, inputData: Map, networkConstraint: Scheduler.NetworkType?) { +class CoroutineAndWorkManagerScheduler( + foregroundContext: CoroutineContext, + backgroundContext: CoroutineContext, + private val workManager: WorkManager +) : CoroutineScheduler(foregroundContext, backgroundContext) { + + constructor(workManager: WorkManager) : this( + Dispatchers.Main, + Dispatchers.IO, + workManager + ) // Needed for Java construction + + override fun networkDeferred( + tag: String, + spec: TaskSpec, + inputData: Map, + networkConstraint: Scheduler.NetworkType? + ) { val constraintNetworkType = when (networkConstraint) { Scheduler.NetworkType.WIFI -> NetworkType.UNMETERED Scheduler.NetworkType.CELLULAR -> NetworkType.METERED - null -> NetworkType.CONNECTED + else -> NetworkType.CONNECTED } val constraints = Constraints.Builder() .setRequiredNetworkType(constraintNetworkType) .build() - val workManagerInputData = Data.Builder().putAll(inputData).build() + val workManagerInputData = Data.Builder() + .putString(TaskSpecWorker.DATA_TASK_SPEC_CLASS, spec.javaClass.name) + .putBoolean( + TaskSpecWorker.DATA_CELLULAR_ONLY, + networkConstraint == Scheduler.NetworkType.CELLULAR + ) + .putAll(inputData) + .build() - val worker = spec.getWorkManagerAdapter() - val workRequest = OneTimeWorkRequest.Builder(worker) + val workRequest = OneTimeWorkRequest.Builder(TaskSpecWorker::class.java) .addTag(tag) .setConstraints(constraints) .setInputData(workManagerInputData) @@ -40,15 +59,26 @@ class CoroutineAndWorkManagerScheduler(foregroundContext: CoroutineContext, back workManager.beginUniqueWork(tag, ExistingWorkPolicy.REPLACE, workRequest).enqueue() } - override fun networkDeferredRepeat(tag: String, spec: TaskSpec, repeatPeriod: Long, inputData: Map) { + override fun networkDeferredRepeat( + tag: String, + spec: TaskSpec, + repeatPeriod: Long, + inputData: Map + ) { val constraints = Constraints.Builder() .setRequiredNetworkType(NetworkType.CONNECTED) .build() - val workManagerInputData = Data.Builder().putAll(inputData).build() + val workManagerInputData = Data.Builder() + .putString(TaskSpecWorker.DATA_TASK_SPEC_CLASS, spec.javaClass.name) + .putAll(inputData) + .build() - val worker = spec.getWorkManagerAdapter() - val builder = PeriodicWorkRequest.Builder(worker, repeatPeriod, TimeUnit.MILLISECONDS) + val builder = PeriodicWorkRequest.Builder( + TaskSpecWorker::class.java, + repeatPeriod, + TimeUnit.MILLISECONDS + ) .addTag(tag) .setInputData(workManagerInputData) .setConstraints(constraints) @@ -59,7 +89,11 @@ class CoroutineAndWorkManagerScheduler(foregroundContext: CoroutineContext, back } } - workManager.enqueueUniquePeriodicWork(tag, ExistingPeriodicWorkPolicy.REPLACE, builder.build()) + workManager.enqueueUniquePeriodicWork( + tag, + ExistingPeriodicWorkPolicy.REPLACE, + builder.build() + ) } override fun cancelDeferred(tag: String) { diff --git a/async/src/main/java/org/odk/collect/async/Scheduler.kt b/async/src/main/java/org/odk/collect/async/Scheduler.kt index 763e26da5c2..2d80d54bb26 100644 --- a/async/src/main/java/org/odk/collect/async/Scheduler.kt +++ b/async/src/main/java/org/odk/collect/async/Scheduler.kt @@ -81,7 +81,8 @@ interface Scheduler { enum class NetworkType { WIFI, - CELLULAR + CELLULAR, + OTHER } } diff --git a/async/src/main/java/org/odk/collect/async/TaskSpec.kt b/async/src/main/java/org/odk/collect/async/TaskSpec.kt index 9a17d788a54..d6643e0842c 100644 --- a/async/src/main/java/org/odk/collect/async/TaskSpec.kt +++ b/async/src/main/java/org/odk/collect/async/TaskSpec.kt @@ -18,10 +18,4 @@ interface TaskSpec { * once instead of doing that after every single execution. */ fun getTask(context: Context, inputData: Map, isLastUniqueExecution: Boolean): Supplier - - /** - * Returns class that can be used to schedule this task using Android's - * WorkManager framework - */ - fun getWorkManagerAdapter(): Class } diff --git a/async/src/main/java/org/odk/collect/async/TaskSpecWorker.kt b/async/src/main/java/org/odk/collect/async/TaskSpecWorker.kt new file mode 100644 index 00000000000..c3e41880436 --- /dev/null +++ b/async/src/main/java/org/odk/collect/async/TaskSpecWorker.kt @@ -0,0 +1,47 @@ +package org.odk.collect.async + +import android.content.Context +import androidx.work.Worker +import androidx.work.WorkerParameters +import org.odk.collect.analytics.Analytics +import org.odk.collect.async.network.ConnectivityProvider + +class TaskSpecWorker( + context: Context, + workerParams: WorkerParameters +) : Worker(context, workerParams) { + + private val connectivityProvider: ConnectivityProvider = ConnectivityProvider(context) + + override fun doWork(): Result { + val cellularOnly = inputData.getBoolean(DATA_CELLULAR_ONLY, false) + if (cellularOnly && connectivityProvider.currentNetwork != Scheduler.NetworkType.CELLULAR) { + Analytics.setUserProperty("EncounteredMeteredNonCellularInTasks", "true") + return Result.retry() + } + + val specClass = inputData.getString(DATA_TASK_SPEC_CLASS)!! + val spec = Class.forName(specClass).getConstructor().newInstance() as TaskSpec + + val stringInputData = inputData.keyValueMap.mapValues { it.value.toString() } + val completed = + spec.getTask(applicationContext, stringInputData, isLastUniqueExecution(spec)).get() + val maxRetries = spec.maxRetries + + return if (completed) { + Result.success() + } else if (maxRetries == null || runAttemptCount < maxRetries) { + Result.retry() + } else { + Result.failure() + } + } + + private fun isLastUniqueExecution(spec: TaskSpec) = + spec.maxRetries?.let { runAttemptCount >= it } ?: true + + companion object { + const val DATA_TASK_SPEC_CLASS = "taskSpecClass" + const val DATA_CELLULAR_ONLY = "cellularOnly" + } +} diff --git a/async/src/main/java/org/odk/collect/async/WorkerAdapter.kt b/async/src/main/java/org/odk/collect/async/WorkerAdapter.kt deleted file mode 100644 index fc65d1575e0..00000000000 --- a/async/src/main/java/org/odk/collect/async/WorkerAdapter.kt +++ /dev/null @@ -1,28 +0,0 @@ -package org.odk.collect.async - -import android.content.Context -import androidx.work.Worker -import androidx.work.WorkerParameters - -abstract class WorkerAdapter( - private val spec: TaskSpec, - context: Context, - workerParams: WorkerParameters -) : Worker(context, workerParams) { - - override fun doWork(): Result { - val stringInputData = inputData.keyValueMap.mapValues { it.value.toString() } - val completed = spec.getTask(applicationContext, stringInputData, isLastUniqueExecution()).get() - val maxRetries = spec.maxRetries - - return if (completed) { - Result.success() - } else if (maxRetries == null || runAttemptCount < maxRetries) { - Result.retry() - } else { - Result.failure() - } - } - - private fun isLastUniqueExecution() = spec.maxRetries?.let { runAttemptCount >= it } ?: true -} diff --git a/async/src/main/java/org/odk/collect/async/network/ConnectivityProvider.kt b/async/src/main/java/org/odk/collect/async/network/ConnectivityProvider.kt new file mode 100644 index 00000000000..526ad50c363 --- /dev/null +++ b/async/src/main/java/org/odk/collect/async/network/ConnectivityProvider.kt @@ -0,0 +1,23 @@ +package org.odk.collect.async.network + +import android.content.Context +import android.net.ConnectivityManager +import org.odk.collect.async.Scheduler + +class ConnectivityProvider(private val context: Context) : NetworkStateProvider { + override val currentNetwork: Scheduler.NetworkType? + get() { + return if (connectivityManager.activeNetworkInfo?.isConnected == true) { + when (connectivityManager.activeNetworkInfo?.type) { + ConnectivityManager.TYPE_WIFI -> Scheduler.NetworkType.WIFI + ConnectivityManager.TYPE_MOBILE -> Scheduler.NetworkType.CELLULAR + else -> Scheduler.NetworkType.OTHER + } + } else { + null + } + } + + private val connectivityManager: ConnectivityManager + get() = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager +} diff --git a/async/src/main/java/org/odk/collect/async/network/NetworkStateProvider.kt b/async/src/main/java/org/odk/collect/async/network/NetworkStateProvider.kt new file mode 100644 index 00000000000..410454a5106 --- /dev/null +++ b/async/src/main/java/org/odk/collect/async/network/NetworkStateProvider.kt @@ -0,0 +1,12 @@ +package org.odk.collect.async.network + +import org.odk.collect.async.Scheduler + +interface NetworkStateProvider { + val currentNetwork: Scheduler.NetworkType? + + val isDeviceOnline: Boolean + get() { + return currentNetwork != null + } +} diff --git a/async/src/test/java/org/odk/collect/async/WorkerAdapterTest.kt b/async/src/test/java/org/odk/collect/async/TaskSpecWorkerTest.kt similarity index 51% rename from async/src/test/java/org/odk/collect/async/WorkerAdapterTest.kt rename to async/src/test/java/org/odk/collect/async/TaskSpecWorkerTest.kt index b5ad9d83c19..df69f12ee0f 100644 --- a/async/src/test/java/org/odk/collect/async/WorkerAdapterTest.kt +++ b/async/src/test/java/org/odk/collect/async/TaskSpecWorkerTest.kt @@ -3,118 +3,157 @@ package org.odk.collect.async import android.content.Context import androidx.test.core.app.ApplicationProvider import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.work.BackoffPolicy +import androidx.work.Data import androidx.work.ListenableWorker import androidx.work.Worker -import androidx.work.WorkerParameters import androidx.work.testing.TestWorkerBuilder import org.hamcrest.CoreMatchers.`is` import org.hamcrest.MatcherAssert.assertThat +import org.hamcrest.Matchers.equalTo import org.junit.Before import org.junit.Test import org.junit.runner.RunWith -import org.mockito.kotlin.any -import org.mockito.kotlin.eq -import org.mockito.kotlin.mock -import org.mockito.kotlin.verify -import org.mockito.kotlin.whenever import java.util.concurrent.Executors import java.util.function.Supplier @RunWith(AndroidJUnit4::class) -class WorkerAdapterTest { +class TaskSpecWorkerTest { private lateinit var worker: Worker - companion object { - private lateinit var spec: TaskSpec - } @Before fun setup() { - spec = mock() - worker = TestWorkerBuilder( + worker = TestWorkerBuilder( context = ApplicationProvider.getApplicationContext(), executor = Executors.newSingleThreadExecutor(), + inputData = Data.Builder() + .putString(TaskSpecWorker.DATA_TASK_SPEC_CLASS, TestTaskSpec::class.java.name) + .build(), runAttemptCount = 0 // without setting this explicitly attempts in tests are counted starting from 1 instead of 0 like in production code ).build() + + TestTaskSpec.reset() } @Test - fun `when task returns true should work succeed`() { - whenever(spec.getTask(any(), any(), any())).thenReturn(Supplier { true }) - + fun `when task returns true work should succeed`() { + TestTaskSpec.doReturn(true) assertThat(worker.doWork(), `is`(ListenableWorker.Result.success())) } @Test fun `when task returns false, retries if maxRetries not specified`() { - whenever(spec.getTask(any(), any(), any())).thenReturn(Supplier { false }) - whenever(spec.maxRetries).thenReturn(null) - + TestTaskSpec.doReturn(false) assertThat(worker.doWork(), `is`(ListenableWorker.Result.retry())) } @Test fun `when task returns false, retries if maxRetries is specified and is higher than runAttemptCount`() { - whenever(spec.getTask(any(), any(), any())).thenReturn(Supplier { false }) - whenever(spec.maxRetries).thenReturn(3) + TestTaskSpec + .withMaxRetries(1) + .doReturn(false) assertThat(worker.doWork(), `is`(ListenableWorker.Result.retry())) } @Test fun `when task returns false, fails if maxRetries is specified and is equal to runAttemptCount`() { - whenever(spec.getTask(any(), any(), any())).thenReturn(Supplier { false }) - whenever(spec.maxRetries).thenReturn(0) + TestTaskSpec + .withMaxRetries(0) + .doReturn(false) assertThat(worker.doWork(), `is`(ListenableWorker.Result.failure())) } @Test fun `when task returns false, fails if maxRetries is specified and is lower than runAttemptCount`() { - whenever(spec.getTask(any(), any(), any())).thenReturn(Supplier { false }) - whenever(spec.maxRetries).thenReturn(0) + TestTaskSpec + .withMaxRetries(-1) + .doReturn(false) assertThat(worker.doWork(), `is`(ListenableWorker.Result.failure())) } @Test fun `when maxRetries is not specified, task called with isLastUniqueExecution true`() { - whenever(spec.maxRetries).thenReturn(null) - whenever(spec.getTask(any(), any(), any())).thenReturn(Supplier { true }) + TestTaskSpec + .doReturn(false) worker.doWork() - - verify(spec).getTask(any(), any(), eq(true)) + assertThat(TestTaskSpec.wasLastUniqueExecution, equalTo(true)) } @Test fun `when maxRetries is specified and it is higher than runAttemptCount, task called with isLastUniqueExecution false`() { - whenever(spec.maxRetries).thenReturn(3) - whenever(spec.getTask(any(), any(), any())).thenReturn(Supplier { true }) + TestTaskSpec + .withMaxRetries(1) + .doReturn(false) worker.doWork() - - verify(spec).getTask(any(), any(), eq(false)) + assertThat(TestTaskSpec.wasLastUniqueExecution, equalTo(false)) } @Test fun `when maxRetries is specified and it is equal to runAttemptCount, task called with isLastUniqueExecution true`() { - whenever(spec.maxRetries).thenReturn(0) - whenever(spec.getTask(any(), any(), any())).thenReturn(Supplier { true }) + TestTaskSpec + .withMaxRetries(0) + .doReturn(false) worker.doWork() - - verify(spec).getTask(any(), any(), eq(true)) + assertThat(TestTaskSpec.wasLastUniqueExecution, equalTo(true)) } @Test fun `when maxRetries is specified and it is lower than runAttemptCount, task called with isLastUniqueExecution true`() { - whenever(spec.maxRetries).thenReturn(0) - whenever(spec.getTask(any(), any(), any())).thenReturn(Supplier { true }) + TestTaskSpec + .withMaxRetries(-1) + .doReturn(false) worker.doWork() + assertThat(TestTaskSpec.wasLastUniqueExecution, equalTo(true)) + } +} - verify(spec).getTask(any(), any(), eq(true)) +private class TestTaskSpec : TaskSpec { + + companion object { + + private var maxRetries: Int? = null + private var returnValue = true + + var wasLastUniqueExecution = false + private set + + fun reset() { + returnValue = true + maxRetries = null + wasLastUniqueExecution = false + } + + fun doReturn(value: Boolean): Companion { + returnValue = value + return this + } + + fun withMaxRetries(maxRetries: Int): Companion { + this.maxRetries = maxRetries + return this + } } - class TestWorker(context: Context, parameters: WorkerParameters) : WorkerAdapter(spec, context, parameters) + override val maxRetries: Int? = Companion.maxRetries + override val backoffPolicy: BackoffPolicy? = null + override val backoffDelay: Long? = null + + override fun getTask( + context: Context, + inputData: Map, + isLastUniqueExecution: Boolean + ): Supplier { + wasLastUniqueExecution = isLastUniqueExecution + + return Supplier { + returnValue + } + } } diff --git a/collect_app/src/androidTest/java/org/odk/collect/android/feature/formmanagement/BulkFinalizationTest.kt b/collect_app/src/androidTest/java/org/odk/collect/android/feature/formmanagement/BulkFinalizationTest.kt index f0bc52421f5..b50d46a28d5 100644 --- a/collect_app/src/androidTest/java/org/odk/collect/android/feature/formmanagement/BulkFinalizationTest.kt +++ b/collect_app/src/androidTest/java/org/odk/collect/android/feature/formmanagement/BulkFinalizationTest.kt @@ -171,7 +171,10 @@ class BulkFinalizationTest { @Test fun whenAutoSendIsEnabled_draftsAreSentAfterFinalizing() { val mainMenuPage = rule.withProject(testDependencies.server.url) - .enableAutoSend(testDependencies.scheduler) + .enableAutoSend( + testDependencies.scheduler, + string.wifi_cellular_autosend + ) .copyForm("one-question.xml", testDependencies.server.hostName) .startBlankForm("One Question") @@ -190,6 +193,26 @@ class BulkFinalizationTest { assertThat(testDependencies.server.submissions.size, equalTo(1)) } + @Test + fun whenDraftFormHasAutoSendEnabled_draftsAreSentAfterFinalizing() { + val mainMenuPage = rule.withProject(testDependencies.server.url) + .copyForm("one-question-autosend.xml", testDependencies.server.hostName) + .startBlankForm("One Question Autosend") + .fillOutAndSave(QuestionAndAnswer("what is your age", "97")) + + .clickDrafts(1) + .clickFinalizeAll(1) + .clickFinalize() + .pressBack(MainMenuPage()) + + testDependencies.scheduler.runDeferredTasks() + + mainMenuPage.clickViewSentForm(1) + .assertText("One Question Autosend") + + assertThat(testDependencies.server.submissions.size, equalTo(1)) + } + @Test fun canCancel() { rule.withProject("http://example.com") diff --git a/collect_app/src/androidTest/java/org/odk/collect/android/feature/instancemanagement/AutoSendTest.kt b/collect_app/src/androidTest/java/org/odk/collect/android/feature/instancemanagement/AutoSendTest.kt index 7180ed94b92..bc4e5c1e37d 100644 --- a/collect_app/src/androidTest/java/org/odk/collect/android/feature/instancemanagement/AutoSendTest.kt +++ b/collect_app/src/androidTest/java/org/odk/collect/android/feature/instancemanagement/AutoSendTest.kt @@ -13,6 +13,8 @@ import org.odk.collect.android.support.pages.ViewSentFormPage import org.odk.collect.android.support.rules.CollectTestRule import org.odk.collect.android.support.rules.NotificationDrawerRule import org.odk.collect.android.support.rules.TestRuleChain +import org.odk.collect.async.Scheduler +import org.odk.collect.strings.R @RunWith(AndroidJUnit4::class) class AutoSendTest { @@ -29,7 +31,10 @@ class AutoSendTest { fun whenAutoSendEnabled_fillingAndFinalizingForm_sendsFormAndNotifiesUser() { val mainMenuPage = rule.startAtMainMenu() .setServer(testDependencies.server.url) - .enableAutoSend(testDependencies.scheduler) + .enableAutoSend( + testDependencies.scheduler, + R.string.wifi_cellular_autosend + ) .copyForm("one-question.xml") .startBlankForm("One Question") .inputText("31") @@ -58,7 +63,10 @@ class AutoSendTest { val mainMenuPage = rule.startAtMainMenu() .setServer(testDependencies.server.url) - .enableAutoSend(testDependencies.scheduler) + .enableAutoSend( + testDependencies.scheduler, + R.string.wifi_cellular_autosend + ) .copyForm("one-question.xml") .startBlankForm("One Question") .inputText("31") @@ -82,15 +90,20 @@ class AutoSendTest { } @Test - fun whenFormHasAutoSend_fillingAndFinalizingForm_sendsFormAndNotifiesUser() { + fun whenFormHasAutoSend_fillingAndFinalizingForm_sendsFormAndNotifiesUser_regardlessOfSetting() { val mainMenuPage = rule.startAtMainMenu() .setServer(testDependencies.server.url) + .enableAutoSend( + testDependencies.scheduler, + R.string.wifi_autosend + ) .copyForm("one-question-autosend.xml") .startBlankForm("One Question Autosend") .inputText("31") .swipeToEndScreen() .clickSend() + testDependencies.networkStateProvider.goOnline(Scheduler.NetworkType.CELLULAR) testDependencies.scheduler.runDeferredTasks() mainMenuPage @@ -108,17 +121,44 @@ class AutoSendTest { } @Test - fun whenFormHasAutoSend_fillingAndFinalizingForm_notifiesUserWhenSendingFails() { + fun whenFormHasAutoSend_canAutoSendMultipleForms() { + val mainMenuPage = rule.startAtMainMenu() + .setServer(testDependencies.server.url) + .copyForm("one-question-autosend.xml") + + .startBlankForm("One Question Autosend") + .inputText("31") + .swipeToEndScreen() + .clickSend() + + .startBlankForm("One Question Autosend") + .inputText("32") + .swipeToEndScreen() + .clickSend() + + testDependencies.scheduler.runDeferredTasks() + + mainMenuPage + .clickViewSentForm(2) + } + + @Test + fun whenFormHasAutoSend_fillingAndFinalizingForm_notifiesUserWhenSendingFails_regardlessOfSetting() { testDependencies.server.alwaysReturnError() val mainMenuPage = rule.startAtMainMenu() .setServer(testDependencies.server.url) + .enableAutoSend( + testDependencies.scheduler, + R.string.wifi_autosend + ) .copyForm("one-question-autosend.xml") .startBlankForm("One Question Autosend") .inputText("31") .swipeToEndScreen() .clickSend() + testDependencies.networkStateProvider.goOnline(Scheduler.NetworkType.CELLULAR) testDependencies.scheduler.runDeferredTasks() mainMenuPage.clickViewSentForm(1) @@ -135,6 +175,24 @@ class AutoSendTest { ) } + @Test + fun whenFormHasAutoSendDisabled_fillingAndFinalizingForm_doesNotSendForm_regardlessOfSetting() { + val mainMenuPage = rule.startAtMainMenu() + .setServer(testDependencies.server.url) + .enableAutoSend( + testDependencies.scheduler, + R.string.wifi_cellular_autosend + ) + .copyForm("one-question-autosend-disabled.xml") + .startBlankForm("One Question Autosend Disabled") + .inputText("31") + .swipeToEndScreen() + .clickFinalize() + + testDependencies.scheduler.runDeferredTasks() + mainMenuPage.assertNumberOfFinalizedForms(1) + } + @Test fun whenAutoSendDisabled_fillingAndFinalizingForm_doesNotSendFormAutomatically() { val mainMenuPage = rule.startAtMainMenu() diff --git a/collect_app/src/androidTest/java/org/odk/collect/android/feature/settings/ConfigureWithQRCodeTest.java b/collect_app/src/androidTest/java/org/odk/collect/android/feature/settings/ConfigureWithQRCodeTest.java index 0aa8b6231d5..7c52401dc15 100644 --- a/collect_app/src/androidTest/java/org/odk/collect/android/feature/settings/ConfigureWithQRCodeTest.java +++ b/collect_app/src/androidTest/java/org/odk/collect/android/feature/settings/ConfigureWithQRCodeTest.java @@ -1,10 +1,11 @@ package org.odk.collect.android.feature.settings; +import static androidx.test.core.app.ApplicationProvider.getApplicationContext; + import android.graphics.Bitmap; import android.graphics.BitmapFactory; import androidx.test.ext.junit.runners.AndroidJUnit4; -import androidx.work.WorkManager; import org.junit.After; import org.junit.Rule; @@ -15,34 +16,31 @@ import org.odk.collect.android.configure.qr.AppConfigurationGenerator; import org.odk.collect.android.configure.qr.QRCodeGenerator; import org.odk.collect.android.injection.config.AppDependencyModule; -import org.odk.collect.android.support.rules.CollectTestRule; -import org.odk.collect.android.support.rules.ResetStateRule; -import org.odk.collect.android.support.rules.RunnableRule; import org.odk.collect.android.support.StubBarcodeViewDecoder; -import org.odk.collect.android.support.TestScheduler; -import org.odk.collect.android.support.pages.ProjectSettingsPage; +import org.odk.collect.android.support.TestDependencies; import org.odk.collect.android.support.pages.MainMenuPage; +import org.odk.collect.android.support.pages.ProjectSettingsPage; import org.odk.collect.android.support.pages.QRCodePage; +import org.odk.collect.android.support.rules.CollectTestRule; +import org.odk.collect.android.support.rules.ResetStateRule; +import org.odk.collect.android.support.rules.RunnableRule; import org.odk.collect.android.support.rules.TestRuleChain; import org.odk.collect.android.views.BarcodeViewDecoder; -import org.odk.collect.async.Scheduler; import java.io.File; import java.io.FileOutputStream; import java.util.Collection; -import static androidx.test.core.app.ApplicationProvider.getApplicationContext; - @RunWith(AndroidJUnit4.class) public class ConfigureWithQRCodeTest { + private final TestDependencies testDependencies = new TestDependencies(); private final CollectTestRule rule = new CollectTestRule(); private final StubQRCodeGenerator stubQRCodeGenerator = new StubQRCodeGenerator(); private final StubBarcodeViewDecoder stubBarcodeViewDecoder = new StubBarcodeViewDecoder(); - private final TestScheduler testScheduler = new TestScheduler(); @Rule - public RuleChain copyFormChain = TestRuleChain.chain() + public RuleChain copyFormChain = TestRuleChain.chain(testDependencies) .around(new ResetStateRule(new AppDependencyModule() { @Override @@ -54,11 +52,6 @@ public BarcodeViewDecoder providesBarcodeViewDecoder() { public QRCodeGenerator providesQRCodeGenerator() { return stubQRCodeGenerator; } - - @Override - public Scheduler providesScheduler(WorkManager workManager) { - return testScheduler; - } })) .around(new RunnableRule(stubQRCodeGenerator::setup)) .around(rule); diff --git a/collect_app/src/androidTest/java/org/odk/collect/android/support/FakeNetworkStateProvider.kt b/collect_app/src/androidTest/java/org/odk/collect/android/support/FakeNetworkStateProvider.kt new file mode 100644 index 00000000000..395e59f4db8 --- /dev/null +++ b/collect_app/src/androidTest/java/org/odk/collect/android/support/FakeNetworkStateProvider.kt @@ -0,0 +1,20 @@ +package org.odk.collect.android.support + +import org.odk.collect.async.Scheduler +import org.odk.collect.async.network.NetworkStateProvider + +class FakeNetworkStateProvider : NetworkStateProvider { + + private var type: Scheduler.NetworkType? = Scheduler.NetworkType.WIFI + + fun goOnline(networkType: Scheduler.NetworkType) { + type = networkType + } + + fun goOffline() { + type = null + } + + override val currentNetwork: Scheduler.NetworkType? + get() = type +} diff --git a/collect_app/src/androidTest/java/org/odk/collect/android/support/TestDependencies.java b/collect_app/src/androidTest/java/org/odk/collect/android/support/TestDependencies.java index 0b3b8b3427f..440b5144c74 100644 --- a/collect_app/src/androidTest/java/org/odk/collect/android/support/TestDependencies.java +++ b/collect_app/src/androidTest/java/org/odk/collect/android/support/TestDependencies.java @@ -1,6 +1,7 @@ package org.odk.collect.android.support; import android.app.Application; +import android.content.Context; import android.webkit.MimeTypeMap; import androidx.work.WorkManager; @@ -11,12 +12,14 @@ import org.odk.collect.android.version.VersionInformation; import org.odk.collect.android.views.BarcodeViewDecoder; import org.odk.collect.async.Scheduler; +import org.odk.collect.async.network.NetworkStateProvider; import org.odk.collect.utilities.UserAgentProvider; public class TestDependencies extends AppDependencyModule { public final StubOpenRosaServer server = new StubOpenRosaServer(); - public final TestScheduler scheduler = new TestScheduler(); + public final FakeNetworkStateProvider networkStateProvider = new FakeNetworkStateProvider(); + public final TestScheduler scheduler = new TestScheduler(networkStateProvider); public final StoragePathProvider storagePathProvider = new StoragePathProvider(); public final StubBarcodeViewDecoder stubBarcodeViewDecoder = new StubBarcodeViewDecoder(); @@ -34,4 +37,9 @@ public Scheduler providesScheduler(WorkManager workManager) { public BarcodeViewDecoder providesBarcodeViewDecoder() { return stubBarcodeViewDecoder; } + + @Override + public NetworkStateProvider providesNetworkStateProvider(Context context) { + return networkStateProvider; + } } diff --git a/collect_app/src/androidTest/java/org/odk/collect/android/support/TestScheduler.kt b/collect_app/src/androidTest/java/org/odk/collect/android/support/TestScheduler.kt index 6222acad5b7..6a3c2092c72 100644 --- a/collect_app/src/androidTest/java/org/odk/collect/android/support/TestScheduler.kt +++ b/collect_app/src/androidTest/java/org/odk/collect/android/support/TestScheduler.kt @@ -11,11 +11,12 @@ import org.odk.collect.async.Cancellable import org.odk.collect.async.CoroutineAndWorkManagerScheduler import org.odk.collect.async.Scheduler import org.odk.collect.async.TaskSpec +import org.odk.collect.async.network.NetworkStateProvider import java.util.function.Consumer import java.util.function.Supplier import kotlin.coroutines.CoroutineContext -class TestScheduler : Scheduler, CoroutineDispatcher() { +class TestScheduler(private val networkStateProvider: NetworkStateProvider) : Scheduler, CoroutineDispatcher() { private val wrappedScheduler: Scheduler private val lock = Any() @@ -55,7 +56,8 @@ class TestScheduler : Scheduler, CoroutineDispatcher() { inputData: Map, networkConstraint: Scheduler.NetworkType? ) { - deferredTasks.add(DeferredTask(tag, spec, null, inputData)) + cancelDeferred(tag) + deferredTasks.add(DeferredTask(tag, spec, null, inputData, networkConstraint)) } override fun networkDeferredRepeat( @@ -65,7 +67,7 @@ class TestScheduler : Scheduler, CoroutineDispatcher() { inputData: Map ) { cancelDeferred(tag) - deferredTasks.add(DeferredTask(tag, spec, repeatPeriod, inputData)) + deferredTasks.add(DeferredTask(tag, spec, repeatPeriod, inputData, null)) } override fun cancelDeferred(tag: String) { @@ -77,13 +79,18 @@ class TestScheduler : Scheduler, CoroutineDispatcher() { } fun runDeferredTasks() { - val applicationContext = ApplicationProvider.getApplicationContext() - for (deferredTask in deferredTasks) { - deferredTask.spec.getTask(applicationContext, deferredTask.inputData, true).get() + if (networkStateProvider.isDeviceOnline) { + val applicationContext = ApplicationProvider.getApplicationContext() + deferredTasks.removeIf { deferredTask -> + if (deferredTask.networkConstraint == null || deferredTask.networkConstraint == networkStateProvider.currentNetwork) { + deferredTask.spec.getTask(applicationContext, deferredTask.inputData, true) + .get() + deferredTask.repeatPeriod == null + } else { + false + } + } } - - // Remove non repeating tasks - deferredTasks.removeIf { deferredTask: DeferredTask -> deferredTask.repeatPeriod == null } } fun setFinishedCallback(callback: Runnable?) { @@ -130,6 +137,7 @@ class TestScheduler : Scheduler, CoroutineDispatcher() { val tag: String, val spec: TaskSpec, val repeatPeriod: Long?, - val inputData: Map + val inputData: Map, + val networkConstraint: Scheduler.NetworkType? ) } diff --git a/collect_app/src/androidTest/java/org/odk/collect/android/support/pages/MainMenuPage.java b/collect_app/src/androidTest/java/org/odk/collect/android/support/pages/MainMenuPage.java index 5741b6b8d62..aa5c7b9b893 100644 --- a/collect_app/src/androidTest/java/org/odk/collect/android/support/pages/MainMenuPage.java +++ b/collect_app/src/androidTest/java/org/odk/collect/android/support/pages/MainMenuPage.java @@ -191,12 +191,12 @@ public MainMenuPage enableMatchExactly() { .pressBack(new MainMenuPage()); } - public MainMenuPage enableAutoSend(TestScheduler scheduler) { + public MainMenuPage enableAutoSend(TestScheduler scheduler, int setting) { MainMenuPage mainMenuPage = openProjectSettingsDialog() .clickSettings() .clickFormManagement() .clickOnString(org.odk.collect.strings.R.string.autosend) - .clickOnString(org.odk.collect.strings.R.string.wifi_cellular_autosend) + .clickOnString(setting) .pressBack(new ProjectSettingsPage()) .pressBack(new MainMenuPage()); diff --git a/collect_app/src/main/java/org/odk/collect/android/activities/FormDownloadListActivity.java b/collect_app/src/main/java/org/odk/collect/android/activities/FormDownloadListActivity.java index 93d26061120..1693ff3a603 100644 --- a/collect_app/src/main/java/org/odk/collect/android/activities/FormDownloadListActivity.java +++ b/collect_app/src/main/java/org/odk/collect/android/activities/FormDownloadListActivity.java @@ -57,7 +57,7 @@ import org.odk.collect.android.utilities.DialogUtils; import org.odk.collect.android.utilities.WebCredentialsUtils; import org.odk.collect.android.views.DayNightProgressDialog; -import org.odk.collect.androidshared.network.NetworkStateProvider; +import org.odk.collect.async.network.NetworkStateProvider; import org.odk.collect.androidshared.ui.DialogFragmentUtils; import org.odk.collect.androidshared.ui.ToastUtils; import org.odk.collect.forms.FormSourceException; diff --git a/collect_app/src/main/java/org/odk/collect/android/activities/FormEntryViewModelFactory.kt b/collect_app/src/main/java/org/odk/collect/android/activities/FormEntryViewModelFactory.kt index 679f8a51723..fd9c6007091 100644 --- a/collect_app/src/main/java/org/odk/collect/android/activities/FormEntryViewModelFactory.kt +++ b/collect_app/src/main/java/org/odk/collect/android/activities/FormEntryViewModelFactory.kt @@ -19,6 +19,7 @@ import org.odk.collect.android.formentry.backgroundlocation.BackgroundLocationMa import org.odk.collect.android.formentry.backgroundlocation.BackgroundLocationViewModel import org.odk.collect.android.formentry.saving.DiskFormSaver import org.odk.collect.android.formentry.saving.FormSaveViewModel +import org.odk.collect.android.instancemanagement.InstancesDataService import org.odk.collect.android.instancemanagement.autosend.AutoSendSettingsProvider import org.odk.collect.android.projects.ProjectsDataService import org.odk.collect.android.utilities.ApplicationConstants @@ -55,7 +56,8 @@ class FormEntryViewModelFactory( private val instancesRepositoryProvider: InstancesRepositoryProvider, private val savepointsRepositoryProvider: SavepointsRepositoryProvider, private val qrCodeCreator: QRCodeCreator, - private val htmlPrinter: HtmlPrinter + private val htmlPrinter: HtmlPrinter, + private val instancesDataService: InstancesDataService ) : AbstractSavedStateViewModelFactory(owner, null) { override fun create( @@ -86,7 +88,8 @@ class FormEntryViewModelFactory( formSessionRepository.get(sessionId), entitiesRepositoryProvider.get(projectId), instancesRepositoryProvider.get(projectId), - savepointsRepositoryProvider.get(projectId) + savepointsRepositoryProvider.get(projectId), + instancesDataService ) } diff --git a/collect_app/src/main/java/org/odk/collect/android/activities/FormFillingActivity.java b/collect_app/src/main/java/org/odk/collect/android/activities/FormFillingActivity.java index 93f0c63cc96..b06279e6fbc 100644 --- a/collect_app/src/main/java/org/odk/collect/android/activities/FormFillingActivity.java +++ b/collect_app/src/main/java/org/odk/collect/android/activities/FormFillingActivity.java @@ -88,7 +88,6 @@ import org.odk.collect.android.audio.AudioControllerView; import org.odk.collect.android.audio.AudioRecordingControllerFragment; import org.odk.collect.android.audio.M4AAppender; -import org.odk.collect.android.backgroundwork.InstanceSubmitScheduler; import org.odk.collect.android.dao.helpers.InstancesDaoHelper; import org.odk.collect.android.entities.EntitiesRepositoryProvider; import org.odk.collect.android.exception.JavaRosaException; @@ -135,6 +134,7 @@ import org.odk.collect.android.fragments.dialogs.NumberPickerDialog; import org.odk.collect.android.fragments.dialogs.RankingWidgetDialog; import org.odk.collect.android.fragments.dialogs.SelectMinimalDialog; +import org.odk.collect.android.instancemanagement.InstancesDataService; import org.odk.collect.android.instancemanagement.autosend.AutoSendSettingsProvider; import org.odk.collect.android.javarosawrapper.FailedValidationResult; import org.odk.collect.android.javarosawrapper.FormController; @@ -309,7 +309,7 @@ public void allowSwiping(boolean doSwipe) { PropertyManager propertyManager; @Inject - InstanceSubmitScheduler instanceSubmitScheduler; + InstancesDataService instancesDataService; @Inject Scheduler scheduler; @@ -433,7 +433,8 @@ public void onCreate(Bundle savedInstanceState) { instancesRepositoryProvider, new SavepointsRepositoryProvider(this, storagePathProvider), new QRCodeCreatorImpl(), - new HtmlPrinter() + new HtmlPrinter(), + instancesDataService ); this.getSupportFragmentManager().setFragmentFactory(new FragmentFactoryBuilder() @@ -1561,10 +1562,6 @@ private void handleSaveResult(FormSaveViewModel.SaveResult result) { DialogFragmentUtils.dismissDialog(ChangesReasonPromptDialogFragment.class, getSupportFragmentManager()); if (result.getRequest().viewExiting()) { - if (result.getRequest().shouldFinalize()) { - instanceSubmitScheduler.scheduleSubmit(projectsDataService.getCurrentProject().getUuid()); - } - finishAndReturnInstance(); } else { showShortToast(this, org.odk.collect.strings.R.string.data_saved_ok); diff --git a/collect_app/src/main/java/org/odk/collect/android/application/Collect.java b/collect_app/src/main/java/org/odk/collect/android/application/Collect.java index 602451ffeb0..b4ca27264d7 100644 --- a/collect_app/src/main/java/org/odk/collect/android/application/Collect.java +++ b/collect_app/src/main/java/org/odk/collect/android/application/Collect.java @@ -41,7 +41,7 @@ import org.odk.collect.android.utilities.LocaleHelper; import org.odk.collect.androidshared.data.AppState; import org.odk.collect.androidshared.data.StateStore; -import org.odk.collect.androidshared.network.NetworkStateProvider; +import org.odk.collect.async.network.NetworkStateProvider; import org.odk.collect.androidshared.system.ExternalFilesUtils; import org.odk.collect.async.Scheduler; import org.odk.collect.audiorecorder.AudioRecorderDependencyComponent; diff --git a/collect_app/src/main/java/org/odk/collect/android/application/initialization/FormUpdatesUpgrade.kt b/collect_app/src/main/java/org/odk/collect/android/application/initialization/ScheduledWorkUpgrade.kt similarity index 57% rename from collect_app/src/main/java/org/odk/collect/android/application/initialization/FormUpdatesUpgrade.kt rename to collect_app/src/main/java/org/odk/collect/android/application/initialization/ScheduledWorkUpgrade.kt index f3067e6e869..bb2b89d513d 100644 --- a/collect_app/src/main/java/org/odk/collect/android/application/initialization/FormUpdatesUpgrade.kt +++ b/collect_app/src/main/java/org/odk/collect/android/application/initialization/ScheduledWorkUpgrade.kt @@ -1,14 +1,19 @@ package org.odk.collect.android.application.initialization import org.odk.collect.android.backgroundwork.FormUpdateScheduler +import org.odk.collect.android.backgroundwork.InstanceSubmitScheduler import org.odk.collect.async.Scheduler import org.odk.collect.projects.ProjectsRepository import org.odk.collect.upgrade.Upgrade -class FormUpdatesUpgrade( +/** + * Reschedule all background work to prevent problems with tag or class name changes etc + */ +class ScheduledWorkUpgrade( private val scheduler: Scheduler, private val projectsRepository: ProjectsRepository, - private val formUpdateScheduler: FormUpdateScheduler + private val formUpdateScheduler: FormUpdateScheduler, + private val instanceSubmitScheduler: InstanceSubmitScheduler ) : Upgrade { override fun key(): String? { @@ -20,6 +25,8 @@ class FormUpdatesUpgrade( projectsRepository.getAll().forEach { formUpdateScheduler.scheduleUpdates(it.uuid) + instanceSubmitScheduler.scheduleAutoSend(it.uuid) + instanceSubmitScheduler.scheduleFormAutoSend(it.uuid) } } } diff --git a/collect_app/src/main/java/org/odk/collect/android/application/initialization/upgrade/UpgradeInitializer.kt b/collect_app/src/main/java/org/odk/collect/android/application/initialization/upgrade/UpgradeInitializer.kt index c5769313eb4..d7e24270755 100644 --- a/collect_app/src/main/java/org/odk/collect/android/application/initialization/upgrade/UpgradeInitializer.kt +++ b/collect_app/src/main/java/org/odk/collect/android/application/initialization/upgrade/UpgradeInitializer.kt @@ -4,9 +4,9 @@ import android.content.Context import org.odk.collect.android.BuildConfig import org.odk.collect.android.application.initialization.ExistingProjectMigrator import org.odk.collect.android.application.initialization.ExistingSettingsMigrator -import org.odk.collect.android.application.initialization.FormUpdatesUpgrade import org.odk.collect.android.application.initialization.GoogleDriveProjectsDeleter import org.odk.collect.android.application.initialization.SavepointsImporter +import org.odk.collect.android.application.initialization.ScheduledWorkUpgrade import org.odk.collect.settings.SettingsProvider import org.odk.collect.settings.keys.MetaKeys import org.odk.collect.upgrade.AppUpgrader @@ -16,7 +16,7 @@ class UpgradeInitializer( private val settingsProvider: SettingsProvider, private val existingProjectMigrator: ExistingProjectMigrator, private val existingSettingsMigrator: ExistingSettingsMigrator, - private val formUpdatesUpgrade: FormUpdatesUpgrade, + private val scheduledWorkUpgrade: ScheduledWorkUpgrade, private val googleDriveProjectsDeleter: GoogleDriveProjectsDeleter, private val savepointsImporter: SavepointsImporter ) { @@ -30,7 +30,7 @@ class UpgradeInitializer( listOf( existingProjectMigrator, existingSettingsMigrator, - formUpdatesUpgrade, + scheduledWorkUpgrade, googleDriveProjectsDeleter, savepointsImporter ) diff --git a/collect_app/src/main/java/org/odk/collect/android/backgroundwork/AutoUpdateTaskSpec.kt b/collect_app/src/main/java/org/odk/collect/android/backgroundwork/AutoUpdateTaskSpec.kt index 4588d142eb7..067baeedc4c 100644 --- a/collect_app/src/main/java/org/odk/collect/android/backgroundwork/AutoUpdateTaskSpec.kt +++ b/collect_app/src/main/java/org/odk/collect/android/backgroundwork/AutoUpdateTaskSpec.kt @@ -17,11 +17,9 @@ package org.odk.collect.android.backgroundwork import android.content.Context import androidx.work.BackoffPolicy -import androidx.work.WorkerParameters import org.odk.collect.android.formmanagement.FormsDataService import org.odk.collect.android.injection.DaggerUtils import org.odk.collect.async.TaskSpec -import org.odk.collect.async.WorkerAdapter import java.util.function.Supplier import javax.inject.Inject @@ -45,11 +43,4 @@ class AutoUpdateTaskSpec : TaskSpec { } } } - - override fun getWorkManagerAdapter(): Class { - return Adapter::class.java - } - - class Adapter(context: Context, workerParams: WorkerParameters) : - WorkerAdapter(AutoUpdateTaskSpec(), context, workerParams) } diff --git a/collect_app/src/main/java/org/odk/collect/android/backgroundwork/FormUpdateAndInstanceSubmitScheduler.java b/collect_app/src/main/java/org/odk/collect/android/backgroundwork/FormUpdateAndInstanceSubmitScheduler.java index 4b7229b095d..1ae95cf086c 100644 --- a/collect_app/src/main/java/org/odk/collect/android/backgroundwork/FormUpdateAndInstanceSubmitScheduler.java +++ b/collect_app/src/main/java/org/odk/collect/android/backgroundwork/FormUpdateAndInstanceSubmitScheduler.java @@ -68,19 +68,31 @@ public void cancelUpdates(String projectId) { } @Override - public void scheduleSubmit(String projectId) { - Scheduler.NetworkType networkType = null; + public void scheduleAutoSend(String projectId) { + Scheduler.NetworkType networkConstraint; Settings settings = settingsProvider.getUnprotectedSettings(projectId); AutoSend autoSendSetting = StringIdEnumUtils.getAutoSend(settings, application); if (autoSendSetting == AutoSend.WIFI_ONLY) { - networkType = Scheduler.NetworkType.WIFI; + networkConstraint = Scheduler.NetworkType.WIFI; } else if (autoSendSetting == AutoSend.CELLULAR_ONLY) { - networkType = Scheduler.NetworkType.CELLULAR; + networkConstraint = Scheduler.NetworkType.CELLULAR; + } else if (autoSendSetting == AutoSend.WIFI_AND_CELLULAR) { + networkConstraint = null; + } else { + return; } HashMap inputData = new HashMap<>(); inputData.put(TaskData.DATA_PROJECT_ID, projectId); - scheduler.networkDeferred(getAutoSendTag(projectId), new AutoSendTaskSpec(), inputData, networkType); + scheduler.networkDeferred(getAutoSendTag(projectId), new SendFormsTaskSpec(), inputData, networkConstraint); + } + + @Override + public void scheduleFormAutoSend(String projectId) { + HashMap inputData = new HashMap<>(); + inputData.put(TaskData.DATA_PROJECT_ID, projectId); + inputData.put(TaskData.DATA_FORM_AUTO_SEND, ""); + scheduler.networkDeferred(getAutoSendFormTag(projectId), new SendFormsTaskSpec(), inputData, null); } @Override @@ -93,6 +105,10 @@ public String getAutoSendTag(String projectId) { return "AutoSendWorker:" + projectId; } + public String getAutoSendFormTag(String projectId) { + return "auto_send_form:" + projectId; + } + @NotNull private String getMatchExactlyTag(String projectId) { return "match_exactly:" + projectId; diff --git a/collect_app/src/main/java/org/odk/collect/android/backgroundwork/InstanceSubmitScheduler.java b/collect_app/src/main/java/org/odk/collect/android/backgroundwork/InstanceSubmitScheduler.java index a2a16a6dc12..651750bbe99 100644 --- a/collect_app/src/main/java/org/odk/collect/android/backgroundwork/InstanceSubmitScheduler.java +++ b/collect_app/src/main/java/org/odk/collect/android/backgroundwork/InstanceSubmitScheduler.java @@ -2,7 +2,9 @@ public interface InstanceSubmitScheduler { - void scheduleSubmit(String projectId); + void scheduleAutoSend(String projectId); + + void scheduleFormAutoSend(String projectId); void cancelSubmit(String projectId); } diff --git a/collect_app/src/main/java/org/odk/collect/android/backgroundwork/AutoSendTaskSpec.kt b/collect_app/src/main/java/org/odk/collect/android/backgroundwork/SendFormsTaskSpec.kt similarity index 76% rename from collect_app/src/main/java/org/odk/collect/android/backgroundwork/AutoSendTaskSpec.kt rename to collect_app/src/main/java/org/odk/collect/android/backgroundwork/SendFormsTaskSpec.kt index 6fbe7700c52..6cedcb5aeb9 100644 --- a/collect_app/src/main/java/org/odk/collect/android/backgroundwork/AutoSendTaskSpec.kt +++ b/collect_app/src/main/java/org/odk/collect/android/backgroundwork/SendFormsTaskSpec.kt @@ -15,19 +15,17 @@ package org.odk.collect.android.backgroundwork import android.content.Context import androidx.work.BackoffPolicy -import androidx.work.WorkerParameters import org.odk.collect.android.injection.DaggerUtils import org.odk.collect.android.instancemanagement.InstancesDataService import org.odk.collect.async.TaskSpec -import org.odk.collect.async.WorkerAdapter import java.util.function.Supplier import javax.inject.Inject -class AutoSendTaskSpec : TaskSpec { +class SendFormsTaskSpec : TaskSpec { @Inject lateinit var instancesDataService: InstancesDataService - override val maxRetries: Int? = null + override val maxRetries: Int = 13 // Stop trying when backoff is > 5 days override val backoffPolicy = BackoffPolicy.EXPONENTIAL override val backoffDelay: Long = 60_000 @@ -35,18 +33,16 @@ class AutoSendTaskSpec : TaskSpec { DaggerUtils.getComponent(context).inject(this) return Supplier { val projectId = inputData[TaskData.DATA_PROJECT_ID] + val formAutoSend = inputData[TaskData.DATA_FORM_AUTO_SEND] != null if (projectId != null) { - instancesDataService.autoSendInstances(projectId) + if (formAutoSend) { + instancesDataService.sendInstances(projectId, formAutoSend = true) + } else { + instancesDataService.sendInstances(projectId) + } } else { throw IllegalArgumentException("No project ID provided!") } } } - - override fun getWorkManagerAdapter(): Class { - return Adapter::class.java - } - - class Adapter(context: Context, workerParams: WorkerParameters) : - WorkerAdapter(AutoSendTaskSpec(), context, workerParams) } diff --git a/collect_app/src/main/java/org/odk/collect/android/backgroundwork/SyncFormsTaskSpec.kt b/collect_app/src/main/java/org/odk/collect/android/backgroundwork/SyncFormsTaskSpec.kt index 8e39bb62d0b..36d286ac0eb 100644 --- a/collect_app/src/main/java/org/odk/collect/android/backgroundwork/SyncFormsTaskSpec.kt +++ b/collect_app/src/main/java/org/odk/collect/android/backgroundwork/SyncFormsTaskSpec.kt @@ -2,11 +2,9 @@ package org.odk.collect.android.backgroundwork import android.content.Context import androidx.work.BackoffPolicy -import androidx.work.WorkerParameters import org.odk.collect.android.formmanagement.FormsDataService import org.odk.collect.android.injection.DaggerUtils import org.odk.collect.async.TaskSpec -import org.odk.collect.async.WorkerAdapter import java.util.function.Supplier import javax.inject.Inject @@ -29,11 +27,4 @@ class SyncFormsTaskSpec : TaskSpec { } } } - - override fun getWorkManagerAdapter(): Class { - return Adapter::class.java - } - - class Adapter(context: Context, workerParams: WorkerParameters) : - WorkerAdapter(SyncFormsTaskSpec(), context, workerParams) } diff --git a/collect_app/src/main/java/org/odk/collect/android/backgroundwork/TaskData.kt b/collect_app/src/main/java/org/odk/collect/android/backgroundwork/TaskData.kt index cc95ff0a663..3d71dac0d42 100644 --- a/collect_app/src/main/java/org/odk/collect/android/backgroundwork/TaskData.kt +++ b/collect_app/src/main/java/org/odk/collect/android/backgroundwork/TaskData.kt @@ -2,4 +2,5 @@ package org.odk.collect.android.backgroundwork object TaskData { const val DATA_PROJECT_ID = "projectId" + const val DATA_FORM_AUTO_SEND = "formAutoSend" } diff --git a/collect_app/src/main/java/org/odk/collect/android/formentry/saving/FormSaveViewModel.java b/collect_app/src/main/java/org/odk/collect/android/formentry/saving/FormSaveViewModel.java index 4b531f21aee..f31e6be7281 100644 --- a/collect_app/src/main/java/org/odk/collect/android/formentry/saving/FormSaveViewModel.java +++ b/collect_app/src/main/java/org/odk/collect/android/formentry/saving/FormSaveViewModel.java @@ -22,6 +22,7 @@ import org.odk.collect.android.formentry.FormSession; import org.odk.collect.android.formentry.audit.AuditEvent; import org.odk.collect.android.formentry.audit.AuditUtils; +import org.odk.collect.android.instancemanagement.InstancesDataService; import org.odk.collect.android.javarosawrapper.FormController; import org.odk.collect.android.projects.ProjectsDataService; import org.odk.collect.android.tasks.SaveFormToDisk; @@ -90,12 +91,13 @@ public class FormSaveViewModel extends ViewModel implements MaterialProgressDial private Form form; private Instance instance; private final Cancellable formSessionObserver; + private InstancesDataService instancesDataService; public FormSaveViewModel(SavedStateHandle stateHandle, Supplier clock, FormSaver formSaver, MediaUtils mediaUtils, Scheduler scheduler, AudioRecorder audioRecorder, ProjectsDataService projectsDataService, LiveData formSession, EntitiesRepository entitiesRepository, InstancesRepository instancesRepository, - SavepointsRepository savepointsRepository + SavepointsRepository savepointsRepository, InstancesDataService instancesDataService ) { this.stateHandle = stateHandle; this.clock = clock; @@ -107,6 +109,7 @@ public FormSaveViewModel(SavedStateHandle stateHandle, Supplier clock, For this.entitiesRepository = entitiesRepository; this.instancesRepository = instancesRepository; this.savepointsRepository = savepointsRepository; + this.instancesDataService = instancesDataService; if (stateHandle.get(ORIGINAL_FILES) != null) { originalFiles = stateHandle.get(ORIGINAL_FILES); @@ -268,6 +271,8 @@ private void handleTaskResult(SaveToDiskResult taskResult, SaveRequest saveReque if (saveRequest.shouldFinalize) { formController.getAuditEventLogger().logEvent(AuditEvent.AuditEventType.FORM_EXIT, false, clock.get()); formController.getAuditEventLogger().logEvent(AuditEvent.AuditEventType.FORM_FINALIZE, true, clock.get()); + + instancesDataService.instanceFinalized(projectsDataService.getCurrentProject().getUuid(), form); } else { formController.getAuditEventLogger().logEvent(AuditEvent.AuditEventType.FORM_EXIT, true, clock.get()); } diff --git a/collect_app/src/main/java/org/odk/collect/android/formhierarchy/FormHierarchyActivity.java b/collect_app/src/main/java/org/odk/collect/android/formhierarchy/FormHierarchyActivity.java index 9805ccaf458..77d4f5ff444 100644 --- a/collect_app/src/main/java/org/odk/collect/android/formhierarchy/FormHierarchyActivity.java +++ b/collect_app/src/main/java/org/odk/collect/android/formhierarchy/FormHierarchyActivity.java @@ -54,6 +54,7 @@ import org.odk.collect.android.formentry.ODKView; import org.odk.collect.android.formentry.repeats.DeleteRepeatDialogFragment; import org.odk.collect.android.injection.DaggerUtils; +import org.odk.collect.android.instancemanagement.InstancesDataService; import org.odk.collect.android.instancemanagement.autosend.AutoSendSettingsProvider; import org.odk.collect.android.javarosawrapper.FormController; import org.odk.collect.android.javarosawrapper.JavaRosaFormController; @@ -195,6 +196,9 @@ public class FormHierarchyActivity extends LocalizedActivity implements DeleteRe @Inject public StoragePathProvider storagePathProvider; + @Inject + public InstancesDataService instancesDataService; + protected final OnBackPressedCallback onBackPressedCallback = new OnBackPressedCallback(true) { @Override public void handleOnBackPressed() { @@ -230,7 +234,8 @@ public void onCreate(Bundle savedInstanceState) { instancesRepositoryProvider, new SavepointsRepositoryProvider(this, storagePathProvider), new QRCodeCreatorImpl(), - new HtmlPrinter() + new HtmlPrinter(), + instancesDataService ); this.getSupportFragmentManager().setFragmentFactory(new FragmentFactoryBuilder() diff --git a/collect_app/src/main/java/org/odk/collect/android/formlists/blankformlist/BlankFormListActivity.kt b/collect_app/src/main/java/org/odk/collect/android/formlists/blankformlist/BlankFormListActivity.kt index bbd4b92f2f9..2b17b3226a8 100644 --- a/collect_app/src/main/java/org/odk/collect/android/formlists/blankformlist/BlankFormListActivity.kt +++ b/collect_app/src/main/java/org/odk/collect/android/formlists/blankformlist/BlankFormListActivity.kt @@ -14,9 +14,9 @@ import org.odk.collect.android.activities.FormMapActivity import org.odk.collect.android.formmanagement.FormFillingIntentFactory import org.odk.collect.android.injection.DaggerUtils import org.odk.collect.android.preferences.dialogs.ServerAuthDialogFragment -import org.odk.collect.androidshared.network.NetworkStateProvider import org.odk.collect.androidshared.ui.DialogFragmentUtils import org.odk.collect.androidshared.ui.SnackbarUtils +import org.odk.collect.async.network.NetworkStateProvider import org.odk.collect.lists.EmptyListView import org.odk.collect.lists.RecyclerViewUtils import org.odk.collect.permissions.PermissionListener diff --git a/collect_app/src/main/java/org/odk/collect/android/formlists/blankformlist/BlankFormListMenuProvider.kt b/collect_app/src/main/java/org/odk/collect/android/formlists/blankformlist/BlankFormListMenuProvider.kt index 5fb459ed6be..502ff038e32 100644 --- a/collect_app/src/main/java/org/odk/collect/android/formlists/blankformlist/BlankFormListMenuProvider.kt +++ b/collect_app/src/main/java/org/odk/collect/android/formlists/blankformlist/BlankFormListMenuProvider.kt @@ -9,9 +9,9 @@ import androidx.core.view.MenuProvider import org.odk.collect.android.R import org.odk.collect.android.formlists.sorting.FormListSortingBottomSheetDialog import org.odk.collect.android.formlists.sorting.FormListSortingOption -import org.odk.collect.androidshared.network.NetworkStateProvider import org.odk.collect.androidshared.ui.ToastUtils import org.odk.collect.androidshared.ui.multiclicksafe.MultiClickGuard +import org.odk.collect.async.network.NetworkStateProvider class BlankFormListMenuProvider( private val activity: ComponentActivity, diff --git a/collect_app/src/main/java/org/odk/collect/android/injection/config/AppDependencyComponent.java b/collect_app/src/main/java/org/odk/collect/android/injection/config/AppDependencyComponent.java index 33299d6cf6b..245cf06bada 100644 --- a/collect_app/src/main/java/org/odk/collect/android/injection/config/AppDependencyComponent.java +++ b/collect_app/src/main/java/org/odk/collect/android/injection/config/AppDependencyComponent.java @@ -16,7 +16,7 @@ import org.odk.collect.android.application.initialization.ExistingProjectMigrator; import org.odk.collect.android.audio.AudioRecordingControllerFragment; import org.odk.collect.android.audio.AudioRecordingErrorDialogFragment; -import org.odk.collect.android.backgroundwork.AutoSendTaskSpec; +import org.odk.collect.android.backgroundwork.SendFormsTaskSpec; import org.odk.collect.android.backgroundwork.AutoUpdateTaskSpec; import org.odk.collect.android.backgroundwork.SyncFormsTaskSpec; import org.odk.collect.android.configure.qr.QRCodeScannerFragment; @@ -78,7 +78,7 @@ import org.odk.collect.android.utilities.ThemeUtils; import org.odk.collect.android.widgets.QuestionWidget; import org.odk.collect.android.widgets.items.SelectOneFromMapDialogFragment; -import org.odk.collect.androidshared.network.NetworkStateProvider; +import org.odk.collect.async.network.NetworkStateProvider; import org.odk.collect.async.Scheduler; import org.odk.collect.draw.DrawActivity; import org.odk.collect.googlemaps.GoogleMapFragment; @@ -169,7 +169,7 @@ interface Builder { void inject(ShowQRCodeFragment showQRCodeFragment); - void inject(AutoSendTaskSpec autoSendTaskSpec); + void inject(SendFormsTaskSpec sendFormsTaskSpec); void inject(AdminPasswordDialogFragment adminPasswordDialogFragment); diff --git a/collect_app/src/main/java/org/odk/collect/android/injection/config/AppDependencyModule.java b/collect_app/src/main/java/org/odk/collect/android/injection/config/AppDependencyModule.java index c1acd235054..77da92bd5d7 100644 --- a/collect_app/src/main/java/org/odk/collect/android/injection/config/AppDependencyModule.java +++ b/collect_app/src/main/java/org/odk/collect/android/injection/config/AppDependencyModule.java @@ -29,10 +29,10 @@ import org.odk.collect.android.application.initialization.ApplicationInitializer; import org.odk.collect.android.application.initialization.ExistingProjectMigrator; import org.odk.collect.android.application.initialization.ExistingSettingsMigrator; -import org.odk.collect.android.application.initialization.FormUpdatesUpgrade; import org.odk.collect.android.application.initialization.GoogleDriveProjectsDeleter; import org.odk.collect.android.application.initialization.MapsInitializer; import org.odk.collect.android.application.initialization.SavepointsImporter; +import org.odk.collect.android.application.initialization.ScheduledWorkUpgrade; import org.odk.collect.android.application.initialization.upgrade.UpgradeInitializer; import org.odk.collect.android.backgroundwork.FormUpdateAndInstanceSubmitScheduler; import org.odk.collect.android.backgroundwork.FormUpdateScheduler; @@ -95,8 +95,8 @@ import org.odk.collect.android.version.VersionInformation; import org.odk.collect.android.views.BarcodeViewDecoder; import org.odk.collect.androidshared.bitmap.ImageCompressor; -import org.odk.collect.androidshared.network.ConnectivityProvider; -import org.odk.collect.androidshared.network.NetworkStateProvider; +import org.odk.collect.async.network.ConnectivityProvider; +import org.odk.collect.async.network.NetworkStateProvider; import org.odk.collect.androidshared.system.IntentLauncher; import org.odk.collect.androidshared.system.IntentLauncherImpl; import org.odk.collect.androidshared.utils.ScreenUtils; @@ -443,7 +443,7 @@ public InstancesDataService providesInstancesDataService(Application application return null; }; - return new InstancesDataService(application, instanceSubmitScheduler, projectsDependencyProviderFactory, notifier, propertyManager, httpInterface, onUpdate); + return new InstancesDataService(getState(application), instanceSubmitScheduler, projectsDependencyProviderFactory, notifier, propertyManager, httpInterface, onUpdate); } @Provides @@ -522,8 +522,8 @@ public ExistingProjectMigrator providesExistingProjectMigrator(Context context, } @Provides - public FormUpdatesUpgrade providesFormUpdatesUpgrader(Scheduler scheduler, ProjectsRepository projectsRepository, FormUpdateScheduler formUpdateScheduler) { - return new FormUpdatesUpgrade(scheduler, projectsRepository, formUpdateScheduler); + public ScheduledWorkUpgrade providesFormUpdatesUpgrader(Scheduler scheduler, ProjectsRepository projectsRepository, FormUpdateScheduler formUpdateScheduler, InstanceSubmitScheduler instanceSubmitScheduler) { + return new ScheduledWorkUpgrade(scheduler, projectsRepository, formUpdateScheduler, instanceSubmitScheduler); } @Provides @@ -537,13 +537,13 @@ public GoogleDriveProjectsDeleter providesGoogleDriveProjectsDeleter(ProjectsRep } @Provides - public UpgradeInitializer providesUpgradeInitializer(Context context, SettingsProvider settingsProvider, ExistingProjectMigrator existingProjectMigrator, ExistingSettingsMigrator existingSettingsMigrator, FormUpdatesUpgrade formUpdatesUpgrade, GoogleDriveProjectsDeleter googleDriveProjectsDeleter, ProjectsRepository projectsRepository, ProjectDependencyProviderFactory projectDependencyProviderFactory) { + public UpgradeInitializer providesUpgradeInitializer(Context context, SettingsProvider settingsProvider, ExistingProjectMigrator existingProjectMigrator, ExistingSettingsMigrator existingSettingsMigrator, ScheduledWorkUpgrade scheduledWorkUpgrade, GoogleDriveProjectsDeleter googleDriveProjectsDeleter, ProjectsRepository projectsRepository, ProjectDependencyProviderFactory projectDependencyProviderFactory) { return new UpgradeInitializer( context, settingsProvider, existingProjectMigrator, existingSettingsMigrator, - formUpdatesUpgrade, + scheduledWorkUpgrade, googleDriveProjectsDeleter, new SavepointsImporter(projectsRepository, projectDependencyProviderFactory) ); diff --git a/collect_app/src/main/java/org/odk/collect/android/instancemanagement/InstancesDataService.kt b/collect_app/src/main/java/org/odk/collect/android/instancemanagement/InstancesDataService.kt index 1ad666cd226..310bda069c2 100644 --- a/collect_app/src/main/java/org/odk/collect/android/instancemanagement/InstancesDataService.kt +++ b/collect_app/src/main/java/org/odk/collect/android/instancemanagement/InstancesDataService.kt @@ -1,6 +1,5 @@ package org.odk.collect.android.instancemanagement -import android.app.Application import androidx.lifecycle.LiveData import kotlinx.coroutines.flow.Flow import org.odk.collect.analytics.Analytics @@ -15,13 +14,14 @@ import org.odk.collect.android.openrosa.OpenRosaHttpInterface import org.odk.collect.android.projects.ProjectDependencyProviderFactory import org.odk.collect.android.utilities.ExternalizableFormDefCache import org.odk.collect.android.utilities.FormsUploadResultInterpreter -import org.odk.collect.androidshared.data.getState +import org.odk.collect.androidshared.data.AppState +import org.odk.collect.forms.Form import org.odk.collect.forms.instances.Instance import org.odk.collect.metadata.PropertyManager import java.io.File class InstancesDataService( - private val application: Application, + private val appState: AppState, private val instanceSubmitScheduler: InstanceSubmitScheduler, private val projectDependencyProviderFactory: ProjectDependencyProviderFactory, private val notifier: Notifier, @@ -29,7 +29,6 @@ class InstancesDataService( private val httpInterface: OpenRosaHttpInterface, private val onUpdate: () -> Unit ) { - private val appState = application.getState() val editableCount: LiveData = appState.getLive(EDITABLE_COUNT_KEY, 0) val sendableCount: LiveData = appState.getLive(SENDABLE_COUNT_KEY, 0) val sentCount: LiveData = appState.getLive(SENT_COUNT_KEY, 0) @@ -122,6 +121,7 @@ class InstancesDataService( if (finalizedInstance == null) { result.copy(failureCount = result.failureCount + 1) } else { + instanceFinalized(projectId, form) result } } @@ -133,7 +133,6 @@ class InstancesDataService( } update(projectId) - instanceSubmitScheduler.scheduleSubmit(projectId) return result.copy(successCount = instances.size - result.failureCount) } @@ -178,7 +177,7 @@ class InstancesDataService( } } - fun autoSendInstances(projectId: String): Boolean { + fun sendInstances(projectId: String, formAutoSend: Boolean = false): Boolean { val projectDependencyProvider = projectDependencyProviderFactory.create(projectId) @@ -195,10 +194,9 @@ class InstancesDataService( ).withLock { acquiredLock: Boolean -> if (acquiredLock) { val toUpload = InstanceAutoSendFetcher.getInstancesToAutoSend( - application, projectDependencyProvider.instancesRepository, projectDependencyProvider.formsRepository, - projectDependencyProvider.settingsProvider + formAutoSend ) if (toUpload.isNotEmpty()) { @@ -216,6 +214,14 @@ class InstancesDataService( } } + fun instanceFinalized(projectId: String, form: Form) { + if (form.autoSend != null && form.autoSend == "true") { + instanceSubmitScheduler.scheduleFormAutoSend(projectId) + } else { + instanceSubmitScheduler.scheduleAutoSend(projectId) + } + } + companion object { private const val EDITABLE_COUNT_KEY = "instancesEditableCount" private const val SENDABLE_COUNT_KEY = "instancesSendableCount" diff --git a/collect_app/src/main/java/org/odk/collect/android/instancemanagement/autosend/AutoSendSettingsProvider.kt b/collect_app/src/main/java/org/odk/collect/android/instancemanagement/autosend/AutoSendSettingsProvider.kt index 814c60c4da9..46c4eadb201 100644 --- a/collect_app/src/main/java/org/odk/collect/android/instancemanagement/autosend/AutoSendSettingsProvider.kt +++ b/collect_app/src/main/java/org/odk/collect/android/instancemanagement/autosend/AutoSendSettingsProvider.kt @@ -1,8 +1,8 @@ package org.odk.collect.android.instancemanagement.autosend import android.app.Application -import android.net.ConnectivityManager -import org.odk.collect.androidshared.network.NetworkStateProvider +import org.odk.collect.async.Scheduler +import org.odk.collect.async.network.NetworkStateProvider import org.odk.collect.settings.SettingsProvider import org.odk.collect.settings.enums.AutoSend import org.odk.collect.settings.enums.StringIdEnumUtils.getAutoSend @@ -14,7 +14,7 @@ class AutoSendSettingsProvider( ) { fun isAutoSendEnabledInSettings(projectId: String? = null): Boolean { - val currentNetworkInfo = networkStateProvider.networkInfo ?: return false + val currentNetworkType = networkStateProvider.currentNetwork ?: return false val autosend = settingsProvider.getUnprotectedSettings(projectId).getAutoSend(application) var sendwifi = autosend == AutoSend.WIFI_ONLY @@ -25,7 +25,7 @@ class AutoSendSettingsProvider( sendnetwork = true } - return currentNetworkInfo.type == ConnectivityManager.TYPE_WIFI && - sendwifi || currentNetworkInfo.type == ConnectivityManager.TYPE_MOBILE && sendnetwork + return currentNetworkType == Scheduler.NetworkType.WIFI && + sendwifi || currentNetworkType == Scheduler.NetworkType.CELLULAR && sendnetwork } } diff --git a/collect_app/src/main/java/org/odk/collect/android/instancemanagement/autosend/InstanceAutoSendFetcher.kt b/collect_app/src/main/java/org/odk/collect/android/instancemanagement/autosend/InstanceAutoSendFetcher.kt index eaed1ca48ff..25ff079ade1 100644 --- a/collect_app/src/main/java/org/odk/collect/android/instancemanagement/autosend/InstanceAutoSendFetcher.kt +++ b/collect_app/src/main/java/org/odk/collect/android/instancemanagement/autosend/InstanceAutoSendFetcher.kt @@ -1,33 +1,31 @@ package org.odk.collect.android.instancemanagement.autosend -import android.app.Application +import org.odk.collect.forms.Form import org.odk.collect.forms.FormsRepository import org.odk.collect.forms.instances.Instance import org.odk.collect.forms.instances.InstancesRepository -import org.odk.collect.settings.SettingsProvider -import org.odk.collect.settings.enums.AutoSend -import org.odk.collect.settings.enums.StringIdEnumUtils.getAutoSend object InstanceAutoSendFetcher { fun getInstancesToAutoSend( - application: Application, instancesRepository: InstancesRepository, formsRepository: FormsRepository, - settingsProvider: SettingsProvider + formAutoSend: Boolean = false ): List { val allFinalizedForms = instancesRepository.getAllByStatus( Instance.STATUS_COMPLETE, Instance.STATUS_SUBMISSION_FAILED ) - val autoSendSetting = - settingsProvider.getUnprotectedSettings().getAutoSend(application) + val filter: (Form) -> Boolean = if (formAutoSend) { + { form -> form.autoSend != null && form.autoSend == "true" } + } else { + { form -> form.autoSend == null } + } return allFinalizedForms.filter { - formsRepository.getLatestByFormIdAndVersion(it.formId, it.formVersion)?.let { form -> - form.shouldFormBeSentAutomatically(autoSendSetting != AutoSend.OFF) - } ?: false + formsRepository.getLatestByFormIdAndVersion(it.formId, it.formVersion) + ?.let { form -> filter(form) } ?: false } } } diff --git a/collect_app/src/main/java/org/odk/collect/android/instancemanagement/send/InstanceUploaderListActivity.java b/collect_app/src/main/java/org/odk/collect/android/instancemanagement/send/InstanceUploaderListActivity.java index c499dfdf88d..2850da8e6f2 100644 --- a/collect_app/src/main/java/org/odk/collect/android/instancemanagement/send/InstanceUploaderListActivity.java +++ b/collect_app/src/main/java/org/odk/collect/android/instancemanagement/send/InstanceUploaderListActivity.java @@ -63,7 +63,7 @@ import org.odk.collect.android.mainmenu.MainMenuActivity; import org.odk.collect.android.preferences.screens.ProjectPreferencesActivity; import org.odk.collect.android.projects.ProjectsDataService; -import org.odk.collect.androidshared.network.NetworkStateProvider; +import org.odk.collect.async.network.NetworkStateProvider; import org.odk.collect.androidshared.ui.MenuExtKt; import org.odk.collect.androidshared.ui.ToastUtils; import org.odk.collect.androidshared.ui.multiclicksafe.MultiClickGuard; diff --git a/collect_app/src/main/java/org/odk/collect/android/preferences/screens/FormManagementPreferencesFragment.java b/collect_app/src/main/java/org/odk/collect/android/preferences/screens/FormManagementPreferencesFragment.java index 867b695984d..7828144fba0 100644 --- a/collect_app/src/main/java/org/odk/collect/android/preferences/screens/FormManagementPreferencesFragment.java +++ b/collect_app/src/main/java/org/odk/collect/android/preferences/screens/FormManagementPreferencesFragment.java @@ -83,7 +83,7 @@ public void onSettingChanged(@NotNull String key) { } if (key.equals(KEY_AUTOSEND) && !StringIdEnumUtils.getAutoSend(settingsProvider.getUnprotectedSettings(), requireContext()).equals(AutoSend.OFF)) { - instanceSubmitScheduler.scheduleSubmit(projectsDataService.getCurrentProject().getUuid()); + instanceSubmitScheduler.scheduleAutoSend(projectsDataService.getCurrentProject().getUuid()); } } diff --git a/collect_app/src/test/java/org/odk/collect/android/application/initialization/FormUpdatesUpgradeTest.kt b/collect_app/src/test/java/org/odk/collect/android/application/initialization/FormUpdatesUpgradeTest.kt deleted file mode 100644 index 35c3de430f8..00000000000 --- a/collect_app/src/test/java/org/odk/collect/android/application/initialization/FormUpdatesUpgradeTest.kt +++ /dev/null @@ -1,35 +0,0 @@ -package org.odk.collect.android.application.initialization - -import org.junit.Test -import org.mockito.kotlin.mock -import org.mockito.kotlin.verify -import org.odk.collect.android.backgroundwork.FormUpdateScheduler -import org.odk.collect.async.Scheduler -import org.odk.collect.projects.InMemProjectsRepository -import org.odk.collect.projects.Project - -class FormUpdatesUpgradeTest { - - @Test - fun `cancels all existing background jobs`() { - val scheduler = mock() - val formUpdatesUpgrade = FormUpdatesUpgrade(scheduler, InMemProjectsRepository(), mock()) - - formUpdatesUpgrade.run() - verify(scheduler).cancelAllDeferred() - } - - @Test - fun `schedules updates for all projects`() { - val projectsRepository = InMemProjectsRepository() - val project1 = projectsRepository.save(Project.New("1", "1", "#ffffff")) - val project2 = projectsRepository.save(Project.New("2", "2", "#ffffff")) - - val formUpdateScheduler = mock() - val formUpdatesUpgrade = FormUpdatesUpgrade(mock(), projectsRepository, formUpdateScheduler) - - formUpdatesUpgrade.run() - verify(formUpdateScheduler).scheduleUpdates(project1.uuid) - verify(formUpdateScheduler).scheduleUpdates(project2.uuid) - } -} diff --git a/collect_app/src/test/java/org/odk/collect/android/application/initialization/ScheduledWorkUpgradeTest.kt b/collect_app/src/test/java/org/odk/collect/android/application/initialization/ScheduledWorkUpgradeTest.kt new file mode 100644 index 00000000000..984d2164725 --- /dev/null +++ b/collect_app/src/test/java/org/odk/collect/android/application/initialization/ScheduledWorkUpgradeTest.kt @@ -0,0 +1,67 @@ +package org.odk.collect.android.application.initialization + +import org.junit.Test +import org.mockito.kotlin.mock +import org.mockito.kotlin.verify +import org.odk.collect.android.backgroundwork.FormUpdateScheduler +import org.odk.collect.android.backgroundwork.InstanceSubmitScheduler +import org.odk.collect.async.Scheduler +import org.odk.collect.projects.InMemProjectsRepository +import org.odk.collect.projects.Project + +class ScheduledWorkUpgradeTest { + + @Test + fun `cancels all existing background jobs`() { + val scheduler = mock() + val scheduledWorkUpgrade = ScheduledWorkUpgrade( + scheduler, + InMemProjectsRepository(), + mock(), + mock() + ) + + scheduledWorkUpgrade.run() + verify(scheduler).cancelAllDeferred() + } + + @Test + fun `schedules updates for all projects`() { + val projectsRepository = InMemProjectsRepository() + val project1 = projectsRepository.save(Project.New("1", "1", "#ffffff")) + val project2 = projectsRepository.save(Project.New("2", "2", "#ffffff")) + + val formUpdateScheduler = mock() + val scheduledWorkUpgrade = ScheduledWorkUpgrade( + mock(), + projectsRepository, + formUpdateScheduler, + mock() + ) + + scheduledWorkUpgrade.run() + verify(formUpdateScheduler).scheduleUpdates(project1.uuid) + verify(formUpdateScheduler).scheduleUpdates(project2.uuid) + } + + @Test + fun `schedules submits for all projects`() { + val projectsRepository = InMemProjectsRepository() + val project1 = projectsRepository.save(Project.New("1", "1", "#ffffff")) + val project2 = projectsRepository.save(Project.New("2", "2", "#ffffff")) + + val instanceSubmitScheduler = mock() + val scheduledWorkUpgrade = ScheduledWorkUpgrade( + mock(), + projectsRepository, + mock(), + instanceSubmitScheduler + ) + + scheduledWorkUpgrade.run() + verify(instanceSubmitScheduler).scheduleAutoSend(project1.uuid) + verify(instanceSubmitScheduler).scheduleFormAutoSend(project1.uuid) + verify(instanceSubmitScheduler).scheduleAutoSend(project2.uuid) + verify(instanceSubmitScheduler).scheduleFormAutoSend(project2.uuid) + } +} diff --git a/collect_app/src/test/java/org/odk/collect/android/backgroundwork/FormUpdateAndInstanceSubmitSchedulerTest.kt b/collect_app/src/test/java/org/odk/collect/android/backgroundwork/FormUpdateAndInstanceSubmitSchedulerTest.kt index b27fe87dc0b..545e5a622c4 100644 --- a/collect_app/src/test/java/org/odk/collect/android/backgroundwork/FormUpdateAndInstanceSubmitSchedulerTest.kt +++ b/collect_app/src/test/java/org/odk/collect/android/backgroundwork/FormUpdateAndInstanceSubmitSchedulerTest.kt @@ -9,6 +9,7 @@ import org.mockito.kotlin.any import org.mockito.kotlin.eq import org.mockito.kotlin.mock import org.mockito.kotlin.verify +import org.mockito.kotlin.verifyNoInteractions import org.odk.collect.android.TestSettingsProvider import org.odk.collect.async.Scheduler import org.odk.collect.settings.enums.FormUpdateMode.MATCH_EXACTLY @@ -83,50 +84,60 @@ class FormUpdateAndInstanceSubmitSchedulerTest { } @Test - fun `scheduleSubmit passes current project ID`() { + fun `scheduleAutoSend passes current project ID`() { settingsProvider.getUnprotectedSettings("myProject") .save(ProjectKeys.KEY_AUTOSEND, "wifi_and_cellular") val manager = FormUpdateAndInstanceSubmitScheduler(scheduler, settingsProvider, application) - manager.scheduleSubmit("myProject") + manager.scheduleAutoSend("myProject") verify(scheduler).networkDeferred( eq("AutoSendWorker:myProject"), - any(), + any(), eq(mapOf(TaskData.DATA_PROJECT_ID to "myProject")), eq(null) ) } @Test - fun `scheduleSubmit uses wifi network type when set in settings`() { + fun `scheduleAutoSend uses wifi network type when set in settings`() { settingsProvider.getUnprotectedSettings("myProject") .save(ProjectKeys.KEY_AUTOSEND, "wifi_only") val manager = FormUpdateAndInstanceSubmitScheduler(scheduler, settingsProvider, application) - manager.scheduleSubmit("myProject") + manager.scheduleAutoSend("myProject") verify(scheduler).networkDeferred( eq("AutoSendWorker:myProject"), - any(), + any(), eq(mapOf(TaskData.DATA_PROJECT_ID to "myProject")), eq(Scheduler.NetworkType.WIFI) ) } @Test - fun `scheduleSubmit uses cellular network type when set in settings`() { + fun `scheduleAutoSend uses cellular network type when set in settings`() { settingsProvider.getUnprotectedSettings("myProject") .save(ProjectKeys.KEY_AUTOSEND, "cellular_only") val manager = FormUpdateAndInstanceSubmitScheduler(scheduler, settingsProvider, application) - manager.scheduleSubmit("myProject") + manager.scheduleAutoSend("myProject") verify(scheduler).networkDeferred( eq("AutoSendWorker:myProject"), - any(), + any(), eq(mapOf(TaskData.DATA_PROJECT_ID to "myProject")), eq(Scheduler.NetworkType.CELLULAR) ) } + @Test + fun `scheduleAutoSend does nothing if auto send is disabled`() { + settingsProvider.getUnprotectedSettings("myProject") + .save(ProjectKeys.KEY_AUTOSEND, "off") + val manager = FormUpdateAndInstanceSubmitScheduler(scheduler, settingsProvider, application) + + manager.scheduleAutoSend("myProject") + verifyNoInteractions(scheduler) + } + @Test fun `cancelSubmit cancels auto send for current project`() { val manager = FormUpdateAndInstanceSubmitScheduler(scheduler, settingsProvider, application) diff --git a/collect_app/src/test/java/org/odk/collect/android/backgroundwork/AutoSendTaskSpecTest.kt b/collect_app/src/test/java/org/odk/collect/android/backgroundwork/SendFormsTaskSpecTest.kt similarity index 86% rename from collect_app/src/test/java/org/odk/collect/android/backgroundwork/AutoSendTaskSpecTest.kt rename to collect_app/src/test/java/org/odk/collect/android/backgroundwork/SendFormsTaskSpecTest.kt index 6cf2c3091bb..915ef9b1192 100644 --- a/collect_app/src/test/java/org/odk/collect/android/backgroundwork/AutoSendTaskSpecTest.kt +++ b/collect_app/src/test/java/org/odk/collect/android/backgroundwork/SendFormsTaskSpecTest.kt @@ -22,7 +22,7 @@ import org.odk.collect.metadata.PropertyManager import org.odk.collect.testshared.RobolectricHelpers @RunWith(AndroidJUnit4::class) -class AutoSendTaskSpecTest { +class SendFormsTaskSpecTest { private val instancesDataService = mock() private lateinit var projectId: String @@ -47,27 +47,22 @@ class AutoSendTaskSpecTest { projectId = CollectHelpers.setupDemoProject() } - @Test - fun `maxRetries should not be limited`() { - assertThat(AutoSendTaskSpec().maxRetries, equalTo(null)) - } - @Test fun `returns false if sending instances fails`() { - whenever(instancesDataService.autoSendInstances(projectId)).doReturn(false) + whenever(instancesDataService.sendInstances(projectId)).doReturn(false) val inputData = mapOf(TaskData.DATA_PROJECT_ID to projectId) - val spec = AutoSendTaskSpec() + val spec = SendFormsTaskSpec() val task = spec.getTask(ApplicationProvider.getApplicationContext(), inputData, true) assertThat(task.get(), equalTo(false)) } @Test fun `returns true if sending instances succeeds`() { - whenever(instancesDataService.autoSendInstances(projectId)).doReturn(true) + whenever(instancesDataService.sendInstances(projectId)).doReturn(true) val inputData = mapOf(TaskData.DATA_PROJECT_ID to projectId) - val spec = AutoSendTaskSpec() + val spec = SendFormsTaskSpec() val task = spec.getTask(ApplicationProvider.getApplicationContext(), inputData, true) assertThat(task.get(), equalTo(true)) } diff --git a/collect_app/src/test/java/org/odk/collect/android/formentry/audit/FormSaveViewModelTest.java b/collect_app/src/test/java/org/odk/collect/android/formentry/audit/FormSaveViewModelTest.java index 585a3ca481f..a6d19e7d108 100644 --- a/collect_app/src/test/java/org/odk/collect/android/formentry/audit/FormSaveViewModelTest.java +++ b/collect_app/src/test/java/org/odk/collect/android/formentry/audit/FormSaveViewModelTest.java @@ -101,7 +101,7 @@ public void setup() { when(projectsDataService.getCurrentProject()).thenReturn(Project.Companion.getDEMO_PROJECT()); formSession = new MutableLiveData<>(new FormSession(formController, form)); - viewModel = new FormSaveViewModel(savedStateHandle, () -> CURRENT_TIME, formSaver, mediaUtils, scheduler, audioRecorder, projectsDataService, formSession, entitiesRepository, instancesRepository, savepointsRepository); + viewModel = new FormSaveViewModel(savedStateHandle, () -> CURRENT_TIME, formSaver, mediaUtils, scheduler, audioRecorder, projectsDataService, formSession, entitiesRepository, instancesRepository, savepointsRepository, mock()); CollectHelpers.createDemoProject(); // Needed to deal with `new StoragePathProvider()` calls in `FormSaveViewModel` } @@ -386,7 +386,7 @@ public void deleteAnswerFile_whenAnswerFileHasAlreadyBeenDeleted_actuallyDeletes public void deleteAnswerFile_whenAnswerFileHasAlreadyBeenDeleted_onRecreatingViewModel_actuallyDeletesNewFile() { viewModel.deleteAnswerFile("index", "blah1"); - FormSaveViewModel restoredViewModel = new FormSaveViewModel(savedStateHandle, () -> CURRENT_TIME, formSaver, mediaUtils, scheduler, mock(AudioRecorder.class), projectsDataService, liveDataOf(new FormSession(formController, form)), entitiesRepository, instancesRepository, savepointsRepository); + FormSaveViewModel restoredViewModel = new FormSaveViewModel(savedStateHandle, () -> CURRENT_TIME, formSaver, mediaUtils, scheduler, mock(AudioRecorder.class), projectsDataService, liveDataOf(new FormSession(formController, form)), entitiesRepository, instancesRepository, savepointsRepository, mock()); restoredViewModel.deleteAnswerFile("index", "blah2"); verify(mediaUtils).deleteMediaFile("blah2"); @@ -408,7 +408,7 @@ public void replaceAnswerFile_whenAnswerFileHasAlreadyBeenReplaced_deletesPrevio public void replaceAnswerFile_whenAnswerFileHasAlreadyBeenReplaced_afterRecreatingViewModel_deletesPreviousReplacement() { viewModel.replaceAnswerFile("index", "blah1"); - FormSaveViewModel restoredViewModel = new FormSaveViewModel(savedStateHandle, () -> CURRENT_TIME, formSaver, mediaUtils, scheduler, mock(AudioRecorder.class), projectsDataService, liveDataOf(new FormSession(formController, form)), entitiesRepository, instancesRepository, savepointsRepository); + FormSaveViewModel restoredViewModel = new FormSaveViewModel(savedStateHandle, () -> CURRENT_TIME, formSaver, mediaUtils, scheduler, mock(AudioRecorder.class), projectsDataService, liveDataOf(new FormSession(formController, form)), entitiesRepository, instancesRepository, savepointsRepository, mock()); restoredViewModel.replaceAnswerFile("index", "blah2"); verify(mediaUtils).deleteMediaFile("blah1"); @@ -482,7 +482,7 @@ public void isSavingFileAnswerFile_isTrueWhenWhileIsSaving() throws Exception { @Test public void ignoreChanges_whenFormControllerNotSet_doesNothing() { - FormSaveViewModel viewModel = new FormSaveViewModel(savedStateHandle, () -> CURRENT_TIME, formSaver, mediaUtils, scheduler, mock(AudioRecorder.class), projectsDataService, liveDataOf(new FormSession(formController, form)), entitiesRepository, instancesRepository, savepointsRepository); + FormSaveViewModel viewModel = new FormSaveViewModel(savedStateHandle, () -> CURRENT_TIME, formSaver, mediaUtils, scheduler, mock(AudioRecorder.class), projectsDataService, liveDataOf(new FormSession(formController, form)), entitiesRepository, instancesRepository, savepointsRepository, mock()); viewModel.ignoreChanges(); // Checks nothing explodes } diff --git a/collect_app/src/test/java/org/odk/collect/android/formlists/blankformlist/BlankFormListMenuProviderTest.kt b/collect_app/src/test/java/org/odk/collect/android/formlists/blankformlist/BlankFormListMenuProviderTest.kt index 0aabbb025e5..b324cf20346 100644 --- a/collect_app/src/test/java/org/odk/collect/android/formlists/blankformlist/BlankFormListMenuProviderTest.kt +++ b/collect_app/src/test/java/org/odk/collect/android/formlists/blankformlist/BlankFormListMenuProviderTest.kt @@ -23,7 +23,7 @@ import org.mockito.kotlin.whenever import org.odk.collect.android.R import org.odk.collect.android.formlists.sorting.FormListSortingBottomSheetDialog import org.odk.collect.android.support.CollectHelpers -import org.odk.collect.androidshared.network.NetworkStateProvider +import org.odk.collect.async.network.NetworkStateProvider import org.robolectric.Shadows import org.robolectric.fakes.RoboMenuItem import org.robolectric.shadows.ShadowDialog diff --git a/collect_app/src/test/java/org/odk/collect/android/instancemanagement/InstancesDataServiceTest.kt b/collect_app/src/test/java/org/odk/collect/android/instancemanagement/InstancesDataServiceTest.kt index 13358a88773..b88b3eed413 100644 --- a/collect_app/src/test/java/org/odk/collect/android/instancemanagement/InstancesDataServiceTest.kt +++ b/collect_app/src/test/java/org/odk/collect/android/instancemanagement/InstancesDataServiceTest.kt @@ -19,6 +19,7 @@ import org.odk.collect.android.projects.ProjectDependencyProviderFactory import org.odk.collect.android.utilities.ChangeLockProvider import org.odk.collect.android.utilities.FormsRepositoryProvider import org.odk.collect.android.utilities.InstancesRepositoryProvider +import org.odk.collect.androidshared.data.AppState import org.odk.collect.forms.instances.Instance.STATUS_COMPLETE import org.odk.collect.formstest.FormFixtures import org.odk.collect.formstest.InMemFormsRepository @@ -26,7 +27,6 @@ import org.odk.collect.formstest.InMemInstancesRepository import org.odk.collect.formstest.InstanceFixtures import org.odk.collect.projects.Project import org.odk.collect.settings.InMemSettingsProvider -import org.odk.collect.settings.enums.AutoSend import org.odk.collect.settings.keys.ProjectKeys import org.odk.collect.testshared.BooleanChangeLock @@ -50,8 +50,6 @@ class InstancesDataServiceTest { val settingsProvider = InMemSettingsProvider().also { it.getUnprotectedSettings(project.uuid) .save(ProjectKeys.KEY_SERVER_URL, "http://example.com") - it.getUnprotectedSettings() - .save(ProjectKeys.KEY_AUTOSEND, AutoSend.WIFI_ONLY.getValue(application)) } private val projectsDependencyProviderFactory = ProjectDependencyProviderFactory( @@ -71,7 +69,7 @@ class InstancesDataServiceTest { private val instancesDataService = InstancesDataService( - application, + AppState(), mock(), projectsDependencyProviderFactory, notifier, @@ -94,19 +92,19 @@ class InstancesDataServiceTest { } @Test - fun `autoSendInstances() returns true when there are no instances to send`() { - val result = instancesDataService.autoSendInstances(project.uuid) + fun `sendInstances() returns true when there are no instances to send`() { + val result = instancesDataService.sendInstances(project.uuid) assertThat(result, equalTo(true)) } @Test - fun `autoSendInstances() does not notify when there are no instances to send`() { - instancesDataService.autoSendInstances(project.uuid) + fun `sendInstances() does not notify when there are no instances to send`() { + instancesDataService.sendInstances(project.uuid) verifyNoInteractions(notifier) } @Test - fun `autoSendInstances() returns false when an instance fails to send`() { + fun `sendInstances() returns false when an instance fails to send`() { val formsRepository = projectDependencyProvider.formsRepository val form = formsRepository.save(FormFixtures.form()) @@ -116,7 +114,7 @@ class InstancesDataServiceTest { whenever(httpInterface.executeGetRequest(any(), any(), any())) .doReturn(HttpGetResult(null, emptyMap(), "", 500)) - val result = instancesDataService.autoSendInstances(project.uuid) + val result = instancesDataService.sendInstances(project.uuid) assertThat(result, equalTo(false)) } } diff --git a/collect_app/src/test/java/org/odk/collect/android/instancemanagement/autosend/AutoSendSettingsProviderTest.kt b/collect_app/src/test/java/org/odk/collect/android/instancemanagement/autosend/AutoSendSettingsProviderTest.kt index 2f231fcdfbc..b41b652a45e 100644 --- a/collect_app/src/test/java/org/odk/collect/android/instancemanagement/autosend/AutoSendSettingsProviderTest.kt +++ b/collect_app/src/test/java/org/odk/collect/android/instancemanagement/autosend/AutoSendSettingsProviderTest.kt @@ -1,8 +1,6 @@ package org.odk.collect.android.instancemanagement.autosend import android.app.Application -import android.net.ConnectivityManager -import android.net.NetworkInfo import androidx.test.core.app.ApplicationProvider import androidx.test.ext.junit.runners.AndroidJUnit4 import org.junit.Assert.assertFalse @@ -11,7 +9,8 @@ import org.junit.Test import org.junit.runner.RunWith import org.mockito.kotlin.mock import org.mockito.kotlin.whenever -import org.odk.collect.androidshared.network.NetworkStateProvider +import org.odk.collect.async.Scheduler +import org.odk.collect.async.network.NetworkStateProvider import org.odk.collect.projects.Project import org.odk.collect.settings.InMemSettingsProvider import org.odk.collect.settings.enums.AutoSend @@ -39,7 +38,7 @@ class AutoSendSettingsProviderTest { fun `return false when autosend is disabled in settings and network type is wifi`() { val autoSendSettingsProvider = setupAutoSendSettingProvider( autoSendOption = AutoSend.OFF.getValue(application), - networkType = ConnectivityManager.TYPE_WIFI + networkType = Scheduler.NetworkType.WIFI ) assertFalse(autoSendSettingsProvider.isAutoSendEnabledInSettings(projectId)) @@ -49,7 +48,7 @@ class AutoSendSettingsProviderTest { fun `return false when autosend is disabled in settings and network type is cellular`() { val autoSendSettingsProvider = setupAutoSendSettingProvider( autoSendOption = AutoSend.OFF.getValue(application), - networkType = ConnectivityManager.TYPE_MOBILE + networkType = Scheduler.NetworkType.CELLULAR ) assertFalse(autoSendSettingsProvider.isAutoSendEnabledInSettings(projectId)) @@ -69,7 +68,7 @@ class AutoSendSettingsProviderTest { fun `return false when autosend is enabled for 'wifi_only' and network type is cellular`() { val autoSendSettingsProvider = setupAutoSendSettingProvider( autoSendOption = AutoSend.WIFI_ONLY.getValue(application), - networkType = ConnectivityManager.TYPE_MOBILE + networkType = Scheduler.NetworkType.CELLULAR ) assertFalse(autoSendSettingsProvider.isAutoSendEnabledInSettings(projectId)) @@ -79,7 +78,7 @@ class AutoSendSettingsProviderTest { fun `return true when autosend is enabled for 'wifi_only' and network type is wifi`() { val autoSendSettingsProvider = setupAutoSendSettingProvider( autoSendOption = AutoSend.WIFI_ONLY.getValue(application), - networkType = ConnectivityManager.TYPE_WIFI + networkType = Scheduler.NetworkType.WIFI ) assertTrue(autoSendSettingsProvider.isAutoSendEnabledInSettings(projectId)) @@ -99,7 +98,7 @@ class AutoSendSettingsProviderTest { fun `return false when autosend is enabled for 'cellular_only' and network type is wifi`() { val autoSendSettingsProvider = setupAutoSendSettingProvider( autoSendOption = AutoSend.CELLULAR_ONLY.getValue(application), - networkType = ConnectivityManager.TYPE_WIFI + networkType = Scheduler.NetworkType.WIFI ) assertFalse(autoSendSettingsProvider.isAutoSendEnabledInSettings(projectId)) @@ -109,7 +108,7 @@ class AutoSendSettingsProviderTest { fun `return true when autosend is enabled for 'cellular_only' and network type is cellular`() { val autoSendSettingsProvider = setupAutoSendSettingProvider( autoSendOption = AutoSend.CELLULAR_ONLY.getValue(application), - networkType = ConnectivityManager.TYPE_MOBILE + networkType = Scheduler.NetworkType.CELLULAR ) assertTrue(autoSendSettingsProvider.isAutoSendEnabledInSettings(projectId)) @@ -129,7 +128,7 @@ class AutoSendSettingsProviderTest { fun `return true when autosend is enabled for 'wifi_and_cellular' and network type is wifi`() { val autoSendSettingsProvider = setupAutoSendSettingProvider( autoSendOption = AutoSend.WIFI_AND_CELLULAR.getValue(application), - networkType = ConnectivityManager.TYPE_WIFI + networkType = Scheduler.NetworkType.WIFI ) assertTrue(autoSendSettingsProvider.isAutoSendEnabledInSettings(projectId)) @@ -139,7 +138,7 @@ class AutoSendSettingsProviderTest { fun `return true when autosend is enabled for 'wifi_and_cellular' and network type is cellular`() { val autoSendSettingsProvider = setupAutoSendSettingProvider( autoSendOption = AutoSend.WIFI_AND_CELLULAR.getValue(application), - networkType = ConnectivityManager.TYPE_MOBILE + networkType = Scheduler.NetworkType.CELLULAR ) assertTrue(autoSendSettingsProvider.isAutoSendEnabledInSettings(projectId)) @@ -147,15 +146,9 @@ class AutoSendSettingsProviderTest { private fun setupAutoSendSettingProvider( autoSendOption: String? = null, - networkType: Int? = null + networkType: Scheduler.NetworkType? = null ): AutoSendSettingsProvider { - var networkInfo: NetworkInfo? = null - networkType?.let { - networkInfo = mock().also { - whenever(it.type).thenReturn(networkType) - } - } - whenever(networkStateProvider.networkInfo).thenReturn(networkInfo) + whenever(networkStateProvider.currentNetwork).thenReturn(networkType) settingsProvider.getUnprotectedSettings(projectId).save(ProjectKeys.KEY_AUTOSEND, autoSendOption) return AutoSendSettingsProvider(application, networkStateProvider, settingsProvider) diff --git a/collect_app/src/test/java/org/odk/collect/android/instancemanagement/autosend/InstanceAutoSendFetcherTest.kt b/collect_app/src/test/java/org/odk/collect/android/instancemanagement/autosend/InstanceAutoSendFetcherTest.kt index 16da2257080..aa0028a1f00 100644 --- a/collect_app/src/test/java/org/odk/collect/android/instancemanagement/autosend/InstanceAutoSendFetcherTest.kt +++ b/collect_app/src/test/java/org/odk/collect/android/instancemanagement/autosend/InstanceAutoSendFetcherTest.kt @@ -1,7 +1,5 @@ package org.odk.collect.android.instancemanagement.autosend -import android.app.Application -import androidx.test.core.app.ApplicationProvider import androidx.test.ext.junit.runners.AndroidJUnit4 import org.hamcrest.MatcherAssert.assertThat import org.hamcrest.Matchers.contains @@ -12,9 +10,6 @@ import org.odk.collect.formstest.FormUtils.buildForm import org.odk.collect.formstest.InMemFormsRepository import org.odk.collect.formstest.InMemInstancesRepository import org.odk.collect.formstest.InstanceUtils.buildInstance -import org.odk.collect.settings.InMemSettingsProvider -import org.odk.collect.settings.enums.AutoSend -import org.odk.collect.settings.keys.ProjectKeys import org.odk.collect.shared.TempFiles.createTempDir @RunWith(AndroidJUnit4::class) @@ -51,10 +46,8 @@ class InstanceAutoSendFetcherTest { private val instanceOfFormWithCustomAutoSendSubmissionFailed = buildInstance("4", "1", "instance 3", Instance.STATUS_SUBMISSION_FAILED, null, createTempDir().absolutePath).build() private val instanceOfFormWithCustomAutoSendSubmitted = buildInstance("4", "1", "instance 4", Instance.STATUS_SUBMITTED, null, createTempDir().absolutePath).build() - private val application = ApplicationProvider.getApplicationContext() - @Test - fun `return all finalized instances of forms that do not have auto send disabled on a form level`() { + fun `return all finalized instances of forms that do not have auto send on a form level`() { formsRepository.save(formWithEnabledAutoSend) formsRepository.save(formWithoutSpecifiedAutoSend) formsRepository.save(formWithDisabledAutoSend) @@ -82,62 +75,60 @@ class InstanceAutoSendFetcherTest { save(instanceOfFormWithCustomAutoSendSubmitted) } - val settingsProvider = InMemSettingsProvider().also { - it.getUnprotectedSettings() - .save(ProjectKeys.KEY_AUTOSEND, AutoSend.WIFI_ONLY.getValue(application)) - } - val instancesToSend = InstanceAutoSendFetcher.getInstancesToAutoSend( - application, instancesRepository, - formsRepository, - settingsProvider + formsRepository ) assertThat( instancesToSend.map { it.instanceFilePath }, contains( - instanceOfFormWithEnabledAutoSendComplete.instanceFilePath, - instanceOfFormWithEnabledAutoSendSubmissionFailed.instanceFilePath, instanceOfFormWithoutSpecifiedAutoSendComplete.instanceFilePath, instanceOfFormWithoutSpecifiedAutoSendSubmissionFailed.instanceFilePath, - instanceOfFormWithCustomAutoSendComplete.instanceFilePath, - instanceOfFormWithCustomAutoSendSubmissionFailed.instanceFilePath ) ) } @Test - fun `if there are multiple versions of one form and only one has auto-send enabled take only instances of that form`() { - val formWithEnabledAutoSendV1 = buildForm("1", "1", createTempDir().absolutePath, autosend = "false").build() - val instanceOfFormWithEnabledAutoSendCompleteV1 = buildInstance("1", "1", "instance 2", Instance.STATUS_COMPLETE, null, createTempDir().absolutePath).build() + fun `return all finalized forms with autosend when formAutoSend is true`() { + formsRepository.save(formWithEnabledAutoSend) + formsRepository.save(formWithoutSpecifiedAutoSend) + formsRepository.save(formWithDisabledAutoSend) + formsRepository.save(formWithCustomAutoSend) - val formWithEnabledAutoSendV2 = buildForm("1", "2", createTempDir().absolutePath, autosend = "true").build() - val instanceOfFormWithEnabledAutoSendCompleteV2 = buildInstance("1", "2", "instance 2", Instance.STATUS_COMPLETE, null, createTempDir().absolutePath).build() + instancesRepository.apply { + save(instanceOfFormWithEnabledAutoSendIncomplete) + save(instanceOfFormWithEnabledAutoSendComplete) + save(instanceOfFormWithEnabledAutoSendSubmissionFailed) + save(instanceOfFormWithEnabledAutoSendSubmitted) - formsRepository.save(formWithEnabledAutoSendV1) - formsRepository.save(formWithEnabledAutoSendV2) + save(instanceOfFormWithoutSpecifiedAutoSendIncomplete) + save(instanceOfFormWithoutSpecifiedAutoSendComplete) + save(instanceOfFormWithoutSpecifiedAutoSendSubmissionFailed) + save(instanceOfFormWithoutSpecifiedAutoSendSubmitted) - instancesRepository.apply { - save(instanceOfFormWithEnabledAutoSendCompleteV1) - save(instanceOfFormWithEnabledAutoSendCompleteV2) - } + save(instanceOfFormWithDisabledAutoSendIncomplete) + save(instanceOfFormWithDisabledAutoSendComplete) + save(instanceOfFormWithDisabledAutoSendSubmissionFailed) + save(instanceOfFormWithDisabledAutoSendSubmitted) - val settingsProvider = InMemSettingsProvider().also { - it.getUnprotectedSettings() - .save(ProjectKeys.KEY_AUTOSEND, AutoSend.WIFI_ONLY.getValue(application)) + save(instanceOfFormWithCustomAutoSendIncomplete) + save(instanceOfFormWithCustomAutoSendComplete) + save(instanceOfFormWithCustomAutoSendSubmissionFailed) + save(instanceOfFormWithCustomAutoSendSubmitted) } val instancesToSend = InstanceAutoSendFetcher.getInstancesToAutoSend( - application, instancesRepository, formsRepository, - settingsProvider + formAutoSend = true ) + assertThat( instancesToSend.map { it.instanceFilePath }, contains( - instanceOfFormWithEnabledAutoSendCompleteV2.instanceFilePath + instanceOfFormWithEnabledAutoSendComplete.instanceFilePath, + instanceOfFormWithEnabledAutoSendSubmissionFailed.instanceFilePath, ) ) } diff --git a/collect_app/src/test/java/org/odk/collect/android/preferences/screens/FormManagementPreferencesFragmentTest.kt b/collect_app/src/test/java/org/odk/collect/android/preferences/screens/FormManagementPreferencesFragmentTest.kt index d06804cffae..bf04357f08a 100644 --- a/collect_app/src/test/java/org/odk/collect/android/preferences/screens/FormManagementPreferencesFragmentTest.kt +++ b/collect_app/src/test/java/org/odk/collect/android/preferences/screens/FormManagementPreferencesFragmentTest.kt @@ -455,7 +455,7 @@ class FormManagementPreferencesFragmentTest { scenario.onFragment { fragment: FormManagementPreferencesFragment -> fragment.findPreference(ProjectKeys.KEY_AUTOSEND)!!.value = AutoSend.WIFI_ONLY.getValue(context) } - verify(instanceSubmitScheduler).scheduleSubmit(projectID) + verify(instanceSubmitScheduler).scheduleAutoSend(projectID) } @Test @@ -464,6 +464,6 @@ class FormManagementPreferencesFragmentTest { scenario.onFragment { fragment: FormManagementPreferencesFragment -> fragment.findPreference(ProjectKeys.KEY_AUTOSEND)!!.value = AutoSend.OFF.getValue(context) } - verify(instanceSubmitScheduler, never()).scheduleSubmit(projectID) + verify(instanceSubmitScheduler, never()).scheduleAutoSend(projectID) } } diff --git a/mapbox/build.gradle.kts b/mapbox/build.gradle.kts index 48f2867203f..ee99e953e4e 100644 --- a/mapbox/build.gradle.kts +++ b/mapbox/build.gradle.kts @@ -44,6 +44,7 @@ dependencies { implementation(project(":settings")) implementation(project(":shared")) implementation(project(":strings")) + implementation(project(":async")) implementation(Dependencies.play_services_location) implementation(Dependencies.androidx_preference_ktx) implementation(Dependencies.mapbox_android_sdk) diff --git a/mapbox/src/main/java/org/odk/collect/mapbox/MapBoxInitializationFragment.kt b/mapbox/src/main/java/org/odk/collect/mapbox/MapBoxInitializationFragment.kt index 780258d433b..9895300e904 100644 --- a/mapbox/src/main/java/org/odk/collect/mapbox/MapBoxInitializationFragment.kt +++ b/mapbox/src/main/java/org/odk/collect/mapbox/MapBoxInitializationFragment.kt @@ -12,7 +12,7 @@ import com.mapbox.maps.MapView import com.mapbox.maps.Style import com.mapbox.maps.loader.MapboxMapsInitializer import org.odk.collect.androidshared.data.getState -import org.odk.collect.androidshared.network.NetworkStateProvider +import org.odk.collect.async.network.NetworkStateProvider import org.odk.collect.settings.SettingsProvider import org.odk.collect.settings.keys.MetaKeys import org.odk.collect.shared.injection.ObjectProviderHost diff --git a/test-forms/src/main/resources/forms/one-question-autosend-disabled.xml b/test-forms/src/main/resources/forms/one-question-autosend-disabled.xml new file mode 100644 index 00000000000..30ef523a4c5 --- /dev/null +++ b/test-forms/src/main/resources/forms/one-question-autosend-disabled.xml @@ -0,0 +1,20 @@ + + + + One Question Autosend Disabled + + + + + + + + + + + + + + + +