From f8996ca167eca5f252d12c496f680237742582d4 Mon Sep 17 00:00:00 2001 From: Callum Stott Date: Thu, 23 Oct 2025 13:24:39 +0100 Subject: [PATCH 01/19] Remove test replaced by FormDownloadActionTest --- .../tasks/DownloadFormListTaskTest.java | 42 ------------------- 1 file changed, 42 deletions(-) delete mode 100644 collect_app/src/androidTest/java/org/odk/collect/android/instrumented/tasks/DownloadFormListTaskTest.java diff --git a/collect_app/src/androidTest/java/org/odk/collect/android/instrumented/tasks/DownloadFormListTaskTest.java b/collect_app/src/androidTest/java/org/odk/collect/android/instrumented/tasks/DownloadFormListTaskTest.java deleted file mode 100644 index 4fcd76e12c9..00000000000 --- a/collect_app/src/androidTest/java/org/odk/collect/android/instrumented/tasks/DownloadFormListTaskTest.java +++ /dev/null @@ -1,42 +0,0 @@ -package org.odk.collect.android.instrumented.tasks; - -import org.junit.Test; -import org.odk.collect.android.formmanagement.ServerFormsDetailsFetcher; -import org.odk.collect.android.tasks.DownloadFormListTask; -import org.odk.collect.android.utilities.WebCredentialsUtils; - -import static org.mockito.ArgumentMatchers.anyString; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; - -public class DownloadFormListTaskTest { - - @Test - public void whenAlternateCredentialsAreSet_shouldServerFormsDetailsFetcherBeUpdated() { - ServerFormsDetailsFetcher serverFormsDetailsFetcher = mock(ServerFormsDetailsFetcher.class); - WebCredentialsUtils webCredentialsUtils = mock(WebCredentialsUtils.class); - - DownloadFormListTask task = new DownloadFormListTask(serverFormsDetailsFetcher); - task.setAlternateCredentials(webCredentialsUtils, "https://test-server.com", "testUser", "testPassword"); - verify(serverFormsDetailsFetcher).updateUrl("https://test-server.com"); - verify(serverFormsDetailsFetcher).updateCredentials(webCredentialsUtils); - } - - @Test - public void whenAlternateCredentialsDoNotContainUrl_shouldNotUrlBeUpdated() { - ServerFormsDetailsFetcher serverFormsDetailsFetcher = mock(ServerFormsDetailsFetcher.class); - WebCredentialsUtils webCredentialsUtils = mock(WebCredentialsUtils.class); - - DownloadFormListTask task = new DownloadFormListTask(serverFormsDetailsFetcher); - - task.setAlternateCredentials(webCredentialsUtils, null, "testUser", "testPassword"); - verify(serverFormsDetailsFetcher, never()).updateUrl(anyString()); - verify(serverFormsDetailsFetcher).updateCredentials(webCredentialsUtils); - - task.setAlternateCredentials(webCredentialsUtils, "", "testUser", "testPassword"); - verify(serverFormsDetailsFetcher, never()).updateUrl(anyString()); - verify(serverFormsDetailsFetcher, times(2)).updateCredentials(webCredentialsUtils); - } -} From bf57e78f03739b1bfc9790871d30eb3ba3466b3e Mon Sep 17 00:00:00 2001 From: Callum Stott Date: Thu, 23 Oct 2025 13:28:05 +0100 Subject: [PATCH 02/19] Remove unneeded open --- .../formmanagement/ServerFormsDetailsFetcher.kt | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/collect_app/src/main/java/org/odk/collect/android/formmanagement/ServerFormsDetailsFetcher.kt b/collect_app/src/main/java/org/odk/collect/android/formmanagement/ServerFormsDetailsFetcher.kt index 1b344614df9..895debf641f 100644 --- a/collect_app/src/main/java/org/odk/collect/android/formmanagement/ServerFormsDetailsFetcher.kt +++ b/collect_app/src/main/java/org/odk/collect/android/formmanagement/ServerFormsDetailsFetcher.kt @@ -27,23 +27,20 @@ import org.odk.collect.openrosa.forms.OpenRosaClient import org.odk.collect.shared.strings.Md5.getMd5Hash import timber.log.Timber -/** - * Open to allow mocking (used in existing Java tests) - */ -open class ServerFormsDetailsFetcher( +class ServerFormsDetailsFetcher( private val formsRepository: FormsRepository, private val formSource: FormSource ) { - open fun updateUrl(url: String) { + fun updateUrl(url: String) { (formSource as OpenRosaClient).updateUrl(url) } - open fun updateCredentials(webCredentialsUtils: WebCredentialsUtils) { + fun updateCredentials(webCredentialsUtils: WebCredentialsUtils) { (formSource as OpenRosaClient).updateWebCredentialsUtils(webCredentialsUtils) } @Throws(FormSourceException::class) - open fun fetchFormDetails(): List { + fun fetchFormDetails(): List { val formList = formSource.fetchFormList() return formList.map { listItem -> val manifestFile = listItem.manifestURL?.let { From b79777217892bb03477c6008a0f690e4ced2bdbf Mon Sep 17 00:00:00 2001 From: Callum Stott Date: Thu, 23 Oct 2025 13:36:22 +0100 Subject: [PATCH 03/19] Move ServerFormDetailsFetcher logic to use case --- .../formmanagement/ServerFormUseCases.kt | 78 ++++++++++++++++++ .../ServerFormsDetailsFetcher.kt | 80 +------------------ 2 files changed, 79 insertions(+), 79 deletions(-) diff --git a/collect_app/src/main/java/org/odk/collect/android/formmanagement/ServerFormUseCases.kt b/collect_app/src/main/java/org/odk/collect/android/formmanagement/ServerFormUseCases.kt index c76a77a3cc6..e02d2261133 100644 --- a/collect_app/src/main/java/org/odk/collect/android/formmanagement/ServerFormUseCases.kt +++ b/collect_app/src/main/java/org/odk/collect/android/formmanagement/ServerFormUseCases.kt @@ -6,6 +6,7 @@ import org.odk.collect.android.formmanagement.download.FormDownloadException import org.odk.collect.android.formmanagement.download.FormDownloader import org.odk.collect.android.instancemanagement.autosend.getLastUpdated import org.odk.collect.android.utilities.FileUtils +import org.odk.collect.android.utilities.FormUtils import org.odk.collect.async.OngoingWorkListener import org.odk.collect.entities.LocalEntityUseCases import org.odk.collect.entities.server.EntitySource @@ -14,13 +15,64 @@ import org.odk.collect.forms.Form import org.odk.collect.forms.FormSource import org.odk.collect.forms.FormSourceException import org.odk.collect.forms.FormsRepository +import org.odk.collect.forms.ManifestFile import org.odk.collect.forms.MediaFile import org.odk.collect.shared.strings.Md5.getMd5Hash +import timber.log.Timber import java.io.File import java.io.IOException object ServerFormUseCases { + fun fetchFormList( + formsRepository: FormsRepository, + formSource: FormSource + ): List { + val formList = formSource.fetchFormList() + return formList.map { listItem -> + val manifestFile = listItem.manifestURL?.let { + getManifestFile(formSource, it) + } + + val forms = formsRepository.getAllNotDeletedByFormId(listItem.formID) + val thisFormAlreadyDownloaded = forms.isNotEmpty() + + val formHash = listItem.hash + val existingForm = if (formHash != null) { + formsRepository.getOneByMd5Hash(formHash) + } else { + null + } + + val isNewerFormVersionAvailable = listItem.hash.let { + if (it == null) { + false + } else if (thisFormAlreadyDownloaded) { + existingForm == null || existingForm.isDeleted + } else { + false + } + } + + val areNewerMediaFilesAvailable = if (existingForm != null && manifestFile != null) { + areNewerMediaFilesAvailable(existingForm, manifestFile.mediaFiles) + } else { + false + } + + ServerFormDetails( + listItem.name, + listItem.downloadURL, + listItem.formID, + listItem.version, + listItem.hash, + !thisFormAlreadyDownloaded, + isNewerFormVersionAvailable || areNewerMediaFilesAvailable, + manifestFile + ) + } + } + fun downloadForms( forms: List, formDownloader: FormDownloader, @@ -233,6 +285,32 @@ object ServerFormUseCases { null } } + + private fun getManifestFile(formSource: FormSource, manifestUrl: String): ManifestFile? { + return try { + formSource.fetchManifest(manifestUrl) + } catch (formSourceException: FormSourceException) { + Timber.w(formSourceException) + null + } + } + + private fun areNewerMediaFilesAvailable( + existingForm: Form, + newMediaFiles: List + ): Boolean { + if (newMediaFiles.isEmpty()) { + return false + } + + val localMediaHashes = FormUtils.getMediaFiles(existingForm) + .map { it.getMd5Hash() } + .toSet() + + return newMediaFiles.any { + !it.filename.endsWith(".zip") && it.hash !in localMediaHashes + } + } } class EntityListUpdateException(cause: Throwable) : Exception(cause) diff --git a/collect_app/src/main/java/org/odk/collect/android/formmanagement/ServerFormsDetailsFetcher.kt b/collect_app/src/main/java/org/odk/collect/android/formmanagement/ServerFormsDetailsFetcher.kt index 895debf641f..f4138b21fe0 100644 --- a/collect_app/src/main/java/org/odk/collect/android/formmanagement/ServerFormsDetailsFetcher.kt +++ b/collect_app/src/main/java/org/odk/collect/android/formmanagement/ServerFormsDetailsFetcher.kt @@ -15,17 +15,11 @@ */ package org.odk.collect.android.formmanagement -import org.odk.collect.android.utilities.FormUtils import org.odk.collect.android.utilities.WebCredentialsUtils -import org.odk.collect.forms.Form import org.odk.collect.forms.FormSource import org.odk.collect.forms.FormSourceException import org.odk.collect.forms.FormsRepository -import org.odk.collect.forms.ManifestFile -import org.odk.collect.forms.MediaFile import org.odk.collect.openrosa.forms.OpenRosaClient -import org.odk.collect.shared.strings.Md5.getMd5Hash -import timber.log.Timber class ServerFormsDetailsFetcher( private val formsRepository: FormsRepository, @@ -41,78 +35,6 @@ class ServerFormsDetailsFetcher( @Throws(FormSourceException::class) fun fetchFormDetails(): List { - val formList = formSource.fetchFormList() - return formList.map { listItem -> - val manifestFile = listItem.manifestURL?.let { - getManifestFile(formSource, it) - } - - val forms = formsRepository.getAllNotDeletedByFormId(listItem.formID) - val thisFormAlreadyDownloaded = forms.isNotEmpty() - - val formHash = listItem.hash - val existingForm = if (formHash != null) { - getFormByHash(formHash) - } else { - null - } - - val isNewerFormVersionAvailable = listItem.hash.let { - if (it == null) { - false - } else if (thisFormAlreadyDownloaded) { - existingForm == null || existingForm.isDeleted - } else { - false - } - } - - val areNewerMediaFilesAvailable = if (existingForm != null && manifestFile != null) { - areNewerMediaFilesAvailable(existingForm, manifestFile.mediaFiles) - } else { - false - } - - ServerFormDetails( - listItem.name, - listItem.downloadURL, - listItem.formID, - listItem.version, - listItem.hash, - !thisFormAlreadyDownloaded, - isNewerFormVersionAvailable || areNewerMediaFilesAvailable, - manifestFile - ) - } - } - - private fun getManifestFile(formSource: FormSource, manifestUrl: String): ManifestFile? { - return try { - formSource.fetchManifest(manifestUrl) - } catch (formSourceException: FormSourceException) { - Timber.w(formSourceException) - null - } - } - - private fun areNewerMediaFilesAvailable( - existingForm: Form, - newMediaFiles: List - ): Boolean { - if (newMediaFiles.isEmpty()) { - return false - } - - val localMediaHashes = FormUtils.getMediaFiles(existingForm) - .map { it.getMd5Hash() } - .toSet() - - return newMediaFiles.any { - !it.filename.endsWith(".zip") && it.hash !in localMediaHashes - } - } - - private fun getFormByHash(hash: String): Form? { - return formsRepository.getOneByMd5Hash(hash) + return ServerFormUseCases.fetchFormList(formsRepository, formSource) } } From 36560a9143c5f61a23530c6bcc39faa8e0c9c351 Mon Sep 17 00:00:00 2001 From: Callum Stott Date: Thu, 23 Oct 2025 13:50:02 +0100 Subject: [PATCH 04/19] Isolate authentication overriding to where its used --- .../activities/FormDownloadListActivity.java | 8 ++--- .../formmanagement/ServerFormUseCases.kt | 2 ++ .../ServerFormsDetailsFetcher.kt | 10 ------ .../injection/config/AppDependencyModule.java | 10 ------ .../android/tasks/DownloadFormListTask.java | 31 ++++++++++++++----- 5 files changed, 27 insertions(+), 34 deletions(-) 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 87cc84eaa2d..3a183c23942 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 @@ -42,7 +42,6 @@ import org.odk.collect.android.formmanagement.FormSourceExceptionMapper; import org.odk.collect.android.formmanagement.FormsDataService; import org.odk.collect.android.formmanagement.ServerFormDetails; -import org.odk.collect.android.formmanagement.ServerFormsDetailsFetcher; import org.odk.collect.android.formmanagement.download.FormDownloadException; import org.odk.collect.android.fragments.dialogs.FormsDownloadResultDialog; import org.odk.collect.android.injection.DaggerUtils; @@ -121,9 +120,6 @@ public class FormDownloadListActivity extends FormListActivity implements FormLi @Inject WebCredentialsUtils webCredentialsUtils; - @Inject - ServerFormsDetailsFetcher serverFormsDetailsFetcher; - @Inject NetworkStateProvider connectivityProvider; @@ -294,12 +290,12 @@ private void downloadFormList() { if (viewModel.isDownloadOnlyMode()) { // Handle external app download case with different server - downloadFormListTask = new DownloadFormListTask(serverFormsDetailsFetcher); + downloadFormListTask = new DownloadFormListTask(); downloadFormListTask.setAlternateCredentials(webCredentialsUtils, viewModel.getUrl(), viewModel.getUsername(), viewModel.getPassword()); downloadFormListTask.setDownloaderListener(this); downloadFormListTask.execute(); } else { - downloadFormListTask = new DownloadFormListTask(serverFormsDetailsFetcher); + downloadFormListTask = new DownloadFormListTask(); downloadFormListTask.setDownloaderListener(this); downloadFormListTask.execute(); } diff --git a/collect_app/src/main/java/org/odk/collect/android/formmanagement/ServerFormUseCases.kt b/collect_app/src/main/java/org/odk/collect/android/formmanagement/ServerFormUseCases.kt index e02d2261133..7134c56e852 100644 --- a/collect_app/src/main/java/org/odk/collect/android/formmanagement/ServerFormUseCases.kt +++ b/collect_app/src/main/java/org/odk/collect/android/formmanagement/ServerFormUseCases.kt @@ -24,6 +24,8 @@ import java.io.IOException object ServerFormUseCases { + @JvmStatic + @Throws(FormSourceException::class) fun fetchFormList( formsRepository: FormsRepository, formSource: FormSource diff --git a/collect_app/src/main/java/org/odk/collect/android/formmanagement/ServerFormsDetailsFetcher.kt b/collect_app/src/main/java/org/odk/collect/android/formmanagement/ServerFormsDetailsFetcher.kt index f4138b21fe0..254fdfc61f9 100644 --- a/collect_app/src/main/java/org/odk/collect/android/formmanagement/ServerFormsDetailsFetcher.kt +++ b/collect_app/src/main/java/org/odk/collect/android/formmanagement/ServerFormsDetailsFetcher.kt @@ -15,24 +15,14 @@ */ package org.odk.collect.android.formmanagement -import org.odk.collect.android.utilities.WebCredentialsUtils import org.odk.collect.forms.FormSource import org.odk.collect.forms.FormSourceException import org.odk.collect.forms.FormsRepository -import org.odk.collect.openrosa.forms.OpenRosaClient class ServerFormsDetailsFetcher( private val formsRepository: FormsRepository, private val formSource: FormSource ) { - fun updateUrl(url: String) { - (formSource as OpenRosaClient).updateUrl(url) - } - - fun updateCredentials(webCredentialsUtils: WebCredentialsUtils) { - (formSource as OpenRosaClient).updateWebCredentialsUtils(webCredentialsUtils) - } - @Throws(FormSourceException::class) fun fetchFormDetails(): List { return ServerFormUseCases.fetchFormList(formsRepository, formSource) 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 02cac5f42c8..d3fe8c4a33f 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 @@ -51,7 +51,6 @@ import org.odk.collect.android.formmanagement.CollectFormEntryControllerFactory; import org.odk.collect.android.formmanagement.FormsDataService; import org.odk.collect.android.formmanagement.OpenRosaClientProvider; -import org.odk.collect.android.formmanagement.ServerFormsDetailsFetcher; import org.odk.collect.android.geo.MapConfiguratorProvider; import org.odk.collect.android.geo.MapFragmentFactoryImpl; import org.odk.collect.android.instancemanagement.InstancesDataService; @@ -104,7 +103,6 @@ import org.odk.collect.audiorecorder.recording.AudioRecorder; import org.odk.collect.audiorecorder.recording.AudioRecorderFactory; import org.odk.collect.entities.storage.EntitiesRepository; -import org.odk.collect.forms.FormsRepository; import org.odk.collect.imageloader.GlideImageLoader; import org.odk.collect.imageloader.ImageLoader; import org.odk.collect.location.GoogleFusedLocationClient; @@ -125,7 +123,6 @@ import org.odk.collect.permissions.ContextCompatPermissionChecker; import org.odk.collect.permissions.PermissionsChecker; import org.odk.collect.permissions.PermissionsProvider; -import org.odk.collect.projects.Project; import org.odk.collect.projects.ProjectCreator; import org.odk.collect.projects.ProjectsRepository; import org.odk.collect.projects.SettingsConnectionMatcher; @@ -331,13 +328,6 @@ public QRCodeDecoder providesQRCodeDecoder() { return new QRCodeDecoderImpl(); } - @Provides - public ServerFormsDetailsFetcher providesServerFormDetailsFetcher(FormsRepositoryProvider formsRepositoryProvider, OpenRosaClientProvider formSourceProvider, ProjectsDataService projectsDataService) { - Project.Saved currentProject = projectsDataService.requireCurrentProject(); - FormsRepository formsRepository = formsRepositoryProvider.create(currentProject.getUuid()); - return new ServerFormsDetailsFetcher(formsRepository, formSourceProvider.create(currentProject.getUuid())); - } - @Provides public Notifier providesNotifier(Application application, SettingsProvider settingsProvider, ProjectsRepository projectsRepository, UniqueIdGenerator uniqueIdGenerator) { return new NotificationManagerNotifier(application, settingsProvider, projectsRepository, uniqueIdGenerator); diff --git a/collect_app/src/main/java/org/odk/collect/android/tasks/DownloadFormListTask.java b/collect_app/src/main/java/org/odk/collect/android/tasks/DownloadFormListTask.java index 608ea44d891..0d9a2286afe 100644 --- a/collect_app/src/main/java/org/odk/collect/android/tasks/DownloadFormListTask.java +++ b/collect_app/src/main/java/org/odk/collect/android/tasks/DownloadFormListTask.java @@ -22,12 +22,17 @@ import org.odk.collect.android.application.Collect; import org.odk.collect.android.formmanagement.FormsDataService; import org.odk.collect.android.formmanagement.ServerFormDetails; -import org.odk.collect.android.formmanagement.ServerFormsDetailsFetcher; +import org.odk.collect.android.formmanagement.ServerFormUseCases; import org.odk.collect.android.injection.DaggerUtils; +import org.odk.collect.android.injection.config.ProjectDependencyModuleFactory; import org.odk.collect.android.listeners.FormListDownloaderListener; +import org.odk.collect.android.projects.ProjectDependencyModule; import org.odk.collect.android.projects.ProjectsDataService; import org.odk.collect.android.utilities.WebCredentialsUtils; +import org.odk.collect.forms.FormSource; import org.odk.collect.forms.FormSourceException; +import org.odk.collect.forms.FormsRepository; +import org.odk.collect.openrosa.forms.OpenRosaClient; import java.util.HashMap; import java.util.List; @@ -47,7 +52,9 @@ @Deprecated public class DownloadFormListTask extends AsyncTask, FormSourceException>> { - private final ServerFormsDetailsFetcher serverFormsDetailsFetcher; + private final FormsRepository formsRepository; + private final FormSource formSource; + private final String projectId; private FormListDownloaderListener stateListener; private WebCredentialsUtils webCredentialsUtils; @@ -61,14 +68,21 @@ public class DownloadFormListTask extends AsyncTask, FormSourceException> doInBackground(Void... values) { - formsDataService.refresh(projectsDataService.requireCurrentProject().getUuid()); + formsDataService.refresh(projectId); if (webCredentialsUtils != null) { setTemporaryCredentials(); @@ -78,7 +92,7 @@ protected Pair, FormSourceException> doInBackground(Void FormSourceException exception = null; try { - formList = serverFormsDetailsFetcher.fetchFormDetails(); + formList = ServerFormUseCases.fetchFormList(formsRepository, formSource); } catch (FormSourceException e) { exception = e; } finally { @@ -116,11 +130,12 @@ public void setDownloaderListener(FormListDownloaderListener sl) { public void setAlternateCredentials(WebCredentialsUtils webCredentialsUtils, String url, String username, String password) { this.webCredentialsUtils = webCredentialsUtils; - serverFormsDetailsFetcher.updateCredentials(webCredentialsUtils); + OpenRosaClient openRosaClient = (OpenRosaClient) formSource; + openRosaClient.updateWebCredentialsUtils(webCredentialsUtils); this.url = url; if (url != null && !url.isEmpty()) { - serverFormsDetailsFetcher.updateUrl(url); + openRosaClient.updateUrl(url); } this.username = username; From 6ab2581725f9fcc8729c0b8e1368bf6df2c00a71 Mon Sep 17 00:00:00 2001 From: Callum Stott Date: Thu, 23 Oct 2025 19:15:37 +0100 Subject: [PATCH 05/19] Convert ServerFormDetailsFetcher to interface --- .../formmanagement/FormsDataService.kt | 36 +++++++++---------- .../formmanagement/ServerFormUseCases.kt | 2 +- .../ServerFormsDetailsFetcher.kt | 9 ++--- .../matchexactly/ServerFormsSynchronizer.java | 11 +++--- .../android/tasks/DownloadFormListTask.java | 2 +- .../ServerFormsDetailsFetcherTest.kt | 29 +++++++-------- .../ServerFormsSynchronizerTest.java | 18 +++++----- 7 files changed, 51 insertions(+), 56 deletions(-) diff --git a/collect_app/src/main/java/org/odk/collect/android/formmanagement/FormsDataService.kt b/collect_app/src/main/java/org/odk/collect/android/formmanagement/FormsDataService.kt index 2582613e25b..21535c8cb7e 100644 --- a/collect_app/src/main/java/org/odk/collect/android/formmanagement/FormsDataService.kt +++ b/collect_app/src/main/java/org/odk/collect/android/formmanagement/FormsDataService.kt @@ -14,12 +14,13 @@ import org.odk.collect.android.state.DataKeys import org.odk.collect.androidshared.data.AppState import org.odk.collect.androidshared.data.DataService import org.odk.collect.forms.Form +import org.odk.collect.forms.FormSource import org.odk.collect.forms.FormSourceException +import org.odk.collect.forms.FormsRepository import org.odk.collect.projects.ProjectDependencyFactory import org.odk.collect.settings.keys.ProjectKeys import java.io.File import java.util.function.Supplier -import java.util.stream.Collectors class FormsDataService( appState: AppState, @@ -111,15 +112,14 @@ class FormsDataService( if (acquiredLock) { syncWithStorage(projectId) - val serverFormsDetailsFetcher = serverFormsDetailsFetcher(projectDependencies) val formDownloader = formDownloader(projectDependencies, clock) try { - val serverForms: List = - serverFormsDetailsFetcher.fetchFormDetails() - val updatedForms = - serverForms.stream().filter { obj: ServerFormDetails -> obj.isUpdated } - .collect(Collectors.toList()) + val serverForms = ServerFormUseCases.fetchFormDetails( + projectDependencies.formsRepository, + projectDependencies.formSource + ) + val updatedForms = serverForms.filter { it.isUpdated } if (updatedForms.isNotEmpty()) { if (projectDependencies.generalSettings.getBoolean(ProjectKeys.KEY_AUTOMATIC_UPDATE)) { val results = ServerFormUseCases.downloadForms( @@ -157,14 +157,21 @@ class FormsDataService( startSync(projectId) syncWithStorage(projectId) - val serverFormsDetailsFetcher = serverFormsDetailsFetcher(projectDependencies) val formDownloader = formDownloader(projectDependencies, clock) val serverFormsSynchronizer = ServerFormsSynchronizer( - serverFormsDetailsFetcher, + object : ServerFormsDetailsFetcher { + override fun fetchFormDetails( + repository: FormsRepository, + source: FormSource + ): List { + return ServerFormUseCases.fetchFormDetails(repository, source) + } + }, projectDependencies.formsRepository, projectDependencies.instancesRepository, - formDownloader + formDownloader, + projectDependencies.formSource ) val exception = try { @@ -243,12 +250,3 @@ private fun formDownloader( projectDependencyModule.entitySource ) } - -private fun serverFormsDetailsFetcher( - projectDependencyModule: ProjectDependencyModule -): ServerFormsDetailsFetcher { - return ServerFormsDetailsFetcher( - projectDependencyModule.formsRepository, - projectDependencyModule.formSource - ) -} diff --git a/collect_app/src/main/java/org/odk/collect/android/formmanagement/ServerFormUseCases.kt b/collect_app/src/main/java/org/odk/collect/android/formmanagement/ServerFormUseCases.kt index 7134c56e852..5fd93b7b3e0 100644 --- a/collect_app/src/main/java/org/odk/collect/android/formmanagement/ServerFormUseCases.kt +++ b/collect_app/src/main/java/org/odk/collect/android/formmanagement/ServerFormUseCases.kt @@ -26,7 +26,7 @@ object ServerFormUseCases { @JvmStatic @Throws(FormSourceException::class) - fun fetchFormList( + fun fetchFormDetails( formsRepository: FormsRepository, formSource: FormSource ): List { diff --git a/collect_app/src/main/java/org/odk/collect/android/formmanagement/ServerFormsDetailsFetcher.kt b/collect_app/src/main/java/org/odk/collect/android/formmanagement/ServerFormsDetailsFetcher.kt index 254fdfc61f9..8569d22ff21 100644 --- a/collect_app/src/main/java/org/odk/collect/android/formmanagement/ServerFormsDetailsFetcher.kt +++ b/collect_app/src/main/java/org/odk/collect/android/formmanagement/ServerFormsDetailsFetcher.kt @@ -19,12 +19,7 @@ import org.odk.collect.forms.FormSource import org.odk.collect.forms.FormSourceException import org.odk.collect.forms.FormsRepository -class ServerFormsDetailsFetcher( - private val formsRepository: FormsRepository, - private val formSource: FormSource -) { +interface ServerFormsDetailsFetcher { @Throws(FormSourceException::class) - fun fetchFormDetails(): List { - return ServerFormUseCases.fetchFormList(formsRepository, formSource) - } + fun fetchFormDetails(repository: FormsRepository, source: FormSource): List } diff --git a/collect_app/src/main/java/org/odk/collect/android/formmanagement/matchexactly/ServerFormsSynchronizer.java b/collect_app/src/main/java/org/odk/collect/android/formmanagement/matchexactly/ServerFormsSynchronizer.java index 7edc3f36ce9..23cad41234c 100644 --- a/collect_app/src/main/java/org/odk/collect/android/formmanagement/matchexactly/ServerFormsSynchronizer.java +++ b/collect_app/src/main/java/org/odk/collect/android/formmanagement/matchexactly/ServerFormsSynchronizer.java @@ -1,11 +1,12 @@ package org.odk.collect.android.formmanagement.matchexactly; import org.odk.collect.android.formmanagement.LocalFormUseCases; -import org.odk.collect.android.formmanagement.download.FormDownloadException; -import org.odk.collect.android.formmanagement.download.FormDownloader; import org.odk.collect.android.formmanagement.ServerFormDetails; import org.odk.collect.android.formmanagement.ServerFormsDetailsFetcher; +import org.odk.collect.android.formmanagement.download.FormDownloadException; +import org.odk.collect.android.formmanagement.download.FormDownloader; import org.odk.collect.forms.Form; +import org.odk.collect.forms.FormSource; import org.odk.collect.forms.FormSourceException; import org.odk.collect.forms.FormsRepository; import org.odk.collect.forms.instances.InstancesRepository; @@ -18,16 +19,18 @@ public class ServerFormsSynchronizer { private final InstancesRepository instancesRepository; private final FormDownloader formDownloader; private final ServerFormsDetailsFetcher serverFormsDetailsFetcher; + private final FormSource formSource; - public ServerFormsSynchronizer(ServerFormsDetailsFetcher serverFormsDetailsFetcher, FormsRepository formsRepository, InstancesRepository instancesRepository, FormDownloader formDownloader) { + public ServerFormsSynchronizer(ServerFormsDetailsFetcher serverFormsDetailsFetcher, FormsRepository formsRepository, InstancesRepository instancesRepository, FormDownloader formDownloader, FormSource formSource) { this.serverFormsDetailsFetcher = serverFormsDetailsFetcher; this.formsRepository = formsRepository; this.instancesRepository = instancesRepository; this.formDownloader = formDownloader; + this.formSource = formSource; } public void synchronize() throws FormSourceException { - List formList = serverFormsDetailsFetcher.fetchFormDetails(); + List formList = serverFormsDetailsFetcher.fetchFormDetails(formsRepository, formSource); List
formsOnDevice = formsRepository.getAll(); formsOnDevice.stream().forEach(form -> { if (formList.stream().noneMatch(f -> form.getFormId().equals(f.getFormId()))) { diff --git a/collect_app/src/main/java/org/odk/collect/android/tasks/DownloadFormListTask.java b/collect_app/src/main/java/org/odk/collect/android/tasks/DownloadFormListTask.java index 0d9a2286afe..662848cb0da 100644 --- a/collect_app/src/main/java/org/odk/collect/android/tasks/DownloadFormListTask.java +++ b/collect_app/src/main/java/org/odk/collect/android/tasks/DownloadFormListTask.java @@ -92,7 +92,7 @@ protected Pair, FormSourceException> doInBackground(Void FormSourceException exception = null; try { - formList = ServerFormUseCases.fetchFormList(formsRepository, formSource); + formList = ServerFormUseCases.fetchFormDetails(formsRepository, formSource); } catch (FormSourceException e) { exception = e; } finally { diff --git a/collect_app/src/test/java/org/odk/collect/android/formmanagement/ServerFormsDetailsFetcherTest.kt b/collect_app/src/test/java/org/odk/collect/android/formmanagement/ServerFormsDetailsFetcherTest.kt index 1700b4ab8ae..de368c1f772 100644 --- a/collect_app/src/test/java/org/odk/collect/android/formmanagement/ServerFormsDetailsFetcherTest.kt +++ b/collect_app/src/test/java/org/odk/collect/android/formmanagement/ServerFormsDetailsFetcherTest.kt @@ -35,16 +35,13 @@ class ServerFormsDetailsFetcherTest { ) } - private val fetcher = - ServerFormsDetailsFetcher(formsRepository, formSource) - @Test fun whenFormHasManifestUrl_returnsMediaFilesInDetails() { whenever(formSource.fetchFormList()).thenReturn( listOf(FORM_WITHOUT_MANIFEST, FORM_WITH_MANIFEST) ) - val serverFormDetails = fetcher.fetchFormDetails() + val serverFormDetails = ServerFormUseCases.fetchFormDetails(formsRepository, formSource) assertThat(getFormFromList(serverFormDetails, "form-1").manifest, nullValue()) assertThat( getFormFromList(serverFormDetails, "form-2").manifest!!.mediaFiles, @@ -56,7 +53,7 @@ class ServerFormsDetailsFetcherTest { fun whenFormDoesNotExist_isNotOnDevice() { whenever(formSource.fetchFormList()).thenReturn(listOf(FORM_WITHOUT_MANIFEST)) - val serverFormDetails = fetcher.fetchFormDetails() + val serverFormDetails = ServerFormUseCases.fetchFormDetails(formsRepository, formSource) assertThat(getFormFromList(serverFormDetails, "form-1").isNotOnDevice, `is`(true)) } @@ -73,7 +70,7 @@ class ServerFormsDetailsFetcherTest { .build() ) - val serverFormDetails = fetcher.fetchFormDetails() + val serverFormDetails = ServerFormUseCases.fetchFormDetails(formsRepository, formSource) assertThat(getFormFromList(serverFormDetails, "form-1").isNotOnDevice, `is`(true)) } @@ -88,7 +85,7 @@ class ServerFormsDetailsFetcherTest { .build() ) - val serverFormDetails = fetcher.fetchFormDetails() + val serverFormDetails = ServerFormUseCases.fetchFormDetails(formsRepository, formSource) assertThat(getFormFromList(serverFormDetails, "form-1").isUpdated, `is`(true)) } @@ -105,7 +102,7 @@ class ServerFormsDetailsFetcherTest { .build() ) - val serverFormDetails = fetcher.fetchFormDetails() + val serverFormDetails = ServerFormUseCases.fetchFormDetails(formsRepository, formSource) assertThat(getFormFromList(serverFormDetails, "form-2").isUpdated, `is`(true)) } @@ -126,7 +123,7 @@ class ServerFormsDetailsFetcherTest { val oldMediaFile = TempFiles.createTempFile(mediaDir, "blah", ".csv") writeToFile(oldMediaFile, "blah before") - val serverFormDetails = fetcher.fetchFormDetails() + val serverFormDetails = ServerFormUseCases.fetchFormDetails(formsRepository, formSource) assertThat(getFormFromList(serverFormDetails, "form-2").isUpdated, `is`(true)) } @@ -160,7 +157,7 @@ class ServerFormsDetailsFetcherTest { val mediaFile2 = TempFiles.createTempFile(mediaDir2, MEDIA_FILE.filename) writeToFile(mediaFile2, FILE_CONTENT) - val serverFormDetails = fetcher.fetchFormDetails() + val serverFormDetails = ServerFormUseCases.fetchFormDetails(formsRepository, formSource) val form = getFormFromList(serverFormDetails, "form-2") assertThat(form.isUpdated, `is`(false)) assertThat(form.isNotOnDevice, `is`(false)) @@ -188,7 +185,7 @@ class ServerFormsDetailsFetcherTest { .build() ) - val serverFormDetails = fetcher.fetchFormDetails() + val serverFormDetails = ServerFormUseCases.fetchFormDetails(formsRepository, formSource) val form = getFormFromList(serverFormDetails, "form-1") assertThat(form.isUpdated, `is`(true)) assertThat(form.isNotOnDevice, `is`(false)) @@ -225,7 +222,7 @@ class ServerFormsDetailsFetcherTest { val mediaFile2 = TempFiles.createTempFile(mediaDir2, MEDIA_FILE.filename) writeToFile(mediaFile2, FILE_CONTENT) - val serverFormDetails = fetcher.fetchFormDetails() + val serverFormDetails = ServerFormUseCases.fetchFormDetails(formsRepository, formSource) val form = getFormFromList(serverFormDetails, "form-2") assertThat(form.isUpdated, `is`(true)) assertThat(form.isNotOnDevice, `is`(false)) @@ -244,7 +241,7 @@ class ServerFormsDetailsFetcherTest { .build() ) - val serverFormDetails = fetcher.fetchFormDetails() + val serverFormDetails = ServerFormUseCases.fetchFormDetails(formsRepository, formSource) val form = getFormFromList(serverFormDetails, "form-1") assertThat(form.isUpdated, `is`(false)) assertThat(form.isNotOnDevice, `is`(false)) @@ -267,7 +264,7 @@ class ServerFormsDetailsFetcherTest { val mediaFile = TempFiles.createTempFile(mediaDir, "blah", ".csv") writeToFile(mediaFile, FILE_CONTENT) - val serverFormDetails = fetcher.fetchFormDetails() + val serverFormDetails = ServerFormUseCases.fetchFormDetails(formsRepository, formSource) val form = getFormFromList(serverFormDetails, "form-2") assertThat(form.isNotOnDevice, `is`(false)) assertThat(form.isUpdated, `is`(false)) @@ -289,7 +286,7 @@ class ServerFormsDetailsFetcherTest { val localMediaFile = TempFiles.createTempFile(mediaDir, "blah", ".csv") writeToFile(localMediaFile, FILE_CONTENT) - val serverFormDetails = fetcher.fetchFormDetails() + val serverFormDetails = ServerFormUseCases.fetchFormDetails(formsRepository, formSource) assertThat(getFormFromList(serverFormDetails, "form-2").isUpdated, `is`(true)) } @@ -308,7 +305,7 @@ class ServerFormsDetailsFetcherTest { .build() ) - val serverFormDetails = fetcher.fetchFormDetails() + val serverFormDetails = ServerFormUseCases.fetchFormDetails(formsRepository, formSource) val form = getFormFromList(serverFormDetails, "form-2") assertThat(form.isUpdated, `is`(false)) assertThat(form.isNotOnDevice, `is`(false)) diff --git a/collect_app/src/test/java/org/odk/collect/android/formmanagement/ServerFormsSynchronizerTest.java b/collect_app/src/test/java/org/odk/collect/android/formmanagement/ServerFormsSynchronizerTest.java index 064b1411732..8c379b54d45 100644 --- a/collect_app/src/test/java/org/odk/collect/android/formmanagement/ServerFormsSynchronizerTest.java +++ b/collect_app/src/test/java/org/odk/collect/android/formmanagement/ServerFormsSynchronizerTest.java @@ -5,6 +5,7 @@ import org.odk.collect.android.formmanagement.download.FormDownloader; import org.odk.collect.android.formmanagement.matchexactly.ServerFormsSynchronizer; import org.odk.collect.forms.Form; +import org.odk.collect.forms.FormSource; import org.odk.collect.forms.FormSourceException; import org.odk.collect.forms.FormsRepository; import org.odk.collect.forms.instances.InstancesRepository; @@ -32,11 +33,12 @@ public class ServerFormsSynchronizerTest { private final InstancesRepository instancesRepository = new InMemInstancesRepository(); private final RecordingFormDownloader formDownloader = new RecordingFormDownloader(); private final ServerFormsDetailsFetcher serverFormDetailsFetcher = mock(ServerFormsDetailsFetcher.class); - private final ServerFormsSynchronizer synchronizer = new ServerFormsSynchronizer(serverFormDetailsFetcher, formsRepository, instancesRepository, formDownloader); + private final FormSource formSource = mock(FormSource.class); + private final ServerFormsSynchronizer synchronizer = new ServerFormsSynchronizer(serverFormDetailsFetcher, formsRepository, instancesRepository, formDownloader, formSource); @Test public void downloadsNewForms() throws Exception { - when(serverFormDetailsFetcher.fetchFormDetails()).thenReturn(asList( + when(serverFormDetailsFetcher.fetchFormDetails(formsRepository, formSource)).thenReturn(asList( new ServerFormDetails("form-1", "http://example.com/form-1", "form-1", "server", "form-1-hash", true, false, null) )); @@ -46,7 +48,7 @@ public void downloadsNewForms() throws Exception { @Test public void downloadsUpdatedForms() throws Exception { - when(serverFormDetailsFetcher.fetchFormDetails()).thenReturn(asList( + when(serverFormDetailsFetcher.fetchFormDetails(formsRepository, formSource)).thenReturn(asList( new ServerFormDetails("form-1", "http://example.com/form-1", "form-1", "server", "form-1-hash", false, true, null) )); @@ -63,7 +65,7 @@ public void deletesFormsNotInList() throws Exception { .formFilePath(FormUtils.createXFormFile("1", "1").getAbsolutePath()) .build()); - when(serverFormDetailsFetcher.fetchFormDetails()).thenReturn(asList( + when(serverFormDetailsFetcher.fetchFormDetails(formsRepository, formSource)).thenReturn(asList( new ServerFormDetails("form-1", "http://example.com/form-1", "form-1", "server", "form-1-hash", false, false, null) )); @@ -73,7 +75,7 @@ public void deletesFormsNotInList() throws Exception { @Test public void doesNotDownloadExistingForms() throws Exception { - when(serverFormDetailsFetcher.fetchFormDetails()).thenReturn(asList( + when(serverFormDetailsFetcher.fetchFormDetails(formsRepository, formSource)).thenReturn(asList( new ServerFormDetails("form-1", "http://example.com/form-1", "form-1", "server", "form-1-hash", false, false, null) )); @@ -84,7 +86,7 @@ public void doesNotDownloadExistingForms() throws Exception { @Test public void whenFetchingFormDetailsThrowsAnError_throwsError() throws Exception { FormSourceException exception = new FormSourceException.AuthRequired(); - when(serverFormDetailsFetcher.fetchFormDetails()).thenThrow(exception); + when(serverFormDetailsFetcher.fetchFormDetails(formsRepository, formSource)).thenThrow(exception); try { synchronizer.synchronize(); @@ -100,12 +102,12 @@ public void whenDownloadingFormThrowsAnError_throwsErrorAndDownloadsOtherForms() new ServerFormDetails("form-2", "http://example.com/form-2", "form-2", "server", "form-2-hash", true, false, null) ); - when(serverFormDetailsFetcher.fetchFormDetails()).thenReturn(serverForms); + when(serverFormDetailsFetcher.fetchFormDetails(formsRepository, formSource)).thenReturn(serverForms); FormDownloader formDownloader = mock(FormDownloader.class); doThrow(new FormDownloadException.FormSourceError(new FormSourceException.FetchError())).when(formDownloader).downloadForm(serverForms.get(0), null, null); - ServerFormsSynchronizer synchronizer = new ServerFormsSynchronizer(serverFormDetailsFetcher, formsRepository, instancesRepository, formDownloader); + ServerFormsSynchronizer synchronizer = new ServerFormsSynchronizer(serverFormDetailsFetcher, formsRepository, instancesRepository, formDownloader, formSource); try { synchronizer.synchronize(); From aaf058bc460b59fdaf26600a2eb28d2ee84961e4 Mon Sep 17 00:00:00 2001 From: Callum Stott Date: Fri, 24 Oct 2025 10:31:45 +0100 Subject: [PATCH 06/19] Only use interface for Java code --- .../formmanagement/FormsDataService.kt | 11 +------- .../ServerFormsDetailsFetcher.kt | 25 ------------------- .../matchexactly/ServerFormsSynchronizer.java | 5 +++- .../ServerFormsSynchronizerTest.java | 22 ++++++++-------- 4 files changed, 16 insertions(+), 47 deletions(-) delete mode 100644 collect_app/src/main/java/org/odk/collect/android/formmanagement/ServerFormsDetailsFetcher.kt diff --git a/collect_app/src/main/java/org/odk/collect/android/formmanagement/FormsDataService.kt b/collect_app/src/main/java/org/odk/collect/android/formmanagement/FormsDataService.kt index 21535c8cb7e..e2851e18087 100644 --- a/collect_app/src/main/java/org/odk/collect/android/formmanagement/FormsDataService.kt +++ b/collect_app/src/main/java/org/odk/collect/android/formmanagement/FormsDataService.kt @@ -14,9 +14,7 @@ import org.odk.collect.android.state.DataKeys import org.odk.collect.androidshared.data.AppState import org.odk.collect.androidshared.data.DataService import org.odk.collect.forms.Form -import org.odk.collect.forms.FormSource import org.odk.collect.forms.FormSourceException -import org.odk.collect.forms.FormsRepository import org.odk.collect.projects.ProjectDependencyFactory import org.odk.collect.settings.keys.ProjectKeys import java.io.File @@ -160,14 +158,7 @@ class FormsDataService( val formDownloader = formDownloader(projectDependencies, clock) val serverFormsSynchronizer = ServerFormsSynchronizer( - object : ServerFormsDetailsFetcher { - override fun fetchFormDetails( - repository: FormsRepository, - source: FormSource - ): List { - return ServerFormUseCases.fetchFormDetails(repository, source) - } - }, + { repository, source -> ServerFormUseCases.fetchFormDetails(repository, source) }, projectDependencies.formsRepository, projectDependencies.instancesRepository, formDownloader, diff --git a/collect_app/src/main/java/org/odk/collect/android/formmanagement/ServerFormsDetailsFetcher.kt b/collect_app/src/main/java/org/odk/collect/android/formmanagement/ServerFormsDetailsFetcher.kt deleted file mode 100644 index 8569d22ff21..00000000000 --- a/collect_app/src/main/java/org/odk/collect/android/formmanagement/ServerFormsDetailsFetcher.kt +++ /dev/null @@ -1,25 +0,0 @@ -/* - * Copyright 2018 Nafundi - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.odk.collect.android.formmanagement - -import org.odk.collect.forms.FormSource -import org.odk.collect.forms.FormSourceException -import org.odk.collect.forms.FormsRepository - -interface ServerFormsDetailsFetcher { - @Throws(FormSourceException::class) - fun fetchFormDetails(repository: FormsRepository, source: FormSource): List -} diff --git a/collect_app/src/main/java/org/odk/collect/android/formmanagement/matchexactly/ServerFormsSynchronizer.java b/collect_app/src/main/java/org/odk/collect/android/formmanagement/matchexactly/ServerFormsSynchronizer.java index 23cad41234c..9c08001210c 100644 --- a/collect_app/src/main/java/org/odk/collect/android/formmanagement/matchexactly/ServerFormsSynchronizer.java +++ b/collect_app/src/main/java/org/odk/collect/android/formmanagement/matchexactly/ServerFormsSynchronizer.java @@ -2,7 +2,6 @@ import org.odk.collect.android.formmanagement.LocalFormUseCases; import org.odk.collect.android.formmanagement.ServerFormDetails; -import org.odk.collect.android.formmanagement.ServerFormsDetailsFetcher; import org.odk.collect.android.formmanagement.download.FormDownloadException; import org.odk.collect.android.formmanagement.download.FormDownloader; import org.odk.collect.forms.Form; @@ -56,4 +55,8 @@ public void synchronize() throws FormSourceException { throw new FormSourceException.FetchError(); } } + + public interface ServerFormsDetailsFetcher { + List fetchFormDetails(FormsRepository formsRepository, FormSource formSource) throws FormSourceException; + } } diff --git a/collect_app/src/test/java/org/odk/collect/android/formmanagement/ServerFormsSynchronizerTest.java b/collect_app/src/test/java/org/odk/collect/android/formmanagement/ServerFormsSynchronizerTest.java index 8c379b54d45..2f50214839f 100644 --- a/collect_app/src/test/java/org/odk/collect/android/formmanagement/ServerFormsSynchronizerTest.java +++ b/collect_app/src/test/java/org/odk/collect/android/formmanagement/ServerFormsSynchronizerTest.java @@ -1,5 +1,15 @@ package org.odk.collect.android.formmanagement; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.containsInAnyOrder; +import static org.hamcrest.Matchers.empty; +import static org.hamcrest.Matchers.is; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import static java.util.Arrays.asList; + import org.junit.Test; import org.odk.collect.android.formmanagement.download.FormDownloadException; import org.odk.collect.android.formmanagement.download.FormDownloader; @@ -17,22 +27,12 @@ import java.util.List; import java.util.function.Supplier; -import static java.util.Arrays.asList; -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.containsInAnyOrder; -import static org.hamcrest.Matchers.empty; -import static org.hamcrest.Matchers.is; -import static org.mockito.Mockito.doThrow; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - public class ServerFormsSynchronizerTest { private final FormsRepository formsRepository = new InMemFormsRepository(); private final InstancesRepository instancesRepository = new InMemInstancesRepository(); private final RecordingFormDownloader formDownloader = new RecordingFormDownloader(); - private final ServerFormsDetailsFetcher serverFormDetailsFetcher = mock(ServerFormsDetailsFetcher.class); + private final ServerFormsSynchronizer.ServerFormsDetailsFetcher serverFormDetailsFetcher = mock(ServerFormsSynchronizer.ServerFormsDetailsFetcher.class); private final FormSource formSource = mock(FormSource.class); private final ServerFormsSynchronizer synchronizer = new ServerFormsSynchronizer(serverFormDetailsFetcher, formsRepository, instancesRepository, formDownloader, formSource); From 6e09b317193bba3148a143ec069d0ef9f711f9fb Mon Sep 17 00:00:00 2001 From: Callum Stott Date: Fri, 24 Oct 2025 14:39:44 +0100 Subject: [PATCH 07/19] Separate out tests for ServerFormUseCases These test file names are pretty long, but they allow IntelliJ to navigate between the file and the test. --- ...wnloadMediaFilesServerFormUseCasesTest.kt} | 217 +++++++----------- .../DownloadUpdatesServerFormUseCasesTest.kt | 48 ++++ ...FetchFormDetailsServerFormUseCasesTest.kt} | 2 +- 3 files changed, 138 insertions(+), 129 deletions(-) rename collect_app/src/test/java/org/odk/collect/android/formmanagement/{ServerFormUseCasesTest.kt => DownloadMediaFilesServerFormUseCasesTest.kt} (86%) create mode 100644 collect_app/src/test/java/org/odk/collect/android/formmanagement/DownloadUpdatesServerFormUseCasesTest.kt rename collect_app/src/test/java/org/odk/collect/android/formmanagement/{ServerFormsDetailsFetcherTest.kt => FetchFormDetailsServerFormUseCasesTest.kt} (99%) diff --git a/collect_app/src/test/java/org/odk/collect/android/formmanagement/ServerFormUseCasesTest.kt b/collect_app/src/test/java/org/odk/collect/android/formmanagement/DownloadMediaFilesServerFormUseCasesTest.kt similarity index 86% rename from collect_app/src/test/java/org/odk/collect/android/formmanagement/ServerFormUseCasesTest.kt rename to collect_app/src/test/java/org/odk/collect/android/formmanagement/DownloadMediaFilesServerFormUseCasesTest.kt index 50e9b81e211..48982cb2dfb 100644 --- a/collect_app/src/test/java/org/odk/collect/android/formmanagement/ServerFormUseCasesTest.kt +++ b/collect_app/src/test/java/org/odk/collect/android/formmanagement/DownloadMediaFilesServerFormUseCasesTest.kt @@ -3,17 +3,11 @@ package org.odk.collect.android.formmanagement import org.hamcrest.CoreMatchers.equalTo import org.hamcrest.MatcherAssert.assertThat import org.junit.Test -import org.mockito.Mockito.any -import org.mockito.Mockito.doAnswer -import org.mockito.invocation.InvocationOnMock import org.mockito.kotlin.doAnswer import org.mockito.kotlin.doReturn import org.mockito.kotlin.mock import org.mockito.kotlin.times import org.mockito.kotlin.verify -import org.mockito.stubbing.Answer -import org.odk.collect.android.formmanagement.download.FormDownloadException -import org.odk.collect.android.formmanagement.download.FormDownloader import org.odk.collect.android.utilities.FileUtils import org.odk.collect.entities.storage.InMemEntitiesRepository import org.odk.collect.forms.Form @@ -27,128 +21,7 @@ import org.odk.collect.shared.TempFiles import org.odk.collect.shared.strings.Md5.getMd5Hash import java.io.File -class ServerFormUseCasesTest { - - @Test - fun `#downloadUpdates returns completed downloads when cancelled`() { - val formDownloader = mock() - - val serverForms = listOf( - ServerFormDetails("", "", "", "", "", false, true, ManifestFile("", emptyList())), - ServerFormDetails("", "", "", "", "", false, true, ManifestFile("", emptyList())) - ) - - // Cancel form download after downloading one form - doAnswer(object : Answer { - private var calledBefore = false - - @Throws(Throwable::class) - override fun answer(invocation: InvocationOnMock) { - calledBefore = if (!calledBefore) { - true - } else { - throw FormDownloadException.DownloadingInterrupted() - } - } - }).`when`(formDownloader).downloadForm(any(), any(), any()) - - val results = ServerFormUseCases.downloadForms( - serverForms, - formDownloader - ) - - assertThat(results.size, equalTo(1)) - assertThat(results[serverForms[0]], equalTo(null)) - } - - @Test - fun `#copySavedFileFromPreviousFormVersionIfExists does not copy any file if there is no matching last-saved file`() { - val destinationMediaDirPath = TempFiles.createTempDir().absolutePath - ServerFormUseCases.copySavedFileFromPreviousFormVersionIfExists( - InMemFormsRepository(), - "1", - destinationMediaDirPath - ) - - val resultFile = File(destinationMediaDirPath, FileUtils.LAST_SAVED_FILENAME) - assertThat(resultFile.exists(), equalTo(false)) - } - - @Test - fun `#copySavedFileFromPreviousFormVersionIfExists copies the newest matching last-saved file for given formId`() { - val tempDir1 = TempFiles.createTempDir() - val file1 = TempFiles.createTempFile(tempDir1, "last-saved", ".xml") - org.apache.commons.io.FileUtils.writeByteArrayToFile(file1, "file1".toByteArray()) - - val tempDir2 = TempFiles.createTempDir() - val file2 = TempFiles.createTempFile(tempDir2, "last-saved", ".xml") - org.apache.commons.io.FileUtils.writeByteArrayToFile(file2, "file2".toByteArray()) - - val tempDir3 = TempFiles.createTempDir() - val file3 = TempFiles.createTempFile(tempDir3, "last-saved", ".xml") - org.apache.commons.io.FileUtils.writeByteArrayToFile(file3, "file3".toByteArray()) - - val tempDir4 = TempFiles.createTempDir() - val file4 = TempFiles.createTempFile(tempDir4, "last-saved", ".xml") - org.apache.commons.io.FileUtils.writeByteArrayToFile(file4, "file4".toByteArray()) - - val formsRepository = InMemFormsRepository().also { - it.save( - Form.Builder() - .dbId(1) - .formId("1") - .version("1") - .date(0) - .formFilePath(FormUtils.createXFormFile("1", "1").absolutePath) - .formMediaPath(file1.parent) - .build() - ) - - it.save( - Form.Builder() - .dbId(2) - .formId("1") - .version("2") - .date(2) - .formFilePath(FormUtils.createXFormFile("1", "2").absolutePath) - .formMediaPath(file2.parent) - .build() - ) - - it.save( - Form.Builder() - .dbId(3) - .formId("1") - .version("3") - .date(1) - .formFilePath(FormUtils.createXFormFile("1", "3").absolutePath) - .formMediaPath(file3.parent) - .build() - ) - - it.save( - Form.Builder() - .dbId(4) - .formId("2") - .version("1") - .date(3) - .formFilePath(FormUtils.createXFormFile("2", "1").absolutePath) - .formMediaPath(file4.parent) - .build() - ) - } - - val destinationMediaDirPath = TempFiles.createTempDir().absolutePath - ServerFormUseCases.copySavedFileFromPreviousFormVersionIfExists( - formsRepository, - "1", - destinationMediaDirPath - ) - - val resultFile = File(destinationMediaDirPath, FileUtils.LAST_SAVED_FILENAME) - assertThat(resultFile.readText(), equalTo("file2")) - } - +class DownloadMediaFilesServerFormUseCasesTest { @Test fun `#downloadMediaFiles returns false when there is an existing copy of a media file and an older one`() { var date: Long = 0 @@ -281,4 +154,92 @@ class ServerFormUseCasesTest { verify(formSource, times(1)).fetchMediaFile(mediaFile.downloadUrl) } + + @Test + fun `#copySavedFileFromPreviousFormVersionIfExists does not copy any file if there is no matching last-saved file`() { + val destinationMediaDirPath = TempFiles.createTempDir().absolutePath + ServerFormUseCases.copySavedFileFromPreviousFormVersionIfExists( + InMemFormsRepository(), + "1", + destinationMediaDirPath + ) + + val resultFile = File(destinationMediaDirPath, FileUtils.LAST_SAVED_FILENAME) + assertThat(resultFile.exists(), equalTo(false)) + } + + @Test + fun `#copySavedFileFromPreviousFormVersionIfExists copies the newest matching last-saved file for given formId`() { + val tempDir1 = TempFiles.createTempDir() + val file1 = TempFiles.createTempFile(tempDir1, "last-saved", ".xml") + org.apache.commons.io.FileUtils.writeByteArrayToFile(file1, "file1".toByteArray()) + + val tempDir2 = TempFiles.createTempDir() + val file2 = TempFiles.createTempFile(tempDir2, "last-saved", ".xml") + org.apache.commons.io.FileUtils.writeByteArrayToFile(file2, "file2".toByteArray()) + + val tempDir3 = TempFiles.createTempDir() + val file3 = TempFiles.createTempFile(tempDir3, "last-saved", ".xml") + org.apache.commons.io.FileUtils.writeByteArrayToFile(file3, "file3".toByteArray()) + + val tempDir4 = TempFiles.createTempDir() + val file4 = TempFiles.createTempFile(tempDir4, "last-saved", ".xml") + org.apache.commons.io.FileUtils.writeByteArrayToFile(file4, "file4".toByteArray()) + + val formsRepository = InMemFormsRepository().also { + it.save( + Form.Builder() + .dbId(1) + .formId("1") + .version("1") + .date(0) + .formFilePath(FormUtils.createXFormFile("1", "1").absolutePath) + .formMediaPath(file1.parent) + .build() + ) + + it.save( + Form.Builder() + .dbId(2) + .formId("1") + .version("2") + .date(2) + .formFilePath(FormUtils.createXFormFile("1", "2").absolutePath) + .formMediaPath(file2.parent) + .build() + ) + + it.save( + Form.Builder() + .dbId(3) + .formId("1") + .version("3") + .date(1) + .formFilePath(FormUtils.createXFormFile("1", "3").absolutePath) + .formMediaPath(file3.parent) + .build() + ) + + it.save( + Form.Builder() + .dbId(4) + .formId("2") + .version("1") + .date(3) + .formFilePath(FormUtils.createXFormFile("2", "1").absolutePath) + .formMediaPath(file4.parent) + .build() + ) + } + + val destinationMediaDirPath = TempFiles.createTempDir().absolutePath + ServerFormUseCases.copySavedFileFromPreviousFormVersionIfExists( + formsRepository, + "1", + destinationMediaDirPath + ) + + val resultFile = File(destinationMediaDirPath, FileUtils.LAST_SAVED_FILENAME) + assertThat(resultFile.readText(), equalTo("file2")) + } } diff --git a/collect_app/src/test/java/org/odk/collect/android/formmanagement/DownloadUpdatesServerFormUseCasesTest.kt b/collect_app/src/test/java/org/odk/collect/android/formmanagement/DownloadUpdatesServerFormUseCasesTest.kt new file mode 100644 index 00000000000..37818d94238 --- /dev/null +++ b/collect_app/src/test/java/org/odk/collect/android/formmanagement/DownloadUpdatesServerFormUseCasesTest.kt @@ -0,0 +1,48 @@ +package org.odk.collect.android.formmanagement + +import org.hamcrest.CoreMatchers.equalTo +import org.hamcrest.MatcherAssert.assertThat +import org.junit.Test +import org.mockito.Mockito.any +import org.mockito.Mockito.doAnswer +import org.mockito.invocation.InvocationOnMock +import org.mockito.kotlin.mock +import org.mockito.stubbing.Answer +import org.odk.collect.android.formmanagement.download.FormDownloadException +import org.odk.collect.android.formmanagement.download.FormDownloader +import org.odk.collect.forms.ManifestFile + +class DownloadUpdatesServerFormUseCasesTest { + + @Test + fun `#downloadUpdates returns completed downloads when cancelled`() { + val formDownloader = mock() + + val serverForms = listOf( + ServerFormDetails("", "", "", "", "", false, true, ManifestFile("", emptyList())), + ServerFormDetails("", "", "", "", "", false, true, ManifestFile("", emptyList())) + ) + + // Cancel form download after downloading one form + doAnswer(object : Answer { + private var calledBefore = false + + @Throws(Throwable::class) + override fun answer(invocation: InvocationOnMock) { + calledBefore = if (!calledBefore) { + true + } else { + throw FormDownloadException.DownloadingInterrupted() + } + } + }).`when`(formDownloader).downloadForm(any(), any(), any()) + + val results = ServerFormUseCases.downloadForms( + serverForms, + formDownloader + ) + + assertThat(results.size, equalTo(1)) + assertThat(results[serverForms[0]], equalTo(null)) + } +} diff --git a/collect_app/src/test/java/org/odk/collect/android/formmanagement/ServerFormsDetailsFetcherTest.kt b/collect_app/src/test/java/org/odk/collect/android/formmanagement/FetchFormDetailsServerFormUseCasesTest.kt similarity index 99% rename from collect_app/src/test/java/org/odk/collect/android/formmanagement/ServerFormsDetailsFetcherTest.kt rename to collect_app/src/test/java/org/odk/collect/android/formmanagement/FetchFormDetailsServerFormUseCasesTest.kt index de368c1f772..10d8ce8e5fc 100644 --- a/collect_app/src/test/java/org/odk/collect/android/formmanagement/ServerFormsDetailsFetcherTest.kt +++ b/collect_app/src/test/java/org/odk/collect/android/formmanagement/FetchFormDetailsServerFormUseCasesTest.kt @@ -23,7 +23,7 @@ import java.io.ByteArrayInputStream import java.io.File import java.io.FileWriter -class ServerFormsDetailsFetcherTest { +class FetchFormDetailsServerFormUseCasesTest { private val formsRepository: FormsRepository = InMemFormsRepository() private val formSource = mock { From 98fd70df89c843e75d1bc284ae35a5cd76d1804d Mon Sep 17 00:00:00 2001 From: Callum Stott Date: Fri, 24 Oct 2025 14:52:48 +0100 Subject: [PATCH 08/19] Remove unused annotation --- .../org/odk/collect/android/formmanagement/ServerFormDetails.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/collect_app/src/main/java/org/odk/collect/android/formmanagement/ServerFormDetails.kt b/collect_app/src/main/java/org/odk/collect/android/formmanagement/ServerFormDetails.kt index 01a5b396cdd..27511f362b6 100644 --- a/collect_app/src/main/java/org/odk/collect/android/formmanagement/ServerFormDetails.kt +++ b/collect_app/src/main/java/org/odk/collect/android/formmanagement/ServerFormDetails.kt @@ -16,7 +16,7 @@ package org.odk.collect.android.formmanagement import org.odk.collect.forms.ManifestFile import java.io.Serializable -data class ServerFormDetails @JvmOverloads constructor( +data class ServerFormDetails( val formName: String?, val downloadUrl: String?, val formId: String?, From acb89bfe98789dd97e1981df3386b620614e6342 Mon Sep 17 00:00:00 2001 From: Callum Stott Date: Fri, 24 Oct 2025 17:25:32 +0100 Subject: [PATCH 09/19] Add enum to replace booleans --- .../formmanagement/ServerFormDetails.kt | 42 +++++++++++-- .../formmanagement/ServerFormUseCases.kt | 29 +++++++-- .../FetchFormDetailsServerFormUseCasesTest.kt | 61 ++++++++++++------- 3 files changed, 101 insertions(+), 31 deletions(-) diff --git a/collect_app/src/main/java/org/odk/collect/android/formmanagement/ServerFormDetails.kt b/collect_app/src/main/java/org/odk/collect/android/formmanagement/ServerFormDetails.kt index 27511f362b6..388e711ba8e 100644 --- a/collect_app/src/main/java/org/odk/collect/android/formmanagement/ServerFormDetails.kt +++ b/collect_app/src/main/java/org/odk/collect/android/formmanagement/ServerFormDetails.kt @@ -16,18 +16,52 @@ package org.odk.collect.android.formmanagement import org.odk.collect.forms.ManifestFile import java.io.Serializable -data class ServerFormDetails( +data class ServerFormDetails @JvmOverloads constructor( val formName: String?, val downloadUrl: String?, val formId: String?, val formVersion: String?, val hash: String?, - val isNotOnDevice: Boolean, - val isUpdated: Boolean, - val manifest: ManifestFile? + @Deprecated( + message = "Use type instead", + replaceWith = ReplaceWith("type") + ) val isNotOnDevice: Boolean, + @Deprecated( + message = "Use type instead", + replaceWith = ReplaceWith("type") + ) val isUpdated: Boolean, + val manifest: ManifestFile?, + val type: Type? = null ) : Serializable { companion object { private const val serialVersionUID = 3L } + + enum class Type { + /** + * The form is on the device already + */ + OnDevice, + + /** + * The form is not on the device + */ + New, + + /** + * The form is on the device, but this is a new version with a new version and hash + */ + UpdatedVersion, + + /** + * The form is on the device, but this is a new version with a new hash + */ + UpdatedHash, + + /** + * This version of the form is on the device, but new/updated media files are available + */ + UpdatedMedia + } } diff --git a/collect_app/src/main/java/org/odk/collect/android/formmanagement/ServerFormUseCases.kt b/collect_app/src/main/java/org/odk/collect/android/formmanagement/ServerFormUseCases.kt index 5fd93b7b3e0..bec112900f1 100644 --- a/collect_app/src/main/java/org/odk/collect/android/formmanagement/ServerFormUseCases.kt +++ b/collect_app/src/main/java/org/odk/collect/android/formmanagement/ServerFormUseCases.kt @@ -47,10 +47,8 @@ object ServerFormUseCases { } val isNewerFormVersionAvailable = listItem.hash.let { - if (it == null) { - false - } else if (thisFormAlreadyDownloaded) { - existingForm == null || existingForm.isDeleted + if (thisFormAlreadyDownloaded) { + existingForm == null } else { false } @@ -62,6 +60,26 @@ object ServerFormUseCases { false } + val type = if (existingForm != null) { + if (existingForm.isDeleted) { + ServerFormDetails.Type.New + } else if (areNewerMediaFilesAvailable) { + ServerFormDetails.Type.UpdatedMedia + } else { + ServerFormDetails.Type.OnDevice + } + } else if (thisFormAlreadyDownloaded) { + if (listItem.hash == null) { + ServerFormDetails.Type.OnDevice + } else if (forms.any { it.version == listItem.version }) { + ServerFormDetails.Type.UpdatedHash + } else { + ServerFormDetails.Type.UpdatedVersion + } + } else { + ServerFormDetails.Type.New + } + ServerFormDetails( listItem.name, listItem.downloadURL, @@ -70,7 +88,8 @@ object ServerFormUseCases { listItem.hash, !thisFormAlreadyDownloaded, isNewerFormVersionAvailable || areNewerMediaFilesAvailable, - manifestFile + manifestFile, + type ) } } diff --git a/collect_app/src/test/java/org/odk/collect/android/formmanagement/FetchFormDetailsServerFormUseCasesTest.kt b/collect_app/src/test/java/org/odk/collect/android/formmanagement/FetchFormDetailsServerFormUseCasesTest.kt index 10d8ce8e5fc..74eae0da49b 100644 --- a/collect_app/src/test/java/org/odk/collect/android/formmanagement/FetchFormDetailsServerFormUseCasesTest.kt +++ b/collect_app/src/test/java/org/odk/collect/android/formmanagement/FetchFormDetailsServerFormUseCasesTest.kt @@ -2,7 +2,7 @@ package org.odk.collect.android.formmanagement import org.hamcrest.MatcherAssert.assertThat import org.hamcrest.Matchers.contains -import org.hamcrest.Matchers.`is` +import org.hamcrest.Matchers.equalTo import org.hamcrest.Matchers.nullValue import org.junit.Test import org.mockito.kotlin.doReturn @@ -54,7 +54,8 @@ class FetchFormDetailsServerFormUseCasesTest { whenever(formSource.fetchFormList()).thenReturn(listOf(FORM_WITHOUT_MANIFEST)) val serverFormDetails = ServerFormUseCases.fetchFormDetails(formsRepository, formSource) - assertThat(getFormFromList(serverFormDetails, "form-1").isNotOnDevice, `is`(true)) + val form = getFormFromList(serverFormDetails, "form-1") + assertThat(form.type, equalTo(ServerFormDetails.Type.New)) } @Test @@ -71,11 +72,12 @@ class FetchFormDetailsServerFormUseCasesTest { ) val serverFormDetails = ServerFormUseCases.fetchFormDetails(formsRepository, formSource) - assertThat(getFormFromList(serverFormDetails, "form-1").isNotOnDevice, `is`(true)) + val form = getFormFromList(serverFormDetails, "form-1") + assertThat(form.type, equalTo(ServerFormDetails.Type.New)) } @Test - fun whenAFormExists_andListContainsVersionWithDifferentHash_isUpdated() { + fun whenAFormExists_andListContainsNewVersionWithDifferentHash_isUpdated() { whenever(formSource.fetchFormList()).thenReturn(listOf(FORM_WITHOUT_MANIFEST)) formsRepository.save( Form.Builder() @@ -86,7 +88,25 @@ class FetchFormDetailsServerFormUseCasesTest { ) val serverFormDetails = ServerFormUseCases.fetchFormDetails(formsRepository, formSource) - assertThat(getFormFromList(serverFormDetails, "form-1").isUpdated, `is`(true)) + val form = getFormFromList(serverFormDetails, "form-1") + assertThat(form.type, equalTo(ServerFormDetails.Type.UpdatedVersion)) + } + + @Test + fun whenAFormExists_andListContainsSameVersionWithDifferentHash_isUpdated() { + whenever(formSource.fetchFormList()).thenReturn(listOf(FORM_WITHOUT_MANIFEST)) + formsRepository.save( + Form.Builder() + .formId("form-1") + .version("1") + .md5Hash("form-1-hash-old") + .formFilePath(FormUtils.createXFormFile("form-1", null).absolutePath) + .build() + ) + + val serverFormDetails = ServerFormUseCases.fetchFormDetails(formsRepository, formSource) + val form = getFormFromList(serverFormDetails, "form-1") + assertThat(form.type, equalTo(ServerFormDetails.Type.UpdatedHash)) } @Test @@ -103,7 +123,8 @@ class FetchFormDetailsServerFormUseCasesTest { ) val serverFormDetails = ServerFormUseCases.fetchFormDetails(formsRepository, formSource) - assertThat(getFormFromList(serverFormDetails, "form-2").isUpdated, `is`(true)) + val form = getFormFromList(serverFormDetails, "form-2") + assertThat(form.type, equalTo(ServerFormDetails.Type.UpdatedMedia)) } @Test @@ -124,7 +145,8 @@ class FetchFormDetailsServerFormUseCasesTest { writeToFile(oldMediaFile, "blah before") val serverFormDetails = ServerFormUseCases.fetchFormDetails(formsRepository, formSource) - assertThat(getFormFromList(serverFormDetails, "form-2").isUpdated, `is`(true)) + val form = getFormFromList(serverFormDetails, "form-2") + assertThat(form.type, equalTo(ServerFormDetails.Type.UpdatedMedia)) } @Test @@ -159,12 +181,11 @@ class FetchFormDetailsServerFormUseCasesTest { val serverFormDetails = ServerFormUseCases.fetchFormDetails(formsRepository, formSource) val form = getFormFromList(serverFormDetails, "form-2") - assertThat(form.isUpdated, `is`(false)) - assertThat(form.isNotOnDevice, `is`(false)) + assertThat(form.type, equalTo(ServerFormDetails.Type.OnDevice)) } @Test - fun whenAFormExists_andItsNewerVersionHasBeenAlreadyDownloadedButThenSoftDeleted_isUpdated() { + fun whenAFormExists_andItsNewerVersionHasBeenAlreadyDownloadedButThenSoftDeleted_isNew() { whenever(formSource.fetchFormList()).thenReturn(listOf(FORM_WITHOUT_MANIFEST)) formsRepository.save( @@ -187,12 +208,11 @@ class FetchFormDetailsServerFormUseCasesTest { val serverFormDetails = ServerFormUseCases.fetchFormDetails(formsRepository, formSource) val form = getFormFromList(serverFormDetails, "form-1") - assertThat(form.isUpdated, `is`(true)) - assertThat(form.isNotOnDevice, `is`(false)) + assertThat(form.type, equalTo(ServerFormDetails.Type.New)) } @Test - fun whenAFormExists_andItsNewerVersionWithMediaFilesHasBeenAlreadyDownloadedButThenSoftDeleted_isUpdated() { + fun whenAFormExists_andItsNewerVersionWithMediaFilesHasBeenAlreadyDownloadedButThenSoftDeleted_isNew() { whenever(formSource.fetchFormList()).thenReturn(listOf(FORM_WITH_MANIFEST)) val mediaDir1 = TempFiles.createTempDir() @@ -224,8 +244,7 @@ class FetchFormDetailsServerFormUseCasesTest { val serverFormDetails = ServerFormUseCases.fetchFormDetails(formsRepository, formSource) val form = getFormFromList(serverFormDetails, "form-2") - assertThat(form.isUpdated, `is`(true)) - assertThat(form.isNotOnDevice, `is`(false)) + assertThat(form.type, equalTo(ServerFormDetails.Type.New)) } @Test @@ -243,8 +262,7 @@ class FetchFormDetailsServerFormUseCasesTest { val serverFormDetails = ServerFormUseCases.fetchFormDetails(formsRepository, formSource) val form = getFormFromList(serverFormDetails, "form-1") - assertThat(form.isUpdated, `is`(false)) - assertThat(form.isNotOnDevice, `is`(false)) + assertThat(form.type, equalTo(ServerFormDetails.Type.OnDevice)) } @Test @@ -266,8 +284,7 @@ class FetchFormDetailsServerFormUseCasesTest { val serverFormDetails = ServerFormUseCases.fetchFormDetails(formsRepository, formSource) val form = getFormFromList(serverFormDetails, "form-2") - assertThat(form.isNotOnDevice, `is`(false)) - assertThat(form.isUpdated, `is`(false)) + assertThat(form.type, equalTo(ServerFormDetails.Type.OnDevice)) } @Test @@ -287,7 +304,8 @@ class FetchFormDetailsServerFormUseCasesTest { writeToFile(localMediaFile, FILE_CONTENT) val serverFormDetails = ServerFormUseCases.fetchFormDetails(formsRepository, formSource) - assertThat(getFormFromList(serverFormDetails, "form-2").isUpdated, `is`(true)) + val form = getFormFromList(serverFormDetails, "form-2") + assertThat(form.type, equalTo(ServerFormDetails.Type.UpdatedVersion)) } @Test @@ -307,8 +325,7 @@ class FetchFormDetailsServerFormUseCasesTest { val serverFormDetails = ServerFormUseCases.fetchFormDetails(formsRepository, formSource) val form = getFormFromList(serverFormDetails, "form-2") - assertThat(form.isUpdated, `is`(false)) - assertThat(form.isNotOnDevice, `is`(false)) + assertThat(form.type, equalTo(ServerFormDetails.Type.OnDevice)) } private fun writeToFile(mediaFile: File, blah: String) { From f7de4962b230f340ca31a677173a06a47fd67355 Mon Sep 17 00:00:00 2001 From: Callum Stott Date: Fri, 24 Oct 2025 17:44:06 +0100 Subject: [PATCH 10/19] Convert boolean to proxies for type --- .../formmanagement/ServerFormDetails.kt | 54 ++++++++++++++++--- .../formmanagement/ServerFormUseCases.kt | 2 - .../NotificationManagerNotifier.kt | 5 +- 3 files changed, 51 insertions(+), 10 deletions(-) diff --git a/collect_app/src/main/java/org/odk/collect/android/formmanagement/ServerFormDetails.kt b/collect_app/src/main/java/org/odk/collect/android/formmanagement/ServerFormDetails.kt index 388e711ba8e..eb9127d7f59 100644 --- a/collect_app/src/main/java/org/odk/collect/android/formmanagement/ServerFormDetails.kt +++ b/collect_app/src/main/java/org/odk/collect/android/formmanagement/ServerFormDetails.kt @@ -22,20 +22,62 @@ data class ServerFormDetails @JvmOverloads constructor( val formId: String?, val formVersion: String?, val hash: String?, + val manifest: ManifestFile?, + val type: Type +) : Serializable { + + @Deprecated(message = "Use primary constructor instead") + constructor( + formName: String?, + downloadUrl: String?, + formId: String?, + formVersion: String?, + hash: String?, + isNotOnDevice: Boolean, + isUpdated: Boolean, + manifest: ManifestFile?, + ) : this( + formName, + downloadUrl, + formId, + formVersion, + hash, + manifest, + if (isNotOnDevice) { + Type.New + } else if (isUpdated) { + Type.UpdatedVersion + } else { + Type.OnDevice + } + ) + @Deprecated( message = "Use type instead", replaceWith = ReplaceWith("type") - ) val isNotOnDevice: Boolean, + ) + val isNotOnDevice: Boolean = when (type) { + Type.OnDevice -> false + Type.New -> true + Type.UpdatedVersion -> false + Type.UpdatedHash -> false + Type.UpdatedMedia -> false + } + @Deprecated( message = "Use type instead", replaceWith = ReplaceWith("type") - ) val isUpdated: Boolean, - val manifest: ManifestFile?, - val type: Type? = null -) : Serializable { + ) + val isUpdated: Boolean = when (type) { + Type.OnDevice -> false + Type.New -> false + Type.UpdatedVersion -> true + Type.UpdatedHash -> true + Type.UpdatedMedia -> true + } companion object { - private const val serialVersionUID = 3L + private const val serialVersionUID = 4L } enum class Type { diff --git a/collect_app/src/main/java/org/odk/collect/android/formmanagement/ServerFormUseCases.kt b/collect_app/src/main/java/org/odk/collect/android/formmanagement/ServerFormUseCases.kt index bec112900f1..f3a82b7a231 100644 --- a/collect_app/src/main/java/org/odk/collect/android/formmanagement/ServerFormUseCases.kt +++ b/collect_app/src/main/java/org/odk/collect/android/formmanagement/ServerFormUseCases.kt @@ -86,8 +86,6 @@ object ServerFormUseCases { listItem.formID, listItem.version, listItem.hash, - !thisFormAlreadyDownloaded, - isNewerFormVersionAvailable || areNewerMediaFilesAvailable, manifestFile, type ) diff --git a/collect_app/src/main/java/org/odk/collect/android/notifications/NotificationManagerNotifier.kt b/collect_app/src/main/java/org/odk/collect/android/notifications/NotificationManagerNotifier.kt index 786ab8a2ece..86a42880be2 100644 --- a/collect_app/src/main/java/org/odk/collect/android/notifications/NotificationManagerNotifier.kt +++ b/collect_app/src/main/java/org/odk/collect/android/notifications/NotificationManagerNotifier.kt @@ -34,8 +34,9 @@ class NotificationManagerNotifier( val notificationId = uniqueIdGenerator.getInt(FORM_UPDATE_NOTIFICATION_IDENTIFIER) val metaPrefs = settingsProvider.getMetaSettings() - val updateId = updates - .mapTo(HashSet()) { (_, _, formId, _, hash, _, _, manifest) -> formId + hash + manifest?.hash } + val updateId = updates.mapTo(HashSet()) { (_, _, formId, _, hash, manifest, _) -> + formId + hash + manifest?.hash + } if (metaPrefs.getStringSet(MetaKeys.LAST_UPDATED_NOTIFICATION) != updateId) { notificationManager.notify( notificationId, From a224e46a2741d67176e9016935293f08ed303781 Mon Sep 17 00:00:00 2001 From: Callum Stott Date: Fri, 24 Oct 2025 18:03:13 +0100 Subject: [PATCH 11/19] Remove isNotOnDevice --- .../android/activities/FormDownloadListActivity.java | 2 +- .../android/formmanagement/ServerFormDetails.kt | 12 ------------ .../matchexactly/ServerFormsSynchronizer.java | 2 +- 3 files changed, 2 insertions(+), 14 deletions(-) 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 3a183c23942..389ccd990c2 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 @@ -460,7 +460,7 @@ public boolean isLocalFormSuperseded(String formId) { } ServerFormDetails form = viewModel.getFormDetailsByFormId().get(formId); - return form.isNotOnDevice() || form.isUpdated(); + return form.getType() != ServerFormDetails.Type.OnDevice; } /** diff --git a/collect_app/src/main/java/org/odk/collect/android/formmanagement/ServerFormDetails.kt b/collect_app/src/main/java/org/odk/collect/android/formmanagement/ServerFormDetails.kt index eb9127d7f59..0c31fb9249f 100644 --- a/collect_app/src/main/java/org/odk/collect/android/formmanagement/ServerFormDetails.kt +++ b/collect_app/src/main/java/org/odk/collect/android/formmanagement/ServerFormDetails.kt @@ -52,18 +52,6 @@ data class ServerFormDetails @JvmOverloads constructor( } ) - @Deprecated( - message = "Use type instead", - replaceWith = ReplaceWith("type") - ) - val isNotOnDevice: Boolean = when (type) { - Type.OnDevice -> false - Type.New -> true - Type.UpdatedVersion -> false - Type.UpdatedHash -> false - Type.UpdatedMedia -> false - } - @Deprecated( message = "Use type instead", replaceWith = ReplaceWith("type") diff --git a/collect_app/src/main/java/org/odk/collect/android/formmanagement/matchexactly/ServerFormsSynchronizer.java b/collect_app/src/main/java/org/odk/collect/android/formmanagement/matchexactly/ServerFormsSynchronizer.java index 9c08001210c..cbee935927a 100644 --- a/collect_app/src/main/java/org/odk/collect/android/formmanagement/matchexactly/ServerFormsSynchronizer.java +++ b/collect_app/src/main/java/org/odk/collect/android/formmanagement/matchexactly/ServerFormsSynchronizer.java @@ -40,7 +40,7 @@ public void synchronize() throws FormSourceException { boolean downloadException = false; for (ServerFormDetails form : formList) { - if (form.isNotOnDevice() || form.isUpdated()) { + if (form.getType() != ServerFormDetails.Type.OnDevice) { try { formDownloader.downloadForm(form, null, null); } catch (FormDownloadException.DownloadingInterrupted e) { From 17cfc35642a377c8df4f0abcccc9ee2209af0c7e Mon Sep 17 00:00:00 2001 From: Callum Stott Date: Fri, 24 Oct 2025 18:19:44 +0100 Subject: [PATCH 12/19] Replace isUpdated property with helper --- .../android/formmanagement/FormsDataService.kt | 2 +- .../android/formmanagement/ServerFormDetails.kt | 12 ++---------- 2 files changed, 3 insertions(+), 11 deletions(-) diff --git a/collect_app/src/main/java/org/odk/collect/android/formmanagement/FormsDataService.kt b/collect_app/src/main/java/org/odk/collect/android/formmanagement/FormsDataService.kt index e2851e18087..20bc1a614db 100644 --- a/collect_app/src/main/java/org/odk/collect/android/formmanagement/FormsDataService.kt +++ b/collect_app/src/main/java/org/odk/collect/android/formmanagement/FormsDataService.kt @@ -117,7 +117,7 @@ class FormsDataService( projectDependencies.formsRepository, projectDependencies.formSource ) - val updatedForms = serverForms.filter { it.isUpdated } + val updatedForms = serverForms.filter { it.isUpdated() } if (updatedForms.isNotEmpty()) { if (projectDependencies.generalSettings.getBoolean(ProjectKeys.KEY_AUTOMATIC_UPDATE)) { val results = ServerFormUseCases.downloadForms( diff --git a/collect_app/src/main/java/org/odk/collect/android/formmanagement/ServerFormDetails.kt b/collect_app/src/main/java/org/odk/collect/android/formmanagement/ServerFormDetails.kt index 0c31fb9249f..0b006bbe559 100644 --- a/collect_app/src/main/java/org/odk/collect/android/formmanagement/ServerFormDetails.kt +++ b/collect_app/src/main/java/org/odk/collect/android/formmanagement/ServerFormDetails.kt @@ -52,16 +52,8 @@ data class ServerFormDetails @JvmOverloads constructor( } ) - @Deprecated( - message = "Use type instead", - replaceWith = ReplaceWith("type") - ) - val isUpdated: Boolean = when (type) { - Type.OnDevice -> false - Type.New -> false - Type.UpdatedVersion -> true - Type.UpdatedHash -> true - Type.UpdatedMedia -> true + fun isUpdated(): Boolean { + return type != Type.OnDevice && type != Type.New } companion object { From ad16c3d65102623f4cce94ebc9f461e56dc945f3 Mon Sep 17 00:00:00 2001 From: Callum Stott Date: Mon, 27 Oct 2025 13:43:43 +0000 Subject: [PATCH 13/19] Change target for benchmark --- .../org/odk/collect/android/benchmark/EntitiesBenchmarkTest.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/collect_app/src/androidTest/java/org/odk/collect/android/benchmark/EntitiesBenchmarkTest.kt b/collect_app/src/androidTest/java/org/odk/collect/android/benchmark/EntitiesBenchmarkTest.kt index 8a2b85ab5ca..ebafe26a99d 100644 --- a/collect_app/src/androidTest/java/org/odk/collect/android/benchmark/EntitiesBenchmarkTest.kt +++ b/collect_app/src/androidTest/java/org/odk/collect/android/benchmark/EntitiesBenchmarkTest.kt @@ -127,7 +127,7 @@ class EntitiesBenchmarkTest { .clickGetBlankForm() .benchmark( "Redownloading a form with 1k media files and entity list when there are no updates", - 15, + 5, benchmarker ) { it From d71bfcc248dbe998f9d20cfa078fb2ee43590ddd Mon Sep 17 00:00:00 2001 From: Callum Stott Date: Mon, 27 Oct 2025 15:40:46 +0000 Subject: [PATCH 14/19] Optimize updating media --- .../odk/collect/android/formmanagement/ServerFormUseCases.kt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/collect_app/src/main/java/org/odk/collect/android/formmanagement/ServerFormUseCases.kt b/collect_app/src/main/java/org/odk/collect/android/formmanagement/ServerFormUseCases.kt index f3a82b7a231..065b503f3b5 100644 --- a/collect_app/src/main/java/org/odk/collect/android/formmanagement/ServerFormUseCases.kt +++ b/collect_app/src/main/java/org/odk/collect/android/formmanagement/ServerFormUseCases.kt @@ -227,7 +227,9 @@ object ServerFormUseCases { val existingFileHash = existingFile.getMd5Hash() if (existingFileHash.contentEquals(mediaFile.hash)) { - copyFileToDirectory(existingFile, tempMediaDir) + if (formToDownload.type != ServerFormDetails.Type.UpdatedMedia) { + copyFileToDirectory(existingFile, tempMediaDir) + } } else { downloadMediaFile( formSource, From 7643da9b3700e682c930d383da78914d2bbc1ccf Mon Sep 17 00:00:00 2001 From: Callum Stott Date: Mon, 27 Oct 2025 15:42:22 +0000 Subject: [PATCH 15/19] Add clarifying comment --- .../odk/collect/android/benchmark/EntitiesBenchmarkTest.kt | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/collect_app/src/androidTest/java/org/odk/collect/android/benchmark/EntitiesBenchmarkTest.kt b/collect_app/src/androidTest/java/org/odk/collect/android/benchmark/EntitiesBenchmarkTest.kt index ebafe26a99d..1473be57667 100644 --- a/collect_app/src/androidTest/java/org/odk/collect/android/benchmark/EntitiesBenchmarkTest.kt +++ b/collect_app/src/androidTest/java/org/odk/collect/android/benchmark/EntitiesBenchmarkTest.kt @@ -101,6 +101,10 @@ class EntitiesBenchmarkTest { * [THOUSAND_MEDIA_FILE_ENTITY_LIST_PROJECT_URL] should be set to a project that contains the * "1000-media-files-entity-list" form. * + * This scenario could also arise when updating a form that has a single new/updated non-entity + * media file, but in practice this will probably be most common with entity forms as the list + * will always force a media file update. + * * Devices that currently pass: * - Fairphone 3 */ From f9db6d5c75b231a624cb07b50ad41043d2ccef12 Mon Sep 17 00:00:00 2001 From: Callum Stott Date: Mon, 27 Oct 2025 15:44:33 +0000 Subject: [PATCH 16/19] Remove unneeded cache clear --- .../collect/android/benchmark/EntitiesBenchmarkTest.kt | 9 --------- 1 file changed, 9 deletions(-) diff --git a/collect_app/src/androidTest/java/org/odk/collect/android/benchmark/EntitiesBenchmarkTest.kt b/collect_app/src/androidTest/java/org/odk/collect/android/benchmark/EntitiesBenchmarkTest.kt index 1473be57667..f41b6df4b59 100644 --- a/collect_app/src/androidTest/java/org/odk/collect/android/benchmark/EntitiesBenchmarkTest.kt +++ b/collect_app/src/androidTest/java/org/odk/collect/android/benchmark/EntitiesBenchmarkTest.kt @@ -1,7 +1,5 @@ package org.odk.collect.android.benchmark -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.blankOrNullString @@ -44,7 +42,6 @@ class EntitiesBenchmarkTest { ENTITIES_FILTER_PROJECT_URL, not(blankOrNullString()) ) - clearAndroidCache() val benchmarker = Benchmarker() @@ -142,9 +139,3 @@ class EntitiesBenchmarkTest { benchmarker.assertResults() } } - -private fun clearAndroidCache() { - val application = ApplicationProvider.getApplicationContext() - application.cacheDir.deleteRecursively() - application.cacheDir.mkdir() -} From 73f7eb7e7a8f957202337be7fee8a4c1ac8af7b6 Mon Sep 17 00:00:00 2001 From: Callum Stott Date: Wed, 26 Nov 2025 13:38:40 +0000 Subject: [PATCH 17/19] Remove unused variable --- .../collect/android/formmanagement/ServerFormUseCases.kt | 8 -------- 1 file changed, 8 deletions(-) diff --git a/collect_app/src/main/java/org/odk/collect/android/formmanagement/ServerFormUseCases.kt b/collect_app/src/main/java/org/odk/collect/android/formmanagement/ServerFormUseCases.kt index 065b503f3b5..587fb32c3aa 100644 --- a/collect_app/src/main/java/org/odk/collect/android/formmanagement/ServerFormUseCases.kt +++ b/collect_app/src/main/java/org/odk/collect/android/formmanagement/ServerFormUseCases.kt @@ -46,14 +46,6 @@ object ServerFormUseCases { null } - val isNewerFormVersionAvailable = listItem.hash.let { - if (thisFormAlreadyDownloaded) { - existingForm == null - } else { - false - } - } - val areNewerMediaFilesAvailable = if (existingForm != null && manifestFile != null) { areNewerMediaFilesAvailable(existingForm, manifestFile.mediaFiles) } else { From ee6de920868b85e0043e977439f618ab00b0b1b6 Mon Sep 17 00:00:00 2001 From: Callum Stott Date: Wed, 26 Nov 2025 14:19:51 +0000 Subject: [PATCH 18/19] Fix comment --- .../org/odk/collect/android/formmanagement/ServerFormDetails.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/collect_app/src/main/java/org/odk/collect/android/formmanagement/ServerFormDetails.kt b/collect_app/src/main/java/org/odk/collect/android/formmanagement/ServerFormDetails.kt index 0b006bbe559..b65c6c66027 100644 --- a/collect_app/src/main/java/org/odk/collect/android/formmanagement/ServerFormDetails.kt +++ b/collect_app/src/main/java/org/odk/collect/android/formmanagement/ServerFormDetails.kt @@ -72,7 +72,7 @@ data class ServerFormDetails @JvmOverloads constructor( New, /** - * The form is on the device, but this is a new version with a new version and hash + * The form is on the device, but this is a new version with a new form version and hash */ UpdatedVersion, From 493711ec804abf8ed54d1150e297acc44e866516 Mon Sep 17 00:00:00 2001 From: Callum Stott Date: Wed, 26 Nov 2025 14:36:36 +0000 Subject: [PATCH 19/19] Update test names --- .../FetchFormDetailsServerFormUseCasesTest.kt | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/collect_app/src/test/java/org/odk/collect/android/formmanagement/FetchFormDetailsServerFormUseCasesTest.kt b/collect_app/src/test/java/org/odk/collect/android/formmanagement/FetchFormDetailsServerFormUseCasesTest.kt index 74eae0da49b..643fc7dd523 100644 --- a/collect_app/src/test/java/org/odk/collect/android/formmanagement/FetchFormDetailsServerFormUseCasesTest.kt +++ b/collect_app/src/test/java/org/odk/collect/android/formmanagement/FetchFormDetailsServerFormUseCasesTest.kt @@ -50,7 +50,7 @@ class FetchFormDetailsServerFormUseCasesTest { } @Test - fun whenFormDoesNotExist_isNotOnDevice() { + fun whenFormDoesNotExist_isNew() { whenever(formSource.fetchFormList()).thenReturn(listOf(FORM_WITHOUT_MANIFEST)) val serverFormDetails = ServerFormUseCases.fetchFormDetails(formsRepository, formSource) @@ -59,7 +59,7 @@ class FetchFormDetailsServerFormUseCasesTest { } @Test - fun whenAFormIsSoftDeleted_isNotOnDevice() { + fun whenAFormIsSoftDeleted_isNew() { whenever(formSource.fetchFormList()).thenReturn(listOf(FORM_WITHOUT_MANIFEST)) formsRepository.save( Form.Builder() @@ -77,7 +77,7 @@ class FetchFormDetailsServerFormUseCasesTest { } @Test - fun whenAFormExists_andListContainsNewVersionWithDifferentHash_isUpdated() { + fun whenAFormExists_andListContainsNewVersionWithDifferentHash_isUpdatedVersion() { whenever(formSource.fetchFormList()).thenReturn(listOf(FORM_WITHOUT_MANIFEST)) formsRepository.save( Form.Builder() @@ -93,7 +93,7 @@ class FetchFormDetailsServerFormUseCasesTest { } @Test - fun whenAFormExists_andListContainsSameVersionWithDifferentHash_isUpdated() { + fun whenAFormExists_andListContainsSameVersionWithDifferentHash_isUpdatedHash() { whenever(formSource.fetchFormList()).thenReturn(listOf(FORM_WITHOUT_MANIFEST)) formsRepository.save( Form.Builder() @@ -110,7 +110,7 @@ class FetchFormDetailsServerFormUseCasesTest { } @Test - fun whenAFormExists_andHasNewMediaFileOnServer_isUpdated() { + fun whenAFormExists_andHasNewMediaFileOnServer_isUpdatedMedia() { whenever(formSource.fetchFormList()).thenReturn(listOf(FORM_WITH_MANIFEST)) formsRepository.save( Form.Builder() @@ -128,7 +128,7 @@ class FetchFormDetailsServerFormUseCasesTest { } @Test - fun whenAFormExists_andHasUpdatedMediaFileOnServer_isUpdated() { + fun whenAFormExists_andHasUpdatedMediaFileOnServer_isUpdatedMedia() { whenever(formSource.fetchFormList()).thenReturn(listOf(FORM_WITH_MANIFEST)) val mediaDir = TempFiles.createTempDir() @@ -150,7 +150,7 @@ class FetchFormDetailsServerFormUseCasesTest { } @Test - fun whenAFormExists_andItsNewerVersionWithUpdatedMediaFilesHasBeenAlreadyDownloaded_isNotNewOrUpdated() { + fun whenAFormExists_andItsNewerVersionWithUpdatedMediaFilesHasBeenAlreadyDownloaded_isOnDevice() { whenever(formSource.fetchFormList()).thenReturn(listOf(FORM_WITH_MANIFEST)) val mediaDir1 = TempFiles.createTempDir() @@ -248,7 +248,7 @@ class FetchFormDetailsServerFormUseCasesTest { } @Test - fun whenAFormExists_andIsNotUpdatedOnServer_andDoesNotHaveAManifest_isNotNewOrUpdated() { + fun whenAFormExists_andIsNotUpdatedOnServer_andDoesNotHaveAManifest_isOnDevice() { whenever(formSource.fetchFormList()).thenReturn(listOf(FORM_WITHOUT_MANIFEST)) formsRepository.save( @@ -266,7 +266,7 @@ class FetchFormDetailsServerFormUseCasesTest { } @Test - fun whenFormExists_andMediaFilesExist_isNotNewOrUpdated() { + fun whenFormExists_andMediaFilesExist_isOnDevice() { whenever(formSource.fetchFormList()).thenReturn(listOf(FORM_WITH_MANIFEST)) val mediaDir = TempFiles.createTempDir() @@ -288,7 +288,7 @@ class FetchFormDetailsServerFormUseCasesTest { } @Test - fun whenAFormExists_andIsUpdatedOnServer_andDoesNotHaveNewMedia_isUpdated() { + fun whenAFormExists_andIsUpdatedOnServer_andDoesNotHaveNewMedia_isUpdatedVersion() { whenever(formSource.fetchFormList()).thenReturn(listOf(FORM_WITH_MANIFEST)) val mediaDir = TempFiles.createTempDir() @@ -309,7 +309,7 @@ class FetchFormDetailsServerFormUseCasesTest { } @Test - fun whenAFormExists_andItsNewerVersionWithManifestIsAvailableButHasNullHash_isNotNewOrUpdated() { + fun whenAFormExists_andItsNewerVersionWithManifestIsAvailableButHasNullHash_isOnDevice() { whenever(formSource.fetchFormList()).thenReturn( listOf(FORM_WITH_MANIFEST.copy(hash = null)) )